// ── 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 = `