@bottobot/td-mcp
Version:
TouchDesigner MCP Server v2.8.0 - 21 MCP tools, 629 operators with clean parameter data, 69 Python API classes, 14 tutorials, 32 workflow patterns. Includes version system, experimental techniques KB, core tool enhancements, and experimental build support
105 lines (104 loc) • 22.6 kB
JSON
{
"category": "python-advanced",
"displayName": "Advanced Python",
"description": "Advanced Python programming patterns in TouchDesigner including asyncio for non-blocking operations, tdu.Dependency reactive patterns, threading safety, and numpy/scipy/OpenCV integration for image and data processing.",
"techniques": [
{
"id": "asyncio_in_td",
"name": "asyncio in TouchDesigner",
"subcategory": "asyncio",
"description": "Using Python's asyncio for non-blocking operations inside TouchDesigner. Enables HTTP requests, file I/O, and long-running computations without stalling the cook thread.",
"difficulty": "advanced",
"operators": ["Execute DAT", "Script CHOP", "Script DAT"],
"tags": ["asyncio", "async", "await", "non-blocking", "concurrent", "HTTP", "coroutine"],
"notes": "TouchDesigner runs its own event loop. Use td.asyncio or run coroutines with run() on the existing loop. Blocking calls inside cook() stall the entire TD cook thread — use asyncio for I/O-bound work.",
"code": {
"language": "python",
"filename": "asyncio_td.py",
"snippet": "# asyncio in TouchDesigner\n# TD maintains its own event loop — integrate carefully\n\nimport asyncio\nimport urllib.request\nimport threading\n\n# --- Pattern 1: Run async task in background thread ---\n# TD's cook thread must not be blocked\n# Best practice: run a background thread with its own event loop\n\n_bg_loop = None\n_bg_thread = None\n_results = {}\n\ndef start_background_loop():\n \"\"\"Start a background asyncio event loop in a daemon thread.\"\"\"\n global _bg_loop, _bg_thread\n \n def run_loop():\n global _bg_loop\n _bg_loop = asyncio.new_event_loop()\n asyncio.set_event_loop(_bg_loop)\n _bg_loop.run_forever()\n \n _bg_thread = threading.Thread(target=run_loop, daemon=True)\n _bg_thread.start()\n print('[Async] Background event loop started')\n\nasync def fetch_url_async(url, key):\n \"\"\"Async HTTP fetch — runs in background loop.\"\"\"\n import urllib.request\n loop = asyncio.get_event_loop()\n # Run blocking urllib in executor to keep it truly async\n response = await loop.run_in_executor(\n None,\n lambda: urllib.request.urlopen(url).read().decode('utf-8')\n )\n _results[key] = response\n print(f'[Async] Fetched {url}: {len(response)} chars')\n\ndef fetch_url(url, key='last'):\n \"\"\"Schedule an async fetch from TD code (non-blocking).\"\"\"\n if _bg_loop is None:\n start_background_loop()\n asyncio.run_coroutine_threadsafe(fetch_url_async(url, key), _bg_loop)\n\ndef get_result(key='last'):\n \"\"\"Get fetch result (may be None if not yet complete).\"\"\"\n return _results.get(key)\n\n# Usage in Execute DAT onStart:\n# fetch_url('https://api.example.com/data', 'api_data')\n# In a Script CHOP cook(): result = get_result('api_data')\n\n\n# --- Pattern 2: Using td.ui.undo() context async-safely ---\nasync def delayed_operation(delay_seconds, callback):\n \"\"\"Execute callback after a delay without using run().\"\"\"\n await asyncio.sleep(delay_seconds)\n callback()\n\n# --- Pattern 3: Async WebSocket client ---\nasync def websocket_client(uri, on_message):\n \"\"\"\n Maintain a persistent WebSocket connection asynchronously.\n Call from background loop for non-blocking operation.\n \"\"\"\n try:\n import websockets\n async with websockets.connect(uri) as ws:\n print(f'[WS] Connected to {uri}')\n while True:\n msg = await ws.recv()\n on_message(msg) # call TD code to process message\n except Exception as e:\n print(f'[WS] Disconnected: {e}')"
}
},
{
"id": "tdu_dependency",
"name": "tdu.Dependency Reactive Patterns",
"subcategory": "dependency",
"description": "Using tdu.Dependency to create reactive data bindings in TouchDesigner. Operators and scripts automatically re-cook when dependency values change, enabling clean data-flow architectures.",
"difficulty": "intermediate",
"operators": ["Execute DAT", "Script CHOP", "DAT Execute"],
"tags": ["tdu", "Dependency", "reactive", "binding", "data-flow", "observer"],
"notes": "tdu.Dependency is a built-in TD class that wraps values with change-notification. Any cook() that reads a Dependency will be dirtied automatically when the value changes. Store Dependency objects at module level.",
"code": {
"language": "python",
"filename": "tdu_dependency.py",
"snippet": "# tdu.Dependency Reactive Patterns in TouchDesigner\n\n# --- Basic Dependency ---\n# Create reactive value at module level\nmy_color = tdu.Dependency([1.0, 0.0, 0.0, 1.0]) # RGBA\nmy_scale = tdu.Dependency(1.0)\n\ndef update_color(r, g, b, a=1.0):\n \"\"\"Update the reactive color — all dependents auto-recook.\"\"\"\n my_color.val = [r, g, b, a]\n\ndef update_scale(s):\n my_scale.val = float(s)\n\n# In a Script TOP or GLSL TOP's beforeCook:\n# Read the dependency — this registers a cook dependency\n# col = parent().mod('state_module').my_color.val\n\n\n# --- Advanced: Dependency Graph ---\n# Build a reactive computation graph\n\nclass ReactiveState:\n \"\"\"\n Central state store using tdu.Dependency.\n Any cook that reads from this state will re-cook on changes.\n \"\"\"\n def __init__(self):\n # Primary data\n self.audio_level = tdu.Dependency(0.0)\n self.beat = tdu.Dependency(False)\n self.bpm = tdu.Dependency(120.0)\n \n # Derived reactive values\n self.bass_color = tdu.Dependency([0.0, 0.0, 0.0])\n self.scale = tdu.Dependency(1.0)\n \n # Configuration\n self.base_color = tdu.Dependency([0.2, 0.5, 1.0])\n \n def update_audio(self, level):\n self.audio_level.val = float(level)\n # Update derived values\n self._recompute_color()\n self._recompute_scale()\n \n def _recompute_color(self):\n base = self.base_color.val\n level = self.audio_level.val\n self.bass_color.val = [\n min(1.0, base[0] + level * 0.5),\n base[1] * (1.0 - level * 0.3),\n base[2]\n ]\n \n def _recompute_scale(self):\n self.scale.val = 0.5 + self.audio_level.val * 2.0\n \n def on_beat(self):\n self.beat.val = True\n run('me.parent().mod(\"state\").state.beat.val = False', delayFrames=2)\n\n# Instantiate in a module DAT\n# state = ReactiveState()\n\n# In a Script CHOP cook():\n# level = parent().mod('state_module').state.audio_level.val # registers dep\n# scriptOp['scale'][0] = parent().mod('state_module').state.scale.val\n\n\n# --- Dependency as event bus ---\nclass EventBus:\n \"\"\"Simple pub/sub using tdu.Dependency.\"\"\"\n def __init__(self):\n self._events = {}\n \n def get_event(self, name):\n if name not in self._events:\n self._events[name] = tdu.Dependency(None)\n return self._events[name]\n \n def emit(self, name, data=None):\n self.get_event(name).val = data\n \n def subscribe(self, name):\n \"\"\"Read in cook() to register dependency on this event.\"\"\"\n return self.get_event(name).val\n\n# Global event bus\nbus = EventBus()\n# emit: bus.emit('user_input', {'x': 0.5, 'y': 0.3})\n# receive: data = parent().mod('bus_module').bus.subscribe('user_input')"
}
},
{
"id": "threading_safety",
"name": "Threading Safety in TouchDesigner",
"subcategory": "threading",
"description": "Safely using Python threads alongside TouchDesigner's main cook thread. Patterns for producer-consumer queues, thread-safe data sharing, and avoiding common race conditions.",
"difficulty": "advanced",
"operators": ["Script CHOP", "Execute DAT", "Script TOP"],
"tags": ["threading", "thread-safe", "queue", "concurrent", "producer-consumer", "safety"],
"notes": "TouchDesigner's main cook thread must never be blocked. Do I/O-bound work on background threads. Use queue.Queue for thread-safe communication. Never modify TD parameters from a background thread — schedule via run().",
"code": {
"language": "python",
"filename": "threading_safety.py",
"snippet": "# Thread-Safe Patterns for TouchDesigner\nimport threading\nimport queue\nfrom collections import deque\n\n# --- Thread-Safe Result Queue ---\n# Background thread posts results; TD main thread reads them each frame\n\n_result_queue = queue.Queue(maxsize=100)\n_worker_thread = None\n\ndef start_worker():\n global _worker_thread\n \n def worker():\n while True:\n try:\n # Process work items\n work_item = _work_queue.get(timeout=1.0)\n if work_item is None:\n break # shutdown signal\n \n result = do_heavy_work(work_item)\n _result_queue.put_nowait(result)\n except queue.Empty:\n continue\n except queue.Full:\n pass # drop result if queue full\n \n _worker_thread = threading.Thread(target=worker, daemon=True)\n _worker_thread.start()\n print('[Thread] Worker started')\n\n_work_queue = queue.Queue(maxsize=10)\n\ndef submit_work(item):\n \"\"\"Submit work from main TD thread (non-blocking).\"\"\"\n try:\n _work_queue.put_nowait(item)\n except queue.Full:\n pass # drop if worker is busy\n\ndef do_heavy_work(item):\n \"\"\"Runs in background thread — can block freely.\"\"\"\n import time\n time.sleep(0.01) # simulate work\n return {'processed': item, 'result': item * 2}\n\n# In Script CHOP cook() — drain results non-blocking:\ndef cook(scriptOp):\n # Process up to N results per frame\n for _ in range(5):\n try:\n result = _result_queue.get_nowait()\n # Apply result to parameters (safe: we're in main thread)\n scriptOp['output'][0] = result.get('result', 0.0)\n except queue.Empty:\n break\n\n\n# --- Read-Write Lock for shared data structures ---\nclass RWLock:\n \"\"\"Allow multiple concurrent readers or one writer.\"\"\"\n def __init__(self):\n self._read_ready = threading.Condition(threading.Lock())\n self._readers = 0\n \n def acquire_read(self):\n with self._read_ready:\n self._readers += 1\n \n def release_read(self):\n with self._read_ready:\n self._readers -= 1\n if self._readers == 0:\n self._read_ready.notify_all()\n \n def acquire_write(self):\n self._read_ready.acquire()\n while self._readers > 0:\n self._read_ready.wait()\n \n def release_write(self):\n self._read_ready.release()\n\n\n# --- CRITICAL: Schedule UI/parameter changes on main thread ---\ndef safe_set_param(op_path, param_name, value):\n \"\"\"\n Safely set a TD parameter from a background thread.\n run() schedules execution on the main cook thread.\n \"\"\"\n run(f'op(\"{op_path}\").par.{param_name} = {repr(value)}')"
}
},
{
"id": "numpy_image_processing",
"name": "NumPy Image Processing in TouchDesigner",
"subcategory": "numpy",
"description": "High-performance image processing using NumPy arrays in Script TOP and Script CHOP. Enables custom filters, compositing, data visualization, and real-time image analysis.",
"difficulty": "intermediate",
"operators": ["Script TOP", "Script CHOP"],
"tags": ["numpy", "image", "array", "processing", "matrix", "pixel", "filter"],
"notes": "TOP.numpyArray() returns float32 RGBA arrays with shape (H, W, 4). Values are 0.0..1.0. Use scriptOp.copyNumpyArray() to write output. NumPy is pre-installed in TD's Python.",
"code": {
"language": "python",
"filename": "numpy_processing.py",
"snippet": "# NumPy Image Processing in Script TOP\nimport numpy as np\n\ndef cook(scriptOp):\n if not scriptOp.inputs:\n return\n \n # Read input as float32 RGBA numpy array\n frame = scriptOp.inputs[0].numpyArray(delayed=False)\n if frame is None:\n return\n \n # frame.shape = (height, width, 4) — RGBA, float32, 0..1\n \n # --- Convolution Kernel (box blur 3x3) ---\n def box_blur(img):\n # Pad image for border handling\n padded = np.pad(img, ((1,1),(1,1),(0,0)), mode='edge')\n out = np.zeros_like(img)\n for dy in range(3):\n for dx in range(3):\n out += padded[dy:dy+img.shape[0], dx:dx+img.shape[1]] / 9.0\n return out\n \n # --- Channel Splitting and Processing ---\n r = frame[:,:,0]\n g = frame[:,:,1]\n b = frame[:,:,2]\n a = frame[:,:,3]\n \n # Luminance\n luma = 0.2126 * r + 0.7152 * g + 0.0722 * b\n \n # Sobel Edge Detection on luminance\n def sobel(img):\n # Horizontal\n kx = np.array([[-1,0,1],[-2,0,2],[-1,0,1]], dtype=np.float32)\n ky = kx.T\n from scipy.ndimage import convolve\n gx = convolve(img, kx)\n gy = convolve(img, ky)\n return np.sqrt(gx**2 + gy**2)\n \n # --- Color Matrix Transform ---\n def color_matrix(img, matrix):\n \"\"\"Apply 3x3 color matrix to RGB channels.\"\"\"\n rgb = img[:,:,:3].reshape(-1, 3)\n rgb_out = rgb @ matrix.T\n out = img.copy()\n out[:,:,:3] = np.clip(rgb_out.reshape(img.shape[:2] + (3,)), 0, 1)\n return out\n \n # Sepia matrix\n sepia = np.array([\n [0.393, 0.769, 0.189],\n [0.349, 0.686, 0.168],\n [0.272, 0.534, 0.131]\n ], dtype=np.float32)\n \n # result = color_matrix(frame, sepia)\n \n # --- Vectorized Threshold ---\n threshold = 0.5\n mask = (luma > threshold).astype(np.float32)\n result = frame * mask[:,:,np.newaxis]\n \n scriptOp.copyNumpyArray(result)"
}
},
{
"id": "scipy_signal_processing",
"name": "SciPy Signal Processing",
"subcategory": "scipy",
"description": "Using SciPy for advanced signal processing in TouchDesigner: custom digital filters, FFT analysis, spectrogram generation, curve fitting, and statistical analysis of CHOP data.",
"difficulty": "advanced",
"operators": ["Script CHOP", "Script TOP"],
"tags": ["scipy", "signal", "filter", "FFT", "IIR", "FIR", "spectrogram"],
"notes": "SciPy is pre-installed in TD's Python. Use scipy.signal for digital filters, scipy.fft for Fourier analysis. Apply IIR/FIR filters to CHOP data for custom smoothing and frequency shaping.",
"code": {
"language": "python",
"filename": "scipy_signal.py",
"snippet": "# SciPy Signal Processing in Script CHOP\nimport numpy as np\nfrom scipy import signal\n\n# --- Pre-compute filter coefficients at setup time ---\n_butter_b = None\n_butter_a = None\n_filter_state = None\n\ndef onSetupParameters(scriptOp):\n global _butter_b, _butter_a, _filter_state\n # Design a 4th-order Butterworth low-pass filter\n # cutoff = 10 Hz, sample rate = 30 fps\n fs = 30.0\n cutoff = 5.0 # Hz\n _butter_b, _butter_a = signal.butter(4, cutoff / (fs / 2.0), btype='low')\n _filter_state = signal.lfilter_zi(_butter_b, _butter_a)\n print(f'[SciPy] Butterworth filter: {cutoff}Hz cutoff at {fs}fps')\n\ndef cook(scriptOp):\n global _filter_state\n \n rawChop = op('raw_input') # CHOP with raw noisy data\n if not rawChop or _butter_b is None:\n return\n \n raw_vals = np.array([rawChop[0][i] for i in range(rawChop.numSamples)])\n \n # Apply causal IIR filter\n filtered, _filter_state = signal.lfilter(_butter_b, _butter_a, raw_vals, zi=_filter_state)\n \n for i in range(len(filtered)):\n scriptOp['filtered'][i] = float(filtered[i])\n\n\n# --- Spectrogram Generation (for visualization) ---\ndef generate_spectrogram(audio_signal, fs=48000, nperseg=1024):\n \"\"\"\n Generate a mel-scaled spectrogram from audio data.\n Returns frequency bins, time bins, and power spectrum.\n \"\"\"\n freqs, times, Sxx = signal.spectrogram(\n audio_signal, fs=fs, nperseg=nperseg,\n noverlap=nperseg//2, scaling='spectrum'\n )\n # Convert to dB scale\n Sxx_db = 10 * np.log10(np.maximum(Sxx, 1e-10))\n # Normalize to 0..1\n Sxx_norm = (Sxx_db - Sxx_db.min()) / (Sxx_db.max() - Sxx_db.min() + 1e-8)\n return freqs, times, Sxx_norm\n\n\n# --- Curve Fitting ---\ndef fit_gaussian_to_spectrum(freqs, magnitudes):\n \"\"\"\n Fit a Gaussian to a spectrum peak to find precise frequency and width.\n \"\"\"\n from scipy.optimize import curve_fit\n \n def gaussian(x, amplitude, mean, sigma):\n return amplitude * np.exp(-0.5 * ((x - mean) / sigma) ** 2)\n \n # Initial guess: peak amplitude, peak frequency, width=200Hz\n peak_idx = np.argmax(magnitudes)\n p0 = [magnitudes[peak_idx], freqs[peak_idx], 200.0]\n \n try:\n popt, _ = curve_fit(gaussian, freqs, magnitudes, p0=p0)\n amplitude, center_freq, bandwidth = popt\n return {'amplitude': amplitude, 'center': center_freq, 'bandwidth': bandwidth}\n except RuntimeError:\n return None"
}
},
{
"id": "opencv_integration",
"name": "OpenCV Integration",
"subcategory": "opencv",
"description": "Integrating OpenCV (cv2) into TouchDesigner for computer vision: feature detection, optical flow, background subtraction, contour finding, and camera calibration.",
"difficulty": "intermediate",
"operators": ["Script TOP", "Script CHOP"],
"tags": ["OpenCV", "cv2", "computer-vision", "feature-detection", "optical-flow", "contour"],
"notes": "Install OpenCV into TD's Python: [TD_PYTHON_EXE] -m pip install opencv-python-headless. Note: use headless version to avoid conflicts with TD's own OpenGL/Qt context. OpenCV uses BGR channel order and uint8 by default.",
"code": {
"language": "python",
"filename": "opencv_integration.py",
"snippet": "# OpenCV Integration in Script TOP\n# Install: [TD_PYTHON] -m pip install opencv-python-headless\nimport numpy as np\n\n# --- Feature Detection ---\ndef cook_feature_detect(scriptOp):\n if not scriptOp.inputs: return\n \n frame = scriptOp.inputs[0].numpyArray(delayed=False)\n if frame is None: return\n \n import cv2\n \n # Convert float32 RGBA 0..1 -> uint8 BGR for OpenCV\n rgb_uint8 = (frame[:,:,:3] * 255).astype(np.uint8)\n bgr = cv2.cvtColor(rgb_uint8, cv2.COLOR_RGB2BGR)\n gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)\n \n # Harris Corner Detection\n corners = cv2.cornerHarris(gray.astype(np.float32), blockSize=2, ksize=3, k=0.04)\n corners_norm = cv2.normalize(corners, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n \n # ORB Feature Points\n orb = cv2.ORB_create(nfeatures=500)\n keypoints = orb.detect(gray, None)\n \n # Draw keypoints on output\n output_bgr = cv2.drawKeypoints(bgr, keypoints, None,\n flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)\n output_rgb = cv2.cvtColor(output_bgr, cv2.COLOR_BGR2RGB)\n output_float = output_rgb.astype(np.float32) / 255.0\n output_rgba = np.dstack([output_float, frame[:,:,3]])\n \n scriptOp.copyNumpyArray(output_rgba)\n \n # Store keypoints for CHOP use\n scriptOp.store('keypoints', [(kp.pt[0], kp.pt[1], kp.response) for kp in keypoints])\n\n\n# --- Background Subtraction (MOG2) ---\n_bg_subtractor = None\n\ndef init_bg_subtractor():\n global _bg_subtractor\n import cv2\n _bg_subtractor = cv2.createBackgroundSubtractorMOG2(\n history=500, varThreshold=16, detectShadows=True\n )\n\ndef cook_bg_subtraction(scriptOp):\n global _bg_subtractor\n if _bg_subtractor is None:\n init_bg_subtractor()\n \n if not scriptOp.inputs: return\n frame = scriptOp.inputs[0].numpyArray(delayed=False)\n if frame is None: return\n \n import cv2\n rgb_uint8 = (frame[:,:,:3] * 255).astype(np.uint8)\n bgr = cv2.cvtColor(rgb_uint8, cv2.COLOR_RGB2BGR)\n \n fg_mask = _bg_subtractor.apply(bgr)\n fg_float = fg_mask.astype(np.float32) / 255.0\n output = np.stack([fg_float, fg_float, fg_float, np.ones_like(fg_float)], axis=-1)\n \n scriptOp.copyNumpyArray(output)\n\n\n# --- Optical Flow (Dense, Farneback) ---\n_prev_gray = None\n\ndef cook_optical_flow(scriptOp):\n global _prev_gray\n if not scriptOp.inputs: return\n \n frame = scriptOp.inputs[0].numpyArray(delayed=False)\n if frame is None: return\n \n import cv2\n rgb_uint8 = (frame[:,:,:3] * 255).astype(np.uint8)\n gray = cv2.cvtColor(rgb_uint8, cv2.COLOR_RGB2GRAY)\n \n if _prev_gray is None:\n _prev_gray = gray\n return\n \n flow = cv2.calcOpticalFlowFarneback(\n _prev_gray, gray, None,\n pyr_scale=0.5, levels=3, winsize=15,\n iterations=3, poly_n=5, poly_sigma=1.2, flags=0\n )\n _prev_gray = gray\n \n # flow shape: (H, W, 2) — x and y flow vectors\n # Convert to HSV for visualization\n mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])\n hsv = np.zeros((*gray.shape, 3), dtype=np.uint8)\n hsv[...,0] = ang * 180 / np.pi / 2 # Hue = direction\n hsv[...,1] = 255\n hsv[...,2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n rgb_out = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)\n \n output = rgb_out.astype(np.float32) / 255.0\n output_rgba = np.dstack([output, np.ones(output.shape[:2], dtype=np.float32)])\n scriptOp.copyNumpyArray(output_rgba)\n \n # Store flow statistics\n scriptOp.store('flow_magnitude_mean', float(mag.mean()))\n scriptOp.store('flow_magnitude_max', float(mag.max()))"
}
}
],
"resources": [
{ "title": "tdu.Dependency Documentation", "url": "https://docs.derivative.ca/Dependency" },
{ "title": "NumPy Documentation", "url": "https://numpy.org/doc/stable/" },
{ "title": "SciPy Signal Processing", "url": "https://docs.scipy.org/doc/scipy/reference/signal.html" },
{ "title": "OpenCV Python Tutorials", "url": "https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html" },
{ "title": "Python asyncio Documentation", "url": "https://docs.python.org/3/library/asyncio.html" }
]
}