// ── STT -> TTS ─────────────────────────────────────────────────────────── let sttTtsSourceId = null; let sttTtsOutputBlob = null; let _sttBackends = []; let sttTtsRecorder = null; let sttTtsRecordStream = null; let sttTtsRecordChunks = []; let sttTtsRecordTimer = null; let sttTtsRecordSecs = 0; function sttTtsSelectedSttBackend() { return $('stt-tts-stt-backend')?.value || 'configured'; } function sttBackendOptionHtml(selected = 'configured') { if (!_sttBackends.length) return ''; const preferred = _sttBackends.some(b => b.id === selected && b.available) ? selected : (_sttBackends.find(b => b.available)?.id || selected); return _sttBackends.map(b => { const suffix = b.available ? '' : ' (unavailable)'; const disabled = b.available ? '' : ' disabled'; return ``; }).join(''); } function updateSttBackendHelp() { const selected = sttTtsSelectedSttBackend(); const b = _sttBackends.find(item => item.id === selected) || _sttBackends.find(item => item.available) || null; const help = $('stt-tts-stt-help'); if (!help) return; if (!b) { help.textContent = 'No STT engine status loaded yet.'; return; } help.innerHTML = sttBackendHelpHtml(b); } async function refreshSttBackends(selected = '') { try { const d = await fetch('/api/stt-backends').then(r => r.json()); _sttBackends = (d.backends || []).filter(b => b && b.id); } catch (_) { _sttBackends = []; } ['stt-tts-stt-backend', 'clone-stt-backend'].forEach(id => { const sel = $(id); if (!sel) return; const prev = selected || sel.value || 'configured'; sel.innerHTML = sttBackendOptionHtml(prev); sel.disabled = !_sttBackends.some(b => b.available); }); updateSttBackendHelp(); if (typeof window.updateStatusBar === 'function') window.updateStatusBar(); if (typeof window.refreshStatusBarEngines === 'function') window.refreshStatusBarEngines(); } function sttTtsSelectedBackend() { return $('stt-tts-backend-select')?.value || ''; } function sttTtsDownload(blob, name) { if (!blob) return; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); } async function sttTtsUploadFile(file) { if (!file) return; $('stt-tts-source-status').textContent = 'Uploading ' + file.name + '...'; sttTtsSourceId = null; sttTtsOutputBlob = null; $('stt-tts-transcribe-btn').disabled = true; $('stt-tts-copy-preview-btn').disabled = true; $('stt-tts-save-mp3-btn').disabled = true; $('stt-tts-save-wav-btn').disabled = true; const fd = new FormData(); fd.append('file', file); try { const r = await fetch('/api/upload', {method:'POST', body:fd}); if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); } const d = await r.json(); sttTtsSourceId = d.id; const audio = $('stt-tts-source-audio'); audio.src = '/api/audio/' + encodeURIComponent(d.id); audio.style.display = ''; $('stt-tts-source-status').textContent = `${d.filename || file.name} loaded (${Number(d.duration || 0).toFixed(1)} s).`; $('stt-tts-transcribe-btn').disabled = false; toast('Speech audio loaded', 'success'); } catch (e) { $('stt-tts-source-status').textContent = 'Upload failed.'; toast('STT source upload failed: ' + e.message, 'error'); } } async function sttTtsFetchVoices() { const backend = sttTtsSelectedBackend(); if (!backend) throw new Error('No available TTS backend'); const rawVoices = await fetch('/api/tts-voices?backend=' + encodeURIComponent(backend)).then(r => r.json()); let voices = Array.isArray(rawVoices) ? rawVoices : []; if (shouldFilterBackendVoices(backend)) { const activeIds = await activeLibraryVoiceIds(); voices = voices.filter(v => activeIds.has(backendVoiceId(v))); } const sel = $('stt-tts-voice-select'), prev = sel.value; sel.innerHTML = ''; voices.forEach(v => { const id = backendVoiceId(v); const opt = document.createElement('option'); opt.value = opt.textContent = id; sel.appendChild(opt); }); if (prev && voices.some(v => backendVoiceId(v) === prev)) sel.value = prev; return voices.length; } $('stt-tts-file')?.addEventListener('change', async () => { const input = $('stt-tts-file'); if (input.files && input.files.length) await sttTtsUploadFile(input.files[0]); input.value = ''; }); $('stt-tts-refresh-stt-btn')?.addEventListener('click', async () => { const btn = $('stt-tts-refresh-stt-btn'); btn.disabled = true; try { await refreshSttBackends(sttTtsSelectedSttBackend()); toast('STT engines refreshed', 'success'); } finally { btn.disabled = false; } }); $('stt-tts-stt-backend')?.addEventListener('change', updateSttBackendHelp); function sttTtsSetRecording(on) { $('stt-tts-rec-start').disabled = on; $('stt-tts-rec-stop').disabled = !on; } function sttTtsStopTracks() { if (sttTtsRecordStream) sttTtsRecordStream.getTracks().forEach(t => t.stop()); sttTtsRecordStream = null; } $('stt-tts-rec-start')?.addEventListener('click', async () => { try { sttTtsRecordStream = await requestMicrophoneStream(); sttTtsRecordChunks = []; sttTtsRecordSecs = 0; $('stt-tts-rec-time').textContent = '0:00'; $('stt-tts-source-status').textContent = 'Recording...'; sttTtsSetRecording(true); sttTtsRecordTimer = setInterval(() => { sttTtsRecordSecs++; $('stt-tts-rec-time').textContent = Math.floor(sttTtsRecordSecs / 60) + ':' + String(sttTtsRecordSecs % 60).padStart(2, '0'); }, 1000); sttTtsRecorder = new MediaRecorder(sttTtsRecordStream, {audioBitsPerSecond: 256000}); sttTtsRecorder.ondataavailable = e => { if (e.data.size) sttTtsRecordChunks.push(e.data); }; sttTtsRecorder.onstop = async () => { clearInterval(sttTtsRecordTimer); sttTtsRecordTimer = null; sttTtsSetRecording(false); sttTtsStopTracks(); const mime = sttTtsRecorder.mimeType || 'audio/webm'; const blob = new Blob(sttTtsRecordChunks, {type:mime}); const ext = mime.includes('ogg') ? '.ogg' : '.webm'; if (!blob.size) { $('stt-tts-source-status').textContent = 'Recording was empty.'; toast('Recording was empty', 'error'); return; } await sttTtsUploadFile(new File([blob], 'stt-recording' + ext, {type:mime})); }; sttTtsRecorder.start(100); toast('Recording started', 'success'); } catch (e) { sttTtsSetRecording(false); sttTtsStopTracks(); const message = await microphoneErrorMessage(e); $('stt-tts-source-status').textContent = message; toast(message, 'error'); } }); $('stt-tts-rec-stop')?.addEventListener('click', () => { if (sttTtsRecorder && sttTtsRecorder.state !== 'inactive') sttTtsRecorder.stop(); }); $('stt-tts-transcribe-btn')?.addEventListener('click', async () => { if (!sttTtsSourceId) { toast('Load speech audio first', 'error'); return; } const btn = $('stt-tts-transcribe-btn'); btn.disabled = true; $('stt-tts-source-status').textContent = 'Transcribing...'; try { const r = await fetch('/api/transcribe', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id:sttTtsSourceId, backend:sttTtsSelectedSttBackend()})}); if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); } const d = await r.json(); $('stt-tts-text').value = d.text || ''; $('stt-tts-copy-preview-btn').disabled = !(d.text || '').trim(); const used = d.backend ? ' via ' + d.backend : ''; $('stt-tts-source-status').textContent = 'Transcription ready' + used + '.'; if (typeof updateRefineButtonState === 'function') updateRefineButtonState(); toast('Transcription ready', 'success'); } catch (e) { $('stt-tts-source-status').textContent = 'Transcription failed.'; toast('STT failed: ' + e.message, 'error'); } finally { btn.disabled = false; } }); $('stt-tts-copy-preview-btn')?.addEventListener('click', () => { const text = $('stt-tts-text').value.trim(); if (!text) return; $('preview-text-area').value = text; switchTab('generation'); toast('Copied transcription to TTS Generation', 'success'); }); $('stt-tts-backend-select')?.addEventListener('change', () => { $('stt-tts-voice-select').innerHTML = ''; sttTtsOutputBlob = null; $('stt-tts-save-mp3-btn').disabled = true; $('stt-tts-save-wav-btn').disabled = true; updateBackendHelp(); }); $('stt-tts-fetch-voices-btn')?.addEventListener('click', async () => { const btn = $('stt-tts-fetch-voices-btn'); btn.disabled = true; try { const count = await sttTtsFetchVoices(); toast('Fetched ' + count + ' voices', 'success'); } catch (e) { toast('Fetch failed: ' + e.message, 'error'); } finally { btn.disabled = false; } }); $('stt-tts-generate-btn')?.addEventListener('click', async () => { const backend = sttTtsSelectedBackend(); const voice = $('stt-tts-voice-select').value; const text = $('stt-tts-text').value.trim(); const instruct = $('stt-tts-style-instruction').value.trim(); if (!backend) { toast('No available TTS backend', 'error'); return; } if (!voice) { toast('Select a TTS voice', 'error'); return; } if (!text) { toast('Transcribe or enter text first', 'error'); return; } const btn = $('stt-tts-generate-btn'); btn.disabled = true; $('stt-tts-save-mp3-btn').disabled = true; $('stt-tts-save-wav-btn').disabled = true; try { const source = await createTtsAudioSource(voice, text, backend, $('stt-tts-playback-mode').value, instruct); sttTtsOutputBlob = source.blob; const audio = $('stt-tts-output-audio'); audio.src = source.url; audio.style.display = ''; await audio.play(); $('stt-tts-save-mp3-btn').disabled = false; $('stt-tts-save-wav-btn').disabled = source.streaming; toast(source.streaming ? 'Streaming synthesized speech' : 'Synthesized speech ready', 'success'); } catch (e) { toast('TTS failed: ' + e.message, 'error'); } finally { btn.disabled = false; } }); $('stt-tts-save-mp3-btn')?.addEventListener('click', async () => { const backend = sttTtsSelectedBackend(); const voice = $('stt-tts-voice-select').value; const text = $('stt-tts-text').value.trim(); const instruct = $('stt-tts-style-instruction').value.trim(); if (!backend || !voice || !text) return; const btn = $('stt-tts-save-mp3-btn'); btn.disabled = true; try { const blob = await fetchTtsPreviewBlob(voice, text, 'mp3', instruct, backend); sttTtsDownload(blob, (voice || 'stt_tts') + '_stt_tts.mp3'); toast('MP3 saved', 'success'); } catch (e) { toast('MP3 save failed: ' + e.message, 'error'); } finally { btn.disabled = false; } }); $('stt-tts-save-wav-btn')?.addEventListener('click', () => { if (!sttTtsOutputBlob) return; sttTtsDownload(sttTtsOutputBlob, ($('stt-tts-voice-select').value || 'stt_tts') + '_stt_tts.wav'); });