diff --git a/server.py b/server.py index 1b1ad8e..779b787 100644 --- a/server.py +++ b/server.py @@ -105,6 +105,29 @@ _registry: dict[str, Path] = {} app = FastAPI(title="TTS Voice Creator - Clone and Design") +# ── In-memory log buffer ─────────────────────────────────────────────────────── +_LOG_BUFFER_MAX = 400 +_log_buffer: list[dict] = [] + + +class _BufferHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + try: + _log_buffer.insert(0, { + "ts": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(), + "level": record.levelname, + "name": record.name, + "msg": record.getMessage(), + }) + del _log_buffer[_LOG_BUFFER_MAX:] + except Exception: + pass + + +_buf_handler = _BufferHandler() +_buf_handler.setLevel(logging.DEBUG) +logging.getLogger().addHandler(_buf_handler) + _ROUTING_LOG_MAX = int(os.environ.get("TTS_ROUTING_LOG_MAX", "120")) _routing_log: list[dict] = [] @@ -721,6 +744,18 @@ async def clear_tts_routing_log(): return {"ok": True, "items": []} +@app.get("/api/logs") +async def get_server_logs(limit: int = 200): + limit = max(1, min(int(limit or 200), _LOG_BUFFER_MAX)) + return {"items": _log_buffer[:limit], "max": _LOG_BUFFER_MAX} + + +@app.delete("/api/logs") +async def clear_server_logs(): + _log_buffer.clear() + return {"ok": True} + + # ── VoiceDesign virtual voice presets ───────────────────────────────────────── _DEFAULT_DESIGN_PRESETS = { diff --git a/static/app.js b/static/app.js index 3c53cea..014631e 100644 --- a/static/app.js +++ b/static/app.js @@ -517,11 +517,13 @@ async function clientAutoTrimBounds(fid) { function applyTheme(t) { document.documentElement.dataset.theme = t; - $('theme-btn').innerHTML = t === 'dark' ? '' : ''; - $('theme-btn').title = t === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'; + const btn = $('theme-btn'); + if (btn) { btn.innerHTML = t === 'dark' ? '' : ''; btn.title = t === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'; } + const sel = $('s-theme-select'); + if (sel) sel.value = t; localStorage.setItem('vcf-theme', t); } -$('theme-btn').addEventListener('click', () => +$('theme-btn')?.addEventListener('click', () => applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark') ); applyTheme(localStorage.getItem('vcf-theme') || 'dark'); @@ -1944,14 +1946,16 @@ async function loadSettings() { $('s-vd-key').value = s.voice_design_api_key || ''; $('s-voices-scan-dir').value = s.voices_scan_dir || ''; $('s-output-dir').value = s.output_dir || ''; + const themeEl = $('s-theme-select'); + if (themeEl) themeEl.value = document.documentElement.dataset.theme || 'dark'; await refreshTtsBackendAvailability(); + renderSettingsAbout(); } function markSettingsSeen() { localStorage.setItem(SETTINGS_SEEN_KEY, '1'); } function openSettings(firstRun = false) { - $('settings-first-run-note').style.display = firstRun ? '' : 'none'; if (firstRun) markSettingsSeen(); switchTab('settings'); } @@ -1965,7 +1969,7 @@ document.querySelectorAll('.s-eye-btn').forEach(btn => { inp.type = inp.type === 'password' ? 'text' : 'password'; }); }); -$('settings-btn').addEventListener('click', async () => { await loadSettings(); openSettings(false); }); +$('settings-btn')?.addEventListener('click', async () => { await loadSettings(); openSettings(false); }); $('s-close-btn').addEventListener('click', async () => { await loadSettings(); toast('Settings reloaded', 'success'); }); $('s-use-parakeet-asr')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-nvidia-asr-url').value || 'http://host.docker.internal:8092'; }); $('s-use-nvidia-router')?.addEventListener('click', () => { const url = $('s-nvidia-router-url').value || 'http://host.docker.internal:8090'; $('s-whisper-url').value = url; $('s-nvidia-tts-url').value = url; }); @@ -2032,7 +2036,11 @@ $('s-save-btn').addEventListener('click', async () => { markSettingsSeen(); renderIntegrationSnippets(); toast('Settings saved', 'success'); - toast('Settings saved', 'success'); +}); + +// Theme select in General settings +document.addEventListener('change', e => { + if (e.target.id === 's-theme-select') applyTheme(e.target.value); }); // ── Voice ID field (tab 3) ──────────────────────────────────────────────── @@ -7137,6 +7145,84 @@ $('refine-restore-btn')?.addEventListener('click', () => { toast('Original transcription restored', 'success'); }); +// ── Settings: Logs ──────────────────────────────────────────────────────── + +let _logsLiveTimer = null; +let _logsActiveLevel = ''; + +function escLog(s) { return String(s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); } + +window.loadSettingsLogs = async function() { + const viewer = $('s-log-viewer'); + if (!viewer) return; + try { + const data = await fetch('/api/logs?limit=300').then(r => r.json()); + const items = (data.items || []).filter(item => + !_logsActiveLevel || item.level === _logsActiveLevel + ); + const count = $('s-log-count'); + if (count) count.textContent = items.length + ' entries'; + if (!items.length) { + viewer.innerHTML = '
No log entries
'; + return; + } + viewer.innerHTML = items.map(item => { + const ts = item.ts ? item.ts.replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : ''; + return `
+ ${escLog(ts)} + ${escLog(item.level)} + ${escLog(item.msg)} +
`; + }).join(''); + } catch(e) { + viewer.innerHTML = `
Failed to load logs: ${escLog(e.message)}
`; + } +}; + +$('s-logs-refresh-btn')?.addEventListener('click', () => loadSettingsLogs()); + +$('s-logs-clear-btn')?.addEventListener('click', async () => { + await fetch('/api/logs', { method: 'DELETE' }); + const viewer = $('s-log-viewer'); + if (viewer) viewer.innerHTML = '
Logs cleared
'; + const count = $('s-log-count'); + if (count) count.textContent = ''; +}); + +$('s-logs-live-toggle')?.addEventListener('change', function() { + clearInterval(_logsLiveTimer); + if (this.checked) { + loadSettingsLogs(); + _logsLiveTimer = setInterval(loadSettingsLogs, 3000); + } +}); + +document.querySelectorAll('.s-log-filter').forEach(btn => { + btn.addEventListener('click', function() { + document.querySelectorAll('.s-log-filter').forEach(b => b.classList.remove('is-active')); + this.classList.add('is-active'); + _logsActiveLevel = this.dataset.logLevel; + loadSettingsLogs(); + }); +}); + +// ── Settings: About ─────────────────────────────────────────────────────── + +function renderSettingsAbout() { + const el = $('s-about-backends'); + if (!el) return; + const available = new Set((_ttsBackends || []).filter(b => b.available).map(b => b.id)); + const all = (_ttsBackends || []); + if (!all.length) { el.innerHTML = ''; return; } + el.innerHTML = all.map(b => { + const online = available.has(b.id); + return ` + ${escHtml(b.label)} + + `; + }).join(''); +} + // ── Voices import ────────────────────────────────────────────────────────── $('s-import-voices-file')?.addEventListener('change', async function () { diff --git a/static/index.html b/static/index.html index e28c4d0..a0f7422 100644 --- a/static/index.html +++ b/static/index.html @@ -76,14 +76,26 @@ -