Tourismustal: Vorproduzierte Sprach-Audios statt Browser-TTS

52 MP3s (OpenAI gpt-4o-mini-tts, Stimme coral, warme deutsche
Erzaehlstimme) fuer alle Info-Karten: Erst-Bau-Erklaerungen, Meilensteine,
Events, Szenen-Intros, Auftraege, Megabauwerk. Texte automatisch aus
data.js gezogen (Generator im Scratchpad-Muster). Abspiel-Logik:
assets/audio-info/<id>.mp3, Browser-TTS nur Not-Fallback. Intro-IDs je
Szene eindeutig, Onboarding bleibt immer modal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 12:47:46 +02:00
parent 7e3158aede
commit 32793d0bca
55 changed files with 29 additions and 10 deletions
+25 -6
View File
@@ -146,7 +146,7 @@ export class UI {
this.infoMode = this.infoMode === 'audio' ? 'read' : 'audio';
localStorage.setItem('tt_infomode', this.infoMode);
this._syncInfoModeBtn(e.currentTarget);
this._speak(null);
this._stopVoice();
this.toast(this.infoMode === 'audio'
? '🔊 Audio-Modus: Meldungen werden vorgelesen, das Spiel läuft weiter. Entscheidungen pausieren weiterhin.'
: '📖 Lese-Modus: Meldungen öffnen sich als Karte, das Spiel pausiert.', 5000);
@@ -486,12 +486,31 @@ export class UI {
el.classList.remove('hidden');
el.onclick = () => {
el.classList.add('hidden');
this._speak(null); // Vorlesen stoppen
this._stopVoice();
this.showCard({ ...card, transient: true, forceModal: true });
};
clearTimeout(this._bannerTimer);
this._bannerTimer = setTimeout(() => el.classList.add('hidden'), 9000);
this._speak(`${card.title}. ${card.text}`);
this._speakCard(card);
}
// Vorproduzierte Sprach-Audios (assets/audio-info/<id>.mp3, OpenAI-TTS).
// Browser-TTS nur als Not-Fallback, falls eine Datei fehlt.
_stopVoice() {
if (this._voice) { this._voice.pause(); this._voice = null; }
if ('speechSynthesis' in window) speechSynthesis.cancel();
}
_speakCard(card) {
this._stopVoice();
const id = card.audio || card.id;
if (!id) { this._speak(`${card.title}. ${card.text}`); return; }
const a = new Audio(`assets/audio-info/${id}.mp3`);
a.volume = 0.95;
this._voice = a;
const fallback = () => { if (this._voice === a) this._speak(`${card.title}. ${card.text}`); };
a.onerror = fallback;
a.play().catch(fallback);
}
// Sprachausgabe (Browser-TTS, deutsche Stimme). null = stoppen.
@@ -686,7 +705,7 @@ export class UI {
// ---------- Onboarding ----------
showOnboarding() {
this.showCard({
icon: '🏔️', transient: true,
icon: '🏔️', transient: true, forceModal: true,
title: 'Willkommen! (1/3) So spielst du',
text: 'Rechts ein Gebäude wählen, dann auf die Karte tippen fertig. '
+ 'Nach dem Bau legst du das Werkzeug automatisch ab.\n\n'
@@ -696,7 +715,7 @@ export class UI {
+ 'Schritt für Schritt frei.',
});
this.showCard({
icon: '🚶', transient: true,
icon: '🚶', transient: true, forceModal: true,
title: 'Die Gäste (2/3) dein Kreislauf',
text: 'Jede Figur steht für etwa 10 Gäste. Sie kommen mit dem Bus, '
+ 'wandern, kehren ein und reisen wieder ab.\n\n'
@@ -707,7 +726,7 @@ export class UI {
+ 'Wer konsumiert, bringt Geld wer nicht kommen kann, keins.',
});
this.showCard({
icon: '🔍', transient: true,
icon: '🔍', transient: true, forceModal: true,
title: 'Durchblick (3/3) warum läuft es (nicht)?',
text: 'Links stehen deine Kennzahlen. Kommen weniger Gäste als möglich, '
+ 'erscheint dort der GRUND (z.B. „Engpass: Anreise").\n\n'