mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(studio,lint,skills): a bus is never a carve bed
A music bed came out carved twice. The `music` bus carried three
`fromCarve` peaking filters against `voiceover`, and its one member clip
`bgm` carried six of its own — the bed ran through both. Same on the SFX
side: the `sfx` bus plus `sfx-pan-2`.
**The bus classified as a bed.** `useFxCarve` is shared — a group's rack
reaches it too (`propertyPanelAudioFxGroup.tsx`, and the comment there
says so) — and `carveBedRoles` reads `data-label` so that a name living
outside a filename still counts. A bus labelled "Music bed" therefore
read as music: `couldBeBed` offered it the control and `autoBed` wrote
one without being asked, entirely independently of the member clip that
had already done the same. Nothing caught the collision, because the only
guard, `carverAgainst`, asks "is somebody naming ME as a source" — never
"is my own bus, or my own member, already carved".
**And a bus cannot carve properly anyway.** The level half of the
analysis measures the bed's own audio against the voice, read from
`element.getAttribute("src")`. A bus has no `src`, so `bedBuffer` was
null, `duck` was empty, and every bus carve was the spectral half alone:
filters and no level match. That is exactly the shape found in the file —
three peaking nodes and no gain node on the bus, five and a gain on the
clip.
So `carveBedRoles` now refuses a bus outright, both halves: `couldBeBed`
false alone would still leave `autoBed` free to fire off the label, which
is the half that wrote these. An already-configured bus carve still shows
its module (`carve !== null`), so it can be switched off rather than
becoming unreachable — the same escape hatch a38785003 kept.
`audio_group_carve_attr` names what is already written down, since the
Studio fix cannot unwrite it. Warning, beside `audio_group_timing_attrs`,
which is the same shape of mistake: an attribute a bus has no use for.
The skill has said "a carve stays on the clip" since the bus was
documented; it now says why, and says not to wrap a single clip in a bus
at all — with the one real exception, which is wanting a
composition-time automation clock.
Both guards mutation-checked: dropping the tag check fails the Studio
test, and `if (false)` in place of the attribute check fails both quiet
cases.
This commit is contained in:
@@ -651,6 +651,43 @@ describe("audio_group_timing_attrs", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("audio_group_carve_attr", () => {
|
||||
const doc = (busAttrs: string) => `<!DOCTYPE html><html><body>
|
||||
<div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="10">
|
||||
<hf-audio-group id="music" data-label="Music bed" ${busAttrs}></hf-audio-group>
|
||||
<audio id="bgm" src="bgm.mp3" data-start="0" data-duration="10" data-audio-group="music"></audio>
|
||||
</div>
|
||||
</body></html>`;
|
||||
|
||||
// The observed bug: the bus and its one member each carried a carve against
|
||||
// the same voiceover, so the bed ran through both sets of filters.
|
||||
it("warns on a carve written onto a bus", async () => {
|
||||
const res = await lintHyperframeHtml(
|
||||
doc(`data-fx-carve='{"enabled":true,"sources":["voiceover"],"strength":0.25}'`),
|
||||
);
|
||||
const finding = res.findings.find((f) => f.code === "audio_group_carve_attr");
|
||||
expect(finding?.severity).toBe("warning");
|
||||
expect(finding?.elementId).toBe("music");
|
||||
expect(finding?.message).toContain("data-fx-carve");
|
||||
});
|
||||
|
||||
it("stays quiet on a bus carrying only its own attributes", async () => {
|
||||
const res = await lintHyperframeHtml(doc(`data-volume="0.4"`));
|
||||
expect(res.findings.some((f) => f.code === "audio_group_carve_attr")).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves a carve on the clip alone", async () => {
|
||||
const res = await lintHyperframeHtml(`<!DOCTYPE html><html><body>
|
||||
<div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="10">
|
||||
<hf-audio-group id="music" data-label="Music bed"></hf-audio-group>
|
||||
<audio id="bgm" src="bgm.mp3" data-start="0" data-duration="10" data-audio-group="music"
|
||||
data-fx-carve='{"enabled":true,"sources":["voiceover"],"strength":0.25}'></audio>
|
||||
</div>
|
||||
</body></html>`);
|
||||
expect(res.findings.some((f) => f.code === "audio_group_carve_attr")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("audio_carve_ungrouped_sources", () => {
|
||||
const withCarve = (carveJson: string, extra = "") => `<!DOCTYPE html><html><body>
|
||||
<div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="10">
|
||||
|
||||
@@ -640,6 +640,9 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
|
||||
// audio_group_timing_attrs
|
||||
findAudioGroupTimingAttrFindings,
|
||||
|
||||
// audio_group_carve_attr
|
||||
findAudioGroupCarveAttrFindings,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -866,3 +869,35 @@ function findAudioGroupTimingAttrFindings(ctx: LintContext): HyperframeLintFindi
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
/**
|
||||
* A carve on a bus is half an effect, applied twice.
|
||||
*
|
||||
* `data-fx-carve` is a CLIP attribute. The bed being carved is one track, and
|
||||
* the level half of the analysis measures that track's own audio against the
|
||||
* voice — a bus has no `src`, so a carve there can only ever produce the
|
||||
* spectral half: filters with no level match.
|
||||
*
|
||||
* Worse, it stacks. A bus and a member clip are the same signal path, so a
|
||||
* carve on each puts the bed through both sets of filters — which is exactly
|
||||
* what happened when a bus labelled "Music bed" classified as one and carved
|
||||
* itself (fixed in Studio; this catches what was already written down).
|
||||
*/
|
||||
function findAudioGroupCarveAttrFindings(ctx: LintContext): HyperframeLintFinding[] {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
for (const tag of ctx.tags) {
|
||||
if (tag.name !== "hf-audio-group") continue;
|
||||
if (!hasAttrName(tag.raw, "data-fx-carve")) continue;
|
||||
const elementId = readAttr(tag.raw, "id") || undefined;
|
||||
findings.push({
|
||||
code: "audio_group_carve_attr",
|
||||
severity: "warning",
|
||||
message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved — a bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
|
||||
elementId,
|
||||
fixHint:
|
||||
"Remove `data-fx-carve` and the `fromCarve` nodes it wrote into this bus's `data-fx-chain`, and carve the bed clip instead.",
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user