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:
111
scripts/vrm_export.py
Normal file
111
scripts/vrm_export.py
Normal 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()
|
||||
Reference in New Issue
Block a user