Skip to content

Commit ba02c37

Browse files
feat: add Speechify TTS extension
1 parent fb9ed1f commit ba02c37

18 files changed

Lines changed: 1612 additions & 0 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Speechify TTS Python Extension
2+
3+
A Text-to-Speech extension for TEN Framework using the [Speechify](https://speechify.ai) API.
4+
5+
## Features
6+
7+
- Real-time text-to-speech synthesis via Speechify's `simba-3.2` streaming-native model
8+
- HTTP chunked audio streaming (`POST /v1/audio/stream`) for low-latency playback
9+
- Immediate cancellation support for flush/interrupt scenarios
10+
- Configurable audio parameters (sample rate, language, loudness/text normalization)
11+
- Audio dump functionality for debugging
12+
13+
## Architecture
14+
15+
Unlike ElevenLabs' persistent bidirectional websocket, Speechify's public API is a
16+
one-shot HTTP request/response stream: each TTS request buffers incoming text deltas
17+
until `text_input_end`, then issues a single `POST /v1/audio/stream` call whose
18+
chunked response is forwarded to TEN as they arrive. The `speechify-api` Python SDK
19+
(`AsyncSpeechify`) is used for all outbound calls, with `Speechify-Caller: ten-framework`
20+
set on every request so usage is attributed to this integration.
21+
22+
## API
23+
24+
Refer to the `api` definition in [manifest.json](manifest.json) and default values in
25+
[property.json](property.json).
26+
27+
## Development
28+
29+
### Build
30+
31+
Install dependencies:
32+
```bash
33+
pip install -r requirements.txt
34+
```
35+
36+
### Unit test
37+
38+
Run tests using pytest:
39+
```bash
40+
pytest tests/
41+
```
42+
43+
## Configuration
44+
45+
Configure the extension in `property.json`:
46+
47+
```json
48+
{
49+
"params": {
50+
"base_url": "https://api.speechify.ai",
51+
"key": "your_speechify_api_key",
52+
"model": "simba-3.2",
53+
"voice_id": "george",
54+
"sample_rate": 24000
55+
}
56+
}
57+
```
58+
59+
`key` and `voice_id` are required. `base_url` defaults to the public Speechify API and
60+
should not normally be overridden. `model` defaults to `simba-3.2`, the recommended
61+
streaming-native Simba 3 model.
62+
63+
## Documentation
64+
65+
- [API Reference](manifest.json) - Complete API specification
66+
- [Configuration](property.json) - Default configuration values
67+
- [Speechify API docs](https://docs.speechify.ai)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#
2+
# This file is part of TEN Framework, an open source project.
3+
# Licensed under the Apache License, Version 2.0.
4+
# See the LICENSE file for more information.
5+
#
6+
from . import addon
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#
2+
# This file is part of TEN Framework, an open source project.
3+
# Licensed under the Apache License, Version 2.0.
4+
# See the LICENSE file for more information.
5+
#
6+
from ten_runtime import (
7+
Addon,
8+
register_addon_as_extension,
9+
TenEnv,
10+
)
11+
12+
13+
@register_addon_as_extension("speechify_tts_python")
14+
class SpeechifyTTSExtensionAddon(Addon):
15+
16+
def on_create_instance(self, ten_env: TenEnv, name: str, context) -> None:
17+
from .extension import SpeechifyTTSExtension
18+
19+
ten_env.log_info("SpeechifyTTSExtensionAddon on_create_instance")
20+
ten_env.on_create_instance_done(SpeechifyTTSExtension(name), context)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Any, Dict, List
2+
from pydantic import BaseModel
3+
from ten_ai_base import utils
4+
5+
6+
class SpeechifyTTSConfig(BaseModel):
7+
dump: bool = False
8+
dump_path: str = "./"
9+
params: Dict[str, Any] = {}
10+
black_list_keys: List[str] = []
11+
12+
# query params
13+
sample_rate: int = 24000
14+
15+
def to_str(self, sensitive_handling: bool = False) -> str:
16+
if not sensitive_handling:
17+
return f"{self}"
18+
19+
config = self.copy(deep=True)
20+
if config.params.get("key"):
21+
config.params["key"] = utils.encrypt(config.params["key"])
22+
return f"{config}"
23+
24+
def update_params(self) -> None:
25+
# This function allows overriding default config values with 'params' from property.json
26+
# pylint: disable=no-member
27+
28+
for key, value in self.params.items():
29+
if hasattr(self, key):
30+
setattr(self, key, value)
31+
32+
# Delete keys after iteration is complete
33+
for key in self.black_list_keys:
34+
if key in self.params:
35+
del self.params[key]

0 commit comments

Comments
 (0)