Saya Comfy Couple - HiDream - COPY
The HiDream copy node that won't reload a 13.8 GB text encoder every queue
- main_positive
- person_1_positive
- person_2_positive
- negative
- latent
- couple_config
- clip
- positive
- detailer_positive
- negative
- mask_person_1
- mask_person_2
What it's for
SayaComfyCoupleHiDreamCopy is the second half of the HiDream pair. Same native regional conditioning - masked per-region CONDITIONING entries, no model patch - but it consumes a couple_config instead of exposing geometry widgets, so a later pass reproduces the exact regions the first pass used. Layout comes from one place; only the prompts change per pass.
There's a second job, and it's the one that will actually save you time: this node can build the HiDream conditionings itself, lazily and from cache, which is what keeps a quad CLIP that's about 13.8 GB from being loaded on every queue.
How the deferred encode works
You wire four CONDITIONING inputs and a couple_config. In the pack's phased workflow those four arrive empty on purpose - the SDXL phase never encodes HiDream, because paying for that TE residency before you know whether you even want a HiDream refinement pass is a waste. When the four inputs are empty, COPY reads the four original prompt strings out of prompt_bundle_json and materialises the conditionings:
- cache hit - it reads the four conditionings from
<ComfyUI>/user/saya_hidream_conditioning_cache/and never loads a CLIP; - cache miss - it loads the quad CLIP once, encodes all four prompts back to back, writes the conditionings to disk, then unloads the text encoder before diffusion sampling. It only unloads what it loaded itself.
The cache key hashes the final injected prompt texts, the text-encoder file fingerprints and the encoder recipe. That produces a genuinely useful consequence: changing a diffusion-LoRA filename or strength does not invalidate your text conditioning, because the LoRA is a MODEL-side patch and the text is unchanged. Change a prompt and it re-encodes, as it should.
If you wire pre-encoded HiDream conditionings in instead, all of that is skipped and it behaves like the plain regional builder. Both paths are live; pick one.
Inputs and outputs
Required: main_positive, person_1_positive, person_2_positive, negative, latent, couple_config. Optional: context_mode (default inherit - it uses the master's mode; pick a real mode to override it), include_main_contact, add_global_main_entry, the re-encode trio clip / main_text / person_1_text / person_2_text, negative_text, plus the two workflow wires prompt_bundle_json and hidream_trigger.
Outputs: positive → KSampler.positive, detailer_positive → detailers only and never this sampler, negative → the sampler's negative, mask_person_1 and mask_person_2 as pixel-space masks. No config output - COPY consumes one rather than producing one.
Install
cd /path/to/ComfyUI/custom_nodes
git clone https://github.com/alphaziod/saya-comfy-couple-plus.git
Restart, refresh. Once. Two installs means duplicate node IDs - a leftover folder named comfy_saya_couple will do it.
The pack's requirements.txt wants NumPy and Pillow and deliberately does not touch PyTorch. If you're on a GGUF HiDream checkpoint, install ComfyUI-GGUF as well; the node preserves the GGUF patcher subclass rather than fighting it.
Where people get caught
Don't wire your CLIP into clip "just in case". This is the specific trap in this node. A CLIP wired into that socket is evaluated upstream, before COPY gets to check its cache, so you load the quad CLIP on every single queue - exactly the cost the deferred path exists to avoid. The README says it outright: wiring a CLIP is supported for custom graphs, but leave it disconnected to preserve the no-text-encoder-load cache-hit path.
You bypassed the phase's STOP node. The pack's phases end with explicit STOP/unload nodes, and the memory story only holds if they run. Bypass LOAD/STOP rather than deleting them, and let the phase finish.
You expected the trigger on the negative. It isn't applied there. Trigger injection is Base, Person 1 and Person 2 only; SDXL and Naturalize prompts are untouched too. A trigger you typed into an original prompt by hand also stays put if you disable the panel - the node doesn't remove text you wrote.
Empty bundle, empty prompts. If prompt_bundle_json isn't wired from the phase that produced it and the four conditionings are also empty, there's nothing to encode. That's a wiring problem, not a cache problem - the cache just stores what it was given.
Inputs (16)
| Name | Type | Default | Description |
|---|---|---|---|
| main_positive | CONDITIONING | — | |
| person_1_positive | CONDITIONING | — | |
| person_2_positive | CONDITIONING | — | |
| negative | CONDITIONING | — | |
| latent | LATENT | — | |
| couple_config | SAYA_COUPLE_CONFIG | — | |
| context_modeopt | COMBO | inherit | 4 options: inherit, main_plus_person_concat, person_only, main_plus_person_reencoded |
| include_main_contactopt | BOOLEAN | false | — |
| add_global_main_entryopt | BOOLEAN | false | — |
| clipopt | CLIP | — | |
| main_textopt | STRING | — | |
| person_1_textopt | STRING | — | |
| person_2_textopt | STRING | — | |
| negative_textopt | STRING | — | |
| prompt_bundle_jsonopt | STRING | — | |
| hidream_triggeropt | STRING | — |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| positive | CONDITIONING | — |
| detailer_positive | CONDITIONING | — |
| negative | CONDITIONING | — |
| mask_person_1 | MASK | — |
| mask_person_2 | MASK | — |