inworld: docstring fix
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
"""Inworld's text-to-speech service implementations."""
|
"""Inworld's text-to-speech service implementations."""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
@@ -15,7 +16,6 @@ from typing import AsyncGenerator, List, Optional, Union
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
import io, json, base64
|
|
||||||
|
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
CancelFrame,
|
CancelFrame,
|
||||||
@@ -89,7 +89,7 @@ class InworldHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
language: Optional[Language] = Language.EN
|
language: Optional[Language] = Language.EN
|
||||||
voice_id: str = "Ashley" ## QUESTION: How to make this modifyable/how to modify?
|
voice_id: str = "Ashley" ## QUESTION: How to make this modifyable/how to modify?
|
||||||
# QUESTION: What about speed, pitch, and temperature??
|
# QUESTION: What about speed, pitch, and temperature??
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -109,9 +109,8 @@ class InworldHttpTTSService(TTSService):
|
|||||||
Args:
|
Args:
|
||||||
api_key: Inworld API key for authentication.
|
api_key: Inworld API key for authentication.
|
||||||
aiohttp_session: Shared aiohttp session for HTTP requests.
|
aiohttp_session: Shared aiohttp session for HTTP requests.
|
||||||
voice_id: ID of the voice to use for synthesis.
|
model: TTS model to use (e.g., "inworld-tts-1").
|
||||||
model: TTS model to use (e.g., "sonic-2").
|
base_url: Base URL for Inworld HTTP API.
|
||||||
endpoint_url: Base URL for Inworld HTTP API.
|
|
||||||
sample_rate: Audio sample rate. If None, uses default.
|
sample_rate: Audio sample rate. If None, uses default.
|
||||||
encoding: Audio encoding format.
|
encoding: Audio encoding format.
|
||||||
params: Additional input parameters for voice customization.
|
params: Additional input parameters for voice customization.
|
||||||
@@ -138,7 +137,6 @@ class InworldHttpTTSService(TTSService):
|
|||||||
self.set_voice(params.voice_id)
|
self.set_voice(params.voice_id)
|
||||||
self.set_model_name(model)
|
self.set_model_name(model)
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -187,6 +185,8 @@ class InworldHttpTTSService(TTSService):
|
|||||||
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
|
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
|
||||||
"""Generate speech from text using Inworld's HTTP API.
|
"""Generate speech from text using Inworld's HTTP API.
|
||||||
|
|
||||||
|
This implementation streams audio chunk by chunk as it's received.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: The text to synthesize into speech.
|
text: The text to synthesize into speech.
|
||||||
|
|
||||||
@@ -213,52 +213,134 @@ class InworldHttpTTSService(TTSService):
|
|||||||
|
|
||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
|
|
||||||
async with self._session.post(self._base_url, json=payload, headers=headers) as response:
|
# A flag to ensure we only strip the header from the very first chunk.
|
||||||
|
is_first_chunk = True
|
||||||
|
|
||||||
|
async with self._session.post(
|
||||||
|
self._base_url, json=payload, headers=headers
|
||||||
|
) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
error_text = await response.text()
|
error_text = await response.text()
|
||||||
logger.error(f"Inworld API error: {error_text}")
|
logger.error(f"Inworld API error: {error_text}")
|
||||||
await self.push_error(ErrorFrame(f"Inworld API error: {error_text}"))
|
await self.push_error(ErrorFrame(f"Inworld API error: {error_text}"))
|
||||||
return
|
return
|
||||||
|
|
||||||
raw_audio_data = io.BytesIO()
|
# Process the stream line by line.
|
||||||
|
|
||||||
async for line in response.content.iter_lines():
|
async for line in response.content.iter_lines():
|
||||||
line_str = line.decode('utf-8').strip()
|
line_str = line.decode("utf-8").strip()
|
||||||
if not line_str:
|
if not line_str:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
chunk = json.loads(line_str)
|
chunk = json.loads(line_str)
|
||||||
if "result" in chunk and "audioContent" in chunk["result"]:
|
if "result" in chunk and "audioContent" in chunk["result"]:
|
||||||
audio_chunk = base64.b64decode(chunk["result"]["audioContent"])
|
audio_chunk = base64.b64decode(chunk["result"]["audioContent"])
|
||||||
# Skip WAV header if present (first 44 bytes)
|
audio_data = audio_chunk
|
||||||
if len(audio_chunk) > 44 and audio_chunk.startswith(b"RIFF"):
|
|
||||||
|
# Correctly strip the header only from the first chunk.
|
||||||
|
if (
|
||||||
|
is_first_chunk
|
||||||
|
and len(audio_chunk) > 44
|
||||||
|
and audio_chunk.startswith(b"RIFF")
|
||||||
|
):
|
||||||
audio_data = audio_chunk[44:]
|
audio_data = audio_chunk[44:]
|
||||||
else:
|
is_first_chunk = False # Unset the flag.
|
||||||
audio_data = audio_chunk
|
|
||||||
raw_audio_data.write(audio_data)
|
# Yield each audio frame as it's processed.
|
||||||
|
yield TTSAudioRawFrame(
|
||||||
|
audio=audio_data,
|
||||||
|
sample_rate=self.sample_rate,
|
||||||
|
num_channels=1,
|
||||||
|
)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
await self.start_tts_usage_metrics(text)
|
await self.start_tts_usage_metrics(text)
|
||||||
|
|
||||||
audio_bytes = raw_audio_data.getvalue()
|
|
||||||
if not audio_bytes:
|
|
||||||
logger.error("No audio data received from Inworld API")
|
|
||||||
await self.push_error(ErrorFrame("No audio data received"))
|
|
||||||
return
|
|
||||||
|
|
||||||
frame = TTSAudioRawFrame(
|
|
||||||
audio=audio_bytes,
|
|
||||||
sample_rate=self.sample_rate,
|
|
||||||
num_channels=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
yield frame
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self} exception: {e}")
|
logger.error(f"{self} exception: {e}")
|
||||||
await self.push_error(ErrorFrame(f"Error generating TTS: {e}"))
|
await self.push_error(ErrorFrame(f"Error generating TTS: {e}"))
|
||||||
finally:
|
finally:
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
|
|
||||||
|
# @traced_tts
|
||||||
|
# async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
|
||||||
|
# """Generate speech from text using Inworld's HTTP API.
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# text: The text to synthesize into speech.
|
||||||
|
|
||||||
|
# Yields:
|
||||||
|
# Frame: Audio frames containing the synthesized speech.
|
||||||
|
# """
|
||||||
|
# logger.debug(f"{self}: Generating TTS [{text}]")
|
||||||
|
|
||||||
|
# payload = {
|
||||||
|
# "text": text,
|
||||||
|
# "voiceId": self._settings["voiceId"],
|
||||||
|
# "modelId": self._settings["modelId"],
|
||||||
|
# "audio_config": self._settings["audio_config"],
|
||||||
|
# "language": self._settings["language"],
|
||||||
|
# }
|
||||||
|
|
||||||
|
# headers = {
|
||||||
|
# "Authorization": f"Basic {self._api_key}",
|
||||||
|
# "Content-Type": "application/json",
|
||||||
|
# }
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# await self.start_ttfb_metrics()
|
||||||
|
|
||||||
|
# yield TTSStartedFrame()
|
||||||
|
|
||||||
|
# async with self._session.post(self._base_url, json=payload, headers=headers) as response:
|
||||||
|
# if response.status != 200:
|
||||||
|
# error_text = await response.text()
|
||||||
|
# logger.error(f"Inworld API error: {error_text}")
|
||||||
|
# await self.push_error(ErrorFrame(f"Inworld API error: {error_text}"))
|
||||||
|
# return
|
||||||
|
|
||||||
|
# raw_audio_data = io.BytesIO()
|
||||||
|
|
||||||
|
# async for line in response.content.iter_lines():
|
||||||
|
# line_str = line.decode('utf-8').strip()
|
||||||
|
# if not line_str:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# chunk = json.loads(line_str)
|
||||||
|
# if "result" in chunk and "audioContent" in chunk["result"]:
|
||||||
|
# audio_chunk = base64.b64decode(chunk["result"]["audioContent"])
|
||||||
|
# # Skip WAV header if present (first 44 bytes)
|
||||||
|
# if len(audio_chunk) > 44 and audio_chunk.startswith(b"RIFF"):
|
||||||
|
# audio_data = audio_chunk[44:]
|
||||||
|
# else:
|
||||||
|
# audio_data = audio_chunk
|
||||||
|
# raw_audio_data.write(audio_data)
|
||||||
|
# except json.JSONDecodeError:
|
||||||
|
# continue
|
||||||
|
|
||||||
|
# await self.start_tts_usage_metrics(text)
|
||||||
|
|
||||||
|
# audio_bytes = raw_audio_data.getvalue()
|
||||||
|
# if not audio_bytes:
|
||||||
|
# logger.error("No audio data received from Inworld API")
|
||||||
|
# await self.push_error(ErrorFrame("No audio data received"))
|
||||||
|
# return
|
||||||
|
|
||||||
|
# frame = TTSAudioRawFrame(
|
||||||
|
# audio=audio_bytes,
|
||||||
|
# sample_rate=self.sample_rate,
|
||||||
|
# num_channels=1,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# yield frame
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.error(f"{self} exception: {e}")
|
||||||
|
# await self.push_error(ErrorFrame(f"Error generating TTS: {e}"))
|
||||||
|
# finally:
|
||||||
|
# await self.stop_ttfb_metrics()
|
||||||
|
# yield TTSStoppedFrame()
|
||||||
|
|||||||
Reference in New Issue
Block a user