// ── Benchmark Voice Picker ─────────────────────────────────────────────── // Searchable benchmark voice selector with avatars plus duration/speed stats. (function () { 'use strict'; const pickers = {}; function esc(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function norm(value) { if (typeof value === 'string') return { id: value, label: value, meta: null, raw: value }; const id = value?.id || value?.voice || value?.name || value?.voice_id || ''; const hasExplicitMeta = Object.prototype.hasOwnProperty.call(value || {}, 'meta'); const looksLikeVoice = !!(value?.display_name || value?.duration || value?.benchmark || value?.has_picture || value?.transcript); return { id, label: value?.label || value?.display_name || value?.name || value?.voice || id, meta: hasExplicitMeta ? value.meta : (looksLikeVoice ? value : null), raw: value, }; } function libraryVoice(id) { return (window._voices || []).find(v => v && v.id === id) || null; } function metaFor(item) { return item?.meta || libraryVoice(item?.id) || item?.raw || null; } function langColor(lang, id) { const str = String(lang || id || '').toLowerCase(); if (str.startsWith('de')) return '#3b82f6'; if (str.startsWith('en')) return '#10b981'; if (str.startsWith('fr')) return '#8b5cf6'; if (str.startsWith('es')) return '#f59e0b'; if (str.startsWith('it')) return '#ef4444'; if (str.startsWith('zh')) return '#ec4899'; if (str.startsWith('ja')) return '#f97316'; const palette = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#06b6d4', '#84cc16']; let h = 0; for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0; return palette[h % palette.length]; } function avatarHtml(item, size) { const id = item?.id || ''; const meta = metaFor(item); const s = `${size}px`; const r = `${Math.round(size / 2)}px`; if (meta?.has_picture) { return ``; } const icon = window.voiceAvatarIcon ? window.voiceAvatarIcon(meta?.avatar, size) : null; if (icon) return icon.replace(/vp-avatar/g, 'bvp-avatar'); const color = langColor(meta?.lang || meta?.language, id); const init = (item?.label || id || '?').trim()[0]?.toUpperCase() || '?'; return `${esc(init)}`; } function numeric(value) { const n = Number(value); return Number.isFinite(n) ? n : null; } function statsFor(item) { const meta = metaFor(item) || {}; const b = meta.benchmark || item?.benchmark || {}; const parts = []; const duration = numeric(meta.duration ?? item?.duration); const speed = numeric(b.speed ?? b.avg_speed ?? meta.speed); const rtf = numeric(b.rtf ?? b.avg_rtf ?? meta.rtf); const elapsed = numeric(b.elapsed_sec ?? b.avg_elapsed_sec); if (duration != null && duration > 0) parts.push(`${duration.toFixed(1)}s`); if (speed != null && speed > 0) parts.push(`${speed.toFixed(2)}x`); else if (rtf != null && rtf > 0) parts.push(`RTF ${rtf.toFixed(2)}`); if (!parts.length && elapsed != null && elapsed > 0) parts.push(`${elapsed.toFixed(1)}s TTS`); if (!parts.length && b.ok === false) parts.push('benchmark failed'); return parts.join(' · '); } function searchText(item) { const meta = metaFor(item) || {}; return [ item?.id, item?.label, meta.display_name, meta.name, meta.lang, meta.language, Array.isArray(meta.tags) ? meta.tags.join(' ') : meta.tags, statsFor(item), ].filter(Boolean).join(' ').toLowerCase(); } function optionLabel(item) { const stats = statsFor(item); return `${item.label || item.id}${stats ? ` (${stats})` : ''}`; } function renderTrigger(picker) { const value = picker.select.value; const item = picker.items.find(v => v.id === value); const content = picker.trigger.querySelector('.bvp-trigger-content'); if (!item) { content.innerHTML = `${esc(picker.placeholder)}`; return; } const stats = statsFor(item); content.innerHTML = `${avatarHtml(item, 24)}${esc(item.label || item.id)}${stats ? `${esc(stats)}` : ''}`; } function renderList(picker, filter) { const q = String(filter || '').trim().toLowerCase(); const items = q ? picker.items.filter(item => searchText(item).includes(q)) : picker.items; if (!items.length) { picker.list.innerHTML = `
${esc(q ? 'No voices found' : picker.empty)}
`; return; } picker.list.innerHTML = items.map(item => { const meta = metaFor(item) || {}; const stats = statsFor(item); const sub = [meta.lang || meta.language, meta.file_type].filter(Boolean).join(' · '); return ``; }).join(''); } function close(picker) { if (!picker) return; picker.drop.hidden = true; picker.drop.style.display = 'none'; picker.trigger.classList.remove('open'); } function closeAll(except) { Object.values(pickers).forEach(p => { if (p !== except) close(p); }); } function open(picker) { closeAll(picker); picker.drop.hidden = false; picker.drop.style.display = ''; picker.trigger.classList.add('open'); picker.search.value = ''; renderList(picker, ''); picker.search.focus(); requestAnimationFrame(() => picker.list.querySelector('.bvp-item.selected')?.scrollIntoView({ block: 'nearest' })); } function setValue(picker, value, dispatch) { picker.select.value = value || ''; renderTrigger(picker); renderList(picker, picker.search.value); close(picker); if (dispatch !== false) picker.select.dispatchEvent(new Event('change', { bubbles: true })); picker.trigger.focus(); } function upgrade(selectId, options) { const select = document.getElementById(selectId); if (!select) return null; if (pickers[selectId]) return pickers[selectId]; const picker = { select, items: [], placeholder: options?.placeholder || Array.from(select.options).find(o => !o.value)?.textContent || '— select after fetch —', empty: options?.empty || 'No voices', }; select.dataset.bvpUpgraded = '1'; select.style.display = 'none'; const root = document.createElement('div'); root.className = 'bvp-root'; root.dataset.bvpFor = selectId; select.parentNode.insertBefore(root, select); root.innerHTML = ` `; Object.assign(picker, { root, trigger: root.querySelector('.bvp-trigger'), drop: root.querySelector('.bvp-drop'), search: root.querySelector('.bvp-search'), list: root.querySelector('.bvp-list'), }); picker.trigger.addEventListener('click', e => { e.preventDefault(); picker.drop.hidden ? open(picker) : close(picker); }); picker.trigger.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(picker); } if (e.key === 'ArrowDown') { e.preventDefault(); open(picker); picker.list.querySelector('.bvp-item')?.focus(); } }); picker.search.addEventListener('input', () => renderList(picker, picker.search.value)); picker.search.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(picker); picker.trigger.focus(); } if (e.key === 'ArrowDown') { e.preventDefault(); picker.list.querySelector('.bvp-item')?.focus(); } }); picker.list.addEventListener('click', e => { const item = e.target.closest('.bvp-item'); if (item) setValue(picker, item.dataset.value, true); }); picker.list.addEventListener('keydown', e => { const item = e.target.closest('.bvp-item'); if (!item) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setValue(picker, item.dataset.value, true); } if (e.key === 'ArrowDown') { e.preventDefault(); (item.nextElementSibling || item).focus(); } if (e.key === 'ArrowUp') { e.preventDefault(); (item.previousElementSibling || picker.search).focus(); } if (e.key === 'Escape') { e.preventDefault(); close(picker); picker.trigger.focus(); } }); select.addEventListener('change', () => { renderTrigger(picker); close(picker); }); document.addEventListener('click', e => { if (!root.contains(e.target)) close(picker); }, true); pickers[selectId] = picker; syncFromSelect(selectId); return picker; } function populate(selectId, values, options) { const picker = upgrade(selectId, options || {}); if (!picker) return; picker.placeholder = options?.placeholder || picker.placeholder; picker.empty = options?.empty || picker.empty; const previous = picker.select.value; picker.items = (values || []).map(norm).filter(item => item.id); picker.select.innerHTML = `` + picker.items.map(item => ``).join(''); if (previous && picker.items.some(item => item.id === previous)) picker.select.value = previous; renderTrigger(picker); renderList(picker, picker.search?.value || ''); } function syncFromSelect(selectId) { const picker = pickers[selectId]; if (!picker) return; picker.items = Array.from(picker.select.options).filter(o => o.value).map(o => ({ id: o.value, label: o.textContent || o.value, meta: libraryVoice(o.value) || {} })); renderTrigger(picker); renderList(picker, ''); } function getValue(selectId) { return document.getElementById(selectId)?.value || ''; } function set(selectId, value) { const picker = pickers[selectId] || upgrade(selectId); if (!picker) return; setValue(picker, value, true); } window.BenchmarkVoicePicker = { upgrade, populate, syncFromSelect, getValue, set }; })();