Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,129 +3,69 @@
import base64
from typing import Any, Unpack

import httpx
from celeste_google.generate_content import config
from celeste_google.cloud_tts.client import GoogleCloudTTSClient

from celeste.artifacts import AudioArtifact
from celeste.mime_types import ApplicationMimeType, AudioMimeType
from celeste.mime_types import AudioMimeType
from celeste.parameters import ParameterMapper
from celeste_speech_generation.client import SpeechGenerationClient
from celeste_speech_generation.io import (
SpeechGenerationInput,
SpeechGenerationUsage,
)
from celeste_speech_generation.parameters import SpeechGenerationParameters
from celeste_speech_generation.parameters import (
SpeechGenerationParameter,
SpeechGenerationParameters,
)

from .parameters import GOOGLE_PARAMETER_MAPPERS

# PCM Audio Format Specifications
# Source: Google Gemini TTS docs (ffmpeg -f s16le -ar 24000 -ac 1)
PCM_SAMPLE_RATE = 24000 # Hz
PCM_CHANNELS = 1 # Mono
PCM_SAMPLE_WIDTH = 2 # Bytes (16-bit)


class GoogleSpeechGenerationClient(SpeechGenerationClient):
class GoogleSpeechGenerationClient(GoogleCloudTTSClient, SpeechGenerationClient):
"""Google client for speech generation."""

@classmethod
def parameter_mappers(cls) -> list[ParameterMapper]:
return GOOGLE_PARAMETER_MAPPERS

def _init_request(self, inputs: SpeechGenerationInput) -> dict[str, Any]:
"""Initialize request from Google contents array format."""
contents = [
{
"parts": [{"text": inputs.text}],
}
]

generation_config: dict[str, Any] = {
"responseModalities": ["AUDIO"],
"speechConfig": {},
}

"""Initialize request from Cloud TTS API format."""
return {
"contents": contents,
"generationConfig": generation_config,
"input": {"text": inputs.text},
"voice": {"modelName": self.model.id},
"audioConfig": {},
}

def _parse_usage(self, response_data: dict[str, Any]) -> SpeechGenerationUsage:
"""Parse usage from response.

Google Gemini doesn't return usage metrics for TTS.
"""
return SpeechGenerationUsage()
"""Parse usage from response."""
usage = super()._parse_usage(response_data)
return SpeechGenerationUsage(**usage)

def _parse_content(
self,
response_data: dict[str, Any],
**parameters: Unpack[SpeechGenerationParameters],
) -> AudioArtifact:
"""Parse content from response."""
candidates = response_data.get("candidates", [])
if not candidates:
msg = "No candidates in response"
raise ValueError(msg)

candidate = candidates[0]
content = candidate.get("content", {})
parts = content.get("parts", [])

if not parts:
msg = "No parts in candidate content"
raise ValueError(msg)

inline_data = parts[0].get("inlineData", {})
base64_audio = inline_data.get("data")

if not base64_audio:
msg = "No audio data in response"
raise ValueError(msg)
audio_b64 = super()._parse_content(response_data)
audio_bytes = base64.b64decode(audio_b64)

# Decode base64 to get raw PCM bytes
# Google returns PCM audio (24kHz, 16-bit, mono)
pcm_bytes = base64.b64decode(base64_audio)
# Get output_format from parameters, default to MP3
output_format = parameters.get(SpeechGenerationParameter.OUTPUT_FORMAT)
mime_type = self._get_mime_type(output_format)

return AudioArtifact(
data=pcm_bytes,
mime_type=AudioMimeType.PCM,
metadata={
"sample_rate": PCM_SAMPLE_RATE,
"channels": PCM_CHANNELS,
"sample_width": PCM_SAMPLE_WIDTH,
},
data=audio_bytes,
mime_type=mime_type,
metadata={"format": str(mime_type)},
)

def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]:
"""Build metadata dictionary from response data."""
# Filter content field before calling super
content_fields = {"candidates"}
filtered_data = {
k: v for k, v in response_data.items() if k not in content_fields
}
return super()._build_metadata(filtered_data)

async def _make_request(
self,
request_body: dict[str, Any],
**parameters: Unpack[SpeechGenerationParameters],
) -> httpx.Response:
"""Make HTTP request(s) and return response object."""
endpoint = config.GoogleGenerateContentEndpoint.GENERATE_CONTENT.format(
model_id=self.model.id
)

headers = {
**self.auth.get_headers(),
"Content-Type": ApplicationMimeType.JSON,
}
def _get_mime_type(self, output_format: str | None) -> AudioMimeType:
"""Get AudioMimeType from output_format parameter."""
if output_format is None:
return AudioMimeType.MP3

return await self.http_client.post(
f"{config.BASE_URL}{endpoint}",
headers=headers,
json_body=request_body,
)
return AudioMimeType(output_format)


__all__ = ["GoogleSpeechGenerationClient"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Google TTS parameter mappings."""

from celeste.mime_types import AudioMimeType
from celeste_speech_generation.languages import Language

# Map Language enum values (ISO 639-1) to Google BCP-47 locale codes
LOCALE_MAP: dict[str, str] = {
Language.ARABIC: "ar-EG",
Language.GERMAN: "de-DE",
Language.ENGLISH: "en-US",
Language.SPANISH: "es-US",
Language.FRENCH: "fr-FR",
Language.HINDI: "hi-IN",
Language.INDONESIAN: "id-ID",
Language.ITALIAN: "it-IT",
Language.JAPANESE: "ja-JP",
Language.KOREAN: "ko-KR",
Language.PORTUGUESE: "pt-BR",
Language.RUSSIAN: "ru-RU",
Language.DUTCH: "nl-NL",
Language.POLISH: "pl-PL",
Language.THAI: "th-TH",
Language.TURKISH: "tr-TR",
Language.VIETNAMESE: "vi-VN",
Language.ROMANIAN: "ro-RO",
Language.UKRAINIAN: "uk-UA",
Language.TAMIL: "ta-IN",
}

ENCODING_MAP: dict[AudioMimeType, str] = {
AudioMimeType.MP3: "MP3",
AudioMimeType.WAV: "LINEAR16",
AudioMimeType.OGG: "OGG_OPUS",
AudioMimeType.PCM: "PCM",
}

__all__ = ["ENCODING_MAP", "LOCALE_MAP"]
Original file line number Diff line number Diff line change
@@ -1,31 +1,77 @@
"""Google models for speech generation."""
"""Google TTS models for speech generation."""

from celeste import Model, Provider
from celeste.constraints import Str
from celeste.constraints import Choice, Str
from celeste.mime_types import AudioMimeType
from celeste_speech_generation.constraints import VoiceConstraint
from celeste_speech_generation.languages import Language
from celeste_speech_generation.parameters import SpeechGenerationParameter

from .voices import GOOGLE_VOICES

# Supported output formats for Google TTS
GOOGLE_SUPPORTED_FORMATS = [
AudioMimeType.MP3,
AudioMimeType.WAV,
AudioMimeType.OGG,
AudioMimeType.PCM,
]

# Supported languages for Google TTS (subset of Language enum)
GOOGLE_SUPPORTED_LANGUAGES = [
Language.ARABIC,
Language.GERMAN,
Language.ENGLISH,
Language.SPANISH,
Language.FRENCH,
Language.HINDI,
Language.INDONESIAN,
Language.ITALIAN,
Language.JAPANESE,
Language.KOREAN,
Language.PORTUGUESE,
Language.RUSSIAN,
Language.DUTCH,
Language.POLISH,
Language.THAI,
Language.TURKISH,
Language.VIETNAMESE,
Language.ROMANIAN,
Language.UKRAINIAN,
Language.TAMIL,
]

MODELS: list[Model] = [
Model(
id="gemini-2.5-flash-preview-tts",
id="gemini-2.5-flash-tts",
provider=Provider.GOOGLE,
display_name="Gemini 2.5 Flash TTS (Preview)",
display_name="Google TTS Gemini 2.5 Flash",
streaming=False,
parameter_constraints={
SpeechGenerationParameter.VOICE: VoiceConstraint(voices=GOOGLE_VOICES),
SpeechGenerationParameter.LANGUAGE: Choice(
options=GOOGLE_SUPPORTED_LANGUAGES
),
SpeechGenerationParameter.PROMPT: Str(),
SpeechGenerationParameter.OUTPUT_FORMAT: Choice(
options=GOOGLE_SUPPORTED_FORMATS
),
},
),
Model(
id="gemini-2.5-pro-preview-tts",
id="gemini-2.5-pro-tts",
provider=Provider.GOOGLE,
display_name="Gemini 2.5 Pro TTS (Preview)",
display_name="Google TTS Gemini 2.5 Pro",
streaming=False,
parameter_constraints={
SpeechGenerationParameter.VOICE: VoiceConstraint(voices=GOOGLE_VOICES),
SpeechGenerationParameter.LANGUAGE: Choice(
options=GOOGLE_SUPPORTED_LANGUAGES
),
SpeechGenerationParameter.PROMPT: Str(),
SpeechGenerationParameter.OUTPUT_FORMAT: Choice(
options=GOOGLE_SUPPORTED_FORMATS
),
},
),
]
Original file line number Diff line number Diff line change
@@ -1,64 +1,47 @@
"""Google parameter mappers for speech generation."""
"""Google Cloud TTS parameter mappers for speech generation."""

from celeste_google.cloud_tts.parameters import (
AudioEncodingMapper as _AudioEncodingMapper,
)
from celeste_google.cloud_tts.parameters import (
LanguageMapper as _LanguageMapper,
)
from celeste_google.cloud_tts.parameters import (
PromptMapper as _PromptMapper,
)
from celeste_google.cloud_tts.parameters import (
VoiceMapper as _VoiceMapper,
)

from typing import Any

from celeste.models import Model
from celeste.parameters import ParameterMapper
from celeste_speech_generation.parameters import SpeechGenerationParameter

from .mappings import ENCODING_MAP, LOCALE_MAP

class VoiceMapper(ParameterMapper):
"""Map voice parameter to Google speechConfig.voiceConfig.prebuiltVoiceConfig.voiceName."""

class VoiceMapper(_VoiceMapper):
name = SpeechGenerationParameter.VOICE

def map(
self,
request: dict[str, Any],
value: object,
model: Model,
) -> dict[str, Any]:
"""Transform voice into provider request."""
validated_value = self._validate_value(value, model)
if validated_value is None:
return request

request.setdefault("generationConfig", {}).setdefault(
"speechConfig", {}
).setdefault("voiceConfig", {}).setdefault("prebuiltVoiceConfig", {})[
"voiceName"
] = validated_value
return request

class LanguageMapper(_LanguageMapper):
name = SpeechGenerationParameter.LANGUAGE
locale_map = LOCALE_MAP

class PromptMapper(ParameterMapper):
"""Map prompt parameter to style instructions in text."""

class PromptMapper(_PromptMapper):
name = SpeechGenerationParameter.PROMPT

def map(
self,
request: dict[str, Any],
value: object,
model: Model,
) -> dict[str, Any]:
"""Prepend prompt as style instruction to text using SSML-like tags."""
if not value or not isinstance(value, str):
return request

contents = request.get("contents", [])
if contents and contents[0].get("parts"):
text = contents[0]["parts"][0].get("text", "")
contents[0]["parts"][0]["text"] = (
f"<style instruction='{value}'>{text}</style>"
)

return request
class OutputFormatMapper(_AudioEncodingMapper):
name = SpeechGenerationParameter.OUTPUT_FORMAT
encoding_map = ENCODING_MAP


GOOGLE_PARAMETER_MAPPERS: list[ParameterMapper] = [
VoiceMapper(),
LanguageMapper(),
PromptMapper(),
OutputFormatMapper(),
]

__all__ = ["GOOGLE_PARAMETER_MAPPERS"]
Loading
Loading