# 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 1. **Copy existing workflow JSONs** to `_v2` variants (preserve originals): ```bash cp /tmp/api_2clip_C1.json /tmp/api_2clip_C1_v2.json cp /tmp/api_2clip_C2.json /tmp/api_2clip_C2_v2.json ``` 2. **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: ```bash # 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 n8n@10.0.0.202:/tmp/ sshpass -p 'passw0rd' ssh n8n@10.0.0.202 python3 /tmp/modify_v3.py ``` **Why scp, not heredoc:** SSH heredoc quoting strips quotes from Python string-keyed dicts, causing `KeyError` on 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. 3. **Verify all changes** before submitting: ```python 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 ``` 4. **Submit to queue:** ```bash 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 ``` 5. **Wait for queue drain** (blocking poll from Hermes host): ```bash sshpass -p 'passw0rd' ssh n8n@10.0.0.202 "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" ``` 6. **Identify new outputs** (search both output directories): ```bash find ~/comfy-ui/output -name "*.mp4" -newer /tmp/api_2clip_C1_v2.json -type f ``` 7. **Verify frame counts:** ```bash ffprobe -v quiet -show_entries stream=nb_frames,codec_name,width,height,duration -of csv=p=0 ``` 8. **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, `sshpass` is not installed on .202. Use `sshpass -p 'passw0rd' scp n8n@10.0.0.202: ` from the Hermes host (pull), not `ssh ... 'sshpass scp ...'` from .202 (push). Confirmed 2026-07-22: push attempt failed with `sshpass: 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 inspect `imageFile` and `prompt` before modifying: ```bash 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])" ``` Confirmed 2026-07-22: pointed delegation at wrong template, produced living room output instead of cyberpunk. - **Must update BOTH node-level AND timeline_data JSON fields.** The LTXDirector reads `segment["length"]` from timeline_data for actual frame count. Node-level `duration_frames` alone won't change output duration. - **121 frames = 8×15+1** satisfies the 8n+1 requirement for LTX. - **5.04 seconds** is the correct `duration_seconds` for 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: ```bash curl -s http://localhost:8188/history/ | python3 -c " import json,sys d=json.load(sys.stdin) entry=d.get('',{}) outputs=entry.get('outputs',{}) node37=outputs.get('37',{}) images=node37.get('images',[]) for img in images: print(img.get('filename','?')) " ``` This works regardless of which output directory the workflow uses. Fallback: `find ~/comfy-ui/output -name "*.mp4" -newer -type f`.