uncloseai-speech/Makefile
russell@unturf.com b315659be6 Make Qwen3-TTS the default engine, add CPU-only docker support
- Switch default TTS engine from Piper to Qwen3-TTS (1.7B params)
- Upgrade to Python 3.12
- Add docker-compose.cpu.yml for CPU-only deployments
- Improve GPU configuration with NVIDIA environment variables
- Comment out optional engines (Piper, XTTS, Silero, Kokoro) in requirements
- Update Makefile with local/local-cpu targets and venv support
- Simplify voice_to_speaker.default.yaml for Qwen3-TTS voices
- Update docs/MODELS.md with Qwen3-TTS documentation
- Add git commit guidelines to CLAUDE.md
2026-01-26 10:41:23 -05:00

394 lines
16 KiB
Makefile
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Raccoon Mission: uncloseai-speech Development Makefile
# Deploy to remote server with ease
# Configuration is loaded from vars.sh (copy vars.sh.example to vars.sh)
# Load configuration from vars.sh if it exists
ifneq (,$(wildcard vars.sh))
include vars.sh
export
endif
# Fallback defaults if vars.sh is not found
REMOTE_HOST ?= localhost
REMOTE_USER ?= $(USER)
REMOTE_PATH ?= ~/uncloseai-speech
CONTAINER_NAME ?= uncloseai-speech-server-1
.PHONY: help deploy sync restart logs test clean stop start voices voices-qwen voices-piper voices-xtts voices-kokoro test-kokoro voices-silero test-silero voices-chatterbox test-chatterbox push-all hydrate load-test test-qwen venv venv-run local local-cpu
help:
@echo "🦝 Raccoon TTS Mission - Development Commands"
@echo ""
@echo "Quick Start (Docker with GPU):"
@echo " make local - Build and run locally with GPU support"
@echo " make local-cpu - Build and run locally (CPU only, slower)"
@echo " make test - Test Qwen3-TTS endpoint"
@echo " make logs - Tail container logs"
@echo ""
@echo "Quick Start (No Docker):"
@echo " make venv - Create Python virtual environment"
@echo " make venv-run - Run server in virtual environment"
@echo ""
@echo "Remote Deployment:"
@echo " make deploy - Full deploy: sync files, restart container"
@echo " make sync - Sync local files to remote server"
@echo " make restart - Restart the Docker container"
@echo ""
@echo "Testing:"
@echo " make test - Test TTS endpoint (Qwen3-TTS)"
@echo " make test-qwen - Test Qwen3-TTS voice cloning"
@echo " make hydrate - Test all configured voices"
@echo " make load-test - Concurrent load test"
@echo ""
@echo "Other Engines (disabled by default):"
@echo " make test-xtts - Test XTTS HD endpoint"
@echo " make test-kokoro - Test Kokoro fast TTS"
@echo " make test-silero - Test Silero TTS endpoint"
@echo " make voices-piper - Download Piper voices"
@echo " make voices-xtts - Download XTTS voices"
@echo " make voices-kokoro - Download Kokoro models"
@echo " make voices-silero - Download Silero models"
@echo ""
@echo "Container Management:"
@echo " make start - Start Docker container"
@echo " make stop - Stop Docker container"
@echo " make clean - Stop and remove container"
@echo ""
@echo "Git:"
@echo " make push-all - Push to all git remotes"
sync:
@echo "📦 Syncing files to $(REMOTE_HOST)..."
rsync -avz --exclude '.git' --exclude '__pycache__' --exclude '*.pyc' \
--exclude 'voices/*' --exclude 'config/voice_to_speaker.yaml' \
./ $(REMOTE_USER)@$(REMOTE_HOST):$(REMOTE_PATH)/
@echo "📝 Ensuring speech.env exists..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "cd $(REMOTE_PATH) && [ -f speech.env ] || cp sample.env speech.env"
deploy: sync restart
@echo "✅ Deployment complete!"
# ============================================================================
# Local Development (Docker)
# ============================================================================
local:
@echo "🐳 Building and running with GPU support..."
@[ -f speech.env ] || cp sample.env speech.env
docker compose up -d --build
@echo ""
@echo "✅ Container started! Qwen3-TTS model will download on first request (~3.4GB)"
@echo " Test with: make test"
@echo " View logs: make logs"
local-cpu:
@echo "🐳 Building and running (CPU only - slower inference)..."
@[ -f speech.env ] || cp sample.env speech.env
docker compose -f docker-compose.cpu.yml up -d --build
@echo ""
@echo "✅ Container started (CPU mode)!"
@echo " Note: Qwen3-TTS is ~10x slower on CPU"
@echo " Test with: make test"
# ============================================================================
# Local Development (Python venv - no Docker)
# ============================================================================
VENV_DIR := .venv
PYTHON := python3
venv:
@echo "🐍 Creating Python virtual environment..."
@if [ ! -d "$(VENV_DIR)" ]; then \
$(PYTHON) -m venv $(VENV_DIR); \
echo "✅ Virtual environment created at $(VENV_DIR)"; \
else \
echo " Virtual environment already exists at $(VENV_DIR)"; \
fi
@echo ""
@echo "📦 Installing dependencies..."
$(VENV_DIR)/bin/pip install --upgrade pip
$(VENV_DIR)/bin/pip install -r requirements.txt
@echo ""
@echo "✅ Setup complete!"
@echo ""
@echo "To activate manually:"
@echo " source $(VENV_DIR)/bin/activate"
@echo ""
@echo "To run the server:"
@echo " make venv-run"
@echo ""
@echo "Or run directly:"
@echo " $(VENV_DIR)/bin/python speech.py"
venv-run:
@echo "🚀 Starting uncloseai-speech server..."
@if [ ! -d "$(VENV_DIR)" ]; then \
echo "❌ Virtual environment not found. Run 'make venv' first."; \
exit 1; \
fi
@[ -d "config" ] || mkdir -p config
@[ -d "voices" ] || mkdir -p voices
@echo ""
@echo "Server starting on http://localhost:8000"
@echo "Qwen3-TTS model will download on first request (~3.4GB)"
@echo ""
$(VENV_DIR)/bin/python speech.py
venv-clean:
@echo "🧹 Removing virtual environment..."
rm -rf $(VENV_DIR)
@echo "✅ Virtual environment removed"
restart:
@echo "🔄 Rebuilding and restarting container on $(REMOTE_HOST)..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "cd $(REMOTE_PATH) && docker compose up -d --build"
stop:
@echo "🛑 Stopping container on $(REMOTE_HOST)..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "cd $(REMOTE_PATH) && docker compose down"
start:
@echo "▶️ Starting container on $(REMOTE_HOST)..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "cd $(REMOTE_PATH) && docker compose up -d"
clean: stop
@echo "🧹 Removing container..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "cd $(REMOTE_PATH) && docker compose down -v"
logs:
@echo "📋 Tailing logs from $(REMOTE_HOST)..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "docker logs -f $(CONTAINER_NAME)"
test: test-qwen
test-qwen:
@echo "🧪 Testing Qwen3-TTS endpoint (default model)..."
curl -X POST http://$(REMOTE_HOST):8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1-qwen","voice":"alloy","input":"Raccoon mission TTS test with Qwen three"}' \
-o /tmp/qwen_test.mp3
@echo "✅ Test complete! Playing audio..."
@firefox /tmp/qwen_test.mp3 || mpv /tmp/qwen_test.mp3 || echo "Install firefox or mpv to play audio"
voices: voices-qwen
@echo "✅ Qwen3-TTS ready (model downloads automatically on first use)"
voices-qwen:
@echo "🎤 Qwen3-TTS models download automatically on first use"
@echo " Model: Qwen/Qwen3-TTS-12Hz-1.7B-Base (~3.4GB)"
@echo " The model will be cached in /app/voices/hub/"
@echo ""
@echo "To pre-download, run: make test-qwen"
voices-all: voices-qwen voices-piper voices-xtts voices-silero
@echo "✅ All voices downloaded (Qwen, Piper, XTTS, Silero)!"
voices-piper:
@echo "🎤 Downloading all Piper voices..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "docker exec $(CONTAINER_NAME) bash -c '\
set -e; \
BASE_URL=\"https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0\"; \
download_voice() { \
local path=\"\$$1\"; \
local name=\"\$$2\"; \
mkdir -p \"/app/voices/\$$path\"; \
cd \"/app/voices/\$$path\"; \
echo \"Downloading \$$name...\"; \
curl -f -L \"\$$BASE_URL/\$$path/\$$name.onnx\" -o \"\$$name.onnx\" || echo \"Failed to download \$$name.onnx\"; \
curl -f -L \"\$$BASE_URL/\$$path/\$$name.onnx.json\" -o \"\$$name.onnx.json\" || echo \"Failed to download \$$name.onnx.json\"; \
}; \
echo \"=== Downloading English US voices ===\"; \
download_voice \"en/en_US/libritts_r/medium\" \"en_US-libritts_r-medium\"; \
download_voice \"en/en_US/amy/medium\" \"en_US-amy-medium\"; \
download_voice \"en/en_US/arctic/medium\" \"en_US-arctic-medium\"; \
download_voice \"en/en_US/bryce/medium\" \"en_US-bryce-medium\"; \
download_voice \"en/en_US/danny/low\" \"en_US-danny-low\"; \
download_voice \"en/en_US/hfc_female/medium\" \"en_US-hfc_female-medium\"; \
download_voice \"en/en_US/hfc_male/medium\" \"en_US-hfc_male-medium\"; \
download_voice \"en/en_US/joe/medium\" \"en_US-joe-medium\"; \
download_voice \"en/en_US/john/medium\" \"en_US-john-medium\"; \
download_voice \"en/en_US/kathleen/low\" \"en_US-kathleen-low\"; \
download_voice \"en/en_US/kristin/medium\" \"en_US-kristin-medium\"; \
download_voice \"en/en_US/kusal/medium\" \"en_US-kusal-medium\"; \
download_voice \"en/en_US/l2arctic/medium\" \"en_US-l2arctic-medium\"; \
download_voice \"en/en_US/lessac/medium\" \"en_US-lessac-medium\"; \
download_voice \"en/en_US/libritts/high\" \"en_US-libritts-high\"; \
download_voice \"en/en_US/ljspeech/medium\" \"en_US-ljspeech-medium\"; \
download_voice \"en/en_US/norman/medium\" \"en_US-norman-medium\"; \
download_voice \"en/en_US/reza_ibrahim/medium\" \"en_US-reza_ibrahim-medium\"; \
download_voice \"en/en_US/ryan/high\" \"en_US-ryan-high\"; \
download_voice \"en/en_US/sam/medium\" \"en_US-sam-medium\"; \
echo \"=== Downloading English GB voices ===\"; \
download_voice \"en/en_GB/northern_english_male/medium\" \"en_GB-northern_english_male-medium\"; \
download_voice \"en/en_GB/alan/medium\" \"en_GB-alan-medium\"; \
download_voice \"en/en_GB/alba/medium\" \"en_GB-alba-medium\"; \
download_voice \"en/en_GB/aru/medium\" \"en_GB-aru-medium\"; \
download_voice \"en/en_GB/cori/medium\" \"en_GB-cori-medium\"; \
download_voice \"en/en_GB/jenny_dioco/medium\" \"en_GB-jenny_dioco-medium\"; \
download_voice \"en/en_GB/semaine/medium\" \"en_GB-semaine-medium\"; \
download_voice \"en/en_GB/southern_english_female/low\" \"en_GB-southern_english_female-low\"; \
download_voice \"en/en_GB/vctk/medium\" \"en_GB-vctk-medium\"; \
echo \"=== All Piper voices downloaded! ===\"'"
@echo "✅ All Piper voices downloaded!"
voices-xtts:
@echo "🎤 Downloading XTTS speaker samples..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "docker exec $(CONTAINER_NAME) bash -c 'cd /app && ./scripts/download_samples.sh'"
@echo "✅ XTTS speaker samples downloaded!"
voices-kokoro:
@echo "🎤 Downloading Kokoro TTS models..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "docker exec $(CONTAINER_NAME) bash -c '\
mkdir -p /app/voices/kokoro && \
cd /app/voices/kokoro && \
huggingface-cli download hexgrad/kokoro-82m --local-dir .'"
@echo "✅ Kokoro models downloaded!"
test-kokoro:
@echo "🧪 Testing Kokoro fast synthesis..."
curl -X POST http://$(REMOTE_HOST):8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1-kokoro","voice":"alloy","input":"Testing Kokoro fast decoder synthesis"}' \
-o /tmp/kokoro_test.mp3
@echo "✅ Test complete! Playing audio..."
@firefox /tmp/kokoro_test.mp3 || mpv /tmp/kokoro_test.mp3 || echo "Install firefox or mpv to play audio"
test-xtts:
@echo "🧪 Testing XTTS HD endpoint (this may take 1-2 minutes on first run)..."
curl -X POST http://$(REMOTE_HOST):8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1-hd","voice":"alloy","input":"Testing XTTS high definition"}' \
-o /tmp/xtts_test.mp3
@echo "✅ Test complete! Playing audio..."
@firefox /tmp/xtts_test.mp3 || mpv /tmp/xtts_test.mp3 || echo "Install firefox or mpv to play audio"
push-all:
@echo "🚀 Pushing to all remotes..."
git push origin main
git push github main
@echo "✅ Pushed to origin and github!"
voices-silero:
@echo "🎤 Downloading Silero TTS models..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "docker exec $(CONTAINER_NAME) bash -c '\
cd /app/voices && \
python3 -c \"import torch; \
for lang in [\"\"en\"\", \"\"ru\"\", \"\"de\"\", \"\"es\"\", \"\"fr\"\"]: \
model, *_ = torch.hub.load(repo_or_dir=\"\"snakers4/silero-models\"\", model=\"\"silero_tts\"\", language=lang, speaker=\"\"v4_\"\"+lang if lang==\"\"en\"\" else \"\"v3_\"\"+lang); \
print(f\"\"Downloaded Silero {lang}\"\")\"'"
@echo "✅ Silero models downloaded!"
test-silero:
@echo "🧪 Testing Silero endpoint..."
curl -X POST http://$(REMOTE_HOST):8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1-silero","voice":"alloy","input":"Testing Silero fast synthesis"}' \
-o /tmp/silero_test.mp3
@echo "✅ Test complete! Playing audio..."
@firefox /tmp/silero_test.mp3 || mpv /tmp/silero_test.mp3 || echo "Install firefox or mpv to play audio"
voices-chatterbox:
@echo "🎤 Downloading Chatterbox models..."
ssh $(REMOTE_USER)@$(REMOTE_HOST) "docker exec $(CONTAINER_NAME) bash -c '\
mkdir -p /app/voices/chatterbox && \
cd /app/voices/chatterbox && \
huggingface-cli download resemble-ai/chatterbox --local-dir .'"
@echo "✅ Chatterbox models downloaded!"
test-chatterbox:
@echo "🧪 Testing Chatterbox with emotion control..."
curl -X POST http://$(REMOTE_HOST):8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"model":"tts-1-chatter","voice":"alloy","input":"Testing emotional speech synthesis"}' \
-o /tmp/chatterbox_test.mp3
@echo "✅ Test complete! Playing audio..."
@firefox /tmp/chatterbox_test.mp3 || mpv /tmp/chatterbox_test.mp3 || echo "Install firefox or mpv to play audio"
hydrate:
@echo "🦝 Hydrating all TTS models by testing ALL voices..."
@echo "This will test all 227 voices across 4 engines (Piper, XTTS, Silero, Kokoro)"
@echo ""
@mkdir -p /tmp/hydrate_test
@curl -s http://$(REMOTE_HOST):8000/v1/voices | jq -r '.data[] as $$model | $$model.voices[] | "\($$model.id):\(.)"' > /tmp/hydrate_voices.txt
@TOTAL=$$(wc -l < /tmp/hydrate_voices.txt); \
COUNT=0; \
FAILED=0; \
START_TIME=$$(date +%s); \
while IFS=: read -r MODEL VOICE; do \
COUNT=$$((COUNT + 1)); \
printf "[%3d/%3d] Testing %-20s %-30s ... " "$$COUNT" "$$TOTAL" "$$MODEL" "$$VOICE"; \
if curl -s -X POST http://$(REMOTE_HOST):8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d "{\"voice\":\"$$VOICE\",\"input\":\"Hydration test\"}" \
-o /tmp/hydrate_test/$${MODEL}_$${VOICE}.mp3 2>&1 | grep -q "error"; then \
echo "❌ FAILED"; \
FAILED=$$((FAILED + 1)); \
else \
SIZE=$$(stat -c%s /tmp/hydrate_test/$${MODEL}_$${VOICE}.mp3 2>/dev/null || echo 0); \
if [ "$$SIZE" -gt 1000 ]; then \
echo "✅ OK ($${SIZE} bytes)"; \
else \
echo "⚠️ SMALL ($${SIZE} bytes)"; \
FAILED=$$((FAILED + 1)); \
fi; \
fi; \
done < /tmp/hydrate_voices.txt; \
END_TIME=$$(date +%s); \
DURATION=$$((END_TIME - START_TIME)); \
echo ""; \
echo "🎉 Hydration complete!"; \
echo " Total voices: $$TOTAL"; \
echo " Successful: $$((TOTAL - FAILED))"; \
echo " Failed: $$FAILED"; \
echo " Duration: $${DURATION}s"; \
echo " Output: /tmp/hydrate_test/"
load-test:
@echo "🚀 Load testing TTS service with random concurrent requests..."
@echo "This will send 100 concurrent requests with random voices across all models"
@echo ""
@mkdir -p /tmp/load_test
@curl -s http://$(REMOTE_HOST):8000/v1/voices | jq -r '.data[] as $$model | $$model.voices[] | "\($$model.id):\(.)"' > /tmp/load_test_voices.txt
@TOTAL_VOICES=$$(wc -l < /tmp/load_test_voices.txt); \
REQUESTS=100; \
CONCURRENT=10; \
echo "Available voices: $$TOTAL_VOICES"; \
echo "Total requests: $$REQUESTS"; \
echo "Concurrent: $$CONCURRENT"; \
echo ""; \
START_TIME=$$(date +%s); \
seq 1 $$REQUESTS | xargs -P$$CONCURRENT -I{} bash -c " \
TOTAL_VOICES=\$$(wc -l < /tmp/load_test_voices.txt); \
LINE=\$$((RANDOM % \$$TOTAL_VOICES + 1)); \
VOICE_SPEC=\$$(sed -n \"\$${LINE}p\" /tmp/load_test_voices.txt); \
MODEL=\$$(echo \$$VOICE_SPEC | cut -d: -f1); \
VOICE=\$$(echo \$$VOICE_SPEC | cut -d: -f2); \
NUM={}; \
START=\$$(date +%s%3N); \
if curl -s -X POST http://$(REMOTE_HOST):8000/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{\"model\":\"'\$$MODEL'\",\"voice\":\"'\$$VOICE'\",\"input\":\"Load test number '\$$NUM'\"}' \
-o /tmp/load_test/request_\$${NUM}.mp3 2>&1; then \
END=\$$(date +%s%3N); \
DURATION=\$$((END - START)); \
SIZE=\$$(stat -c%s /tmp/load_test/request_\$${NUM}.mp3 2>/dev/null || echo 0); \
printf '[%3d] %-20s %-25s %5dms %6d bytes\n' \"\$$NUM\" \"\$$MODEL\" \"\$$VOICE\" \"\$$DURATION\" \"\$$SIZE\"; \
else \
printf '[%3d] %-20s %-25s FAILED\n' \"\$$NUM\" \"\$$MODEL\" \"\$$VOICE\"; \
fi \
"; \
END_TIME=$$(date +%s); \
DURATION=$$((END_TIME - START_TIME)); \
SUCCESS=$$(ls /tmp/load_test/*.mp3 2>/dev/null | wc -l); \
VALID=$$(find /tmp/load_test -name '*.mp3' -size +1000c 2>/dev/null | wc -l); \
echo ""; \
echo "🎉 Load test complete!"; \
echo " Total requests: $$REQUESTS"; \
echo " Files created: $$SUCCESS"; \
echo " Valid audio (>1KB): $$VALID"; \
echo " Failed: $$((REQUESTS - VALID))"; \
echo " Duration: $${DURATION}s"; \
echo " Avg: $$((DURATION * 1000 / REQUESTS))ms per request"; \
echo " Throughput: $$((REQUESTS / DURATION)) req/s"; \
echo " Output: /tmp/load_test/"