Operators (16 total):
- fal_hunyuan3d_v21: Hunyuan3D 2.1 single-image (live-verified dash variant,
~90s; v21 multi-view is broken on fal — v2 stays the multi-view path)
- fal_bg_remove (BiRefNet v2): subject cutout pre-pass — the biggest quality
lever before image->3D
- fal_upscale (SeedVR faithful upscale), fal_image_edit (nano-banana prompt
edits), fal_text_image (Ideogram v3 — first no-input operator)
- fal_common: collect='images' mode; recursive URL extractor now takes the
wanted extension set
UI: operator dropdown grouped by category (optgroup); operators with
accepts: [] run without a selected asset ('No input asset needed' note);
run-button/launch logic updated accordingly.
Review fixes (Opus Phase 1 commit):
- runner._run_lane crashed (TypeError) when a queued job was deleted before
the worker picked it up
- PUT /api/settings treated empty string as a masked placeholder, making
secrets impossible to clear from the UI
- store.register_file mutated the caller's meta dict via pop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""Shared helper for fal.ai operators: upload input image, call an endpoint with
|
|
streamed progress, and download every 3D file found in the result.
|
|
|
|
Runs in the server's own venv (fal-client is dependency-light). Kept generic so
|
|
operators only supply endpoint id + argument mapping; unknown/new fal args pass
|
|
through via an `extra_args` JSON param so the UI keeps full control."""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
MODEL_EXTS = {".glb", ".gltf", ".obj", ".fbx", ".usdz", ".ply", ".stl", ".zip"}
|
|
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp"}
|
|
|
|
|
|
def _log(msg):
|
|
print(msg, flush=True)
|
|
|
|
|
|
def _find_file_urls(obj, acc, wanted_exts):
|
|
"""Recursively collect (url, ext) for any file-like url in the result JSON."""
|
|
if isinstance(obj, dict):
|
|
url = obj.get("url")
|
|
if isinstance(url, str):
|
|
ext = Path(url.split("?")[0]).suffix.lower()
|
|
if ext in wanted_exts:
|
|
acc.append((url, ext, obj.get("file_name")))
|
|
for v in obj.values():
|
|
_find_file_urls(v, acc, wanted_exts)
|
|
elif isinstance(obj, list):
|
|
for v in obj:
|
|
_find_file_urls(v, acc, wanted_exts)
|
|
|
|
|
|
def parse_args():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--input", action="append", default=[])
|
|
ap.add_argument("--outdir", required=True)
|
|
ap.add_argument("--params", default="{}")
|
|
a = ap.parse_args()
|
|
return a, json.loads(a.params)
|
|
|
|
|
|
def run(endpoint, arguments, outdir, image_path=None, image_arg="image_url",
|
|
image_as_list=False, extra_args=None, out_stem="model", collect="models"):
|
|
"""collect: 'models' (default) downloads 3D files from the result;
|
|
'images' downloads image files (for cutout/upscale/edit/gen endpoints)."""
|
|
import fal_client
|
|
|
|
if image_path:
|
|
_log(f"uploading {Path(image_path).name} to fal ...")
|
|
url = fal_client.upload_file(image_path)
|
|
arguments[image_arg] = [url] if image_as_list else url
|
|
|
|
if extra_args:
|
|
try:
|
|
arguments.update(json.loads(extra_args) if isinstance(extra_args, str) else extra_args)
|
|
except ValueError as e:
|
|
_log(f"warning: could not parse extra_args ({e}); ignoring")
|
|
|
|
# drop None values so we send only what the user set
|
|
arguments = {k: v for k, v in arguments.items() if v is not None and v != ""}
|
|
_log(f"calling {endpoint}")
|
|
_log(f"arguments: {json.dumps({k: v for k, v in arguments.items() if k != image_arg})}")
|
|
|
|
def on_update(update):
|
|
for entry in getattr(update, "logs", None) or []:
|
|
msg = entry.get("message") if isinstance(entry, dict) else str(entry)
|
|
if msg:
|
|
_log(f" {msg}")
|
|
|
|
result = fal_client.subscribe(endpoint, arguments=arguments, with_logs=True,
|
|
on_queue_update=on_update)
|
|
|
|
outdir = Path(outdir)
|
|
(outdir / "fal_result.json").write_text(json.dumps(result, indent=2))
|
|
|
|
wanted = IMAGE_EXTS if collect == "images" else MODEL_EXTS
|
|
urls = []
|
|
_find_file_urls(result, urls, wanted)
|
|
if not urls:
|
|
_log(f"ERROR: no {collect} file url found in fal result. Raw result saved to fal_result.json:")
|
|
_log(json.dumps(result, indent=2)[:2000])
|
|
sys.exit(1)
|
|
|
|
outputs = []
|
|
for i, (url, ext, fname) in enumerate(urls):
|
|
name = fname or (f"{out_stem}{ext}" if i == 0 else f"{out_stem}_{i}{ext}")
|
|
dest = outdir / name
|
|
_log(f"downloading {name} ...")
|
|
urllib.request.urlretrieve(url, dest)
|
|
outputs.append({"path": name, "meta": {"endpoint": endpoint, "source_url": url}})
|
|
|
|
(outdir / "result.json").write_text(json.dumps({"outputs": outputs}))
|
|
_log(f"done — {len(outputs)} file(s)")
|