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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
python-version: ${{ inputs.python-version || '3.12' }}
- run: |
if [ -d "packages" ]; then
uv run mypy -p celeste && uv run mypy tests/ && uv run mypy packages/image-generation packages/text-generation packages/video-generation
uv run mypy -p celeste && uv run mypy tests/ && uv run mypy packages/image-generation packages/text-generation packages/video-generation packages/speech-generation
else
uv run mypy -p celeste && uv run mypy tests/
fi
Expand Down
77 changes: 70 additions & 7 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,74 @@ jobs:
uses: ./.github/workflows/ci.yml
secrets: inherit

integration-test:
needs: [validate-release, run-ci]
detect-changes:
needs: run-ci
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.build-matrix.outputs.packages }}
has-changes: ${{ steps.build-matrix.outputs.has-changes }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dorny/paths-filter@v3
id: filter
with:
base: ${{ github.event.before }}
filters: |
text-generation:
- 'packages/text-generation/**'
image-generation:
- 'packages/image-generation/**'
video-generation:
- 'packages/video-generation/**'
speech-generation:
- 'packages/speech-generation/**'
core:
- 'src/celeste/**'
- id: build-matrix
run: |
PACKAGES=()
CORE="${{ steps.filter.outputs.core }}"

if [[ "$CORE" == "true" || "${{ steps.filter.outputs.text-generation }}" == "true" ]]; then
PACKAGES+=("text-generation")
fi
if [[ "$CORE" == "true" || "${{ steps.filter.outputs.image-generation }}" == "true" ]]; then
PACKAGES+=("image-generation")
fi
if [[ "$CORE" == "true" || "${{ steps.filter.outputs.video-generation }}" == "true" ]]; then
PACKAGES+=("video-generation")
fi
if [[ "$CORE" == "true" || "${{ steps.filter.outputs.speech-generation }}" == "true" ]]; then
PACKAGES+=("speech-generation")
fi

# Convert to JSON array
JSON=$(printf '%s\n' "${PACKAGES[@]}" | jq -R . | jq -s -c .)
echo "packages=$JSON" >> $GITHUB_OUTPUT

if [[ ${#PACKAGES[@]} -gt 0 ]]; then
echo "has-changes=true" >> $GITHUB_OUTPUT
else
echo "has-changes=false" >> $GITHUB_OUTPUT
fi

integration-tests:
needs: [validate-release, run-ci, detect-changes]
if: needs.detect-changes.outputs.has-changes == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
environment:
name: integration-tests
strategy:
fail-fast: false
matrix:
package: ${{ fromJSON(needs.detect-changes.outputs.packages) }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: ./.github/actions/setup-python-uv
- name: Run integration tests
- name: Run ${{ matrix.package }} integration tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Expand All @@ -61,10 +118,16 @@ jobs:
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
BYTEDANCE_API_KEY: ${{ secrets.BYTEDANCE_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
run: make integration-test
ELEVENLABS_API_KEY: ${{ secrets.ELEVENLABS_API_KEY }}
run: uv run pytest packages/${{ matrix.package }}/tests/integration_tests/ -m integration -v

build:
needs: [validate-release, run-ci, integration-test]
needs: [validate-release, run-ci, detect-changes, integration-tests]
if: |
always() &&
needs.validate-release.result == 'success' &&
needs.run-ci.result == 'success' &&
(needs.integration-tests.result == 'success' || needs.integration-tests.result == 'skipped')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ format:

# Type checking (fail fast on any error)
typecheck:
@uv run mypy -p celeste && uv run mypy tests/ && uv run mypy packages/image-generation packages/text-generation packages/video-generation
@uv run mypy -p celeste && uv run mypy tests/ && uv run mypy packages/image-generation packages/text-generation packages/video-generation packages/speech-generation

# Testing
test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ def _parse_finish_reason(
"""Parse finish reason from provider response."""

def _create_inputs(
self, *args: str, **parameters: Unpack[ImageGenerationParameters]
self,
*args: str,
prompt: str | None = None,
**parameters: Unpack[ImageGenerationParameters],
) -> ImageGenerationInput:
"""Map positional arguments to Input type."""
if args:
return ImageGenerationInput(prompt=args[0])
prompt: str | None = parameters.get("prompt")
if prompt is None:
msg = (
"prompt is required (either as positional argument or keyword argument)"
Expand Down
79 changes: 79 additions & 0 deletions packages/speech-generation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<div align="center">

# <img src="../../logo.svg" width="48" height="48" alt="Celeste Logo" style="vertical-align: middle;"> Celeste Speech Generation

**Speech Generation capability for Celeste AI**

[![Python](https://img.shields.io/badge/Python-3.12+-blue?style=for-the-badge)](https://www.python.org/)
[![License](https://img.shields.io/badge/License-Apache_2.0-red?style=for-the-badge)](../../LICENSE)

[Quick Start](#-quick-start) β€’ [Documentation](https://withceleste.ai/docs) β€’ [Request Provider](https://github.com/withceleste/celeste-python/issues/new)

</div>

---

## πŸš€ Quick Start

```python
from celeste import create_client, Capability, Provider

client = create_client(
capability=Capability.SPEECH_GENERATION,
provider=Provider.ELEVENLABS,
)

response = await client.generate(text="Welcome to Celeste AI. Transform your text into natural, expressive speech with just a few lines of code.")
# response.content is an AudioArtifact with binary audio data
```

**Install:**
```bash
uv add "celeste-ai[speech-generation]"
```

---

## Supported Providers


<div align="center">

<img src="https://www.google.com/s2/favicons?domain=openai.com&sz=64" width="64" height="64" alt="OpenAI" title="OpenAI">
<img src="https://www.google.com/s2/favicons?domain=google.com&sz=64" width="64" height="64" alt="Google" title="Google">
<img src="https://www.google.com/s2/favicons?domain=elevenlabs.io&sz=64" width="64" height="64" alt="ElevenLabs" title="ElevenLabs">


**Missing a provider?** [Request it](https://github.com/withceleste/celeste-python/issues/new) – ⚑ **we ship fast**.

</div>

---

**Streaming**: βœ… Supported

**Parameters**: See [API Documentation](https://withceleste.ai/docs/api) for full parameter reference.

---

## 🀝 Contributing

See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.

**Request a provider:** [GitHub Issues](https://github.com/withceleste/celeste-python/issues/new)

---

## πŸ“„ License

Apache 2.0 License – see [LICENSE](../../LICENSE) for details.

---

<div align="center">

**[Get Started](https://withceleste.ai/docs/quickstart)** β€’ **[Documentation](https://withceleste.ai/docs)** β€’ **[GitHub](https://github.com/withceleste/celeste-python)**

Made with ❀️ by developers tired of framework lock-in

</div>
39 changes: 39 additions & 0 deletions packages/speech-generation/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[project]
name = "celeste-speech-generation"
version = "0.2.8"
description = "Speech generation package for Celeste AI. Unified interface for all providers"
authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}]
readme = "README.md"
license = {text = "Apache-2.0"}
requires-python = ">=3.12"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Typing :: Typed",
]
keywords = ["ai", "speech-generation", "tts", "text-to-speech", "openai", "google", "elevenlabs", "audio-ai"]

[project.urls]
Homepage = "https://withceleste.ai"
Documentation = "https://withceleste.ai/docs"
Repository = "https://github.com/withceleste/celeste-python"
Issues = "https://github.com/withceleste/celeste-python/issues"

[tool.uv.sources]
celeste-ai = { workspace = true }

[project.entry-points."celeste.packages"]
speech-generation = "celeste_speech_generation:register_package"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/celeste_speech_generation"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Celeste speech generation capability."""


def register_package() -> None:
"""Register speech generation package (client and models)."""
from celeste.client import register_client
from celeste.core import Capability
from celeste.models import register_models
from celeste_speech_generation.models import MODELS
from celeste_speech_generation.providers import PROVIDERS

for provider, client_class in PROVIDERS:
register_client(Capability.SPEECH_GENERATION, provider, client_class)

register_models(MODELS, capability=Capability.SPEECH_GENERATION)


from celeste_speech_generation.io import ( # noqa: E402
SpeechGenerationChunk,
SpeechGenerationInput,
SpeechGenerationOutput,
SpeechGenerationUsage,
)

# Aggregate voices from all providers (after Voice is imported)
from celeste_speech_generation.providers.elevenlabs.voices import ( # noqa: E402
ELEVENLABS_VOICES,
)
from celeste_speech_generation.providers.google.voices import ( # noqa: E402
GOOGLE_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] = [
*GOOGLE_VOICES,
*OPENAI_VOICES,
*ELEVENLABS_VOICES,
]

__all__ = [
"VOICES",
"SpeechGenerationChunk",
"SpeechGenerationInput",
"SpeechGenerationOutput",
"SpeechGenerationStream",
"SpeechGenerationUsage",
"Voice",
"register_package",
]
71 changes: 71 additions & 0 deletions packages/speech-generation/src/celeste_speech_generation/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Base client for speech generation."""

from abc import abstractmethod
from typing import Any, Unpack

import httpx

from celeste.artifacts import AudioArtifact
from celeste.client import Client
from celeste.exceptions import ValidationError
from celeste_speech_generation.io import (
SpeechGenerationInput,
SpeechGenerationOutput,
SpeechGenerationUsage,
)
from celeste_speech_generation.parameters import SpeechGenerationParameters


class SpeechGenerationClient(
Client[SpeechGenerationInput, SpeechGenerationOutput, SpeechGenerationParameters]
):
"""Client for speech generation operations."""

@abstractmethod
def _init_request(self, inputs: SpeechGenerationInput) -> dict[str, Any]:
"""Initialize provider-specific request structure."""

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

@abstractmethod
def _parse_content(
self,
response_data: dict[str, Any],
**parameters: Unpack[SpeechGenerationParameters],
) -> AudioArtifact:
"""Parse content from provider response."""

def _create_inputs(
self,
*args: str,
text: str | None = None,
**parameters: Unpack[SpeechGenerationParameters],
) -> SpeechGenerationInput:
"""Map positional arguments to Input type."""
if args:
return SpeechGenerationInput(text=args[0])
if text is None:
msg = "text is required (either as positional argument or keyword argument)"
raise ValidationError(msg)
return SpeechGenerationInput(text=text)

@classmethod
def _output_class(cls) -> type[SpeechGenerationOutput]:
"""Return the Output class for this client."""
return SpeechGenerationOutput

def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]:
"""Build metadata dictionary from response data."""
metadata = super()._build_metadata(response_data)
metadata["raw_response"] = response_data
return metadata

@abstractmethod
async def _make_request(
self,
request_body: dict[str, Any],
**parameters: Unpack[SpeechGenerationParameters],
) -> httpx.Response:
"""Make HTTP request(s) and return response object."""
Loading
Loading