ekko-stt 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.
- ekko_stt-0.1.0/LICENSE +21 -0
- ekko_stt-0.1.0/PKG-INFO +95 -0
- ekko_stt-0.1.0/README.md +57 -0
- ekko_stt-0.1.0/THIRD_PARTY_NOTICES.md +17 -0
- ekko_stt-0.1.0/pyproject.toml +53 -0
- ekko_stt-0.1.0/setup.cfg +4 -0
- ekko_stt-0.1.0/src/ekko/__init__.py +10 -0
- ekko_stt-0.1.0/src/ekko/__main__.py +4 -0
- ekko_stt-0.1.0/src/ekko/artifacts.py +371 -0
- ekko_stt-0.1.0/src/ekko/audio.py +82 -0
- ekko_stt-0.1.0/src/ekko/cli.py +202 -0
- ekko_stt-0.1.0/src/ekko/pnc.py +199 -0
- ekko_stt-0.1.0/src/ekko/release_manifest.json +193 -0
- ekko_stt-0.1.0/src/ekko/runtime.py +342 -0
- ekko_stt-0.1.0/src/ekko/tiny_onnx.py +157 -0
- ekko_stt-0.1.0/src/ekko_stt.egg-info/PKG-INFO +95 -0
- ekko_stt-0.1.0/src/ekko_stt.egg-info/SOURCES.txt +27 -0
- ekko_stt-0.1.0/src/ekko_stt.egg-info/dependency_links.txt +1 -0
- ekko_stt-0.1.0/src/ekko_stt.egg-info/entry_points.txt +2 -0
- ekko_stt-0.1.0/src/ekko_stt.egg-info/requires.txt +19 -0
- ekko_stt-0.1.0/src/ekko_stt.egg-info/top_level.txt +1 -0
- ekko_stt-0.1.0/tests/test_artifacts.py +181 -0
- ekko_stt-0.1.0/tests/test_audio.py +76 -0
- ekko_stt-0.1.0/tests/test_browser_space.py +273 -0
- ekko_stt-0.1.0/tests/test_cli.py +201 -0
- ekko_stt-0.1.0/tests/test_pnc.py +115 -0
- ekko_stt-0.1.0/tests/test_release_manifest.py +59 -0
- ekko_stt-0.1.0/tests/test_runtime.py +227 -0
- ekko_stt-0.1.0/tests/test_tiny_onnx.py +139 -0
ekko_stt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emil Schledermann / RyeAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ekko_stt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ekko-stt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local Danish speech recognition and punctuation with Ekko Tiny and PnC
|
|
5
|
+
Author: Emil Schledermann / RyeAI
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Rye-A1/ekko-danish-stt
|
|
8
|
+
Project-URL: Issues, https://github.com/Rye-A1/ekko-danish-stt/issues
|
|
9
|
+
Keywords: asr,danish,speech-to-text,onnx,gguf
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
21
|
+
Requires-Dist: filelock<4,>=3.13
|
|
22
|
+
Requires-Dist: huggingface-hub<2,>=0.27
|
|
23
|
+
Requires-Dist: numpy<3,>=1.26
|
|
24
|
+
Requires-Dist: soundfile<1,>=0.12
|
|
25
|
+
Requires-Dist: soxr<2,>=0.5
|
|
26
|
+
Provides-Extra: pnc
|
|
27
|
+
Requires-Dist: onnxruntime==1.29.0; extra == "pnc"
|
|
28
|
+
Requires-Dist: tokenizers==0.22.2; extra == "pnc"
|
|
29
|
+
Provides-Extra: tiny-onnx
|
|
30
|
+
Requires-Dist: sherpa-onnx==1.13.6; extra == "tiny-onnx"
|
|
31
|
+
Requires-Dist: sherpa-onnx-core==1.13.6; extra == "tiny-onnx"
|
|
32
|
+
Provides-Extra: local
|
|
33
|
+
Requires-Dist: onnxruntime==1.29.0; extra == "local"
|
|
34
|
+
Requires-Dist: sherpa-onnx==1.13.6; extra == "local"
|
|
35
|
+
Requires-Dist: sherpa-onnx-core==1.13.6; extra == "local"
|
|
36
|
+
Requires-Dist: tokenizers==0.22.2; extra == "local"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# Ekko STT
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
Local inference for [Ekko v1 Tiny](https://huggingface.co/RyeAI/ekko-v1-tiny),
|
|
44
|
+
a Danish speech recognizer with word timestamps, and
|
|
45
|
+
[Ekko PnC](https://huggingface.co/RyeAI/ekko-pnc), its optional punctuation and
|
|
46
|
+
capitalization model. This repository contains the Python runtime and the
|
|
47
|
+
[browser demo](https://huggingface.co/spaces/RyeAI/ekko-tiny-browser).
|
|
48
|
+
|
|
49
|
+
| Command | Runtime | Default model |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `ekko tiny` | parakeet.cpp | Q5_0 GGUF |
|
|
52
|
+
| `ekko tiny-onnx` | sherpa-onnx | int8 ONNX |
|
|
53
|
+
| `ekko pnc` | ONNX Runtime | PnC int8 |
|
|
54
|
+
|
|
55
|
+
## Run locally
|
|
56
|
+
|
|
57
|
+
Install [uv](https://docs.astral.sh/uv/), then from this checkout:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv sync --locked --no-editable --extra pnc
|
|
61
|
+
uv run --no-sync ekko tiny audio.wav --pnc --format json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To format an existing transcript without running speech recognition:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv run --no-sync ekko pnc "hej mit navn er emil hvordan går det i dag"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For the ONNX runtime:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync --locked --no-editable --extra tiny-onnx --extra pnc
|
|
74
|
+
uv run --no-sync ekko tiny-onnx audio.wav --precision int8 --pnc --format json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The first run downloads model artifacts from Hugging Face and the native
|
|
78
|
+
parakeet.cpp library when needed. Downloads use immutable revisions and
|
|
79
|
+
SHA-256 checks in [the release manifest](https://github.com/Rye-A1/ekko-danish-stt/blob/main/src/ekko/release_manifest.json).
|
|
80
|
+
Set `EKKO_OFFLINE=1` after prefetching to prevent network access.
|
|
81
|
+
|
|
82
|
+
The [browser demo](https://huggingface.co/spaces/RyeAI/ekko-tiny-browser) runs Tiny and PnC locally
|
|
83
|
+
through WebAssembly. Audio and transcripts stay on the device.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv sync --locked --extra local
|
|
89
|
+
uv run --no-sync python -m unittest discover -s tests -p 'test_*.py'
|
|
90
|
+
uv build
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The source code is MIT licensed. Tiny and PnC weights have their own terms in
|
|
94
|
+
their Hugging Face repositories. See [third-party notices](https://github.com/Rye-A1/ekko-danish-stt/blob/main/THIRD_PARTY_NOTICES.md)
|
|
95
|
+
for runtime dependencies.
|
ekko_stt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Ekko STT
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Local inference for [Ekko v1 Tiny](https://huggingface.co/RyeAI/ekko-v1-tiny),
|
|
6
|
+
a Danish speech recognizer with word timestamps, and
|
|
7
|
+
[Ekko PnC](https://huggingface.co/RyeAI/ekko-pnc), its optional punctuation and
|
|
8
|
+
capitalization model. This repository contains the Python runtime and the
|
|
9
|
+
[browser demo](https://huggingface.co/spaces/RyeAI/ekko-tiny-browser).
|
|
10
|
+
|
|
11
|
+
| Command | Runtime | Default model |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `ekko tiny` | parakeet.cpp | Q5_0 GGUF |
|
|
14
|
+
| `ekko tiny-onnx` | sherpa-onnx | int8 ONNX |
|
|
15
|
+
| `ekko pnc` | ONNX Runtime | PnC int8 |
|
|
16
|
+
|
|
17
|
+
## Run locally
|
|
18
|
+
|
|
19
|
+
Install [uv](https://docs.astral.sh/uv/), then from this checkout:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv sync --locked --no-editable --extra pnc
|
|
23
|
+
uv run --no-sync ekko tiny audio.wav --pnc --format json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
To format an existing transcript without running speech recognition:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv run --no-sync ekko pnc "hej mit navn er emil hvordan går det i dag"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For the ONNX runtime:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv sync --locked --no-editable --extra tiny-onnx --extra pnc
|
|
36
|
+
uv run --no-sync ekko tiny-onnx audio.wav --precision int8 --pnc --format json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The first run downloads model artifacts from Hugging Face and the native
|
|
40
|
+
parakeet.cpp library when needed. Downloads use immutable revisions and
|
|
41
|
+
SHA-256 checks in [the release manifest](https://github.com/Rye-A1/ekko-danish-stt/blob/main/src/ekko/release_manifest.json).
|
|
42
|
+
Set `EKKO_OFFLINE=1` after prefetching to prevent network access.
|
|
43
|
+
|
|
44
|
+
The [browser demo](https://huggingface.co/spaces/RyeAI/ekko-tiny-browser) runs Tiny and PnC locally
|
|
45
|
+
through WebAssembly. Audio and transcripts stay on the device.
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv sync --locked --extra local
|
|
51
|
+
uv run --no-sync python -m unittest discover -s tests -p 'test_*.py'
|
|
52
|
+
uv build
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The source code is MIT licensed. Tiny and PnC weights have their own terms in
|
|
56
|
+
their Hugging Face repositories. See [third-party notices](https://github.com/Rye-A1/ekko-danish-stt/blob/main/THIRD_PARTY_NOTICES.md)
|
|
57
|
+
for runtime dependencies.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Ekko source code is MIT licensed. It integrates these separately licensed
|
|
4
|
+
projects and artifacts:
|
|
5
|
+
|
|
6
|
+
- [parakeet.cpp](https://github.com/mudler/parakeet.cpp), MIT. Downloaded native
|
|
7
|
+
bundles include the upstream license.
|
|
8
|
+
- [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx), Apache-2.0.
|
|
9
|
+
- [ONNX Runtime](https://github.com/microsoft/onnxruntime), MIT.
|
|
10
|
+
- [Hugging Face tokenizers](https://github.com/huggingface/tokenizers), Apache-2.0.
|
|
11
|
+
- [libsndfile](https://github.com/libsndfile/libsndfile), LGPL-2.1-or-later.
|
|
12
|
+
- [libsoxr](https://sourceforge.net/projects/soxr/), LGPL-2.1-or-later.
|
|
13
|
+
|
|
14
|
+
Model weights have separate terms. See the
|
|
15
|
+
[Tiny model license](https://huggingface.co/RyeAI/ekko-v1-tiny/blob/main/MODEL_LICENSE.md)
|
|
16
|
+
and [PnC model license](https://huggingface.co/RyeAI/ekko-pnc/blob/main/MODEL_LICENSE.md)
|
|
17
|
+
with their bundled notices on Hugging Face.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==84.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ekko-stt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local Danish speech recognition and punctuation with Ekko Tiny and PnC"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]
|
|
13
|
+
authors = [{name = "Emil Schledermann / RyeAI"}]
|
|
14
|
+
keywords = ["asr", "danish", "speech-to-text", "onnx", "gguf"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Operating System :: MacOS",
|
|
18
|
+
"Operating System :: Microsoft :: Windows",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"filelock>=3.13,<4",
|
|
26
|
+
"huggingface-hub>=0.27,<2",
|
|
27
|
+
"numpy>=1.26,<3",
|
|
28
|
+
"soundfile>=0.12,<1",
|
|
29
|
+
"soxr>=0.5,<2",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
pnc = ["onnxruntime==1.29.0", "tokenizers==0.22.2"]
|
|
34
|
+
tiny-onnx = ["sherpa-onnx==1.13.6", "sherpa-onnx-core==1.13.6"]
|
|
35
|
+
local = [
|
|
36
|
+
"onnxruntime==1.29.0",
|
|
37
|
+
"sherpa-onnx==1.13.6",
|
|
38
|
+
"sherpa-onnx-core==1.13.6",
|
|
39
|
+
"tokenizers==0.22.2",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
ekko = "ekko.cli:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/Rye-A1/ekko-danish-stt"
|
|
47
|
+
Issues = "https://github.com/Rye-A1/ekko-danish-stt/issues"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
ekko = ["release_manifest.json"]
|
ekko_stt-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Local inference adapters for Ekko v1 Tiny."""
|
|
2
|
+
|
|
3
|
+
from .artifacts import RELEASE_MANIFEST
|
|
4
|
+
from .pnc import Punctuator
|
|
5
|
+
from .runtime import (EkkoError, TinyBackend, TinyCLIBackend, Transcript, Word, validate_transcript)
|
|
6
|
+
from .tiny_onnx import TinyOnnxBackend
|
|
7
|
+
|
|
8
|
+
__all__ = ["EkkoError", "Punctuator", "RELEASE_MANIFEST", "TinyBackend",
|
|
9
|
+
"TinyCLIBackend", "TinyOnnxBackend", "Transcript", "Word",
|
|
10
|
+
"validate_transcript"]
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import importlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
import sys
|
|
11
|
+
import tarfile
|
|
12
|
+
import tempfile
|
|
13
|
+
import urllib.request
|
|
14
|
+
import zipfile
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from importlib.resources import files
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from filelock import FileLock
|
|
20
|
+
|
|
21
|
+
from .runtime import EkkoError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class NativeArtifact:
|
|
26
|
+
archive: str
|
|
27
|
+
sha256: str
|
|
28
|
+
directory: str
|
|
29
|
+
library: str
|
|
30
|
+
library_sha256: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class HubArtifact:
|
|
35
|
+
repo_id: str
|
|
36
|
+
revision: str
|
|
37
|
+
filename: str
|
|
38
|
+
sha256: str
|
|
39
|
+
dialect: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
RELEASE_MANIFEST = json.loads(
|
|
43
|
+
files("ekko").joinpath("release_manifest.json").read_text(encoding="utf-8")
|
|
44
|
+
)
|
|
45
|
+
HUB_ARTIFACTS = {
|
|
46
|
+
name: HubArtifact(**metadata)
|
|
47
|
+
for name, metadata in RELEASE_MANIFEST["artifacts"].items()
|
|
48
|
+
}
|
|
49
|
+
HUB_LIBRARY_NAME = RELEASE_MANIFEST["distribution"]
|
|
50
|
+
HUB_LIBRARY_VERSION = RELEASE_MANIFEST["release_version"]
|
|
51
|
+
PARAKEET_VERSION = RELEASE_MANIFEST["native_runtime"]["version"]
|
|
52
|
+
PARAKEET_RELEASE_URL = RELEASE_MANIFEST["native_runtime"]["release_url"]
|
|
53
|
+
_PARAKEET_ARTIFACTS = {
|
|
54
|
+
tuple(key.split("/", 1)): NativeArtifact(**metadata)
|
|
55
|
+
for key, metadata in RELEASE_MANIFEST["native_runtime"]["artifacts"].items()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cache_root() -> Path:
|
|
60
|
+
override = os.environ.get("EKKO_CACHE_DIR")
|
|
61
|
+
if override:
|
|
62
|
+
return Path(override).expanduser()
|
|
63
|
+
current_platform = _platform_name()
|
|
64
|
+
if current_platform == "darwin":
|
|
65
|
+
return Path.home() / "Library" / "Caches" / "ekko"
|
|
66
|
+
if current_platform == "win32":
|
|
67
|
+
return Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) / "ekko"
|
|
68
|
+
return Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "ekko"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _platform_name() -> str:
|
|
72
|
+
return sys.platform
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _machine(value: str | None = None) -> str:
|
|
76
|
+
machine = (value or platform.machine()).lower()
|
|
77
|
+
return {"arm64": "arm64", "aarch64": "aarch64", "amd64": "amd64", "x86_64": "x86_64"}.get(
|
|
78
|
+
machine, machine
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def native_artifact(
|
|
83
|
+
*,
|
|
84
|
+
system: str | None = None,
|
|
85
|
+
machine: str | None = None,
|
|
86
|
+
libc: tuple[str, str] | None = None,
|
|
87
|
+
) -> NativeArtifact:
|
|
88
|
+
current_system = system or _platform_name()
|
|
89
|
+
current_machine = _machine(machine)
|
|
90
|
+
key = (current_system, current_machine)
|
|
91
|
+
if key == ("linux", "arm64"):
|
|
92
|
+
key = ("linux", "aarch64")
|
|
93
|
+
if key == ("win32", "x86_64"):
|
|
94
|
+
key = ("win32", "amd64")
|
|
95
|
+
try:
|
|
96
|
+
artifact = _PARAKEET_ARTIFACTS[key]
|
|
97
|
+
except KeyError as error:
|
|
98
|
+
raise EkkoError(
|
|
99
|
+
f"no prebuilt parakeet.cpp {PARAKEET_VERSION} library for "
|
|
100
|
+
f"{current_system}/{current_machine}"
|
|
101
|
+
) from error
|
|
102
|
+
if current_system == "linux":
|
|
103
|
+
libc_name, libc_version = libc or platform.libc_ver()
|
|
104
|
+
try:
|
|
105
|
+
version = tuple(int(part) for part in libc_version.split(".")[:2])
|
|
106
|
+
except ValueError:
|
|
107
|
+
version = ()
|
|
108
|
+
if libc_name.lower() != "glibc" or version < (2, 34):
|
|
109
|
+
raise EkkoError(
|
|
110
|
+
"the published parakeet.cpp Linux library requires glibc >= 2.34; "
|
|
111
|
+
f"detected {libc_name or 'unknown'} {libc_version or 'unknown'}"
|
|
112
|
+
)
|
|
113
|
+
return artifact
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def sha256_file(path: Path) -> str:
|
|
117
|
+
digest = hashlib.sha256()
|
|
118
|
+
with path.open("rb") as stream:
|
|
119
|
+
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
120
|
+
digest.update(block)
|
|
121
|
+
return digest.hexdigest()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _download_verified_hub_file(
|
|
125
|
+
hub: object,
|
|
126
|
+
*,
|
|
127
|
+
repo_id: str,
|
|
128
|
+
revision: str,
|
|
129
|
+
filename: str,
|
|
130
|
+
expected_sha256: str,
|
|
131
|
+
local_only: bool,
|
|
132
|
+
) -> Path:
|
|
133
|
+
lock_root = cache_root() / "locks"
|
|
134
|
+
lock_root.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
lock = FileLock(str(lock_root / f"hub-{expected_sha256}.lock"))
|
|
136
|
+
|
|
137
|
+
def download(*, force: bool) -> Path:
|
|
138
|
+
try:
|
|
139
|
+
return Path(
|
|
140
|
+
hub.hf_hub_download(
|
|
141
|
+
repo_id=repo_id,
|
|
142
|
+
filename=filename,
|
|
143
|
+
revision=revision,
|
|
144
|
+
cache_dir=cache_root() / "huggingface",
|
|
145
|
+
local_files_only=local_only,
|
|
146
|
+
force_download=force,
|
|
147
|
+
library_name=HUB_LIBRARY_NAME,
|
|
148
|
+
library_version=HUB_LIBRARY_VERSION,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
except Exception as error:
|
|
152
|
+
raise EkkoError(
|
|
153
|
+
f"could not download {repo_id}/{filename} at {revision}: {error}"
|
|
154
|
+
) from error
|
|
155
|
+
|
|
156
|
+
def actual_hash(path: Path) -> str:
|
|
157
|
+
try:
|
|
158
|
+
return sha256_file(path)
|
|
159
|
+
except OSError as error:
|
|
160
|
+
raise EkkoError(f"could not hash cached artifact {path}: {error}") from error
|
|
161
|
+
|
|
162
|
+
with lock:
|
|
163
|
+
path = download(force=False)
|
|
164
|
+
actual = actual_hash(path)
|
|
165
|
+
if actual == expected_sha256:
|
|
166
|
+
return path
|
|
167
|
+
if not local_only:
|
|
168
|
+
path = download(force=True)
|
|
169
|
+
actual = actual_hash(path)
|
|
170
|
+
if actual == expected_sha256:
|
|
171
|
+
return path
|
|
172
|
+
raise EkkoError(
|
|
173
|
+
f"checksum mismatch for {repo_id}/{filename}: "
|
|
174
|
+
f"expected {expected_sha256}, got {actual}; remove the cached file "
|
|
175
|
+
"and retry"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _ensure_model_licenses(
|
|
180
|
+
hub: object, *, repo_id: str, revision: str, local_only: bool
|
|
181
|
+
) -> None:
|
|
182
|
+
model = next(
|
|
183
|
+
(
|
|
184
|
+
metadata
|
|
185
|
+
for metadata in RELEASE_MANIFEST["models"].values()
|
|
186
|
+
if metadata["repo_id"] == repo_id
|
|
187
|
+
),
|
|
188
|
+
None,
|
|
189
|
+
)
|
|
190
|
+
if model is None:
|
|
191
|
+
return
|
|
192
|
+
if model["snapshot_revision"] != revision:
|
|
193
|
+
raise EkkoError(f"model and legal files use different revisions for {repo_id}")
|
|
194
|
+
for filename, expected_sha256 in model["license_files"].items():
|
|
195
|
+
_download_verified_hub_file(
|
|
196
|
+
hub,
|
|
197
|
+
repo_id=repo_id,
|
|
198
|
+
revision=revision,
|
|
199
|
+
filename=filename,
|
|
200
|
+
expected_sha256=expected_sha256,
|
|
201
|
+
local_only=local_only,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def ensure_hub_artifact(name: str, *, offline: bool = False) -> Path:
|
|
206
|
+
try:
|
|
207
|
+
artifact = HUB_ARTIFACTS[name]
|
|
208
|
+
except KeyError as error:
|
|
209
|
+
raise EkkoError(f"unknown Ekko artifact: {name}") from error
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
hub = importlib.import_module("huggingface_hub")
|
|
213
|
+
except ImportError as error:
|
|
214
|
+
raise EkkoError(
|
|
215
|
+
"model download requires huggingface-hub; install ekko-stt"
|
|
216
|
+
) from error
|
|
217
|
+
|
|
218
|
+
local_only = offline or os.environ.get("EKKO_OFFLINE") == "1"
|
|
219
|
+
path = _download_verified_hub_file(
|
|
220
|
+
hub,
|
|
221
|
+
repo_id=artifact.repo_id,
|
|
222
|
+
revision=artifact.revision,
|
|
223
|
+
filename=artifact.filename,
|
|
224
|
+
expected_sha256=artifact.sha256,
|
|
225
|
+
local_only=local_only,
|
|
226
|
+
)
|
|
227
|
+
_ensure_model_licenses(
|
|
228
|
+
hub,
|
|
229
|
+
repo_id=artifact.repo_id,
|
|
230
|
+
revision=artifact.revision,
|
|
231
|
+
local_only=local_only,
|
|
232
|
+
)
|
|
233
|
+
return path
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def ensure_pnc_directory(*, offline: bool = False) -> Path:
|
|
237
|
+
paths = [
|
|
238
|
+
ensure_hub_artifact(name, offline=offline)
|
|
239
|
+
for name in ("pnc-model", "pnc-tokenizer", "pnc-config")
|
|
240
|
+
]
|
|
241
|
+
parents = {path.parent for path in paths}
|
|
242
|
+
if len(parents) != 1:
|
|
243
|
+
raise EkkoError("PnC files did not resolve to one pinned Hub snapshot")
|
|
244
|
+
return parents.pop()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def ensure_tiny_onnx_directory(
|
|
248
|
+
precision: str, *, offline: bool = False
|
|
249
|
+
) -> Path:
|
|
250
|
+
if precision not in {"fp32", "int8"}:
|
|
251
|
+
raise EkkoError(f"unsupported Tiny ONNX precision: {precision}")
|
|
252
|
+
paths = [
|
|
253
|
+
ensure_hub_artifact(
|
|
254
|
+
f"tiny-onnx-{precision}-{component}", offline=offline
|
|
255
|
+
)
|
|
256
|
+
for component in ("encoder", "decoder", "joiner")
|
|
257
|
+
]
|
|
258
|
+
paths.append(ensure_hub_artifact("tiny-onnx-tokens", offline=offline))
|
|
259
|
+
parents = {path.parent for path in paths}
|
|
260
|
+
if len(parents) != 1:
|
|
261
|
+
raise EkkoError("Tiny ONNX files did not resolve to one pinned Hub snapshot")
|
|
262
|
+
return parents.pop()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _safe_target(root: Path, name: str) -> Path:
|
|
266
|
+
target = (root / name).resolve()
|
|
267
|
+
if target != root.resolve() and root.resolve() not in target.parents:
|
|
268
|
+
raise EkkoError(f"unsafe path in native runtime archive: {name}")
|
|
269
|
+
return target
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _extract_archive(archive: Path, destination: Path) -> None:
|
|
273
|
+
if archive.suffix == ".zip":
|
|
274
|
+
with zipfile.ZipFile(archive) as bundle:
|
|
275
|
+
for member in bundle.infolist():
|
|
276
|
+
_safe_target(destination, member.filename)
|
|
277
|
+
mode = member.external_attr >> 16
|
|
278
|
+
if stat.S_ISLNK(mode):
|
|
279
|
+
raise EkkoError(
|
|
280
|
+
f"links are not allowed in native runtime archive: "
|
|
281
|
+
f"{member.filename}"
|
|
282
|
+
)
|
|
283
|
+
bundle.extractall(destination)
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
with tarfile.open(archive, "r:gz") as bundle:
|
|
287
|
+
for member in bundle.getmembers():
|
|
288
|
+
_safe_target(destination, member.name)
|
|
289
|
+
if member.issym() or member.islnk():
|
|
290
|
+
raise EkkoError(
|
|
291
|
+
f"links are not allowed in native runtime archive: {member.name}"
|
|
292
|
+
)
|
|
293
|
+
bundle.extractall(destination)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def ensure_parakeet_library(*, offline: bool = False) -> Path:
|
|
297
|
+
override = os.environ.get("EKKO_PARAKEET_LIBRARY")
|
|
298
|
+
if override:
|
|
299
|
+
path = Path(override).expanduser()
|
|
300
|
+
if not path.is_file():
|
|
301
|
+
raise EkkoError(f"EKKO_PARAKEET_LIBRARY does not exist: {path}")
|
|
302
|
+
return path
|
|
303
|
+
|
|
304
|
+
artifact = native_artifact()
|
|
305
|
+
install_root = (
|
|
306
|
+
cache_root()
|
|
307
|
+
/ "native"
|
|
308
|
+
/ f"parakeet-{PARAKEET_VERSION}-{artifact.sha256[:12]}"
|
|
309
|
+
)
|
|
310
|
+
library = install_root / artifact.directory / artifact.library
|
|
311
|
+
install_root.parent.mkdir(parents=True, exist_ok=True)
|
|
312
|
+
lock = FileLock(str(install_root) + ".lock")
|
|
313
|
+
with lock:
|
|
314
|
+
if library.is_file():
|
|
315
|
+
actual = sha256_file(library)
|
|
316
|
+
if actual != artifact.library_sha256:
|
|
317
|
+
raise EkkoError(
|
|
318
|
+
f"cached native library checksum mismatch at {library}; "
|
|
319
|
+
"remove its versioned cache directory and retry"
|
|
320
|
+
)
|
|
321
|
+
return library
|
|
322
|
+
if offline or os.environ.get("EKKO_OFFLINE") == "1":
|
|
323
|
+
raise EkkoError(f"parakeet.cpp runtime is not cached at {library}")
|
|
324
|
+
|
|
325
|
+
with tempfile.TemporaryDirectory(
|
|
326
|
+
prefix="parakeet-download-", dir=install_root.parent
|
|
327
|
+
) as temporary:
|
|
328
|
+
temporary_path = Path(temporary)
|
|
329
|
+
archive = temporary_path / artifact.archive
|
|
330
|
+
request = urllib.request.Request(
|
|
331
|
+
f"{PARAKEET_RELEASE_URL}/{artifact.archive}",
|
|
332
|
+
headers={"User-Agent": "ekko-runtime"},
|
|
333
|
+
)
|
|
334
|
+
try:
|
|
335
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
336
|
+
with archive.open("wb") as output:
|
|
337
|
+
shutil.copyfileobj(response, output)
|
|
338
|
+
except OSError as error:
|
|
339
|
+
raise EkkoError(
|
|
340
|
+
f"could not download parakeet.cpp runtime: {error}"
|
|
341
|
+
) from error
|
|
342
|
+
actual = sha256_file(archive)
|
|
343
|
+
if actual != artifact.sha256:
|
|
344
|
+
raise EkkoError(
|
|
345
|
+
f"parakeet.cpp archive checksum mismatch: expected "
|
|
346
|
+
f"{artifact.sha256}, got {actual}"
|
|
347
|
+
)
|
|
348
|
+
extracted = temporary_path / "extracted"
|
|
349
|
+
extracted.mkdir()
|
|
350
|
+
_extract_archive(archive, extracted)
|
|
351
|
+
candidate = extracted / artifact.directory / artifact.library
|
|
352
|
+
if not candidate.is_file():
|
|
353
|
+
raise EkkoError(f"native runtime archive lacks {artifact.library}")
|
|
354
|
+
actual_library = sha256_file(candidate)
|
|
355
|
+
if actual_library != artifact.library_sha256:
|
|
356
|
+
raise EkkoError(
|
|
357
|
+
f"parakeet.cpp library checksum mismatch: expected "
|
|
358
|
+
f"{artifact.library_sha256}, got {actual_library}"
|
|
359
|
+
)
|
|
360
|
+
try:
|
|
361
|
+
os.replace(extracted, install_root)
|
|
362
|
+
except FileExistsError:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
if not library.is_file():
|
|
366
|
+
raise EkkoError(
|
|
367
|
+
f"native runtime installation failed: {library}"
|
|
368
|
+
)
|
|
369
|
+
if sha256_file(library) != artifact.library_sha256:
|
|
370
|
+
raise EkkoError(f"installed native library checksum mismatch: {library}")
|
|
371
|
+
return library
|