feat: scaffold avatar pipeline with ComfyUI driver, MLflow logging, and rclone promotion

- setup.sh: automated desktop env setup (ComfyUI, 3D-Pack, UniRig, Blender, Ray)
- ray-join.sh: join Ray cluster as external worker with 3d_gen resource label
- vrm_export.py: headless Blender GLB→VRM conversion script
- generate.py: ComfyUI API driver (submit workflow JSON, poll, download outputs)
- log_mlflow.py: REST-only MLflow experiment tracking (no SDK dependency)
- promote.py: rclone promotion of VRM files to gravenhollow S3
- CLI entry points: avatar-generate, avatar-promote
- workflows/ placeholder for ComfyUI exported workflow JSONs

Implements ADR-0063 (ComfyUI + TRELLIS + UniRig 3D avatar pipeline)
This commit is contained in:
2026-02-24 05:44:04 -05:00
parent a0c24406bd
commit 202b4e1d61
11 changed files with 1138 additions and 0 deletions

111
scripts/vrm_export.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Headless Blender script for GLB → VRM conversion.
Invoked via:
blender --background --python scripts/vrm_export.py -- \\
--input rigged_model.glb \\
--output avatar.vrm \\
--name "Avatar Name" \\
--author "DaviesTechLabs Pipeline"
Requires:
- Blender 4.x
- VRM Add-on for Blender (https://vrm-addon-for-blender.info/en/)
"""
import argparse
import sys
# Blender's bpy is only available when running inside Blender
try:
import bpy # type: ignore[import-untyped]
except ImportError:
print("ERROR: This script must be run inside Blender.")
print("Usage: blender --background --python scripts/vrm_export.py -- --input model.glb --output model.vrm")
sys.exit(1)
def parse_args() -> argparse.Namespace:
"""Parse arguments after the '--' separator in Blender CLI."""
argv = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
parser = argparse.ArgumentParser(description="Convert rigged GLB to VRM")
parser.add_argument("--input", required=True, help="Path to rigged GLB file")
parser.add_argument("--output", required=True, help="Output VRM file path")
parser.add_argument("--name", default="Generated Avatar", help="Avatar display name")
parser.add_argument("--author", default="DaviesTechLabs Pipeline", help="Author name")
return parser.parse_args(argv)
def clear_scene() -> None:
"""Remove all objects from the scene."""
bpy.ops.wm.read_factory_settings(use_empty=True)
def import_glb(path: str) -> None:
"""Import a GLB file into the current scene."""
bpy.ops.import_scene.gltf(filepath=path)
print(f"Imported: {path}")
print(f" Objects: {[obj.name for obj in bpy.data.objects]}")
def find_armature() -> "bpy.types.Object":
"""Find the armature object in the scene."""
armatures = [obj for obj in bpy.data.objects if obj.type == "ARMATURE"]
if not armatures:
print("ERROR: No armature found in imported model.")
print(" Available objects:", [obj.name for obj in bpy.data.objects])
sys.exit(1)
if len(armatures) > 1:
print(f"WARNING: Multiple armatures found, using first: {armatures[0].name}")
return armatures[0]
def configure_vrm_metadata(armature: "bpy.types.Object", name: str, author: str) -> None:
"""Set VRM 1.0 metadata on the armature."""
# The VRM Add-on stores metadata as a custom property
armature["vrm_addon_extension"] = {
"spec_version": "1.0",
"vrm0": {
"meta": {
"title": name,
"author": author,
"allowedUserName": "Everyone",
"violentUsageName": "Disallow",
"sexualUsageName": "Disallow",
"commercialUsageName": "Allow",
"licenseName": "MIT",
}
},
}
print(f" VRM metadata: name={name}, author={author}")
def export_vrm(path: str) -> None:
"""Export the scene as VRM."""
bpy.ops.export_scene.vrm(filepath=path)
print(f"Exported VRM: {path}")
def main() -> None:
args = parse_args()
print(f"Converting GLB to VRM:")
print(f" Input: {args.input}")
print(f" Output: {args.output}")
print(f" Name: {args.name}")
clear_scene()
import_glb(args.input)
armature = find_armature()
bpy.context.view_layer.objects.active = armature
armature.select_set(True)
configure_vrm_metadata(armature, args.name, args.author)
export_vrm(args.output)
print("Done.")
if __name__ == "__main__":
main()