1 Commits

14 changed files with 129 additions and 34 deletions

View File

@@ -28,18 +28,40 @@ OUT_AT=$(echo "$D - 2.5" | bc)
# A slow A-minor-ish drone, left and right a little apart, echoed and soft. # A slow A-minor-ish drone, left and right a little apart, echoed and soft.
PAD="0.06*(sin(2*PI*110*t)+0.7*sin(2*PI*164.8*t)+0.5*sin(2*PI*220*t)+0.35*sin(2*PI*261.6*t)*(0.5+0.5*sin(2*PI*0.08*t)))*(0.75+0.25*sin(2*PI*0.21*t))" PAD="0.06*(sin(2*PI*110*t)+0.7*sin(2*PI*164.8*t)+0.5*sin(2*PI*220*t)+0.35*sin(2*PI*261.6*t)*(0.5+0.5*sin(2*PI*0.08*t)))*(0.75+0.25*sin(2*PI*0.21*t))"
PAD_R="0.06*(sin(2*PI*110.4*t)+0.7*sin(2*PI*165.2*t)+0.5*sin(2*PI*219.6*t)+0.35*sin(2*PI*262*t)*(0.5+0.5*cos(2*PI*0.08*t)))*(0.75+0.25*cos(2*PI*0.21*t))" PAD_R="0.06*(sin(2*PI*110.4*t)+0.7*sin(2*PI*165.2*t)+0.5*sin(2*PI*219.6*t)+0.35*sin(2*PI*262*t)*(0.5+0.5*cos(2*PI*0.08*t)))*(0.75+0.25*cos(2*PI*0.21*t))"
# Old Tam (0.6.2: tools/trailer/voice, made by make_trailer_voice.py), each # Old Tam narrates (0.6.2: tools/trailer/voice, made by make_trailer_voice.py):
# line a moment into its shot: the dawn flyover (shot 0), the wolves (3) and # each line `at` seconds into its shot, by the shots' start times in the
# the Cove (6). The shots' start times are in the render log. # render log. The game's sound and the music dip under him.
at() { awk -v n="$1" -v d="$2" '$1 == "[shot]" && $2 == n { printf "%d", ($3 + d) * 1000 }' build/trailer/render.log; }
V=tools/trailer/voice V=tools/trailer/voice
T_DAWN=$(at 0 2.2); T_WOLVES=$(at 3 1.0); T_COVE=$(at 6 1.2) NARR=$(python3 - "$V/lines.json" build/trailer/render.log <<'EOF'
[ -n "$T_DAWN" ] && [ -n "$T_WOLVES" ] && [ -n "$T_COVE" ] || { echo "no shot times in build/trailer/render.log"; exit 1; } import json, sys
ffmpeg -v error -y -i "$RAW" -f lavfi -i "aevalsrc='$PAD|$PAD_R':s=48000:d=$D" \ lines = json.load(open(sys.argv[1]))
-i "$V/dawn.ogg" -i "$V/wolves.ogg" -i "$V/cove.ogg" \ starts = {}
for l in open(sys.argv[2]):
p = l.split()
if len(p) == 3 and p[0] == "[shot]":
starts[int(p[1])] = float(p[2])
inputs, chains, labels = [], [], []
for n, x in enumerate(lines):
if x["shot"] not in starts:
sys.exit("no start time for shot %d in the render log" % x["shot"])
ms = int((starts[x["shot"]] + x["at"]) * 1000)
inputs.append("%s/%s" % (sys.argv[1].rsplit("/", 1)[0], x["file"]))
chains.append("[%d:a]aresample=48000,adelay=%d|%d,volume=1.7[n%d]" % (n + 2, ms, ms, n))
labels.append("[n%d]" % n)
print("\t".join(inputs))
print(";".join(chains) + ";" + "".join(labels) + "amix=inputs=%d:duration=longest:normalize=0[v]" % len(labels))
EOF
) || exit 1
NARR_IN=$(printf '%s\n' "$NARR" | head -1)
NARR_FX=$(printf '%s\n' "$NARR" | tail -1)
set --
OLDIFS=$IFS; IFS=' '
for f in $NARR_IN; do set -- "$@" -i "$f"; done
IFS=$OLDIFS
ffmpeg -v error -y -i "$RAW" -f lavfi -i "aevalsrc='$PAD|$PAD_R':s=48000:d=$D" "$@" \
-filter_complex "[1:a]lowpass=f=1800,aecho=0.8:0.7:180|340:0.35|0.25,afade=t=in:d=2.5,afade=t=out:st=$OUT_AT:d=2.5[m];[0:a]volume=0.8[g];\ -filter_complex "[1:a]lowpass=f=1800,aecho=0.8:0.7:180|340:0.35|0.25,afade=t=in:d=2.5,afade=t=out:st=$OUT_AT:d=2.5[m];[0:a]volume=0.8[g];\
[2:a]aresample=48000,adelay=$T_DAWN|$T_DAWN,volume=1.6[v0];[3:a]aresample=48000,adelay=$T_WOLVES|$T_WOLVES,volume=1.6[v1];[4:a]aresample=48000,adelay=$T_COVE|$T_COVE,volume=1.6[v2];\ $NARR_FX;[v]asplit=2[v1][vk];[g][m]amix=inputs=2:duration=first:normalize=0[bed];\
[v0][v1][v2]amix=inputs=3:duration=longest:normalize=0[v];[g][m][v]amix=inputs=3:duration=first:normalize=0[a]" \ [bed][vk]sidechaincompress=threshold=0.02:ratio=6:attack=40:release=500[ducked];[ducked][v1]amix=inputs=2:duration=first:normalize=0[a]" \
-map 0:v -map "[a]" -vf "scale=in_range=pc:out_range=tv,format=yuv420p" -color_range tv \ -map 0:v -map "[a]" -vf "scale=in_range=pc:out_range=tv,format=yuv420p" -color_range tv \
-colorspace bt709 -color_primaries bt709 -color_trc bt709 \ -colorspace bt709 -color_primaries bt709 -color_trc bt709 \
-c:v libx264 -profile:v high -level 4.0 -preset slow -crf 23 -maxrate 3500k -bufsize 7000k -tag:v avc1 \ -c:v libx264 -profile:v high -level 4.0 -preset slow -crf 23 -maxrate 3500k -bufsize 7000k -tag:v avc1 \

View File

@@ -1,7 +1,10 @@
"""Old Tam's lines for the trailer (tools/trailer/make_trailer.sh mixes them """Old Tam narrates the trailer (tools/trailer/make_trailer.sh mixes it in):
in): short ones, in his voice (the same clone as tools/voice/make_voice.py), one line for each shot, in his voice (the same clone as
each checked by Whisper and the best of three takes kept. Output: tools/voice/make_voice.py), each checked by Whisper, the best of three takes
tools/trailer/voice/<name>.ogg. Run with the voice environment: kept, and sped up a touch if it would run past the start of the next line.
Output: tools/trailer/voice/<NN>.ogg and lines.json ([{file, shot, at, text}]:
`at` is seconds into its shot; make_trailer.sh reads the shots' start times
from the render log). Run with the voice environment:
tools/voice/.venv-qwen/bin/python tools/trailer/make_trailer_voice.py tools/voice/.venv-qwen/bin/python tools/trailer/make_trailer_voice.py
""" """
import json, os, subprocess, sys, tempfile import json, os, subprocess, sys, tempfile
@@ -11,12 +14,20 @@ sys.path.insert(0, os.path.join(HERE, "..", "voice"))
import make_voice as mv # (its voice, its checks) import make_voice as mv # (its voice, its checks)
OUT = os.path.join(HERE, "voice") OUT = os.path.join(HERE, "voice")
## name -> what he says (make_trailer.sh times each against the shots). ## The shots (trailer.gd), their lengths in seconds, and what he says over
LINES = { ## each, starting `at` seconds in.
"dawn": "Ha! Awake at last! Washed up on my island, eh?", SHOTS = [7.5, 5.0, 6.0, 5.5, 4.5, 5.5, 6.0, 6.0]
"wolves": "Hush now. See it, there, at the edge of the light?", LINES = [
"cove": "Well, you made it down!", (0, 2.0, "Welcome to Cinderwild. You wake on a wild island, washed up with nothing but the clothes on your back."),
} (1, 0.3, "Gather what you can. Hunt, forage, and cook over an open fire."),
(2, 0.3, "Build yourself a home, plank by plank, before the nights draw in."),
(3, 0.3, "Because when the sun goes down, the wolves come out. Keep your fire burning."),
(4, 0.3, "Weather the storms, and the hard winters."),
(5, 0.3, "Then dig down, through the sea cliffs."),
(6, 0.3, "And find out what lies beyond."),
(7, 0.6, "Play alone, or with your friends, on your own server. Cinderwild."),
]
GAP = 0.25 # seconds of quiet before the next line
def main(): def main():
@@ -25,26 +36,43 @@ def main():
import mlx_whisper import mlx_whisper
from mlx_audio.tts.utils import load_model from mlx_audio.tts.utils import load_model
os.makedirs(OUT, exist_ok=True) os.makedirs(OUT, exist_ok=True)
for f in os.listdir(OUT):
if f.endswith(".ogg"):
os.remove(os.path.join(OUT, f))
model = load_model(mv.MODEL) model = load_model(mv.MODEL)
sr = getattr(model, "sample_rate", 24000) sr = getattr(model, "sample_rate", 24000)
tmp = tempfile.mkdtemp() tmp = tempfile.mkdtemp()
for name, text in LINES.items(): out = []
for n, (shot, at, text) in enumerate(LINES):
# Room: to the next line's start (or the end of the film).
if n + 1 < len(LINES):
nshot, nat, _ = LINES[n + 1]
room = sum(SHOTS[shot:nshot]) - at + nat - GAP
else:
room = sum(SHOTS[shot:]) - at - 0.5
best = None best = None
for k in range(3): for k in range(3):
res = list(model.generate(text=text, ref_audio=mv.REF_AUDIO, ref_text=mv.REF_TEXT)) res = list(model.generate(text=text, ref_audio=mv.REF_AUDIO, ref_text=mv.REF_TEXT))
audio = np.concatenate([np.array(r.audio) for r in res]) audio = np.concatenate([np.array(r.audio) for r in res])
wav = os.path.join(tmp, "%s_%d.wav" % (name, k)) wav = os.path.join(tmp, "%02d_%d.wav" % (n, k))
sf.write(wav, audio, sr) sf.write(wav, audio, sr)
heard = mlx_whisper.transcribe(wav, path_or_hf_repo=mv.WHISPER, language="en")["text"].strip() heard = mlx_whisper.transcribe(wav, path_or_hf_repo=mv.WHISPER, language="en")["text"].strip()
score = mv.match(text, heard) secs = len(audio) / sr
# Words first; then the one that fits.
score = mv.match(text, heard) - (0.05 if secs > room * 1.15 else 0.0)
if best is None or score > best[0]: if best is None or score > best[0]:
best = (score, wav, len(audio) / sr) best = (score, wav, secs)
if score >= 0.98: if score >= 0.98 and secs <= room:
break break
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", best[1], "-ac", "2", "-c:a", "vorbis", "-strict", "-2", score, wav, secs = best
"-q:a", "5", os.path.join(OUT, name + ".ogg")], check=True) tempo = min(max(secs / room, 1.0), 1.2) if room > 0 else 1.0
print(" %s (%.1f s, words %.0f%%)" % (name, best[2], best[0] * 100), flush=True) name = "%02d.ogg" % n
json.dump(LINES, open(os.path.join(OUT, "lines.json"), "w"), indent=2) subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", wav, "-af", "atempo=%.3f" % tempo, "-ac", "2",
"-c:a", "vorbis", "-strict", "-2", "-q:a", "5", os.path.join(OUT, name)], check=True)
out.append({"file": name, "shot": shot, "at": at, "text": text})
print(" %s shot %d: %.1f s%s (room %.1f s, words %.0f%%)" % (name, shot, secs / tempo,
" (x%.2f)" % tempo if tempo > 1.0 else "", room, score * 100), flush=True)
json.dump(out, open(os.path.join(OUT, "lines.json"), "w"), indent=2)
if __name__ == "__main__": if __name__ == "__main__":

BIN
tools/trailer/voice/00.ogg Normal file

Binary file not shown.

BIN
tools/trailer/voice/01.ogg Normal file

Binary file not shown.

BIN
tools/trailer/voice/02.ogg Normal file

Binary file not shown.

BIN
tools/trailer/voice/03.ogg Normal file

Binary file not shown.

BIN
tools/trailer/voice/04.ogg Normal file

Binary file not shown.

BIN
tools/trailer/voice/05.ogg Normal file

Binary file not shown.

BIN
tools/trailer/voice/06.ogg Normal file

Binary file not shown.

BIN
tools/trailer/voice/07.ogg Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,50 @@
[
{ {
"dawn": "Ha! Awake at last! Washed up on my island, eh?", "file": "00.ogg",
"wolves": "Hush now. See it, there, at the edge of the light?", "shot": 0,
"cove": "Well, you made it down!" "at": 2.0,
"text": "Welcome to Cinderwild. You wake on a wild island, washed up with nothing but the clothes on your back."
},
{
"file": "01.ogg",
"shot": 1,
"at": 0.3,
"text": "Gather what you can. Hunt, forage, and cook over an open fire."
},
{
"file": "02.ogg",
"shot": 2,
"at": 0.3,
"text": "Build yourself a home, plank by plank, before the nights draw in."
},
{
"file": "03.ogg",
"shot": 3,
"at": 0.3,
"text": "Because when the sun goes down, the wolves come out. Keep your fire burning."
},
{
"file": "04.ogg",
"shot": 4,
"at": 0.3,
"text": "Weather the storms, and the hard winters."
},
{
"file": "05.ogg",
"shot": 5,
"at": 0.3,
"text": "Then dig down, through the sea cliffs."
},
{
"file": "06.ogg",
"shot": 6,
"at": 0.3,
"text": "And find out what lies beyond."
},
{
"file": "07.ogg",
"shot": 7,
"at": 0.6,
"text": "Play alone, or with your friends, on your own server. Cinderwild."
} }
]

Binary file not shown.