#!/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. python3 video/monta.py ~/Workspaces/qemu/qemu-iso-lab/artifacts/tour/out/demo-prototipo - 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 ripresa parte qualche secondo prima del copione (ffmpeg si avvia prima): lo scarto si misura dalla durata. """ import json import subprocess import sys from pathlib import Path 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" FASCIA = 110 # px sopra il gioco per le didascalie FONDO = "0x1B1420" # il colore di fondo della pagina del gioco VOLUME = 0.5 FADE = 1.2 def durata(f: Path) -> float: return float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(f)], capture_output=True, text=True, check=True).stdout) def ts(s: float) -> str: s = max(0.0, s) h, m = int(s // 3600), int(s % 3600 // 60) return f"{h:02}:{m:02}:{s % 60:06.3f}".replace(".", ",") 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()) raw = d / "raw.mkv" totale = durata(raw) cues = meta["cues"] # rec.mjs ferma ffmpeg 1,2 s dopo la fine dell'ultima didascalia: quello che avanza è l'anticipo dell'inizio. scarto = max(0.0, totale - (cues[-1]["end"] + 1.2)) print(f"durata {totale:.1f} s, scarto d'inizio {scarto:.2f} s") righe = [] for i, c in enumerate(cues): inizio = c["start"] + scarto lettura = max(4.0, len(c["it"]) / 14 + 1.5) 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.write_text("\n".join(righe), encoding="utf-8") # Un segmento per brano: dall'istante del cambio al successivo, più la dissolvenza in uscita. # 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) 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]" 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" 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", "-c:a", "aac", "-b:a", "160k", "-movflags", "+faststart", str(finale)], check=True) print(f"-> {finale} ({durata(finale) / 60:.1f} min)") if __name__ == "__main__": main()