Redesign settings page (Voicebox-style) and fix remaining old icons
- Settings: flat section layout with consistent 32px padding, groups with inline title+subtitle, clean accordion details with chevron, focus rings - Replace all remaining emoji (⏰ ⭐ 💾 🌙 ☀️ ⚠ ✓ ↓ ★) with MDI icons - s-studio.html: globe emoji → mdi-earth, star filter → mdi-star-outline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
61a48ee84a
commit
f871486635
@ -517,7 +517,7 @@ async function clientAutoTrimBounds(fid) {
|
|||||||
|
|
||||||
function applyTheme(t) {
|
function applyTheme(t) {
|
||||||
document.documentElement.dataset.theme = t;
|
document.documentElement.dataset.theme = t;
|
||||||
$('theme-btn').textContent = t === 'dark' ? '☀️' : '🌙';
|
$('theme-btn').innerHTML = t === 'dark' ? '<span class="mdi mdi-weather-sunny"></span>' : '<span class="mdi mdi-weather-night"></span>';
|
||||||
$('theme-btn').title = t === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
$('theme-btn').title = t === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
|
||||||
localStorage.setItem('vcf-theme', t);
|
localStorage.setItem('vcf-theme', t);
|
||||||
}
|
}
|
||||||
@ -6244,7 +6244,7 @@ loadSettings().then(() => {
|
|||||||
const key = (s.elevenlabs_api_key || '').trim();
|
const key = (s.elevenlabs_api_key || '').trim();
|
||||||
const inp = $('el-api-key'), st = $('el-key-status');
|
const inp = $('el-api-key'), st = $('el-key-status');
|
||||||
if (inp) inp.placeholder = key ? '••••••••••••••• (key saved)' : 'ElevenLabs API key (free — unlocks full library)…';
|
if (inp) inp.placeholder = key ? '••••••••••••••• (key saved)' : 'ElevenLabs API key (free — unlocks full library)…';
|
||||||
if (st) { st.textContent = key ? '✓ Key active' : 'No key — max 3 results'; st.className = 'el-key-status ' + (key ? 'el-key-ok' : 'el-key-none'); }
|
if (st) { st.innerHTML = key ? '<span class="mdi mdi-check-circle-outline"></span> Key active' : 'No key — max 3 results'; st.className = 'el-key-status ' + (key ? 'el-key-ok' : 'el-key-none'); }
|
||||||
elFetch();
|
elFetch();
|
||||||
}).catch(() => elFetch());
|
}).catch(() => elFetch());
|
||||||
}
|
}
|
||||||
@ -6256,7 +6256,7 @@ loadSettings().then(() => {
|
|||||||
fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ elevenlabs_api_key: val }) })
|
fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ elevenlabs_api_key: val }) })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const st = $('el-key-status');
|
const st = $('el-key-status');
|
||||||
if (st) { st.textContent = '✓ Key saved'; st.className = 'el-key-status el-key-ok'; }
|
if (st) { st.innerHTML = '<span class="mdi mdi-check-circle-outline"></span> Key saved'; st.className = 'el-key-status el-key-ok'; }
|
||||||
if (inp) { inp.value = ''; inp.placeholder = '••••••••••••••• (key saved)'; }
|
if (inp) { inp.value = ''; inp.placeholder = '••••••••••••••• (key saved)'; }
|
||||||
_elPage = 0; elFetch();
|
_elPage = 0; elFetch();
|
||||||
});
|
});
|
||||||
@ -6306,7 +6306,7 @@ loadSettings().then(() => {
|
|||||||
const res = await fetch('/api/elevenlabs/voices?' + elParams());
|
const res = await fetch('/api/elevenlabs/voices?' + elParams());
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.detail) {
|
if (data.detail) {
|
||||||
if (grid) grid.innerHTML = `<div class="el-error">⚠ ${_elEsc(data.detail?.message || JSON.stringify(data.detail))}</div>`;
|
if (grid) grid.innerHTML = `<div class="el-error"><span class="mdi mdi-alert-outline"></span> ${_elEsc(data.detail?.message || JSON.stringify(data.detail))}</div>`;
|
||||||
if (sb) sb.hidden = true;
|
if (sb) sb.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -6334,7 +6334,7 @@ loadSettings().then(() => {
|
|||||||
if (pi) pi.textContent = `Page ${_elPage + 1}${_elTotal > 24 ? ' of ~' + Math.ceil(_elTotal / 24) : ''}`;
|
if (pi) pi.textContent = `Page ${_elPage + 1}${_elTotal > 24 ? ' of ~' + Math.ceil(_elTotal / 24) : ''}`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (grid) grid.innerHTML = `<div class="el-error">⚠ ${_elEsc(e.message)}</div>`;
|
if (grid) grid.innerHTML = `<div class="el-error"><span class="mdi mdi-alert-outline"></span> ${_elEsc(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6357,8 +6357,8 @@ loadSettings().then(() => {
|
|||||||
uc ? `<span class="el-tag el-uc">${_elEsc(uc)}</span>` : '',
|
uc ? `<span class="el-tag el-uc">${_elEsc(uc)}</span>` : '',
|
||||||
(v.accent && v.accent !== 'standard') ? `<span class="el-tag">${_elEsc(v.accent)}</span>` : '',
|
(v.accent && v.accent !== 'standard') ? `<span class="el-tag">${_elEsc(v.accent)}</span>` : '',
|
||||||
v.free_users_allowed ? '<span class="el-tag el-free">Free</span>' : '',
|
v.free_users_allowed ? '<span class="el-tag el-free">Free</span>' : '',
|
||||||
v.featured ? '<span class="el-tag el-feat">★</span>' : '',
|
v.featured ? '<span class="el-tag el-feat"><span class="mdi mdi-star"></span></span>' : '',
|
||||||
cl ? `<span class="el-tag el-clones">↓ ${cl}</span>` : '',
|
cl ? `<span class="el-tag el-clones"><span class="mdi mdi-download-outline"></span> ${cl}</span>` : '',
|
||||||
].join('');
|
].join('');
|
||||||
return `<div class="el-card-voice">
|
return `<div class="el-card-voice">
|
||||||
<div class="el-vc-av" style="--elh:${hue}">${letter}</div>
|
<div class="el-vc-av" style="--elh:${hue}">${letter}</div>
|
||||||
@ -6456,16 +6456,20 @@ function renderLocalContainers(containers) {
|
|||||||
'magpie-tts': '🐦',
|
'magpie-tts': '🐦',
|
||||||
'parakeet-rnnt-nim': '🦜',
|
'parakeet-rnnt-nim': '🦜',
|
||||||
};
|
};
|
||||||
|
const _t = (v) => `<span class="mdi mdi-lightning-bolt"></span>`;
|
||||||
|
const _l = (v) => `<span class="mdi mdi-clock-outline"></span>`;
|
||||||
|
const _q = (v) => `<span class="mdi mdi-star-outline"></span>`;
|
||||||
|
const _m = (v) => `<span class="mdi mdi-memory"></span>`;
|
||||||
const DC_METRICS = {
|
const DC_METRICS = {
|
||||||
'faster-qwen3-tts-voiceclone': [['<span class="mdi mdi-lightning-bolt"></span>','~0.3× GPU'],['⏰','1–3 s'],['⭐','Premium clone'],['💾','6–8 GB VRAM']],
|
'faster-qwen3-tts-voiceclone': [[_t(),'~0.3× GPU'],[_l(),'1–3 s'],[_q(),'Premium clone'],[_m(),'6–8 GB VRAM']],
|
||||||
'faster-qwen3-tts-voicedesign': [['<span class="mdi mdi-lightning-bolt"></span>','~0.4× GPU'],['⏰','1–3 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']],
|
'faster-qwen3-tts-voicedesign': [[_t(),'~0.4× GPU'],[_l(),'1–3 s'],[_q(),'Premium'], [_m(),'6–8 GB VRAM']],
|
||||||
'faster-qwen3-tts-customvoice': [['<span class="mdi mdi-lightning-bolt"></span>','~0.3× GPU'],['⏰','1–3 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']],
|
'faster-qwen3-tts-customvoice': [[_t(),'~0.3× GPU'],[_l(),'1–3 s'],[_q(),'Premium'], [_m(),'6–8 GB VRAM']],
|
||||||
'faster-qwen3-tts-streaming': [['<span class="mdi mdi-lightning-bolt"></span>','~0.1× GPU'],['⏰','0.5–1 s'],['⭐','Premium'], ['💾','6–8 GB VRAM']],
|
'faster-qwen3-tts-streaming': [[_t(),'~0.1× GPU'],[_l(),'0.5–1 s'],[_q(),'Premium'], [_m(),'6–8 GB VRAM']],
|
||||||
'magpie-tts': [['<span class="mdi mdi-lightning-bolt"></span>','~0.05× GPU'],['⏰','0.3–0.8 s'],['⭐','High'], ['💾','4–6 GB VRAM']],
|
'magpie-tts': [[_t(),'~0.05× GPU'],[_l(),'0.3–0.8 s'],[_q(),'High'], [_m(),'4–6 GB VRAM']],
|
||||||
'parakeet-asr': [['<span class="mdi mdi-lightning-bolt"></span>','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-TDT'],['💾','2 GB VRAM']],
|
'parakeet-asr': [[_t(),'~200× RT GPU'],[_l(),'<0.3 s'],[_q(),'Parakeet-TDT'],[_m(),'2 GB VRAM']],
|
||||||
'parakeet-rnnt-nim': [['<span class="mdi mdi-lightning-bolt"></span>','~200× RT GPU'],['⏰','<0.3 s'],['⭐','Parakeet-1B'], ['💾','2 GB VRAM']],
|
'parakeet-rnnt-nim': [[_t(),'~200× RT GPU'],[_l(),'<0.3 s'],[_q(),'Parakeet-1B'], [_m(),'2 GB VRAM']],
|
||||||
};
|
};
|
||||||
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>' };
|
const roleIcon = { tts: '<span class="mdi mdi-speaker-outline"></span>', stt: '<span class="mdi mdi-microphone-variant"></span>', 'stt+tts': '<span class="mdi mdi-swap-horizontal"></span>', 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';
|
||||||
@ -6605,7 +6609,7 @@ $('dc-refresh-btn')?.addEventListener('click', loadLocalContainers);
|
|||||||
const btn = card.querySelector('.llm-local-ping');
|
const btn = card.querySelector('.llm-local-ping');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
if (connected) {
|
if (connected) {
|
||||||
btn.textContent = '✓ Disconnect'; btn.dataset.action = 'disconnect';
|
btn.innerHTML = '<span class="mdi mdi-check"></span> Disconnect'; btn.dataset.action = 'disconnect';
|
||||||
btn.className = 'llm-local-ping ok';
|
btn.className = 'llm-local-ping ok';
|
||||||
} else {
|
} else {
|
||||||
btn.textContent = 'Connect'; btn.dataset.action = 'connect';
|
btn.textContent = 'Connect'; btn.dataset.action = 'connect';
|
||||||
@ -6866,7 +6870,7 @@ function renderHistory() {
|
|||||||
<div class="history-item-actions">
|
<div class="history-item-actions">
|
||||||
<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-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-reuse-btn"><span class="mdi mdi-restore"></span> Reuse</button>
|
||||||
<button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?'':'disabled'}>+ Playlist</button>
|
<button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?'':'disabled'}><span class="mdi mdi-plus"></span> Playlist</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
@ -8,144 +8,131 @@
|
|||||||
|
|
||||||
<div class="tab-content" id="tab-settings">
|
<div class="tab-content" id="tab-settings">
|
||||||
<div class="card" id="settings-box">
|
<div class="card" id="settings-box">
|
||||||
<div class="settings-title-row">
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="s-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<p class="card-subtitle">Configure the service URLs you actually use first. Advanced payloads, folders, and keys are tucked away below.</p>
|
<p class="s-subtitle">Configure the service URLs you use. Advanced payloads, folders, and keys are tucked away below.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-status-pill">Local stack</div>
|
<span class="s-stack-badge"><span class="mdi mdi-check-circle-outline"></span> Local stack</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setup-intro settings-checklist" id="settings-first-run-note">
|
<!-- Core connections -->
|
||||||
<strong>Quick setup</strong>
|
<div class="s-section">
|
||||||
<span>1. Check core TTS URLs.</span>
|
<div class="s-section-label">Core connections</div>
|
||||||
<span>2. Point STT at Whisper, Parakeet, or the NVIDIA router.</span>
|
<p class="s-section-desc">These are the endpoints you change most often. NVIDIA TTS and STT are grouped separately.</p>
|
||||||
<span>3. Save settings.</span>
|
|
||||||
|
<div class="s-group">
|
||||||
|
<div class="s-group-head">
|
||||||
|
<strong>TTS — Text to Speech</strong>
|
||||||
|
<span>Qwen3 engines: clone, design, custom, streaming · Kokoro FastAPI</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-grid compact">
|
||||||
|
<div class="s-field">
|
||||||
|
<label>Voice Clone / Base URL <span class="s-label-note">(WAV voices)</span></label>
|
||||||
|
<input type="text" id="s-tts-url" placeholder="http://host.docker.internal:8020">
|
||||||
|
<span class="s-hint">Uploaded/cloned WAV voices. Expected: <code>POST /v1/audio/speech</code>.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>Voice Design URL <span class="s-label-note">(instruction voices)</span></label>
|
||||||
|
<input type="text" id="s-voice-design-url" placeholder="http://host.docker.internal:8021">
|
||||||
|
<span class="s-hint">Prompt-designed voices and <code>vd_...</code> virtual voices.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>CustomVoice URL</label>
|
||||||
|
<input type="text" id="s-customvoice-url" placeholder="http://host.docker.internal:8022">
|
||||||
|
<span class="s-hint">Style over configured speakers such as Ryan, Vivian, Serena.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>Streaming URL</label>
|
||||||
|
<input type="text" id="s-tts-stream-url" placeholder="http://host.docker.internal:8023">
|
||||||
|
<span class="s-hint">Progressive low-latency WAV playback.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>Kokoro FastAPI URL <span class="s-label-note">(82M · CPU-friendly)</span></label>
|
||||||
|
<input type="text" id="s-kokoro-url" placeholder="http://host.docker.internal:8880/v1">
|
||||||
|
<span class="s-hint">OpenAI-compatible TTS. Built-in voices: af_bella, bf_emma, am_adam… ~300 MB RAM.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-group">
|
||||||
|
<div class="s-group-head">
|
||||||
|
<strong>NVIDIA speech stack</strong>
|
||||||
|
<span>router, Magpie TTS, Parakeet ASR, clone NIM</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-grid compact">
|
||||||
|
<div class="s-field">
|
||||||
|
<label>NVIDIA router URL <span class="s-label-note">(TTS + STT)</span></label>
|
||||||
|
<input type="text" id="s-nvidia-router-url" placeholder="http://host.docker.internal:8090">
|
||||||
|
<span class="s-hint">OpenAI-compatible base URL for the NVIDIA speech router.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>NVIDIA Magpie TTS URL</label>
|
||||||
|
<input type="text" id="s-nvidia-tts-url" placeholder="http://host.docker.internal:8091">
|
||||||
|
<span class="s-hint">Direct Magpie endpoint. Fixed speakers, not WAV cloning.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>NVIDIA Parakeet ASR URL</label>
|
||||||
|
<input type="text" id="s-nvidia-asr-url" placeholder="http://host.docker.internal:8092">
|
||||||
|
<span class="s-hint">Direct Parakeet endpoint for transcription.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>NVIDIA Zeroshot NIM URL</label>
|
||||||
|
<input type="text" id="s-nvidia-zeroshot-url" placeholder="http://host.docker.internal:8093">
|
||||||
|
<span class="s-hint">Magpie Zeroshot clone endpoint with <code>audio_prompt</code>.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>NVIDIA Flow NIM URL</label>
|
||||||
|
<input type="text" id="s-nvidia-flow-url" placeholder="http://host.docker.internal:8094">
|
||||||
|
<span class="s-hint">Magpie Flow clone endpoint with <code>audio_prompt</code> and transcript.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-group">
|
||||||
|
<div class="s-group-head">
|
||||||
|
<strong>STT — Speech to Text</strong>
|
||||||
|
<span>Whisper, faster-whisper, whisper.cpp, Groq, Parakeet, NVIDIA router</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-grid stt-settings-grid">
|
||||||
|
<div class="s-field stt-url-field">
|
||||||
|
<label>Active STT URL</label>
|
||||||
|
<input type="text" id="s-whisper-url" placeholder="http://host.docker.internal:8010">
|
||||||
|
<span class="s-hint">Default recognition endpoint. Expected: <code>POST /v1/audio/transcriptions</code>.</span>
|
||||||
|
<div class="btn-row settings-mini-actions">
|
||||||
|
<button class="btn-secondary" id="s-use-parakeet-asr" type="button">Use Parakeet</button>
|
||||||
|
<button class="btn-secondary" id="s-use-nvidia-router" type="button">Use router</button>
|
||||||
|
<button class="btn-secondary" id="s-use-faster-whisper" type="button">Use faster-whisper</button>
|
||||||
|
<button class="btn-secondary" id="s-use-whisper-cpp" type="button">Use whisper.cpp</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>faster-whisper-server URL <span class="s-label-note">(CTranslate2 GPU)</span></label>
|
||||||
|
<input type="text" id="s-faster-whisper-url" placeholder="http://host.docker.internal:8000">
|
||||||
|
<span class="s-hint">GPU-accelerated Whisper via CTranslate2. ~70× RT · 1.5 GB VRAM.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>whisper.cpp URL <span class="s-label-note">(CPU/CUDA)</span></label>
|
||||||
|
<input type="text" id="s-whisper-cpp-url" placeholder="http://host.docker.internal:8080">
|
||||||
|
<span class="s-hint">Lightweight C++ Whisper server. ~8–15× RT CPU · ~1 GB RAM.</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-field">
|
||||||
|
<label>Groq API key <span class="s-label-note">(Groq Whisper & LLM)</span></label>
|
||||||
|
<div class="s-key-row">
|
||||||
|
<input type="password" id="s-groq-api-key" placeholder="gsk_…" autocomplete="off">
|
||||||
|
<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 and LLM. Free: 2 000 req/day. Fastest cloud transcription.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="settings-section settings-section-primary">
|
<!-- Playback behavior -->
|
||||||
<div class="settings-section-head">
|
<div class="s-section">
|
||||||
<div>
|
<div class="s-section-label">Playback behavior</div>
|
||||||
<h3>Core connections</h3>
|
|
||||||
<p>These are the endpoints you change most often. Qwen3 TTS, NVIDIA TTS, and STT are grouped separately.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-cluster-grid">
|
|
||||||
<div class="settings-cluster">
|
|
||||||
<div class="settings-cluster-head">
|
|
||||||
<strong>TTS Text to Speech</strong>
|
|
||||||
<span>Qwen3 engines: clone, design, custom, streaming · Kokoro FastAPI</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-grid compact">
|
|
||||||
<div class="s-field">
|
|
||||||
<label>Voice Clone/Base URL <span style="font-weight:400">(WAV voices)</span></label>
|
|
||||||
<input type="text" id="s-tts-url" placeholder="http://host.docker.internal:8020">
|
|
||||||
<span class="s-hint">Uploaded/cloned WAV voices. Expected: <code>POST /v1/audio/speech</code>.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>Voice Design URL <span style="font-weight:400">(instruction voices)</span></label>
|
|
||||||
<input type="text" id="s-voice-design-url" placeholder="http://host.docker.internal:8021">
|
|
||||||
<span class="s-hint">Prompt-designed voices and <code>vd_...</code> virtual voices.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>CustomVoice URL</label>
|
|
||||||
<input type="text" id="s-customvoice-url" placeholder="http://host.docker.internal:8022">
|
|
||||||
<span class="s-hint">Style over configured speakers such as Ryan, Vivian, Serena.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>Streaming URL</label>
|
|
||||||
<input type="text" id="s-tts-stream-url" placeholder="http://host.docker.internal:8023">
|
|
||||||
<span class="s-hint">Progressive low-latency WAV playback.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>Kokoro FastAPI URL <span style="font-weight:400">(82M · CPU-friendly)</span></label>
|
|
||||||
<input type="text" id="s-kokoro-url" placeholder="http://host.docker.internal:8880/v1">
|
|
||||||
<span class="s-hint">OpenAI-compatible TTS. Built-in voices: af_bella, bf_emma, am_adam… ~300 MB RAM · ~0.1× CPU RTF.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-cluster">
|
|
||||||
<div class="settings-cluster-head">
|
|
||||||
<strong>NVIDIA speech stack</strong>
|
|
||||||
<span>router, Magpie TTS, Parakeet ASR, clone NIM</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-grid compact">
|
|
||||||
<div class="s-field">
|
|
||||||
<label>NVIDIA router URL <span style="font-weight:400">(TTS + STT)</span></label>
|
|
||||||
<input type="text" id="s-nvidia-router-url" placeholder="http://host.docker.internal:8090">
|
|
||||||
<span class="s-hint">OpenAI-compatible base URL for the NVIDIA speech router.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>NVIDIA Magpie TTS URL</label>
|
|
||||||
<input type="text" id="s-nvidia-tts-url" placeholder="http://host.docker.internal:8091">
|
|
||||||
<span class="s-hint">Direct Magpie endpoint. Fixed speakers, not WAV cloning.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>NVIDIA Parakeet ASR URL</label>
|
|
||||||
<input type="text" id="s-nvidia-asr-url" placeholder="http://host.docker.internal:8092">
|
|
||||||
<span class="s-hint">Direct Parakeet endpoint for transcription.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>NVIDIA Zeroshot NIM URL</label>
|
|
||||||
<input type="text" id="s-nvidia-zeroshot-url" placeholder="http://host.docker.internal:8093">
|
|
||||||
<span class="s-hint">Magpie Zeroshot clone endpoint with <code>audio_prompt</code>.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>NVIDIA Flow NIM URL</label>
|
|
||||||
<input type="text" id="s-nvidia-flow-url" placeholder="http://host.docker.internal:8094">
|
|
||||||
<span class="s-hint">Magpie Flow clone endpoint with <code>audio_prompt</code> and transcript.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-cluster settings-cluster-stt">
|
|
||||||
<div class="settings-cluster-head">
|
|
||||||
<strong>STT Speech to Text</strong>
|
|
||||||
<span>Whisper, faster-whisper, whisper.cpp, Groq, Parakeet, NVIDIA router</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-grid compact stt-settings-grid">
|
|
||||||
<div class="s-field stt-url-field">
|
|
||||||
<label>Configured Whisper/STT URL</label>
|
|
||||||
<input type="text" id="s-whisper-url" placeholder="http://host.docker.internal:8010">
|
|
||||||
<span class="s-hint">Default recognition endpoint. Expected: <code>POST /v1/audio/transcriptions</code>.</span>
|
|
||||||
<div class="btn-row settings-mini-actions">
|
|
||||||
<button class="btn-secondary" id="s-use-parakeet-asr" type="button">Use Parakeet</button>
|
|
||||||
<button class="btn-secondary" id="s-use-nvidia-router" type="button">Use router</button>
|
|
||||||
<button class="btn-secondary" id="s-use-faster-whisper" type="button">Use faster-whisper</button>
|
|
||||||
<button class="btn-secondary" id="s-use-whisper-cpp" type="button">Use whisper.cpp</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>faster-whisper-server URL <span style="font-weight:400">(CTranslate2 GPU)</span></label>
|
|
||||||
<input type="text" id="s-faster-whisper-url" placeholder="http://host.docker.internal:8000">
|
|
||||||
<span class="s-hint">GPU-accelerated Whisper via CTranslate2. ~70× RT · 1.5 GB VRAM · <code>POST /v1/audio/transcriptions</code>.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>whisper.cpp URL <span style="font-weight:400">(CPU/CUDA)</span></label>
|
|
||||||
<input type="text" id="s-whisper-cpp-url" placeholder="http://host.docker.internal:8080">
|
|
||||||
<span class="s-hint">Lightweight C++ Whisper server. ~8–15× RT CPU · ~1 GB RAM · <code>POST /v1/audio/transcriptions</code>.</span>
|
|
||||||
</div>
|
|
||||||
<div class="s-field">
|
|
||||||
<label>Groq API key <span style="font-weight:400">(Groq Whisper & LLM)</span></label>
|
|
||||||
<div class="s-key-row">
|
|
||||||
<input type="password" id="s-groq-api-key" placeholder="gsk_…" autocomplete="off">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="settings-section">
|
|
||||||
<div class="settings-section-head">
|
|
||||||
<div>
|
|
||||||
<h3>Playback behavior</h3>
|
|
||||||
<p>Small behavior switches for previews and OpenAI-compatible TTS calls.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-grid settings-behavior-grid">
|
<div class="settings-grid settings-behavior-grid">
|
||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>TTS preview playback</label>
|
<label>TTS preview playback</label>
|
||||||
@ -167,16 +154,19 @@
|
|||||||
<span class="s-hint">Controls payload shape for the normal TTS API URL.</span>
|
<span class="s-hint">Controls payload shape for the normal TTS API URL.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<details class="settings-details">
|
<!-- Advanced payloads -->
|
||||||
|
<details class="s-details">
|
||||||
<summary>
|
<summary>
|
||||||
<span>Advanced request payloads</span>
|
<span class="mdi mdi-code-braces"></span>
|
||||||
|
<span class="s-details-title">Advanced request payloads</span>
|
||||||
<small>JSON extras sent to each backend</small>
|
<small>JSON extras sent to each backend</small>
|
||||||
|
<span class="mdi mdi-chevron-down s-details-chevron"></span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="settings-grid three settings-param-grid">
|
<div class="settings-grid three settings-param-grid s-details-body">
|
||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>Voice Clone/Base params</label>
|
<label>Voice Clone / Base params</label>
|
||||||
<textarea id="s-tts-extra-voice-clone" spellcheck="false" placeholder='{"temperature":0.1,"top_p":0.8,"seed":0}'></textarea>
|
<textarea id="s-tts-extra-voice-clone" spellcheck="false" placeholder='{"temperature":0.1,"top_p":0.8,"seed":0}'></textarea>
|
||||||
<span class="s-hint">Extra fields for the 8020 WAV voice clone/base model.</span>
|
<span class="s-hint">Extra fields for the 8020 WAV voice clone/base model.</span>
|
||||||
</div>
|
</div>
|
||||||
@ -198,12 +188,12 @@
|
|||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>NVIDIA Magpie params</label>
|
<label>NVIDIA Magpie params</label>
|
||||||
<textarea id="s-tts-extra-nvidia-magpie" spellcheck="false" placeholder="{}"></textarea>
|
<textarea id="s-tts-extra-nvidia-magpie" spellcheck="false" placeholder="{}"></textarea>
|
||||||
<span class="s-hint">Usually empty. Magpie accepts fixed speaker voices such as sofia, aria, jason, leo, and john.</span>
|
<span class="s-hint">Usually empty. Magpie accepts fixed speakers: sofia, aria, jason, leo, john.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>NVIDIA Zeroshot params</label>
|
<label>NVIDIA Zeroshot params</label>
|
||||||
<textarea id="s-tts-extra-nvidia-zeroshot" spellcheck="false" placeholder='{"zero_shot_quality":20}'></textarea>
|
<textarea id="s-tts-extra-nvidia-zeroshot" spellcheck="false" placeholder='{"zero_shot_quality":20}'></textarea>
|
||||||
<span class="s-hint">Optional multipart fields. The app supplies <code>text</code>, <code>language</code>, and <code>audio_prompt</code>.</span>
|
<span class="s-hint">Optional multipart fields. The app supplies <code>text</code>, <code>language</code>, <code>audio_prompt</code>.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>NVIDIA Flow params</label>
|
<label>NVIDIA Flow params</label>
|
||||||
@ -213,17 +203,20 @@
|
|||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>Kokoro FastAPI params</label>
|
<label>Kokoro FastAPI params</label>
|
||||||
<textarea id="s-tts-extra-kokoro" spellcheck="false" placeholder="{}"></textarea>
|
<textarea id="s-tts-extra-kokoro" spellcheck="false" placeholder="{}"></textarea>
|
||||||
<span class="s-hint">Extra JSON fields for the Kokoro backend. Usually empty — voice is selected from the voice dropdown.</span>
|
<span class="s-hint">Extra JSON fields for Kokoro. Usually empty — voice is selected from the dropdown.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="settings-details">
|
<!-- Voice folders -->
|
||||||
|
<details class="s-details">
|
||||||
<summary>
|
<summary>
|
||||||
<span>Voice folders</span>
|
<span class="mdi mdi-folder-outline"></span>
|
||||||
|
<span class="s-details-title">Voice folders</span>
|
||||||
<small>container paths and Portainer volume mounts</small>
|
<small>container paths and Portainer volume mounts</small>
|
||||||
|
<span class="mdi mdi-chevron-down s-details-chevron"></span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="settings-grid">
|
<div class="settings-grid s-details-body">
|
||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>Voice scan directory</label>
|
<label>Voice scan directory</label>
|
||||||
<input type="text" id="s-voices-scan-dir" placeholder="/voices">
|
<input type="text" id="s-voices-scan-dir" placeholder="/voices">
|
||||||
@ -237,28 +230,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="settings-details">
|
<!-- API keys -->
|
||||||
|
<details class="s-details">
|
||||||
<summary>
|
<summary>
|
||||||
<span>API keys</span>
|
<span class="mdi mdi-key-outline"></span>
|
||||||
|
<span class="s-details-title">API keys</span>
|
||||||
<small>usually empty for local containers</small>
|
<small>usually empty for local containers</small>
|
||||||
|
<span class="mdi mdi-chevron-down s-details-chevron"></span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="settings-grid three">
|
<div class="settings-grid three s-details-body">
|
||||||
<div class="s-field">
|
<div class="s-field">
|
||||||
<label>TTS API key <span style="font-weight:400">(optional)</span></label>
|
<label>TTS API key <span class="s-label-note">(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"><span class="mdi mdi-eye-outline"></span></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 class="s-label-note">(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"><span class="mdi mdi-eye-outline"></span></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 class="s-label-note">(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"><span class="mdi mdi-eye-outline"></span></button>
|
<button type="button" class="s-eye-btn" data-target="s-whisper-key"><span class="mdi mdi-eye-outline"></span></button>
|
||||||
@ -267,22 +263,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
<div class="btn-row settings-actions">
|
<div class="btn-row settings-actions">
|
||||||
<button class="btn-primary" id="s-save-btn">Save settings</button>
|
<button class="btn-primary" id="s-save-btn">Save settings</button>
|
||||||
<button class="btn-secondary" id="s-close-btn">Reload settings</button>
|
<button class="btn-secondary" id="s-close-btn">Reload settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section-head" style="margin-top:28px">
|
<!-- Backup & restore -->
|
||||||
<h3>Backup & restore</h3>
|
<div class="s-section s-section-last">
|
||||||
<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 class="s-section-label">Backup & restore</div>
|
||||||
</div>
|
<p class="s-hint">Export all voices and settings as a ZIP for backup or migration. Import to restore. API keys are excluded from exports.</p>
|
||||||
<div class="btn-row" style="margin-top:12px;gap:10px;flex-wrap:wrap">
|
<div class="btn-row" style="margin-top:8px;gap:10px;flex-wrap:wrap">
|
||||||
<a class="btn-secondary" id="s-export-voices-btn" href="/api/voices/export" download><span class="mdi mdi-download"></span> 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">
|
||||||
<span class="mdi mdi-upload"></span> 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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /tab-settings -->
|
</div><!-- /tab-settings -->
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<span class="section-icon">🌐</span>
|
<span class="section-icon"><span class="mdi mdi-earth"></span></span>
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<h2>Get Voices Online</h2>
|
<h2>Get Voices Online</h2>
|
||||||
<p>Browse public voice clip sources, preview direct audio files, and import voices from the web.</p>
|
<p>Browse public voice clip sources, preview direct audio files, and import voices from the web.</p>
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<!-- Category pills -->
|
<!-- Category pills -->
|
||||||
<div class="el-cats" id="el-cats">
|
<div class="el-cats" id="el-cats">
|
||||||
<button class="el-cat active" data-el-use_case="" data-el-featured="" data-el-category="">All</button>
|
<button class="el-cat active" data-el-use_case="" data-el-featured="" data-el-category="">All</button>
|
||||||
<button class="el-cat" data-el-featured="true">⭐ Featured</button>
|
<button class="el-cat" data-el-featured="true"><span class="mdi mdi-star-outline"></span> Featured</button>
|
||||||
<button class="el-cat" data-el-category="professional">Professional</button>
|
<button class="el-cat" data-el-category="professional">Professional</button>
|
||||||
<button class="el-cat" data-el-use_case="narration">Narration</button>
|
<button class="el-cat" data-el-use_case="narration">Narration</button>
|
||||||
<button class="el-cat" data-el-use_case="conversational">Conversational</button>
|
<button class="el-cat" data-el-use_case="conversational">Conversational</button>
|
||||||
|
|||||||
@ -77,7 +77,7 @@
|
|||||||
<button class="btn-primary" id="preview-btn"><span class="mdi mdi-play"></span> Generate & play</button>
|
<button class="btn-primary" id="preview-btn"><span class="mdi mdi-play"></span> Generate & 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-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="save-preview-btn" disabled><span class="mdi mdi-download"></span> Save WAV</button>
|
||||||
<button class="btn-secondary" id="add-to-playlist-btn" disabled>+ Playlist</button>
|
<button class="btn-secondary" id="add-to-playlist-btn" disabled><span class="mdi mdi-plus"></span> Playlist</button>
|
||||||
</div>
|
</div>
|
||||||
<audio id="preview-audio" controls style="display:none"></audio>
|
<audio id="preview-audio" controls style="display:none"></audio>
|
||||||
<div id="preview-chunk-progress" class="chunk-progress" hidden></div>
|
<div id="preview-chunk-progress" class="chunk-progress" hidden></div>
|
||||||
|
|||||||
121
static/style.css
121
static/style.css
@ -318,62 +318,75 @@ audio { width: 100%; }
|
|||||||
@media (max-width: 900px) { .preview-match-panel { grid-template-columns: 1fr; } }
|
@media (max-width: 900px) { .preview-match-panel { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
/* ── Settings ───────────────────────────────────────────────────────────── */
|
/* ── Settings ───────────────────────────────────────────────────────────── */
|
||||||
#settings-box { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: none; box-sizing: border-box; }
|
#settings-box { display:flex; flex-direction:column; gap:0; width:100%; max-width:none; box-sizing:border-box; padding:0; overflow:hidden; }
|
||||||
#settings-box h2 { font-size: 17px; font-weight: 700; }
|
|
||||||
.settings-section { border: 1px solid var(--border); border-radius: 6px; background: rgba(37,99,235,.03); padding: 14px; display: flex; flex-direction: column; gap: 12px; }
|
/* Header */
|
||||||
.settings-section-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
.s-header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; padding:28px 32px 24px; border-bottom:1px solid var(--border); }
|
||||||
.settings-section-head h3 { font-size: 13px; color: var(--text); margin: 0; text-transform: uppercase; letter-spacing: .07em; }
|
.s-header h2 { font-size:18px; font-weight:700; margin:0 0 5px; }
|
||||||
.settings-section-head p { font-size: 13px; color: var(--subtext); line-height: 1.45; margin: 4px 0 0; max-width: 980px; }
|
.s-subtitle { font-size:13px; color:var(--subtext); line-height:1.55; margin:0; max-width:580px; }
|
||||||
.settings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; }
|
.s-stack-badge { display:inline-flex; align-items:center; gap:5px; border:1px solid color-mix(in srgb,var(--green) 40%,transparent); background:rgba(22,163,74,.07); color:var(--green); border-radius:999px; padding:5px 11px; font-size:12px; font-weight:600; white-space:nowrap; flex-shrink:0; margin-top:2px; }
|
||||||
.settings-grid.three { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
|
|
||||||
.settings-actions { padding-top: 12px; margin-top: 2px; border-top: 1px solid var(--border); }
|
/* Sections */
|
||||||
.setup-intro { background: rgba(37,99,235,.06); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; color: var(--subtext); font-size: 13px; line-height: 1.55; }
|
.s-section { padding:26px 32px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:22px; }
|
||||||
.setup-intro strong { color: var(--text); }
|
.s-section-last { border-bottom:none; }
|
||||||
.s-group { font-size: 14px; font-weight: 700; color: var(--subtext); text-transform: uppercase; letter-spacing: .09em; padding-top: 6px; border-top: 1px solid var(--border); }
|
.s-section-label { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:var(--subtext); }
|
||||||
.s-field { display: flex; flex-direction: column; gap: 5px; }
|
.s-section-desc { font-size:13px; color:var(--subtext); margin:0; line-height:1.5; }
|
||||||
.s-field label { font-size: 13px; color: var(--subtext); font-weight: 500; }
|
|
||||||
.s-field input, .s-field select, .s-field textarea { background: var(--panel); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); padding: 9px 14px; font-size: 14px; }
|
/* Field groups within a section */
|
||||||
.s-field textarea { min-height: 76px; resize: vertical; font-family: monospace; }
|
.s-group { display:flex; flex-direction:column; gap:14px; padding-top:4px; }
|
||||||
.s-hint { font-size: 12px; color: var(--subtext); }
|
.s-group + .s-group { border-top:1px solid var(--border); padding-top:22px; }
|
||||||
.s-key-row { display: flex; gap: 6px; }
|
.s-group-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
|
||||||
.s-key-row input { flex: 1; }
|
.s-group-head strong { font-size:14px; font-weight:600; color:var(--text); }
|
||||||
.s-eye-btn { background: var(--panel); border: 1px solid var(--border); color: var(--subtext); border-radius: var(--radius); padding: 0 13px; font-size: 15px; cursor: pointer; flex-shrink: 0; }
|
.s-group-head span { font-size:12px; color:var(--subtext); }
|
||||||
.s-eye-btn:hover { color: var(--text); }
|
|
||||||
.settings-title-row { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; }
|
/* Field grids */
|
||||||
.settings-title-row h2 { margin:0; }
|
.settings-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(280px,1fr)); gap:16px; }
|
||||||
.settings-title-row .note { margin-top:4px; max-width:760px; }
|
.settings-grid.three { grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); }
|
||||||
.settings-status-pill { border:1px solid var(--border); background:rgba(22,163,74,.10); color:var(--green); border-radius:999px; padding:5px 10px; font-size:12px; font-weight:700; white-space:nowrap; }
|
.settings-grid.compact { grid-template-columns:repeat(2, minmax(220px,1fr)); gap:14px; }
|
||||||
.settings-checklist { display:flex; flex-wrap:wrap; align-items:center; gap:8px 14px; padding:10px 12px; }
|
.settings-behavior-grid { grid-template-columns:minmax(220px,.6fr) minmax(220px,.6fr); gap:16px; align-items:start; }
|
||||||
.settings-checklist strong { margin-right:2px; }
|
|
||||||
.settings-checklist span { display:inline-flex; align-items:center; min-height:24px; padding:2px 9px; border:1px solid var(--border); border-radius:999px; background:var(--panel); color:var(--subtext); }
|
/* Individual field */
|
||||||
.settings-section { padding:12px; gap:10px; }
|
.s-field { display:flex; flex-direction:column; gap:6px; }
|
||||||
.settings-section-primary { background:rgba(37,99,235,.04); }
|
.s-field label { font-size:13px; color:var(--text); font-weight:500; }
|
||||||
.settings-section-head h3 { font-size:12px; }
|
.s-label-note { font-weight:400; color:var(--subtext); }
|
||||||
.settings-cluster-grid { display:grid; grid-template-columns: repeat(2, minmax(320px,1fr)); gap:12px; }
|
.s-field input, .s-field select, .s-field textarea { background:var(--panel); border:1px solid var(--border); color:var(--text); border-radius:var(--radius); padding:9px 12px; font-size:14px; transition:border-color .15s,box-shadow .15s; width:100%; box-sizing:border-box; }
|
||||||
.settings-cluster { border:1px solid var(--border); border-radius:7px; background:var(--surface); padding:12px; display:flex; flex-direction:column; gap:10px; min-width:0; }
|
.s-field input:focus, .s-field select:focus, .s-field textarea:focus { border-color:var(--accent); outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent); }
|
||||||
.settings-cluster-head { display:flex; justify-content:space-between; gap:10px; align-items:baseline; border-bottom:1px solid var(--border); padding-bottom:8px; }
|
.s-field textarea { min-height:76px; resize:vertical; font-family:monospace; }
|
||||||
.settings-cluster-head strong { color:var(--text); font-size:13px; text-transform:uppercase; letter-spacing:.07em; }
|
.s-hint { font-size:12px; color:var(--subtext); line-height:1.5; }
|
||||||
.settings-cluster-head span { color:var(--subtext); font-size:12px; text-align:right; }
|
.s-key-row { display:flex; gap:6px; }
|
||||||
.settings-grid.compact { grid-template-columns: repeat(2, minmax(220px,1fr)); gap:10px; }
|
.s-key-row input { flex:1; }
|
||||||
.settings-behavior-grid { grid-template-columns: minmax(220px,.7fr) minmax(220px,.7fr) minmax(340px,1.1fr); align-items:start; }
|
.s-eye-btn { background:var(--panel); border:1px solid var(--border); color:var(--subtext); border-radius:var(--radius); padding:0 13px; font-size:15px; cursor:pointer; flex-shrink:0; transition:color .15s; }
|
||||||
.settings-mini-actions { margin-top:2px; gap:8px; }
|
.s-eye-btn:hover { color:var(--text); }
|
||||||
.settings-mini-actions button { min-height:34px; padding:7px 12px; }
|
|
||||||
.settings-details { border:1px solid var(--border); border-radius:7px; background:rgba(37,99,235,.025); overflow:hidden; }
|
/* STT layout */
|
||||||
.settings-details summary { list-style:none; cursor:pointer; padding:12px 14px; display:flex; align-items:center; justify-content:space-between; gap:12px; color:var(--text); font-weight:800; text-transform:uppercase; letter-spacing:.07em; font-size:12px; }
|
.stt-settings-grid { grid-template-columns:minmax(320px,.65fr) minmax(240px,1fr); }
|
||||||
.settings-details summary::-webkit-details-marker { display:none; }
|
.settings-mini-actions { margin-top:4px; gap:8px; flex-wrap:wrap; }
|
||||||
.settings-details summary::before { content:'+'; width:20px; height:20px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; border:1px solid var(--border); color:var(--accent); margin-right:2px; flex:0 0 auto; }
|
.settings-mini-actions button { min-height:32px; padding:6px 11px; font-size:12px; }
|
||||||
.settings-details[open] summary::before { content:'-'; }
|
|
||||||
.settings-details summary span { display:flex; align-items:center; gap:8px; margin-right:auto; }
|
/* Expandable details */
|
||||||
.settings-details summary small { color:var(--subtext); font-weight:500; text-transform:none; letter-spacing:0; font-size:12px; }
|
.s-details { border-bottom:1px solid var(--border); }
|
||||||
.settings-details > .settings-grid { padding:0 14px 14px; }
|
.s-details summary { list-style:none; cursor:pointer; padding:16px 32px; display:flex; align-items:center; gap:10px; color:var(--text); font-size:14px; font-weight:600; user-select:none; transition:background .15s; }
|
||||||
|
.s-details summary:hover { background:rgba(0,0,0,.02); }
|
||||||
|
.s-details summary::-webkit-details-marker { display:none; }
|
||||||
|
.s-details-title { flex:1; }
|
||||||
|
.s-details summary small { color:var(--subtext); font-weight:400; font-size:13px; }
|
||||||
|
.s-details-chevron { color:var(--subtext); font-size:18px; transition:transform .2s; flex-shrink:0; }
|
||||||
|
.s-details[open] .s-details-chevron { transform:rotate(180deg); }
|
||||||
|
.s-details-body { padding:4px 32px 24px; }
|
||||||
.settings-param-grid textarea { min-height:58px; }
|
.settings-param-grid textarea { min-height:58px; }
|
||||||
.settings-actions { display:flex; gap:10px; align-items:center; padding:12px 0 0; }
|
|
||||||
|
/* Actions row */
|
||||||
|
.settings-actions { display:flex; gap:10px; align-items:center; padding:20px 32px; }
|
||||||
.settings-actions button { min-width:128px; }
|
.settings-actions button { min-width:128px; }
|
||||||
.settings-cluster-stt { grid-column: 1 / -1; }
|
|
||||||
.stt-settings-grid { grid-template-columns: minmax(320px, .65fr) minmax(240px, 1fr); }
|
/* Responsive */
|
||||||
.stt-settings-grid .stt-url-field { max-width: 720px; }
|
@media (max-width:1100px) { .settings-grid.compact { grid-template-columns:1fr; } .settings-behavior-grid { grid-template-columns:1fr; } }
|
||||||
@media (max-width: 1100px) { .settings-cluster-grid { grid-template-columns:1fr; } .settings-behavior-grid { grid-template-columns:1fr; } }
|
@media (max-width:900px) {
|
||||||
@media (max-width: 900px) { .stt-settings-grid { grid-template-columns: 1fr; } .stt-settings-grid .stt-url-field { max-width: none; } }
|
.stt-settings-grid { grid-template-columns:1fr; }
|
||||||
|
.s-header, .s-section, .settings-actions { padding-left:18px; padding-right:18px; }
|
||||||
|
.s-details summary { padding-left:18px; padding-right:18px; }
|
||||||
|
.s-details-body { padding-left:18px; padding-right:18px; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Toast ──────────────────────────────────────────────────────────────── */
|
/* ── Toast ──────────────────────────────────────────────────────────────── */
|
||||||
#toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 14px 20px; border-radius: var(--radius); font-size: 14px; opacity: 0; transition: opacity .3s; pointer-events: none; z-index: 200; max-width: 380px; box-shadow: var(--shadow); }
|
#toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 14px 20px; border-radius: var(--radius); font-size: 14px; opacity: 0; transition: opacity .3s; pointer-events: none; z-index: 200; max-width: 380px; box-shadow: var(--shadow); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user