Code review fixes: wardrobe migration, response validation, path traversal guard, deduplication
- Migrate 11 character JSONs from old wardrobe keys to _BODY_GROUP_KEYS format - Add is_favourite/is_nsfw columns to Preset model - Add HTTP response validation and timeouts to ComfyUI client - Add path traversal protection on replace cover route - Deduplicate services/mcp.py (4 functions → 2 generic + 2 wrappers) - Extract apply_library_filters() and clean_html_text() shared helpers - Add named constants for 17 ComfyUI workflow node IDs - Fix bare except clauses in services/llm.py - Fix tags schema in ensure_default_outfit() (list → dict) - Convert f-string logging to lazy % formatting - Add 5-minute polling timeout to frontend waitForJob() - Improve migration error handling (non-duplicate errors log at WARNING) - Update CLAUDE.md to reflect all changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import json
|
||||
import logging
|
||||
import requests
|
||||
from flask import current_app
|
||||
from services.workflow import NODE_CHECKPOINT
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -14,9 +15,11 @@ def get_loaded_checkpoint():
|
||||
if resp.ok:
|
||||
history = resp.json()
|
||||
if history:
|
||||
latest = max(history.values(), key=lambda j: j.get('status', {}).get('status_str', ''))
|
||||
# Sort by prompt ID (numeric string) to get the most recent job
|
||||
latest_id = max(history.keys())
|
||||
latest = history[latest_id]
|
||||
nodes = latest.get('prompt', [None, None, {}])[2]
|
||||
return nodes.get('4', {}).get('inputs', {}).get('ckpt_name')
|
||||
return nodes.get(NODE_CHECKPOINT, {}).get('inputs', {}).get('ckpt_name')
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
@@ -34,26 +37,27 @@ def _ensure_checkpoint_loaded(checkpoint_path):
|
||||
if resp.ok:
|
||||
history = resp.json()
|
||||
if history:
|
||||
latest = max(history.values(), key=lambda j: j.get('status', {}).get('status_str', ''))
|
||||
latest_id = max(history.keys())
|
||||
latest = history[latest_id]
|
||||
nodes = latest.get('prompt', [None, None, {}])[2]
|
||||
loaded_ckpt = nodes.get('4', {}).get('inputs', {}).get('ckpt_name')
|
||||
loaded_ckpt = nodes.get(NODE_CHECKPOINT, {}).get('inputs', {}).get('ckpt_name')
|
||||
|
||||
# If the loaded checkpoint matches what we want, no action needed
|
||||
if loaded_ckpt == checkpoint_path:
|
||||
logger.info(f"Checkpoint {checkpoint_path} already loaded in ComfyUI")
|
||||
logger.info("Checkpoint %s already loaded in ComfyUI", checkpoint_path)
|
||||
return
|
||||
|
||||
# Checkpoint doesn't match or couldn't determine - force unload all models
|
||||
logger.info(f"Forcing ComfyUI to unload models to ensure {checkpoint_path} loads")
|
||||
logger.info("Forcing ComfyUI to unload models to ensure %s loads", checkpoint_path)
|
||||
requests.post(f'{url}/free', json={'unload_models': True}, timeout=5)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to check/force checkpoint reload: {e}")
|
||||
logger.warning("Failed to check/force checkpoint reload: %s", e)
|
||||
|
||||
|
||||
def queue_prompt(prompt_workflow, client_id=None):
|
||||
"""POST a workflow to ComfyUI's /prompt endpoint."""
|
||||
# Ensure the checkpoint in the workflow is loaded in ComfyUI
|
||||
checkpoint_path = prompt_workflow.get('4', {}).get('inputs', {}).get('ckpt_name')
|
||||
checkpoint_path = prompt_workflow.get(NODE_CHECKPOINT, {}).get('inputs', {}).get('ckpt_name')
|
||||
_ensure_checkpoint_loaded(checkpoint_path)
|
||||
|
||||
p = {"prompt": prompt_workflow}
|
||||
@@ -72,7 +76,10 @@ def queue_prompt(prompt_workflow, client_id=None):
|
||||
logger.debug("=" * 80)
|
||||
|
||||
data = json.dumps(p).encode('utf-8')
|
||||
response = requests.post(f"{url}/prompt", data=data)
|
||||
response = requests.post(f"{url}/prompt", data=data, timeout=30)
|
||||
if not response.ok:
|
||||
logger.error("ComfyUI returned HTTP %s: %s", response.status_code, response.text[:500])
|
||||
raise RuntimeError(f"ComfyUI returned HTTP {response.status_code}")
|
||||
response_json = response.json()
|
||||
|
||||
# Log the response from ComfyUI
|
||||
@@ -90,7 +97,7 @@ def queue_prompt(prompt_workflow, client_id=None):
|
||||
def get_history(prompt_id):
|
||||
"""Poll ComfyUI /history for results of a given prompt_id."""
|
||||
url = current_app.config['COMFYUI_URL']
|
||||
response = requests.get(f"{url}/history/{prompt_id}")
|
||||
response = requests.get(f"{url}/history/{prompt_id}", timeout=10)
|
||||
history_json = response.json()
|
||||
|
||||
# Log detailed history response for debugging
|
||||
@@ -128,6 +135,6 @@ def get_image(filename, subfolder, folder_type):
|
||||
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
||||
logger.debug("Fetching image from ComfyUI: filename=%s, subfolder=%s, type=%s",
|
||||
filename, subfolder, folder_type)
|
||||
response = requests.get(f"{url}/view", params=data)
|
||||
response = requests.get(f"{url}/view", params=data, timeout=30)
|
||||
logger.debug("Image retrieved: %d bytes (status: %s)", len(response.content), response.status_code)
|
||||
return response.content
|
||||
|
||||
@@ -205,13 +205,13 @@ def call_llm(prompt, system_prompt="You are a creative assistant."):
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_body = ""
|
||||
try: error_body = f" - Body: {response.text}"
|
||||
except: pass
|
||||
except Exception: pass
|
||||
raise RuntimeError(f"LLM API request failed: {str(e)}{error_body}") from e
|
||||
except (KeyError, IndexError) as e:
|
||||
# Log the raw response to help diagnose the issue
|
||||
raw = ""
|
||||
try: raw = response.text[:500]
|
||||
except: pass
|
||||
except Exception: pass
|
||||
logger.warning("Unexpected LLM response format (key=%s). Raw response: %s", e, raw)
|
||||
if format_retries > 0:
|
||||
format_retries -= 1
|
||||
|
||||
176
services/mcp.py
176
services/mcp.py
@@ -12,147 +12,83 @@ CHAR_MCP_COMPOSE_DIR = os.path.join(MCP_TOOLS_DIR, 'character-mcp')
|
||||
CHAR_MCP_REPO_URL = 'https://git.liveaodh.com/aodhan/character-mcp.git'
|
||||
|
||||
|
||||
def _ensure_mcp_repo():
|
||||
"""Clone or update the danbooru-mcp source repository inside tools/.
|
||||
def _ensure_repo(compose_dir, repo_url, name):
|
||||
"""Clone or update an MCP source repository inside tools/.
|
||||
|
||||
- If ``tools/danbooru-mcp/`` does not exist, clone from MCP_REPO_URL.
|
||||
- If the directory does not exist, clone from repo_url.
|
||||
- If it already exists, run ``git pull`` to fetch the latest changes.
|
||||
Errors are non-fatal.
|
||||
"""
|
||||
os.makedirs(MCP_TOOLS_DIR, exist_ok=True)
|
||||
try:
|
||||
if not os.path.isdir(MCP_COMPOSE_DIR):
|
||||
logger.info('Cloning danbooru-mcp from %s …', MCP_REPO_URL)
|
||||
if not os.path.isdir(compose_dir):
|
||||
logger.info('Cloning %s from %s …', name, repo_url)
|
||||
subprocess.run(
|
||||
['git', 'clone', MCP_REPO_URL, MCP_COMPOSE_DIR],
|
||||
['git', 'clone', repo_url, compose_dir],
|
||||
timeout=120, check=True,
|
||||
)
|
||||
logger.info('danbooru-mcp cloned successfully.')
|
||||
logger.info('%s cloned successfully.', name)
|
||||
else:
|
||||
logger.info('Updating danbooru-mcp via git pull …')
|
||||
logger.info('Updating %s via git pull …', name)
|
||||
subprocess.run(
|
||||
['git', 'pull'],
|
||||
cwd=MCP_COMPOSE_DIR,
|
||||
cwd=compose_dir,
|
||||
timeout=60, check=True,
|
||||
)
|
||||
logger.info('danbooru-mcp updated.')
|
||||
logger.info('%s updated.', name)
|
||||
except FileNotFoundError:
|
||||
logger.warning('git not found on PATH — danbooru-mcp repo will not be cloned/updated.')
|
||||
logger.warning('git not found on PATH — %s repo will not be cloned/updated.', name)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning('git operation failed for danbooru-mcp: %s', e)
|
||||
logger.warning('git operation failed for %s: %s', name, e)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning('git timed out while cloning/updating danbooru-mcp.')
|
||||
logger.warning('git timed out while cloning/updating %s.', name)
|
||||
except Exception as e:
|
||||
logger.warning('Could not clone/update danbooru-mcp repo: %s', e)
|
||||
logger.warning('Could not clone/update %s repo: %s', name, e)
|
||||
|
||||
|
||||
def _ensure_server_running(compose_dir, repo_url, container_name, name):
|
||||
"""Ensure an MCP repo is present/up-to-date, then start the Docker
|
||||
container if it is not already running.
|
||||
|
||||
Uses ``docker compose up -d`` so the image is built automatically on first
|
||||
run. Errors are non-fatal — the app will still start even if Docker is
|
||||
unavailable.
|
||||
|
||||
Skipped when ``SKIP_MCP_AUTOSTART=true`` (set by docker-compose, where the
|
||||
MCP service is managed by compose instead).
|
||||
"""
|
||||
if os.environ.get('SKIP_MCP_AUTOSTART', '').lower() == 'true':
|
||||
logger.info('SKIP_MCP_AUTOSTART set — skipping %s auto-start.', name)
|
||||
return
|
||||
_ensure_repo(compose_dir, repo_url, name)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', 'ps', '--filter', f'name={container_name}', '--format', '{{.Names}}'],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if container_name in result.stdout:
|
||||
logger.info('%s container already running.', name)
|
||||
return
|
||||
logger.info('Starting %s container via docker compose …', name)
|
||||
subprocess.run(
|
||||
['docker', 'compose', 'up', '-d'],
|
||||
cwd=compose_dir,
|
||||
timeout=120,
|
||||
)
|
||||
logger.info('%s container started.', name)
|
||||
except FileNotFoundError:
|
||||
logger.warning('docker not found on PATH — %s will not be started automatically.', name)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning('docker timed out while starting %s.', name)
|
||||
except Exception as e:
|
||||
logger.warning('Could not ensure %s is running: %s', name, e)
|
||||
|
||||
|
||||
def ensure_mcp_server_running():
|
||||
"""Ensure the danbooru-mcp repo is present/up-to-date, then start the
|
||||
Docker container if it is not already running.
|
||||
|
||||
Uses ``docker compose up -d`` so the image is built automatically on first
|
||||
run. Errors are non-fatal — the app will still start even if Docker is
|
||||
unavailable.
|
||||
|
||||
Skipped when ``SKIP_MCP_AUTOSTART=true`` (set by docker-compose, where the
|
||||
danbooru-mcp service is managed by compose instead).
|
||||
"""
|
||||
if os.environ.get('SKIP_MCP_AUTOSTART', '').lower() == 'true':
|
||||
logger.info('SKIP_MCP_AUTOSTART set — skipping danbooru-mcp auto-start.')
|
||||
return
|
||||
_ensure_mcp_repo()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', 'ps', '--filter', 'name=danbooru-mcp', '--format', '{{.Names}}'],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if 'danbooru-mcp' in result.stdout:
|
||||
logger.info('danbooru-mcp container already running.')
|
||||
return
|
||||
# Container not running — start it via docker compose
|
||||
logger.info('Starting danbooru-mcp container via docker compose …')
|
||||
subprocess.run(
|
||||
['docker', 'compose', 'up', '-d'],
|
||||
cwd=MCP_COMPOSE_DIR,
|
||||
timeout=120,
|
||||
)
|
||||
logger.info('danbooru-mcp container started.')
|
||||
except FileNotFoundError:
|
||||
logger.warning('docker not found on PATH — danbooru-mcp will not be started automatically.')
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning('docker timed out while starting danbooru-mcp.')
|
||||
except Exception as e:
|
||||
logger.warning('Could not ensure danbooru-mcp is running: %s', e)
|
||||
|
||||
|
||||
def _ensure_character_mcp_repo():
|
||||
"""Clone or update the character-mcp source repository inside tools/.
|
||||
|
||||
- If ``tools/character-mcp/`` does not exist, clone from CHAR_MCP_REPO_URL.
|
||||
- If it already exists, run ``git pull`` to fetch the latest changes.
|
||||
Errors are non-fatal.
|
||||
"""
|
||||
os.makedirs(MCP_TOOLS_DIR, exist_ok=True)
|
||||
try:
|
||||
if not os.path.isdir(CHAR_MCP_COMPOSE_DIR):
|
||||
logger.info('Cloning character-mcp from %s …', CHAR_MCP_REPO_URL)
|
||||
subprocess.run(
|
||||
['git', 'clone', CHAR_MCP_REPO_URL, CHAR_MCP_COMPOSE_DIR],
|
||||
timeout=120, check=True,
|
||||
)
|
||||
logger.info('character-mcp cloned successfully.')
|
||||
else:
|
||||
logger.info('Updating character-mcp via git pull …')
|
||||
subprocess.run(
|
||||
['git', 'pull'],
|
||||
cwd=CHAR_MCP_COMPOSE_DIR,
|
||||
timeout=60, check=True,
|
||||
)
|
||||
logger.info('character-mcp updated.')
|
||||
except FileNotFoundError:
|
||||
logger.warning('git not found on PATH — character-mcp repo will not be cloned/updated.')
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning('git operation failed for character-mcp: %s', e)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning('git timed out while cloning/updating character-mcp.')
|
||||
except Exception as e:
|
||||
logger.warning('Could not clone/update character-mcp repo: %s', e)
|
||||
"""Ensure the danbooru-mcp Docker container is running."""
|
||||
_ensure_server_running(MCP_COMPOSE_DIR, MCP_REPO_URL, 'danbooru-mcp', 'danbooru-mcp')
|
||||
|
||||
|
||||
def ensure_character_mcp_server_running():
|
||||
"""Ensure the character-mcp repo is present/up-to-date, then start the
|
||||
Docker container if it is not already running.
|
||||
|
||||
Uses ``docker compose up -d`` so the image is built automatically on first
|
||||
run. Errors are non-fatal — the app will still start even if Docker is
|
||||
unavailable.
|
||||
|
||||
Skipped when ``SKIP_MCP_AUTOSTART=true`` (set by docker-compose, where the
|
||||
character-mcp service is managed by compose instead).
|
||||
"""
|
||||
if os.environ.get('SKIP_MCP_AUTOSTART', '').lower() == 'true':
|
||||
logger.info('SKIP_MCP_AUTOSTART set — skipping character-mcp auto-start.')
|
||||
return
|
||||
_ensure_character_mcp_repo()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', 'ps', '--filter', 'name=character-mcp', '--format', '{{.Names}}'],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if 'character-mcp' in result.stdout:
|
||||
logger.info('character-mcp container already running.')
|
||||
return
|
||||
# Container not running — start it via docker compose
|
||||
logger.info('Starting character-mcp container via docker compose …')
|
||||
subprocess.run(
|
||||
['docker', 'compose', 'up', '-d'],
|
||||
cwd=CHAR_MCP_COMPOSE_DIR,
|
||||
timeout=120,
|
||||
)
|
||||
logger.info('character-mcp container started.')
|
||||
except FileNotFoundError:
|
||||
logger.warning('docker not found on PATH — character-mcp will not be started automatically.')
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning('docker timed out while starting character-mcp.')
|
||||
except Exception as e:
|
||||
logger.warning('Could not ensure character-mcp is running: %s', e)
|
||||
"""Ensure the character-mcp Docker container is running."""
|
||||
_ensure_server_running(CHAR_MCP_COMPOSE_DIR, CHAR_MCP_REPO_URL, 'character-mcp', 'character-mcp')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import re
|
||||
from models import db, Character
|
||||
from utils import _IDENTITY_KEYS, _WARDROBE_KEYS, _BODY_GROUP_KEYS, parse_orientation
|
||||
from utils import _BODY_GROUP_KEYS, parse_orientation
|
||||
|
||||
|
||||
def _dedup_tags(prompt_str):
|
||||
@@ -57,7 +57,7 @@ def _ensure_character_fields(character, selected_fields, include_wardrobe=True,
|
||||
include_defaults — also inject defaults::expression and defaults::pose (for outfit/look previews)
|
||||
"""
|
||||
identity = character.data.get('identity', {})
|
||||
for key in _IDENTITY_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if identity.get(key):
|
||||
field_key = f'identity::{key}'
|
||||
if field_key not in selected_fields:
|
||||
@@ -72,7 +72,7 @@ def _ensure_character_fields(character, selected_fields, include_wardrobe=True,
|
||||
selected_fields.append('special::name')
|
||||
if include_wardrobe:
|
||||
wardrobe = character.get_active_wardrobe()
|
||||
for key in _WARDROBE_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if wardrobe.get(key):
|
||||
field_key = f'wardrobe::{key}'
|
||||
if field_key not in selected_fields:
|
||||
|
||||
@@ -193,7 +193,7 @@ def ensure_default_outfit():
|
||||
"lora_weight": 0.8,
|
||||
"lora_triggers": ""
|
||||
},
|
||||
"tags": []
|
||||
"tags": {"outfit_type": "Default", "nsfw": False}
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -9,6 +9,27 @@ 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
|
||||
@@ -22,7 +43,7 @@ def _log_workflow_prompts(label, workflow):
|
||||
lora_details = []
|
||||
|
||||
# Collect detailed LoRA information
|
||||
for node_id, label_str in [("16", "char/look"), ("17", "outfit"), ("18", "action"), ("19", "style/detail/scene"), ("20", "char_b")]:
|
||||
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:
|
||||
@@ -41,13 +62,13 @@ def _log_workflow_prompts(label, workflow):
|
||||
|
||||
# Extract VAE information
|
||||
vae_info = "(integrated)"
|
||||
if '21' in workflow:
|
||||
vae_info = workflow['21']['inputs'].get('vae_name', '(custom)')
|
||||
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 [("11", "Face"), ("13", "Hand")]:
|
||||
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', '?')}, "
|
||||
@@ -59,24 +80,24 @@ def _log_workflow_prompts(label, workflow):
|
||||
f"cfg={workflow[node_id]['inputs'].get('cfg', '?')}, "
|
||||
f"denoise={workflow[node_id]['inputs'].get('denoise', '?')}")
|
||||
|
||||
face_text = workflow.get('14', {}).get('inputs', {}).get('text', '')
|
||||
hand_text = workflow.get('15', {}).get('inputs', {}).get('text', '')
|
||||
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['4']['inputs'].get('ckpt_name', '(not set)')}",
|
||||
f" Checkpoint : {workflow[NODE_CHECKPOINT]['inputs'].get('ckpt_name', '(not set)')}",
|
||||
f" VAE : {vae_info}",
|
||||
"",
|
||||
" GENERATION SETTINGS:",
|
||||
f" Seed : {workflow['3']['inputs'].get('seed', '(not set)')}",
|
||||
f" Resolution : {workflow['5']['inputs'].get('width', '?')} x {workflow['5']['inputs'].get('height', '?')}",
|
||||
f" Sampler : {workflow['3']['inputs'].get('sampler_name', '?')} / {workflow['3']['inputs'].get('scheduler', '?')}",
|
||||
f" Steps : {workflow['3']['inputs'].get('steps', '?')}",
|
||||
f" CFG Scale : {workflow['3']['inputs'].get('cfg', '?')}",
|
||||
f" Denoise : {workflow['3']['inputs'].get('denoise', '1.0')}",
|
||||
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
|
||||
@@ -98,8 +119,8 @@ def _log_workflow_prompts(label, workflow):
|
||||
lines.extend([
|
||||
"",
|
||||
" PROMPTS:",
|
||||
f" [+] Positive : {workflow['6']['inputs'].get('text', '')}",
|
||||
f" [-] Negative : {workflow['7']['inputs'].get('text', '')}",
|
||||
f" [+] Positive : {workflow[NODE_POSITIVE]['inputs'].get('text', '')}",
|
||||
f" [-] Negative : {workflow[NODE_NEGATIVE]['inputs'].get('text', '')}",
|
||||
])
|
||||
|
||||
if face_text:
|
||||
@@ -128,17 +149,17 @@ def _apply_checkpoint_settings(workflow, ckpt_data):
|
||||
vae = ckpt_data.get('vae', 'integrated')
|
||||
|
||||
# KSampler (node 3)
|
||||
if steps and '3' in workflow:
|
||||
workflow['3']['inputs']['steps'] = int(steps)
|
||||
if cfg and '3' in workflow:
|
||||
workflow['3']['inputs']['cfg'] = float(cfg)
|
||||
if sampler_name and '3' in workflow:
|
||||
workflow['3']['inputs']['sampler_name'] = sampler_name
|
||||
if scheduler and '3' in workflow:
|
||||
workflow['3']['inputs']['scheduler'] = scheduler
|
||||
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 ['11', '13'] + _SEGS_DETAILER_NODES:
|
||||
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)
|
||||
@@ -151,25 +172,25 @@ def _apply_checkpoint_settings(workflow, ckpt_data):
|
||||
|
||||
# Prepend base_positive to all positive prompt nodes
|
||||
if base_positive:
|
||||
for node_id in ['6', '14', '15'] + _SEGS_PROMPT_NODES:
|
||||
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 '7' in workflow:
|
||||
workflow['7']['inputs']['text'] = f"{workflow['7']['inputs']['text']}, {base_negative}"
|
||||
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['21'] = {
|
||||
workflow[NODE_VAE_LOADER] = {
|
||||
'inputs': {'vae_name': vae},
|
||||
'class_type': 'VAELoader'
|
||||
}
|
||||
if '8' in workflow:
|
||||
workflow['8']['inputs']['vae'] = ['21', 0]
|
||||
for node_id in ['11', '13'] + _SEGS_DETAILER_NODES:
|
||||
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'] = ['21', 0]
|
||||
workflow[node_id]['inputs']['vae'] = [NODE_VAE_LOADER, 0]
|
||||
|
||||
return workflow
|
||||
|
||||
@@ -190,7 +211,7 @@ def _get_default_checkpoint():
|
||||
try:
|
||||
with open('comfy_workflow.json', 'r') as f:
|
||||
workflow = json.load(f)
|
||||
ckpt_path = workflow.get('4', {}).get('inputs', {}).get('ckpt_name')
|
||||
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
|
||||
@@ -231,11 +252,11 @@ def _inject_multi_char_detailers(workflow, prompts, model_source, clip_source):
|
||||
|
||||
Image flow: VAEDecode(8) → PersonA(46) → PersonB(47) → FaceA(53) → FaceB(54) → Hand(13)
|
||||
"""
|
||||
vae_source = ["4", 2]
|
||||
vae_source = [NODE_CHECKPOINT, 2]
|
||||
|
||||
# Remove old single face detailer and its prompt — we replace them
|
||||
workflow.pop('11', None)
|
||||
workflow.pop('14', None)
|
||||
workflow.pop(NODE_FACE_DETAILER, None)
|
||||
workflow.pop(NODE_FACE_PROMPT, None)
|
||||
|
||||
# --- Person detection ---
|
||||
workflow['40'] = {
|
||||
@@ -246,7 +267,7 @@ def _inject_multi_char_detailers(workflow, prompts, model_source, clip_source):
|
||||
workflow['41'] = {
|
||||
'inputs': {
|
||||
'bbox_detector': ['40', 0],
|
||||
'image': ['8', 0],
|
||||
'image': [NODE_VAE_DECODE, 0],
|
||||
'threshold': 0.5,
|
||||
'dilation': 10,
|
||||
'crop_factor': 3.0,
|
||||
@@ -313,13 +334,13 @@ def _inject_multi_char_detailers(workflow, prompts, model_source, clip_source):
|
||||
workflow['46'] = {
|
||||
'inputs': {
|
||||
**_person_base,
|
||||
'image': ['8', 0],
|
||||
'image': [NODE_VAE_DECODE, 0],
|
||||
'segs': ['42', 0],
|
||||
'model': model_source,
|
||||
'clip': clip_source,
|
||||
'vae': vae_source,
|
||||
'positive': ['44', 0],
|
||||
'negative': ['7', 0],
|
||||
'negative': [NODE_NEGATIVE, 0],
|
||||
},
|
||||
'class_type': 'DetailerForEach'
|
||||
}
|
||||
@@ -333,7 +354,7 @@ def _inject_multi_char_detailers(workflow, prompts, model_source, clip_source):
|
||||
'clip': clip_source,
|
||||
'vae': vae_source,
|
||||
'positive': ['45', 0],
|
||||
'negative': ['7', 0],
|
||||
'negative': [NODE_NEGATIVE, 0],
|
||||
},
|
||||
'class_type': 'DetailerForEach'
|
||||
}
|
||||
@@ -413,7 +434,7 @@ def _inject_multi_char_detailers(workflow, prompts, model_source, clip_source):
|
||||
'clip': clip_source,
|
||||
'vae': vae_source,
|
||||
'positive': ['51', 0],
|
||||
'negative': ['7', 0],
|
||||
'negative': [NODE_NEGATIVE, 0],
|
||||
},
|
||||
'class_type': 'DetailerForEach'
|
||||
}
|
||||
@@ -427,29 +448,29 @@ def _inject_multi_char_detailers(workflow, prompts, model_source, clip_source):
|
||||
'clip': clip_source,
|
||||
'vae': vae_source,
|
||||
'positive': ['52', 0],
|
||||
'negative': ['7', 0],
|
||||
'negative': [NODE_NEGATIVE, 0],
|
||||
},
|
||||
'class_type': 'DetailerForEach'
|
||||
}
|
||||
|
||||
# Rewire hand detailer: image input from last face detailer instead of old node 11
|
||||
if '13' in workflow:
|
||||
workflow['13']['inputs']['image'] = ['54', 0]
|
||||
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["6"]["inputs"]["text"] = workflow["6"]["inputs"]["text"].replace("{{POSITIVE_PROMPT}}", prompts["main"])
|
||||
workflow[NODE_POSITIVE]["inputs"]["text"] = workflow[NODE_POSITIVE]["inputs"]["text"].replace("{{POSITIVE_PROMPT}}", prompts["main"])
|
||||
|
||||
if custom_negative:
|
||||
workflow["7"]["inputs"]["text"] = f"{custom_negative}, {workflow['7']['inputs']['text']}"
|
||||
workflow[NODE_NEGATIVE]["inputs"]["text"] = f"{custom_negative}, {workflow[NODE_NEGATIVE]['inputs']['text']}"
|
||||
|
||||
if "14" in workflow:
|
||||
workflow["14"]["inputs"]["text"] = workflow["14"]["inputs"]["text"].replace("{{FACE_PROMPT}}", prompts["face"])
|
||||
if "15" in workflow:
|
||||
workflow["15"]["inputs"]["text"] = workflow["15"]["inputs"]["text"].replace("{{HAND_PROMPT}}", prompts["hand"])
|
||||
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:
|
||||
@@ -458,20 +479,20 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
|
||||
if not checkpoint_data:
|
||||
checkpoint_data = default_ckpt_data
|
||||
if checkpoint:
|
||||
workflow["4"]["inputs"]["ckpt_name"] = 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 = ["4", 0]
|
||||
clip_source = ["4", 1]
|
||||
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["7"]["inputs"]["text"] = f"{look_negative}, {workflow['7']['inputs']['text']}"
|
||||
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:
|
||||
@@ -480,47 +501,47 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
|
||||
char_lora_data = character.data.get('lora', {}) if character else {}
|
||||
char_lora_name = char_lora_data.get('lora_name')
|
||||
|
||||
if char_lora_name and "16" in workflow:
|
||||
if char_lora_name and NODE_LORA_CHAR in workflow:
|
||||
_w16 = _resolve_lora_weight(char_lora_data)
|
||||
workflow["16"]["inputs"]["lora_name"] = char_lora_name
|
||||
workflow["16"]["inputs"]["strength_model"] = _w16
|
||||
workflow["16"]["inputs"]["strength_clip"] = _w16
|
||||
workflow["16"]["inputs"]["model"] = ["4", 0] # From checkpoint
|
||||
workflow["16"]["inputs"]["clip"] = ["4", 1] # From checkpoint
|
||||
model_source = ["16", 0]
|
||||
clip_source = ["16", 1]
|
||||
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 "17" in workflow:
|
||||
if outfit_lora_name and NODE_LORA_OUTFIT in workflow:
|
||||
_w17 = _resolve_lora_weight({**{'lora_weight': 0.8}, **outfit_lora_data})
|
||||
workflow["17"]["inputs"]["lora_name"] = outfit_lora_name
|
||||
workflow["17"]["inputs"]["strength_model"] = _w17
|
||||
workflow["17"]["inputs"]["strength_clip"] = _w17
|
||||
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["17"]["inputs"]["model"] = model_source
|
||||
workflow["17"]["inputs"]["clip"] = clip_source
|
||||
model_source = ["17", 0]
|
||||
clip_source = ["17", 1]
|
||||
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 "18" in workflow:
|
||||
if action_lora_name and NODE_LORA_ACTION in workflow:
|
||||
_w18 = _resolve_lora_weight(action_lora_data)
|
||||
workflow["18"]["inputs"]["lora_name"] = action_lora_name
|
||||
workflow["18"]["inputs"]["strength_model"] = _w18
|
||||
workflow["18"]["inputs"]["strength_clip"] = _w18
|
||||
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["18"]["inputs"]["model"] = model_source
|
||||
workflow["18"]["inputs"]["clip"] = clip_source
|
||||
model_source = ["18", 0]
|
||||
clip_source = ["18", 1]
|
||||
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
|
||||
@@ -529,31 +550,31 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
|
||||
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 "19" in workflow:
|
||||
if style_lora_name and NODE_LORA_STYLE in workflow:
|
||||
_w19 = _resolve_lora_weight(style_lora_data)
|
||||
workflow["19"]["inputs"]["lora_name"] = style_lora_name
|
||||
workflow["19"]["inputs"]["strength_model"] = _w19
|
||||
workflow["19"]["inputs"]["strength_clip"] = _w19
|
||||
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["19"]["inputs"]["model"] = model_source
|
||||
workflow["19"]["inputs"]["clip"] = clip_source
|
||||
model_source = ["19", 0]
|
||||
clip_source = ["19", 1]
|
||||
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 "20" in workflow:
|
||||
if char_b_lora_name and NODE_LORA_CHAR_B in workflow:
|
||||
_w20 = _resolve_lora_weight(char_b_lora_data)
|
||||
workflow["20"]["inputs"]["lora_name"] = char_b_lora_name
|
||||
workflow["20"]["inputs"]["strength_model"] = _w20
|
||||
workflow["20"]["inputs"]["strength_clip"] = _w20
|
||||
workflow["20"]["inputs"]["model"] = model_source
|
||||
workflow["20"]["inputs"]["clip"] = clip_source
|
||||
model_source = ["20", 0]
|
||||
clip_source = ["20", 1]
|
||||
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)
|
||||
@@ -561,35 +582,35 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
|
||||
_inject_multi_char_detailers(workflow, prompts, model_source, clip_source)
|
||||
|
||||
# Apply connections to all model/clip consumers (conditional on node existence)
|
||||
for nid in ["3", "11", "13"] + _SEGS_DETAILER_NODES:
|
||||
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 ["6", "7", "11", "13", "14", "15"] + _SEGS_PROMPT_NODES:
|
||||
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 ["3", "11", "13"] + _SEGS_DETAILER_NODES:
|
||||
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 "5" in workflow:
|
||||
if NODE_LATENT in workflow:
|
||||
if width:
|
||||
workflow["5"]["inputs"]["width"] = int(width)
|
||||
workflow[NODE_LATENT]["inputs"]["width"] = int(width)
|
||||
if height:
|
||||
workflow["5"]["inputs"]["height"] = int(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["3"]["inputs"].get("sampler_name")
|
||||
scheduler = workflow["3"]["inputs"].get("scheduler")
|
||||
for node_id in ["11", "13"] + _SEGS_DETAILER_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
|
||||
@@ -598,11 +619,11 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
|
||||
|
||||
# 8. Cross-deduplicate: remove tags shared between positive and negative
|
||||
pos_text, neg_text = _cross_dedup_prompts(
|
||||
workflow["6"]["inputs"]["text"],
|
||||
workflow["7"]["inputs"]["text"]
|
||||
workflow[NODE_POSITIVE]["inputs"]["text"],
|
||||
workflow[NODE_NEGATIVE]["inputs"]["text"]
|
||||
)
|
||||
workflow["6"]["inputs"]["text"] = pos_text
|
||||
workflow["7"]["inputs"]["text"] = neg_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)
|
||||
|
||||
Reference in New Issue
Block a user