# speaches — OpenAI-compatible ASR (faster-whisper / CTranslate2) on irv-ml1. # # Consumer: Eyra (meeting recorder). Its hallucination gate keys on the # per-segment `no_speech_prob` field returned by `response_format=verbose_json`, # so that field surviving the API boundary is LOAD-BEARING, not cosmetic. # # API: # POST /v1/audio/transcriptions — multipart; response_format=verbose_json # GET /v1/models — registry of available STT models # GET /health # # Coexists with the `parakeet` stack (:8765). Parakeet is a TDT/transducer model # returning a bare {"text": ...} — it has no `no_speech_prob` concept at all, so # it cannot serve this consumer. Two ASR services on one box is deliberate. # # All tunables live in .env — edit that, not this file. services: speaches: image: ${SPEACHES_IMAGE} container_name: speaches restart: unless-stopped ports: - "${SPEACHES_BIND:-0.0.0.0}:${SPEACHES_PORT}:8000" environment: - UVICORN_HOST=0.0.0.0 - UVICORN_PORT=8000 - LOG_LEVEL=${SPEACHES_LOG_LEVEL:-info} # ── VAD: PINNED OFF at the consumer's explicit request ────────────── # Eyra VAD-gates upstream on the capture edge and sends only speech # segments. A second VAD here would re-chunk the audio and therefore # shift what `no_speech_prob` MEANS per segment, underneath a gate # calibrated against this stack. # ⚠ The upstream default is True, and the variable name is explicitly # marked unstable (leading underscore). That is exactly why # SPEACHES_IMAGE is pinned to a digest below — an unpinned bump could # silently rename this var, restoring VAD and moving the gate. - _UNSTABLE_VAD_FILTER=${SPEACHES_VAD_FILTER:-False} # ── Whisper / CTranslate2 ─────────────────────────────────────────── # float16: irv-ml1 is Ampere (sm_86) — no native FP8/NVFP4, fp16 is the # correct compute type here. device_index 0 is the index INSIDE the # container, which the device_ids pin below maps to the A6000. - WHISPER__INFERENCE_DEVICE=cuda - WHISPER__DEVICE_INDEX=0 - WHISPER__COMPUTE_TYPE=${SPEACHES_COMPUTE_TYPE:-float16} - WHISPER__NUM_WORKERS=${SPEACHES_NUM_WORKERS:-1} # ── Residency ─────────────────────────────────────────────────────── # -1 = never unload. Deliberate: a mid-meeting model reload would be a # multi-second stall on a latency-sensitive draft-caption tier. Both # tiers resident is ~4.6 GB against 26 GB free on the A6000. Set to 300 # (the upstream default) to trade that latency back for VRAM if the card # gets tight — e.g. when Eyra's diarization workload lands here. - STT_MODEL_TTL=${SPEACHES_STT_MODEL_TTL:--1} - ENABLE_UI=${SPEACHES_ENABLE_UI:-true} - PRELOAD_MODELS=${SPEACHES_PRELOAD_MODELS} - HF_HOME=/home/ubuntu/.cache/huggingface volumes: # Model cache persists across recreates so a bounce is not a re-download. - ${SPEACHES_CACHE_DIR}:/home/ubuntu/.cache/huggingface deploy: resources: reservations: devices: - driver: nvidia # "1" = RTX A6000 (verified empirically — nvidia-smi index 1; # note native CUDA on this host enumerates the A6000 as 0, the # docker view is what matters here). "0" would be the 3090, # which already hosts parakeet + the TTS stacks. device_ids: ["${SPEACHES_GPU_ID:-1}"] capabilities: [gpu] healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:8000/health || exit 1"] interval: 30s timeout: 10s retries: 3 # First boot downloads ~4.6 GB of CT2 weights for the two preloaded tiers. start_period: 600s labels: - homepage.group=AI - Audio Tools - homepage.name=Speaches ASR - homepage.icon=mdi-text-to-speech - homepage.description=OpenAI-compatible faster-whisper STT, verbose_json (irv-ml1) - homepage.href=http://10.100.79.3:${SPEACHES_PORT}