# Deploy OmniVoice (https://github.com/k2-fsa/OmniVoice) to irv-ml1, GPU 0 # (RTX 3090). Apache-2.0 zero-shot multilingual voice-cloning TTS, served # behind our OWN FastAPI wrapper (app.py): batch /v1/audio/speech plus a # streaming /tts driven by the vendored buffer-ratchet scheduler. # # Builds the image locally from stacks/omnivoice/Dockerfile (CUDA 12.8 + # torch 2.8.0 + omnivoice from PyPI + vendored scheduler.py/sanitize.py), # stages the build context under /opt/docker/compose/omnivoice/, brings it # up, and waits for /healthz on :8199. # # First run is slow: ~5-10 min docker build + a one-time HF weight pre-warm # (k2-fsa/OmniVoice) on first container start (entrypoint.sh). The wait loop # below allows up to ~20 min for build-then-up + pre-warm. # # Usage: # scripts/elway irv-ml1 --playbook playbooks/deploy-omnivoice.yaml # # Idempotent — every step is creates-/when-gated; rerun is safe. vars: compose_dir: /opt/docker/compose/omnivoice cache_dir: /worktank/omnivoice/hf_cache voices_dir: /worktank/omnivoice/voices host_port: "8199" steps: # ── host-side dirs ────────────────────────────────────────────────── - name: Ensure /worktank/omnivoice root exists (one-time, sudo) shell: mkdir -p /worktank/omnivoice sudo: true creates: /worktank/omnivoice - name: Chown /worktank/omnivoice to lkraven shell: chown lkraven:lkraven /worktank/omnivoice sudo: true when: '[ "$(stat -c %U /worktank/omnivoice)" != lkraven ]' - name: Ensure cache dir exists shell: mkdir -p {{ cache_dir }} creates: "{{ cache_dir }}" - name: Ensure voices dir exists shell: mkdir -p {{ voices_dir }} creates: "{{ voices_dir }}" - name: Ensure compose dir exists shell: mkdir -p {{ compose_dir }} creates: "{{ compose_dir }}" # ── deploy build context (compose, dockerfile, entrypoint, env) ─────── - name: Upload compose.yaml upload: src: stacks/omnivoice/compose.yaml dest: "{{ compose_dir }}/compose.yaml" mode: "0644" - name: Upload Dockerfile upload: src: stacks/omnivoice/Dockerfile dest: "{{ compose_dir }}/Dockerfile" mode: "0644" - name: Upload app.py (batch + streaming FastAPI wrapper) upload: src: stacks/omnivoice/app.py dest: "{{ compose_dir }}/app.py" mode: "0644" - name: Upload scheduler.py (vendored buffer-ratchet streaming scheduler) upload: src: stacks/omnivoice/scheduler.py dest: "{{ compose_dir }}/scheduler.py" mode: "0644" - name: Upload sanitize.py (language-safe TTS text sanitizer) upload: src: stacks/omnivoice/sanitize.py dest: "{{ compose_dir }}/sanitize.py" mode: "0644" - name: Stage chatterbox reference voices for cloning (skip _*.wav artifacts) shell: | set -e mkdir -p {{ voices_dir }} docker exec chatterbox-fast sh -c 'ls /refs/*.wav' | while read -r f; do b=$(basename "$f") case "$b" in _*) continue;; esac docker cp "chatterbox-fast:$f" "{{ voices_dir }}/$b" done echo "staged:"; ls {{ voices_dir }} # Skip if already staged (Emily.wav is a proxy for "voices present"). when: "[ ! -f {{ voices_dir }}/Emily.wav ]" - name: Upload entrypoint.sh upload: src: stacks/omnivoice/entrypoint.sh dest: "{{ compose_dir }}/entrypoint.sh" mode: "0755" - name: Seed .env from template (only if absent) upload: src: stacks/omnivoice/.env.example dest: "{{ compose_dir }}/.env" mode: "0644" when: "[ ! -f {{ compose_dir }}/.env ]" # ── build + bring up ──────────────────────────────────────────────── - name: docker compose build (~5-10 min first time; cached after) shell: | set -o pipefail cd {{ compose_dir }} && docker compose build --progress=plain 2>&1 \ | grep -vE '^#[0-9]+ [0-9.]+ (Downloading|Collecting|Requirement|Using cached|Installing collected|Successfully (installed|built)|━|Resolved|Prepared|Built)' - name: docker compose up -d shell: cd {{ compose_dir }} && docker compose up -d - name: Wait for /healthz (allow ~25 min for weight + Whisper pre-warm + voice cloning) shell: | for i in $(seq 1 300); do curl -sf -o /dev/null --max-time 3 http://localhost:{{ host_port }}/healthz && exit 0 sleep 5 done exit 1 changed_when: "false" verify: - name: /healthz returns 200 shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/healthz changed_when: "false" - name: /v1/audio/voices lists the reused chatterbox voices shell: curl -sf http://localhost:{{ host_port }}/v1/audio/voices | grep -q '"voices"' changed_when: "false" - name: Container is running shell: docker inspect omnivoice --format '{{.State.Status}}' | grep -q running changed_when: "false"