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);
wrap.classList.remove('vr-selected');
_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;
}
@ -73,7 +73,7 @@ function selectVoice(wrap) {
const flagIconHtml = inspFlagIcon || '';
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('');
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 ──────────────────────────────────────────────────────
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));
}
@ -1789,10 +1789,10 @@ function backendHelpHtml(b, compact = false) {
b.true_streaming ? ['good', 'true streaming'] : ['', 'buffered/normal'],
].map(([cls, text]) => `<span class="backend-tag ${cls}">${escHtml(text)}</span>`).join('');
const metricParts = [];
if (b.speed) metricParts.push(`<span class="backend-metric-tag">&#9889; ${escHtml(b.speed)}</span>`);
if (b.latency) metricParts.push(`<span class="backend-metric-tag">&#9200; ${escHtml(b.latency)}</span>`);
if (b.quality) metricParts.push(`<span class="backend-metric-tag">&#11088; ${escHtml(b.quality)}</span>`);
if (b.ram) metricParts.push(`<span class="backend-metric-tag">&#128190; ${escHtml(b.ram)}</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"><span class="mdi mdi-clock-outline"></span> ${escHtml(b.latency)}</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"><span class="mdi mdi-memory"></span> ${escHtml(b.ram)}</span>`);
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 || '')}`;
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.';
const m = b.metrics || {};
const metricParts = [];
if (m.speed) metricParts.push(`<span class="backend-metric-tag">&#9889; ${escHtml(m.speed)}</span>`);
if (m.latency) metricParts.push(`<span class="backend-metric-tag">&#9200; ${escHtml(m.latency)}</span>`);
if (m.quality) metricParts.push(`<span class="backend-metric-tag">&#11088; ${escHtml(m.quality)}</span>`);
if (m.ram) metricParts.push(`<span class="backend-metric-tag">&#128190; ${escHtml(m.ram)}</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"><span class="mdi mdi-clock-outline"></span> ${escHtml(m.latency)}</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"><span class="mdi mdi-memory"></span> ${escHtml(m.ram)}</span>`);
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 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
? ` Try <strong>${escHtml(styleAwareBacks[0].label)}</strong> instead.`
: ' 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 = `
<div class="voices-empty-cases">
<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>
<p>Set the path where your <code>.wav</code> voice files live <em>inside the container</em>.</p>
<div class="voices-folder-row">
<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>
</div>
<div class="vef-dir-browser" id="vef-dir-browser" hidden>
@ -4217,12 +4217,12 @@ function renderVoiceList() {
</div>
<div class="voices-empty-divider">or</div>
<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>
<p>Create your first voice from a recording or download ready-made voices.</p>
<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-secondary voices-empty-action-btn" id="voices-goto-studio">&#127760; Get Voices Online</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"><span class="mdi mdi-earth"></span> Get Voices Online</button>
</div>
</div>
</div>
@ -4262,7 +4262,7 @@ function renderVoiceList() {
vefDirList.innerHTML = '<span class="vef-empty">No subdirectories here.</span>';
} else {
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('');
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
const flagOpts = langOpts.length > 1 ? langOpts : ALL_FLAGS;
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) : '?';
wrap.dataset.flagEmoji = flagEmoji;
wrap.dataset.flagCc = currentFlag || '';
@ -4432,7 +4432,7 @@ function makeVoiceRow(v) {
const gender = v.gender || '';
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('');
const picSrc = v.has_picture ? `/api/voice/picture/${encodeURIComponent(v.id)}` : null;
@ -4468,14 +4468,14 @@ function makeVoiceRow(v) {
</div>
</div>
<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-synth"><button class="play-btn-synth" title="Generate and play TTS preview">&#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"><span class="mdi mdi-play"></span></button></div>
</div>
</div>
<div class="vr-main-row">
<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/*">
</div>
@ -4497,8 +4497,8 @@ function makeVoiceRow(v) {
<span class="vr-name-text" title="${escHtml(v.id)}">${escHtml(v.id)}</span>
<span class="rename-confirm">
<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-cancel" title="Cancel">&#10005;</button>
<button class="icon-btn rename-ok" title="Save"><span class="mdi mdi-check"></span></button>
<button class="icon-btn rename-cancel" title="Cancel"><span class="mdi mdi-close"></span></button>
</span>
</div>
</div>
@ -4519,15 +4519,15 @@ function makeVoiceRow(v) {
<div class="vr-rating">${starsHtml}</div>
<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 class="vr-play-group">
<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 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>
@ -4538,7 +4538,7 @@ function makeVoiceRow(v) {
<label>Reference</label>
<div class="vr-inline">
<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>
@ -5338,7 +5338,7 @@ function makeVoiceRow(v) {
// Play original recording or synthesized sample
const originalPlayBtn = wrap.querySelector('.vr-play-original 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) {
btn.classList.toggle('is-generating', state === 'generating');
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>${max} ms</strong> worst</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>'; }
}
@ -6370,7 +6370,7 @@ loadSettings().then(() => {
<button class="el-play" title="Play preview" data-preview="${prev}"> Play</button>
</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>`;
}
@ -6448,24 +6448,24 @@ function renderLocalContainers(containers) {
const ROLE_LABEL = { tts: 'TTS', stt: 'STT', 'stt+tts': 'STT · TTS', llm: 'LLM' };
const DC_ICONS = {
'faster-qwen3-tts-voiceclone': '🔊',
'faster-qwen3-tts-voicedesign': '',
'faster-qwen3-tts-voiceclone': '<span class="mdi mdi-speaker-outline"></span>',
'faster-qwen3-tts-voicedesign': '<span class="mdi mdi-auto-fix"></span>',
'faster-qwen3-tts-customvoice': '🎭',
'faster-qwen3-tts-streaming': '',
'faster-qwen3-tts-streaming': '<span class="mdi mdi-lightning-bolt"></span>',
'parakeet-asr': '🦜',
'magpie-tts': '🐦',
'parakeet-rnnt-nim': '🦜',
};
const DC_METRICS = {
'faster-qwen3-tts-voiceclone': [['','~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-customvoice': [['','~0.3× GPU'],['⏰','13 s'],['⭐','Premium'], ['💾','68 GB VRAM']],
'faster-qwen3-tts-streaming': [['','~0.1× GPU'],['⏰','0.51 s'],['⭐','Premium'], ['💾','68 GB VRAM']],
'magpie-tts': [['','~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-rnnt-nim': [['','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-1B'], ['💾','2 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': [['<span class="mdi mdi-lightning-bolt"></span>','~0.4× 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': [['<span class="mdi mdi-lightning-bolt"></span>','~0.1× GPU'],['⏰','0.51 s'],['⭐','Premium'], ['💾','68 GB VRAM']],
'magpie-tts': [['<span class="mdi mdi-lightning-bolt"></span>','~0.05× GPU'],['⏰','0.30.8 s'],['⭐','High'], ['💾','46 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': [['<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 => {
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-secondary dc-btn" data-dc-action="restart" data-dc-name="${n}">Restart</button>`)
: (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' : ''}">
@ -6864,8 +6864,8 @@ function renderHistory() {
</div>
<div class="history-item-text">${preview}</div>
<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-reuse-btn">&#8629; Reuse</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"><span class="mdi mdi-restore"></span> Reuse</button>
<button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?'':'disabled'}>&#43; Playlist</button>
</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>
</div>
<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-dn" ${i===_playlist.length-1?'disabled':''}>&#8595;</button>
<button class="btn-secondary btn-xs pl-rm">&#10005;</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':''}><span class="mdi mdi-download"></span></button>
<button class="btn-secondary btn-xs pl-rm"><span class="mdi mdi-close"></span></button>
</div>
</div>`).join('');

View File

@ -7,6 +7,7 @@
<!-- 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/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="/static/style.css">
</head>
@ -32,17 +33,17 @@
<!-- Sidebar -->
<aside id="sidebar">
<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>
</div>
<nav class="sidebar-nav">
<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')">
<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-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 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>
@ -53,36 +54,36 @@
<div class="nav-tree-item" data-voice-cat="tools" onclick="navVoicesCat('tools')">Library tools</div>
</div>
<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 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 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 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 class="nav-group-label" style="margin-top:6px">Setup</div>
<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 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 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 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>
</nav>
<div class="sidebar-footer">
<button id="theme-btn" title="Switch theme" style="display:none">&#9728;</button>
<button id="settings-btn" onclick="navTo('s-settings')" title="Open settings">&#9881; Settings</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"><span class="mdi mdi-cog-outline"></span> Settings</button>
</div>
</aside>

View File

@ -1,5 +1,5 @@
<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">
<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>
@ -31,8 +31,8 @@
<h2>Microphone</h2>
<p class="card-subtitle">Record a fresh sample with live input level monitoring.</p>
<div class="btn-row">
<button class="btn-red" id="rec-start-btn">&#9679; Record</button>
<button class="btn-secondary" id="rec-stop-btn" disabled>&#9632; Stop</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><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>
</div>
@ -48,8 +48,8 @@
<div class="btn-row">
<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="play-btn">&#9654; Play</button>
<button class="btn-secondary" id="play-selection-btn">&#9654; Play selection</button>
<button class="btn-secondary" id="play-btn"><span class="mdi mdi-play"></span> Play</button>
<button class="btn-secondary" id="play-selection-btn"><span class="mdi mdi-play"></span> Play selection</button>
</div>
</div>
</div><!-- /tab-source -->
@ -95,7 +95,7 @@
<h2>Transcript (reference text)</h2>
<p class="card-subtitle">The spoken text that matches this audio — type it or auto-transcribe.</p>
<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>
</div>
<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>
<p class="card-subtitle">Write the cloned voice to your active voices folder.</p>
<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 id="save-result" class="note" style="display:none"></div>
</div>

View File

@ -1,5 +1,5 @@
<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">
<h2>Connect Your Apps</h2>
<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">
<span class="section-icon">&#10024;</span>
<span class="section-icon"><span class="mdi mdi-auto-fix"></span></span>
<div class="section-title">
<h2>Design a Voice</h2>
<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>
<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">
<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>
</div>
</div>
@ -115,9 +115,9 @@
<textarea id="d-transcript" placeholder="The spoken text from the sample (auto-filled)&hellip;"></textarea>
</div>
<div class="btn-row">
<button class="btn-secondary" id="design-retry-btn">&#8635; Regenerate</button>
<button class="btn-secondary" id="design-download-btn">&#8595; Download WAV</button>
<button class="btn-green" id="design-save-btn">&#128190; Export to Voice Clone Library</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"><span class="mdi mdi-download"></span> Download WAV</button>
<button class="btn-green" id="design-save-btn"><span class="mdi mdi-memory"></span> Export to Voice Clone Library</button>
</div>
<div id="design-save-result" style="display:none">
<hr>

View File

@ -1,5 +1,5 @@
<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">
<h2>AI Backends</h2>
<p>Connect local or cloud services for speech recognition, synthesis, and text generation.</p>
@ -8,8 +8,8 @@
<!-- Category tabs: Local / Online -->
<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" data-llm-cat="online">&#127760; Online APIs</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"><span class="mdi mdi-earth"></span> Online APIs</button>
</div>
<!-- ── LOCAL section ──────────────────────────────────────── -->
@ -28,7 +28,7 @@
<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>
</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 id="dc-grid" class="dc-grid">
<div class="dc-loading">Checking container status&hellip;</div>
@ -40,7 +40,7 @@
<div class="llm-local-card">
<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-compat">OpenAI-compat</span>
</div>
@ -58,12 +58,12 @@
<pre>curl https://ollama.ai/install.sh | sh
ollama pull llama3.3</pre>
</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 class="llm-local-card llm-local-card-running">
<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-compat llm-compat-detected">Detected in stack</span>
</div>
@ -81,7 +81,7 @@ ollama pull llama3.3</pre>
<pre>--model Qwen/Qwen3-8B \
--served-model-name qwen3-8b</pre>
</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 class="llm-local-card">
@ -101,7 +101,7 @@ ollama pull llama3.3</pre>
<pre>Developer tab → Start server → Port 1234
Check "Enable CORS" for browser access</pre>
</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 class="llm-local-card">
@ -125,7 +125,7 @@ Check "Enable CORS" for browser access</pre>
-hf QuantFactory/Meta-Llama-3-8B-GGUF \
-hff Meta-Llama-3-8B.Q4_K_M.gguf</pre>
</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>
@ -136,15 +136,15 @@ Check "Enable CORS" for browser access</pre>
<div class="llm-local-card">
<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-compat">OpenAI-compat</span>
</div>
<div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~70&times; RT GPU</span>
<span class="llm-metric-chip">&#9200; 0.52 s</span>
<span class="llm-metric-chip">&#11088; large-v3</span>
<span class="llm-metric-chip">&#128190; 1.5 GB VRAM</span>
<span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~70&times; RT GPU</span>
<span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> 0.52 s</span>
<span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> large-v3</span>
<span class="llm-metric-chip"><span class="mdi mdi-memory"></span> 1.5 GB VRAM</span>
</div>
<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">
@ -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>
</div>
<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 class="llm-local-snippet">
<div class="llm-snippet-bar">
@ -171,7 +171,7 @@ Check "Enable CORS" for browser access</pre>
reservations:
devices: [{driver: nvidia, count: 1, capabilities: [gpu]}]</pre>
</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 class="llm-local-card">
@ -181,10 +181,10 @@ Check "Enable CORS" for browser access</pre>
<span class="llm-local-compat">HTTP server</span>
</div>
<div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~815&times; RT CPU</span>
<span class="llm-metric-chip">&#9200; 15 s</span>
<span class="llm-metric-chip">&#11088; large-v3 Q5</span>
<span class="llm-metric-chip">&#128190; ~1 GB RAM</span>
<span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~815&times; RT CPU</span>
<span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> 15 s</span>
<span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> large-v3 Q5</span>
<span class="llm-metric-chip"><span class="mdi mdi-memory"></span> ~1 GB RAM</span>
</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>
<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>
</div>
<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 class="llm-local-snippet">
<div class="llm-snippet-bar">
@ -205,7 +205,7 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
./build/bin/whisper-server \
-m models/ggml-large-v3.bin --port 8080</pre>
</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>
@ -216,15 +216,15 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
<div class="llm-local-card">
<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-compat">Fast &middot; offline</span>
</div>
<div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~1× CPU realtime</span>
<span class="llm-metric-chip">&#9200; &lt;50 ms</span>
<span class="llm-metric-chip">&#11088; Good (VITS)</span>
<span class="llm-metric-chip">&#128190; ~50 MB RAM</span>
<span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~1× CPU realtime</span>
<span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> &lt;50 ms</span>
<span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> Good (VITS)</span>
<span class="llm-metric-chip"><span class="mdi mdi-memory"></span> ~50 MB RAM</span>
</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>
<div class="llm-local-url">
@ -241,7 +241,7 @@ cd whisper.cpp && cmake -B build && cmake --build build -j
rhasspy/wyoming-piper \
--voice en_US-lessac-medium</pre>
</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 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>
</div>
<div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~0.1&times; CPU RTF</span>
<span class="llm-metric-chip">&#9200; ~200 ms</span>
<span class="llm-metric-chip">&#11088; High (82M)</span>
<span class="llm-metric-chip">&#128190; 300 MB CPU</span>
<span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~0.1&times; CPU RTF</span>
<span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> ~200 ms</span>
<span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> High (82M)</span>
<span class="llm-metric-chip"><span class="mdi mdi-memory"></span> 300 MB CPU</span>
</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>
<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>
</div>
<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 class="llm-local-snippet">
<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 \
ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.2</pre>
</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 class="llm-local-card">
@ -286,10 +286,10 @@ docker run -p 8880:8880 --gpus all \
<span class="llm-local-compat">Voice cloning</span>
</div>
<div class="llm-local-metrics">
<span class="llm-metric-chip">&#9889; ~0.5× GPU RTF</span>
<span class="llm-metric-chip">&#9200; 13 s</span>
<span class="llm-metric-chip">&#11088; High (WAV clone)</span>
<span class="llm-metric-chip">&#128190; 34 GB VRAM</span>
<span class="llm-metric-chip"><span class="mdi mdi-lightning-bolt"></span> ~0.5× GPU RTF</span>
<span class="llm-metric-chip"><span class="mdi mdi-clock-outline"></span> 13 s</span>
<span class="llm-metric-chip"><span class="mdi mdi-star-circle-outline"></span> High (WAV clone)</span>
<span class="llm-metric-chip"><span class="mdi mdi-memory"></span> 34 GB VRAM</span>
</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>
<div class="llm-local-url">
@ -306,13 +306,13 @@ docker run -p 8880:8880 --gpus all \
-v /voices:/voices \
daswer123/xtts-api-server:latest</pre>
</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 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">
<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>
@ -332,12 +332,12 @@ docker run -p 8880:8880 --gpus all \
</div>
<!-- ── 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-card llm-card-highlight">
<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 class="llm-card-name">Groq Whisper</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 class="llm-card-stats">
<span>2 000 req / day</span>
<span>&#9889; Fastest cloud STT</span>
<span>&#128190; Cloud &middot; 0 VRAM</span>
<span><span class="mdi mdi-lightning-bolt"></span> Fastest cloud STT</span>
<span><span class="mdi mdi-memory"></span> Cloud &middot; 0 VRAM</span>
</div>
<div class="llm-field-row">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<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-head">
<span class="llm-card-icon">&#129303;</span>
<span class="llm-card-icon"><span class="mdi mdi-emoticon-happy-outline"></span></span>
<div>
<div class="llm-card-name">HuggingFace Inference</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<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-head">
<span class="llm-card-icon">&#128203;</span>
<span class="llm-card-icon"><span class="mdi mdi-content-copy"></span></span>
<div>
<div class="llm-card-name">AssemblyAI</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>
@ -419,12 +419,12 @@ docker run -p 8880:8880 --gpus all \
</div>
<!-- ── 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-card llm-card-highlight">
<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 class="llm-card-name">ElevenLabs</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<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-head">
<span class="llm-card-icon">&#127981;</span>
<span class="llm-card-icon"><span class="mdi mdi-fish"></span></span>
<div>
<div class="llm-card-name">Fish Audio</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<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-head">
<span class="llm-card-icon">&#127925;</span>
<span class="llm-card-icon"><span class="mdi mdi-music-note"></span></span>
<div>
<div class="llm-card-name">Kokoro TTS</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">
Use the HF Spaces web demo for quick tests, or run Kokoro locally via Docker for production use.
<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>
<!-- ── 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-card llm-card-highlight">
<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 class="llm-card-name">Groq</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<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-head">
<span class="llm-card-icon">&#127760;</span>
<span class="llm-card-icon"><span class="mdi mdi-earth"></span></span>
<div>
<div class="llm-card-name">OpenRouter</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<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-head">
<span class="llm-card-icon">&#128142;</span>
<span class="llm-card-icon"><span class="mdi mdi-diamond-outline"></span></span>
<div>
<div class="llm-card-name">Google Gemini</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<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-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 class="llm-card-name">Mistral AI</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">
<label class="llm-label">API key</label>
<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 class="llm-endpoint">
<span class="llm-endpoint-label">Endpoint</span>

View File

@ -1,5 +1,5 @@
<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">
<h2>App Routing</h2>
<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">
<span class="section-icon">&#9881;</span>
<span class="section-icon"><span class="mdi mdi-cog-outline"></span></span>
<div class="section-title">
<h2>Settings</h2>
<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>
<div class="s-key-row">
<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>
<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>
@ -247,21 +247,21 @@
<label>TTS API key <span style="font-weight:400">(optional)</span></label>
<div class="s-key-row">
<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 class="s-field">
<label>Voice Design API key <span style="font-weight:400">(optional)</span></label>
<div class="s-key-row">
<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 class="s-field">
<label>Whisper API key <span style="font-weight:400">(optional)</span></label>
<div class="s-key-row">
<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>
@ -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>
</div>
<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">
&#8593; Import voices
<span class="mdi mdi-upload"></span> Import voices
<input type="file" id="s-import-voices-file" accept=".zip" style="display:none">
</label>
<span class="note" id="s-import-status"></span>

View File

@ -1,5 +1,5 @@
<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">
<h2>Try It Out</h2>
<p>Generate speech from text using any backend and voice. Also transcribe audio and re-speak it.</p>
@ -74,9 +74,9 @@
</label>
</div>
<div class="btn-row">
<button class="btn-primary" id="preview-btn">&#9654; 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-btn" disabled>&#8595; Save WAV</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><span class="mdi mdi-download"></span> Save MP3</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>
</div>
<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>
<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>
</div>
</div>
@ -164,7 +164,7 @@
<!-- STT -> TTS -->
<div class="tab-content" id="tab-stt-tts">
<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>
</div>
<div class="card">
@ -183,7 +183,7 @@
<label>Speech audio</label>
<div class="btn-row" style="gap:8px;align-items:center">
<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>
<span class="mic-timer" id="stt-tts-rec-time">0:00</span>
</div>
@ -227,7 +227,7 @@
<label class="refine-toggle"><input type="checkbox" id="refine-punctuation" checked> Fix punctuation</label>
</div>
<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>
<span class="note" id="refine-status"></span>
</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>
</div>
<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>
</div>
<div id="perf-progress" class="note" style="display:none"></div>

View File

@ -1,5 +1,5 @@
<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">
<h2>My Voices</h2>
<p>Pick a voice on the left, edit on the right.</p>
@ -20,7 +20,7 @@
<!-- TTS synth mode + preview sentence -->
<div class="vl-synth-panel">
<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>
<div class="vl-synth-seg" id="vl-synth-mode-seg">
<button class="vl-synth-seg-btn active" data-mode="preview"
@ -61,7 +61,7 @@
<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="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="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>
@ -78,7 +78,7 @@
<!-- RIGHT: inspector / editor pane -->
<div class="voices-inspector-pane" id="voices-inspector">
<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>
</div>
</div>
@ -162,14 +162,14 @@
<div class="mic-buttons">
<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-red" id="lib-add-rec-start">&#9679; Record</button>
<button class="btn-secondary" id="lib-add-rec-stop" disabled>&#9632; Stop</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><span class="mdi mdi-stop-circle-outline"></span> Stop</button>
<span class="mic-timer" id="lib-add-rec-time">0:00</span>
</div>
<div class="mic-monitor-box">
<div class="mic-monitor-head">
<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 class="mic-meter" id="lib-add-mic-meter" aria-hidden="true"></div>
<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) ──────────────────────────────── */
:root {
--bg: #F5F3EE;