Files
agr-assistent/src/agr_assistent/audio/player.py
T
mrleo1nidandClaude Opus 5 fec52dd1c2 Этап 2: озвучка ответов через Silero
- Потоковая озвучка: ответ режется на фразы, синтез и воспроизведение в отдельных потоках
- Подготовка текста: числа словами, без markdown, ссылок и блоков кода
- Модель Silero скачивается в LOCALAPPDATA и прогревается при старте
- Состояние «говорю», переключатель озвучки в трее, «Стоп» прерывает речь
- Тесты нарезки текста и оркестратора озвучки

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 03:20:07 +03:00

54 lines
2.1 KiB
Python

"""Воспроизведение звука с возможностью быстро прервать."""
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