From ff783a8ab9fd0c2477d0c31f05980d40c6430b3e Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 26 May 2026 13:14:40 +0200 Subject: [PATCH] Add ElevenLabs Voice Library browser to Get Voices Online MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend proxy at /api/elevenlabs/voices forwarding to ElevenLabs shared-voices API - elevenlabs_api_key added to settings (free key unlocks 12 000+ voices; 3 without key) - Category pills: All, Featured, Professional, Narration, Conversational, News, Characters, Meditation, Gaming, Training - Language filter with most common European languages + Arabic/Hindi/ZH/JA/KO - Gender and age filters, debounced search, pagination (prev/next) - Voice cards: colored avatar, name, language/gender/age/use-case tags, description, ▶ Play and ↓ Clone buttons - Play button streams preview MP3 directly; Clone imports the audio into the Clone a Voice flow Co-Authored-By: Claude Sonnet 4.6 --- server.py | 25 ++++- static/app.js | 184 ++++++++++++++++++++++++++++++++++ static/sections/s-studio.html | 103 +++++++++++++++++++ static/style.css | 64 +++++++++++- 4 files changed, 371 insertions(+), 5 deletions(-) diff --git a/server.py b/server.py index 4a031fa..34c5f83 100644 --- a/server.py +++ b/server.py @@ -229,7 +229,7 @@ _SETTINGS_KEYS = { "output_dir", "voices_scan_dir", "voice_design_url", "customvoice_url", "nvidia_router_url", "nvidia_tts_url", "nvidia_asr_url", "nvidia_clone_url", "nvidia_zeroshot_url", "nvidia_flow_url", - "whisper_api_key", "tts_api_key", "voice_design_api_key", + "whisper_api_key", "tts_api_key", "voice_design_api_key", "elevenlabs_api_key", "tts_stability_enabled", "tts_extra_params", "tts_extra_params_by_backend", } @@ -2721,6 +2721,29 @@ def _fetch_backend_voices(settings: dict, backend: str) -> list: return [] +# ── ElevenLabs shared voice library proxy ──────────────────────────────────── + +@app.get("/api/elevenlabs/voices") +async def elevenlabs_shared_voices(request: Request): + settings = _load_settings() + api_key = (settings.get("elevenlabs_api_key") or "").strip() + _allowed = {"page_size", "page", "language", "gender", "age", "accent", + "use_case", "category", "search", "featured"} + params: dict = {k: v for k, v in request.query_params.items() if k in _allowed} + params.setdefault("page_size", "24") + headers = {"xi-api-key": api_key} if api_key else {} + try: + r = requests.get( + "https://api.elevenlabs.io/v1/shared-voices", + params=params, + headers=headers, + timeout=15, + ) + return r.json() + except requests.exceptions.RequestException as exc: + raise HTTPException(502, f"ElevenLabs API error: {exc}") + + @app.get("/api/tts-voices") async def tts_voices(backend: str = "voice_clone"): return _fetch_backend_voices(_load_settings(), backend) diff --git a/static/app.js b/static/app.js index 360ca79..1b21f99 100644 --- a/static/app.js +++ b/static/app.js @@ -5906,4 +5906,188 @@ loadSettings().then(() => { renderIntegrationSnippets(); if (!localStorage.getItem(SETTINGS_SEEN_KEY)) openSettings(true); }).catch(e => status('Settings load failed: ' + e.message)); + +// ── ElevenLabs Voice Library Browser ────────────────────────────────────── + +(function initElevenLabsBrowser() { + if (!$('el-browser-card')) return; + + let _elPage = 0, _elHasMore = false, _elTotal = 0; + let _elAudio = null, _elAudioBtn = null; + let _elFilters = {}; + let _elSearchTimer; + + const _elHue = str => { + let h = 0; + for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) & 0xffffffff; + return Math.abs(h) % 360; + }; + const _elEsc = s => String(s || '') + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + + // Load key state from settings + function elLoadKey() { + fetch('/api/settings').then(r => r.json()).then(s => { + const key = (s.elevenlabs_api_key || '').trim(); + const inp = $('el-api-key'), st = $('el-key-status'); + if (inp) inp.placeholder = key ? '••••••••••••••• (key saved)' : 'ElevenLabs API key (free — unlocks full library)…'; + if (st) { st.textContent = key ? '✓ Key active' : 'No key — max 3 results'; st.className = 'el-key-status ' + (key ? 'el-key-ok' : 'el-key-none'); } + elFetch(); + }).catch(() => elFetch()); + } + + $('el-key-save').addEventListener('click', () => { + const inp = $('el-api-key'); + const val = (inp?.value || '').trim(); + if (!val || val.startsWith('•')) { _elPage = 0; elFetch(); return; } + fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ elevenlabs_api_key: val }) }) + .then(() => { + const st = $('el-key-status'); + if (st) { st.textContent = '✓ Key saved'; st.className = 'el-key-status el-key-ok'; } + if (inp) { inp.value = ''; inp.placeholder = '••••••••••••••• (key saved)'; } + _elPage = 0; elFetch(); + }); + }); + $('el-api-key')?.addEventListener('keydown', e => { if (e.key === 'Enter') $('el-key-save')?.click(); }); + + $('el-cats')?.querySelectorAll('.el-cat').forEach(pill => { + pill.addEventListener('click', () => { + $('el-cats').querySelectorAll('.el-cat').forEach(p => p.classList.remove('active')); + pill.classList.add('active'); + _elFilters = {}; + for (const attr of pill.getAttributeNames()) { + if (attr.startsWith('data-el-') && pill.getAttribute(attr)) + _elFilters[attr.slice('data-el-'.length)] = pill.getAttribute(attr); + } + _elPage = 0; elFetch(); + }); + }); + + ['el-lang', 'el-gender', 'el-age'].forEach(id => { + $(id)?.addEventListener('change', () => { _elPage = 0; elFetch(); }); + }); + $('el-search')?.addEventListener('input', () => { + clearTimeout(_elSearchTimer); + _elSearchTimer = setTimeout(() => { _elPage = 0; elFetch(); }, 420); + }); + $('el-fetch')?.addEventListener('click', () => { _elPage = 0; elFetch(); }); + $('el-prev')?.addEventListener('click', () => { if (_elPage > 0) { _elPage--; elFetch(); } }); + $('el-next')?.addEventListener('click', () => { if (_elHasMore) { _elPage++; elFetch(); } }); + + function elParams() { + const p = new URLSearchParams({ page_size: '24', page: String(_elPage) }); + const lang = $('el-lang')?.value, gender = $('el-gender')?.value, age = $('el-age')?.value; + const search = ($('el-search')?.value || '').trim(); + if (lang) p.set('language', lang); + if (gender) p.set('gender', gender); + if (age) p.set('age', age); + if (search) p.set('search', search); + Object.entries(_elFilters).forEach(([k, v]) => { if (v) p.set(k, v); }); + return p.toString(); + } + + async function elFetch() { + const grid = $('el-grid'), sb = $('el-status-bar'), st = $('el-status-text'); + if (grid) grid.innerHTML = '
Loading voices…
'; + try { + const res = await fetch('/api/elevenlabs/voices?' + elParams()); + const data = await res.json(); + if (data.detail) { + if (grid) grid.innerHTML = `
⚠ ${_elEsc(data.detail?.message || JSON.stringify(data.detail))}
`; + if (sb) sb.hidden = true; + return; + } + _elHasMore = !!data.has_more; + _elTotal = data.total_count || 0; + const voices = data.voices || []; + if (st) { + const cap = voices.length < 24 && _elTotal > 3 ? ' · \u{1F511} Add your free API key for full access' : ''; + st.innerHTML = `${_elTotal.toLocaleString()} voices · showing ${voices.length} · page ${_elPage + 1}${cap}`; + } + if (sb) sb.hidden = false; + if (!voices.length) { + if (grid) grid.innerHTML = '
No voices found — try different filters.
'; + } else { + if (grid) { grid.innerHTML = voices.map(elCard).join(''); } + grid?.querySelectorAll('.el-play').forEach(b => b.addEventListener('click', () => elPlay(b))); + grid?.querySelectorAll('.el-clone').forEach(b => b.addEventListener('click', () => elImport(b))); + } + const pager = $('el-pager'); + if (pager) { + pager.hidden = _elPage === 0 && !_elHasMore; + const pb = $('el-prev'), nb = $('el-next'), pi = $('el-pager-info'); + if (pb) pb.disabled = _elPage === 0; + if (nb) nb.disabled = !_elHasMore; + if (pi) pi.textContent = `Page ${_elPage + 1}${_elTotal > 24 ? ' of ~' + Math.ceil(_elTotal / 24) : ''}`; + } + } catch (e) { + if (grid) grid.innerHTML = `
⚠ ${_elEsc(e.message)}
`; + } + } + + function elCard(v) { + const hue = _elHue(v.voice_id || v.name || ''); + const letter = _elEsc((v.name || '?')[0].toUpperCase()); + const lang = (v.language || '').toUpperCase(); + const g = v.gender === 'female' ? 'F' : v.gender === 'male' ? 'M' : (v.gender || ''); + const age = (v.age || '').replace(/_/g, ' '); + const uc = (v.use_case || '').replace(/_/g, ' '); + const clones = v.cloned_by_count || 0; + const cl = clones >= 1000 ? (clones / 1000).toFixed(1) + 'k' : clones > 0 ? String(clones) : ''; + const desc = (v.description || '').slice(0, 88) + ((v.description || '').length > 88 ? '…' : ''); + const prev = _elEsc(v.preview_url || ''); + const nm = _elEsc(v.name || ''); + const tags = [ + lang ? `${lang}` : '', + g ? `${g}` : '', + age ? `${_elEsc(age)}` : '', + uc ? `${_elEsc(uc)}` : '', + (v.accent && v.accent !== 'standard') ? `${_elEsc(v.accent)}` : '', + v.free_users_allowed ? 'Free' : '', + v.featured ? '' : '', + cl ? `↓ ${cl}` : '', + ].join(''); + return `
+
${letter}
+
+
${nm}
+
${tags}
+ ${desc ? `
${_elEsc(desc)}
` : ''} +
+
+ + ${prev ? `` : ''} +
+
`; + } + + function elPlay(btn) { + const url = btn.dataset.preview; + if (!url) return; + if (_elAudio && _elAudioBtn === btn) { + _elAudio.paused ? _elAudio.play() : _elAudio.pause(); + btn.textContent = _elAudio.paused ? '▶' : '⏸'; + return; + } + if (_elAudio) { _elAudio.pause(); if (_elAudioBtn) _elAudioBtn.textContent = '▶'; } + _elAudio = new Audio(url); + _elAudioBtn = btn; + btn.textContent = '⏸'; + _elAudio.play().catch(() => { btn.textContent = '▶'; _elAudio = null; _elAudioBtn = null; }); + _elAudio.addEventListener('ended', () => { btn.textContent = '▶'; _elAudio = null; _elAudioBtn = null; }); + } + + function elImport(btn) { + const url = btn.dataset.preview; + if (!url) return; + if (typeof navTo === 'function') navTo('s-clone'); + setTimeout(() => { + const inp = $('lib-add-url'), btn2 = $('lib-add-url-btn'); + if (inp) { inp.value = url; inp.dispatchEvent(new Event('input')); } + if (btn2) btn2.click(); + }, 120); + } + + elLoadKey(); +})(); loadVoiceLibrary().then(renderIntegrationSnippets).catch(e => status('Voice library load failed: ' + e.message)); diff --git a/static/sections/s-studio.html b/static/sections/s-studio.html index 0606b61..36c88c1 100644 --- a/static/sections/s-studio.html +++ b/static/sections/s-studio.html @@ -34,3 +34,106 @@

Click Scrape sources to fetch Aiartes VoiceAI clips, yaph/tts-samples MP3 files, and the jim-schwoebel voice dataset index.

+ + +
+ + +
+
+ +
+

ElevenLabs Voice Library

+

Browse 12 000+ shared voices and import previews directly into Clone. elevenlabs.io ↗

+
+
+
+
+ + +
+ No key — max 3 results + Get free key ↗ +
+
+ + +
+ + + + + + + + + + +
+ + +
+ + + + + +
+ + + + + +
+
Use the filters above and click Browse — or enter your API key for full access to 12 000+ voices.
+
+ + + + +
diff --git a/static/style.css b/static/style.css index a093adf..6b69738 100644 --- a/static/style.css +++ b/static/style.css @@ -514,7 +514,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .vl-row.edit-open .vr-optimizer { display: block; } .optimizer-grid { display: grid; grid-template-columns: minmax(280px,1.2fr) minmax(280px,1fr); gap: 12px; align-items: start; } .opt-group { box-shadow: var(--shadow); margin-top: 20px; margin-right: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); padding: 10px; min-width: 0; } -.opt-group-title { font-size: 14px; font-weight: 800; color: var(--accent); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 8px; } +.opt-group-title { font-size: 14px; font-weight: 800; color: var(--accent); text-transform: uppercase; letter-spacing: .08em;} .opt-group-note { font-size: 12px; color: var(--subtext); line-height: 1.4; margin-top: 7px; } .opt-compare-panel { grid-column: 1 / -1; } .opt-compare-grid { display: grid; grid-template-columns: repeat(2, minmax(240px,1fr)); gap: 10px; } @@ -1139,9 +1139,9 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami } .inspector-body .opt-group-title { display: flex; align-items: center; gap: 8px; - padding: 9px 14px; font-size: 10.5px; font-weight: 700; letter-spacing: .05em; + padding: 10px 10px; font-size: 14px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--subtext); - background: var(--panel); cursor: pointer; user-select: none; + cursor: pointer; user-select: none; transition: background .15s; } .inspector-body .opt-group-title:hover { background: var(--border); } @@ -1162,7 +1162,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .inspector-body .opt-group .opt-group-body > textarea { display: block; width: 100%; box-sizing: border-box; padding: 10px 14px; margin: 0; background: transparent; border: none; outline: none; resize: vertical; - font-size: 13px; color: var(--text); font-family: inherit; min-height: 80px; + font-size: 16px; color: var(--text); font-family: inherit; min-height: 80px; } /* Section order */ @@ -1434,4 +1434,60 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .llm-howto-body p { margin: 0; color: var(--subtext); } .llm-howto-body em { font-style: normal; font-weight: 600; color: var(--text); } +/* ── ElevenLabs Voice Browser ──────────────────────────────────────────────── */ +.el-browser { padding: 0; overflow: hidden; } +.el-head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 14px; padding: 16px 20px 14px; border-bottom: 1px solid var(--border); } +.el-brand { display: flex; align-items: flex-start; gap: 12px; flex: 1; min-width: 200px; } +.el-brand-logo { display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: 9px; background: #131314; color: #fff; font-weight: 800; font-size: 13px; letter-spacing: -0.5px; flex-shrink: 0; margin-top: 3px; } +.el-brand h2 { margin: 0 0 3px; font-size: 14px; font-weight: 700; } +.el-brand p { margin: 0; font-size: 12px; } +.el-key-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } +.el-key-field { display: flex; gap: 6px; } +.el-key-field input { width: 230px; font-size: 12px; padding: 5px 9px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-family: monospace; } +.el-key-btn { font-size: 12px; padding: 5px 12px; } +.el-key-status { font-size: 11px; font-weight: 600; } +.el-key-ok { color: #38a169; } +.el-key-none { color: var(--subtext); } + +.el-cats { display: flex; flex-wrap: wrap; gap: 6px; padding: 11px 20px; border-bottom: 1px solid var(--border); } +.el-cat { padding: 4px 13px; border-radius: 20px; border: 1px solid var(--border); background: transparent; color: var(--text); font-size: 12px; cursor: pointer; transition: border-color .15s, color .15s, background .15s; white-space: nowrap; } +.el-cat:hover { border-color: var(--accent); color: var(--accent); } +.el-cat.active { background: var(--accent); border-color: var(--accent); color: #fff; } + +.el-filters { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 20px; border-bottom: 1px solid var(--border); align-items: center; } +.el-search { flex: 1; min-width: 130px; font-size: 13px; padding: 5px 9px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-family: inherit; } +.el-filters select { font-size: 12px; padding: 5px 8px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); font-family: inherit; } + +.el-status-bar { padding: 6px 20px; background: var(--panel); border-bottom: 1px solid var(--border); font-size: 12px; color: var(--subtext); } + +.el-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(272px, 1fr)); } +.el-grid > .el-loading, +.el-grid > .el-error, +.el-grid > .el-empty, +.el-grid > .el-idle { grid-column: 1/-1; padding: 36px 20px; text-align: center; color: var(--subtext); font-size: 13px; } +.el-spinner { display: inline-block; width: 13px; height: 13px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: elSpin .75s linear infinite; vertical-align: middle; margin-right: 6px; } +@keyframes elSpin { to { transform: rotate(360deg); } } +.el-error { color: #e53e3e !important; } + +.el-card-voice { display: flex; align-items: flex-start; gap: 10px; padding: 11px 13px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); transition: background .12s; } +.el-card-voice:hover { background: var(--panel); } +.el-vc-av { flex-shrink: 0; width: 34px; height: 34px; border-radius: 50%; background: hsl(var(--elh, 220), 52%, 52%); color: #fff; font-weight: 700; font-size: 15px; display: flex; align-items: center; justify-content: center; margin-top: 3px; } +.el-vc-body { flex: 1; min-width: 0; } +.el-vc-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px; } +.el-vc-tags { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: 4px; } +.el-tag { font-size: 10px; padding: 1px 5px; border-radius: 3px; background: var(--panel); border: 1px solid var(--border); color: var(--subtext); white-space: nowrap; } +.el-lang { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 700; } +.el-uc { color: var(--accent); border-color: var(--accent); background: transparent; } +.el-free { color: #38a169; border-color: #38a169; background: transparent; } +.el-feat { color: #d69e2e; border-color: #d69e2e; background: transparent; } +.el-clones { opacity: .7; } +.el-vc-desc { font-size: 11px; color: var(--subtext); line-height: 1.4; } +.el-vc-btns { display: flex; flex-direction: column; gap: 5px; flex-shrink: 0; padding-top: 2px; } +.el-play, .el-clone { border: 1px solid var(--border); border-radius: 5px; background: var(--surface); color: var(--text); font-size: 11px; padding: 4px 8px; cursor: pointer; white-space: nowrap; transition: border-color .15s, color .15s; font-family: inherit; } +.el-play:hover { border-color: var(--accent); color: var(--accent); } +.el-clone:hover { border-color: #38a169; color: #38a169; } + +.el-pager { display: flex; align-items: center; gap: 12px; padding: 12px 20px; border-top: 1px solid var(--border); } +.el-pager-info { flex: 1; text-align: center; font-size: 12px; color: var(--subtext); } + /* Bottom action buttons */