diff --git a/docker-compose.yml b/docker-compose.yml index 9907dbf..414725d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,6 +79,18 @@ services: STT_LANGUAGE: ${STT_LANGUAGE:-ko} VOICE_SILENCE_MS: ${VOICE_SILENCE_MS:-600} BRIDGE_URL: http://127.0.0.1:8765 + # Split-deployment role: full (default, all-in-one), browser (only the + # desktop + Chrome + CDP, reused over the LAN), or bot (only bot + bridge + # + TTS, driving a remote browser via CDP_HOST). See docker/run-if-role.sh. + JARVIS_ROLE: ${JARVIS_ROLE:-full} + # Chrome CDP bind address INSIDE the container. 0.0.0.0 lets a remote bot + # (JARVIS_ROLE=bot on another PC) drive this host's browser. Loopback by + # default so the all-in-one layout stays unreachable off-host. + CDP_BIND: ${CDP_BIND:-127.0.0.1} + CDP_PORT: ${CDP_PORT:-9222} + # Where the bot drives Chrome. Loopback for full/browser; on a remote bot + # set CDP_HOST to the browser host's LAN IP (e.g. 192.168.10.9). + CDP_HOST: ${CDP_HOST:-127.0.0.1} depends_on: - ollama # GPU: accelerates Whisper STT (and anything else CUDA) in this container. @@ -95,6 +107,9 @@ services: # .env pins VNC_PORT=5902. - "${VNC_BIND:-127.0.0.1}:${VNC_PORT:-5901}:5901" # VNC - "${VNC_BIND:-127.0.0.1}:${NOVNC_PORT:-6080}:6080" # noVNC (browser) + # Chrome CDP for a remote bot (JARVIS_ROLE=bot). Loopback by default; for a + # LAN browser-host set CDP_PUBLISH_BIND=0.0.0.0 (internal network, no auth). + - "${CDP_PUBLISH_BIND:-127.0.0.1}:${CDP_PORT:-9222}:9222" # Chrome CDP # The brain bridge is NOT published: it binds the container's loopback # (BRIDGE_HOST=127.0.0.1) and is only consumed by the bot in this same # container, so it needs no host port and stays unreachable off-container. diff --git a/docker/run-chrome.sh b/docker/run-chrome.sh index d1d5e05..76f527a 100755 --- a/docker/run-chrome.sh +++ b/docker/run-chrome.sh @@ -19,7 +19,7 @@ exec google-chrome \ --disable-translate \ --disable-features=Translate,TranslateUI,InfoBars \ --remote-debugging-port="${CDP_PORT:-9222}" \ - --remote-debugging-address=127.0.0.1 \ + --remote-debugging-address="${CDP_BIND:-127.0.0.1}" \ --user-data-dir="${CHROME_PROFILE_DIR:-/root/chrome-profile}" \ --password-store=basic --start-maximized \ "${CHROME_START_URL:-about:blank}" diff --git a/docker/run-if-role.sh b/docker/run-if-role.sh new file mode 100755 index 0000000..589c1e3 --- /dev/null +++ b/docker/run-if-role.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Role guard for split deployments. +# +# run-if-role.sh +# +# Runs only when JARVIS_ROLE is one of (or "full"/unset). +# Otherwise it idles so supervisord keeps the program slot "running" without +# doing any work. This lets ONE image serve three layouts: +# +# JARVIS_ROLE=full (default) everything in one container +# JARVIS_ROLE=browser only the desktop + Chrome + CDP (reused over the LAN) +# JARVIS_ROLE=bot only the bot + bridge + TTS (drives a remote browser +# via CDP_HOST/CDP_PORT) +set -e +want="$1"; shift +role="${JARVIS_ROLE:-full}" +if [ "$role" = "full" ]; then exec "$@"; fi +case ",$want," in + *",$role,"*) exec "$@" ;; +esac +echo "[role-guard] JARVIS_ROLE=$role not in '$want' — idling: $*" >&2 +exec sleep infinity diff --git a/docker/supervisord.conf b/docker/supervisord.conf index c2e8ea7..5ea64fb 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -14,7 +14,7 @@ serverurl=unix:///run/supervisor.sock supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [program:xvnc] -command=/app/docker/run-xvnc.sh +command=/app/docker/run-if-role.sh full,browser /app/docker/run-xvnc.sh priority=100 autorestart=true stdout_logfile=/dev/stdout @@ -23,7 +23,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:pulse] -command=/app/docker/run-pulse.sh +command=/app/docker/run-if-role.sh full,browser /app/docker/run-pulse.sh priority=150 autorestart=true stdout_logfile=/dev/stdout @@ -32,7 +32,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:xfce] -command=/app/docker/run-xfce.sh +command=/app/docker/run-if-role.sh full,browser /app/docker/run-xfce.sh priority=200 autorestart=true stdout_logfile=/dev/stdout @@ -41,7 +41,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:novnc] -command=websockify --web=/usr/share/novnc 6080 localhost:5901 +command=/app/docker/run-if-role.sh full,browser websockify --web=/usr/share/novnc 6080 localhost:5901 priority=250 autorestart=true stdout_logfile=/dev/stdout @@ -52,7 +52,7 @@ stderr_logfile_maxbytes=0 [program:melo-worker] ; Warm MeloTTS Korean voice (speed 1.5) in its own py3.11 venv. The bridge's ; synthesize() POSTs here; if this is down the bridge falls back to Piper. -command=/opt/melo/bin/python /app/bridge/melo_worker.py +command=/app/docker/run-if-role.sh full,bot /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 brain's whisper_cache volume is mounted over @@ -73,7 +73,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:bridge] -command=/opt/venv/bin/python -m bridge.server +command=/app/docker/run-if-role.sh full,bot /opt/venv/bin/python -m bridge.server directory=/app priority=300 autorestart=true @@ -83,7 +83,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:chrome] -command=/app/docker/run-chrome.sh +command=/app/docker/run-if-role.sh full,browser /app/docker/run-chrome.sh priority=350 autorestart=true stdout_logfile=/dev/stdout @@ -92,7 +92,7 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:bot] -command=/app/docker/run-bot.sh +command=/app/docker/run-if-role.sh full,bot /app/docker/run-bot.sh directory=/app/bot priority=400 autorestart=true