mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
* feat(hyperframes-creative): add frame-preset library Add a library of ready-made visual frame presets (claude, biennale-yellow, blockframe, blue-professional, bold-poster, broadside, capsule, cartesian, cobalt-grid, coral, creative-mode, daisy-days, editorial-forest, …), each with a FRAME.md spec, a frame-showcase.html, and a per-preset caption-skin.html. Registered in the creative design-spec so workflows can remix a preset onto brand tokens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hyperframes-media): shared TTS/BGM/SFX audio engine Add a shared audio engine under hyperframes-media (scripts/audio.mjs + lib/ tts.mjs, bgm.mjs, sfx.mjs, heygen.mjs) plus a bundled SFX pack and manifest. Workflows resolve this engine by path (../../hyperframes-media/scripts/ audio.mjs) for text-to-speech, background music, and sound effects, so audio is authored once and reused across skills instead of duplicated per workflow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(skills): gate render on user review; refresh router, core, general-video - hyperframes-cli: render is now user-gated — preview opens Studio (the timeline editor where the user can hand-edit anything, not just watch); never auto-render once checks pass, pause at preview and render only after approval. - hyperframes (router): tighten the entry SKILL.md description + routing. - hyperframes-core: rewrite SKILL.md and add script-format.md + storyboard-format.md references for the script-driven authoring architecture. - general-video: tidy the fallback-workflow description and routing table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(hyperframes-creative): reformat frame-preset showcase HTML Run the HTML formatter over the frame-showcase.html files (indentation, self-closing void tags, one CSS declaration per line). Formatting only — no content or markup changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hyperframes-media): correct wait-bgm field mapping and guard credential parse Two correctness fixes from review (#1632): - wait-bgm.mjs read audioMeta.bgm_path / audioMeta.bgm_enabled, but audio.mjs writes the path nested as bgm.path and the flag as bgm_pending. The detached generate path (Lyria/MusicGen) therefore always saw an empty path and exited status: disabled, silently dropping the music track even while generation was running. Read audioMeta.bgm?.path and gate on bgm_pending. - heygenCredential() had an unguarded JSON.parse despite documenting that it never throws — a malformed ~/.heygen credentials file crashed the engine at startup instead of degrading to no-credential. Wrap the parse and return null. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(hyperframes): add router tag to entry skill metadata Fold the router metadata tag into the foundation rewrite of the entry SKILL.md. This file is owned by this PR (the full router rewrite); keeping the tag tweak here — instead of a separate edit on the pre-rewrite version in another PR — avoids a guaranteed merge conflict between the two. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
168 lines
5.4 KiB
JavaScript
168 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Phase 4c pre-assemble helper — wait for detached BGM, then write status.
|
|
//
|
|
// audio.mjs may launch Lyria / MusicGen in a detached process so voice work can
|
|
// keep moving. Before assemble-index.mjs decides whether to emit the BGM audio
|
|
// track, this script gives the background renderer a bounded chance to finish
|
|
// and converts log/process state into a small bgm_status.json file.
|
|
//
|
|
// Always exits 0 for normal pipeline use: missing/failed BGM should not block a
|
|
// voice/captions/SFX render. Structural invocation errors still exit 1.
|
|
//
|
|
// Usage:
|
|
// node wait-bgm.mjs --audio-meta ./audio_meta.json --hyperframes . \
|
|
// [--timeout-ms 120000] [--interval-ms 2000] [--out ./bgm_status.json]
|
|
|
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
|
|
const argv = process.argv.slice(2);
|
|
const flag = (name, def) => {
|
|
const i = argv.indexOf(`--${name}`);
|
|
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
|
|
};
|
|
|
|
function die(msg) {
|
|
console.error(`✗ wait-bgm.mjs: ${msg}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const audioMetaPath = resolve(flag("audio-meta", "./audio_meta.json"));
|
|
const hyperframesDir = resolve(flag("hyperframes", "."));
|
|
const outPath = resolve(flag("out", join(hyperframesDir, "bgm_status.json")));
|
|
const timeoutMs = Math.max(0, Number(flag("timeout-ms", "120000")) || 0);
|
|
const intervalMs = Math.max(250, Number(flag("interval-ms", "2000")) || 2000);
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
}
|
|
|
|
function isProcessAlive(pid) {
|
|
if (!pid || !Number.isFinite(Number(pid))) return false;
|
|
try {
|
|
process.kill(Number(pid), 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function readTail(path, maxChars = 6000) {
|
|
if (!path || !existsSync(path)) return "";
|
|
const s = statSync(path);
|
|
const txt = readFileSync(path, "utf8");
|
|
return txt.slice(Math.max(0, txt.length - Math.min(maxChars, s.size)));
|
|
}
|
|
|
|
function detectFailure(logTail) {
|
|
if (!logTail) return "";
|
|
const lines = logTail.split("\n");
|
|
// Bare "out of range" over-matched benign BGM-renderer logs (e.g. a "sample rate
|
|
// out of range, resampling" notice), mislabelling a healthy track as failed and
|
|
// silently dropping the music. Anchor to the actual crash strings instead:
|
|
// Python "(list) index out of range" and torch "index … out of bounds".
|
|
const idx = lines.findIndex((line) =>
|
|
/(Traceback|IndexError|RuntimeError|Exception|Killed|No space left|Cannot allocate|index out of range|out of bounds)/i.test(
|
|
line,
|
|
),
|
|
);
|
|
if (idx < 0) return "";
|
|
return lines.slice(idx).join("\n").trim();
|
|
}
|
|
|
|
function writeStatus(status) {
|
|
const payload = {
|
|
generated_at: new Date().toISOString(),
|
|
...status,
|
|
};
|
|
writeFileSync(outPath, JSON.stringify(payload, null, 2) + "\n");
|
|
return payload;
|
|
}
|
|
|
|
if (!existsSync(audioMetaPath)) die(`audio_meta.json missing at ${audioMetaPath}`);
|
|
|
|
const audioMeta = JSON.parse(readFileSync(audioMetaPath, "utf8"));
|
|
const bgmPath = audioMeta.bgm?.path || "";
|
|
const bgmAbsPath = bgmPath ? join(hyperframesDir, bgmPath) : "";
|
|
const logPath = audioMeta.bgm_log || "";
|
|
const pid = audioMeta.bgm_pid || null;
|
|
|
|
const base = {
|
|
enabled: Boolean(audioMeta.bgm_pending && bgmPath),
|
|
provider: audioMeta.bgm_provider || null,
|
|
mode: audioMeta.bgm_mode || null,
|
|
path: bgmPath || null,
|
|
log: logPath || null,
|
|
pid,
|
|
target_duration_s: audioMeta.bgm_target_duration_s || null,
|
|
seed_duration_s: audioMeta.bgm_seed_duration_s || null,
|
|
loop_count: audioMeta.bgm_loop_count || null,
|
|
timeout_ms: timeoutMs,
|
|
};
|
|
|
|
if (!base.enabled) {
|
|
const status = writeStatus({
|
|
...base,
|
|
status: "disabled",
|
|
ready: false,
|
|
waited_ms: 0,
|
|
message: "BGM not requested or disabled in audio_meta.json.",
|
|
});
|
|
console.log(`✓ bgm: ${status.status} (${status.message})`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const started = Date.now();
|
|
let lastFailure = "";
|
|
let lastTail = "";
|
|
|
|
while (Date.now() - started <= timeoutMs) {
|
|
if (existsSync(bgmAbsPath)) {
|
|
const size = statSync(bgmAbsPath).size;
|
|
writeStatus({
|
|
...base,
|
|
status: "ready",
|
|
ready: true,
|
|
waited_ms: Date.now() - started,
|
|
size_bytes: size,
|
|
message: `BGM ready at ${bgmPath}.`,
|
|
});
|
|
console.log(`✓ bgm: ready (${bgmPath}, ${size}B)`);
|
|
process.exit(0);
|
|
}
|
|
|
|
lastTail = readTail(logPath);
|
|
lastFailure = detectFailure(lastTail);
|
|
const alive = isProcessAlive(pid);
|
|
if (lastFailure || (!alive && logPath && existsSync(logPath))) {
|
|
const message = lastFailure
|
|
? `BGM renderer failed; see ${logPath}.`
|
|
: `BGM renderer exited without writing ${bgmPath}; see ${logPath}.`;
|
|
const status = writeStatus({
|
|
...base,
|
|
status: "failed",
|
|
ready: false,
|
|
waited_ms: Date.now() - started,
|
|
process_alive: alive,
|
|
message,
|
|
error_tail: lastFailure || lastTail.slice(-2000),
|
|
});
|
|
console.log(`! bgm: failed (${status.message})`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (timeoutMs === 0) break;
|
|
await sleep(Math.min(intervalMs, Math.max(0, timeoutMs - (Date.now() - started))));
|
|
}
|
|
|
|
const status = writeStatus({
|
|
...base,
|
|
status: "timeout",
|
|
ready: false,
|
|
waited_ms: Date.now() - started,
|
|
process_alive: isProcessAlive(pid),
|
|
message: `Timed out waiting for ${bgmPath}; assemble-index will skip BGM if still absent.`,
|
|
log_tail: lastTail.slice(-2000),
|
|
});
|
|
console.log(`! bgm: timeout after ${status.waited_ms}ms (${bgmPath})`);
|