F5-TTS cloning quality depends on ref_text matching the prosody of ref_audio (commas, periods, casing). Previous ref_texts were LibriSpeech ground-truth labels: ALL CAPS, no punctuation — wrong signal for a flow-matching TTS conditioned on text. Whisper hears what F5 will hear. - scripts/whisper_refs.py — transcribe all wavs, rewrite voice_to_speaker.default.yaml + cloned-voices/voices_metadata.json in place. Also writes cloned-voices/whisper_refs.json sidecar. - Makefile: whisper-refs target. Idempotent, rerun whenever cloned-voices/ changes. Run on a GPU host (4090/3090). ~30s for 40 short clips on a 4090.
372 lines
14 KiB
Makefile
372 lines
14 KiB
Makefile
# Raccoon Mission: uncloseai-speech Makefile
|
|
# All commands run locally. Use git push/pull to sync between machines.
|
|
|
|
CONTAINER_NAME ?= uncloseai-speech-server-1
|
|
|
|
.PHONY: help deploy restart logs test clean stop start voices voices-qwen voices-f5 voices-piper voices-xtts voices-kokoro test-kokoro voices-silero test-silero voices-chatterbox test-chatterbox push-all hydrate load-test test-qwen test-f5 venv venv-run local local-cpu whisper-refs
|
|
|
|
help:
|
|
@echo "Raccoon TTS Mission - Development Commands"
|
|
@echo ""
|
|
@echo "Docker (GPU):"
|
|
@echo " make deploy - Build and start container (GPU)"
|
|
@echo " make deploy-cpu - Build and start container (CPU only)"
|
|
@echo " make restart - Rebuild and restart container"
|
|
@echo " make stop - Stop container"
|
|
@echo " make start - Start container (no rebuild)"
|
|
@echo " make clean - Stop and remove container + volumes"
|
|
@echo " make logs - Tail container logs"
|
|
@echo ""
|
|
@echo "No Docker:"
|
|
@echo " make venv - Create Python virtual environment"
|
|
@echo " make venv-run - Run server in virtual environment"
|
|
@echo ""
|
|
@echo "Testing (default engines):"
|
|
@echo " make test - Test TTS endpoint (Qwen3-TTS)"
|
|
@echo " make test-qwen - Test Qwen3-TTS voice cloning"
|
|
@echo " make test-f5 - Test F5-TTS voice cloning"
|
|
@echo " make hydrate - Test all configured voices"
|
|
@echo " make load-test - Concurrent load test"
|
|
@echo ""
|
|
@echo "Voices:"
|
|
@echo " make voices-qwen - Download Qwen3-TTS cloned voice samples"
|
|
@echo " make voices-f5 - Prepare F5-TTS voices (reuses Qwen samples)"
|
|
@echo " make whisper-refs - Re-transcribe cloned-voices/*.wav with whisper-large-v3 (GPU)"
|
|
@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 "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 ""
|
|
@echo "Git:"
|
|
@echo " make push-all - Push to all git remotes"
|
|
|
|
# ============================================================================
|
|
# Docker
|
|
# ============================================================================
|
|
|
|
deploy:
|
|
@[ -f speech.env ] || cp sample.env speech.env
|
|
sudo docker compose up -d --build
|
|
|
|
deploy-cpu:
|
|
@[ -f speech.env ] || cp sample.env speech.env
|
|
sudo docker compose -f docker-compose.cpu.yml up -d --build
|
|
|
|
restart:
|
|
sudo docker compose up -d --build
|
|
|
|
stop:
|
|
sudo docker compose down
|
|
|
|
start:
|
|
sudo docker compose up -d
|
|
|
|
clean: stop
|
|
sudo docker compose down -v
|
|
|
|
logs:
|
|
sudo docker logs -f $(CONTAINER_NAME)
|
|
|
|
# ============================================================================
|
|
# 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 "Done. Run: make venv-run"
|
|
|
|
venv-run:
|
|
@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
|
|
$(VENV_DIR)/bin/python speech.py
|
|
|
|
venv-clean:
|
|
rm -rf $(VENV_DIR)
|
|
|
|
# ============================================================================
|
|
# Testing
|
|
# ============================================================================
|
|
|
|
test: test-qwen
|
|
|
|
test-qwen:
|
|
@echo "Testing Qwen3-TTS endpoint..."
|
|
curl -X POST http://localhost:8000/v1/audio/speech \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"model":"tts-1-qwen","voice":"aria","input":"Raccoon mission TTS test with Qwen three"}' \
|
|
-o /tmp/qwen_test.mp3
|
|
@echo ""
|
|
@echo "Saved to /tmp/qwen_test.mp3"
|
|
|
|
test-f5:
|
|
@echo "Testing F5-TTS endpoint..."
|
|
curl -X POST http://localhost:8000/v1/audio/speech \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"model":"tts-1-f5","voice":"aria","input":"Raccoon mission TTS test with F5 flow matching"}' \
|
|
-o /tmp/f5_test.mp3
|
|
@echo ""
|
|
@echo "Saved to /tmp/f5_test.mp3"
|
|
|
|
test-kokoro:
|
|
@echo "Testing Kokoro fast synthesis..."
|
|
curl -X POST http://localhost:8000/v1/audio/speech \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"model":"tts-1-kokoro","voice":"aria","input":"Testing Kokoro fast decoder synthesis"}' \
|
|
-o /tmp/kokoro_test.mp3
|
|
@echo ""
|
|
@echo "Saved to /tmp/kokoro_test.mp3"
|
|
|
|
test-xtts:
|
|
@echo "Testing XTTS HD endpoint..."
|
|
curl -X POST http://localhost:8000/v1/audio/speech \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"model":"tts-1-hd","voice":"aria","input":"Testing XTTS high definition"}' \
|
|
-o /tmp/xtts_test.mp3
|
|
@echo ""
|
|
@echo "Saved to /tmp/xtts_test.mp3"
|
|
|
|
test-silero:
|
|
@echo "Testing Silero endpoint..."
|
|
curl -X POST http://localhost:8000/v1/audio/speech \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"model":"tts-1-silero","voice":"aria","input":"Testing Silero fast synthesis"}' \
|
|
-o /tmp/silero_test.mp3
|
|
@echo ""
|
|
@echo "Saved to /tmp/silero_test.mp3"
|
|
|
|
test-chatterbox:
|
|
@echo "Testing Chatterbox with emotion control..."
|
|
curl -X POST http://localhost:8000/v1/audio/speech \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"model":"tts-1-chatter","voice":"aria","input":"Testing emotional speech synthesis"}' \
|
|
-o /tmp/chatterbox_test.mp3
|
|
@echo ""
|
|
@echo "Saved to /tmp/chatterbox_test.mp3"
|
|
|
|
# ============================================================================
|
|
# Voice Downloads
|
|
# ============================================================================
|
|
|
|
voices: voices-qwen
|
|
@echo "Qwen3-TTS ready (model downloads automatically on first use)"
|
|
|
|
voices-qwen:
|
|
@echo "Downloading Qwen3-TTS voice samples from LibriSpeech..."
|
|
@if [ -d ".venv" ]; then \
|
|
.venv/bin/python scripts/download_diverse_voices.py; \
|
|
else \
|
|
python3 scripts/download_diverse_voices.py; \
|
|
fi
|
|
@echo "Qwen3-TTS voice samples ready in cloned-voices/"
|
|
@echo " Model (~3.4GB) downloads automatically on first use"
|
|
|
|
voices-f5: voices-qwen
|
|
@echo "F5-TTS reuses the same cloned-voices/ samples as Qwen3-TTS"
|
|
@echo " Model (~1.5GB: F5-TTS_v1 + Vocos) downloads automatically on first use"
|
|
|
|
whisper-refs:
|
|
@echo "Transcribing cloned-voices/*.wav with whisper-large-v3 (GPU)..."
|
|
@if [ -d ".venv" ]; then \
|
|
.venv/bin/python scripts/whisper_refs.py; \
|
|
else \
|
|
python3 scripts/whisper_refs.py; \
|
|
fi
|
|
@echo "Done. Review diff: git diff voice_to_speaker.default.yaml cloned-voices/voices_metadata.json"
|
|
|
|
voices-all: voices-qwen voices-f5 voices-piper voices-xtts voices-silero
|
|
@echo "All voices downloaded!"
|
|
|
|
voices-piper:
|
|
@echo "Downloading all Piper voices..."
|
|
sudo 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: $$name.onnx"; \
|
|
curl -f -L "$$BASE_URL/$$path/$$name.onnx.json" -o "$$name.onnx.json" || echo "Failed: $$name.onnx.json"; \
|
|
}; \
|
|
echo "=== 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 "=== 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 "=== Done ==="'
|
|
|
|
voices-xtts:
|
|
@echo "Downloading XTTS speaker samples..."
|
|
sudo docker exec $(CONTAINER_NAME) bash -c 'cd /app && ./scripts/download_samples.sh'
|
|
|
|
voices-kokoro:
|
|
@echo "Downloading Kokoro TTS models..."
|
|
sudo docker exec $(CONTAINER_NAME) bash -c '\
|
|
mkdir -p /app/voices/kokoro && \
|
|
cd /app/voices/kokoro && \
|
|
huggingface-cli download hexgrad/kokoro-82m --local-dir .'
|
|
|
|
voices-silero:
|
|
@echo "Downloading Silero TTS models..."
|
|
sudo 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}\")"'
|
|
|
|
voices-chatterbox:
|
|
@echo "Downloading Chatterbox models..."
|
|
sudo docker exec $(CONTAINER_NAME) bash -c '\
|
|
mkdir -p /app/voices/chatterbox && \
|
|
cd /app/voices/chatterbox && \
|
|
huggingface-cli download resemble-ai/chatterbox --local-dir .'
|
|
|
|
# ============================================================================
|
|
# Git
|
|
# ============================================================================
|
|
|
|
push-all:
|
|
git push origin main
|
|
git push github main
|
|
|
|
# ============================================================================
|
|
# Stress Testing
|
|
# ============================================================================
|
|
|
|
hydrate:
|
|
@echo "Hydrating all TTS models by testing ALL voices..."
|
|
@echo ""
|
|
@mkdir -p /tmp/hydrate_test
|
|
@curl -s http://localhost: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://localhost: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 ""
|
|
@mkdir -p /tmp/load_test
|
|
@curl -s http://localhost: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://localhost: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/"
|