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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ TOPAZLABS_API_KEY=your-topazlabs-api-key-here

# Perplexity
PERPLEXITY_API_KEY=your-perplexity-api-key-here

# Gradium
GRADIUM_API_KEY=your-gradium-api-key-here
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ jobs:
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
GRADIUM_API_KEY: ${{ secrets.GRADIUM_API_KEY }}
run: uv run pytest packages/capabilities/${{ matrix.package }}/tests/integration_tests/ -m integration -v

build:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,4 @@ uv.lock

# Security reports
bandit-report.json
mureka.md
6 changes: 4 additions & 2 deletions packages/capabilities/speech-generation/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "celeste-speech-generation"
version = "0.3.0"
version = "0.3.1"
description = "Speech generation package for Celeste AI. Unified interface for all providers"
authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}]
readme = "README.md"
Expand All @@ -17,7 +17,7 @@ classifiers = [
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Typing :: Typed",
]
keywords = ["ai", "speech-generation", "tts", "text-to-speech", "openai", "google", "elevenlabs", "audio-ai"]
keywords = ["ai", "speech-generation", "tts", "text-to-speech", "openai", "google", "elevenlabs", "gradium", "audio-ai"]

[project.urls]
Homepage = "https://withceleste.ai"
Expand All @@ -28,6 +28,8 @@ Issues = "https://github.com/withceleste/celeste-python/issues"
[tool.uv.sources]
celeste-ai = { workspace = true }
celeste-elevenlabs = { workspace = true }
celeste-google = { workspace = true }
celeste-gradium = { workspace = true }
celeste-openai = { workspace = true }

[project.entry-points."celeste.packages"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,20 @@ def register_package() -> None:
from celeste_speech_generation.providers.google.voices import ( # noqa: E402
GOOGLE_VOICES,
)
from celeste_speech_generation.providers.gradium.voices import ( # noqa: E402
GRADIUM_VOICES,
)
from celeste_speech_generation.providers.openai.voices import ( # noqa: E402
OPENAI_VOICES,
)
from celeste_speech_generation.streaming import SpeechGenerationStream # noqa: E402
from celeste_speech_generation.voices import Voice # noqa: E402

VOICES: list[Voice] = [
*ELEVENLABS_VOICES,
*GOOGLE_VOICES,
*GRADIUM_VOICES,
*OPENAI_VOICES,
*ELEVENLABS_VOICES,
]

__all__ = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
MODELS as ELEVENLABS_MODELS,
)
from celeste_speech_generation.providers.google.models import MODELS as GOOGLE_MODELS
from celeste_speech_generation.providers.gradium.models import MODELS as GRADIUM_MODELS
from celeste_speech_generation.providers.openai.models import MODELS as OPENAI_MODELS

MODELS: list[Model] = [
*GOOGLE_MODELS,
*OPENAI_MODELS,
*ELEVENLABS_MODELS,
*GRADIUM_MODELS,
]
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]:
from celeste_speech_generation.providers.google.client import (
GoogleSpeechGenerationClient,
)
from celeste_speech_generation.providers.gradium.client import (
GradiumSpeechGenerationClient,
)
from celeste_speech_generation.providers.openai.client import (
OpenAISpeechGenerationClient,
)
Expand All @@ -22,6 +25,7 @@ def _get_providers() -> list[tuple[Provider, type[Client]]]:
(Provider.GOOGLE, GoogleSpeechGenerationClient),
(Provider.OPENAI, OpenAISpeechGenerationClient),
(Provider.ELEVENLABS, ElevenLabsSpeechGenerationClient),
(Provider.GRADIUM, GradiumSpeechGenerationClient),
]


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Gradium provider for speech generation."""

from .client import GradiumSpeechGenerationClient
from .models import MODELS

__all__ = [
"MODELS",
"GradiumSpeechGenerationClient",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Gradium client implementation for speech generation."""

from typing import Any, Unpack

from celeste_gradium.text_to_speech.client import GradiumTextToSpeechClient

from celeste.artifacts import AudioArtifact
from celeste.parameters import ParameterMapper
from celeste_speech_generation.client import SpeechGenerationClient
from celeste_speech_generation.io import (
SpeechGenerationInput,
SpeechGenerationOutput,
SpeechGenerationUsage,
)
from celeste_speech_generation.parameters import SpeechGenerationParameters

from .parameters import GRADIUM_PARAMETER_MAPPERS


class GradiumSpeechGenerationClient(GradiumTextToSpeechClient, SpeechGenerationClient):
"""Gradium client for speech generation."""

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

def _init_request(self, inputs: SpeechGenerationInput) -> dict[str, Any]:
"""Initialize request from Gradium API format."""
return {"text": inputs.text}

def _parse_usage(self, response_data: dict[str, Any]) -> 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.

Note: This method is not used for Gradium TTS since we override generate()
to handle WebSocket responses. Kept for interface compliance.
"""
msg = "Gradium TTS uses WebSocket, use generate() override"
raise NotImplementedError(msg)

async def generate(
self,
*args: str,
**parameters: Unpack[SpeechGenerationParameters],
) -> SpeechGenerationOutput:
"""Generate speech from text.

Override base generate() to use WebSocket instead of HTTP.
"""
inputs = self._create_inputs(*args, **parameters)
inputs, parameters = self._validate_artifacts(inputs, **parameters)
request_body = self._build_request(inputs, **parameters)

# Use WebSocket TTS flow
audio_bytes, output_format = await self._websocket_tts(request_body)

if not audio_bytes:
msg = "No audio data in response"
raise ValueError(msg)

# Determine MIME type from output_format
mime_type = self._map_output_format_to_mime_type(output_format)

return self._output_class()(
content=AudioArtifact(data=audio_bytes, mime_type=mime_type),
usage=SpeechGenerationUsage(),
metadata=self._build_metadata({}),
)


__all__ = ["GradiumSpeechGenerationClient"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Gradium model definitions for speech generation."""

from celeste import Model, Provider
from celeste.constraints import Choice, Range
from celeste_speech_generation.constraints import VoiceConstraint
from celeste_speech_generation.parameters import SpeechGenerationParameter

from .voices import GRADIUM_VOICES

MODELS: list[Model] = [
Model(
id="default",
provider=Provider.GRADIUM,
display_name="Gradium Default TTS",
streaming=False,
parameter_constraints={
SpeechGenerationParameter.VOICE: VoiceConstraint(voices=GRADIUM_VOICES),
SpeechGenerationParameter.OUTPUT_FORMAT: Choice(
options=[
"wav",
"pcm",
"opus",
"ulaw_8000",
"alaw_8000",
"pcm_16000",
"pcm_24000",
]
),
SpeechGenerationParameter.SPEED: Range(min=0.25, max=4.0),
},
),
]

__all__ = ["MODELS"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Gradium Text-to-Speech parameter mappers for speech generation."""

from typing import Any

from celeste_gradium.text_to_speech.parameters import (
OutputFormatMapper as _OutputFormatMapper,
)
from celeste_gradium.text_to_speech.parameters import (
PaddingBonusMapper as _PaddingBonusMapper,
)
from celeste_gradium.text_to_speech.parameters import (
VoiceMapper as _VoiceMapper,
)

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


class VoiceMapper(_VoiceMapper):
name = SpeechGenerationParameter.VOICE


class OutputFormatMapper(_OutputFormatMapper):
name = SpeechGenerationParameter.OUTPUT_FORMAT


class SpeedMapper(_PaddingBonusMapper):
"""Translate unified speed to Gradium padding_bonus.

speed 1.0 → padding_bonus 0 (normal)
speed 0.5 → padding_bonus 2.0 (slower)
speed 2.0 → padding_bonus -4.0 (faster)
"""

name = SpeechGenerationParameter.SPEED

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

# Translate: speed → padding_bonus
padding_bonus = (1.0 - validated_value) * 4.0
return super().map(request, padding_bonus, model)


GRADIUM_PARAMETER_MAPPERS: list[ParameterMapper] = [
VoiceMapper(),
OutputFormatMapper(),
SpeedMapper(),
]

__all__ = ["GRADIUM_PARAMETER_MAPPERS"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Gradium voice definitions for speech generation."""

from celeste import Provider
from celeste_speech_generation.voices import Voice

# Gradium flagship voices
# Full list at https://gradium.ai/api_dpocs.html
GRADIUM_VOICES = [
# English (US)
Voice(
id="YTpq7expH9539ERJ",
provider=Provider.GRADIUM,
name="Emma",
languages={"en"},
),
Voice(
id="LFZvm12tW_z0xfGo",
provider=Provider.GRADIUM,
name="Kent",
languages={"en"},
),
Voice(
id="jtEKaLYNn6iif5PR",
provider=Provider.GRADIUM,
name="Sydney",
languages={"en"},
),
Voice(
id="KWJiFWu2O9nMPYcR",
provider=Provider.GRADIUM,
name="John",
languages={"en"},
),
# English (GB)
Voice(
id="ubuXFxVQwVYnZQhy",
provider=Provider.GRADIUM,
name="Eva",
languages={"en"},
),
Voice(
id="m86j6D7UZpGzHsNu",
provider=Provider.GRADIUM,
name="Jack",
languages={"en"},
),
# French
Voice(
id="b35yykvVppLXyw_l",
provider=Provider.GRADIUM,
name="Elise",
languages={"fr"},
),
Voice(
id="axlOaUiFyOZhy4nv",
provider=Provider.GRADIUM,
name="Leo",
languages={"fr"},
),
# German
Voice(
id="-uP9MuGtBqAvEyxI",
provider=Provider.GRADIUM,
name="Mia",
languages={"de"},
),
Voice(
id="0y1VZjPabOBU3rWy",
provider=Provider.GRADIUM,
name="Maximilian",
languages={"de"},
),
# Spanish
Voice(
id="B36pbz5_UoWn4BDl",
provider=Provider.GRADIUM,
name="Valentina",
languages={"es"},
),
Voice(
id="xu7iJ_fn2ElcWp2s",
provider=Provider.GRADIUM,
name="Sergio",
languages={"es"},
),
# Portuguese (Brazilian)
Voice(
id="pYcGZz9VOo4n2ynh",
provider=Provider.GRADIUM,
name="Alice",
languages={"pt"},
),
Voice(
id="M-FvVo9c-jGR4PgP",
provider=Provider.GRADIUM,
name="Davi",
languages={"pt"},
),
]

__all__ = ["GRADIUM_VOICES"]
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"eleven_flash_v2_5",
{"voice": "Rachel", "output_format": "mp3_44100_128"},
),
(Provider.GRADIUM, "default", {}),
],
)
@pytest.mark.integration
Expand Down
Loading
Loading