hermes-agent/skills/creative/touchdesigner/SKILL.md
kshitijk4poor 7a5371b20d feat: add TouchDesigner integration skill
New skill: creative/touchdesigner — control a running TouchDesigner
instance via REST API. Build real-time visual networks programmatically.

Architecture:
  Hermes Agent -> HTTP REST (curl) -> TD WebServer DAT -> TD Python env

Key features:
- Custom API handler (scripts/custom_api_handler.py) that creates a
  self-contained WebServer DAT + callback in TD. More reliable than the
  official mcp_webserver_base.tox which frequently fails module imports.
- Discovery-first workflow: never hardcode TD parameter names. Always
  probe the running instance first since names change across versions.
- Persistent setup: save the TD project once with the API handler baked
  in. TD auto-opens the last project on launch, so port 9981 is live
  with zero manual steps after first-time setup.
- Works via curl in execute_code (no MCP dependency required).
- Optional MCP server config for touchdesigner-mcp-server npm package.

Skill structure (2823 lines total):
  SKILL.md (209 lines) — setup, workflow, key rules, operator reference
  references/pitfalls.md (276 lines) — 24 hard-won lessons
  references/operators.md (239 lines) — all 6 operator families
  references/network-patterns.md (589 lines) — audio-reactive, generative,
    video processing, GLSL, instancing, live performance recipes
  references/mcp-tools.md (501 lines) — 13 MCP tool schemas
  references/python-api.md (443 lines) — TD Python scripting patterns
  references/troubleshooting.md (274 lines) — connection diagnostics
  scripts/custom_api_handler.py (140 lines) — REST API handler for TD
  scripts/setup.sh (152 lines) — prerequisite checker

Tested on TouchDesigner 099 Non-Commercial (macOS/darwin).
2026-04-18 17:43:42 -07:00

11 KiB
Raw Blame History

name description version author license metadata
touchdesigner Control a running TouchDesigner instance programmatically — create operators, set parameters, wire connections, execute Python, build real-time visuals. Covers: GLSL shaders, audio-reactive, generative art, video processing, instancing, and live performance. 3.0.0 Hermes Agent MIT
hermes
tags related_skills security
TouchDesigner
MCP
creative-coding
real-time-visuals
generative-art
audio-reactive
VJ
installation
GLSL
native-mcp
ascii-video
manim-video
hermes-video
allow_network allow_install allow_config_write
true true true

TouchDesigner Integration

Architecture

Hermes Agent -> HTTP REST (curl) -> TD WebServer DAT (port 9981) -> TD Python environment.

The agent controls a running TouchDesigner instance via a REST API on port 9981. It does NOT generate .toe files from scratch.

First-Time Setup (one-time, persists across sessions)

1. Verify TD is running and check for existing API

lsof -i :9981 -P -n | grep LISTEN   # TD listening?
curl -s --max-time 5 http://127.0.0.1:9981/api/td/server/td  # API working?

If HTTP 200 + JSON → skip to Discovery. Setup is already done.

2. If no API: deploy the custom handler

The user must paste ONE line into TD Textport (Alt+T / Dialogs > Textport and DATs):

exec(open('PATH_TO_SKILL/scripts/custom_api_handler.py').read())

Copy this to their clipboard with pbcopy. This creates a WebServer DAT + callback handler pair in /project1 that implements the REST API. No external dependencies.

Why not the official .tox? The mcp_webserver_base.tox from 8beeeaaat/touchdesigner-mcp frequently fails to import its Python modules after drag-drop (relative path resolution issue). Our custom handler is self-contained and more reliable. See references/pitfalls.md #1-2.

3. Save the project to persist the API

After the handler is running, save the project so the API auto-starts on every future TD launch:

td_exec("project.save(os.path.expanduser('~/Documents/HermesAgent.toe'))")

TD auto-opens the last saved project on launch. From now on, open /Applications/TouchDesigner.app → port 9981 is live → agent can connect immediately.

To launch TD with this project explicitly:

open /Applications/TouchDesigner.app ~/Documents/HermesAgent.toe

4. Optional: Configure Hermes MCP

Add under mcp_servers: in the user's Hermes config:

touchdesigner:
  command: npx
  args: ["-y", "touchdesigner-mcp-server@latest"]
  env:
    TD_API_URL: "http://127.0.0.1:9981"
  timeout: 120

This is optional — the agent works fully via curl to the REST API using execute_code. MCP tools are a convenience layer.

Talking to TD (the td_exec pattern)

All communication uses this pattern in execute_code:

import json, shlex
from hermes_tools import terminal

API = "http://127.0.0.1:9981"
def td_exec(script):
    payload = json.dumps({"script": script})
    cmd = f"curl -s --max-time 15 -X POST -H 'Content-Type: application/json' -d {shlex.quote(payload)} '{API}/api/td/server/exec'"
    r = terminal(cmd, timeout=20)
    return json.loads(r['output'])

# Returns: {"result": <value>, "stdout": "...", "stderr": "..."}

For large GLSL shaders: write to a temp file, then td_exec("op('...').text = open('/tmp/shader.glsl').read()").

Workflow

Step 0: Discovery (MANDATORY — never skip)

Never hardcode parameter names. They change between TD versions. Run this first:

td_exec("""
import sys
info = {'version': str(app.version), 'platform': sys.platform}
root = op('/project1')
for name, optype in [('glslTOP', glslTOP), ('constantTOP', constantTOP),
                      ('blurTOP', blurTOP), ('textTOP', textTOP),
                      ('levelTOP', levelTOP), ('compositeTOP', compositeTOP),
                      ('transformTOP', transformTOP), ('feedbackTOP', feedbackTOP),
                      ('windowCOMP', windowCOMP)]:
    n = root.create(optype, '_d_' + name)
    kw = ['color','size','font','dat','alpha','opacity','resolution','text',
          'extend','operand','top','pixel','format','win','type']
    info[name] = [p.name for p in n.pars() if any(k in p.name.lower() for k in kw)]
    n.destroy()
result = info
""")

Use the returned param names for ALL subsequent calls. Store them in your session context.

Step 1: Clean + Build

Build the entire network in ONE td_exec call (batching avoids round-trip overhead and ensures TD advances frames between calls):

td_exec("""
root = op('/project1')
keep = {'api_server', 'api_handler'}
for child in list(root.children):  # snapshot before destroying
    if child.name not in keep and child.valid:
        child.destroy()

# Create nodes, set params (using discovered names), wire, verify
...
result = {'nodes': len(list(root.children)), 'errors': [...]}
""")

Step 2: Wire connections

gl.outputConnectors[0].connect(comp.inputConnectors[0])

Step 3: Verify

for c in list(root.children):
    e = c.errors(); w = c.warnings()
    if e: print(c.name, 'ERR:', e)

Step 4: Display

win = root.create(windowCOMP, 'display')
win.par.winop = out.path    # discovered param name
win.par.winw = 1280; win.par.winh = 720
win.par.winopen.pulse()

Key Implementation Rules

Always clean safely: list(root.children) before iterating + child.valid check.

GLSL time: No uTDCurrentTime in TD 099. Feed time via 1x1 Constant TOP. CRITICAL: must use rgba32float format — the default 8-bit format clamps values to 0-1, so absTime.seconds % 1000.0 becomes 1.0 and the shader appears frozen:

t = root.create(constantTOP, 'time_driver')
t.par.format = 'rgba32float'  # ← REQUIRED or time is stuck at 1.0
t.par.outputresolution = 'custom'
t.par.resolutionw = 1
t.par.resolutionh = 1
t.par.colorr.expr = "absTime.seconds % 1000.0"
t.par.colorg.expr = "int(absTime.seconds / 1000.0)"
t.outputConnectors[0].connect(glsl.inputConnectors[0])
# In GLSL: vec4 td = texture(sTD2DInputs[0], vec2(.5)); float t = td.r + td.g*1000.;

Feedback TOP: Use top parameter reference (not direct input wire). The "Not enough sources" error resolves after first cook. The "Cook dependency loop" warning is expected.

Resolution: Non-Commercial caps at 1280×1280. Use outputresolution = 'custom'.

Large shaders: Write GLSL to /tmp/file.glsl, then td_exec("op('shader').text = open('/tmp/file.glsl').read()").

WebServer DAT quirk: Response body goes in response['data'] not response['body']. Request POST body comes as bytes in request['data'].

Recording / Exporting Video

To capture TD output as video or image sequence for external use (e.g., ASCII video pipeline):

# Put a Null TOP before the recorder (official best practice)
rec = root.create(moviefileoutTOP, 'recorder')
null_out.outputConnectors[0].connect(rec.inputConnectors[0])

rec.par.type = 'movie'
rec.par.file = '/tmp/output.mov'
rec.par.videocodec = 'mjpa'  # Motion JPEG — works on Non-Commercial

# Start/stop recording (par.record is a toggle, NOT .record() method)
rec.par.record = True   # start
# ... wait ...
rec.par.record = False  # stop

H.264/H.265 require a Commercial license — use mjpa (Motion JPEG) or prores on Non-Commercial. Extract frames afterward with ffmpeg if needed:

ffmpeg -i /tmp/output.mov -vframes 120 /tmp/frames/frame_%06d.png

Image Sequence Export

rec.par.type = 'imagesequence'
rec.par.imagefiletype = 'png'
rec.par.file.expr = "'/tmp/frames/out' + me.fileSuffix"  # fileSuffix is REQUIRED
rec.par.record = True

Pitfalls

  • Race condition: When setting par.file and starting recording in the same script, use run("...", delayFrames=2) so the file path is applied before recording begins.
  • TOP.save() is useless for animation: Calling op('null1').save(path) in a loop or rapid API calls captures the same GPU texture every time — TD doesn't cook new frames between save calls. Always use MovieFileOut for animated output.
  • See references/pitfalls.md #25-27 for full details.

Audio-Reactive GLSL (Proven Recipe)

Complete chain for music-driven visuals: AudioFileIn → AudioSpectrum → Math (boost) → Resample (256) → CHOP To TOP → GLSL TOP (spectrum sampled per-pixel). See references/network-patterns.md Pattern 3b for the full working recipe with shader code.

Audio-Reactive Visuals

The most powerful TD workflow for the agent: play an audio file, analyze its spectrum, and drive a GLSL shader in real-time. The agent builds the entire signal chain programmatically.

Signal chain:

AudioFileIn CHOP → AudioSpectrum CHOP → Math CHOP (gain=5)
  → Resample CHOP (256) → CHOP To TOP (spectrum texture)
                                  ↓ (GLSL input 1)
  Constant TOP (rgba32float, time) → GLSL TOP → Null TOP → MovieFileOut
        (input 0)

Key technique: The spectrum becomes a 256×1 texture. In GLSL, texture(sTD2DInputs[1], vec2(x, 0.0)).r samples frequency at position x (0=bass, 1=treble). This lets the shader react per-pixel to different frequency bands.

Smoothing is critical: Raw FFT jitters. Use Math CHOP gain to boost weak signal, then the GLSL shader's own temporal integration (via feedback or time-smoothed params) handles visual smoothing.

See references/network-patterns.md Pattern 9b for the complete build script + shader code.

Operator Quick Reference

Family Color Examples Suffix
TOP Purple noiseTop, glslTop, compositeTop, levelTop, blurTop, textTop, nullTop, feedbackTop, renderTop TOP
CHOP Green audiofileinChop, audiospectrumChop, mathChop, lfoChop, constantChop CHOP
SOP Blue gridSop, sphereSop, transformSop, noiseSop SOP
DAT White textDat, tableDat, scriptDat, webserverDAT DAT
MAT Yellow phongMat, pbrMat, glslMat, constMat MAT
COMP Gray geometryComp, containerComp, cameraComp, lightComp, windowCOMP COMP

See references/operators.md for full catalog. See references/network-patterns.md for recipes.

References

File What
references/pitfalls.md READ FIRST — 31 hard-won lessons from real sessions
references/operators.md All operator families with params and use cases
references/network-patterns.md Recipes: audio-reactive, generative, video, GLSL, instancing
references/mcp-tools.md MCP tool schemas (optional — curl works without MCP)
references/python-api.md TD Python: op(), scripting, extensions
references/troubleshooting.md Connection diagnostics, param debugging, performance
scripts/custom_api_handler.py Self-contained REST API handler for TD WebServer DAT