uncompose-engine 0.1.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,117 @@
|
|
|
1
|
+
"""Engine Contract entry point: `python -m uncompose_engine`.
|
|
2
|
+
|
|
3
|
+
Reads one JSON request from stdin, runs the model via audio-separator, and
|
|
4
|
+
emits JSONL events on stdout. Everything else (library logging, tqdm bars,
|
|
5
|
+
stray prints) is forced onto stderr so the event stream stays parseable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.metadata
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
from audio_separator.separator import Separator
|
|
17
|
+
|
|
18
|
+
# Models known to this engine: audio-separator filename plus the mapping
|
|
19
|
+
# from audio-separator's stem names to Uncompose's preset-level stem names
|
|
20
|
+
# (keys.wav, not piano.wav — the model is piano-trained, the name is stable).
|
|
21
|
+
MODELS = {
|
|
22
|
+
"mel_band_roformer_kim": {
|
|
23
|
+
"filename": "vocals_mel_band_roformer.ckpt",
|
|
24
|
+
# audio-separator calls this model's non-vocal stem "Other".
|
|
25
|
+
"output_names": {
|
|
26
|
+
"Vocals": "vocals",
|
|
27
|
+
"Other": "instrumental",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
"htdemucs_6s": {
|
|
31
|
+
"filename": "htdemucs_6s.yaml",
|
|
32
|
+
"output_names": {
|
|
33
|
+
"Vocals": "vocals",
|
|
34
|
+
"Drums": "drums",
|
|
35
|
+
"Bass": "bass",
|
|
36
|
+
"Guitar": "guitar",
|
|
37
|
+
"Piano": "keys",
|
|
38
|
+
"Other": "other",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> int:
|
|
45
|
+
# Keep a private handle to the real stdout for events, then point fd 1
|
|
46
|
+
# at stderr so nothing the ML stack prints can corrupt the JSONL stream.
|
|
47
|
+
events = os.fdopen(os.dup(1), "w", buffering=1)
|
|
48
|
+
os.dup2(2, 1)
|
|
49
|
+
sys.stdout = sys.stderr
|
|
50
|
+
|
|
51
|
+
def emit(event: dict) -> None:
|
|
52
|
+
events.write(json.dumps(event) + "\n")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
request = json.load(sys.stdin)
|
|
56
|
+
return run(request, emit)
|
|
57
|
+
except Exception as exc: # terminal: report on the contract, exit nonzero
|
|
58
|
+
logging.exception("engine failed")
|
|
59
|
+
emit({"event": "error", "message": str(exc)})
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run(request: dict, emit) -> int:
|
|
64
|
+
model = MODELS.get(request["model_id"])
|
|
65
|
+
if model is None:
|
|
66
|
+
raise ValueError(f"unknown model id: {request['model_id']}")
|
|
67
|
+
|
|
68
|
+
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
|
|
69
|
+
|
|
70
|
+
emit({"event": "stage", "stage": "model_load", "message": request["model_id"]})
|
|
71
|
+
t0 = time.monotonic()
|
|
72
|
+
separator = Separator(
|
|
73
|
+
output_dir=request["output_dir"],
|
|
74
|
+
model_file_dir=request["model_dir"],
|
|
75
|
+
output_format="wav",
|
|
76
|
+
)
|
|
77
|
+
separator.load_model(model_filename=model["filename"])
|
|
78
|
+
model_load_secs = time.monotonic() - t0
|
|
79
|
+
|
|
80
|
+
emit({"event": "stage", "stage": "separate"})
|
|
81
|
+
t1 = time.monotonic()
|
|
82
|
+
separator.separate(request["audio_path"], custom_output_names=model["output_names"])
|
|
83
|
+
separate_secs = time.monotonic() - t1
|
|
84
|
+
|
|
85
|
+
# audio-separator can swallow decode failures and return with nothing
|
|
86
|
+
# written; missing stems are a failed job, not a quiet success. Each stem
|
|
87
|
+
# is staged as `<stem>.wav.partial`; the core promotes it to its final
|
|
88
|
+
# name when it sees the stem event, so a crash leaves partials behind.
|
|
89
|
+
missing = []
|
|
90
|
+
for stem_name in model["output_names"].values():
|
|
91
|
+
final = os.path.join(request["output_dir"], f"{stem_name}.wav")
|
|
92
|
+
if os.path.exists(final):
|
|
93
|
+
partial = final + ".partial"
|
|
94
|
+
os.replace(final, partial)
|
|
95
|
+
emit({"event": "stem", "name": stem_name, "path": partial})
|
|
96
|
+
else:
|
|
97
|
+
missing.append(stem_name)
|
|
98
|
+
if missing:
|
|
99
|
+
raise RuntimeError(f"separation produced no output for: {', '.join(missing)}")
|
|
100
|
+
|
|
101
|
+
emit(
|
|
102
|
+
{
|
|
103
|
+
"event": "done",
|
|
104
|
+
"model_id": request["model_id"],
|
|
105
|
+
"engine_version": importlib.metadata.version("uncompose-engine"),
|
|
106
|
+
"device": "cuda" if torch.cuda.is_available() else "cpu",
|
|
107
|
+
"timings": {
|
|
108
|
+
"model_load_secs": round(model_load_secs, 2),
|
|
109
|
+
"separate_secs": round(separate_secs, 2),
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
sys.exit(main())
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
uncompose_engine/__init__.py,sha256=gVCgmAo13iXQwFpSBQG0jwCHVsBHHaKqnuJ5I7XZ4So,196
|
|
2
|
+
uncompose_engine/__main__.py,sha256=FSOyHK7i5OLz9C5aAieQ58ZmKYNMGzkzeeATit2MLJU,3940
|
|
3
|
+
uncompose_engine-0.1.0.dist-info/METADATA,sha256=TfA0DW6iFpBgAC2R72A8Poe48bdalfpaM1FCZD6jPLQ,249
|
|
4
|
+
uncompose_engine-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
uncompose_engine-0.1.0.dist-info/RECORD,,
|