audioreconstructor 1.0.0__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.
@@ -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,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,7 @@
1
+ audioreconstructor/__init__.py,sha256=ZAzxkQLFTR5oTN7XxnRKFRjMpVoHIzt2NI67J91LovQ,94
2
+ audioreconstructor/__main__.py,sha256=PSQ4rpL0dG6f-qH4N7H-gD9igQkdHzH4yVZDcW8lfZo,80
3
+ audioreconstructor/cli.py,sha256=Sx3P_dXpnibCyQOjCni7d72Bpuy33WyGVKwdhhOGdbQ,16344
4
+ audioreconstructor-1.0.0.dist-info/METADATA,sha256=s6TTUZZ1P5br-WIgYQmZFvLm88bNca5hiJmodGTpYLo,2698
5
+ audioreconstructor-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
6
+ audioreconstructor-1.0.0.dist-info/entry_points.txt,sha256=s2EKhHQfXWq0nQHHr9Jom5oKBbqlKdl8b-67_QLRdhg,67
7
+ audioreconstructor-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ audioreconstructor = audioreconstructor.cli:main