5.8 KiB
Modify-and-Re-Render Recipe
Proven pattern for changing settings on existing workflow JSONs and re-rendering without rebuilding from scratch. Used 2026-07-22 for the sampler+duration 2-clip test.
When to Use
- Changing sampler, steps, scheduler, duration, or other render settings
- Testing a new LoRA or model chain variant on existing scenes
- Re-running a failed batch with corrected settings
Steps
-
Copy existing workflow JSONs to
_v2variants (preserve originals):cp /tmp/api_2clip_C1.json /tmp/api_2clip_C1_v2.json cp /tmp/api_2clip_C2.json /tmp/api_2clip_C2_v2.json -
Modify in-place with Python via scp (SSH heredoc quoting is fragile — scp the script, then run it):
Write the modification script locally, scp it to .202, then execute:
# Write script locally cat > /tmp/modify_v3.py << 'PYEOF' import json for fname in ["/tmp/api_boss_C1_v3.json", "/tmp/api_boss_C2_v3.json"]: with open(fname) as f: d = json.load(f) p = d["prompt"] # Sampler change (node 20 is KSamplerSelect, not KSampler) p["20"]["inputs"]["sampler_name"] = "euler" # Duration change (node-level) p["131"]["inputs"]["duration_frames"] = 121 p["131"]["inputs"]["end_frame"] = 121 p["131"]["inputs"]["duration_seconds"] = 5.04 # Duration change (timeline_data JSON — CRITICAL, don't skip) td = json.loads(p["131"]["inputs"]["timeline_data"]) td["normalDurationFrames"] = 121 for seg in td["segments"]: seg["length"] = 121 seg["end"] = 5.04 p["131"]["inputs"]["timeline_data"] = json.dumps(td) with open(fname, "w") as f: json.dump(d, f, indent=2) print(f"Updated {fname}: sampler={p['20']['inputs']['sampler_name']}") PYEOF # scp to .202 and run sshpass -p 'passw0rd' scp /tmp/modify_v3.py [email protected]:/tmp/ sshpass -p 'passw0rd' ssh [email protected] python3 /tmp/modify_v3.pyWhy scp, not heredoc: SSH heredoc quoting strips quotes from Python string-keyed dicts, causing
KeyErroron nodes like"20". The scp approach preserves the script exactly as written. Confirmed 2026-07-22: heredoc failed twice (KeyError on node 20); scp worked first try. -
Verify all changes before submitting:
for fname in files: d = json.load(open(fname)) p = d["prompt"] assert p["20"]["inputs"]["sampler_name"] == "euler_ancestral_cfg_pp" assert p["131"]["inputs"]["duration_frames"] == 121 td = json.loads(p["131"]["inputs"]["timeline_data"]) assert td["segments"][0]["length"] == 121 -
Submit to queue:
curl -s -X POST http://localhost:8188/prompt -H "Content-Type: application/json" -d @/tmp/api_2clip_C1_v2.json curl -s -X POST http://localhost:8188/prompt -H "Content-Type: application/json" -d @/tmp/api_2clip_C2_v2.json -
Wait for queue drain (blocking poll from Hermes host):
sshpass -p 'passw0rd' ssh [email protected] "while true; do q=\$(curl -s http://localhost:8188/queue | python3 -c \"import json,sys; d=json.load(sys.stdin); print(len(d['queue_running'])+len(d['queue_pending']))\"); if [ \"\$q\" -eq 0 ]; then echo 'QUEUE_EMPTY'; break; fi; echo \"queue: \$q\"; sleep 30; done" -
Identify new outputs (search both output directories):
find ~/comfy-ui/output -name "*.mp4" -newer /tmp/api_2clip_C1_v2.json -type f -
Verify frame counts:
ffprobe -v quiet -show_entries stream=nb_frames,codec_name,width,height,duration -of csv=p=0 <file> -
Concat, scp, upload to TrueNAS (standard pipeline steps 7-8).
Key Pitfalls
- sshpass is NOT on .202 — pull files from Hermes host instead of pushing from .202. When transferring output files from .202 to the Hermes host,
sshpassis not installed on .202. Usesshpass -p 'passw0rd' scp [email protected]:<remote_path> <local_path>from the Hermes host (pull), notssh ... 'sshpass scp ...'from .202 (push). Confirmed 2026-07-22: push attempt failed withsshpass: command not found. - CRITICAL: Verify you have the RIGHT template before modifying.
/tmp/accumulates multiple generations with confusingly similar names:api_2clip_C1.json(old living room),api_cyberpunk_C1.json(cyberpunk),api_2clip_01.json(earlier variant). ALWAYS inspectimageFileandpromptbefore modifying:Confirmed 2026-07-22: pointed delegation at wrong template, produced living room output instead of cyberpunk.python3 -c "import json; d=json.load(open('/tmp/api_XXX.json')); td=json.loads(d['prompt']['131']['inputs']['timeline_data']); print(td['segments'][0]['imageFile'], td['segments'][0]['prompt'][:80])" - Must update BOTH node-level AND timeline_data JSON fields. The LTXDirector reads
segment["length"]from timeline_data for actual frame count. Node-levelduration_framesalone won't change output duration. - 121 frames = 8×15+1 satisfies the 8n+1 requirement for LTX.
- 5.04 seconds is the correct
duration_secondsfor 121 frames at 24fps (121/24 ≈ 5.0417). - Verify with ffprobe, not by file size. File size varies with content complexity; frame count is the ground truth.
- Use ComfyUI history API to identify outputs reliably. After queue drains, query each prompt_id:
This works regardless of which output directory the workflow uses. Fallback:
curl -s http://localhost:8188/history/<prompt_id> | python3 -c " import json,sys d=json.load(sys.stdin) entry=d.get('<prompt_id>',{}) outputs=entry.get('outputs',{}) node37=outputs.get('37',{}) images=node37.get('images',[]) for img in images: print(img.get('filename','?')) "find ~/comfy-ui/output -name "*.mp4" -newer <reference_file> -type f.