livekit-plugins-vakyam 1.8.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.
@@ -0,0 +1,180 @@
1
+ **/.vscode
2
+ **/.DS_Store
3
+ .env
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # trunk
151
+ .trunk/
152
+
153
+ # Pyre type checker
154
+ .pyre/
155
+
156
+ # pytype static type analyzer
157
+ .pytype/
158
+
159
+ # Cython debug symbols
160
+ cython_debug/
161
+
162
+ # PyCharm
163
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
164
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
165
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
166
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
167
+ .idea/
168
+
169
+ node_modules
170
+
171
+ credentials.json
172
+ pyrightconfig.json
173
+ docs/
174
+
175
+ # Database files
176
+ *.db
177
+
178
+
179
+ # Examples for development
180
+ examples/dev/*
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.5
2
+ Name: livekit-plugins-vakyam
3
+ Version: 1.8.0
4
+ Summary: LiveKit Agents plugin for Vakyam AI TTS (Raaga 1) — Indian-language text-to-speech
5
+ Project-URL: Documentation, https://docs.livekit.io
6
+ Project-URL: Website, https://livekit.io/
7
+ Project-URL: Source, https://github.com/livekit/agents
8
+ Author-email: LiveKit <hello@livekit.io>
9
+ License-Expression: Apache-2.0
10
+ Keywords: audio,indian-languages,livekit,raaga,realtime,text-to-speech,tts,vakyam,webrtc
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Multimedia :: Sound/Audio
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.10.0
21
+ Requires-Dist: livekit-agents[codecs]>=1.8.0
22
+ Requires-Dist: websockets<16.0,>=14.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Vakyam AI plugin for LiveKit Agents
26
+
27
+ Support for voice synthesis with [Vakyam AI](https://vakyam.ai/) Raaga 1 —
28
+ text-to-speech for Indian languages.
29
+
30
+ See [https://docs.vakyam.ai/integrations/livekit](https://docs.vakyam.ai/integrations/livekit)
31
+ for provider docs.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install livekit-plugins-vakyam
37
+ ```
38
+
39
+ Or with the LiveKit Agents extra:
40
+
41
+ ```bash
42
+ uv add "livekit-agents[vakyam]"
43
+ ```
44
+
45
+ ## Pre-requisites
46
+
47
+ You'll need an API key from [Vakyam](https://dashboard.vakyam.ai/api-keys).
48
+ Set it as an environment variable:
49
+
50
+ ```bash
51
+ export VAKYAM_API_KEY="vak_live_..."
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ```python
57
+ from livekit.agents import AgentSession
58
+ from livekit.plugins import vakyam
59
+
60
+ session = AgentSession(
61
+ tts=vakyam.TTS(
62
+ model="raaga-v1",
63
+ voice="Archana",
64
+ language="ta-IN",
65
+ sample_rate=24000,
66
+ ),
67
+ # ... stt, llm, vad
68
+ )
69
+ ```
70
+
71
+ `stream()` uses the realtime WebSocket API and sentence-tokenizes LLM text so
72
+ each utterance is one complete sentence (Vakyam does not accept partial
73
+ tokens). `synthesize()` uses HTTP streaming (`POST /v1/tts/stream`) and
74
+ returns PCM audio.
75
+
76
+ WebSocket connections are pooled and reused between sequential agent turns.
77
+ Each active synthesis stream has exclusive ownership of its connection, so an
78
+ overlapping stream uses a separate connection. On interruption, the plugin
79
+ sends `cancel`, drains through Vakyam's cancellation acknowledgement, and
80
+ returns the healthy connection to the pool.
@@ -0,0 +1,56 @@
1
+ # Vakyam AI plugin for LiveKit Agents
2
+
3
+ Support for voice synthesis with [Vakyam AI](https://vakyam.ai/) Raaga 1 —
4
+ text-to-speech for Indian languages.
5
+
6
+ See [https://docs.vakyam.ai/integrations/livekit](https://docs.vakyam.ai/integrations/livekit)
7
+ for provider docs.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install livekit-plugins-vakyam
13
+ ```
14
+
15
+ Or with the LiveKit Agents extra:
16
+
17
+ ```bash
18
+ uv add "livekit-agents[vakyam]"
19
+ ```
20
+
21
+ ## Pre-requisites
22
+
23
+ You'll need an API key from [Vakyam](https://dashboard.vakyam.ai/api-keys).
24
+ Set it as an environment variable:
25
+
26
+ ```bash
27
+ export VAKYAM_API_KEY="vak_live_..."
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```python
33
+ from livekit.agents import AgentSession
34
+ from livekit.plugins import vakyam
35
+
36
+ session = AgentSession(
37
+ tts=vakyam.TTS(
38
+ model="raaga-v1",
39
+ voice="Archana",
40
+ language="ta-IN",
41
+ sample_rate=24000,
42
+ ),
43
+ # ... stt, llm, vad
44
+ )
45
+ ```
46
+
47
+ `stream()` uses the realtime WebSocket API and sentence-tokenizes LLM text so
48
+ each utterance is one complete sentence (Vakyam does not accept partial
49
+ tokens). `synthesize()` uses HTTP streaming (`POST /v1/tts/stream`) and
50
+ returns PCM audio.
51
+
52
+ WebSocket connections are pooled and reused between sequential agent turns.
53
+ Each active synthesis stream has exclusive ownership of its connection, so an
54
+ overlapping stream uses a separate connection. On interruption, the plugin
55
+ sends `cancel`, drains through Vakyam's cancellation acknowledgement, and
56
+ returns the healthy connection to the pool.
@@ -0,0 +1,45 @@
1
+ # Copyright 2025 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Vakyam AI plugin for LiveKit Agents
16
+
17
+ Support for text-to-speech with [Vakyam AI](https://vakyam.ai/) Raaga 1.
18
+
19
+ See https://docs.vakyam.ai/guides/realtime-websocket for protocol details.
20
+ """
21
+
22
+ from .tts import TTS, ChunkedStream, SynthesizeStream
23
+ from .version import __version__
24
+
25
+ __all__ = ["TTS", "ChunkedStream", "SynthesizeStream", "__version__"]
26
+
27
+ from livekit.agents import Plugin
28
+
29
+ from .log import logger
30
+
31
+
32
+ class VakyamPlugin(Plugin):
33
+ def __init__(self) -> None:
34
+ super().__init__(__name__, __version__, __package__, logger)
35
+
36
+
37
+ Plugin.register_plugin(VakyamPlugin())
38
+
39
+ _module = dir()
40
+ NOT_IN_ALL = [m for m in _module if m not in __all__]
41
+
42
+ __pdoc__ = {}
43
+
44
+ for n in NOT_IN_ALL:
45
+ __pdoc__[n] = False
@@ -0,0 +1,214 @@
1
+ # Copyright 2025 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from typing import Any
19
+ from urllib.parse import urlparse
20
+
21
+ from livekit.agents import APIStatusError
22
+
23
+ from .models import (
24
+ CUSTOM_VOICE_PREFIX,
25
+ MAX_SPEED,
26
+ MAX_TEXT_CHARACTERS,
27
+ MIN_SPEED,
28
+ SUPPORTED_LANGUAGES,
29
+ SUPPORTED_MODELS,
30
+ SUPPORTED_SAMPLE_RATES,
31
+ TTS_STREAM_PATH,
32
+ TTS_WEBSOCKET_PATH,
33
+ )
34
+
35
+ _RETRYABLE_WS_CODES = {
36
+ "rate_limit_exceeded",
37
+ "concurrency_limit_exceeded",
38
+ "tts_workers_busy",
39
+ "tts_workers_unconfigured",
40
+ "tts_workers_unavailable",
41
+ "internal_error",
42
+ }
43
+
44
+
45
+ def normalize_voice(voice: str) -> str:
46
+ """Normalize a preset voice name or custom ``vc_`` voice ID."""
47
+ if not isinstance(voice, str) or not voice.strip():
48
+ raise ValueError("voice must be a non-empty string")
49
+ normalized = voice.strip()
50
+ if normalized == CUSTOM_VOICE_PREFIX:
51
+ raise ValueError("custom voice IDs must include a value after 'vc_'")
52
+ return normalized
53
+
54
+
55
+ def normalize_base_url(base_url: str, *, allow_insecure_base_url: bool = False) -> str:
56
+ """Normalize and validate a public API base URL."""
57
+ normalized = base_url.rstrip("/")
58
+ parsed = urlparse(normalized)
59
+ if parsed.scheme != "http":
60
+ return normalized
61
+
62
+ host = parsed.hostname or ""
63
+ if allow_insecure_base_url or host in {"localhost", "127.0.0.1", "::1"}:
64
+ return normalized
65
+
66
+ raise ValueError(
67
+ "base_url must use HTTPS unless it points to localhost. "
68
+ "Pass allow_insecure_base_url=True only for trusted development networks."
69
+ )
70
+
71
+
72
+ def websocket_url(base_url: str) -> str:
73
+ """Build the Vakyam TTS WebSocket URL from an HTTP(S) or WS(S) base URL."""
74
+ root = base_url.rstrip("/")
75
+ if root.startswith("https://"):
76
+ root = "wss://" + root[len("https://") :]
77
+ elif root.startswith("http://"):
78
+ root = "ws://" + root[len("http://") :]
79
+ elif not root.startswith(("ws://", "wss://")):
80
+ root = "wss://" + root
81
+ if root.endswith(TTS_WEBSOCKET_PATH):
82
+ return root
83
+ return root + TTS_WEBSOCKET_PATH
84
+
85
+
86
+ def http_stream_url(base_url: str) -> str:
87
+ """Build the Vakyam HTTP streaming TTS URL."""
88
+ return base_url.rstrip("/") + TTS_STREAM_PATH
89
+
90
+
91
+ def validate_tts_options(
92
+ *,
93
+ model: str,
94
+ language: str,
95
+ sample_rate: int,
96
+ speed: float,
97
+ voice: str,
98
+ ) -> None:
99
+ """Validate constructor / update_options values."""
100
+ if model not in SUPPORTED_MODELS:
101
+ valid = ", ".join(sorted(SUPPORTED_MODELS))
102
+ raise ValueError(f"model '{model}' is not supported. Valid values are: {valid}.")
103
+ if language not in SUPPORTED_LANGUAGES:
104
+ valid = ", ".join(sorted(SUPPORTED_LANGUAGES))
105
+ raise ValueError(f"language '{language}' is not supported. Valid values are: {valid}.")
106
+ if sample_rate not in SUPPORTED_SAMPLE_RATES:
107
+ valid = ", ".join(str(v) for v in sorted(SUPPORTED_SAMPLE_RATES))
108
+ raise ValueError(
109
+ f"sample_rate '{sample_rate}' is not supported. Valid values are: {valid}."
110
+ )
111
+ if not MIN_SPEED <= speed <= MAX_SPEED:
112
+ raise ValueError(f"speed must be between {MIN_SPEED} and {MAX_SPEED}")
113
+ normalize_voice(voice)
114
+
115
+
116
+ def validate_text(text: str) -> None:
117
+ """Validate utterance text for a single synthesis request."""
118
+ if not isinstance(text, str) or not text:
119
+ raise ValueError("text is required")
120
+ character_count = len(text)
121
+ if character_count > MAX_TEXT_CHARACTERS:
122
+ raise ValueError(
123
+ f"Input text is {character_count} characters. Maximum allowed is "
124
+ f"{MAX_TEXT_CHARACTERS} Unicode characters."
125
+ )
126
+
127
+
128
+ def split_text(text: str, *, max_characters: int = MAX_TEXT_CHARACTERS) -> list[str]:
129
+ """Split an oversized utterance at whitespace, falling back to a hard boundary."""
130
+ if max_characters <= 0:
131
+ raise ValueError("max_characters must be greater than zero")
132
+
133
+ remaining = text.strip()
134
+ chunks: list[str] = []
135
+ while len(remaining) > max_characters:
136
+ split_at = remaining.rfind(" ", 0, max_characters + 1)
137
+ if split_at <= 0:
138
+ split_at = max_characters
139
+ chunk = remaining[:split_at].strip()
140
+ if chunk:
141
+ chunks.append(chunk)
142
+ remaining = remaining[split_at:].lstrip()
143
+ if remaining:
144
+ chunks.append(remaining)
145
+ return chunks
146
+
147
+
148
+ def speech_payload(
149
+ *,
150
+ text: str,
151
+ model: str,
152
+ voice: str,
153
+ language: str,
154
+ sample_rate: int,
155
+ speed: float,
156
+ output_format: str = "pcm",
157
+ ) -> dict[str, Any]:
158
+ """JSON body for HTTP generate/stream requests."""
159
+ validate_text(text)
160
+ validate_tts_options(
161
+ model=model, language=language, sample_rate=sample_rate, speed=speed, voice=voice
162
+ )
163
+ return {
164
+ "text": text,
165
+ "model_id": model,
166
+ "voice": normalize_voice(voice),
167
+ "language": language,
168
+ "output_format": output_format,
169
+ "sample_rate": sample_rate,
170
+ "speed": speed,
171
+ }
172
+
173
+
174
+ def raise_http_error(status: int, body: str) -> None:
175
+ """Raise ``APIStatusError`` from a Vakyam HTTP error envelope."""
176
+ parsed: object | None = None
177
+ try:
178
+ parsed = json.loads(body) if body else None
179
+ except json.JSONDecodeError:
180
+ parsed = None
181
+
182
+ error_code: str | None = None
183
+ if isinstance(parsed, dict) and isinstance(parsed.get("error"), dict):
184
+ error = parsed["error"]
185
+ code = error.get("code")
186
+ if isinstance(code, (str, int)):
187
+ error_code = str(code)
188
+
189
+ message = f"Vakyam TTS request failed with status {status}"
190
+ safe_body: dict[str, object] = {"status_code": status}
191
+ if error_code is not None:
192
+ message += f" (error code: {error_code})"
193
+ safe_body["error_code"] = error_code
194
+
195
+ raise APIStatusError(message, status_code=status, body=safe_body)
196
+
197
+
198
+ def raise_ws_error(data: dict[str, Any]) -> None:
199
+ """Raise ``APIStatusError`` from a Vakyam WebSocket ``error`` frame."""
200
+ error = data.get("error") if isinstance(data.get("error"), dict) else {}
201
+ code = error.get("code") if isinstance(error, dict) else None
202
+ error_code = str(code) if isinstance(code, (str, int)) else None
203
+ retryable = error_code in _RETRYABLE_WS_CODES
204
+ message = "Vakyam TTS WebSocket request failed"
205
+ safe_body: dict[str, str] = {"type": "error"}
206
+ if error_code is not None:
207
+ message += f" (error code: {error_code})"
208
+ safe_body["code"] = error_code
209
+ raise APIStatusError(
210
+ message,
211
+ status_code=-1,
212
+ body=safe_body,
213
+ retryable=retryable,
214
+ )