import logging from comfy_api.latest import io from .nodes import _encode_relay from .prompt_relay import get_raw_tokenizer from .parser import parse_smart_prompt log = logging.getLogger(__name__) class PromptRelaySmartEncode(io.ComfyNode): """Parses advanced syntax into Prompt Relay segments and lengths.""" @classmethod def define_schema(cls): return io.Schema( node_id="PromptRelaySmartEncode", display_name="Prompt Relay Encode (Smart)", category="conditioning/prompt_relay", description="Parses syntax like [0-50] or block headers (Second 1:) to automatically calculate segment lengths.", inputs=[ io.Model.Input("model"), io.Clip.Input("clip"), io.Latent.Input("latent"), io.String.Input( "global_prompt", multiline=True, default="", tooltip="Conditions entire video. Leave empty to auto-use the first parsed segment from smart_prompt as the global anchor." ), io.String.Input( "smart_prompt", multiline=True, default="", tooltip="Enter prompt using Smart Syntax:\\n1. Inline: 'text one [0-50] | text two [50-100]'\\n2. Block: 'Second 1:\\ntext one\\nSecond 2:\\ntext two'\\nSyntax is auto-stripped and normalized evenly or proportionally." ), io.Boolean.Input("normalize_by_tokens", default=False, tooltip="If true, scales the calculated length of each segment by its token count."), io.Float.Input("epsilon", default=1e-3, min=1e-6, max=0.99, step=1e-4), ], outputs=[ io.Model.Output(display_name="model"), io.Conditioning.Output(display_name="positive"), ], ) @classmethod def execute(cls, model, clip, latent, global_prompt, smart_prompt, normalize_by_tokens, epsilon) -> io.NodeOutput: parsed = parse_smart_prompt(smart_prompt) valid_segments = [s for s in parsed if s["text"].strip()] if not valid_segments: valid_segments = [{"text": " ", "weight": 1.0}] raw_tokenizer = get_raw_tokenizer(clip) if normalize_by_tokens else None local_prompts_list = [] weights_list = [] for seg in valid_segments: text = seg["text"] weight = seg["weight"] if normalize_by_tokens and raw_tokenizer: try: tokens = raw_tokenizer(text)["input_ids"] has_eos = getattr(raw_tokenizer, "add_eos", False) token_count = len(tokens) - (1 if has_eos else 0) token_count = max(1, token_count) weight *= token_count except Exception as e: log.warning(f"Token counting failed for segment '{text}': {e}") local_prompts_list.append(text) weights_list.append(weight) local_prompts_str = " | ".join(local_prompts_list) scale_factor = 100000.0 segment_lengths_str = ", ".join(str(int(w * scale_factor)) for w in weights_list) global_prompt_str = global_prompt.strip() if not global_prompt_str and valid_segments: global_prompt_str = valid_segments[0]["text"] patched, conditioning = _encode_relay( model, clip, latent, global_prompt_str, local_prompts_str, segment_lengths_str, epsilon ) return io.NodeOutput(patched, conditioning) class PromptRelaySmartEncodeTest(io.ComfyNode): """Test node for Prompt Relay Smart Encode syntax parsing.""" @classmethod def define_schema(cls): return io.Schema( node_id="PromptRelaySmartEncodeTest", display_name="Prompt Relay Smart Encode Test", category="conditioning/prompt_relay", description="Outputs the parsed syntax for testing purposes.", inputs=[ io.String.Input( "smart_prompt", multiline=True, default="", tooltip="Enter prompt using Smart Syntax:\\n1. Inline: 'text one [0-50] | text two [50-100]'\\n2. Block: 'Second 1:\\ntext one\\nSecond 2:\\ntext two'\\nSyntax is auto-stripped and normalized evenly or proportionally." ), io.Boolean.Input("normalize_by_tokens", default=False), io.Clip.Input("clip", optional=True), ], outputs=[ io.String.Output(display_name="parsed_output"), ], ) @classmethod def execute(cls, smart_prompt, normalize_by_tokens, clip=None) -> io.NodeOutput: parsed = parse_smart_prompt(smart_prompt) valid_segments = [s for s in parsed if s["text"].strip()] if not valid_segments: valid_segments = [{"text": " ", "weight": 1.0}] raw_tokenizer = None if normalize_by_tokens and clip is not None: from .prompt_relay import get_raw_tokenizer raw_tokenizer = get_raw_tokenizer(clip) output_lines = [] for i, seg in enumerate(valid_segments): text = seg["text"] weight = seg["weight"] base_weight = weight token_count = None if normalize_by_tokens and raw_tokenizer: try: tokens = raw_tokenizer(text)["input_ids"] has_eos = getattr(raw_tokenizer, "add_eos", False) token_count = len(tokens) - (1 if has_eos else 0) token_count = max(1, token_count) weight *= token_count except Exception: pass line = f"Segment {i+1}: text='{text}', base_weight={base_weight}" if token_count is not None: line += f", tokens={token_count}, final_weight={weight}" output_lines.append(line) return io.NodeOutput("\n".join(output_lines))