// -- Get voices --------------------------------------------------------------- const DEFAULT_VOICE_SOURCE_URLS = [ 'https://aiartes.com/voiceai', 'https://sample-files.com/downloads/audio/wav/voice-sample.wav', 'https://freesound.org/people/Scott%20Simpson/', 'https://lanceblairvo.com/raw-voiceover-samples/', 'https://github.com/yaph/tts-samples/tree/main/mp3', 'https://github.com/jim-schwoebel/voice_datasets', ]; const VOICE_SOURCE_STORAGE_KEY = 'ttsvc-getvoices-sources'; let _voiceSourcePayload = null; let _voiceSourceItems = []; function sourceTextareaValueFromDefaults() { return DEFAULT_VOICE_SOURCE_URLS.join('\n'); } function initGetVoiceSourcesEditor() { const box = $('getvoices-sources'); if (!box || box.dataset.ready) return; box.value = localStorage.getItem(VOICE_SOURCE_STORAGE_KEY) || sourceTextareaValueFromDefaults(); box.dataset.ready = '1'; box.addEventListener('input', () => { localStorage.setItem(VOICE_SOURCE_STORAGE_KEY, box.value); _voiceSourcePayload = null; $('getvoices-status').textContent = 'Source list changed.'; }); } function getEditableVoiceSourceUrls() { initGetVoiceSourcesEditor(); return ($('getvoices-sources')?.value || '') .split(/\r?\n/) .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } function getVoiceSourceItems() { const sources = (_voiceSourcePayload && _voiceSourcePayload.sources) || []; return sources.flatMap(src => (src.items || []).map(item => ({...item, _sourceName: src.name, _sourceHomepage: src.homepage}))); } function voiceSourceSearchText(item) { return [item.name, item.kind, item.category, item.language, item.gender, item.description, item.source, item._sourceName].join(' ').toLowerCase(); } function setOptions(selectId, values, allLabel) { const sel = $(selectId); if (!sel) return; const current = sel.value || 'all'; sel.innerHTML = `` + values.map(value => ``).join(''); sel.value = values.includes(current) ? current : 'all'; } function renderGetVoices() { initGetVoiceSourcesEditor(); const list = $('getvoices-list'); const summary = $('getvoices-summary'); if (!list || !summary) return; const payload = _voiceSourcePayload || {sources:[], total:0, direct_audio:0, errors:[]}; const sources = payload.sources || []; const sourceFilter = $('getvoices-source-filter')?.value || 'all'; const languageFilter = $('getvoices-language-filter')?.value || 'all'; const genderFilter = $('getvoices-gender-filter')?.value || 'all'; const filetypeFilter = $('getvoices-filetype-filter')?.value || 'all'; const q = ($('getvoices-search')?.value || '').trim().toLowerCase(); const directOnly = !!$('getvoices-direct-only')?.checked; _voiceSourceItems = getVoiceSourceItems(); summary.innerHTML = [ [`${payload.total || 0}`, 'Items found'], [`${payload.direct_audio || 0}`, 'Direct audio'], [`${sources.length}`, 'Sources OK'], [`${(payload.errors || []).length}`, 'Errors'], ].map(([value, label]) => `
${escHtml(value)}${escHtml(label)}
`).join(''); setOptions('getvoices-source-filter', sources.map(src => src.id).filter(Boolean), 'Source: all'); const sourceSelect = $('getvoices-source-filter'); if (sourceSelect) { [...sourceSelect.options].forEach(option => { if (option.value === 'all') return; const src = sources.find(s => s.id === option.value); if (src) option.textContent = src.name || src.id; }); } const languages = [...new Set(_voiceSourceItems.map(item => item.language || 'Unknown'))].sort((a, b) => a.localeCompare(b)); const genders = [...new Set(_voiceSourceItems.map(item => item.gender || 'Unknown'))].sort((a, b) => a.localeCompare(b)); const filetypes = [...new Set(_voiceSourceItems.map(item => (item.file_type || (item.direct_audio ? 'audio' : 'page')).toUpperCase()))].sort((a, b) => a.localeCompare(b)); setOptions('getvoices-language-filter', languages, 'Language: all'); setOptions('getvoices-gender-filter', genders, 'Sex: all'); setOptions('getvoices-filetype-filter', filetypes, 'Filetype: all'); let items = _voiceSourceItems.filter(item => { if (sourceFilter !== 'all' && item.source_id !== sourceFilter) return false; if (languageFilter !== 'all' && (item.language || 'Unknown') !== languageFilter) return false; if (genderFilter !== 'all' && (item.gender || 'Unknown') !== genderFilter) return false; const itemFiletype = (item.file_type || (item.direct_audio ? 'audio' : 'page')).toUpperCase(); if (filetypeFilter !== 'all' && itemFiletype !== filetypeFilter) return false; if (directOnly && !item.direct_audio) return false; if (q && !voiceSourceSearchText(item).includes(q)) return false; return true; }); const shown = items.slice(0, 240); const more = items.length - shown.length; if (!shown.length) { const errorText = (payload.errors || []).map(e => `${e.source}: ${e.detail}`).join(' | '); list.innerHTML = `

No matching sources found.${errorText ? ' Source errors: ' + escHtml(errorText) : ''}

`; return; } list.innerHTML = shown.map(item => { const thumb = item.image_url ? `` : `
`; const audio = item.audio_url ? `` : ''; const audioLink = item.audio_url ? `Open audio` : ''; const canGetVoice = !!(item.import_url || item.audio_url); const importUrl = item.import_url || item.audio_url; const getVoice = canGetVoice ? `` : ''; const type = item.file_type ? `${escHtml(String(item.file_type).toUpperCase())}` : ''; const language = item.language ? `${escHtml(item.language)}` : ''; const gender = item.gender ? `${escHtml(item.gender)}` : ''; return `
${thumb}
${escHtml(item.name || 'Untitled')} ${escHtml(item._sourceName || item.source || '')}${item.category ? ' · ' + escHtml(item.category) : ''}
${escHtml(item.kind || '')}${item.description ? ' - ' + escHtml(item.description) : ''}
${audio}
${type}${language}${gender} ${audioLink} Source page
${getVoice ? `` : ''}
`; }).join('') + (more > 0 ? `

${more} more matches. Narrow the search or filters to see them.

` : ''); } async function loadGetVoices(force = false) { initGetVoiceSourcesEditor(); if (_voiceSourcePayload && !force) { renderGetVoices(); return; } const urls = getEditableVoiceSourceUrls(); const list = $('getvoices-list'); if (!urls.length) { if (list) list.innerHTML = '

Add at least one source URL, then scrape again.

'; $('getvoices-status').textContent = 'No sources.'; return; } if (list) list.innerHTML = loadingMarkup('Scraping voice sources', `Fetching ${urls.length} source${urls.length === 1 ? '' : 's'} from the editable list.`, 6); $('getvoices-status').textContent = 'Scraping...'; $('getvoices-refresh-btn').disabled = true; try { const r = await fetch('/api/voice-sources', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({urls}), }); if (!r.ok) { const text = await r.text().catch(() => ''); let message = r.statusText || `HTTP ${r.status}`; try { message = JSON.parse(text).detail || message; } catch (_) { if (text) message = text.slice(0, 160); } throw new Error(message); } _voiceSourcePayload = await r.json(); renderGetVoices(); const errors = (_voiceSourcePayload.errors || []).length; $('getvoices-status').textContent = `${_voiceSourcePayload.total || 0} found${errors ? `, ${errors} source errors` : ''}`; } catch(e) { if (list) list.innerHTML = `

Scrape failed: ${escHtml(e.message)}

Check that the TTS Voice Creator backend is restarted and that at least one source URL is reachable.

`; $('getvoices-status').textContent = 'Scrape failed'; toast('Voice source scrape failed: ' + e.message, 'error'); } finally { $('getvoices-refresh-btn').disabled = false; } } ['getvoices-search', 'getvoices-source-filter', 'getvoices-language-filter', 'getvoices-gender-filter', 'getvoices-filetype-filter', 'getvoices-direct-only'].forEach(id => { const el = $(id); if (el) el.addEventListener(id === 'getvoices-search' ? 'input' : 'change', renderGetVoices); }); $('getvoices-refresh-btn')?.addEventListener('click', () => loadGetVoices(true)); $('getvoices-reset-sources-btn')?.addEventListener('click', () => { const box = $('getvoices-sources'); if (!box) return; box.value = sourceTextareaValueFromDefaults(); localStorage.setItem(VOICE_SOURCE_STORAGE_KEY, box.value); _voiceSourcePayload = null; renderGetVoices(); $('getvoices-status').textContent = 'Source list reset.'; }); const SOURCE_LANGUAGE_CODES = { english:'EN', german:'DE', deutsch:'DE', french:'FR', spanish:'ES', japanese:'JA', korean:'KO', italian:'IT', portuguese:'PT', russian:'RU', arabic:'AR', polish:'PL', dutch:'NL', swedish:'SV', turkish:'TR', hindi:'HI', chinese:'ZH' }; function sourceLanguageCode(language) { const raw = String(language || '').trim(); if (/^[A-Z]{2}$/.test(raw)) return raw; return SOURCE_LANGUAGE_CODES[raw.toLowerCase()] || 'EN'; } function sourceGenderCode(gender, name = '') { const text = `${gender || ''} ${name || ''}`.toLowerCase(); if (/female|woman|girl|\bf\b/.test(text)) return 'F'; if (/male|man|boy|\bm\b/.test(text)) return 'M'; return 'N'; } function suggestedVoiceIdFromSourceName(name, language = 'EN', gender = 'N') { const base = String(name || 'SourceVoice') .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^A-Za-z0-9]+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 48) || 'SourceVoice'; return `${language || 'EN'}_${gender || 'N'}_${base}`; } function setLibAddSourcePreview(meta = {}) { const sourceBox = document.querySelector('.lib-add-source-box'); if (!sourceBox) return; let preview = $('lib-add-source-preview'); if (!preview) { preview = document.createElement('div'); preview.id = 'lib-add-source-preview'; preview.className = 'lib-add-source-preview'; sourceBox.appendChild(preview); } if (!meta.name && !meta.imageUrl) { preview.classList.remove('open'); preview.innerHTML = ''; return; } const image = meta.imageUrl ? `` : '
'; preview.innerHTML = `${image}
${escHtml(meta.name || 'Source voice')}${escHtml([meta.language, meta.gender, meta.kind].filter(Boolean).join(' · ') || 'Source metadata will be saved with the voice')}
`; preview.classList.add('open'); } async function getSourceVoiceInLibrary(meta) { if (!meta.url) { toast('This source has no direct audio URL', 'error'); return; } const lang = sourceLanguageCode(meta.language); const gender = sourceGenderCode(meta.gender, meta.name); const voiceId = suggestedVoiceIdFromSourceName(meta.name, lang, gender); toast(`Importing ${meta.name || 'voice'}…`, 'info'); try { const r = await fetch('/api/quick-import-voice', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({audio_url: meta.url, voice_id: voiceId, transcript: ''}) }); if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); } const d = await r.json(); // Tag the imported voice with its source (host of the source page) + a picture if any let srcTag = 'online'; try { if (meta.pageUrl) srcTag = new URL(meta.pageUrl).hostname.replace(/^www\./, '').split('.')[0] || 'online'; } catch (_) {} if (typeof saveMeta === 'function') { await saveMeta(d.voice_id, { name: meta.name || d.voice_id, tag: srcTag, group: srcTag, origin: 'cloned', gender: (gender || '').toUpperCase(), note: (meta.description || '').slice(0, 180), }).catch(() => {}); } if (meta.imageUrl) { await fetch('/api/voice/picture-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ voice_id: d.voice_id, image_url: meta.imageUrl }), }).catch(() => {}); } _pendingSelectId = d.voice_id; navTo('s-voices'); _libraryLoadPromise = null; await loadVoiceLibrary(); toast(`Saved as ${d.voice_id}`, 'success'); } catch(e) { toast('Import failed: ' + e.message, 'error'); } } $('getvoices-list')?.addEventListener('click', async e => { const getBtn = e.target.closest('.get-source-voice'); if (getBtn) { await getSourceVoiceInLibrary({ url: getBtn.dataset.url || '', name: getBtn.dataset.name || '', imageUrl: getBtn.dataset.image || '', language: getBtn.dataset.language || '', gender: getBtn.dataset.gender || '', kind: getBtn.dataset.kind || '', description: getBtn.dataset.description || '', pageUrl: getBtn.dataset.page || '' }); return; } const btn = e.target.closest('.copy-source-url'); if (!btn) return; await copyText(btn.dataset.url || ''); toast('Source URL copied', 'success'); });