Voci dei personaggi con XTTS: una per battuta, castello e leone con voci proprie
tools/voci.mjs (make voci) genera un mp3 per ogni battuta con la voce del personaggio scelta in tools/voci.json, solo per i testi nuovi o cambiati; tools/voci_xtts.py divide le frasi e toglie punti finali e caporali, che XTTS in italiano leggeva («Buongiorno, punto»). Nel gioco il fumetto dura quanto la voce, la musica si abbassa mentre si parla, il muto spegne anche le voci. Il leone ora parla e i personaggi parlano dalla posizione che la scena dichiara (speakers), che prima era ignorata. Tutte le voci verificate con Whisper; test: ogni battuta ha la sua voce, il fumetto non finisce prima della voce, la musica si abbassa (provati con errori iniettati). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UdeKd62opy9PKgpQXtVctF
This commit is contained in:
+17
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user