Spaces:
Running
Running
Commit ·
b6f3797
1
Parent(s): b46e23a
Change to subprocess
Browse files- Dockerfile +2 -1
- README.md +15 -16
- backend_worker.py +29 -54
- frontend_app.py +21 -25
- start.sh +0 -10
Dockerfile
CHANGED
|
@@ -12,7 +12,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|
| 12 |
PIP_NO_BUILD_ISOLATION=1 \
|
| 13 |
FRONTEND_VENV=/opt/frontend-py310 \
|
| 14 |
BACKEND_VENV=/opt/backend-py39 \
|
| 15 |
-
|
|
|
|
| 16 |
PORT=7860
|
| 17 |
|
| 18 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
|
| 12 |
PIP_NO_BUILD_ISOLATION=1 \
|
| 13 |
FRONTEND_VENV=/opt/frontend-py310 \
|
| 14 |
BACKEND_VENV=/opt/backend-py39 \
|
| 15 |
+
BACKEND_PYTHON=/opt/backend-py39/bin/python \
|
| 16 |
+
BACKEND_SCRIPT=/app/backend_worker.py \
|
| 17 |
PORT=7860
|
| 18 |
|
| 19 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
README.md
CHANGED
|
@@ -16,9 +16,9 @@ needs two Python interpreters in one container:
|
|
| 16 |
|
| 17 |
- **Frontend:** Python 3.10 virtual environment for Gradio / pyharp.
|
| 18 |
- **Backend:** Python 3.9 virtual environment for BeatNet and madmom.
|
| 19 |
-
- **Transport:**
|
| 20 |
-
file path. This
|
| 21 |
-
|
| 22 |
|
| 23 |
The public `teamup-tech/BeatNet` Space currently uses a single
|
| 24 |
`python:3.10-slim` Docker image and starts `app.py` directly. Replace that setup
|
|
@@ -31,23 +31,23 @@ with these files, or adapt the same pattern in-place.
|
|
| 31 |
| `Dockerfile` | Builds separate Python 3.9 and 3.10 virtual environments. |
|
| 32 |
| `requirements-backend-py39.txt` | BeatNet/madmom dependencies only. |
|
| 33 |
| `requirements-frontend-py310.txt` | Gradio/pyharp dependencies only. |
|
| 34 |
-
| `backend_worker.py` |
|
| 35 |
| `frontend_app.py` | Gradio app running under Python 3.10. |
|
| 36 |
-
| `start.sh` | Starts the
|
| 37 |
|
| 38 |
-
## Why
|
| 39 |
|
| 40 |
`madmom` and BeatNet can be sensitive to Python, NumPy, Cython, and build
|
| 41 |
isolation versions. Running BeatNet/madmom in a backend-only Python 3.9
|
| 42 |
environment avoids the madmom compatibility patching needed by newer Python
|
| 43 |
versions, while the public web layer runs independently on Python 3.10. The
|
| 44 |
-
frontend
|
| 45 |
|
| 46 |
-
```
|
| 47 |
-
|
| 48 |
```
|
| 49 |
|
| 50 |
-
The backend
|
| 51 |
|
| 52 |
```json
|
| 53 |
{"ok": true, "beats": [{"time": 0.42, "beat": 1.0}]}
|
|
@@ -64,10 +64,9 @@ The backend responds with:
|
|
| 64 |
4. Push to the Space. Hugging Face will build the Docker image and route traffic
|
| 65 |
to `PORT=7860`.
|
| 66 |
|
| 67 |
-
## Alternative:
|
| 68 |
|
| 69 |
-
If
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
because there is no polling delay and no stale-file cleanup path.
|
|
|
|
| 16 |
|
| 17 |
- **Frontend:** Python 3.10 virtual environment for Gradio / pyharp.
|
| 18 |
- **Backend:** Python 3.9 virtual environment for BeatNet and madmom.
|
| 19 |
+
- **Transport:** on-demand Python 3.9 subprocess calls, with uploaded audio
|
| 20 |
+
passed as a file path. This avoids importing BeatNet/madmom in the frontend
|
| 21 |
+
interpreter without keeping a backend process alive.
|
| 22 |
|
| 23 |
The public `teamup-tech/BeatNet` Space currently uses a single
|
| 24 |
`python:3.10-slim` Docker image and starts `app.py` directly. Replace that setup
|
|
|
|
| 31 |
| `Dockerfile` | Builds separate Python 3.9 and 3.10 virtual environments. |
|
| 32 |
| `requirements-backend-py39.txt` | BeatNet/madmom dependencies only. |
|
| 33 |
| `requirements-frontend-py310.txt` | Gradio/pyharp dependencies only. |
|
| 34 |
+
| `backend_worker.py` | One-shot BeatNet subprocess runner under Python 3.9. |
|
| 35 |
| `frontend_app.py` | Gradio app running under Python 3.10. |
|
| 36 |
+
| `start.sh` | Starts the frontend. |
|
| 37 |
|
| 38 |
+
## Why a subprocess instead of importing both stacks?
|
| 39 |
|
| 40 |
`madmom` and BeatNet can be sensitive to Python, NumPy, Cython, and build
|
| 41 |
isolation versions. Running BeatNet/madmom in a backend-only Python 3.9
|
| 42 |
environment avoids the madmom compatibility patching needed by newer Python
|
| 43 |
versions, while the public web layer runs independently on Python 3.10. The
|
| 44 |
+
frontend invokes the backend script only when processing audio:
|
| 45 |
|
| 46 |
+
```sh
|
| 47 |
+
/opt/backend-py39/bin/python /app/backend_worker.py /tmp/gradio/upload.wav
|
| 48 |
```
|
| 49 |
|
| 50 |
+
The backend prints stable JSON to stdout:
|
| 51 |
|
| 52 |
```json
|
| 53 |
{"ok": true, "beats": [{"time": 0.42, "beat": 1.0}]}
|
|
|
|
| 64 |
4. Push to the Space. Hugging Face will build the Docker image and route traffic
|
| 65 |
to `PORT=7860`.
|
| 66 |
|
| 67 |
+
## Alternative: persistent worker transport
|
| 68 |
|
| 69 |
+
If startup latency becomes a problem, keep the same two virtual environments and
|
| 70 |
+
run `backend_worker.py` as a long-lived process behind a Unix socket or file
|
| 71 |
+
queue. The subprocess approach uses less idle memory, while a persistent worker
|
| 72 |
+
can reuse the loaded BeatNet model across requests.
|
|
|
backend_worker.py
CHANGED
|
@@ -1,73 +1,48 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""BeatNet backend
|
| 3 |
|
| 4 |
-
The
|
| 5 |
-
|
| 6 |
-
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
|
|
|
| 11 |
import json
|
| 12 |
-
import
|
| 13 |
-
import socket
|
| 14 |
-
from pathlib import Path
|
| 15 |
from typing import Any
|
| 16 |
|
| 17 |
-
from BeatNet.BeatNet import BeatNet
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
if not chunk:
|
| 27 |
-
break
|
| 28 |
-
chunks.append(chunk)
|
| 29 |
-
if not chunks:
|
| 30 |
-
return {}
|
| 31 |
-
return json.loads(b"".join(chunks).decode("utf-8"))
|
| 32 |
|
| 33 |
|
| 34 |
-
def
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
if SOCKET_PATH.exists():
|
| 41 |
-
SOCKET_PATH.unlink()
|
| 42 |
-
|
| 43 |
-
estimator = BeatNet(1, mode="offline", inference_model="DBN", plot=[], thread=False)
|
| 44 |
-
|
| 45 |
-
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
|
| 46 |
-
server.bind(str(SOCKET_PATH))
|
| 47 |
-
server.listen(16)
|
| 48 |
-
os.chmod(SOCKET_PATH, 0o666)
|
| 49 |
-
print(f"BeatNet backend listening on {SOCKET_PATH}", flush=True)
|
| 50 |
-
|
| 51 |
-
while True:
|
| 52 |
-
conn, _ = server.accept()
|
| 53 |
-
with conn:
|
| 54 |
-
try:
|
| 55 |
-
request = _read_message(conn)
|
| 56 |
-
audio_path = request["audio_path"]
|
| 57 |
-
beats = estimator.process(audio_path)
|
| 58 |
-
_send_message(
|
| 59 |
-
conn,
|
| 60 |
-
{
|
| 61 |
-
"ok": True,
|
| 62 |
-
"beats": [
|
| 63 |
-
{"time": float(timestamp), "beat": float(beat)}
|
| 64 |
-
for timestamp, beat in beats
|
| 65 |
-
],
|
| 66 |
-
},
|
| 67 |
-
)
|
| 68 |
-
except Exception as exc: # Keep worker alive across bad requests.
|
| 69 |
-
_send_message(conn, {"ok": False, "error": repr(exc)})
|
| 70 |
|
| 71 |
|
| 72 |
if __name__ == "__main__":
|
| 73 |
-
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""One-shot BeatNet backend runner for the Python 3.9 environment.
|
| 3 |
|
| 4 |
+
The frontend invokes this script as a subprocess and passes uploaded audio by
|
| 5 |
+
file path, avoiding cross-interpreter object sharing without keeping a backend
|
| 6 |
+
worker process alive.
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
| 11 |
+
import contextlib
|
| 12 |
import json
|
| 13 |
+
import sys
|
|
|
|
|
|
|
| 14 |
from typing import Any
|
| 15 |
|
|
|
|
| 16 |
|
| 17 |
+
def run_beatnet(audio_path: str) -> list[dict[str, float]]:
|
| 18 |
+
with contextlib.redirect_stdout(sys.stderr):
|
| 19 |
+
from BeatNet.BeatNet import BeatNet
|
| 20 |
|
| 21 |
+
estimator = BeatNet(1, mode="offline", inference_model="DBN", plot=[], thread=False)
|
| 22 |
+
beats = estimator.process(audio_path)
|
| 23 |
|
| 24 |
+
return [
|
| 25 |
+
{"time": float(timestamp), "beat": float(beat)}
|
| 26 |
+
for timestamp, beat in beats
|
| 27 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
+
def main(argv: list[str]) -> int:
|
| 31 |
+
if len(argv) != 2:
|
| 32 |
+
print(
|
| 33 |
+
json.dumps({"ok": False, "error": "Usage: backend_worker.py <audio_path>"}),
|
| 34 |
+
flush=True,
|
| 35 |
+
)
|
| 36 |
+
return 2
|
| 37 |
|
| 38 |
+
try:
|
| 39 |
+
payload: dict[str, Any] = {"ok": True, "beats": run_beatnet(argv[1])}
|
| 40 |
+
except Exception as exc:
|
| 41 |
+
payload = {"ok": False, "error": repr(exc)}
|
| 42 |
|
| 43 |
+
print(json.dumps(payload), flush=True)
|
| 44 |
+
return 0 if payload["ok"] else 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
if __name__ == "__main__":
|
| 48 |
+
raise SystemExit(main(sys.argv))
|
frontend_app.py
CHANGED
|
@@ -5,41 +5,37 @@ from __future__ import annotations
|
|
| 5 |
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
-
import
|
| 9 |
-
import time
|
| 10 |
-
from pathlib import Path
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
from pyharp import AudioLabel, LabelList, OutputLabel
|
| 15 |
from pyharp.core import ModelCard, build_endpoint
|
| 16 |
|
| 17 |
-
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
def call_backend(audio_path: str, timeout_s: float = 120.0) -> list[dict[str, float]]:
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
break
|
| 38 |
-
chunks.append(chunk)
|
| 39 |
-
|
| 40 |
-
response: dict[str, Any] = json.loads(b"".join(chunks).decode("utf-8"))
|
| 41 |
if not response.get("ok"):
|
| 42 |
-
|
|
|
|
| 43 |
return response["beats"]
|
| 44 |
|
| 45 |
|
|
|
|
| 5 |
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
+
import subprocess
|
|
|
|
|
|
|
| 9 |
from typing import Any
|
| 10 |
|
| 11 |
import gradio as gr
|
| 12 |
from pyharp import AudioLabel, LabelList, OutputLabel
|
| 13 |
from pyharp.core import ModelCard, build_endpoint
|
| 14 |
|
| 15 |
+
BACKEND_PYTHON = os.environ.get("BACKEND_PYTHON", "/opt/backend-py39/bin/python")
|
| 16 |
+
BACKEND_SCRIPT = os.environ.get("BACKEND_SCRIPT", "/app/backend_worker.py")
|
| 17 |
|
| 18 |
|
| 19 |
def call_backend(audio_path: str, timeout_s: float = 120.0) -> list[dict[str, float]]:
|
| 20 |
+
completed = subprocess.run(
|
| 21 |
+
[BACKEND_PYTHON, BACKEND_SCRIPT, audio_path],
|
| 22 |
+
capture_output=True,
|
| 23 |
+
check=False,
|
| 24 |
+
text=True,
|
| 25 |
+
timeout=timeout_s,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
response: dict[str, Any] = json.loads(completed.stdout)
|
| 30 |
+
except json.JSONDecodeError as exc:
|
| 31 |
+
raise RuntimeError(
|
| 32 |
+
"BeatNet backend returned invalid JSON. "
|
| 33 |
+
f"stdout={completed.stdout!r} stderr={completed.stderr!r}"
|
| 34 |
+
) from exc
|
| 35 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
if not response.get("ok"):
|
| 37 |
+
error = response.get("error") or completed.stderr or "BeatNet backend failed"
|
| 38 |
+
raise RuntimeError(error)
|
| 39 |
return response["beats"]
|
| 40 |
|
| 41 |
|
start.sh
CHANGED
|
@@ -1,14 +1,4 @@
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
|
| 4 |
-
rm -f "${BEATNET_SOCKET:-/tmp/beatnet_backend.sock}"
|
| 5 |
-
|
| 6 |
-
"${BACKEND_VENV:-/opt/backend-py39}/bin/python" -u /app/backend_worker.py &
|
| 7 |
-
backend_pid="$!"
|
| 8 |
-
|
| 9 |
-
cleanup() {
|
| 10 |
-
kill "$backend_pid" 2>/dev/null || true
|
| 11 |
-
}
|
| 12 |
-
trap cleanup EXIT INT TERM
|
| 13 |
-
|
| 14 |
exec "${FRONTEND_VENV:-/opt/frontend-py310}/bin/python" -u /app/frontend_app.py
|
|
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
exec "${FRONTEND_VENV:-/opt/frontend-py310}/bin/python" -u /app/frontend_app.py
|