Claude 在 Blender 指揮製作完整 YouTube 影片所需的技術架構
The Stack I'd Need for Claude to Direct a Whole YouTube Video in Blender
作者搭建一套 Python 技術架構,讓 Claude 透過工具型智能體連接自然語言與 Blender Python API,目標是在無需人工介入下製作完整 YouTube 影片。架構包含在 Blender 內運行的 JSON-RPC 伺服器、工具定義、智能體循環及渲染工具,並以白名單操作取代執行任意程式碼。
Introduction
I've been experimenting with LLM agents that control Blender. The goal: have Claude generate a complete YouTube video—script, 3D scenes, animations, camera moves, and render—without human intervention. The result is a stack of Python components that bridge natural language to Blender's Python API. This post outlines the architecture, the code, and the hard limits I hit.
Problem
Blender is a GUI application with a massive Python API (bpy). An LLM can emit Python code, but directly executing it is dangerous and brittle. You need:
- Safety: Arbitrary code execution can delete files or crash the system.
- State management: Blender scenes are complex; the agent must track objects, materials, and timelines.
- Iterative refinement: A single prompt won't produce a polished video. The agent must plan, execute, observe, and fix.
- Rendering: Final output requires invoking Blender's render engine, which can take hours.
Naively asking Claude to "write a Blender script" yields code that often fails due to API changes or missing context.
Solution
I built a tool-using agent loop (inspired by ReAct-style prompting) where Claude calls a set of Python functions that manipulate Blender via bpy. The agent runs inside Blender's Python environment (or a subprocess with blender --background). The loop terminates when the agent calls a finish tool or exceeds a step limit.
Key components:
- Blender Bridge: A JSON-RPC server running inside Blender that executes whitelisted operations.
- Tool Definitions: Python functions decorated with metadata for Claude's tool-use API.
- Agent Loop: Sends conversation + tool results to Claude, parses tool calls, executes them, and appends results.
-
Renderer: A final tool that triggers
bpy.ops.render.render(animation=True).
This avoids eval of arbitrary code. Each tool is a specific, auditable function.
Implementation
1. Blender Bridge (inside Blender)
Run this script in Blender's scripting tab or via blender --background --python bridge.py. It listens on a local socket for JSON commands.
# bridge.py
import bpy
import json
import socket
import threading
HOST = '127.0.0.1'
PORT = 65432
# Whitelisted operations
def op_create_cube(name, location):
bpy.ops.mesh.primitive_cube_add(location=location)
obj = bpy.context.active_object
obj.name = name
return {'name': obj.name, 'location': list(obj.location)}
def op_set_camera(location, rotation):
cam = bpy.context.scene.camera
if not cam:
bpy.ops.object.camera_add()
cam = bpy.context.active_object
bpy.context.scene.camera = cam
cam.location = location
cam.rotation_euler = rotation
return {'camera': cam.name}
def op_render(output_path, frame_start=1, frame_end=1):
bpy.context.scene.frame_start = frame_start
bpy.context.scene.frame_end = frame_end
bpy.context.scene.render.filepath = output_path
bpy.ops.render.render(animation=True)
return {'rendered': output_path}
OPERATIONS = {
'create_cube': op_create_cube,
'set_camera': op_set_camera,
'render': op_render,
}
def handle_client(conn):
with conn:
data = conn.recv(4096).decode('utf-8')
if not data:
return
try:
msg = json.loads(data)
op = msg.get('operation')
args = msg.get('args', {})
if op not in OPERATIONS:
response = {'error': f'Unknown operation: {op}'}
else:
result = OPERATIONS[op](**args)
response = {'result': result}
except Exception as e:
response = {'error': str(e)}
conn.sendall(json.dumps(response).encode('utf-8'))
def start_server():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
print(f'Bridge listening on {HOST}:{PORT}')
while True:
conn, _ = s.accept()
threading.Thread(target=handle_client, args=(conn,)).start()
# Start server in a background thread so Blender's UI remains responsive
threading.Thread(target=start_server, daemon=True).start()
Security note: This bridge executes only whitelisted functions. Never expose it to untrusted networks. Bind to 127.0.0.1 and use authentication if needed.
2. Tool Definitions for Claude
Define tools in a format compatible with Anthropic's tool-use API.
# tools.py
tools = [
{
"name": "create_cube",
"description": "Create a cube mesh at a given location",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"location": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3}
},
"required": ["name", "location"]
}
},
{
"name": "set_camera",
"description": "Set the active camera's location and rotation (Euler angles in radians)",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3},
"rotation": {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3}
},
"required": ["location", "rotation"]
}
},
{
"name": "render",
"description": "Render an animation to the given output path",
"input_schema": {
"type": "object",
"properties": {
"output_path": {"type": "string"},
"frame_start": {"type": "integer"},
"frame_end": {"type": "integer"}
},
"required": ["output_path"]
}
}
]
3. Agent Loop
This loop sends messages to Claude, executes tool calls via the bridge, and appends results. It terminates when Claude calls a finish tool (not shown) or after max_steps.
# agent.py
import json
import socket
import anthropic
client = anthropic.Anthropic(api_key="YOUR_API_KEY")
def call_bridge(operation, args):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(('127.0.0.1', 65432))
s.sendall(json.dumps({'operation': operation, 'args': args}).encode('utf-8'))
response = s.recv(4096).decode('utf-8')
return json.loads(response)
def run_agent(prompt, max_steps=20):
messages = [{"role": "user", "content": prompt}]
for step in range(max_steps):
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
# Append assistant's response
messages.append({"role": "assistant", "content": response.content})
tool_calls = [block for block in response.content if block.type == "tool_use"]
if not tool_calls:
print("No tool calls, ending.")
break
for tool_call in tool_calls:
if tool_call.name == "finish":
print("Agent finished.")
return
result = call_bridge(tool_call.name, tool_call.input)
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": json.dumps(result)
}]
})
else:
print(f"Reached max steps ({max_steps}).")
4. Orchestration
Start Blender with the bridge, then run the agent from a separate process.
blender --background --python bridge.py &
python agent.py
Key Takeaways
-
Tool whitelisting beats code generation: Instead of letting Claude write arbitrary
bpycode, expose a small set of safe operations. This prevents catastrophic mistakes and makes debugging easier. -
Explicit termination: The agent loop stops on a
finishtool call or a step limit. Without this, it could run indefinitely. -
Security first: Never
evalLLM output. My bridge uses a whitelist and runs locally. If you must execute generated code, sandbox it (e.g.,RestrictedPythonor a container). - Iterative refinement is key: The agent can observe results (e.g., object locations) and adjust. For a full video, you'd add tools for keyframing, text, and compositing.
- Rendering is the bottleneck: Blender renders can take hours. The agent should plan the entire scene before triggering a render.
This stack is a starting point. With more tools (e.g., add_keyframe, set_material), Claude can direct increasingly complex animations. The code is available on GitHub—link in comments.
來源:DEV Community · dev.to