diff --git a/voice-to-mermaid/backend/config.yaml b/voice-to-mermaid/backend/config.yaml index 199cae0..5f40488 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: false # set true only when requirements.stt.txt deps are installed + model: "medium.en" + language: "en" + device: "auto" # "auto", "cuda", "cpu" — maps to WhisperLiveKitConfig backend + +paths: + prompt: "prompts/mermaid.txt" + log_dir: "data/logs" 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 diff --git a/voice-to-mermaid/backend/main.py b/voice-to-mermaid/backend/main.py index 01d80f0..8442ff2 100644 --- a/voice-to-mermaid/backend/main.py +++ b/voice-to-mermaid/backend/main.py @@ -47,39 +47,47 @@ 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 [])] + 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: - 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 {} -_MODEL_FILTER: list[str] = _load_model_filter() +_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 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_models_cache: list[dict] | None = None async def _fetch_ollama_models() -> list[dict]: @@ -248,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, @@ -268,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")