Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
@@ -0,0 +1,34 @@
# Typecast TTS Extension

Typecast text-to-speech extension for TEN Framework.

The extension uses the official `typecast-python` SDK and Typecast's
`/v1/text-to-speech/stream` endpoint. Typecast streams WAV audio at 32 kHz,
16-bit, mono. The extension strips the initial WAV header and forwards PCM16
mono audio to TEN.

## Configuration

```json
{
"params": {
"api_key": "<typecast-api-key>",
"voice_id": "tc_60e5426de8b95f1d3000d7b5",
"model": "ssfm-v30",
"output": {
"audio_format": "wav"
}
},
"chunk_size": 8192
}
```

`output.audio_format` is forced to `wav` because TEN consumes PCM audio frames.
MP3 streaming would require an additional decoder and is intentionally not used.

## Validation

```bash
tman -y install --standalone
./tests/bin/start
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#
# This file is part of TEN Framework, an open source project.
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file for more information.
#
from . import addon
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#
# This file is part of TEN Framework, an open source project.
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file for more information.
#
from ten_runtime import Addon, TenEnv, register_addon_as_extension

from .extension import TypecastTTSExtension


@register_addon_as_extension("typecast_tts_python")
class TypecastTTSExtensionAddon(Addon):
def on_create_instance(self, ten_env: TenEnv, name: str, context) -> None:
ten_env.log_info("TypecastTTSExtensionAddon on_create_instance")
ten_env.on_create_instance_done(TypecastTTSExtension(name), context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#
# This file is part of TEN Framework, an open source project.
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file for more information.
#
from pathlib import Path
from typing import Any
import copy

from pydantic import Field
from ten_ai_base import utils
from ten_ai_base.tts2_http import AsyncTTS2HttpConfig


class TypecastTTSConfig(AsyncTTS2HttpConfig):
"""Typecast TTS config."""

dump: bool = Field(default=False, description="Typecast TTS dump")
dump_path: str = Field(
default_factory=lambda: str(
Path(__file__).parent / "typecast_tts_in.pcm"
),
description="Typecast TTS dump path",
)
host: str = Field(
Comment thread
YiminW marked this conversation as resolved.
Outdated
default="https://api.typecast.ai",
description="Typecast API host",
)
params: dict[str, Any] = Field(
default_factory=dict,
description="Typecast TTS params",
)
chunk_size: int = Field(
default=8192,
ge=1,
description="Maximum bytes read from each Typecast streaming chunk",
)

def update_params(self) -> None:
if "host" in self.params:
self.host = str(
self.params.pop("host") # pylint: disable=no-member
).rstrip("/")
else:
self.host = self.host.rstrip("/")

if "model" not in self.params:
self.params["model"] = "ssfm-v30"

output = dict(
self.params.get("output") or {} # pylint: disable=no-member
)
output["audio_format"] = "wav"
self.params["output"] = output

def validate(self) -> None:
if not self.params.get("api_key"): # pylint: disable=no-member
raise ValueError("api_key is required for Typecast TTS")
if not self.params.get("voice_id"): # pylint: disable=no-member
raise ValueError("voice_id is required for Typecast TTS")
if not self.params.get("model"): # pylint: disable=no-member
raise ValueError("model is required for Typecast TTS")

def to_str(self, sensitive_handling: bool = True) -> str:
if not sensitive_handling:
return f"{self}"

config = copy.deepcopy(self)
if config.params and "api_key" in config.params:
config.params["api_key"] = utils.encrypt(config.params["api_key"])
return f"{config}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# This file is part of TEN Framework, an open source project.
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file for more information.
#
from ten_ai_base.tts2_http import (
AsyncTTS2HttpClient,
AsyncTTS2HttpConfig,
AsyncTTS2HttpExtension,
)
from ten_runtime import AsyncTenEnv

from .config import TypecastTTSConfig
from .typecast_tts import TYPECAST_STREAM_SAMPLE_RATE, TypecastTTSClient


class TypecastTTSExtension(AsyncTTS2HttpExtension):
"""Typecast TTS extension using Typecast streaming TTS."""

def __init__(self, name: str) -> None:
super().__init__(name)
self.config: TypecastTTSConfig = None
self.client: TypecastTTSClient = None

async def create_config(self, config_json_str: str) -> AsyncTTS2HttpConfig:
return TypecastTTSConfig.model_validate_json(config_json_str)

async def create_client(
self, config: AsyncTTS2HttpConfig, ten_env: AsyncTenEnv
) -> AsyncTTS2HttpClient:
return TypecastTTSClient(config=config, ten_env=ten_env)

def vendor(self) -> str:
return "typecast"

def synthesize_audio_sample_rate(self) -> int:
return TYPECAST_STREAM_SAMPLE_RATE
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"type": "extension",
"name": "typecast_tts_python",
"version": "0.1.0",
"dependencies": [
{
"type": "system",
"name": "ten_runtime_python",
"version": "0.11"
},
{
"type": "system",
"name": "ten_ai_base",
"version": "0.7"
}
],
"package": {
"include": [
"manifest.json",
"property.json",
"BUILD.gn",
"**.tent",
"**.py",
"README.md",
"pyproject.toml",
"requirements.txt"
]
},
"api": {
"interface": [
{
"import_uri": "../../system/ten_ai_base/api/tts-interface.json"
}
],
"property": {
"properties": {
"params": {
"type": "object",
"properties": {
"api_key": {
"type": "string"
},
"host": {
"type": "string"
},
"voice_id": {
"type": "string"
},
"model": {
"type": "string"
},
"language": {
"type": "string"
},
"output": {
"type": "object",
"properties": {
"audio_pitch": {
"type": "int64"
},
"audio_tempo": {
"type": "float64"
},
"audio_format": {
"type": "string"
},
"target_lufs": {
"type": "float64"
}
}
},
"seed": {
"type": "int64"
}
}
},
"chunk_size": {
"type": "int64"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#
# This file is part of TEN Framework, an open source project.
# Licensed under the Apache License, Version 2.0.
# See the LICENSE file for more information.
#
BYTES_PER_SAMPLE = 2
NUMBER_OF_CHANNELS = 1
WAV_HEADER_BYTES = 44


class StreamingWavToPcm16:
"""Strip the first WAV header and return PCM16-aligned chunks."""

def __init__(self) -> None:
self._header = bytearray()
self._remainder = bytearray()
self._header_stripped = False

def feed(self, chunk: bytes) -> bytes:
if not chunk:
return b""

if not self._header_stripped:
needed = WAV_HEADER_BYTES - len(self._header)
self._header.extend(chunk[:needed])
chunk = chunk[needed:]
if len(self._header) < WAV_HEADER_BYTES:
return b""
self._header_stripped = True

if self._remainder:
chunk = bytes(self._remainder) + chunk
self._remainder.clear()

frame_size = BYTES_PER_SAMPLE * NUMBER_OF_CHANNELS
left_size = len(chunk) % frame_size
if left_size:
self._remainder.extend(chunk[-left_size:])
chunk = chunk[:-left_size]

return bytes(chunk)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"params": {
"api_key": "",
"voice_id": "tc_60e5426de8b95f1d3000d7b5",
"model": "ssfm-v30",
"output": {
"audio_format": "wav"
}
},
"chunk_size": 8192,
"dump": false,
"dump_path": "/tmp/typecast_tts_audio_dump"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "typecast-tts-python"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"typecast-python==0.3.9",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
typecast-python==0.3.9
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

set -e

cd "$(dirname "${BASH_SOURCE[0]}")/../.."

export PYTHONPATH=.ten/app:.ten/app/ten_packages/system/ten_runtime_python/lib:.ten/app/ten_packages/system/ten_runtime_python/interface:.ten/app/ten_packages/system/ten_ai_base/interface:$PYTHONPATH

pytest -s tests/ "$@"
Loading
Loading