edgespeak 0.6.4__py3-none-any.whl

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.
edgespeak/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """EdgeSpeak's Python SDK for the local CLI runtime."""
2
+ from ._client import EdgeSpeak, EdgeSpeakError
3
+ from ._process import RuntimeStartError
4
+ from ._bootstrap import MIN_CLI_VERSION
5
+ from ._streaming import EventStream
6
+ from ._types import AudioChannel, ReferenceFormat, SearchEffort, TimestampGranularity, TranscriptionFormat
7
+
8
+ from ._version import __version__ as __version__
9
+ __all__ = ["EdgeSpeak", "EdgeSpeakError", "RuntimeStartError", "MIN_CLI_VERSION", "EventStream",
10
+ "AudioChannel", "ReferenceFormat", "SearchEffort", "TimestampGranularity", "TranscriptionFormat"]
@@ -0,0 +1,246 @@
1
+ """Discover a compatible CLI, installing the official standalone runtime if needed."""
2
+ from __future__ import annotations
3
+
4
+ from contextlib import contextmanager
5
+ import logging
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import platform
10
+ import re
11
+ import shutil
12
+ import signal
13
+ import subprocess
14
+ import tempfile
15
+ import time
16
+ import threading
17
+
18
+ import httpx
19
+
20
+ from ._process import RuntimeStartError
21
+
22
+ # Both the released CLI version floor and SDK protocol support are required.
23
+ # Each SDK release ships its own floor. Raise this value
24
+ # when that SDK release relies on newer CLI features. It is a compatibility
25
+ # requirement, not a download pin: installation always uses the latest stable CLI.
26
+ MIN_CLI_VERSION = "0.6.4"
27
+ SDK_PROTOCOL = "edgespeak-sdk-v2"
28
+ _LOG = logging.getLogger("edgespeak")
29
+ _lock_streams = set()
30
+ _stream_guard = threading.Lock()
31
+
32
+
33
+ def _release_inherited_locks():
34
+ # Raw FileIO has no buffered-I/O mutex inherited from another thread.
35
+ for stream in _lock_streams:
36
+ stream.close()
37
+ _lock_streams.clear()
38
+ _stream_guard.release()
39
+
40
+
41
+ if hasattr(os, "register_at_fork"):
42
+ os.register_at_fork(before=_stream_guard.acquire,
43
+ after_in_parent=_stream_guard.release,
44
+ after_in_child=_release_inherited_locks)
45
+
46
+
47
+ @contextmanager
48
+ def _lock_file(path):
49
+ with _stream_guard:
50
+ stream = path.open("a+b", buffering=0)
51
+ _lock_streams.add(stream)
52
+ try:
53
+ yield stream
54
+ finally:
55
+ with _stream_guard:
56
+ _lock_streams.discard(stream)
57
+ stream.close()
58
+
59
+
60
+ def _environment():
61
+ # Neither discovery nor installation needs account/gateway credentials or
62
+ # inherited runtime/download overrides. In particular, probe the sibling host.
63
+ return {k: v for k, v in os.environ.items() if not k.upper().startswith("EDGESPEAK_")}
64
+
65
+
66
+ def _candidates():
67
+ home = Path.home()
68
+ executable = "edgespeak-cli.exe" if os.name == "nt" else "edgespeak-cli"
69
+ found = shutil.which(executable)
70
+ paths = [Path(found)] if found else []
71
+ # Only parse the exact launcher format emitted by the official Windows
72
+ # installer, never execute arbitrary shell text from a .cmd file.
73
+ if os.name == "nt":
74
+ shim = shutil.which("edgespeak-cli.cmd")
75
+ if shim:
76
+ try:
77
+ match = re.fullmatch(r'@echo off\s+"([^"\r\n]+)" %\*\s*', Path(shim).read_text(encoding="ascii"))
78
+ if match and Path(match[1]).is_absolute():
79
+ paths.append(Path(match[1]))
80
+ except (OSError, UnicodeError):
81
+ pass
82
+ try:
83
+ generation = json.loads((home / ".edgespeak/sdk/current.json").read_text())["generation"]
84
+ if isinstance(generation, str) and re.fullmatch(r"runtime-[a-zA-Z0-9_-]+", generation):
85
+ paths.insert(0, home / ".edgespeak/sdk" / generation / "runtime/Resources" / executable)
86
+ except (OSError, ValueError, KeyError, TypeError):
87
+ pass
88
+ paths += [home / ".edgespeak/runtime/Resources" / executable,
89
+ home / ".local/bin" / executable]
90
+ if platform.system() == "Darwin":
91
+ paths += [base / "EdgeSpeak.app/Contents/Resources/edgespeak-cli"
92
+ for base in (Path("/Applications"), home / "Applications")]
93
+ if os.name == "nt":
94
+ local = Path(os.environ.get("LOCALAPPDATA", home / "AppData/Local"))
95
+ paths += [local / "EdgeSpeak/bin" / executable]
96
+ return dict.fromkeys(path.resolve() for path in paths if path.is_file())
97
+
98
+
99
+ def compatible_version(version: str) -> bool:
100
+ match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", version)
101
+ return bool(match and tuple(map(int, match.groups())) >= tuple(map(int, MIN_CLI_VERSION.split("."))))
102
+
103
+
104
+ def _compatible(cli: Path) -> bool:
105
+ options = {"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {}
106
+ try:
107
+ version = subprocess.run([str(cli), "--version"], stdin=subprocess.DEVNULL,
108
+ capture_output=True, timeout=10, env=_environment(), **options)
109
+ match = re.fullmatch(rb"edgespeak(?:-cli)?\s+(\d+)\.(\d+)\.(\d+)\s*", version.stdout.strip())
110
+ if version.returncode or not match:
111
+ return False
112
+ if tuple(map(int, match.groups())) < tuple(map(int, MIN_CLI_VERSION.split("."))):
113
+ return False
114
+ help_result = subprocess.run([str(cli), "serve", "--help"], stdin=subprocess.DEVNULL,
115
+ capture_output=True, timeout=10, env=_environment(), **options)
116
+ return help_result.returncode == 0 and re.search(rb"(?<![\w-])" + re.escape(SDK_PROTOCOL.encode()) + rb"(?![\w-])", help_result.stdout) is not None and re.search(rb"(?<!\S)\[?--sdk(?:\]|\s|$)", help_result.stdout) is not None
117
+ except (OSError, subprocess.TimeoutExpired):
118
+ return False
119
+
120
+
121
+ def _find():
122
+ return next((cli for cli in _candidates() if _compatible(cli)), None)
123
+
124
+
125
+ @contextmanager
126
+ def _installation_lock():
127
+ # OS locks survive stale lock files and are released even if Python is killed.
128
+ directory = Path.home() / ".edgespeak/sdk"
129
+ directory.mkdir(parents=True, exist_ok=True)
130
+ with _lock_file(directory / "install.lock") as stream:
131
+ if stream.seek(0, 2) == 0:
132
+ stream.write(b"\0")
133
+ deadline = time.monotonic() + 1800
134
+ while True:
135
+ try:
136
+ stream.seek(0)
137
+ if os.name == "nt":
138
+ import msvcrt
139
+ msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK, 1)
140
+ else:
141
+ import fcntl
142
+ fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB)
143
+ break
144
+ except OSError:
145
+ if time.monotonic() >= deadline:
146
+ raise RuntimeStartError("Timed out waiting for CLI installation.") from None
147
+ time.sleep(.1)
148
+ try:
149
+ yield
150
+ finally:
151
+ if os.name == "nt":
152
+ stream.seek(0)
153
+ msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1)
154
+ else:
155
+ fcntl.flock(stream, fcntl.LOCK_UN)
156
+
157
+
158
+ def _install():
159
+ """Install the latest official stable CLI and verify this SDK's requirements.
160
+
161
+ The official stable installer selects the latest platform release. Never pass
162
+ MIN_CLI_VERSION as a target download version; it is only the acceptance floor.
163
+ """
164
+ system, machine = platform.system(), platform.machine().lower()
165
+ # The official installer distinguishes Intel Macs from Rosetta on Apple Silicon.
166
+ if (system, machine) not in {("Darwin", "arm64"), ("Darwin", "x86_64"), ("Linux", "x86_64"),
167
+ ("Windows", "amd64"), ("Windows", "x86_64")}:
168
+ raise RuntimeStartError("Automatic CLI installation is unavailable on this platform.")
169
+ windows = system == "Windows"
170
+ shell = shutil.which("powershell.exe" if windows else "sh")
171
+ if not shell:
172
+ raise RuntimeStartError("The system shell required by the CLI installer is unavailable.")
173
+ suffix = ".ps1" if windows else ".sh"
174
+ _LOG.warning("Installing a compatible EdgeSpeak CLI (requires >= %s and %s).",
175
+ MIN_CLI_VERSION, SDK_PROTOCOL)
176
+ root = Path.home() / ".edgespeak/sdk"
177
+ root.mkdir(parents=True, exist_ok=True)
178
+ # Never replace a running runtime. Each attempt has its own directory/lock;
179
+ # failed or interrupted attempts cannot block a later generation.
180
+ directory = Path(tempfile.mkdtemp(prefix="runtime-", dir=root))
181
+ script = directory / ("install" + suffix)
182
+ try:
183
+ with httpx.Client(timeout=60, follow_redirects=False) as http:
184
+ response = http.get("https://edgespeak.com/install" + suffix)
185
+ response.raise_for_status()
186
+ if (not response.content or len(response.content) > 1024 * 1024
187
+ or b"EDGESPEAK_SDK_INSTALL_V1" not in response.content):
188
+ raise RuntimeStartError("The official installer does not yet support SDK-managed installation. A matching installer release is required.")
189
+ script.write_bytes(response.content)
190
+ except httpx.HTTPError:
191
+ raise RuntimeStartError("Cannot download the official CLI installer. Check your network and retry.") from None
192
+ env = _environment()
193
+ env["EDGESPEAK_DATA_DIR"] = str(directory / "data")
194
+ env["EDGESPEAK_RUNTIME_DIR"] = str(directory / "runtime")
195
+ env["EDGESPEAK_INSTALL_DIR"] = str(directory / "bin")
196
+ env["EDGESPEAK_INSTALL_CONTEXT"] = "sdk"
197
+ env["EDGESPEAK_LEGACY_INSTALL_PATH"] = str(directory / "unused-legacy-entry")
198
+ command = ([shell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(script)]
199
+ if windows else [shell, str(script)])
200
+ options = ({"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW}
201
+ if windows else {"start_new_session": True})
202
+ process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
203
+ stderr=subprocess.DEVNULL, env=env, **options)
204
+ try:
205
+ code = process.wait(timeout=1200)
206
+ except BaseException:
207
+ # Bounded cleanup, with no Popen context manager (its exit waits forever).
208
+ try:
209
+ if windows:
210
+ subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"],
211
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
212
+ else:
213
+ os.killpg(process.pid, signal.SIGKILL)
214
+ except (OSError, subprocess.TimeoutExpired):
215
+ pass
216
+ try:
217
+ if process.poll() is None:
218
+ process.kill()
219
+ process.wait(timeout=5)
220
+ except (OSError, subprocess.TimeoutExpired):
221
+ pass
222
+ raise
223
+ if code:
224
+ raise RuntimeStartError("Official CLI installation failed. Check network, disk space and installation permissions, then retry.")
225
+ cli = directory / "runtime/Resources" / ("edgespeak-cli.exe" if windows else "edgespeak-cli")
226
+ if not _compatible(cli):
227
+ raise RuntimeStartError(f"The available CLI does not meet SDK requirements: CLI >= {MIN_CLI_VERSION} with {SDK_PROTOCOL}. A compatible official CLI release is required.")
228
+ # Publish only after validation. Existing generations remain valid for live
229
+ # clients and other SDK versions; never delete them during an update.
230
+ pointer = directory / "current.json"
231
+ pointer.write_text(json.dumps({"generation": directory.name}))
232
+ os.replace(pointer, root / "current.json")
233
+ return cli
234
+
235
+
236
+ def ensure_cli() -> Path:
237
+ """Reuse compatible installations without network access; bootstrap once otherwise."""
238
+ cli = _find()
239
+ if cli:
240
+ return cli
241
+ try:
242
+ with _installation_lock():
243
+ cli = _find() # Another Python process may have just finished installing.
244
+ return cli if cli else _install()
245
+ except (OSError, subprocess.TimeoutExpired):
246
+ raise RuntimeStartError("CLI installation could not finish. Check network and local installation permissions, then retry.") from None