Files
Alfredv2/alfred/app/services/piper_tts.py
T

153 lines
4.4 KiB
Python

import asyncio
import os
import subprocess
import tempfile
import time
import threading # ALFRED_DUPLEX_BARGE_IN_V1
import wave
from wyoming.audio import AudioChunk, AudioStart, AudioStop
from wyoming.client import AsyncTcpClient
from wyoming.tts import Synthesize, SynthesizeVoice
PIPER_HOST = os.getenv("PIPER_HOST", "127.0.0.1")
PIPER_PORT = int(os.getenv("PIPER_PORT", "10200"))
AUDIO_DEVICE = os.getenv("AUDIO_OUTPUT_DEVICE", "plughw:1,0")
async def synthesize(text, wav_path):
client = AsyncTcpClient(PIPER_HOST, PIPER_PORT)
await client.connect()
try:
await client.write_event(
Synthesize(
text=text,
voice=SynthesizeVoice(
name=os.getenv("PIPER_VOICE", "fr_FR-tom-medium")
)).event()
)
wav_file = None
try:
while True:
event = await asyncio.wait_for(
client.read_event(),
timeout=30
)
if event is None:
raise RuntimeError("Connexion Piper interrompue")
if AudioStart.is_type(event.type):
start = AudioStart.from_event(event)
wav_file = wave.open(wav_path, "wb")
wav_file.setnchannels(start.channels)
wav_file.setsampwidth(start.width)
wav_file.setframerate(start.rate)
elif AudioChunk.is_type(event.type):
chunk = AudioChunk.from_event(event)
if wav_file is None:
wav_file = wave.open(wav_path, "wb")
wav_file.setnchannels(chunk.channels)
wav_file.setsampwidth(chunk.width)
wav_file.setframerate(chunk.rate)
wav_file.writeframes(chunk.audio)
elif AudioStop.is_type(event.type):
break
finally:
if wav_file is not None:
wav_file.close()
finally:
await client.disconnect()
# ALFRED_DUPLEX_BARGE_IN_V1: control only this module's aplay process.
_playback_lock = threading.RLock()
_playback_cancel = threading.Event()
_playback_proc = None
def stop_playback():
"""Interrupts Piper playback or marks pending synth for cancellation."""
global _playback_proc
_playback_cancel.set()
with _playback_lock:
process = _playback_proc
if process is not None and process.poll() is None:
try:
process.terminate()
except OSError:
pass
def speak(text):
if not text or not text.strip():
return
_playback_cancel.clear() # ALFRED_DUPLEX_BARGE_IN_V1
fd, wav_path = tempfile.mkstemp(
prefix="alfred_piper_",
suffix=".wav"
)
os.close(fd)
start = time.monotonic()
try:
print("[ALFRED PIPER] Synthèse...", flush=True)
asyncio.run(synthesize(text, wav_path))
print("[ALFRED PIPER] Lecture...", flush=True)
# ALFRED_DUPLEX_BARGE_IN_V1: allow a concurrent microphone detector
# to terminate playback, without killing Hermes or the entire service.
global _playback_proc
if _playback_cancel.is_set():
print("[ALFRED PIPER] Annulé avant lecture", flush=True)
return
proc = subprocess.Popen(["aplay", "-q", "-D", AUDIO_DEVICE, wav_path])
with _playback_lock:
_playback_proc = proc
if _playback_cancel.is_set() and proc.poll() is None:
proc.terminate()
try:
result = proc.wait(timeout=120)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
raise
finally:
with _playback_lock:
if _playback_proc is proc:
_playback_proc = None
if _playback_cancel.is_set():
print("[ALFRED PIPER] Lecture interrompue par la voix", flush=True)
return
if result:
raise subprocess.CalledProcessError(result, proc.args)
print(
f"[ALFRED PIPER] Terminé en {time.monotonic() - start:.2f}s",
flush=True
)
except Exception as e:
print(f"[ALFRED PIPER] Erreur : {e}", flush=True)
raise
finally:
if os.path.exists(wav_path):
os.remove(wav_path)