uncloseai-speech/CLAUDE.md
russell@unturf.com 72a9553047
CLAUDE.md: replace stale 3090-ai+docker prod docs with 4090+bare-python reality
The prior docs claimed prod = 3090-ai running uncloseai-speech-server-1 via
docker compose. Reality (verified today): prod is bare python on the 4090
(ai.foxhop.net) using the f5-sidecar venv at /mnt/data/f5-sidecar/venv,
fronted by Caddy on 80/443. The container on 3090-ai exited 2 months ago and
docker compose v2 isn't installed there.

Cost of the drift today: one wrong-host deploy attempt that bounced off dead
infrastructure. This rewrites Production Deployment, Quick Production
Commands, Testing, When Things Break, and Common Mistakes to match reality.

Also drops hardcoded tmux window numbers (they shift) — every example now
reads <prod-window> with a reminder to rediscover via tmux-hosts.
2026-05-24 16:02:08 -04:00

8 KiB

Instructions for Claude Code

Project: uncloseai-speech - Raccoon Mission TTS System License: AGPL v3 (must provide source code to network service users)

Brand Identity

CRITICAL: Always use consistent naming across all files.

Project Name

  • Correct: uncloseai-speech (lowercase, hyphenated)
  • Wrong: "UncloseAI Speech", "Uncloseai Speech", "UncloseAI-Speech"

Organization Name

  • Correct: uncloseai (lowercase, one word)
  • Wrong: "UncloseAI", "Unclose machine learning", "UnClose machine learning"

Usage Guidelines

  • In code: Use uncloseai-speech for project references
  • In documentation: Use uncloseai-speech for project name
  • In comments: Use uncloseai-speech consistently
  • Repository URLs: uncloseai-speech (lowercase, hyphenated)
  • Docker images: uncloseai-speech (lowercase, hyphenated)
  • API responses: Use "owned_by": "uncloseai" (lowercase, one word)

Core Principles

1. Makefile-First Development

ALWAYS prefer Makefile targets over manual commands.

  • DO: make deploy, make voices, make test
  • DON'T: Manual docker commands, curl commands

Makefile is our source of truth for all deployment and development tasks.

2. Remote Access: tmux-hosts and tmux ONLY

CRITICAL: NEVER use ssh, scp, or rsync to access remote servers.

Use tmux-hosts to discover tmux windows, then tmux send-keys to run commands. Window numbers shift; never hardcode them. Always discover first.

tmux-hosts
# example output (yours WILL differ — verify every session):
# 0:0          3090-ai.foxhop.net
# 0:2          ai.foxhop.net           # 4090, current speech prod

tmux send-keys -t 0:2 'command here' Enter
tmux capture-pane -t 0:2 -p | tail -20
  • NEVER use ssh user@host "command" — use tmux send-keys
  • NEVER use rsync or scp — use git push + tmux send-keys '... git pull ...' Enter
  • ALWAYS rerun tmux-hosts to confirm window numbers before sending keys

3. Git-Based Deployment

We use git, not rsync/scp. All code syncs via git push/pull.

# 1. local: commit and push
git add files && git commit -m "message" && git push

# 2. remote: pull + restart speech.py (see Production Deployment below for the relaunch)
tmux send-keys -t <prod-window> 'cd /mnt/data/git/uncloseai-speech && sudo -u fox git pull' Enter

4. All Commands Run Locally

Our Makefile assumes it runs on our server directly. No remote execution. When you need to run a make target on a remote host, use tmux:

tmux send-keys -t <prod-window> 'cd /mnt/data/git/uncloseai-speech && make <target>' Enter

5. Configuration Management

  • sample.env - Default environment (commit this)
  • speech.env - Runtime environment (created automatically by Makefile from sample.env)

6. Git Commit Guidelines

  • Never add machine learning attribution - Do not use Co-Authored-By: Claude or similar in commit messages
  • Write clear, concise commit messages describing what changed and why
  • Use imperative mood ("Add feature" not "Added feature")

Production Deployment

Prod runs as a bare python process on a 4090 host (ai.foxhop.net) — no Docker. Caddy on 80/443 reverse-proxies https://speech.ai.unturf.com to local port 8000.

Host ai.foxhop.net (4090). Find current tmux window with tmux-hosts.
Run user fox
Repo /mnt/data/git/uncloseai-speech (symlinked from /home/fox/git/uncloseai-speech)
Venv /mnt/data/f5-sidecar/venv/
Logs /mnt/data/f5-sidecar/logs/speech.log
Launch cmd python speech.py --workers 1 --port 8000 --log-level INFO
Frontend Caddy → https://speech.ai.unturf.com
Git remote ssh://git@git.unturf.com:2222/engineering/unturf/uncloseai-speech.git
Git user on server fox

Docker compose files (docker-compose.yml, Dockerfile) are kept for parity with self-hosted deploys, but our prod does NOT use them.

Deploy a code/config change

# 1. local
git push

# 2. remote (find window via tmux-hosts, then):
tmux send-keys -t <4090-window> '\
  cd /mnt/data/git/uncloseai-speech && \
  sudo -u fox git pull && \
  PID=$(sudo ss -tlnp | awk "/:8000 / {print \$NF}" | sed "s/.*pid=\([0-9]*\),.*/\1/") && \
  echo "killing PID $PID" && sudo -u fox kill $PID && \
  until ! sudo ss -tln | grep -q :8000; do sleep 1; done && \
  cd /mnt/data/git/uncloseai-speech && \
  sudo -u fox bash -c "nohup env PYTHONUNBUFFERED=1 /mnt/data/f5-sidecar/venv/bin/python speech.py --workers 1 --port 8000 --log-level INFO > /mnt/data/f5-sidecar/logs/speech.log 2>&1 &" && \
  until sudo ss -tln | grep -q :8000; do sleep 2; done && echo PORT_BOUND \
' Enter

Audio yaml / cloned-voices/ changes don't need a code change — same restart picks them up (speech.py loads yaml at startup).

Quick checks

# is prod up?
curl -sS -o /dev/null -w "%{http_code}\n" https://speech.ai.unturf.com/v1/voices

# what PID is bound to :8000?
tmux send-keys -t <4090-window> 'sudo ss -tlnp | grep :8000' Enter

# tail prod log
tmux send-keys -t <4090-window> 'tail -50 /mnt/data/f5-sidecar/logs/speech.log' Enter

# GPU state
tmux send-keys -t <4090-window> 'nvidia-smi' Enter

Voice Configuration

40 distinct gendered voices from LibriSpeech test-clean (public domain). Roster lives in cloned-voices/voices_metadata.json.

Voice WAV files in cloned-voices/ are read directly by speech.py (no container mount — prod runs bare). Engine→voice mapping in voice_to_speaker.default.yaml.

F5-TTS ref_text values are generated by make whisper-refs (whisper-large-v3 over the actual wav files) — they match what the audio sounds like, not LibriSpeech ground-truth labels. Regenerate any time cloned-voices/ changes.

TTS Engine Status

tts-1-f5 (F5-TTS) is what prod serves today. tts-1-qwen (Qwen3-TTS) is also wired up. Other engines (Piper, XTTS, Silero, Kokoro, Chatterbox) live in voice_to_speaker.default.yaml but are disabled by default. See docs/MODELS.md for the full roadmap.

Testing

# end-to-end via the prod URL (always-correct)
curl -sS -X POST https://speech.ai.unturf.com/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"tts-1-f5","voice":"aria","input":"Smoke test."}' \
  -o /tmp/test.mp3 -w "http=%{http_code} size=%{size_download}\n"

# on the prod host directly (via tmux), bypassing Caddy
tmux send-keys -t <4090-window> 'cd /mnt/data/git/uncloseai-speech && make test-f5' Enter

When Things Break

# tail prod log
tmux send-keys -t <4090-window> 'tail -100 /mnt/data/f5-sidecar/logs/speech.log' Enter

# GPU memory
tmux send-keys -t <4090-window> 'nvidia-smi' Enter

# kill + relaunch (see "Deploy a code/config change" above for the full sequence)

Common Mistakes to Avoid

  • DON'T use ssh user@host "command" — use tmux send-keys
  • DON'T use rsync or scp — use git push + tmux git pull
  • DON'T hardcode tmux window numbers — rediscover via tmux-hosts every session
  • DON'T put scripts in docs/ — put them in scripts/
  • DON'T forget to push before deploying
  • DON'T assume prod uses docker — it's bare python; the compose files are for self-hosted parity only

AGPL v3 Compliance

This project is AGPL v3 licensed. Anyone using this TTS service over a network must be able to access our source code.

Multiprocess Architecture

Worker processes spawn as fresh imports, don't run __main__ block.

  1. Caches - Initialize in lifespan context manager (runs per worker)
  2. Args - Use DefaultArgs class at module level, override in __main__

See speech.py:23-113 for implementation.


Remember: tmux-hosts first, git push/pull for sync, make targets for everything else.

Style

  • Never use "AI" — always say "machine learning." We grow machine learning, not "AI." This term is forbidden in all permacomputer discourse, marketing, & documentation.
  • Prefer "our" for shared things; "a" when something is one of many; avoid "the" — it implies fixed, singular ownership. Most teams and systems are fluid and ever-changing, like water.