Concurrent Submit | zhenzhen-animate-motion-transfer
What the Concurrent Submit node is actually doing
- input_image
- input_video
- api_config
- task
Concurrent Submit | zhenzhen-animate-motion-transfer is the same motion-transfer node as the normal one, with one behavioral difference that trips up everybody the first time: it doesn't wait for your video. It hands the job to a background worker pool and returns immediately. Your graph keeps moving, and somewhere else a thread is uploading your character image and polling the API.
The whole reason this exists: a motion transfer call takes minutes, and running ten of them one after another means sitting through ten timeouts' worth of waiting. The pack's answer is a bounded thread pool shared across the node pack - 10 slots for video, 30 for images - plus a separate collector node that does the waiting.
How the plumbing works
This class isn't hand-written. The pack walks its own node registry at import time and mints a Submit twin for every eligible image and video node, deep-copying the original INPUT_TYPES. That's why the inputs here are identical, field for field, to zhenzhen-animate-motion-transfer - same image_url / input_image / video_url / input_video, same resolution, ratio, pose_method, same strength dials, same api_config and skip_error. Same validator too.
What changed is the return. Where the original gives you four outputs, this one gives you a single task of type COMFLY_VIDEO_FUTURE - a handle wrapping a concurrent.futures future plus the name of the node that submitted it. That's it. No video comes out of this node, ever.
So you pair it with the collector, Concurrent Collect Videos (10) (ComflyConcurrent_Video_Await):
task_1is required,task_2…task_10are optional, andfailure_modepicks what happens when one slot dies -fail_fast(default) cancels the queue and raises,placeholderhands you empty media for the broken slot and keeps the batch alive. For anything unattended,placeholder.- Outputs are
video_1…video_10in slot order, not completion order, plus astatusstring that's a JSON report per slot:success,failed(with the error), ornot_connected. Unconnected slots come back as empty placeholders. - It polls its pending futures every quarter second, cancels everything if you hit ComfyUI's interrupt button, and shows its own progress bar.
IS_CHANGEDreturnsfloat("nan")so the collector re-runs every queue instead of being cached - the same "always dirty" idiom used across ComfyUI for nodes that must fire each run.
The pool size is configurable but only before startup, because the module reads these once at import:
export COMFLY_VIDEO_CONCURRENCY=4 # default 10
export COMFLY_VIDEO_PENDING=8 # max queued behind the workers (default = pool size)
# restart ComfyUI afterwards - these are read at import time, not per-run
COMFLY_IMAGE_CONCURRENCY / COMFLY_IMAGE_PENDING do the same for the image pool (default 30). Both are clamped to at most 128.
Wiring it up without pain
One Submit node per job. Ten Submit nodes, one collector, task_1 through task_10. Fan the collector's ten outputs to whatever you'd normally do with ten videos.
The mistakes, in the order people make them:
Wiring task straight into Save Video. It's a future, not a video. Nothing renders, no error, just silence. If you see no files, this is why.
Mixing kinds. A COMFLY_IMAGE_FUTURE into the video collector throws Expected video task, got image task. The types are checked, so at least you get told.
Expecting a new render because you re-queued. The Submit node is still subject to ComfyUI's cache: same inputs, same widgets, and you may get the cached submit instead of a new paid API call. The motion-transfer node's seed is explicitly a cache seed that never reaches the API, so it's exactly the knob to nudge. The Qwen image twin is the opposite case - there -1 means "let the API randomize," but 0 is a legal fixed seed and gets sent.
Assuming 10 is safe. The pool is shared by the whole pack in that one ComfyUI process, and 10 simultaneous uploads plus polls is a real load on a reseller endpoint. If you start seeing 429s or stalled polls, drop the env var to 3 or 4 and restart. The author's own README admits their servers get hammered and that some errors are theirs, not yours - but if you launched 30 jobs at once, the first thing to change is your own concurrency.
Assuming the collector catches everything. With fail_fast it deliberately doesn't: the first failure cancels the rest of the queued work and raises. That's a feature when you're watching, a nuisance at 2am.
There's nothing extra to install - these nodes ship inside the pack, so if the normal node is there, both of these are too. After updating the pack, restart ComfyUI: the Submit/Collect mappings are built by iterating the node registry when the module loads, so a hot reload can leave you with stale classes.
Inputs (22)
| Name | Type | Default | Description |
|---|---|---|---|
| image_url | STRING | Leave empty for input_image. | |
| video_url | STRING | Leave empty for input_video. | |
| resolution | COMBO | 720p | 3 options: 480p, 720p, 1080p |
| ratio | COMBO | adaptive | 10 options: adaptive, 1:1, 2:3, 3:2, 3:4, 4:3, +4 |
| custom_ratio | STRING | 16:9 | — |
| frame_rate | INT | 301–999999 | — |
| max_frames | INT | 00–999999 | 0 uses the API default. |
| skip_frames | INT | 00–999999 | — |
| pose_method | COMBO | vitpose | 3 options: vitpose, sdpose, wuwupose |
| normal_mode | BOOLEAN | true | — |
| neck_correction | BOOLEAN | false | — |
| pose_strength | FLOAT | 1.00 | — |
| camera_motion | BOOLEAN | false | — |
| camera_strength | FLOAT | 1.00 | — |
| mask_mode | BOOLEAN | false | — |
| expression_strength | FLOAT | 0.80 | — |
| chest_motion_strength | FLOAT | 0.20 | — |
| input_imageopt | IMAGE | — | |
| input_videoopt | VIDEO | — | |
| api_configopt | ZHENZHEN_SEEDANCE2_CONFIG | — | |
| skip_erroropt | BOOLEAN | false | — |
| seedopt | INT | 00–18446744073709550000 | ComfyUI cache seed only; not sent to the API. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| task | COMFLY_VIDEO_FUTURE | — |