wxDai Claude Fable 5 commited on
Commit
9d92b15
·
1 Parent(s): 046c341

Bind mp4-mode result download to its own session via rec id

Browse files

recording_finalized now carries the fork's rec dir name over the ws;
/download_last requires ?rec=<dir> (validated, must exist under the
record dir). Drop the <record_dir>/.last cross-process marker — it was
a server-wide global that could serve one user's output to another
(and was readable by anyone via a bare GET). Client no longer falls
back to "latest recording" on finalize timeout; it reports failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

app.py CHANGED
@@ -521,16 +521,14 @@ async def health():
521
 
522
 
523
  @app.get("/download_last")
524
- async def download_last():
525
- # Port of the local server's endpoint; the finalized dir comes from the fork
526
- # via the <record_dir>/.last marker (forks cannot set main-process state).
527
  if _RECORD_DIR is None:
528
  return JSONResponse({"error": "Recording is not enabled."}, status_code=404)
529
- marker = Path(_RECORD_DIR) / ".last"
530
- rec_dir = marker.read_text(encoding="utf-8").strip() if marker.is_file() else ""
531
- if not rec_dir:
532
- return JSONResponse({"error": "No downloadable result yet. Send an edit first."}, status_code=404)
533
- base = Path(rec_dir)
534
  segments = sorted(base.glob("output_*.mp4"))
535
  if not segments:
536
  return JSONResponse({"error": "Recording file has not been generated yet. Try again later."}, status_code=404)
 
521
 
522
 
523
  @app.get("/download_last")
524
+ async def download_last(rec: str = ""):
 
 
525
  if _RECORD_DIR is None:
526
  return JSONResponse({"error": "Recording is not enabled."}, status_code=404)
527
+ if not re.fullmatch(r"\d+_\d+", rec):
528
+ return JSONResponse({"error": "Missing or invalid rec id."}, status_code=400)
529
+ base = Path(_RECORD_DIR) / rec
530
+ if not base.is_dir():
531
+ return JSONResponse({"error": "No such recording."}, status_code=404)
532
  segments = sorted(base.glob("output_*.mp4"))
533
  if not segments:
534
  return JSONResponse({"error": "Recording file has not been generated yet. Try again later."}, status_code=404)
static/index.html CHANGED
@@ -2432,13 +2432,17 @@ function handleVideoEnded() {
2432
  updateMetrics();
2433
  if (ws && ws.readyState === WebSocket.OPEN && sessionGranted) {
2434
  finalizeIsForResult = true;
 
2435
  updateUploadProgress();
2436
  if (finalizeWatchdog) clearTimeout(finalizeWatchdog);
2437
  finalizeWatchdog = setTimeout(() => {
2438
  finalizeWatchdog = null;
2439
  if (!finalizeIsForResult) return;
2440
  finalizeIsForResult = false;
2441
- fetchAndShowResult();
 
 
 
2442
  }, 30000);
2443
  try { ws.send(JSON.stringify({ type: "finalize_recording" })); }
2444
  catch (err) {}
@@ -2488,7 +2492,7 @@ function setSourceSeg(src) {
2488
  if (segM) segM.addEventListener("click", () => { document.getElementById("videoFile").click(); });
2489
  }
2490
  const downloadBubble = document.getElementById("downloadBubble");
2491
- let downloadPending = false;
2492
  function showDownloadBubble(show) {
2493
  if (!downloadBubble) return;
2494
  if (show && !(SERVER_DEFAULTS && SERVER_DEFAULTS.record_enabled)) return;
@@ -2558,6 +2562,7 @@ function stopResultSync() {
2558
  function clearResultVideo() {
2559
  stopResultSync();
2560
  finalizeIsForResult = false;
 
2561
  resultEpoch += 1;
2562
  if (typeof showDownloadBubble === "function") showDownloadBubble(false);
2563
  if (outputResult) {
@@ -2584,29 +2589,13 @@ function triggerBrowserDownload(url, filename) {
2584
  a.click();
2585
  a.remove();
2586
  }
2587
- async function fetchAndSaveRecording() {
2588
- if (resultBlobUrl) {
2589
- triggerBrowserDownload(resultBlobUrl, "joyomni_output.mp4");
2590
- return;
2591
- }
2592
- try {
2593
- const resp = await fetch("/download_last", { cache: "no-store" });
2594
- if (!resp.ok) {
2595
- let detail = "";
2596
- try { const j = await resp.json(); detail = j.error || ""; } catch (e) {}
2597
- return;
2598
- }
2599
- const blob = await resp.blob();
2600
- const url = URL.createObjectURL(blob);
2601
- triggerBrowserDownload(url, "joyomni_output.mp4");
2602
- setTimeout(() => { try { URL.revokeObjectURL(url); } catch (e) {} }, 60000);
2603
- } catch (err) {
2604
- }
2605
  }
2606
  async function fetchAndShowResult() {
2607
  const myEpoch = resultEpoch;
2608
  try {
2609
- const resp = await fetch("/download_last", { cache: "no-store" });
2610
  if (myEpoch !== resultEpoch) return;
2611
  if (!resp.ok) {
2612
  let detail = "";
@@ -2628,8 +2617,8 @@ async function fetchAndShowResult() {
2628
  }
2629
  function onRecordingFinalized(msg) {
2630
  if (finalizeWatchdog) { clearTimeout(finalizeWatchdog); finalizeWatchdog = null; }
2631
- downloadPending = false;
2632
- if (msg && msg.ok === false) {
2633
  finalizeIsForResult = false;
2634
  stopResultSync();
2635
  if (outputWaitOverlay) outputWaitOverlay.style.display = "none";
 
2432
  updateMetrics();
2433
  if (ws && ws.readyState === WebSocket.OPEN && sessionGranted) {
2434
  finalizeIsForResult = true;
2435
+ lastRecId = null;
2436
  updateUploadProgress();
2437
  if (finalizeWatchdog) clearTimeout(finalizeWatchdog);
2438
  finalizeWatchdog = setTimeout(() => {
2439
  finalizeWatchdog = null;
2440
  if (!finalizeIsForResult) return;
2441
  finalizeIsForResult = false;
2442
+ stopResultSync();
2443
+ if (outputWaitOverlay) outputWaitOverlay.style.display = "none";
2444
+ showOutputIdle(true);
2445
+ setSendBusy(false, "");
2446
  }, 30000);
2447
  try { ws.send(JSON.stringify({ type: "finalize_recording" })); }
2448
  catch (err) {}
 
2492
  if (segM) segM.addEventListener("click", () => { document.getElementById("videoFile").click(); });
2493
  }
2494
  const downloadBubble = document.getElementById("downloadBubble");
2495
+ let lastRecId = null;
2496
  function showDownloadBubble(show) {
2497
  if (!downloadBubble) return;
2498
  if (show && !(SERVER_DEFAULTS && SERVER_DEFAULTS.record_enabled)) return;
 
2562
  function clearResultVideo() {
2563
  stopResultSync();
2564
  finalizeIsForResult = false;
2565
+ lastRecId = null;
2566
  resultEpoch += 1;
2567
  if (typeof showDownloadBubble === "function") showDownloadBubble(false);
2568
  if (outputResult) {
 
2589
  a.click();
2590
  a.remove();
2591
  }
2592
+ function fetchAndSaveRecording() {
2593
+ if (resultBlobUrl) triggerBrowserDownload(resultBlobUrl, "joyomni_output.mp4");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2594
  }
2595
  async function fetchAndShowResult() {
2596
  const myEpoch = resultEpoch;
2597
  try {
2598
+ const resp = await fetch("/download_last?rec=" + encodeURIComponent(lastRecId), { cache: "no-store" });
2599
  if (myEpoch !== resultEpoch) return;
2600
  if (!resp.ok) {
2601
  let detail = "";
 
2617
  }
2618
  function onRecordingFinalized(msg) {
2619
  if (finalizeWatchdog) { clearTimeout(finalizeWatchdog); finalizeWatchdog = null; }
2620
+ lastRecId = msg.rec || null;
2621
+ if (!lastRecId) {
2622
  finalizeIsForResult = false;
2623
  stopResultSync();
2624
  if (outputWaitOverlay) outputWaitOverlay.style.display = "none";
xvideo/serving/serve_joyomni_streaming.py CHANGED
@@ -8,6 +8,7 @@ import json
8
  import math
9
  import os
10
  import queue
 
11
  import sys
12
  import tempfile
13
  import threading
@@ -619,8 +620,6 @@ def create_app(args: argparse.Namespace) -> FastAPI:
619
 
620
  app.state.session_gate = SessionGate()
621
 
622
- app.state.last_recording_dir = None
623
-
624
  @app.get("/")
625
  def index() -> HTMLResponse:
626
  server_defaults = {
@@ -706,13 +705,14 @@ def create_app(args: argparse.Namespace) -> FastAPI:
706
  return JSONResponse({"ok": True, "elapsed": time.time() - started})
707
 
708
  @app.get("/download_last")
709
- async def download_last() -> Response:
710
  if args.record_dir is None:
711
  return JSONResponse({"error": "Recording is not enabled (--record-dir is unset)."}, status_code=404)
712
- rec_dir = getattr(app.state, "last_recording_dir", None)
713
- if not rec_dir:
714
- return JSONResponse({"error": "No downloadable result yet. Send an edit first."}, status_code=404)
715
- base = Path(rec_dir)
 
716
  segments = sorted(base.glob("output_*.mp4"))
717
  if not segments:
718
  return JSONResponse({"error": "Recording file has not been generated yet. Try again later."}, status_code=404)
@@ -1522,11 +1522,10 @@ def create_app(args: argparse.Namespace) -> FastAPI:
1522
  await _stop_output_task()
1523
  finalized = rec_base
1524
  await asyncio.to_thread(_stop_recorders)
1525
- if finalized is not None:
1526
- app.state.last_recording_dir = str(finalized)
1527
  await _send_json({
1528
  "type": "recording_finalized",
1529
  "ok": finalized is not None,
 
1530
  "message": None if finalized is not None
1531
  else "No downloadable result yet. Send an edit first.",
1532
  })
 
8
  import math
9
  import os
10
  import queue
11
+ import re
12
  import sys
13
  import tempfile
14
  import threading
 
620
 
621
  app.state.session_gate = SessionGate()
622
 
 
 
623
  @app.get("/")
624
  def index() -> HTMLResponse:
625
  server_defaults = {
 
705
  return JSONResponse({"ok": True, "elapsed": time.time() - started})
706
 
707
  @app.get("/download_last")
708
+ async def download_last(rec: str = "") -> Response:
709
  if args.record_dir is None:
710
  return JSONResponse({"error": "Recording is not enabled (--record-dir is unset)."}, status_code=404)
711
+ if not re.fullmatch(r"\d+_\d+", rec):
712
+ return JSONResponse({"error": "Missing or invalid rec id."}, status_code=400)
713
+ base = Path(args.record_dir) / rec
714
+ if not base.is_dir():
715
+ return JSONResponse({"error": "No such recording."}, status_code=404)
716
  segments = sorted(base.glob("output_*.mp4"))
717
  if not segments:
718
  return JSONResponse({"error": "Recording file has not been generated yet. Try again later."}, status_code=404)
 
1522
  await _stop_output_task()
1523
  finalized = rec_base
1524
  await asyncio.to_thread(_stop_recorders)
 
 
1525
  await _send_json({
1526
  "type": "recording_finalized",
1527
  "ok": finalized is not None,
1528
+ "rec": finalized.name if finalized is not None else None,
1529
  "message": None if finalized is not None
1530
  else "No downloadable result yet. Send an edit first.",
1531
  })
xvideo/serving/zerogpu_engine.py CHANGED
@@ -521,14 +521,8 @@ async def _session_loop(runtime, args, websocket: _QueueWebSocket) -> None:
521
  await _stop_output()
522
  finalized = rec_base
523
  await asyncio.to_thread(_stop_recorders)
524
- if finalized is not None:
525
- # Cross-process handoff: /download_last runs in the main web
526
- # process, which cannot see this fork's memory.
527
- try:
528
- (Path(args.record_dir) / ".last").write_text(str(finalized), encoding="utf-8")
529
- except Exception: # noqa: BLE001
530
- pass
531
  await _send_json({"type": "recording_finalized", "ok": finalized is not None,
 
532
  "message": None if finalized is not None
533
  else "No downloadable result yet. Send an edit first."})
534
  elif msg_type == "frame_meta":
 
521
  await _stop_output()
522
  finalized = rec_base
523
  await asyncio.to_thread(_stop_recorders)
 
 
 
 
 
 
 
524
  await _send_json({"type": "recording_finalized", "ok": finalized is not None,
525
+ "rec": finalized.name if finalized is not None else None,
526
  "message": None if finalized is not None
527
  else "No downloadable result yet. Send an edit first."})
528
  elif msg_type == "frame_meta":