uniforge3d / agent_api.py
Mike0021's picture
Presets: measured end-to-end times in the API docs and the UI
e915ba1 verified
Raw History Blame Contribute Delete
6.12 kB
"""What the API documents for agents: /gradio_api/info, which the Space's agents.md points to.
- texture_size: Gradio's clients render every enum as strings (python_type Literal['1024', '2048'],
"2048" in the code snippets), so the documented choices are the strings "1024" and "2048".
The numbers 1024 and 2048 are accepted too.
- Endpoint descriptions: parameters, timing, auth, and where the GLB is in the result.
- Code snippets: the bash one is generated with the public host (Gradio uses the host of the
first /info request, which on Spaces is often http://localhost:7860), sends the HF token, and
keeps EVENT_ID in bash (Gradio's `| read EVENT_ID` runs in a subshell there, so it is empty).
"""
import os
import re
import gradio as gr
import gradio.routes
TEXTURE_SIZES = ("1024", "2048")
class TextureSize(gr.Radio):
"""The texture size radio (create it with choices TEXTURE_SIZES); passes the size to the
function as an int. No __init__ of its own: Gradio builds a component's config from its
constructor's parameters."""
is_template = True # rendered by the frontend as a gr.Radio
def preprocess(self, payload):
if payload is None:
payload = self.value
if isinstance(payload, (int, float)) and not isinstance(payload, bool) and float(payload).is_integer():
payload = str(int(payload))
elif isinstance(payload, str):
payload = payload.strip()
if payload not in TEXTURE_SIZES:
raise gr.Error(f'texture_size must be "1024" or "2048" (got {payload!r}).')
return int(super().preprocess(payload))
def api_info(self):
return {
**super().api_info(),
"description": 'texture resolution in pixels: "1024" or "2048" (the numbers 1024 and 2048 work too)',
}
# name: (steps, faces, texture_size, end-to-end time). Times measured on the Space (2026-10-16) for
# a rocking chair, a bonsai and an espresso machine; detailed objects take the longer times.
PRESETS = {
"Fast": (8, 100_000, "1024", "1-3 min"),
"Balanced": (12, 300_000, "2048", "1.5-4 min"),
"Quality": (20, 300_000, "2048", "1.5-5.5 min"),
}
_COMMON = (
"faces: 100000-500000, default 300000. "
'texture_size: "1024" or "2048", default "2048". '
"steps: 8-20, default 12 (20 is the paper setting, slower). "
"Presets, each verified end to end: "
+ "; ".join(
f'{name.lower()}: steps {steps}, faces {faces}, texture_size "{texture}" ({time})'
for name, (steps, faces, texture, time) in PRESETS.items()
)
+ ". Balanced is the default; detailed objects take the longer times. "
"Runs use the caller's ZeroGPU quota, so send your "
"Hugging Face token (Authorization: Bearer $HF_TOKEN). "
"Poll GET /gradio_api/call/{endpoint}/{event_id}: it streams `generating` updates for the web page, then "
"`event: complete` with data = [progress HTML, 3D viewer state, GLB file, seed used]. "
"Download the model from data[2].url. A failure ends the stream with `event: error` and a message."
)
TEXT_API = (
"Text to a textured 3D model (GLB): Krea 2 draws the object, Qwen-Image-Edit-2511 generates its "
"right, back and left views, and UniForge3D builds the model from all four. "
'subject (required): the object, e.g. "a red vintage rotary telephone". '
"seed: integer, used only when randomize is false. randomize: default true. " + _COMMON
)
IMAGE_API = (
"One image of a single object to a textured 3D model (GLB): the background is removed "
"automatically, Qwen-Image-Edit-2511 generates the right, back and left views, and UniForge3D "
"builds the model from all four. "
"image (required): a photo or render of the object seen from the front (PNG, JPEG or WebP). "
"Upload it with POST /gradio_api/upload -F files=@object.jpg and pass "
'{"path": "<returned path>", "meta": {"_type": "gradio.FileData"}}, or pass a public image URL '
'as {"path": "https://...", "meta": {"_type": "gradio.FileData"}}. '
"seed: integer, default 42, used only when randomize is false. randomize: default false. " + _COMMON
)
AUTH = '-H "Authorization: Bearer $HF_TOKEN"'
_BASH_CALL = re.compile(
r'curl -X POST (?P<post>\S+) -s -H "Content-Type: application/json" \\\n'
r"(?P<data> -d '.*') \\\n"
r" \| awk -F'\"' '\{ print \$4\}' \\\n"
r" \| read EVENT_ID; curl -N (?P<get>\S+)$"
)
def _fix_bash(snippet):
match = _BASH_CALL.search(snippet)
if not match: # a Gradio version with another template: keep it
return snippet
call = (
f'EVENT_ID=$(curl -s -X POST {match["post"]} -H "Content-Type: application/json" {AUTH} \\\n'
f'{match["data"]} \\\n'
" | awk -F'\"' '{ print $4}')\n"
f'curl -sS -N {AUTH} {match["get"]}'
)
snippet = snippet[: match.start()] + call
return snippet.replace("FILE_PATH=$(curl -s -X POST ", f"FILE_PATH=$(curl -s {AUTH} -X POST ", 1)
def document_only(blocks, endpoints):
"""Keep the API docs to `endpoints`. While gr.Examples caches, Gradio registers a public
endpoint named after the example function, and /info is computed once, on the first request,
so whether such endpoints were documented depended on timing."""
get_api_info = blocks.get_api_info
def api_info(all_endpoints=False):
info = get_api_info(all_endpoints)
if not all_endpoints:
info["named_endpoints"] = {k: v for k, v in info["named_endpoints"].items() if k in endpoints}
info["unnamed_endpoints"] = {}
return info
blocks.get_api_info = api_info
_generate_code_snippets = gradio.routes.generate_code_snippets
def _code_snippets(api_name, endpoint_info, root, space_id=None, api_prefix="/"):
host = os.environ.get("SPACE_HOST")
if host:
root = f"https://{host}"
snippets = _generate_code_snippets(api_name, endpoint_info, root, space_id=space_id, api_prefix=api_prefix)
snippets["bash"] = _fix_bash(snippets["bash"])
return snippets
gradio.routes.generate_code_snippets = _code_snippets