feat(hanzo_engine): music + dub nodes, textured 3D, flagship pipeline

Add HanzoMusic (POST /v1/audio/music -> ACE-Step stereo AUDIO) and HanzoDub
(POST /v1/animate -> MuseTalk lip-sync: portrait IMAGE + driving AUDIO -> frames
+ audio). Fix HanzoImageTo3D to send the now-live `texture` flag (textured GLB
via SLAT + FlexiCubes) and offer obj. Correct HanzoTTS response_format to the
engine's actual wav/pcm. Drop the torchaudio dependency: studio_audio_to_wav_bytes
now uses the stdlib wave module, and studio_audio_to_data_url feeds the dub's
driving audio.

Ship user/default/workflows/hanzo-full-generative-pipeline.json: a prompt is
expanded by Chat and fans out to Image Gen -> (textured Image-to-3D AND WAN
Text-to-Video), plus TTS narration and ACE-Step Music -- every modality in one
graph, all on the native engine.

contract_check.py drives all 8 nodes over real HTTP against a stub engine that
enforces each Rust handler's request fields and the async job submit->poll->content
protocol.
This commit is contained in:
Hanzo Dev
2026-07-10 16:48:53 -07:00
parent ace9d057f8
commit cea869bd14
5 changed files with 1086 additions and 20 deletions
+32 -11
View File
@@ -27,11 +27,13 @@ server keeps running.
| **Hanzo Chat (Engine)** | `POST /v1/chat/completions` (streams) | model + prompt -> `text` |
| **Hanzo Image Gen (Engine)** | `POST /v1/images/generations` | prompt + width/height -> `IMAGE` |
| **Hanzo TTS (Engine)** | `POST /v1/audio/speech` | text -> `AUDIO` |
| **Hanzo Music (Engine)** | `POST /v1/audio/music` | prompt -> `AUDIO` |
| **Hanzo ASR (Engine)** | `POST /v1/audio/transcriptions` | `AUDIO` -> `text` |
| **Hanzo Engine Request** | any `/v1` path | method + path + JSON -> `response` |
| **Hanzo Vision Caption (Engine)** | `POST /v1/chat/completions` (vision) | `IMAGE` + prompt -> `caption` |
| **Hanzo Text to Video (Engine)** | async `/v1/videos` | prompt -> `IMAGE` frames |
| **Hanzo Image to 3D (Engine)** | async `/v1/3d` | `IMAGE` -> mesh path |
| **Hanzo Image to 3D (Engine)** | async `/v1/3d` | `IMAGE` (+`texture`) -> mesh path |
| **Hanzo Dub / Lip-Sync (Engine)** | `POST /v1/animate` | `IMAGE` + `AUDIO` -> `IMAGE` frames + `AUDIO` |
| **Hanzo Save Text** | -- | `text` -> writes `output/*.txt` |
- **Chat** streams tokens over SSE, driving the node progress bar, and returns
@@ -39,21 +41,40 @@ server keeps running.
from `/v1/models`; `default` always routes to the loaded model.
- **Image Gen** sends the engine's `width`/`height`/`n` schema and decodes the
`b64_json` (or `url`) response into a Studio `IMAGE`.
- **TTS/ASR** speak the Studio `AUDIO` type (`{waveform, sample_rate}`) and
compose with the built-in Load/Save/Preview Audio nodes.
- **TTS/Music/ASR** speak the Studio `AUDIO` type (`{waveform, sample_rate}`)
and compose with the built-in Load/Save/Preview Audio nodes. TTS and Music
serve `wav`/`pcm` (the engine's synchronous audio formats); Music is stereo
44.1 kHz ACE-Step.
- **Engine Request** is the forward-compatible escape hatch: reach any endpoint
(music, dub, ...) the day it ships, then promote it to a dedicated node here.
the day it ships, then promote it to a dedicated node here.
- **Image to 3D** exposes the `texture` flag: on (default) runs the full
SLAT + FlexiCubes stage for a textured GLB; off returns the coarse mesh.
- **Text to Video / Image to 3D** use the engine's async job protocol
(`submit -> poll -> fetch content`) and report progress.
(`submit -> poll -> fetch content`) and report progress. **Dub** is
synchronous: it posts the portrait `IMAGE` + driving `AUDIO` and returns the
animated frames plus the audio (mux them with a video-combine node).
## Requirements
Only libraries already in Hanzo Studio: `requests`, `Pillow`, `numpy`, `torch`
(always), plus `av`/`torchaudio` (loaded lazily, for the audio/video nodes).
No extra install.
(always), plus `av` (loaded lazily, for the audio/video decode paths). WAV
encode/decode uses the stdlib `wave` module. No extra install.
## Example
## Verification
`user/default/workflows/hanzo-native-pipeline.json` -- Chat writes an image
prompt, Image Gen renders it, the image and the prompt text are saved. All
compute runs on the native engine.
`contract_check.py` drives every node through real HTTP against a stub engine
that enforces the exact request fields each Rust handler deserializes and the
async job protocol (submit -> poll -> content):
```bash
python custom_nodes/hanzo_engine/contract_check.py
```
## Examples
- `user/default/workflows/hanzo-native-pipeline.json` -- Chat writes an image
prompt, Image Gen renders it, image + prompt are saved.
- `user/default/workflows/hanzo-full-generative-pipeline.json` -- the flagship
multimodal pipeline: Chat expands a prompt, which fans out to Image Gen ->
(textured Image-to-3D **and** WAN Text-to-Video), plus TTS narration and
ACE-Step Music. One graph, every modality, all on the native engine.
+17 -3
View File
@@ -293,12 +293,26 @@ def audio_bytes_to_studio(data: bytes) -> dict:
def studio_audio_to_wav_bytes(audio: dict) -> bytes:
"""Encode a Studio AUDIO dict to WAV bytes for multipart upload (ASR)."""
import torchaudio
"""Encode a Studio AUDIO dict ({waveform:[B,C,T], sample_rate}) to 16-bit PCM
WAV bytes for multipart upload (ASR) or a data URL (dub). Stdlib `wave` only."""
import wave
wav = audio["waveform"]
if wav.dim() == 3: # [B,C,T] -> [C,T]
wav = wav[0]
arr = wav.detach().cpu().float().clamp_(-1.0, 1.0).numpy() # [C,T]
pcm16 = (arr * 32767.0).astype(np.int16)
interleaved = pcm16.T.reshape(-1).tobytes() # [T,C] interleaved
buf = io.BytesIO()
torchaudio.save(buf, wav.cpu(), int(audio["sample_rate"]), format="wav")
with wave.open(buf, "wb") as w:
w.setnchannels(arr.shape[0])
w.setsampwidth(2)
w.setframerate(int(audio["sample_rate"]))
w.writeframes(interleaved)
return buf.getvalue()
def studio_audio_to_data_url(audio: dict) -> str:
"""Encode a Studio AUDIO dict to a `data:audio/wav` URL (driving audio for
/v1/animate, which takes audio as an http/file/data URL)."""
return "data:audio/wav;base64," + base64.b64encode(studio_audio_to_wav_bytes(audio)).decode("ascii")
+212
View File
@@ -0,0 +1,212 @@
"""Contract test: drive every Hanzo Engine node through REAL HTTP against a stub
engine that enforces the exact request fields the Rust handlers deserialize
(ThreeDGenerationRequest, VideoGenerationRequest, AnimateRequest, Speech/Music
requests) and the async-job protocol (submit -> poll -> content). This is a real
request/response cycle over sockets + the requests lib + client.run_job polling,
not a monkeypatched mock. Run: python contract_check.py
"""
# ruff: noqa: T201 (a CLI harness prints its results)
import base64
import io
import json
import os
import sys
import threading
import types
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import numpy as np
import torch
# Stub out the ComfyUI-side modules the node pack imports so nodes.py loads.
fp = types.ModuleType("folder_paths")
fp.get_output_directory = lambda: "/tmp"
fp.get_save_image_path = lambda pre, d: (d, pre, 1, "", "")
sys.modules["folder_paths"] = fp
# Import the pack as a package so its `from . import client` resolves.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from hanzo_engine import nodes # noqa: E402
RECORDED = {} # path -> last JSON/body seen
def _wav_bytes(seconds=0.1, sr=44100, ch=2):
import wave
n = int(seconds * sr)
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
w.setnchannels(ch)
w.setsampwidth(2)
w.setframerate(sr)
w.writeframes(b"\x00\x00" * n * ch)
return buf.getvalue()
def _png_b64():
from PIL import Image
buf = io.BytesIO()
Image.new("RGB", (8, 8), (128, 64, 32)).save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("ascii")
def _mp4_bytes(frames=3, w=16, h=16):
import av
buf = io.BytesIO()
container = av.open(buf, mode="w", format="mp4")
stream = container.add_stream("libx264", rate=25)
stream.width, stream.height, stream.pix_fmt = w, h, "yuv420p"
for i in range(frames):
arr = np.full((h, w, 3), i * 20 % 255, dtype=np.uint8)
frame = av.VideoFrame.from_ndarray(arr, format="rgb24")
for pkt in stream.encode(frame):
container.mux(pkt)
for pkt in stream.encode():
container.mux(pkt)
container.close()
return buf.getvalue()
class Handler(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def _send(self, code, body, ctype="application/json"):
if isinstance(body, (dict, list)):
body = json.dumps(body).encode()
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _body_json(self):
n = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(n) if n else b"{}"
try:
return json.loads(raw)
except ValueError:
return {"_raw": raw}
def do_GET(self):
p = self.path
if p == "/v1/models":
return self._send(200, {"data": [{"id": "stub-model"}]})
if p.startswith("/v1/3d/") and p.endswith("/content"):
return self._send(200, base64.b64decode(_PLACEHOLDER_GLB_B64), "model/gltf-binary")
if p.startswith("/v1/videos/") and p.endswith("/content"):
return self._send(200, _mp4_bytes(), "video/mp4")
if p.startswith("/v1/3d/") or p.startswith("/v1/videos/"):
# poll: report completed immediately
return self._send(200, {"id": p.split("/")[3], "status": "completed", "progress": 1.0})
return self._send(404, {"message": "no"})
def do_POST(self):
try:
self._do_POST()
except Exception as exc: # surface stub-side errors to the client
self._send(500, {"stub_error": repr(exc)})
def _do_POST(self):
p = self.path
# multipart (ASR) is not JSON
if p == "/v1/audio/transcriptions":
n = int(self.headers.get("Content-Length", 0))
_ = self.rfile.read(n)
RECORDED[p] = {"multipart": True}
return self._send(200, {"text": "stub transcript"})
body = self._body_json()
RECORDED[p] = body
if p == "/v1/chat/completions":
return self._send(200, {"choices": [{"message": {"content": "expanded prompt"}}]})
if p == "/v1/images/generations":
return self._send(200, {"data": [{"b64_json": _png_b64()}]})
if p == "/v1/audio/speech":
assert "input" in body and "model" in body, "speech needs input+model"
return self._send(200, _wav_bytes(ch=1, sr=24000), "audio/wav")
if p == "/v1/audio/music":
assert "input" in body and "model" in body, "music needs input+model"
return self._send(200, _wav_bytes(ch=2, sr=44100), "audio/wav")
if p == "/v1/animate":
assert set(body) >= {"model", "audio", "visual"}, "animate needs model+audio+visual"
assert body["visual"].startswith("data:image/"), "visual must be an image data url"
assert body["audio"].startswith("data:audio/"), "audio must be an audio data url"
return self._send(200, _mp4_bytes(), "video/mp4")
if p == "/v1/videos":
assert "prompt" in body, "video needs prompt"
return self._send(202, {"id": "vid-1", "status": "queued"})
if p == "/v1/3d":
assert "image" in body, "3d needs image"
return self._send(202, {"id": "threed-1", "status": "queued", "progress": 0.0})
return self._send(404, {"message": "no"})
# A 1-triangle binary glTF is overkill; the node just writes bytes to disk, so any
# non-empty payload proves the content fetch + save path. Use a tiny placeholder.
_PLACEHOLDER_GLB_B64 = base64.b64encode(b"glTF\x02\x00\x00\x00stub-mesh").decode("ascii")
def main():
srv = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
port = srv.server_address[1]
threading.Thread(target=srv.serve_forever, daemon=True).start()
import os
os.environ["HANZO_ENGINE_URL"] = f"http://127.0.0.1:{port}"
img = torch.rand(1, 8, 8, 3)
audio = {"waveform": torch.zeros(1, 2, 4410), "sample_rate": 44100}
passed = []
# Chat (non-stream path)
(text,) = nodes.HanzoChat().generate(model="default", prompt="a red dragon", stream=False)["result"]
assert text == "expanded prompt", text
assert RECORDED["/v1/chat/completions"]["messages"][-1]["content"] == "a red dragon"
passed.append("HanzoChat -> /v1/chat/completions")
# ImageGen
(imgs,) = nodes.HanzoImageGen().generate("a red dragon", 64, 64, n=1)
assert imgs.shape[-1] == 3
passed.append("HanzoImageGen -> /v1/images/generations")
# TTS
(aud,) = nodes.HanzoTTS().speak("hello world", response_format="wav")
assert "waveform" in aud and aud["sample_rate"] == 24000
passed.append("HanzoTTS -> /v1/audio/speech")
# Music (NEW)
(music,) = nodes.HanzoMusic().compose("epic orchestral", response_format="wav")
assert music["sample_rate"] == 44100 and music["waveform"].shape[1] == 2, music["waveform"].shape
assert RECORDED["/v1/audio/music"]["input"] == "epic orchestral"
passed.append("HanzoMusic -> /v1/audio/music (input field, stereo 44.1k)")
# ASR
(tr,) = nodes.HanzoASR().transcribe(audio)
assert tr == "stub transcript"
passed.append("HanzoASR -> /v1/audio/transcriptions")
# ImageTo3D (texture flag fix) -- async job submit+poll+content
out = nodes.HanzoImageTo3D().generate(img, "glb", True, 0, 4)
assert RECORDED["/v1/3d"]["texture"] is True, "texture flag not sent"
assert RECORDED["/v1/3d"]["format"] == "glb"
assert out["result"][0].endswith(".glb")
passed.append("HanzoImageTo3D -> /v1/3d (texture=true sent; job submit->poll->content)")
# TextToVideo -- async job
(frames,) = nodes.HanzoTextToVideo().generate("a red dragon flying", 3, 16, 16, 4)
assert frames.dim() == 4 and frames.shape[0] >= 1
assert RECORDED["/v1/videos"]["num_frames"] == 3
passed.append("HanzoTextToVideo -> /v1/videos (job submit->poll->content)")
# Dub (NEW) -- synchronous mp4
(dframes, daudio) = nodes.HanzoDub().dub(img, audio, fps=25.0)
assert dframes.dim() == 4
assert daudio is audio
passed.append("HanzoDub -> /v1/animate (image+audio data urls; frames+audio out)")
srv.shutdown()
print("\n".join(f"PASS {p}" for p in passed))
print(f"\nALL {len(passed)} NODE CONTRACT CHECKS PASSED against a live HTTP engine stub.")
if __name__ == "__main__":
main()
+81 -6
View File
@@ -8,11 +8,13 @@ http://127.0.0.1:1234, override with HANZO_ENGINE_URL). Paths are always
HanzoChat -> POST /v1/chat/completions (prompt -> text)
HanzoImageGen -> POST /v1/images/generations (prompt -> IMAGE)
HanzoTTS -> POST /v1/audio/speech (text -> AUDIO)
HanzoMusic -> POST /v1/audio/music (prompt -> AUDIO)
HanzoASR -> POST /v1/audio/transcriptions (AUDIO -> text)
HanzoEngineRequest -> POST|GET <any /v1 path> (JSON -> response)
HanzoVisionCaption -> POST /v1/chat/completions (vision) (IMAGE -> text)
HanzoTextToVideo -> async /v1/videos (prompt -> frames)
HanzoImageTo3D -> async /v1/3d (IMAGE -> mesh path)
HanzoDub -> POST /v1/animate (IMAGE+AUDIO -> frames)
HanzoSaveText -> writes a STRING to output/*.txt
The generic HanzoEngineRequest is the forward-compatible escape hatch: a new
@@ -129,7 +131,7 @@ class HanzoTTS:
},
"optional": {
"model": (_model_combo(),),
"response_format": (["wav", "flac", "mp3", "opus", "aac"], {"default": "wav"}),
"response_format": (["wav", "pcm"], {"default": "wav"}),
},
}
@@ -302,15 +304,17 @@ class HanzoTextToVideo:
class HanzoImageTo3D:
"""Image-to-3D via the async job /v1/3d (native Rust). Saves the GLB/PLY to
the output dir and returns its path for a 3D-viewer node."""
"""Image-to-3D via the async job /v1/3d (native Rust TRELLIS/Pixal3D). Saves
the mesh to the output dir and returns its path for a 3D-viewer node. Enable
`texture` for the full SLAT + FlexiCubes stage (textured GLB vs coarse mesh)."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"format": (["glb", "ply"], {"default": "glb"}),
"format": (["glb", "ply", "obj"], {"default": "glb"}),
"texture": ("BOOLEAN", {"default": True}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
"steps": ("INT", {"default": 30, "min": 1, "max": 150}),
},
@@ -322,10 +326,11 @@ class HanzoImageTo3D:
CATEGORY = CATEGORY
OUTPUT_NODE = True
def generate(self, image, format, seed, steps):
def generate(self, image, format, texture, seed, steps):
raw = client.run_job(
"3d",
{"image": client.tensor_to_data_url(image), "format": format, "seed": seed, "steps": steps},
{"image": client.tensor_to_data_url(image), "format": format,
"texture": texture, "seed": seed, "steps": steps},
job_timeout=900,
)
full_output_folder, filename, counter, _sub, _pre = folder_paths.get_save_image_path(
@@ -337,6 +342,72 @@ class HanzoImageTo3D:
return {"ui": {"text": [path]}, "result": (path,)}
class HanzoMusic:
"""Text/tag/lyric prompt -> music via /v1/audio/music (native Rust ACE-Step).
Returns a Studio AUDIO you can save/preview with the built-in audio nodes."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True,
"default": "epic cinematic orchestral, driving percussion, brass swells"}),
},
"optional": {
"model": (_model_combo(),),
"response_format": (["wav", "pcm"], {"default": "wav"}),
},
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
FUNCTION = "compose"
CATEGORY = CATEGORY
def compose(self, prompt, model="default", response_format="wav"):
raw = client.post_bytes(
"/v1/audio/music",
{"model": model, "input": prompt, "response_format": response_format},
timeout=900,
)
return (client.audio_bytes_to_studio(raw),)
class HanzoDub:
"""Lip-sync / avatar dub via /v1/animate (native Rust MuseTalk). Drives a
portrait IMAGE (or the first video frame) with an AUDIO track and returns the
animated frames as an IMAGE batch plus the driving audio, so a video-combine
node can mux them."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"audio": ("AUDIO",),
},
"optional": {
"model": (_model_combo(),),
"fps": ("FLOAT", {"default": 25.0, "min": 1.0, "max": 60.0, "step": 1.0}),
},
}
RETURN_TYPES = ("IMAGE", "AUDIO")
RETURN_NAMES = ("frames", "audio")
FUNCTION = "dub"
CATEGORY = CATEGORY
def dub(self, image, audio, model="default", fps=25.0):
payload = {
"model": model,
"audio": client.studio_audio_to_data_url(audio),
"visual": client.tensor_to_data_url(image),
"fps": fps,
}
raw = client.post_bytes("/v1/animate", payload, timeout=1800)
return (client.mp4_bytes_to_image_batch(raw), audio)
class HanzoSaveText:
"""Write a STRING to output/*.txt (image/caption pairs for dataset prep)
and preview it in the node."""
@@ -372,11 +443,13 @@ NODE_CLASS_MAPPINGS = {
"HanzoChat": HanzoChat,
"HanzoImageGen": HanzoImageGen,
"HanzoTTS": HanzoTTS,
"HanzoMusic": HanzoMusic,
"HanzoASR": HanzoASR,
"HanzoEngineRequest": HanzoEngineRequest,
"HanzoVisionCaption": HanzoVisionCaption,
"HanzoTextToVideo": HanzoTextToVideo,
"HanzoImageTo3D": HanzoImageTo3D,
"HanzoDub": HanzoDub,
"HanzoSaveText": HanzoSaveText,
}
@@ -384,10 +457,12 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"HanzoChat": "Hanzo Chat (Engine)",
"HanzoImageGen": "Hanzo Image Gen (Engine)",
"HanzoTTS": "Hanzo TTS (Engine)",
"HanzoMusic": "Hanzo Music (Engine)",
"HanzoASR": "Hanzo ASR (Engine)",
"HanzoEngineRequest": "Hanzo Engine Request",
"HanzoVisionCaption": "Hanzo Vision Caption (Engine)",
"HanzoTextToVideo": "Hanzo Text to Video (Engine)",
"HanzoImageTo3D": "Hanzo Image to 3D (Engine)",
"HanzoDub": "Hanzo Dub / Lip-Sync (Engine)",
"HanzoSaveText": "Hanzo Save Text",
}
@@ -0,0 +1,744 @@
{
"id": "hanzo-full-generative-pipeline",
"revision": 0,
"last_node_id": 11,
"last_link_id": 9,
"nodes": [
{
"id": 1,
"type": "HanzoChat",
"pos": [
40,
40
],
"size": [
420,
340
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "COMBO",
"widget": {
"name": "model"
},
"link": null
},
{
"name": "prompt",
"type": "STRING",
"widget": {
"name": "prompt"
},
"link": null
},
{
"name": "system",
"type": "STRING",
"widget": {
"name": "system"
},
"link": null
},
{
"name": "temperature",
"type": "FLOAT",
"widget": {
"name": "temperature"
},
"link": null
},
{
"name": "max_tokens",
"type": "INT",
"widget": {
"name": "max_tokens"
},
"link": null
},
{
"name": "stream",
"type": "BOOLEAN",
"widget": {
"name": "stream"
},
"link": null
}
],
"outputs": [
{
"name": "text",
"type": "STRING",
"links": [
1,
2,
3,
4
]
}
],
"properties": {
"Node name for S&R": "HanzoChat"
},
"widgets_values": [
"default",
"/no_think Write one vivid comma-separated image prompt for a majestic bronze dragon perched on a cliff at golden hour. Output only the prompt.",
"You are a concise text-to-image prompt engineer.",
0.4,
200,
true
]
},
{
"id": 2,
"type": "HanzoImageGen",
"pos": [
500,
40
],
"size": [
340,
220
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"widget": {
"name": "prompt"
},
"link": 1
},
{
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": null
},
{
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": null
},
{
"name": "model",
"type": "COMBO",
"widget": {
"name": "model"
},
"link": null
},
{
"name": "n",
"type": "INT",
"widget": {
"name": "n"
},
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
5,
6
]
}
],
"properties": {
"Node name for S&R": "HanzoImageGen"
},
"widgets_values": [
"",
1024,
1024,
"default",
1
]
},
{
"id": 3,
"type": "HanzoImageTo3D",
"pos": [
880,
40
],
"size": [
320,
200
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 5
},
{
"name": "format",
"type": "COMBO",
"widget": {
"name": "format"
},
"link": null
},
{
"name": "texture",
"type": "BOOLEAN",
"widget": {
"name": "texture"
},
"link": null
},
{
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": null
},
{
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
}
],
"outputs": [
{
"name": "mesh_path",
"type": "STRING",
"links": []
}
],
"properties": {
"Node name for S&R": "HanzoImageTo3D"
},
"widgets_values": [
"glb",
true,
0,
30
]
},
{
"id": 4,
"type": "HanzoTextToVideo",
"pos": [
500,
320
],
"size": [
340,
260
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"widget": {
"name": "prompt"
},
"link": 2
},
{
"name": "num_frames",
"type": "INT",
"widget": {
"name": "num_frames"
},
"link": null
},
{
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": null
},
{
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": null
},
{
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"name": "model",
"type": "COMBO",
"widget": {
"name": "model"
},
"link": null
}
],
"outputs": [
{
"name": "frames",
"type": "IMAGE",
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "HanzoTextToVideo"
},
"widgets_values": [
"",
81,
832,
480,
30,
"default"
]
},
{
"id": 5,
"type": "HanzoTTS",
"pos": [
500,
620
],
"size": [
340,
160
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 3
},
{
"name": "model",
"type": "COMBO",
"widget": {
"name": "model"
},
"link": null
},
{
"name": "response_format",
"type": "COMBO",
"widget": {
"name": "response_format"
},
"link": null
}
],
"outputs": [
{
"name": "audio",
"type": "AUDIO",
"links": [
8
]
}
],
"properties": {
"Node name for S&R": "HanzoTTS"
},
"widgets_values": [
"default",
"wav"
]
},
{
"id": 6,
"type": "HanzoMusic",
"pos": [
40,
620
],
"size": [
420,
200
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "prompt",
"type": "STRING",
"widget": {
"name": "prompt"
},
"link": null
},
{
"name": "model",
"type": "COMBO",
"widget": {
"name": "model"
},
"link": null
},
{
"name": "response_format",
"type": "COMBO",
"widget": {
"name": "response_format"
},
"link": null
}
],
"outputs": [
{
"name": "audio",
"type": "AUDIO",
"links": [
9
]
}
],
"properties": {
"Node name for S&R": "HanzoMusic"
},
"widgets_values": [
"epic cinematic orchestral, driving percussion, brass swells, heroic",
"default",
"wav"
]
},
{
"id": 7,
"type": "SaveImage",
"pos": [
1240,
40
],
"size": [
380,
380
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 6
},
{
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage"
},
"widgets_values": [
"hanzo_full_pipeline_still"
]
},
{
"id": 8,
"type": "SaveImage",
"pos": [
880,
320
],
"size": [
340,
300
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 7
},
{
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage"
},
"widgets_values": [
"hanzo_full_pipeline_video_frames"
]
},
{
"id": 9,
"type": "SaveAudio",
"pos": [
880,
620
],
"size": [
320,
140
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "audio",
"type": "AUDIO",
"link": 8
},
{
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveAudio"
},
"widgets_values": [
"audio/hanzo_full_pipeline_narration"
]
},
{
"id": 10,
"type": "SaveAudio",
"pos": [
40,
840
],
"size": [
420,
140
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "audio",
"type": "AUDIO",
"link": 9
},
{
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveAudio"
},
"widgets_values": [
"audio/hanzo_full_pipeline_music"
]
},
{
"id": 11,
"type": "HanzoSaveText",
"pos": [
1240,
460
],
"size": [
340,
160
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 4
},
{
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "HanzoSaveText"
},
"widgets_values": [
"hanzo_full_pipeline_prompt"
]
}
],
"links": [
[
1,
1,
0,
2,
0,
"STRING"
],
[
2,
1,
0,
4,
0,
"STRING"
],
[
3,
1,
0,
5,
0,
"STRING"
],
[
4,
1,
0,
11,
0,
"STRING"
],
[
5,
2,
0,
3,
0,
"IMAGE"
],
[
6,
2,
0,
7,
0,
"IMAGE"
],
[
7,
4,
0,
8,
0,
"IMAGE"
],
[
8,
5,
0,
9,
0,
"AUDIO"
],
[
9,
6,
0,
10,
0,
"AUDIO"
]
],
"groups": [
{
"title": "Prompt",
"bounding": [
20,
0,
460,
380
],
"color": "#3f789e"
},
{
"title": "Image + 3D",
"bounding": [
480,
0,
1140,
300
],
"color": "#a1309b"
},
{
"title": "Video",
"bounding": [
480,
300,
740,
320
],
"color": "#b58b2a"
},
{
"title": "Audio (speech + music)",
"bounding": [
20,
600,
1180,
400
],
"color": "#2a7d4f"
}
],
"config": {},
"extra": {},
"version": 0.4
}