Spaces:
Running on Zero
Running on Zero
Report a time-based brew estimate so the progress bar moves on ZeroGPU (Stage 3)
Browse filesThe @spaces.GPU worker is a separate process, so it can't push per-chunk progress back to /api/progress - the brew bar froze at 0% then jumped to 100% on the Space. /api/progress now returns a smooth time-based estimate (fractional done, capped at 92%) computed from the brew's start + a rough per-length budget, snapping to 100% when the tape lands. Local/stub paths keep their real per-chunk progress (the estimate only kicks in while a ZeroGPU brew is active).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -23,6 +23,7 @@ import io
|
|
| 23 |
import json
|
| 24 |
import os
|
| 25 |
import threading
|
|
|
|
| 26 |
import wave
|
| 27 |
from pathlib import Path
|
| 28 |
|
|
@@ -81,6 +82,11 @@ ALLOWED_SECONDS = (30, 60, 90)
|
|
| 81 |
DEFAULT_SECONDS = int(os.getenv("LOFINITY_DURATION", "30"))
|
| 82 |
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
|
| 83 |
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3.2:3b")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
app = Server(title="LoFinity")
|
| 86 |
|
|
@@ -89,6 +95,10 @@ app = Server(title="LoFinity")
|
|
| 89 |
# frontend polls /api/progress to fill its brewing bar.
|
| 90 |
_PROGRESS = {"done": 0, "total": 1}
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
# --- prompt enrichment --------------------------------------------------------
|
| 93 |
|
| 94 |
ENRICH_SYSTEM = """\
|
|
@@ -419,9 +429,10 @@ def _gpu_budget(prompt: str, seconds: int = CHUNK_S) -> int:
|
|
| 419 |
def gpu_brew(prompt: str, seconds: int = CHUNK_S) -> tuple:
|
| 420 |
"""ZeroGPU entry point — enrichment (MiniCPM) AND MusicGen on the real GPU in
|
| 421 |
a single acquisition. Takes the raw vibe and returns
|
| 422 |
-
(music_prompt, title, bed, samples, rate).
|
| 423 |
-
|
| 424 |
-
|
|
|
|
| 425 |
music_prompt, title, bed = enrich_prompt(prompt)
|
| 426 |
samples, rate = musicgen_engine(music_prompt, seconds)
|
| 427 |
return music_prompt, title, bed, samples, rate
|
|
@@ -463,11 +474,20 @@ def generate_song(prompt: str, seconds: int = DEFAULT_SECONDS) -> dict:
|
|
| 463 |
_PROGRESS.update(done=0, total=chunks)
|
| 464 |
|
| 465 |
if IS_ZEROGPU and ENGINE != "stub":
|
| 466 |
-
# On ZeroGPU
|
| 467 |
-
#
|
| 468 |
-
#
|
| 469 |
-
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
print(f"[lofinity] brewed {title!r} :: {music_prompt} [+ {bed}]")
|
| 472 |
else:
|
| 473 |
# Local / stub: enrich in-process (Ollama or fallback), then render with
|
|
@@ -493,7 +513,15 @@ def generate_song(prompt: str, seconds: int = DEFAULT_SECONDS) -> dict:
|
|
| 493 |
|
| 494 |
@app.get("/api/progress")
|
| 495 |
def progress() -> dict:
|
| 496 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
return dict(_PROGRESS)
|
| 498 |
|
| 499 |
|
|
|
|
| 23 |
import json
|
| 24 |
import os
|
| 25 |
import threading
|
| 26 |
+
import time
|
| 27 |
import wave
|
| 28 |
from pathlib import Path
|
| 29 |
|
|
|
|
| 82 |
DEFAULT_SECONDS = int(os.getenv("LOFINITY_DURATION", "30"))
|
| 83 |
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
|
| 84 |
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3.2:3b")
|
| 85 |
+
# A ZeroGPU brew renders in a separate GPU worker process, so /api/progress can't
|
| 86 |
+
# read real per-chunk progress from it; it reports a time-based estimate from this
|
| 87 |
+
# rough per-length budget instead (tunable; only affects the cosmetic brew bar).
|
| 88 |
+
GPU_WARMUP_S = 5.0 # enrichment + cold-start/queue allowance before audio flows
|
| 89 |
+
GPU_SECS_PER_CHUNK = 10.0 # rough GPU render time per 30s chunk
|
| 90 |
|
| 91 |
app = Server(title="LoFinity")
|
| 92 |
|
|
|
|
| 95 |
# frontend polls /api/progress to fill its brewing bar.
|
| 96 |
_PROGRESS = {"done": 0, "total": 1}
|
| 97 |
|
| 98 |
+
# A ZeroGPU brew's wall-clock start + estimated total, so /api/progress can report
|
| 99 |
+
# a smooth time-based estimate while the GPU worker is busy (see progress()).
|
| 100 |
+
_BREW = {"active": False, "start": 0.0, "est": 1.0, "total": 1}
|
| 101 |
+
|
| 102 |
# --- prompt enrichment --------------------------------------------------------
|
| 103 |
|
| 104 |
ENRICH_SYSTEM = """\
|
|
|
|
| 429 |
def gpu_brew(prompt: str, seconds: int = CHUNK_S) -> tuple:
|
| 430 |
"""ZeroGPU entry point — enrichment (MiniCPM) AND MusicGen on the real GPU in
|
| 431 |
a single acquisition. Takes the raw vibe and returns
|
| 432 |
+
(music_prompt, title, bed, samples, rate). It runs in a separate GPU worker
|
| 433 |
+
process and can't push per-chunk progress back to the web process, so
|
| 434 |
+
/api/progress reports a time-based estimate for the bar. This path is
|
| 435 |
+
Space-only."""
|
| 436 |
music_prompt, title, bed = enrich_prompt(prompt)
|
| 437 |
samples, rate = musicgen_engine(music_prompt, seconds)
|
| 438 |
return music_prompt, title, bed, samples, rate
|
|
|
|
| 474 |
_PROGRESS.update(done=0, total=chunks)
|
| 475 |
|
| 476 |
if IS_ZEROGPU and ENGINE != "stub":
|
| 477 |
+
# On ZeroGPU enrichment (MiniCPM) and MusicGen share ONE @spaces.GPU
|
| 478 |
+
# acquisition in a separate worker process, which can't push real progress
|
| 479 |
+
# back — so /api/progress reports a smooth time-based ESTIMATE driven by
|
| 480 |
+
# this brew's start + budget (capped <100% until the tape actually lands).
|
| 481 |
+
est = GPU_WARMUP_S + GPU_SECS_PER_CHUNK * chunks
|
| 482 |
+
_BREW.update(active=True, start=time.monotonic(), est=est, total=chunks)
|
| 483 |
+
print(f"[lofinity] brewing on GPU :: {prompt!r} ({seconds}s, ~{est:.0f}s est)")
|
| 484 |
+
try:
|
| 485 |
+
music_prompt, title, bed, samples, rate = gpu_brew(prompt, seconds)
|
| 486 |
+
finally:
|
| 487 |
+
# top the bar off BEFORE clearing active, so a poll landing in between
|
| 488 |
+
# reads 100% (from _PROGRESS), never the 0% this brew started at
|
| 489 |
+
_PROGRESS.update(done=chunks, total=chunks)
|
| 490 |
+
_BREW.update(active=False)
|
| 491 |
print(f"[lofinity] brewed {title!r} :: {music_prompt} [+ {bed}]")
|
| 492 |
else:
|
| 493 |
# Local / stub: enrich in-process (Ollama or fallback), then render with
|
|
|
|
| 513 |
|
| 514 |
@app.get("/api/progress")
|
| 515 |
def progress() -> dict:
|
| 516 |
+
"""Progress for the brewing bar. Local/stub report real per-chunk progress via
|
| 517 |
+
_PROGRESS. A ZeroGPU brew runs in a separate GPU worker that can't push
|
| 518 |
+
progress back, so report a smooth time-based ESTIMATE instead: a fractional
|
| 519 |
+
`done` (the frontend fills the bar to done/total) capped below 100% until the
|
| 520 |
+
real tape lands and _PROGRESS tops it off."""
|
| 521 |
+
if _BREW["active"] and _BREW["est"] > 0:
|
| 522 |
+
elapsed = time.monotonic() - _BREW["start"]
|
| 523 |
+
frac = min(0.92, elapsed / _BREW["est"])
|
| 524 |
+
return {"done": round(frac * _BREW["total"], 3), "total": _BREW["total"]}
|
| 525 |
return dict(_PROGRESS)
|
| 526 |
|
| 527 |
|