diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c10026e..5f67fea 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -49,10 +49,10 @@ body: label: "Environment" description: "Your setup details." placeholder: | - - OS: macOS 15 / Ubuntu 24.04 / Raspberry Pi OS + - OS: macOS 15 / Ubuntu 24.04 / Debian 12 / Raspberry Pi OS - Python version: 3.11.x - Node version: 20.x - - Deployment mode: local dev / Pi production / other + - Deployment mode: local dev / Linux-host production / other - VocalizeAI version / commit hash: validations: required: true @@ -61,7 +61,7 @@ body: id: logs attributes: label: "Relevant logs" - description: "Paste any relevant log output. Use `journalctl -u vocalize` on Pi, or the uvicorn terminal output locally." + description: "Paste any relevant log output. Use `journalctl -u vocalize` on a systemd-installed host, or the uvicorn terminal output locally." render: shell validations: required: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4341644..b8b30e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ cd frontend && npm run test:integration ``` Note: `tests/integration/` release-audio cases require a physical audio setup -(microphone + speaker) and a live Pi orchestrator. These are gated behind +(microphone + speaker) and a live Linux-host orchestrator. These are gated behind `--release-audio` and do not run in PR CI. All checks must pass on your PR before merge. CI runs lint (ruff + mypy + tsc), diff --git a/README.md b/README.md index ed2aec1..a74d0da 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ across languages when needed. ## Current Status -**v1 ships** the universal phone-task engine, Web console, and Raspberry Pi -orchestrator deployment. The backend 5-layer prompt architecture +**v1 ships** the universal phone-task engine, Web console, and a Linux-host +orchestrator deployment (Raspberry Pi was the original reference target; +any modern Linux host with systemd works). The backend 5-layer prompt architecture (task_planner / preflight / merchant_agent / clarification_collector / relay) handles any phone task — restaurant bookings, service appointments, balance inquiries, status checks, and more. An OSS mirror is available at @@ -91,12 +92,12 @@ VocalizeAI/ │ ├── messages/ # next-intl zh/en bundles │ └── tests/ # vitest unit tests ├── demos/ # runnable demos -├── infra/ # deployment scripts (GPU node, Pi orchestrator) +├── infra/ # deployment scripts (GPU node, Linux orchestrator) ├── tests/ # pytest suite │ └── integration/ # Playwright laptop-loopback + AI-merchant harness ├── install/ # one-shot install scripts │ ├── dev-install.sh # Mac/Linux local dev setup -│ └── pi-install.sh # Raspberry Pi production deploy +│ └── install.sh # Linux production deploy (Raspberry Pi is one example target) ├── docs/ # architecture, deploy guides, release evidence ├── scripts/ # smoke test and utility scripts │ └── smoke.sh # post-install end-to-end verification @@ -117,13 +118,14 @@ VocalizeAI/ See `.env.example` for the full env-var inventory including LLM, GPU service, and frontend build-time variables. -For the full production Pi deployment runbook, see [docs/deploy/pi.md](docs/deploy/pi.md). +For the full Linux-host production deployment runbook (Raspberry Pi is one +example target), see [docs/deploy/linux.md](docs/deploy/linux.md). ### GPU node requirements SenseVoice (STT) and CosyVoice (TTS) run as separate GPU services and connect -to the Pi orchestrator over Tailscale. GPU services are optional for local dev -(the LLM path works without them). See [docs/deploy/pi.md](docs/deploy/pi.md) +to the orchestrator host over Tailscale. GPU services are optional for local dev +(the LLM path works without them). See [docs/deploy/linux.md](docs/deploy/linux.md) for the GPU node setup. ## Run the dev server diff --git a/README.zh-CN.md b/README.zh-CN.md index 570dcf1..2382df1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -85,12 +85,12 @@ VocalizeAI/ │ ├── messages/ # next-intl zh/en bundles │ └── tests/ # vitest unit tests ├── demos/ # runnable demos -├── infra/ # deployment scripts (GPU node, Pi orchestrator) +├── infra/ # 部署脚本(GPU 节点、Linux 编排器) ├── tests/ # pytest suite │ └── integration/ # Playwright laptop-loopback + AI-merchant harness ├── install/ # 一键安装脚本 │ ├── dev-install.sh # Mac/Linux 本地开发环境安装 -│ └── pi-install.sh # 树莓派生产部署安装 +│ └── install.sh # Linux 生产部署安装(树莓派是一种受支持的目标) ├── docs/ # 架构文档、部署指南、发布记录 ├── scripts/ # smoke 测试和工具脚本 │ └── smoke.sh # 安装后端到端验证脚本 @@ -110,13 +110,13 @@ VocalizeAI/ 完整环境变量清单(含 LLM、GPU 服务、前端构建变量)见 `.env.example`。 -完整的树莓派生产部署手册,见 [docs/deploy/pi.md](docs/deploy/pi.md)。 +完整的树莓派生产部署手册,见 [docs/deploy/linux.md](docs/deploy/linux.md)。 ### GPU 节点要求 SenseVoice(STT)和 CosyVoice(TTS)作为独立 GPU 服务运行,通过 Tailscale 与树莓派编排器连接。本地开发不需要 GPU(只需 LLM 路径即可运行)。GPU 节点配置见 -[docs/deploy/pi.md](docs/deploy/pi.md)。 +[docs/deploy/linux.md](docs/deploy/linux.md)。 ## 跑开发服务器 diff --git a/docs/architecture.md b/docs/architecture.md index b080669..01f130c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -556,7 +556,7 @@ See: `src/vocalize/server/ws.py`, `frontend/lib/audio*`, `frontend/components/Br | Env/config loading | `src/vocalize/config.py` | | Asyncio main pipeline | `src/vocalize/pipeline.py` | | Frontend (Next.js 14) | `frontend/` | -| Pi deployment assets | `infra/pi-orchestrator/` | +| Pi deployment assets | `infra/orchestrator/` | | GPU services setup | `infra/gpu-services/` | | Backend tests (pytest) | `tests/` | | Integration tests (Playwright) | `tests/integration/` | @@ -611,5 +611,5 @@ See: `src/vocalize/server/ws.py`, `src/vocalize/server/sessions.py` ## Further Reading - **[docs/deploy/local.md](docs/deploy/local.md)** — Mac/Linux dev environment setup and env-var reference -- **[docs/deploy/pi.md](docs/deploy/pi.md)** — End-to-end Pi production deployment runbook +- **[docs/deploy/linux.md](docs/deploy/linux.md)** — End-to-end Pi production deployment runbook - **[CONTRIBUTING.md](../CONTRIBUTING.md)** — Contributor flow, code style, commit conventions diff --git a/docs/deploy/pi.md b/docs/deploy/linux.md similarity index 72% rename from docs/deploy/pi.md rename to docs/deploy/linux.md index 1a56e20..a44a91a 100644 --- a/docs/deploy/pi.md +++ b/docs/deploy/linux.md @@ -1,57 +1,62 @@ -# Deploying VocalizeAI on a Raspberry Pi +# Deploying VocalizeAI on a Linux Host -This runbook covers end-to-end production deployment of VocalizeAI on a -Raspberry Pi: the orchestrator runs on the Pi; GPU services (SenseVoice STT + -CosyVoice TTS) run on a separate machine reachable over Tailscale; a Cloudflare -Tunnel fronts the Pi to the public internet. +This runbook covers end-to-end production deployment of VocalizeAI on any +modern Linux host with systemd: the orchestrator runs on this host, GPU +services (SenseVoice STT + CosyVoice TTS) run on a separate machine reachable +over Tailscale, and a Cloudflare Tunnel fronts the orchestrator host to the +public internet. + +Tested on **Debian 12**, **Ubuntu 22.04 / 24.04**, and **Raspberry Pi OS +(Bookworm)**. A Raspberry Pi was the original reference target — see +["Hardware example: Raspberry Pi"](#hardware-example-raspberry-pi) below for +the BOM, OS imaging, and SSH bootstrap steps for that specific target. --- -## Hardware Bill of Materials +## Bill of Materials -**Raspberry Pi:** -- Raspberry Pi 4 or Pi 5, **8 GB RAM recommended** (4 GB works for the orchestrator - alone but is tight if other services run alongside) -- 32 GB+ microSD card or USB SSD (SSD strongly recommended for production) -- Reliable internet connection (Cloudflare Tunnel requires outbound HTTPS) +**Orchestrator host:** +- Any 64-bit Linux box with systemd, ≥ 2 GB RAM, ≥ 16 GB free disk. +- Python 3.11 (installed by step 1 of `install/install.sh`). +- Persistent internet connection (Cloudflare Tunnel requires outbound HTTPS). **GPU node (separate machine):** -- NVIDIA RTX-class GPU (GTX 1080 or better; RTX 30/40 series recommended) -- Windows + WSL2 or Linux (PyTorch 2.7.1+cu128) -- Reachable from the Pi over Tailscale on the configured `GPU_HOST` IP/hostname +- NVIDIA RTX-class GPU (GTX 1080 or better; RTX 30/40 series recommended). +- Windows + WSL2 or Linux (PyTorch 2.7.1+cu128). +- Reachable from the orchestrator host over Tailscale on the configured + `GPU_HOST` IP/hostname. **Network:** -- Tailscale account (free tier is sufficient) with both the Pi and GPU node enrolled -- Cloudflare account with a domain pointed at Cloudflare DNS (free tier is sufficient) +- Tailscale account (free tier is sufficient) with both the orchestrator host + and the GPU node enrolled. +- Cloudflare account with a domain pointed at Cloudflare DNS (free tier is + sufficient). --- ## OS Preparation ```bash -# Flash Raspberry Pi OS Lite (64-bit) to the SD card / SSD using Raspberry Pi Imager. -# In Imager, pre-configure: -# - hostname -# - SSH enabled -# - SSH public key (paste your ~/.ssh/id_ed25519.pub or generate one first) -# - Wi-Fi credentials (if not using Ethernet) - -# After first boot, SSH in and update the system: -ssh pi@ +# On the orchestrator host (any modern 64-bit Linux with systemd): +ssh @ sudo apt-get update && sudo apt-get upgrade -y # Ensure git and curl are present: sudo apt-get install -y git curl ``` +For the Raspberry Pi-specific imaging / first-boot steps, see +["Hardware example: Raspberry Pi"](#hardware-example-raspberry-pi). + --- ## Tailscale Setup -Tailscale provides the encrypted overlay network between the Pi and the GPU node. +Tailscale provides the encrypted overlay network between the orchestrator +host and the GPU node. ```bash -# Install Tailscale on the Pi: +# Install Tailscale on the orchestrator host: curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up @@ -66,7 +71,7 @@ tailscale status Set `GPU_HOST` in `/opt/vocalize/.env` to the GPU node's Tailscale IP. -If the GPU services are not yet running, use `install/pi-install.sh --skip-gpu` +If the GPU services are not yet running, use `install/install.sh --skip-gpu` to proceed with installation without the GPU-reachability check. --- @@ -74,20 +79,20 @@ to proceed with installation without the GPU-reachability check. ## Clone and Install ```bash -# Clone the repository to the Pi: +# Clone the repository to the orchestrator host: git clone https://github.com/DGPisces/VocalizeAI.git /opt/vocalize cd /opt/vocalize # Dry-run first to preview all 7 steps: -bash install/pi-install.sh --dry-run +bash install/install.sh --dry-run # Run the full installer: -bash install/pi-install.sh +bash install/install.sh # Or run selectively: -bash install/pi-install.sh --steps "1,2,6" # apt + venv + systemd only -bash install/pi-install.sh --skip-gpu # skip GPU-reachability check in step 7 -bash install/pi-install.sh --skip-tunnel # skip step 5 (Cloudflare Tunnel info) +bash install/install.sh --steps "1,2,6" # apt + venv + systemd only +bash install/install.sh --skip-gpu # skip GPU-reachability check in step 7 +bash install/install.sh --skip-tunnel # skip step 5 (Cloudflare Tunnel info) ``` **Installer steps:** @@ -96,7 +101,7 @@ bash install/pi-install.sh --skip-tunnel # skip step 5 (Cloudflare Tunnel i |------|--------| | 1 | `apt-get install` python3.11 python3.11-venv python3-pip build-essential rsync | | 2 | Create `.venv` in `/opt/vocalize`, `pip install -e .` | -| 3 | GPU services note (GPU lives on a separate host; no on-Pi install) | +| 3 | GPU services note (GPU lives on a separate host; no on-orchestrator install) | | 4 | Tailscale presence check (warns if absent) | | 5 | Cloudflare Tunnel token-install instructions | | 6 | Copy `vocalize.service` to `/etc/systemd/system/`, copy `.env.template` to `/opt/vocalize/.env` if absent, `systemctl enable vocalize` | @@ -124,9 +129,9 @@ sudo nano /opt/vocalize/.env | `GPU_HOST` | yes (if using GPU) | STT/TTS host — Tailscale IP of your GPU node | | `SENSEVOICE_WS_PORT` | default ok | STT port; default `8000` | | `COSYVOICE_WS_PORT` | default ok | TTS port; default `8001` | -| `VOCALIZE_HOST` | default ok | uvicorn bind host; set to `0.0.0.0` for Pi production | +| `VOCALIZE_HOST` | default ok | uvicorn bind host; set to `0.0.0.0` for production | | `VOCALIZE_PORT` | default ok | uvicorn bind port; default `8080` | -| `ORCHESTRATOR_LISTEN_PORT` | default ok | Pi service port; default `8080` (legacy compatibility) | +| `ORCHESTRATOR_LISTEN_PORT` | default ok | Orchestrator service port; default `8080` (legacy compatibility) | | `VOCALIZE_WS_BASE_URL` | **yes** | Public WS base URL; e.g. `wss://api.` — startup raises if missing in non-localhost mode | | `VOCALIZE_CORS_ORIGINS` | default ok | Comma-separated allowed CORS origins; default auto-picked from VOCALIZE_HOST | | `DEFAULT_LANGUAGE` | default ok | `zh` or `en`; default `zh` | @@ -153,7 +158,7 @@ VOCALIZE_CORS_ORIGINS=https:// ## Cloudflare Tunnel -The Cloudflare Tunnel connects the Pi to the public internet without exposing SSH +The Cloudflare Tunnel connects the orchestrator host to the public internet without exposing SSH or opening firewall ports. **Token-based install (recommended):** @@ -163,7 +168,7 @@ or opening firewall ports. # Zero Trust -> Networks -> Tunnels -> [your tunnel] -> Configure # -> "Install and run a connector" -> Copy the displayed token -# Install the tunnel service on the Pi: +# Install the tunnel service on the orchestrator host: sudo cloudflared service install # Verify the service is running: @@ -171,7 +176,7 @@ sudo systemctl status cloudflared ``` The reference ingress shape for this project is documented in -`infra/pi-orchestrator/cloudflared-config.yml` (maps +`infra/orchestrator/cloudflared-config.yml` (maps `vocalize-api.` → `http://localhost:8080` and `vocalize.` → `http://localhost:3000`). Configure the actual public hostname routing in the Cloudflare dashboard under @@ -186,7 +191,7 @@ tunnel name; tunnels are account-specific. ### vocalize.service -The `vocalize.service` unit file is at `infra/pi-orchestrator/vocalize.service` +The `vocalize.service` unit file is at `infra/orchestrator/vocalize.service` and is copied to `/etc/systemd/system/vocalize.service` by step 6 of the installer. ```ini @@ -240,7 +245,7 @@ sudo systemctl restart vocalize After the installer completes (step 7 runs this automatically), verify the deployment: ```bash -# Smoke test against the Pi's local port: +# Smoke test against the orchestrator's local port: VOCALIZE_API_BASE=http://127.0.0.1:8080 bash scripts/smoke.sh # Exit 0 = working deployment @@ -251,7 +256,7 @@ VOCALIZE_API_BASE=https://api. bash scripts/smoke.sh The smoke script exercises 6 round-trips: `GET /health`, `POST /api/sessions`, `POST /api/sessions/{id}/task`, WS upgrade + send/recv, `DELETE /api/sessions/{id}`. -Note: the local smoke on the Pi uses port 8080 (production port), not 8000 (dev +Note: the local smoke uses port 8080 (production port), not 8000 (dev port). Make sure `VOCALIZE_API_BASE` is set accordingly. --- @@ -311,3 +316,39 @@ Common causes: **Port conflicts:** - `VOCALIZE_PORT` defaults to 8080. If another service occupies that port, change `VOCALIZE_PORT` in `.env` and update the Cloudflare Tunnel ingress rule accordingly. + +--- + +## Hardware example: Raspberry Pi + +The Raspberry Pi was the original reference target for this runbook. None of +the steps above are Pi-specific; this section just captures the bits that +differ when the orchestrator host happens to be a Pi. + +### BOM + +- Raspberry Pi 4 or Pi 5, **8 GB RAM recommended** (4 GB works for the + orchestrator alone but is tight if other services run alongside). +- 32 GB+ microSD card or USB SSD (SSD strongly recommended for production). +- Reliable internet connection (Cloudflare Tunnel requires outbound HTTPS). + +### Imaging and first boot + +```bash +# Flash Raspberry Pi OS Lite (64-bit) to the SD card / SSD using Raspberry +# Pi Imager. In Imager, pre-configure: +# - hostname +# - SSH enabled +# - SSH public key (paste your ~/.ssh/id_ed25519.pub or generate one first) +# - Wi-Fi credentials (if not using Ethernet) + +# After first boot, SSH in and update the system: +ssh pi@ +sudo apt-get update && sudo apt-get upgrade -y + +# Ensure git and curl are present: +sudo apt-get install -y git curl +``` + +From here on, the rest of this runbook (Tailscale, install, Cloudflare +Tunnel, smoke) applies unchanged. diff --git a/docs/deploy/local.md b/docs/deploy/local.md index 631d5fb..b7b83b0 100644 --- a/docs/deploy/local.md +++ b/docs/deploy/local.md @@ -72,12 +72,12 @@ $EDITOR .env | `OPENAI_API_KEY` | **yes** | LLM authentication — any OpenAI-compatible provider (OpenAI, DeepSeek, Qwen, etc.) | | `OPENAI_BASE_URL` | default ok | LLM endpoint; default `https://api.deepseek.com/v1` | | `OPENAI_MODEL` | default ok | Model name; default `deepseek-chat` | -| `GPU_HOST` | only if using GPU | STT/TTS host; use `localhost` for single-machine dev, Tailscale IP for Pi deployment | +| `GPU_HOST` | only if using GPU | STT/TTS host; use `localhost` for single-machine dev, Tailscale IP for remote-GPU deployment (e.g. Raspberry Pi orchestrator → GPU node) | | `SENSEVOICE_WS_PORT` | default ok | SenseVoice STT WebSocket port; default `8000` | | `COSYVOICE_WS_PORT` | default ok | CosyVoice TTS WebSocket port; default `8001` | | `VOCALIZE_HOST` | default ok | uvicorn bind host; `127.0.0.1` for local dev, `0.0.0.0` for production | | `VOCALIZE_PORT` | default ok | uvicorn bind port; default `8080` (note: dev `main.py` defaults to 8000) | -| `ORCHESTRATOR_LISTEN_PORT` | default ok | Pi service port; default `8080` (legacy; mirrors `VOCALIZE_PORT`) | +| `ORCHESTRATOR_LISTEN_PORT` | default ok | Orchestrator service port; default `8080` (legacy; mirrors `VOCALIZE_PORT`) | | `VOCALIZE_WS_BASE_URL` | required when non-localhost | Public WS base URL (e.g. `wss://api.example.com`); startup raises if missing in non-localhost mode (D-11) | | `VOCALIZE_CORS_ORIGINS` | default ok | Comma-separated allowed CORS origins; auto-picked from VOCALIZE_HOST in dev mode | | `DEFAULT_LANGUAGE` | default ok | Session default language; `zh` or `en`; default `zh` | @@ -178,7 +178,7 @@ cd frontend && npm run test:integration ``` Note: `tests/integration/` release-audio cases require a physical audio setup -(microphone + speaker) and a live Pi orchestrator. These are gated behind +(microphone + speaker) and a live Linux-host orchestrator. These are gated behind `--release-audio` and do not run in PR CI. The standard integration test suite (`npm run test:integration`) runs the 8 text-bypass AI-merchant scenarios and does not require physical hardware. diff --git a/infra/README.md b/infra/README.md index 32b7f0c..3b9548c 100644 --- a/infra/README.md +++ b/infra/README.md @@ -5,8 +5,10 @@ home of one runtime artifact: - `gpu-services/` — Docker Compose stack for SenseVoice (STT) and CosyVoice (TTS) inference servers (`docker-compose.yml`, `healthcheck.sh`, model dirs). -- `pi-orchestrator/` — Raspberry Pi deployment for the FastAPI orchestrator - (`vocalize.service`, `cloudflared-config.yml`, `deploy.sh`, `setup.sh`). +- `orchestrator/` — Linux-host deployment for the FastAPI orchestrator, + with systemd + Cloudflare Tunnel wiring (`vocalize.service`, + `cloudflared-config.yml`, `deploy.sh`, `setup.sh`). Tested on Debian / + Ubuntu / Raspberry Pi OS; any modern Linux host with systemd works. Contrast with `scripts/` (maintainer-run utilities, not deployed) and `tools/` (release tooling, currently empty). diff --git a/infra/pi-orchestrator/.env.template b/infra/orchestrator/.env.template similarity index 100% rename from infra/pi-orchestrator/.env.template rename to infra/orchestrator/.env.template diff --git a/infra/pi-orchestrator/.gitkeep b/infra/orchestrator/.gitkeep similarity index 100% rename from infra/pi-orchestrator/.gitkeep rename to infra/orchestrator/.gitkeep diff --git a/infra/pi-orchestrator/cloudflared-config.yml b/infra/orchestrator/cloudflared-config.yml similarity index 93% rename from infra/pi-orchestrator/cloudflared-config.yml rename to infra/orchestrator/cloudflared-config.yml index 92f4bbc..59c79c6 100644 --- a/infra/pi-orchestrator/cloudflared-config.yml +++ b/infra/orchestrator/cloudflared-config.yml @@ -1,6 +1,6 @@ -# Reference only — DO NOT deploy this file to /etc/cloudflared/ on Pi. +# Reference only — DO NOT deploy this file to /etc/cloudflared/. # -# Pi runs cloudflared via token-based service install: +# Hosts run cloudflared via token-based service install: # sudo cloudflared service install # That command embeds the tunnel id, credentials, and ingress config pulled from # the Cloudflare dashboard. No on-disk config.yml is required for runtime. diff --git a/infra/orchestrator/deploy.sh b/infra/orchestrator/deploy.sh new file mode 100755 index 0000000..ba84445 --- /dev/null +++ b/infra/orchestrator/deploy.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# deploy.sh — rsync VocalizeAI from your workstation to a remote Linux host +# and restart the vocalize + cloudflared services. +# +# Usage: +# TARGET_HOST= ./deploy.sh +# +# Prerequisites: +# - SSH key auth to ${TARGET_HOST} +# - setup.sh has been run on the host at least once +# - cloudflared-config.yml has a real tunnel ID (not placeholder) +# +# Env vars (PI_HOST / PI_USER are accepted as legacy aliases of TARGET_HOST / TARGET_USER): +# TARGET_HOST Remote host IP or hostname (required). +# TARGET_USER SSH user on the remote host (default: current user). +# VOCALIZE_HOME Remote project directory (default: /home/${TARGET_USER}/vocalize). + +TARGET_HOST="${TARGET_HOST:-${PI_HOST:-}}" +if [ -z "${TARGET_HOST}" ]; then + echo "ERROR: set TARGET_HOST (or legacy PI_HOST) to your remote host IP or hostname." >&2 + exit 1 +fi +TARGET_USER="${TARGET_USER:-${PI_USER:-$(whoami)}}" +TARGET_PROJECT_DIR="${VOCALIZE_HOME:-/home/${TARGET_USER}/vocalize}" +LOCAL_PROJECT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" + +echo "=== Deploying VocalizeAI to ${TARGET_USER}@${TARGET_HOST} ===" + +echo "[1/5] Rsync project to remote host..." +rsync -avz \ + --exclude '.venv' \ + --exclude '__pycache__' \ + --exclude '*.pyc' \ + --exclude '.git' \ + --exclude 'node_modules' \ + --exclude '.omc' \ + --exclude '.planning' \ + --exclude '.env' \ + "${LOCAL_PROJECT_DIR}/" \ + "${TARGET_USER}@${TARGET_HOST}:${TARGET_PROJECT_DIR}/" + +echo "[2/5] Install/update Python deps on remote host..." +ssh "${TARGET_USER}@${TARGET_HOST}" \ + "cd ${TARGET_PROJECT_DIR} && .venv/bin/pip install -e ." + +echo "[3/5] Restart vocalize service..." +ssh "${TARGET_USER}@${TARGET_HOST}" \ + "sudo systemctl restart vocalize" + +echo "[4/5] Restart cloudflared service..." +ssh "${TARGET_USER}@${TARGET_HOST}" \ + "sudo systemctl restart cloudflared" + +echo "[5/5] Check service status..." +ssh "${TARGET_USER}@${TARGET_HOST}" \ + "sudo systemctl status vocalize cloudflared --no-pager -l" + +echo "=== Deploy complete ===" diff --git a/infra/orchestrator/setup.sh b/infra/orchestrator/setup.sh new file mode 100755 index 0000000..67888ed --- /dev/null +++ b/infra/orchestrator/setup.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -euo pipefail + +# setup.sh — one-time setup for running VocalizeAI on a remote Linux host. +# +# Tested on Debian 12, Ubuntu 22.04 / 24.04, and Raspberry Pi OS (Bookworm). +# Any modern Linux distro with systemd works. +# +# Usage: +# 1. Run this script from your workstation: TARGET_HOST= ./setup.sh +# 2. Follow the manual cloudflared token-install + service-start steps printed at the end. +# +# Prerequisites: +# - Remote host running a recent Linux release with systemd +# - SSH key auth: ssh ${TARGET_USER}@${TARGET_HOST} +# - cloudflared installed on the host (sudo snap install cloudflared OR apt install cloudflared) +# +# Env vars (PI_HOST / PI_USER are accepted as legacy aliases of TARGET_HOST / TARGET_USER): +# TARGET_HOST Remote host IP or hostname (required). +# TARGET_USER SSH user on the remote host (default: current user). +# VOCALIZE_HOME Remote project directory (default: /home/${TARGET_USER}/vocalize). + +TARGET_HOST="${TARGET_HOST:-${PI_HOST:-}}" +if [ -z "${TARGET_HOST}" ]; then + echo "ERROR: set TARGET_HOST (or legacy PI_HOST) to your remote host IP or hostname." >&2 + exit 1 +fi +TARGET_USER="${TARGET_USER:-${PI_USER:-$(whoami)}}" +TARGET_PROJECT_DIR="${VOCALIZE_HOME:-/home/${TARGET_USER}/vocalize}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "=== VocalizeAI Orchestrator One-Time Setup ===" +echo "Target: ${TARGET_USER}@${TARGET_HOST}:${TARGET_PROJECT_DIR}" +echo "" + +# ---- Step 1: Rsync repo to remote host ---- +echo "[1/5] Copying project to remote host..." +rsync -avz \ + --exclude '.venv' \ + --exclude '__pycache__' \ + --exclude '*.pyc' \ + --exclude '.git' \ + --exclude 'node_modules' \ + --exclude '.omc' \ + --exclude '.planning' \ + --exclude '.env' \ + "${SCRIPT_DIR}/../../" \ + "${TARGET_USER}@${TARGET_HOST}:${TARGET_PROJECT_DIR}/" + +# ---- Step 2: Create venv and install deps ---- +echo "[2/5] Creating Python venv on remote host..." +ssh "${TARGET_USER}@${TARGET_HOST}" "python3 -m venv ${TARGET_PROJECT_DIR}/.venv" + +echo "[3/5] Installing Python dependencies..." +ssh "${TARGET_USER}@${TARGET_HOST}" "${TARGET_PROJECT_DIR}/.venv/bin/pip install --upgrade pip" +ssh "${TARGET_USER}@${TARGET_HOST}" "${TARGET_PROJECT_DIR}/.venv/bin/pip install -e ${TARGET_PROJECT_DIR}" + +# ---- Step 3: Create .env if not exists ---- +echo "[4/5] Setting up .env..." +ssh "${TARGET_USER}@${TARGET_HOST}" "test -f ${TARGET_PROJECT_DIR}/.env || cp ${TARGET_PROJECT_DIR}/infra/orchestrator/.env.template ${TARGET_PROJECT_DIR}/.env" +echo " -> .env created from template (edit it with real values before starting the service)" + +# ---- Step 4: Install systemd service ---- +echo "[5/5] Installing systemd vocalize service..." + +VOCALIZE_SERVICE=$(cat <<'SERVICEEOF' +[Unit] +Description=VocalizeAI orchestrator service +After=network-online.target +Wants=network-online.target + +[Service] +# Install path is /opt/vocalize by convention; override by editing this unit +# or by running setup.sh with VOCALIZE_HOME=/your/path set. +Type=simple +User=vocalize +WorkingDirectory=/opt/vocalize +EnvironmentFile=/opt/vocalize/.env +ExecStart=/opt/vocalize/.venv/bin/python -m vocalize.main +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +SERVICEEOF +) + +ssh "${TARGET_USER}@${TARGET_HOST}" "echo '${VOCALIZE_SERVICE}' | sudo tee /etc/systemd/system/vocalize.service > /dev/null" +ssh "${TARGET_USER}@${TARGET_HOST}" "sudo systemctl daemon-reload" +ssh "${TARGET_USER}@${TARGET_HOST}" "sudo systemctl enable vocalize" + +echo " -> vocalize.service installed and enabled" + +echo "" +echo "=== Setup complete ===" +echo "" +echo "=== MANUAL STEPS REQUIRED ===" +echo "" +echo "1. Edit .env on the remote host with real values:" +echo " ssh ${TARGET_USER}@${TARGET_HOST} 'nano ${TARGET_PROJECT_DIR}/.env'" +echo "" +echo "2. Install cloudflared service on the host using a tunnel token (token-based auth):" +echo " a. Get the connector token from the Cloudflare dashboard:" +echo " Zero Trust → Networks → Tunnels → your-tunnel-name → Configure" +echo " → Install and run a connector → copy the long token string" +echo " b. SSH to the host and install the service with that token:" +echo " ssh ${TARGET_USER}@${TARGET_HOST} 'sudo cloudflared service install '" +echo " The token embeds tunnel id, credentials, and ingress config — no on-disk" +echo " config.yml is needed. (If you ever want to inspect intended ingress, see" +echo " infra/orchestrator/cloudflared-config.yml; that file is reference-only.)" +echo "" +echo "3. Make sure Public Hostname routing is configured in the dashboard:" +echo " api.example.com → http://localhost:8080" +echo "" +echo "4. Start services:" +echo " ssh ${TARGET_USER}@${TARGET_HOST} 'sudo systemctl start vocalize cloudflared'" +echo "" +echo "5. Verify both services are running and the public URL responds:" +echo " ssh ${TARGET_USER}@${TARGET_HOST} 'sudo systemctl status vocalize cloudflared --no-pager'" +echo " curl -fsS https://api.example.com/health" +echo "" +echo "After completing manual steps, use deploy.sh for subsequent deploys." diff --git a/infra/pi-orchestrator/vocalize.service b/infra/orchestrator/vocalize.service similarity index 100% rename from infra/pi-orchestrator/vocalize.service rename to infra/orchestrator/vocalize.service diff --git a/infra/pi-orchestrator/deploy.sh b/infra/pi-orchestrator/deploy.sh deleted file mode 100755 index 31333f1..0000000 --- a/infra/pi-orchestrator/deploy.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# deploy.sh — rsync VocalizeAI from Mac to Raspberry Pi and restart the vocalize service. -# -# Usage: -# ./deploy.sh -# -# Prerequisites: -# - SSH key auth to Pi at ${PI_HOST} -# - setup.sh has been run on the Pi at least once -# - cloudflared-config.yml has a real tunnel ID (not placeholder) - -PI_HOST="${PI_HOST:?set to your Pi IP or hostname}" -PI_USER="${PI_USER:-$(whoami)}" -PI_PROJECT_DIR="${VOCALIZE_HOME:-/home/${PI_USER}/vocalize}" -MAC_PROJECT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" - -echo "=== Deploying VocalizeAI to Pi ===" - -echo "[1/4] Rsync project to Pi..." -rsync -avz \ - --exclude '.venv' \ - --exclude '__pycache__' \ - --exclude '*.pyc' \ - --exclude '.git' \ - --exclude 'node_modules' \ - --exclude '.omc' \ - --exclude '.planning' \ - --exclude '.env' \ - "${MAC_PROJECT_DIR}/" \ - "${PI_USER}@${PI_HOST}:${PI_PROJECT_DIR}/" - -echo "[2/4] Install/update Python deps on Pi..." -ssh "${PI_USER}@${PI_HOST}" \ - "cd ${PI_PROJECT_DIR} && .venv/bin/pip install -e ." - -echo "[3/5] Restart vocalize service..." -ssh "${PI_USER}@${PI_HOST}" \ - "sudo systemctl restart vocalize" - -echo "[4/5] Restart cloudflared service..." -ssh "${PI_USER}@${PI_HOST}" \ - "sudo systemctl restart cloudflared" - -echo "[5/5] Check service status..." -ssh "${PI_USER}@${PI_HOST}" \ - "sudo systemctl status vocalize cloudflared --no-pager -l" - -echo "=== Deploy complete ===" diff --git a/infra/pi-orchestrator/setup.sh b/infra/pi-orchestrator/setup.sh deleted file mode 100755 index 99146cd..0000000 --- a/infra/pi-orchestrator/setup.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# setup.sh — one-time setup for running VocalizeAI on a Raspberry Pi. -# -# Usage: -# 1. Run this script from the Mac: ./setup.sh -# 2. Follow the manual cloudflared token-install + service-start steps printed at the end. -# -# Prerequisites: -# - Raspberry Pi running Ubuntu 24.04 -# - SSH key auth: ssh @ -# - cloudflared installed on Pi (sudo snap install cloudflared OR apt install cloudflared) - -PI_HOST="${PI_HOST:?set to your Pi IP or hostname}" -PI_USER="${PI_USER:-$(whoami)}" -PI_PROJECT_DIR="${VOCALIZE_HOME:-/home/${PI_USER}/vocalize}" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -echo "=== VocalizeAI Pi One-Time Setup ===" -echo "" - -# ---- Step 1: Rsync repo to Pi ---- -echo "[1/5] Copying project to Pi..." -rsync -avz \ - --exclude '.venv' \ - --exclude '__pycache__' \ - --exclude '*.pyc' \ - --exclude '.git' \ - --exclude 'node_modules' \ - --exclude '.omc' \ - --exclude '.planning' \ - --exclude '.env' \ - "${SCRIPT_DIR}/../../" \ - "${PI_USER}@${PI_HOST}:${PI_PROJECT_DIR}/" - -# ---- Step 2: Create venv and install deps ---- -echo "[2/5] Creating Python venv on Pi..." -ssh "${PI_USER}@${PI_HOST}" "python3 -m venv ${PI_PROJECT_DIR}/.venv" - -echo "[3/5] Installing Python dependencies..." -ssh "${PI_USER}@${PI_HOST}" "${PI_PROJECT_DIR}/.venv/bin/pip install --upgrade pip" -ssh "${PI_USER}@${PI_HOST}" "${PI_PROJECT_DIR}/.venv/bin/pip install -e ${PI_PROJECT_DIR}" - -# ---- Step 3: Create .env if not exists ---- -echo "[4/5] Setting up .env..." -ssh "${PI_USER}@${PI_HOST}" "test -f ${PI_PROJECT_DIR}/.env || cp ${PI_PROJECT_DIR}/infra/pi-orchestrator/.env.template ${PI_PROJECT_DIR}/.env" -echo " -> .env created from template (edit it with real values before starting the service)" - -# ---- Step 4: Install systemd service ---- -echo "[5/5] Installing systemd vocalize service..." - -VOCALIZE_SERVICE=$(cat <<'SERVICEEOF' -[Unit] -Description=VocalizeAI orchestrator service -After=network-online.target -Wants=network-online.target - -[Service] -# Install path is /opt/vocalize by convention; override by editing this unit -# or by running setup.sh with VOCALIZE_HOME=/your/path set. -Type=simple -User=vocalize -WorkingDirectory=/opt/vocalize -EnvironmentFile=/opt/vocalize/.env -ExecStart=/opt/vocalize/.venv/bin/python -m vocalize.main -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target -SERVICEEOF -) - -ssh "${PI_USER}@${PI_HOST}" "echo '${VOCALIZE_SERVICE}' | sudo tee /etc/systemd/system/vocalize.service > /dev/null" -ssh "${PI_USER}@${PI_HOST}" "sudo systemctl daemon-reload" -ssh "${PI_USER}@${PI_HOST}" "sudo systemctl enable vocalize" - -echo " -> vocalize.service installed and enabled" - -echo "" -echo "=== Setup complete ===" -echo "" -echo "=== MANUAL STEPS REQUIRED ===" -echo "" -echo "1. Edit .env on the Pi with real values:" -echo " ssh ${PI_USER}@${PI_HOST} 'nano ${PI_PROJECT_DIR}/.env'" -echo "" -echo "2. Install cloudflared service on Pi using a tunnel token (token-based auth):" -echo " a. Get the connector token from the Cloudflare dashboard:" -echo " Zero Trust → Networks → Tunnels → your-tunnel-name → Configure" -echo " → Install and run a connector → copy the long token string" -echo " b. SSH to Pi and install the service with that token:" -echo " ssh ${PI_USER}@${PI_HOST} 'sudo cloudflared service install '" -echo " The token embeds tunnel id, credentials, and ingress config — no on-disk" -echo " config.yml is needed. (If you ever want to inspect intended ingress, see" -echo " infra/pi-orchestrator/cloudflared-config.yml; that file is reference-only.)" -echo "" -echo "3. Make sure Public Hostname routing is configured in the dashboard:" -echo " api.example.com → http://localhost:8080" -echo "" -echo "4. Start services:" -echo " ssh ${PI_USER}@${PI_HOST} 'sudo systemctl start vocalize cloudflared'" -echo "" -echo "5. Verify both services are running and the public URL responds:" -echo " ssh ${PI_USER}@${PI_HOST} 'sudo systemctl status vocalize cloudflared --no-pager'" -echo " curl -fsS https://api.example.com/health" -echo "" -echo "After completing manual steps, use deploy.sh for subsequent deploys." diff --git a/install/pi-install.sh b/install/install.sh similarity index 93% rename from install/pi-install.sh rename to install/install.sh index 511c54b..f360b2d 100755 --- a/install/pi-install.sh +++ b/install/install.sh @@ -6,10 +6,10 @@ IFS=$'\n\t' # VocalizeAI Pi installer # # Deploys the VocalizeAI orchestrator on a Raspberry Pi. -# Wraps the existing infra/pi-orchestrator/ assets. +# Wraps the existing infra/orchestrator/ assets. # # Usage: -# bash install/pi-install.sh [--dry-run] [--steps "1,3,5"] [--skip-tunnel] [--skip-gpu] +# bash install/install.sh [--dry-run] [--steps "1,3,5"] [--skip-tunnel] [--skip-gpu] # # Flags: # --dry-run Print planned actions without performing any mutations. @@ -53,7 +53,7 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown flag: $1" - echo "Usage: bash install/pi-install.sh [--dry-run] [--steps '1,3,5'] [--skip-tunnel] [--skip-gpu]" + echo "Usage: bash install/install.sh [--dry-run] [--steps '1,3,5'] [--skip-tunnel] [--skip-gpu]" exit 1 ;; esac @@ -128,7 +128,7 @@ step3_gpu_services() { echo "[3/7] GPU services setup..." echo " GPU services (SenseVoice STT + CosyVoice TTS) run on a separate host." echo " Ensure GPU_HOST in /opt/vocalize/.env points to that host's Tailscale IP." - echo " This installer does not configure the GPU host — see docs/deploy/pi.md for details." + echo " This installer does not configure the GPU host — see docs/deploy/linux.md for details." echo "[3/7] Done (note only — no mutation performed)." } @@ -147,7 +147,7 @@ step4_tailscale_check() { else echo " WARNING: Tailscale not found. Install it with:" echo " curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up" - echo " Tailscale is required for the Pi to reach the GPU host." + echo " Tailscale is required for this host to reach the GPU host." fi fi echo "[4/7] Done." @@ -167,7 +167,7 @@ step5_cloudflared_tunnel() { echo " Zero Trust -> Networks -> Tunnels -> [your tunnel] -> Configure" echo " -> Install and run a connector -> Copy the token" echo "" - echo " Reference ingress shape (docs only): infra/pi-orchestrator/cloudflared-config.yml" + echo " Reference ingress shape (docs only): infra/orchestrator/cloudflared-config.yml" echo " (Actual ingress routing is configured in the Cloudflare dashboard, not in a file.)" if [ "$DRY_RUN" = true ]; then echo "[DRY] would run: sudo cloudflared service install (requires manual token)" @@ -183,9 +183,9 @@ step6_systemd_unit() { echo "" echo "[6/7] Installing systemd unit and environment file..." - SERVICE_SRC="${REPO_ROOT}/infra/pi-orchestrator/vocalize.service" + SERVICE_SRC="${REPO_ROOT}/infra/orchestrator/vocalize.service" SERVICE_DST="/etc/systemd/system/vocalize.service" - ENV_SRC="${REPO_ROOT}/infra/pi-orchestrator/.env.template" + ENV_SRC="${REPO_ROOT}/infra/orchestrator/.env.template" ENV_DST="${INSTALL_DIR}/.env" # Install vocalize.service @@ -237,7 +237,7 @@ step7_start_and_smoke() { # Main dispatcher # --------------------------------------------------------------------------- -echo "=== VocalizeAI Pi Installer ===" +echo "=== VocalizeAI Linux Installer ===" if [ "$DRY_RUN" = true ]; then echo "(DRY RUN — no system changes will be made)" fi @@ -272,7 +272,7 @@ for i in 1 2 3 4 5 6 7; do done echo "" -echo "=== Pi install complete ===" +echo "=== Install complete ===" echo "Steps run: ${STEPS_RUN[*]:-none}" if [ "$DRY_RUN" = true ]; then echo "(DRY RUN — re-run without --dry-run to apply changes)" diff --git a/scripts/README.md b/scripts/README.md index 3ecb754..12f6f16 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,8 +7,9 @@ Each script is standalone and invoked from the repo root. Current scripts: - `build-public-filelist.py` — builds the filtered file list for the `sync-private-to-public` skill (consumes `install/public-allowlist.md` and `.public-sync-deny`). -- `stability-24h-driver.py` — drives the 24-hour Pi stability rehearsal - (Phase 4 DEPLOY-02 evidence harness). +- `stability-24h-driver.py` — drives the 24-hour orchestrator stability + rehearsal (Phase 4 DEPLOY-02 evidence harness). Hardware-agnostic; the + reference run was executed against a Raspberry Pi orchestrator. The dead-code-scanner whitelist (`vulture-whitelist.py`) was relocated to `.tooling/vulture-whitelist.py` in Phase 6 (maintainer-only scan tooling lives under `.tooling/` and is excluded from the public mirror).