Files
my_speaker_RPi3/piper_TTS.py
T

362 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
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.
import socket
import json
import time
import wave
import sys
import select
import os
import logging
import pyaudio
import io
from app.core.config import get_settings
# Получаем настройки из конфигурации
config = get_settings()
tts_config = config.tts
audio_settings = config.audio_settings
# Настройки из конфигурации
HOST = tts_config.host
PORT = tts_config.port
MAX_WAIT = tts_config.timeout
PAUSE_DURATION = tts_config.pause_duration
POST_STOP_WAIT = tts_config.post_stop_wait
SAVE_TTS_FILES = tts_config.save_TTS_files
PLAYBACK_DEVICE = audio_settings.playback_device
# Директория для сохранения TTS файлов
TTS_SAVE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "recordings", "TTS")
if not os.path.exists(TTS_SAVE_DIR):
os.makedirs(TTS_SAVE_DIR, exist_ok=True)
# Список доступных голосов
VOICES = [
"ru_RU-denis-medium",
"ru_RU-irina-medium",
"ru_RU-dmitri-medium",
"ru_RU-ruslan-medium"
]
DEFAULT_VOICE = tts_config.voice
# Глобальные переменные для аудиопотока
PAUDIO = None
AUDIO_STREAM = None
DEFAULT_AUDIO_PARAMS = {"rate": 22050, "width": 2, "channels": 1}
# Используем логгер из основного приложения
logger = logging.getLogger('VADApp')
def initialize_audio(device_index=PLAYBACK_DEVICE):
"""Инициализирует аудиоустройство и воспроизводит Hello.wav."""
global PAUDIO, AUDIO_STREAM
try:
PAUDIO = pyaudio.PyAudio()
logger.info("Доступные аудиоустройства:")
for i in range(PAUDIO.get_device_count()):
device_info = PAUDIO.get_device_info_by_index(i)
logger.info("Устройство %d: %s (выходные каналы: %d)", i, device_info['name'], device_info['maxOutputChannels'])
AUDIO_STREAM = PAUDIO.open(
format=PAUDIO.get_format_from_width(DEFAULT_AUDIO_PARAMS["width"]),
channels=DEFAULT_AUDIO_PARAMS["channels"],
rate=DEFAULT_AUDIO_PARAMS["rate"],
output=True,
output_device_index=device_index,
frames_per_buffer=4096
)
logger.info("Открыт аудиопоток: частота=%d, ширина=%d, каналы=%d, устройство=%d",
DEFAULT_AUDIO_PARAMS["rate"], DEFAULT_AUDIO_PARAMS["width"],
DEFAULT_AUDIO_PARAMS["channels"], device_index)
# Воспроизведение Hello.wav
try:
with wave.open("Hello.wav", 'rb') as wf:
if (wf.getframerate() != DEFAULT_AUDIO_PARAMS["rate"] or
wf.getsampwidth() != DEFAULT_AUDIO_PARAMS["width"] or
wf.getnchannels() != DEFAULT_AUDIO_PARAMS["channels"]):
logger.warning("Параметры Hello.wav не соответствуют: частота=%d, ширина=%d, каналы=%d",
wf.getframerate(), wf.getsampwidth(), wf.getnchannels())
else:
logger.info("Воспроизведение Hello.wav")
data = wf.readframes(1024)
while data:
AUDIO_STREAM.write(data)
data = wf.readframes(1024)
# Ожидание завершения воспроизведения
while AUDIO_STREAM.get_write_available() < AUDIO_STREAM._frames_per_buffer:
time.sleep(0.01)
logger.info("Завершено воспроизведение Hello.wav")
except FileNotFoundError:
logger.error("Файл Hello.wav не найден")
except Exception as e:
logger.error("Ошибка воспроизведения Hello.wav: %s", e)
except Exception as e:
logger.error("Не удалось инициализировать аудио: %s", e)
if AUDIO_STREAM:
AUDIO_STREAM.stop_stream()
AUDIO_STREAM.close()
AUDIO_STREAM = None
if PAUDIO:
PAUDIO.terminate()
PAUDIO = None
def close_audio():
"""Закрывает аудиопоток и PyAudio."""
global PAUDIO, AUDIO_STREAM
if AUDIO_STREAM:
try:
AUDIO_STREAM.stop_stream()
AUDIO_STREAM.close()
logger.debug("Аудиопоток закрыт")
except Exception as e:
logger.error("Ошибка закрытия аудиопотока: %s", e)
AUDIO_STREAM = None
if PAUDIO:
try:
PAUDIO.terminate()
logger.debug("PyAudio завершен")
except Exception as e:
logger.error("Ошибка завершения PyAudio: %s", e)
PAUDIO = None
def play_audio_stream(stream, audio_data):
"""Воспроизводит аудиоданные через существующий аудиопоток.
Args:
stream: Открытый аудиопоток PyAudio.
audio_data: Бинарные аудиоданные для воспроизведения.
"""
if not audio_data:
logger.warning("Пустые аудиоданные, пропуск воспроизведения")
return
try:
stream.write(audio_data)
logger.debug("Воспроизведен аудиофрагмент: %d байт", len(audio_data))
except Exception as e:
logger.error("Ошибка воспроизведения: %s", e)
def send_msg(sock, typ, data_obj=None, payload=b""):
"""Отправляет сообщение в формате Wyoming Protocol."""
try:
data_bytes = json.dumps(data_obj, ensure_ascii=False).encode('utf-8') if data_obj else b''
header = {"type": typ, "data_length": len(data_bytes), "payload_length": len(payload)}
sock.sendall(json.dumps(header, separators=(',', ':')).encode('utf-8') + b'\n')
if data_bytes:
sock.sendall(data_bytes)
if payload:
sock.sendall(payload)
logger.debug("Отправлено: %s (длина_данных=%d, длина_полезной_нагрузки=%d)", typ, len(data_bytes), len(payload))
except socket.error as e:
logger.error("Ошибка отправки сообщения: %s", e)
raise
def recv_into_buffer(sock, buf, timeout):
"""Читает данные из сокета в буфер."""
r, _, _ = select.select([sock], [], [], timeout)
if not r:
return 0 # Нет данных, но не ошибка
data = sock.recv(8192)
if not data:
return -1 # EOF
buf.extend(data)
logger.debug("Получено %d байт (размер буфера: %d)", len(data), len(buf))
return len(data)
def pop_line_from_buffer(buf):
"""Извлекает JSON-линию из буфера."""
idx = buf.find(b'\n')
if idx == -1:
return None
line = bytes(buf[:idx])
del buf[:idx+1]
try:
return line.decode('utf-8')
except UnicodeDecodeError as e:
logger.warning("Ошибка декодирования UTF-8: %s", e)
return None
def read_exact_from_buffer(sock, buf, n, deadline):
"""Читает ровно n байт из буфера."""
while len(buf) < n:
timeout = max(0.0, deadline - time.time())
if timeout <= 0:
raise TimeoutError(f"Таймаут чтения (требуется %d, получено %d)", n, len(buf))
recv_len = recv_into_buffer(sock, buf, timeout)
if recv_len == -1:
raise ConnectionError("Соединение закрыто сервером")
data = bytes(buf[:n])
del buf[:n]
return data
def add_pause(audio_buf, rate, width=2, duration=PAUSE_DURATION):
"""Добавляет паузу (нулевые байты) в аудио буфер."""
samples = int(rate * duration)
bytes_per_sample = width
pause_bytes = b'\x00' * (samples * bytes_per_sample)
audio_buf.extend(pause_bytes)
logger.debug("Добавлена пауза: %d байт (%s с при %d Гц, ширина=%d)", len(pause_bytes), duration, rate, width)
def get_next_filename(prefix="tts_"):
"""Получает уникальное имя файла для сохранения TTS вывода."""
os.makedirs(TTS_SAVE_DIR, exist_ok=True)
i = 0
while True:
filename = os.path.join(TTS_SAVE_DIR, f"{prefix}{i:03d}.wav")
if not os.path.exists(filename):
return filename
i += 1
def synthesize_stream(text, voice_name=None, out_filename="out.wav"):
"""Отправляет текст на сервер Piper TTS, сохраняет и воспроизводит аудио."""
sock = socket.create_connection((HOST, PORT), timeout=10)
sock.settimeout(None)
buf = bytearray()
try:
# Отправляем потоковый запрос
send_msg(sock, "synthesize-start", {"voice": {"name": voice_name}} if voice_name else {})
send_msg(sock, "synthesize-chunk", {"text": text})
send_msg(sock, "synthesize", {"text": text}) # Для обратной совместимости
send_msg(sock, "synthesize-stop", None)
audio_buf = bytearray()
meta = {"rate": 22050, "width": 2, "channels": 1}
deadline = time.time() + MAX_WAIT
chunk_count = 0
last_activity = time.time()
while time.time() < deadline:
line = pop_line_from_buffer(buf)
if line is None:
recv_len = recv_into_buffer(sock, buf, 0.1)
if recv_len == 0:
if time.time() - last_activity > POST_STOP_WAIT:
logger.debug("Больше нет данных, завершение")
break
continue
elif recv_len == -1:
logger.debug("Соединение закрыто сервером")
break
last_activity = time.time()
continue
last_activity = time.time()
logger.debug("Заголовок сервера: %s", line)
try:
hdr = json.loads(line)
except json.JSONDecodeError as e:
logger.warning("Ошибка разбора JSON: %s", e)
continue
typ = hdr.get("type")
dl = int(hdr.get("data_length", 0))
pl = int(hdr.get("payload_length", 0))
data_json = None
if dl:
try:
data_bytes = read_exact_from_buffer(sock, buf, dl, deadline)
data_json = json.loads(data_bytes.decode('utf-8'))
logger.debug("Данные: %s", data_json)
except Exception as e:
logger.warning("Ошибка чтения данных: %s", e)
data_json = None
if typ == "audio-start" and data_json:
meta["rate"] = int(data_json.get("rate", meta["rate"]))
meta["width"] = int(data_json.get("width", meta["width"]))
meta["channels"] = int(data_json.get("channels", meta["channels"]))
logger.info("Получены аудио метаданные: частота=%d, ширина=%d, каналы=%d",
meta["rate"], meta["width"], meta["channels"])
# Проверка совместимости параметров с открытым потоком
if AUDIO_STREAM:
if (meta["rate"] != DEFAULT_AUDIO_PARAMS["rate"] or
meta["width"] != DEFAULT_AUDIO_PARAMS["width"] or
meta["channels"] != DEFAULT_AUDIO_PARAMS["channels"]):
logger.warning("Несоответствие аудио параметров: ожидалось частота=%d, ширина=%d, каналы=%d; получено частота=%d, ширина=%d, каналы=%d",
DEFAULT_AUDIO_PARAMS["rate"], DEFAULT_AUDIO_PARAMS["width"],
DEFAULT_AUDIO_PARAMS["channels"], meta["rate"], meta["width"], meta["channels"])
return None # Прерываем, так как поток несовместим
if pl:
try:
chunk = read_exact_from_buffer(sock, buf, pl, deadline)
audio_buf.extend(chunk)
chunk_count += 1
logger.debug("Получен аудио фрагмент %d (%d байт, всего %d)", chunk_count, len(chunk), len(audio_buf))
if AUDIO_STREAM:
play_audio_stream(AUDIO_STREAM, chunk)
except Exception as e:
logger.warning("Ошибка чтения полезной нагрузки: %s", e)
if typ == "audio-stop":
logger.debug("Audio-stop: добавление паузы между предложениями")
add_pause(audio_buf, meta["rate"], meta["width"])
post_deadline = time.time() + POST_STOP_WAIT
while time.time() < post_deadline:
recv_len = recv_into_buffer(sock, buf, 0.1)
if recv_len == 0:
continue
elif recv_len == -1:
break
break
last_activity = time.time()
elif typ == "error":
logger.error("Ошибка сервера: %s", data_json)
break
send_msg(sock, "synthesize-stopped", None)
# Ожидание завершения воспроизведения
if AUDIO_STREAM:
try:
while AUDIO_STREAM.get_write_available() < AUDIO_STREAM._frames_per_buffer:
time.sleep(0.01)
logger.debug("Буфер воспроизведения очищен")
except Exception as e:
logger.error("Ошибка очистки буфера воспроизведения: %s", e)
if audio_buf and SAVE_TTS_FILES:
try:
with wave.open(out_filename, "wb") as wf:
wf.setnchannels(int(meta["channels"]))
wf.setsampwidth(int(meta["width"]))
wf.setframerate(int(meta["rate"]))
wf.writeframes(bytes(audio_buf))
logger.info("Сохранено: %s (всего аудио байт: %d, фрагментов: %d)", out_filename, len(audio_buf), chunk_count)
return out_filename
except Exception as e:
logger.error("Не удалось сохранить аудио: %s", e)
return None
else:
logger.debug("Аудио не сохранено (SAVE_TTS_FILES=%s)", SAVE_TTS_FILES)
return None
except Exception as e:
logger.error("Error: %s", e)
return None
finally:
sock.close()
def synthesize_text(text, voice_name=None, out_filename=None):
"""
Синтезирует речь из текста, воспроизводит и возвращает путь к аудиофайлу.
Args:
text (str): Текст для синтеза
voice_name (str, optional): Имя голоса. По умолчанию используется голос из конфигурации.
out_filename (str, optional): Имя выходного файла. По умолчанию генерируется автоматически.
Returns:
str: Путь к созданному аудиофайлу или None в случае ошибки
"""
if not voice_name:
voice_name = DEFAULT_VOICE
if not out_filename:
out_filename = get_next_filename()
logger.info(f"Синтезирую текст: '{text}' с голосом {voice_name} в файл {out_filename}")
return synthesize_stream(text, voice_name=voice_name, out_filename=out_filename)