voxedge 0.0.1a0__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.
Files changed (49) hide show
  1. voxedge/__init__.py +10 -0
  2. voxedge/artifacts/__init__.py +48 -0
  3. voxedge/artifacts/download.py +213 -0
  4. voxedge/artifacts/manifest.json +58 -0
  5. voxedge/artifacts/manifest.py +209 -0
  6. voxedge/backends/__init__.py +36 -0
  7. voxedge/backends/_deps.py +100 -0
  8. voxedge/backends/base.py +525 -0
  9. voxedge/backends/jetson/__init__.py +42 -0
  10. voxedge/backends/jetson/_trt_edge_llm_util.py +220 -0
  11. voxedge/backends/jetson/_util.py +276 -0
  12. voxedge/backends/jetson/kokoro_trt.py +1385 -0
  13. voxedge/backends/jetson/matcha_trt.py +848 -0
  14. voxedge/backends/jetson/moss_tts_nano.py +533 -0
  15. voxedge/backends/jetson/paraformer_trt.py +1210 -0
  16. voxedge/backends/jetson/qwen3_trt.py +689 -0
  17. voxedge/backends/jetson/trt_edge_llm_asr.py +1168 -0
  18. voxedge/backends/jetson/trt_edge_llm_ipc.py +295 -0
  19. voxedge/backends/jetson/trt_edge_llm_tts.py +1369 -0
  20. voxedge/backends/jetson/worker_io.py +307 -0
  21. voxedge/backends/llm_base.py +29 -0
  22. voxedge/backends/llm_translator.py +224 -0
  23. voxedge/backends/mock.py +344 -0
  24. voxedge/backends/nllb_translator.py +179 -0
  25. voxedge/backends/rk/__init__.py +30 -0
  26. voxedge/backends/rk/_util.py +100 -0
  27. voxedge/backends/rk/artifacts.py +221 -0
  28. voxedge/backends/rk/asr.py +580 -0
  29. voxedge/backends/rk/runtime.py +170 -0
  30. voxedge/backends/rk/tts.py +280 -0
  31. voxedge/backends/sherpa/__init__.py +22 -0
  32. voxedge/backends/sherpa/_util.py +98 -0
  33. voxedge/backends/sherpa/asr.py +419 -0
  34. voxedge/backends/sherpa/tts.py +325 -0
  35. voxedge/engine/__init__.py +23 -0
  36. voxedge/engine/asr_session_manager.py +457 -0
  37. voxedge/engine/builtin_tools.py +32 -0
  38. voxedge/engine/capability_resolver.py +238 -0
  39. voxedge/engine/concurrency_capability.py +60 -0
  40. voxedge/engine/conversation.py +1607 -0
  41. voxedge/engine/coordinator.py +138 -0
  42. voxedge/engine/tool_registry.py +452 -0
  43. voxedge/engine/tts_buffer.py +167 -0
  44. voxedge/transport/__init__.py +10 -0
  45. voxedge/transport/base.py +255 -0
  46. voxedge-0.0.1a0.dist-info/METADATA +74 -0
  47. voxedge-0.0.1a0.dist-info/RECORD +49 -0
  48. voxedge-0.0.1a0.dist-info/WHEEL +5 -0
  49. voxedge-0.0.1a0.dist-info/top_level.txt +1 -0
voxedge/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """voxedge — edge-native real-time voice conversation library.
2
+
3
+ Phase 1a: pure-Python foundation. Core stays importable with no CUDA / torch /
4
+ tensorrt — heavy backends live behind optional extras.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ __version__ = "0.0.1a0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,48 @@
1
+ """Backend-agnostic runtime artifact manifest + env-free download helper.
2
+
3
+ The package is
4
+ pure-Python and env-free; ``huggingface_hub`` is an optional dependency only
5
+ needed for the default network downloader (``voxedge[artifacts]``).
6
+
7
+ Public API::
8
+
9
+ from voxedge.artifacts import (
10
+ load_manifest, parse_manifest, default_manifest_path,
11
+ resolve_artifact, ArtifactInstallResult,
12
+ ManifestError, ArtifactError,
13
+ )
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from .download import (
19
+ DEFAULT_HF_ENDPOINT,
20
+ ArtifactError,
21
+ ArtifactInstallResult,
22
+ resolve_artifact,
23
+ )
24
+ from .manifest import (
25
+ SCHEMA_VERSION,
26
+ ArtifactFile,
27
+ ArtifactManifest,
28
+ ArtifactSpec,
29
+ ManifestError,
30
+ default_manifest_path,
31
+ load_manifest,
32
+ parse_manifest,
33
+ )
34
+
35
+ __all__ = [
36
+ "SCHEMA_VERSION",
37
+ "DEFAULT_HF_ENDPOINT",
38
+ "ArtifactFile",
39
+ "ArtifactSpec",
40
+ "ArtifactManifest",
41
+ "ArtifactInstallResult",
42
+ "ManifestError",
43
+ "ArtifactError",
44
+ "parse_manifest",
45
+ "load_manifest",
46
+ "default_manifest_path",
47
+ "resolve_artifact",
48
+ ]
@@ -0,0 +1,213 @@
1
+ """Env-free, backend-agnostic artifact download + install helper.
2
+
3
+ Generalises the RK-specific ``ensure_rk_artifacts`` prototype
4
+ (``voxedge/backends/rk/artifacts.py``) into a backend-agnostic helper keyed by
5
+ a stable ``artifact_ref``. The public entry point is :func:`resolve_artifact`,
6
+ which:
7
+
8
+ 1. looks up the artifact by ``artifact_ref`` in the manifest,
9
+ 2. for each declared file, downloads it from the HF repo (or any injected
10
+ downloader) into ``install_root/<path>``,
11
+ 3. verifies the SHA-256 of every file (already-present + correct files are
12
+ skipped),
13
+ 4. fails loudly with :class:`ArtifactError` on a missing file or SHA mismatch
14
+ (preflight semantics — e.g. a missing ``style.npy`` voice pack must fail,
15
+ not silently produce silent audio),
16
+ 5. returns an :class:`ArtifactInstallResult` recording what was installed vs
17
+ skipped.
18
+
19
+ **Env-free**: every input is a function argument; nothing reads ``os.environ``.
20
+ The install root is supplied by the caller (product / profile) — the manifest
21
+ only stores artifact-relative paths.
22
+
23
+ ``huggingface_hub`` is an OPTIONAL dependency. The default downloader uses it,
24
+ but tests (and offline callers) inject a local downloader via the
25
+ ``download_fn`` parameter, so the manifest/SHA logic needs no network.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import hashlib
31
+ from dataclasses import dataclass, field
32
+ from pathlib import Path
33
+ from typing import Callable, Optional
34
+
35
+ from .manifest import (
36
+ ArtifactFile,
37
+ ArtifactManifest,
38
+ ArtifactSpec,
39
+ default_manifest_path,
40
+ load_manifest,
41
+ )
42
+
43
+ DEFAULT_HF_ENDPOINT = "https://huggingface.co"
44
+
45
+ # A downloader fetches one file from an HF repo into ``dest``.
46
+ # Signature: (repo_id, revision, source_path, dest, endpoint) -> None
47
+ DownloadFn = Callable[[str, str, str, Path, str], None]
48
+
49
+
50
+ class ArtifactError(RuntimeError):
51
+ """Raised when an artifact cannot be resolved, downloaded, or verified."""
52
+
53
+
54
+ @dataclass
55
+ class ArtifactInstallResult:
56
+ """Outcome of :func:`resolve_artifact`."""
57
+
58
+ artifact_ref: str
59
+ install_root: Path
60
+ installed: list[str] = field(default_factory=list) # rel paths downloaded
61
+ skipped: list[str] = field(default_factory=list) # rel paths already present
62
+ files: list[Path] = field(default_factory=list) # absolute installed paths
63
+
64
+ @property
65
+ def ready(self) -> bool:
66
+ """True when every declared file is present on disk (installed or
67
+ already-present). ``resolve_artifact`` raises before returning if any
68
+ file is missing/mismatched, so a returned result is always ready."""
69
+ return bool(self.files) and not self.missing()
70
+
71
+ @property
72
+ def all_skipped(self) -> bool:
73
+ """True when nothing needed downloading (every file already present)."""
74
+ return bool(self.files) and not self.installed
75
+
76
+ def missing(self) -> list[str]: # pragma: no cover - resolve raises first
77
+ return [str(p) for p in self.files if not p.exists()]
78
+
79
+
80
+ def _sha256(path: Path) -> str:
81
+ h = hashlib.sha256()
82
+ with path.open("rb") as fh:
83
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
84
+ h.update(chunk)
85
+ return h.hexdigest()
86
+
87
+
88
+ def _hf_download(
89
+ repo_id: str, revision: str, source_path: str, dest: Path, endpoint: str
90
+ ) -> None:
91
+ """Default downloader backed by ``huggingface_hub.hf_hub_download``.
92
+
93
+ Imported lazily so the core package stays installable without the optional
94
+ ``huggingface_hub`` dependency.
95
+ """
96
+ try:
97
+ from huggingface_hub import hf_hub_download # type: ignore
98
+ except ImportError as exc: # pragma: no cover - exercised only without extra
99
+ raise ArtifactError(
100
+ "huggingface_hub is required to download artifacts; install "
101
+ "voxedge[artifacts] or inject a custom download_fn."
102
+ ) from exc
103
+
104
+ import shutil
105
+
106
+ src = hf_hub_download(
107
+ repo_id=repo_id,
108
+ filename=source_path,
109
+ revision=revision or "main",
110
+ endpoint=endpoint or DEFAULT_HF_ENDPOINT,
111
+ )
112
+ dest.parent.mkdir(parents=True, exist_ok=True)
113
+ if Path(src).resolve() != dest.resolve():
114
+ shutil.copyfile(src, dest)
115
+
116
+
117
+ def _resolve_one(
118
+ spec: ArtifactSpec,
119
+ item: ArtifactFile,
120
+ install_root: Path,
121
+ repo_id: str,
122
+ revision: str,
123
+ endpoint: str,
124
+ download_fn: DownloadFn,
125
+ result: ArtifactInstallResult,
126
+ ) -> None:
127
+ dest = install_root / item.rel_path
128
+ result.files.append(dest)
129
+
130
+ # Already present and correct → skip.
131
+ if dest.exists() and _sha256(dest) == item.sha256:
132
+ result.skipped.append(item.rel_path)
133
+ return
134
+
135
+ if not repo_id:
136
+ raise ArtifactError(
137
+ f"artifact {spec.artifact_ref!r} file {item.rel_path!r} is missing "
138
+ f"and no hf_repo is declared to download it from "
139
+ f"(preflight fail: required file absent)."
140
+ )
141
+
142
+ dest.parent.mkdir(parents=True, exist_ok=True)
143
+ try:
144
+ download_fn(repo_id, revision, item.source_rel, dest, endpoint)
145
+ except ArtifactError:
146
+ raise
147
+ except Exception as exc: # noqa: BLE001 - normalise any downloader failure
148
+ raise ArtifactError(
149
+ f"download failed for {spec.artifact_ref!r} file "
150
+ f"{item.source_rel!r}: {exc}"
151
+ ) from exc
152
+
153
+ if not dest.exists():
154
+ raise ArtifactError(
155
+ f"download reported success but file missing: {dest} "
156
+ f"(artifact {spec.artifact_ref!r})"
157
+ )
158
+
159
+ got = _sha256(dest)
160
+ if got != item.sha256:
161
+ dest.unlink(missing_ok=True)
162
+ raise ArtifactError(
163
+ f"sha256 mismatch for {item.rel_path!r} in artifact "
164
+ f"{spec.artifact_ref!r}: got {got}, expected {item.sha256}"
165
+ )
166
+ result.installed.append(item.rel_path)
167
+
168
+
169
+ def resolve_artifact(
170
+ artifact_ref: str,
171
+ install_root: str | Path,
172
+ *,
173
+ manifest_path: Optional[str | Path] = None,
174
+ manifest: Optional[ArtifactManifest] = None,
175
+ hf_endpoint: str = DEFAULT_HF_ENDPOINT,
176
+ download_fn: Optional[DownloadFn] = None,
177
+ ) -> ArtifactInstallResult:
178
+ """Resolve, download, SHA-verify and install a named artifact.
179
+
180
+ Args:
181
+ artifact_ref: stable artifact name to resolve in the manifest.
182
+ install_root: caller-chosen base dir; files land at
183
+ ``install_root/<file.path>`` (manifest stores relative paths only).
184
+ manifest_path: path to a JSON manifest. Defaults to the bundled sample
185
+ manifest when neither ``manifest_path`` nor ``manifest`` is given.
186
+ manifest: a pre-parsed manifest (takes precedence over ``manifest_path``).
187
+ hf_endpoint: HF endpoint base URL (env-free; passed by the caller).
188
+ download_fn: per-file downloader override. Defaults to a
189
+ ``huggingface_hub``-backed downloader. Tests inject a local one.
190
+
191
+ Returns:
192
+ :class:`ArtifactInstallResult` recording installed/skipped files.
193
+
194
+ Raises:
195
+ ArtifactError: artifact_ref missing, a required file is absent with no
196
+ repo to fetch it, a download fails, or a SHA-256 mismatch occurs.
197
+ """
198
+ if manifest is None:
199
+ path = manifest_path if manifest_path is not None else default_manifest_path()
200
+ manifest = load_manifest(path)
201
+
202
+ spec = manifest.get(artifact_ref)
203
+
204
+ root = Path(install_root)
205
+ endpoint = (hf_endpoint or manifest.hf_endpoint or DEFAULT_HF_ENDPOINT).rstrip("/")
206
+ repo_id = spec.hf_repo
207
+ revision = spec.revision or "main"
208
+ dl = download_fn or _hf_download
209
+
210
+ result = ArtifactInstallResult(artifact_ref=artifact_ref, install_root=root)
211
+ for item in spec.file_list:
212
+ _resolve_one(spec, item, root, repo_id, revision, endpoint, dl, result)
213
+ return result
@@ -0,0 +1,58 @@
1
+ {
2
+ "schema_version": 1,
3
+ "hf_endpoint": "https://huggingface.co",
4
+ "artifacts": {
5
+ "rk3588-kokoro-hybrid-2026-05-23": {
6
+ "artifact_ref": "rk3588-kokoro-hybrid-2026-05-23",
7
+ "backend_key": "rk.tts",
8
+ "device": "rk3588",
9
+ "precision": "int8",
10
+ "hf_repo": "example-org/example-artifacts",
11
+ "revision": "main",
12
+ "sha256": "",
13
+ "runtime_contract": {
14
+ "tts_path": "kokoro_hybrid",
15
+ "closed_loop_validated": true,
16
+ "env": {
17
+ "TTS_BACKEND": "kokoro_rknn",
18
+ "KOKORO_RKNN_MODE": "hybrid",
19
+ "KOKORO_PREFIX_ONNX": "kokoro-prefix-cpu.onnx",
20
+ "KOKORO_FRONT_RKNN": "rk3588/kokoro-decoder-front.int8.rknn",
21
+ "KOKORO_TAIL_ONNX": "kokoro-generator-tail-cpu.onnx"
22
+ }
23
+ },
24
+ "file_list": [
25
+ {
26
+ "path": "opt/kokoro-rknn/kokoro-prefix-cpu.onnx",
27
+ "source_path": "rk3588/kokoro-hybrid-v1/kokoro-prefix-cpu.onnx",
28
+ "size_bytes": 22922434,
29
+ "sha256": "549fece07d8696ae9ea60f4667e2680b5ca1f41735af545489a378eed3fe1f1c"
30
+ },
31
+ {
32
+ "path": "opt/kokoro-rknn/kokoro-generator-tail-cpu.onnx",
33
+ "source_path": "rk3588/kokoro-hybrid-v1/kokoro-generator-tail-cpu.onnx",
34
+ "size_bytes": 89009749,
35
+ "sha256": "1bdbb9fdea62922e79fa29891471e7797047f16636bff9bfb8944b28ee451bcb"
36
+ },
37
+ {
38
+ "path": "opt/kokoro-rknn/rk3588/kokoro-decoder-front.int8.rknn",
39
+ "source_path": "rk3588/kokoro-hybrid-v1/rk3588/kokoro-decoder-front.int8.rknn",
40
+ "size_bytes": 40599018,
41
+ "sha256": "a6de7a2512d83a22cfba8e1793b1f463108126dfee4eb8592a28fe7c268d6861"
42
+ },
43
+ {
44
+ "path": "opt/kokoro-rknn/default.npy",
45
+ "source_path": "rk3588/kokoro-hybrid-v1/default.npy",
46
+ "size_bytes": 1152,
47
+ "sha256": "e844b0d3200a28b47a2472d4052c2696ac639197aa0ab06bb3fb708781b5a09d"
48
+ },
49
+ {
50
+ "path": "opt/kokoro-rknn/style.npy",
51
+ "source_path": "rk3588/kokoro-hybrid-v1/style.npy",
52
+ "size_bytes": 1152,
53
+ "sha256": "e844b0d3200a28b47a2472d4052c2696ac639197aa0ab06bb3fb708781b5a09d"
54
+ }
55
+ ]
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,209 @@
1
+ """Backend-agnostic runtime artifact manifest — schema + loader.
2
+
3
+ This module is **env-free** and **pure-Python** (stdlib + dataclasses only):
4
+ no ``os.environ`` reads, no ``huggingface_hub`` import, no network. It only
5
+ parses and validates a JSON manifest that describes the runtime artifacts a
6
+ backend needs (engines / plugins / model weights / voice-pack sidecars), each
7
+ pinned by SHA-256 and referenced by a stable ``artifact_ref`` name.
8
+
9
+ Schema (a backend-agnostic generalisation of a per-device artifact manifest):
10
+
11
+ {
12
+ "schema_version": 1,
13
+ "artifacts": {
14
+ "<artifact_ref>": {
15
+ "backend_key": "rk.tts" | "jetson.trt_edge_llm" | ...,
16
+ "artifact_ref": "<stable name>", # echoed; must match the key
17
+ "device": "rk3588" | "jetson-orin-sm87" | ...,
18
+ "precision": "fp16" | "int8" | "w4a16" | ...,
19
+ "hf_repo": "<org>/<repo>",
20
+ "revision": "main" | "<commit-sha>",
21
+ "sha256": "<top-level digest, optional>",
22
+ "runtime_contract": { ... }, # env/profile constraints
23
+ "file_list": [
24
+ { "path": "<artifact-relative path, NO install root>",
25
+ "source_path": "<HF source path, defaults to path>",
26
+ "sha256": "<per-file digest>",
27
+ "size_bytes": <int, optional> },
28
+ ...
29
+ ]
30
+ }
31
+ }
32
+ }
33
+
34
+ ``file_list[].path`` is the artifact-relative install path; the install root is
35
+ chosen by the caller (product / profile) at :func:`resolve_artifact` time — the
36
+ manifest never encodes an absolute install root.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ from dataclasses import dataclass, field
43
+ from pathlib import Path
44
+ from typing import Any, Optional
45
+
46
+ SCHEMA_VERSION = 1
47
+
48
+
49
+ class ManifestError(ValueError):
50
+ """Raised when a manifest is malformed or an artifact_ref is missing."""
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class ArtifactFile:
55
+ """One file in an artifact's ``file_list``.
56
+
57
+ ``path`` is the artifact-relative destination (no install root). ``source_path``
58
+ is the path inside the HF repo to download from; it defaults to ``path``.
59
+ """
60
+
61
+ path: str
62
+ sha256: str
63
+ source_path: str = ""
64
+ size_bytes: Optional[int] = None
65
+
66
+ def __post_init__(self) -> None:
67
+ if not self.path or not str(self.path).strip():
68
+ raise ManifestError("file_list entry missing 'path'")
69
+ if not self.sha256 or not str(self.sha256).strip():
70
+ raise ManifestError(f"file_list entry {self.path!r} missing 'sha256'")
71
+ # Normalise source_path to default to path (frozen → object.__setattr__).
72
+ if not self.source_path:
73
+ object.__setattr__(self, "source_path", self.path)
74
+
75
+ @property
76
+ def rel_path(self) -> str:
77
+ """Artifact-relative install path, leading slash stripped."""
78
+ return self.path.lstrip("/")
79
+
80
+ @property
81
+ def source_rel(self) -> str:
82
+ """HF-source-relative path, leading slash stripped."""
83
+ return self.source_path.lstrip("/")
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class ArtifactSpec:
88
+ """A single named runtime artifact (resolved by ``artifact_ref``)."""
89
+
90
+ artifact_ref: str
91
+ file_list: tuple[ArtifactFile, ...]
92
+ backend_key: str = ""
93
+ device: str = ""
94
+ precision: str = ""
95
+ hf_repo: str = ""
96
+ revision: str = "main"
97
+ sha256: str = ""
98
+ runtime_contract: dict[str, Any] = field(default_factory=dict)
99
+
100
+ def __post_init__(self) -> None:
101
+ if not self.artifact_ref:
102
+ raise ManifestError("artifact spec missing 'artifact_ref'")
103
+ if not self.file_list:
104
+ raise ManifestError(
105
+ f"artifact {self.artifact_ref!r} has an empty file_list"
106
+ )
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class ArtifactManifest:
111
+ """Parsed, validated manifest. Keyed by ``artifact_ref``."""
112
+
113
+ schema_version: int
114
+ artifacts: dict[str, ArtifactSpec]
115
+ hf_endpoint: str = ""
116
+
117
+ def get(self, artifact_ref: str) -> ArtifactSpec:
118
+ """Return the :class:`ArtifactSpec` for ``artifact_ref`` or raise."""
119
+ spec = self.artifacts.get(artifact_ref)
120
+ if spec is None:
121
+ raise ManifestError(
122
+ f"artifact_ref {artifact_ref!r} not found; "
123
+ f"available={sorted(self.artifacts)}"
124
+ )
125
+ return spec
126
+
127
+ def __contains__(self, artifact_ref: str) -> bool:
128
+ return artifact_ref in self.artifacts
129
+
130
+
131
+ def _parse_file(raw: dict[str, Any]) -> ArtifactFile:
132
+ if not isinstance(raw, dict):
133
+ raise ManifestError(f"file_list entry must be an object, got {type(raw)!r}")
134
+ return ArtifactFile(
135
+ path=str(raw.get("path", "")),
136
+ sha256=str(raw.get("sha256", "")),
137
+ source_path=str(raw.get("source_path", "") or ""),
138
+ size_bytes=raw.get("size_bytes"),
139
+ )
140
+
141
+
142
+ def _parse_spec(artifact_ref: str, raw: dict[str, Any]) -> ArtifactSpec:
143
+ if not isinstance(raw, dict):
144
+ raise ManifestError(
145
+ f"artifact {artifact_ref!r} must be an object, got {type(raw)!r}"
146
+ )
147
+ declared_ref = str(raw.get("artifact_ref", "") or artifact_ref)
148
+ if declared_ref != artifact_ref:
149
+ raise ManifestError(
150
+ f"artifact key {artifact_ref!r} != declared artifact_ref "
151
+ f"{declared_ref!r}"
152
+ )
153
+ files_raw = raw.get("file_list")
154
+ if not isinstance(files_raw, list) or not files_raw:
155
+ raise ManifestError(
156
+ f"artifact {artifact_ref!r} must declare a non-empty 'file_list'"
157
+ )
158
+ file_list = tuple(_parse_file(f) for f in files_raw)
159
+ return ArtifactSpec(
160
+ artifact_ref=artifact_ref,
161
+ file_list=file_list,
162
+ backend_key=str(raw.get("backend_key", "") or ""),
163
+ device=str(raw.get("device", "") or ""),
164
+ precision=str(raw.get("precision", "") or ""),
165
+ hf_repo=str(raw.get("hf_repo", "") or ""),
166
+ revision=str(raw.get("revision", "") or "main"),
167
+ sha256=str(raw.get("sha256", "") or ""),
168
+ runtime_contract=dict(raw.get("runtime_contract") or {}),
169
+ )
170
+
171
+
172
+ def parse_manifest(data: dict[str, Any]) -> ArtifactManifest:
173
+ """Validate and parse an in-memory manifest dict."""
174
+ if not isinstance(data, dict):
175
+ raise ManifestError(f"manifest must be a JSON object, got {type(data)!r}")
176
+ schema_version = data.get("schema_version")
177
+ if schema_version != SCHEMA_VERSION:
178
+ raise ManifestError(
179
+ f"unsupported schema_version {schema_version!r} "
180
+ f"(expected {SCHEMA_VERSION})"
181
+ )
182
+ artifacts_raw = data.get("artifacts")
183
+ if not isinstance(artifacts_raw, dict) or not artifacts_raw:
184
+ raise ManifestError("manifest must declare a non-empty 'artifacts' map")
185
+ artifacts = {
186
+ ref: _parse_spec(ref, spec) for ref, spec in artifacts_raw.items()
187
+ }
188
+ return ArtifactManifest(
189
+ schema_version=schema_version,
190
+ artifacts=artifacts,
191
+ hf_endpoint=str(data.get("hf_endpoint", "") or ""),
192
+ )
193
+
194
+
195
+ def load_manifest(manifest_path: str | Path) -> ArtifactManifest:
196
+ """Load and validate a manifest from a local JSON file path."""
197
+ path = Path(manifest_path)
198
+ if not path.exists():
199
+ raise ManifestError(f"manifest_path not found: {path}")
200
+ try:
201
+ data = json.loads(path.read_text())
202
+ except json.JSONDecodeError as exc:
203
+ raise ManifestError(f"manifest is not valid JSON: {path}: {exc}") from exc
204
+ return parse_manifest(data)
205
+
206
+
207
+ def default_manifest_path() -> Path:
208
+ """Path to the sample manifest bundled with the voxedge package."""
209
+ return Path(__file__).with_name("manifest.json")
@@ -0,0 +1,36 @@
1
+ """voxedge backend ABCs + mock implementations."""
2
+ from __future__ import annotations
3
+
4
+ from voxedge.backends.base import (
5
+ ASRBackend,
6
+ ASRCapability,
7
+ ASRStream,
8
+ LLMBackend,
9
+ LLMEvent,
10
+ TranscriptionResult,
11
+ TranslationResult,
12
+ TranslatorBackend,
13
+ TranslatorCapability,
14
+ TranslatorConfig,
15
+ TTSBackend,
16
+ TTSCapability,
17
+ VADBackend,
18
+ VADSession,
19
+ )
20
+
21
+ __all__ = [
22
+ "ASRBackend",
23
+ "ASRCapability",
24
+ "ASRStream",
25
+ "LLMBackend",
26
+ "LLMEvent",
27
+ "TranscriptionResult",
28
+ "TranslationResult",
29
+ "TranslatorBackend",
30
+ "TranslatorCapability",
31
+ "TranslatorConfig",
32
+ "TTSBackend",
33
+ "TTSCapability",
34
+ "VADBackend",
35
+ "VADSession",
36
+ ]
@@ -0,0 +1,100 @@
1
+ """Friendly optional-dependency checks for voxedge backend extras.
2
+
3
+ The heavy backend adapters (jetson / rk / sherpa) import their native runtime
4
+ packages LAZILY (inside ``__init__`` / ``preload`` / per-call methods) so the
5
+ core stays pip-installable on a CUDA-less, NPU-less dev box (see pyproject
6
+ ``[project.optional-dependencies]``). The flip side: when a user selects a
7
+ backend whose extra was never installed, the lazy import previously surfaced as
8
+ a bare, opaque ``ModuleNotFoundError: No module named 'sherpa_onnx'`` — with no
9
+ hint about which extra to install.
10
+
11
+ This module centralises a single ``require()`` helper that turns a missing
12
+ package into a clear, actionable ``ImportError`` naming both the missing
13
+ distribution and the exact ``pip install voxedge[<extra>]`` incantation. Each
14
+ backend's lazy import site wraps its import in ``require(...)`` (or calls the
15
+ convenience ``_check_*`` helpers) so the failure mode is explicit.
16
+
17
+ This file imports nothing heavy itself — it is pure stdlib and safe to import
18
+ from the pure-Python core.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import importlib
24
+ from typing import Optional
25
+
26
+
27
+ def require(
28
+ module: str,
29
+ *,
30
+ extra: str,
31
+ package: Optional[str] = None,
32
+ ):
33
+ """Import ``module`` or raise a clear, actionable ImportError.
34
+
35
+ Parameters
36
+ ----------
37
+ module:
38
+ The importable module name (e.g. ``"sherpa_onnx"``).
39
+ extra:
40
+ The voxedge optional-dependency extra that provides it
41
+ (e.g. ``"sherpa"`` → ``pip install voxedge[sherpa]``).
42
+ package:
43
+ The pip distribution name when it differs from ``module``
44
+ (e.g. module ``cuda`` ships in ``cuda-python``). Used only to make the
45
+ error message accurate; defaults to ``module``.
46
+
47
+ Returns
48
+ -------
49
+ The imported module object.
50
+
51
+ Raises
52
+ ------
53
+ ImportError
54
+ With a message naming the missing package + the install command.
55
+ """
56
+ try:
57
+ return importlib.import_module(module)
58
+ except ImportError as exc:
59
+ dist = package or module
60
+ raise ImportError(
61
+ f"voxedge[{extra}] requires {dist!r} (module {module!r}), which is "
62
+ f"not installed. Install the extra with: "
63
+ f"pip install 'voxedge[{extra}]'"
64
+ ) from exc
65
+
66
+
67
+ def require_all(specs, *, extra: str) -> None:
68
+ """Check several modules up front; raise on the first that is missing.
69
+
70
+ ``specs`` is an iterable of either ``module`` strings or
71
+ ``(module, package)`` tuples. Use this in a backend ``preload()`` / first
72
+ use to fail fast with a friendly message before any partial work.
73
+ """
74
+ for spec in specs:
75
+ if isinstance(spec, (tuple, list)):
76
+ module, package = spec[0], spec[1]
77
+ else:
78
+ module, package = spec, None
79
+ require(module, extra=extra, package=package)
80
+
81
+
82
+ # ── convenience wrappers per extra ───────────────────────────────────────────
83
+
84
+
85
+ def check_sherpa_deps() -> None:
86
+ """Fail fast with a friendly error if the ``sherpa`` extra is missing."""
87
+ require_all([("sherpa_onnx", "sherpa-onnx")], extra="sherpa")
88
+
89
+
90
+ def check_rk_deps() -> None:
91
+ """Fail fast with a friendly error if the ``rk`` extra is missing."""
92
+ require_all([("rkvoice_stream", "rkvoice-stream")], extra="rk")
93
+
94
+
95
+ def check_translator_deps() -> None:
96
+ """Fail fast with a friendly error if the ``translator`` extra is missing."""
97
+ require_all(
98
+ [("ctranslate2", "ctranslate2"), ("sentencepiece", "sentencepiece")],
99
+ extra="translator",
100
+ )