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, "

\n") .replace(/<\/h[1-6]>/gi, "<\/h[1-6]>\n"); let tempDiv = document.createElement("div"); tempDiv.innerHTML = workingHtml; const text = tempDiv.innerText || tempDiv.textContent; if (!text.trim()) return; // Text zeilenweise/absatzweise aufteilen const paragraphs = text.split(/\n+/); globalSentences = []; let spanHtml = ""; let globalIdx = 0; paragraphs.forEach((para) => { if (!para.trim()) return; // Sätze innerhalb des Absatzes isolieren const sentences = para.match(/[^.!?]+[.!?]+(\s|$)/g) || [para]; sentences.forEach((sentence, sIdx) => { if (!sentence.trim()) return; // Prüfen, ob dies der letzte Satz des Absatzes ist const isParagraphEnd = (sIdx === sentences.length - 1); globalSentences.push({ text: sentence, isParagraphEnd: isParagraphEnd }); spanHtml += `${sentence}`; globalIdx++; }); // Fügt nach jedem verarbeiteten Absatz im sichtbaren HTML wieder einen Umbruch ein spanHtml += "

"; }); section.innerHTML = spanHtml; // Starten speakCurrentSentence(); } // 5. INFRASTRUKTUR BUTTON-STEUERUNG if (playBtn) { playBtn.addEventListener("click", function() { if (window.speechSynthesis.paused || isSpokenPaused) { isSpokenPaused = false; window.speechSynthesis.resume(); // Absicherung für Browser, bei denen resume() hakt: if (!window.speechSynthesis.speaking) { speakCurrentSentence(); } } else if (!window.speechSynthesis.speaking && activeSection) { speakCurrentSentence(); } }); } if (pauseBtn) { pauseBtn.addEventListener("click", function() { if (window.speechSynthesis.speaking && !window.speechSynthesis.paused) { window.speechSynthesis.pause(); isSpokenPaused = true; } }); } if (stopBtn) { stopBtn.addEventListener("click", function() { isSpokenPaused = false; window.speechSynthesis.cancel(); clearHighlight(); activeSection = null; globalSentences = []; currentSentenceIdx = 0; }); } // 6. LIVE-ANZEIGE FÜR DIE SCHIEBEREGLER if (pitchSelect && document.getElementById("pitch-val")) { pitchSelect.addEventListener("input", function() { document.getElementById("pitch-val").textContent = parseFloat(this.value).toFixed(1); }); } if (rateSelect && document.getElementById("rate-val")) { rateSelect.addEventListener("input", function() { document.getElementById("rate-val").textContent = parseFloat(this.value).toFixed(1) + "x"; }); } });