Video della partita completa (pomeriggio → sera) con voci e musiche rimontate dal registro audio del copione; inventario: nomi lunghi su due righe dentro il pulsante
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
+55
-20
@@ -1,12 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Montaggio della demo (video/SCALETTA.md): raw.mkv + cues.json della ripresa e video/out/musica.json
|
||||
-> video/out/demo-prototipo.mp4.
|
||||
"""Montaggio di una ripresa del gioco (video/SCALETTA.md): raw.mkv + cues.json della ripresa e l'audio annotato dal
|
||||
copione -> video/out/<clip>.mp4.
|
||||
|
||||
python3 video/monta.py ~/Workspaces/qemu/qemu-iso-lab/artifacts/tour/out/demo-prototipo
|
||||
python3 video/monta.py ~/Workspaces/qemu/qemu-iso-lab/artifacts/tour/out/partita-completa
|
||||
|
||||
- ritaglia lo schermo 1600x900 della VM sull'area del gioco e lascia sopra una fascia per le didascalie;
|
||||
- le didascalie (cues.json, solo «it») restano a schermo il tempo di leggerle, non fino alla successiva;
|
||||
- la colonna sonora si rimonta dai cambi di brano registrati dal copione, con le dissolvenze del gioco (1,2 s).
|
||||
- la colonna sonora si rimonta dai cambi di brano registrati dal copione, con le dissolvenze del gioco (1,2 s);
|
||||
- le voci (solo in video/out/<clip>-audio.json) tornano al loro istante, sopra la musica tenuta bassa.
|
||||
La ripresa parte qualche secondo prima del copione (ffmpeg si avvia prima): lo scarto si misura dalla durata.
|
||||
"""
|
||||
import json
|
||||
@@ -18,10 +20,12 @@ VIDEO = Path(__file__).resolve().parent
|
||||
GIOCO = VIDEO.parent
|
||||
OUT = VIDEO / "out"
|
||||
# Area del gioco nello schermo della VM (Chromium a tutto schermo, zoom 125 %): misurata con «convert -trim».
|
||||
TAGLIO = "crop=1076:780:262:60"
|
||||
TAGLIO = "crop=1044:760:278:70"
|
||||
FASCIA = 110 # px sopra il gioco per le didascalie
|
||||
FONDO = "0x1B1420" # il colore di fondo della pagina del gioco
|
||||
VOLUME = 0.5
|
||||
VOLUME = 0.5 # musica della demo-prototipo (senza voci)
|
||||
VOLUME_SOTTO = 0.22 # musica sotto le voci: bassa (Andrea, 25 ottobre)
|
||||
VOLUME_VOCI = 1.0
|
||||
FADE = 1.2
|
||||
|
||||
|
||||
@@ -39,7 +43,25 @@ def ts(s: float) -> str:
|
||||
def main() -> None:
|
||||
d = Path(sys.argv[1]).expanduser()
|
||||
meta = json.loads((d / "cues.json").read_text())
|
||||
musica = json.loads((OUT / "musica.json").read_text())
|
||||
nome = meta["clip"]
|
||||
registro = OUT / f"{nome}-audio.json"
|
||||
voci = []
|
||||
if registro.exists():
|
||||
# Il registro del copione: {t, music} a ogni cambio di brano, {t, voice} e {t, voiceEnd} per le voci.
|
||||
eventi = json.loads(registro.read_text())["eventi"]
|
||||
musica = {"cambi": [{"t": e["t"], "src": e["music"]} for e in eventi if "music" in e]}
|
||||
aperta = None
|
||||
for e in eventi:
|
||||
if "voice" in e:
|
||||
aperta = {"t": e["t"], "src": e["voice"].split("?")[0]}
|
||||
voci.append(aperta)
|
||||
elif "voiceEnd" in e and aperta:
|
||||
aperta["fine"] = e["t"]
|
||||
aperta = None
|
||||
volume = VOLUME_SOTTO
|
||||
else:
|
||||
musica = json.loads((OUT / "musica.json").read_text())
|
||||
volume = VOLUME
|
||||
raw = d / "raw.mkv"
|
||||
totale = durata(raw)
|
||||
cues = meta["cues"]
|
||||
@@ -54,26 +76,39 @@ def main() -> None:
|
||||
fine = min(c["end"] + scarto if c["end"] is not None else inizio + lettura, inizio + lettura)
|
||||
righe += [str(i + 1), f"{ts(inizio)} --> {ts(fine)}", "{\\an8}" + c["it"], ""]
|
||||
OUT.mkdir(exist_ok=True)
|
||||
srt = OUT / "demo-prototipo.it.srt"
|
||||
srt = OUT / f"{nome}.it.srt"
|
||||
srt.write_text("\n".join(righe), encoding="utf-8")
|
||||
|
||||
# Un segmento per brano: dall'istante del cambio al successivo, più la dissolvenza in uscita.
|
||||
# Un segmento per brano: dall'istante del cambio al cambio successivo (anche al silenzio), più la dissolvenza.
|
||||
# Il primo brano parte da zero (nel browser aspetta il primo clic, nel video no).
|
||||
cambi = [c for c in musica["cambi"] if c["src"]]
|
||||
cambi[0]["t"] = -scarto
|
||||
ingressi, catene = [], []
|
||||
for i, c in enumerate(cambi):
|
||||
a = c["t"] + scarto
|
||||
b = (cambi[i + 1]["t"] + scarto) if i + 1 < len(cambi) else totale
|
||||
lungo = b - a + (FADE if i + 1 < len(cambi) else 0)
|
||||
tutti = musica["cambi"]
|
||||
primo = next(i for i, c in enumerate(tutti) if c["src"])
|
||||
tutti[primo]["t"] = -scarto
|
||||
ingressi, catene, pezzi = [], [], []
|
||||
for i, c in enumerate(tutti):
|
||||
if not c["src"]:
|
||||
continue
|
||||
a = max(0.0, c["t"] + scarto)
|
||||
ultimo = i + 1 >= len(tutti)
|
||||
b = totale if ultimo else tutti[i + 1]["t"] + scarto
|
||||
lungo = max(0.5, b - a + (0 if ultimo else FADE))
|
||||
ingressi += ["-stream_loop", "-1", "-i", str(GIOCO / c["src"])]
|
||||
uscita = f",afade=t=out:st={lungo - FADE:.3f}:d={FADE}" if i + 1 < len(cambi) else f",afade=t=out:st={lungo - 3:.3f}:d=3"
|
||||
catene.append(f"[{i + 1}:a]atrim=0:{lungo:.3f},asetpts=PTS-STARTPTS,volume={VOLUME},"
|
||||
f"afade=t=in:d={FADE if i else 0.01}{uscita},adelay={int(a * 1000)}:all=1[m{i}]")
|
||||
mix = "".join(f"[m{i}]" for i in range(len(cambi))) + f"amix=inputs={len(cambi)}:normalize=0:duration=longest[a]"
|
||||
n = ingressi.count("-i") # l'ingresso 0 è il video
|
||||
uscita = f",afade=t=out:st={max(0.0, lungo - 3):.3f}:d=3" if ultimo else f",afade=t=out:st={max(0.0, lungo - FADE):.3f}:d={FADE}"
|
||||
catene.append(f"[{n}:a]atrim=0:{lungo:.3f},asetpts=PTS-STARTPTS,volume={volume},"
|
||||
f"afade=t=in:d={FADE if i > primo else 0.01}{uscita},adelay={int(a * 1000)}:all=1[m{len(pezzi)}]")
|
||||
pezzi.append(f"[m{len(pezzi)}]")
|
||||
# Le voci: ognuna dal suo istante, tagliata dove il gioco l'ha fermata (battuta saltata o finita).
|
||||
for v in voci:
|
||||
ingressi += ["-i", str(GIOCO / v["src"])]
|
||||
n = ingressi.count("-i")
|
||||
lungo = f"atrim=0:{v['fine'] - v['t'] + 0.1:.3f}," if "fine" in v else ""
|
||||
catene.append(f"[{n}:a]{lungo}asetpts=PTS-STARTPTS,volume={VOLUME_VOCI},adelay={int((v['t'] + scarto) * 1000)}:all=1[v{len(pezzi)}]")
|
||||
pezzi.append(f"[v{len(pezzi)}]")
|
||||
mix = "".join(pezzi) + f"amix=inputs={len(pezzi)}:normalize=0:duration=longest[a]"
|
||||
stile = f"FontName=DejaVu Sans,FontSize=11,Bold=1,MarginV=4,Outline=1,Shadow=0,PrimaryColour=&H007AD2FF"
|
||||
video = f"[0:v]{TAGLIO},pad=iw:ih+{FASCIA}:0:{FASCIA}:{FONDO},subtitles={srt}:force_style='{stile}'[v]"
|
||||
finale = OUT / "demo-prototipo.mp4"
|
||||
finale = OUT / f"{nome}.mp4"
|
||||
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(raw), *ingressi,
|
||||
"-filter_complex", ";".join([video, *catene, mix]), "-map", "[v]", "-map", "[a]",
|
||||
"-t", f"{totale:.3f}", "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p",
|
||||
|
||||
Reference in New Issue
Block a user