Inhaltsverzeichnis
keine Gliederung (function() {
/* === INJECT CSS === */
var css = ''
+ '#helikonTTS { max-width:700px; margin:0; font-family:"Courier New",Courier,monospace; }'
+ '#ttsControls { display:flex; align-items:center; gap:10px; padding:0; }'
+ '#ttsToggle { background:#fff; border:2px solid #0077aa; color:#0077aa; padding:10px 20px; cursor:pointer; font-family:inherit; font-size:14px; display:inline-block; border-radius:4px; }'
+ '#ttsToggle:hover { background:#0077aa; color:#fff; }'
+ '#ttsToggle.active { background:#0077aa; color:#fff; }'
+ '#ttsStatus { color:#999; font-size:11px; margin-left:8px; }'
+ '.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-play-btn:disabled { opacity:0.4; cursor:default; }'
+ '.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 toggleBtn = document.getElementById('ttsToggle');
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) {
props['_href_' + short] = href;
}
var textNode = contentsEl[0].textContent;
if (textNode) {
props[short] = textNode.replace(/^\s+|\s+$/g, '');
}
}
}
} catch(e) { /* parse error */ }
}
/* 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();
}
/* === CLEAN TEXT FOR SPEECH === */
function cleanText(text) {
/* Build regex patterns via new RegExp to avoid MindTouch HTML parser issues */
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, '');
/* Remove the > prefix from buddy messages */
if (clean.indexOf('>') === 0) {
clean = clean.substring(1).replace(/^\s+/, '');
}
return clean;
}
/* === TTS API CALL === */
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;
}
/* Truncate very long texts */
if (clean.length > 5000) {
clean = clean.substring(0, 5000);
}
if (typeof console !== 'undefined') {
console.log('TTS speak: "' + clean.substring(0, 80) + '..." (' + 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 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 (typeof console !== 'undefined') {
console.log('TTS API response: ' + xhr.status + ' | size: ' + (xhr.response ? xhr.response.size : 0) + ' bytes');
}
if (xhr.status === 200) {
var blob = xhr.response;
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(e) {
isSpeaking = false;
if (typeof console !== 'undefined') {
console.log('TTS Audio playback error:', e);
}
setStatus('Wiedergabefehler');
URL.revokeObjectURL(audioUrl);
currentAudio = null;
if (onDone) onDone();
};
currentAudio.play().then(function() {
if (typeof console !== 'undefined') {
console.log('TTS Audio playing!');
}
}).catch(function(err) {
if (typeof console !== 'undefined') {
console.log('TTS Audio play() rejected:', err);
}
setStatus('Autoplay blockiert — klick vorlesen');
isSpeaking = false;
if (onDone) onDone();
});
} else {
isSpeaking = false;
var errMsg = 'TTS-Fehler ' + xhr.status;
if (xhr.status === 401) errMsg = 'TTS-Key ungueltig (401)';
if (xhr.status === 403) errMsg = 'TTS-Key gesperrt (403)';
if (xhr.status === 429) errMsg = 'TTS Rate-Limit (429)';
if (typeof console !== 'undefined') {
/* Try to read error body */
try {
var reader = new FileReader();
reader.onload = function() {
console.log('TTS Error Body: ' + reader.result);
};
reader.readAsText(xhr.response);
} catch(ignore) {}
}
setStatus(errMsg);
if (onDone) onDone();
}
};
xhr.onerror = function() {
isSpeaking = false;
if (typeof console !== 'undefined') {
console.log('TTS XHR network error');
}
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() {
var chatLog = document.getElementById('chatLog');
if (autoplay) {
toggleBtn.className = 'active';
toggleBtn.innerHTML = '● Sprache: An';
if (chatLog) chatLog.className = chatLog.className.replace(' tts-active', '') + ' tts-active';
} else {
toggleBtn.className = '';
toggleBtn.innerHTML = '○ Sprache: Aus';
if (chatLog) chatLog.className = chatLog.className.replace(' tts-active', '');
stopAudio();
}
}
/* === GET CLEAN TEXT FROM BUDDY MESSAGE (without play button) === */
function getBuddyText(el) {
var clone = el.cloneNode(true);
/* Remove any play buttons from clone */
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 || '';
}
/* === ADD PLAY BUTTON TO BUDDY MESSAGE === */
function addPlayButton(msgEl) {
/* Text is directly in the .chatMsg.buddy element */
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';
var rawText = getBuddyText(msgEl);
speak(rawText, function() {
playBtn.innerHTML = '▶ vorlesen';
playBtn.className = 'tts-play-btn';
});
};
msgEl.appendChild(playBtn);
}
/* === CHECK IF ELEMENT IS A BUDDY MESSAGE === */
function isBuddyMessage(node) {
if (!node.className) return false;
var cls = node.className;
/* Match: chatMsg buddy (but not typing, not user, not system) */
if (cls.indexOf('chatMsg') !== -1 && cls.indexOf('buddy') !== -1) {
if (cls.indexOf('typing') !== -1) return false;
return true;
}
return false;
}
/* === WATCH CHAT FOR NEW BOT MESSAGES === */
function watchChat() {
var chatLog = document.getElementById('chatLog');
if (!chatLog) {
setTimeout(watchChat, 500);
return;
}
if (typeof console !== 'undefined') {
console.log('TTS: chatLog gefunden, observer startet');
}
/* Add play buttons to existing buddy messages */
var existing = chatLog.querySelectorAll('.chatMsg.buddy');
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;
if (isBuddyMessage(node)) {
if (typeof console !== 'undefined') {
console.log('TTS: neue Buddy-Nachricht! class="' + node.className + '"');
}
/* Small delay to let text render fully */
(function(el) {
setTimeout(function() {
if (!el.querySelector('.tts-play-btn')) {
addPlayButton(el);
}
if (autoplay && ttsApiKey && voiceId) {
var msgText = getBuddyText(el);
if (msgText) {
speak(msgText.replace(/^\s+|\s+$/g, ''));
}
}
}, 200);
})(node);
}
}
}
});
var obsConfig = new Object();
obsConfig.childList = true;
obsConfig.subtree = false;
observer.observe(chatLog, obsConfig);
if (typeof console !== 'undefined') {
console.log('TTS: MutationObserver aktiv auf chatLog');
}
}
}
/* === TOGGLE HANDLER === */
toggleBtn.onclick = function() {
if (!ttsApiKey || !voiceId) {
setStatus('buddy.ttsapikey / buddy.ttsvoiceid fehlen!');
return;
}
autoplay = !autoplay;
updateToggleUI();
if (autoplay) {
setStatus('Autoplay aktiv');
} else {
stopAudio();
setStatus('Autoplay deaktiviert');
}
};
/* === INIT === */
setStatus('lade TTS...');
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 TTS v2 INIT ===');
console.log('TTS Key: ' + (ttsApiKey ? ttsApiKey.substring(0,8) + '...' : 'FEHLT'));
console.log('Voice ID: ' + (voiceId || 'FEHLT'));
console.log('Model: ' + modelId);
console.log('Page ID: ' + pageId);
}
if (ttsApiKey && voiceId) {
setStatus('bereit');
} else {
var missing = [];
if (!ttsApiKey) missing.push('buddy.ttsapikey');
if (!voiceId) missing.push('buddy.ttsvoiceid');
setStatus(missing.join(' + ') + ' fehlt');
}
watchChat();
});
updateToggleUI();
})(); NoElements {}