document.addEventListener("DOMContentLoaded", function() {
const sections = document.querySelectorAll("[id]");
const toc = document.getElementById("toc");
const voiceSelect = document.getElementById("voice-select");
const pitchSelect = document.getElementById("pitch-select");
const rateSelect = document.getElementById("rate-select");
const playBtn = document.getElementById("play");
const pauseBtn = document.getElementById("pause");
const stopBtn = document.getElementById("stop");
let activeSection = null;
let isSpokenPaused = false;
// NEU: Globale Variablen zur exakten Satz-Verfolgung
let globalSentences = [];
let currentSentenceIdx = 0;
// 1. STIMMEN LADEN
function populateVoiceList() {
if (!voiceSelect) return false;
const voices = window.speechSynthesis.getVoices();
if (voices.length === 0) return false;
voiceSelect.innerHTML = "";
voices.forEach(function(voice, index) {
const option = document.createElement("option");
option.textContent = `${voice.name} (${voice.lang})`;
option.value = index;
if (voice.lang.startsWith("de")) option.selected = true;
voiceSelect.appendChild(option);
});
return true;
}
const success = populateVoiceList();
if (!success) {
let attempts = 0;
const voiceInterval = setInterval(function() {
attempts++;
if (populateVoiceList() || attempts > 16) clearInterval(voiceInterval);
}, 250);
}
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = populateVoiceList;
}
// 2. TOC-LINKS ERSTELLEN
sections.forEach(function(section) {
const heading = section.querySelector("h1, h2, h3");
if (heading && toc) {
const link = document.createElement("a");
link.href = "#" + section.id;
link.textContent = heading.textContent;
link.addEventListener("click", function(e) {
e.preventDefault();
section.scrollIntoView({ behavior: "smooth" });
readSection(section);
});
toc.appendChild(link);
toc.appendChild(document.createElement("br"));
}
});
// Helper: Entfernt alle Markierungen und stellt das Original-HTML wieder her
function clearHighlight() {
document.querySelectorAll(".reading-highlight").forEach(el => {
el.classList.remove("reading-highlight");
});
if (activeSection && activeSection.dataset.originalHtml) {
activeSection.innerHTML = activeSection.dataset.originalHtml;
delete activeSection.dataset.originalHtml;
}
}
// 3. ERWEITERTE LOGIK: INTULATION UND DYNAMISCHE PAUSEN
function speakCurrentSentence() {
if (currentSentenceIdx >= globalSentences.length) {
clearHighlight();
activeSection = null;
isSpokenPaused = false;
return;
}
// 1. Visuelles Highlight verschieben
activeSection.querySelectorAll(".voice-sentence").forEach(el => {
el.classList.remove("reading-highlight");
});
const activeSpan = activeSection.querySelector(`.voice-sentence[data-index="${currentSentenceIdx}"]`);
if (activeSpan) {
activeSpan.classList.add("reading-highlight");
activeSpan.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
const sentenceObj = globalSentences[currentSentenceIdx];
const sentenceText = sentenceObj.text;
const utterance = new SpeechSynthesisUtterance(sentenceText);
// Einstellungen laden
const voices = window.speechSynthesis.getVoices();
if (voiceSelect && voices.length > 0) {
const selectedIndex = parseInt(voiceSelect.value, 10);
if (voices[selectedIndex]) {
utterance.voice = voices[selectedIndex];
utterance.lang = voices[selectedIndex].lang;
}
}
let userPitch = pitchSelect ? parseFloat(pitchSelect.value) : 1.0;
if (isNaN(userPitch)) userPitch = 1.0;
if (rateSelect) utterance.rate = parseFloat(rateSelect.value) || 1.0;
// --- DEUTSCHE INTONATIONSSIMULATION ---
const trimmedText = sentenceText.trim();
if (trimmedText.endsWith("?")) {
// Bei Fragen steigt die Stimme am Ende an (+15%)
utterance.pitch = userPitch * 1.15;
} else if (trimmedText.endsWith("!")) {
// Ausrufezeichen bleiben druckvoll/normal
utterance.pitch = userPitch;
} else {
// Normale Aussagesätze senken den Pitch leicht ab (-8%)
utterance.pitch = userPitch * 0.92;
}
// Wenn der Satz endet, berechne die Pause zum nächsten Satz
utterance.onend = function() {
if (!isSpokenPaused) {
currentSentenceIdx++;
// Bestimme die Pausenlänge: Wenn der aktuelle Satz ein Absatzende markiert,
// machen wir eine lange Pause (1500ms), ansonsten eine normale Satzpause (600ms).
const pauseDuration = sentenceObj.isParagraphEnd ? 1500 : 600;
// Verzögerter Start des nächsten Satzes für natürliches Atmen
setTimeout(function() {
// Erneute Prüfung, falls der User während der Pause auf 'Pause' gedrückt hat
if (!isSpokenPaused) {
speakCurrentSentence();
}
}, pauseDuration);
}
};
window.speechSynthesis.speak(utterance);
}
// 4. TEXTSPLITTER MIT ABSATZ-ERKENNUNG
function readSection(section) {
window.speechSynthesis.cancel();
clearHighlight();
activeSection = section;
isSpokenPaused = false;
currentSentenceIdx = 0;
if (!activeSection.dataset.originalHtml) {
activeSection.dataset.originalHtml = activeSection.innerHTML;
}
// Wir holen den Text, behalten aber Absätze über temporäre Platzhalter im Blick
// Ersetzt sichtbare Block-Elemente und
durch ein eindeutiges Umbruchzeichen
let workingHtml = section.innerHTML
.replace(/
/gi, "\n")
.replace(/<\/p>/gi, "