mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio,lint): carve targets voiceover groups — always, when plural
Plural voiceover carve now targets a group instead of naming each clip: `resolveCarveSourceIds` (core `audioGroups.ts`) expands a group id to its current members at analysis time, so a clip added to the group later is covered without touching `sources`. The picker (`useFxCarve.ts`) offers a grouped voice as one option instead of one row per member, tests overlap as a union of member spans (a group overlaps the bed if ANY member does), and prefers a qualifying group over its individual members in `autoSourceIds`. Picking two or more ungrouped voice clips in the carve flow now mints a group behind them (`mintGroupId`, de-duped against every id in the document) and writes `data-audio-group` on each picked clip atomically, one undo entry — `createAudioGroupAndAssignMembers` in `timelineTrackVisibility.ts` copies `setElementsHidden`'s multi-target write shape. The DSP is untouched: `mixCarveSources` already sums multiple sources correctly (verified in the design doc's own investigation) — this only fixes the picker. New lint rule `audio_carve_ungrouped_sources` (`packages/lint/src/rules/ media.ts`, alongside `audio_volume_double_automation`) warns when a `data-fx-carve`'s `sources` names two or more plain clip ids instead of a group — the shape that silently rots when a clip is added. `/hyperframes- audio` states the same rule as an invariant, not a tip, with the grouped- narration HTML example from the design doc. The group-matching and auto-group logic (`withAutoGroupedSources`, `collectCarveCandidates`) is split into `useFxCarveGrouping.ts` — `useFxCarve.ts` was pushing past the 600-line cap. `resolveNextCarveSettings` is deliberately NOT an `async function`: wrapping it in one would force a microtask on every call, including the synchronous branch — the exact bug `withAutoGroupedSources`'s own sync-when-possible contract exists to avoid, and one caught via `propertyPanelAudioFxGroup.test.tsx` (10 failures) before fixing it back to a plain function the caller conditionally awaits. Also extracted `useEffectiveTimelineDuration` out of `App.tsx` and `useRemoveBackground` out of `StudioRightPanel.tsx` (both pushed past 600 lines from an added prop wire), and decomposed `useFxCarve.ts`'s picker IIFE to clear fallow's complexity gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8d48a6f52f
commit
1abad17650
@@ -1,5 +1,10 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { audioGroupOf, HF_AUDIO_GROUP_ATTR, resolveAudioGroups } from "./audioGroups.js";
|
||||
import {
|
||||
audioGroupOf,
|
||||
HF_AUDIO_GROUP_ATTR,
|
||||
resolveAudioGroups,
|
||||
resolveCarveSourceIds,
|
||||
} from "./audioGroups.js";
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
@@ -64,6 +69,47 @@ describe("audioGroupOf", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCarveSourceIds", () => {
|
||||
it("expands a group id to its current members", () => {
|
||||
document.body.innerHTML = `
|
||||
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
||||
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
||||
`;
|
||||
expect(resolveCarveSourceIds(document, ["voiceover"])).toEqual(["vo-1", "vo-2"]);
|
||||
});
|
||||
|
||||
it("picks up a member added to the group after the carve was set (analysis-time, not frozen)", () => {
|
||||
document.body.innerHTML = `
|
||||
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
||||
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
||||
`;
|
||||
expect(resolveCarveSourceIds(document, ["voiceover"])).toEqual(["vo-1", "vo-2"]);
|
||||
document.body.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<audio id="vo-3" data-audio-group="voiceover"></audio>`,
|
||||
);
|
||||
expect(resolveCarveSourceIds(document, ["voiceover"])).toEqual(["vo-1", "vo-2", "vo-3"]);
|
||||
});
|
||||
|
||||
it("passes through a plain clip id that still exists", () => {
|
||||
document.body.innerHTML = `<audio id="vo-1"></audio>`;
|
||||
expect(resolveCarveSourceIds(document, ["vo-1"])).toEqual(["vo-1"]);
|
||||
});
|
||||
|
||||
it("drops an id that resolves to nothing — a deleted clip, an empty or vanished group", () => {
|
||||
document.body.innerHTML = `<audio id="vo-1"></audio>`;
|
||||
expect(resolveCarveSourceIds(document, ["vo-1", "deleted", "no-such-group"])).toEqual(["vo-1"]);
|
||||
});
|
||||
|
||||
it("dedupes and preserves first-seen order across a mix of group and plain ids", () => {
|
||||
document.body.innerHTML = `
|
||||
<audio id="vo-1" data-audio-group="voiceover"></audio>
|
||||
<audio id="vo-2" data-audio-group="voiceover"></audio>
|
||||
`;
|
||||
expect(resolveCarveSourceIds(document, ["voiceover", "vo-1"])).toEqual(["vo-1", "vo-2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(HF_AUDIO_GROUP_ATTR, () => {
|
||||
it("is the attribute name membership is keyed on", () => {
|
||||
expect(HF_AUDIO_GROUP_ATTR).toBe("data-audio-group");
|
||||
|
||||
@@ -51,6 +51,37 @@ export function resolveAudioGroups(root: ParentNode): HfAudioGroup[] {
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a list of source ids for a carve: a plain id passes through if it
|
||||
* still exists, a group id expands to its CURRENT members. Resolved fresh
|
||||
* every time — group membership is never frozen into the carve's own
|
||||
* attribute, so adding a fourth voice to a group already named in a carve's
|
||||
* `sources` picks it up on the next analysis without editing that carve.
|
||||
*
|
||||
* Dedupes and preserves first-seen order; an id that resolves to nothing
|
||||
* (a deleted clip, an empty or vanished group) is dropped rather than kept
|
||||
* as a dangling reference the analysis would only fail to find anyway.
|
||||
*/
|
||||
export function resolveCarveSourceIds(doc: Document, ids: readonly string[]): string[] {
|
||||
const groupsById = new Map(resolveAudioGroups(doc).map((group) => [group.id, group] as const));
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
const add = (id: string): void => {
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
out.push(id);
|
||||
};
|
||||
for (const id of ids) {
|
||||
const group = groupsById.get(id);
|
||||
if (group) {
|
||||
group.memberIds.forEach(add);
|
||||
} else if (doc.getElementById(id)) {
|
||||
add(id);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The group a member belongs to, or null. Groups do not nest — this ignores
|
||||
* `data-audio-group` on an `<hf-audio-group>` element itself.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user