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();
// ElevenLabs' API returns language as a lowercase ISO code ("fr", "de"),
// not the uppercase form this used to require — the case-sensitive regex
// rejected it, and the name-lookup table below only has full language
// NAMES as keys ("french"), not ISO codes, so it silently fell through to
// the 'EN' default for every non-English ElevenLabs import (confirmed
// live: a voice tagged "FR" throughout the browse UI still imported as
// EN_...). Match the 2-letter code regardless of case.
if (/^[A-Za-z]{2}$/.test(raw)) return raw.toUpperCase();
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');
});