From 844c929fdbce3e425d334b162621f54c3e734357 Mon Sep 17 00:00:00 2001 From: jaewilson07 Date: Sun, 19 Apr 2026 23:04:56 -0600 Subject: [PATCH 1/6] fix: add restart: unless-stopped to vtm-api and ollama services Ensures containers recover automatically after crashes or Docker restarts, supporting Docker Desktop auto-start on Windows login. Co-Authored-By: Claude Sonnet 4.6 --- voice-to-mermaid/backend/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/voice-to-mermaid/backend/docker-compose.yml b/voice-to-mermaid/backend/docker-compose.yml index 2ee800c..1d5ee68 100644 --- a/voice-to-mermaid/backend/docker-compose.yml +++ b/voice-to-mermaid/backend/docker-compose.yml @@ -2,6 +2,7 @@ services: ollama: image: ollama/ollama:latest container_name: vtm-ollama + restart: unless-stopped ports: - "11434:11434" volumes: @@ -18,6 +19,7 @@ services: api: build: . container_name: vtm-api + restart: unless-stopped ports: - "${PORT:-7625}:7625" env_file: .env From cfa20943f3475ef372ede7692efd63a364426729 Mon Sep 17 00:00:00 2001 From: jaewilson07 Date: Mon, 20 Apr 2026 13:48:53 -0600 Subject: [PATCH 2/6] feat: move non-secret settings from .env to config.yaml whisper.enabled/model/language/device, ollama.default_model, openai.model, and paths now live in config.yaml (volume-mounted, editable without rebuild). .env reduced to secrets only (OLLAMA_URL). Env var overrides still work. Co-Authored-By: Claude Sonnet 4.6 --- voice-to-mermaid/backend/config.yaml | 21 +++++++++++-- voice-to-mermaid/backend/main.py | 46 +++++++++++++++------------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/voice-to-mermaid/backend/config.yaml b/voice-to-mermaid/backend/config.yaml index 199cae0..5b859ac 100644 --- a/voice-to-mermaid/backend/config.yaml +++ b/voice-to-mermaid/backend/config.yaml @@ -1,11 +1,28 @@ # voice-to-mermaid backend — non-sensitive app config -# Volume-mounted in Docker; edit without rebuilding. +# Volume-mounted in Docker; edit here without rebuilding. # -# Secrets (OLLAMA_URL, API_KEY, etc.) stay in .env / Infisical. +# Secrets (OLLAMA_URL, OPENAI_API_KEY, etc.) stay in .env / Infisical. ollama: + default_model: "qwen3:8b" # Models shown in the UI picker — exact names or prefixes (e.g. "qwen2.5:"). # Leave empty to show all installed models. model_filter: - "qwen2.5:14b" - "qwen3.5:27b" + - "qwen3:8b" + - "qwen3:14b" + - "llama3.2:3b" + +openai: + model: "gpt-4o-mini" + +whisper: + enabled: true + model: "medium.en" + language: "en" + device: "auto" # "auto", "cuda", "cpu" + +paths: + prompt: "prompts/mermaid.txt" + log_dir: "data/logs" diff --git a/voice-to-mermaid/backend/main.py b/voice-to-mermaid/backend/main.py index 01d80f0..1caa7f4 100644 --- a/voice-to-mermaid/backend/main.py +++ b/voice-to-mermaid/backend/main.py @@ -47,39 +47,43 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger(__name__) -# ── Config from env ─────────────────────────────────────────────────────────── - -OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/") -OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen3:8b") -OPENAI_URL = os.environ.get("OPENAI_BASE_URL", "").rstrip("/") -OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "") -OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini") -WHISPER_ENABLED = os.environ.get("WHISPER_ENABLED", "0") == "1" -WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "medium.en") -WHISPER_LANG = os.environ.get("WHISPER_LANG", "en") -PROMPT_PATH = Path(os.environ.get("MERMAID_PROMPT_PATH", "prompts/mermaid.txt")) -LOG_DIR = Path(os.environ.get("LOG_DIR", "data/logs")) - -_ollama_models_cache: list[dict] | None = None +# ── Config (config.yaml for settings, .env for secrets) ────────────────────── # config.yaml lives next to this file in source; /app/config.yaml in Docker _CONFIG_PATH = Path(__file__).parent / "config.yaml" -def _load_model_filter() -> list[str]: +def _load_config() -> dict: try: import yaml # type: ignore[import-untyped] - - raw = yaml.safe_load(_CONFIG_PATH.read_text()) or {} - return [str(p) for p in (raw.get("ollama", {}).get("model_filter") or [])] + return yaml.safe_load(_CONFIG_PATH.read_text()) or {} except FileNotFoundError: - return [] + log.warning("config.yaml not found — using defaults") + return {} except Exception as exc: log.warning("Failed to load config.yaml: %s", exc) - return [] + return {} + + +_cfg = _load_config() + +# Secrets — env only +OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/") +OPENAI_URL = os.environ.get("OPENAI_BASE_URL", "").rstrip("/") +OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "") +# Settings — config.yaml (env var override still works if needed) +OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL") or _cfg.get("ollama", {}).get("default_model", "qwen3:8b") +OPENAI_MODEL = os.environ.get("OPENAI_MODEL") or _cfg.get("openai", {}).get("model", "gpt-4o-mini") +WHISPER_ENABLED = _cfg.get("whisper", {}).get("enabled", False) +WHISPER_MODEL = _cfg.get("whisper", {}).get("model", "medium.en") +WHISPER_LANG = _cfg.get("whisper", {}).get("language", "en") +WHISPER_DEVICE = _cfg.get("whisper", {}).get("device", "auto") +PROMPT_PATH = Path(_cfg.get("paths", {}).get("prompt", "prompts/mermaid.txt")) +LOG_DIR = Path(_cfg.get("paths", {}).get("log_dir", "data/logs")) +_MODEL_FILTER: list[str] = [str(p) for p in (_cfg.get("ollama", {}).get("model_filter") or [])] -_MODEL_FILTER: list[str] = _load_model_filter() +_ollama_models_cache: list[dict] | None = None async def _fetch_ollama_models() -> list[dict]: From d87cd932e8e9d6b1c1e9f6e23d9c7481690fbb3b Mon Sep 17 00:00:00 2001 From: jaewilson07 Date: Mon, 20 Apr 2026 13:58:15 -0600 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20address=20PR=20review=20comments=20?= =?UTF-8?q?=E2=80=94=20dict=20validation,=20ImportError=20fallback,=20WHIS?= =?UTF-8?q?PER=5FDEVICE=20wired,=20default=20enabled=3Dfalse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- voice-to-mermaid/backend/config.yaml | 4 ++-- voice-to-mermaid/backend/main.py | 29 ++++++++++++++++++---------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/voice-to-mermaid/backend/config.yaml b/voice-to-mermaid/backend/config.yaml index 5b859ac..5f40488 100644 --- a/voice-to-mermaid/backend/config.yaml +++ b/voice-to-mermaid/backend/config.yaml @@ -18,10 +18,10 @@ openai: model: "gpt-4o-mini" whisper: - enabled: true + enabled: false # set true only when requirements.stt.txt deps are installed model: "medium.en" language: "en" - device: "auto" # "auto", "cuda", "cpu" + device: "auto" # "auto", "cuda", "cpu" — maps to WhisperLiveKitConfig backend paths: prompt: "prompts/mermaid.txt" diff --git a/voice-to-mermaid/backend/main.py b/voice-to-mermaid/backend/main.py index 1caa7f4..8442ff2 100644 --- a/voice-to-mermaid/backend/main.py +++ b/voice-to-mermaid/backend/main.py @@ -56,7 +56,11 @@ def _load_config() -> dict: try: import yaml # type: ignore[import-untyped] - return yaml.safe_load(_CONFIG_PATH.read_text()) or {} + raw = yaml.safe_load(_CONFIG_PATH.read_text()) + if not isinstance(raw, dict): + log.warning("config.yaml is not a mapping — using defaults") + return {} + return raw except FileNotFoundError: log.warning("config.yaml not found — using defaults") return {} @@ -72,9 +76,9 @@ def _load_config() -> dict: OPENAI_URL = os.environ.get("OPENAI_BASE_URL", "").rstrip("/") OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "") -# Settings — config.yaml (env var override still works if needed) -OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL") or _cfg.get("ollama", {}).get("default_model", "qwen3:8b") -OPENAI_MODEL = os.environ.get("OPENAI_MODEL") or _cfg.get("openai", {}).get("model", "gpt-4o-mini") +# Settings — config.yaml only; edit config.yaml to change these +OLLAMA_MODEL = _cfg.get("ollama", {}).get("default_model", "qwen3:8b") +OPENAI_MODEL = _cfg.get("openai", {}).get("model", "gpt-4o-mini") WHISPER_ENABLED = _cfg.get("whisper", {}).get("enabled", False) WHISPER_MODEL = _cfg.get("whisper", {}).get("model", "medium.en") WHISPER_LANG = _cfg.get("whisper", {}).get("language", "en") @@ -252,15 +256,20 @@ async def _lifespan(app: FastAPI): global _transcription_engine _init_log_file() if WHISPER_ENABLED: - from whisperlivekit import TranscriptionEngine - from whisperlivekit.config import WhisperLiveKitConfig - - log.info("Loading WhisperLiveKit model=%s lang=%s", WHISPER_MODEL, WHISPER_LANG) + try: + from whisperlivekit import TranscriptionEngine + from whisperlivekit.config import WhisperLiveKitConfig + except ImportError: + log.warning("whisper.enabled=true but whisperlivekit not installed — STT disabled. Install requirements.stt.txt to enable.") + yield + return + + log.info("Loading WhisperLiveKit model=%s lang=%s device=%s", WHISPER_MODEL, WHISPER_LANG, WHISPER_DEVICE) _transcription_engine = TranscriptionEngine( config=WhisperLiveKitConfig( model_size=WHISPER_MODEL, lan=WHISPER_LANG, - backend="auto", + backend=WHISPER_DEVICE, backend_policy="simulstreaming", vad=True, vac=False, @@ -272,7 +281,7 @@ async def _lifespan(app: FastAPI): ) log.info("WhisperLiveKit ready") else: - log.info("STT disabled (WHISPER_ENABLED=0) — audio transcription unavailable") + log.info("STT disabled (whisper.enabled=false) — audio transcription unavailable") yield log.info("Server shutting down") From 49ab161eada779457a92ac642547c64ff7d79c8e Mon Sep 17 00:00:00 2001 From: jaewilson07 Date: Tue, 21 Apr 2026 09:12:49 -0600 Subject: [PATCH 4/6] feat: enable whisper STT locally (medium.en, auto device) Co-Authored-By: Claude Sonnet 4.6 --- voice-to-mermaid/backend/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice-to-mermaid/backend/config.yaml b/voice-to-mermaid/backend/config.yaml index 5f40488..00cca39 100644 --- a/voice-to-mermaid/backend/config.yaml +++ b/voice-to-mermaid/backend/config.yaml @@ -18,7 +18,7 @@ openai: model: "gpt-4o-mini" whisper: - enabled: false # set true only when requirements.stt.txt deps are installed + enabled: true model: "medium.en" language: "en" device: "auto" # "auto", "cuda", "cpu" — maps to WhisperLiveKitConfig backend From 4a1b10e68d20639915d651c76c27c14c50453f5a Mon Sep 17 00:00:00 2001 From: jaewilson07 Date: Tue, 21 Apr 2026 09:38:46 -0600 Subject: [PATCH 5/6] fix: _stt_available flag, _config_section() safe reads, correct stt_enabled in /v1/config Address Copilot review on PR #5: - Add _config_section() helper to guard against non-dict config sections - Replace bare _cfg.get("x", {}).get() chains with per-section dicts - Introduce _stt_available flag (False until WhisperLiveKit loads); stt_enabled in /v1/config now reflects actual runtime state instead of config intent Co-Authored-By: Claude Sonnet 4.6 --- voice-to-mermaid/backend/main.py | 43 +++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/voice-to-mermaid/backend/main.py b/voice-to-mermaid/backend/main.py index 8442ff2..1f96c8b 100644 --- a/voice-to-mermaid/backend/main.py +++ b/voice-to-mermaid/backend/main.py @@ -71,21 +71,39 @@ def _load_config() -> dict: _cfg = _load_config() + +def _config_section(name: str) -> dict: + section = _cfg.get(name, {}) + if isinstance(section, dict): + return section + log.warning("config.%s is not a mapping — using defaults for that section", name) + return {} + + +_ollama_cfg = _config_section("ollama") +_openai_cfg = _config_section("openai") +_whisper_cfg = _config_section("whisper") +_paths_cfg = _config_section("paths") + # Secrets — env only OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/") OPENAI_URL = os.environ.get("OPENAI_BASE_URL", "").rstrip("/") OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "") # Settings — config.yaml only; edit config.yaml to change these -OLLAMA_MODEL = _cfg.get("ollama", {}).get("default_model", "qwen3:8b") -OPENAI_MODEL = _cfg.get("openai", {}).get("model", "gpt-4o-mini") -WHISPER_ENABLED = _cfg.get("whisper", {}).get("enabled", False) -WHISPER_MODEL = _cfg.get("whisper", {}).get("model", "medium.en") -WHISPER_LANG = _cfg.get("whisper", {}).get("language", "en") -WHISPER_DEVICE = _cfg.get("whisper", {}).get("device", "auto") -PROMPT_PATH = Path(_cfg.get("paths", {}).get("prompt", "prompts/mermaid.txt")) -LOG_DIR = Path(_cfg.get("paths", {}).get("log_dir", "data/logs")) -_MODEL_FILTER: list[str] = [str(p) for p in (_cfg.get("ollama", {}).get("model_filter") or [])] +OLLAMA_MODEL = _ollama_cfg.get("default_model", "qwen3:8b") +OPENAI_MODEL = _openai_cfg.get("model", "gpt-4o-mini") +WHISPER_ENABLED = _whisper_cfg.get("enabled", False) +WHISPER_MODEL = _whisper_cfg.get("model", "medium.en") +WHISPER_LANG = _whisper_cfg.get("language", "en") +WHISPER_DEVICE = _whisper_cfg.get("device", "auto") +PROMPT_PATH = Path(_paths_cfg.get("prompt", "prompts/mermaid.txt")) +LOG_DIR = Path(_paths_cfg.get("log_dir", "data/logs")) +_MODEL_FILTER: list[str] = [str(p) for p in (_ollama_cfg.get("model_filter") or [])] + +# Set True only after WhisperLiveKit loads successfully at startup. +# WHISPER_ENABLED=True but import fails → _stt_available stays False. +_stt_available: bool = False _ollama_models_cache: list[dict] | None = None @@ -253,7 +271,7 @@ async def _to_mermaid( @asynccontextmanager async def _lifespan(app: FastAPI): - global _transcription_engine + global _transcription_engine, _stt_available _init_log_file() if WHISPER_ENABLED: try: @@ -279,6 +297,7 @@ async def _lifespan(app: FastAPI): pcm_input=False, ) ) + _stt_available = True log.info("WhisperLiveKit ready") else: log.info("STT disabled (whisper.enabled=false) — audio transcription unavailable") @@ -319,8 +338,8 @@ async def config(): "ollama_model": OLLAMA_MODEL, "ollama_models": _ollama_models_cache, "openai_model": OPENAI_MODEL, - "stt_enabled": WHISPER_ENABLED, - "whisper_model": WHISPER_MODEL if WHISPER_ENABLED else None, + "stt_enabled": _stt_available, + "whisper_model": WHISPER_MODEL if _stt_available else None, } From 9c94da5f104c5c695c722402ef8c834015e2099c Mon Sep 17 00:00:00 2001 From: jaewilson07 Date: Tue, 21 Apr 2026 10:05:14 -0600 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20address=20PR=20review=20comments=20?= =?UTF-8?q?=E2=80=94=20clarify=20config.yaml=20ownership,=20clean=20up=20.?= =?UTF-8?q?env.example=20and=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.yaml: add comment explaining whisper.enabled=true is safe (graceful failure via _stt_available) - .env.example: remove OLLAMA_MODEL, WHISPER_MODEL/DEVICE, MERMAID_PROMPT_PATH (moved to config.yaml) - README: split env table into config.yaml section and .env secrets-only section Co-Authored-By: Claude Sonnet 4.6 --- voice-to-mermaid/README.md | 23 ++++++++++++++++++----- voice-to-mermaid/backend/.env.example | 20 ++++---------------- voice-to-mermaid/backend/config.yaml | 4 ++++ 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/voice-to-mermaid/README.md b/voice-to-mermaid/README.md index 6c2b94d..e693236 100644 --- a/voice-to-mermaid/README.md +++ b/voice-to-mermaid/README.md @@ -66,18 +66,31 @@ WebSocket /ws/mermaid --- -## Environment Variables +## Configuration -### Backend (`backend/.env`) +### `backend/config.yaml` — non-secret settings + +Edit this file to change model names, Whisper settings, prompt path, and model filter. +It is volume-mounted in Docker, so changes take effect on restart without rebuilding. + +| Key | Default | Description | +|---|---|---| +| `ollama.default_model` | `qwen3:8b` | Ollama model for diagram generation | +| `ollama.model_filter` | _(list)_ | Model name prefixes shown in the UI picker | +| `openai.model` | `gpt-4o-mini` | OpenAI-compatible model name | +| `whisper.enabled` | `false` | Set `true` after installing `requirements.stt.txt` | +| `whisper.model` | `medium.en` | Whisper model size | +| `whisper.device` | `auto` | `auto` / `cpu` / `cuda` | +| `paths.prompt` | `prompts/mermaid.txt` | Path to LLM prompt template | +| `paths.log_dir` | `data/logs` | Directory for generation logs | + +### Backend (`backend/.env`) — secrets only | Variable | Default | Description | |---|---|---| | `OLLAMA_URL` | `http://localhost:11434` | Ollama server URL | -| `OLLAMA_MODEL` | `qwen2.5:14b` | Model for diagram generation | | `OPENAI_BASE_URL` | _(empty)_ | OpenAI-compatible API base (optional) | | `OPENAI_API_KEY` | _(empty)_ | API key for OpenAI-compatible API | -| `WHISPER_MODEL` | `medium` | `tiny` / `base` / `small` / `medium` / `large-v3` | -| `WHISPER_DEVICE` | `auto` | `auto` / `cpu` / `cuda` | | `API_KEY` | _(empty)_ | If set, require `X-Api-Key` header on all requests | | `PORT` | `7625` | Server port | diff --git a/voice-to-mermaid/backend/.env.example b/voice-to-mermaid/backend/.env.example index b30b093..1ac6706 100644 --- a/voice-to-mermaid/backend/.env.example +++ b/voice-to-mermaid/backend/.env.example @@ -1,31 +1,19 @@ # voice-to-mermaid backend — copy to .env and fill in +# +# Non-secret settings (model names, Whisper config, prompt path, model filter) +# are in config.yaml — edit that file instead of adding them here. -# ── LLM backend (pick one) ──────────────────────────────────────────────── +# ── LLM backend URLs (secrets — keep out of config.yaml) ───────────────── # Option A: Ollama (local, recommended for self-hosting) OLLAMA_URL=http://localhost:11434 -OLLAMA_MODEL=qwen2.5:14b -# Comma-separated model name prefixes shown in the picker (empty = show all) -OLLAMA_MODEL_FILTER=qwen2.5:,qwen3.5: # Option B: OpenAI-compatible API (vLLM, OpenAI, Together, etc.) # OPENAI_BASE_URL=https://api.openai.com/v1 # OPENAI_API_KEY=sk-... -# OPENAI_MODEL=gpt-4o-mini - -# ── Whisper (transcription) ─────────────────────────────────────────────── -# Model size: tiny | base | small | medium | large-v3 -# Device: auto | cpu | cuda -WHISPER_MODEL=medium -WHISPER_DEVICE=auto # ── Server ──────────────────────────────────────────────────────────────── PORT=7625 # Shared secret — Next.js proxy sends this as X-Api-Key header. # Leave blank to disable auth (dev only). API_KEY= - -# ── Prompts ─────────────────────────────────────────────────────────────── -# Path to mermaid prompt template (relative to this file). -# Edit prompts/mermaid.txt to customise diagram generation behaviour. -MERMAID_PROMPT_PATH=prompts/mermaid.txt diff --git a/voice-to-mermaid/backend/config.yaml b/voice-to-mermaid/backend/config.yaml index 00cca39..9acf4ae 100644 --- a/voice-to-mermaid/backend/config.yaml +++ b/voice-to-mermaid/backend/config.yaml @@ -18,6 +18,10 @@ openai: model: "gpt-4o-mini" whisper: + # true = attempt to load WhisperLiveKit at startup. + # If import fails (deps not installed), _stt_available stays False and + # /v1/config returns stt_enabled: false — no crash. + # Install requirements.stt.txt (or add it to Dockerfile) before setting true. enabled: true model: "medium.en" language: "en"