mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
fix(skills): pipeline fixes from prompt-guide validation (BGM, caption accent, voice, PR version)
Behavior fixes surfaced by the prompt-guide validation campaign (Tier 1+2 of the upstream bug list; Tier 3 tracked in #2107). Split out from the doc-only updates, which follow in a separate PR. - BGM level: default bed volume under narration was 0.8 linear (~-2 dB, ~16 dB too hot vs voice). Now 0.12 (~-18 dB) via shared bgmDefaultVolume() in media-use bgm.mjs + assemble-index fallbacks in faceless-explainer / pr-to-video / product-launch-video. Explicit volume still wins; silent-film 0.9 and music-to-video unchanged. Adds bgm.test.mjs (3 cases); bgm.md reference updated to match. - Caption accent: semanticColors() ranked accents purely by chroma, so a preserved status red (#dc2626) outranked the brand accent and captions highlighted in error-red. Status-keyed colors now excluded via shared STATUS_ROLE_KEY regex consumed by both tokens.mjs and build-frame.mjs (all three skill copies kept in sync). - Voice threading: workflow SKILL.md Step 3.1 blocks now instruct choosing the narration voice from the user's ask and passing --voice <id>; previously "a male voice" was silently ignored and the default (Marcia/am_michael) always won. - fetch-pr shipping version: MERGED PRs get best-effort shipped_version + version_source in pr.json (first release published at/after merge, else default-branch package.json marked unreleased); ingest surfaces it as a 'Shipped in:' brief line; story-design.md forbids inventing versions when absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d4b3bcaba3
commit
c992a136bf
@@ -22,14 +22,14 @@ One music bed per composition, produced by the shared audio engine (`scripts/aud
|
||||
```jsonc
|
||||
{
|
||||
"path": "assets/bgm/track.mp3",
|
||||
"volume": 0.8,
|
||||
"volume": 0.12,
|
||||
"mode": "retrieve",
|
||||
"query": "calm cinematic underscore",
|
||||
"duration_s": 42.0,
|
||||
}
|
||||
```
|
||||
|
||||
`volume` is 0.8 under narration, 0.9 for a silent film (no voice). `bgm_pending` is `false` — the file is on disk when the engine returns.
|
||||
`volume` is 0.12 (≈ -18 dB — a bed under the voice) under narration, 0.9 for a silent film (no voice). An explicit `volume` in `audio_meta.json` always overrides this default. `bgm_pending` is `false` — the file is on disk when the engine returns.
|
||||
|
||||
## Local generation (fallback) — Lyria → MusicGen
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ import { pythonInvocation } from "./python.mjs";
|
||||
const r3 = (x) => Number(x.toFixed(3));
|
||||
const lyriaKey = () => process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || "";
|
||||
|
||||
// Default BGM level. Under narration music is a bed that must stay under the
|
||||
// voice — 0.12 linear ≈ -18 dB. A silent film (no voice) has no voice to duck
|
||||
// beneath, so BGM sits forward at 0.9. Callers may override per composition.
|
||||
export const BGM_BED_VOLUME = 0.12;
|
||||
export const BGM_SILENT_VOLUME = 0.9;
|
||||
export const bgmDefaultVolume = (hasVoice) => (hasVoice ? BGM_BED_VOLUME : BGM_SILENT_VOLUME);
|
||||
|
||||
const BGM_PY_DEPS = ["transformers", "torch", "soundfile", "numpy"];
|
||||
const BGM_PY_PROBE =
|
||||
"import transformers, soundfile, torch, numpy; from transformers import MusicgenForConditionalGeneration";
|
||||
@@ -51,7 +58,7 @@ export async function retrieveBgm({ query, headers, hyperframesDir, hasVoice })
|
||||
await downloadTo(top.audio_url, join(hyperframesDir, rel));
|
||||
return {
|
||||
path: rel,
|
||||
volume: hasVoice ? 0.8 : 0.9,
|
||||
volume: bgmDefaultVolume(hasVoice),
|
||||
query: q,
|
||||
mode: "retrieve",
|
||||
duration_s: typeof top.duration === "number" ? r3(top.duration) : null,
|
||||
@@ -116,7 +123,7 @@ export function generateBgmDetached({
|
||||
mkdirSync(join(hyperframesDir, "assets", "bgm"), { recursive: true });
|
||||
const log = join(hyperframesDir, "assets", "bgm", `bgm-${Date.now()}.log`);
|
||||
const targetS = Math.max(1, durationS);
|
||||
const baseMeta = { path: rel, mode: null, volume: hasVoice ? 0.8 : 0.9, pending: true };
|
||||
const baseMeta = { path: rel, mode: null, volume: bgmDefaultVolume(hasVoice), pending: true };
|
||||
|
||||
const lyriaConfigured = !!lyriaKey() && !!lyriaRecipe && existsSync(lyriaRecipe);
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { BGM_BED_VOLUME, BGM_SILENT_VOLUME, bgmDefaultVolume } from "./bgm.mjs";
|
||||
|
||||
// Regression: narrated pipelines used to ship BGM at 0.8 (≈ -2 dB), ~16 dB
|
||||
// hotter than a music bed under a voice should be. The default under narration
|
||||
// must be a proper bed (≈ -18 dB); a silent film keeps the louder default.
|
||||
|
||||
const dbfs = (linear) => 20 * Math.log10(linear);
|
||||
|
||||
test("BGM under narration is a bed near -18 dB", () => {
|
||||
assert.equal(bgmDefaultVolume(true), BGM_BED_VOLUME);
|
||||
assert.equal(BGM_BED_VOLUME, 0.12);
|
||||
const db = dbfs(BGM_BED_VOLUME);
|
||||
assert.ok(db < -17 && db > -19, `bed should be ≈ -18 dB, got ${db.toFixed(1)} dB`);
|
||||
});
|
||||
|
||||
test("a silent film (no voice) keeps BGM forward", () => {
|
||||
assert.equal(bgmDefaultVolume(false), BGM_SILENT_VOLUME);
|
||||
assert.equal(BGM_SILENT_VOLUME, 0.9);
|
||||
});
|
||||
|
||||
test("the narrated default is well below the voice (≈ 0 dBFS)", () => {
|
||||
// Voice sits at data-volume="1" (0 dBFS); the bed must be ~16+ dB under it.
|
||||
const separation = dbfs(1) - dbfs(bgmDefaultVolume(true));
|
||||
assert.ok(
|
||||
separation >= 16,
|
||||
`bed should sit ≥16 dB under the voice, got ${separation.toFixed(1)} dB`,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user