import json import logging import random from flask import session from models import Settings, Checkpoint from utils import _resolve_lora_weight from services.prompts import _cross_dedup_prompts logger = logging.getLogger('gaze') # --------------------------------------------------------------------------- # ComfyUI workflow node IDs (must match comfy_workflow.json) # --------------------------------------------------------------------------- NODE_KSAMPLER = "3" NODE_CHECKPOINT = "4" NODE_LATENT = "5" NODE_POSITIVE = "6" NODE_NEGATIVE = "7" NODE_VAE_DECODE = "8" NODE_SAVE = "9" NODE_FACE_DETAILER = "11" NODE_HAND_DETAILER = "13" NODE_FACE_PROMPT = "14" NODE_HAND_PROMPT = "15" NODE_LORA_CHAR = "16" NODE_LORA_OUTFIT = "17" NODE_LORA_ACTION = "18" NODE_LORA_STYLE = "19" NODE_LORA_CHAR_B = "20" NODE_VAE_LOADER = "21" # Node IDs used by DetailerForEach in multi-char mode _SEGS_DETAILER_NODES = ['46', '47', '53', '54'] # Node IDs for per-character CLIP prompts in multi-char mode _SEGS_PROMPT_NODES = ['44', '45', '51', '52'] def _log_workflow_prompts(label, workflow): """Log the final assembled ComfyUI prompts in a consistent, readable block.""" sep = "=" * 72 active_loras = [] lora_details = [] # Collect detailed LoRA information for node_id, label_str in [(NODE_LORA_CHAR, "char/look"), (NODE_LORA_OUTFIT, "outfit"), (NODE_LORA_ACTION, "action"), (NODE_LORA_STYLE, "style/detail/scene"), (NODE_LORA_CHAR_B, "char_b")]: if node_id in workflow: name = workflow[node_id]["inputs"].get("lora_name", "") if name: strength_model = workflow[node_id]["inputs"].get("strength_model", "?") strength_clip = workflow[node_id]["inputs"].get("strength_clip", "?") # Short version for summary if isinstance(strength_model, float): active_loras.append(f"{label_str}:{name.split('/')[-1]}@{strength_model:.3f}") else: active_loras.append(f"{label_str}:{name.split('/')[-1]}@{strength_model}") # Detailed version lora_details.append(f" Node {node_id} ({label_str}): {name}") lora_details.append(f" strength_model={strength_model}, strength_clip={strength_clip}") # Extract VAE information vae_info = "(integrated)" if NODE_VAE_LOADER in workflow: vae_info = workflow[NODE_VAE_LOADER]['inputs'].get('vae_name', '(custom)') # Extract adetailer information adetailer_info = [] # Single-char mode: FaceDetailer nodes 11 + 13 for node_id, node_name in [(NODE_FACE_DETAILER, "Face"), (NODE_HAND_DETAILER, "Hand")]: if node_id in workflow: adetailer_info.append(f" {node_name} (Node {node_id}): steps={workflow[node_id]['inputs'].get('steps', '?')}, " f"cfg={workflow[node_id]['inputs'].get('cfg', '?')}, " f"denoise={workflow[node_id]['inputs'].get('denoise', '?')}") # Multi-char mode: SEGS DetailerForEach nodes for node_id, node_name in [("46", "Person A"), ("47", "Person B"), ("53", "Face A"), ("54", "Face B")]: if node_id in workflow: adetailer_info.append(f" {node_name} (Node {node_id}): steps={workflow[node_id]['inputs'].get('steps', '?')}, " f"cfg={workflow[node_id]['inputs'].get('cfg', '?')}, " f"denoise={workflow[node_id]['inputs'].get('denoise', '?')}") face_text = workflow.get(NODE_FACE_PROMPT, {}).get('inputs', {}).get('text', '') hand_text = workflow.get(NODE_HAND_PROMPT, {}).get('inputs', {}).get('text', '') lines = [ sep, f" WORKFLOW PROMPTS [{label}]", sep, " MODEL CONFIGURATION:", f" Checkpoint : {workflow[NODE_CHECKPOINT]['inputs'].get('ckpt_name', '(not set)')}", f" VAE : {vae_info}", "", " GENERATION SETTINGS:", f" Seed : {workflow[NODE_KSAMPLER]['inputs'].get('seed', '(not set)')}", f" Resolution : {workflow[NODE_LATENT]['inputs'].get('width', '?')} x {workflow[NODE_LATENT]['inputs'].get('height', '?')}", f" Sampler : {workflow[NODE_KSAMPLER]['inputs'].get('sampler_name', '?')} / {workflow[NODE_KSAMPLER]['inputs'].get('scheduler', '?')}", f" Steps : {workflow[NODE_KSAMPLER]['inputs'].get('steps', '?')}", f" CFG Scale : {workflow[NODE_KSAMPLER]['inputs'].get('cfg', '?')}", f" Denoise : {workflow[NODE_KSAMPLER]['inputs'].get('denoise', '1.0')}", ] # Add LoRA details if active_loras: lines.append("") lines.append(" LORA CONFIGURATION:") lines.extend(lora_details) else: lines.append("") lines.append(" LORA CONFIGURATION: (none)") # Add adetailer details if adetailer_info: lines.append("") lines.append(" ADETAILER CONFIGURATION:") lines.extend(adetailer_info) # Add prompts lines.extend([ "", " PROMPTS:", f" [+] Positive : {workflow[NODE_POSITIVE]['inputs'].get('text', '')}", f" [-] Negative : {workflow[NODE_NEGATIVE]['inputs'].get('text', '')}", ]) if face_text: lines.append(f" [F] Face : {face_text}") if hand_text: lines.append(f" [H] Hand : {hand_text}") # Multi-char per-character prompts for node_id, lbl in [("44", "Person A"), ("45", "Person B"), ("51", "Face A"), ("52", "Face B")]: txt = workflow.get(node_id, {}).get('inputs', {}).get('text', '') if txt: lines.append(f" [{lbl}] : {txt}") lines.append(sep) logger.info("\n%s", "\n".join(lines)) def _apply_checkpoint_settings(workflow, ckpt_data): """Apply checkpoint-specific sampler/prompt/VAE settings to the workflow.""" steps = ckpt_data.get('steps') cfg = ckpt_data.get('cfg') sampler_name = ckpt_data.get('sampler_name') scheduler = ckpt_data.get('scheduler') base_positive = ckpt_data.get('base_positive', '') base_negative = ckpt_data.get('base_negative', '') vae = ckpt_data.get('vae', 'integrated') # KSampler (node 3) if steps and NODE_KSAMPLER in workflow: workflow[NODE_KSAMPLER]['inputs']['steps'] = int(steps) if cfg and NODE_KSAMPLER in workflow: workflow[NODE_KSAMPLER]['inputs']['cfg'] = float(cfg) if sampler_name and NODE_KSAMPLER in workflow: workflow[NODE_KSAMPLER]['inputs']['sampler_name'] = sampler_name if scheduler and NODE_KSAMPLER in workflow: workflow[NODE_KSAMPLER]['inputs']['scheduler'] = scheduler # Face/hand detailers (nodes 11, 13) + multi-char SEGS detailers for node_id in [NODE_FACE_DETAILER, NODE_HAND_DETAILER] + _SEGS_DETAILER_NODES: if node_id in workflow: if steps: workflow[node_id]['inputs']['steps'] = int(steps) if cfg: workflow[node_id]['inputs']['cfg'] = float(cfg) if sampler_name: workflow[node_id]['inputs']['sampler_name'] = sampler_name if scheduler: workflow[node_id]['inputs']['scheduler'] = scheduler # Prepend base_positive to all positive prompt nodes if base_positive: for node_id in [NODE_POSITIVE, NODE_FACE_PROMPT, NODE_HAND_PROMPT] + _SEGS_PROMPT_NODES: if node_id in workflow: workflow[node_id]['inputs']['text'] = f"{base_positive}, {workflow[node_id]['inputs']['text']}" # Append base_negative to negative prompt (shared by main + detailers via node 7) if base_negative and NODE_NEGATIVE in workflow: workflow[NODE_NEGATIVE]['inputs']['text'] = f"{workflow[NODE_NEGATIVE]['inputs']['text']}, {base_negative}" # VAE: if not integrated, inject a VAELoader node and rewire if vae and vae != 'integrated': workflow[NODE_VAE_LOADER] = { 'inputs': {'vae_name': vae}, 'class_type': 'VAELoader' } if NODE_VAE_DECODE in workflow: workflow[NODE_VAE_DECODE]['inputs']['vae'] = [NODE_VAE_LOADER, 0] for node_id in [NODE_FACE_DETAILER, NODE_HAND_DETAILER] + _SEGS_DETAILER_NODES: if node_id in workflow: workflow[node_id]['inputs']['vae'] = [NODE_VAE_LOADER, 0] return workflow def _get_default_checkpoint(): """Return (checkpoint_path, checkpoint_data) from the database Settings, session, or fall back to workflow file.""" ckpt_path = session.get('default_checkpoint') # If no session checkpoint, try to read from database Settings if not ckpt_path: settings = Settings.query.first() if settings and settings.default_checkpoint: ckpt_path = settings.default_checkpoint logger.debug("Loaded default checkpoint from database: %s", ckpt_path) # If still no checkpoint, try to read from the workflow file if not ckpt_path: try: with open('comfy_workflow.json', 'r') as f: workflow = json.load(f) ckpt_path = workflow.get(NODE_CHECKPOINT, {}).get('inputs', {}).get('ckpt_name') logger.debug("Loaded default checkpoint from workflow file: %s", ckpt_path) except Exception: pass if not ckpt_path: return None, None ckpt = Checkpoint.query.filter_by(checkpoint_path=ckpt_path).first() if not ckpt: # Checkpoint path exists but not in DB - return path with empty data return ckpt_path, {} return ckpt.checkpoint_path, ckpt.data or {} def _inject_multi_char_detailers(workflow, prompts, model_source, clip_source): """Replace single FaceDetailer (node 11) with per-character SEGS-based detailers. Injects person detection + face detection pipelines that order detections left-to-right and apply character A's prompt to the left person/face and character B's prompt to the right person/face. Nodes added: 40 - Person detector (UltralyticsDetectorProvider) 41 - Person SEGS detection (BboxDetectorSEGS) 42 - Filter: left person (char A) 43 - Filter: right person (char B) 44 - CLIPTextEncode: char A body prompt 45 - CLIPTextEncode: char B body prompt 46 - DetailerForEach: person A 47 - DetailerForEach: person B 48 - Face SEGS detection (BboxDetectorSEGS, reuses face detector node 10) 49 - Filter: left face (char A) 50 - Filter: right face (char B) 51 - CLIPTextEncode: char A face prompt 52 - CLIPTextEncode: char B face prompt 53 - DetailerForEach: face A 54 - DetailerForEach: face B Image flow: VAEDecode(8) → PersonA(46) → PersonB(47) → FaceA(53) → FaceB(54) → Hand(13) """ vae_source = [NODE_CHECKPOINT, 2] # Remove old single face detailer and its prompt — we replace them workflow.pop(NODE_FACE_DETAILER, None) workflow.pop(NODE_FACE_PROMPT, None) # --- Person detection --- workflow['40'] = { 'inputs': {'model_name': 'segm/person_yolov8m-seg.pt'}, 'class_type': 'UltralyticsDetectorProvider' } workflow['41'] = { 'inputs': { 'bbox_detector': ['40', 0], 'image': [NODE_VAE_DECODE, 0], 'threshold': 0.5, 'dilation': 10, 'crop_factor': 3.0, 'drop_size': 10, 'labels': 'all', }, 'class_type': 'BboxDetectorSEGS' } # Order by x1 ascending (left to right), pick index 0 = leftmost person workflow['42'] = { 'inputs': { 'segs': ['41', 0], 'target': 'x1', 'order': False, 'take_start': 0, 'take_count': 1, }, 'class_type': 'ImpactSEGSOrderedFilter' } # Pick index 1 = rightmost person workflow['43'] = { 'inputs': { 'segs': ['41', 0], 'target': 'x1', 'order': False, 'take_start': 1, 'take_count': 1, }, 'class_type': 'ImpactSEGSOrderedFilter' } # --- Per-character body prompts --- workflow['44'] = { 'inputs': {'text': prompts.get('char_a_main', ''), 'clip': clip_source}, 'class_type': 'CLIPTextEncode' } workflow['45'] = { 'inputs': {'text': prompts.get('char_b_main', ''), 'clip': clip_source}, 'class_type': 'CLIPTextEncode' } # --- Person detailing (DetailerForEach) --- _person_base = { 'guide_size': 512, 'guide_size_for': True, 'max_size': 1024, 'seed': 0, # overwritten by seed step 'steps': 20, # overwritten by checkpoint settings 'cfg': 3.5, # overwritten by checkpoint settings 'sampler_name': 'euler_ancestral', 'scheduler': 'normal', 'denoise': 0.4, 'feather': 5, 'noise_mask': True, 'force_inpaint': True, 'wildcard': '', 'cycle': 1, 'inpaint_model': False, 'noise_mask_feather': 20, } workflow['46'] = { 'inputs': { **_person_base, 'image': [NODE_VAE_DECODE, 0], 'segs': ['42', 0], 'model': model_source, 'clip': clip_source, 'vae': vae_source, 'positive': ['44', 0], 'negative': [NODE_NEGATIVE, 0], }, 'class_type': 'DetailerForEach' } workflow['47'] = { 'inputs': { **_person_base, 'image': ['46', 0], # chains from person A output 'segs': ['43', 0], 'model': model_source, 'clip': clip_source, 'vae': vae_source, 'positive': ['45', 0], 'negative': [NODE_NEGATIVE, 0], }, 'class_type': 'DetailerForEach' } # --- Face detection (on person-detailed image) --- workflow['48'] = { 'inputs': { 'bbox_detector': ['10', 0], # reuse existing face YOLO detector 'image': ['47', 0], 'threshold': 0.5, 'dilation': 10, 'crop_factor': 3.0, 'drop_size': 10, 'labels': 'all', }, 'class_type': 'BboxDetectorSEGS' } workflow['49'] = { 'inputs': { 'segs': ['48', 0], 'target': 'x1', 'order': False, 'take_start': 0, 'take_count': 1, }, 'class_type': 'ImpactSEGSOrderedFilter' } workflow['50'] = { 'inputs': { 'segs': ['48', 0], 'target': 'x1', 'order': False, 'take_start': 1, 'take_count': 1, }, 'class_type': 'ImpactSEGSOrderedFilter' } # --- Per-character face prompts --- workflow['51'] = { 'inputs': {'text': prompts.get('char_a_face', ''), 'clip': clip_source}, 'class_type': 'CLIPTextEncode' } workflow['52'] = { 'inputs': {'text': prompts.get('char_b_face', ''), 'clip': clip_source}, 'class_type': 'CLIPTextEncode' } # --- Face detailing (DetailerForEach) --- _face_base = { 'guide_size': 384, 'guide_size_for': True, 'max_size': 1024, 'seed': 0, 'steps': 20, 'cfg': 3.5, 'sampler_name': 'euler_ancestral', 'scheduler': 'normal', 'denoise': 0.5, 'feather': 5, 'noise_mask': True, 'force_inpaint': True, 'wildcard': '', 'cycle': 1, 'inpaint_model': False, 'noise_mask_feather': 20, } workflow['53'] = { 'inputs': { **_face_base, 'image': ['47', 0], 'segs': ['49', 0], 'model': model_source, 'clip': clip_source, 'vae': vae_source, 'positive': ['51', 0], 'negative': [NODE_NEGATIVE, 0], }, 'class_type': 'DetailerForEach' } workflow['54'] = { 'inputs': { **_face_base, 'image': ['53', 0], # chains from face A output 'segs': ['50', 0], 'model': model_source, 'clip': clip_source, 'vae': vae_source, 'positive': ['52', 0], 'negative': [NODE_NEGATIVE, 0], }, 'class_type': 'DetailerForEach' } # Rewire hand detailer: image input from last face detailer instead of old node 11 if NODE_HAND_DETAILER in workflow: workflow[NODE_HAND_DETAILER]['inputs']['image'] = ['54', 0] logger.debug("Injected multi-char SEGS detailers (nodes 40-54)") def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None, outfit=None, action=None, style=None, detailer=None, scene=None, width=None, height=None, checkpoint_data=None, look=None, fixed_seed=None, character_b=None): # 1. Update prompts using replacement to preserve embeddings workflow[NODE_POSITIVE]["inputs"]["text"] = workflow[NODE_POSITIVE]["inputs"]["text"].replace("{{POSITIVE_PROMPT}}", prompts["main"]) if custom_negative: workflow[NODE_NEGATIVE]["inputs"]["text"] = f"{custom_negative}, {workflow[NODE_NEGATIVE]['inputs']['text']}" if NODE_FACE_PROMPT in workflow: workflow[NODE_FACE_PROMPT]["inputs"]["text"] = workflow[NODE_FACE_PROMPT]["inputs"]["text"].replace("{{FACE_PROMPT}}", prompts["face"]) if NODE_HAND_PROMPT in workflow: workflow[NODE_HAND_PROMPT]["inputs"]["text"] = workflow[NODE_HAND_PROMPT]["inputs"]["text"].replace("{{HAND_PROMPT}}", prompts["hand"]) # 2. Update Checkpoint - always set one, fall back to default if not provided if not checkpoint: default_ckpt, default_ckpt_data = _get_default_checkpoint() checkpoint = default_ckpt if not checkpoint_data: checkpoint_data = default_ckpt_data if checkpoint: workflow[NODE_CHECKPOINT]["inputs"]["ckpt_name"] = checkpoint else: raise ValueError("No checkpoint specified and no default checkpoint configured") # 3. Handle LoRAs - Node 16 for character, Node 17 for outfit, Node 18 for action, Node 19 for style/detailer # Start with direct checkpoint connections model_source = [NODE_CHECKPOINT, 0] clip_source = [NODE_CHECKPOINT, 1] # Look negative prompt (applied before character LoRA) if look: look_negative = look.data.get('negative', '') if look_negative: workflow[NODE_NEGATIVE]["inputs"]["text"] = f"{look_negative}, {workflow[NODE_NEGATIVE]['inputs']['text']}" # Character LoRA (Node 16) — look LoRA overrides character LoRA when present if look: char_lora_data = look.data.get('lora', {}) else: char_lora_data = character.data.get('lora', {}) if character else {} char_lora_name = char_lora_data.get('lora_name') if char_lora_name and NODE_LORA_CHAR in workflow: _w16 = _resolve_lora_weight(char_lora_data) workflow[NODE_LORA_CHAR]["inputs"]["lora_name"] = char_lora_name workflow[NODE_LORA_CHAR]["inputs"]["strength_model"] = _w16 workflow[NODE_LORA_CHAR]["inputs"]["strength_clip"] = _w16 workflow[NODE_LORA_CHAR]["inputs"]["model"] = [NODE_CHECKPOINT, 0] # From checkpoint workflow[NODE_LORA_CHAR]["inputs"]["clip"] = [NODE_CHECKPOINT, 1] # From checkpoint model_source = [NODE_LORA_CHAR, 0] clip_source = [NODE_LORA_CHAR, 1] logger.debug("Character LoRA: %s @ %s", char_lora_name, _w16) # Outfit LoRA (Node 17) - chains from character LoRA or checkpoint outfit_lora_data = outfit.data.get('lora', {}) if outfit else {} outfit_lora_name = outfit_lora_data.get('lora_name') if outfit_lora_name and NODE_LORA_OUTFIT in workflow: _w17 = _resolve_lora_weight({**{'lora_weight': 0.8}, **outfit_lora_data}) workflow[NODE_LORA_OUTFIT]["inputs"]["lora_name"] = outfit_lora_name workflow[NODE_LORA_OUTFIT]["inputs"]["strength_model"] = _w17 workflow[NODE_LORA_OUTFIT]["inputs"]["strength_clip"] = _w17 # Chain from character LoRA (node 16) or checkpoint (node 4) workflow[NODE_LORA_OUTFIT]["inputs"]["model"] = model_source workflow[NODE_LORA_OUTFIT]["inputs"]["clip"] = clip_source model_source = [NODE_LORA_OUTFIT, 0] clip_source = [NODE_LORA_OUTFIT, 1] logger.debug("Outfit LoRA: %s @ %s", outfit_lora_name, _w17) # Action LoRA (Node 18) - chains from previous LoRA or checkpoint action_lora_data = action.data.get('lora', {}) if action else {} action_lora_name = action_lora_data.get('lora_name') if action_lora_name and NODE_LORA_ACTION in workflow: _w18 = _resolve_lora_weight(action_lora_data) workflow[NODE_LORA_ACTION]["inputs"]["lora_name"] = action_lora_name workflow[NODE_LORA_ACTION]["inputs"]["strength_model"] = _w18 workflow[NODE_LORA_ACTION]["inputs"]["strength_clip"] = _w18 # Chain from previous source workflow[NODE_LORA_ACTION]["inputs"]["model"] = model_source workflow[NODE_LORA_ACTION]["inputs"]["clip"] = clip_source model_source = [NODE_LORA_ACTION, 0] clip_source = [NODE_LORA_ACTION, 1] logger.debug("Action LoRA: %s @ %s", action_lora_name, _w18) # Style/Detailer/Scene LoRA (Node 19) - chains from previous LoRA or checkpoint # Priority: Style > Detailer > Scene (Scene LoRAs are rare but supported) target_obj = style or detailer or scene style_lora_data = target_obj.data.get('lora', {}) if target_obj else {} style_lora_name = style_lora_data.get('lora_name') if style_lora_name and NODE_LORA_STYLE in workflow: _w19 = _resolve_lora_weight(style_lora_data) workflow[NODE_LORA_STYLE]["inputs"]["lora_name"] = style_lora_name workflow[NODE_LORA_STYLE]["inputs"]["strength_model"] = _w19 workflow[NODE_LORA_STYLE]["inputs"]["strength_clip"] = _w19 # Chain from previous source workflow[NODE_LORA_STYLE]["inputs"]["model"] = model_source workflow[NODE_LORA_STYLE]["inputs"]["clip"] = clip_source model_source = [NODE_LORA_STYLE, 0] clip_source = [NODE_LORA_STYLE, 1] logger.debug("Style/Detailer LoRA: %s @ %s", style_lora_name, _w19) # Second character LoRA (Node 20) - for multi-character generation if character_b: char_b_lora_data = character_b.data.get('lora', {}) char_b_lora_name = char_b_lora_data.get('lora_name') if char_b_lora_name and NODE_LORA_CHAR_B in workflow: _w20 = _resolve_lora_weight(char_b_lora_data) workflow[NODE_LORA_CHAR_B]["inputs"]["lora_name"] = char_b_lora_name workflow[NODE_LORA_CHAR_B]["inputs"]["strength_model"] = _w20 workflow[NODE_LORA_CHAR_B]["inputs"]["strength_clip"] = _w20 workflow[NODE_LORA_CHAR_B]["inputs"]["model"] = model_source workflow[NODE_LORA_CHAR_B]["inputs"]["clip"] = clip_source model_source = [NODE_LORA_CHAR_B, 0] clip_source = [NODE_LORA_CHAR_B, 1] logger.debug("Character B LoRA: %s @ %s", char_b_lora_name, _w20) # 3b. Multi-char: inject per-character SEGS detailers (replaces node 11/14) if character_b: _inject_multi_char_detailers(workflow, prompts, model_source, clip_source) # Apply connections to all model/clip consumers (conditional on node existence) for nid in [NODE_KSAMPLER, NODE_FACE_DETAILER, NODE_HAND_DETAILER] + _SEGS_DETAILER_NODES: if nid in workflow: workflow[nid]["inputs"]["model"] = model_source for nid in [NODE_POSITIVE, NODE_NEGATIVE, NODE_FACE_DETAILER, NODE_HAND_DETAILER, NODE_FACE_PROMPT, NODE_HAND_PROMPT] + _SEGS_PROMPT_NODES: if nid in workflow: workflow[nid]["inputs"]["clip"] = clip_source # 4. Randomize seeds (or use a fixed seed for reproducible batches like Strengths Gallery) gen_seed = fixed_seed if fixed_seed is not None else random.randint(1, 10**15) for nid in [NODE_KSAMPLER, NODE_FACE_DETAILER, NODE_HAND_DETAILER] + _SEGS_DETAILER_NODES: if nid in workflow: workflow[nid]["inputs"]["seed"] = gen_seed # 5. Set image dimensions if NODE_LATENT in workflow: if width: workflow[NODE_LATENT]["inputs"]["width"] = int(width) if height: workflow[NODE_LATENT]["inputs"]["height"] = int(height) # 6. Apply checkpoint-specific settings (steps, cfg, sampler, base prompts, VAE) if checkpoint_data: workflow = _apply_checkpoint_settings(workflow, checkpoint_data) # 7. Sync sampler/scheduler from main KSampler to adetailer nodes sampler_name = workflow[NODE_KSAMPLER]["inputs"].get("sampler_name") scheduler = workflow[NODE_KSAMPLER]["inputs"].get("scheduler") for node_id in [NODE_FACE_DETAILER, NODE_HAND_DETAILER] + _SEGS_DETAILER_NODES: if node_id in workflow: if sampler_name: workflow[node_id]["inputs"]["sampler_name"] = sampler_name if scheduler: workflow[node_id]["inputs"]["scheduler"] = scheduler # 8. Cross-deduplicate: remove tags shared between positive and negative pos_text, neg_text = _cross_dedup_prompts( workflow[NODE_POSITIVE]["inputs"]["text"], workflow[NODE_NEGATIVE]["inputs"]["text"] ) workflow[NODE_POSITIVE]["inputs"]["text"] = pos_text workflow[NODE_NEGATIVE]["inputs"]["text"] = neg_text # 9. Final prompt debug — logged after all transformations are complete _log_workflow_prompts("_prepare_workflow", workflow) return workflow