Carte che volano: dalla mano al tavolo e, finita la presa, sul mucchietto di chi ha preso (briscola, tressette, scopa; Andrea: «non si capisce quando vengono buttate le carte»). Il valzer della balera ai tavoli da carte (tools/valzer_carte.py: fisarmonica musette, basso, melodia di mandolino col tremolo), musica per primo piano; video «Come si gioca»: cache del Chromium della VM spenta, narratore e personaggi a turno, musica di sottofondo, pronunce del narratore
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
+24
-1
@@ -22,10 +22,16 @@ OUT = VIDEO / "out"
|
||||
# Area del gioco nello schermo della VM (Chromium a tutto schermo, zoom 125 %): misurata con «convert -trim».
|
||||
TAGLIO = "crop=1044:760:278:70"
|
||||
FASCIA = 110 # px sopra il gioco per le didascalie
|
||||
# I video «Come si gioca» (--tutorial): ai tavoli la pagina usa tutta l'altezza, comandi sotto compresi; didascalie
|
||||
# lunghe anche su tre righe.
|
||||
TAGLIO_TUTORIAL = "crop=1046:900:268:0"
|
||||
FASCIA_TUTORIAL = 132
|
||||
FONDO = "0x1B1420" # il colore di fondo della pagina del gioco
|
||||
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
|
||||
VOLUME_NARRATORE = 1.15
|
||||
VOLUME_TUTORIAL = 0.07 # musica dei video «Come si gioca»: sottofondo # il narratore dei video «Come si gioca», sopra la musica bassa
|
||||
FADE = 1.2
|
||||
|
||||
|
||||
@@ -42,6 +48,8 @@ def ts(s: float) -> str:
|
||||
|
||||
def main() -> None:
|
||||
d = Path(sys.argv[1]).expanduser()
|
||||
tutorial = "--tutorial" in sys.argv[2:]
|
||||
taglio, fascia = (TAGLIO_TUTORIAL, FASCIA_TUTORIAL) if tutorial else (TAGLIO, FASCIA)
|
||||
meta = json.loads((d / "cues.json").read_text())
|
||||
nome = meta["clip"]
|
||||
registro = OUT / f"{nome}-audio.json"
|
||||
@@ -59,6 +67,10 @@ def main() -> None:
|
||||
aperta["fine"] = e["t"]
|
||||
aperta = None
|
||||
volume = VOLUME_SOTTO
|
||||
# Nei video «Come si gioca» parla il narratore quasi sempre: la musica resta un sottofondo (Andrea, 26 ottobre:
|
||||
# «troppo alta la musica del gioco»).
|
||||
if (OUT / f"{nome}-narrazione.json").exists():
|
||||
volume = VOLUME_TUTORIAL
|
||||
else:
|
||||
musica = json.loads((OUT / "musica.json").read_text())
|
||||
volume = VOLUME
|
||||
@@ -105,9 +117,20 @@ def main() -> None:
|
||||
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)}]")
|
||||
# Il narratore dei video «Come si gioca» (video/narra.py): ogni frase all'inizio della sua battuta.
|
||||
narrazione = OUT / f"{nome}-narrazione.json"
|
||||
if narrazione.exists():
|
||||
frasi = {v["id"]: v["src"] for v in json.loads(narrazione.read_text())["voci"]}
|
||||
for c in cues:
|
||||
if c["id"] not in frasi:
|
||||
continue
|
||||
ingressi += ["-i", str(GIOCO / frasi[c["id"]])]
|
||||
n = ingressi.count("-i")
|
||||
catene.append(f"[{n}:a]asetpts=PTS-STARTPTS,volume={VOLUME_NARRATORE},adelay={int((c['start'] + scarto) * 1000)}:all=1[n{len(pezzi)}]")
|
||||
pezzi.append(f"[n{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]"
|
||||
video = f"[0:v]{taglio},pad=iw:ih+{fascia}:0:{fascia}:{FONDO},subtitles={srt}:force_style='{stile}'[v]"
|
||||
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]",
|
||||
|
||||
+4
-1
@@ -36,7 +36,10 @@ def main() -> None:
|
||||
# XTTS dice «punto» se la frase finisce col punto: si toglie, come fa narrate.py del tour.
|
||||
lavori = [{"text": c["it"].rstrip(". "), "out": str(cartella / f"{c['id']}.wav"), "lang": "it", "speaker": NARRATORE} for c in cues]
|
||||
(cartella / "lavori.json").write_text(json.dumps(lavori, ensure_ascii=False, indent=1))
|
||||
pron = ["--pronounce", str(PRONUNCIA)] if PRONUNCIA.exists() else []
|
||||
# Le pronunce del gioco più quelle del narratore, in un file solo per `mbot parla`.
|
||||
regole = {k: v for f in (PRONUNCIA, VIDEO / "pronuncia-narratore.json") if f.exists() for k, v in json.loads(f.read_text()).items() if not k.startswith("_")}
|
||||
(cartella / "pronuncia.json").write_text(json.dumps(regole, ensure_ascii=False, indent=1))
|
||||
pron = ["--pronounce", str(cartella / "pronuncia.json")]
|
||||
subprocess.run(["mbot", "parla", "--jobs", str(cartella / "lavori.json"), *pron], check=True)
|
||||
ascolto = [{"file": l["out"], "expect": l["text"]} for l in lavori]
|
||||
(cartella / "ascolto.json").write_text(json.dumps(ascolto, ensure_ascii=False, indent=1))
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"_nota": "Pronunce in più per il narratore dei video «Come si gioca» (video/narra.py), oltre a tools/pronuncia.json. L'accento scritto sposta quello di XTTS (Andrea, 26 ottobre: «piccola» e «volta» con l'accento sbagliato).",
|
||||
"piccola": "pìccola",
|
||||
"piccole": "pìccole",
|
||||
"volta": "vòlta",
|
||||
"volte": "vòlte"
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const cues = [
|
||||
c("pesca", "Chi prende pesca per primo ed esce alla mano dopo. E non c'è l'obbligo di rispondere al seme"),
|
||||
c("consiglio", "Un consiglio: su un liscio rispondi con un liscio, e i carichi dell'avversario prendili con una briscola piccola"),
|
||||
c("tastiera", "Si gioca toccando la carta, oppure coi pulsanti sotto il tavolo, o con le frecce e Invio"),
|
||||
c("conto", "Finito il mazzo si contano le prese, una carta alla volta. Vince chi arriva a sessantuno punti, e sessanta a sessanta è patta"),
|
||||
c("conto", "Finito il mazzo si contano le prese, una carta alla volta. Vince chi arriva a sessantuno punti; sessanta a sessanta è pari, e non vince nessuno"),
|
||||
c("fine", "Poi si torna all'oratorio, e si può chiedere la rivincita. Buona briscola!"),
|
||||
];
|
||||
|
||||
@@ -21,6 +21,7 @@ export const setup = base;
|
||||
|
||||
export async function run(d) {
|
||||
const p = d.page, r = regista(d, "tutorial-briscola");
|
||||
// Il registro dell'audio parte con la ripresa: monta.py rimette le voci del gioco al loro istante.
|
||||
await r.ascolta();
|
||||
await r.vaiA("oratorio", [470, 300]);
|
||||
await r.cue("dove");
|
||||
@@ -30,12 +31,14 @@ export async function run(d) {
|
||||
await r.scegli("Con le toscane.");
|
||||
await r.cue("livello");
|
||||
await r.scegli("Seria.");
|
||||
// Una mano fissa, la stessa a ogni ripresa.
|
||||
// Una mano fissa, la stessa a ogni ripresa: giocando attento, Andrea vince 68 a 52.
|
||||
await p.evaluate(async () => {
|
||||
const { newBriscola } = await import("/src/engine/briscola.js");
|
||||
game.closeup = { id: "briscola", ...newBriscola(91421, { level: "seria", deck: "toscane" }) };
|
||||
game.closeup = { id: "briscola", ...newBriscola(8, { level: "seria", deck: "toscane" }) };
|
||||
});
|
||||
await sleep(800);
|
||||
// Il tavolo ha la sua musica: se non parte, il gioco servito nella VM è vecchio.
|
||||
await p.waitForFunction(() => window.music?.want === "assets/audio/carte.mp3", null, { timeout: 10000 });
|
||||
await r.cue("tavolo");
|
||||
await r.muovi([150, 160], 1.0); await sleep(600); await r.muovi([320, 280], 1.0);
|
||||
|
||||
@@ -69,6 +72,7 @@ export async function run(d) {
|
||||
await p.waitForFunction(() => game.closeup?.step === "result", null, { timeout: 60000 });
|
||||
await sleep(2500);
|
||||
await r.cue("fine");
|
||||
await r.aspetta(); // la battuta del giocatore, uscendo, dopo quella del narratore
|
||||
await d.click(p.locator("#closeup-close"), 0.6);
|
||||
await r.fermi(800);
|
||||
await r.fine();
|
||||
|
||||
@@ -15,6 +15,11 @@ export function narrazione(clip) {
|
||||
|
||||
export async function setup(d) {
|
||||
const p = d.page;
|
||||
// Niente cache: il Chromium della VM teneva i moduli JavaScript di una ripresa prima (il valzer non partiva).
|
||||
const cdp = await p.context().newCDPSession(p);
|
||||
await cdp.send("Network.enable");
|
||||
await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });
|
||||
await cdp.send("Network.clearBrowserCache");
|
||||
await p.goto(URL_GIOCO);
|
||||
await p.evaluate(() => localStorage.clear());
|
||||
await p.reload();
|
||||
@@ -26,13 +31,16 @@ export function regista(d, clip) {
|
||||
const p = d.page, voce = narrazione(clip);
|
||||
let inizio = 0, dura = 0;
|
||||
const r = {
|
||||
// Una battuta del narratore: prima si lascia finire quella di prima.
|
||||
// Una battuta del narratore: prima si lascia finire quella di prima, e chi sta parlando nel gioco.
|
||||
async cue(id) {
|
||||
const resto = inizio + dura + 350 - Date.now();
|
||||
if (resto > 0) await sleep(resto);
|
||||
await r.aspetta();
|
||||
for (let i = 0; i < 300 && (await p.evaluate(() => !!game.speech || !!(window.voices?.audio && !window.voices.audio.paused && !window.voices.audio.ended))); i++) await sleep(100);
|
||||
await sleep(250);
|
||||
await d.cue(id);
|
||||
inizio = Date.now(); dura = voce[id] ?? 0;
|
||||
},
|
||||
// Prima di far parlare un personaggio, il narratore deve aver finito: le due voci non si sovrappongono.
|
||||
async aspetta() { const resto = inizio + dura + 350 - Date.now(); if (resto > 0) await sleep(resto); },
|
||||
async fine() { const resto = inizio + dura + 600 - Date.now(); if (resto > 0) await sleep(resto); },
|
||||
// Puntatore vero sulla scena (coordinate 640×360).
|
||||
async punto(x, y) {
|
||||
@@ -54,7 +62,7 @@ export function regista(d, clip) {
|
||||
}
|
||||
await sleep(pausa);
|
||||
},
|
||||
async scegli(testo) { await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await r.fermi(); },
|
||||
async scegli(testo) { await r.aspetta(); await d.click(p.locator("#choices li", { hasText: testo }), 0.6); await r.fermi(); },
|
||||
// Partita nuova per Campomigliaio, introduzione saltata, dritti nella scena del gioco.
|
||||
async vaiA(scena, at) {
|
||||
await d.click(p.locator("#new-game"));
|
||||
@@ -62,10 +70,12 @@ export function regista(d, clip) {
|
||||
await d.click(p.locator('#rione [data-rione="campomigliaio"]'));
|
||||
await sleep(800);
|
||||
if (await p.isVisible("#skip-intro")) await d.click(p.locator("#skip-intro"), 0.5);
|
||||
await p.evaluate(([s, a]) => { game.go(s, a); }, [scena, at]);
|
||||
// Le battute d'apertura di Andrea (il palio sparito) qui non servono: si saltano tutte.
|
||||
await p.evaluate(([s, a]) => { for (let i = 0; i < 50 && (game.speech || game.queue.length); i++) { game.skip(); game.update(0.05); } game.go(s, a); }, [scena, at]);
|
||||
await r.fermi(1200);
|
||||
},
|
||||
async parlaCon(id) {
|
||||
await r.aspetta();
|
||||
const [x, y] = await p.evaluate((id) => { const h = game.hotspot(id); return [h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]; }, id);
|
||||
await r.clicca([x, y]);
|
||||
await r.fermi();
|
||||
|
||||
Reference in New Issue
Block a user