From 844c929fdbce3e425d334b162621f54c3e734357 Mon Sep 17 00:00:00 2001 From: jaewilson07 Date: Sun, 19 Apr 2026 23:04:56 -0600 Subject: [PATCH 1/3] 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/3] 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/3] =?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")