Files
hermes-skills/ltx-video-pipeline/references/modify-and-re-render-recipe.md
T

119 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 [email protected]:/tmp/
sshpass -p 'passw0rd' ssh [email protected] 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 [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"
```
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 <file>
```
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:<remote_path> <local_path>` 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/<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','?'))
"
```
This works regardless of which output directory the workflow uses. Fallback: `find ~/comfy-ui/output -name "*.mp4" -newer <reference_file> -type f`.