Replace all icons with MDI (Material Design Icons) v7.4.47 — 171 replacements across 10 files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-05-27 02:36:33 +02:00
parent 21a219f816
commit 61a48ee84a
11 changed files with 180 additions and 165 deletions

View File

@ -38,7 +38,7 @@ function selectVoice(wrap) {
restoreToRow(wrap); restoreToRow(wrap);
wrap.classList.remove('vr-selected'); wrap.classList.remove('vr-selected');
_selectedVoiceWrap = null; _selectedVoiceWrap = null;
inspector.innerHTML = '<div class="inspector-placeholder"><span>&#128266;</span><p>Pick a voice on the left<br>to edit it here</p></div>'; inspector.innerHTML = '<div class="inspector-placeholder"><span><span class="mdi mdi-account-voice"></span></span><p>Pick a voice on the left<br>to edit it here</p></div>';
return; return;
} }
@ -73,7 +73,7 @@ function selectVoice(wrap) {
const flagIconHtml = inspFlagIcon || ''; const flagIconHtml = inspFlagIcon || '';
const makeStars = n => [1,2,3,4,5].map(i => const makeStars = n => [1,2,3,4,5].map(i =>
`<span class="insp-star${i <= n ? ' on' : ''}" data-val="${i}">&#9733;</span>` `<span class="insp-star${i <= n ? ' on' : ''}" data-val="${i}"><span class="mdi mdi-star"></span></span>`
).join(''); ).join('');
const LANGS = ['EN','DE','IT','ES','FR','PT','NL','PL','ZH','JA','KO','AR','RU','TR','HI','SV','DA','FI','NB','HU','CS','RO','UK']; const LANGS = ['EN','DE','IT','ES','FR','PT','NL','PL','ZH','JA','KO','AR','RU','TR','HI','SV','DA','FI','NB','HU','CS','RO','UK'];
@ -529,7 +529,7 @@ applyTheme(localStorage.getItem('vcf-theme') || 'dark');
// ── Language helpers ────────────────────────────────────────────────────── // ── Language helpers ──────────────────────────────────────────────────────
function cc2flag(cc) { function cc2flag(cc) {
if (!cc || cc.length !== 2) return '🌐'; if (!cc || cc.length !== 2) return '<span class="mdi mdi-earth"></span>';
return cc.toUpperCase().replace(/./g, c => String.fromCodePoint(c.charCodeAt(0) + 127397)); return cc.toUpperCase().replace(/./g, c => String.fromCodePoint(c.charCodeAt(0) + 127397));
} }
@ -1789,10 +1789,10 @@ function backendHelpHtml(b, compact = false) {
b.true_streaming ? ['good', 'true streaming'] : ['', 'buffered/normal'], b.true_streaming ? ['good', 'true streaming'] : ['', 'buffered/normal'],
].map(([cls, text]) => `<span class="backend-tag ${cls}">${escHtml(text)}</span>`).join(''); ].map(([cls, text]) => `<span class="backend-tag ${cls}">${escHtml(text)}</span>`).join('');
const metricParts = []; const metricParts = [];
if (b.speed) metricParts.push(`<span class="backend-metric-tag">&#9889; ${escHtml(b.speed)}</span>`); if (b.speed) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-lightning-bolt"></span> ${escHtml(b.speed)}</span>`);
if (b.latency) metricParts.push(`<span class="backend-metric-tag">&#9200; ${escHtml(b.latency)}</span>`); if (b.latency) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-clock-outline"></span> ${escHtml(b.latency)}</span>`);
if (b.quality) metricParts.push(`<span class="backend-metric-tag">&#11088; ${escHtml(b.quality)}</span>`); if (b.quality) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-star-circle-outline"></span> ${escHtml(b.quality)}</span>`);
if (b.ram) metricParts.push(`<span class="backend-metric-tag">&#128190; ${escHtml(b.ram)}</span>`); if (b.ram) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-memory"></span> ${escHtml(b.ram)}</span>`);
const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : ''; const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : '';
const detail = compact ? escHtml(b.best_for || '') : `${escHtml(b.purpose || '')}<br><strong>Identity:</strong> ${escHtml(b.identity || '')}<br><strong>Style:</strong> ${escHtml(b.style || '')}<br><strong>Best for:</strong> ${escHtml(b.best_for || '')}`; const detail = compact ? escHtml(b.best_for || '') : `${escHtml(b.purpose || '')}<br><strong>Identity:</strong> ${escHtml(b.identity || '')}<br><strong>Style:</strong> ${escHtml(b.style || '')}<br><strong>Best for:</strong> ${escHtml(b.best_for || '')}`;
return `<strong>${escHtml(b.label)}</strong><div class="backend-help-tags">${tags}</div>${metrics}<div>${detail}</div>`; return `<strong>${escHtml(b.label)}</strong><div class="backend-help-tags">${tags}</div>${metrics}<div>${detail}</div>`;
@ -1802,10 +1802,10 @@ function sttBackendHelpHtml(b) {
if (!b) return 'No STT engine selected.'; if (!b) return 'No STT engine selected.';
const m = b.metrics || {}; const m = b.metrics || {};
const metricParts = []; const metricParts = [];
if (m.speed) metricParts.push(`<span class="backend-metric-tag">&#9889; ${escHtml(m.speed)}</span>`); if (m.speed) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-lightning-bolt"></span> ${escHtml(m.speed)}</span>`);
if (m.latency) metricParts.push(`<span class="backend-metric-tag">&#9200; ${escHtml(m.latency)}</span>`); if (m.latency) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-clock-outline"></span> ${escHtml(m.latency)}</span>`);
if (m.quality) metricParts.push(`<span class="backend-metric-tag">&#11088; ${escHtml(m.quality)}</span>`); if (m.quality) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-star-circle-outline"></span> ${escHtml(m.quality)}</span>`);
if (m.ram) metricParts.push(`<span class="backend-metric-tag">&#128190; ${escHtml(m.ram)}</span>`); if (m.ram) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-memory"></span> ${escHtml(m.ram)}</span>`);
const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : ''; const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : '';
const modelList = Array.isArray(b.models) && b.models.length ? ' Models: ' + b.models.slice(0, 4).join(', ') + '.' : ''; const modelList = Array.isArray(b.models) && b.models.length ? ' Models: ' + b.models.slice(0, 4).join(', ') + '.' : '';
const avail = b.available ? `<span class="backend-tag good">ready</span>` : `<span class="backend-tag warn">unavailable</span>`; const avail = b.available ? `<span class="backend-tag good">ready</span>` : `<span class="backend-tag warn">unavailable</span>`;
@ -1832,7 +1832,7 @@ function updateStyleBackendHelp(scope = document) {
const suggestion = styleAwareBacks.length const suggestion = styleAwareBacks.length
? ` Try <strong>${escHtml(styleAwareBacks[0].label)}</strong> instead.` ? ` Try <strong>${escHtml(styleAwareBacks[0].label)}</strong> instead.`
: ' No style-aware backend is currently reachable.'; : ' No style-aware backend is currently reachable.';
box.innerHTML += `<div class="style-backend-warn">&#9888; This backend ignores the style instruction — output will sound the same regardless of what you type.${suggestion}</div>`; box.innerHTML += `<div class="style-backend-warn"><span class="mdi mdi-alert-outline"></span> This backend ignores the style instruction — output will sound the same regardless of what you type.${suggestion}</div>`;
} }
}); });
} }
@ -4195,12 +4195,12 @@ function renderVoiceList() {
emptyEl.innerHTML = ` emptyEl.innerHTML = `
<div class="voices-empty-cases"> <div class="voices-empty-cases">
<div class="voices-empty-case"> <div class="voices-empty-case">
<div class="voices-empty-case-icon">&#128193;</div> <div class="voices-empty-case-icon"><span class="mdi mdi-folder-outline"></span></div>
<h4>Wrong folder path?</h4> <h4>Wrong folder path?</h4>
<p>Set the path where your <code>.wav</code> voice files live <em>inside the container</em>.</p> <p>Set the path where your <code>.wav</code> voice files live <em>inside the container</em>.</p>
<div class="voices-folder-row"> <div class="voices-folder-row">
<input id="voices-empty-scan-dir" type="text" class="voices-folder-input" placeholder="/voices"> <input id="voices-empty-scan-dir" type="text" class="voices-folder-input" placeholder="/voices">
<button class="btn-secondary" id="voices-empty-browse-btn" title="Browse container filesystem">&#128193; Browse</button> <button class="btn-secondary" id="voices-empty-browse-btn" title="Browse container filesystem"><span class="mdi mdi-folder-outline"></span> Browse</button>
<button class="btn-primary" id="voices-empty-save-btn">Set &amp; reload</button> <button class="btn-primary" id="voices-empty-save-btn">Set &amp; reload</button>
</div> </div>
<div class="vef-dir-browser" id="vef-dir-browser" hidden> <div class="vef-dir-browser" id="vef-dir-browser" hidden>
@ -4217,12 +4217,12 @@ function renderVoiceList() {
</div> </div>
<div class="voices-empty-divider">or</div> <div class="voices-empty-divider">or</div>
<div class="voices-empty-case"> <div class="voices-empty-case">
<div class="voices-empty-case-icon">&#127908;</div> <div class="voices-empty-case-icon"><span class="mdi mdi-microphone-variant"></span></div>
<h4>Folder is empty?</h4> <h4>Folder is empty?</h4>
<p>Create your first voice from a recording or download ready-made voices.</p> <p>Create your first voice from a recording or download ready-made voices.</p>
<div class="voices-empty-actions"> <div class="voices-empty-actions">
<button class="btn-primary voices-empty-action-btn" id="voices-goto-clone">&#9889; Clone a Voice</button> <button class="btn-primary voices-empty-action-btn" id="voices-goto-clone"><span class="mdi mdi-lightning-bolt"></span> Clone a Voice</button>
<button class="btn-secondary voices-empty-action-btn" id="voices-goto-studio">&#127760; Get Voices Online</button> <button class="btn-secondary voices-empty-action-btn" id="voices-goto-studio"><span class="mdi mdi-earth"></span> Get Voices Online</button>
</div> </div>
</div> </div>
</div> </div>
@ -4262,7 +4262,7 @@ function renderVoiceList() {
vefDirList.innerHTML = '<span class="vef-empty">No subdirectories here.</span>'; vefDirList.innerHTML = '<span class="vef-empty">No subdirectories here.</span>';
} else { } else {
vefDirList.innerHTML = data.dirs.map(d => vefDirList.innerHTML = data.dirs.map(d =>
`<button class="vef-dir-item" data-path="${escHtml(data.path === '/' ? '/' + d : data.path + '/' + d)}">&#128193; ${escHtml(d)}</button>` `<button class="vef-dir-item" data-path="${escHtml(data.path === '/' ? '/' + d : data.path + '/' + d)}"><span class="mdi mdi-folder-outline"></span> ${escHtml(d)}</button>`
).join(''); ).join('');
vefDirList.querySelectorAll('.vef-dir-item').forEach(b => b.addEventListener('click', () => vefNavigate(b.dataset.path))); vefDirList.querySelectorAll('.vef-dir-item').forEach(b => b.addEventListener('click', () => vefNavigate(b.dataset.path)));
} }
@ -4413,7 +4413,7 @@ function makeVoiceRow(v) {
// Use language-specific variants if there are multiple; fall back to world picker otherwise // Use language-specific variants if there are multiple; fall back to world picker otherwise
const flagOpts = langOpts.length > 1 ? langOpts : ALL_FLAGS; const flagOpts = langOpts.length > 1 ? langOpts : ALL_FLAGS;
const currentFlag = v.flag || LANG_FLAG_DEFAULT[langCode] || ''; const currentFlag = v.flag || LANG_FLAG_DEFAULT[langCode] || '';
const flagEmoji = currentFlag ? cc2flag(currentFlag) : '🌐'; const flagEmoji = currentFlag ? cc2flag(currentFlag) : '<span class="mdi mdi-earth"></span>';
const flagCode = currentFlag ? ccDisplay(currentFlag) : '?'; const flagCode = currentFlag ? ccDisplay(currentFlag) : '?';
wrap.dataset.flagEmoji = flagEmoji; wrap.dataset.flagEmoji = flagEmoji;
wrap.dataset.flagCc = currentFlag || ''; wrap.dataset.flagCc = currentFlag || '';
@ -4432,7 +4432,7 @@ function makeVoiceRow(v) {
const gender = v.gender || ''; const gender = v.gender || '';
const starsHtml = [1,2,3,4,5].map(i => const starsHtml = [1,2,3,4,5].map(i =>
`<span class="star${i<=(v.rating||0)?' on':''}" data-val="${i}">&#9733;</span>` `<span class="star${i<=(v.rating||0)?' on':''}" data-val="${i}"><span class="mdi mdi-star"></span></span>`
).join(''); ).join('');
const picSrc = v.has_picture ? `/api/voice/picture/${encodeURIComponent(v.id)}` : null; const picSrc = v.has_picture ? `/api/voice/picture/${encodeURIComponent(v.id)}` : null;
@ -4468,14 +4468,14 @@ function makeVoiceRow(v) {
</div> </div>
</div> </div>
<div class="vr-play-group"> <div class="vr-play-group">
<div class="vr-play vr-play-original"><button class="play-btn-orig" title="Play original recording">&#9654;</button></div> <div class="vr-play vr-play-original"><button class="play-btn-orig" title="Play original recording"><span class="mdi mdi-play"></span></button></div>
<div class="vr-play vr-play-synth"><button class="play-btn-synth" title="Generate and play TTS preview">&#9654;</button></div> <div class="vr-play vr-play-synth"><button class="play-btn-synth" title="Generate and play TTS preview"><span class="mdi mdi-play"></span></button></div>
</div> </div>
</div> </div>
<div class="vr-main-row"> <div class="vr-main-row">
<div class="vr-photo" title="Click to upload photo"> <div class="vr-photo" title="Click to upload photo">
${picSrc ? `<img src="${picSrc}" alt="">` : '<div class="ph-icon">&#128100;</div>'} ${picSrc ? `<img src="${picSrc}" alt="">` : '<div class="ph-icon"><span class="mdi mdi-account-outline"></span></div>'}
<input type="file" class="photo-input" accept="image/*"> <input type="file" class="photo-input" accept="image/*">
</div> </div>
@ -4497,8 +4497,8 @@ function makeVoiceRow(v) {
<span class="vr-name-text" title="${escHtml(v.id)}">${escHtml(v.id)}</span> <span class="vr-name-text" title="${escHtml(v.id)}">${escHtml(v.id)}</span>
<span class="rename-confirm"> <span class="rename-confirm">
<input class="vr-name-input" value="${escHtml(v.id)}" spellcheck="false"> <input class="vr-name-input" value="${escHtml(v.id)}" spellcheck="false">
<button class="icon-btn rename-ok" title="Save">&#10003;</button> <button class="icon-btn rename-ok" title="Save"><span class="mdi mdi-check"></span></button>
<button class="icon-btn rename-cancel" title="Cancel">&#10005;</button> <button class="icon-btn rename-cancel" title="Cancel"><span class="mdi mdi-close"></span></button>
</span> </span>
</div> </div>
</div> </div>
@ -4519,15 +4519,15 @@ function makeVoiceRow(v) {
<div class="vr-rating">${starsHtml}</div> <div class="vr-rating">${starsHtml}</div>
<div class="vr-edit"> <div class="vr-edit">
<button class="edit-audio-btn" title="Edit audio">&#9998;</button> <button class="edit-audio-btn" title="Edit audio"><span class="mdi mdi-pencil-outline"></span></button>
</div> </div>
<div class="vr-play-group"> <div class="vr-play-group">
<div class="vr-play vr-play-original"> <div class="vr-play vr-play-original">
<button title="Play / pause original recording">&#9654;</button> <button title="Play / pause original recording"><span class="mdi mdi-play"></span></button>
</div> </div>
<div class="vr-play vr-play-synth"> <div class="vr-play vr-play-synth">
<button title="Generate and play synthesized sample">&#9654;</button> <button title="Generate and play synthesized sample"><span class="mdi mdi-play"></span></button>
</div> </div>
</div> </div>
@ -4538,7 +4538,7 @@ function makeVoiceRow(v) {
<label>Reference</label> <label>Reference</label>
<div class="vr-inline"> <div class="vr-inline">
<input type="text" value="${escHtml(v.transcript||'')}" placeholder="No reference text" title="${escHtml(v.transcript||'')}"> <input type="text" value="${escHtml(v.transcript||'')}" placeholder="No reference text" title="${escHtml(v.transcript||'')}">
<button class="ref-transcribe-btn" title="Recognise reference text" style="${v.transcript ? 'display:none' : ''}">&#128172;</button> <button class="ref-transcribe-btn" title="Recognise reference text" style="${v.transcript ? 'display:none' : ''}"><span class="mdi mdi-message-text-outline"></span></button>
</div> </div>
</div> </div>
@ -5338,7 +5338,7 @@ function makeVoiceRow(v) {
// Play original recording or synthesized sample // Play original recording or synthesized sample
const originalPlayBtn = wrap.querySelector('.vr-play-original button'); const originalPlayBtn = wrap.querySelector('.vr-play-original button');
const synthPlayBtn = wrap.querySelector('.vr-play-synth button'); const synthPlayBtn = wrap.querySelector('.vr-play-synth button');
const playIcon = '&#9654;', pauseIcon = '&#10073;&#10073;', generatingIcon = '&#8987;'; const playIcon = '<span class="mdi mdi-play"></span>', pauseIcon = '<span class="mdi mdi-pause-circle-outline"></span>', generatingIcon = '<span class="mdi mdi-timer-sand"></span>';
function setLibraryPlayButtonState(btn, state) { function setLibraryPlayButtonState(btn, state) {
btn.classList.toggle('is-generating', state === 'generating'); btn.classList.toggle('is-generating', state === 'generating');
btn.innerHTML = state === 'playing' ? pauseIcon : (state === 'generating' ? generatingIcon : playIcon); btn.innerHTML = state === 'playing' ? pauseIcon : (state === 'generating' ? generatingIcon : playIcon);
@ -5840,7 +5840,7 @@ $('save-preview-btn').addEventListener('click', () => {
<span class="perf-stat"><strong>${min} ms</strong> best</span> <span class="perf-stat"><strong>${min} ms</strong> best</span>
<span class="perf-stat"><strong>${max} ms</strong> worst</span> <span class="perf-stat"><strong>${max} ms</strong> worst</span>
<span class="perf-stat"><strong>${avgRtf.toFixed(2)}</strong> avg RTF</span> <span class="perf-stat"><strong>${avgRtf.toFixed(2)}</strong> avg RTF</span>
<span class="perf-stat ${avgRtf < 1 ? 'perf-good' : 'perf-slow'}">${avgRtf < 1 ? '&#9989; Real-time capable' : '&#9888; Slower than real-time'}</span> <span class="perf-stat ${avgRtf < 1 ? 'perf-good' : 'perf-slow'}">${avgRtf < 1 ? '<span class="mdi mdi-check-circle-outline"></span> Real-time capable' : '<span class="mdi mdi-alert-outline"></span> Slower than real-time'}</span>
`; `;
} else { perfSummary.innerHTML = '<span class="perf-err">All runs failed</span>'; } } else { perfSummary.innerHTML = '<span class="perf-err">All runs failed</span>'; }
} }
@ -6370,7 +6370,7 @@ loadSettings().then(() => {
<button class="el-play" title="Play preview" data-preview="${prev}"> Play</button> <button class="el-play" title="Play preview" data-preview="${prev}"> Play</button>
</div> </div>
</div> </div>
${prev ? `<div class="el-vc-footer"><button class="el-clone" title="Import to Clone a Voice" data-preview="${prev}" data-name="${nm}">&#8595; Clone</button></div>` : ''} ${prev ? `<div class="el-vc-footer"><button class="el-clone" title="Import to Clone a Voice" data-preview="${prev}" data-name="${nm}"><span class="mdi mdi-download"></span> Clone</button></div>` : ''}
</div>`; </div>`;
} }
@ -6448,24 +6448,24 @@ function renderLocalContainers(containers) {
const ROLE_LABEL = { tts: 'TTS', stt: 'STT', 'stt+tts': 'STT · TTS', llm: 'LLM' }; const ROLE_LABEL = { tts: 'TTS', stt: 'STT', 'stt+tts': 'STT · TTS', llm: 'LLM' };
const DC_ICONS = { const DC_ICONS = {
'faster-qwen3-tts-voiceclone': '🔊', 'faster-qwen3-tts-voiceclone': '<span class="mdi mdi-speaker-outline"></span>',
'faster-qwen3-tts-voicedesign': '', 'faster-qwen3-tts-voicedesign': '<span class="mdi mdi-auto-fix"></span>',
'faster-qwen3-tts-customvoice': '🎭', 'faster-qwen3-tts-customvoice': '🎭',
'faster-qwen3-tts-streaming': '', 'faster-qwen3-tts-streaming': '<span class="mdi mdi-lightning-bolt"></span>',
'parakeet-asr': '🦜', 'parakeet-asr': '🦜',
'magpie-tts': '🐦', 'magpie-tts': '🐦',
'parakeet-rnnt-nim': '🦜', 'parakeet-rnnt-nim': '🦜',
}; };
const DC_METRICS = { const DC_METRICS = {
'faster-qwen3-tts-voiceclone': [['','~0.3× GPU'],['⏰','13 s'],['⭐','Premium clone'],['💾','68 GB VRAM']], 'faster-qwen3-tts-voiceclone': [['<span class="mdi mdi-lightning-bolt"></span>','~0.3× GPU'],['⏰','13 s'],['⭐','Premium clone'],['💾','68 GB VRAM']],
'faster-qwen3-tts-voicedesign': [['','~0.4× GPU'],['⏰','13 s'],['⭐','Premium'], ['💾','68 GB VRAM']], 'faster-qwen3-tts-voicedesign': [['<span class="mdi mdi-lightning-bolt"></span>','~0.4× GPU'],['⏰','13 s'],['⭐','Premium'], ['💾','68 GB VRAM']],
'faster-qwen3-tts-customvoice': [['','~0.3× GPU'],['⏰','13 s'],['⭐','Premium'], ['💾','68 GB VRAM']], 'faster-qwen3-tts-customvoice': [['<span class="mdi mdi-lightning-bolt"></span>','~0.3× GPU'],['⏰','13 s'],['⭐','Premium'], ['💾','68 GB VRAM']],
'faster-qwen3-tts-streaming': [['','~0.1× GPU'],['⏰','0.51 s'],['⭐','Premium'], ['💾','68 GB VRAM']], 'faster-qwen3-tts-streaming': [['<span class="mdi mdi-lightning-bolt"></span>','~0.1× GPU'],['⏰','0.51 s'],['⭐','Premium'], ['💾','68 GB VRAM']],
'magpie-tts': [['','~0.05× GPU'],['⏰','0.30.8 s'],['⭐','High'], ['💾','46 GB VRAM']], 'magpie-tts': [['<span class="mdi mdi-lightning-bolt"></span>','~0.05× GPU'],['⏰','0.30.8 s'],['⭐','High'], ['💾','46 GB VRAM']],
'parakeet-asr': [['','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-TDT'],['💾','2 GB VRAM']], 'parakeet-asr': [['<span class="mdi mdi-lightning-bolt"></span>','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-TDT'],['💾','2 GB VRAM']],
'parakeet-rnnt-nim': [['','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-1B'], ['💾','2 GB VRAM']], 'parakeet-rnnt-nim': [['<span class="mdi mdi-lightning-bolt"></span>','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-1B'], ['💾','2 GB VRAM']],
}; };
const roleIcon = { tts: '🔊', stt: '🎙', 'stt+tts': '🔄', llm: '🤖' }; const roleIcon = { tts: '<span class="mdi mdi-speaker-outline"></span>', stt: '<span class="mdi mdi-microphone-variant"></span>', 'stt+tts': '🔄', llm: '<span class="mdi mdi-robot-outline"></span>' };
grid.innerHTML = containers.map(c => { grid.innerHTML = containers.map(c => {
const st = c.status || 'not_found'; const st = c.status || 'not_found';
@ -6493,7 +6493,7 @@ function renderLocalContainers(containers) {
: `<button class="btn-primary dc-btn" data-dc-action="start" data-dc-name="${n}">Start</button> : `<button class="btn-primary dc-btn" data-dc-action="start" data-dc-name="${n}">Start</button>
<button class="btn-secondary dc-btn" data-dc-action="restart" data-dc-name="${n}">Restart</button>`) <button class="btn-secondary dc-btn" data-dc-action="restart" data-dc-name="${n}">Restart</button>`)
: (c.repo : (c.repo
? `<a class="btn-secondary dc-btn" href="${escHtml(c.repo)}" target="_blank" rel="noopener">View on GitHub &#8599;</a>` ? `<a class="btn-secondary dc-btn" href="${escHtml(c.repo)}" target="_blank" rel="noopener">View on GitHub <span class="mdi mdi-open-in-new link-icon"></span></a>`
: ''); : '');
return `<div class="dc-card${st === 'running' ? ' dc-card-running' : ''}"> return `<div class="dc-card${st === 'running' ? ' dc-card-running' : ''}">
@ -6864,8 +6864,8 @@ function renderHistory() {
</div> </div>
<div class="history-item-text">${preview}</div> <div class="history-item-text">${preview}</div>
<div class="history-item-actions"> <div class="history-item-actions">
<button class="btn-secondary btn-xs hist-play-btn" ${item.blob?'':'disabled'}>&#9654; Play</button> <button class="btn-secondary btn-xs hist-play-btn" ${item.blob?'':'disabled'}><span class="mdi mdi-play"></span> Play</button>
<button class="btn-secondary btn-xs hist-reuse-btn">&#8629; Reuse</button> <button class="btn-secondary btn-xs hist-reuse-btn"><span class="mdi mdi-restore"></span> Reuse</button>
<button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?'':'disabled'}>&#43; Playlist</button> <button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?'':'disabled'}>&#43; Playlist</button>
</div> </div>
</div>`; </div>`;
@ -6932,9 +6932,9 @@ function renderPlaylist() {
<span class="playlist-text">${escHtml(item.text.length > 70 ? item.text.slice(0,70)+'…' : item.text)}</span> <span class="playlist-text">${escHtml(item.text.length > 70 ? item.text.slice(0,70)+'…' : item.text)}</span>
</div> </div>
<div class="playlist-item-actions"> <div class="playlist-item-actions">
<button class="btn-secondary btn-xs pl-up" ${i===0?'disabled':''}>&#8593;</button> <button class="btn-secondary btn-xs pl-up" ${i===0?'disabled':''}><span class="mdi mdi-upload"></span></button>
<button class="btn-secondary btn-xs pl-dn" ${i===_playlist.length-1?'disabled':''}>&#8595;</button> <button class="btn-secondary btn-xs pl-dn" ${i===_playlist.length-1?'disabled':''}><span class="mdi mdi-download"></span></button>
<button class="btn-secondary btn-xs pl-rm">&#10005;</button> <button class="btn-secondary btn-xs pl-rm"><span class="mdi mdi-close"></span></button>
</div> </div>
</div>`).join(''); </div>`).join('');

View File

@ -7,6 +7,7 @@
<!-- WaveSurfer must load synchronously before app.js initialises audio --> <!-- WaveSurfer must load synchronously before app.js initialises audio -->
<script src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"></script> <script src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"></script>
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script> <script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.3.2/css/flag-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.3.2/css/flag-icons.min.css">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
@ -32,17 +33,17 @@
<!-- Sidebar --> <!-- Sidebar -->
<aside id="sidebar"> <aside id="sidebar">
<div class="sidebar-brand"> <div class="sidebar-brand">
<h1>&#127908; Voice Creator</h1> <h1><span class="mdi mdi-microphone-variant"></span> Voice Creator</h1>
<p>Clone &middot; Design &middot; Deploy</p> <p>Clone &middot; Design &middot; Deploy</p>
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<div class="nav-group-label">Voices</div> <div class="nav-group-label">Voices</div>
<div class="nav-item nav-tree-head active" data-nav-section="s-voices" id="nav-voices-head" onclick="navTo('s-voices')"> <div class="nav-item nav-tree-head active" data-nav-section="s-voices" id="nav-voices-head" onclick="navTo('s-voices')">
<span class="nav-icon">&#128266;</span> <span class="nav-icon"><span class="mdi mdi-account-voice"></span></span>
<span class="nav-label">My Voices</span> <span class="nav-label">My Voices</span>
<span class="nav-badge" id="nav-voices-count"></span> <span class="nav-badge" id="nav-voices-count"></span>
<span class="nav-chevron" id="nav-voices-chevron">&#8964;</span> <span class="nav-chevron" id="nav-voices-chevron"><span class="mdi mdi-chevron-down"></span></span>
</div> </div>
<div class="nav-tree open" id="nav-voices-tree"> <div class="nav-tree open" id="nav-voices-tree">
<div class="nav-tree-item is-active" data-voice-cat="all" onclick="navVoicesCat('all')">All voices <span class="ntc" id="ntc-all"></span></div> <div class="nav-tree-item is-active" data-voice-cat="all" onclick="navVoicesCat('all')">All voices <span class="ntc" id="ntc-all"></span></div>
@ -53,36 +54,36 @@
<div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')">Library tools</div> <div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')">Library tools</div>
</div> </div>
<div class="nav-item" data-nav-section="s-clone" onclick="navTo('s-clone')"> <div class="nav-item" data-nav-section="s-clone" onclick="navTo('s-clone')">
<span class="nav-icon">&#127908;</span> Clone a Voice <span class="nav-icon"><span class="mdi mdi-microphone-variant"></span></span> Clone a Voice
</div> </div>
<div class="nav-item" data-nav-section="s-design" onclick="navTo('s-design')"> <div class="nav-item" data-nav-section="s-design" onclick="navTo('s-design')">
<span class="nav-icon">&#10024;</span> Design a Voice <span class="nav-icon"><span class="mdi mdi-auto-fix"></span></span> Design a Voice
</div> </div>
<div class="nav-item" data-nav-section="s-studio" onclick="navTo('s-studio')"> <div class="nav-item" data-nav-section="s-studio" onclick="navTo('s-studio')">
<span class="nav-icon">&#127760;</span> Get Voices Online <span class="nav-icon"><span class="mdi mdi-earth"></span></span> Get Voices Online
</div> </div>
<div class="nav-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')"> <div class="nav-item" data-nav-section="s-tryout" onclick="navTo('s-tryout')">
<span class="nav-icon">&#9654;</span> Try It Out <span class="nav-icon"><span class="mdi mdi-play"></span></span> Try It Out
</div> </div>
<div class="nav-group-label" style="margin-top:6px">Setup</div> <div class="nav-group-label" style="margin-top:6px">Setup</div>
<div class="nav-item" data-nav-section="s-llms" onclick="navTo('s-llms')"> <div class="nav-item" data-nav-section="s-llms" onclick="navTo('s-llms')">
<span class="nav-icon">&#129504;</span> LLMs <span class="nav-icon"><span class="mdi mdi-brain"></span></span> LLMs
</div> </div>
<div class="nav-item" data-nav-section="s-routing" onclick="navTo('s-routing')"> <div class="nav-item" data-nav-section="s-routing" onclick="navTo('s-routing')">
<span class="nav-icon">&#8652;</span> App Routing <span class="nav-icon"><span class="mdi mdi-swap-horizontal"></span></span> App Routing
</div> </div>
<div class="nav-item" data-nav-section="s-connect" onclick="navTo('s-connect')"> <div class="nav-item" data-nav-section="s-connect" onclick="navTo('s-connect')">
<span class="nav-icon">&#128279;</span> Connect Apps <span class="nav-icon"><span class="mdi mdi-api"></span></span> Connect Apps
</div> </div>
<div class="nav-item" data-nav-section="s-settings" onclick="navTo('s-settings')"> <div class="nav-item" data-nav-section="s-settings" onclick="navTo('s-settings')">
<span class="nav-icon">&#9881;</span> Settings <span class="nav-icon"><span class="mdi mdi-cog-outline"></span></span> Settings
</div> </div>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<button id="theme-btn" title="Switch theme" style="display:none">&#9728;</button> <button id="theme-btn" title="Switch theme" style="display:none"><span class="mdi mdi-weather-sunny"></span></button>
<button id="settings-btn" onclick="navTo('s-settings')" title="Open settings">&#9881; Settings</button> <button id="settings-btn" onclick="navTo('s-settings')" title="Open settings"><span class="mdi mdi-cog-outline"></span> Settings</button>
</div> </div>
</aside> </aside>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#127908;</span> <span class="section-icon"><span class="mdi mdi-microphone-variant"></span></span>
<div class="section-title"> <div class="section-title">
<h2>Clone a Voice</h2> <h2>Clone a Voice</h2>
<p>Upload or record 3-20 seconds of audio, trim it, then save it as a reusable voice clone.</p> <p>Upload or record 3-20 seconds of audio, trim it, then save it as a reusable voice clone.</p>
@ -31,8 +31,8 @@
<h2>Microphone</h2> <h2>Microphone</h2>
<p class="card-subtitle">Record a fresh sample with live input level monitoring.</p> <p class="card-subtitle">Record a fresh sample with live input level monitoring.</p>
<div class="btn-row"> <div class="btn-row">
<button class="btn-red" id="rec-start-btn">&#9679; Record</button> <button class="btn-red" id="rec-start-btn"><span class="mdi mdi-record-circle-outline"></span> Record</button>
<button class="btn-secondary" id="rec-stop-btn" disabled>&#9632; Stop</button> <button class="btn-secondary" id="rec-stop-btn" disabled><span class="mdi mdi-stop-circle-outline"></span> Stop</button>
<div id="rec-indicator"><div id="rec-dot"></div><span id="rec-time">0:00</span></div> <div id="rec-indicator"><div id="rec-dot"></div><span id="rec-time">0:00</span></div>
</div> </div>
</div> </div>
@ -48,8 +48,8 @@
<div class="btn-row"> <div class="btn-row">
<button class="btn-primary" id="trim-btn">Confirm trim &rarr; Name &amp; Save</button> <button class="btn-primary" id="trim-btn">Confirm trim &rarr; Name &amp; Save</button>
<button class="btn-secondary" id="auto-trim-btn">Auto trim</button> <button class="btn-secondary" id="auto-trim-btn">Auto trim</button>
<button class="btn-secondary" id="play-btn">&#9654; Play</button> <button class="btn-secondary" id="play-btn"><span class="mdi mdi-play"></span> Play</button>
<button class="btn-secondary" id="play-selection-btn">&#9654; Play selection</button> <button class="btn-secondary" id="play-selection-btn"><span class="mdi mdi-play"></span> Play selection</button>
</div> </div>
</div> </div>
</div><!-- /tab-source --> </div><!-- /tab-source -->
@ -95,7 +95,7 @@
<h2>Transcript (reference text)</h2> <h2>Transcript (reference text)</h2>
<p class="card-subtitle">The spoken text that matches this audio — type it or auto-transcribe.</p> <p class="card-subtitle">The spoken text that matches this audio — type it or auto-transcribe.</p>
<div class="btn-row"> <div class="btn-row">
<button class="btn-secondary" id="transcribe-btn">&#128172; Auto-transcribe</button> <button class="btn-secondary" id="transcribe-btn"><span class="mdi mdi-message-text-outline"></span> Auto-transcribe</button>
<span id="transcribe-status" style="font-size:13px;color:var(--subtext)"></span> <span id="transcribe-status" style="font-size:13px;color:var(--subtext)"></span>
</div> </div>
<textarea id="transcript-area" placeholder="Type or auto-transcribe the spoken text&hellip;"></textarea> <textarea id="transcript-area" placeholder="Type or auto-transcribe the spoken text&hellip;"></textarea>
@ -110,7 +110,7 @@
<h2>Step 4 &mdash; Save to library</h2> <h2>Step 4 &mdash; Save to library</h2>
<p class="card-subtitle">Write the cloned voice to your active voices folder.</p> <p class="card-subtitle">Write the cloned voice to your active voices folder.</p>
<div class="btn-row"> <div class="btn-row">
<button class="btn-green" id="save-btn">&#128190; Save to Voice Library</button> <button class="btn-green" id="save-btn"><span class="mdi mdi-memory"></span> Save to Voice Library</button>
</div> </div>
<div id="save-result" class="note" style="display:none"></div> <div id="save-result" class="note" style="display:none"></div>
</div> </div>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#128279;</span> <span class="section-icon"><span class="mdi mdi-api"></span></span>
<div class="section-title"> <div class="section-title">
<h2>Connect Your Apps</h2> <h2>Connect Your Apps</h2>
<p>Copy ready-made configuration snippets for SillyTavern, Open WebUI, Home Assistant, and more.</p> <p>Copy ready-made configuration snippets for SillyTavern, Open WebUI, Home Assistant, and more.</p>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#10024;</span> <span class="section-icon"><span class="mdi mdi-auto-fix"></span></span>
<div class="section-title"> <div class="section-title">
<h2>Design a Voice</h2> <h2>Design a Voice</h2>
<p>Describe a voice in words and let the AI create it. No recording needed.</p> <p>Describe a voice in words and let the AI create it. No recording needed.</p>
@ -70,7 +70,7 @@
<p class="card-subtitle">This shared reference transcript is used by samples, prompt presets, generation, preview, download, and export to the Voice Clone Library.</p> <p class="card-subtitle">This shared reference transcript is used by samples, prompt presets, generation, preview, download, and export to the Voice Clone Library.</p>
<textarea id="design-sample-text" placeholder="The exact text the generated voice should speak...">Hello! This is a voice design sample. I hope you enjoy listening to me.</textarea> <textarea id="design-sample-text" placeholder="The exact text the generated voice should speak...">Hello! This is a voice design sample. I hope you enjoy listening to me.</textarea>
<div class="btn-row"> <div class="btn-row">
<button class="btn-teal" id="design-generate-btn" style="align-self:flex-end">&#10024; Generate voice</button> <button class="btn-teal" id="design-generate-btn" style="align-self:flex-end"><span class="mdi mdi-auto-fix"></span> Generate voice</button>
<span id="design-status" style="font-size:13px;color:var(--subtext);align-self:flex-end"></span> <span id="design-status" style="font-size:13px;color:var(--subtext);align-self:flex-end"></span>
</div> </div>
</div> </div>
@ -115,9 +115,9 @@
<textarea id="d-transcript" placeholder="The spoken text from the sample (auto-filled)&hellip;"></textarea> <textarea id="d-transcript" placeholder="The spoken text from the sample (auto-filled)&hellip;"></textarea>
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button class="btn-secondary" id="design-retry-btn">&#8635; Regenerate</button> <button class="btn-secondary" id="design-retry-btn"><span class="mdi mdi-refresh"></span> Regenerate</button>
<button class="btn-secondary" id="design-download-btn">&#8595; Download WAV</button> <button class="btn-secondary" id="design-download-btn"><span class="mdi mdi-download"></span> Download WAV</button>
<button class="btn-green" id="design-save-btn">&#128190; Export to Voice Clone Library</button> <button class="btn-green" id="design-save-btn"><span class="mdi mdi-memory"></span> Export to Voice Clone Library</button>
</div> </div>
<div id="design-save-result" style="display:none"> <div id="design-save-result" style="display:none">
<hr> <hr>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#129302;</span> <span class="section-icon"><span class="mdi mdi-robot-outline"></span></span>
<div class="section-title"> <div class="section-title">
<h2>AI Backends</h2> <h2>AI Backends</h2>
<p>Connect local or cloud services for speech recognition, synthesis, and text generation.</p> <p>Connect local or cloud services for speech recognition, synthesis, and text generation.</p>
@ -8,8 +8,8 @@
<!-- Category tabs: Local / Online --> <!-- Category tabs: Local / Online -->
<div class="llm-cat-tabs" id="llm-cat-tabs"> <div class="llm-cat-tabs" id="llm-cat-tabs">
<button class="llm-cat-tab active" data-llm-cat="local">&#128187; Local</button> <button class="llm-cat-tab active" data-llm-cat="local"><span class="mdi mdi-laptop"></span> Local</button>
<button class="llm-cat-tab" data-llm-cat="online">&#127760; Online APIs</button> <button class="llm-cat-tab" data-llm-cat="online"><span class="mdi mdi-earth"></span> Online APIs</button>
</div> </div>
<!-- ── LOCAL section ──────────────────────────────────────── --> <!-- ── LOCAL section ──────────────────────────────────────── -->
@ -28,7 +28,7 @@
<h4 class="llm-local-cat" style="margin:0">Active Docker Stack</h4> <h4 class="llm-local-cat" style="margin:0">Active Docker Stack</h4>
<p class="llm-sec-note" style="margin:2px 0 0">Start, stop, or install TTS and ASR containers running on this machine.</p> <p class="llm-sec-note" style="margin:2px 0 0">Start, stop, or install TTS and ASR containers running on this machine.</p>
</div> </div>
<button class="btn-secondary" id="dc-refresh-btn" type="button">&#8635; Refresh</button> <button class="btn-secondary" id="dc-refresh-btn" type="button"><span class="mdi mdi-refresh"></span> Refresh</button>
</div> </div>
<div id="dc-grid" class="dc-grid"> <div id="dc-grid" class="dc-grid">
<div class="dc-loading">Checking container status&hellip;</div> <div class="dc-loading">Checking container status&hellip;</div>
@ -40,7 +40,7 @@
<div class="llm-local-card"> <div class="llm-local-card">
<div class="llm-local-head"> <div class="llm-local-head">
<span class="llm-local-icon">🦙</span> <span class="llm-local-icon"><span class="mdi mdi-head-dots-horizontal-outline"></span></span>
<span class="llm-local-name">Ollama</span> <span class="llm-local-name">Ollama</span>
<span class="llm-local-compat">OpenAI-compat</span> <span class="llm-local-compat">OpenAI-compat</span>
</div> </div>
@ -58,12 +58,12 @@
<pre>curl https://ollama.ai/install.sh | sh <pre>curl https://ollama.ai/install.sh | sh
ollama pull llama3.3</pre> ollama pull llama3.3</pre>
</div> </div>
<a class="llm-local-link" href="https://ollama.ai" target="_blank" rel="noopener">ollama.ai &#8599;</a> <a class="llm-local-link" href="https://ollama.ai" target="_blank" rel="noopener">ollama.ai <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-local-card llm-local-card-running"> <div class="llm-local-card llm-local-card-running">
<div class="llm-local-head"> <div class="llm-local-head">
<span class="llm-local-icon"></span> <span class="llm-local-icon"><span class="mdi mdi-lightning-bolt"></span></span>
<span class="llm-local-name">vLLM</span> <span class="llm-local-name">vLLM</span>
<span class="llm-local-compat llm-compat-detected">Detected in stack</span> <span class="llm-local-compat llm-compat-detected">Detected in stack</span>
</div> </div>
@ -81,7 +81,7 @@ ollama pull llama3.3</pre>
<pre>--model Qwen/Qwen3-8B \ <pre>--model Qwen/Qwen3-8B \
--served-model-name qwen3-8b</pre> --served-model-name qwen3-8b</pre>
</div> </div>
<a class="llm-local-link" href="https://docs.vllm.ai" target="_blank" rel="noopener">docs.vllm.ai &#8599;</a> <a class="llm-local-link" href="https://docs.vllm.ai" target="_blank" rel="noopener">docs.vllm.ai <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-local-card"> <div class="llm-local-card">
@ -101,7 +101,7 @@ ollama pull llama3.3</pre>
<pre>Developer tab → Start server → Port 1234 <pre>Developer tab → Start server → Port 1234
Check "Enable CORS" for browser access</pre> Check "Enable CORS" for browser access</pre>
</div> </div>
<a class="llm-local-link" href="https://lmstudio.ai" target="_blank" rel="noopener">lmstudio.ai &#8599;</a> <a class="llm-local-link" href="https://lmstudio.ai" target="_blank" rel="noopener">lmstudio.ai <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-local-card"> <div class="llm-local-card">
@ -125,7 +125,7 @@ Check "Enable CORS" for browser access</pre>
-hf QuantFactory/Meta-Llama-3-8B-GGUF \ -hf QuantFactory/Meta-Llama-3-8B-GGUF \
-hff Meta-Llama-3-8B.Q4_K_M.gguf</pre> -hff Meta-Llama-3-8B.Q4_K_M.gguf</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener">github.com/ggml-org/llama.cpp &#8599;</a> <a class="llm-local-link" href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener">github.com/ggml-org/llama.cpp <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
</div> </div>
@ -136,15 +136,15 @@ Check "Enable CORS" for browser access</pre>
<div class="llm-local-card"> <div class="llm-local-card">
<div class="llm-local-head"> <div class="llm-local-head">
<span class="llm-local-icon">🎙</span> <span class="llm-local-icon"><span class="mdi mdi-microphone-variant"></span></span>
<span class="llm-local-name">faster-whisper-server</span> <span class="llm-local-name">faster-whisper-server</span>
<span class="llm-local-compat">OpenAI-compat</span> <span class="llm-local-compat">OpenAI-compat</span>
</div> </div>
<div class="llm-local-metrics"> <div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~70&times; RT GPU</span> <span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~70&times; RT GPU</span>
<span class="llm-metric-chip">&#9200; 0.52 s</span> <span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> 0.52 s</span>
<span class="llm-metric-chip">&#11088; large-v3</span> <span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> large-v3</span>
<span class="llm-metric-chip">&#128190; 1.5 GB VRAM</span> <span class="llm-metric-chip"><span class="mdi mdi-memory"></span> 1.5 GB VRAM</span>
</div> </div>
<p class="llm-local-desc">Drop-in local replacement for the Whisper API. GPU-accelerated via CTranslate2. OpenAI-compatible endpoint.</p> <p class="llm-local-desc">Drop-in local replacement for the Whisper API. GPU-accelerated via CTranslate2. OpenAI-compatible endpoint.</p>
<div class="llm-local-url"> <div class="llm-local-url">
@ -153,7 +153,7 @@ Check "Enable CORS" for browser access</pre>
<button class="llm-local-ping" data-ping-key="faster-whisper" title="Test connection">Connect</button> <button class="llm-local-ping" data-ping-key="faster-whisper" title="Test connection">Connect</button>
</div> </div>
<div class="llm-local-actions"> <div class="llm-local-actions">
<button class="llm-use-btn" id="llm-use-faster-whisper-stt" type="button" title="Copy URL to Settings → faster-whisper-server URL and set as active STT">&#128203; Use as STT</button> <button class="llm-use-btn" id="llm-use-faster-whisper-stt" type="button" title="Copy URL to Settings → faster-whisper-server URL and set as active STT"><span class="mdi mdi-content-copy"></span> Use as STT</button>
</div> </div>
<div class="llm-local-snippet"> <div class="llm-local-snippet">
<div class="llm-snippet-bar"> <div class="llm-snippet-bar">
@ -171,7 +171,7 @@ Check "Enable CORS" for browser access</pre>
reservations: reservations:
devices: [{driver: nvidia, count: 1, capabilities: [gpu]}]</pre> devices: [{driver: nvidia, count: 1, capabilities: [gpu]}]</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/fedirz/faster-whisper-server" target="_blank" rel="noopener">github.com/fedirz/faster-whisper-server &#8599;</a> <a class="llm-local-link" href="https://github.com/fedirz/faster-whisper-server" target="_blank" rel="noopener">github.com/fedirz/faster-whisper-server <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-local-card"> <div class="llm-local-card">
@ -181,10 +181,10 @@ Check "Enable CORS" for browser access</pre>
<span class="llm-local-compat">HTTP server</span> <span class="llm-local-compat">HTTP server</span>
</div> </div>
<div class="llm-local-metrics"> <div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~815&times; RT CPU</span> <span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~815&times; RT CPU</span>
<span class="llm-metric-chip">&#9200; 15 s</span> <span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> 15 s</span>
<span class="llm-metric-chip">&#11088; large-v3 Q5</span> <span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> large-v3 Q5</span>
<span class="llm-metric-chip">&#128190; ~1 GB RAM</span> <span class="llm-metric-chip"><span class="mdi mdi-memory"></span> ~1 GB RAM</span>
</div> </div>
<p class="llm-local-desc">Minimal C++ Whisper with a built-in HTTP server. CPU or Metal/CUDA. Low memory, fast on consumer hardware.</p> <p class="llm-local-desc">Minimal C++ Whisper with a built-in HTTP server. CPU or Metal/CUDA. Low memory, fast on consumer hardware.</p>
<div class="llm-local-url"> <div class="llm-local-url">
@ -193,7 +193,7 @@ Check "Enable CORS" for browser access</pre>
<button class="llm-local-ping" data-ping-key="whisper-cpp" title="Test connection">Connect</button> <button class="llm-local-ping" data-ping-key="whisper-cpp" title="Test connection">Connect</button>
</div> </div>
<div class="llm-local-actions"> <div class="llm-local-actions">
<button class="llm-use-btn" id="llm-use-whisper-cpp-stt" type="button" title="Copy URL to Settings → whisper.cpp URL and set as active STT">&#128203; Use as STT</button> <button class="llm-use-btn" id="llm-use-whisper-cpp-stt" type="button" title="Copy URL to Settings → whisper.cpp URL and set as active STT"><span class="mdi mdi-content-copy"></span> Use as STT</button>
</div> </div>
<div class="llm-local-snippet"> <div class="llm-local-snippet">
<div class="llm-snippet-bar"> <div class="llm-snippet-bar">
@ -205,7 +205,7 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
./build/bin/whisper-server \ ./build/bin/whisper-server \
-m models/ggml-large-v3.bin --port 8080</pre> -m models/ggml-large-v3.bin --port 8080</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/ggml-org/whisper.cpp" target="_blank" rel="noopener">github.com/ggml-org/whisper.cpp &#8599;</a> <a class="llm-local-link" href="https://github.com/ggml-org/whisper.cpp" target="_blank" rel="noopener">github.com/ggml-org/whisper.cpp <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
</div> </div>
@ -216,15 +216,15 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
<div class="llm-local-card"> <div class="llm-local-card">
<div class="llm-local-head"> <div class="llm-local-head">
<span class="llm-local-icon">🔊</span> <span class="llm-local-icon"><span class="mdi mdi-speaker-outline"></span></span>
<span class="llm-local-name">Piper TTS</span> <span class="llm-local-name">Piper TTS</span>
<span class="llm-local-compat">Fast &middot; offline</span> <span class="llm-local-compat">Fast &middot; offline</span>
</div> </div>
<div class="llm-local-metrics"> <div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~1× CPU realtime</span> <span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~1× CPU realtime</span>
<span class="llm-metric-chip">&#9200; &lt;50 ms</span> <span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> &lt;50 ms</span>
<span class="llm-metric-chip">&#11088; Good (VITS)</span> <span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> Good (VITS)</span>
<span class="llm-metric-chip">&#128190; ~50 MB RAM</span> <span class="llm-metric-chip"><span class="mdi mdi-memory"></span> ~50 MB RAM</span>
</div> </div>
<p class="llm-local-desc">Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Uses Wyoming protocol (port 10200) &mdash; not directly OpenAI-compatible.</p> <p class="llm-local-desc">Lightning-fast offline TTS. Runs on CPU in real time. 50+ language voices available. Uses Wyoming protocol (port 10200) &mdash; not directly OpenAI-compatible.</p>
<div class="llm-local-url"> <div class="llm-local-url">
@ -241,7 +241,7 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
rhasspy/wyoming-piper \ rhasspy/wyoming-piper \
--voice en_US-lessac-medium</pre> --voice en_US-lessac-medium</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/rhasspy/piper" target="_blank" rel="noopener">github.com/rhasspy/piper &#8599;</a> <a class="llm-local-link" href="https://github.com/rhasspy/piper" target="_blank" rel="noopener">github.com/rhasspy/piper <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-local-card"> <div class="llm-local-card">
@ -251,10 +251,10 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
<span class="llm-local-compat">OpenAI-compat TTS</span> <span class="llm-local-compat">OpenAI-compat TTS</span>
</div> </div>
<div class="llm-local-metrics"> <div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~0.1&times; CPU RTF</span> <span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~0.1&times; CPU RTF</span>
<span class="llm-metric-chip">&#9200; ~200 ms</span> <span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> ~200 ms</span>
<span class="llm-metric-chip">&#11088; High (82M)</span> <span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> High (82M)</span>
<span class="llm-metric-chip">&#128190; 300 MB CPU</span> <span class="llm-metric-chip"><span class="mdi mdi-memory"></span> 300 MB CPU</span>
</div> </div>
<p class="llm-local-desc">Kokoro-82M running behind an OpenAI-compatible TTS endpoint. 11 built-in voices (af_bella, bf_emma, am_adam&hellip;). Drop-in replacement for OpenAI&rsquo;s TTS API.</p> <p class="llm-local-desc">Kokoro-82M running behind an OpenAI-compatible TTS endpoint. 11 built-in voices (af_bella, bf_emma, am_adam&hellip;). Drop-in replacement for OpenAI&rsquo;s TTS API.</p>
<div class="llm-local-url"> <div class="llm-local-url">
@ -263,7 +263,7 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
<button class="llm-local-ping" data-ping-key="kokoro" title="Test connection">Connect</button> <button class="llm-local-ping" data-ping-key="kokoro" title="Test connection">Connect</button>
</div> </div>
<div class="llm-local-actions"> <div class="llm-local-actions">
<button class="llm-use-btn" id="llm-use-kokoro-tts" type="button" title="Copy URL to Settings → Kokoro FastAPI URL and enable Kokoro in Try It Out">&#128203; Use as TTS</button> <button class="llm-use-btn" id="llm-use-kokoro-tts" type="button" title="Copy URL to Settings → Kokoro FastAPI URL and enable Kokoro in Try It Out"><span class="mdi mdi-content-copy"></span> Use as TTS</button>
</div> </div>
<div class="llm-local-snippet"> <div class="llm-local-snippet">
<div class="llm-snippet-bar"> <div class="llm-snippet-bar">
@ -276,7 +276,7 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
docker run -p 8880:8880 --gpus all \ docker run -p 8880:8880 --gpus all \
ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.2</pre> ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.2</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/remsky/Kokoro-FastAPI" target="_blank" rel="noopener">github.com/remsky/Kokoro-FastAPI &#8599;</a> <a class="llm-local-link" href="https://github.com/remsky/Kokoro-FastAPI" target="_blank" rel="noopener">github.com/remsky/Kokoro-FastAPI <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-local-card"> <div class="llm-local-card">
@ -286,10 +286,10 @@ docker run -p 8880:8880 --gpus all \
<span class="llm-local-compat">Voice cloning</span> <span class="llm-local-compat">Voice cloning</span>
</div> </div>
<div class="llm-local-metrics"> <div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~0.5× GPU RTF</span> <span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~0.5× GPU RTF</span>
<span class="llm-metric-chip">&#9200; 13 s</span> <span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> 13 s</span>
<span class="llm-metric-chip">&#11088; High (WAV clone)</span> <span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> High (WAV clone)</span>
<span class="llm-metric-chip">&#128190; 34 GB VRAM</span> <span class="llm-metric-chip"><span class="mdi mdi-memory"></span> 34 GB VRAM</span>
</div> </div>
<p class="llm-local-desc">Coqui XTTS &mdash; multilingual voice cloning from a 6-second sample. 17 languages. Compatible with this app&rsquo;s voice library.</p> <p class="llm-local-desc">Coqui XTTS &mdash; multilingual voice cloning from a 6-second sample. 17 languages. Compatible with this app&rsquo;s voice library.</p>
<div class="llm-local-url"> <div class="llm-local-url">
@ -306,13 +306,13 @@ docker run -p 8880:8880 --gpus all \
-v /voices:/voices \ -v /voices:/voices \
daswer123/xtts-api-server:latest</pre> daswer123/xtts-api-server:latest</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/daswer123/xtts-api-server" target="_blank" rel="noopener">xtts-api-server &#8599;</a> <a class="llm-local-link" href="https://github.com/daswer123/xtts-api-server" target="_blank" rel="noopener">xtts-api-server <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
</div> </div>
<div class="llm-local-howto"> <div class="llm-local-howto">
<div class="llm-howto-icon">&#128161;</div> <div class="llm-howto-icon"><span class="mdi mdi-lightbulb-outline"></span></div>
<div class="llm-howto-body"> <div class="llm-howto-body">
<strong>Adding a local service to this app</strong> <strong>Adding a local service to this app</strong>
<p>All OpenAI-compatible services work the same way: open <em>App Routing</em> in the sidebar, add a new backend entry with the local URL, leave the API key field empty, and set it as the default backend for the voice type you want.</p> <p>All OpenAI-compatible services work the same way: open <em>App Routing</em> in the sidebar, add a new backend entry with the local URL, leave the API key field empty, and set it as the default backend for the voice type you want.</p>
@ -332,12 +332,12 @@ docker run -p 8880:8880 --gpus all \
</div> </div>
<!-- ── ASR / STT ─────────────────────────────────────────── --> <!-- ── ASR / STT ─────────────────────────────────────────── -->
<h4 class="llm-local-cat" style="margin-bottom:12px">&#127908; ASR &middot; Speech-to-Text</h4> <h4 class="llm-local-cat" style="margin-bottom:12px"><span class="mdi mdi-microphone-variant"></span> ASR &middot; Speech-to-Text</h4>
<div class="llm-service-grid"> <div class="llm-service-grid">
<div class="llm-card llm-card-highlight"> <div class="llm-card llm-card-highlight">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#9889;</span> <span class="llm-card-icon"><span class="mdi mdi-lightning-bolt"></span></span>
<div> <div>
<div class="llm-card-name">Groq Whisper</div> <div class="llm-card-name">Groq Whisper</div>
<div class="llm-card-sub">whisper-large-v3-turbo &middot; OpenAI-compatible</div> <div class="llm-card-sub">whisper-large-v3-turbo &middot; OpenAI-compatible</div>
@ -346,13 +346,13 @@ docker run -p 8880:8880 --gpus all \
</div> </div>
<div class="llm-card-stats"> <div class="llm-card-stats">
<span>2 000 req / day</span> <span>2 000 req / day</span>
<span>&#9889; Fastest cloud STT</span> <span><span class="mdi mdi-lightning-bolt"></span> Fastest cloud STT</span>
<span>&#128190; Cloud &middot; 0 VRAM</span> <span><span class="mdi mdi-memory"></span> Cloud &middot; 0 VRAM</span>
</div> </div>
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="gsk_&hellip;" data-llm-key="groq_stt"> <input type="password" class="llm-input" placeholder="gsk_&hellip;" data-llm-key="groq_stt">
<a class="llm-link" href="https://console.groq.com" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://console.groq.com" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -368,7 +368,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-card"> <div class="llm-card">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#129303;</span> <span class="llm-card-icon"><span class="mdi mdi-emoticon-happy-outline"></span></span>
<div> <div>
<div class="llm-card-name">HuggingFace Inference</div> <div class="llm-card-name">HuggingFace Inference</div>
<div class="llm-card-sub">Serverless Whisper models</div> <div class="llm-card-sub">Serverless Whisper models</div>
@ -383,7 +383,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="hf_&hellip;" data-llm-key="hf_stt"> <input type="password" class="llm-input" placeholder="hf_&hellip;" data-llm-key="hf_stt">
<a class="llm-link" href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -393,7 +393,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-card"> <div class="llm-card">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#128203;</span> <span class="llm-card-icon"><span class="mdi mdi-content-copy"></span></span>
<div> <div>
<div class="llm-card-name">AssemblyAI</div> <div class="llm-card-name">AssemblyAI</div>
<div class="llm-card-sub">High-accuracy transcription + speaker diarization</div> <div class="llm-card-sub">High-accuracy transcription + speaker diarization</div>
@ -408,7 +408,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="AssemblyAI key&hellip;" data-llm-key="assemblyai"> <input type="password" class="llm-input" placeholder="AssemblyAI key&hellip;" data-llm-key="assemblyai">
<a class="llm-link" href="https://www.assemblyai.com" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://www.assemblyai.com" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -419,12 +419,12 @@ docker run -p 8880:8880 --gpus all \
</div> </div>
<!-- ── TTS ──────────────────────────────────────────────── --> <!-- ── TTS ──────────────────────────────────────────────── -->
<h4 class="llm-local-cat" style="margin-top:28px;margin-bottom:12px">&#128266; Text-to-Speech</h4> <h4 class="llm-local-cat" style="margin-top:28px;margin-bottom:12px"><span class="mdi mdi-account-voice"></span> Text-to-Speech</h4>
<div class="llm-service-grid"> <div class="llm-service-grid">
<div class="llm-card llm-card-highlight"> <div class="llm-card llm-card-highlight">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#128137;</span> <span class="llm-card-icon"><span class="mdi mdi-waveform"></span></span>
<div> <div>
<div class="llm-card-name">ElevenLabs</div> <div class="llm-card-name">ElevenLabs</div>
<div class="llm-card-sub">High-quality voice cloning &amp; synthesis</div> <div class="llm-card-sub">High-quality voice cloning &amp; synthesis</div>
@ -439,7 +439,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="xi-api-key&hellip;" data-llm-key="elevenlabs"> <input type="password" class="llm-input" placeholder="xi-api-key&hellip;" data-llm-key="elevenlabs">
<a class="llm-link" href="https://elevenlabs.io" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://elevenlabs.io" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -449,7 +449,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-card"> <div class="llm-card">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#127981;</span> <span class="llm-card-icon"><span class="mdi mdi-fish"></span></span>
<div> <div>
<div class="llm-card-name">Fish Audio</div> <div class="llm-card-name">Fish Audio</div>
<div class="llm-card-sub">Voice cloning &amp; multilingual TTS</div> <div class="llm-card-sub">Voice cloning &amp; multilingual TTS</div>
@ -464,7 +464,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="Fish Audio key&hellip;" data-llm-key="fish_audio"> <input type="password" class="llm-input" placeholder="Fish Audio key&hellip;" data-llm-key="fish_audio">
<a class="llm-link" href="https://fish.audio" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://fish.audio" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -474,7 +474,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-card"> <div class="llm-card">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#127925;</span> <span class="llm-card-icon"><span class="mdi mdi-music-note"></span></span>
<div> <div>
<div class="llm-card-name">Kokoro TTS</div> <div class="llm-card-name">Kokoro TTS</div>
<div class="llm-card-sub">82M model &middot; HuggingFace Spaces demo</div> <div class="llm-card-sub">82M model &middot; HuggingFace Spaces demo</div>
@ -489,19 +489,19 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-info-box"> <div class="llm-info-box">
Use the HF Spaces web demo for quick tests, or run Kokoro locally via Docker for production use. Use the HF Spaces web demo for quick tests, or run Kokoro locally via Docker for production use.
<br> <br>
<a class="llm-link-inline" href="https://huggingface.co/spaces/hexgrad/Kokoro-TTS" target="_blank" rel="noopener">&#8594; Open Kokoro HF Space</a> <a class="llm-link-inline" href="https://huggingface.co/spaces/hexgrad/Kokoro-TTS" target="_blank" rel="noopener"><span class="mdi mdi-arrow-right"></span> Open Kokoro HF Space</a>
</div> </div>
</div> </div>
</div> </div>
<!-- ── LLM ──────────────────────────────────────────────── --> <!-- ── LLM ──────────────────────────────────────────────── -->
<h4 class="llm-local-cat" style="margin-top:28px;margin-bottom:12px">&#129302; Large Language Models</h4> <h4 class="llm-local-cat" style="margin-top:28px;margin-bottom:12px"><span class="mdi mdi-robot-outline"></span> Large Language Models</h4>
<div class="llm-service-grid"> <div class="llm-service-grid">
<div class="llm-card llm-card-highlight"> <div class="llm-card llm-card-highlight">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#9889;</span> <span class="llm-card-icon"><span class="mdi mdi-lightning-bolt"></span></span>
<div> <div>
<div class="llm-card-name">Groq</div> <div class="llm-card-name">Groq</div>
<div class="llm-card-sub">Ultra-fast inference &middot; OpenAI-compatible</div> <div class="llm-card-sub">Ultra-fast inference &middot; OpenAI-compatible</div>
@ -516,7 +516,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="gsk_&hellip;" data-llm-key="groq_llm"> <input type="password" class="llm-input" placeholder="gsk_&hellip;" data-llm-key="groq_llm">
<a class="llm-link" href="https://console.groq.com" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://console.groq.com" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -531,7 +531,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-card"> <div class="llm-card">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#127760;</span> <span class="llm-card-icon"><span class="mdi mdi-earth"></span></span>
<div> <div>
<div class="llm-card-name">OpenRouter</div> <div class="llm-card-name">OpenRouter</div>
<div class="llm-card-sub">50+ free models &middot; OpenAI-compatible</div> <div class="llm-card-sub">50+ free models &middot; OpenAI-compatible</div>
@ -546,7 +546,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="sk-or-&hellip;" data-llm-key="openrouter"> <input type="password" class="llm-input" placeholder="sk-or-&hellip;" data-llm-key="openrouter">
<a class="llm-link" href="https://openrouter.ai" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://openrouter.ai" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -561,7 +561,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-card"> <div class="llm-card">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#128142;</span> <span class="llm-card-icon"><span class="mdi mdi-diamond-outline"></span></span>
<div> <div>
<div class="llm-card-name">Google Gemini</div> <div class="llm-card-name">Google Gemini</div>
<div class="llm-card-sub">Gemini 2.5 Flash &middot; Generous free tier</div> <div class="llm-card-sub">Gemini 2.5 Flash &middot; Generous free tier</div>
@ -576,7 +576,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="AIza&hellip;" data-llm-key="gemini"> <input type="password" class="llm-input" placeholder="AIza&hellip;" data-llm-key="gemini">
<a class="llm-link" href="https://ai.google.dev" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://ai.google.dev" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>
@ -587,7 +587,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-card"> <div class="llm-card">
<div class="llm-card-head"> <div class="llm-card-head">
<span class="llm-card-icon">&#127315;</span> <span class="llm-card-icon"><span class="mdi mdi-alpha-m-circle-outline"></span></span>
<div> <div>
<div class="llm-card-name">Mistral AI</div> <div class="llm-card-name">Mistral AI</div>
<div class="llm-card-sub">OpenAI-compatible &middot; EU-based</div> <div class="llm-card-sub">OpenAI-compatible &middot; EU-based</div>
@ -602,7 +602,7 @@ docker run -p 8880:8880 --gpus all \
<div class="llm-field-row"> <div class="llm-field-row">
<label class="llm-label">API key</label> <label class="llm-label">API key</label>
<input type="password" class="llm-input" placeholder="Mistral key&hellip;" data-llm-key="mistral"> <input type="password" class="llm-input" placeholder="Mistral key&hellip;" data-llm-key="mistral">
<a class="llm-link" href="https://mistral.ai" target="_blank" rel="noopener">Get key &#8599;</a> <a class="llm-link" href="https://mistral.ai" target="_blank" rel="noopener">Get key <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>
<div class="llm-endpoint"> <div class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span> <span class="llm-endpoint-label">Endpoint</span>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#8652;</span> <span class="section-icon"><span class="mdi mdi-swap-horizontal"></span></span>
<div class="section-title"> <div class="section-title">
<h2>App Routing</h2> <h2>App Routing</h2>
<p>Map incoming requests from apps like Open WebUI or SillyTavern to specific voices and backends.</p> <p>Map incoming requests from apps like Open WebUI or SillyTavern to specific voices and backends.</p>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#9881;</span> <span class="section-icon"><span class="mdi mdi-cog-outline"></span></span>
<div class="section-title"> <div class="section-title">
<h2>Settings</h2> <h2>Settings</h2>
<p>Configure backend URLs, API keys, voice folders, and playback preferences.</p> <p>Configure backend URLs, API keys, voice folders, and playback preferences.</p>
@ -130,7 +130,7 @@
<label>Groq API key <span style="font-weight:400">(Groq Whisper &amp; LLM)</span></label> <label>Groq API key <span style="font-weight:400">(Groq Whisper &amp; LLM)</span></label>
<div class="s-key-row"> <div class="s-key-row">
<input type="password" id="s-groq-api-key" placeholder="gsk_&hellip;" autocomplete="off"> <input type="password" id="s-groq-api-key" placeholder="gsk_&hellip;" autocomplete="off">
<button type="button" class="s-eye-btn" data-target="s-groq-api-key">&#128065;</button> <button type="button" class="s-eye-btn" data-target="s-groq-api-key"><span class="mdi mdi-eye-outline"></span></button>
</div> </div>
<span class="s-hint">Used for Groq Whisper STT (whisper-large-v3-turbo) and Groq LLM. Free: 2 000 req/day. Fastest cloud transcription.</span> <span class="s-hint">Used for Groq Whisper STT (whisper-large-v3-turbo) and Groq LLM. Free: 2 000 req/day. Fastest cloud transcription.</span>
</div> </div>
@ -247,21 +247,21 @@
<label>TTS API key <span style="font-weight:400">(optional)</span></label> <label>TTS API key <span style="font-weight:400">(optional)</span></label>
<div class="s-key-row"> <div class="s-key-row">
<input type="password" id="s-tts-key" placeholder="dummy, sk-local, or leave empty" autocomplete="off"> <input type="password" id="s-tts-key" placeholder="dummy, sk-local, or leave empty" autocomplete="off">
<button type="button" class="s-eye-btn" data-target="s-tts-key">&#128065;</button> <button type="button" class="s-eye-btn" data-target="s-tts-key"><span class="mdi mdi-eye-outline"></span></button>
</div> </div>
</div> </div>
<div class="s-field"> <div class="s-field">
<label>Voice Design API key <span style="font-weight:400">(optional)</span></label> <label>Voice Design API key <span style="font-weight:400">(optional)</span></label>
<div class="s-key-row"> <div class="s-key-row">
<input type="password" id="s-vd-key" placeholder="dummy, sk-local, or leave empty" autocomplete="off"> <input type="password" id="s-vd-key" placeholder="dummy, sk-local, or leave empty" autocomplete="off">
<button type="button" class="s-eye-btn" data-target="s-vd-key">&#128065;</button> <button type="button" class="s-eye-btn" data-target="s-vd-key"><span class="mdi mdi-eye-outline"></span></button>
</div> </div>
</div> </div>
<div class="s-field"> <div class="s-field">
<label>Whisper API key <span style="font-weight:400">(optional)</span></label> <label>Whisper API key <span style="font-weight:400">(optional)</span></label>
<div class="s-key-row"> <div class="s-key-row">
<input type="password" id="s-whisper-key" placeholder="dummy, sk-local, or leave empty" autocomplete="off"> <input type="password" id="s-whisper-key" placeholder="dummy, sk-local, or leave empty" autocomplete="off">
<button type="button" class="s-eye-btn" data-target="s-whisper-key">&#128065;</button> <button type="button" class="s-eye-btn" data-target="s-whisper-key"><span class="mdi mdi-eye-outline"></span></button>
</div> </div>
</div> </div>
</div> </div>
@ -277,9 +277,9 @@
<p class="s-hint" style="margin-top:4px">Export all voices and settings as a ZIP for backup or migration. Import to restore. API keys are excluded from exports.</p> <p class="s-hint" style="margin-top:4px">Export all voices and settings as a ZIP for backup or migration. Import to restore. API keys are excluded from exports.</p>
</div> </div>
<div class="btn-row" style="margin-top:12px;gap:10px;flex-wrap:wrap"> <div class="btn-row" style="margin-top:12px;gap:10px;flex-wrap:wrap">
<a class="btn-secondary" id="s-export-voices-btn" href="/api/voices/export" download>&#8595; Export voices</a> <a class="btn-secondary" id="s-export-voices-btn" href="/api/voices/export" download><span class="mdi mdi-download"></span> Export voices</a>
<label class="btn-secondary" style="cursor:pointer" title="Import a voices ZIP"> <label class="btn-secondary" style="cursor:pointer" title="Import a voices ZIP">
&#8593; Import voices <span class="mdi mdi-upload"></span> Import voices
<input type="file" id="s-import-voices-file" accept=".zip" style="display:none"> <input type="file" id="s-import-voices-file" accept=".zip" style="display:none">
</label> </label>
<span class="note" id="s-import-status"></span> <span class="note" id="s-import-status"></span>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#9654;</span> <span class="section-icon"><span class="mdi mdi-play"></span></span>
<div class="section-title"> <div class="section-title">
<h2>Try It Out</h2> <h2>Try It Out</h2>
<p>Generate speech from text using any backend and voice. Also transcribe audio and re-speak it.</p> <p>Generate speech from text using any backend and voice. Also transcribe audio and re-speak it.</p>
@ -74,9 +74,9 @@
</label> </label>
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button class="btn-primary" id="preview-btn">&#9654; Generate &amp; play</button> <button class="btn-primary" id="preview-btn"><span class="mdi mdi-play"></span> Generate &amp; play</button>
<button class="btn-secondary" id="save-preview-mp3-btn" disabled>&#8595; Save MP3</button> <button class="btn-secondary" id="save-preview-mp3-btn" disabled><span class="mdi mdi-download"></span> Save MP3</button>
<button class="btn-secondary" id="save-preview-btn" disabled>&#8595; Save WAV</button> <button class="btn-secondary" id="save-preview-btn" disabled><span class="mdi mdi-download"></span> Save WAV</button>
<button class="btn-secondary" id="add-to-playlist-btn" disabled>&#43; Playlist</button> <button class="btn-secondary" id="add-to-playlist-btn" disabled>&#43; Playlist</button>
</div> </div>
<audio id="preview-audio" controls style="display:none"></audio> <audio id="preview-audio" controls style="display:none"></audio>
@ -154,7 +154,7 @@
<div class="history-empty">No clips in playlist. Use <strong>+ Playlist</strong> after generating.</div> <div class="history-empty">No clips in playlist. Use <strong>+ Playlist</strong> after generating.</div>
</div> </div>
<div class="btn-row" style="margin-top:12px"> <div class="btn-row" style="margin-top:12px">
<button class="btn-primary" id="playlist-export-btn" disabled>&#8595; Export merged WAV</button> <button class="btn-primary" id="playlist-export-btn" disabled><span class="mdi mdi-download"></span> Export merged WAV</button>
<button class="btn-secondary btn-sm" id="playlist-clear-btn">Clear</button> <button class="btn-secondary btn-sm" id="playlist-clear-btn">Clear</button>
</div> </div>
</div> </div>
@ -164,7 +164,7 @@
<!-- STT -> TTS --> <!-- STT -> TTS -->
<div class="tab-content" id="tab-stt-tts"> <div class="tab-content" id="tab-stt-tts">
<div class="card"> <div class="card">
<h2>STT &#8594; TTS workspace</h2> <h2>STT <span class="mdi mdi-arrow-right"></span> TTS workspace</h2>
<p class="card-subtitle">Upload speech audio, transcribe it with the configured STT endpoint, then synthesize the resulting text with any available TTS backend.</p> <p class="card-subtitle">Upload speech audio, transcribe it with the configured STT endpoint, then synthesize the resulting text with any available TTS backend.</p>
</div> </div>
<div class="card"> <div class="card">
@ -183,7 +183,7 @@
<label>Speech audio</label> <label>Speech audio</label>
<div class="btn-row" style="gap:8px;align-items:center"> <div class="btn-row" style="gap:8px;align-items:center">
<input type="file" id="stt-tts-file" accept="audio/*,video/*"> <input type="file" id="stt-tts-file" accept="audio/*,video/*">
<button class="btn-secondary" id="stt-tts-rec-start" type="button" title="Record from microphone">&#127908; Record</button> <button class="btn-secondary" id="stt-tts-rec-start" type="button" title="Record from microphone"><span class="mdi mdi-microphone-variant"></span> Record</button>
<button class="btn-secondary" id="stt-tts-rec-stop" type="button" disabled>Stop</button> <button class="btn-secondary" id="stt-tts-rec-stop" type="button" disabled>Stop</button>
<span class="mic-timer" id="stt-tts-rec-time">0:00</span> <span class="mic-timer" id="stt-tts-rec-time">0:00</span>
</div> </div>
@ -227,7 +227,7 @@
<label class="refine-toggle"><input type="checkbox" id="refine-punctuation" checked> Fix punctuation</label> <label class="refine-toggle"><input type="checkbox" id="refine-punctuation" checked> Fix punctuation</label>
</div> </div>
<div class="btn-row" style="margin-top:12px"> <div class="btn-row" style="margin-top:12px">
<button class="btn-primary" id="refine-btn" disabled>&#10024; Refine transcription</button> <button class="btn-primary" id="refine-btn" disabled><span class="mdi mdi-auto-fix"></span> Refine transcription</button>
<button class="btn-secondary" id="refine-restore-btn" disabled>Restore original</button> <button class="btn-secondary" id="refine-restore-btn" disabled>Restore original</button>
<span class="note" id="refine-status"></span> <span class="note" id="refine-status"></span>
</div> </div>
@ -305,7 +305,7 @@
<textarea id="perf-text" rows="3">Hello, how are you today? Please read this sample clearly for a fair voice benchmark.</textarea> <textarea id="perf-text" rows="3">Hello, how are you today? Please read this sample clearly for a fair voice benchmark.</textarea>
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button class="btn-primary" id="perf-run-btn">&#9654; Run benchmark</button> <button class="btn-primary" id="perf-run-btn"><span class="mdi mdi-play"></span> Run benchmark</button>
<button class="btn-secondary" id="perf-clear-btn">Clear results</button> <button class="btn-secondary" id="perf-clear-btn">Clear results</button>
</div> </div>
<div id="perf-progress" class="note" style="display:none"></div> <div id="perf-progress" class="note" style="display:none"></div>

View File

@ -1,5 +1,5 @@
<div class="section-head"> <div class="section-head">
<span class="section-icon">&#128266;</span> <span class="section-icon"><span class="mdi mdi-account-voice"></span></span>
<div class="section-title"> <div class="section-title">
<h2>My Voices</h2> <h2>My Voices</h2>
<p>Pick a voice on the left, edit on the right.</p> <p>Pick a voice on the left, edit on the right.</p>
@ -20,7 +20,7 @@
<!-- TTS synth mode + preview sentence --> <!-- TTS synth mode + preview sentence -->
<div class="vl-synth-panel"> <div class="vl-synth-panel">
<div class="vl-synth-mode-row"> <div class="vl-synth-mode-row">
<span class="vl-synth-icon">&#9654;&#9654;</span> <span class="vl-synth-icon"><span class="mdi mdi-play"></span><span class="mdi mdi-play"></span></span>
<span class="vl-synth-label">Synth uses</span> <span class="vl-synth-label">Synth uses</span>
<div class="vl-synth-seg" id="vl-synth-mode-seg"> <div class="vl-synth-seg" id="vl-synth-mode-seg">
<button class="vl-synth-seg-btn active" data-mode="preview" <button class="vl-synth-seg-btn active" data-mode="preview"
@ -61,7 +61,7 @@
<div id="voice-list"></div> <div id="voice-list"></div>
<div class="vl-toolbar"> <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="refresh-voices-btn" title="Refresh voice list"><span class="mdi mdi-refresh"></span> 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="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="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="benchmark-voices-btn" title="Benchmark TTS speed">Benchmark</button>
@ -78,7 +78,7 @@
<!-- RIGHT: inspector / editor pane --> <!-- RIGHT: inspector / editor pane -->
<div class="voices-inspector-pane" id="voices-inspector"> <div class="voices-inspector-pane" id="voices-inspector">
<div class="inspector-placeholder"> <div class="inspector-placeholder">
<span>&#128266;</span> <span><span class="mdi mdi-account-voice"></span></span>
<p>Pick a voice on the left<br>to edit it here</p> <p>Pick a voice on the left<br>to edit it here</p>
</div> </div>
</div> </div>
@ -162,14 +162,14 @@
<div class="mic-buttons"> <div class="mic-buttons">
<button class="btn-secondary" id="lib-add-monitor-btn">Check level</button> <button class="btn-secondary" id="lib-add-monitor-btn">Check level</button>
<button class="btn-secondary" id="lib-add-monitor-stop" disabled>Stop monitor</button> <button class="btn-secondary" id="lib-add-monitor-stop" disabled>Stop monitor</button>
<button class="btn-red" id="lib-add-rec-start">&#9679; Record</button> <button class="btn-red" id="lib-add-rec-start"><span class="mdi mdi-record-circle-outline"></span> Record</button>
<button class="btn-secondary" id="lib-add-rec-stop" disabled>&#9632; Stop</button> <button class="btn-secondary" id="lib-add-rec-stop" disabled><span class="mdi mdi-stop-circle-outline"></span> Stop</button>
<span class="mic-timer" id="lib-add-rec-time">0:00</span> <span class="mic-timer" id="lib-add-rec-time">0:00</span>
</div> </div>
<div class="mic-monitor-box"> <div class="mic-monitor-box">
<div class="mic-monitor-head"> <div class="mic-monitor-head">
<span>Input level</span> <span>Input level</span>
<span class="meter-readout" id="lib-add-db-readout">-&#8734; dB</span> <span class="meter-readout" id="lib-add-db-readout">-<span class="mdi mdi-infinity"></span> dB</span>
</div> </div>
<div class="mic-meter" id="lib-add-mic-meter" aria-hidden="true"></div> <div class="mic-meter" id="lib-add-mic-meter" aria-hidden="true"></div>
<div class="mic-gain-row"> <div class="mic-gain-row">

View File

@ -1,3 +1,17 @@
/* ── MDI icon integration ────────────────────────────────────────────────── */
.mdi {
vertical-align: -0.125em;
line-height: 1;
display: inline-block;
}
button .mdi, a .mdi { font-size: 15px; }
.nav-icon .mdi { font-size: 15px; line-height: 1; }
.section-icon .mdi { font-size: 22px; line-height: 1; }
.llm-card-icon .mdi { font-size: inherit; }
.llm-local-icon.mdi { font-size: 20px; }
.backend-metric-tag .mdi, .llm-metric-chip .mdi, .dc-metric-chip .mdi { font-size: 11px; vertical-align: -0.05em; }
.link-icon { font-size: 11px; opacity: .7; margin-left: 1px; }
/* ── Theme tokens (warm cream light theme) ──────────────────────────────── */ /* ── Theme tokens (warm cream light theme) ──────────────────────────────── */
:root { :root {
--bg: #F5F3EE; --bg: #F5F3EE;