Publishing & Client Handoff for TouchDesigner Projects
Package, document, and hand off a professional TouchDesigner installation with consolidated assets, a custom Perform UI, automated startup, remote monitoring, and full client documentation.
The last 20% of a TD project — packaging, handoff, and long-term support — is where most technical directors underinvest. A client who can’t restart the installation without calling you is not a successful deployment. A show file that only runs on your development laptop is not a deliverable. This article covers the full handoff process.
Consolidating Assets
TD’s File > Consolidate Assets (or File > Backup > Consolidate) copies all externally referenced files — movies, audio, GLSL files, Python modules, images, .tox operators — into the project directory. Run this before every delivery so the project is self-contained.
After consolidation, verify by temporarily renaming the original asset directories and relaunching TD. Any missing file references will show as errors in the console. Common items that consolidation misses:
- Absolute paths hardcoded in Python scripts (
/data/my_video.movinstead ofproject.folder + '/assets/my_video.mov') - Files referenced inside
.toxcomponent files that were built on a different machine - Python packages imported at module level from system-wide
site-packagesrather than TD’s bundled packages
Fix absolute paths before consolidation by using TD’s built-in path helpers:
# Correct — relative to .toe file
video_path = project.folder + '/assets/videos/intro.mp4'
# Also correct — using app.samplesFolder for TD's built-in samples
font_path = app.samplesFolder + '/Fonts/ClearSans-Regular.ttf'
# Wrong — breaks on any machine that isn't yours
video_path = 'C:/Users/username/Documents/MyProject/assets/intro.mp4'
Documenting Custom .tox Operators
Any .tox you hand off should be self-documenting. Open the .tox’s component editor, go to the Custom Parameters page, and add a Help page:
- Right-click any parameter → Add Parameter → choose type
- In the parameter’s page properties, add a Help DAT reference containing parameter descriptions
- Use the component’s Info section to document inputs, outputs, and expected cook behaviour
For complex operators, write an Info CHOP channel list that describes what each output channel means — this appears in TD’s Info popup when the operator is selected:
# In a CPlusPlus CHOP or Script CHOP info callback
def getInfoCHOPChan(index):
descriptions = [
('status', 'Connection status: 1=connected, 0=disconnected'),
('latency_ms', 'Round-trip network latency in milliseconds'),
('packets_rx', 'Total packets received since startup'),
]
if index < len(descriptions):
return descriptions[index]
Writing a Technical Rider
A technical rider is the hardware specification document that the venue or client must satisfy before install day. It is not a wish list — it is a contract. Include:
TECHNICAL RIDER — [Project Name] v1.2
Date: [Date]
COMPUTING
GPU: NVIDIA RTX 4080 (16 GB VRAM) or equivalent
Acceptable alternatives: RTX 4090, RTX A4000, RTX A5000
NOT acceptable: AMD GPUs (CUDA dependencies), any <12 GB VRAM card
CPU: 12-core Intel Core i9-13900K or AMD Ryzen 9 7950X
RAM: 64 GB DDR5 (minimum 32 GB DDR4)
Storage: 500 GB NVMe SSD (Samsung 970 Pro or equivalent)
OS: Windows 11 Pro 22H2, clean install, no antivirus, no automatic updates
TD Build: TouchDesigner 2023.11600 (exact build required — not newer, not older)
NETWORKING
Dedicated gigabit switch (unmanaged or IGMP-snooping only)
No internet connection during show — offline only
Fixed IP: 192.168.1.10 (master), .11, .12 (slaves)
DISPLAY
Output connector: DisplayPort 1.4 (adapters to HDMI 2.1 acceptable, not DP 1.2)
Target display: [Projector model and keystone corrections pre-calibrated by venue]
POWER
UPS for each machine: minimum 15-minute runtime at 1200W peak draw
All machines on same phase to avoid ground loops
Building a Perform-Mode UI
The Perform UI is what a non-technical operator sees and uses during the show. Design it for clarity over density:
# In the Perform UI Base COMP — create large, clearly labelled controls
class PerformUI:
def __init__(self, owner):
self.owner = owner
def build_ui(self):
"""Called once to construct the Perform Mode UI layout."""
# Use Container COMP with Button COMPs and Slider COMPs
# Each button gets a meaningful label and confirmation dialog for destructive actions
pass
def on_next_cue(self):
"""Called by NEXT CUE button."""
op('/project1/cue_system/cue_runner').go(None)
# Visual feedback: flash the button green briefly
btn = self.owner.op('btn_next_cue')
btn.par.Bgcolor1r = 0.2
btn.par.Bgcolor1g = 0.8
btn.par.Bgcolor1b = 0.2
run("args[0].par.Bgcolor1r = 0.15; args[0].par.Bgcolor1g = 0.15; args[0].par.Bgcolor1b = 0.15",
btn, delayFrames=10)
def on_emergency_black(self):
"""Emergency blackout — mutes all output immediately."""
op('/project1/output/master_level').par.Value0 = 0.0
print(f'[PERFORM] Emergency black at {absTime.seconds:.1f}s')
Design principles for the Perform UI:
- Minimum button size: 120×60 pixels — operators work in the dark with adrenaline
- Colour code by function: green = go/advance, red = blackout/stop, yellow = warning states
- Show current cue name and next cue name prominently
- Add a hardware health summary: GPU temp, cook fps, any disconnected sources
- Include an emergency blackout button that requires no confirmation
Remote Monitoring via Web Server DAT
The Web Server DAT (Operators > DAT > Web Server) lets you serve a status page accessible from any browser on the local network — useful for show technicians monitoring from FOH or a backstage tablet.
# In the Web Server DAT's Callbacks DAT
def onHTTPRequest(webServerDAT, request, response):
if request['uri'] == '/status':
import json
status = {
'timestamp': absTime.seconds,
'fps': round(1.0 / max(me.cookTime, 0.001), 1),
'current_scene': op('/project1/master_controller').par.Sceneindex.val,
'gpu_temp': get_gpu_temp(),
'ndi_sources': check_ndi_sources(),
'data_age_seconds': absTime.seconds - op('data_store')['last_update'],
}
response['statusCode'] = 200
response['statusReason'] = 'OK'
response['data'] = json.dumps(status)
response['headers'] = {'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'}
return response
elif request['uri'] == '/':
# Serve a simple HTML dashboard
response['statusCode'] = 200
response['data'] = build_dashboard_html()
response['headers'] = {'Content-Type': 'text/html'}
return response
def get_gpu_temp():
import subprocess
result = subprocess.run(['nvidia-smi', '--query-gpu=temperature.gpu',
'--format=csv,noheader'], capture_output=True, text=True, timeout=2)
return int(result.stdout.strip()) if result.returncode == 0 else -1
Set the Web Server DAT to port 8080, start it in a startup Execute DAT, and give the client the URL http://[machine-ip]:8080 for the dashboard.
Automated Startup
Create a Windows Task Scheduler task that launches TD on system boot:
@echo off
REM td_launch.bat — place in C:\Show\
REM Wait for Windows to finish booting (GPU drivers, etc.)
timeout /t 30 /nobreak >nul
REM Set machine role environment variable
set TD_MACHINE_ROLE=master
REM Launch TD in Perform mode, load the show file
"C:\Program Files\Derivative\TouchDesigner\TouchDesigner.exe" ^
"C:\Show\show.toe" ^
--perform ^
--fullscreen ^
--monitor 1
REM If TD exits (crash or deliberate), wait and relaunch
timeout /t 5 /nobreak >nul
goto :restart_label
:restart_label
echo [%date% %time%] Relaunching TouchDesigner...
"C:\Program Files\Derivative\TouchDesigner\TouchDesigner.exe" ^
"C:\Show\show.toe" ^
--perform ^
--fullscreen ^
--monitor 1
In Task Scheduler: create a new task, trigger = At Startup, action = run C:\Show\td_launch.bat, run with highest privileges, check “Run whether user is logged on or not”.
Version Archiving with Git LFS
TD’s .toe files are binary — standard Git diffs are useless for them. Use Git LFS (Large File Storage) to version binary assets while keeping your repo functional:
# One-time setup
git lfs install
git lfs track "*.toe"
git lfs track "*.tox"
git lfs track "*.mov"
git lfs track "*.mp4"
git lfs track "*.wav"
echo "*.glsl" >> .gitattributes # Text GLSL files — normal git tracking is fine
git add .gitattributes
git commit -m "Add Git LFS tracking for binary TD files"
For installations without internet access (no GitHub), use folder-based versioning with a Python script that creates timestamped archives:
# archive_version.py — run manually before each show day
import shutil, datetime, os
project_dir = r'C:\Show'
archive_dir = r'D:\Show_Archives'
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M')
dest = os.path.join(archive_dir, f'show_{timestamp}')
shutil.copytree(project_dir, dest)
print(f'Archived to {dest}')
The Complete Handoff Package
Deliver as a USB drive or network share with this structure:
SHOW_HANDOFF_v1.2/
show/ ← Consolidated TD project
show.toe
assets/
td_launch.bat
td_watchdog.bat
documentation/
technical_rider.pdf
quick_start_guide.pdf ← 2-page laminated card for operators
network_diagram.pdf
parameter_guide.pdf ← What each knob/slider does
installers/
TouchDesigner_2023.11600.exe
NVIDIA_Driver_537.13.exe
NDI_Tools_5.5.exe
monitoring/
status_url.txt ← http://192.168.1.10:8080
video_walkthrough/
01_startup_procedure.mp4 ← 5-minute screen recording
02_daily_operation.mp4
03_troubleshooting.mp4
The video walkthrough is the single most valuable piece of documentation — a 5-minute recording of you operating the show, talking through what you’re doing and why, is worth more than 20 pages of written documentation for a non-technical client. Record it in Perform Mode showing the actual UI they will use. Cover the three most common failure cases (NDI source drops, the show file won’t open, the wrong monitor is full-screen) with on-camera solutions.
The quick_start_guide.pdf is a two-page laminated card that lives at the installation’s control point permanently. It contains: startup procedure (numbered steps with screenshots), shutdown procedure, one emergency contact number, and the three most common problems with their solutions. Nothing more — brevity is the point.