audioreconstructor 1.0.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,31 @@
1
+ # dataset files
2
+ data/
3
+
4
+ # Python-generated files
5
+ __pycache__/
6
+ *.py[oc]
7
+ build/
8
+ dist/
9
+ wheels/
10
+ *.egg-info
11
+
12
+ # Virtual environments
13
+ .venv
14
+
15
+ # outputfiles
16
+ output/
17
+
18
+ # model checkpoints
19
+ model/checkpoints/*
20
+ !model/checkpoints/best/*
21
+ !model/checkpoints/config.json
22
+
23
+
24
+ media/recordings/*
25
+ !media/recordings/*.gif
26
+
27
+ design/index.html
28
+
29
+ .claude/skills/impeccable/scripts
30
+ .github/skills/impeccable/scripts
31
+ .opencode/skills/impeccable/scripts/
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: audioreconstructor
3
+ Version: 1.0.0
4
+ Summary: Command-line audio reconstruction powered by the Audioreconstruction ONNX model
5
+ Project-URL: Homepage, https://github.com/rohan-prasen/audioreconstruction
6
+ Project-URL: Repository, https://github.com/rohan-prasen/audioreconstruction
7
+ Project-URL: Issues, https://github.com/rohan-prasen/audioreconstruction/issues
8
+ Author-email: Rohan Prasen Kedari <rohanprasenkedari@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: audio,cli,onnx,super-resolution
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Sound/Audio
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+
25
+ # audioreconstructor
26
+
27
+ `audioreconstructor` is the command-line release of Audioreconstruction's ONNX
28
+ audio enhancer. It installs a small Python launcher; the ONNX model and the native
29
+ runtime are downloaded only when you explicitly run setup.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install audioreconstructor
35
+ audioreconstructor --setup
36
+ ```
37
+
38
+ `--setup` downloads the native executable, `model.onnx`, and `config.json` from the
39
+ GitHub Release matching the installed package version. It verifies SHA-256 hashes
40
+ before making them available locally.
41
+
42
+ The current release supports 64-bit Linux and 64-bit Windows.
43
+
44
+ ## Use
45
+
46
+ ```bash
47
+ audioreconstructor --input song.mp3 --output song_enhanced.flac
48
+ ```
49
+
50
+ Choose the ONNX execution provider when needed:
51
+
52
+ ```bash
53
+ audioreconstructor --input song.mp3 --output song_enhanced.flac --provider cpu
54
+ ```
55
+
56
+ `auto` is the default. Windows first tries DirectML and falls back to CPU; Linux uses
57
+ CPU. The native executable reads supported audio through libsndfile and always writes
58
+ FLAC output.
59
+
60
+ ## Verify installation
61
+
62
+ ```bash
63
+ audioreconstructor --doctor
64
+ ```
65
+
66
+ Doctor verifies all cached release assets and runs an actual synthetic-audio ONNX
67
+ inference test. A healthy installation exits with status `0`.
68
+
69
+ ## Cache locations
70
+
71
+ Setup prints the exact locations it uses. By default they are:
72
+
73
+ - Linux: `$XDG_CACHE_HOME/audioreconstructor/<version>` or
74
+ `~/.cache/audioreconstructor/<version>`
75
+ - Windows: `%LOCALAPPDATA%\\audioreconstructor\\Cache\\<version>`
76
+
77
+ Running setup for an upgraded package version removes older cached versions after the
78
+ new version has been fully verified.
@@ -0,0 +1,54 @@
1
+ # audioreconstructor
2
+
3
+ `audioreconstructor` is the command-line release of Audioreconstruction's ONNX
4
+ audio enhancer. It installs a small Python launcher; the ONNX model and the native
5
+ runtime are downloaded only when you explicitly run setup.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install audioreconstructor
11
+ audioreconstructor --setup
12
+ ```
13
+
14
+ `--setup` downloads the native executable, `model.onnx`, and `config.json` from the
15
+ GitHub Release matching the installed package version. It verifies SHA-256 hashes
16
+ before making them available locally.
17
+
18
+ The current release supports 64-bit Linux and 64-bit Windows.
19
+
20
+ ## Use
21
+
22
+ ```bash
23
+ audioreconstructor --input song.mp3 --output song_enhanced.flac
24
+ ```
25
+
26
+ Choose the ONNX execution provider when needed:
27
+
28
+ ```bash
29
+ audioreconstructor --input song.mp3 --output song_enhanced.flac --provider cpu
30
+ ```
31
+
32
+ `auto` is the default. Windows first tries DirectML and falls back to CPU; Linux uses
33
+ CPU. The native executable reads supported audio through libsndfile and always writes
34
+ FLAC output.
35
+
36
+ ## Verify installation
37
+
38
+ ```bash
39
+ audioreconstructor --doctor
40
+ ```
41
+
42
+ Doctor verifies all cached release assets and runs an actual synthetic-audio ONNX
43
+ inference test. A healthy installation exits with status `0`.
44
+
45
+ ## Cache locations
46
+
47
+ Setup prints the exact locations it uses. By default they are:
48
+
49
+ - Linux: `$XDG_CACHE_HOME/audioreconstructor/<version>` or
50
+ `~/.cache/audioreconstructor/<version>`
51
+ - Windows: `%LOCALAPPDATA%\\audioreconstructor\\Cache\\<version>`
52
+
53
+ Running setup for an upgraded package version removes older cached versions after the
54
+ new version has been fully verified.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "audioreconstructor"
7
+ version = "1.0.0"
8
+ description = "Command-line audio reconstruction powered by the Audioreconstruction ONNX model"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Rohan Prasen Kedari", email = "rohanprasenkedari@gmail.com" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: Microsoft :: Windows",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Multimedia :: Sound/Audio",
27
+ ]
28
+ keywords = ["audio", "onnx", "super-resolution", "cli"]
29
+ dependencies = []
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/rohan-prasen/audioreconstruction"
33
+ Repository = "https://github.com/rohan-prasen/audioreconstruction"
34
+ Issues = "https://github.com/rohan-prasen/audioreconstruction/issues"
35
+
36
+ [project.scripts]
37
+ audioreconstructor = "audioreconstructor.cli:main"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/audioreconstructor"]
@@ -0,0 +1,3 @@
1
+ """PyPI launcher for the Audioreconstructor native ONNX executable."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,442 @@
1
+ """Install and invoke versioned Audioreconstructor GitHub Release assets."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import platform
9
+ import re
10
+ import shutil
11
+ import stat
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ from dataclasses import dataclass
16
+ from importlib.metadata import PackageNotFoundError, version as installed_version
17
+ from pathlib import Path
18
+ from typing import Callable, Mapping, Sequence
19
+ from urllib.error import HTTPError, URLError
20
+ from urllib.parse import quote
21
+ from urllib.request import Request, urlopen
22
+
23
+ from . import __version__
24
+
25
+ PACKAGE_NAME = "audioreconstructor"
26
+ REPOSITORY = "rohan-prasen/audioreconstruction"
27
+ MANIFEST_NAME = "manifest.json"
28
+ MODEL_NAME = "model.onnx"
29
+ CONFIG_NAME = "config.json"
30
+ DOWNLOAD_TIMEOUT_SECONDS = 30
31
+ SELF_TEST_TIMEOUT_SECONDS = 600
32
+ CHUNK_SIZE = 1024 * 1024
33
+ SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
34
+ VERSION_DIR_RE = re.compile(r"^\d+(?:\.\d+)+(?:[a-zA-Z0-9._-]+)?$")
35
+
36
+
37
+ class CliError(Exception):
38
+ """An expected CLI failure that should be rendered without a traceback."""
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Target:
43
+ system: str
44
+ architecture: str
45
+ asset_name: str
46
+ executable_name: str
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Artifact:
51
+ name: str
52
+ sha256: str
53
+ size: int
54
+
55
+
56
+ def get_package_version() -> str:
57
+ try:
58
+ return installed_version(PACKAGE_NAME)
59
+ except PackageNotFoundError:
60
+ return __version__
61
+
62
+
63
+ def release_tag(version: str) -> str:
64
+ return f"audioreconstructor-v{version}"
65
+
66
+
67
+ def release_url(version: str, asset_name: str) -> str:
68
+ tag = quote(release_tag(version), safe="")
69
+ asset = quote(asset_name, safe="")
70
+ return f"https://github.com/{REPOSITORY}/releases/download/{tag}/{asset}"
71
+
72
+
73
+ def detect_target(system: str | None = None, machine: str | None = None) -> Target:
74
+ system = system or platform.system()
75
+ machine = (machine or platform.machine()).lower()
76
+ if machine not in {"x86_64", "amd64"}:
77
+ raise CliError(f"unsupported architecture: {machine}. Only x86-64 is supported.")
78
+ if system == "Linux":
79
+ return Target(system, "x86_64", "audioreconstructor-linux-x86_64", "audioreconstructor")
80
+ if system == "Windows":
81
+ return Target(system, "x86_64", "audioreconstructor-windows-x86_64.exe", "audioreconstructor.exe")
82
+ raise CliError(f"unsupported operating system: {system}. Only Linux and Windows are supported.")
83
+
84
+
85
+ def get_cache_root(
86
+ system: str,
87
+ environ: Mapping[str, str] | None = None,
88
+ home: Path | None = None,
89
+ ) -> Path:
90
+ environ = os.environ if environ is None else environ
91
+ home = Path.home() if home is None else home
92
+ if system == "Linux":
93
+ return Path(environ["XDG_CACHE_HOME"]) / PACKAGE_NAME if environ.get("XDG_CACHE_HOME") else home / ".cache" / PACKAGE_NAME
94
+ if system == "Windows":
95
+ local_app_data = environ.get("LOCALAPPDATA")
96
+ base = Path(local_app_data) if local_app_data else home / "AppData" / "Local"
97
+ return base / PACKAGE_NAME / "Cache"
98
+ raise CliError(f"unsupported operating system: {system}")
99
+
100
+
101
+ def installation_paths(cache_root: Path, version: str, target: Target) -> dict[str, Path]:
102
+ directory = cache_root / version
103
+ return {
104
+ "directory": directory,
105
+ "manifest": directory / MANIFEST_NAME,
106
+ "binary": directory / target.executable_name,
107
+ "model": directory / MODEL_NAME,
108
+ "config": directory / CONFIG_NAME,
109
+ }
110
+
111
+
112
+ def _hash_file(path: Path) -> tuple[str, int]:
113
+ digest = hashlib.sha256()
114
+ size = 0
115
+ with path.open("rb") as handle:
116
+ while data := handle.read(CHUNK_SIZE):
117
+ digest.update(data)
118
+ size += len(data)
119
+ return digest.hexdigest(), size
120
+
121
+
122
+ def artifact_is_valid(path: Path, artifact: Artifact) -> tuple[bool, str]:
123
+ if not path.is_file():
124
+ return False, "missing"
125
+ try:
126
+ digest, size = _hash_file(path)
127
+ except OSError as exc:
128
+ return False, str(exc)
129
+ if size != artifact.size:
130
+ return False, f"size is {size} bytes; expected {artifact.size}"
131
+ if digest != artifact.sha256:
132
+ return False, "SHA-256 mismatch"
133
+ return True, "valid"
134
+
135
+
136
+ def _read_manifest(path: Path, package_version: str, required_assets: Sequence[str]) -> dict[str, Artifact]:
137
+ try:
138
+ raw = json.loads(path.read_text(encoding="utf-8"))
139
+ except (OSError, json.JSONDecodeError) as exc:
140
+ raise CliError(f"could not read {MANIFEST_NAME}: {exc}") from exc
141
+
142
+ if raw.get("schemaVersion") != 1:
143
+ raise CliError(f"{MANIFEST_NAME} has an unsupported schema version")
144
+ if raw.get("version") != package_version:
145
+ raise CliError(f"{MANIFEST_NAME} is for version {raw.get('version')!r}, expected {package_version!r}")
146
+ if raw.get("releaseTag") != release_tag(package_version):
147
+ raise CliError(f"{MANIFEST_NAME} does not match release {release_tag(package_version)}")
148
+ files = raw.get("files")
149
+ if not isinstance(files, dict):
150
+ raise CliError(f"{MANIFEST_NAME} has no files mapping")
151
+
152
+ artifacts: dict[str, Artifact] = {}
153
+ for name in required_assets:
154
+ item = files.get(name)
155
+ if not isinstance(item, dict):
156
+ raise CliError(f"{MANIFEST_NAME} is missing metadata for {name}")
157
+ digest = item.get("sha256")
158
+ size = item.get("bytes")
159
+ if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
160
+ raise CliError(f"{MANIFEST_NAME} has an invalid SHA-256 for {name}")
161
+ if not isinstance(size, int) or size <= 0:
162
+ raise CliError(f"{MANIFEST_NAME} has an invalid byte count for {name}")
163
+ artifacts[name] = Artifact(name=name, sha256=digest, size=size)
164
+ return artifacts
165
+
166
+
167
+ def _temporary_path(directory: Path, filename: str) -> Path:
168
+ handle = tempfile.NamedTemporaryFile(prefix=f".{filename}.", suffix=".part", dir=directory, delete=False)
169
+ handle.close()
170
+ return Path(handle.name)
171
+
172
+
173
+ def download(url: str, destination: Path, expected: Artifact | None = None) -> None:
174
+ """Download one release asset and validate it before returning."""
175
+ request = Request(url, headers={"User-Agent": f"{PACKAGE_NAME}/{get_package_version()}"})
176
+ try:
177
+ with urlopen(request, timeout=DOWNLOAD_TIMEOUT_SECONDS) as response, destination.open("wb") as handle:
178
+ digest = hashlib.sha256()
179
+ size = 0
180
+ while data := response.read(CHUNK_SIZE):
181
+ handle.write(data)
182
+ digest.update(data)
183
+ size += len(data)
184
+ except HTTPError as exc:
185
+ raise CliError(f"download failed ({exc.code}) for {url}") from exc
186
+ except URLError as exc:
187
+ raise CliError(f"download failed for {url}: {exc.reason}") from exc
188
+ except OSError as exc:
189
+ raise CliError(f"could not save {destination.name}: {exc}") from exc
190
+
191
+ if expected is not None:
192
+ if size != expected.size:
193
+ raise CliError(f"downloaded {expected.name} has {size} bytes; expected {expected.size}")
194
+ if digest.hexdigest() != expected.sha256:
195
+ raise CliError(f"downloaded {expected.name} failed SHA-256 verification")
196
+
197
+
198
+ def _make_executable(path: Path, target: Target) -> None:
199
+ if target.system != "Linux":
200
+ return
201
+ try:
202
+ path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
203
+ except OSError as exc:
204
+ raise CliError(f"could not make {path.name} executable: {exc}") from exc
205
+
206
+
207
+ def _load_or_download_manifest(paths: Mapping[str, Path], version: str, required_assets: Sequence[str]) -> dict[str, Artifact]:
208
+ manifest = paths["manifest"]
209
+ try:
210
+ return _read_manifest(manifest, version, required_assets)
211
+ except CliError:
212
+ pass
213
+
214
+ temporary = _temporary_path(paths["directory"], MANIFEST_NAME)
215
+ try:
216
+ download(release_url(version, MANIFEST_NAME), temporary)
217
+ artifacts = _read_manifest(temporary, version, required_assets)
218
+ os.replace(temporary, manifest)
219
+ return artifacts
220
+ finally:
221
+ temporary.unlink(missing_ok=True)
222
+
223
+
224
+ def _prune_old_versions(cache_root: Path, current_version: str, report: Callable[[str], None]) -> None:
225
+ try:
226
+ children = list(cache_root.iterdir())
227
+ except OSError:
228
+ return
229
+ for child in children:
230
+ if child.name == current_version or not VERSION_DIR_RE.fullmatch(child.name):
231
+ continue
232
+ try:
233
+ if child.is_symlink():
234
+ child.unlink()
235
+ elif child.is_dir():
236
+ shutil.rmtree(child)
237
+ except OSError as exc:
238
+ report(f"Warning: could not remove old cache {child}: {exc}")
239
+
240
+
241
+ def setup_assets(
242
+ *,
243
+ package_version: str | None = None,
244
+ target: Target | None = None,
245
+ cache_root: Path | None = None,
246
+ report: Callable[[str], None] = print,
247
+ ) -> dict[str, Path]:
248
+ """Install the current package version's native assets into the user cache."""
249
+ package_version = package_version or get_package_version()
250
+ target = target or detect_target()
251
+ cache_root = cache_root or get_cache_root(target.system)
252
+ paths = installation_paths(cache_root, package_version, target)
253
+ paths["directory"].mkdir(parents=True, exist_ok=True)
254
+
255
+ required_assets = (target.asset_name, MODEL_NAME, CONFIG_NAME)
256
+ artifacts = _load_or_download_manifest(paths, package_version, required_assets)
257
+ path_by_asset = {
258
+ target.asset_name: paths["binary"],
259
+ MODEL_NAME: paths["model"],
260
+ CONFIG_NAME: paths["config"],
261
+ }
262
+
263
+ for asset_name in required_assets:
264
+ destination = path_by_asset[asset_name]
265
+ valid, _ = artifact_is_valid(destination, artifacts[asset_name])
266
+ if valid:
267
+ report(f"Using cached {asset_name}")
268
+ else:
269
+ report(f"Downloading {asset_name}")
270
+ temporary = _temporary_path(paths["directory"], asset_name)
271
+ try:
272
+ download(release_url(package_version, asset_name), temporary, artifacts[asset_name])
273
+ if asset_name == target.asset_name:
274
+ _make_executable(temporary, target)
275
+ os.replace(temporary, destination)
276
+ finally:
277
+ temporary.unlink(missing_ok=True)
278
+ if asset_name == target.asset_name:
279
+ _make_executable(destination, target)
280
+
281
+ _prune_old_versions(cache_root, package_version, report)
282
+ report("Setup complete.")
283
+ report(f"Cache: {paths['directory']}")
284
+ report(f"Binary: {paths['binary']}")
285
+ report(f"Model: {paths['model']}")
286
+ report(f"Config: {paths['config']}")
287
+ return paths
288
+
289
+
290
+ def _self_test(paths: Mapping[str, Path]) -> tuple[bool, str]:
291
+ command = [
292
+ str(paths["binary"]),
293
+ "--model",
294
+ str(paths["model"]),
295
+ "--config",
296
+ str(paths["config"]),
297
+ "--provider",
298
+ "auto",
299
+ "--self-test",
300
+ ]
301
+ try:
302
+ result = subprocess.run(command, capture_output=True, text=True, timeout=SELF_TEST_TIMEOUT_SECONDS, check=False)
303
+ except OSError as exc:
304
+ return False, str(exc)
305
+ except subprocess.TimeoutExpired:
306
+ return False, f"timed out after {SELF_TEST_TIMEOUT_SECONDS} seconds"
307
+ if result.returncode == 0 and "SELF-TEST PASS" in result.stdout:
308
+ return True, "passed"
309
+ detail = (result.stderr or result.stdout).strip().splitlines()
310
+ return False, detail[-1] if detail else f"exited with code {result.returncode}"
311
+
312
+
313
+ def doctor(
314
+ *,
315
+ package_version: str | None = None,
316
+ target: Target | None = None,
317
+ cache_root: Path | None = None,
318
+ report: Callable[[str], None] = print,
319
+ ) -> int:
320
+ package_version = package_version or get_package_version()
321
+ try:
322
+ target = target or detect_target()
323
+ except CliError as exc:
324
+ report(f"[FAIL] Platform: {exc}")
325
+ report("Status: UNHEALTHY")
326
+ return 1
327
+ cache_root = cache_root or get_cache_root(target.system)
328
+ paths = installation_paths(cache_root, package_version, target)
329
+ required_assets = (target.asset_name, MODEL_NAME, CONFIG_NAME)
330
+ report(f"Version: {package_version}")
331
+ report(f"Platform: {target.system} {target.architecture}")
332
+ report(f"Cache: {paths['directory']}")
333
+
334
+ try:
335
+ artifacts = _read_manifest(paths["manifest"], package_version, required_assets)
336
+ except CliError as exc:
337
+ report(f"[FAIL] Manifest: {exc}")
338
+ report("Status: UNHEALTHY")
339
+ return 1
340
+ report("[PASS] Manifest")
341
+
342
+ path_by_asset = {
343
+ target.asset_name: paths["binary"],
344
+ MODEL_NAME: paths["model"],
345
+ CONFIG_NAME: paths["config"],
346
+ }
347
+ healthy = True
348
+ for asset_name in required_assets:
349
+ valid, detail = artifact_is_valid(path_by_asset[asset_name], artifacts[asset_name])
350
+ label = "PASS" if valid else "FAIL"
351
+ report(f"[{label}] {asset_name}: {detail}")
352
+ healthy = healthy and valid
353
+
354
+ if target.system == "Linux":
355
+ executable = paths["binary"].is_file() and os.access(paths["binary"], os.X_OK)
356
+ report(f"[{'PASS' if executable else 'FAIL'}] Binary execute permission")
357
+ healthy = healthy and executable
358
+
359
+ if healthy:
360
+ report("[PASS] Bundled runtime dependencies")
361
+ passed, detail = _self_test(paths)
362
+ report(f"[{'PASS' if passed else 'FAIL'}] Runtime self-test: {detail}")
363
+ healthy = healthy and passed
364
+ else:
365
+ report("[SKIP] Runtime self-test: setup is incomplete")
366
+
367
+ report(f"Status: {'HEALTHY' if healthy else 'UNHEALTHY'}")
368
+ return 0 if healthy else 1
369
+
370
+
371
+ def run_inference(
372
+ input_path: Path,
373
+ output_path: Path,
374
+ provider: str,
375
+ *,
376
+ package_version: str | None = None,
377
+ target: Target | None = None,
378
+ cache_root: Path | None = None,
379
+ ) -> int:
380
+ package_version = package_version or get_package_version()
381
+ target = target or detect_target()
382
+ cache_root = cache_root or get_cache_root(target.system)
383
+ paths = installation_paths(cache_root, package_version, target)
384
+ missing = [key for key in ("manifest", "binary", "model", "config") if not paths[key].is_file()]
385
+ if missing:
386
+ names = ", ".join(paths[key].name for key in missing)
387
+ raise CliError(f"setup is incomplete ({names}). Run '{PACKAGE_NAME} --setup'.")
388
+ if target.system == "Linux" and not os.access(paths["binary"], os.X_OK):
389
+ raise CliError(f"{paths['binary']} is not executable. Run '{PACKAGE_NAME} --setup'.")
390
+ command = [
391
+ str(paths["binary"]),
392
+ "--model",
393
+ str(paths["model"]),
394
+ "--config",
395
+ str(paths["config"]),
396
+ "--input",
397
+ str(input_path),
398
+ "--output",
399
+ str(output_path),
400
+ "--provider",
401
+ provider,
402
+ ]
403
+ try:
404
+ return subprocess.run(command, check=False).returncode
405
+ except OSError as exc:
406
+ raise CliError(f"could not start {paths['binary']}: {exc}") from exc
407
+
408
+
409
+ def build_parser() -> argparse.ArgumentParser:
410
+ parser = argparse.ArgumentParser(description="Enhance audio with the Audioreconstructor ONNX model.")
411
+ actions = parser.add_mutually_exclusive_group()
412
+ actions.add_argument("--setup", action="store_true", help="download and verify the native runtime and model")
413
+ actions.add_argument("--doctor", action="store_true", help="verify cached assets and run a runtime self-test")
414
+ parser.add_argument("--version", action="version", version=get_package_version())
415
+ parser.add_argument("--input", type=Path, help="input audio file")
416
+ parser.add_argument("--output", type=Path, help="output FLAC file")
417
+ parser.add_argument("--provider", choices=("auto", "cpu", "directml"), default="auto", help="ONNX provider (default: auto)")
418
+ return parser
419
+
420
+
421
+ def main(argv: Sequence[str] | None = None) -> int:
422
+ parser = build_parser()
423
+ args = parser.parse_args(argv)
424
+ try:
425
+ if args.setup:
426
+ setup_assets()
427
+ return 0
428
+ if args.doctor:
429
+ return doctor()
430
+ if args.input is None and args.output is None:
431
+ parser.print_help()
432
+ return 0
433
+ if args.input is None or args.output is None:
434
+ parser.error("--input and --output must be used together")
435
+ return run_inference(args.input, args.output, args.provider)
436
+ except CliError as exc:
437
+ print(f"Error: {exc}", file=sys.stderr)
438
+ return 1
439
+
440
+
441
+ if __name__ == "__main__":
442
+ raise SystemExit(main())
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import unittest
11
+ from pathlib import Path
12
+ from types import SimpleNamespace
13
+ from unittest import mock
14
+
15
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
16
+
17
+ from audioreconstructor import cli
18
+
19
+
20
+ TARGET = cli.Target("Linux", "x86_64", "audioreconstructor-linux-x86_64", "audioreconstructor")
21
+
22
+
23
+ def description(data: bytes) -> dict[str, int | str]:
24
+ return {"sha256": hashlib.sha256(data).hexdigest(), "bytes": len(data)}
25
+
26
+
27
+ class ReleaseFixture:
28
+ version = "1.0.0"
29
+
30
+ def __init__(self, root: Path) -> None:
31
+ self.root = root
32
+ self.assets = {
33
+ TARGET.asset_name: b"linux executable\n",
34
+ "audioreconstructor-windows-x86_64.exe": b"windows executable\r\n",
35
+ cli.MODEL_NAME: b"model data",
36
+ cli.CONFIG_NAME: b'{"sample_rate": 44100}',
37
+ }
38
+ self.sources: dict[str, Path] = {}
39
+ for name, data in self.assets.items():
40
+ source = root / name
41
+ source.write_bytes(data)
42
+ self.sources[f"test://{name}"] = source
43
+ manifest = {
44
+ "schemaVersion": 1,
45
+ "version": self.version,
46
+ "releaseTag": cli.release_tag(self.version),
47
+ "files": {name: description(data) for name, data in self.assets.items()},
48
+ }
49
+ manifest_path = root / cli.MANIFEST_NAME
50
+ manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
51
+ self.sources[f"test://{cli.MANIFEST_NAME}"] = manifest_path
52
+ self.downloaded: list[str] = []
53
+
54
+ def download(self, url: str, destination: Path, expected: cli.Artifact | None = None) -> None:
55
+ self.downloaded.append(url)
56
+ shutil.copyfile(self.sources[url], destination)
57
+ if expected is not None:
58
+ valid, detail = cli.artifact_is_valid(destination, expected)
59
+ if not valid:
60
+ raise AssertionError(detail)
61
+
62
+
63
+ class CliTests(unittest.TestCase):
64
+ def setUp(self) -> None:
65
+ self.temp_dir = tempfile.TemporaryDirectory()
66
+ self.root = Path(self.temp_dir.name)
67
+ self.cache = self.root / "cache"
68
+ (self.root / "release").mkdir(exist_ok=True)
69
+ self.release = ReleaseFixture(self.root / "release")
70
+
71
+ def tearDown(self) -> None:
72
+ self.temp_dir.cleanup()
73
+
74
+ def install(self) -> tuple[dict[str, Path], list[str]]:
75
+ messages: list[str] = []
76
+ with mock.patch.object(cli, "release_url", side_effect=lambda _version, name: f"test://{name}"), mock.patch.object(
77
+ cli, "download", side_effect=self.release.download
78
+ ):
79
+ paths = cli.setup_assets(
80
+ package_version=self.release.version,
81
+ target=TARGET,
82
+ cache_root=self.cache,
83
+ report=messages.append,
84
+ )
85
+ return paths, messages
86
+
87
+ def test_setup_downloads_validates_and_reuses_assets(self) -> None:
88
+ old_cache = self.cache / "0.9.0"
89
+ old_cache.mkdir(parents=True)
90
+ (old_cache / "unused").write_text("old", encoding="utf-8")
91
+
92
+ paths, messages = self.install()
93
+
94
+ self.assertTrue(paths["binary"].is_file())
95
+ self.assertTrue(os.access(paths["binary"], os.X_OK))
96
+ self.assertEqual(paths["model"].read_bytes(), self.release.assets[cli.MODEL_NAME])
97
+ self.assertFalse(old_cache.exists())
98
+ self.assertIn("Setup complete.", messages)
99
+ self.assertEqual(len(self.release.downloaded), 4)
100
+
101
+ self.release.downloaded.clear()
102
+ _, messages = self.install()
103
+ self.assertEqual(self.release.downloaded, [])
104
+ self.assertIn(f"Using cached {TARGET.asset_name}", messages)
105
+
106
+ def test_setup_replaces_a_corrupt_asset(self) -> None:
107
+ paths, _ = self.install()
108
+ paths["model"].write_bytes(b"corrupt")
109
+ self.release.downloaded.clear()
110
+
111
+ self.install()
112
+
113
+ self.assertEqual(paths["model"].read_bytes(), self.release.assets[cli.MODEL_NAME])
114
+ self.assertEqual(self.release.downloaded, [f"test://{cli.MODEL_NAME}"])
115
+
116
+ def test_doctor_runs_self_test_after_integrity_checks(self) -> None:
117
+ self.install()
118
+ messages: list[str] = []
119
+ with mock.patch.object(cli, "_self_test", return_value=(True, "passed")) as self_test:
120
+ result = cli.doctor(
121
+ package_version=self.release.version,
122
+ target=TARGET,
123
+ cache_root=self.cache,
124
+ report=messages.append,
125
+ )
126
+
127
+ self.assertEqual(result, 0)
128
+ self_test.assert_called_once()
129
+ self.assertIn("[PASS] Runtime self-test: passed", messages)
130
+ self.assertEqual(messages[-1], "Status: HEALTHY")
131
+
132
+ def test_doctor_fails_without_a_required_asset(self) -> None:
133
+ paths, _ = self.install()
134
+ paths["config"].unlink()
135
+ messages: list[str] = []
136
+
137
+ result = cli.doctor(
138
+ package_version=self.release.version,
139
+ target=TARGET,
140
+ cache_root=self.cache,
141
+ report=messages.append,
142
+ )
143
+
144
+ self.assertEqual(result, 1)
145
+ self.assertIn("[FAIL] config.json: missing", messages)
146
+ self.assertEqual(messages[-1], "Status: UNHEALTHY")
147
+
148
+ def test_inference_injects_cached_model_paths_and_preserves_exit_code(self) -> None:
149
+ paths, _ = self.install()
150
+ with mock.patch.object(cli.subprocess, "run", return_value=SimpleNamespace(returncode=3)) as run:
151
+ result = cli.run_inference(
152
+ Path("source song.mp3"),
153
+ Path("result.flac"),
154
+ "cpu",
155
+ package_version=self.release.version,
156
+ target=TARGET,
157
+ cache_root=self.cache,
158
+ )
159
+
160
+ self.assertEqual(result, 3)
161
+ command = run.call_args.args[0]
162
+ self.assertEqual(command[0], str(paths["binary"]))
163
+ self.assertEqual(command[command.index("--model") + 1], str(paths["model"]))
164
+ self.assertEqual(command[command.index("--config") + 1], str(paths["config"]))
165
+ self.assertEqual(command[command.index("--input") + 1], "source song.mp3")
166
+
167
+ def test_platform_and_cache_resolution(self) -> None:
168
+ linux = cli.get_cache_root("Linux", {"XDG_CACHE_HOME": "/tmp/xdg"}, Path("/home/test"))
169
+ windows = cli.get_cache_root("Windows", {"LOCALAPPDATA": "C:/Users/test/AppData/Local"}, Path("/home/test"))
170
+ self.assertEqual(linux, Path("/tmp/xdg") / cli.PACKAGE_NAME)
171
+ self.assertEqual(windows, Path("C:/Users/test/AppData/Local") / cli.PACKAGE_NAME / "Cache")
172
+ with self.assertRaises(cli.CliError):
173
+ cli.detect_target("Darwin", "arm64")
174
+
175
+ def test_release_manifest_generator_records_each_asset(self) -> None:
176
+ assets_dir = self.root / "release-assets"
177
+ assets_dir.mkdir()
178
+ for name, data in self.release.assets.items():
179
+ (assets_dir / name).write_bytes(data)
180
+ generator = Path(__file__).resolve().parents[1] / "tools" / "generate_manifest.py"
181
+
182
+ result = subprocess.run(
183
+ [sys.executable, str(generator), "--version", self.release.version, "--assets-dir", str(assets_dir)],
184
+ capture_output=True,
185
+ text=True,
186
+ check=False,
187
+ )
188
+
189
+ self.assertEqual(result.returncode, 0, result.stderr)
190
+ manifest = json.loads((assets_dir / cli.MANIFEST_NAME).read_text(encoding="utf-8"))
191
+ self.assertEqual(manifest["releaseTag"], "audioreconstructor-v1.0.0")
192
+ self.assertEqual(manifest["files"][cli.MODEL_NAME], description(self.release.assets[cli.MODEL_NAME]))
193
+
194
+
195
+ if __name__ == "__main__":
196
+ unittest.main()
@@ -0,0 +1,50 @@
1
+ """Create the manifest.json required by an Audioreconstructor GitHub Release."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+
9
+ ASSET_NAMES = (
10
+ "audioreconstructor-linux-x86_64",
11
+ "audioreconstructor-windows-x86_64.exe",
12
+ "model.onnx",
13
+ "config.json",
14
+ )
15
+ CHUNK_SIZE = 1024 * 1024
16
+
17
+
18
+ def describe(path: Path) -> dict[str, int | str]:
19
+ digest = hashlib.sha256()
20
+ size = 0
21
+ with path.open("rb") as handle:
22
+ while chunk := handle.read(CHUNK_SIZE):
23
+ digest.update(chunk)
24
+ size += len(chunk)
25
+ return {"sha256": digest.hexdigest(), "bytes": size}
26
+
27
+
28
+ def main() -> None:
29
+ parser = argparse.ArgumentParser(description=__doc__)
30
+ parser.add_argument("--version", required=True, help="PyPI version, for example 1.0.0")
31
+ parser.add_argument("--assets-dir", type=Path, required=True, help="directory containing the four release assets")
32
+ parser.add_argument("--output", type=Path, help="manifest path (default: <assets-dir>/manifest.json)")
33
+ args = parser.parse_args()
34
+
35
+ missing = [name for name in ASSET_NAMES if not (args.assets_dir / name).is_file()]
36
+ if missing:
37
+ parser.error(f"missing release assets: {', '.join(missing)}")
38
+ output = args.output or args.assets_dir / "manifest.json"
39
+ manifest = {
40
+ "schemaVersion": 1,
41
+ "version": args.version,
42
+ "releaseTag": f"audioreconstructor-v{args.version}",
43
+ "files": {name: describe(args.assets_dir / name) for name in ASSET_NAMES},
44
+ }
45
+ output.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
46
+ print(output)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
@@ -0,0 +1,8 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "audioreconstructor"
7
+ version = "1.0.0"
8
+ source = { editable = "." }