akhaliq HF Staff commited on
Commit
3b8c22c
·
1 Parent(s): c386668

Pass file URLs between pipeline stages instead of re-uploading blobs

Browse files

The JS client was re-fetching each stage's output, wrapping it as a File,
and re-uploading it to the next endpoint. The server-side preprocess then
tried to move the resulting /gradio_api/file=... URL into its cache
directory, which fails because that path doesn't exist on disk —
hence the KeyError: 'path' on the second call.

Switch the downstream endpoints (export_ply, export_viewer, export_html,
viewer_html) to take plain string URLs and return {"url": ...} dicts.
The JS client now passes each result's URL straight into the next call
via plain string parameters, no handle_file() wrapping, no re-upload.
reconstruct still uses FileData because it needs the user-uploaded bytes.

Files changed (1) hide show
  1. app.py +51 -29
app.py CHANGED
@@ -14,6 +14,8 @@ from fastapi.responses import HTMLResponse
14
  from gradio import Server
15
  from gradio.data_classes import FileData
16
 
 
 
17
  from src.demo.hf_runtime import (
18
  InfiniSplatRuntime,
19
  ViewerTemplate,
@@ -44,15 +46,40 @@ def _log(stage: str, **metrics) -> None:
44
  print(f"INFINISPLAT_TIMING {json.dumps({'stage': stage, **metrics}, sort_keys=True)}", flush=True)
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  @app.api(name="reconstruct", queue=True, concurrency_limit=1, concurrency_id="gpu")
48
  @spaces.GPU(duration=GPU_DURATION_SECONDS)
49
- def reconstruct(image_path: FileData) -> FileData:
50
- """Run GPU reconstruction and return the artifact path."""
51
  started = time.perf_counter()
52
  request_dir = OUTPUT_ROOT / uuid.uuid4().hex
53
  request_dir.mkdir(parents=True, exist_ok=True)
54
  artifact = runtime.infer_to_artifact(
55
- image_path=Path(image_path["path"]),
56
  artifact_path=request_dir / "gaussians.pt",
57
  )
58
  _log(
@@ -61,14 +88,14 @@ def reconstruct(image_path: FileData) -> FileData:
61
  seconds=round(time.perf_counter() - started, 3),
62
  bytes=artifact.stat().st_size,
63
  )
64
- return FileData(path=str(artifact))
65
 
66
 
67
  @app.api(name="export_ply", queue=True, concurrency_limit=2)
68
- def export_ply(artifact: FileData) -> FileData:
69
- """Filter and expose a PLY artifact."""
70
  started = time.perf_counter()
71
- internal = Path(artifact["path"])
72
  scene_ply = export_filtered_gaussian_ply(
73
  artifact_path=internal,
74
  output_dir=internal.parent,
@@ -80,15 +107,16 @@ def export_ply(artifact: FileData) -> FileData:
80
  seconds=round(time.perf_counter() - started, 3),
81
  bytes=scene_ply.stat().st_size,
82
  )
83
- return FileData(path=str(scene_ply))
84
 
85
 
86
  @app.api(name="export_viewer", queue=True, concurrency_limit=2)
87
- def export_viewer(scene_ply: FileData) -> FileData:
88
- """Build the browser viewer and return its iframe-ready HTML."""
89
  started = time.perf_counter()
 
90
  exported = export_browser_viewer(
91
- scene_ply=Path(scene_ply["path"]),
92
  viewer_template=viewer_template,
93
  )
94
  _log(
@@ -96,17 +124,18 @@ def export_viewer(scene_ply: FileData) -> FileData:
96
  request=exported.viewer_html.parent.name,
97
  seconds=round(time.perf_counter() - started, 3),
98
  sog_bytes=exported.scene_sog.stat().st_size,
99
- html_bytes=exported.viewer_html.stat().st_size,
100
  )
101
- return FileData(path=str(exported.viewer_html))
102
 
103
 
104
  @app.api(name="export_html", queue=True, concurrency_limit=2)
105
- def export_html(viewer_html: FileData) -> FileData:
106
- """Bundle a standalone HTML viewer for download."""
107
  started = time.perf_counter()
 
108
  standalone = export_standalone_viewer(
109
- viewer_html=Path(viewer_html["path"]),
110
  viewer_template=viewer_template,
111
  )
112
  _log(
@@ -115,13 +144,13 @@ def export_html(viewer_html: FileData) -> FileData:
115
  seconds=round(time.perf_counter() - started, 3),
116
  bytes=standalone.stat().st_size,
117
  )
118
- return FileData(path=str(standalone))
119
 
120
 
121
  @app.api(name="viewer_html", queue=False)
122
- def viewer_html() -> FileData:
123
  """Serve the preloaded viewer template HTML for fast first paint."""
124
- return FileData(path=str(viewer_template.viewer_html))
125
 
126
 
127
  INDEX_HTML = r"""<!doctype html>
@@ -703,18 +732,14 @@ INDEX_HTML = r"""<!doctype html>
703
  setViewerState("loading", { title: "Reconstructing scene", detail: "Running model inference", progress: 15 });
704
  const recon = await client.predict("/reconstruct", { image_path: handle_file(pendingFile) });
705
  const artifactUrl = recon.data[0].url;
706
- const artifactResp = await fetch(artifactUrl);
707
- const artifactBlob = await artifactResp.blob();
708
- const artifactFile = new File([artifactBlob], "gaussians.pt");
709
 
710
  setViewerState("loading", { title: "Preparing PLY", detail: "Filtering Gaussians", progress: 45 });
711
- const ply = await client.predict("/export_ply", { artifact: handle_file(artifactFile) });
712
  const plyUrl = ply.data[0].url;
713
- lastPlyUrl = plyUrl;
714
  setDownload(dlPly, plyUrl, "Download PLY — Ready");
715
 
716
  setViewerState("loading", { title: "Encoding viewer", detail: "Building WebGL scene", progress: 70 });
717
- const view = await client.predict("/export_viewer", { scene_ply: handle_file(await (await fetch(plyUrl)).blob()) });
718
  const viewerUrl = view.data[0].url + "?v=" + Date.now();
719
  viewerFrame.src = viewerUrl;
720
  viewerFrame.onload = () => {
@@ -723,11 +748,8 @@ INDEX_HTML = r"""<!doctype html>
723
  };
724
 
725
  setViewerState("loading", { title: "Bundling HTML", detail: "Embedding assets for download", progress: 92 });
726
- const html = await client.predict("/export_html", {
727
- viewer_html: handle_file(await (await fetch(viewerUrl)).blob())
728
- });
729
  const htmlUrl = html.data[0].url;
730
- lastHtmlUrl = htmlUrl;
731
  setDownload(dlHtml, htmlUrl, "Download HTML viewer — Ready");
732
 
733
  setViewerState("idle");
 
14
  from gradio import Server
15
  from gradio.data_classes import FileData
16
 
17
+ from urllib.parse import quote, urlparse
18
+
19
  from src.demo.hf_runtime import (
20
  InfiniSplatRuntime,
21
  ViewerTemplate,
 
46
  print(f"INFINISPLAT_TIMING {json.dumps({'stage': stage, **metrics}, sort_keys=True)}", flush=True)
47
 
48
 
49
+ def _file_url(path: Path) -> str:
50
+ """Return the public Gradio file URL for a server-side path."""
51
+ return f"/gradio_api/file={quote(str(path.resolve()))}"
52
+
53
+
54
+ def _resolve_file_path(value: FileData | str) -> Path:
55
+ """Resolve a FileData input (or URL/path string) back to a local file path."""
56
+ if isinstance(value, str):
57
+ path_str = value
58
+ elif isinstance(value, dict):
59
+ path_str = value.get("path") or value.get("url") or ""
60
+ else:
61
+ path_str = str(value)
62
+ if not path_str:
63
+ raise ValueError(f"Cannot resolve file path from input: {value!r}")
64
+ # Strip Gradio's /gradio_api/file= URL prefix to get the real path
65
+ if path_str.startswith("/gradio_api/file="):
66
+ decoded = urlparse(path_str).path[len("/gradio_api/file="):]
67
+ return Path(decoded)
68
+ # Already a server-local path
69
+ if path_str.startswith("/"):
70
+ return Path(path_str)
71
+ return Path(path_str)
72
+
73
+
74
  @app.api(name="reconstruct", queue=True, concurrency_limit=1, concurrency_id="gpu")
75
  @spaces.GPU(duration=GPU_DURATION_SECONDS)
76
+ def reconstruct(image_path: FileData) -> dict:
77
+ """Run GPU reconstruction and return a public URL for the artifact."""
78
  started = time.perf_counter()
79
  request_dir = OUTPUT_ROOT / uuid.uuid4().hex
80
  request_dir.mkdir(parents=True, exist_ok=True)
81
  artifact = runtime.infer_to_artifact(
82
+ image_path=_resolve_file_path(image_path),
83
  artifact_path=request_dir / "gaussians.pt",
84
  )
85
  _log(
 
88
  seconds=round(time.perf_counter() - started, 3),
89
  bytes=artifact.stat().st_size,
90
  )
91
+ return {"url": _file_url(artifact)}
92
 
93
 
94
  @app.api(name="export_ply", queue=True, concurrency_limit=2)
95
+ def export_ply(artifact_url: str) -> dict:
96
+ """Filter one PLY artifact and return its public URL."""
97
  started = time.perf_counter()
98
+ internal = _resolve_file_path(artifact_url)
99
  scene_ply = export_filtered_gaussian_ply(
100
  artifact_path=internal,
101
  output_dir=internal.parent,
 
107
  seconds=round(time.perf_counter() - started, 3),
108
  bytes=scene_ply.stat().st_size,
109
  )
110
+ return {"url": _file_url(scene_ply)}
111
 
112
 
113
  @app.api(name="export_viewer", queue=True, concurrency_limit=2)
114
+ def export_viewer(scene_ply_url: str) -> dict:
115
+ """Build the browser viewer and return its iframe-ready HTML URL."""
116
  started = time.perf_counter()
117
+ scene_ply_path = _resolve_file_path(scene_ply_url)
118
  exported = export_browser_viewer(
119
+ scene_ply=scene_ply_path,
120
  viewer_template=viewer_template,
121
  )
122
  _log(
 
124
  request=exported.viewer_html.parent.name,
125
  seconds=round(time.perf_counter() - started, 3),
126
  sog_bytes=exported.scene_sog.stat().st_size,
127
+ viewer_html_bytes=exported.viewer_html.stat().st_size,
128
  )
129
+ return {"url": _file_url(exported.viewer_html)}
130
 
131
 
132
  @app.api(name="export_html", queue=True, concurrency_limit=2)
133
+ def export_html(viewer_html_url: str) -> dict:
134
+ """Bundle a standalone HTML viewer and return its public URL."""
135
  started = time.perf_counter()
136
+ viewer_html_path = _resolve_file_path(viewer_html_url)
137
  standalone = export_standalone_viewer(
138
+ viewer_html=viewer_html_path,
139
  viewer_template=viewer_template,
140
  )
141
  _log(
 
144
  seconds=round(time.perf_counter() - started, 3),
145
  bytes=standalone.stat().st_size,
146
  )
147
+ return {"url": _file_url(standalone)}
148
 
149
 
150
  @app.api(name="viewer_html", queue=False)
151
+ def viewer_html() -> dict:
152
  """Serve the preloaded viewer template HTML for fast first paint."""
153
+ return {"url": _file_url(viewer_template.viewer_html)}
154
 
155
 
156
  INDEX_HTML = r"""<!doctype html>
 
732
  setViewerState("loading", { title: "Reconstructing scene", detail: "Running model inference", progress: 15 });
733
  const recon = await client.predict("/reconstruct", { image_path: handle_file(pendingFile) });
734
  const artifactUrl = recon.data[0].url;
 
 
 
735
 
736
  setViewerState("loading", { title: "Preparing PLY", detail: "Filtering Gaussians", progress: 45 });
737
+ const ply = await client.predict("/export_ply", { artifact_url: artifactUrl });
738
  const plyUrl = ply.data[0].url;
 
739
  setDownload(dlPly, plyUrl, "Download PLY — Ready");
740
 
741
  setViewerState("loading", { title: "Encoding viewer", detail: "Building WebGL scene", progress: 70 });
742
+ const view = await client.predict("/export_viewer", { scene_ply_url: plyUrl });
743
  const viewerUrl = view.data[0].url + "?v=" + Date.now();
744
  viewerFrame.src = viewerUrl;
745
  viewerFrame.onload = () => {
 
748
  };
749
 
750
  setViewerState("loading", { title: "Bundling HTML", detail: "Embedding assets for download", progress: 92 });
751
+ const html = await client.predict("/export_html", { viewer_html_url: viewerUrl.replace(/[?#].*/, "") });
 
 
752
  const htmlUrl = html.data[0].url;
 
753
  setDownload(dlHtml, htmlUrl, "Download HTML viewer — Ready");
754
 
755
  setViewerState("idle");