$ 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