# Mavéa — local speech services. Works with Podman Compose or Docker Compose.
#
#   pnpm dev               →  starts this container AND the dev server (the normal way in)
#   podman compose up -d   →  Kokoro TTS on :8880 + whisper.cpp STT on :8100
#
# Everything else runs on the host: `pnpm dev` serves the app at http://localhost:5173 and
# proxies /tts → localhost:8880, so the browser never talks to Kokoro directly (no CORS).
# Without these containers Mavéa still works through typing and written captions; microphone
# transcription stays unavailable rather than sending speech to a browser-vendor service.
# The optional actions gateway is plain Node (`pnpm actions`), no container needed.

services:
  # Kokoro TTS — OpenAI-compatible /v1/audio/speech on :8880. Mavéa's only voice. The wrapper and
  # Kokoro-82M weights are Apache-2.0. The separately pulled image also contains GPL-3.0-or-later
  # eSpeak NG; commercial use is allowed, and Mavéa neither bundles nor links that separate service.
  # CPU image; the first-run model download means the first few lines may be silent
  # until it's ready — speak() no-ops gracefully until then.
  kokoro:
    image: ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4@sha256:c8812546d358cbfd6a5c4087a28795b2b001d8e32d7a322eedd246e6bc13cb55
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    pids_limit: 256
    environment:
      # How many cores the voice is allowed. Left to itself, torch sizes its pool from
      # hardware_concurrency() and takes every one — past the peak of this model's curve. Measured
      # end-to-end on an 8-vCPU VM, one clause of speech: 4 threads renders at 4.23x realtime on
      # 387% CPU, 8 threads at 4.01x on 543%. Slower AND a third more CPU, because an 82M-param
      # model spends the extra threads in OpenMP barriers rather than matmuls.
      #
      # 4 is the safe default: it is the measured peak, and a host with 4 cores or fewer already
      # picks 4 or fewer on its own, so it only binds where the collapse lives. But the right number
      # is a property of the machine, not of this file — synthesis only has to outrun a playhead
      # moving at 1x, and a fast machine clears that on 2 threads while a slow one needs every core
      # it has. `pnpm dev` measures this box once and sets the value; see scripts/dev.mjs.
      OMP_NUM_THREADS: ${MAVEA_VOICE_THREADS:-4}
    ports:
      - '127.0.0.1:8880:8880'

  # whisper.cpp v1.9.1 — MIT-licensed local transcription with the MIT-licensed quantized
  # small.en model. The source archive and model are checksum-verified before they execute.
  whisper:
    image: mavea-whisper-cpp:1.9.1
    build:
      context: .
      dockerfile: voice/whisper.Dockerfile
    restart: unless-stopped
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    pids_limit: 256
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=64m
    environment:
      # whisper.cpp takes --threads literally: 4 here is 4 threads on a two-core laptop as surely
      # as on a sixteen-core desktop, and transcription competes for the same cores the browser is
      # rendering on. There is nothing to measure the way synthesis is measured — transcription has
      # no playhead to outrun — so `pnpm dev` (and `npx mavea`) simply bound it by the cores this
      # box actually has; see scripts/dev.mjs.
      MAVEA_STT_THREADS: ${MAVEA_STT_THREADS:-4}
    volumes:
      - whisper-models:/models
    ports:
      - '127.0.0.1:8100:8080'

volumes:
  whisper-models:
