Voci: anche le risposte del motore e le scelte dei dialoghi hanno la voce; make ascolta le riascolta con Whisper; rifatte quelle storpiate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
2026-10-24 20:49:52 +02:00
co-authored by Claude Opus 5.5
parent bde9a790e5
commit 96b3779ad1
65 changed files with 232 additions and 34 deletions
+3
View File
@@ -57,6 +57,9 @@ verify: test browser ## Tutti i controlli
voci: ## Voci dei personaggi con XTTS sulla GPU: solo le battute nuove o cambiate (RIFAI='testo' per ritentarne una) voci: ## Voci dei personaggi con XTTS sulla GPU: solo le battute nuove o cambiate (RIFAI='testo' per ritentarne una)
$(NODE) tools/voci.mjs $(if $(RIFAI),--rifai "$(RIFAI)") $(NODE) tools/voci.mjs $(if $(RIFAI),--rifai "$(RIFAI)")
ascolta: ## Riascolta le voci con Whisper (mbot ascolta) e elenca quelle che dicono altro (SOLO='testo' per alcune)
$(NODE) tools/ascolta.mjs $(if $(SOLO),"$(SOLO)")
voci-elenco: ## Quali battute mancano di voce, senza generare niente voci-elenco: ## Quali battute mancano di voce, senza generare niente
$(NODE) tools/voci.mjs --dry $(NODE) tools/voci.mjs --dry
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+145 -29
View File
@@ -1,7 +1,7 @@
{ {
"andrea|San Piero a Sieve, nel Mugello. Fine maggio 2007.": { "andrea|San Piero a Sieve, nel Mugello. Fine maggio 2007.": {
"file": "andrea-4167cc8c5e.mp3", "file": "andrea-4167cc8c5e.mp3",
"sec": 4.59 "sec": 4.84
}, },
"andrea|In paese si prepara il Palio della Fortezza: i quattro rioni si sfidano, e chi vince si porta a casa il palio, il quadro dipinto dai ragazzi delle medie.": { "andrea|In paese si prepara il Palio della Fortezza: i quattro rioni si sfidano, e chi vince si porta a casa il palio, il quadro dipinto dai ragazzi delle medie.": {
"file": "andrea-fdeb17aa77.mp3", "file": "andrea-fdeb17aa77.mp3",
@@ -136,8 +136,8 @@
"sec": 5.94 "sec": 5.94
}, },
"andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": { "andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": {
"file": "andrea-0b61233e59.mp3", "file": "andrea-d31f6e291b.mp3",
"sec": 9.95 "sec": 8.71
}, },
"andrea|Arrampicarmi sul bastione? Ho visto film che finivano meglio.": { "andrea|Arrampicarmi sul bastione? Ho visto film che finivano meglio.": {
"file": "andrea-625ef8dc45.mp3", "file": "andrea-625ef8dc45.mp3",
@@ -257,7 +257,7 @@
}, },
"andrea|La Pieve di San Pietro. Il rione Pieve, quelli in bianco.": { "andrea|La Pieve di San Pietro. Il rione Pieve, quelli in bianco.": {
"file": "andrea-c41f9eaaf3.mp3", "file": "andrea-c41f9eaaf3.mp3",
"sec": 4.86 "sec": 4.44
}, },
"andrea|Il prete è a benedire le carrette del Palio. Torno dopo.": { "andrea|Il prete è a benedire le carrette del Palio. Torno dopo.": {
"file": "andrea-bd4991b099.mp3", "file": "andrea-bd4991b099.mp3",
@@ -369,7 +369,7 @@
}, },
"andrea|Una gomma da masticare ancora incartata, persa davanti al palco. Qualcuno ha perso l'appetito.": { "andrea|Una gomma da masticare ancora incartata, persa davanti al palco. Qualcuno ha perso l'appetito.": {
"file": "andrea-f2ea95922d.mp3", "file": "andrea-f2ea95922d.mp3",
"sec": 7.49 "sec": 6.54
}, },
"andrea|Una gomma alla fragola. Me la tengo.": { "andrea|Una gomma alla fragola. Me la tengo.": {
"file": "andrea-7009fd9d71.mp3", "file": "andrea-7009fd9d71.mp3",
@@ -469,7 +469,7 @@
}, },
"andrea|La panchina rossa. Qui si scambiavano le figurine: celo, celo, manca.": { "andrea|La panchina rossa. Qui si scambiavano le figurine: celo, celo, manca.": {
"file": "andrea-84a2614ea0.mp3", "file": "andrea-84a2614ea0.mp3",
"sec": 5.7 "sec": 6.03
}, },
"andrea|Laggiù le poste. Mia nonna ci andava a ritirare la pensione e tornava con tutte le notizie del paese.": { "andrea|Laggiù le poste. Mia nonna ci andava a ritirare la pensione e tornava con tutte le notizie del paese.": {
"file": "andrea-c716ab2381.mp3", "file": "andrea-c716ab2381.mp3",
@@ -521,11 +521,11 @@
}, },
"andrea|La chiavetta entra nel lucchetto... clic!": { "andrea|La chiavetta entra nel lucchetto... clic!": {
"file": "andrea-35b92c1da8.mp3", "file": "andrea-35b92c1da8.mp3",
"sec": 3.91 "sec": 3.04
}, },
"andrea|Prolunghe, prolunghe, prolunghe... e una torcia.": { "andrea|Prolunghe, prolunghe, prolunghe... e una torcia.": {
"file": "andrea-8abb6148b1.mp3", "file": "andrea-8abb6148b1.mp3",
"sec": 5.98 "sec": 4.54
}, },
"andrea|Chiusa col lucchetto. Il mistero resta.": { "andrea|Chiusa col lucchetto. Il mistero resta.": {
"file": "andrea-a8dfddbe65.mp3", "file": "andrea-a8dfddbe65.mp3",
@@ -560,12 +560,12 @@
"sec": 2.18 "sec": 2.18
}, },
"andrea|Il capannone verde del padel.": { "andrea|Il capannone verde del padel.": {
"file": "andrea-a16136da3a.mp3", "file": "andrea-d15c90f036.mp3",
"sec": 1.63 "sec": 1.79
}, },
"andrea|Il padel, nel 2007, non so nemmeno cosa sia. Tra qualche anno ci giocheranno tutti, me compreso.": { "andrea|Il padel, nel 2007, non so nemmeno cosa sia. Tra qualche anno ci giocheranno tutti, me compreso.": {
"file": "andrea-1baedec826.mp3", "file": "andrea-4e98630ef3.mp3",
"sec": 7.77 "sec": 7.9
}, },
"andrea|Chiuso. E poi non saprei nemmeno da che parte si tiene la racchetta.": { "andrea|Chiuso. E poi non saprei nemmeno da che parte si tiene la racchetta.": {
"file": "andrea-e47b3cebb5.mp3", "file": "andrea-e47b3cebb5.mp3",
@@ -661,7 +661,7 @@
}, },
"andrea|Un muro di sassi a secco, con la ringhiera sopra. Ne ha visti passare di pali.": { "andrea|Un muro di sassi a secco, con la ringhiera sopra. Ne ha visti passare di pali.": {
"file": "andrea-9f914e2a8b.mp3", "file": "andrea-9f914e2a8b.mp3",
"sec": 5.35 "sec": 5.4
}, },
"andrea|Una porta marrone, chiusa.": { "andrea|Una porta marrone, chiusa.": {
"file": "andrea-29c2c930b4.mp3", "file": "andrea-29c2c930b4.mp3",
@@ -757,7 +757,7 @@
}, },
"tosca|Eccomi, Andrea! Cosa ti do? Fanta, Coca, cedrata, spuma bionda... o un ghiacciolo?": { "tosca|Eccomi, Andrea! Cosa ti do? Fanta, Coca, cedrata, spuma bionda... o un ghiacciolo?": {
"file": "tosca-75db2f3b4e.mp3", "file": "tosca-75db2f3b4e.mp3",
"sec": 8.26 "sec": 9.26
}, },
"andrea|Il chiosco si apre da dentro. Basta chiamare la Tosca.": { "andrea|Il chiosco si apre da dentro. Basta chiamare la Tosca.": {
"file": "andrea-acc1a379b7.mp3", "file": "andrea-acc1a379b7.mp3",
@@ -813,7 +813,7 @@
}, },
"andrea|Il mercoledì sera è ancora lontano. Prima il palio.": { "andrea|Il mercoledì sera è ancora lontano. Prima il palio.": {
"file": "andrea-e4f78223c4.mp3", "file": "andrea-e4f78223c4.mp3",
"sec": 4.39 "sec": 3.88
}, },
"andrea|Villa Adami. Gialla come un tuorlo, con le persiane verdi.": { "andrea|Villa Adami. Gialla come un tuorlo, con le persiane verdi.": {
"file": "andrea-225e29ed43.mp3", "file": "andrea-225e29ed43.mp3",
@@ -889,11 +889,11 @@
}, },
"andrea|Fare i ponti di sassi? Prima il palio. Poi le dighe.": { "andrea|Fare i ponti di sassi? Prima il palio. Poi le dighe.": {
"file": "andrea-5594f1f70c.mp3", "file": "andrea-5594f1f70c.mp3",
"sec": 5.02 "sec": 4.54
}, },
"andrea|La griglia. Ormai dentro ci sono solo foglie.": { "andrea|La griglia. Ormai dentro ci sono solo foglie.": {
"file": "andrea-0a30866235.mp3", "file": "andrea-0a30866235.mp3",
"sec": 3.6 "sec": 3.63
}, },
"andrea|Dentro la griglia luccica qualcosa: una chiavetta! È caduta fra le sbarre.": { "andrea|Dentro la griglia luccica qualcosa: una chiavetta! È caduta fra le sbarre.": {
"file": "andrea-c6acc0d948.mp3", "file": "andrea-c6acc0d948.mp3",
@@ -915,9 +915,9 @@
"file": "andrea-29863877af.mp3", "file": "andrea-29863877af.mp3",
"sec": 4.54 "sec": 4.54
}, },
"andrea|Presa!": { "andrea|Eccola, presa!": {
"file": "andrea-872d650e46.mp3", "file": "andrea-8209ff51e1.mp3",
"sec": 0.69 "sec": 1.53
}, },
"andrea|Lo stecco ci arriva, ma la chiavetta non si attacca. Serve qualcosa di appiccicoso in cima.": { "andrea|Lo stecco ci arriva, ma la chiavetta non si attacca. Serve qualcosa di appiccicoso in cima.": {
"file": "andrea-af37e73b26.mp3", "file": "andrea-af37e73b26.mp3",
@@ -957,7 +957,7 @@
}, },
"andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": { "andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": {
"file": "andrea-8945f1786d.mp3", "file": "andrea-8945f1786d.mp3",
"sec": 4.18 "sec": 3.89
}, },
"andrea|Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.": { "andrea|Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.": {
"file": "andrea-4708fda5ef.mp3", "file": "andrea-4708fda5ef.mp3",
@@ -989,7 +989,7 @@
}, },
"andrea|Tornare a scuola? A ventisette anni? Nemmeno per sogno.": { "andrea|Tornare a scuola? A ventisette anni? Nemmeno per sogno.": {
"file": "andrea-8ae8691b63.mp3", "file": "andrea-8ae8691b63.mp3",
"sec": 5.44 "sec": 5.39
}, },
"andrea|Le palazzine gialle accanto alla scuola, con la scala arancione.": { "andrea|Le palazzine gialle accanto alla scuola, con la scala arancione.": {
"file": "andrea-528eda3257.mp3", "file": "andrea-528eda3257.mp3",
@@ -1069,7 +1069,7 @@
}, },
"andrea|Leggere adesso? Prima il palio. E poi, a quanto pare, si fa presto.": { "andrea|Leggere adesso? Prima il palio. E poi, a quanto pare, si fa presto.": {
"file": "andrea-3a6af3897f.mp3", "file": "andrea-3a6af3897f.mp3",
"sec": 6.52 "sec": 6.06
}, },
"andrea|Un ghiacciolo al limone. Si scioglie a guardarlo.": { "andrea|Un ghiacciolo al limone. Si scioglie a guardarlo.": {
"file": "andrea-8c9713081a.mp3", "file": "andrea-8c9713081a.mp3",
@@ -1081,7 +1081,7 @@
}, },
"andrea|Ghiacciolo dentro la cedrata... si scioglie, frizza, è verde e giallo insieme.": { "andrea|Ghiacciolo dentro la cedrata... si scioglie, frizza, è verde e giallo insieme.": {
"file": "andrea-629bfcac17.mp3", "file": "andrea-629bfcac17.mp3",
"sec": 6.24 "sec": 5.53
}, },
"andrea|Esagerato. Buonissimo. La merenda dei campioni.": { "andrea|Esagerato. Buonissimo. La merenda dei campioni.": {
"file": "andrea-4a2ae1d92d.mp3", "file": "andrea-4a2ae1d92d.mp3",
@@ -1167,6 +1167,30 @@
"file": "andrea-60cdae2e35.mp3", "file": "andrea-60cdae2e35.mp3",
"sec": 5.1 "sec": 5.1
}, },
"andrea|Una Fanta.": {
"file": "andrea-8927b87738.mp3",
"sec": 0.65
},
"andrea|Una Coca.": {
"file": "andrea-15618ccedd.mp3",
"sec": 0.78
},
"andrea|Una spuma bionda.": {
"file": "andrea-db75339f79.mp3",
"sec": 1.21
},
"andrea|Una cedrata, da portare via.": {
"file": "andrea-3a253ec0a8.mp3",
"sec": 1.85
},
"andrea|Un ghiacciolo al limone.": {
"file": "andrea-2d0e3c759e.mp3",
"sec": 1.39
},
"andrea|Niente, grazie Tosca.": {
"file": "andrea-2637918e77.mp3",
"sec": 1.32
},
"tosca|Eccola, ghiacciata.": { "tosca|Eccola, ghiacciata.": {
"file": "tosca-133c83efc3.mp3", "file": "tosca-133c83efc3.mp3",
"sec": 1.79 "sec": 1.79
@@ -1181,7 +1205,7 @@
}, },
"tosca|Cedrata. Bella fresca.": { "tosca|Cedrata. Bella fresca.": {
"file": "tosca-d6b114d197.mp3", "file": "tosca-d6b114d197.mp3",
"sec": 2.24 "sec": 2.31
}, },
"tosca|Al limone, l'ultimo della cassetta.": { "tosca|Al limone, l'ultimo della cassetta.": {
"file": "tosca-376fbb9f17.mp3", "file": "tosca-376fbb9f17.mp3",
@@ -1191,6 +1215,26 @@
"file": "tosca-db2da31cef.mp3", "file": "tosca-db2da31cef.mp3",
"sec": 2.31 "sec": 2.31
}, },
"andrea|Cinque.": {
"file": "andrea-0b1872ef8c.mp3",
"sec": 0.61
},
"andrea|Sette.": {
"file": "andrea-88aff362ec.mp3",
"sec": 0.7
},
"andrea|Nove.": {
"file": "andrea-22477cfcba.mp3",
"sec": 0.57
},
"andrea|Dodici.": {
"file": "andrea-2442d01b65.mp3",
"sec": 0.51
},
"andrea|Ci penso e torno.": {
"file": "andrea-d2ce5bb455.mp3",
"sec": 1.56
},
"san_pietro|Cinque? Ne hai contati la metà. Vai a guardarli bene, lassù.": { "san_pietro|Cinque? Ne hai contati la metà. Vai a guardarli bene, lassù.": {
"file": "san_pietro-4f6f201875.mp3", "file": "san_pietro-4f6f201875.mp3",
"sec": 6.53 "sec": 6.53
@@ -1200,12 +1244,12 @@
"sec": 4.44 "sec": 4.44
}, },
"san_pietro|Bravo! Nove, come li volle Cosimo I.": { "san_pietro|Bravo! Nove, come li volle Cosimo I.": {
"file": "san_pietro-3a2e3a543b.mp3", "file": "san_pietro-9bb2be5ec6.mp3",
"sec": 4.25 "sec": 4.55
}, },
"san_pietro|Eccoti la chiave dell'oratorio. Trattala bene: è più vecchia di tuo nonno.": { "san_pietro|Eccoti la chiave dell'oratorio. Trattala bene: è più vecchia di tuo nonno.": {
"file": "san_pietro-9e24f3914e.mp3", "file": "san_pietro-9e24f3914e.mp3",
"sec": 6.14 "sec": 6.11
}, },
"andrea|Grazie, San Pietro!": { "andrea|Grazie, San Pietro!": {
"file": "andrea-149f790fa1.mp3", "file": "andrea-149f790fa1.mp3",
@@ -1219,6 +1263,22 @@
"file": "san_pietro-6c85ba44c5.mp3", "file": "san_pietro-6c85ba44c5.mp3",
"sec": 3.63 "sec": 3.63
}, },
"andrea|Avete visto chi ha preso il palio?": {
"file": "andrea-e673c57e60.mp3",
"sec": 1.65
},
"andrea|Avete notato niente di strano stanotte?": {
"file": "andrea-f51c5ac76e.mp3",
"sec": 2.41
},
"andrea|Come vi sembra il Palio di quest'anno?": {
"file": "andrea-379131645c.mp3",
"sec": 1.98
},
"andrea|Ci vediamo dopo.": {
"file": "andrea-7e9841c23a.mp3",
"sec": 0.95
},
"vecchietta|È stato il Regolo! Quello che cadde dal cielo quando scavavano le fondamenta.": { "vecchietta|È stato il Regolo! Quello che cadde dal cielo quando scavavano le fondamenta.": {
"file": "vecchietta-e35443db85.mp3", "file": "vecchietta-e35443db85.mp3",
"sec": 5.13 "sec": 5.13
@@ -1259,6 +1319,18 @@
"file": "vecchietta-814e558af5.mp3", "file": "vecchietta-814e558af5.mp3",
"sec": 1.47 "sec": 1.47
}, },
"andrea|Sono bloccato. Mi dai un consiglio?": {
"file": "andrea-42b02eb9ea.mp3",
"sec": 3.9
},
"andrea|Ma tu ce l'hai mai visto, il Regolo?": {
"file": "andrea-01d1ccb933.mp3",
"sec": 2.09
},
"andrea|Niente, volevo solo un po' di pace.": {
"file": "andrea-131481b503.mp3",
"sec": 2.79
},
"castello|Siediti. Guarda il Mugello. Respira.": { "castello|Siediti. Guarda il Mugello. Respira.": {
"file": "castello-72e22c6b59.mp3", "file": "castello-72e22c6b59.mp3",
"sec": 4.92 "sec": 4.92
@@ -1277,7 +1349,7 @@
}, },
"castello|Hai la luce, e sotto la Fortezza c'è il buio. Mettili insieme.": { "castello|Hai la luce, e sotto la Fortezza c'è il buio. Mettili insieme.": {
"file": "castello-b87ef89eaf.mp3", "file": "castello-b87ef89eaf.mp3",
"sec": 6.25 "sec": 5.59
}, },
"castello|Una chiavetta piccola apre una cosa piccola. All'area feste c'è una casetta che aspetta.": { "castello|Una chiavetta piccola apre una cosa piccola. All'area feste c'è una casetta che aspetta.": {
"file": "castello-f6d0f19ad6.mp3", "file": "castello-f6d0f19ad6.mp3",
@@ -1322,5 +1394,49 @@
"castello|Quella qui non manca mai. Torna quando vuoi.": { "castello|Quella qui non manca mai. Torna quando vuoi.": {
"file": "castello-dc8a873d86.mp3", "file": "castello-dc8a873d86.mp3",
"sec": 4.36 "sec": 4.36
},
"andrea|Niente di speciale.": {
"file": "andrea-6ebcf8e5de.mp3",
"sec": 1.04
},
"andrea|Non credo di poterlo prendere.": {
"file": "andrea-cb7a4064f2.mp3",
"sec": 1.69
},
"andrea|Non so proprio come usarlo.": {
"file": "andrea-de371ab565.mp3",
"sec": 1.57
},
"andrea|Non mi sembra un gran chiacchierone.": {
"file": "andrea-d129373272.mp3",
"sec": 1.84
},
"andrea|Non si apre.": {
"file": "andrea-481b81e3fe.mp3",
"sec": 0.95
},
"andrea|Non credo che lo voglia.": {
"file": "andrea-e6bf7b3266.mp3",
"sec": 1.37
},
"andrea|Non è roba mia da buttare.": {
"file": "andrea-7dba950893.mp3",
"sec": 1.62
},
"andrea|Non funziona.": {
"file": "andrea-a72d2697e3.mp3",
"sec": 0.95
},
"andrea|Ce l'ho già.": {
"file": "andrea-a6e94b369c.mp3",
"sec": 0.59
},
"andrea|Questo non mi serve: via.": {
"file": "andrea-4270930279.mp3",
"sec": 1.9
},
"andrea|Meglio tenerlo: potrebbe servirmi.": {
"file": "andrea-6496042236.mp3",
"sec": 2.4
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -2
View File
@@ -15,6 +15,9 @@ const DEFAULT_LINES = {
drop: 'Non è roba mia da buttare.', drop: 'Non è roba mia da buttare.',
with: 'Non funziona.', with: 'Non funziona.',
}; };
const HAVE_IT = 'Ce l\'ho già.', DROPPED = 'Questo non mi serve: via.', KEEP_IT = 'Meglio tenerlo: potrebbe servirmi.';
// Le battute che il motore dice da sé (quando la storia non ne ha una): anche loro hanno la voce (make voci).
export const ENGINE_LINES = [...Object.values(DEFAULT_LINES).filter(Boolean), HAVE_IT, DROPPED, KEEP_IT];
const SPEED = 95; const SPEED = 95;
// Solo profilo, come i cicli di camminata: 'up' e 'down' lasciano il verso com'è. // Solo profilo, come i cicli di camminata: 'up' e 'down' lasciano il verso com'è.
const FACING = { left: -1, right: 1 }; const FACING = { left: -1, right: 1 };
@@ -181,9 +184,9 @@ export class Game {
} }
if (def.actions?.[verb]) return def.actions[verb]; if (def.actions?.[verb]) return def.actions[verb];
if (verb === 'walk') return h?.exit ? [{ go: h.exit.scene, at: h.exit.at, face: h.exit.face }] : []; if (verb === 'walk') return h?.exit ? [{ go: h.exit.scene, at: h.exit.at, face: h.exit.face }] : [];
if (verb === 'take' && !h) return [{ say: 'Ce l\'ho già.' }]; if (verb === 'take' && !h) return [{ say: HAVE_IT }];
// Si butta solo quello che non serve alla storia (`droppable`): gli oggetti indispensabili restano. // Si butta solo quello che non serve alla storia (`droppable`): gli oggetti indispensabili restano.
if (verb === 'drop' && !h) return def.droppable ? [{ say: 'Questo non mi serve: via.' }, { drop: target }] : [{ say: 'Meglio tenerlo: potrebbe servirmi.' }]; if (verb === 'drop' && !h) return def.droppable ? [{ say: DROPPED }, { drop: target }] : [{ say: KEEP_IT }];
return DEFAULT_LINES[verb] ? [{ say: DEFAULT_LINES[verb] }] : []; return DEFAULT_LINES[verb] ? [{ say: DEFAULT_LINES[verb] }] : [];
} }
+5
View File
@@ -1,6 +1,8 @@
// Voci dei personaggi: un mp3 per battuta (generati da tools/voci.mjs), cercati per chi parla e testo. // Voci dei personaggi: un mp3 per battuta (generati da tools/voci.mjs), cercati per chi parla e testo.
// Senza assets/voices/index.json, o per una battuta nuova non ancora generata, il gioco resta muto e va avanti. // Senza assets/voices/index.json, o per una battuta nuova non ancora generata, il gioco resta muto e va avanti.
import { ENGINE_LINES } from './game.js';
export const voiceKey = (who, text) => `${who}|${text}`; export const voiceKey = (who, text) => `${who}|${text}`;
// Tutte le battute della storia, ovunque stiano (scene, oggetti, dialoghi, rami `if`), più le righe dell'introduzione // Tutte le battute della storia, ovunque stiano (scene, oggetti, dialoghi, rami `if`), più le righe dell'introduzione
@@ -12,12 +14,15 @@ export function voiceLines(story) {
if (Array.isArray(value)) return value.forEach(visit); if (Array.isArray(value)) return value.forEach(visit);
if (!value || typeof value !== 'object') return; if (!value || typeof value !== 'object') return;
if (typeof value.say === 'string') add(value.who ?? 'andrea', value.say); if (typeof value.say === 'string') add(value.who ?? 'andrea', value.say);
// Le scelte dei dialoghi: Andrea ripete a voce l'opzione scelta (quando non ha una `say` sua).
if (Array.isArray(value.options)) for (const o of value.options) if (typeof o.text === 'string' && o.say == null) add('andrea', o.text);
Object.values(value).forEach(visit); Object.values(value).forEach(visit);
}; };
for (const line of story.intro ?? []) for (const text of typeof line === 'string' ? [line] : Object.values(line.rione)) add('andrea', text); for (const line of story.intro ?? []) for (const text of typeof line === 'string' ? [line] : Object.values(line.rione)) add('andrea', text);
visit(story.scenes); visit(story.scenes);
visit(story.items); visit(story.items);
visit(story.dialogues); visit(story.dialogues);
for (const text of ENGINE_LINES) add('andrea', text);
return [...seen.values()]; return [...seen.values()];
} }
+1 -1
View File
@@ -1509,7 +1509,7 @@ export const story = {
take: [{ if: 'chiavetta_presa', then: [{ say: 'Non c\'è più niente da prendere.' }], else: [{ say: 'Le dita non passano fra le sbarre. Ci vorrebbe qualcosa di lungo e sottile... e appiccicoso.' }] }], take: [{ if: 'chiavetta_presa', then: [{ say: 'Non c\'è più niente da prendere.' }], else: [{ say: 'Le dita non passano fra le sbarre. Ci vorrebbe qualcosa di lungo e sottile... e appiccicoso.' }] }],
}, },
with: { with: {
stecco_gomma: [{ if: 'chiavetta_presa', then: [{ say: 'La chiavetta l\'ho già ripescata.' }], else: [{ say: 'Giù lo stecco, piano piano... la gomma tocca la chiavetta...' }, { say: 'Presa!' }, { give: 'chiavetta' }, { set: 'chiavetta_presa' }] }], stecco_gomma: [{ if: 'chiavetta_presa', then: [{ say: 'La chiavetta l\'ho già ripescata.' }], else: [{ say: 'Giù lo stecco, piano piano... la gomma tocca la chiavetta...' }, { say: 'Eccola, presa!' }, { give: 'chiavetta' }, { set: 'chiavetta_presa' }] }],
stecco: [{ say: 'Lo stecco ci arriva, ma la chiavetta non si attacca. Serve qualcosa di appiccicoso in cima.' }], stecco: [{ say: 'Lo stecco ci arriva, ma la chiavetta non si attacca. Serve qualcosa di appiccicoso in cima.' }],
gomma: [{ say: 'La gomma da sola non ci arriva. Serve qualcosa per spingerla giù.' }], gomma: [{ say: 'La gomma da sola non ci arriva. Serve qualcosa per spingerla giù.' }],
}, },
+26 -1
View File
@@ -2,7 +2,8 @@ import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs'; import { existsSync, readFileSync } from 'node:fs';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.js'; import { Game, VERBS } from '../src/engine/game.js';
import { solution } from '../src/story/solution.js';
import { voiceLines, voiceKey, Voices } from '../src/engine/voices.js'; import { voiceLines, voiceKey, Voices } from '../src/engine/voices.js';
import { settle } from './helpers.mjs'; import { settle } from './helpers.mjs';
@@ -29,6 +30,30 @@ test('ogni battuta ha la sua voce (sennò: make voci) e ogni personaggio una voc
} }
}); });
// Quello che si sente davvero giocando: la soluzione completa, e ogni verbo su ogni cosa di ogni scena (anche le
// risposte che il motore dà da sé, e le scelte dei dialoghi ripetute da Andrea). Nessuna battuta resta muta.
test('giocando, ogni battuta pronunciata ha la sua voce', () => {
const mute = new Set(), listen = said => { for (const s of said) if (s.text.trim() && !/^[.…!?\s]+$/.test(s.text) && !index[voiceKey(s.who, s.text)]) mute.add(`${s.who}: ${s.text}`); };
const g = new Game(story);
g.go(story.start.scene, story.start.at);
listen(settle(g));
for (const [verb, target, item] of solution) {
if (verb === 'choose') g.choose(g.options().findIndex(o => o.text === target)); else g.act(verb, target, item);
listen(settle(g));
}
for (const id of Object.keys(story.scenes)) {
const fresh = () => { const t = new Game(story); t.go(id); settle(t); return t; };
for (const h of fresh().hotspots()) for (const verb of VERBS) {
if (h.exit && verb === 'walk') continue;
const t = fresh();
t.act(verb, h.id);
listen(settle(t));
if (t.dialogue) for (let i = 0; i < 6 && t.dialogue && t.options().length; i++) { t.choose(0); listen(settle(t)); }
}
}
assert.deepEqual([...mute], [], 'battute senza voce (make voci)');
});
test('il castello e il leone hanno voci diverse da tutti gli altri', () => { test('il castello e il leone hanno voci diverse da tutti gli altri', () => {
for (const special of ['castello', 'leone']) for (const special of ['castello', 'leone'])
for (const [who, v] of Object.entries(voci)) for (const [who, v] of Object.entries(voci))
+44
View File
@@ -0,0 +1,44 @@
// Riascolta le voci del gioco (make ascolta): ogni mp3 di assets/voices/ passa da `mbot ascolta` (faster-whisper sulla
// GPU, skill «trascrittore») e si confronta col testo letto dalla voce. Elenca quelle che dicono altro: si rifanno
// con `node tools/voci.mjs --rifai 'pezzo di testo'`, oppure si aggiunge una riga a tools/pronuncia.json.
// node tools/ascolta.mjs [--min 0.8] [testo...] (con un testo: solo le battute che lo contengono)
import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { spoken } from './pronuncia.mjs';
const ROOT = new URL('..', import.meta.url).pathname;
const index = JSON.parse(readFileSync(join(ROOT, 'assets/voices/index.json'), 'utf8'));
const args = process.argv.slice(2), at = args.indexOf('--min');
const min = at >= 0 ? args.splice(at, 2)[1] : '0.8';
const only = args;
// I numeri Whisper li scrive in cifre («Nove.» → «9.»): per il confronto le cifre tornano parole.
const NUMERI = ['zero', 'uno', 'due', 'tre', 'quattro', 'cinque', 'sei', 'sette', 'otto', 'nove', 'dieci', 'undici', 'dodici'];
const plain = t => t.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/g, ' ').trim();
const jobs = Object.entries(index)
.map(([key, e]) => ({ who: key.split('|')[0], text: key.slice(key.indexOf('|') + 1), file: join(ROOT, 'assets/voices', e.file) }))
.filter(j => !only.length || only.some(o => j.text.includes(o)))
.map(j => ({ ...j, expect: spoken(j.text) }));
const dir = join(tmpdir(), 'leggenda-regolo-ascolto');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'ascolto.json'), JSON.stringify(jobs.map(({ file, expect }) => ({ file, expect }))));
let out = '';
try {
out = execFileSync('mbot', ['ascolta', '--jobs', join(dir, 'ascolto.json'), '--json', '--min', min],
{ encoding: 'utf8', maxBuffer: 64 << 20, stdio: ['ignore', 'pipe', 'inherit'] });
} catch (e) { out = e.stdout ?? ''; }
const byFile = new Map(jobs.map(j => [j.file, j]));
const bad = [];
for (const line of out.split('\n').filter(l => l.startsWith('{'))) {
const r = JSON.parse(line);
if (r.ok !== false) continue;
const j = byFile.get(r.file), heard = r.text ?? r.heard ?? '';
if (j && plain(heard.replace(/\b(\d{1,2})\b/g, (m, n) => NUMERI[+n] ?? m)) === plain(j.expect)) continue;
bad.push({ ...j, heard, score: r.score ?? r.similarity });
}
for (const b of bad) console.log(`${b.score ?? '?'} ${b.who}: «${b.text}»\n sentito: «${b.heard}»`);
console.log(`${jobs.length} voci ascoltate, ${bad.length} da guardare`);
+3 -1
View File
@@ -1,4 +1,6 @@
{ {
"_nota": "Parole che XTTS legge male in italiano: a sinistra come sono scritte nella storia (e nel fumetto), a destra come vanno lette dalla voce. Si cerca la parola intera, senza badare alle maiuscole. Cambiare una riga rigenera solo le battute che la contengono (make voci). Esempio: «celo» (ce l'ho, delle figurine) XTTS lo legge «cielo».", "_nota": "Parole che XTTS legge male in italiano: a sinistra come sono scritte nella storia (e nel fumetto), a destra come vanno lette dalla voce. Si cerca la parola intera, senza badare alle maiuscole. Cambiare una riga rigenera solo le battute che la contengono (make voci). Esempio: «celo» (ce l'ho, delle figurine) XTTS lo legge «cielo».",
"celo": "ce l'ho" "celo": "ce l'ho",
"Cosimo I": "Cosimo primo",
"padel": "pàdel"
} }