Этап 2: озвучка ответов через Silero
- Потоковая озвучка: ответ режется на фразы, синтез и воспроизведение в отдельных потоках - Подготовка текста: числа словами, без markdown, ссылок и блоков кода - Модель Silero скачивается в LOCALAPPDATA и прогревается при старте - Состояние «говорю», переключатель озвучки в трее, «Стоп» прерывает речь - Тесты нарезки текста и оркестратора озвучки Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7933613234
commit
fec52dd1c2
@@ -0,0 +1,53 @@
|
||||
"""Воспроизведение звука с возможностью быстро прервать."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import numpy as np
|
||||
import sounddevice as sd
|
||||
|
||||
# Как часто проверяем, не пора ли остановиться
|
||||
_BLOCK_SECONDS = 0.05
|
||||
|
||||
|
||||
class AudioPlayer:
|
||||
"""Не потокобезопасен: все методы вызываются из одного потока воспроизведения."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._stream: sd.OutputStream | None = None
|
||||
|
||||
def play(
|
||||
self, audio: np.ndarray, sample_rate: int, should_continue: Callable[[], bool]
|
||||
) -> None:
|
||||
"""Блокирует до конца фрагмента; если should_continue() вернул False — обрывает звук."""
|
||||
stream = self._open(sample_rate)
|
||||
samples = np.asarray(audio, dtype=np.float32).reshape(-1, 1)
|
||||
block = max(1, int(sample_rate * _BLOCK_SECONDS))
|
||||
for start in range(0, len(samples), block):
|
||||
if not should_continue():
|
||||
self.abort()
|
||||
return
|
||||
stream.write(samples[start : start + block])
|
||||
|
||||
def finish(self) -> None:
|
||||
"""Дожидается, пока доиграет буфер, и освобождает устройство."""
|
||||
if self._stream is not None:
|
||||
stream, self._stream = self._stream, None
|
||||
stream.stop()
|
||||
stream.close()
|
||||
|
||||
def abort(self) -> None:
|
||||
"""Немедленно глушит звук и освобождает устройство."""
|
||||
if self._stream is not None:
|
||||
stream, self._stream = self._stream, None
|
||||
stream.abort()
|
||||
stream.close()
|
||||
|
||||
def _open(self, sample_rate: int) -> sd.OutputStream:
|
||||
if self._stream is not None and self._stream.samplerate != sample_rate:
|
||||
self.finish()
|
||||
if self._stream is None:
|
||||
self._stream = sd.OutputStream(samplerate=sample_rate, channels=1, dtype="float32")
|
||||
self._stream.start()
|
||||
return self._stream
|
||||
Reference in New Issue
Block a user