85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
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})`);
|