Sprache

Von $1

    Inhaltsverzeichnis
    keine Gliederung

    Version seit 18:55, 11 Aug 2026

    zu dieser Version.

    Zurück zu Versionshistorie.

    Zeige aktuelle Version

    Credits OK
    ○ Text→Sprache ○ Sprache→Sprache ○ Sprache→Text ☎ Helikon anrufen ● Wiki-Suche
    Stimme:�� Stimme 1�� Stimme 2
     
    ● Aufnehmen 0s  
       
    (function() {
    
        /* ============================================================
         * HELIKON VOICE v6.0 — 2026-02-27
         * Animated wave bars, voice selector, credit lamp
         * ============================================================ */
    
        var css = ''
        + '#helikonTTS { max-width:700px; margin:0; font-family:"Press Start 2P","Courier New",Courier,monospace; border:2px solid #0077aa; border-radius:8px; background:#0a0a1a; padding:12px; position:relative; overflow:hidden; }'
        + '#ttsWaveWrap { height:40px; margin:0 0 10px 0; overflow:hidden; border-radius:4px; background:#0d0d24; border:1px solid #1a1a3a; }'
        + '#ttsWave { display:flex; align-items:flex-end; justify-content:center; gap:3px; height:100%; padding:4px 8px; }'
        + '.ttsBar { display:inline-block; width:6px; background:#0077aa; border-radius:2px 2px 0 0; height:4px; opacity:0.3; transition:height 0.15s ease, opacity 0.15s ease; }'
        + '#helikonTTS.tts-on .ttsBar { opacity:1; }'
        + '@keyframes waveIdle { 0%,100% { height:4px; opacity:0.3; } 50% { height:8px; opacity:0.5; } }'
        + '#helikonTTS.tts-on .ttsBar { animation:waveIdle 1.5s ease-in-out infinite; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(1) { animation-delay:0s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(2) { animation-delay:0.1s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(3) { animation-delay:0.2s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(4) { animation-delay:0.3s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(5) { animation-delay:0.4s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(6) { animation-delay:0.5s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(7) { animation-delay:0.6s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(8) { animation-delay:0.7s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(9) { animation-delay:0.6s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(10) { animation-delay:0.5s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(11) { animation-delay:0.4s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(12) { animation-delay:0.3s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(13) { animation-delay:0.2s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(14) { animation-delay:0.1s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(15) { animation-delay:0s; }'
        + '#helikonTTS.tts-on .ttsBar:nth-child(16) { animation-delay:0.1s; }'
        + '@keyframes waveTalk { 0% { height:4px; } 15% { height:28px; } 30% { height:10px; } 45% { height:22px; } 60% { height:6px; } 75% { height:32px; } 90% { height:14px; } 100% { height:4px; } }'
        + '#helikonTTS.tts-speaking .ttsBar { opacity:1; animation:waveTalk 0.8s ease-in-out infinite; background:linear-gradient(0deg, #0077aa, #00ccff); }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(1) { animation-delay:0.05s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(2) { animation-delay:0.1s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(3) { animation-delay:0.15s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(4) { animation-delay:0.2s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(5) { animation-delay:0.25s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(6) { animation-delay:0.3s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(7) { animation-delay:0.35s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(8) { animation-delay:0.4s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(9) { animation-delay:0.35s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(10) { animation-delay:0.3s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(11) { animation-delay:0.25s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(12) { animation-delay:0.2s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(13) { animation-delay:0.15s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(14) { animation-delay:0.1s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(15) { animation-delay:0.05s; }'
        + '#helikonTTS.tts-speaking .ttsBar:nth-child(16) { animation-delay:0s; }'
        + '#helikonTTS.tts-call .ttsBar { opacity:1; animation:waveIdle 1.2s ease-in-out infinite; background:linear-gradient(0deg, #3a8f3a, #66ff66); }'
        + '#helikonTTS.tts-call.tts-speaking .ttsBar { animation:waveTalk 0.8s ease-in-out infinite; background:linear-gradient(0deg, #3a8f3a, #66ff66); }'
        + '#ttsCreditLamp { position:absolute; top:12px; right:12px; display:flex; align-items:center; gap:6px; z-index:2; }'
        + '#creditDot { display:inline-block; width:10px; height:10px; border-radius:50%; background:#3a8f3a; box-shadow:0 0 6px rgba(58,143,58,0.6); transition:all 0.3s ease; }'
        + '#creditDot.ok { background:#3a8f3a; box-shadow:0 0 6px rgba(58,143,58,0.6); }'
        + '#creditDot.warn { background:#cc8800; box-shadow:0 0 6px rgba(204,136,0,0.6); animation:lampPulse 2s infinite; }'
        + '#creditDot.empty { background:#cc3333; box-shadow:0 0 12px rgba(204,51,51,0.8); animation:lampAlarm 0.6s infinite; }'
        + '#creditLabel { font-size:7px; color:#666; letter-spacing:0.5px; text-transform:uppercase; }'
        + '#creditLabel.warn { color:#cc8800; }'
        + '#creditLabel.empty { color:#cc3333; font-weight:bold; }'
        + '@keyframes lampPulse { 0%,100% { opacity:1; } 50% { opacity:0.5; } }'
        + '@keyframes lampAlarm { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:0.4; transform:scale(1.4); } }'
        + '#ttsControls { display:flex; align-items:center; gap:6px; padding:8px 0; flex-wrap:wrap; }'
        + '.voice-btn { background:#111; border:2px solid #0077aa; color:#0077aa; padding:10px 14px; cursor:pointer; font-family:"Press Start 2P","Courier New",Courier,monospace; font-size:10px; display:inline-block; border-radius:6px; transition:all 0.2s ease; position:relative; overflow:hidden; }'
        + '.voice-btn:hover { background:#0077aa; color:#fff; box-shadow:0 0 12px rgba(0,119,170,0.4); transform:translateY(-1px); }'
        + '.voice-btn.active { background:#0077aa; color:#fff; box-shadow:0 0 16px rgba(0,119,170,0.6); }'
        + '.voice-btn.active::before { content:""; position:absolute; top:0; left:-100%; width:200%; height:100%; background:linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); animation:btnShine 2s infinite; }'
        + '@keyframes btnShine { 0% { left:-100%; } 100% { left:100%; } }'
        + '.voice-btn.disabled { background:#111; border-color:#333; color:#444; cursor:default; }'
        + '.voice-btn.disabled:hover { background:#111; color:#444; box-shadow:none; transform:none; }'
        + '#callToggle { border-color:#3a8f3a; color:#3a8f3a; }'
        + '#callToggle:hover { background:#3a8f3a; color:#fff; box-shadow:0 0 12px rgba(58,143,58,0.4); }'
        + '#callToggle.active { background:#3a8f3a; color:#fff; box-shadow:0 0 20px rgba(58,143,58,0.6); animation:callGlow 2s infinite; }'
        + '@keyframes callGlow { 0%,100% { box-shadow:0 0 12px rgba(58,143,58,0.4); } 50% { box-shadow:0 0 24px rgba(58,143,58,0.8); } }'
        + '#searchToggle { border-color:#cc8800; color:#cc8800; }'
        + '#searchToggle:hover { background:#cc8800; color:#fff; }'
        + '#searchToggle.active { background:#cc8800; color:#fff; }'
        + '#ttsVoiceSel { display:flex; align-items:center; gap:6px; padding:8px 0; border-top:1px solid #1a1a3a; }'
        + '#voiceLabel { font-size:8px; color:#666; text-transform:uppercase; letter-spacing:0.5px; margin-right:4px; }'
        + '.voice-opt { background:#111; border:2px solid #444; color:#888; padding:8px 12px; cursor:pointer; font-family:"Press Start 2P","Courier New",Courier,monospace; font-size:9px; border-radius:6px; transition:all 0.2s ease; }'
        + '.voice-opt:hover { border-color:#0077aa; color:#0077aa; }'
        + '.voice-opt.active { background:#1a1a3a; border-color:#00ccff; color:#00ccff; box-shadow:0 0 10px rgba(0,204,255,0.3); }'
        + '#ttsStatusBar { padding:4px 0; }'
        + '#ttsStatus { color:#555; font-size:8px; letter-spacing:0.5px; text-transform:uppercase; }'
        + '#voiceRecBar { display:none; align-items:center; gap:10px; padding:8px 0; border-top:1px solid #1a1a3a; }'
        + '#voiceRecBar.visible { display:flex; }'
        + '#voiceRecBtn { background:#111; border:2px solid #cc3333; color:#cc3333; padding:10px 14px; cursor:pointer; font-family:"Press Start 2P","Courier New",Courier,monospace; font-size:10px; border-radius:6px; transition:all 0.2s ease; }'
        + '#voiceRecBtn:hover { background:#cc3333; color:#fff; }'
        + '#voiceRecBtn.recording { background:#cc3333; color:#fff; animation:recPulse 1s infinite; box-shadow:0 0 16px rgba(204,51,51,0.6); }'
        + '@keyframes recPulse { 0%,100% { opacity:1; } 50% { opacity:0.6; } }'
        + '#voiceRecTime { color:#cc3333; font-size:10px; min-width:30px; }'
        + '#voiceRecStatus { color:#666; font-size:8px; }'
        + '#callBar { display:none; align-items:center; gap:10px; padding:8px 0; border-top:1px solid #1a1a3a; }'
        + '#callBar.visible { display:flex; }'
        + '#callIndicator { width:12px; height:12px; border-radius:50%; background:#555; 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:#555; }'
        + '@keyframes listenPulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:0.6; transform:scale(1.4); } }'
        + '@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:8px; text-transform:uppercase; letter-spacing:0.5px; }'
        + '.tts-play-btn { background:#111; border:1px solid #333; color:#666; padding:4px 10px; cursor:pointer; font-size:9px; margin-left:8px; font-family:"Press Start 2P","Courier New",Courier,monospace; display:none; border-radius:4px; transition:all 0.2s ease; }'
        + '.tts-play-btn:hover { border-color:#0077aa; color:#0077aa; }'
        + '.tts-play-btn.playing { border-color:#00ccff; color:#00ccff; display:inline-block !important; box-shadow:0 0 8px rgba(0,204,255,0.3); }'
        + '.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);
    
        var fontLink = document.createElement('link');
        fontLink.rel = 'stylesheet';
        fontLink.href = 'https://fonts.googleapis.com/css2?family=Press+Start+2P';
        document.head.appendChild(fontLink);
    
        /* === DOM === */
        var ttsWrap     = document.getElementById('helikonTTS');
        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 ttsStatusEl = 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 callStatusEl = document.getElementById('callStatus');
        var creditDot   = document.getElementById('creditDot');
        var creditLabel = document.getElementById('creditLabel');
        var voiceOpt1   = document.getElementById('voiceOpt1');
        var voiceOpt2   = document.getElementById('voiceOpt2');
        if (!ttsBtn) return;
    
        ttsBtn.className = 'voice-btn';
        stsBtn.className = 'voice-btn disabled';
        sttBtn.className = 'voice-btn';
        callBtn.className = 'voice-btn';
        searchBtn.className = 'voice-btn active';
    
        /* === VOICES === */
        var VOICES = new Array(2);
        VOICES[0] = new Object(); VOICES[0].id = ''; VOICES[0].label = 'Stimme 1';
        VOICES[1] = new Object(); VOICES[1].id = 'rKiu7lQ4c5P3az3745s3'; VOICES[1].label = 'Stimme 2';
        var activeVoiceIdx = 0;
    
        /* === STATE === */
        var ttsActive = false, stsActive = false, sttActive = false, callActive = false;
        var searchActive = true;
        var ttsApiKey = '', voiceId = '', modelId = 'eleven_multilingual_v2';
        var currentAudio = null, isSpeaking = false, isRecording = false;
        var mediaRecorder = null, audioChunks = [], recTimer = null, recSeconds = 0;
        var creditsOk = true;
        var callStream = null, callAnalyser = null, callAudioCtx = null;
        var callPhase = 'idle';
        var SILENCE_THRESHOLD = 15, SILENCE_DURATION = 1800;
    
        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();
        }
    
        /* === CREDIT CHECK === */
        function checkCredits() {
            if (!ttsApiKey) return;
            var xhr = new XMLHttpRequest();
            xhr.open('GET', 'https://api.elevenlabs.io/v1/user/subscription', true);
            xhr.setRequestHeader('xi-api-key', ttsApiKey);
            xhr.onload = function() {
                if (xhr.status === 200) {
                    try {
                        var data = JSON.parse(xhr.responseText);
                        var remaining = data.character_count || 0;
                        var limit = data.character_limit || 1;
                        var pct = Math.round((remaining / limit) * 100);
                        if (typeof console !== 'undefined') { console.log('ElevenLabs Credits: ' + remaining + '/' + limit + ' (' + pct + '%)'); }
                        if (remaining <= 0 || pct <= 0) { setCreditState('empty'); }
                        else if (pct <= 15) { setCreditState('warn'); }
                        else { setCreditState('ok'); }
                    } catch(e) { setCreditState('ok'); }
                } else if (xhr.status === 401) { setCreditState('empty'); }
            };
            xhr.onerror = function() {};
            xhr.send();
        }
    
        function setCreditState(state) {
            if (creditDot) creditDot.className = state;
            if (!creditLabel) return;
            if (state === 'empty') { creditLabel.textContent = 'Kein Guthaben!'; creditLabel.className = 'empty'; creditsOk = false; }
            else if (state === 'warn') { creditLabel.textContent = 'Credits knapp'; creditLabel.className = 'warn'; creditsOk = true; }
            else { creditLabel.textContent = 'Credits OK'; creditLabel.className = ''; creditsOk = true; }
        }
    
        /* === UI HELPERS === */
        function setStatus(msg) { if (ttsStatusEl) ttsStatusEl.textContent = msg; }
        function setRecStatus(msg) { if (recStatus) recStatus.textContent = msg; }
        function setCallStatus(msg) { if (callStatusEl) callStatusEl.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 getActiveVoiceId() { return VOICES[activeVoiceIdx].id || voiceId; }
    
        function updateWrapClass() {
            var cls = '';
            if (ttsActive || sttActive) cls = 'tts-on';
            if (isSpeaking) cls = 'tts-speaking';
            if (callActive && !isSpeaking) cls = 'tts-call';
            if (callActive && isSpeaking) cls = 'tts-call tts-speaking';
            ttsWrap.className = cls;
        }
    
        function updateButtons() {
            ttsBtn.className = 'voice-btn' + (ttsActive ? ' active' : '');
            ttsBtn.textContent = (ttsActive ? '\u25CF' : '\u25CB') + ' Text\u2192Sprache';
            stsBtn.className = 'voice-btn disabled';
            stsBtn.textContent = '\u25CB Sprache\u2192Sprache';
            sttBtn.className = 'voice-btn' + (sttActive ? ' active' : '');
            sttBtn.textContent = (sttActive ? '\u25CF' : '\u25CB') + ' Sprache\u2192Text';
            callBtn.className = 'voice-btn' + (callActive ? ' active' : '');
            callBtn.textContent = callActive ? '\u2706 Auflegen' : '\u2706 Helikon anrufen';
            searchBtn.className = 'voice-btn' + (searchActive ? ' active' : '');
            searchBtn.textContent = (searchActive ? '\u25CF' : '\u25CB') + ' Wiki-Suche';
            window.helikonSearchEnabled = searchActive;
    
            if (sttActive) { recBar.className = 'visible'; callBarEl.className = ''; }
            else if (callActive) { recBar.className = ''; callBarEl.className = 'visible'; }
            else { recBar.className = ''; callBarEl.className = ''; }
    
            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(); }
            }
            updateWrapClass();
        }
    
        /* === VOICE SELECTOR === */
        function selectVoice(idx) {
            activeVoiceIdx = idx;
            voiceOpt1.className = 'voice-opt' + (idx === 0 ? ' active' : '');
            voiceOpt2.className = 'voice-opt' + (idx === 1 ? ' active' : '');
            setStatus('Stimme: ' + VOICES[idx].label);
        }
        voiceOpt1.onclick = function() { selectVoice(0); };
        voiceOpt2.onclick = function() { selectVoice(1); };
    
        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;
        }
    
        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, '');
            return clean;
        }
    
        /* === TTS === */
        function speak(text, onDone) {
            var activeId = getActiveVoiceId();
            if (!ttsApiKey || !activeId) { setStatus('TTS nicht konfiguriert'); if (onDone) onDone(); return; }
            if (!creditsOk) { setStatus('Keine Credits!'); 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 (' + VOICES[activeVoiceIdx].label + '): "' + clean.substring(0, 60) + '..."'); }
            setStatus('spreche...'); isSpeaking = true; updateWrapClass();
            var url = 'https://api.elevenlabs.io/v1/text-to-speech/' + encodeURIComponent(activeId) + '?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 if (xhr.status === 401 || xhr.status === 403) { isSpeaking = false; updateWrapClass(); setCreditState('empty'); setStatus('Credits aufgebraucht!'); if (onDone) onDone(); }
                else { isSpeaking = false; updateWrapClass(); setStatus('TTS-Fehler ' + xhr.status); if (onDone) onDone(); }
                checkCredits();
            };
            xhr.onerror = function() { isSpeaking = false; updateWrapClass(); setStatus('TTS nicht erreichbar'); if (onDone) onDone(); };
            xhr.send(JSON.stringify(bodyObj));
        }
    
        function playBlob(blob, onDone) {
            var audioUrl = URL.createObjectURL(blob);
            stopAudio();
            currentAudio = new Audio(audioUrl);
            isSpeaking = true; updateWrapClass();
            currentAudio.onended = function() { isSpeaking = false; updateWrapClass(); setStatus('bereit'); URL.revokeObjectURL(audioUrl); currentAudio = null; if (onDone) onDone(); };
            currentAudio.onerror = function() { isSpeaking = false; updateWrapClass(); setStatus('Wiedergabefehler'); URL.revokeObjectURL(audioUrl); currentAudio = null; if (onDone) onDone(); };
            currentAudio.play().catch(function() { setStatus('Autoplay blockiert'); isSpeaking = false; updateWrapClass(); if (onDone) onDone(); });
        }
    
        function stopAudio() {
            if (currentAudio) { currentAudio.pause(); currentAudio.currentTime = 0; currentAudio = null; }
            isSpeaking = false; updateWrapClass();
        }
    
        /* === RECORDING === */
        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.textContent = '\u25A0 Stopp'; setRecStatus('aufnahme...');
                mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) { audioChunks.push(e.data); } };
                mediaRecorder.onstop = function() {
                    isRecording = false; recBtn.className = ''; recBtn.textContent = '\u25CF 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 bt = new Object(); bt.type = 'audio/webm'; var blob = new Blob(audioChunks, bt); 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; } }
    
        /* === 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 r = JSON.parse(xhr.responseText); callback(r.text || ''); } catch(e) { callback(''); } }
                else { setRecStatus('STT-Fehler ' + xhr.status); callback(''); }
            };
            xhr.onerror = function() { setRecStatus('STT nicht erreichbar'); callback(''); };
            xhr.send(formData);
        }
    
        /* === STS (disabled) === */
        function transformVoice(blob) {
            setRecStatus('transformiere...'); 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(getActiveVoiceId()) + '?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) { playBlob(xhr.response, function() { setRecStatus('bereit'); }); } else { setRecStatus('STS-Fehler ' + xhr.status); } };
            xhr.onerror = function() { setRecStatus('STS nicht erreichbar'); }; xhr.send(formData);
        }
    
        /* === CALL MODE === */
        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;
                callAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
                var source = callAudioCtx.createMediaStreamSource(stream);
                callAnalyser = callAudioCtx.createAnalyser(); callAnalyser.fftSize = 512; source.connect(callAnalyser);
                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);
                var silenceStart = null, hasSpoken = false;
                function checkVolume() {
                    if (!callActive || callPhase !== 'listening') return;
                    var dataArray = new Uint8Array(callAnalyser.frequencyBinCount); callAnalyser.getByteFrequencyData(dataArray);
                    var sum = 0; for (var vi = 0; vi < dataArray.length; vi++) { sum += dataArray[vi]; } var avg = sum / dataArray.length;
                    if (avg > SILENCE_THRESHOLD) { hasSpoken = true; silenceStart = null; }
                    else if (hasSpoken) { if (!silenceStart) { silenceStart = Date.now(); } else if (Date.now() - silenceStart > SILENCE_DURATION) { callStopListening(); return; } }
                    if (callActive && callPhase === 'listening') { setTimeout(checkVolume, 100); }
                }
                setTimeout(checkVolume, 500);
            }).catch(function() { setCallStatus('Mikrofon verweigert'); callActive = false; updateButtons(); });
        }
    
        function callStopListening() {
            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; }
            setTimeout(function() {
                if (audioChunks.length === 0) { if (callActive) { callStartListening(); } return; }
                setCallPhase('processing');
                var bt = new Object(); bt.type = 'audio/webm'; var blob = new Blob(audioChunks, bt);
                transcribeAudio(blob, function(text) {
                    if (!text || !callActive) { if (callActive) { callStartListening(); } return; }
                    var chatInput = document.getElementById('chatInput'); var chatSend = document.getElementById('chatSend');
                    if (chatInput && chatSend) { chatInput.value = text; setCallPhase('waiting'); chatSend.click(); }
                });
            }, 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 === */
        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.textContent = '\u25B6 vorlesen'; playBtn.title = 'Nachricht vorlesen';
            playBtn.onclick = function() {
                if (isSpeaking && playBtn.className.indexOf('playing') !== -1) { stopAudio(); playBtn.textContent = '\u25B6 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].textContent = '\u25B6 vorlesen'; allBtns[b].className = 'tts-play-btn'; }
                playBtn.textContent = '\u25A0 stopp'; playBtn.className = 'tts-play-btn playing';
                speak(getBuddyText(msgEl), function() { playBtn.textContent = '\u25B6 vorlesen'; playBtn.className = 'tts-play-btn'; });
            };
            msgEl.appendChild(playBtn);
        }
    
        function isBuddyMessage(node) {
            if (!node.className) return false;
            var cls = node.className;
            return (cls.indexOf('chatMsg') !== -1 && cls.indexOf('buddy') !== -1 && cls.indexOf('typing') === -1);
        }
    
        /* === CHAT OBSERVER === */
        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, '');
                                        if (callActive && (callPhase === 'waiting' || callPhase === 'processing')) {
                                            if (msgText) { setCallPhase('speaking'); speak(msgText, function() { if (callActive) { setTimeout(function() { if (callActive) { callStartListening(); } }, 400); } }); }
                                            else { if (callActive) { callStartListening(); } }
                                            return;
                                        }
                                        if (ttsActive && ttsApiKey && getActiveVoiceId()) { 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 || !getActiveVoiceId()) { setStatus('API-Key/Voice fehlt!'); return; } if (callActive) return; ttsActive = !ttsActive; updateButtons(); setStatus(ttsActive ? 'TTS aktiv' : 'TTS aus'); };
        stsBtn.onclick = function() { setStatus('STS deaktiviert'); };
        sttBtn.onclick = function() { if (!ttsApiKey) { setStatus('API-Key fehlt!'); return; } if (callActive) return; sttActive = !sttActive; updateButtons(); setRecStatus(sttActive ? 'bereit' : ''); 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 || !getActiveVoiceId()) { setStatus('API-Key/Voice fehlt!'); return; }
            if (callActive) { callEndAll(); addChatMsg('[Anruf beendet]', 'system'); return; }
            callActive = true; ttsActive = false; sttActive = false; stsActive = false;
            updateButtons(); addChatMsg('[Anruf gestartet — sprich jetzt...]', 'system'); setStatus('Anruf aktiv'); callStartListening();
        };
        recBtn.onclick = function() {
            if (isRecording) { stopRecording(); return; }
            if (sttActive) {
                startRecording(function(blob) {
                    setRecStatus('transkribiere...');
                    transcribeAudio(blob, function(text) {
                        if (text) { var ci = document.getElementById('chatInput'); if (ci) { ci.value = text; ci.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';
            VOICES[0].id = voiceId;
            if (typeof console !== 'undefined') {
                console.log('=== HELIKON VOICE v6 ===');
                console.log('Key: ' + (ttsApiKey ? ttsApiKey.substring(0,8) + '...' : 'FEHLT'));
                console.log('Voice 1: ' + (voiceId || 'FEHLT'));
                console.log('Voice 2: ' + VOICES[1].id);
                console.log('Model: ' + modelId);
            }
            window.helikonSearchEnabled = searchActive;
            if (ttsApiKey) { setStatus('bereit'); checkCredits(); setInterval(checkCredits, 300000); }
            else { setStatus('buddy.ttsapikey fehlt'); }
            updateButtons(); selectVoice(0); 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 :-)