mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix(core,studio): a voice track is never a carve bed, and never carves its own group
A narration clip inside a Voiceover group had a carve pointed at that group — a member ducking the bus it feeds. Three faults, each sufficient on its own. **No bed-eligibility rule.** `couldBeCarveSource` has said since it was written that music and sfx cannot be sources, and it is called from nowhere — exported, tested, dead. Nothing ever asked the near-end question: can this track be the BED. `showCarve` only asked "is anything already carving against me, and is there anything to listen to", so a voice track was offered the control like any other. Added `couldBeCarveBed` beside its sibling and wired it in. **Offering is not applying.** A bed with exactly one candidate carves itself unasked, which is right for a track named `music-bed` and wrong for one named `a1` — a decision taken off a name that said nothing is how a carve appears that nobody remembers configuring. `isNamedCarveBed` gates self-application on a name that positively reads as a bed; the picker stays looser, the same split the source side already makes between `sourceOptions` and `autoSourceIds`. **A bed was offered its own group.** The candidate scan excluded exactly one element, the bed itself. Its siblings survived that filter and rolled up into the very group the bed belongs to, which came back as a candidate — and being the only one, was applied. The mirror case too: a group bed's id matches no <audio> id, so nothing stopped a group carving against itself. `collectCarveCandidates` now takes the bed's id and drops both it and its group. An existing carve still shows its module (`carve !== null`), so nothing already configured becomes unreachable — only newly offered and self-applied ones are refused. The bed/relationship predicates moved to `useFxCarveGrouping.ts`, next to the source-eligibility rules they belong with. That is also what puts `useFxCarve.ts` back under the 600-line ceiling it crossed here. Five tests, each mutation-checked against the pre-fix code. Verified live: selecting `vo-2` renders no carve module; `music-bed` still gets one, listening to `Voiceover (4)`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5c882d8cb7
commit
c3daa90ba7
@@ -10,6 +10,8 @@ import {
|
||||
clipsOverlap,
|
||||
mixCarveSources,
|
||||
couldBeCarveSource,
|
||||
couldBeCarveBed,
|
||||
isNamedCarveBed,
|
||||
DEFAULT_CARVE,
|
||||
normalizeCarveSettings,
|
||||
} from "./audioCarve.js";
|
||||
@@ -538,6 +540,30 @@ describe("classifyAudioName", () => {
|
||||
expect(couldBeCarveSource("sfx-explosion")).toBe(false);
|
||||
});
|
||||
|
||||
// The near-end rule, which nothing used to ask. `couldBeCarveSource` shipped
|
||||
// with its own doc comment ("music and sfx are out") and no caller; the bed
|
||||
// side had no predicate at all, so a narration clip was offered the carve and
|
||||
// — finding one candidate — had one applied for it, against the group it was
|
||||
// a member of.
|
||||
it("never offers a voice track as the bed, but keeps an unnamed one eligible", () => {
|
||||
expect(couldBeCarveBed("music-bed")).toBe(true);
|
||||
expect(couldBeCarveBed("sfx-riser")).toBe(true);
|
||||
expect(couldBeCarveBed("a1")).toBe(true);
|
||||
expect(couldBeCarveBed("vo-2")).toBe(false);
|
||||
expect(couldBeCarveBed("voiceover")).toBe(false);
|
||||
expect(couldBeCarveBed("narration-3")).toBe(false);
|
||||
});
|
||||
|
||||
// Showing the control is a suggestion; writing the attribute is a decision.
|
||||
// A decision taken off a name that said nothing is how a carve appears that
|
||||
// nobody remembers configuring — so `a1` may be offered but never chosen.
|
||||
it("only self-applies to a name that positively reads as a bed", () => {
|
||||
expect(isNamedCarveBed("music-bed")).toBe(true);
|
||||
expect(isNamedCarveBed("sfx-riser")).toBe(true);
|
||||
expect(isNamedCarveBed("a1")).toBe(false);
|
||||
expect(isNamedCarveBed("vo-2")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats underscores as separators, not word characters, for short hints", () => {
|
||||
// `\b` treats `_` as a word character, so `\bbed\b` used to miss `bed_01` —
|
||||
// an underscore-separated bed classified as "unknown" and could end up
|
||||
|
||||
@@ -188,6 +188,40 @@ export function couldBeCarveSource(...parts: readonly (string | null | undefined
|
||||
return kind === "voice" || kind === "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Could this track be the BED a carve is written onto?
|
||||
*
|
||||
* The other half of `couldBeCarveSource`, and the half nothing used to ask. A
|
||||
* carve makes room in a bed for a voice; a voice track has no room to make for
|
||||
* itself, and offering it the control is offering a track to duck against its
|
||||
* own kind. Observed: a narration clip in a Voiceover group carved against that
|
||||
* group — a member ducking the bus it feeds.
|
||||
*
|
||||
* Loose in the same direction as its sibling: a name that says nothing stays
|
||||
* eligible, because a name is a hint and an author may know better. Only a name
|
||||
* that positively reads as speech is refused.
|
||||
*/
|
||||
export function couldBeCarveBed(...parts: readonly (string | null | undefined)[]): boolean {
|
||||
return classifyAudioName(...parts) !== "voice";
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this track's name positively say "bed"?
|
||||
*
|
||||
* Stricter than `couldBeCarveBed`, for the one act the author did not ask for:
|
||||
* applying a carve on their behalf. Offering the control on a track named `a1`
|
||||
* is a suggestion they can ignore; writing `data-fx-carve` onto it is a decision,
|
||||
* and a decision taken off a name that said nothing is how a carve appears that
|
||||
* nobody remembers configuring.
|
||||
*
|
||||
* The same split the source side already makes between what the picker may show
|
||||
* and what `autoSourceIds` may choose unprompted.
|
||||
*/
|
||||
export function isNamedCarveBed(...parts: readonly (string | null | undefined)[]): boolean {
|
||||
const kind = classifyAudioName(...parts);
|
||||
return kind === "music" || kind === "sfx";
|
||||
}
|
||||
|
||||
export const DEFAULT_CARVE: HfCarveSettings = {
|
||||
enabled: true,
|
||||
sources: [],
|
||||
|
||||
Reference in New Issue
Block a user