Spaces:
Running on Zero
Running on Zero
deploy: piano-roll notes output (runtime files only)
Browse files- app.py +12 -2
- stemflipper/export.py +27 -0
- stemflipper/pipeline.py +3 -0
app.py
CHANGED
|
@@ -5,6 +5,7 @@ separation stage is GPU-relevant, so it alone is wrapped with @spaces.GPU
|
|
| 5 |
(a no-op everywhere else).
|
| 6 |
"""
|
| 7 |
|
|
|
|
| 8 |
import os
|
| 9 |
import tempfile
|
| 10 |
from pathlib import Path
|
|
@@ -104,7 +105,13 @@ def flip(audio_path, model, progress=gr.Progress()):
|
|
| 104 |
else None
|
| 105 |
for name in PREVIEW_STEMS
|
| 106 |
]
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
|
| 110 |
with gr.Blocks(title="StemFlipper") as demo:
|
|
@@ -125,10 +132,13 @@ with gr.Blocks(title="StemFlipper") as demo:
|
|
| 125 |
preview_outs = [
|
| 126 |
gr.Audio(label=name, interactive=False) for name in PREVIEW_STEMS
|
| 127 |
]
|
|
|
|
|
|
|
|
|
|
| 128 |
go_btn.click(
|
| 129 |
flip,
|
| 130 |
inputs=[audio_in, model_in],
|
| 131 |
-
outputs=[zip_out, summary_out, *preview_outs],
|
| 132 |
api_name="flip",
|
| 133 |
)
|
| 134 |
|
|
|
|
| 5 |
(a no-op everywhere else).
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
import json
|
| 9 |
import os
|
| 10 |
import tempfile
|
| 11 |
from pathlib import Path
|
|
|
|
| 105 |
else None
|
| 106 |
for name in PREVIEW_STEMS
|
| 107 |
]
|
| 108 |
+
|
| 109 |
+
# Per-stem detected notes for the client piano-roll (appended LAST so the preview
|
| 110 |
+
# output indices above stay stable for existing API callers).
|
| 111 |
+
notes_path = bundle / "notes.json"
|
| 112 |
+
notes = json.loads(notes_path.read_text()) if notes_path.exists() else {"stems": {}}
|
| 113 |
+
|
| 114 |
+
return str(result["zip_path"]), summary, *previews, notes
|
| 115 |
|
| 116 |
|
| 117 |
with gr.Blocks(title="StemFlipper") as demo:
|
|
|
|
| 132 |
preview_outs = [
|
| 133 |
gr.Audio(label=name, interactive=False) for name in PREVIEW_STEMS
|
| 134 |
]
|
| 135 |
+
# Per-stem detected notes → the static web frontend draws piano-rolls from this.
|
| 136 |
+
# Hidden in the Gradio UI itself (visible=False) but present in the API output.
|
| 137 |
+
notes_out = gr.JSON(visible=False)
|
| 138 |
go_btn.click(
|
| 139 |
flip,
|
| 140 |
inputs=[audio_in, model_in],
|
| 141 |
+
outputs=[zip_out, summary_out, *preview_outs, notes_out],
|
| 142 |
api_name="flip",
|
| 143 |
)
|
| 144 |
|
stemflipper/export.py
CHANGED
|
@@ -135,6 +135,33 @@ def write_rpp(
|
|
| 135 |
return path
|
| 136 |
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
def write_manifest(bundle_dir: str | Path, meta: dict) -> Path:
|
| 139 |
path = Path(bundle_dir) / "manifest.json"
|
| 140 |
path.write_text(json.dumps(meta, indent=2))
|
|
|
|
| 135 |
return path
|
| 136 |
|
| 137 |
|
| 138 |
+
def write_notes(tracks: dict, duration: float, bundle_dir: str | Path) -> Path:
|
| 139 |
+
"""Per-stem detected notes → notes.json, for UI piano-rolls (and the download).
|
| 140 |
+
|
| 141 |
+
tracks: {stem_name: {"notes": [{pitch,start,end,velocity}], "is_drum": bool}}.
|
| 142 |
+
Each stem's notes are stored as compact ``[pitch, start, end, velocity]`` rows
|
| 143 |
+
(ints/rounded floats) so the file stays small and a client can draw them without
|
| 144 |
+
parsing MIDI. ``duration`` bounds the time axis. Stems with no notes are omitted.
|
| 145 |
+
"""
|
| 146 |
+
stems = {}
|
| 147 |
+
for name, data in tracks.items():
|
| 148 |
+
notes = data.get("notes") or []
|
| 149 |
+
if not notes:
|
| 150 |
+
continue
|
| 151 |
+
stems[name] = {
|
| 152 |
+
"is_drum": bool(data.get("is_drum")),
|
| 153 |
+
"notes": [
|
| 154 |
+
[int(n["pitch"]), round(float(n["start"]), 4),
|
| 155 |
+
round(float(n["end"]), 4), int(n.get("velocity", 100))]
|
| 156 |
+
for n in notes
|
| 157 |
+
],
|
| 158 |
+
}
|
| 159 |
+
payload = {"duration": round(float(duration), 3), "stems": stems}
|
| 160 |
+
path = Path(bundle_dir) / "notes.json"
|
| 161 |
+
path.write_text(json.dumps(payload))
|
| 162 |
+
return path
|
| 163 |
+
|
| 164 |
+
|
| 165 |
def write_manifest(bundle_dir: str | Path, meta: dict) -> Path:
|
| 166 |
path = Path(bundle_dir) / "manifest.json"
|
| 167 |
path.write_text(json.dumps(meta, indent=2))
|
stemflipper/pipeline.py
CHANGED
|
@@ -142,6 +142,9 @@ def run_pipeline(
|
|
| 142 |
|
| 143 |
report(0.9, "Writing MIDI, manifest, Reaper project")
|
| 144 |
export.write_midi(tracks, analysis.tempo, bundle_dir / "midi")
|
|
|
|
|
|
|
|
|
|
| 145 |
manifest = export.make_manifest_meta(input_path.name, analysis, model, stems_meta)
|
| 146 |
export.write_manifest(bundle_dir, manifest)
|
| 147 |
export.write_readme(bundle_dir, input_path.name, analysis)
|
|
|
|
| 142 |
|
| 143 |
report(0.9, "Writing MIDI, manifest, Reaper project")
|
| 144 |
export.write_midi(tracks, analysis.tempo, bundle_dir / "midi")
|
| 145 |
+
export.write_notes(tracks, analysis.duration, bundle_dir)
|
| 146 |
+
for name, meta in stems_meta.items():
|
| 147 |
+
meta["notes"] = "notes.json" if tracks.get(name, {}).get("notes") else None
|
| 148 |
manifest = export.make_manifest_meta(input_path.name, analysis, model, stems_meta)
|
| 149 |
export.write_manifest(bundle_dir, manifest)
|
| 150 |
export.write_readme(bundle_dir, input_path.name, analysis)
|