PiperHttpTTSService: allow passing a voice id
This commit is contained in:
@@ -13,7 +13,6 @@ from typing import AsyncGenerator, AsyncIterator, Optional
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.audio.utils import create_stream_resampler
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
ErrorFrame,
|
ErrorFrame,
|
||||||
Frame,
|
Frame,
|
||||||
@@ -63,8 +62,6 @@ class PiperTTSService(TTSService):
|
|||||||
|
|
||||||
self._voice_id = voice_id
|
self._voice_id = voice_id
|
||||||
|
|
||||||
self._resampler = create_stream_resampler()
|
|
||||||
|
|
||||||
download_dir = download_dir or Path.cwd()
|
download_dir = download_dir or Path.cwd()
|
||||||
|
|
||||||
model_file = f"{voice_id}.onnx"
|
model_file = f"{voice_id}.onnx"
|
||||||
@@ -105,17 +102,12 @@ class PiperTTSService(TTSService):
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def async_iterator(iterator, sample_rate: int) -> AsyncIterator[bytes]:
|
async def async_iterator(iterator) -> AsyncIterator[bytes]:
|
||||||
while True:
|
while True:
|
||||||
item = await asyncio.to_thread(async_next, iterator)
|
item = await asyncio.to_thread(async_next, iterator)
|
||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
|
yield item.audio_int16_bytes
|
||||||
audio_data = await self._resampler.resample(
|
|
||||||
item.audio_int16_bytes, sample_rate, self.sample_rate
|
|
||||||
)
|
|
||||||
|
|
||||||
yield audio_data
|
|
||||||
|
|
||||||
logger.debug(f"{self}: Generating TTS [{text}]")
|
logger.debug(f"{self}: Generating TTS [{text}]")
|
||||||
|
|
||||||
@@ -127,8 +119,8 @@ class PiperTTSService(TTSService):
|
|||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
|
|
||||||
async for frame in self._stream_audio_frames_from_iterator(
|
async for frame in self._stream_audio_frames_from_iterator(
|
||||||
async_iterator(self._voice.synthesize(text), self._voice.config.sample_rate),
|
async_iterator(self._voice.synthesize(text)),
|
||||||
strip_wav_header=False,
|
in_sample_rate=self._voice.config.sample_rate,
|
||||||
):
|
):
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
yield frame
|
yield frame
|
||||||
@@ -143,6 +135,12 @@ class PiperTTSService(TTSService):
|
|||||||
|
|
||||||
# This assumes a running TTS service running:
|
# This assumes a running TTS service running:
|
||||||
# https://github.com/OHF-Voice/piper1-gpl/blob/main/docs/API_HTTP.md
|
# https://github.com/OHF-Voice/piper1-gpl/blob/main/docs/API_HTTP.md
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
#
|
||||||
|
# $ uv pip install "piper-tts[http]"
|
||||||
|
# $ uv run python -m piper.http_server -m en_US-ryan-high
|
||||||
|
#
|
||||||
class PiperHttpTTSService(TTSService):
|
class PiperHttpTTSService(TTSService):
|
||||||
"""Piper HTTP TTS service implementation.
|
"""Piper HTTP TTS service implementation.
|
||||||
|
|
||||||
@@ -156,9 +154,7 @@ class PiperHttpTTSService(TTSService):
|
|||||||
*,
|
*,
|
||||||
base_url: str,
|
base_url: str,
|
||||||
aiohttp_session: aiohttp.ClientSession,
|
aiohttp_session: aiohttp.ClientSession,
|
||||||
# When using Piper, the sample rate of the generated audio depends on the
|
voice_id: Optional[str] = None,
|
||||||
# voice model being used.
|
|
||||||
sample_rate: Optional[int] = None,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Initialize the Piper TTS service.
|
"""Initialize the Piper TTS service.
|
||||||
@@ -166,10 +162,10 @@ class PiperHttpTTSService(TTSService):
|
|||||||
Args:
|
Args:
|
||||||
base_url: Base URL for the Piper TTS HTTP server.
|
base_url: Base URL for the Piper TTS HTTP server.
|
||||||
aiohttp_session: aiohttp ClientSession for making HTTP requests.
|
aiohttp_session: aiohttp ClientSession for making HTTP requests.
|
||||||
sample_rate: Output sample rate. If None, uses the voice model's native rate.
|
voice_id: Piper voice model identifier (e.g. `en_US-ryan-high`).
|
||||||
**kwargs: Additional arguments passed to the parent TTSService.
|
**kwargs: Additional arguments passed to the parent TTSService.
|
||||||
"""
|
"""
|
||||||
super().__init__(sample_rate=sample_rate, **kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
if base_url.endswith("/"):
|
if base_url.endswith("/"):
|
||||||
logger.warning("Base URL ends with a slash, this is not allowed.")
|
logger.warning("Base URL ends with a slash, this is not allowed.")
|
||||||
@@ -177,7 +173,7 @@ class PiperHttpTTSService(TTSService):
|
|||||||
|
|
||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._session = aiohttp_session
|
self._session = aiohttp_session
|
||||||
self._settings = {"base_url": base_url}
|
self._model_id = voice_id
|
||||||
|
|
||||||
def can_generate_metrics(self) -> bool:
|
def can_generate_metrics(self) -> bool:
|
||||||
"""Check if this service can generate processing metrics.
|
"""Check if this service can generate processing metrics.
|
||||||
@@ -204,9 +200,12 @@ class PiperHttpTTSService(TTSService):
|
|||||||
try:
|
try:
|
||||||
await self.start_ttfb_metrics()
|
await self.start_ttfb_metrics()
|
||||||
|
|
||||||
async with self._session.post(
|
data = {
|
||||||
self._base_url, json={"text": text}, headers=headers
|
"text": text,
|
||||||
) as response:
|
"voice": self._model_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
async with self._session.post(self._base_url, json=data, headers=headers) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
error = await response.text()
|
error = await response.text()
|
||||||
yield ErrorFrame(
|
yield ErrorFrame(
|
||||||
|
|||||||
Reference in New Issue
Block a user