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.
+
+
+
+
+
+
+
+
11
+
+
ElevenLabs Voice Library
+
Browse 12 000+ shared voices and import previews directly into Clone. elevenlabs.io ↗
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 */