Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47966e464d |
@@ -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 \
|
||||||
|
|||||||
@@ -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
BIN
tools/trailer/voice/00.ogg
Normal file
Binary file not shown.
BIN
tools/trailer/voice/01.ogg
Normal file
BIN
tools/trailer/voice/01.ogg
Normal file
Binary file not shown.
BIN
tools/trailer/voice/02.ogg
Normal file
BIN
tools/trailer/voice/02.ogg
Normal file
Binary file not shown.
BIN
tools/trailer/voice/03.ogg
Normal file
BIN
tools/trailer/voice/03.ogg
Normal file
Binary file not shown.
BIN
tools/trailer/voice/04.ogg
Normal file
BIN
tools/trailer/voice/04.ogg
Normal file
Binary file not shown.
BIN
tools/trailer/voice/05.ogg
Normal file
BIN
tools/trailer/voice/05.ogg
Normal file
Binary file not shown.
BIN
tools/trailer/voice/06.ogg
Normal file
BIN
tools/trailer/voice/06.ogg
Normal file
Binary file not shown.
BIN
tools/trailer/voice/07.ogg
Normal file
BIN
tools/trailer/voice/07.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,50 @@
|
|||||||
{
|
[
|
||||||
"dawn": "Ha! Awake at last! Washed up on my island, eh?",
|
{
|
||||||
"wolves": "Hush now. See it, there, at the edge of the light?",
|
"file": "00.ogg",
|
||||||
"cove": "Well, you made it down!"
|
"shot": 0,
|
||||||
}
|
"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.
Reference in New Issue
Block a user