inworld: docstring fix

This commit is contained in:
padillamt
2025-07-18 15:13:19 -07:00
parent f3984aec33
commit 1bc442e329

View File

@@ -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,
@@ -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,17 +213,21 @@ 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
@@ -231,30 +235,28 @@ class InworldHttpTTSService(TTSService):
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)
if len(audio_chunk) > 44 and audio_chunk.startswith(b"RIFF"):
audio_data = audio_chunk[44:]
else:
audio_data = audio_chunk audio_data = audio_chunk
raw_audio_data.write(audio_data)
except json.JSONDecodeError:
continue
await self.start_tts_usage_metrics(text) # 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:]
is_first_chunk = False # Unset the flag.
audio_bytes = raw_audio_data.getvalue() # Yield each audio frame as it's processed.
if not audio_bytes: yield TTSAudioRawFrame(
logger.error("No audio data received from Inworld API") audio=audio_data,
await self.push_error(ErrorFrame("No audio data received"))
return
frame = TTSAudioRawFrame(
audio=audio_bytes,
sample_rate=self.sample_rate, sample_rate=self.sample_rate,
num_channels=1, num_channels=1,
) )
yield frame except json.JSONDecodeError:
continue
await self.start_tts_usage_metrics(text)
except Exception as e: except Exception as e:
logger.error(f"{self} exception: {e}") logger.error(f"{self} exception: {e}")
@@ -262,3 +264,83 @@ class InworldHttpTTSService(TTSService):
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()