From 0bdc0a5e424fec10b731d558549e8eb1ad5c0fbd Mon Sep 17 00:00:00 2001 From: "thomas.kopp" Date: Wed, 1 Apr 2026 20:48:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20settings=20page=20=E2=80=94=20PipeWire?= =?UTF-8?q?=20audio=20device=20+=20remote=20Whisper/Ollama=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- api/router.py | 9 ++++ frontend/settings.html | 101 ++++++++++++++++++++++++++++++++++++++ frontend/settings.js | 108 +++++++++++++++++++++++++++++++++++++++++ main.py | 5 ++ 4 files changed, 223 insertions(+) create mode 100644 frontend/settings.html create mode 100644 frontend/settings.js diff --git a/api/router.py b/api/router.py index 9430e5e..ce2dad2 100644 --- a/api/router.py +++ b/api/router.py @@ -279,6 +279,15 @@ async def create_combined_source(body: dict, user: dict = Depends(current_user)) return {"device": "transkriptor-combined.monitor", "module_ids": ids} +@router.get("/settings") +async def settings_page_route(user: dict = Depends(current_user)): + from fastapi.responses import FileResponse, RedirectResponse + from pathlib import Path + if not user.get("is_admin"): + return RedirectResponse("/") + return FileResponse(str(Path(__file__).parent.parent / "frontend" / "settings.html")) + + @router.websocket("/ws") async def websocket_endpoint(ws: WebSocket): from auth import get_user_for_token diff --git a/frontend/settings.html b/frontend/settings.html new file mode 100644 index 0000000..2e2582d --- /dev/null +++ b/frontend/settings.html @@ -0,0 +1,101 @@ + + + + + + tüit Transkriptor — Einstellungen + + + + + +
+ +
+ Transkriptor — Einstellungen + +
+
+
+

Audio

+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Verarbeitung

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + + diff --git a/frontend/settings.js b/frontend/settings.js new file mode 100644 index 0000000..b5fbcff --- /dev/null +++ b/frontend/settings.js @@ -0,0 +1,108 @@ +const token = sessionStorage.getItem('token'); +function authHeaders() { + return token ? { 'Authorization': 'Bearer ' + token } : {}; +} +function apiFetch(url, options) { + options = options || {}; + return fetch(url, Object.assign({}, options, { + headers: Object.assign({'Content-Type': 'application/json'}, authHeaders(), options.headers || {}), + })); +} + +let _devices = []; + +function showToast(msg) { + const t = document.getElementById('toast'); + t.textContent = msg; + t.classList.add('show'); + setTimeout(function() { t.classList.remove('show'); }, 2500); +} + +async function loadDevices() { + const r = await apiFetch('/audio/devices'); + if (!r.ok) return; + _devices = await r.json(); + const sel = document.getElementById('audio-device'); + const current = sel.value; + sel.replaceChildren(new Option('Systemstandard', '')); + _devices.forEach(function(d) { sel.appendChild(new Option(d.name, d.name)); }); + if (current) sel.value = current; + ['combined-mic', 'combined-monitor'].forEach(function(id) { + const el = document.getElementById(id); + el.replaceChildren(); + _devices.forEach(function(d) { el.appendChild(new Option(d.name, d.name)); }); + }); +} + +async function loadOllamaModels(baseUrl, current) { + try { + const r = await fetch(baseUrl + '/api/tags'); + if (!r.ok) return; + const data = await r.json(); + const sel = document.getElementById('ollama-model'); + sel.replaceChildren(); + (data.models || []).forEach(function(m) { sel.appendChild(new Option(m.name, m.name)); }); + if (current) sel.value = current; + } catch(e) {} +} + +async function loadConfig() { + const r = await apiFetch('/config'); + if (!r.ok) return; + const cfg = await r.json(); + document.getElementById('audio-device').value = (cfg.audio && cfg.audio.device) || ''; + document.getElementById('whisper-url').value = (cfg.whisper && cfg.whisper.base_url) || ''; + document.getElementById('whisper-model').value = (cfg.whisper && cfg.whisper.model) || 'large-v3'; + const ollamaUrl = (cfg.ollama && cfg.ollama.base_url) || 'http://localhost:11434'; + document.getElementById('ollama-url').value = ollamaUrl; + await loadOllamaModels(ollamaUrl, cfg.ollama && cfg.ollama.model); +} + +document.getElementById('refresh-devices-btn').addEventListener('click', loadDevices); + +document.getElementById('create-combined-btn').addEventListener('click', function() { + document.getElementById('combined-form').classList.toggle('visible'); +}); +document.getElementById('combined-cancel-btn').addEventListener('click', function() { + document.getElementById('combined-form').classList.remove('visible'); +}); +document.getElementById('combined-confirm-btn').addEventListener('click', async function() { + const mic = document.getElementById('combined-mic').value; + const monitor = document.getElementById('combined-monitor').value; + const r = await apiFetch('/audio/combined', { + method: 'POST', + body: JSON.stringify({ mic: mic, monitor: monitor }), + }); + if (!r.ok) { showToast('Fehler beim Erstellen'); return; } + const data = await r.json(); + showToast('Erstellt: ' + data.device); + document.getElementById('combined-form').classList.remove('visible'); + await loadDevices(); + document.getElementById('audio-device').value = data.device; +}); + +document.getElementById('ollama-url').addEventListener('change', function(e) { + loadOllamaModels(e.target.value, document.getElementById('ollama-model').value); +}); + +document.getElementById('save-btn').addEventListener('click', async function() { + const body = { + audio: { device: document.getElementById('audio-device').value }, + whisper: { + base_url: document.getElementById('whisper-url').value, + model: document.getElementById('whisper-model').value, + }, + ollama: { + base_url: document.getElementById('ollama-url').value, + model: document.getElementById('ollama-model').value, + }, + }; + const r = await apiFetch('/config', { method: 'PUT', body: JSON.stringify(body) }); + if (r.ok) { showToast('Gespeichert'); } else { showToast('Fehler beim Speichern'); } +}); + +(async function() { + if (!token) { location.href = '/login'; return; } + await loadDevices(); + await loadConfig(); +})(); diff --git a/main.py b/main.py index 095e890..acd4f8b 100644 --- a/main.py +++ b/main.py @@ -44,6 +44,11 @@ async def logo(): return FileResponse(str(FRONTEND_DIR / "logo.svg"), media_type="image/svg+xml") +@app.get("/settings.js") +async def settingsjs(): + return FileResponse(str(FRONTEND_DIR / "settings.js")) + + # ── PID file ─────────────────────────────────────────────────────────────────── def write_pid(pid_path: str):