Add multilingual preview text picker with resizable textarea

- Preview input changed to a resizable textarea (resize: vertical)
- Flag button before textarea opens searchable language picker (23 languages)
- Selecting a language fills textarea with a pre-translated benchmark sentence
- VL_SAMPLE_TEXTS map covers EN/DE/FR/ES/PT/IT/NL/PL/SV/DA/NB/FI/HU/CS/RO/UK/RU/TR/AR/HI/ZH/JA/KO
- setPreviewLang / openPreviewLangPicker / syncPreviewLangBtn wired up globally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-05-26 12:49:47 +02:00
parent ecef913837
commit 79aaed4b7d
3 changed files with 128 additions and 25 deletions

View File

@ -562,6 +562,66 @@ const ALL_FLAGS = uniqueFlagOptions([ENGLISH_FLAGS, EUROPE_FLAGS, ASIA_FLAGS, LA
['EG','EGY - Egyptian'], ['MA','MAR - Moroccan'], ['ZA','ZAF - South African'], ['NG','NGA - Nigerian'], ['KE','KEN - Kenyan'], ['GH','GHA - Ghanaian'],
]]);
// ── Preview sample texts per language ────────────────────────────────────
const VL_SAMPLE_TEXTS = {
EN: 'Hello, how are you today? Please read this sample clearly for a fair voice benchmark.',
DE: 'Hallo, wie geht es Ihnen heute? Bitte lesen Sie diesen Text deutlich vor — für einen fairen Stimmvergleich.',
FR: 'Bonjour, comment allez-vous aujourd\'hui ? Veuillez lire ce texte clairement pour un test vocal équitable.',
ES: '¡Hola! ¿Cómo estás hoy? Por favor, lee este texto con claridad para una evaluación justa de la voz.',
PT: 'Olá, como vai você hoje? Por favor, leia este texto claramente para uma avaliação justa da voz.',
IT: 'Ciao, come stai oggi? Per favore leggi questo testo in modo chiaro per una valutazione equa della voce.',
NL: 'Hallo, hoe gaat het vandaag? Lees dit voorbeeld alstublieft duidelijk voor een eerlijke stembeoordeling.',
PL: 'Cześć, jak się dziś masz? Przeczytaj proszę ten tekst wyraźnie, aby dokonać rzetelnej oceny głosu.',
SV: 'Hej, hur mår du idag? Vänligen läs detta exempel tydligt för en rättvis röstbedömning.',
DA: 'Hej, hvordan har du det i dag? Læs venligst dette eksempel tydeligt for en retfærdig stemmevurdering.',
NB: 'Hei, hvordan har du det i dag? Vennligst les dette eksempelet tydelig for en rettferdig stemmevurdering.',
FI: 'Hei, kuinka voit tänään? Lue tämä esimerkki selkeästi reilua ääniarviointia varten.',
HU: 'Szia, hogy vagy ma? Kérlek, olvasd fel ezt a szöveget érthetően az igazságos hangértékelés érdekében.',
CS: 'Dobrý den, jak se máte? Přečtěte prosím tento text zřetelně pro spravedlivé hodnocení hlasu.',
RO: 'Bună ziua, cum vă simțiți astăzi? Vă rugăm citiți acest text clar pentru o evaluare corectă a vocii.',
UK: 'Привіт, як ви сьогодні? Будь ласка, прочитайте цей текст чітко для справедливого тестування голосу.',
RU: 'Здравствуйте, как вы сегодня? Пожалуйста, прочитайте этот текст чётко для справедливого тестирования голоса.',
TR: 'Merhaba, bugün nasılsınız? Adil bir ses değerlendirmesi için lütfen bu örneği açıkça okuyun.',
AR: 'مرحباً، كيف حالك اليوم؟ يرجى قراءة هذا النص بوضوح لإجراء اختبار صوت عادل.',
HI: 'नमस्ते, आप आज कैसे हैं? कृपया इस नमूने को स्पष्ट रूप से पढ़ें ताकि एक उचित आवाज़ मूल्यांकन हो सके।',
ZH: '你好,你今天怎么样?请清晰地朗读这段示例,以便进行公正的语音评测。',
JA: 'こんにちは、今日はいかがですか?公平な音声評価のために、このサンプルをはっきりと読んでください。',
KO: '안녕하세요, 오늘 어떠세요? 공정한 음성 벤치마크를 위해 이 샘플을 명확하게 읽어주세요.',
};
const _VL_LANG_FLAG = Object.assign({ DA:'DK', NB:'NO', FI:'FI', HU:'HU', CS:'CZ', RO:'RO', UK:'UA' }, LANG_FLAG_DEFAULT);
function setPreviewLang(lang) {
const ta = $('vl-preview-sample');
const hidden = $('benchmark-sample-text');
if (!ta) return;
const text = VL_SAMPLE_TEXTS[lang] || VL_SAMPLE_TEXTS.EN;
ta.value = text;
ta.dispatchEvent(new Event('input'));
if (hidden) { hidden.value = text; hidden.dispatchEvent(new Event('input')); }
try { localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY, text); } catch {}
const btn = $('vl-preview-lang-btn');
if (btn) {
const cc = (_VL_LANG_FLAG[lang] || 'gb').toLowerCase();
btn.innerHTML = `<span class="fi fi-${cc}"></span>`;
btn.title = (LANGUAGE_LABELS[lang] || lang) + ' — click to change';
btn.dataset.lang = lang;
}
}
function openPreviewLangPicker(anchor) {
const items = Object.keys(VL_SAMPLE_TEXTS).map(l => [l, LANGUAGE_LABELS[l] ? `${LANGUAGE_LABELS[l]} (${l})` : l]);
createSearchablePicker(anchor, items, setPreviewLang, {
placeholder: 'Sample language…',
renderItem: (l) => {
const cc = (_VL_LANG_FLAG[l] || 'gb').toLowerCase();
return `<span class="fi fi-${cc}" style="width:20px;height:14px;background-size:cover;border-radius:2px;flex-shrink:0;display:inline-block"></span>`
+ `<span class="ipi-code">${escHtml(l)}</span>`
+ `<span>${escHtml(LANGUAGE_LABELS[l] || l)}</span>`;
},
});
}
// ── Searchable picker dropdown ────────────────────────────────────────────
function createSearchablePicker(anchor, items, onSelect, { placeholder = 'Search…', renderItem } = {}) {
document.querySelector('.insp-picker')?.remove();
@ -5815,6 +5875,19 @@ initBenchmarkSampleControls();
});
})();
// Sync flag button to current preview text language (best-match)
(function syncPreviewLangBtn() {
const ta = $('vl-preview-sample');
const btn = $('vl-preview-lang-btn');
if (!ta || !btn) return;
const cur = ta.value.trim();
const matched = Object.keys(VL_SAMPLE_TEXTS).find(l => VL_SAMPLE_TEXTS[l] === cur) || 'EN';
const cc = (_VL_LANG_FLAG[matched] || 'gb').toLowerCase();
btn.innerHTML = `<span class="fi fi-${cc}"></span>`;
btn.title = (LANGUAGE_LABELS[matched] || matched) + ' — click to change sample language';
btn.dataset.lang = matched;
})();
// Synth mode segmented control (preview text vs. reference transcript)
(function initSynthMode() {
const seg = $('vl-synth-mode-seg');

View File

@ -17,14 +17,6 @@
<!-- LEFT: compact voice list -->
<div class="voices-list-pane">
<div class="vl-toolbar">
<button class="btn-secondary vl-tb-btn" id="refresh-voices-btn" title="Refresh voice list">&#8635; Refresh</button>
<button class="btn-secondary vl-tb-btn" id="sync-voice-folders-btn" title="Sync active/hidden folders">Sync</button>
<button class="btn-secondary vl-tb-btn" id="calculate-db-btn" title="Calculate dBFS">Calc dB</button>
<button class="btn-secondary vl-tb-btn" id="benchmark-voices-btn" title="Benchmark TTS speed">Benchmark</button>
<button class="btn-secondary vl-tb-btn" id="copy-active-voices-btn" title="Copy active voice names">Copy active</button>
</div>
<!-- TTS synth mode + preview sentence -->
<div class="vl-synth-panel">
<div class="vl-synth-mode-row">
@ -37,10 +29,19 @@
title="Synthesize this voice&#x2019;s saved reference transcript — good for quality check vs. original recording">Reference transcript</button>
</div>
</div>
<div class="vl-preview-text-wrap" id="vl-preview-text-wrap">
<input type="text" class="vl-preview-input" id="vl-preview-sample"
placeholder="Sample sentence for TTS preview&#x2026;"
oninput="var t=document.getElementById('benchmark-sample-text');if(t){t.value=this.value;t.dispatchEvent(new Event('input'));}">
<div class="vl-preview-row">
<button class="vl-preview-lang-btn" id="vl-preview-lang-btn"
title="English — click to change sample language"
onclick="openPreviewLangPicker(this)" data-lang="EN">
<span class="fi fi-gb"></span>
</button>
<textarea class="vl-preview-input" id="vl-preview-sample" rows="2"
placeholder="Sample sentence for TTS preview&#x2026;"
oninput="var t=document.getElementById('benchmark-sample-text');if(t){t.value=this.value;t.dispatchEvent(new Event('input'));}"
></textarea>
</div>
</div>
</div>
@ -59,6 +60,13 @@
</div>
<div id="voice-list"></div>
<div class="vl-toolbar">
<button class="btn-secondary vl-tb-btn" id="refresh-voices-btn" title="Refresh voice list">&#8635; Refresh</button>
<button class="btn-secondary vl-tb-btn" id="sync-voice-folders-btn" title="Sync active/hidden folders">Sync</button>
<button class="btn-secondary vl-tb-btn" id="calculate-db-btn" title="Calculate dBFS">Calc dB</button>
<button class="btn-secondary vl-tb-btn" id="benchmark-voices-btn" title="Benchmark TTS speed">Benchmark</button>
<button class="btn-secondary vl-tb-btn" id="copy-active-voices-btn" title="Copy active voice names">Copy active</button>
</div>
<div class="vl-footer">
<span id="voice-count" class="note"></span>

View File

@ -6,6 +6,7 @@
--border: #E5E7EB;
--text: #111827;
--subtext: #6B7280;
--placehld: #6B7280;
--accent: #2563EB;
--green: #16A34A;
--red: #DC2626;
@ -135,7 +136,9 @@ body {
}
.section-search:focus, .section-search:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37,99,235,.12); }
.section-new-voice-btn { padding: 6px 14px; font-size: 13px; white-space: nowrap; }
.section-search::placeholder {
color: var(--placehld);
}
/* ── Force all tab-content visible ──────────────────────────────────────── */
.tab-content {
display: flex !important; flex-direction: column; gap: 18px; padding: 0;
@ -203,7 +206,7 @@ button:not(:disabled):hover { filter: brightness(0.95); }
flex: 1; background: var(--panel); border: 1px solid var(--border);
color: var(--text); border-radius: var(--radius); padding: 9px 14px; font-size: 14px;
}
input::placeholder, textarea::placeholder { color: var(--border); }
input::placeholder, textarea::placeholder { color: var(--placehld); }
#yt-progress { font-size: 13px; color: var(--subtext); min-height: 1.4em; font-family: monospace; }
/* ── Recording ──────────────────────────────────────────────────────────── */
@ -500,12 +503,12 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.vr-ref .vr-inline, .vr-note .vr-inline { display: flex; gap: 6px; align-items: center; min-width: 0; }
.vr-ref input { width: 100%; min-width: 0; background: transparent; border: none; border-bottom: 1px solid transparent; color: var(--subtext); font-size: 13px; padding: 3px 5px; font-family: inherit; outline: none; }
.vr-ref input:focus { border-bottom-color: var(--accent); color: var(--text); }
.vr-ref input::placeholder { color: var(--border); font-style: italic; }
.vr-ref input::placeholder { color: var(--placehld); font-style: italic; }
.ref-transcribe-btn { width: 28px; height: 28px; padding: 0; border-radius: 6px; background: none; border: 1px solid transparent; color: var(--subtext); flex-shrink: 0; font-size: 14px; }
.ref-transcribe-btn:hover { background: var(--border); color: var(--text); filter: none; }
.vr-note input { width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--border); color: var(--text); font-size: 13px; padding: 3px 5px; font-family: inherit; outline: none; }
.vr-note input:focus { border-bottom-color: var(--accent); }
.vr-note input::placeholder { color: var(--border); }
.vr-note input::placeholder { color: var(--placehld); }
.vr-row-subtle { color: var(--subtext); font-size: 14px; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vr-optimizer { grid-column: auto; display: none; margin: 6px 0 0; width: 100%; box-sizing: border-box; }
.vl-row.edit-open .vr-optimizer { display: block; }
@ -783,7 +786,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
display: flex; flex-wrap: wrap; gap: 4px;
padding: 8px 10px 7px; border-bottom: 1px solid var(--border);
}
.vl-tb-btn { padding: 3px 8px !important; font-size: 14px !important; flex: 0 0 auto; }
.vl-tb-btn { padding: 3px 8px !important; font-size: 12px !important; flex: 0 0 auto; }
.vl-filters {
display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
padding: 7px 10px; border-bottom: 1px solid var(--border);
@ -793,9 +796,12 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
border: 1px solid var(--border); border-radius: var(--radius);
background: var(--bg); color: var(--text);
}
.vl-search input::placeholder {
color: var(--placehld);
}
.vl-search:focus { outline: none; border-color: var(--accent); }
.vl-filters select {
padding: 4px 5px; font-size: 14.5px; border: 1px solid var(--border);
padding: 4px 5px; font-size: 12px; border: 1px solid var(--border);
border-radius: var(--radius); background: var(--bg); color: var(--text); max-width: 80px;
}
.vl-disabled-label { font-size: 14.5px; color: var(--subtext); white-space: nowrap; cursor: pointer; gap: 3px; display: flex; align-items: center; }
@ -872,11 +878,11 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.vl-synth-panel { background: var(--surface); border-bottom: 1px solid var(--border); }
.vl-synth-mode-row { display: flex; align-items: center; gap: 7px; padding: 5px 8px 5px; }
.vl-synth-icon { font-size: 9px; color: var(--teal); flex-shrink: 0; }
.vl-synth-label { font-size: 10px; font-weight: 700; color: var(--subtext); white-space: nowrap; flex-shrink: 0; }
.vl-synth-label { font-size: 14px; font-weight: 700; color: var(--subtext); white-space: nowrap; flex-shrink: 0; }
.vl-synth-seg { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; flex-shrink: 0; }
.vl-synth-seg-btn {
background: var(--panel); border: none; padding: 3px 9px;
font-size: 10.5px; font-weight: 600; color: var(--subtext);
font-size: 14.5px; font-weight: 600; color: var(--subtext);
cursor: pointer; transition: background .12s, color .12s; white-space: nowrap;
border-right: 1px solid var(--border);
}
@ -884,7 +890,23 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.vl-synth-seg-btn.active { background: var(--teal); color: #fff; }
.vl-synth-seg-btn:not(.active):hover { background: var(--border); color: var(--text); }
.vl-preview-text-wrap { padding: 0 8px 6px; }
.vl-preview-input { width: 100%; font-size: 14px; background: var(--bg); border: 1px solid var(--border); border-radius: 5px; color: var(--text); padding: 3px 7px; font-family: var(--font); }
.vl-preview-row { display: flex; align-items: flex-start; gap: 6px; }
.vl-preview-lang-btn {
flex-shrink: 0; width: 34px; height: 34px; margin-top: 1px;
border: 1px solid var(--border); border-radius: 6px;
background: var(--surface); padding: 0; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: border-color .15s, box-shadow .15s;
}
.vl-preview-lang-btn:hover { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(37,99,235,.12); filter: none; }
.vl-preview-lang-btn .fi { width: 22px; height: 16px; background-size: cover; background-position: center; border-radius: 2px; display: block; }
.vl-preview-input {
flex: 1; min-width: 0; display: block; box-sizing: border-box;
font-size: 13px; line-height: 1.5;
background: var(--bg); border: 1px solid var(--border); border-radius: 5px;
color: var(--text); padding: 5px 7px; font-family: var(--font);
resize: vertical; min-height: 42px;
}
.vl-preview-input:focus { outline: none; border-color: var(--accent); }
.vl-preview-text-wrap.hidden { display: none; }
@ -897,7 +919,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.inspector-placeholder {
height: 100%; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 10px;
color: var(--subtext); text-align: center; padding: 24px;
color: var(--placehld); text-align: center; padding: 24px;
}
.inspector-placeholder span { font-size: 40px; opacity: .4; }
.inspector-placeholder p { font-size: 13px; line-height: 1.6; opacity: .7; }
@ -985,7 +1007,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
width: 100%; padding: 2px 0; font-size: 14px; font-style: italic; font-family: inherit;
border: none; background: transparent; color: var(--text); outline: none; transition: color .15s;
}
.insp-note-slot .vr-note .vr-inline input::placeholder { color: var(--subtext); opacity: .6; }
.insp-note-slot .vr-note .vr-inline input::placeholder { color: var(--placehld); opacity: .6; }
.insp-note-slot .vr-note .vr-inline input:focus { font-style: normal; }
.insp-note-slot .ref-transcribe-btn { display: none !important; }
.insp-actions-delete { flex-shrink: 0; display: flex; align-items: center; padding-left: 20px; margin-right: 20px }
@ -1051,7 +1073,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
padding: 0 6px; min-width: 50px;
transition: background .15s, color .15s;
}
.insp-tag-input::placeholder { color: var(--subtext); opacity: .5; font-style: italic; }
.insp-tag-input::placeholder { color: var(--placehld); opacity: .5; font-style: italic; }
.insp-tag-input:hover { color: var(--text); }
.insp-tag-input:focus { background: var(--panel); color: var(--text); }
@ -1087,7 +1109,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
font-size: 12px; font-family: inherit; background: var(--panel);
color: var(--text); outline: none; flex-shrink: 0;
}
.insp-picker-search::placeholder { color: var(--subtext); opacity: .65; }
.insp-picker-search::placeholder { color: var(--placehld); opacity: .65; }
.insp-picker-list { flex: 1; overflow-y: auto; padding: 3px 0; }
.insp-picker-item {
display: flex; align-items: center; width: 100%; gap: 8px;