Skip to content
This repository was archived by the owner on Apr 24, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions voice-to-mermaid/backend/config.yaml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions voice-to-mermaid/backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ services:
ollama:
image: ollama/ollama:latest
container_name: vtm-ollama
restart: unless-stopped
ports:
- "11434:11434"
volumes:
Expand All @@ -18,6 +19,7 @@ services:
api:
build: .
container_name: vtm-api
restart: unless-stopped
ports:
- "${PORT:-7625}:7625"
env_file: .env
Expand Down
67 changes: 40 additions & 27 deletions voice-to-mermaid/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Comment on lines +56 to +69

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_config() returns the raw result of yaml.safe_load(), but later code assumes _cfg is a dict (calls _cfg.get(...)). If config.yaml is edited into a non-mapping YAML value (e.g., a list/string), the app will crash at import time with AttributeError. Consider validating the loaded value is a dict and otherwise falling back to {} with a warning.

Copilot uses AI. Check for mistakes.


_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")

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHISPER_DEVICE is loaded from config.yaml but never used elsewhere in this module. Either wire it into the WhisperLiveKit configuration (so the setting actually takes effect) or remove it to avoid misleading configuration surface area.

Suggested change
WHISPER_DEVICE = _cfg.get("whisper", {}).get("device", "auto")

Copilot uses AI. Check for mistakes.
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]:
Expand Down Expand Up @@ -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,
Expand All @@ -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")

Expand Down
Loading