friday-framework-speech 0.1.0a0__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,24 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .venv/
5
+ .idea/
6
+ .vscode/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ chroma_db/
11
+ .friday/
12
+ /vector/
13
+ /exports/
14
+ tests/integration/memory/vector/*/
15
+ tests/integration/memory/vector/backups/*/
16
+ tests/integration/memory/vector/reorganization_logs/*
17
+ *.gpickle
18
+ keys.txt
19
+ experiments/*/config/runtime.local.yaml
20
+ experiments/*/artifacts/*
21
+ !experiments/*/artifacts/.gitkeep
22
+ scripts/source-friday-chat-example-env.sh
23
+ scripts/pypi.sh
24
+ /docs/reference/PyPI-Recovery-Codes-cichuck-2026-07-20T15_36_49.709334.txt
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: friday-framework-speech
3
+ Version: 0.1.0a0
4
+ Summary: Speech services (TTS, wake word) for Friday
5
+ Project-URL: Homepage, https://github.com/CIChuck/agent-framework
6
+ Project-URL: Repository, https://github.com/CIChuck/agent-framework
7
+ Project-URL: Issues, https://github.com/CIChuck/agent-framework/issues
8
+ Project-URL: Documentation, https://github.com/CIChuck/agent-framework/tree/main/docs
9
+ Project-URL: Source, https://github.com/CIChuck/agent-framework/tree/main/packages/friday-speech
10
+ Author: Friday Team
11
+ License-Expression: MIT
12
+ Keywords: assistant,audio,speech,tts,wakeword
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: friday-framework-core==0.1.0a0
15
+ Requires-Dist: pydantic>=2.0.0
16
+ Requires-Dist: pyyaml>=6.0.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
20
+ Provides-Extra: elevenlabs
21
+ Requires-Dist: elevenlabs>=1.0.0; extra == 'elevenlabs'
22
+ Provides-Extra: picovoice
23
+ Requires-Dist: pvcobra>=2.0.0; extra == 'picovoice'
24
+ Requires-Dist: pvleopard>=2.0.0; extra == 'picovoice'
25
+ Requires-Dist: pvorca>=0.1.0; extra == 'picovoice'
26
+ Requires-Dist: pvporcupine>=3.0.0; extra == 'picovoice'
27
+ Requires-Dist: pvrecorder>=1.2.0; extra == 'picovoice'
28
+ Requires-Dist: pvrhino>=2.0.0; extra == 'picovoice'
29
+ Requires-Dist: pvspeaker>=0.1.0; extra == 'picovoice'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Friday Framework Speech
33
+
34
+ Speech service package for the Friday Framework.
35
+
36
+ This package provides speech service abstractions and optional text-to-speech,
37
+ wake-word, recording, playback, and speech-provider integrations.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install friday-framework-speech
43
+ ```
44
+
45
+ Optional speech providers are available through extras such as `elevenlabs` and
46
+ `picovoice`.
47
+
48
+ ## Import Package
49
+
50
+ ```python
51
+ import friday_speech
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,25 @@
1
+ # Friday Framework Speech
2
+
3
+ Speech service package for the Friday Framework.
4
+
5
+ This package provides speech service abstractions and optional text-to-speech,
6
+ wake-word, recording, playback, and speech-provider integrations.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install friday-framework-speech
12
+ ```
13
+
14
+ Optional speech providers are available through extras such as `elevenlabs` and
15
+ `picovoice`.
16
+
17
+ ## Import Package
18
+
19
+ ```python
20
+ import friday_speech
21
+ ```
22
+
23
+ ## License
24
+
25
+ MIT
@@ -0,0 +1,55 @@
1
+ [project]
2
+ name = "friday-framework-speech"
3
+ version = "0.1.0a0"
4
+ description = "Speech services (TTS, wake word) for Friday"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [{ name = "Friday Team" }]
9
+ keywords = ["speech", "tts", "wakeword", "audio", "assistant"]
10
+
11
+ dependencies = [
12
+ "pydantic>=2.0.0",
13
+ "friday-framework-core==0.1.0a0",
14
+ "pyyaml>=6.0.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/CIChuck/agent-framework"
19
+ Repository = "https://github.com/CIChuck/agent-framework"
20
+ Issues = "https://github.com/CIChuck/agent-framework/issues"
21
+ Documentation = "https://github.com/CIChuck/agent-framework/tree/main/docs"
22
+ Source = "https://github.com/CIChuck/agent-framework/tree/main/packages/friday-speech"
23
+
24
+ [project.optional-dependencies]
25
+ elevenlabs = [
26
+ "elevenlabs>=1.0.0",
27
+ ]
28
+ picovoice = [
29
+ "pvporcupine>=3.0.0",
30
+ "pvrecorder>=1.2.0",
31
+ "pvorca>=0.1.0",
32
+ "pvspeaker>=0.1.0",
33
+ "pvcobra>=2.0.0",
34
+ "pvleopard>=2.0.0",
35
+ "pvrhino>=2.0.0",
36
+ ]
37
+ dev = [
38
+ "pytest>=8.0.0",
39
+ "pytest-asyncio>=0.23.0",
40
+ ]
41
+
42
+ [build-system]
43
+ requires = ["hatchling"]
44
+ build-backend = "hatchling.build"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/friday_speech"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests_speech"]
51
+ asyncio_mode = "auto"
52
+ asyncio_default_fixture_loop_scope = "function"
53
+
54
+ [tool.uv.sources]
55
+ friday-framework-core = { workspace = true }
@@ -0,0 +1,44 @@
1
+ # mypy: ignore-errors
2
+ """Friday speech package."""
3
+
4
+ from .config import SpeechConfig
5
+ from .errors import (
6
+ AdapterError,
7
+ DependencyError,
8
+ PlaybackError,
9
+ SelectionError,
10
+ SpeechConfigError,
11
+ SpeechError,
12
+ STTError,
13
+ WakeWordError,
14
+ )
15
+ from .harness import SpeechHarness
16
+ from .interfaces import (
17
+ AdapterRegistry,
18
+ AudioFormat,
19
+ AudioStream,
20
+ STTAdapter,
21
+ TTSAdapter,
22
+ WakeWordAdapter,
23
+ )
24
+ from .service import SpeechService
25
+
26
+ __all__ = [
27
+ "AdapterError",
28
+ "AdapterRegistry",
29
+ "AudioFormat",
30
+ "AudioStream",
31
+ "DependencyError",
32
+ "PlaybackError",
33
+ "SelectionError",
34
+ "SpeechConfig",
35
+ "SpeechConfigError",
36
+ "SpeechError",
37
+ "SpeechHarness",
38
+ "SpeechService",
39
+ "STTAdapter",
40
+ "STTError",
41
+ "TTSAdapter",
42
+ "WakeWordAdapter",
43
+ "WakeWordError",
44
+ ]
@@ -0,0 +1,43 @@
1
+ # mypy: ignore-errors
2
+ """Adapter factory helpers for friday-speech."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from ..config import SpeechConfig, TTSProvider, WakeWordProvider
7
+ from ..errors import AdapterError
8
+ from ..interfaces import STTAdapter, TTSAdapter, WakeWordAdapter
9
+
10
+
11
+ def build_tts_adapter(config: SpeechConfig) -> TTSAdapter:
12
+ """Create a TTS adapter for the configured provider."""
13
+ if config.tts.provider == TTSProvider.ELEVENLABS:
14
+ from .elevenlabs import ElevenLabsAdapter
15
+
16
+ return ElevenLabsAdapter(config.elevenlabs)
17
+ if config.tts.provider == TTSProvider.PICOVOICE:
18
+ from .picovoice import PicovoiceOrcaAdapter
19
+
20
+ return PicovoiceOrcaAdapter(config.picovoice)
21
+ raise AdapterError(f"Unsupported TTS provider: {config.tts.provider}")
22
+
23
+
24
+ def build_wakeword_adapter(config: SpeechConfig) -> WakeWordAdapter | None:
25
+ """Create a wake word adapter if enabled."""
26
+ if not config.wakeword.enabled:
27
+ return None
28
+ if config.wakeword.provider == WakeWordProvider.PICOVOICE:
29
+ from .picovoice import PicovoicePorcupineAdapter
30
+
31
+ return PicovoicePorcupineAdapter(config.picovoice)
32
+ raise AdapterError(f"Unsupported wake word provider: {config.wakeword.provider}")
33
+
34
+
35
+ def build_stt_adapter(config: SpeechConfig) -> STTAdapter | None:
36
+ """Create a speech-to-text adapter if enabled."""
37
+ if not config.stt.enabled:
38
+ return None
39
+ if config.stt.provider.value == "picovoice":
40
+ from .picovoice import PicovoiceLeopardAdapter
41
+
42
+ return PicovoiceLeopardAdapter(config.picovoice)
43
+ raise AdapterError(f"Unsupported STT provider: {config.stt.provider}")
@@ -0,0 +1,183 @@
1
+ # mypy: ignore-errors
2
+ """ElevenLabs TTS adapter (best-effort streaming)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import re
8
+ import threading
9
+ from collections.abc import AsyncIterator, Iterable
10
+ from typing import Any
11
+
12
+ from ..config import ElevenLabsConfig, VoiceSettingsConfig
13
+ from ..errors import AdapterError, DependencyError
14
+ from ..interfaces import AudioFormat, AudioStream, TTSAdapter
15
+
16
+
17
+ class ElevenLabsAdapter(TTSAdapter):
18
+ """ElevenLabs adapter using the official Python SDK when available."""
19
+
20
+ def __init__(self, config: ElevenLabsConfig) -> None:
21
+ self._config = config
22
+ self.output_format = _infer_audio_format(config.output_format)
23
+ self.sample_rate = _infer_sample_rate(config.output_format)
24
+ self.channels = None
25
+
26
+ async def stream(
27
+ self,
28
+ text: str,
29
+ voice_id: str | None,
30
+ settings: VoiceSettingsConfig,
31
+ ) -> AudioStream:
32
+ if not self._config.api_key:
33
+ raise AdapterError("ElevenLabs API key is required")
34
+
35
+ elevenlabs = _import_elevenlabs()
36
+ client = _build_client(elevenlabs, self._config.api_key)
37
+ payload = _build_voice_settings(settings)
38
+
39
+ text_to_speech = getattr(client, "text_to_speech", None)
40
+ if text_to_speech is None:
41
+ raise AdapterError("ElevenLabs client missing text_to_speech interface")
42
+
43
+ if hasattr(text_to_speech, "convert_as_stream"):
44
+ sync_stream = text_to_speech.convert_as_stream(
45
+ text=text,
46
+ voice_id=voice_id or self._config.voice_id,
47
+ model_id=self._config.model_id,
48
+ output_format=self._config.output_format,
49
+ voice_settings=payload or None,
50
+ )
51
+ chunks = _async_from_sync_iterator(sync_stream)
52
+ return AudioStream(
53
+ format=self.output_format,
54
+ chunks=chunks,
55
+ sample_rate=self.sample_rate,
56
+ channels=self.channels,
57
+ )
58
+
59
+ if hasattr(text_to_speech, "convert"):
60
+ audio = await asyncio.to_thread(
61
+ text_to_speech.convert,
62
+ text=text,
63
+ voice_id=voice_id or self._config.voice_id,
64
+ model_id=self._config.model_id,
65
+ output_format=self._config.output_format,
66
+ voice_settings=payload or None,
67
+ )
68
+ chunks = _async_single_chunk(audio)
69
+ return AudioStream(
70
+ format=self.output_format,
71
+ chunks=chunks,
72
+ sample_rate=self.sample_rate,
73
+ channels=self.channels,
74
+ )
75
+
76
+ raise AdapterError(
77
+ "Unsupported ElevenLabs SDK: expected text_to_speech.convert[_as_stream]"
78
+ )
79
+
80
+
81
+ def _import_elevenlabs() -> Any:
82
+ try:
83
+ import elevenlabs # type: ignore
84
+
85
+ return elevenlabs
86
+ except Exception as exc: # pragma: no cover - dependency gate
87
+ raise DependencyError(
88
+ "ElevenLabs SDK not installed. Install with: pip install elevenlabs"
89
+ ) from exc
90
+
91
+
92
+ def _build_client(module: Any, api_key: str) -> Any:
93
+ for path in ("client", None):
94
+ try:
95
+ if path:
96
+ client_mod = __import__(f"elevenlabs.{path}", fromlist=["ElevenLabs"])
97
+ client_cls = getattr(client_mod, "ElevenLabs", None)
98
+ else:
99
+ client_cls = getattr(module, "ElevenLabs", None)
100
+ if client_cls:
101
+ return client_cls(api_key=api_key)
102
+ except Exception:
103
+ continue
104
+ raise AdapterError("Unable to initialize ElevenLabs client")
105
+
106
+
107
+ def _build_voice_settings(settings: VoiceSettingsConfig) -> dict[str, Any]:
108
+ payload: dict[str, Any] = {}
109
+ if settings.stability is not None:
110
+ payload["stability"] = settings.stability
111
+ if settings.similarity_boost is not None:
112
+ payload["similarity_boost"] = settings.similarity_boost
113
+ if settings.style is not None:
114
+ payload["style"] = settings.style
115
+ if settings.speed is not None:
116
+ payload["speed"] = settings.speed
117
+ return payload
118
+
119
+
120
+ def _infer_audio_format(output_format: str) -> AudioFormat:
121
+ value = output_format.lower()
122
+ if value.startswith("mp3"):
123
+ return AudioFormat.MP3
124
+ if value.startswith("pcm"):
125
+ return AudioFormat.PCM
126
+ raise AdapterError(f"Unsupported ElevenLabs output_format: {output_format}")
127
+
128
+
129
+ def _infer_sample_rate(output_format: str) -> int | None:
130
+ match = re.search(r"(\d{4,6})", output_format)
131
+ if not match:
132
+ return None
133
+ try:
134
+ return int(match.group(1))
135
+ except ValueError:
136
+ return None
137
+
138
+
139
+ def _async_single_chunk(audio: Any) -> AsyncIterator[bytes]:
140
+ async def _generator() -> AsyncIterator[bytes]:
141
+ if isinstance(audio, (bytes, bytearray)):
142
+ yield bytes(audio)
143
+ return
144
+ if isinstance(audio, Iterable):
145
+ for chunk in audio:
146
+ if isinstance(chunk, (bytes, bytearray)):
147
+ yield bytes(chunk)
148
+ else:
149
+ raise AdapterError("ElevenLabs stream yielded non-bytes chunk")
150
+ return
151
+ raise AdapterError("ElevenLabs audio payload is not bytes or iterable")
152
+
153
+ return _generator()
154
+
155
+
156
+ def _async_from_sync_iterator(stream: Iterable[Any]) -> AsyncIterator[bytes]:
157
+ async def _generator() -> AsyncIterator[bytes]:
158
+ queue: asyncio.Queue[Any] = asyncio.Queue()
159
+ sentinel = object()
160
+ loop = asyncio.get_running_loop()
161
+
162
+ def _worker() -> None:
163
+ try:
164
+ for chunk in stream:
165
+ loop.call_soon_threadsafe(queue.put_nowait, chunk)
166
+ except Exception as exc:
167
+ loop.call_soon_threadsafe(queue.put_nowait, exc)
168
+ finally:
169
+ loop.call_soon_threadsafe(queue.put_nowait, sentinel)
170
+
171
+ threading.Thread(target=_worker, daemon=True).start()
172
+
173
+ while True:
174
+ item = await queue.get()
175
+ if item is sentinel:
176
+ break
177
+ if isinstance(item, Exception):
178
+ raise item
179
+ if not isinstance(item, (bytes, bytearray)):
180
+ raise AdapterError("ElevenLabs stream yielded non-bytes chunk")
181
+ yield bytes(item)
182
+
183
+ return _generator()