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.
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-speechfor project references - In documentation: Use
uncloseai-speechfor project name - In comments: Use
uncloseai-speechconsistently - 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"— usetmux send-keys - NEVER use
rsyncorscp— usegit push+tmux send-keys '... git pull ...' Enter - ALWAYS rerun
tmux-hoststo 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: Claudeor 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"— usetmux send-keys - DON'T use
rsyncorscp— usegit push+ tmuxgit pull - DON'T hardcode tmux window numbers — rediscover via
tmux-hostsevery 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.
- Caches - Initialize in
lifespancontext manager (runs per worker) - Args - Use
DefaultArgsclass 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.