feat(core): the audio group model — element, membership, helpers (#3278)

* feat(core,studio): the character presets pitch shift unlocks

Chipmunk, Giant, and Monster ship as presets on the pitchshift worklet
P1 added: Chipmunk pitches up and adds sparkle, Giant pitches down with
weight and a compressor to hold the extra low end together, Monster pitches
down further with saturation growl and a close, tight reverb. Every param
verified against the live effect registry rather than sketched — the
compressor/reverb/saturate/shelf keys all match exactly.

Each gets its own title treatment (font, size, tracking, hue) so the FX
rack's per-preset styling coverage and hue-distance/background-uniqueness
tests extend cleanly to the three new entries, and complaint-line copy in
the non-voice vocabulary the audit test enforces (no speech words — "Giant"
over CapCut's "Deep Voice", as the design doc records).

Updates plans/audio-fx-presets.md's two limits paragraphs to record that
pitch shift landed and this half of the character list now ships; Robot and
Alien stay out of scope (ring modulation, still unbuilt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(core): the audio group model — element, membership, helpers

Introduces <hf-audio-group> and data-audio-group as the group model B2–B7
and C1 build on: a non-rendering group element carries a label and (later)
an FX chain, membership lives on the member's own data-audio-group
attribute rather than DOM nesting, so a track removed from the document
simply drops out of the group on the next resolve — nothing dangles.
Groups do not nest: data-audio-group on the group element itself is
ignored. A group with members but no <hf-audio-group> element still
resolves, label falling back to the id, so hand-authored HTML degrades
gracefully. Audio only in v1 — video members are ignored.

Parse-only: nothing routes or sums audio yet (B3/B4). Adds the
audio-groups canary at percentage: 0 gating the future Studio UI; the
element and attribute parse and play regardless of enrollment.

Verified rather than assumed per this plan's standing rule: the timeline's
clip-collection selector ([data-start], [data-track-index],
[data-composition-id], video, audio, img) already excludes the group
element with zero changes, and no lint rule flags unknown elements or
data-* attributes, so neither needed touching — confirmed by grep and by
running `hyperframes lint` against a fixture containing the element (0
findings referencing it). The step doc's suggested display:none injection
point (an existing base stylesheet in the runtime) does not exist in this
codebase; skipped rather than inventing new infrastructure, since an empty,
childless custom element already renders as a zero-size inline box with no
visible output — the same reasoning the lint check above confirms
empirically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-08-21 09:34:47 -07:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 5e0cc75115
commit 5240367150
7 changed files with 297 additions and 0 deletions
@@ -1376,6 +1376,30 @@ describe("useDomEditCommits attribute persist handling", () => {
}
});
it("sets and removes data-audio-group like any other data attribute", async () => {
stubPatchFetch({ ok: true, changed: true, matched: true });
const { iframe, element } = createPreviewElement();
const rendered = renderDomEditCommits(createSelection(element), iframe);
try {
await act(async () => {
await rendered.hook.handleDomAttributeLiveCommit("audio-group", "voiceover", undefined, {
previewOnly: true,
});
});
expect(element.getAttribute("data-audio-group")).toBe("voiceover");
await act(async () => {
await rendered.hook.handleDomAttributeLiveCommit("audio-group", "", undefined, {
previewOnly: true,
});
});
expect(element.getAttribute("data-audio-group")).toBeNull();
} finally {
rendered.cleanup();
}
});
it("keeps a data-attribute commit on success", async () => {
stubPatchFetch({
ok: true,