Spaces:
Running on Zero
Running on Zero
Pass file URLs between pipeline stages instead of re-uploading blobs
Browse filesThe 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.
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) ->
|
| 50 |
-
"""Run GPU reconstruction and return the artifact
|
| 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=
|
| 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
|
| 65 |
|
| 66 |
|
| 67 |
@app.api(name="export_ply", queue=True, concurrency_limit=2)
|
| 68 |
-
def export_ply(
|
| 69 |
-
"""Filter and
|
| 70 |
started = time.perf_counter()
|
| 71 |
-
internal =
|
| 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
|
| 84 |
|
| 85 |
|
| 86 |
@app.api(name="export_viewer", queue=True, concurrency_limit=2)
|
| 87 |
-
def export_viewer(
|
| 88 |
-
"""Build the browser viewer and return its iframe-ready HTML."""
|
| 89 |
started = time.perf_counter()
|
|
|
|
| 90 |
exported = export_browser_viewer(
|
| 91 |
-
scene_ply=
|
| 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 |
-
|
| 100 |
)
|
| 101 |
-
return
|
| 102 |
|
| 103 |
|
| 104 |
@app.api(name="export_html", queue=True, concurrency_limit=2)
|
| 105 |
-
def export_html(
|
| 106 |
-
"""Bundle a standalone HTML viewer
|
| 107 |
started = time.perf_counter()
|
|
|
|
| 108 |
standalone = export_standalone_viewer(
|
| 109 |
-
viewer_html=
|
| 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
|
| 119 |
|
| 120 |
|
| 121 |
@app.api(name="viewer_html", queue=False)
|
| 122 |
-
def viewer_html() ->
|
| 123 |
"""Serve the preloaded viewer template HTML for fast first paint."""
|
| 124 |
-
return
|
| 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", {
|
| 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", {
|
| 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");
|