diff --git a/.env.example b/.env.example index 67b57c4..197d72b 100644 --- a/.env.example +++ b/.env.example @@ -36,12 +36,20 @@ WHISPER_DEVICE=cuda WHISPER_COMPUTE_TYPE=float16 # Optional explicit Piper voice model (.onnx). If empty, the jarvis default is used. TTS_PIPER_MODEL_PATH= -# TTS engine: "edge" (default) uses Microsoft Edge TTS, a natural Korean neural -# voice. Set to "piper" for the offline English voice. NOTE: edge is ONLINE — -# reply text is sent to Microsoft's servers and needs internet. -TTS_ENGINE=edge -# Edge voice + speaking rate. Rate is a percentage (+45% ≈ 1.45×). Korean voices: -# ko-KR-HyunsuMultilingualNeural (M), ko-KR-InJoonNeural (M), ko-KR-SunHiNeural (F). +# TTS engine: +# melo (default) GPU MeloTTS — offline Korean voice, privacy-first (text never +# leaves the host), synthesises on the GPU via the melo-worker. Needs a +# GPU (MELO_DEVICE=cuda); falls back to CPU (slow) with MELO_DEVICE=cpu. +# edge Microsoft Edge TTS — natural Korean neural voice, but ONLINE: reply +# text is sent to Microsoft's servers and needs internet. +# piper offline English voice (fallback). +TTS_ENGINE=melo +# GPU MeloTTS knobs (used when TTS_ENGINE=melo). +MELO_DEVICE=cuda +MELO_SPEED=1.5 +# Edge voice + speaking rate (used when TTS_ENGINE=edge). Rate is a percentage +# (+45% ≈ 1.45×). Korean voices: ko-KR-HyunsuMultilingualNeural (M), +# ko-KR-InJoonNeural (M), ko-KR-SunHiNeural (F). EDGE_TTS_VOICE=ko-KR-HyunsuMultilingualNeural EDGE_TTS_RATE=+45% # Neural-only by default: if synthesis fails the bridge returns no audio rather diff --git a/Dockerfile b/Dockerfile index fd19fdb..cba935f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,10 +65,19 @@ RUN ls -d /opt/venv/lib/python*/site-packages/nvidia/cublas/lib \ > /etc/ld.so.conf.d/nvidia-cu12.conf 2>/dev/null \ && /sbin/ldconfig || true -# --- Korean voice: Microsoft Edge TTS (online neural). No model is baked — the -# `edge-tts` pip package (in requirements-bridge.txt) calls the MS service at -# runtime and the bridge transcodes the MP3 to PCM16 with ffmpeg. No heavy -# TTS build layer is needed. --- +# --- Korean voice: GPU MeloTTS (offline, privacy-first) in a separate /opt/melo +# py3.11 venv; see docker/setup-melo.sh. Heavy layer (torch cu128 GPU + +# transformers + MeCab + baked KR model), placed before the app COPY so it +# stays cached across source-only changes. Runtime device is MELO_DEVICE=cuda +# (compose), so TTS synthesises on the GPU next to Ollama + Whisper. Started +# only in full/bot/brain roles (see docker/supervisord.conf). The offline +# Edge TTS (`edge-tts`, in requirements-bridge.txt) remains available as an +# opt-in fallback via TTS_ENGINE=edge, but it is ONLINE (text leaves the +# host), so MeloTTS is the default. --- +COPY docker/setup-melo.sh /app/docker/setup-melo.sh +# Strip CR first: a Windows (autocrlf) checkout yields CRLF, which makes bash +# read `set -euxo pipefail\r` and abort. Idempotent on an LF tree. +RUN sed -i 's/\r$//' /app/docker/setup-melo.sh && bash /app/docker/setup-melo.sh # --- Human input + window management for the on-screen Chrome control tool. # xdotool injects real X pointer/keyboard events (visible cursor, diff --git a/docker-compose.yml b/docker-compose.yml index 7ab9daa..859846c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,11 +87,17 @@ services: WHISPER_DEVICE: ${WHISPER_DEVICE:-cuda} WHISPER_COMPUTE_TYPE: ${WHISPER_COMPUTE_TYPE:-float16} # TTS engine. Rendered into /app/config/jarvis.json via envsubst (the - # bridge reads that JSON BEFORE the env, so it must carry the real engine, - # not a hardcoded one — otherwise Korean text is read by the English Piper - # voice). Default edge; .env can override (e.g. piper for offline). - TTS_ENGINE: ${TTS_ENGINE:-edge} - # Edge TTS voice + rate (the chosen natural Korean voice). NOTE: edge is an + # bridge reads that JSON BEFORE the env, so it must carry the real engine). + # Default melo: GPU MeloTTS (offline, privacy-first) via the melo-worker. + # Override with TTS_ENGINE=edge for the online Microsoft neural voice (text + # leaves the host) or piper for the offline English voice. + TTS_ENGINE: ${TTS_ENGINE:-melo} + # GPU MeloTTS worker knobs. MELO_DEVICE=cuda runs synth on the GPU (next to + # Ollama + Whisper); cpu is the no-GPU fallback. MELO_SPEED must always be + # set — supervisord's melo-worker expands %(ENV_MELO_SPEED)s at startup. + MELO_DEVICE: ${MELO_DEVICE:-cuda} + MELO_SPEED: ${MELO_SPEED:-1.5} + # Edge TTS voice + rate (used only when TTS_ENGINE=edge). NOTE: edge is an # ONLINE engine — reply text is sent to Microsoft and needs internet. EDGE_TTS_VOICE: ${EDGE_TTS_VOICE:-ko-KR-HyunsuMultilingualNeural} EDGE_TTS_RATE: ${EDGE_TTS_RATE:-+45%} diff --git a/docker/supervisord.conf b/docker/supervisord.conf index dab8639..766b9f5 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -49,8 +49,27 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -# (No TTS worker program: the default Edge TTS engine synthesises in-process in -# the bridge via the `edge-tts` package — no warm model/worker is needed.) +[program:melo-worker] +; Warm GPU MeloTTS Korean voice (offline) in its own py3.11 venv (/opt/melo). The +; bridge's synthesize() POSTs here when TTS_ENGINE=melo. Runs wherever the bridge +; runs (full/bot/brain) so a GPU brain host serves TTS on the GPU. If TTS_ENGINE +; =edge the bridge synthesises in-process and never calls this worker. +command=/app/docker/run-if-role.sh full,bot,brain /opt/melo/bin/python /app/bridge/melo_worker.py +directory=/app +; HF_HOME points at the dedicated, image-baked melo cache (warmed in +; setup-melo.sh). The whisper_cache volume is mounted over /root/.cache/huggingface, +; so without this the pre-cached BERT + KR checkpoint would be shadowed and +; re-downloaded (and would fail offline). HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE +; force pure-cache reads. MELO_DEVICE/MELO_SPEED inherit from the container env +; (compose sets both with defaults cuda / 1.5), so MELO_SPEED must always be set +; in the env or this %(ENV_x)s expansion fails at startup (compose guarantees it). +environment=MELO_LANGUAGE="KR",MELO_SPEED="%(ENV_MELO_SPEED)s",MELO_DEVICE="%(ENV_MELO_DEVICE)s",MELO_WORKER_HOST="127.0.0.1",MELO_WORKER_PORT="8770",HF_HOME="/opt/melo-cache",HF_HUB_OFFLINE="1",TRANSFORMERS_OFFLINE="1" +priority=280 +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 [program:bridge] command=/app/docker/run-if-role.sh full,bot,brain /opt/venv/bin/python -m bridge.server diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index d874cbf..535dad3 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -100,13 +100,16 @@ BRIDGE_HOST=0.0.0.0 # bridge listens on the LAN SETTINGS_PUBLISH_BIND=0.0.0.0 # publish bridge port 8765 to the LAN OLLAMA_BASE_URL=http://192.168.10.9:11434 # Ollama on this host (bind 0.0.0.0) OLLAMA_CHAT_MODEL=qwen2.5:7b -WHISPER_DEVICE=cuda +WHISPER_DEVICE=cuda # STT on the GPU WHISPER_COMPUTE_TYPE=float16 +TTS_ENGINE=melo # GPU MeloTTS (offline, privacy-first) +MELO_DEVICE=cuda # TTS on the GPU (melo-worker) BROWSER_CONTROL_URL=http://192.168.10.5:8777 # drive the app host's Chrome COMPOSE_FILE=docker-compose.yml:docker-compose.gpu-linux.yml # GPU (":" on Linux) -# Ollama runs on this host (systemd or a container) listening on 0.0.0.0:11434. -docker compose up -d javis # brain bridge only (no bot, no desktop) +# STT (Whisper), LLM (Ollama) and TTS (MeloTTS) all run on this host's GPU. +# Ollama listens on 0.0.0.0:11434 (systemd or a container). +docker compose up -d javis # brain bridge + melo-worker only (no bot, no desktop) ``` ### App host (.5 — bot + browser, no GPU)