Sprache

Von $1

    Inhaltsverzeichnis
    keine Gliederung

    Version seit 18:55, 11 Aug 2026

    zu dieser Version.

    Zurück zu Versionshistorie.

    Zeige aktuelle Version

    ○ Text→Sprache ○ Sprache→Sprache ○ Sprache→Text ☎ Helikon anrufen ● Wiki-Suche  
    ● Aufnehmen 0s  
       
    (function() {
    
        /* === INJECT CSS === */
        var css = ''
        + '#helikonTTS { max-width:700px; margin:0; font-family:"Courier New",Courier,monospace; }'
        + '#ttsControls { display:flex; align-items:center; gap:8px; padding:4px 0; flex-wrap:wrap; }'
        + '.voice-btn { background:#fff; border:2px solid #0077aa; color:#0077aa; padding:8px 14px; cursor:pointer; font-family:inherit; font-size:12px; display:inline-block; border-radius:4px; }'
        + '.voice-btn:hover { background:#0077aa; color:#fff; }'
        + '.voice-btn.active { background:#0077aa; color:#fff; }'
        + '.voice-btn.disabled { background:#eee; border-color:#ccc; color:#aaa; cursor:default; }'
        + '.voice-btn.disabled:hover { background:#eee; color:#aaa; }'
        + '#callToggle { border-color:#3a8f3a; color:#3a8f3a; }'
        + '#callToggle:hover { background:#3a8f3a; color:#fff; }'
        + '#callToggle.active { background:#3a8f3a; color:#fff; box-shadow:0 0 12px rgba(58,143,58,0.5); animation:callGlow 2s infinite; }'
        + '@keyframes callGlow { 0%,100% { box-shadow:0 0 8px rgba(58,143,58,0.4); } 50% { box-shadow:0 0 20px rgba(58,143,58,0.8); } }'
        + '#searchToggle { border-color:#cc8800; color:#cc8800; }'
        + '#searchToggle:hover { background:#cc8800; color:#fff; }'
        + '#searchToggle.active { background:#cc8800; color:#fff; }'
        + '#ttsStatus { color:#999; font-size:11px; margin-left:4px; }'
        + '#voiceRecBar { display:none; align-items:center; gap:10px; padding:6px 0; margin-top:4px; }'
        + '#voiceRecBar.visible { display:flex; }'
        + '#voiceRecBtn { background:#fff; border:2px solid #cc3333; color:#cc3333; padding:8px 14px; cursor:pointer; font-family:inherit; font-size:12px; display:inline-block; border-radius:4px; }'
        + '#voiceRecBtn:hover { background:#cc3333; color:#fff; }'
        + '#voiceRecBtn.recording { background:#cc3333; color:#fff; animation:recPulse 1s infinite; }'
        + '@keyframes recPulse { 0%,100% { opacity:1; } 50% { opacity:0.6; } }'
        + '#voiceRecTime { color:#cc3333; font-size:12px; min-width:30px; }'
        + '#voiceRecStatus { color:#999; font-size:11px; }'
        + '#callBar { display:none; align-items:center; gap:10px; padding:6px 0; margin-top:4px; }'
        + '#callBar.visible { display:flex; }'
        + '#callIndicator { width:10px; height:10px; border-radius:50%; background:#999; display:inline-block; }'
        + '#callIndicator.listening { background:#4CAF50; animation:listenPulse 1.5s infinite; }'
        + '#callIndicator.processing { background:#ff9800; animation:procPulse 0.5s infinite; }'
        + '#callIndicator.speaking { background:#0077aa; animation:speakPulse 0.8s infinite; }'
        + '#callIndicator.waiting { background:#999; }'
        + '@keyframes listenPulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:0.6; transform:scale(1.3); } }'
        + '@keyframes procPulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }'
        + '@keyframes speakPulse { 0%,100% { opacity:1; } 50% { opacity:0.7; } }'
        + '#callStatus { color:#666; font-size:12px; }'
        + '.tts-play-btn { background:#fff; border:1px solid #ccc; color:#999; padding:2px 8px; cursor:pointer; font-size:11px; margin-left:8px; font-family:"Courier New",Courier,monospace; display:none; }'
        + '.tts-play-btn:hover { border-color:#0077aa; color:#0077aa; }'
        + '.tts-play-btn.playing { border-color:#cc6600; color:#cc6600; display:inline-block !important; }'
        + '.tts-active .tts-play-btn { display:inline-block; }';
    
        var styleEl = document.createElement('style');
        styleEl.type = 'text/css';
        if (styleEl.styleSheet) {
            styleEl.styleSheet.cssText = css;
        } else {
            styleEl.appendChild(document.createTextNode(css));
        }
        document.getElementsByTagName('head')[0].appendChild(styleEl);
    
        /* === DOM === */
        var ttsBtn      = document.getElementById('ttsToggle');
        var stsBtn      = document.getElementById('stsToggle');
        var sttBtn      = document.getElementById('sttToggle');
        var callBtn     = document.getElementById('callToggle');
        var searchBtn   = document.getElementById('searchToggle');
        var ttsStatus   = document.getElementById('ttsStatus');
        var recBar      = document.getElementById('voiceRecBar');
        var recBtn      = document.getElementById('voiceRecBtn');
        var recTime     = document.getElementById('voiceRecTime');
        var recStatus   = document.getElementById('voiceRecStatus');
        var callBarEl   = document.getElementById('callBar');
        var callIndic   = document.getElementById('callIndicator');
        var callStatus  = document.getElementById('callStatus');
        if (!ttsBtn) return;
    
        /* Apply button classes */
        ttsBtn.className = 'voice-btn';
        stsBtn.className = 'voice-btn disabled';
        sttBtn.className = 'voice-btn';
        callBtn.className = 'voice-btn';
        searchBtn.className = 'voice-btn active';
    
        /* === STATE === */
        var ttsActive   = false;
        var stsActive   = false;
        var sttActive   = false;
        var callActive  = false;
        var searchActive = true; /* DEFAULT ON */
        var ttsApiKey   = '';
        var voiceId     = '';
        var modelId     = 'eleven_multilingual_v2';
        var currentAudio = null;
        var isSpeaking  = false;
        var isRecording = false;
        var mediaRecorder = null;
        var audioChunks = [];
        var recTimer    = null;
        var recSeconds  = 0;
    
        /* Call mode state */
        var callStream     = null;
        var callAnalyser   = null;
        var callAudioCtx   = null;
        var callSilenceTimer = null;
        var callPhase      = 'idle'; /* idle, listening, processing, speaking, waiting */
        var SILENCE_THRESHOLD = 15;  /* Volume level below which = silence */
        var SILENCE_DURATION  = 1800; /* ms of silence before auto-stop */
    
        /* === GET PAGE ID === */
        var pageId = 0;
        if (typeof Deki !== 'undefined' && Deki.PageId) {
            pageId = Deki.PageId;
        }
    
        /* === FETCH ALL PROPERTIES === */
        function fetchAllProperties(callback) {
            if (!pageId) { callback(new Object()); return; }
            var url = '/@api/deki/pages/' + pageId + '/properties';
            var xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.onload = function() {
                var props = new Object();
                if (xhr.status === 200) {
                    try {
                        var parser = new DOMParser();
                        var xml = parser.parseFromString(xhr.responseText, 'text/xml');
                        var entries = xml.getElementsByTagName('property');
                        for (var i = 0; i < entries.length; i++) {
                            var nameEl = entries[i].getAttribute('name');
                            if (!nameEl) continue;
                            var short = nameEl.replace('urn:custom.mindtouch.com#', '');
                            var contentsEl = entries[i].getElementsByTagName('contents');
                            if (contentsEl.length > 0) {
                                var href = contentsEl[0].getAttribute('href');
                                if (href) { props['_href_' + short] = href; }
                                var textNode = contentsEl[0].textContent;
                                if (textNode) { props[short] = textNode.replace(/^\s+|\s+$/g, ''); }
                            }
                        }
                    } catch(e) {}
                }
                var hrefKeys = [];
                for (var k in props) {
                    if (k.indexOf('_href_') === 0) { hrefKeys.push(k.replace('_href_', '')); }
                }
                if (hrefKeys.length === 0) { callback(props); return; }
                var done = 0;
                for (var h = 0; h < hrefKeys.length; h++) {
                    (function(propName) {
                        var xh = new XMLHttpRequest();
                        xh.open('GET', props['_href_' + propName], true);
                        xh.onload = function() {
                            if (xh.status === 200) { props[propName] = xh.responseText.replace(/^\s+|\s+$/g, ''); }
                            done++;
                            if (done === hrefKeys.length) callback(props);
                        };
                        xh.onerror = function() { done++; if (done === hrefKeys.length) callback(props); };
                        xh.send();
                    })(hrefKeys[h]);
                }
            };
            xhr.onerror = function() { callback(new Object()); };
            xhr.send();
        }
    
        /* === UI HELPERS === */
        function setStatus(msg) { if (ttsStatus) ttsStatus.textContent = msg; }
        function setRecStatus(msg) { if (recStatus) recStatus.textContent = msg; }
        function setCallStatus(msg) { if (callStatus) callStatus.textContent = msg; }
        function setCallPhase(phase) {
            callPhase = phase;
            if (callIndic) callIndic.className = phase;
            var labels = new Object();
            labels.idle = '';
            labels.listening = 'hoert zu...';
            labels.processing = 'verarbeite...';
            labels.speaking = 'Helikon spricht...';
            labels.waiting = 'wartet auf Antwort...';
            setCallStatus(labels[phase] || '');
        }
    
        function updateButtons() {
            ttsBtn.className = 'voice-btn' + (ttsActive ? ' active' : '');
            ttsBtn.innerHTML = (ttsActive ? '●' : '○') + ' Text→Sprache';
    
            stsBtn.className = 'voice-btn disabled';
            stsBtn.innerHTML = '○ Sprache→Sprache';
    
            sttBtn.className = 'voice-btn' + (sttActive ? ' active' : '');
            sttBtn.innerHTML = (sttActive ? '●' : '○') + ' Sprache→Text';
    
            callBtn.className = 'voice-btn' + (callActive ? ' active' : '');
            callBtn.innerHTML = callActive ? '☎ Auflegen' : '☎ Helikon anrufen';
    
            searchBtn.className = 'voice-btn' + (searchActive ? ' active' : '');
            searchBtn.innerHTML = (searchActive ? '●' : '○') + ' Wiki-Suche';
    
            /* Global flag for Search Add-on */
            window.helikonSearchEnabled = searchActive;
    
            /* Show/hide bars */
            if (sttActive) {
                recBar.className = 'visible';
                callBarEl.className = '';
            } else if (callActive) {
                recBar.className = '';
                callBarEl.className = 'visible';
            } else {
                recBar.className = '';
                callBarEl.className = '';
            }
    
            /* TTS play buttons */
            var chatLog = document.getElementById('chatLog');
            if (chatLog) {
                if (ttsActive || callActive) {
                    chatLog.className = chatLog.className.replace(' tts-active', '') + ' tts-active';
                } else {
                    chatLog.className = chatLog.className.replace(' tts-active', '');
                    stopAudio();
                }
            }
        }
    
        /* === ADD CHAT MESSAGE === */
        function addChatMsg(text, type) {
            var chatLog = document.getElementById('chatLog');
            if (!chatLog) return;
            var div = document.createElement('div');
            div.className = 'chatMsg ' + type;
            div.textContent = text;
            chatLog.appendChild(div);
            chatLog.scrollTop = chatLog.scrollHeight;
        }
    
        /* === CLEAN TEXT === */
        function cleanText(text) {
            var LT = String.fromCharCode(60);
            var GT = String.fromCharCode(62);
            var LBRACE = String.fromCharCode(123);
            var RBRACE = String.fromCharCode(125);
            var htmlTagRe = new RegExp(LT + '[^' + GT + ']*' + GT, 'g');
            var headingRe = new RegExp('#' + LBRACE + '1,6' + RBRACE + '\\s', 'g');
            var linkRe = new RegExp('\\[([^\\]]+)\\]\\([^)]+\\)', 'g');
            var clean = text
                .replace(htmlTagRe, '')
                .replace(/\*\*([^*]+)\*\*/g, '$1')
                .replace(/\*([^*]+)\*/g, '$1')
                .replace(/`([^`]+)`/g, '$1')
                .replace(headingRe, '')
                .replace(linkRe, '$1')
                .replace(/\s+/g, ' ')
                .replace(/^\s+|\s+$/g, '');
            if (clean.indexOf('>') === 0) { clean = clean.substring(1).replace(/^\s+/, ''); }
            return clean;
        }
    
        /* ============================================================
           TEXT TO SPEECH (TTS)
           ============================================================ */
        function speak(text, onDone) {
            if (!ttsApiKey || !voiceId) {
                setStatus('TTS nicht konfiguriert');
                if (onDone) onDone();
                return;
            }
            var clean = cleanText(text);
            if (!clean) { if (onDone) onDone(); return; }
            if (clean.length > 5000) { clean = clean.substring(0, 5000); }
    
            if (typeof console !== 'undefined') {
                console.log('TTS speak: "' + clean.substring(0, 60) + '..." (' + clean.length + ' chars)');
            }
    
            setStatus('spreche...');
            isSpeaking = true;
    
            var url = 'https://api.elevenlabs.io/v1/text-to-speech/' + encodeURIComponent(voiceId)
                    + '?output_format=mp3_44100_128';
            var bodyObj = new Object();
            bodyObj.text = clean;
            bodyObj.model_id = modelId;
    
            var xhr = new XMLHttpRequest();
            xhr.open('POST', url, true);
            xhr.responseType = 'blob';
            xhr.setRequestHeader('Content-Type', 'application/json');
            xhr.setRequestHeader('xi-api-key', ttsApiKey);
    
            xhr.onload = function() {
                if (xhr.status === 200) {
                    playBlob(xhr.response, onDone);
                } else {
                    isSpeaking = false;
                    setStatus('TTS-Fehler ' + xhr.status);
                    if (onDone) onDone();
                }
            };
            xhr.onerror = function() {
                isSpeaking = false;
                setStatus('TTS nicht erreichbar');
                if (onDone) onDone();
            };
            xhr.send(JSON.stringify(bodyObj));
        }
    
        /* === AUDIO PLAYBACK HELPER === */
        function playBlob(blob, onDone) {
            var audioUrl = URL.createObjectURL(blob);
            stopAudio();
            currentAudio = new Audio(audioUrl);
            currentAudio.onended = function() {
                isSpeaking = false;
                setStatus('bereit');
                URL.revokeObjectURL(audioUrl);
                currentAudio = null;
                if (onDone) onDone();
            };
            currentAudio.onerror = function() {
                isSpeaking = false;
                setStatus('Wiedergabefehler');
                URL.revokeObjectURL(audioUrl);
                currentAudio = null;
                if (onDone) onDone();
            };
            currentAudio.play().catch(function() {
                setStatus('Autoplay blockiert');
                isSpeaking = false;
                if (onDone) onDone();
            });
        }
    
        function stopAudio() {
            if (currentAudio) {
                currentAudio.pause();
                currentAudio.currentTime = 0;
                currentAudio = null;
            }
            isSpeaking = false;
        }
    
        /* ============================================================
           MICROPHONE RECORDING (for STT manual mode)
           ============================================================ */
        function startRecording(onComplete) {
            if (isRecording) return;
            if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
                setRecStatus('Mikrofon nicht verfuegbar');
                return;
            }
    
            audioChunks = [];
            recSeconds = 0;
            recTime.textContent = '0s';
    
            var constraints = new Object();
            constraints.audio = true;
            constraints.video = false;
    
            navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
                mediaRecorder = new MediaRecorder(stream);
                isRecording = true;
                recBtn.className = 'recording';
                recBtn.innerHTML = '■ Stopp';
                setRecStatus('aufnahme...');
    
                mediaRecorder.ondataavailable = function(e) {
                    if (e.data.size > 0) { audioChunks.push(e.data); }
                };
    
                mediaRecorder.onstop = function() {
                    isRecording = false;
                    recBtn.className = '';
                    recBtn.innerHTML = '● Aufnehmen';
                    clearInterval(recTimer);
                    recTimer = null;
                    var tracks = stream.getTracks();
                    for (var t = 0; t < tracks.length; t++) { tracks[t].stop(); }
                    if (audioChunks.length > 0) {
                        var blobType = new Object();
                        blobType.type = 'audio/webm';
                        var blob = new Blob(audioChunks, blobType);
                        if (onComplete) onComplete(blob);
                    }
                };
    
                mediaRecorder.start();
                recTimer = setInterval(function() {
                    recSeconds++;
                    recTime.textContent = recSeconds + 's';
                    if (recSeconds >= 30) { stopRecording(); }
                }, 1000);
    
            }).catch(function() {
                setRecStatus('Mikrofon verweigert');
            });
        }
    
        function stopRecording() {
            if (mediaRecorder && isRecording) { mediaRecorder.stop(); }
            if (recTimer) { clearInterval(recTimer); recTimer = null; }
        }
    
        /* ============================================================
           SPEECH TO TEXT (STT)
           ============================================================ */
        function transcribeAudio(blob, callback) {
            var formData = new FormData();
            formData.append('file', blob, 'recording.webm');
            formData.append('model_id', 'scribe_v2');
            formData.append('language_code', 'de');
    
            var xhr = new XMLHttpRequest();
            xhr.open('POST', 'https://api.elevenlabs.io/v1/speech-to-text', true);
            xhr.setRequestHeader('xi-api-key', ttsApiKey);
    
            xhr.onload = function() {
                if (xhr.status === 200) {
                    try {
                        var result = JSON.parse(xhr.responseText);
                        var text = result.text || '';
                        if (typeof console !== 'undefined') { console.log('STT: "' + text + '"'); }
                        if (callback) callback(text);
                    } catch(e) {
                        if (callback) callback('');
                    }
                } else {
                    if (typeof console !== 'undefined') { console.log('STT error:', xhr.status); }
                    setRecStatus('STT-Fehler ' + xhr.status);
                    if (callback) callback('');
                }
            };
            xhr.onerror = function() {
                setRecStatus('STT nicht erreichbar');
                if (callback) callback('');
            };
            xhr.send(formData);
        }
    
        /* ============================================================
           SPEECH TO SPEECH (STS) — kept but disabled
           ============================================================ */
        function transformVoice(blob) {
            setRecStatus('transformiere Stimme...');
            var formData = new FormData();
            formData.append('audio', blob, 'recording.webm');
            formData.append('model_id', 'eleven_english_sts_v2');
            var url = 'https://api.elevenlabs.io/v1/speech-to-speech/' + encodeURIComponent(voiceId)
                    + '?output_format=mp3_44100_128';
            var xhr = new XMLHttpRequest();
            xhr.open('POST', url, true);
            xhr.responseType = 'blob';
            xhr.setRequestHeader('xi-api-key', ttsApiKey);
            xhr.onload = function() {
                if (xhr.status === 200) {
                    addChatMsg('[STS] Stimme transformiert', 'system');
                    playBlob(xhr.response, function() { setRecStatus('bereit'); });
                } else {
                    setRecStatus('STS-Fehler ' + xhr.status);
                }
            };
            xhr.onerror = function() { setRecStatus('STS nicht erreichbar'); };
            xhr.send(formData);
        }
    
        /* ============================================================
           CALL MODE — Voice Conversation Loop
           ============================================================ */
        function callStartListening() {
            if (!callActive) return;
            if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
                setCallStatus('Mikrofon nicht verfuegbar');
                return;
            }
    
            setCallPhase('listening');
            audioChunks = [];
    
            var constraints = new Object();
            constraints.audio = true;
            constraints.video = false;
    
            navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
                callStream = stream;
    
                /* Setup audio analyser for silence detection */
                callAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
                var source = callAudioCtx.createMediaStreamSource(stream);
                callAnalyser = callAudioCtx.createAnalyser();
                callAnalyser.fftSize = 512;
                source.connect(callAnalyser);
    
                /* Start recording */
                mediaRecorder = new MediaRecorder(stream);
                isRecording = true;
    
                mediaRecorder.ondataavailable = function(e) {
                    if (e.data.size > 0) { audioChunks.push(e.data); }
                };
    
                mediaRecorder.onstop = function() {
                    isRecording = false;
                };
    
                mediaRecorder.start(250); /* Collect in 250ms chunks */
    
                /* Start silence detection loop */
                var silenceStart = null;
                var hasSpoken = false;
    
                function checkVolume() {
                    if (!callActive || callPhase !== 'listening') return;
    
                    var dataArray = new Uint8Array(callAnalyser.frequencyBinCount);
                    callAnalyser.getByteFrequencyData(dataArray);
    
                    /* Calculate average volume */
                    var sum = 0;
                    for (var vi = 0; vi < dataArray.length; vi++) { sum += dataArray[vi]; }
                    var avg = sum / dataArray.length;
    
                    if (avg > SILENCE_THRESHOLD) {
                        /* Sound detected */
                        hasSpoken = true;
                        silenceStart = null;
                    } else if (hasSpoken) {
                        /* Silence after speech */
                        if (!silenceStart) {
                            silenceStart = Date.now();
                        } else if (Date.now() - silenceStart > SILENCE_DURATION) {
                            /* Silence long enough — process */
                            callStopListening();
                            return;
                        }
                    }
    
                    /* Continue checking */
                    if (callActive && callPhase === 'listening') {
                        setTimeout(checkVolume, 100);
                    }
                }
    
                /* Small delay before starting detection to avoid immediate trigger */
                setTimeout(checkVolume, 500);
    
            }).catch(function() {
                setCallStatus('Mikrofon verweigert');
                callActive = false;
                updateButtons();
            });
        }
    
        function callStopListening() {
            if (mediaRecorder && isRecording) {
                mediaRecorder.stop();
            }
    
            /* Stop stream */
            if (callStream) {
                var tracks = callStream.getTracks();
                for (var t = 0; t < tracks.length; t++) { tracks[t].stop(); }
                callStream = null;
            }
            if (callAudioCtx) {
                callAudioCtx.close();
                callAudioCtx = null;
            }
    
            /* Small delay to collect final audio chunks */
            setTimeout(function() {
                if (audioChunks.length === 0) {
                    /* No audio — restart listening */
                    if (callActive) { callStartListening(); }
                    return;
                }
    
                setCallPhase('processing');
    
                var blobType = new Object();
                blobType.type = 'audio/webm';
                var blob = new Blob(audioChunks, blobType);
    
                /* Transcribe */
                transcribeAudio(blob, function(text) {
                    if (!text || !callActive) {
                        /* No text recognized — restart */
                        if (callActive) { callStartListening(); }
                        return;
                    }
    
                    /* Insert into chat input and send */
                    var chatInput = document.getElementById('chatInput');
                    var chatSend = document.getElementById('chatSend');
    
                    if (chatInput && chatSend) {
                        chatInput.value = text;
                        setCallPhase('waiting');
                        setCallStatus('wartet auf Antwort...');
    
                        /* Click send */
                        chatSend.click();
    
                        /* Now wait for buddy response — handled by chat observer */
                    }
                });
            }, 300);
        }
    
        function callEndAll() {
            callActive = false;
            callPhase = 'idle';
            if (mediaRecorder && isRecording) { mediaRecorder.stop(); }
            if (callStream) {
                var tracks = callStream.getTracks();
                for (var t = 0; t < tracks.length; t++) { tracks[t].stop(); }
                callStream = null;
            }
            if (callAudioCtx) {
                callAudioCtx.close();
                callAudioCtx = null;
            }
            stopAudio();
            updateButtons();
            setCallStatus('');
            setStatus('Anruf beendet');
        }
    
        /* ============================================================
           PLAY BUTTONS ON BUDDY MESSAGES
           ============================================================ */
        function getBuddyText(el) {
            var clone = el.cloneNode(true);
            var btns = clone.querySelectorAll('.tts-play-btn');
            for (var r = 0; r < btns.length; r++) { btns[r].parentNode.removeChild(btns[r]); }
            return clone.textContent || clone.innerText || '';
        }
    
        function addPlayButton(msgEl) {
            var playBtn = document.createElement('span');
            playBtn.className = 'tts-play-btn';
            playBtn.innerHTML = '▶ vorlesen';
            playBtn.title = 'Nachricht vorlesen';
            playBtn.onclick = function() {
                if (isSpeaking && playBtn.className.indexOf('playing') !== -1) {
                    stopAudio();
                    playBtn.innerHTML = '▶ vorlesen';
                    playBtn.className = 'tts-play-btn';
                    setStatus('gestoppt');
                    return;
                }
                stopAudio();
                var allBtns = document.querySelectorAll('.tts-play-btn.playing');
                for (var b = 0; b < allBtns.length; b++) {
                    allBtns[b].innerHTML = '▶ vorlesen';
                    allBtns[b].className = 'tts-play-btn';
                }
                playBtn.innerHTML = '■ stopp';
                playBtn.className = 'tts-play-btn playing';
                speak(getBuddyText(msgEl), function() {
                    playBtn.innerHTML = '▶ vorlesen';
                    playBtn.className = 'tts-play-btn';
                });
            };
            msgEl.appendChild(playBtn);
        }
    
        function isBuddyMessage(node) {
            if (!node.className) return false;
            var cls = node.className;
            if (cls.indexOf('chatMsg') !== -1 && cls.indexOf('buddy') !== -1) {
                if (cls.indexOf('typing') !== -1) return false;
                return true;
            }
            return false;
        }
    
        /* ============================================================
           CHAT OBSERVER — handles both TTS autoplay and Call mode
           ============================================================ */
        function watchChat() {
            var chatLog = document.getElementById('chatLog');
            if (!chatLog) { setTimeout(watchChat, 500); return; }
    
            var existing = chatLog.querySelectorAll('.chatMsg.buddy');
            for (var e = 0; e < existing.length; e++) {
                if (!existing[e].querySelector('.tts-play-btn')) { addPlayButton(existing[e]); }
            }
    
            if (typeof MutationObserver !== 'undefined') {
                var observer = new MutationObserver(function(mutations) {
                    for (var m = 0; m < mutations.length; m++) {
                        var added = mutations[m].addedNodes;
                        for (var n = 0; n < added.length; n++) {
                            var node = added[n];
                            if (node.nodeType !== 1) continue;
                            if (isBuddyMessage(node)) {
                                (function(el) {
                                    setTimeout(function() {
                                        if (!el.querySelector('.tts-play-btn')) { addPlayButton(el); }
    
                                        var msgText = getBuddyText(el).replace(/^\s+|\s+$/g, '');
    
                                        /* CALL MODE: speak response, then resume listening */
                                        if (callActive && (callPhase === 'waiting' || callPhase === 'processing')) {
                                            if (msgText) {
                                                setCallPhase('speaking');
                                                speak(msgText, function() {
                                                    if (callActive) {
                                                        /* Resume listening after TTS done */
                                                        setTimeout(function() {
                                                            if (callActive) { callStartListening(); }
                                                        }, 400);
                                                    }
                                                });
                                            } else {
                                                /* Empty response — resume listening */
                                                if (callActive) { callStartListening(); }
                                            }
                                            return;
                                        }
    
                                        /* Normal TTS autoplay */
                                        if (ttsActive && ttsApiKey && voiceId) {
                                            if (msgText) { speak(msgText); }
                                        }
                                    }, 200);
                                })(node);
                            }
                        }
                    }
                });
                var obsConfig = new Object();
                obsConfig.childList = true;
                obsConfig.subtree = false;
                observer.observe(chatLog, obsConfig);
            }
        }
    
        /* ============================================================
           TOGGLE HANDLERS
           ============================================================ */
        ttsBtn.onclick = function() {
            if (!ttsApiKey || !voiceId) { setStatus('API-Key/Voice fehlt!'); return; }
            if (callActive) return; /* Don't toggle during call */
            ttsActive = !ttsActive;
            updateButtons();
            setStatus(ttsActive ? 'TTS aktiv' : 'TTS aus');
        };
    
        stsBtn.onclick = function() {
            /* Disabled — do nothing */
            setStatus('STS deaktiviert');
        };
    
        sttBtn.onclick = function() {
            if (!ttsApiKey) { setStatus('API-Key fehlt!'); return; }
            if (callActive) return;
            sttActive = !sttActive;
            updateButtons();
            setRecStatus(sttActive ? 'bereit — klick Aufnehmen' : '');
            setStatus(sttActive ? 'STT aktiv' : 'STT aus');
        };
    
        searchBtn.onclick = function() {
            searchActive = !searchActive;
            window.helikonSearchEnabled = searchActive;
            updateButtons();
            setStatus(searchActive ? 'Wiki-Suche aktiv' : 'Wiki-Suche aus');
        };
    
        callBtn.onclick = function() {
            if (!ttsApiKey || !voiceId) { setStatus('API-Key/Voice fehlt!'); return; }
    
            if (callActive) {
                /* Hang up */
                callEndAll();
                addChatMsg('[Anruf beendet]', 'system');
                return;
            }
    
            /* Start call */
            callActive = true;
            ttsActive = false;
            sttActive = false;
            stsActive = false;
            updateButtons();
            addChatMsg('[Anruf gestartet — sprich jetzt...]', 'system');
            setStatus('Anruf aktiv');
    
            /* Begin listening */
            callStartListening();
        };
    
        /* === STT RECORD BUTTON === */
        recBtn.onclick = function() {
            if (isRecording) {
                stopRecording();
                return;
            }
            if (sttActive) {
                startRecording(function(blob) {
                    setRecStatus('transkribiere...');
                    transcribeAudio(blob, function(text) {
                        if (text) {
                            var chatInput = document.getElementById('chatInput');
                            if (chatInput) {
                                chatInput.value = text;
                                chatInput.focus();
                            }
                            addChatMsg('[STT] ' + text, 'system');
                            setRecStatus(text.substring(0, 40) + (text.length > 40 ? '...' : ''));
                        } else {
                            setRecStatus('kein Text erkannt');
                        }
                    });
                });
            }
        };
    
        /* ============================================================
           INIT
           ============================================================ */
        setStatus('lade...');
    
        fetchAllProperties(function(props) {
            ttsApiKey = props['buddy.ttsapikey'] || '';
            voiceId   = props['buddy.ttsvoiceid'] || '';
            modelId   = props['buddy.ttsmodel'] || 'eleven_multilingual_v2';
    
            if (typeof console !== 'undefined') {
                console.log('=== HELIKON VOICE v5 ===');
                console.log('Key: ' + (ttsApiKey ? ttsApiKey.substring(0,8) + '...' : 'FEHLT'));
                console.log('Voice: ' + (voiceId || 'FEHLT'));
                console.log('Model: ' + modelId);
                console.log('Wiki-Suche: AN (default)');
            }
    
            /* Set global search flag */
            window.helikonSearchEnabled = searchActive;
    
            if (ttsApiKey) {
                setStatus('bereit');
            } else {
                setStatus('buddy.ttsapikey fehlt');
            }
    
            updateButtons();
            watchChat();
        });
    
    })();
    NoElements {}

     
    Use the language selection below for English Utilise le sélecteur de langue ci-dessous pour français Per l'italiano, utilizzare la selezione della lingua qui sotto Schön sprichst du deutsch :-)