$ cat node-template.py
R
Retopology
// Retopologizes a 3D mesh: voxel remesh, QuadriFlow quad remesh, Geometry Nodes (bundled templates or a custom .blend), or a Remesh+Decimate pipeline.
Process
3D
#3d#mesh#retopology#remesh#quads
template.py
1"""retopology — retopologize a Model3D via headless Blender (gais-blender).23One node, four methods selected by the `method` enum (voxel / quadriflow /4geometry_nodes / remesh_decimate). All parameters ride to the GAIS SDK; the5service validates them and drops the ones irrelevant to the chosen method,6so this node only normalizes types and stages files. Privileged transport7lives in the gais SDK; no os/pathlib/HTTP here (lint-blocked).8"""910from __future__ import annotations1112import json13import sys14import traceback15from typing import Any1617from gais import Gais1819INPUT_DIR = "/data/input"20OUTPUT_DIR = "/data/output"2122OUTPUT_FILENAME = "retopologized_model.glb"2324METHODS = ("voxel", "quadriflow", "geometry_nodes", "remesh_decimate")252627def _f(value: Any, default: float) -> float:28 try:29 return float(value)30 except (TypeError, ValueError):31 return default323334def _i(value: Any, default: int) -> int:35 try:36 return int(float(value))37 except (TypeError, ValueError):38 return default394041def _b(value: Any, default: bool) -> bool:42 if isinstance(value, bool):43 return value44 if isinstance(value, str):45 return value.strip().lower() in ("1", "true", "yes", "on")46 if value is None:47 return default48 return bool(value)495051def _input_file_path(inputs: dict[str, Any], key: str) -> str | None:52 """Upstream file ports deliver a bare filename under /data/input."""53 name = inputs.get(key)54 if not name or not isinstance(name, str):55 return None56 base = name.replace("\\", "/").rsplit("/", 1)[-1].strip()57 if not base or base in (".", ".."):58 return None59 return f"{INPUT_DIR}/{base}"606162def process(inputs: dict[str, Any]) -> dict[str, Any]:63 mesh_path = _input_file_path(inputs, "mesh")64 if mesh_path is None:65 raise ValueError("Required input 'mesh' not provided")6667 method = str(inputs.get("method") or "voxel")68 if method not in METHODS:69 raise ValueError(f"Unknown method {method!r}; expected one of {METHODS}")7071 blend_path = _input_file_path(inputs, "blend")7273 result = Gais.model.retopologize(74 mesh_path,75 method=method,76 blend=blend_path,77 node_group=str(inputs.get("node_group") or ""),78 gn_template=str(inputs.get("gn_template") or "volume_remesh"),79 voxel_size=_f(inputs.get("voxel_size"), 0.05),80 adaptivity=_f(inputs.get("adaptivity"), 0.0),81 fix_poles=_b(inputs.get("fix_poles"), False),82 preserve_volume=_b(inputs.get("preserve_volume"), True),83 transfer_attributes=_b(inputs.get("transfer_attributes"), True),84 quad_mode=str(inputs.get("quad_mode") or "FACES"),85 target_faces=_i(inputs.get("target_faces"), 5000),86 target_ratio=_f(inputs.get("target_ratio"), 0.25),87 target_edge_length=_f(inputs.get("target_edge_length"), 0.05),88 preserve_sharp=_b(inputs.get("preserve_sharp"), False),89 preserve_boundary=_b(inputs.get("preserve_boundary"), False),90 smooth_normals=_b(inputs.get("smooth_normals"), False),91 use_mesh_symmetry=_b(inputs.get("use_mesh_symmetry"), False),92 seed=_i(inputs.get("seed"), 0),93 remesh_mode=str(inputs.get("remesh_mode") or "VOXEL"),94 octree_depth=_i(inputs.get("octree_depth"), 6),95 decimate_ratio=_f(inputs.get("decimate_ratio"), 0.5),96 )9798 glb_bytes = result.content99 if not glb_bytes:100 raise RuntimeError("gais-blender returned an empty model")101102 with open(f"{OUTPUT_DIR}/{OUTPUT_FILENAME}", "wb") as fh:103 fh.write(glb_bytes)104105 faces_after = result.metadata.get("faces_after")106 if faces_after:107 print(f"[retopology] method={method} faces_after={faces_after}", file=sys.stderr)108109 return {"model": OUTPUT_FILENAME}110111112def main() -> None:113 try:114 envelope = json.loads(sys.stdin.read() or "{}")115 if not isinstance(envelope, dict):116 envelope = {}117 inputs = envelope.get("inputs", {}) or {}118 json.dump(process(inputs), sys.stdout)119 except Exception as e:120 print(121 json.dumps({122 "error": str(e),123 "errorType": type(e).__name__,124 "traceback": traceback.format_exc(),125 }),126 file=sys.stderr,127 )128 sys.exit(1)129130131if __name__ == "__main__":132 main()$ git log --oneline
v1.1.0
HEAD
2026-08-18