Inhaltsverzeichnis
keine Gliederung
(function() {
/* === INJECT CSS === */
var css = ''
+ '#helikonTTS { max-width:700px; margin:0 auto 20px auto; font-family:"Courier New",Courier,monospace; }'
+ '#ttsControls { display:flex; align-items:center; gap:12px; padding:8px 12px; background:#1a1a2e; border:1px solid #333; border-radius:8px; }'
+ '#ttsToggle { background:none; border:2px solid #555; color:#999; padding:6px 14px; border-radius:6px; cursor:pointer; font-family:inherit; font-size:13px; display:flex; align-items:center; gap:6px; transition:all 0.3s ease; }'
+ '#ttsToggle:hover { border-color:#888; color:#ccc; }'
+ '#ttsToggle.active { border-color:#4CAF50; color:#4CAF50; background:rgba(76,175,80,0.1); }'
+ '#ttsIcon { font-size:16px; }'
+ '#ttsLabel { font-size:12px; }'
+ '#ttsStatus { color:#666; font-size:11px; flex:1; text-align:right; }'
+ '.tts-play-btn { background:none; border:1px solid #555; color:#888; padding:2px 8px; border-radius:4px; cursor:pointer; font-size:11px; margin-left:8px; transition:all 0.2s ease; font-family:"Courier New",Courier,monospace; }'
+ '.tts-play-btn:hover { border-color:#4CAF50; color:#4CAF50; }'
+ '.tts-play-btn.playing { border-color:#ff9800; color:#ff9800; }'
+ '.tts-play-btn:disabled { opacity:0.4; cursor:not-allowed; }';
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 toggleBtn = document.getElementById('ttsToggle');
var ttsIcon = document.getElementById('ttsIcon');
var ttsLabel = document.getElementById('ttsLabel');
var ttsStatus = document.getElementById('ttsStatus');
if (!toggleBtn) return;
/* === STATE === */
var autoplay = false;
var ttsApiKey = '';
var voiceId = '';
var modelId = 'eleven_multilingual_v2';
var currentAudio = null;
var isSpeaking = false;
/* === 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) {
/* fetch value sync-ish via nested XHR */
props['_href_' + short] = href;
}
var textNode = contentsEl[0].textContent;
if (textNode) {
props[short] = textNode.replace(/^\s+|\s+$/g, '');
}
}
}
} catch(e) { /* parse error */ }
}
/* Now fetch actual values from href endpoints */
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 propHref = props['_href_' + propName];
var xh = new XMLHttpRequest();
xh.open('GET', propHref, 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();
}
/* === TTS API CALL === */
function speak(text, onDone) {
if (!ttsApiKey || !voiceId) {
setStatus('TTS nicht konfiguriert');
if (onDone) onDone();
return;
}
/* Strip markdown/HTML for cleaner speech */
var clean = text
.replace(/<[^>]*>/g, '')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/#{1,6}\s/g, '')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/\s+/g, ' ')
.replace(/^\s+|\s+$/g, '');
if (!clean) {
if (onDone) onDone();
return;
}
/* Truncate very long texts (ElevenLabs has limits) */
if (clean.length > 5000) {
clean = clean.substring(0, 5000);
}
setStatus('spreche...');
isSpeaking = true;
var url = 'https://api.elevenlabs.io/v1/text-to-speech/' + encodeURIComponent(voiceId)
+ '?output_format=mp3_44100_128';
var body = JSON.stringify(new Object());
/* Build body manually to avoid MindTouch object literal issues */
var bodyObj = new Object();
bodyObj.text = clean;
bodyObj.model_id = modelId;
body = JSON.stringify(bodyObj);
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) {
var blob = xhr.response;
var audioUrl = URL.createObjectURL(blob);
/* Stop previous audio if playing */
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();
} else {
isSpeaking = false;
var errMsg = 'TTS-Fehler ' + xhr.status;
if (xhr.status === 401) errMsg = 'TTS-Key ungueltig';
if (xhr.status === 429) errMsg = 'TTS Rate-Limit erreicht';
setStatus(errMsg);
if (onDone) onDone();
}
};
xhr.onerror = function() {
isSpeaking = false;
setStatus('TTS nicht erreichbar');
if (onDone) onDone();
};
xhr.send(body);
}
function stopAudio() {
if (currentAudio) {
currentAudio.pause();
currentAudio.currentTime = 0;
currentAudio = null;
}
isSpeaking = false;
}
/* === UI HELPERS === */
function setStatus(msg) {
if (ttsStatus) ttsStatus.textContent = msg;
}
function updateToggleUI() {
if (autoplay) {
toggleBtn.className = 'active';
ttsIcon.innerHTML = '';
ttsLabel.textContent = 'Sprache: An';
} else {
toggleBtn.className = '';
ttsIcon.innerHTML = '';
ttsLabel.textContent = 'Sprache: Aus';
}
}
/* === ADD PLAY BUTTON TO BOT MESSAGE === */
function addPlayButton(msgEl) {
var textEl = msgEl.querySelector('.msg-text');
if (!textEl) {
/* fallback: use the element itself */
textEl = msgEl;
}
var playBtn = document.createElement('button');
playBtn.className = 'tts-play-btn';
playBtn.innerHTML = '▶ vorlesen';
playBtn.title = 'Nachricht vorlesen';
playBtn.addEventListener('click', function() {
if (isSpeaking && playBtn.className.indexOf('playing') !== -1) {
stopAudio();
playBtn.innerHTML = '▶ vorlesen';
playBtn.className = 'tts-play-btn';
setStatus('gestoppt');
return;
}
/* Stop any other playing audio */
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';
var rawText = textEl.textContent || textEl.innerText || '';
speak(rawText, function() {
playBtn.innerHTML = '▶ vorlesen';
playBtn.className = 'tts-play-btn';
});
});
msgEl.appendChild(playBtn);
}
/* === WATCH CHAT FOR NEW BOT MESSAGES === */
function watchChat() {
var chatLog = document.getElementById('chatLog');
if (!chatLog) {
/* Retry - chat template might load after TTS template */
setTimeout(watchChat, 500);
return;
}
/* Add play buttons to existing bot messages */
var existing = chatLog.querySelectorAll('.msg-bot');
for (var e = 0; e < existing.length; e++) {
if (!existing[e].querySelector('.tts-play-btn')) {
addPlayButton(existing[e]);
}
}
/* Observe for new messages */
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;
/* Check if it's a bot message */
var isBotMsg = false;
if (node.className && node.className.indexOf('msg-bot') !== -1) {
isBotMsg = true;
}
if (isBotMsg) {
/* Add play button */
if (!node.querySelector('.tts-play-btn')) {
addPlayButton(node);
}
/* Autoplay if enabled */
if (autoplay && ttsApiKey && voiceId) {
var textNode = node.querySelector('.msg-text');
var msgText = textNode ? (textNode.textContent || textNode.innerText) : (node.textContent || node.innerText);
if (msgText) {
speak(msgText.replace(/^\s+|\s+$/g, ''));
}
}
}
}
}
});
var obsConfig = new Object();
obsConfig.childList = true;
obsConfig.subtree = false;
observer.observe(chatLog, obsConfig);
}
}
/* === TOGGLE HANDLER === */
toggleBtn.addEventListener('click', function() {
if (!ttsApiKey || !voiceId) {
setStatus('TTS nicht konfiguriert! buddy.ttsapikey / buddy.ttsvoiceid fehlen');
return;
}
autoplay = !autoplay;
updateToggleUI();
if (autoplay) {
setStatus('Autoplay aktiv');
} else {
stopAudio();
setStatus('Autoplay deaktiviert');
}
});
/* === INIT === */
setStatus('lade TTS-Konfiguration...');
fetchAllProperties(function(props) {
ttsApiKey = props['buddy.ttsapikey'] || '';
voiceId = props['buddy.ttsvoiceid'] || '';
modelId = props['buddy.ttsmodel'] || 'eleven_multilingual_v2';
var keyOk = ttsApiKey ? 'OK' : 'FEHLT';
var voiceOk = voiceId ? 'OK' : 'FEHLT';
setStatus('TTS-Key: ' + keyOk + ' | Voice: ' + voiceOk);
if (ttsApiKey && voiceId) {
setStatus('bereit — Sprache aus (klick zum aktivieren)');
} else {
setStatus('buddy.ttsapikey oder buddy.ttsvoiceid fehlt');
}
/* Start watching chat */
watchChat();
});
updateToggleUI();
})();
NoElements {}