Fix clone UX: sample text, live monitor, quality, STT picker, OGG accept
1. Sample text: expose initCloneSampleText as a window function and call it from nav.js runSideEffects when the clone section is activated, ensuring the textarea is always populated even if the IIFE ran before the element existed. 2. Better sample texts: all 8 languages rewritten to ~38 words / ~15 s, first-person, phonetically rich, proper Unicode diacritics. 3. Live mic monitor: a level-meter (18-bar) + scrolling oscilloscope canvas (ring-buffer, 300 px, colour-coded) added to the microphone card. "Check level" / "Stop monitor" buttons start/stop it independently; clicking Record starts it automatically. Uses raw mic constraints (no echo-cancel / AGC) for cleaner voice clone audio. Mic gain slider and dB readout included. 4. Recording quality: MediaRecorder now requests audioBitsPerSecond:256000 in both voice-clone.js and stt.js. 5. STT engine picker: Recognition engine <select> + Refresh button added above the Auto-transcribe button in Step 3. refreshSttBackends() now syncs both stt-tts-stt-backend and clone-stt-backend. The transcribe call passes the chosen backend to /api/transcribe. 6. File input: explicit extension list added to accept= for OGG/OPUS. 7. CSS: .mic-live-wave style added (dark/light theme variants). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4eb132e2b0
commit
a0d94b140e
@ -39,12 +39,13 @@ async function refreshSttBackends(selected = '') {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
_sttBackends = [];
|
_sttBackends = [];
|
||||||
}
|
}
|
||||||
const sel = $('stt-tts-stt-backend');
|
['stt-tts-stt-backend', 'clone-stt-backend'].forEach(id => {
|
||||||
if (sel) {
|
const sel = $(id);
|
||||||
|
if (!sel) return;
|
||||||
const prev = selected || sel.value || 'configured';
|
const prev = selected || sel.value || 'configured';
|
||||||
sel.innerHTML = sttBackendOptionHtml(prev);
|
sel.innerHTML = sttBackendOptionHtml(prev);
|
||||||
sel.disabled = !_sttBackends.some(b => b.available);
|
sel.disabled = !_sttBackends.some(b => b.available);
|
||||||
}
|
});
|
||||||
updateSttBackendHelp();
|
updateSttBackendHelp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +149,7 @@ $('stt-tts-rec-start')?.addEventListener('click', async () => {
|
|||||||
sttTtsRecordSecs++;
|
sttTtsRecordSecs++;
|
||||||
$('stt-tts-rec-time').textContent = Math.floor(sttTtsRecordSecs / 60) + ':' + String(sttTtsRecordSecs % 60).padStart(2, '0');
|
$('stt-tts-rec-time').textContent = Math.floor(sttTtsRecordSecs / 60) + ':' + String(sttTtsRecordSecs % 60).padStart(2, '0');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
sttTtsRecorder = new MediaRecorder(sttTtsRecordStream);
|
sttTtsRecorder = new MediaRecorder(sttTtsRecordStream, {audioBitsPerSecond: 256000});
|
||||||
sttTtsRecorder.ondataavailable = e => { if (e.data.size) sttTtsRecordChunks.push(e.data); };
|
sttTtsRecorder.ondataavailable = e => { if (e.data.size) sttTtsRecordChunks.push(e.data); };
|
||||||
sttTtsRecorder.onstop = async () => {
|
sttTtsRecorder.onstop = async () => {
|
||||||
clearInterval(sttTtsRecordTimer);
|
clearInterval(sttTtsRecordTimer);
|
||||||
|
|||||||
@ -1,25 +1,29 @@
|
|||||||
// ── Clone: recommended sample sentences ──────────────────────────────────
|
// ── Clone: recommended sample sentences ──────────────────────────────────
|
||||||
|
|
||||||
const CLONE_SAMPLE_TEXTS = {
|
const CLONE_SAMPLE_TEXTS = {
|
||||||
EN: 'The clear morning light warmed the quiet studio as I described a silver train, a bright red apple, and the gentle rhythm of rain on the window.',
|
EN: 'Hello! My name is Sam, and this is my voice. I can speak softly or with great strength. The crisp winter air, warm firelight, and the gentle sound of rain — these are the things I love. Can you hear how clearly I speak?',
|
||||||
DE: 'Das klare Morgenlicht waermte das ruhige Studio, waehrend ich einen silbernen Zug, einen roten Apfel und den sanften Rhythmus des Regens am Fenster beschrieb.',
|
DE: 'Hallo! Ich heiße Alex und das ist meine Stimme. Ich kann leise flüstern oder mit voller Kraft sprechen. Klare Winterluft, warmes Kerzenlicht und der Klang des Regens am Fenster — das liebe ich. Hörst du, wie deutlich ich spreche?',
|
||||||
IT: "La luce chiara del mattino scaldava lo studio tranquillo mentre descrivevo un treno d'argento, una mela rossa e il ritmo leggero della pioggia alla finestra.",
|
IT: "Ciao! Mi chiamo Marco e questa è la mia voce. Posso parlare dolcemente o con grande forza. L'aria fresca d'inverno, la luce calda del fuoco e il suono della pioggia — queste sono le cose che amo. Senti come parlo chiaramente?",
|
||||||
ES: 'La clara luz de la manana calentaba el estudio tranquilo mientras describia un tren plateado, una manzana roja y el suave ritmo de la lluvia en la ventana.',
|
ES: '¡Hola! Me llamo Carlos y esta es mi voz. Puedo hablar suavemente o con gran fuerza. El aire frío del invierno, la cálida luz del fuego y el suave sonido de la lluvia — estas son las cosas que amo. ¿Puedes oír lo claramente que hablo?',
|
||||||
FR: 'La lumiere claire du matin rechauffait le studio calme pendant que je decrivais un train argente, une pomme rouge et le doux rythme de la pluie sur la fenetre.',
|
FR: "Bonjour ! Je m'appelle Sophie et voici ma voix. Je peux parler doucement ou avec grande force. L'air vif de l'hiver, la douce lumière du feu et le son de la pluie — voilà ce que j'aime. Entends-tu comme je parle clairement ?",
|
||||||
PT: 'A luz clara da manha aquecia o estudio tranquilo enquanto eu descrevia um comboio prateado, uma maca vermelha e o ritmo suave da chuva na janela.',
|
PT: 'Olá! Meu nome é Ana e esta é a minha voz. Posso falar suavemente ou com grande força. O ar fresco do inverno, a luz quente do fogo e o som da chuva — estas são as coisas que amo. Consegues ouvir como falo claramente?',
|
||||||
NL: 'Het heldere ochtendlicht verwarmde de stille studio terwijl ik een zilveren trein, een rode appel en het zachte ritme van regen op het raam beschreef.',
|
NL: 'Hoi! Mijn naam is Laura en dit is mijn stem. Ik kan zacht fluisteren of met volle kracht spreken. De frisse winterlucht, het warme kaarslicht en het geluid van de regen — dat zijn de dingen die ik liefheb. Hoor je hoe helder ik spreek?',
|
||||||
PL: 'Jasne poranne swiatlo ogrzewalo ciche studio, gdy opisywalem srebrny pociag, czerwone jablko i lagodny rytm deszczu na oknie.',
|
PL: 'Cześć! Mam na imię Anna i to jest mój głos. Mogę mówić cicho lub z całą mocą. Mroźne zimowe powietrze, ciepłe światło ognia i dźwięk deszczu za oknem — to są rzeczy, które kocham. Czy słyszysz, jak wyraźnie mówię?',
|
||||||
};
|
};
|
||||||
|
|
||||||
(function initCloneSampleText() {
|
window.initCloneSampleText = function () {
|
||||||
const sel = $('clone-sample-lang');
|
const sel = $('clone-sample-lang');
|
||||||
const txt = $('clone-sample-text');
|
const txt = $('clone-sample-text');
|
||||||
if (!sel || !txt) return;
|
if (!sel || !txt) return;
|
||||||
txt.value = CLONE_SAMPLE_TEXTS[sel.value] || CLONE_SAMPLE_TEXTS.EN;
|
if (!txt.value.trim()) txt.value = CLONE_SAMPLE_TEXTS[sel.value] || CLONE_SAMPLE_TEXTS.EN;
|
||||||
sel.addEventListener('change', () => {
|
if (!sel._cloneSampleBound) {
|
||||||
txt.value = CLONE_SAMPLE_TEXTS[sel.value] || CLONE_SAMPLE_TEXTS.EN;
|
sel.addEventListener('change', () => {
|
||||||
});
|
txt.value = CLONE_SAMPLE_TEXTS[sel.value] || CLONE_SAMPLE_TEXTS.EN;
|
||||||
})();
|
});
|
||||||
|
sel._cloneSampleBound = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initCloneSampleText();
|
||||||
|
|
||||||
// ── WaveSurfer ────────────────────────────────────────────────────────────
|
// ── WaveSurfer ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -209,23 +213,135 @@ async function requestMicrophoneStream(options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Clone mic monitor ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _cloneMonState = {
|
||||||
|
stream:null, recordStream:null, audioCtx:null, sourceNode:null,
|
||||||
|
gainNode:null, analyser:null, meterRaf:null, waveRing:null, monitoring:false
|
||||||
|
};
|
||||||
|
|
||||||
|
function _cloneRenderMeter(level = 0, db = -Infinity, clipped = false) {
|
||||||
|
const meter = $('clone-mic-meter');
|
||||||
|
if (!meter) return;
|
||||||
|
if (!meter.children.length) {
|
||||||
|
for (let i = 0; i < 18; i++) { const b = document.createElement('div'); b.className = 'bar'; meter.appendChild(b); }
|
||||||
|
}
|
||||||
|
const active = Math.round(Math.max(0, Math.min(1, level)) * meter.children.length);
|
||||||
|
[...meter.children].forEach((bar, i) => {
|
||||||
|
bar.className = 'bar';
|
||||||
|
bar.style.height = (7 + Math.min(i, active) * 1.55) + 'px';
|
||||||
|
if (i < active) { bar.classList.add('on'); if (db > -12 && i > 11) bar.classList.add('hot'); if (clipped && i > 14) bar.classList.add('clip'); }
|
||||||
|
});
|
||||||
|
const el = $('clone-db-readout');
|
||||||
|
if (el) el.textContent = Number.isFinite(db) ? db.toFixed(1) + ' dB' : '-∞ dB';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cloneStartMeter() {
|
||||||
|
if (!_cloneMonState.analyser) return;
|
||||||
|
if (_cloneMonState.meterRaf) cancelAnimationFrame(_cloneMonState.meterRaf);
|
||||||
|
const data = new Float32Array(_cloneMonState.analyser.fftSize);
|
||||||
|
const canvas = $('clone-live-wave');
|
||||||
|
const RING = 300, ADD = 10;
|
||||||
|
_cloneMonState.waveRing = new Float32Array(RING);
|
||||||
|
const tick = () => {
|
||||||
|
_cloneMonState.analyser.getFloatTimeDomainData(data);
|
||||||
|
let sum = 0, peak = 0;
|
||||||
|
for (const s of data) { sum += s * s; peak = Math.max(peak, Math.abs(s)); }
|
||||||
|
const rms = Math.sqrt(sum / data.length);
|
||||||
|
const db = rms > 0 ? 20 * Math.log10(rms) : -Infinity;
|
||||||
|
const level = Number.isFinite(db) ? (db + 60) / 60 : 0;
|
||||||
|
_cloneRenderMeter(level, db, peak > 0.98);
|
||||||
|
if (canvas && _cloneMonState.waveRing) {
|
||||||
|
const ring = _cloneMonState.waveRing;
|
||||||
|
ring.copyWithin(0, ADD);
|
||||||
|
for (let i = 0; i < ADD; i++) ring[RING - ADD + i] = data[Math.floor(i * data.length / ADD)];
|
||||||
|
const ctx = canvas.getContext('2d'), w = canvas.width, h = canvas.height;
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = peak > 0.98 ? '#f38ba8' : db > -12 ? '#f9e2af' : '#a6e3a1';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
const mid = h / 2;
|
||||||
|
for (let i = 0; i < RING; i++) {
|
||||||
|
const x = (i / RING) * w, y = mid - ring[i] * mid * 0.85;
|
||||||
|
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
_cloneMonState.meterRaf = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _cloneStartMonitor() {
|
||||||
|
if (_cloneMonState.recordStream) return;
|
||||||
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||||
|
_cloneMonState.stream = await requestMicrophoneStream({raw:true});
|
||||||
|
if (AudioCtx) {
|
||||||
|
_cloneMonState.audioCtx = new AudioCtx();
|
||||||
|
_cloneMonState.sourceNode = _cloneMonState.audioCtx.createMediaStreamSource(_cloneMonState.stream);
|
||||||
|
_cloneMonState.gainNode = _cloneMonState.audioCtx.createGain();
|
||||||
|
_cloneMonState.analyser = _cloneMonState.audioCtx.createAnalyser();
|
||||||
|
_cloneMonState.analyser.fftSize = 1024;
|
||||||
|
const dest = _cloneMonState.audioCtx.createMediaStreamDestination();
|
||||||
|
const gainVal = parseFloat($('clone-mic-gain')?.value) || 1;
|
||||||
|
_cloneMonState.gainNode.gain.value = gainVal;
|
||||||
|
_cloneMonState.sourceNode.connect(_cloneMonState.gainNode);
|
||||||
|
_cloneMonState.gainNode.connect(_cloneMonState.analyser);
|
||||||
|
_cloneMonState.gainNode.connect(dest);
|
||||||
|
_cloneMonState.recordStream = dest.stream;
|
||||||
|
_cloneStartMeter();
|
||||||
|
} else {
|
||||||
|
_cloneMonState.recordStream = _cloneMonState.stream;
|
||||||
|
}
|
||||||
|
_cloneMonState.monitoring = true;
|
||||||
|
if ($('clone-monitor-btn')) $('clone-monitor-btn').disabled = true;
|
||||||
|
if ($('clone-monitor-stop')) $('clone-monitor-stop').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cloneStopMonitor() {
|
||||||
|
if (_cloneMonState.meterRaf) cancelAnimationFrame(_cloneMonState.meterRaf);
|
||||||
|
_cloneMonState.meterRaf = null;
|
||||||
|
[_cloneMonState.sourceNode, _cloneMonState.gainNode, _cloneMonState.analyser].forEach(n => { try { if(n) n.disconnect(); } catch(e){} });
|
||||||
|
if (_cloneMonState.stream) _cloneMonState.stream.getTracks().forEach(t => t.stop());
|
||||||
|
if (_cloneMonState.recordStream) _cloneMonState.recordStream.getTracks().forEach(t => t.stop());
|
||||||
|
if (_cloneMonState.audioCtx) _cloneMonState.audioCtx.close().catch(()=>{});
|
||||||
|
Object.assign(_cloneMonState, {stream:null, recordStream:null, sourceNode:null, gainNode:null, analyser:null, audioCtx:null, monitoring:false, waveRing:null});
|
||||||
|
_cloneRenderMeter(0, -Infinity, false);
|
||||||
|
const wc = $('clone-live-wave'); if (wc) wc.getContext('2d').clearRect(0, 0, wc.width, wc.height);
|
||||||
|
if ($('clone-monitor-btn')) $('clone-monitor-btn').disabled = false;
|
||||||
|
if ($('clone-monitor-stop')) $('clone-monitor-stop').disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('clone-monitor-btn')?.addEventListener('click', async () => {
|
||||||
|
try { await _cloneStartMonitor(); status('Mic level monitor active'); }
|
||||||
|
catch(e) { const m = await microphoneErrorMessage(e); toast(m, 'error'); }
|
||||||
|
});
|
||||||
|
$('clone-monitor-stop')?.addEventListener('click', () => { _cloneStopMonitor(); status('Mic level monitor stopped'); });
|
||||||
|
$('clone-mic-gain')?.addEventListener('input', () => {
|
||||||
|
const v = parseFloat($('clone-mic-gain').value) || 0;
|
||||||
|
if ($('clone-mic-gain-value')) $('clone-mic-gain-value').textContent = v.toFixed(2) + 'x';
|
||||||
|
if (_cloneMonState.gainNode) _cloneMonState.gainNode.gain.value = v;
|
||||||
|
});
|
||||||
|
_cloneRenderMeter();
|
||||||
|
|
||||||
let mediaRec=null, recChunks=[], recTimer=null, recSecs=0;
|
let mediaRec=null, recChunks=[], recTimer=null, recSecs=0;
|
||||||
$('rec-start-btn').addEventListener('click', async () => {
|
$('rec-start-btn').addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
const stream = await requestMicrophoneStream();
|
await _cloneStartMonitor();
|
||||||
recChunks=[]; recSecs=0; $('rec-time').textContent='0:00';
|
recChunks=[]; recSecs=0; $('rec-time').textContent='0:00';
|
||||||
$('rec-indicator').classList.add('active'); $('rec-start-btn').disabled=true; $('rec-stop-btn').disabled=false;
|
$('rec-indicator').classList.add('active'); $('rec-start-btn').disabled=true; $('rec-stop-btn').disabled=false;
|
||||||
recTimer = setInterval(() => { recSecs++; $('rec-time').textContent=Math.floor(recSecs/60)+':'+String(recSecs%60).padStart(2,'0'); }, 1000);
|
recTimer = setInterval(() => { recSecs++; $('rec-time').textContent=Math.floor(recSecs/60)+':'+String(recSecs%60).padStart(2,'0'); }, 1000);
|
||||||
mediaRec = new MediaRecorder(stream);
|
mediaRec = new MediaRecorder(_cloneMonState.recordStream || _cloneMonState.stream, {audioBitsPerSecond: 256000});
|
||||||
mediaRec.ondataavailable = e => { if(e.data.size) recChunks.push(e.data); };
|
mediaRec.ondataavailable = e => { if(e.data.size) recChunks.push(e.data); };
|
||||||
mediaRec.onstop = async () => {
|
mediaRec.onstop = async () => {
|
||||||
clearInterval(recTimer); $('rec-indicator').classList.remove('active'); stream.getTracks().forEach(t=>t.stop());
|
clearInterval(recTimer); $('rec-indicator').classList.remove('active');
|
||||||
const blob = new Blob(recChunks, {type:mediaRec.mimeType||'audio/webm'});
|
const blob = new Blob(recChunks, {type:mediaRec.mimeType||'audio/webm'});
|
||||||
const ext = (mediaRec.mimeType||'').includes('ogg') ? '.ogg' : '.webm';
|
const ext = (mediaRec.mimeType||'').includes('ogg') ? '.ogg' : '.webm';
|
||||||
|
_cloneStopMonitor();
|
||||||
await uploadFile(new File([blob], 'recording'+ext, {type:blob.type}));
|
await uploadFile(new File([blob], 'recording'+ext, {type:blob.type}));
|
||||||
};
|
};
|
||||||
mediaRec.start(100); status('Recording…');
|
mediaRec.start(100); status('Recording…');
|
||||||
} catch(e) { toast(await microphoneErrorMessage(e), 'error'); }
|
} catch(e) { _cloneStopMonitor(); toast(await microphoneErrorMessage(e), 'error'); }
|
||||||
});
|
});
|
||||||
$('rec-stop-btn').addEventListener('click', () => {
|
$('rec-stop-btn').addEventListener('click', () => {
|
||||||
if(mediaRec&&mediaRec.state!=='inactive') mediaRec.stop();
|
if(mediaRec&&mediaRec.state!=='inactive') mediaRec.stop();
|
||||||
@ -782,12 +898,19 @@ $('design-download-btn').addEventListener('click', () => {
|
|||||||
|
|
||||||
// ── Transcribe ────────────────────────────────────────────────────────────
|
// ── Transcribe ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$('clone-refresh-stt-btn')?.addEventListener('click', async () => {
|
||||||
|
$('clone-refresh-stt-btn').disabled = true;
|
||||||
|
try { await refreshSttBackends($('clone-stt-backend')?.value); }
|
||||||
|
finally { $('clone-refresh-stt-btn').disabled = false; }
|
||||||
|
});
|
||||||
|
|
||||||
$('transcribe-btn').addEventListener('click', async () => {
|
$('transcribe-btn').addEventListener('click', async () => {
|
||||||
const id = trimmedFileId||designedFileId||currentFileId;
|
const id = trimmedFileId||designedFileId||currentFileId;
|
||||||
if (!id) { toast('No audio to transcribe','error'); return; }
|
if (!id) { toast('No audio to transcribe','error'); return; }
|
||||||
$('transcribe-btn').disabled=true; $('transcribe-status').textContent='Transcribing…';
|
$('transcribe-btn').disabled=true; $('transcribe-status').textContent='Transcribing…';
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/transcribe', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})});
|
const backend = $('clone-stt-backend')?.value || 'configured';
|
||||||
|
const r = await fetch('/api/transcribe', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id, backend})});
|
||||||
if (!r.ok) { const e=await r.json(); throw new Error(e.detail); }
|
if (!r.ok) { const e=await r.json(); throw new Error(e.detail); }
|
||||||
const d = await r.json(); $('transcript-area').value=d.text; $('transcribe-status').textContent='Done';
|
const d = await r.json(); $('transcript-area').value=d.text; $('transcribe-status').textContent='Done';
|
||||||
toast('Transcription complete','success');
|
toast('Transcription complete','success');
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
const SECTIONS = ['s-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation'];
|
const SECTIONS = ['s-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation'];
|
||||||
|
|
||||||
function runSideEffects(name) {
|
function runSideEffects(name) {
|
||||||
|
if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText();
|
||||||
if (name === 'library' && typeof loadVoiceLibrary === 'function') loadVoiceLibrary();
|
if (name === 'library' && typeof loadVoiceLibrary === 'function') loadVoiceLibrary();
|
||||||
if ((name === 'integrations' || name === 'howto') && typeof renderIntegrationSnippets === 'function') {
|
if ((name === 'integrations' || name === 'howto') && typeof renderIntegrationSnippets === 'function') {
|
||||||
if (typeof loadVoiceLibrary === 'function' && !(window._voices && window._voices.length)) loadVoiceLibrary();
|
if (typeof loadVoiceLibrary === 'function' && !(window._voices && window._voices.length)) loadVoiceLibrary();
|
||||||
@ -158,4 +159,56 @@
|
|||||||
showSection(_startSection);
|
showSection(_startSection);
|
||||||
var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library';
|
var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library';
|
||||||
runSideEffects(_startTab);
|
runSideEffects(_startTab);
|
||||||
|
|
||||||
|
// ── Mobile sidebar drawer ────────────────────────────────────────────────
|
||||||
|
var sidebar = document.getElementById('sidebar');
|
||||||
|
var backdrop = document.getElementById('sidebar-backdrop');
|
||||||
|
var toggleBtn = document.getElementById('sidebar-toggle');
|
||||||
|
|
||||||
|
function openSidebar() {
|
||||||
|
if (!sidebar) return;
|
||||||
|
sidebar.classList.add('open');
|
||||||
|
if (backdrop) backdrop.classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden'; // prevent scroll behind drawer
|
||||||
|
}
|
||||||
|
function closeSidebar() {
|
||||||
|
if (!sidebar) return;
|
||||||
|
sidebar.classList.remove('open');
|
||||||
|
if (backdrop) backdrop.classList.remove('open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toggleBtn) toggleBtn.addEventListener('click', openSidebar);
|
||||||
|
if (backdrop) backdrop.addEventListener('click', closeSidebar);
|
||||||
|
|
||||||
|
// Close drawer when a nav item is tapped on mobile
|
||||||
|
document.querySelectorAll('#sidebar .nav-item, #sidebar .nav-tree-item').forEach(function (el) {
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
if (window.innerWidth <= 767) closeSidebar();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Mobile voice inspector: back button ──────────────────────────────────
|
||||||
|
// When a voice row is selected on mobile, show inspector full-height.
|
||||||
|
// The inspector JS will call window.onMobileInspectorOpen/Close as hooks.
|
||||||
|
window.onMobileInspectorOpen = function () {
|
||||||
|
var wb = document.querySelector('.voices-workbench');
|
||||||
|
if (!wb || window.innerWidth > 767) return;
|
||||||
|
wb.classList.add('mobile-inspector-open');
|
||||||
|
// Inject back button if not already there
|
||||||
|
var insp = document.getElementById('voices-inspector');
|
||||||
|
if (insp && !insp.querySelector('.insp-mobile-back')) {
|
||||||
|
var back = document.createElement('div');
|
||||||
|
back.className = 'insp-mobile-back';
|
||||||
|
back.innerHTML = '<span class="mdi mdi-arrow-left"></span> All voices';
|
||||||
|
back.addEventListener('click', function () { window.onMobileInspectorClose && window.onMobileInspectorClose(); });
|
||||||
|
insp.insertBefore(back, insp.firstChild);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.onMobileInspectorClose = function () {
|
||||||
|
var wb = document.querySelector('.voices-workbench');
|
||||||
|
if (wb) wb.classList.remove('mobile-inspector-open');
|
||||||
|
var back = document.querySelector('.insp-mobile-back');
|
||||||
|
if (back) back.remove();
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<strong>Drop an audio / video file here</strong>
|
<strong>Drop an audio / video file here</strong>
|
||||||
<span>WAV · MP3 · OGG · FLAC · M4A · MP4 · MKV · WEBM</span>
|
<span>WAV · MP3 · OGG · FLAC · M4A · MP4 · MKV · WEBM</span>
|
||||||
<span style="margin-top:10px;font-size:12px;color:var(--subtext)">or click to browse</span>
|
<span style="margin-top:10px;font-size:12px;color:var(--subtext)">or click to browse</span>
|
||||||
<input type="file" id="file-input" accept="audio/*,video/*">
|
<input type="file" id="file-input" accept=".wav,.mp3,.ogg,.opus,.flac,.m4a,.mp4,.mkv,.webm,.mov,.avi,audio/*,video/*">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -47,10 +47,26 @@
|
|||||||
<textarea class="sample-sentence" id="clone-sample-text" spellcheck="true"></textarea>
|
<textarea class="sample-sentence" id="clone-sample-text" spellcheck="true"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
|
<button class="btn-secondary" id="clone-monitor-btn"><span class="mdi mdi-waveform"></span> Check level</button>
|
||||||
|
<button class="btn-secondary" id="clone-monitor-stop" disabled>Stop monitor</button>
|
||||||
<button class="btn-red" id="rec-start-btn"><span class="mdi mdi-record-circle-outline"></span> Record</button>
|
<button class="btn-red" id="rec-start-btn"><span class="mdi mdi-record-circle-outline"></span> Record</button>
|
||||||
<button class="btn-secondary" id="rec-stop-btn" disabled><span class="mdi mdi-stop-circle-outline"></span> Stop</button>
|
<button class="btn-secondary" id="rec-stop-btn" disabled><span class="mdi mdi-stop-circle-outline"></span> Stop</button>
|
||||||
<div id="rec-indicator"><div id="rec-dot"></div><span id="rec-time">0:00</span></div>
|
<div id="rec-indicator"><div id="rec-dot"></div><span id="rec-time">0:00</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mic-monitor-box" id="clone-monitor-box" style="margin-top:10px">
|
||||||
|
<div class="mic-monitor-head">
|
||||||
|
<span>Input level</span>
|
||||||
|
<span class="meter-readout" id="clone-db-readout">-∞ dB</span>
|
||||||
|
</div>
|
||||||
|
<div class="mic-meter" id="clone-mic-meter" aria-hidden="true"></div>
|
||||||
|
<canvas id="clone-live-wave" class="mic-live-wave" width="300" height="48" aria-hidden="true"></canvas>
|
||||||
|
<div class="mic-gain-row">
|
||||||
|
<label for="clone-mic-gain">Mic gain</label>
|
||||||
|
<input id="clone-mic-gain" type="range" min="0" max="2" step="0.05" value="1">
|
||||||
|
<span class="mic-gain-value" id="clone-mic-gain-value">1.00x</span>
|
||||||
|
</div>
|
||||||
|
<div class="note" style="margin-top:4px">Best peaks: −18 to −9 dB. Never red.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" id="waveform-card" style="display:none">
|
<div class="card" id="waveform-card" style="display:none">
|
||||||
<h2>Step 2 — Trim selection</h2>
|
<h2>Step 2 — Trim selection</h2>
|
||||||
@ -110,6 +126,11 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Transcript (reference text)</h2>
|
<h2>Transcript (reference text)</h2>
|
||||||
<p class="card-subtitle">The spoken text that matches this audio — type it or auto-transcribe.</p>
|
<p class="card-subtitle">The spoken text that matches this audio — type it or auto-transcribe.</p>
|
||||||
|
<div class="btn-row" style="gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
|
||||||
|
<label style="font-size:13px;color:var(--subtext);white-space:nowrap">Recognition engine:</label>
|
||||||
|
<select id="clone-stt-backend" style="flex:1;min-width:160px"><option value="configured">Configured Whisper/STT</option></select>
|
||||||
|
<button class="btn-secondary" id="clone-refresh-stt-btn" type="button">Refresh</button>
|
||||||
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn-secondary" id="transcribe-btn"><span class="mdi mdi-message-text-outline"></span> Auto-transcribe</button>
|
<button class="btn-secondary" id="transcribe-btn"><span class="mdi mdi-message-text-outline"></span> Auto-transcribe</button>
|
||||||
<span id="transcribe-status" style="font-size:13px;color:var(--subtext)"></span>
|
<span id="transcribe-status" style="font-size:13px;color:var(--subtext)"></span>
|
||||||
|
|||||||
192
static/style.css
192
static/style.css
@ -666,6 +666,8 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.mic-meter .bar.on { opacity: 1; background: var(--green); }
|
.mic-meter .bar.on { opacity: 1; background: var(--green); }
|
||||||
.mic-meter .bar.hot { background: var(--yellow); }
|
.mic-meter .bar.hot { background: var(--yellow); }
|
||||||
.mic-meter .bar.clip { background: var(--red); }
|
.mic-meter .bar.clip { background: var(--red); }
|
||||||
|
.mic-live-wave { width: 100%; height: 52px; border-radius: 4px; display: block; background: rgba(0,0,0,.06); margin-bottom: 4px; }
|
||||||
|
[data-theme="dark"] .mic-live-wave { background: rgba(17,17,27,.6); }
|
||||||
.mic-buttons { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px,1fr)); gap: 8px; align-content: start; min-width: 0; }
|
.mic-buttons { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px,1fr)); gap: 8px; align-content: start; min-width: 0; }
|
||||||
.mic-buttons button { padding-left: 10px; padding-right: 10px; min-height: 44px; }
|
.mic-buttons button { padding-left: 10px; padding-right: 10px; min-height: 44px; }
|
||||||
.mic-timer { font-family: monospace; font-size: 13px; color: var(--subtext); }
|
.mic-timer { font-family: monospace; font-size: 13px; color: var(--subtext); }
|
||||||
@ -797,10 +799,128 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.qwen-sample-actions { justify-content: flex-start; }
|
.qwen-sample-actions { justify-content: flex-start; }
|
||||||
.design-preset-row, .routing-grid { grid-template-columns: 1fr; }
|
.design-preset-row, .routing-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
@media (max-width: 760px) {
|
/* Mobile top bar — hidden on desktop */
|
||||||
#sidebar { display: none; }
|
#mobile-topbar {
|
||||||
#main-content { padding: 0 12px 0; }
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar backdrop (mobile drawer overlay) */
|
||||||
|
#sidebar-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed; inset: 0; z-index: 199;
|
||||||
|
background: rgba(0,0,0,0.45);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
#sidebar-backdrop.open { display: block; }
|
||||||
|
|
||||||
|
/* ── Mobile (≤ 767px) ─────────────────────────────────────────────────────── */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
|
||||||
|
/* Top bar */
|
||||||
|
#mobile-topbar {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
height: 52px; padding: 0 14px;
|
||||||
|
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||||
|
position: sticky; top: 0; z-index: 100;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
body { display: flex; flex-direction: column; }
|
||||||
|
#app-shell { flex: 1; overflow: hidden; }
|
||||||
|
|
||||||
|
#sidebar-toggle {
|
||||||
|
background: none; border: 1px solid var(--border);
|
||||||
|
border-radius: 7px; width: 36px; height: 36px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
cursor: pointer; color: var(--text); font-size: 18px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#sidebar-toggle:active { background: var(--panel); }
|
||||||
|
.mobile-brand { font-size: 15px; font-weight: 800; color: var(--accent); }
|
||||||
|
|
||||||
|
/* Sidebar: slide-in drawer */
|
||||||
|
#sidebar {
|
||||||
|
position: fixed; top: 0; left: -240px; bottom: 0;
|
||||||
|
width: 240px !important; z-index: 200;
|
||||||
|
transition: left 0.22s ease;
|
||||||
|
box-shadow: 4px 0 24px rgba(0,0,0,0.18);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#sidebar.open { left: 0; }
|
||||||
|
|
||||||
|
/* Main content full width, top-padded by topbar height */
|
||||||
|
#main-content { padding: 0 12px 60px; }
|
||||||
|
|
||||||
|
/* Section headings */
|
||||||
|
.section-header { flex-wrap: wrap; gap: 8px; }
|
||||||
|
.section-search { width: 100% !important; max-width: 100%; box-sizing: border-box; }
|
||||||
|
|
||||||
|
/* Toast: full width on small screens */
|
||||||
|
#toast { right: 10px; left: 10px; max-width: none; }
|
||||||
|
.toast-item { max-width: 100%; }
|
||||||
|
|
||||||
|
/* ── Voice workbench ──────────────────────────────────────────────────── */
|
||||||
|
.voices-workbench {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
.voices-list-pane {
|
||||||
|
width: 100% !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.vl-card-list {
|
||||||
|
max-height: 55vh;
|
||||||
|
}
|
||||||
|
.voices-inspector-pane {
|
||||||
|
flex: none;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
/* When a voice is selected on mobile, hide the list and show inspector full-height */
|
||||||
|
.voices-workbench.mobile-inspector-open .voices-list-pane { display: none; }
|
||||||
|
.voices-workbench.mobile-inspector-open .voices-inspector-pane { min-height: calc(100dvh - 52px); }
|
||||||
|
|
||||||
|
/* Inspector back button (injected by JS) */
|
||||||
|
.insp-mobile-back {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 10px 14px; font-size: 13px; font-weight: 600;
|
||||||
|
color: var(--accent); cursor: pointer;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.insp-mobile-back .mdi { font-size: 18px; }
|
||||||
|
|
||||||
|
/* ── Conversation ─────────────────────────────────────────────────────── */
|
||||||
|
.conv-config-row { flex-direction: column; gap: 10px; }
|
||||||
|
.conv-config-group--actions { margin-left: 0; }
|
||||||
|
.conv-url-inp { width: 100% !important; box-sizing: border-box; }
|
||||||
|
.conv-main { flex-direction: column; }
|
||||||
|
.conv-stats-panel { width: 100% !important; flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* ── Routing log ──────────────────────────────────────────────────────── */
|
||||||
|
.routing-log-entry {
|
||||||
|
min-width: 0 !important;
|
||||||
|
grid-template-columns: auto 1fr !important;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
}
|
||||||
|
.routing-log-entry .log-duration,
|
||||||
|
.routing-log-entry .log-meta,
|
||||||
|
.routing-log-entry .log-text { display: none; }
|
||||||
|
|
||||||
|
/* ── Try It Out ───────────────────────────────────────────────────────── */
|
||||||
|
.preview-match-panel { grid-template-columns: 1fr !important; }
|
||||||
|
|
||||||
|
/* ── Clone / Design section toolbar ──────────────────────────────────── */
|
||||||
|
.vl-toolbar { gap: 5px; }
|
||||||
|
|
||||||
|
/* ── Settings ─────────────────────────────────────────────────────────── */
|
||||||
|
.settings-grid { grid-template-columns: 1fr !important; }
|
||||||
|
.settings-cluster-grid { grid-template-columns: 1fr !important; }
|
||||||
|
|
||||||
|
/* ── Generic helpers ──────────────────────────────────────────────────── */
|
||||||
.vl-grid { grid-template-columns: 1fr; }
|
.vl-grid { grid-template-columns: 1fr; }
|
||||||
|
.section-toolbar { flex-wrap: wrap; gap: 6px; }
|
||||||
|
select, input[type="text"], input[type="url"], input[type="number"], textarea {
|
||||||
|
font-size: 16px; /* prevent iOS auto-zoom */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.library-control-panel { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
|
.library-control-panel { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
|
||||||
@ -867,14 +987,23 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
gap: 10px; background: transparent; overflow: visible;
|
gap: 10px; background: transparent; overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List pane */
|
/* List pane — flex column wrapper; visual card appearance is on .vl-card children */
|
||||||
.voices-list-pane {
|
.voices-list-pane {
|
||||||
width: 380px; flex-shrink: 0;
|
width: 380px; flex-shrink: 0;
|
||||||
display: flex; flex-direction: column;
|
display: flex; flex-direction: column; gap: 8px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The two cards inside the list pane */
|
||||||
|
.vl-card {
|
||||||
border: 1px solid var(--border); border-radius: var(--radius);
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
background: var(--surface); overflow: hidden;
|
background: var(--surface); overflow: hidden;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
.vl-card-list {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
flex: 1; min-height: 0; overflow: hidden;
|
||||||
|
}
|
||||||
.vl-toolbar {
|
.vl-toolbar {
|
||||||
display: flex; flex-wrap: wrap; gap: 4px;
|
display: flex; flex-wrap: wrap; gap: 4px;
|
||||||
padding: 8px 10px 7px; border-bottom: 1px solid var(--border);
|
padding: 8px 10px 7px; border-bottom: 1px solid var(--border);
|
||||||
@ -898,6 +1027,33 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
border-radius: var(--radius); background: var(--bg); color: var(--text); max-width: 80px;
|
border-radius: var(--radius); background: var(--bg); color: var(--text); max-width: 80px;
|
||||||
}
|
}
|
||||||
.vl-disabled-label { font-size: 14.5px; color: var(--subtext); white-space: nowrap; cursor: pointer; gap: 3px; display: flex; align-items: center; }
|
.vl-disabled-label { font-size: 14.5px; color: var(--subtext); white-space: nowrap; cursor: pointer; gap: 3px; display: flex; align-items: center; }
|
||||||
|
|
||||||
|
/* Sort bar */
|
||||||
|
.vl-sort-bar {
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
padding: 5px 10px; border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.vl-sort-label {
|
||||||
|
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
|
||||||
|
color: var(--subtext); flex-shrink: 0; margin-right: 1px;
|
||||||
|
}
|
||||||
|
#voice-sort-field {
|
||||||
|
flex: 1; padding: 3px 6px; font-size: 12px;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
background: var(--surface); color: var(--text); cursor: pointer;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
#voice-sort-field:focus { outline: none; border-color: var(--accent); }
|
||||||
|
.vl-sort-dir-btn {
|
||||||
|
flex-shrink: 0; width: 26px; height: 26px; padding: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
background: var(--surface); color: var(--subtext); cursor: pointer;
|
||||||
|
font-size: 14px; transition: background .1s, color .1s;
|
||||||
|
}
|
||||||
|
.vl-sort-dir-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
|
||||||
#voice-list { flex: 1; overflow-y: auto; min-height: 0; }
|
#voice-list { flex: 1; overflow-y: auto; min-height: 0; }
|
||||||
.vl-footer {
|
.vl-footer {
|
||||||
padding: 7px 10px; border-top: 1px solid var(--border);
|
padding: 7px 10px; border-top: 1px solid var(--border);
|
||||||
@ -911,10 +1067,12 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
display: flex !important; flex-direction: row !important; align-items: center !important;
|
display: flex !important; flex-direction: row !important; align-items: center !important;
|
||||||
gap: 9px !important; padding: 7px 10px !important; cursor: pointer;
|
gap: 9px !important; padding: 7px 10px !important; cursor: pointer;
|
||||||
border: none !important; border-left: 3px solid transparent !important;
|
border: none !important; border-left: 3px solid transparent !important;
|
||||||
|
border-bottom: 1px solid var(--border) !important;
|
||||||
border-radius: 0 !important; box-shadow: none !important;
|
border-radius: 0 !important; box-shadow: none !important;
|
||||||
transform: none !important; transition: background .1s; user-select: none;
|
transform: none !important; transition: background .1s; user-select: none;
|
||||||
background: var(--surface) !important;
|
background: var(--surface) !important;
|
||||||
}
|
}
|
||||||
|
.voices-list-pane .vl-row:last-child { border-bottom: none !important; }
|
||||||
.voices-list-pane .vl-row:hover { background: var(--panel) !important; }
|
.voices-list-pane .vl-row:hover { background: var(--panel) !important; }
|
||||||
.voices-list-pane .vl-row.vr-selected { background: rgba(37,99,235,.07) !important; border-left-color: var(--accent) !important; }
|
.voices-list-pane .vl-row.vr-selected { background: rgba(37,99,235,.07) !important; border-left-color: var(--accent) !important; }
|
||||||
.voices-list-pane .vl-row.vr-disabled { opacity: .5; }
|
.voices-list-pane .vl-row.vr-disabled { opacity: .5; }
|
||||||
@ -965,10 +1123,32 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.vl-gender-label.g-m { color: #0ea5e9; }
|
.vl-gender-label.g-m { color: #0ea5e9; }
|
||||||
.vl-gender-label.g-n { color: #8b5cf6; }
|
.vl-gender-label.g-n { color: #8b5cf6; }
|
||||||
.vl-flag { font-size: 14px; color: var(--subtext); }
|
.vl-flag { font-size: 14px; color: var(--subtext); }
|
||||||
|
|
||||||
|
/* Benchmark speed chip — compact list row + inspector header */
|
||||||
|
.vl-meta .vl-bench-chip { margin-left: 2px; }
|
||||||
|
.vl-bench-chip {
|
||||||
|
display: inline-flex; align-items: center; gap: 2px;
|
||||||
|
font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums;
|
||||||
|
padding: 1px 5px; border-radius: 4px;
|
||||||
|
background: rgba(20,184,166,.12); color: var(--subtext);
|
||||||
|
}
|
||||||
|
.vl-bench-chip .mdi { font-size: 10px; }
|
||||||
|
.insp-bench-chip {
|
||||||
|
display: inline-flex; align-items: center; gap: 3px;
|
||||||
|
font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--subtext);
|
||||||
|
}
|
||||||
|
.insp-bench-chip .mdi { font-size: 11px; }
|
||||||
|
.bench-ok { color: var(--green, #22c55e) !important; }
|
||||||
|
.bench-warn{ color: var(--yellow, #eab308) !important; }
|
||||||
|
.bench-bad { color: var(--red, #ef4444) !important; }
|
||||||
|
.vl-bench-chip.bench-ok { background: rgba(34,197,94,.1); }
|
||||||
|
.vl-bench-chip.bench-warn{ background: rgba(234,179,8,.1); }
|
||||||
|
.vl-bench-chip.bench-bad { background: rgba(239,68,68,.1); }
|
||||||
.vl-dbfs { display: none; }
|
.vl-dbfs { display: none; }
|
||||||
|
|
||||||
/* Synth mode panel (segmented control + preview input) */
|
/* Synth mode panel (segmented control + preview input) */
|
||||||
.vl-synth-panel { background: var(--surface); border-bottom: 1px solid var(--border); }
|
.vl-synth-panel { background: var(--surface); }
|
||||||
.vl-synth-mode-row { display: flex; align-items: center; gap: 7px; padding: 5px 8px 5px; }
|
.vl-synth-mode-row { display: flex; align-items: center; gap: 7px; padding: 5px 8px 5px; }
|
||||||
.vl-synth-icon { font-size: 9px; color: var(--teal); flex-shrink: 0; }
|
.vl-synth-icon { font-size: 9px; color: var(--teal); flex-shrink: 0; }
|
||||||
.vl-synth-label { font-size: 14px; font-weight: 700; color: var(--subtext); white-space: nowrap; flex-shrink: 0; }
|
.vl-synth-label { font-size: 14px; font-weight: 700; color: var(--subtext); white-space: nowrap; flex-shrink: 0; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user