fix(db): fix Trellis connector for updated Gradio API
- Add session_hash to all API calls (maintains gr.State between image_to_3d and extract_glb) - Pass is_multiimage=False at position 2 (9-param image_to_3d) - Pass output_buf=None at position 0 (3-param extract_glb) - Document full API parameter reference and failure modes - Add trellis-batch.sh for gentle sequential batch generation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,32 @@ Pipeline: upload image → start session → image_to_3d → extract_glb → dow
|
|||||||
Usage:
|
Usage:
|
||||||
python3 trellis_connector.py health
|
python3 trellis_connector.py health
|
||||||
python3 trellis_connector.py generate image.png [--output model.glb] [--simplify 0.95] [--texture-size 1024] [--seed 42] [--timeout 600]
|
python3 trellis_connector.py generate image.png [--output model.glb] [--simplify 0.95] [--texture-size 1024] [--seed 42] [--timeout 600]
|
||||||
|
|
||||||
|
Gradio API Parameter Reference (TRELLIS v1, microsoft/TRELLIS):
|
||||||
|
|
||||||
|
/image_to_3d — 9 inputs:
|
||||||
|
0: image (Image) preprocessed image from /preprocess_image_1
|
||||||
|
1: multiimages (Gallery) [] for single-image mode
|
||||||
|
2: is_multiimage (State) False for single-image, True for multi-image
|
||||||
|
3: seed (Slider) int, 0-2147483647
|
||||||
|
4: ss_guidance (Slider) float, sparse structure guidance strength (default 7.5)
|
||||||
|
5: ss_steps (Slider) int, sparse structure sampling steps (default 12)
|
||||||
|
6: slat_guidance (Slider) float, structured latent guidance strength (default 3.0)
|
||||||
|
7: slat_steps (Slider) int, structured latent sampling steps (default 12)
|
||||||
|
8: multiimage_algo (Radio) "stochastic" or "multidiffusion"
|
||||||
|
|
||||||
|
/extract_glb — 3 inputs:
|
||||||
|
0: output_buf (State) None — server uses internal state from image_to_3d
|
||||||
|
1: simplify (Slider) float, mesh simplification ratio (default 0.95)
|
||||||
|
2: texture_size (Slider) int, texture resolution (default 1024)
|
||||||
|
|
||||||
|
Common failure modes:
|
||||||
|
- "needed 9, got 8": missing is_multiimage (position 2) — must pass False
|
||||||
|
- "needed 3, got 2": missing output_buf (position 0) — must pass None
|
||||||
|
- "'float' cannot be interpreted as int": numpy version issue on server,
|
||||||
|
Gradio Sliders send all values as float. Fix: patch flow_euler.py on
|
||||||
|
the server to cast steps to int, or pin numpy < 2.0
|
||||||
|
- CUDA device mismatch after crash: restart the container to clear GPU state
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
@@ -55,12 +81,20 @@ def health():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def _call_api(base, endpoint, data, timeout=600):
|
def _call_api(base, endpoint, data, timeout=600, session_hash=None):
|
||||||
"""Call a Gradio API endpoint. Tries sync /api/ first, falls back to SSE /gradio_api/call/."""
|
"""Call a Gradio API endpoint with optional session tracking.
|
||||||
# Trellis uses the sync /api/ pattern
|
|
||||||
|
Gradio gr.State components are stored per session_hash on the server.
|
||||||
|
All calls in a pipeline (image_to_3d → extract_glb) must share the same
|
||||||
|
session_hash so the server can pass state between them.
|
||||||
|
"""
|
||||||
api_url = f"{base}/api{endpoint}"
|
api_url = f"{base}/api{endpoint}"
|
||||||
|
|
||||||
payload = json.dumps({"data": data})
|
body_dict = {"data": data}
|
||||||
|
if session_hash:
|
||||||
|
body_dict["session_hash"] = session_hash
|
||||||
|
|
||||||
|
payload = json.dumps(body_dict)
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
api_url,
|
api_url,
|
||||||
data=payload.encode(),
|
data=payload.encode(),
|
||||||
@@ -73,7 +107,6 @@ def _call_api(base, endpoint, data, timeout=600):
|
|||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
result = json.loads(resp.read())
|
result = json.loads(resp.read())
|
||||||
# Sync Gradio returns {"data": [...], "is_generating": false, ...}
|
|
||||||
if isinstance(result, dict) and "data" in result:
|
if isinstance(result, dict) and "data" in result:
|
||||||
return result["data"]
|
return result["data"]
|
||||||
return result
|
return result
|
||||||
@@ -179,9 +212,16 @@ def generate(image_path, output=None, simplify=0.95, texture_size=1024,
|
|||||||
name = os.path.splitext(os.path.basename(image_path))[0]
|
name = os.path.splitext(os.path.basename(image_path))[0]
|
||||||
output = f"{name}.glb"
|
output = f"{name}.glb"
|
||||||
|
|
||||||
|
# Generate a session hash — Gradio uses this to maintain gr.State between
|
||||||
|
# separate API calls. Without it, image_to_3d's output state is lost before
|
||||||
|
# extract_glb can read it.
|
||||||
|
import random, string
|
||||||
|
session = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
||||||
|
print(f" Session: {session}", file=sys.stderr)
|
||||||
|
|
||||||
# Step 1: Start session
|
# Step 1: Start session
|
||||||
print("Step 1/5: Starting session...", file=sys.stderr)
|
print("Step 1/5: Starting session...", file=sys.stderr)
|
||||||
_call_api(base, "/start_session", [], timeout=30)
|
session_result = _call_api(base, "/start_session", [], timeout=30, session_hash=session)
|
||||||
|
|
||||||
# Step 2: Upload and preprocess image
|
# Step 2: Upload and preprocess image
|
||||||
print("Step 2/5: Uploading and preprocessing image...", file=sys.stderr)
|
print("Step 2/5: Uploading and preprocessing image...", file=sys.stderr)
|
||||||
@@ -190,7 +230,7 @@ def generate(image_path, output=None, simplify=0.95, texture_size=1024,
|
|||||||
"path": uploaded_path,
|
"path": uploaded_path,
|
||||||
"meta": {"_type": "gradio.FileData"}
|
"meta": {"_type": "gradio.FileData"}
|
||||||
}
|
}
|
||||||
preprocess_result = _call_api(base, "/preprocess_image_1", [file_ref], timeout=60)
|
preprocess_result = _call_api(base, "/preprocess_image_1", [file_ref], timeout=60, session_hash=session)
|
||||||
|
|
||||||
# _call_api returns the "data" array directly
|
# _call_api returns the "data" array directly
|
||||||
if isinstance(preprocess_result, list) and len(preprocess_result) > 0:
|
if isinstance(preprocess_result, list) and len(preprocess_result) > 0:
|
||||||
@@ -200,28 +240,34 @@ def generate(image_path, output=None, simplify=0.95, texture_size=1024,
|
|||||||
|
|
||||||
# Step 3: Get seed
|
# Step 3: Get seed
|
||||||
print("Step 3/5: Generating 3D model...", file=sys.stderr)
|
print("Step 3/5: Generating 3D model...", file=sys.stderr)
|
||||||
seed_result = _call_api(base, "/get_seed", [True, seed], timeout=10)
|
seed_result = _call_api(base, "/get_seed", [True, seed], timeout=10, session_hash=session)
|
||||||
if isinstance(seed_result, list) and seed_result:
|
if isinstance(seed_result, list) and seed_result:
|
||||||
actual_seed = seed_result[0]
|
actual_seed = seed_result[0]
|
||||||
else:
|
else:
|
||||||
actual_seed = seed
|
actual_seed = seed
|
||||||
|
|
||||||
# Step 4: Image to 3D
|
# Step 4: Image to 3D
|
||||||
# Parameters: image, multiimages, seed, ss_guidance, ss_steps, slat_guidance, slat_steps, algo
|
# Gradio app has 9 inputs: Image, Gallery, State, Slider×5, Radio
|
||||||
|
# State is a hidden session component at position 3 — pass None.
|
||||||
|
# Slider order (from /info): seed, ss_guidance, ss_steps, slat_guidance, slat_steps
|
||||||
|
# Note: server-side numpy bug requires patching flow_euler.py to cast steps to int.
|
||||||
gen_result = _call_api(base, "/image_to_3d", [
|
gen_result = _call_api(base, "/image_to_3d", [
|
||||||
preprocessed_ref, # image
|
preprocessed_ref, # image
|
||||||
[], # multiimages (empty)
|
[], # multiimages (empty)
|
||||||
actual_seed, # seed
|
False, # is_multiimage (boolean flag, not session state)
|
||||||
|
actual_seed, # seed (Slider, 0-2147483647)
|
||||||
7.5, # ss_guidance_strength
|
7.5, # ss_guidance_strength
|
||||||
12, # ss_sampling_steps
|
12, # ss_sampling_steps
|
||||||
3.0, # slat_guidance_strength
|
3.0, # slat_guidance_strength
|
||||||
12, # slat_sampling_steps
|
12, # slat_sampling_steps
|
||||||
"stochastic", # multiimage_algo
|
"stochastic", # multiimage_algo
|
||||||
], timeout=timeout)
|
], timeout=timeout, session_hash=session)
|
||||||
|
|
||||||
# Step 5: Extract GLB
|
# Step 5: Extract GLB
|
||||||
|
# State (output_buf) is maintained server-side via session_hash.
|
||||||
|
# extract_glb reads it automatically. Pass None as placeholder for the State component.
|
||||||
print("Step 4/5: Extracting GLB...", file=sys.stderr)
|
print("Step 4/5: Extracting GLB...", file=sys.stderr)
|
||||||
glb_result = _call_api(base, "/extract_glb", [simplify, texture_size], timeout=120)
|
glb_result = _call_api(base, "/extract_glb", [None, simplify, texture_size], timeout=120, session_hash=session)
|
||||||
|
|
||||||
# _call_api returns the "data" array: [model_viewer_data, download_button_data]
|
# _call_api returns the "data" array: [model_viewer_data, download_button_data]
|
||||||
glb_url = None
|
glb_url = None
|
||||||
|
|||||||
Executable
+93
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Batch Trellis generation — one at a time, gently.
|
||||||
|
# Usage: ./tooling/trellis-batch.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
INPUT_DIR=".tmp/image-gen/characters/bodies"
|
||||||
|
OUTPUT_DIR=".tmp/glb-gen/characters/bodies"
|
||||||
|
COOLDOWN=15 # seconds between jobs
|
||||||
|
MAX_RETRIES=3
|
||||||
|
RETRY_DELAY=60 # seconds before retry
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
TYPES=(
|
||||||
|
slim_m slim_f
|
||||||
|
average_m average_f
|
||||||
|
stocky_m stocky_f
|
||||||
|
tall_lean_m tall_lean_f
|
||||||
|
short_stout_m short_stout_f
|
||||||
|
athletic_m athletic_f
|
||||||
|
heavyset_m heavyset_f
|
||||||
|
petite_m petite_f
|
||||||
|
)
|
||||||
|
|
||||||
|
TOTAL=${#TYPES[@]}
|
||||||
|
SUCCESS=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
echo "=== Trellis Batch: $TOTAL bodies ==="
|
||||||
|
echo " Cooldown: ${COOLDOWN}s between jobs"
|
||||||
|
echo " Retries: $MAX_RETRIES with ${RETRY_DELAY}s delay"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for i in "${!TYPES[@]}"; do
|
||||||
|
TYPE="${TYPES[$i]}"
|
||||||
|
NUM=$((i + 1))
|
||||||
|
INPUT="$INPUT_DIR/${TYPE}.png"
|
||||||
|
OUTPUT="$OUTPUT_DIR/${TYPE}.glb"
|
||||||
|
|
||||||
|
# Skip if already generated
|
||||||
|
if [ -f "$OUTPUT" ]; then
|
||||||
|
echo "[$NUM/$TOTAL] $TYPE — already exists, skipping"
|
||||||
|
SUCCESS=$((SUCCESS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$INPUT" ]; then
|
||||||
|
echo "[$NUM/$TOTAL] $TYPE — input not found: $INPUT"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
ATTEMPT=0
|
||||||
|
DONE=false
|
||||||
|
while [ "$ATTEMPT" -lt "$MAX_RETRIES" ] && [ "$DONE" = "false" ]; do
|
||||||
|
ATTEMPT=$((ATTEMPT + 1))
|
||||||
|
echo "[$NUM/$TOTAL] $TYPE (attempt $ATTEMPT/$MAX_RETRIES)..."
|
||||||
|
|
||||||
|
if python3 tooling/db/trellis_connector.py generate \
|
||||||
|
"$INPUT" \
|
||||||
|
--output "$OUTPUT" \
|
||||||
|
--simplify 0.95 \
|
||||||
|
--texture-size 1024 \
|
||||||
|
2>&1 | tee /dev/stderr | grep -q '"ok": true'; then
|
||||||
|
echo " OK"
|
||||||
|
SUCCESS=$((SUCCESS + 1))
|
||||||
|
DONE=true
|
||||||
|
else
|
||||||
|
echo " FAILED"
|
||||||
|
if [ "$ATTEMPT" -lt "$MAX_RETRIES" ]; then
|
||||||
|
echo " Waiting ${RETRY_DELAY}s before retry..."
|
||||||
|
sleep "$RETRY_DELAY"
|
||||||
|
else
|
||||||
|
echo " Giving up on $TYPE after $MAX_RETRIES attempts"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cooldown between successful jobs
|
||||||
|
if [ "$DONE" = "true" ] && [ "$NUM" -lt "$TOTAL" ]; then
|
||||||
|
echo " Cooling down ${COOLDOWN}s..."
|
||||||
|
sleep "$COOLDOWN"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Results: $SUCCESS/$TOTAL succeeded, $FAILED failed ==="
|
||||||
|
ls -la "$OUTPUT_DIR"/*.glb 2>/dev/null || echo "No GLB files generated"
|
||||||
Reference in New Issue
Block a user