techfreakworm commited on
Commit
e8627ee
·
unverified ·
1 Parent(s): ebc9cea

fix(backend): compute video path inside @spaces.GPU + filesystem fallback

Browse files

executor.history_result wasn't surviving ZeroGPU's GPU-context wrap
reliably — sampler completed and files were written to disk, but the
parent process saw an empty history_result, so the OutputEvent's
video_path was "" and the gr.Video output stayed blank.

Two changes:
1. _execute_workflow now reads history_result and returns the video path
inline (a plain str pickles cleanly across the boundary).
2. If that still returns "", _newest_recent_video scans the output dir
for the newest mp4/webm/mov modified in the last 60 s — files are on
shared disk so they're visible from the parent regardless of how
ZeroGPU isolates state.

Also adds a "[backend] workflow done; video_path=..." log line so the
next time this hiccups we see where the chain breaks.

Files changed (1) hide show
  1. backend.py +63 -19
backend.py CHANGED
@@ -71,14 +71,35 @@ _GPU = spaces.GPU(duration=300) if (spaces is not None and _on_spaces()) else _i
71
 
72
 
73
  @_GPU
74
- def _execute_workflow(executor: Any, workflow: dict, output_ids: list[str]) -> dict:
 
 
 
 
 
 
 
 
75
  executor.execute(
76
  workflow,
77
  prompt_id="ltx23-aio",
78
  extra_data={"client_id": "ltx23-aio"},
79
  execute_outputs=output_ids,
80
  )
81
- return getattr(executor, "history_result", {}) or {}
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
 
84
  class _StubServer:
@@ -340,10 +361,20 @@ class ComfyUILibraryBackend:
340
  # _execute_workflow is module-level and decorated with
341
  # @spaces.GPU(duration=300) on Spaces — that's what makes the
342
  # heavy compute run on a borrowed H200. Off-Spaces it's a
343
- # plain call.
344
- hist = _execute_workflow(self._executor, workflow, output_ids)
345
- outs = hist.get("outputs") or {}
346
- video_path = _first_video_path(list(outs.values())) or ""
 
 
 
 
 
 
 
 
 
 
347
  _push(OutputEvent(video_path=video_path))
348
  except Exception as exc:
349
  tb_text = tb_mod.format_exc()
@@ -412,16 +443,29 @@ def _free_memory() -> None:
412
  pass
413
 
414
 
415
- def _first_video_path(outputs: Iterable) -> str | None:
416
- """Find the first .mp4 path emitted by VHS_VideoCombine in PromptExecutor outputs."""
417
- for output in outputs:
418
- if not isinstance(output, dict):
419
- continue
420
- for value in output.values():
421
- if isinstance(value, list):
422
- for item in value:
423
- if isinstance(item, dict) and "filename" in item:
424
- fn = item["filename"]
425
- if fn.endswith((".mp4", ".webm", ".mov")):
426
- return item.get("fullpath", fn)
427
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  @_GPU
74
+ def _execute_workflow(executor: Any, workflow: dict, output_ids: list[str]) -> str:
75
+ """Run the workflow on GPU and return the path of the first video output.
76
+
77
+ Returns just the video path (a plain string, picklable across the
78
+ @spaces.GPU subprocess boundary). Returning the full history_result dict
79
+ was unreliable on Spaces — under ZeroGPU's GPU-context wrapping, the
80
+ parent process didn't see the executor's mutated state, so video_path
81
+ came back empty even when the file was on disk.
82
+ """
83
  executor.execute(
84
  workflow,
85
  prompt_id="ltx23-aio",
86
  extra_data={"client_id": "ltx23-aio"},
87
  execute_outputs=output_ids,
88
  )
89
+ hist = getattr(executor, "history_result", {}) or {}
90
+ outs = hist.get("outputs") or {}
91
+ for output in outs.values():
92
+ if not isinstance(output, dict):
93
+ continue
94
+ for value in output.values():
95
+ if not isinstance(value, list):
96
+ continue
97
+ for item in value:
98
+ if isinstance(item, dict):
99
+ fn = item.get("filename") or ""
100
+ if fn.endswith((".mp4", ".webm", ".mov")):
101
+ return item.get("fullpath") or fn
102
+ return ""
103
 
104
 
105
  class _StubServer:
 
361
  # _execute_workflow is module-level and decorated with
362
  # @spaces.GPU(duration=300) on Spaces — that's what makes the
363
  # heavy compute run on a borrowed H200. Off-Spaces it's a
364
+ # plain call. Returns the video path directly (computed
365
+ # inside the GPU context so the executor's history is fresh).
366
+ video_path = _execute_workflow(self._executor, workflow, output_ids)
367
+ # Fallback: if history_result didn't surface a path (rare on
368
+ # Spaces — happens when ZeroGPU's subprocess boundary drops
369
+ # mutated state), scan the output dir for the newest mp4
370
+ # written within the last 60 s.
371
+ if not video_path:
372
+ video_path = _newest_recent_video(self._comfy_dir / "output") or ""
373
+ print(
374
+ f"[backend] workflow done; video_path={video_path!r}",
375
+ file=sys.stderr,
376
+ flush=True,
377
+ )
378
  _push(OutputEvent(video_path=video_path))
379
  except Exception as exc:
380
  tb_text = tb_mod.format_exc()
 
443
  pass
444
 
445
 
446
+ def _newest_recent_video(output_root: pathlib.Path, within_seconds: float = 60.0) -> str | None:
447
+ """Filesystem fallback: return the newest .mp4/.webm/.mov under *output_root*
448
+ that was modified within the last *within_seconds* seconds.
449
+
450
+ Used when the executor's history_result didn't surface a path — typically
451
+ happens when ZeroGPU's subprocess boundary drops the mutation. The disk
452
+ is shared, so the file is there even when the in-memory state isn't.
453
+ """
454
+ import time
455
+
456
+ if not output_root.exists():
457
+ return None
458
+ cutoff = time.time() - within_seconds
459
+ candidates: list[tuple[float, pathlib.Path]] = []
460
+ for ext in (".mp4", ".webm", ".mov"):
461
+ for p in output_root.rglob(f"*{ext}"):
462
+ try:
463
+ mtime = p.stat().st_mtime
464
+ except OSError:
465
+ continue
466
+ if mtime >= cutoff:
467
+ candidates.append((mtime, p))
468
+ if not candidates:
469
+ return None
470
+ candidates.sort(reverse=True)
471
+ return str(candidates[0][1])