diff --git a/.env.example b/.env.example index bc9729f..beec715 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,11 @@ TTS_SAMPLE_RATE=24000 VOICES_DIR=voices +# Path to default reference audio (relative to project root or absolute). +# Providing DEFAULT_VOICE_REF enables instant warm-up and voice cloning. +# DEFAULT_VOICE_REF=voices/rick_ref_clean.wav +# Exact transcript of the reference audio. When set, Whisper transcription is skipped. +# DEFAULT_REF_TEXT=Ва-ба-ла-ба-дап-дап! Рикки-тики-тави, сученька! И вот такие у нас новости! Иди. MIN_SEGMENT_LENGTH=30 MAX_SEGMENT_LENGTH=200 @@ -18,3 +23,6 @@ DEVICE=cuda DTYPE=bfloat16 + +# Run a dummy inference at startup to cache reference and prime CUDA. +WARMUP=true diff --git a/docs/05_usage.md b/docs/05_usage.md index e87731d..98648a6 100644 --- a/docs/05_usage.md +++ b/docs/05_usage.md @@ -29,6 +29,13 @@ # Основной режим: F5-TTS на GPU python -m voice_tts.main +# С настроенным референсом и warm-up (рекомендуется) +TTS_BACKEND=f5_tts \ +DEFAULT_VOICE_REF=voices/rick_ref_clean.wav \ +DEFAULT_REF_TEXT="Ва-ба-ла-ба-дап-дап! Рикки-тики-тави, сученька! И вот такие у нас новости! Иди." \ +WARMUP=true \ +python -m voice_tts.main + # Тестовый режим без модели TTS_BACKEND=dummy python -m voice_tts.main ``` @@ -103,6 +110,10 @@ | `MAX_BUFFER_WAIT_MS` | Макс. ожидание перед flush | `500` | | `DEVICE` | `cuda` или `cpu` | `cuda` | | `DTYPE` | `bfloat16` / `float16` | `bfloat16` | +| `DEFAULT_VOICE_REF` | Путь к референсу по умолчанию | — | +| `DEFAULT_REF_TEXT` | Точный текст референса (skip Whisper) | — | +| `WARMUP` | Прогреть CUDA и кэшировать референс | `false` | +| `WARMUP_TEXT` | Текст для warm-up | `Привет. Это тестовая фраза.` | ## Загрузка модели @@ -141,3 +152,5 @@ - Моно, 16+ кГц. - Длина 3–10 секунд (для F5-TTS). - Чистая речь одного спикера без фонового шума. +- Для мгновенного старта задайте точный `DEFAULT_REF_TEXT` — иначе сервер + будет транскрибировать референс через Whisper при первом запуске (5–6 с). diff --git a/docs/06_technical_notes.md b/docs/06_technical_notes.md index 5cb8339..fd2d5fb 100644 --- a/docs/06_technical_notes.md +++ b/docs/06_technical_notes.md @@ -53,7 +53,8 @@ ### Замеры задержки (RTX 3090, Python 3.11, CUDA 12.6) -- Первый запуск без warm-up: ~5–6 с, большая часть уходит на Whisper-транскрипцию референса. +- Первый запуск без `DEFAULT_REF_TEXT`: ~5–6 с, большая часть уходит на Whisper-транскрипцию референса. +- С `DEFAULT_REF_TEXT` и `WARMUP=true`: warm-up занимает ~2 с (загрузка модели + один инференс). - После warm-up с кэшированным референсом: первый audio-chunk ~1.1 с на коротком сегменте. - 4 сегмента подряд: первый finished ~1.1 с, последний ~4.4 с. - `stop` + возобновление работает без переподключения WebSocket. diff --git a/src/voice_tts/api/server.py b/src/voice_tts/api/server.py index da783a6..b0b62cc 100644 --- a/src/voice_tts/api/server.py +++ b/src/voice_tts/api/server.py @@ -306,15 +306,16 @@ # F5-TTS exposes an async synthesize method that blocks on CPU/CUDA work. # Inside a thread from asyncio.to_thread there is no running loop, so we # drive the coroutine with a fresh transient event loop. - return asyncio.run( - engine.synthesize( - text=text, - ref_audio_path=ref_path, - language=language, - speed=speed, - emotion=emotion, - ) + kwargs: dict = dict( + text=text, + ref_audio_path=ref_path, + language=language, + speed=speed, + emotion=emotion, ) + if isinstance(engine, F5TTSEngine) and settings.default_ref_text: + kwargs["ref_text"] = settings.default_ref_text + return asyncio.run(engine.synthesize(**kwargs)) def _stop_all(self) -> None: self._running = False diff --git a/src/voice_tts/config.py b/src/voice_tts/config.py index 7890b9e..57dedcf 100644 --- a/src/voice_tts/config.py +++ b/src/voice_tts/config.py @@ -17,7 +17,6 @@ # Reference voices directory voices_dir: Path = Path("voices") - default_voice_ref: Path | None = None # env: DEFAULT_VOICE_REF # Segmentation thresholds min_segment_length: int = 30 @@ -28,9 +27,13 @@ device: str = "cuda" # or "cpu" dtype: str = "bfloat16" + # Voice reference + default_voice_ref: Path | None = None # env: DEFAULT_VOICE_REF + default_ref_text: str | None = None # env: DEFAULT_REF_TEXT + # Warm-up warmup: bool = False # run a dummy inference at startup - warmup_text: str = "Hello world." + warmup_text: str = "Привет. Это тестовая фраза." class Config: env_file = ".env" diff --git a/src/voice_tts/tts/f5_backend.py b/src/voice_tts/tts/f5_backend.py index f1f78f9..e9cad82 100644 --- a/src/voice_tts/tts/f5_backend.py +++ b/src/voice_tts/tts/f5_backend.py @@ -83,6 +83,7 @@ self, ref_audio_path: Path | None, emotion: str, + ref_text_override: str | None = None, ) -> tuple[str, str]: if not F5_AVAILABLE: raise RuntimeError("f5-tts/torch is not installed") @@ -99,9 +100,10 @@ if not ref_audio_path.exists(): raise FileNotFoundError(f"Reference audio not found: {ref_audio_path}") + ref_text = ref_text_override or settings.default_ref_text or "" processed_audio, ref_text = preprocess_ref_audio_text( str(ref_audio_path), - "", # empty ref_text triggers automatic transcription + ref_text, ) self._ref_cache[key] = (processed_audio, ref_text) logger.info( @@ -119,6 +121,7 @@ language: str, speed: float, emotion: str, + ref_text: str | None = None, ) -> np.ndarray: if self._f5 is None: self.load() @@ -128,7 +131,7 @@ if isinstance(ref_audio_path, str): ref_audio_path = Path(ref_audio_path) - processed_audio, ref_text = self._ensure_reference(ref_audio_path, emotion) + processed_audio, ref_text = self._ensure_reference(ref_audio_path, emotion, ref_text) wav, sr, _spec = self._f5.infer( ref_file=processed_audio,