diff --git a/README.md b/README.md index 7ef2b7b..717ef9d 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,19 @@ On Linux the native window needs system WebKitGTK (`gir1.2-webkit2-4.1` + `python3-gi`); without it the app degrades to a browser tab. See [docs/desktop-shell.md](docs/desktop-shell.md). +### Terminal (TUI) + +Prefer the keyboard? AmicoScript also ships a terminal interface — same +backend, no browser needed. + +```bash +./tui.sh # macOS/Linux — installs TUI deps on first run, then launches +tui.bat # Windows +``` + +See [tui/README.md](tui/README.md) for keybindings and screenshots. The web +UI's Help modal also has a one-click "Copy command" for this. + ### Tests ```bash diff --git a/backend/api/routes/releases.py b/backend/api/routes/releases.py index de9b25a..d836bee 100644 --- a/backend/api/routes/releases.py +++ b/backend/api/routes/releases.py @@ -3,6 +3,9 @@ from pathlib import Path from fastapi import APIRouter, Request +from pydantic import BaseModel + +from settings import _get_whisper_settings, _save_whisper_settings MODELS_META = [ {"id": "tiny", "name": "Tiny", "params": "~39M", "ram": "~1 GB", "speed": 5, "accuracy": 1}, @@ -32,6 +35,23 @@ def get_models() -> list: return MODELS_META +@router.get("/api/whisper/models") +def get_whisper_models() -> dict: + ws = _get_whisper_settings() + return {"models": MODELS_META, "default": ws["whisper_model"]} + + +class WhisperModelPayload(BaseModel): + model: str + + +@router.post("/api/whisper/models") +async def set_whisper_model(payload: WhisperModelPayload) -> dict: + ws = _get_whisper_settings() + _save_whisper_settings(payload.model, ws["whisper_device"], ws["whisper_compute"]) + return {"ok": True, "model": payload.model} + + @router.get("/api/latest-release") def api_latest_release(request: Request) -> dict: info = getattr(request.app.state, "latest_release", {}) or {} diff --git a/backend/api/routes/settings.py b/backend/api/routes/settings.py index 4a1a37d..b3b6c55 100644 --- a/backend/api/routes/settings.py +++ b/backend/api/routes/settings.py @@ -14,8 +14,10 @@ from settings import ( _get_meeting_capture_enabled, _get_transcription_defaults, + _get_whisper_settings, _load_settings, _save_settings, + _save_whisper_settings, _set_meeting_capture_enabled, _set_transcription_defaults, ) @@ -66,6 +68,7 @@ def get_settings() -> dict: import state settings = _load_settings() defaults = _get_transcription_defaults() + ws = _get_whisper_settings() return { "hf_token": settings.get("hf_token", ""), "exit_token": getattr(state, "exit_token", ""), @@ -75,6 +78,9 @@ def get_settings() -> dict: "default_model": defaults["default_model"], "default_language": defaults["default_language"], "default_diarize": defaults["default_diarize"], + "whisper_model": ws["whisper_model"], + "whisper_device": ws["whisper_device"], + "whisper_compute": ws["whisper_compute"], } @@ -84,6 +90,9 @@ async def save_settings( model: str | None = Form(None), language: str | None = Form(None), diarize: str | None = Form(None), + whisper_model: str | None = Form(None), + whisper_device: str | None = Form(None), + whisper_compute: str | None = Form(None), ) -> dict: """Persist HF token and/or transcription defaults. @@ -100,7 +109,14 @@ async def save_settings( language=language, diarize=_to_bool(diarize) if diarize is not None else None, ) - return {"ok": True, **_get_transcription_defaults()} + if whisper_model: + ws = _get_whisper_settings() + _save_whisper_settings( + whisper_model, + whisper_device or ws["whisper_device"], + whisper_compute or ws["whisper_compute"], + ) + return {"ok": True, **_get_transcription_defaults(), **_get_whisper_settings()} @router.post("/api/settings/meeting-capture") diff --git a/backend/settings.py b/backend/settings.py index 10aab64..333a199 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -119,3 +119,22 @@ def _save_llm_settings(base_url: str, model_name: str, api_key: str) -> None: settings["llm_model_name"] = model_name settings["llm_api_key"] = api_key _save_settings(settings) + + +def _get_whisper_settings() -> dict: + """Return Whisper config: model, device, compute_type.""" + settings = _load_settings() + return { + "whisper_model": settings.get("whisper_model", "small"), + "whisper_device": settings.get("whisper_device", "auto"), + "whisper_compute": settings.get("whisper_compute", "float16"), + } + + +def _save_whisper_settings(model: str, device: str, compute: str) -> None: + """Persist Whisper settings to disk.""" + settings = _load_settings() + settings["whisper_model"] = model + settings["whisper_device"] = device + settings["whisper_compute"] = compute + _save_settings(settings) diff --git a/frontend/index.html b/frontend/index.html index 5577ab4..ad820b1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3162,6 +3162,35 @@
AmicoScript also ships a keyboard-driven terminal + interface (TUI) — same backend, no browser needed.
+
+