Spaces:
Sleeping
Sleeping
File size: 7,960 Bytes
94004a2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | """Phase 4.1 benchmark β is "Regen N changed" actually β€5s on a real clip?
Drives the full pipeline via HTTP: ingest URL β prep β transcribe β full
generate β edit one segment β regen-only β time it. Prints both wall-clocks.
Success criterion: incremental regen β€ 5.0s (roadmap Phase 4.1 exit target).
Usage:
uv run python scripts/bench_incremental.py [URL]
Default URL is the Fireship clip the user picked. Server must be live at
127.0.0.1:3900 with the models loaded (first cold run can take minutes).
"""
from __future__ import annotations
import json
import os
import sys
import time
import urllib.request
from urllib.error import HTTPError, URLError
API = "http://127.0.0.1:3900"
DEFAULT_URL = "https://www.youtube.com/watch?v=ZzI9JE0i6Lc"
TARGET_S = 5.0
# Cap baseline segments to keep full-dub wall-clock tractable. The incremental
# target is single-segment regen speed; 30 segs is plenty to populate caches
# for that measurement. Override via env.
MAX_SEGS = int(os.environ.get("BENCH_MAX_SEGS", "30"))
def post(path: str, body: dict) -> dict:
req = urllib.request.Request(
f"{API}{path}",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=600) as r:
return json.loads(r.read().decode())
def stream_sse(url: str, on_event):
"""Parse SSE frames. Handles both shapes the backend emits:
1. `data: {"type": "X", ...}` (prep pipeline, dub_generate)
2. `event: X\\ndata: {...}` (dub_transcribe β named events)
Normalises both into {"type": <name>, ...payload}. `on_event` returning
truthy stops the loop.
"""
req = urllib.request.Request(f"{API}{url}")
pending_event = None
last = None
# 1 hour read window β full-dub on a 489-seg clip can run long on Apple Silicon.
with urllib.request.urlopen(req, timeout=3600) as r:
for raw in r:
line = raw.decode().rstrip("\r\n")
if line.startswith("event:"):
pending_event = line[6:].strip()
continue
if not line.startswith("data:"):
# Blank line between frames resets named-event state.
if not line:
pending_event = None
continue
body = line[5:].strip()
try:
payload = json.loads(body)
except json.JSONDecodeError:
continue
if pending_event and isinstance(payload, dict) and "type" not in payload:
payload = {"type": pending_event, **payload}
last = payload
if on_event(payload):
return payload
return last
def wait_prep(task_id: str) -> dict:
print(f" Β· streaming /tasks/stream/{task_id} β¦")
def cb(ev):
t = ev.get("type") or ev.get("stage")
if t in ("ready", "done"):
return True
if t == "error":
raise RuntimeError(f"prep error: {ev}")
if ev.get("type") == "progress":
s = ev.get("stage") or ""
p = ev.get("pct")
if p is not None:
print(f" prep {s} {int(p)}%")
else:
print(f" prep {s}")
return False
return stream_sse(f"/tasks/stream/{task_id}", cb)
def transcribe(job_id: str) -> list[dict]:
print(f" Β· streaming /dub/transcribe-stream/{job_id} β¦")
final: list[dict] = []
def cb(ev):
t = ev.get("type")
if t == "final":
# The diarised post-pass replaces per-chunk segs; this is what we want.
final.extend(ev.get("segments", []))
if t == "done":
return True
if t == "error":
raise RuntimeError(f"transcribe error: {ev}")
return False
stream_sse(f"/dub/transcribe-stream/{job_id}", cb)
return final
DUB_SEG_FIELDS = {"start", "end", "text", "instruct", "profile_id", "speed", "gain", "target_lang"}
def to_dub_segment(s: dict) -> dict:
"""Project a raw transcribe-output segment onto DubSegment's schema."""
out = {k: v for k, v in s.items() if k in DUB_SEG_FIELDS}
out.setdefault("text", s.get("text", ""))
out.setdefault("start", float(s.get("start", 0.0)))
out.setdefault("end", float(s.get("end", 0.0)))
return out
def generate(job_id: str, segments: list[dict], *, regen_only: list[str] | None = None) -> dict:
"""POST β {task_id}, then stream /tasks/stream/{task_id} for SSE progress."""
seg_ids = [str(s.get("id") or f"seg_{i}") for i, s in enumerate(segments)]
body = {
"segments": [to_dub_segment(s) for s in segments],
"segment_ids": seg_ids,
"language": "English",
"language_code": "en",
}
if regen_only is not None:
body["regen_only"] = regen_only
r = post(f"/dub/generate/{job_id}", body)
task_id = r["task_id"]
done_ev = None
def cb(ev):
nonlocal done_ev
t = ev.get("type")
if t == "progress":
print(f" gen {ev.get('current', 0)}/{ev.get('total', '?')}",
end="\r", flush=True)
elif t == "done":
done_ev = ev
return True
elif t == "error":
raise RuntimeError(f"generate error: {ev}")
return False
stream_sse(f"/tasks/stream/{task_id}", cb)
print()
if not done_ev:
raise RuntimeError("generate task ended without 'done' event")
return done_ev
def main():
url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URL
print(f"Benchmark target: {TARGET_S}s incremental regen")
print(f"Fixture URL: {url}")
try:
urllib.request.urlopen(f"{API}/system/info", timeout=3)
except (HTTPError, URLError) as e:
sys.exit(f"Server not reachable at {API}: {e}")
# 1. Ingest URL
print("\n[1/4] Ingest URL β¦")
r = post("/dub/ingest-url", {"url": url})
job_id, task_id = r["job_id"], r["task_id"]
print(f" job_id={job_id} task_id={task_id}")
# 2. Wait for prep
print("\n[2/4] Prep (download / extract / demucs / scene) β¦")
t0 = time.perf_counter()
wait_prep(task_id)
print(f" prep: {time.perf_counter() - t0:.1f}s")
# 3. Transcribe
print("\n[3/4] Transcribe β¦")
t0 = time.perf_counter()
segments = transcribe(job_id)
print(f" transcribe: {time.perf_counter() - t0:.1f}s Β· {len(segments)} segments")
if not segments:
sys.exit("No segments returned β can't benchmark.")
if len(segments) > MAX_SEGS:
print(f" Β· capping to first {MAX_SEGS} segs for baseline (BENCH_MAX_SEGS to change)")
segments = segments[:MAX_SEGS]
# 4a. Full generate (baseline β produces on-disk seg_N.wav)
print("\n[4a/4] FULL generate (baseline) β¦")
t0 = time.perf_counter()
done = generate(job_id, segments)
t_full = time.perf_counter() - t0
print(f" full generate: {t_full:.2f}s Β· {done.get('segments_processed', '?')} segs")
# 4b. Edit ONE segment, regen only it
edited = list(segments)
edit_id = str(edited[0].get("id") or "seg_0")
edited[0] = {**edited[0], "text": edited[0].get("text", "") + " (edited)"}
print(f"\n[4b/4] INCREMENTAL regen (seg id={edit_id}) β¦")
t0 = time.perf_counter()
done_inc = generate(job_id, edited, regen_only=[edit_id])
t_inc = time.perf_counter() - t0
print(f" incremental regen: {t_inc:.2f}s Β· {done_inc.get('segments_processed', '?')} segs")
# 5. Verdict
print("\nββ verdict ββ")
print(f" full dub: {t_full:>7.2f}s")
print(f" incremental: {t_inc:>7.2f}s target β€ {TARGET_S:.1f}s")
passed = t_inc <= TARGET_S
print(f" exit 4.1: {'β
PASS' if passed else 'β MISS'}")
print(f" speedup vs full: {t_full / t_inc:.1f}Γ")
sys.exit(0 if passed else 1)
if __name__ == "__main__":
main()
|