tts-voice-creator-clone-and.../static/js/stt.js
mARTin-B78 0b3109cbf4 UX: faster load, skeleton fix, About cleanup, name hint, Connect Apps docs
Loader:
- Post-init modules (engines, ai-backends, generation, conversation) now
  load AFTER the skeleton is removed instead of before. The UI is visible
  ~500 ms sooner on average; those four modules load while the user is
  already browsing Voices / Clone / Design.
- Removed the sttReady event approach that was triggering a duplicate
  /api/stt-backends call; init.js already populates all STT selects once
  on startup.

Skeleton:
- Replaced the card-grid placeholder with a two-column workbench skeleton
  (voice list rows on the left + inspector placeholder on the right) that
  matches the real My Voices layout.

Connect Apps:
- MCP Server, /speak REST endpoint, and Global hotkey daemon sections
  moved from Settings → About to Connect Apps, where they belong.
- About page now has GitHub + Releases links instead.

Clone section:
- Added a hint note beneath the sample-text textarea reminding the user
  to replace the placeholder name (Sam / Alex / Marco …) with their own.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 19:19:20 +02:00

290 lines
11 KiB
JavaScript

// ── 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 '<option value="configured">Configured Whisper/STT</option>';
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 `<option value="${escHtml(b.id)}" ${b.id === preferred ? 'selected' : ''}${disabled}>${escHtml(b.label + suffix)}</option>`;
}).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();
}
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 = '<option value="">-- select after fetch --</option>';
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 = '<option value="">-- select after fetch --</option>';
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');
});