diff --git a/Dockerfile b/Dockerfile index 9ab4a6c..40f13c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ COPY src /usr/share/nginx/html/src COPY assets/scenes /usr/share/nginx/html/assets/scenes COPY assets/characters /usr/share/nginx/html/assets/characters COPY assets/audio /usr/share/nginx/html/assets/audio +COPY assets/voices /usr/share/nginx/html/assets/voices EXPOSE 80 HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 diff --git a/Makefile b/Makefile index 1509499..ceff14f 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ DST ?= assets/scenes/$(notdir $(SRC)) CROP ?= COLORS ?= 64 -.PHONY: help serve open test browser verify sfondi sfondi-genera sfondi-confronto background character \ +.PHONY: help serve open test browser verify voci voci-elenco sfondi sfondi-genera sfondi-confronto background character \ build up down restart logs ps shell image-size clean distclean ## ------------------------------------------------------------ sviluppo @@ -54,6 +54,12 @@ verify: test browser ## Tutti i controlli ## ------------------------------------------------------------ grafica +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)") + +voci-elenco: ## Quali battute mancano di voce, senza generare niente + $(NODE) tools/voci.mjs --dry + sfondi: ## Prova a secco del generatore: quali foto di assets/raw/foto/ genererebbe (nessuna API) $(PYTHON) tools/genera.py --dry-run diff --git a/README.md b/README.md index 6d5fd5a..bf24add 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,19 @@ di prepararne copie a 640×360 per il gioco. python3 tools/genera.py --dry-run # elenco delle foto, nessuna API e nessun file scritto ``` +## Voci dei personaggi + +Ogni battuta ha la sua voce, generata con XTTS-v2 sulla GPU (licenza solo non commerciale): un mp3 per battuta in +`assets/voices/`, la voce di ogni personaggio in `tools/voci.json` (castello e leone con altezza ed eco proprie). + +```zsh +make voci # solo le battute nuove o cambiate; i file che non servono più si tolgono +make voci RIFAI='Ci sei?' # ritenta una battuta storpiata (XTTS è casuale: di solito il secondo tentativo va) +make voci-elenco # cosa manca, senza generare +``` + +Il test `tests/voices.test.mjs` fallisce se una battuta non ha la sua voce: dopo aver cambiato un testo, `make voci`. + ## Giocare in locale ```zsh diff --git a/assets/voices/andrea-058d10ea48.mp3 b/assets/voices/andrea-058d10ea48.mp3 new file mode 100644 index 0000000..a5d5bc3 Binary files /dev/null and b/assets/voices/andrea-058d10ea48.mp3 differ diff --git a/assets/voices/andrea-05d9f5aea0.mp3 b/assets/voices/andrea-05d9f5aea0.mp3 new file mode 100644 index 0000000..5cf8dec Binary files /dev/null and b/assets/voices/andrea-05d9f5aea0.mp3 differ diff --git a/assets/voices/andrea-0b61233e59.mp3 b/assets/voices/andrea-0b61233e59.mp3 new file mode 100644 index 0000000..d1ee54c Binary files /dev/null and b/assets/voices/andrea-0b61233e59.mp3 differ diff --git a/assets/voices/andrea-14cbceece4.mp3 b/assets/voices/andrea-14cbceece4.mp3 new file mode 100644 index 0000000..cbdd6a1 Binary files /dev/null and b/assets/voices/andrea-14cbceece4.mp3 differ diff --git a/assets/voices/andrea-17666fc335.mp3 b/assets/voices/andrea-17666fc335.mp3 new file mode 100644 index 0000000..5d32d30 Binary files /dev/null and b/assets/voices/andrea-17666fc335.mp3 differ diff --git a/assets/voices/andrea-17a13a2303.mp3 b/assets/voices/andrea-17a13a2303.mp3 new file mode 100644 index 0000000..15f01f6 Binary files /dev/null and b/assets/voices/andrea-17a13a2303.mp3 differ diff --git a/assets/voices/andrea-17b8f09af1.mp3 b/assets/voices/andrea-17b8f09af1.mp3 new file mode 100644 index 0000000..da72da6 Binary files /dev/null and b/assets/voices/andrea-17b8f09af1.mp3 differ diff --git a/assets/voices/andrea-1d80123a55.mp3 b/assets/voices/andrea-1d80123a55.mp3 new file mode 100644 index 0000000..240f1be Binary files /dev/null and b/assets/voices/andrea-1d80123a55.mp3 differ diff --git a/assets/voices/andrea-1e8c868d6d.mp3 b/assets/voices/andrea-1e8c868d6d.mp3 new file mode 100644 index 0000000..bb2198c Binary files /dev/null and b/assets/voices/andrea-1e8c868d6d.mp3 differ diff --git a/assets/voices/andrea-1fda2a0f12.mp3 b/assets/voices/andrea-1fda2a0f12.mp3 new file mode 100644 index 0000000..2eb7ba9 Binary files /dev/null and b/assets/voices/andrea-1fda2a0f12.mp3 differ diff --git a/assets/voices/andrea-225e29ed43.mp3 b/assets/voices/andrea-225e29ed43.mp3 new file mode 100644 index 0000000..28488a4 Binary files /dev/null and b/assets/voices/andrea-225e29ed43.mp3 differ diff --git a/assets/voices/andrea-239bf733a6.mp3 b/assets/voices/andrea-239bf733a6.mp3 new file mode 100644 index 0000000..e0c738a Binary files /dev/null and b/assets/voices/andrea-239bf733a6.mp3 differ diff --git a/assets/voices/andrea-23e40a2495.mp3 b/assets/voices/andrea-23e40a2495.mp3 new file mode 100644 index 0000000..1425d6f Binary files /dev/null and b/assets/voices/andrea-23e40a2495.mp3 differ diff --git a/assets/voices/andrea-241b0de20a.mp3 b/assets/voices/andrea-241b0de20a.mp3 new file mode 100644 index 0000000..a64dcaa Binary files /dev/null and b/assets/voices/andrea-241b0de20a.mp3 differ diff --git a/assets/voices/andrea-2671007617.mp3 b/assets/voices/andrea-2671007617.mp3 new file mode 100644 index 0000000..b205846 Binary files /dev/null and b/assets/voices/andrea-2671007617.mp3 differ diff --git a/assets/voices/andrea-351339cdb5.mp3 b/assets/voices/andrea-351339cdb5.mp3 new file mode 100644 index 0000000..b4c6b60 Binary files /dev/null and b/assets/voices/andrea-351339cdb5.mp3 differ diff --git a/assets/voices/andrea-35430efef3.mp3 b/assets/voices/andrea-35430efef3.mp3 new file mode 100644 index 0000000..1e18cd1 Binary files /dev/null and b/assets/voices/andrea-35430efef3.mp3 differ diff --git a/assets/voices/andrea-36af18a0c4.mp3 b/assets/voices/andrea-36af18a0c4.mp3 new file mode 100644 index 0000000..340f028 Binary files /dev/null and b/assets/voices/andrea-36af18a0c4.mp3 differ diff --git a/assets/voices/andrea-3a6af3897f.mp3 b/assets/voices/andrea-3a6af3897f.mp3 new file mode 100644 index 0000000..73fd8ab Binary files /dev/null and b/assets/voices/andrea-3a6af3897f.mp3 differ diff --git a/assets/voices/andrea-4167cc8c5e.mp3 b/assets/voices/andrea-4167cc8c5e.mp3 new file mode 100644 index 0000000..cd35411 Binary files /dev/null and b/assets/voices/andrea-4167cc8c5e.mp3 differ diff --git a/assets/voices/andrea-42760fbe6c.mp3 b/assets/voices/andrea-42760fbe6c.mp3 new file mode 100644 index 0000000..b99a3e4 Binary files /dev/null and b/assets/voices/andrea-42760fbe6c.mp3 differ diff --git a/assets/voices/andrea-42e270af3d.mp3 b/assets/voices/andrea-42e270af3d.mp3 new file mode 100644 index 0000000..333000b Binary files /dev/null and b/assets/voices/andrea-42e270af3d.mp3 differ diff --git a/assets/voices/andrea-43d3435d61.mp3 b/assets/voices/andrea-43d3435d61.mp3 new file mode 100644 index 0000000..b099738 Binary files /dev/null and b/assets/voices/andrea-43d3435d61.mp3 differ diff --git a/assets/voices/andrea-468fe91000.mp3 b/assets/voices/andrea-468fe91000.mp3 new file mode 100644 index 0000000..4df4c3c Binary files /dev/null and b/assets/voices/andrea-468fe91000.mp3 differ diff --git a/assets/voices/andrea-486c980f52.mp3 b/assets/voices/andrea-486c980f52.mp3 new file mode 100644 index 0000000..15ff100 Binary files /dev/null and b/assets/voices/andrea-486c980f52.mp3 differ diff --git a/assets/voices/andrea-4ab41e1b8e.mp3 b/assets/voices/andrea-4ab41e1b8e.mp3 new file mode 100644 index 0000000..d2b2b79 Binary files /dev/null and b/assets/voices/andrea-4ab41e1b8e.mp3 differ diff --git a/assets/voices/andrea-4b733def4f.mp3 b/assets/voices/andrea-4b733def4f.mp3 new file mode 100644 index 0000000..f682732 Binary files /dev/null and b/assets/voices/andrea-4b733def4f.mp3 differ diff --git a/assets/voices/andrea-4d6871c326.mp3 b/assets/voices/andrea-4d6871c326.mp3 new file mode 100644 index 0000000..48df09a Binary files /dev/null and b/assets/voices/andrea-4d6871c326.mp3 differ diff --git a/assets/voices/andrea-4ebc297dfc.mp3 b/assets/voices/andrea-4ebc297dfc.mp3 new file mode 100644 index 0000000..2b7169c Binary files /dev/null and b/assets/voices/andrea-4ebc297dfc.mp3 differ diff --git a/assets/voices/andrea-5206cf85dd.mp3 b/assets/voices/andrea-5206cf85dd.mp3 new file mode 100644 index 0000000..a8da6ed Binary files /dev/null and b/assets/voices/andrea-5206cf85dd.mp3 differ diff --git a/assets/voices/andrea-559fbc00c4.mp3 b/assets/voices/andrea-559fbc00c4.mp3 new file mode 100644 index 0000000..4376731 Binary files /dev/null and b/assets/voices/andrea-559fbc00c4.mp3 differ diff --git a/assets/voices/andrea-566475abe2.mp3 b/assets/voices/andrea-566475abe2.mp3 new file mode 100644 index 0000000..d45c9f6 Binary files /dev/null and b/assets/voices/andrea-566475abe2.mp3 differ diff --git a/assets/voices/andrea-59a60dc5a7.mp3 b/assets/voices/andrea-59a60dc5a7.mp3 new file mode 100644 index 0000000..def1541 Binary files /dev/null and b/assets/voices/andrea-59a60dc5a7.mp3 differ diff --git a/assets/voices/andrea-5e1cd26d46.mp3 b/assets/voices/andrea-5e1cd26d46.mp3 new file mode 100644 index 0000000..6e33bc3 Binary files /dev/null and b/assets/voices/andrea-5e1cd26d46.mp3 differ diff --git a/assets/voices/andrea-5fec22ccd5.mp3 b/assets/voices/andrea-5fec22ccd5.mp3 new file mode 100644 index 0000000..2303cd0 Binary files /dev/null and b/assets/voices/andrea-5fec22ccd5.mp3 differ diff --git a/assets/voices/andrea-6012288000.mp3 b/assets/voices/andrea-6012288000.mp3 new file mode 100644 index 0000000..0afe7c7 Binary files /dev/null and b/assets/voices/andrea-6012288000.mp3 differ diff --git a/assets/voices/andrea-625ef8dc45.mp3 b/assets/voices/andrea-625ef8dc45.mp3 new file mode 100644 index 0000000..2ad3e4b Binary files /dev/null and b/assets/voices/andrea-625ef8dc45.mp3 differ diff --git a/assets/voices/andrea-658f69bfec.mp3 b/assets/voices/andrea-658f69bfec.mp3 new file mode 100644 index 0000000..3e22c15 Binary files /dev/null and b/assets/voices/andrea-658f69bfec.mp3 differ diff --git a/assets/voices/andrea-69337973d0.mp3 b/assets/voices/andrea-69337973d0.mp3 new file mode 100644 index 0000000..2ea1b51 Binary files /dev/null and b/assets/voices/andrea-69337973d0.mp3 differ diff --git a/assets/voices/andrea-72dede01c0.mp3 b/assets/voices/andrea-72dede01c0.mp3 new file mode 100644 index 0000000..97dd3b1 Binary files /dev/null and b/assets/voices/andrea-72dede01c0.mp3 differ diff --git a/assets/voices/andrea-72e4ebb412.mp3 b/assets/voices/andrea-72e4ebb412.mp3 new file mode 100644 index 0000000..f5d7cd1 Binary files /dev/null and b/assets/voices/andrea-72e4ebb412.mp3 differ diff --git a/assets/voices/andrea-73eec7b287.mp3 b/assets/voices/andrea-73eec7b287.mp3 new file mode 100644 index 0000000..ddf2225 Binary files /dev/null and b/assets/voices/andrea-73eec7b287.mp3 differ diff --git a/assets/voices/andrea-77f1348d3a.mp3 b/assets/voices/andrea-77f1348d3a.mp3 new file mode 100644 index 0000000..15a92cb Binary files /dev/null and b/assets/voices/andrea-77f1348d3a.mp3 differ diff --git a/assets/voices/andrea-78a0a84b84.mp3 b/assets/voices/andrea-78a0a84b84.mp3 new file mode 100644 index 0000000..251fe8c Binary files /dev/null and b/assets/voices/andrea-78a0a84b84.mp3 differ diff --git a/assets/voices/andrea-78b87350bf.mp3 b/assets/voices/andrea-78b87350bf.mp3 new file mode 100644 index 0000000..f8f2db4 Binary files /dev/null and b/assets/voices/andrea-78b87350bf.mp3 differ diff --git a/assets/voices/andrea-7f1ad6889b.mp3 b/assets/voices/andrea-7f1ad6889b.mp3 new file mode 100644 index 0000000..5ad3178 Binary files /dev/null and b/assets/voices/andrea-7f1ad6889b.mp3 differ diff --git a/assets/voices/andrea-7fda3c3de7.mp3 b/assets/voices/andrea-7fda3c3de7.mp3 new file mode 100644 index 0000000..e987b8e Binary files /dev/null and b/assets/voices/andrea-7fda3c3de7.mp3 differ diff --git a/assets/voices/andrea-82a210b2d9.mp3 b/assets/voices/andrea-82a210b2d9.mp3 new file mode 100644 index 0000000..87a7d52 Binary files /dev/null and b/assets/voices/andrea-82a210b2d9.mp3 differ diff --git a/assets/voices/andrea-847cbcd7f3.mp3 b/assets/voices/andrea-847cbcd7f3.mp3 new file mode 100644 index 0000000..a645347 Binary files /dev/null and b/assets/voices/andrea-847cbcd7f3.mp3 differ diff --git a/assets/voices/andrea-8822c9e02f.mp3 b/assets/voices/andrea-8822c9e02f.mp3 new file mode 100644 index 0000000..1166301 Binary files /dev/null and b/assets/voices/andrea-8822c9e02f.mp3 differ diff --git a/assets/voices/andrea-88a6f5518b.mp3 b/assets/voices/andrea-88a6f5518b.mp3 new file mode 100644 index 0000000..dbaa081 Binary files /dev/null and b/assets/voices/andrea-88a6f5518b.mp3 differ diff --git a/assets/voices/andrea-8d18461141.mp3 b/assets/voices/andrea-8d18461141.mp3 new file mode 100644 index 0000000..8aedcbd Binary files /dev/null and b/assets/voices/andrea-8d18461141.mp3 differ diff --git a/assets/voices/andrea-9a7a5f0af1.mp3 b/assets/voices/andrea-9a7a5f0af1.mp3 new file mode 100644 index 0000000..ff2a4ab Binary files /dev/null and b/assets/voices/andrea-9a7a5f0af1.mp3 differ diff --git a/assets/voices/andrea-9a8732a3aa.mp3 b/assets/voices/andrea-9a8732a3aa.mp3 new file mode 100644 index 0000000..0bb6319 Binary files /dev/null and b/assets/voices/andrea-9a8732a3aa.mp3 differ diff --git a/assets/voices/andrea-9c9ee9e896.mp3 b/assets/voices/andrea-9c9ee9e896.mp3 new file mode 100644 index 0000000..0285e8c Binary files /dev/null and b/assets/voices/andrea-9c9ee9e896.mp3 differ diff --git a/assets/voices/andrea-a1dab4073c.mp3 b/assets/voices/andrea-a1dab4073c.mp3 new file mode 100644 index 0000000..1803d4d Binary files /dev/null and b/assets/voices/andrea-a1dab4073c.mp3 differ diff --git a/assets/voices/andrea-a2f0124f4d.mp3 b/assets/voices/andrea-a2f0124f4d.mp3 new file mode 100644 index 0000000..163f3b4 Binary files /dev/null and b/assets/voices/andrea-a2f0124f4d.mp3 differ diff --git a/assets/voices/andrea-a69c700102.mp3 b/assets/voices/andrea-a69c700102.mp3 new file mode 100644 index 0000000..cf87f01 Binary files /dev/null and b/assets/voices/andrea-a69c700102.mp3 differ diff --git a/assets/voices/andrea-a9df2e6bbb.mp3 b/assets/voices/andrea-a9df2e6bbb.mp3 new file mode 100644 index 0000000..b4f5438 Binary files /dev/null and b/assets/voices/andrea-a9df2e6bbb.mp3 differ diff --git a/assets/voices/andrea-aa668b37c2.mp3 b/assets/voices/andrea-aa668b37c2.mp3 new file mode 100644 index 0000000..80a68a7 Binary files /dev/null and b/assets/voices/andrea-aa668b37c2.mp3 differ diff --git a/assets/voices/andrea-af4d717814.mp3 b/assets/voices/andrea-af4d717814.mp3 new file mode 100644 index 0000000..b292b7d Binary files /dev/null and b/assets/voices/andrea-af4d717814.mp3 differ diff --git a/assets/voices/andrea-afa6ed50f6.mp3 b/assets/voices/andrea-afa6ed50f6.mp3 new file mode 100644 index 0000000..d898867 Binary files /dev/null and b/assets/voices/andrea-afa6ed50f6.mp3 differ diff --git a/assets/voices/andrea-b3acca1e65.mp3 b/assets/voices/andrea-b3acca1e65.mp3 new file mode 100644 index 0000000..948a9d8 Binary files /dev/null and b/assets/voices/andrea-b3acca1e65.mp3 differ diff --git a/assets/voices/andrea-b444f5f09a.mp3 b/assets/voices/andrea-b444f5f09a.mp3 new file mode 100644 index 0000000..5fb3b36 Binary files /dev/null and b/assets/voices/andrea-b444f5f09a.mp3 differ diff --git a/assets/voices/andrea-b8c4089c30.mp3 b/assets/voices/andrea-b8c4089c30.mp3 new file mode 100644 index 0000000..c892b3f Binary files /dev/null and b/assets/voices/andrea-b8c4089c30.mp3 differ diff --git a/assets/voices/andrea-b9570589ad.mp3 b/assets/voices/andrea-b9570589ad.mp3 new file mode 100644 index 0000000..6a5d681 Binary files /dev/null and b/assets/voices/andrea-b9570589ad.mp3 differ diff --git a/assets/voices/andrea-ba92d2fa16.mp3 b/assets/voices/andrea-ba92d2fa16.mp3 new file mode 100644 index 0000000..f39ba7e Binary files /dev/null and b/assets/voices/andrea-ba92d2fa16.mp3 differ diff --git a/assets/voices/andrea-bd1ea0e1d2.mp3 b/assets/voices/andrea-bd1ea0e1d2.mp3 new file mode 100644 index 0000000..2b6a02b Binary files /dev/null and b/assets/voices/andrea-bd1ea0e1d2.mp3 differ diff --git a/assets/voices/andrea-bd4991b099.mp3 b/assets/voices/andrea-bd4991b099.mp3 new file mode 100644 index 0000000..f2ec498 Binary files /dev/null and b/assets/voices/andrea-bd4991b099.mp3 differ diff --git a/assets/voices/andrea-c0e71bfd36.mp3 b/assets/voices/andrea-c0e71bfd36.mp3 new file mode 100644 index 0000000..8627ba8 Binary files /dev/null and b/assets/voices/andrea-c0e71bfd36.mp3 differ diff --git a/assets/voices/andrea-c41f9eaaf3.mp3 b/assets/voices/andrea-c41f9eaaf3.mp3 new file mode 100644 index 0000000..ae5a005 Binary files /dev/null and b/assets/voices/andrea-c41f9eaaf3.mp3 differ diff --git a/assets/voices/andrea-c57cc17e0e.mp3 b/assets/voices/andrea-c57cc17e0e.mp3 new file mode 100644 index 0000000..df39dcf Binary files /dev/null and b/assets/voices/andrea-c57cc17e0e.mp3 differ diff --git a/assets/voices/andrea-c621bca741.mp3 b/assets/voices/andrea-c621bca741.mp3 new file mode 100644 index 0000000..27b4d5d Binary files /dev/null and b/assets/voices/andrea-c621bca741.mp3 differ diff --git a/assets/voices/andrea-c6f58d3ba7.mp3 b/assets/voices/andrea-c6f58d3ba7.mp3 new file mode 100644 index 0000000..c2d7697 Binary files /dev/null and b/assets/voices/andrea-c6f58d3ba7.mp3 differ diff --git a/assets/voices/andrea-cc75cfed49.mp3 b/assets/voices/andrea-cc75cfed49.mp3 new file mode 100644 index 0000000..6ef8182 Binary files /dev/null and b/assets/voices/andrea-cc75cfed49.mp3 differ diff --git a/assets/voices/andrea-ce906dd378.mp3 b/assets/voices/andrea-ce906dd378.mp3 new file mode 100644 index 0000000..8111b7e Binary files /dev/null and b/assets/voices/andrea-ce906dd378.mp3 differ diff --git a/assets/voices/andrea-cea6462caa.mp3 b/assets/voices/andrea-cea6462caa.mp3 new file mode 100644 index 0000000..2b4e3fd Binary files /dev/null and b/assets/voices/andrea-cea6462caa.mp3 differ diff --git a/assets/voices/andrea-d0420ca9a9.mp3 b/assets/voices/andrea-d0420ca9a9.mp3 new file mode 100644 index 0000000..351d93e Binary files /dev/null and b/assets/voices/andrea-d0420ca9a9.mp3 differ diff --git a/assets/voices/andrea-dacb32e5c6.mp3 b/assets/voices/andrea-dacb32e5c6.mp3 new file mode 100644 index 0000000..9aa7e2c Binary files /dev/null and b/assets/voices/andrea-dacb32e5c6.mp3 differ diff --git a/assets/voices/andrea-dd1ea7377d.mp3 b/assets/voices/andrea-dd1ea7377d.mp3 new file mode 100644 index 0000000..5fc7ba1 Binary files /dev/null and b/assets/voices/andrea-dd1ea7377d.mp3 differ diff --git a/assets/voices/andrea-e5cf871a9d.mp3 b/assets/voices/andrea-e5cf871a9d.mp3 new file mode 100644 index 0000000..6ea24c8 Binary files /dev/null and b/assets/voices/andrea-e5cf871a9d.mp3 differ diff --git a/assets/voices/andrea-e8fba1f859.mp3 b/assets/voices/andrea-e8fba1f859.mp3 new file mode 100644 index 0000000..774a6f3 Binary files /dev/null and b/assets/voices/andrea-e8fba1f859.mp3 differ diff --git a/assets/voices/andrea-eeb5a2273c.mp3 b/assets/voices/andrea-eeb5a2273c.mp3 new file mode 100644 index 0000000..40c0c74 Binary files /dev/null and b/assets/voices/andrea-eeb5a2273c.mp3 differ diff --git a/assets/voices/andrea-f07440a0ee.mp3 b/assets/voices/andrea-f07440a0ee.mp3 new file mode 100644 index 0000000..6e45cd7 Binary files /dev/null and b/assets/voices/andrea-f07440a0ee.mp3 differ diff --git a/assets/voices/andrea-f365205226.mp3 b/assets/voices/andrea-f365205226.mp3 new file mode 100644 index 0000000..7265155 Binary files /dev/null and b/assets/voices/andrea-f365205226.mp3 differ diff --git a/assets/voices/andrea-f607bf7481.mp3 b/assets/voices/andrea-f607bf7481.mp3 new file mode 100644 index 0000000..2489d80 Binary files /dev/null and b/assets/voices/andrea-f607bf7481.mp3 differ diff --git a/assets/voices/andrea-f798fa755d.mp3 b/assets/voices/andrea-f798fa755d.mp3 new file mode 100644 index 0000000..d67f9c4 Binary files /dev/null and b/assets/voices/andrea-f798fa755d.mp3 differ diff --git a/assets/voices/andrea-fc32e07a5a.mp3 b/assets/voices/andrea-fc32e07a5a.mp3 new file mode 100644 index 0000000..d3eb372 Binary files /dev/null and b/assets/voices/andrea-fc32e07a5a.mp3 differ diff --git a/assets/voices/andrea-fd10e3f083.mp3 b/assets/voices/andrea-fd10e3f083.mp3 new file mode 100644 index 0000000..9050ce5 Binary files /dev/null and b/assets/voices/andrea-fd10e3f083.mp3 differ diff --git a/assets/voices/andrea-fdeb17aa77.mp3 b/assets/voices/andrea-fdeb17aa77.mp3 new file mode 100644 index 0000000..e4f0266 Binary files /dev/null and b/assets/voices/andrea-fdeb17aa77.mp3 differ diff --git a/assets/voices/andrea-fe5b32c079.mp3 b/assets/voices/andrea-fe5b32c079.mp3 new file mode 100644 index 0000000..7c8ae10 Binary files /dev/null and b/assets/voices/andrea-fe5b32c079.mp3 differ diff --git a/assets/voices/andrea-feabba4867.mp3 b/assets/voices/andrea-feabba4867.mp3 new file mode 100644 index 0000000..e1de619 Binary files /dev/null and b/assets/voices/andrea-feabba4867.mp3 differ diff --git a/assets/voices/castello-2cdba66b66.mp3 b/assets/voices/castello-2cdba66b66.mp3 new file mode 100644 index 0000000..40645fc Binary files /dev/null and b/assets/voices/castello-2cdba66b66.mp3 differ diff --git a/assets/voices/castello-4f3721ca46.mp3 b/assets/voices/castello-4f3721ca46.mp3 new file mode 100644 index 0000000..859d1ab Binary files /dev/null and b/assets/voices/castello-4f3721ca46.mp3 differ diff --git a/assets/voices/castello-72e22c6b59.mp3 b/assets/voices/castello-72e22c6b59.mp3 new file mode 100644 index 0000000..ccfc5c6 Binary files /dev/null and b/assets/voices/castello-72e22c6b59.mp3 differ diff --git a/assets/voices/castello-888f24e1fd.mp3 b/assets/voices/castello-888f24e1fd.mp3 new file mode 100644 index 0000000..a21f03b Binary files /dev/null and b/assets/voices/castello-888f24e1fd.mp3 differ diff --git a/assets/voices/castello-a1dfd76497.mp3 b/assets/voices/castello-a1dfd76497.mp3 new file mode 100644 index 0000000..cb797cf Binary files /dev/null and b/assets/voices/castello-a1dfd76497.mp3 differ diff --git a/assets/voices/castello-dc8a873d86.mp3 b/assets/voices/castello-dc8a873d86.mp3 new file mode 100644 index 0000000..2efc0cb Binary files /dev/null and b/assets/voices/castello-dc8a873d86.mp3 differ diff --git a/assets/voices/index.json b/assets/voices/index.json new file mode 100644 index 0000000..493da57 --- /dev/null +++ b/assets/voices/index.json @@ -0,0 +1,450 @@ +{ + "andrea|San Piero a Sieve, nel Mugello. Fine maggio 2007.": { + "file": "andrea-4167cc8c5e.mp3", + "sec": 4.59 + }, + "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", + "sec": 9.75 + }, + "andrea|Io corro per Campomigliaio, i blu.": { + "file": "andrea-aa668b37c2.mp3", + "sec": 2.48 + }, + "andrea|E stamattina, in piazza, è successo qualcosa...": { + "file": "andrea-4ab41e1b8e.mp3", + "sec": 3.28 + }, + "andrea|La colonna, il centro del paese. Da qui passa tutto: le notizie, il Palio, i pettegolezzi.": { + "file": "andrea-c0e71bfd36.mp3", + "sec": 7.43 + }, + "andrea|Ai piedi c'è una strana scia luccicante che va a sinistra, verso la salita di Villa Adami.": { + "file": "andrea-c57cc17e0e.mp3", + "sec": 6.06 + }, + "andrea|Se mi porto via la colonna, la piazza come la chiamano?": { + "file": "andrea-f607bf7481.mp3", + "sec": 3.96 + }, + "andrea|Arrampicarmi? Il palio lassù non c'è comunque.": { + "file": "andrea-dacb32e5c6.mp3", + "sec": 4.01 + }, + "andrea|Il bar della piazza. Chi c'era dietro il bancone nel 2007? Questo lo sa solo Andrea.": { + "file": "andrea-4b733def4f.mp3", + "sec": 7.27 + }, + "andrea|Ci torno quando ho qualche soldo in tasca.": { + "file": "andrea-dd1ea7377d.mp3", + "sec": 2.39 + }, + "andrea|Parlare con una porta. Neanche al Palio si arriva a tanto.": { + "file": "andrea-feabba4867.mp3", + "sec": 4.37 + }, + "andrea|Di là c'è la gelateria del Fiocco di Neve.": { + "file": "andrea-17a13a2303.mp3", + "sec": 2.65 + }, + "andrea|Con questo caldo, un bel gelatino ci starebbe proprio bene...": { + "file": "andrea-b9570589ad.mp3", + "sec": 3.98 + }, + "andrea|No. Prima ritrovo il palio, poi mi premio.": { + "file": "andrea-2671007617.mp3", + "sec": 3.65 + }, + "andrea|La strada che porta al Fiocco di Neve. Già sento il profumo di cialda.": { + "file": "andrea-a9df2e6bbb.mp3", + "sec": 4.98 + }, + "andrea|Il programma del Palio della Fortezza. Qualcuno ci ha scritto sopra col pennarello: «IL REGOLO SI È RIPRESO IL PALIO».": { + "file": "andrea-cea6462caa.mp3", + "sec": 7.61 + }, + "andrea|Il programma lo so a memoria. Il problema è il palio.": { + "file": "andrea-559fbc00c4.mp3", + "sec": 4.2 + }, + "andrea|I vecchietti della panchina. Sanno tutto di tutti, soprattutto quello che non è successo.": { + "file": "andrea-af4d717814.mp3", + "sec": 6.44 + }, + "andrea|Se li porto via, il paese resta senza notizie.": { + "file": "andrea-566475abe2.mp3", + "sec": 3.48 + }, + "andrea|Guardate cosa ho trovato.": { + "file": "andrea-f365205226.mp3", + "sec": 1.5 + }, + "vecchietta|Bella! È una patatina?": { + "file": "vecchietta-3c84f4b1cb.mp3", + "sec": 2.21 + }, + "vecchietto|Ma che patatina, è il Regolo che fa la muta!": { + "file": "vecchietto-5acd932b36.mp3", + "sec": 3.16 + }, + "andrea|Me la tengo io, grazie.": { + "file": "andrea-eeb5a2273c.mp3", + "sec": 1.3 + }, + "andrea|Qualcosa luccica, proprio sotto i piedi dei vecchietti.": { + "file": "andrea-9a8732a3aa.mp3", + "sec": 3.97 + }, + "andrea|Polvere, un tappo e una cicca. La solita archeologia di piazza.": { + "file": "andrea-351339cdb5.mp3", + "sec": 5.8 + }, + "andrea|Una cosa verde e luccicante. Non è un tappo.": { + "file": "andrea-ba92d2fa16.mp3", + "sec": 3.52 + }, + "andrea|Una squama! E bella grossa.": { + "file": "andrea-b444f5f09a.mp3", + "sec": 2.6 + }, + "andrea|Il palio è sparito! Il quadro che hanno dipinto i ragazzi delle medie.": { + "file": "andrea-f07440a0ee.mp3", + "sec": 4.48 + }, + "andrea|La festa è domani: se nessuno lo ritrova, salta tutto. Tocca a me.": { + "file": "andrea-69337973d0.mp3", + "sec": 5.94 + }, + "andrea|Nove bastioni, li volle Cosimo I. Per tenere fuori i nemici, dicono. Secondo me per tenere dentro il Regolo.": { + "file": "andrea-0b61233e59.mp3", + "sec": 9.95 + }, + "andrea|Arrampicarmi sul bastione? Ho visto film che finivano meglio.": { + "file": "andrea-625ef8dc45.mp3", + "sec": 4.23 + }, + "andrea|La porta della Fortezza. Chiusa con una catena grossa come il mio braccio.": { + "file": "andrea-9a7a5f0af1.mp3", + "sec": 4.91 + }, + "andrea|Niente da fare. La Fortezza è chiusa da anni, e il lucchetto lo sa.": { + "file": "andrea-cc75cfed49.mp3", + "sec": 4.59 + }, + "andrea|Regolo? Ci sei?": { + "file": "andrea-5206cf85dd.mp3", + "sec": 2.47 + }, + "andrea|Ho sentito qualcosa. O era la mia pancia.": { + "file": "andrea-4ebc297dfc.mp3", + "sec": 3.33 + }, + "andrea|Niente scarpe, niente zampe. Qualcosa è strisciato nell'erba fino alla porta.": { + "file": "andrea-7fda3c3de7.mp3", + "sec": 6.9 + }, + "andrea|Qualcosa di grosso.": { + "file": "andrea-ce906dd378.mp3", + "sec": 1.27 + }, + "andrea|Le impronte non si prendono. Si seguono.": { + "file": "andrea-fd10e3f083.mp3", + "sec": 3.04 + }, + "andrea|Le metto vicine...": { + "file": "andrea-73eec7b287.mp3", + "sec": 0.91 + }, + "andrea|Stesso luccichio, stessa puzza di cantina. Il Regolo è passato di qui!": { + "file": "andrea-847cbcd7f3.mp3", + "sec": 5.66 + }, + "andrea|Per stavolta finisce qui: il resto della storia lo dobbiamo ancora scrivere.": { + "file": "andrea-17b8f09af1.mp3", + "sec": 4.38 + }, + "andrea|La Pieve di San Pietro. Il rione Pieve, quelli in bianco.": { + "file": "andrea-c41f9eaaf3.mp3", + "sec": 4.86 + }, + "andrea|Il prete è a benedire le carrette del Palio. Torno dopo.": { + "file": "andrea-bd4991b099.mp3", + "sec": 4.31 + }, + "andrea|Il campanile della Pieve. Da lassù si vedrebbe tutto il paese. Anche un serpente con le ali.": { + "file": "andrea-6012288000.mp3", + "sec": 7.97 + }, + "andrea|Una statua sul piedistallo. Lei il palio l'ha visto passare di sicuro, ma non parla.": { + "file": "andrea-36af18a0c4.mp3", + "sec": 5.98 + }, + "andrea|Buongiorno.": { + "file": "andrea-05d9f5aea0.mp3", + "sec": 0.63 + }, + "andrea|Come i vecchietti quando gli chiedi dei debiti.": { + "file": "andrea-88a6f5518b.mp3", + "sec": 2.62 + }, + "andrea|Il palco dell'oratorio. D'estate ci fanno le recite, la tombola e i concerti.": { + "file": "andrea-058d10ea48.mp3", + "sec": 7.18 + }, + "andrea|Adesso è vuoto. Peccato, avevo un numero di giocoleria pronto.": { + "file": "andrea-17666fc335.mp3", + "sec": 4.88 + }, + "andrea|Salire sul palco senza pubblico? Mi vergogno pure da solo.": { + "file": "andrea-d0420ca9a9.mp3", + "sec": 4.19 + }, + "andrea|La tettoia, dove si mettono le cucine quando c'è la festa.": { + "file": "andrea-42760fbe6c.mp3", + "sec": 3.95 + }, + "andrea|Le tavolate della festa. Fra qualche giorno qui si mangia per tre ore di fila.": { + "file": "andrea-c6f58d3ba7.mp3", + "sec": 5.84 + }, + "andrea|Mi siedo e aspetto che arrivi la pastasciutta? Ci vorrebbe una settimana.": { + "file": "andrea-f798fa755d.mp3", + "sec": 4.9 + }, + "andrea|Un libro dimenticato sulle tavolate. Sulla copertina c'è scritto «La storia infinita».": { + "file": "andrea-42e270af3d.mp3", + "sec": 6.21 + }, + "andrea|Me lo prendo. Se è suo, il proprietario lo ritrova... prima o poi.": { + "file": "andrea-78a0a84b84.mp3", + "sec": 5.95 + }, + "andrea|Il cancello di dietro.": { + "file": "andrea-a2f0124f4d.mp3", + "sec": 1.41 + }, + "andrea|Chiuso. Si apre solo quando c'è la festa.": { + "file": "andrea-5e1cd26d46.mp3", + "sec": 2.87 + }, + "andrea|Un portone antico con l'inferriata. Chissà cosa c'è dietro: una cantina? Un tesoro? La cantina di un tesoro?": { + "file": "andrea-1fda2a0f12.mp3", + "sec": 9.11 + }, + "andrea|Chiuso. Come tutte le porte interessanti di questo paese.": { + "file": "andrea-4d6871c326.mp3", + "sec": 4.11 + }, + "andrea|L'ingresso del campeggio. A giudicare dalle bandiere, ci dorme mezzo mondo.": { + "file": "andrea-72dede01c0.mp3", + "sec": 5.13 + }, + "andrea|Dentro non conosco nessuno. E il Regolo, che io sappia, non va in campeggio.": { + "file": "andrea-afa6ed50f6.mp3", + "sec": 5.96 + }, + "andrea|Una fila di bandierine di tutti i paesi. Il Palio della Fortezza però è roba nostra.": { + "file": "andrea-c621bca741.mp3", + "sec": 5.68 + }, + "andrea|Senza scala ci arrivo solo con la fantasia.": { + "file": "andrea-43d3435d61.mp3", + "sec": 2.99 + }, + "andrea|Il convento del Bosco ai Frati, in mezzo al bosco. Qui si viene a cercare il fresco e il silenzio.": { + "file": "andrea-486c980f52.mp3", + "sec": 7.48 + }, + "andrea|Meglio non disturbare: i frati sono gli unici in tutto il Mugello che non parlano del Palio.": { + "file": "andrea-8822c9e02f.mp3", + "sec": 5.55 + }, + "andrea|Una porta ad arco col cancelletto di ferro.": { + "file": "andrea-1e8c868d6d.mp3", + "sec": 2.83 + }, + "andrea|Chiusa. A quest'ora i frati pregano. O pranzano. O tutte e due.": { + "file": "andrea-78b87350bf.mp3", + "sec": 6.8 + }, + "andrea|Un tiglio enorme. Profuma che sembra una pasticceria.": { + "file": "andrea-9c9ee9e896.mp3", + "sec": 4.22 + }, + "andrea|Arrampicarmi? Ho i pantaloni buoni.": { + "file": "andrea-8d18461141.mp3", + "sec": 3.5 + }, + "andrea|Villa Adami. Gialla come un tuorlo, con le persiane verdi.": { + "file": "andrea-225e29ed43.mp3", + "sec": 5.12 + }, + "andrea|Mica è casa mia. E poi suonare e scappare non si fa più dalla quinta elementare.": { + "file": "andrea-35430efef3.mp3", + "sec": 6.34 + }, + "andrea|Un leone di terracotta che fa la guardia alla salita. Di serpenti, dice, non ne ha visti passare.": { + "file": "andrea-59a60dc5a7.mp3", + "sec": 6.83 + }, + "andrea|Ciao leone. Visto passare un serpente con le ali?": { + "file": "andrea-a69c700102.mp3", + "sec": 3.53 + }, + "leone|Io fo la guardia alla salita, mica ai serpenti.": { + "file": "leone-dd37833594.mp3", + "sec": 3.63 + }, + "leone|E poi di notte dormo. Come tutti, in questo paese.": { + "file": "leone-0eb6b5702a.mp3", + "sec": 5.04 + }, + "andrea|Un leone di terracotta che parla. Devo dormire di più anch'io.": { + "file": "andrea-e5cf871a9d.mp3", + "sec": 4.43 + }, + "andrea|Pesa più di me. E poi ha un'aria permalosa.": { + "file": "andrea-468fe91000.mp3", + "sec": 3.28 + }, + "andrea|A scuola? A maggio? Nemmeno per sogno.": { + "file": "andrea-72e4ebb412.mp3", + "sec": 3.88 + }, + "andrea|Di là si va alla scuola. Ci passo già abbastanza tempo durante l'anno.": { + "file": "andrea-b3acca1e65.mp3", + "sec": 5.34 + }, + "andrea|I cipressi in fila, dritti come soldatini. Da piccolo li contavo. Non sono mai arrivato in fondo.": { + "file": "andrea-bd1ea0e1d2.mp3", + "sec": 7.77 + }, + "andrea|Un cipresso in tasca? Neanche nei sogni.": { + "file": "andrea-241b0de20a.mp3", + "sec": 3.04 + }, + "andrea|Il Trebbio. Quassù anche i pensieri vanno più piano.": { + "file": "andrea-a1dab4073c.mp3", + "sec": 3.82 + }, + "andrea|La torre del Trebbio. Da ragazzo ci si rifugiò Amerigo Vespucci, per scappare dalla peste di Firenze.": { + "file": "andrea-14cbceece4.mp3", + "sec": 6.99 + }, + "andrea|Poi ha trovato l'America. Anche lui partendo da un posto tranquillo.": { + "file": "andrea-b8c4089c30.mp3", + "sec": 4.91 + }, + "andrea|È un castello, mica una scatola di biscotti.": { + "file": "andrea-5fec22ccd5.mp3", + "sec": 3.15 + }, + "andrea|Il pergolato, coi suoi pilastri. Un giorno li conto tutti.": { + "file": "andrea-658f69bfec.mp3", + "sec": 4.35 + }, + "andrea|Una vite? Ci vuole la vendemmia, non un ragazzo.": { + "file": "andrea-fe5b32c079.mp3", + "sec": 4.13 + }, + "andrea|La scala che sale al pergolato.": { + "file": "andrea-e8fba1f859.mp3", + "sec": 2.27 + }, + "andrea|Più tardi salgo. Per ora mi godo il silenzio.": { + "file": "andrea-82a210b2d9.mp3", + "sec": 3.45 + }, + "andrea|Una squama verde, grande come una moneta da due euro. Puzza di cantina.": { + "file": "andrea-7f1ad6889b.mp3", + "sec": 6.3 + }, + "andrea|Potrei usarla come plettro. Ma prima scopro di chi è.": { + "file": "andrea-fc32e07a5a.mp3", + "sec": 3.74 + }, + "andrea|«La storia infinita». Bello spesso: questo mi tiene compagnia fino a settembre.": { + "file": "andrea-23e40a2495.mp3", + "sec": 5.74 + }, + "andrea|Vediamo da dove comincia...": { + "file": "andrea-239bf733a6.mp3", + "sec": 1.26 + }, + "andrea|C'è una pagina sola. Con scritto: «FINE».": { + "file": "andrea-1d80123a55.mp3", + "sec": 3.93 + }, + "andrea|Infinita, dicevano.": { + "file": "andrea-77f1348d3a.mp3", + "sec": 2.13 + }, + "andrea|Leggere adesso? Prima il palio. E poi, a quanto pare, si fa presto.": { + "file": "andrea-3a6af3897f.mp3", + "sec": 6.52 + }, + "vecchietta|È stato il Regolo! Quello che cadde dal cielo quando scavavano le fondamenta.": { + "file": "vecchietta-e35443db85.mp3", + "sec": 5.13 + }, + "vecchietto|Macché cielo! Mangiava le bestie, e lo presero i soldati di Napoleone.": { + "file": "vecchietto-7f36dffaf1.mp3", + "sec": 4.69 + }, + "vecchietta|Napoleone? A San Piero? Ma se un c'è mai venuto!": { + "file": "vecchietta-2940205d67.mp3", + "sec": 4.82 + }, + "vecchietto|E il Regolo sì?": { + "file": "vecchietto-efb6583ee6.mp3", + "sec": 1.57 + }, + "vecchietto|Io dormivo. Alla mia età si dorme anche in piedi.": { + "file": "vecchietto-284dc88a6a.mp3", + "sec": 3.27 + }, + "vecchietta|Io stamani ho visto luccicare qualcosa sotto la panchina.": { + "file": "vecchietta-8ece5bc22a.mp3", + "sec": 3.51 + }, + "vecchietta|Ma con la mia schiena, chi ci si china?": { + "file": "vecchietta-a77813f2dc.mp3", + "sec": 2.12 + }, + "vecchietto|Senza il palio? Una festa così non s'è mai vista.": { + "file": "vecchietto-2236d755f0.mp3", + "sec": 3.59 + }, + "vecchietta|Nel mio, di Palio, si correva con le carriole vere. Mica come voi.": { + "file": "vecchietta-fe3c07afa2.mp3", + "sec": 5.11 + }, + "vecchietta|Salutaci i tuoi!": { + "file": "vecchietta-814e558af5.mp3", + "sec": 1.47 + }, + "castello|Siediti. Guarda il Mugello. Respira.": { + "file": "castello-72e22c6b59.mp3", + "sec": 4.92 + }, + "castello|Chi sa tutto del paese di solito sta seduto. Prova a chiedere a chi passa le giornate in piazza.": { + "file": "castello-2cdba66b66.mp3", + "sec": 7.97 + }, + "castello|Le cose che luccicano non stanno mai in alto. Guarda in basso, vicino a chi te ne ha parlato.": { + "file": "castello-4f3721ca46.mp3", + "sec": 7.44 + }, + "castello|Una squama da sola non dice niente. Ma vicino alle sue impronte, sotto la Fortezza, magari sì.": { + "file": "castello-888f24e1fd.mp3", + "sec": 8.81 + }, + "castello|Io ho visto passare i Medici, la peste e Vespucci. Un serpente in più non mi spaventa.": { + "file": "castello-a1dfd76497.mp3", + "sec": 6.51 + }, + "castello|Quella qui non manca mai. Torna quando vuoi.": { + "file": "castello-dc8a873d86.mp3", + "sec": 4.36 + } +} diff --git a/assets/voices/leone-0eb6b5702a.mp3 b/assets/voices/leone-0eb6b5702a.mp3 new file mode 100644 index 0000000..9fd5145 Binary files /dev/null and b/assets/voices/leone-0eb6b5702a.mp3 differ diff --git a/assets/voices/leone-dd37833594.mp3 b/assets/voices/leone-dd37833594.mp3 new file mode 100644 index 0000000..4f62148 Binary files /dev/null and b/assets/voices/leone-dd37833594.mp3 differ diff --git a/assets/voices/vecchietta-2940205d67.mp3 b/assets/voices/vecchietta-2940205d67.mp3 new file mode 100644 index 0000000..0b68de0 Binary files /dev/null and b/assets/voices/vecchietta-2940205d67.mp3 differ diff --git a/assets/voices/vecchietta-3c84f4b1cb.mp3 b/assets/voices/vecchietta-3c84f4b1cb.mp3 new file mode 100644 index 0000000..e6a2ad9 Binary files /dev/null and b/assets/voices/vecchietta-3c84f4b1cb.mp3 differ diff --git a/assets/voices/vecchietta-814e558af5.mp3 b/assets/voices/vecchietta-814e558af5.mp3 new file mode 100644 index 0000000..b3a1a37 Binary files /dev/null and b/assets/voices/vecchietta-814e558af5.mp3 differ diff --git a/assets/voices/vecchietta-8ece5bc22a.mp3 b/assets/voices/vecchietta-8ece5bc22a.mp3 new file mode 100644 index 0000000..064e07b Binary files /dev/null and b/assets/voices/vecchietta-8ece5bc22a.mp3 differ diff --git a/assets/voices/vecchietta-a77813f2dc.mp3 b/assets/voices/vecchietta-a77813f2dc.mp3 new file mode 100644 index 0000000..7d172ea Binary files /dev/null and b/assets/voices/vecchietta-a77813f2dc.mp3 differ diff --git a/assets/voices/vecchietta-e35443db85.mp3 b/assets/voices/vecchietta-e35443db85.mp3 new file mode 100644 index 0000000..b506cd9 Binary files /dev/null and b/assets/voices/vecchietta-e35443db85.mp3 differ diff --git a/assets/voices/vecchietta-fe3c07afa2.mp3 b/assets/voices/vecchietta-fe3c07afa2.mp3 new file mode 100644 index 0000000..e3b369b Binary files /dev/null and b/assets/voices/vecchietta-fe3c07afa2.mp3 differ diff --git a/assets/voices/vecchietto-2236d755f0.mp3 b/assets/voices/vecchietto-2236d755f0.mp3 new file mode 100644 index 0000000..7b97dee Binary files /dev/null and b/assets/voices/vecchietto-2236d755f0.mp3 differ diff --git a/assets/voices/vecchietto-284dc88a6a.mp3 b/assets/voices/vecchietto-284dc88a6a.mp3 new file mode 100644 index 0000000..78f618b Binary files /dev/null and b/assets/voices/vecchietto-284dc88a6a.mp3 differ diff --git a/assets/voices/vecchietto-5acd932b36.mp3 b/assets/voices/vecchietto-5acd932b36.mp3 new file mode 100644 index 0000000..353c8aa Binary files /dev/null and b/assets/voices/vecchietto-5acd932b36.mp3 differ diff --git a/assets/voices/vecchietto-7f36dffaf1.mp3 b/assets/voices/vecchietto-7f36dffaf1.mp3 new file mode 100644 index 0000000..c0ed478 Binary files /dev/null and b/assets/voices/vecchietto-7f36dffaf1.mp3 differ diff --git a/assets/voices/vecchietto-efb6583ee6.mp3 b/assets/voices/vecchietto-efb6583ee6.mp3 new file mode 100644 index 0000000..671a681 Binary files /dev/null and b/assets/voices/vecchietto-efb6583ee6.mp3 differ diff --git a/docs/STATO.md b/docs/STATO.md index caf9cd7..f3504ec 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -21,6 +21,11 @@ Ultimo aggiornamento: 23 ottobre 2026. Da leggere insieme a `PROGETTO.md` all'in - **Storia di prova** (`src/story/story.js`): il palio è sparito, i vecchietti litigano sulle due leggende, la squama sotto la panchina, le impronte sotto la Fortezza → fine del prototipo. Il Trebbio fa da "psicologo": parlando con la torre si riceve un indizio in base ai progressi. +- **Cartello d'apertura** «Manzolo (Poca) Art Presents» (5 s, si salta) prima del titolo. +- **Voci** (`make voci`, XTTS-v2 sulla GeForce): 112 battute, cinque voci (Andrea, vecchietta, vecchietto, castello, + leone); il fumetto dura quanto la voce, la musica si abbassa mentre si parla, il muto spegne anche le voci. + Controllate tutte con Whisper il 2026-10-23. Il leone ora parla (battute provvisorie). +- **Il libro della storia infinita** sulle tavolate dell'oratorio; le idee in attesa in `docs/IDEE.md`. - **Test**: `make test` (29 test, compresa la soluzione completa da `src/story/solution.js`) e `make browser` (partita completa con clic veri in Chromium, ripresa del salvataggio, telefono). diff --git a/src/engine/game.js b/src/engine/game.js index d23a04c..cd9de47 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -47,8 +47,10 @@ export function restoreState(story, data) { } export class Game { - constructor(story, { state, onSave } = {}) { + // voiceTime(who, text): secondi della voce registrata per la battuta, se c'è (il fumetto non finisce prima). + constructor(story, { state, onSave, voiceTime } = {}) { this.story = story; + this.voiceTime = voiceTime ?? (() => undefined); this.state = state ? restoreState(story, state) : newState(story); this.onSave = onSave ?? (() => {}); this.queue = []; @@ -189,6 +191,7 @@ export class Game { speakerPoint(who) { if (!who || who === 'andrea') return [this.state.x, this.state.y - 62 * this.scaleAt(this.state.y)]; + if (this.scene.speakers?.[who]) return this.scene.speakers[who]; const h = this.hotspot(who) ?? (this.scene.hotspots ?? []).find(x => x.id === who); if (!h) return [this.state.x, this.state.y - 62]; return h.talkAt ?? [h.box[0] + h.box[2] / 2, h.box[1] - 4]; @@ -232,7 +235,10 @@ export class Game { run(e) { const s = this.state; - if (e.say != null) this.speech = { text: e.say, who: e.who ?? 'andrea', pose: e.pose, left: sayTime(e.say) }; + if (e.say != null) { + const who = e.who ?? 'andrea', voice = this.voiceTime(who, e.say); + this.speech = { text: e.say, who, pose: e.pose, left: Math.max(sayTime(e.say), voice ? voice + 0.35 : 0) }; + } else if (e.walk) { this.task = e; this.startWalk(e.walk); } // `go` prima di `face`: un cambio scena porta con sé il verso d'arrivo. else if (e.go) this.go(e.go, e.at, e.face); diff --git a/src/engine/music.js b/src/engine/music.js index 4331946..9d02d58 100644 --- a/src/engine/music.js +++ b/src/engine/music.js @@ -20,6 +20,7 @@ export class Music { this.want = null; this.current = null; // { src, audio } this.old = []; + this.duck = 1; // 1 = pieno; meno di 1 mentre parla qualcuno } // Chiamata a ogni fotogramma con il brano voluto: avvia, cambia e sfuma. @@ -30,7 +31,7 @@ export class Music { this.current = src ? { src, audio: this.start(src) } : null; } // Il brano in corso sale (o scende) verso il volume scelto; quelli vecchi sfumano fino a zero. - const step = dt / FADE, target = this.muted ? 0 : this.volume; + const step = dt / FADE, target = this.muted ? 0 : this.volume * this.duck; if (this.current) { const a = this.current.audio; a.volume = a.volume < target ? Math.min(target, a.volume + step) : Math.max(target, a.volume - step); diff --git a/src/engine/voices.js b/src/engine/voices.js new file mode 100644 index 0000000..a7b379d --- /dev/null +++ b/src/engine/voices.js @@ -0,0 +1,49 @@ +// 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. + +export const voiceKey = (who, text) => `${who}|${text}`; + +// Tutte le battute della storia, ovunque stiano (scene, oggetti, dialoghi, rami `if`), più le righe dell'introduzione +// che dice Andrea. Una battuta è un oggetto con `say`; senza `who` parla Andrea. +export function voiceLines(story) { + const seen = new Map(); + const add = (who, text) => { if (text.trim() && !/^[.…!?\s]+$/.test(text)) seen.set(voiceKey(who, text), { who, text }); }; + const visit = value => { + if (Array.isArray(value)) return value.forEach(visit); + if (!value || typeof value !== 'object') return; + if (typeof value.say === 'string') add(value.who ?? 'andrea', value.say); + Object.values(value).forEach(visit); + }; + for (const line of story.intro ?? []) add('andrea', line); + visit(story.scenes); + visit(story.items); + visit(story.dialogues); + return [...seen.values()]; +} + +export class Voices { + constructor(index = {}, base = 'assets/voices/') { + this.index = index; + this.base = base; + this.audio = null; + } + + seconds(who, text) { return this.index[voiceKey(who, text)]?.sec; } + + play(who, text, volume = 1) { + this.stop(); + const entry = this.index[voiceKey(who, text)]; + if (!entry || volume <= 0) return false; + this.audio = new Audio(this.base + entry.file); + this.audio.volume = Math.min(1, volume); + this.audio.play().catch(() => {}); + return true; + } + + stop() { + if (this.audio) this.audio.pause(); + this.audio = null; + } + + get playing() { return !!this.audio && !this.audio.paused && !this.audio.ended; } +} diff --git a/src/main.js b/src/main.js index 8a150c4..d104a86 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import { Command } from './engine/command.js'; import { render, loadAssets, WIDTH, HEIGHT } from './engine/render.js'; import { readSave, writeSave, clearSave, readAudio, writeAudio } from './engine/storage.js'; import { Music, trackFor } from './engine/music.js'; +import { Voices } from './engine/voices.js'; const $ = id => document.getElementById(id); const canvas = $('scene'), ctx = canvas.getContext('2d'); @@ -28,8 +29,15 @@ $('volume').addEventListener('input', e => { music.setVolume(e.target.value / 10 showSound(); window.music = music; // per i test nel browser +// --- Voci: l'indice arriva con fetch; finché non c'è (o se manca) le battute restano mute. Seguono il muto della musica. +const voices = new Voices(); +fetch('assets/voices/index.json').then(r => r.ok ? r.json() : {}).then(index => { voices.index = index; }).catch(() => {}); +window.voices = voices; +let voiced = null; +const say = (who, text) => voices.play(who, text, music.muted ? 0 : 1); + function start(state) { - game = new Game(story, { state, onSave: data => storage && writeSave(storage, data) }); + game = new Game(story, { state, onSave: data => storage && writeSave(storage, data), voiceTime: (who, text) => voices.seconds(who, text) }); command = new Command(game); if (!state) game.go(story.start.scene, story.start.at); $('title').hidden = true; @@ -49,9 +57,10 @@ function showIntro() { } function nextIntro() { introLine++; - if (introLine >= story.intro.length) { $('intro').hidden = true; return start(null); } + if (introLine >= story.intro.length) { voices.stop(); $('intro').hidden = true; return start(null); } const p = $('intro-text'); p.textContent = story.intro[introLine]; + say('andrea', story.intro[introLine]); p.style.animation = 'none'; void p.offsetWidth; // riavvia la comparsa p.style.animation = ''; @@ -162,6 +171,11 @@ window.addEventListener('keydown', e => { // --- Fumetto sopra chi parla, tenuto dentro i bordi della scena. function updateSpeech() { const box = $('speech'), sp = game.speech; + // Una battuta nuova fa partire la sua voce; saltata o finita, la voce si ferma. + if (sp !== voiced) { + voiced = sp; + if (sp) say(sp.who, sp.text); else voices.stop(); + } if (!sp) { if (box.textContent) box.textContent = ''; return; } const [x, y] = game.speakerPoint(sp.who); box.textContent = sp.text; @@ -192,6 +206,7 @@ function frame(now) { const dt = Math.max(0, Math.min(0.1, (now - last) / 1000)); last = now; const screen = !$('title').hidden || !$('splash').hidden ? 'title' : !$('intro').hidden ? 'intro' : !$('ending').hidden ? 'ending' : 'game'; + music.duck = voices.playing ? 0.45 : 1; music.update(trackFor(story, { screen, scene: game?.state.scene }), dt); if (game) { if (running) game.update(dt); diff --git a/src/story/story.js b/src/story/story.js index 59adbb4..b5a8360 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -19,6 +19,7 @@ export const story = { vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' }, vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' }, castello: { name: 'Il Castello', color: '#c8f0a0' }, + leone: { name: 'Il leone', color: '#f0a070' }, }, items: { squama: { @@ -384,12 +385,13 @@ export const story = { walk: [[0, 318], [100, 312], [282, 318], [300, 300], [318, 262], [336, 252], [420, 266], [640, 296], [640, 360], [0, 360]], depth: [[252, 0.5], [360, 1.1]], entry: [340, 352], + speakers: { leone: [429, 62] }, hotspots: [ { id: 'leone', name: 'leone di terracotta', box: [412, 66, 34, 30], at: [400, 290], face: 'up', actions: { look: [{ say: 'Un leone di terracotta che fa la guardia alla salita. Di serpenti, dice, non ne ha visti passare.' }], - talk: [{ say: 'Ciao leone. Visto passare un serpente con le ali?' }, { say: 'Il leone fa finta di niente. Come tutti, in questo paese.' }], + talk: [{ say: 'Ciao leone. Visto passare un serpente con le ali?' }, { say: 'Io fo la guardia alla salita, mica ai serpenti.', who: 'leone' }, { say: 'E poi di notte dormo. Come tutti, in questo paese.', who: 'leone' }, { say: 'Un leone di terracotta che parla. Devo dormire di più anch\'io.' }], take: [{ say: 'Pesa più di me. E poi ha un\'aria permalosa.' }], }, }, @@ -438,6 +440,7 @@ export const story = { walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]], depth: [[306, 0.75], [360, 1.05]], entry: [110, 345], + speakers: { castello: [389, 22] }, enter: [{ if: '!trebbio_visto', then: [{ set: 'trebbio_visto' }, { say: 'Il Trebbio. Quassù anche i pensieri vanno più piano.' }] }], hotspots: [ { diff --git a/tests/browser.mjs b/tests/browser.mjs index ea48b80..11829f3 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -59,6 +59,10 @@ try { for (let i = 0; i < 10 && await page.isVisible('#intro'); i++) await page.click('#intro', { position: { x: 40, y: 40 } }); assert.ok(await page.isHidden('#intro'), 'l\'introduzione non finisce'); await page.waitForFunction(() => game.speech); + // La prima battuta ha la sua voce, e sotto la voce la musica si abbassa. + await page.waitForFunction(() => voices.playing, null, { timeout: 5000 }); + assert.match(await page.evaluate(() => voices.audio.src), /assets\/voices\/andrea-[0-9a-f]+\.mp3$/); + await page.waitForFunction(() => music.duck === 0.45, null, { timeout: 2000 }); await page.screenshot({ path: `${OUT}/02-inizio.png` }); const fits = async p => p.evaluate(() => document.querySelector('#panel').getBoundingClientRect().bottom <= innerHeight); assert.ok(await fits(page), 'verbi e inventario fuori dallo schermo'); diff --git a/tests/music.test.mjs b/tests/music.test.mjs index e2bfecd..7dd12f5 100644 --- a/tests/music.test.mjs +++ b/tests/music.test.mjs @@ -28,3 +28,16 @@ test('i brani della storia esistono', () => { assert.ok(files.length); for (const f of files) assert.ok(existsSync(new URL('../' + f, import.meta.url)), `manca ${f}`); }); + +test('mentre parla qualcuno la musica si abbassa, poi risale', () => { + globalThis.Audio ??= class { constructor(src) { this.src = src; this.volume = 0; this.paused = true; } play() { this.paused = false; return Promise.resolve(); } pause() { this.paused = true; } }; + const m = new Music({ volume: 0.6 }); + for (let i = 0; i < 40; i++) m.update('g.mp3', 0.1); + assert.equal(m.current.audio.volume, 0.6); + m.duck = 0.45; + for (let i = 0; i < 40; i++) m.update('g.mp3', 0.1); + assert.ok(Math.abs(m.current.audio.volume - 0.27) < 1e-9, 'abbassata sotto la voce'); + m.duck = 1; + for (let i = 0; i < 40; i++) m.update('g.mp3', 0.1); + assert.equal(m.current.audio.volume, 0.6); +}); diff --git a/tests/voices.test.mjs b/tests/voices.test.mjs new file mode 100644 index 0000000..6c8786a --- /dev/null +++ b/tests/voices.test.mjs @@ -0,0 +1,65 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +import { story } from '../src/story/story.js'; +import { Game } from '../src/engine/game.js'; +import { voiceLines, voiceKey, Voices } from '../src/engine/voices.js'; +import { settle } from './helpers.mjs'; + +const index = JSON.parse(readFileSync(new URL('../assets/voices/index.json', import.meta.url), 'utf8')); +const voci = JSON.parse(readFileSync(new URL('../tools/voci.json', import.meta.url), 'utf8')); + +test('le battute si trovano ovunque: introduzione, scene, oggetti, dialoghi e rami if', () => { + const lines = voiceLines(story), has = (who, part) => lines.some(l => l.who === who && l.text.includes(part)); + assert.ok(has('andrea', 'Fine maggio 2007'), 'introduzione'); + assert.ok(has('andrea', 'Il palio è sparito!'), 'ingresso in una scena, dentro un if'); + assert.ok(has('vecchietta', 'cadde dal cielo'), 'dialogo, con chi parla'); + assert.ok(has('castello', 'vicino alle sue impronte'), 'ramo else annidato'); + assert.ok(has('andrea', 'pagina sola'), 'azione di un oggetto'); + assert.ok(!lines.some(l => /^[.…\s]+$/.test(l.text)), 'i silenzi («...») non hanno voce'); +}); + +test('ogni battuta ha la sua voce (sennò: make voci) e ogni personaggio una voce in tools/voci.json', () => { + for (const { who, text } of voiceLines(story)) { + assert.ok(voci[who]?.speaker, `manca la voce di ${who} in tools/voci.json`); + const entry = index[voiceKey(who, text)]; + assert.ok(entry, `senza voce: ${who}: ${text}`); + assert.ok(existsSync(new URL(`../assets/voices/${entry.file}`, import.meta.url)), `manca il file ${entry.file}`); + assert.ok(entry.sec > 0.3 && entry.sec < 30, `durata strana: ${entry.sec} s per «${text}»`); + } +}); + +test('il castello e il leone hanno voci diverse da tutti gli altri', () => { + for (const special of ['castello', 'leone']) + for (const [who, v] of Object.entries(voci)) + if (who !== special && !who.startsWith('_')) assert.notEqual(v.speaker, voci[special].speaker, `${special} e ${who}`); +}); + +test('il fumetto resta a schermo almeno quanto la voce', () => { + const g = new Game(story, { voiceTime: (who, text) => (text.startsWith('Il palio') ? 9 : undefined) }); + g.go(story.start.scene, story.start.at); + g.update(0.05); + assert.ok(g.speech.text.startsWith('Il palio')); + assert.ok(g.speech.left > 9, 'la voce lunga allunga il fumetto'); + settle(g); + const quiet = new Game(story); + quiet.go(story.start.scene, story.start.at); + quiet.update(0.05); + assert.ok(quiet.speech.left < 9, 'senza voce vale la durata di lettura'); +}); + +test('le voci si cercano per chi parla e testo; senza indice il gioco resta muto', () => { + const v = new Voices({ [voiceKey('leone', 'Grr')]: { file: 'leone-x.mp3', sec: 1.5 } }); + assert.equal(v.seconds('leone', 'Grr'), 1.5); + assert.equal(v.seconds('andrea', 'Grr'), undefined); + assert.equal(new Voices().play('andrea', 'Ciao'), false); + assert.equal(v.playing, false); +}); + +test('il leone e il castello parlano dalla loro posizione, non da quella di Andrea', () => { + const g = new Game(story); + g.go('salita_villa_adami', [340, 352]); + assert.deepEqual(g.speakerPoint('leone'), story.scenes.salita_villa_adami.speakers.leone); + g.go('trebbio', [110, 345]); + assert.deepEqual(g.speakerPoint('castello'), story.scenes.trebbio.speakers.castello); +}); diff --git a/tools/voci.json b/tools/voci.json new file mode 100644 index 0000000..4fa9f38 --- /dev/null +++ b/tools/voci.json @@ -0,0 +1,8 @@ +{ + "_nota": "Una voce XTTS-v2 per personaggio (make voci). speaker: uno dei 58 di XTTS; filtro: catena ffmpeg -af applicata dopo (altezza, velocità, eco). Cambiare una voce rigenera solo le sue battute.", + "andrea": { "speaker": "Andrew Chipper" }, + "vecchietta": { "speaker": "Tammy Grit" }, + "vecchietto": { "speaker": "Viktor Eka" }, + "castello": { "speaker": "Damien Black", "filtro": "asetrate=24000*0.9,aresample=24000,atempo=0.95,aecho=0.8:0.7:90|180:0.25|0.15" }, + "leone": { "speaker": "Baldur Sanjin", "filtro": "asetrate=24000*0.84,aresample=24000,atempo=1.08" } +} diff --git a/tools/voci.mjs b/tools/voci.mjs new file mode 100644 index 0000000..a7a754c --- /dev/null +++ b/tools/voci.mjs @@ -0,0 +1,68 @@ +// Le voci dei personaggi (make voci): ogni battuta della storia diventa un mp3 in assets/voices/, con la voce +// scelta per chi parla in tools/voci.json; assets/voices/index.json dice al gioco quale file e quanto dura. +// Il nome del file contiene un'impronta di testo e voce: si rigenera solo quello che è cambiato, e gli mp3 che +// nessuna battuta usa più si cancellano. +// node tools/voci.mjs [--dry] (--dry: elenca cosa genererebbe, senza toccare niente) +// node tools/voci.mjs --rifai 'Ci sei?' --rifai 'altro testo' (rifà le battute che contengono quel testo: +// XTTS a volte storpia una frase, e un altro tentativo di solito la azzecca) +// XTTS gira nel suo Python (XTTS_PY, TTS_HOME): di default quello di ~/Workspaces/qemu/qemu-iso-lab. +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { story } from '../src/story/story.js'; +import { voiceLines, voiceKey } from '../src/engine/voices.js'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const OUT = join(ROOT, 'assets', 'voices'); +const LAB = join(process.env.HOME, 'Workspaces/qemu/qemu-iso-lab/artifacts/tts'); +const XTTS_PY = process.env.XTTS_PY ?? join(LAB, 'xtts/bin/python'); +const TTS_HOME = process.env.TTS_HOME ?? join(LAB, 'models'); +const CACHE = join(process.env.XDG_CACHE_HOME ?? join(process.env.HOME, '.cache'), 'leggenda-regolo-voci'); +const dry = process.argv.includes('--dry'); +const redo = process.argv.flatMap((a, i, all) => a === '--rifai' && all[i + 1] ? [all[i + 1]] : []); +// Cambia quando cambia il modo di sintetizzare (tools/voci_xtts.py): rigenera tutto. +const SINTESI = 2; + +const config = JSON.parse(readFileSync(join(ROOT, 'tools', 'voci.json'), 'utf8')); +const lines = voiceLines(story); +const missing = [...new Set(lines.map(l => l.who))].filter(w => !config[w]); +if (missing.length) throw new Error(`manca la voce in tools/voci.json per: ${missing.join(', ')}`); + +const index = {}, todo = []; +for (const { who, text } of lines) { + const voice = config[who]; + const hash = createHash('sha1').update(JSON.stringify([SINTESI, who, text, voice])).digest('hex').slice(0, 10); + const file = `${who}-${hash}.mp3`; + index[voiceKey(who, text)] = { file }; + const again = redo.some(r => text.includes(r)); + if (again) { rmSync(join(OUT, file), { force: true }); rmSync(join(CACHE, `${who}-${hash}.wav`), { force: true }); } + if (again || !existsSync(join(OUT, file))) todo.push({ who, text, voice, file, wav: join(CACHE, `${who}-${hash}.wav`) }); +} +console.log(`${lines.length} battute, ${todo.length} da generare`); +if (dry) { for (const t of todo) console.log(` ${t.who}: ${t.text}`); process.exit(0); } + +mkdirSync(OUT, { recursive: true }); +mkdirSync(CACHE, { recursive: true }); +if (todo.length) { + const jobs = join(CACHE, 'lavori.json'); + writeFileSync(jobs, JSON.stringify(todo.map(t => ({ text: t.text, speaker: t.voice.speaker, out: t.wav })))); + execFileSync(XTTS_PY, ['-I', join(ROOT, 'tools', 'voci_xtts.py'), jobs], { stdio: 'inherit', cwd: CACHE, env: { ...process.env, TTS_HOME } }); + // Silenzio tagliato ai due capi, poi il filtro del personaggio; mp3 mono, leggero per il telefono. + const trim = 'silenceremove=start_periods=1:start_threshold=-45dB,areverse,silenceremove=start_periods=1:start_threshold=-45dB,areverse'; + for (const t of todo) { + const af = [trim, t.voice.filtro].filter(Boolean).join(','); + execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', t.wav, '-af', af, '-ac', '1', '-c:a', 'libmp3lame', '-b:a', '56k', join(OUT, t.file)]); + } +} +// La durata di ogni file: il fumetto resta a schermo almeno quanto la voce. +for (const entry of Object.values(index)) { + const sec = execFileSync('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', join(OUT, entry.file)], { encoding: 'utf8' }); + entry.sec = Math.round(Number(sec) * 100) / 100; +} +const used = new Set(Object.values(index).map(e => e.file)); +const orphans = readdirSync(OUT).filter(f => f.endsWith('.mp3') && !used.has(f)); +for (const f of orphans) rmSync(join(OUT, f)); +writeFileSync(join(OUT, 'index.json'), JSON.stringify(index, null, 1) + '\n'); +console.log(`-> assets/voices/index.json (${Object.keys(index).length} voci${orphans.length ? `, ${orphans.length} mp3 vecchi tolti` : ''})`); diff --git a/tools/voci_xtts.py b/tools/voci_xtts.py new file mode 100644 index 0000000..2b0654d --- /dev/null +++ b/tools/voci_xtts.py @@ -0,0 +1,39 @@ +"""Sintesi delle battute con XTTS-v2 (gira nel Python di XTTS, lo lancia tools/voci.mjs): + voci_xtts.py lavori.json +lavori.json: [{"text": ..., "speaker": ..., "out": "/assoluto/x.wav"}, ...]; un lavoro già fatto si salta. +In italiano XTTS legge i punti a fine frase («Buongiorno, punto») e inciampa sui caporali: ogni battuta si +divide in frasi, senza punti finali né caporali, e le frasi si uniscono con una pausa breve. +Il modello è sotto licenza Coqui Public Model License: solo uso non commerciale.""" +import json +import os +import re +import sys + +PAUSA = 0.22 # secondi fra una frase e l'altra +os.environ["COQUI_TOS_AGREED"] = "1" + + +def frasi(testo: str) -> list[str]: + testo = re.sub(r"[«»\"“”]", "", testo) + parti = re.split(r"(?<=[.!?…])\s+", testo.strip()) + pulite = [re.sub(r"(\.\.\.|…|\.)+$", "", p).strip() for p in parti] + return [p for p in pulite if re.search(r"\w", p)] + + +lavori = [j for j in json.load(open(sys.argv[1])) if not os.path.exists(j["out"])] +if lavori: + import torch + import torchaudio + from TTS.api import TTS + tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to("cuda") + sr = tts.synthesizer.output_sample_rate + for i, j in enumerate(lavori, 1): + pezzi = [] + for f in frasi(j["text"]): + if pezzi: + pezzi.append(torch.zeros(int(sr * PAUSA))) + pezzi.append(torch.tensor(tts.tts(f, language="it", speaker=j["speaker"], split_sentences=False), dtype=torch.float32)) + torchaudio.save(j["out"] + ".tmp.wav", torch.cat(pezzi).unsqueeze(0), sr, format="wav") + os.replace(j["out"] + ".tmp.wav", j["out"]) + print(f"{i}/{len(lavori)}", flush=True) +print(f"{len(lavori)} battute sintetizzate")