pipecat-lokutor 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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .env
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .uv/
@@ -0,0 +1,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2024-2026, Lokutor AI
4
+ Copyright (c) 2024-2026, Daily
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: pipecat-lokutor
3
+ Version: 0.1.0
4
+ Summary: Lokutor TTS integration for Pipecat
5
+ Project-URL: Homepage, https://github.com/lokutor-ai/pipecat-lokutor
6
+ Project-URL: Source, https://github.com/lokutor-ai/pipecat-lokutor
7
+ Author-email: Lokutor AI <your-email@lokutor.com>
8
+ License: BSD-2-Clause
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: pipecat>=0.0.86
12
+ Requires-Dist: websockets>=12.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Pipecat Lokutor TTS
16
+
17
+ Lokutor text-to-speech integration for [Pipecat](https://github.com/pipecat-ai/pipecat).
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install pipecat-lokutor
23
+ ```
24
+
25
+ Or with uv:
26
+
27
+ ```bash
28
+ uv add pipecat-lokutor
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from pipecat_lokutor import LokutorTTSService
35
+
36
+ tts = LokutorTTSService(
37
+ api_key="your_api_key",
38
+ voice_id="F1",
39
+ params=LokutorTTSService.InputParams(
40
+ language="en",
41
+ speed=1.0,
42
+ steps=5,
43
+ visemes=False,
44
+ ),
45
+ )
46
+ ```
47
+
48
+ ### Pipecat Pipeline
49
+
50
+ ```python
51
+ from pipecat.pipeline.pipeline import Pipeline
52
+
53
+ pipeline = Pipeline([
54
+ transport.input(),
55
+ stt,
56
+ llm,
57
+ tts,
58
+ transport.output(),
59
+ ])
60
+ ```
61
+
62
+ ## Voices
63
+
64
+ | Voice | Description |
65
+ |-------|-------------|
66
+ | M1-M5 | Male voices |
67
+ | F1-F5 | Female voices |
68
+
69
+ ## Supported Languages
70
+
71
+ EN, ES, FR, PT, KO
72
+
73
+ ## Example
74
+
75
+ See `examples/groq-stt-groq-llm-lokutor-tts.py` for a full example using Groq STT + Groq LLM + Lokutor TTS.
76
+
77
+ ```bash
78
+ # Install dependencies
79
+ uv sync
80
+
81
+ # Set environment variables
82
+ export GROQ_API_KEY="your_groq_api_key"
83
+ export LOKUTOR_API_KEY="your_lokutor_api_key"
84
+
85
+ # Run the example
86
+ python examples/groq-stt-groq-llm-lokutor-tts.py -t webrtc
87
+ ```
88
+
89
+ Then open `http://localhost:7860/client/`.
90
+
91
+ ## Compatibility
92
+
93
+ Tested with Pipecat v0.0.86+.
94
+
95
+ ## License
96
+
97
+ BSD 2-Clause License. See `LICENSE` for details.
@@ -0,0 +1,83 @@
1
+ # Pipecat Lokutor TTS
2
+
3
+ Lokutor text-to-speech integration for [Pipecat](https://github.com/pipecat-ai/pipecat).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pipecat-lokutor
9
+ ```
10
+
11
+ Or with uv:
12
+
13
+ ```bash
14
+ uv add pipecat-lokutor
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from pipecat_lokutor import LokutorTTSService
21
+
22
+ tts = LokutorTTSService(
23
+ api_key="your_api_key",
24
+ voice_id="F1",
25
+ params=LokutorTTSService.InputParams(
26
+ language="en",
27
+ speed=1.0,
28
+ steps=5,
29
+ visemes=False,
30
+ ),
31
+ )
32
+ ```
33
+
34
+ ### Pipecat Pipeline
35
+
36
+ ```python
37
+ from pipecat.pipeline.pipeline import Pipeline
38
+
39
+ pipeline = Pipeline([
40
+ transport.input(),
41
+ stt,
42
+ llm,
43
+ tts,
44
+ transport.output(),
45
+ ])
46
+ ```
47
+
48
+ ## Voices
49
+
50
+ | Voice | Description |
51
+ |-------|-------------|
52
+ | M1-M5 | Male voices |
53
+ | F1-F5 | Female voices |
54
+
55
+ ## Supported Languages
56
+
57
+ EN, ES, FR, PT, KO
58
+
59
+ ## Example
60
+
61
+ See `examples/groq-stt-groq-llm-lokutor-tts.py` for a full example using Groq STT + Groq LLM + Lokutor TTS.
62
+
63
+ ```bash
64
+ # Install dependencies
65
+ uv sync
66
+
67
+ # Set environment variables
68
+ export GROQ_API_KEY="your_groq_api_key"
69
+ export LOKUTOR_API_KEY="your_lokutor_api_key"
70
+
71
+ # Run the example
72
+ python examples/groq-stt-groq-llm-lokutor-tts.py -t webrtc
73
+ ```
74
+
75
+ Then open `http://localhost:7860/client/`.
76
+
77
+ ## Compatibility
78
+
79
+ Tested with Pipecat v0.0.86+.
80
+
81
+ ## License
82
+
83
+ BSD 2-Clause License. See `LICENSE` for details.
@@ -0,0 +1,6 @@
1
+ from .tts import LokutorTTSService, LokutorTTSSettings
2
+
3
+ __all__ = [
4
+ "LokutorTTSService",
5
+ "LokutorTTSSettings",
6
+ ]
@@ -0,0 +1,270 @@
1
+ #
2
+ # Copyright (c) 2024-2026, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ import asyncio
8
+ import json
9
+ from dataclasses import dataclass
10
+ from typing import AsyncGenerator, Optional
11
+
12
+ from loguru import logger
13
+ from pydantic import BaseModel
14
+ from websockets.asyncio.client import connect as websocket_connect
15
+ from websockets.protocol import State
16
+
17
+ from pipecat.frames.frames import (
18
+ ErrorFrame,
19
+ Frame,
20
+ TTSAudioRawFrame,
21
+ TTSStartedFrame,
22
+ TTSStoppedFrame,
23
+ )
24
+ from pipecat.services.settings import TTSSettings
25
+ from pipecat.services.tts_service import WebsocketTTSService
26
+ from pipecat.transcriptions.language import Language
27
+ from pipecat.utils.tracing.service_decorators import traced_tts
28
+
29
+
30
+ @dataclass
31
+ class LokutorTTSSettings(TTSSettings):
32
+ """Settings for Lokutor TTS service."""
33
+
34
+ pass
35
+
36
+
37
+ class LokutorTTSService(WebsocketTTSService):
38
+ """Lokutor TTS service implementation."""
39
+
40
+ Settings = LokutorTTSSettings
41
+ _settings: LokutorTTSSettings
42
+
43
+ class InputParams(BaseModel):
44
+ """Input parameters for Lokutor TTS such as speed and language."""
45
+
46
+ language: Optional[Language] = None
47
+ speed: Optional[float] = 1.0
48
+ steps: Optional[int] = 5
49
+ visemes: Optional[bool] = False
50
+
51
+ SUPPORTED_VOICES = {"M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"}
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ api_key: str,
57
+ voice_id: str = "F1",
58
+ sample_rate: int = 44100,
59
+ params: Optional[InputParams] = None,
60
+ settings: Optional[LokutorTTSSettings] = None,
61
+ base_url: str = "wss://api.lokutor.com/ws",
62
+ **kwargs,
63
+ ):
64
+ if voice_id not in self.SUPPORTED_VOICES:
65
+ raise ValueError(f"Invalid voice_id '{voice_id}'")
66
+
67
+ self._api_key = api_key
68
+ self._voice_id = voice_id
69
+ self._params = params or self.InputParams()
70
+
71
+ default_settings = self.Settings(
72
+ model=None,
73
+ voice=self._voice_id,
74
+ language=None,
75
+ )
76
+
77
+ if params is not None and settings is None:
78
+ default_settings.language = params.language
79
+
80
+ if settings is not None:
81
+ default_settings.apply_update(settings)
82
+
83
+ super().__init__(
84
+ push_start_frame=True,
85
+ push_stop_frames=True,
86
+ pause_frame_processing=True,
87
+ sample_rate=sample_rate,
88
+ settings=default_settings,
89
+ **kwargs,
90
+ )
91
+
92
+ self._sample_rate = sample_rate
93
+ self._base_url = base_url
94
+ self._websocket = None
95
+ self._receive_task = None
96
+
97
+ async def _connect(self):
98
+ await super()._connect()
99
+
100
+ try:
101
+ await self._connect_websocket()
102
+ except Exception as e:
103
+ raise ConnectionError(f"Failed to connect to Lokutor: {e}") from e
104
+
105
+ if self._websocket and not self._receive_task:
106
+ self._receive_task = self.create_task(self._receive_task_handler(self._report_error))
107
+
108
+ async def _disconnect(self):
109
+ await super()._disconnect()
110
+
111
+ if self._receive_task:
112
+ await self.cancel_task(self._receive_task)
113
+ self._receive_task = None
114
+
115
+ await self._disconnect_websocket()
116
+
117
+ async def _connect_websocket(self):
118
+ if self._websocket and self._websocket.state is State.OPEN:
119
+ return
120
+
121
+ logger.debug("Connecting to Lokutor")
122
+ url = f"{self._base_url}?api_key={self._api_key}"
123
+ self._websocket = await websocket_connect(url)
124
+
125
+ await self._call_event_handler("on_connected")
126
+
127
+ async def _disconnect_websocket(self):
128
+ try:
129
+ await self.stop_all_metrics()
130
+ if self._websocket:
131
+ logger.debug("Disconnecting from Lokutor")
132
+ await self._websocket.close()
133
+ except Exception as exc:
134
+ await self.push_error(error_msg=f"Unknown error occurred: {exc}", exception=exc)
135
+ finally:
136
+ self._websocket = None
137
+ await self._call_event_handler("on_disconnected")
138
+
139
+ async def _receive_messages(self):
140
+ """Keep the websocket connection alive.
141
+
142
+ Lokutor uses request-response (send request, receive audio), not streaming.
143
+ All message handling happens in run_tts(). This method just keeps the
144
+ background receive task alive to maintain the persistent connection.
145
+ """
146
+ try:
147
+ while True:
148
+ await asyncio.sleep(1)
149
+ except asyncio.CancelledError:
150
+ pass
151
+
152
+ def _get_websocket(self):
153
+ if self._websocket is None:
154
+ raise ConnectionError("Lokutor websocket not connected")
155
+ return self._websocket
156
+
157
+ def can_generate_metrics(self) -> bool:
158
+ return True
159
+
160
+ @traced_tts
161
+ async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]:
162
+ logger.debug(f"{self}: Generating TTS [{text}]")
163
+
164
+ await self.start_tts_usage_metrics(text)
165
+ yield TTSStartedFrame(context_id=context_id)
166
+
167
+ try:
168
+ if not self._websocket or self._websocket.state is State.CLOSED:
169
+ await self._connect()
170
+
171
+ request = {
172
+ "text": text,
173
+ "voice": self._voice_id,
174
+ "speed": self._params.speed,
175
+ "steps": self._params.steps,
176
+ "visemes": self._params.visemes,
177
+ }
178
+ if self._params.language:
179
+ lokutor_lang = language_to_lokutor_language(self._params.language)
180
+ if lokutor_lang:
181
+ request["lang"] = lokutor_lang
182
+
183
+ request_json = json.dumps(request)
184
+ logger.debug(f"Sending request to Lokutor: {request_json}")
185
+
186
+ await self.start_ttfb_metrics()
187
+ await self._get_websocket().send(request_json)
188
+ logger.debug("Request sent to Lokutor, waiting for first response...")
189
+
190
+ first_audio_received = False
191
+ while True:
192
+ try:
193
+ logger.debug("Waiting for message from Lokutor...")
194
+ message = await asyncio.wait_for(self._get_websocket().recv(), timeout=10.0)
195
+ logger.debug(
196
+ f"Received message from Lokutor: {type(message)} {len(message) if isinstance(message, bytes) else message[:100]}"
197
+ )
198
+ if isinstance(message, str):
199
+ try:
200
+ data = json.loads(message)
201
+ if isinstance(data, dict):
202
+ msg_type = data.get("type")
203
+ if msg_type == "eos":
204
+ logger.debug("Received EOS from Lokutor")
205
+ break
206
+ elif msg_type == "error":
207
+ error_msg = data.get("message", "Unknown error")
208
+ logger.error(f"Lokutor error: {error_msg}")
209
+ yield ErrorFrame(error=f"Lokutor error: {error_msg}")
210
+ break
211
+ elif isinstance(data, list):
212
+ logger.debug(f"Received viseme data: {len(data)} visemes")
213
+ except json.JSONDecodeError:
214
+ logger.warning(f"Received unknown text message: {message}")
215
+ else:
216
+ logger.debug(f"Received audio data: {len(message)} bytes")
217
+ if not first_audio_received:
218
+ logger.debug("First audio chunk received - stopping TTFB metrics")
219
+ await self.stop_ttfb_metrics()
220
+ first_audio_received = True
221
+ yield TTSAudioRawFrame(message, self.sample_rate, 1)
222
+ except asyncio.TimeoutError:
223
+ logger.error("Timeout waiting for Lokutor response")
224
+ yield ErrorFrame(error="Timeout waiting for Lokutor response")
225
+ break
226
+ except Exception as e:
227
+ logger.error(f"Error receiving from Lokutor: {e}")
228
+ yield ErrorFrame(error=f"Error receiving from Lokutor: {e}")
229
+ break
230
+
231
+ except ConnectionError as e:
232
+ logger.error(f"{self} exception: {e}")
233
+ yield ErrorFrame(error=f"Unknown error occurred: {e}")
234
+ except Exception as e:
235
+ logger.error(f"{self} exception: {e}")
236
+ yield ErrorFrame(error=f"Unknown error occurred: {e}")
237
+ finally:
238
+ logger.debug(f"{self}: Finished TTS [{text}]")
239
+ if not first_audio_received:
240
+ await self.stop_ttfb_metrics()
241
+ yield TTSStoppedFrame(context_id=context_id)
242
+
243
+
244
+ def language_to_lokutor_language(language):
245
+ if isinstance(language, Language):
246
+ mapping = {
247
+ Language.EN: "en",
248
+ Language.ES: "es",
249
+ Language.FR: "fr",
250
+ Language.PT: "pt",
251
+ Language.KO: "ko",
252
+ }
253
+ return mapping.get(language)
254
+ if hasattr(language, "value"):
255
+ return language.value
256
+ return str(language)
257
+
258
+
259
+ def lokutor_language_to_language(language):
260
+ if not isinstance(language, str):
261
+ return None
262
+
263
+ mapping = {
264
+ "en": Language.EN,
265
+ "es": Language.ES,
266
+ "fr": Language.FR,
267
+ "pt": Language.PT,
268
+ "ko": Language.KO,
269
+ }
270
+ return mapping.get(language)
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "pipecat-lokutor"
3
+ version = "0.1.0"
4
+ description = "Lokutor TTS integration for Pipecat"
5
+ readme = "README.md"
6
+ license = { text = "BSD-2-Clause" }
7
+ authors = [
8
+ { name = "Lokutor AI", email = "your-email@lokutor.com" },
9
+ ]
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "pipecat>=0.0.86",
13
+ "websockets>=12.0",
14
+ ]
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/pipecat_lokutor"]
22
+
23
+ [tool.hatch.build.targets.sdist]
24
+ packages = ["src/pipecat_lokutor"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/lokutor-ai/pipecat-lokutor"
28
+ Source = "https://github.com/lokutor-ai/pipecat-lokutor"