- 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)
112 lines
3.5 KiB
Python
112 lines
3.5 KiB
Python
#!/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()
|