Fix render
This commit is contained in:
parent
68097386df
commit
bf3f09aeb6
4 changed files with 96 additions and 80 deletions
118
video/render.sh
118
video/render.sh
|
|
@ -22,9 +22,10 @@
|
|||
# ./render.sh --fresh-auth # force re-auth even if cache is fresh
|
||||
# ./render.sh --resume # preserve completed recordings and continue
|
||||
# ./render.sh --no-encode # stop at WebM, skip MP4 encode
|
||||
# ./render.sh --no-audio # skip Qwen3-TTS narration; silent MP4
|
||||
# ./render.sh --no-audio # skip narration synthesis; silent MP4
|
||||
# FORCE_AUTH=1 ./render.sh # same as --fresh-auth
|
||||
# APP_URL=http://localhost:3001 ./render.sh # override frontend URL
|
||||
# CHATTERBOX_URL=http://box:8004 ./render.sh # override the TTS server
|
||||
#
|
||||
# Cred env vars (read for both targets, but prod has no fallback defaults):
|
||||
# LOGIN_EMAIL, LOGIN_PASSWORD: the dashboard account to record as
|
||||
|
|
@ -80,6 +81,9 @@ else
|
|||
fi
|
||||
export PB_BOOTSTRAP_ADMIN
|
||||
export LOGIN_EMAIL LOGIN_PASSWORD
|
||||
# Narration TTS. Not target-specific: Chatterbox is a LAN service, so prod
|
||||
# renders and local renders both narrate from the same box.
|
||||
export CHATTERBOX_URL="${CHATTERBOX_URL:-http://host.docker.internal:8004}"
|
||||
# Per-target storage state: switching targets must not reuse a stale token.
|
||||
# config.ts reads AUTH_STATE_FILE for AUTH_STATE_PATH.
|
||||
export AUTH_STATE_FILE="${AUTH_STATE_FILE:-auth.${TARGET}.json}"
|
||||
|
|
@ -101,7 +105,10 @@ for arg in "${@:-}"; do
|
|||
--no-encode) DO_ENCODE=0 ;;
|
||||
--no-audio) DO_AUDIO=0 ;;
|
||||
-h|--help)
|
||||
sed -n '3,32p' "$0"
|
||||
# Print the header comment block, stopping at the first non-comment
|
||||
# line. A hardcoded end line silently truncates --help mid-sentence the
|
||||
# next time someone documents a flag.
|
||||
sed -n '3,/^[^#]/p' "$0" | sed '$d'
|
||||
exit 0 ;;
|
||||
*) echo "Unknown arg: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
|
|
@ -128,6 +135,29 @@ http_code() {
|
|||
curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 --max-time 5 "$1" || echo "000"
|
||||
}
|
||||
|
||||
# Order storyboards so all English cuts synth together and all non-English
|
||||
# cuts synth together. The Chatterbox server holds one checkpoint at a time
|
||||
# and English lives on a different one from every other language, so an
|
||||
# interleaved order (recording, recording-de, recording-zh, recording-hi)
|
||||
# would pay a ~20s swap three times instead of once. Reads the language each
|
||||
# storyboard declared in the narration script preflight just emitted; any
|
||||
# storyboard whose script is unreadable sorts last and still gets rendered.
|
||||
synth_order() {
|
||||
local sb lang
|
||||
for sb in "$@"; do
|
||||
lang="$(node -e '
|
||||
const fs = require("node:fs");
|
||||
try {
|
||||
process.stdout.write(
|
||||
JSON.parse(fs.readFileSync(process.argv[1], "utf-8")).voice?.language ?? "zz"
|
||||
);
|
||||
} catch { process.stdout.write("zz"); }
|
||||
' "output/$sb/narration-script.json" 2>/dev/null || echo "zz")"
|
||||
# en sorts before everything else; the rest group by language code.
|
||||
printf '%s\t%s\n' "$([ "$lang" = "en" ] && echo "0-en" || echo "1-$lang")" "$sb"
|
||||
done | sort -s -k1,1 | cut -f2
|
||||
}
|
||||
|
||||
wait_for() {
|
||||
local url="$1" desc="$2" timeout="${3:-90}"
|
||||
say "Waiting for $desc ($url)"
|
||||
|
|
@ -292,80 +322,40 @@ for sb in "${STORYBOARDS[@]}"; do
|
|||
fi
|
||||
done
|
||||
|
||||
# -- synth (Qwen3-TTS) -------------------------------------------------------
|
||||
# Synth runs BEFORE recording: one batched generate_voice_clone call per
|
||||
# storyboard so the voice stays consistent within each video. The recorder
|
||||
# reads output/<sb>/audio/index.json for measured per-cue durations and
|
||||
# sizes each cue's wall-clock to fit; --no-audio skips synth and the recorder
|
||||
# falls back to a worst-case estimate.
|
||||
# -- synth (Chatterbox) ------------------------------------------------------
|
||||
# Synth runs BEFORE recording: the recorder reads output/<sb>/audio/index.json
|
||||
# for measured per-cue durations and sizes each cue's wall-clock to fit;
|
||||
# --no-audio skips synth and the recorder falls back to a worst-case estimate.
|
||||
#
|
||||
# Narration is generated by the Chatterbox TTS server over HTTP, so there is
|
||||
# nothing to install and no GPU to schedule here. Voice timbre needs no
|
||||
# defending either: a predefined voice is a fixed asset on the server, so
|
||||
# every cue of every storyboard clones from identical bytes by construction.
|
||||
# (The Qwen3-TTS pipeline this replaced had to mint a reference WAV per render
|
||||
# and hand-copy it between storyboards to stop two voices landing in one cut.)
|
||||
#
|
||||
# Storyboards are synthesised in language order so an en/de/zh set pays the
|
||||
# ~20s English<->multilingual checkpoint swap once instead of once per cut.
|
||||
# synth.py itself only swaps when the server is on the wrong checkpoint.
|
||||
if [ "$DO_AUDIO" = "1" ]; then
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
fail "uv not on PATH (required for Qwen3-TTS synth). Install uv or rerun with --no-audio."
|
||||
fi
|
||||
# The torch/cudnn wheels are ~700MB; uv's 30s default chokes on first sync.
|
||||
export UV_HTTP_TIMEOUT="${UV_HTTP_TIMEOUT:-600}"
|
||||
# Pull in the flash-attn prebuilt wheel (defined as the `gpu` extra) when
|
||||
# the host actually has a GPU. The wheel is bound to torch 2.6 + cu12 +
|
||||
# cp312. See tts/pyproject.toml.
|
||||
uv_sync_extras=()
|
||||
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then
|
||||
uv_sync_extras+=(--extra gpu)
|
||||
fail "uv not on PATH (required for tts/synth.py). Install uv or rerun with --no-audio."
|
||||
fi
|
||||
say "Synchronising tts/ Python deps"
|
||||
uv sync --project tts ${uv_sync_extras[@]+"${uv_sync_extras[@]}"} || fail "uv sync failed in video/tts"
|
||||
uv sync --project tts || fail "uv sync failed in video/tts"
|
||||
|
||||
if [ -z "${TTS_DEVICE:-}" ]; then
|
||||
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then
|
||||
gpu_free_mb="$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ' ')"
|
||||
if [ "${gpu_free_mb:-0}" -ge 8000 ]; then
|
||||
export TTS_DEVICE="cuda:0"
|
||||
else
|
||||
export TTS_DEVICE="cpu"
|
||||
say "GPU has ${gpu_free_mb:-0}MiB free; using CPU for TTS to avoid CUDA OOM"
|
||||
fi
|
||||
else
|
||||
export TTS_DEVICE="cpu"
|
||||
fi
|
||||
cb_code="$(http_code "$CHATTERBOX_URL/api/model-info")"
|
||||
if [ "$cb_code" != "200" ]; then
|
||||
fail "Cannot reach the Chatterbox TTS server at $CHATTERBOX_URL (got $cb_code). Start it, set CHATTERBOX_URL, or rerun with --no-audio."
|
||||
fi
|
||||
|
||||
# Voice consistency: every ad in this set declares the same AD_VOICE
|
||||
# (instruct/seed/temperature/topP/referenceText). Even with seed-locked
|
||||
# VoiceDesign, independent invocations across processes can produce
|
||||
# mildly different reference waveforms, different enough that a
|
||||
# listener notices the timbre shift across ads. To avoid that, we
|
||||
# mint the reference WAV ONCE (from the first storyboard) and reuse
|
||||
# it across the rest of the storyboards by copying _reference.wav +
|
||||
# _reference.meta.json into their audio dirs before their synth runs.
|
||||
# synth.py's _resolve_reference() reuses a matching cached reference
|
||||
# as long as the meta block (instruct/language/seed/etc.) matches.
|
||||
#
|
||||
# We copy ONLY the reference, never the cue wavs or index.json. Copying
|
||||
# the whole audio dir (as an earlier version did) overwrote each later
|
||||
# storyboard's cached index.json with the FIRST storyboard's, which
|
||||
# forced a full re-synth on every run, and in multi-voice sets (the
|
||||
# localized homepage demos: en/de/zh/hi) it clobbered correct localized
|
||||
# audio. With a reference-only copy: same-voice sets reuse the reference
|
||||
# (meta matches); different-voice sets re-mint their own (meta mismatch),
|
||||
# and in both cases an up-to-date cached index.json lets synth skip.
|
||||
shared_ref_wav=""
|
||||
shared_ref_meta=""
|
||||
for sb in "${STORYBOARDS[@]}"; do
|
||||
if [ -n "$shared_ref_wav" ] && [ -f "$shared_ref_wav" ] && [ -f "$shared_ref_meta" ]; then
|
||||
mkdir -p "output/$sb/audio"
|
||||
cp -f "$shared_ref_wav" "output/$sb/audio/_reference.wav"
|
||||
cp -f "$shared_ref_meta" "output/$sb/audio/_reference.meta.json"
|
||||
fi
|
||||
for sb in $(synth_order "${STORYBOARDS[@]}"); do
|
||||
say "Synthesising narration for [$sb]"
|
||||
uv run --project tts python tts/synth.py --storyboard "$sb" \
|
||||
CHATTERBOX_URL="$CHATTERBOX_URL" uv run --project tts python tts/synth.py --storyboard "$sb" \
|
||||
|| fail "tts/synth.py failed for $sb"
|
||||
if [ ! -s "output/$sb/audio/index.json" ]; then
|
||||
fail "synth did not produce output/$sb/audio/index.json"
|
||||
fi
|
||||
if [ -z "$shared_ref_wav" ] && [ -f "output/$sb/audio/_reference.wav" ]; then
|
||||
shared_ref_wav="output/$sb/audio/_reference.wav"
|
||||
shared_ref_meta="output/$sb/audio/_reference.meta.json"
|
||||
say "Locked voice reference to $shared_ref_wav; reusing for the rest of the set"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue