runapi-elevenlabs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runapi_elevenlabs-0.1.0/PKG-INFO +74 -0
- runapi_elevenlabs-0.1.0/README.md +61 -0
- runapi_elevenlabs-0.1.0/pyproject.toml +30 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/__init__.py +24 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/client.py +35 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/py.typed +0 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/resources/__init__.py +13 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/resources/isolate_audio.py +55 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/resources/speech_to_text.py +55 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/resources/text_to_dialogue.py +55 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/resources/text_to_sound.py +62 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/resources/text_to_speech.py +69 -0
- runapi_elevenlabs-0.1.0/src/runapi/elevenlabs/types.py +64 -0
- runapi_elevenlabs-0.1.0/tests/test_client.py +220 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runapi-elevenlabs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ElevenLabs speech, dialogue, sound, and audio client for RunAPI
|
|
5
|
+
Project-URL: Homepage, https://runapi.ai/models/elevenlabs
|
|
6
|
+
Project-URL: Documentation, https://runapi.ai/docs#sdk-elevenlabs
|
|
7
|
+
Author-email: RunAPI <contact@runapi.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: ai,audio,elevenlabs,runapi,sdk,speech-to-text,text-to-speech
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: runapi-core
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Elevenlabs API Python SDK for RunAPI
|
|
15
|
+
|
|
16
|
+
The elevenlabs api Python SDK is the language-specific package for ElevenLabs on RunAPI. Use this elevenlabs api package for voice, dialogue, transcription, sound effect, and cleanup flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
17
|
+
|
|
18
|
+
This elevenlabs api README is the Python package guide inside the public `elevenlabs-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/elevenlabs; for API reference, use https://runapi.ai/docs#elevenlabs; for SDK docs, use https://runapi.ai/docs#sdk-elevenlabs.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install runapi-elevenlabs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from runapi.elevenlabs import ElevenlabsClient
|
|
30
|
+
|
|
31
|
+
client = ElevenlabsClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
32
|
+
|
|
33
|
+
task = client.text_to_speech.create(
|
|
34
|
+
model="text-to-speech-turbo-v2.5",
|
|
35
|
+
text="Hello from RunAPI",
|
|
36
|
+
)
|
|
37
|
+
status = client.text_to_speech.get(task.id)
|
|
38
|
+
|
|
39
|
+
transcription = client.speech_to_text.create(
|
|
40
|
+
source_audio_url="https://example.com/clip.mp3",
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
result = client.text_to_speech.run(
|
|
48
|
+
model="text-to-speech-turbo-v2.5",
|
|
49
|
+
text="A calm narrator voice reading the morning news",
|
|
50
|
+
)
|
|
51
|
+
print(result.audios[0].url)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
55
|
+
|
|
56
|
+
RunAPI-generated file URLs are temporary. Download and store generated images, videos, audio, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
|
|
57
|
+
|
|
58
|
+
## Language notes
|
|
59
|
+
|
|
60
|
+
Pass parameters as keyword arguments and catch the `runapi.elevenlabs` error classes when building audio jobs or scripts. The available resources are `text_to_speech`, `text_to_dialogue`, `text_to_sound`, `speech_to_text`, and `isolate_audio`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
61
|
+
|
|
62
|
+
## Links
|
|
63
|
+
|
|
64
|
+
- Model page: https://runapi.ai/models/elevenlabs
|
|
65
|
+
- SDK docs: https://runapi.ai/docs#sdk-elevenlabs
|
|
66
|
+
- Product docs: https://runapi.ai/docs#elevenlabs
|
|
67
|
+
- Pricing and rate limits: https://runapi.ai/models/elevenlabs/text-to-speech-turbo-v2.5
|
|
68
|
+
- Provider comparison: https://runapi.ai/providers/elevenlabs
|
|
69
|
+
- Full catalog: https://runapi.ai/models
|
|
70
|
+
- Repository: https://github.com/runapi-ai/elevenlabs-sdk
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Elevenlabs API Python SDK for RunAPI
|
|
2
|
+
|
|
3
|
+
The elevenlabs api Python SDK is the language-specific package for ElevenLabs on RunAPI. Use this elevenlabs api package for voice, dialogue, transcription, sound effect, and cleanup flows when your application needs JSON request bodies, task status lookup, and consistent RunAPI errors in Python.
|
|
4
|
+
|
|
5
|
+
This elevenlabs api README is the Python package guide inside the public `elevenlabs-sdk` repository. For the repository overview, start at `../README.md`; for model details, use https://runapi.ai/models/elevenlabs; for API reference, use https://runapi.ai/docs#elevenlabs; for SDK docs, use https://runapi.ai/docs#sdk-elevenlabs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install runapi-elevenlabs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from runapi.elevenlabs import ElevenlabsClient
|
|
17
|
+
|
|
18
|
+
client = ElevenlabsClient() # reads RUNAPI_API_KEY, or pass api_key="sk-..."
|
|
19
|
+
|
|
20
|
+
task = client.text_to_speech.create(
|
|
21
|
+
model="text-to-speech-turbo-v2.5",
|
|
22
|
+
text="Hello from RunAPI",
|
|
23
|
+
)
|
|
24
|
+
status = client.text_to_speech.get(task.id)
|
|
25
|
+
|
|
26
|
+
transcription = client.speech_to_text.create(
|
|
27
|
+
source_audio_url="https://example.com/clip.mp3",
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use `create` when you want to submit a task and return quickly, `get` when you need the latest task state, and `run` when a script should create and poll until completion:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
result = client.text_to_speech.run(
|
|
35
|
+
model="text-to-speech-turbo-v2.5",
|
|
36
|
+
text="A calm narrator voice reading the morning news",
|
|
37
|
+
)
|
|
38
|
+
print(result.audios[0].url)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In web request handlers, prefer `create` plus webhook or later `get` polling so a worker is not held open.
|
|
42
|
+
|
|
43
|
+
RunAPI-generated file URLs are temporary. Download and store generated images, videos, audio, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
|
|
44
|
+
|
|
45
|
+
## Language notes
|
|
46
|
+
|
|
47
|
+
Pass parameters as keyword arguments and catch the `runapi.elevenlabs` error classes when building audio jobs or scripts. The available resources are `text_to_speech`, `text_to_dialogue`, `text_to_sound`, `speech_to_text`, and `isolate_audio`. Keep `RUNAPI_API_KEY` in the environment or your secret manager; never commit API keys or callback secrets.
|
|
48
|
+
|
|
49
|
+
## Links
|
|
50
|
+
|
|
51
|
+
- Model page: https://runapi.ai/models/elevenlabs
|
|
52
|
+
- SDK docs: https://runapi.ai/docs#sdk-elevenlabs
|
|
53
|
+
- Product docs: https://runapi.ai/docs#elevenlabs
|
|
54
|
+
- Pricing and rate limits: https://runapi.ai/models/elevenlabs/text-to-speech-turbo-v2.5
|
|
55
|
+
- Provider comparison: https://runapi.ai/providers/elevenlabs
|
|
56
|
+
- Full catalog: https://runapi.ai/models
|
|
57
|
+
- Repository: https://github.com/runapi-ai/elevenlabs-sdk
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
Licensed under the Apache License, Version 2.0.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "runapi-elevenlabs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "ElevenLabs speech, dialogue, sound, and audio client for RunAPI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "RunAPI", email = "contact@runapi.ai" }]
|
|
13
|
+
keywords = ["runapi", "elevenlabs", "text-to-speech", "speech-to-text", "audio", "ai", "sdk"]
|
|
14
|
+
dependencies = ["runapi-core"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://runapi.ai/models/elevenlabs"
|
|
18
|
+
Documentation = "https://runapi.ai/docs#sdk-elevenlabs"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/runapi"]
|
|
22
|
+
|
|
23
|
+
[tool.uv]
|
|
24
|
+
package = true
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources]
|
|
27
|
+
runapi-core = { path = "../runapi-core", editable = true }
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=8"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""ElevenLabs client for RunAPI."""
|
|
2
|
+
|
|
3
|
+
from runapi.core import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
InsufficientCreditsError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
TaskFailedError,
|
|
9
|
+
TaskTimeoutError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .client import ElevenlabsClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ElevenlabsClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""ElevenLabs client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import ClientOptions, HttpClient, resolve_api_key
|
|
8
|
+
|
|
9
|
+
from .resources.isolate_audio import IsolateAudio
|
|
10
|
+
from .resources.speech_to_text import SpeechToText
|
|
11
|
+
from .resources.text_to_dialogue import TextToDialogue
|
|
12
|
+
from .resources.text_to_sound import TextToSound
|
|
13
|
+
from .resources.text_to_speech import TextToSpeech
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ElevenlabsClient:
|
|
17
|
+
"""ElevenLabs speech, dialogue, sound, and audio client.
|
|
18
|
+
|
|
19
|
+
Example::
|
|
20
|
+
|
|
21
|
+
client = ElevenlabsClient(api_key="sk-...")
|
|
22
|
+
result = client.text_to_speech.run(
|
|
23
|
+
model="text-to-speech-turbo-v2.5", text="Hello from RunAPI"
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
28
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
29
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
30
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
31
|
+
self.text_to_speech = TextToSpeech(http)
|
|
32
|
+
self.text_to_dialogue = TextToDialogue(http)
|
|
33
|
+
self.text_to_sound = TextToSound(http)
|
|
34
|
+
self.speech_to_text = SpeechToText(http)
|
|
35
|
+
self.isolate_audio = IsolateAudio(http)
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .isolate_audio import IsolateAudio
|
|
2
|
+
from .speech_to_text import SpeechToText
|
|
3
|
+
from .text_to_dialogue import TextToDialogue
|
|
4
|
+
from .text_to_sound import TextToSound
|
|
5
|
+
from .text_to_speech import TextToSpeech
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"TextToSpeech",
|
|
9
|
+
"TextToDialogue",
|
|
10
|
+
"TextToSound",
|
|
11
|
+
"SpeechToText",
|
|
12
|
+
"IsolateAudio",
|
|
13
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""ElevenLabs isolate-audio resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, ValidationError
|
|
8
|
+
|
|
9
|
+
from ..types import AudioTaskResponse, CompletedAudioTaskResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IsolateAudio(Resource):
|
|
13
|
+
"""Isolate voice from background noise in audio with ElevenLabs models."""
|
|
14
|
+
|
|
15
|
+
ENDPOINT = "/api/v1/elevenlabs/isolate_audio"
|
|
16
|
+
|
|
17
|
+
RESPONSE_CLASS = AudioTaskResponse
|
|
18
|
+
COMPLETED_RESPONSE_CLASS = CompletedAudioTaskResponse
|
|
19
|
+
|
|
20
|
+
def run(self, **params: Any) -> Any:
|
|
21
|
+
"""Create an isolate-audio task and poll until it completes.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
**params: Isolate-audio parameters (model, prompt, ...).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The completed isolate-audio response.
|
|
28
|
+
"""
|
|
29
|
+
task = self.create(**params)
|
|
30
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
31
|
+
|
|
32
|
+
def create(self, **params: Any) -> Any:
|
|
33
|
+
"""Create an isolate-audio task and return immediately with an id.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
**params: Isolate-audio parameters (model, prompt, ...).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The task creation result with an id.
|
|
40
|
+
"""
|
|
41
|
+
compacted = self._compact_params(params)
|
|
42
|
+
if compacted.get("source_audio_url") is None:
|
|
43
|
+
raise ValidationError("source_audio_url is required")
|
|
44
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
45
|
+
|
|
46
|
+
def get(self, id: str) -> Any:
|
|
47
|
+
"""Fetch the current status of an isolate-audio task.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
id: Task id.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The current isolate-audio status.
|
|
54
|
+
"""
|
|
55
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""ElevenLabs speech-to-text resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, ValidationError
|
|
8
|
+
|
|
9
|
+
from ..types import CompletedSpeechToTextResponse, SpeechToTextResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpeechToText(Resource):
|
|
13
|
+
"""Transcribe speech audio to text with ElevenLabs models."""
|
|
14
|
+
|
|
15
|
+
ENDPOINT = "/api/v1/elevenlabs/speech_to_text"
|
|
16
|
+
|
|
17
|
+
RESPONSE_CLASS = SpeechToTextResponse
|
|
18
|
+
COMPLETED_RESPONSE_CLASS = CompletedSpeechToTextResponse
|
|
19
|
+
|
|
20
|
+
def run(self, **params: Any) -> Any:
|
|
21
|
+
"""Create a speech-to-text task and poll until it completes.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
**params: Speech-to-text parameters (model, prompt, ...).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The completed speech-to-text response.
|
|
28
|
+
"""
|
|
29
|
+
task = self.create(**params)
|
|
30
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
31
|
+
|
|
32
|
+
def create(self, **params: Any) -> Any:
|
|
33
|
+
"""Create a speech-to-text task and return immediately with an id.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
**params: Speech-to-text parameters (model, prompt, ...).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The task creation result with an id.
|
|
40
|
+
"""
|
|
41
|
+
compacted = self._compact_params(params)
|
|
42
|
+
if compacted.get("source_audio_url") is None:
|
|
43
|
+
raise ValidationError("source_audio_url is required")
|
|
44
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
45
|
+
|
|
46
|
+
def get(self, id: str) -> Any:
|
|
47
|
+
"""Fetch the current status of a speech-to-text task.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
id: Task id.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The current speech-to-text status.
|
|
54
|
+
"""
|
|
55
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""ElevenLabs text-to-dialogue resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, ValidationError
|
|
8
|
+
|
|
9
|
+
from ..types import AudioTaskResponse, CompletedAudioTaskResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TextToDialogue(Resource):
|
|
13
|
+
"""Generate multi-speaker dialogue audio with ElevenLabs models."""
|
|
14
|
+
|
|
15
|
+
ENDPOINT = "/api/v1/elevenlabs/text_to_dialogue"
|
|
16
|
+
|
|
17
|
+
RESPONSE_CLASS = AudioTaskResponse
|
|
18
|
+
COMPLETED_RESPONSE_CLASS = CompletedAudioTaskResponse
|
|
19
|
+
|
|
20
|
+
def run(self, **params: Any) -> Any:
|
|
21
|
+
"""Create a text-to-dialogue task and poll until it completes.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
**params: Text-to-dialogue parameters (model, prompt, ...).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The completed text-to-dialogue response.
|
|
28
|
+
"""
|
|
29
|
+
task = self.create(**params)
|
|
30
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
31
|
+
|
|
32
|
+
def create(self, **params: Any) -> Any:
|
|
33
|
+
"""Create a text-to-dialogue task and return immediately with an id.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
**params: Text-to-dialogue parameters (model, prompt, ...).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The task creation result with an id.
|
|
40
|
+
"""
|
|
41
|
+
compacted = self._compact_params(params)
|
|
42
|
+
if compacted.get("dialogue") is None:
|
|
43
|
+
raise ValidationError("dialogue is required")
|
|
44
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
45
|
+
|
|
46
|
+
def get(self, id: str) -> Any:
|
|
47
|
+
"""Fetch the current status of a text-to-dialogue task.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
id: Task id.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The current text-to-dialogue status.
|
|
54
|
+
"""
|
|
55
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""ElevenLabs text-to-sound resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, ValidationError
|
|
8
|
+
|
|
9
|
+
from ..types import (
|
|
10
|
+
TEXT_TO_SOUND_OUTPUT_FORMATS,
|
|
11
|
+
AudioTaskResponse,
|
|
12
|
+
CompletedAudioTaskResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextToSound(Resource):
|
|
17
|
+
"""Generate sound effects from text prompts with ElevenLabs models."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/elevenlabs/text_to_sound"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = AudioTaskResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedAudioTaskResponse
|
|
23
|
+
|
|
24
|
+
def run(self, **params: Any) -> Any:
|
|
25
|
+
"""Create a text-to-sound task and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: Text-to-sound parameters (model, prompt, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed text-to-sound response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(**params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
35
|
+
|
|
36
|
+
def create(self, **params: Any) -> Any:
|
|
37
|
+
"""Create a text-to-sound task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: Text-to-sound parameters (model, prompt, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
if compacted.get("text") is None:
|
|
47
|
+
raise ValidationError("text is required")
|
|
48
|
+
output_format = compacted.get("output_format")
|
|
49
|
+
if output_format is not None and output_format not in TEXT_TO_SOUND_OUTPUT_FORMATS:
|
|
50
|
+
raise ValidationError("Invalid output_format")
|
|
51
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
52
|
+
|
|
53
|
+
def get(self, id: str) -> Any:
|
|
54
|
+
"""Fetch the current status of a text-to-sound task.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
id: Task id.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The current text-to-sound status.
|
|
61
|
+
"""
|
|
62
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""ElevenLabs text-to-speech resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from runapi.core import Resource, ValidationError
|
|
8
|
+
|
|
9
|
+
from ..types import (
|
|
10
|
+
TEXT_TO_SPEECH_MODELS,
|
|
11
|
+
AudioTaskResponse,
|
|
12
|
+
CompletedAudioTaskResponse,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextToSpeech(Resource):
|
|
17
|
+
"""Generate speech audio from text with ElevenLabs models."""
|
|
18
|
+
|
|
19
|
+
ENDPOINT = "/api/v1/elevenlabs/text_to_speech"
|
|
20
|
+
|
|
21
|
+
RESPONSE_CLASS = AudioTaskResponse
|
|
22
|
+
COMPLETED_RESPONSE_CLASS = CompletedAudioTaskResponse
|
|
23
|
+
|
|
24
|
+
def run(self, **params: Any) -> Any:
|
|
25
|
+
"""Create a text-to-speech task and poll until it completes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**params: Text-to-speech parameters (model, prompt, ...).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The completed text-to-speech response.
|
|
32
|
+
"""
|
|
33
|
+
task = self.create(**params)
|
|
34
|
+
return self._poll_until_complete(lambda: self.get(task.id))
|
|
35
|
+
|
|
36
|
+
def create(self, **params: Any) -> Any:
|
|
37
|
+
"""Create a text-to-speech task and return immediately with an id.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
**params: Text-to-speech parameters (model, prompt, ...).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The task creation result with an id.
|
|
44
|
+
"""
|
|
45
|
+
compacted = self._compact_params(params)
|
|
46
|
+
self._validate_params(compacted)
|
|
47
|
+
return self._request("post", self.ENDPOINT, body=compacted)
|
|
48
|
+
|
|
49
|
+
def get(self, id: str) -> Any:
|
|
50
|
+
"""Fetch the current status of a text-to-speech task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: Task id.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The current text-to-speech status.
|
|
57
|
+
"""
|
|
58
|
+
return self._request("get", f"{self.ENDPOINT}/{id}")
|
|
59
|
+
|
|
60
|
+
def _validate_params(self, params: Dict[str, Any]) -> None:
|
|
61
|
+
model = params.get("model")
|
|
62
|
+
if model is None:
|
|
63
|
+
raise ValidationError("model is required")
|
|
64
|
+
if model not in TEXT_TO_SPEECH_MODELS:
|
|
65
|
+
raise ValidationError(f"Invalid model: {model}")
|
|
66
|
+
if params.get("text") is None:
|
|
67
|
+
raise ValidationError("text is required")
|
|
68
|
+
if model == "text-to-speech-multilingual-v2" and params.get("voice") is None:
|
|
69
|
+
raise ValidationError("voice is required for text-to-speech-multilingual-v2")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""ElevenLabs model lists, enums, and response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runapi.core import BaseModel, TaskResponse, optional, required
|
|
6
|
+
|
|
7
|
+
TEXT_TO_SPEECH_MODELS = [
|
|
8
|
+
"text-to-speech-turbo-v2.5",
|
|
9
|
+
"text-to-speech-multilingual-v2",
|
|
10
|
+
]
|
|
11
|
+
DEFAULT_TEXT_TO_SPEECH_VOICE = "EkK5I93UQWFDigLMpZcX"
|
|
12
|
+
TEXT_TO_SOUND_OUTPUT_FORMATS = [
|
|
13
|
+
"mp3_22050_32",
|
|
14
|
+
"mp3_44100_32",
|
|
15
|
+
"mp3_44100_64",
|
|
16
|
+
"mp3_44100_96",
|
|
17
|
+
"mp3_44100_128",
|
|
18
|
+
"mp3_44100_192",
|
|
19
|
+
"pcm_8000",
|
|
20
|
+
"pcm_16000",
|
|
21
|
+
"pcm_22050",
|
|
22
|
+
"pcm_24000",
|
|
23
|
+
"pcm_44100",
|
|
24
|
+
"pcm_48000",
|
|
25
|
+
"ulaw_8000",
|
|
26
|
+
"alaw_8000",
|
|
27
|
+
"opus_48000_32",
|
|
28
|
+
"opus_48000_64",
|
|
29
|
+
"opus_48000_96",
|
|
30
|
+
"opus_48000_128",
|
|
31
|
+
"opus_48000_192",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Audio(BaseModel):
|
|
36
|
+
url = optional(str)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AsyncTaskResponse(TaskResponse):
|
|
40
|
+
id = required(str)
|
|
41
|
+
status = optional(str, enum=lambda: TaskResponse.Status.ALL)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AudioTaskResponse(AsyncTaskResponse):
|
|
45
|
+
"""Task status/result for ElevenLabs audio generation."""
|
|
46
|
+
audios = optional([lambda: Audio])
|
|
47
|
+
error = optional(str)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SpeechToTextResponse(AsyncTaskResponse):
|
|
51
|
+
"""Task status/result for ElevenLabs speech-to-text."""
|
|
52
|
+
text = optional(str)
|
|
53
|
+
error = optional(str)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Narrowed responses returned by ``run()`` methods once polling observes
|
|
57
|
+
# ``status: "completed"``. Result fields are required so consumers never have to
|
|
58
|
+
# null-check them on a successful task.
|
|
59
|
+
class CompletedAudioTaskResponse(AudioTaskResponse):
|
|
60
|
+
audios = required([lambda: Audio])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CompletedSpeechToTextResponse(SpeechToTextResponse):
|
|
64
|
+
text = required(str)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from runapi.core import config
|
|
4
|
+
from runapi.core.errors import AuthenticationError, ValidationError
|
|
5
|
+
from runapi.elevenlabs import ElevenlabsClient
|
|
6
|
+
from runapi.elevenlabs.resources.isolate_audio import IsolateAudio
|
|
7
|
+
from runapi.elevenlabs.resources.speech_to_text import SpeechToText
|
|
8
|
+
from runapi.elevenlabs.resources.text_to_dialogue import TextToDialogue
|
|
9
|
+
from runapi.elevenlabs.resources.text_to_sound import TextToSound
|
|
10
|
+
from runapi.elevenlabs.resources.text_to_speech import TextToSpeech
|
|
11
|
+
from runapi.elevenlabs.types import (
|
|
12
|
+
AudioTaskResponse,
|
|
13
|
+
CompletedAudioTaskResponse,
|
|
14
|
+
CompletedSpeechToTextResponse,
|
|
15
|
+
SpeechToTextResponse,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakeHttp:
|
|
20
|
+
def __init__(self, *responses):
|
|
21
|
+
self._responses = list(responses)
|
|
22
|
+
self.calls = []
|
|
23
|
+
|
|
24
|
+
def request(self, method, path, body=None, options=None):
|
|
25
|
+
self.calls.append((method, path, body))
|
|
26
|
+
if self._responses:
|
|
27
|
+
return self._responses.pop(0)
|
|
28
|
+
return {"id": "task_1", "status": "pending"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(autouse=True)
|
|
32
|
+
def reset_config(monkeypatch):
|
|
33
|
+
monkeypatch.delenv("RUNAPI_API_KEY", raising=False)
|
|
34
|
+
monkeypatch.setattr(config, "api_key", None)
|
|
35
|
+
yield
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- auth -----------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_accepts_api_key_parameter():
|
|
42
|
+
assert isinstance(ElevenlabsClient(api_key="k", http_client=FakeHttp()), ElevenlabsClient)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_falls_back_to_global(monkeypatch):
|
|
46
|
+
monkeypatch.setattr(config, "api_key", "global-key")
|
|
47
|
+
assert isinstance(ElevenlabsClient(http_client=FakeHttp()), ElevenlabsClient)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_falls_back_to_env(monkeypatch):
|
|
51
|
+
monkeypatch.setenv("RUNAPI_API_KEY", "env-key")
|
|
52
|
+
assert isinstance(ElevenlabsClient(http_client=FakeHttp()), ElevenlabsClient)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_raises_without_api_key():
|
|
56
|
+
with pytest.raises(AuthenticationError, match="API key is required"):
|
|
57
|
+
ElevenlabsClient()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --- injection / accessors ------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_uses_injected_http_client():
|
|
64
|
+
fake = FakeHttp()
|
|
65
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
66
|
+
assert client.text_to_speech._http is fake
|
|
67
|
+
assert client.text_to_dialogue._http is fake
|
|
68
|
+
assert client.text_to_sound._http is fake
|
|
69
|
+
assert client.speech_to_text._http is fake
|
|
70
|
+
assert client.isolate_audio._http is fake
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_exposes_resource_accessors():
|
|
74
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
75
|
+
assert isinstance(client.text_to_speech, TextToSpeech)
|
|
76
|
+
assert isinstance(client.text_to_dialogue, TextToDialogue)
|
|
77
|
+
assert isinstance(client.text_to_sound, TextToSound)
|
|
78
|
+
assert isinstance(client.speech_to_text, SpeechToText)
|
|
79
|
+
assert isinstance(client.isolate_audio, IsolateAudio)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --- request shapes -------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_text_to_speech_create_posts_compacted_body():
|
|
86
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
87
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
88
|
+
result = client.text_to_speech.create(
|
|
89
|
+
model="text-to-speech-turbo-v2.5", text="hello", voice=None
|
|
90
|
+
)
|
|
91
|
+
assert fake.calls == [
|
|
92
|
+
("post", "/api/v1/elevenlabs/text_to_speech", {"model": "text-to-speech-turbo-v2.5", "text": "hello"}),
|
|
93
|
+
]
|
|
94
|
+
assert isinstance(result, AudioTaskResponse)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_text_to_speech_get_fetches_by_id():
|
|
98
|
+
fake = FakeHttp({"id": "t1", "status": "processing"})
|
|
99
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
100
|
+
client.text_to_speech.get("t1")
|
|
101
|
+
assert fake.calls == [("get", "/api/v1/elevenlabs/text_to_speech/t1", None)]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_text_to_dialogue_create_shape():
|
|
105
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
106
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
107
|
+
client.text_to_dialogue.create(dialogue=[{"text": "Hi", "voice": "Rachel"}])
|
|
108
|
+
assert fake.calls == [
|
|
109
|
+
("post", "/api/v1/elevenlabs/text_to_dialogue", {"dialogue": [{"text": "Hi", "voice": "Rachel"}]}),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_text_to_sound_create_shape():
|
|
114
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
115
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
116
|
+
client.text_to_sound.create(text="rain on a tin roof", output_format="mp3_44100_128")
|
|
117
|
+
assert fake.calls == [
|
|
118
|
+
("post", "/api/v1/elevenlabs/text_to_sound", {"text": "rain on a tin roof", "output_format": "mp3_44100_128"}),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_speech_to_text_create_shape():
|
|
123
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
124
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
125
|
+
result = client.speech_to_text.create(source_audio_url="https://x/a.mp3")
|
|
126
|
+
assert fake.calls == [
|
|
127
|
+
("post", "/api/v1/elevenlabs/speech_to_text", {"source_audio_url": "https://x/a.mp3"}),
|
|
128
|
+
]
|
|
129
|
+
assert isinstance(result, SpeechToTextResponse)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_isolate_audio_create_shape():
|
|
133
|
+
fake = FakeHttp({"id": "t1", "status": "pending"})
|
|
134
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
135
|
+
client.isolate_audio.create(source_audio_url="https://x/a.mp3")
|
|
136
|
+
assert fake.calls == [
|
|
137
|
+
("post", "/api/v1/elevenlabs/isolate_audio", {"source_audio_url": "https://x/a.mp3"}),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# --- run() narrowing ------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_text_to_speech_run_narrows_completed_type():
|
|
145
|
+
fake = FakeHttp(
|
|
146
|
+
{"id": "t1", "status": "pending"},
|
|
147
|
+
{"id": "t1", "status": "completed", "audios": [{"url": "https://x/y.mp3"}]},
|
|
148
|
+
)
|
|
149
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
150
|
+
result = client.text_to_speech.run(model="text-to-speech-turbo-v2.5", text="hi there")
|
|
151
|
+
assert isinstance(result, CompletedAudioTaskResponse)
|
|
152
|
+
assert result.audios[0].url == "https://x/y.mp3"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_speech_to_text_run_narrows_completed_type():
|
|
156
|
+
fake = FakeHttp(
|
|
157
|
+
{"id": "t1", "status": "pending"},
|
|
158
|
+
{"id": "t1", "status": "completed", "text": "transcribed words"},
|
|
159
|
+
)
|
|
160
|
+
client = ElevenlabsClient(api_key="k", http_client=fake)
|
|
161
|
+
result = client.speech_to_text.run(source_audio_url="https://x/a.mp3")
|
|
162
|
+
assert isinstance(result, CompletedSpeechToTextResponse)
|
|
163
|
+
assert result.text == "transcribed words"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --- validation -----------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_text_to_speech_requires_model():
|
|
170
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
171
|
+
with pytest.raises(ValidationError, match="model is required"):
|
|
172
|
+
client.text_to_speech.create(text="hi")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_text_to_speech_rejects_unknown_model():
|
|
176
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
177
|
+
with pytest.raises(ValidationError, match="Invalid model: nope"):
|
|
178
|
+
client.text_to_speech.create(model="nope", text="hi")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_text_to_speech_requires_text():
|
|
182
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
183
|
+
with pytest.raises(ValidationError, match="text is required"):
|
|
184
|
+
client.text_to_speech.create(model="text-to-speech-turbo-v2.5")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_text_to_speech_multilingual_requires_voice():
|
|
188
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
189
|
+
with pytest.raises(ValidationError, match="voice is required for text-to-speech-multilingual-v2"):
|
|
190
|
+
client.text_to_speech.create(model="text-to-speech-multilingual-v2", text="hi")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_text_to_dialogue_requires_dialogue():
|
|
194
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
195
|
+
with pytest.raises(ValidationError, match="dialogue is required"):
|
|
196
|
+
client.text_to_dialogue.create()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_text_to_sound_requires_text():
|
|
200
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
201
|
+
with pytest.raises(ValidationError, match="text is required"):
|
|
202
|
+
client.text_to_sound.create()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_text_to_sound_rejects_invalid_output_format():
|
|
206
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
207
|
+
with pytest.raises(ValidationError, match="Invalid output_format"):
|
|
208
|
+
client.text_to_sound.create(text="rain", output_format="flac_99")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_speech_to_text_requires_source_audio_url():
|
|
212
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
213
|
+
with pytest.raises(ValidationError, match="source_audio_url is required"):
|
|
214
|
+
client.speech_to_text.create()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_isolate_audio_requires_source_audio_url():
|
|
218
|
+
client = ElevenlabsClient(api_key="k", http_client=FakeHttp())
|
|
219
|
+
with pytest.raises(ValidationError, match="source_audio_url is required"):
|
|
220
|
+
client.isolate_audio.create()
|