Repository navigation
[modular] ConditionalPipelineBlocks should declare its own trigger inputs #15101
Description
When an input exists only to decide which branch a conditional block runs, no sub-block reads it, so nothing declares it. ConditionalPipelineBlocks.inputs only merges the sub-blocks' inputs and does not add the block's own block_trigger_inputs. The repo has three patterns for this:
-
Declare it nowhere. Ernie's
use_peis only listed inblock_trigger_inputs:class ErnieImageAutoPromptEnhancerStep(ConditionalPipelineBlocks): block_trigger_inputs = ["use_pe"] def select_block(self, use_pe=None): return "prompt_enhancer" if use_pe else None
use_peis missing frominputs, from the generated docstring and fromdefault_call_parameters. And it does not work at call time either:ModularPipeline.__call__only copies declared inputs into the state and warns about the rest, sopipe(prompt=..., use_pe=True)prints "Unexpected input 'dict_keys(['use_pe'])' provided. This input will be ignored.", the conditional block readsNonefrom the state, and the prompt enhancer never runs. Only the staticget_execution_blocks(prompt=True, use_pe=True)resolves to it, because that takes kwargs directly. -
Override
inputson the wrapper. Cosmos 3'senable_sound:class Cosmos3AutoCoreDenoiseStep(ConditionalPipelineBlocks): block_trigger_inputs = ["action", "enable_sound", "control_videos"] @property def inputs(self): inputs = super().inputs inputs.append(InputParam(name="enable_sound", type_hint=bool, default=False, description="...")) return inputs
The input is listed at the top level with a type, a default and a description. And because
enable_soundis also a workflow input,get_workflowdoes the right thing with it:>>> blocks = Cosmos3OmniBlocks() >>> wf = blocks.get_workflow("text2video_with_sound") >>> [name for name in wf.sub_blocks if "sound" in name] ['denoise.prepare_sound_latents', 'denoise.pack_sound_sequence', 'denoise.prepare_sound_denoiser_inputs', 'decode.sound'] >>> "enable_sound" in [p.name for p in wf.inputs] False
The workflow resolves to the sound blocks, and the resolved workflow does not list
enable_soundas an input, which is correct: none of the resolved blocks reads it, the workflow entry has already decided it.get_execution_blocksworks the same way,get_execution_blocks(prompt=True, enable_sound=True)resolves to the same sound blocks, sinceget_workflowis justget_execution_blockscalled with the workflow entry. Ifenable_soundwere not a workflow input,get_workflowwould resolve the wrapper with the default and the input would be lost together with it, see [modular] conditional block triggers should be workflow inputs #15100. -
Declare it on every leaf. LTX-2's
enable_prompt_enhancementis read only byLTX2AutoPromptEnhancerStep.select_block, but it is declared as anInputParamon each of the three prompt-enhancer leaves it chooses between:class LTX2PromptEnhancerStep(ModularPipelineBlocks): @property def inputs(self): return [ InputParam.template("prompt", required=True), InputParam("enable_prompt_enhancement", type_hint=bool, default=False, description="..."), ..., ]
The leaves carry an input they never read, each new leaf has to repeat the declaration, and a resolved workflow that keeps such a leaf lists the input although nothing on that workflow acts on it.
I think the Cosmos 3 pattern is the right one: the wrapper is the block that reads the input, so the wrapper declares it, with a type, a default and a description like any other input, so it shows up in inputs, in the generated docstring and in default_call_parameters. The framework should make this the documented way instead of leaving it to each blockset to work out. The Cosmos 3 inputs override is the shape of it; the base class could offer a small hook for it so the override does not have to re-merge the sub-blocks' inputs, but that is a detail.
Posted by Claude Code on behalf of @yiyixuxu, who reviewed and approved this text.
Activity