diff --git a/gpt/index.html b/gpt/index.html
index e2ead0f..3f3734f 100644
--- a/gpt/index.html
+++ b/gpt/index.html
@@ -589,6 +589,7 @@
+
@@ -744,6 +745,50 @@ Date|What is today's date?">
+
+
+
+ ENABLE VOICE READOUT
+
+
+
+ READ USER MESSAGES TOO
+
+
+
+
+
+
+
+
+
+
+ 1.00
+
+
+
+
+
+
+
+
+
+ Web Speech API — voices depend on your browser & OS.
+
+
+
@@ -780,6 +825,12 @@ const DEFAULT_CONFIG = {
macroBar: true,
macros: 'Hello|Hello! Who are you?\nHelp|What can you help me with?\nDate|What is today\'s date?',
enabled: false,
+ tts: false,
+ ttsUser: false,
+ ttsVoice: '',
+ ttsRate: 1.0,
+ ttsPitch: 1.0,
+ ttsVol: 1.0,
};
let cfg = { ...DEFAULT_CONFIG };
@@ -817,8 +868,90 @@ const sidebar = document.getElementById('sidebar');
const debugPre = document.getElementById('debugPre');
/* ══════════════════════════════════════════
- INIT APPLY
+ TTS ENGINE
══════════════════════════════════════════ */
+let ttsVoices = [];
+let ttsSpeaking = false;
+
+function ttsLoadVoices() {
+ ttsVoices = speechSynthesis.getVoices();
+ const sel = document.getElementById('ttsVoiceSelect');
+ if (!sel) return;
+ sel.innerHTML = '';
+ ttsVoices.forEach((v, i) => {
+ const opt = document.createElement('option');
+ opt.value = v.name;
+ opt.textContent = `${v.name} (${v.lang})`;
+ if (cfg.ttsVoice && v.name === cfg.ttsVoice) opt.selected = true;
+ sel.appendChild(opt);
+ });
+}
+
+if ('speechSynthesis' in window) {
+ speechSynthesis.onvoiceschanged = ttsLoadVoices;
+ ttsLoadVoices();
+} else {
+ console.warn('Web Speech API not supported.');
+}
+
+function ttsSpeak(text, isUser = false) {
+ if (!cfg.tts) return;
+ if (isUser && !cfg.ttsUser) return;
+ if (!('speechSynthesis' in window)) return;
+
+ // Strip markdown-ish symbols so they don't get read aloud
+ const clean = text
+ .replace(/```[\s\S]*?```/g, 'code block')
+ .replace(/`[^`]+`/g, 'code')
+ .replace(/[*_#>\[\]]/g, '')
+ .replace(/https?:\/\/\S+/g, 'link')
+ .trim();
+ if (!clean) return;
+
+ const utt = new SpeechSynthesisUtterance(clean);
+ const voice = ttsVoices.find(v => v.name === cfg.ttsVoice);
+ if (voice) utt.voice = voice;
+ utt.rate = cfg.ttsRate;
+ utt.pitch = cfg.ttsPitch;
+ utt.volume = cfg.ttsVol;
+
+ utt.onstart = () => { ttsSpeaking = true; updateTtsIndicator(true); };
+ utt.onend = () => { ttsSpeaking = false; updateTtsIndicator(false); };
+ utt.onerror = () => { ttsSpeaking = false; updateTtsIndicator(false); };
+
+ speechSynthesis.cancel(); // stop any ongoing
+ speechSynthesis.speak(utt);
+}
+
+function ttsStop() {
+ if ('speechSynthesis' in window) speechSynthesis.cancel();
+ ttsSpeaking = false;
+ updateTtsIndicator(false);
+}
+
+function ttsTest() {
+ // Re-read config values from sliders directly so test is live
+ const rate = parseFloat(document.getElementById('ttsRateRange')?.value ?? cfg.ttsRate);
+ const pitch = parseFloat(document.getElementById('ttsPitchRange')?.value ?? cfg.ttsPitch);
+ const vol = parseFloat(document.getElementById('ttsVolRange')?.value ?? cfg.ttsVol);
+ const voice = document.getElementById('ttsVoiceSelect')?.value ?? cfg.ttsVoice;
+
+ const utt = new SpeechSynthesisUtterance('Neural Net Terminal online. Voice readout active. All systems nominal.');
+ const v = ttsVoices.find(v => v.name === voice);
+ if (v) utt.voice = v;
+ utt.rate = rate; utt.pitch = pitch; utt.volume = vol;
+ speechSynthesis.cancel();
+ speechSynthesis.speak(utt);
+}
+
+function updateTtsIndicator(active) {
+ const el = document.getElementById('ttsIndicator');
+ if (!el) return;
+ el.style.opacity = active ? '1' : '0.3';
+ el.style.animation = active ? 'pulse 1s ease-in-out infinite' : 'none';
+}
+
+
applyTheme();
applyInterface();
updateStatus();