echovault-flac-enhancer 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.
- echovault_flac_enhancer-0.1.0/.github/workflows/ci.yml +25 -0
- echovault_flac_enhancer-0.1.0/.github/workflows/publish.yml +21 -0
- echovault_flac_enhancer-0.1.0/.gitignore +8 -0
- echovault_flac_enhancer-0.1.0/CHANGELOG.md +13 -0
- echovault_flac_enhancer-0.1.0/LICENSE +21 -0
- echovault_flac_enhancer-0.1.0/PKG-INFO +73 -0
- echovault_flac_enhancer-0.1.0/README.md +51 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/__init__.py +3 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/__main__.py +4 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/batch.py +181 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/cli.py +181 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/data/manifest.json +14 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/data/requirements-onnx.txt +5 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/inference.py +271 -0
- echovault_flac_enhancer-0.1.0/echovault_flac_enhancer/model_manager.py +227 -0
- echovault_flac_enhancer-0.1.0/pyproject.toml +39 -0
- echovault_flac_enhancer-0.1.0/tests/conftest.py +31 -0
- echovault_flac_enhancer-0.1.0/tests/fixtures/fake_inference.py +44 -0
- echovault_flac_enhancer-0.1.0/tests/test_batch.py +96 -0
- echovault_flac_enhancer-0.1.0/tests/test_cli.py +46 -0
- echovault_flac_enhancer-0.1.0/tests/test_integration_slow.py +16 -0
- echovault_flac_enhancer-0.1.0/tests/test_model_manager.py +116 -0
- echovault_flac_enhancer-0.1.0/tests/test_run_single_file.py +68 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
os: [ubuntu-latest]
|
|
12
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
13
|
+
include:
|
|
14
|
+
- os: windows-latest
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- run: pip install -e .[dev]
|
|
23
|
+
- run: ruff check .
|
|
24
|
+
- run: black --check .
|
|
25
|
+
- run: pytest -m "not slow"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch: {}
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- run: pip install build
|
|
20
|
+
- run: python -m build
|
|
21
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] - Unreleased
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Initial release: `--setup`, `--check`, `--file-name`, `--folder`
|
|
11
|
+
(`--recursive`, `--skip-existing`, `--output-dir`) CLI commands.
|
|
12
|
+
- Model download/verification against a GitHub Release asset, ported from
|
|
13
|
+
EchoVault's own `downloader.js`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EchoVault contributors
|
|
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.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: echovault-flac-enhancer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone CLI for batch-enhancing lossy audio to FLAC via EchoVault's ONNX model
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: mutagen>=1.47
|
|
9
|
+
Requires-Dist: numpy>=1.26
|
|
10
|
+
Requires-Dist: onnxruntime==1.20.1
|
|
11
|
+
Requires-Dist: platformdirs>=4.0
|
|
12
|
+
Requires-Dist: scipy>=1.11
|
|
13
|
+
Requires-Dist: soundfile>=0.13.1
|
|
14
|
+
Requires-Dist: tqdm>=4.66
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: black; extra == 'dev'
|
|
17
|
+
Requires-Dist: build; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# echovault-flac-enhancer
|
|
24
|
+
|
|
25
|
+
Standalone CLI for bulk-enhancing lossy audio (MP3/AAC/OGG/WMA) to FLAC using
|
|
26
|
+
the same ONNX model EchoVault's desktop app uses, independent of Electron.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install echovault-flac-enhancer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# One-time: download the model and verify everything works
|
|
38
|
+
echovault-flac-enhancer --setup
|
|
39
|
+
|
|
40
|
+
# Check runtime/deps/model status
|
|
41
|
+
echovault-flac-enhancer --check
|
|
42
|
+
|
|
43
|
+
# Enhance a single file
|
|
44
|
+
echovault-flac-enhancer --file-name path/to/track.mp3
|
|
45
|
+
|
|
46
|
+
# Enhance every lossy track in a folder
|
|
47
|
+
echovault-flac-enhancer --folder path/to/music [--recursive] [--skip-existing] [--output-dir DIR]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Outputs are written as `<original-stem>.enhanced.flac` alongside the source
|
|
51
|
+
(or under `--output-dir`, mirroring the source's relative subpath).
|
|
52
|
+
|
|
53
|
+
The model (~107MB) is cached at:
|
|
54
|
+
|
|
55
|
+
- Linux/macOS: `~/.cache/echovault-flac-enhancer/model/`
|
|
56
|
+
- Windows: `%LOCALAPPDATA%\echovault-flac-enhancer\model\`
|
|
57
|
+
|
|
58
|
+
## Non-goals (v1)
|
|
59
|
+
|
|
60
|
+
- No GUI — CLI only.
|
|
61
|
+
- No parallel/multi-worker inference (`--workers` must be `1`).
|
|
62
|
+
- No mid-batch resume — re-run with `--skip-existing` to pick up where a
|
|
63
|
+
killed batch left off.
|
|
64
|
+
- `.m4a` files are not auto-globbed by `--folder` (ambiguous AAC vs. ALAC
|
|
65
|
+
codec-in-container — pass an `.m4a` explicitly via `--file-name` if you're
|
|
66
|
+
sure it's lossy AAC).
|
|
67
|
+
|
|
68
|
+
## Releasing
|
|
69
|
+
|
|
70
|
+
Before `--setup` works for end users, a maintainer must create a GitHub
|
|
71
|
+
Release on this repo and upload `model.onnx` + `config.json` as release
|
|
72
|
+
assets, matching the URL/hashes pinned in
|
|
73
|
+
`echovault_flac_enhancer/data/manifest.json`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# echovault-flac-enhancer
|
|
2
|
+
|
|
3
|
+
Standalone CLI for bulk-enhancing lossy audio (MP3/AAC/OGG/WMA) to FLAC using
|
|
4
|
+
the same ONNX model EchoVault's desktop app uses, independent of Electron.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install echovault-flac-enhancer
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# One-time: download the model and verify everything works
|
|
16
|
+
echovault-flac-enhancer --setup
|
|
17
|
+
|
|
18
|
+
# Check runtime/deps/model status
|
|
19
|
+
echovault-flac-enhancer --check
|
|
20
|
+
|
|
21
|
+
# Enhance a single file
|
|
22
|
+
echovault-flac-enhancer --file-name path/to/track.mp3
|
|
23
|
+
|
|
24
|
+
# Enhance every lossy track in a folder
|
|
25
|
+
echovault-flac-enhancer --folder path/to/music [--recursive] [--skip-existing] [--output-dir DIR]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Outputs are written as `<original-stem>.enhanced.flac` alongside the source
|
|
29
|
+
(or under `--output-dir`, mirroring the source's relative subpath).
|
|
30
|
+
|
|
31
|
+
The model (~107MB) is cached at:
|
|
32
|
+
|
|
33
|
+
- Linux/macOS: `~/.cache/echovault-flac-enhancer/model/`
|
|
34
|
+
- Windows: `%LOCALAPPDATA%\echovault-flac-enhancer\model\`
|
|
35
|
+
|
|
36
|
+
## Non-goals (v1)
|
|
37
|
+
|
|
38
|
+
- No GUI — CLI only.
|
|
39
|
+
- No parallel/multi-worker inference (`--workers` must be `1`).
|
|
40
|
+
- No mid-batch resume — re-run with `--skip-existing` to pick up where a
|
|
41
|
+
killed batch left off.
|
|
42
|
+
- `.m4a` files are not auto-globbed by `--folder` (ambiguous AAC vs. ALAC
|
|
43
|
+
codec-in-container — pass an `.m4a` explicitly via `--file-name` if you're
|
|
44
|
+
sure it's lossy AAC).
|
|
45
|
+
|
|
46
|
+
## Releasing
|
|
47
|
+
|
|
48
|
+
Before `--setup` works for end users, a maintainer must create a GitHub
|
|
49
|
+
Release on this repo and upload `model.onnx` + `config.json` as release
|
|
50
|
+
assets, matching the URL/hashes pinned in
|
|
51
|
+
`echovault_flac_enhancer/data/manifest.json`.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Single-file and batch enhancement: spawns inference.py, parses its stdout/stderr
|
|
2
|
+
contract, and drives sequential batch processing over a folder of tracks."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, NamedTuple
|
|
12
|
+
|
|
13
|
+
LOSSY_EXTENSIONS = {
|
|
14
|
+
".mp3",
|
|
15
|
+
".aac",
|
|
16
|
+
".ogg",
|
|
17
|
+
".wma",
|
|
18
|
+
} # .m4a excluded: ambiguous AAC-vs-ALAC codec-in-container
|
|
19
|
+
|
|
20
|
+
PROGRESS_RE = re.compile(r"^PROGRESS\s+(\d+)$")
|
|
21
|
+
DONE_RE = re.compile(r"^DONE\s+(.+)$")
|
|
22
|
+
ERROR_RE = re.compile(r"ERROR\s+(\w+)\s*(.*)")
|
|
23
|
+
|
|
24
|
+
EXIT_CODE_FALLBACK = {
|
|
25
|
+
1: "GENERIC",
|
|
26
|
+
2: "INPUT_READ_FAILED",
|
|
27
|
+
3: "MODEL_NOT_FOUND",
|
|
28
|
+
4: "ORT_INIT_FAILED",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InferenceResult(NamedTuple):
|
|
33
|
+
success: bool
|
|
34
|
+
output_path: Path | None
|
|
35
|
+
error_code: str | None
|
|
36
|
+
error_message: str | None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_single_file(
|
|
40
|
+
inference_script: Path,
|
|
41
|
+
model_path: Path,
|
|
42
|
+
config_path: Path,
|
|
43
|
+
input_path: Path,
|
|
44
|
+
output_path: Path,
|
|
45
|
+
on_progress: Callable[[int], None] = lambda pct: None,
|
|
46
|
+
) -> InferenceResult:
|
|
47
|
+
proc = subprocess.Popen(
|
|
48
|
+
[
|
|
49
|
+
sys.executable,
|
|
50
|
+
str(inference_script),
|
|
51
|
+
"--model",
|
|
52
|
+
str(model_path),
|
|
53
|
+
"--config",
|
|
54
|
+
str(config_path),
|
|
55
|
+
"--input",
|
|
56
|
+
str(input_path),
|
|
57
|
+
"--output",
|
|
58
|
+
str(output_path),
|
|
59
|
+
"--provider",
|
|
60
|
+
"auto",
|
|
61
|
+
],
|
|
62
|
+
stdout=subprocess.PIPE,
|
|
63
|
+
stderr=subprocess.PIPE,
|
|
64
|
+
text=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
stderr_lines: list[str] = []
|
|
68
|
+
|
|
69
|
+
def drain_stderr() -> None:
|
|
70
|
+
for line in proc.stderr:
|
|
71
|
+
stderr_lines.append(line.rstrip("\n"))
|
|
72
|
+
|
|
73
|
+
stderr_thread = threading.Thread(target=drain_stderr, daemon=True)
|
|
74
|
+
stderr_thread.start()
|
|
75
|
+
|
|
76
|
+
done_path: Path | None = None
|
|
77
|
+
for line in proc.stdout:
|
|
78
|
+
line = line.strip()
|
|
79
|
+
if m := PROGRESS_RE.match(line):
|
|
80
|
+
on_progress(int(m.group(1)))
|
|
81
|
+
elif m := DONE_RE.match(line):
|
|
82
|
+
done_path = Path(m.group(1))
|
|
83
|
+
|
|
84
|
+
returncode = proc.wait()
|
|
85
|
+
stderr_thread.join()
|
|
86
|
+
|
|
87
|
+
if returncode == 0:
|
|
88
|
+
if done_path is not None and done_path != output_path:
|
|
89
|
+
print(
|
|
90
|
+
f"WARN inference reported DONE {done_path}, expected {output_path}",
|
|
91
|
+
file=sys.stderr,
|
|
92
|
+
)
|
|
93
|
+
return InferenceResult(
|
|
94
|
+
success=True, output_path=output_path, error_code=None, error_message=None
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
error_code = None
|
|
98
|
+
error_message = None
|
|
99
|
+
for line in stderr_lines:
|
|
100
|
+
if m := ERROR_RE.search(line):
|
|
101
|
+
error_code = m.group(1)
|
|
102
|
+
error_message = m.group(2).strip() or None
|
|
103
|
+
break
|
|
104
|
+
if error_code is None:
|
|
105
|
+
error_code = EXIT_CODE_FALLBACK.get(returncode, "GENERIC")
|
|
106
|
+
|
|
107
|
+
return InferenceResult(
|
|
108
|
+
success=False,
|
|
109
|
+
output_path=None,
|
|
110
|
+
error_code=error_code,
|
|
111
|
+
error_message=error_message,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resolve_files(folder: Path, recursive: bool) -> list[Path]:
|
|
116
|
+
candidates = folder.rglob("*") if recursive else folder.glob("*")
|
|
117
|
+
return sorted(
|
|
118
|
+
p for p in candidates if p.is_file() and p.suffix.lower() in LOSSY_EXTENSIONS
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def output_path_for(
|
|
123
|
+
input_path: Path, source_root: Path, output_dir: Path | None
|
|
124
|
+
) -> Path:
|
|
125
|
+
stem_name = f"{input_path.stem}.enhanced.flac"
|
|
126
|
+
if output_dir is None:
|
|
127
|
+
return input_path.with_name(stem_name)
|
|
128
|
+
rel_parent = input_path.parent.relative_to(source_root)
|
|
129
|
+
dest_dir = output_dir / rel_parent
|
|
130
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
return dest_dir / stem_name
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def should_skip(output_path: Path) -> bool:
|
|
135
|
+
return output_path.exists()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class BatchSummary(NamedTuple):
|
|
139
|
+
succeeded: list[Path]
|
|
140
|
+
skipped: list[Path]
|
|
141
|
+
failed: list[tuple[Path, str, str]] # (input_path, error_code, message)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def run_batch(
|
|
145
|
+
files: list[Path],
|
|
146
|
+
source_root: Path,
|
|
147
|
+
model_path: Path,
|
|
148
|
+
config_path: Path,
|
|
149
|
+
inference_script: Path,
|
|
150
|
+
output_dir: Path | None,
|
|
151
|
+
skip_existing: bool,
|
|
152
|
+
on_file_progress: Callable[[Path, int, int, int], None] = lambda *a: None,
|
|
153
|
+
) -> BatchSummary:
|
|
154
|
+
succeeded: list[Path] = []
|
|
155
|
+
skipped: list[Path] = []
|
|
156
|
+
failed: list[tuple[Path, str, str]] = []
|
|
157
|
+
|
|
158
|
+
for i, input_path in enumerate(files, start=1):
|
|
159
|
+
out_path = output_path_for(input_path, source_root, output_dir)
|
|
160
|
+
if skip_existing and should_skip(out_path):
|
|
161
|
+
skipped.append(input_path)
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
result = run_single_file(
|
|
165
|
+
inference_script,
|
|
166
|
+
model_path,
|
|
167
|
+
config_path,
|
|
168
|
+
input_path,
|
|
169
|
+
out_path,
|
|
170
|
+
on_progress=lambda pct, idx=i, total=len(
|
|
171
|
+
files
|
|
172
|
+
), p=input_path: on_file_progress(p, idx, total, pct),
|
|
173
|
+
)
|
|
174
|
+
if result.success:
|
|
175
|
+
succeeded.append(input_path)
|
|
176
|
+
else:
|
|
177
|
+
failed.append(
|
|
178
|
+
(input_path, result.error_code or "GENERIC", result.error_message or "")
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return BatchSummary(succeeded=succeeded, skipped=skipped, failed=failed)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""argparse CLI surface: --folder/--file-name/--setup/--check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
from . import batch, model_manager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
15
|
+
parser = argparse.ArgumentParser(prog="echovault-flac-enhancer")
|
|
16
|
+
mode = parser.add_mutually_exclusive_group(required=True)
|
|
17
|
+
mode.add_argument(
|
|
18
|
+
"--folder", type=Path, help="Enhance all lossy tracks in a folder"
|
|
19
|
+
)
|
|
20
|
+
mode.add_argument("--file-name", type=Path, help="Enhance a single track")
|
|
21
|
+
mode.add_argument(
|
|
22
|
+
"--setup",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="Download/verify the model and run a self-test",
|
|
25
|
+
)
|
|
26
|
+
mode.add_argument(
|
|
27
|
+
"--check",
|
|
28
|
+
action="store_true",
|
|
29
|
+
help="Report runtime/deps/model/self-test status",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--recursive", action="store_true", help="Walk subfolders (only with --folder)"
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--workers", type=int, default=1, help="Parallel workers (v1: must be 1)"
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--skip-existing",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Skip files with an existing enhanced output",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--output-dir",
|
|
45
|
+
type=Path,
|
|
46
|
+
default=None,
|
|
47
|
+
help="Write outputs here instead of alongside sources",
|
|
48
|
+
)
|
|
49
|
+
return parser
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cmd_setup() -> int:
|
|
53
|
+
def on_progress(name: str, done: int, total: int) -> None:
|
|
54
|
+
pct = round(done / total * 100) if total else 0
|
|
55
|
+
print(f"Downloading {name}: {pct}%", end="\r")
|
|
56
|
+
|
|
57
|
+
print("Downloading model assets...")
|
|
58
|
+
paths = model_manager.ensure_model_assets(on_progress)
|
|
59
|
+
print("\nRunning self-test...")
|
|
60
|
+
inference_script = model_manager.inference_script_path()
|
|
61
|
+
if model_manager.run_self_test(
|
|
62
|
+
inference_script, paths.model_path, paths.config_path
|
|
63
|
+
):
|
|
64
|
+
print("SELF-TEST PASS")
|
|
65
|
+
return 0
|
|
66
|
+
print("SELF-TEST FAILED", file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _cmd_check() -> int:
|
|
71
|
+
status = model_manager.check_status()
|
|
72
|
+
print("Dependencies:")
|
|
73
|
+
for name, version in status["deps"].items():
|
|
74
|
+
print(f" {name}: {version if version else 'MISSING'}")
|
|
75
|
+
print(
|
|
76
|
+
f"Model: ok={status['model']['ok']} ({status['model']['reason']}) {status['model']['path']}"
|
|
77
|
+
)
|
|
78
|
+
print(
|
|
79
|
+
f"Config: ok={status['config']['ok']} ({status['config']['reason']}) {status['config']['path']}"
|
|
80
|
+
)
|
|
81
|
+
return (
|
|
82
|
+
0
|
|
83
|
+
if status["model"]["ok"]
|
|
84
|
+
and status["config"]["ok"]
|
|
85
|
+
and all(status["deps"].values())
|
|
86
|
+
else 1
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _print_summary(summary: batch.BatchSummary) -> None:
|
|
91
|
+
print(
|
|
92
|
+
f"\nSucceeded: {len(summary.succeeded)} Skipped: {len(summary.skipped)} Failed: {len(summary.failed)}"
|
|
93
|
+
)
|
|
94
|
+
for path, code, message in summary.failed:
|
|
95
|
+
print(f" FAILED {path}: {code} {message}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cmd_file(args: argparse.Namespace) -> int:
|
|
99
|
+
paths = model_manager.ensure_model_assets()
|
|
100
|
+
inference_script = model_manager.inference_script_path()
|
|
101
|
+
input_path: Path = args.file_name
|
|
102
|
+
output_path = batch.output_path_for(input_path, input_path.parent, args.output_dir)
|
|
103
|
+
|
|
104
|
+
with tqdm(total=100, desc=input_path.name) as bar:
|
|
105
|
+
|
|
106
|
+
def on_progress(pct: int) -> None:
|
|
107
|
+
bar.n = pct
|
|
108
|
+
bar.refresh()
|
|
109
|
+
|
|
110
|
+
result = batch.run_single_file(
|
|
111
|
+
inference_script,
|
|
112
|
+
paths.model_path,
|
|
113
|
+
paths.config_path,
|
|
114
|
+
input_path,
|
|
115
|
+
output_path,
|
|
116
|
+
on_progress,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if result.success:
|
|
120
|
+
print(f"DONE {result.output_path}")
|
|
121
|
+
return 0
|
|
122
|
+
print(f"ERROR {result.error_code} {result.error_message}", file=sys.stderr)
|
|
123
|
+
return {"MODEL_NOT_FOUND": 3, "INPUT_READ_FAILED": 2, "ORT_INIT_FAILED": 4}.get(
|
|
124
|
+
result.error_code, 1
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _cmd_folder(args: argparse.Namespace) -> int:
|
|
129
|
+
if args.workers > 1:
|
|
130
|
+
print(
|
|
131
|
+
"ERROR: parallel workers not implemented yet — v2. Use --workers 1.",
|
|
132
|
+
file=sys.stderr,
|
|
133
|
+
)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
folder: Path = args.folder
|
|
137
|
+
files = batch.resolve_files(folder, args.recursive)
|
|
138
|
+
if not files:
|
|
139
|
+
print("No lossy tracks found.")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
paths = model_manager.ensure_model_assets()
|
|
143
|
+
inference_script = model_manager.inference_script_path()
|
|
144
|
+
|
|
145
|
+
outer = tqdm(total=len(files), position=0, desc="Batch")
|
|
146
|
+
inner = tqdm(total=100, position=1, leave=False)
|
|
147
|
+
|
|
148
|
+
def on_file_progress(path: Path, idx: int, total: int, pct: int) -> None:
|
|
149
|
+
outer.set_postfix_str(f"Track {idx}/{total}")
|
|
150
|
+
inner.n = pct
|
|
151
|
+
inner.refresh()
|
|
152
|
+
|
|
153
|
+
summary = batch.run_batch(
|
|
154
|
+
files,
|
|
155
|
+
folder,
|
|
156
|
+
paths.model_path,
|
|
157
|
+
paths.config_path,
|
|
158
|
+
inference_script,
|
|
159
|
+
args.output_dir,
|
|
160
|
+
args.skip_existing,
|
|
161
|
+
on_file_progress=on_file_progress,
|
|
162
|
+
)
|
|
163
|
+
for _ in files:
|
|
164
|
+
outer.update(1)
|
|
165
|
+
inner.reset()
|
|
166
|
+
outer.close()
|
|
167
|
+
inner.close()
|
|
168
|
+
|
|
169
|
+
_print_summary(summary)
|
|
170
|
+
return 0 if not summary.failed else 1
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main(argv: list[str] | None = None) -> int:
|
|
174
|
+
args = build_parser().parse_args(argv)
|
|
175
|
+
if args.setup:
|
|
176
|
+
return _cmd_setup()
|
|
177
|
+
if args.check:
|
|
178
|
+
return _cmd_check()
|
|
179
|
+
if args.file_name:
|
|
180
|
+
return _cmd_file(args)
|
|
181
|
+
return _cmd_folder(args)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"baseUrl": "https://github.com/EchoVaultHQ/EchoVault-FLAC-Enhancer/releases/download/v0.1.0",
|
|
4
|
+
"model": {
|
|
5
|
+
"file": "model.onnx",
|
|
6
|
+
"sha256": "b08db07825ebb164a927e9ef50dc5e566d1c13c0515af039fc2350b7a523c379",
|
|
7
|
+
"bytes": 112608277
|
|
8
|
+
},
|
|
9
|
+
"config": {
|
|
10
|
+
"file": "config.json",
|
|
11
|
+
"sha256": "5368dceb6297a20fb1ea3768ffc132585776a2edc173c0e164d8ce1bc871425a",
|
|
12
|
+
"bytes": 280
|
|
13
|
+
}
|
|
14
|
+
}
|