Files
slahiri-ComfyUI-AI-Photogra…/config_loader.py
slahiriandClaude Opus 4.5 f5827c3a86 chore: remove dead code after mode consolidation
- Remove get_agentic_prompt from config_loader.py (unused after _agentic_generate deletion)
- Remove get_agentic_prompt from prompt_generator_core.py (duplicate dead function)
- Remove duplicate get_component_prompt definition from prompt_generator_core.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 13:06:10 +05:30

700 lines
23 KiB
Python

# -*- coding: utf-8 -*-
"""
ComfyUI-AI-Photography-Toolkit - Configuration Loader
TOML configuration loader for provider settings, prompts, and filters.
Loads and caches configuration from TOML files for:
- Provider settings (tiers, stop strings, capabilities)
- Prompts (system/user prompts by tier and mode)
- Components (agentic analysis prompts)
- Filters (example text patterns to remove)
Uses Python 3.11+ tomllib or falls back to tomli package.
Author: Siddhartha Lahiri
Email: siddhartha.lahiri@gmail.com
License: MIT
"""
import os
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
# Try Python 3.11+ built-in, fall back to tomli
try:
import tomllib
except ImportError:
try:
import tomli as tomllib
except ImportError:
tomllib = None
# =============================================================================
# Configuration Cache
# =============================================================================
_config_cache: Dict[str, Dict] = {}
_CONFIG_DIR = Path(__file__).parent / "config"
_PROMPTS_DIR = _CONFIG_DIR / "prompts"
def clear_config_cache(filename: str = None):
"""
Clear the configuration cache to force reload from disk.
Args:
filename: Specific file to clear, or None to clear all.
Usage:
clear_config_cache() # Clear all
clear_config_cache("templates.toml") # Clear specific file
clear_config_cache("_modular_prompts") # Clear modular prompts cache
"""
global _config_cache
if filename:
if filename in _config_cache:
del _config_cache[filename]
print(f"[SID-Config] Cleared cache for: {filename}")
else:
_config_cache.clear()
print("[SID-Config] Cleared all config cache - files will reload on next access")
def reload_all_configs():
"""Clear cache and force reload of all config files."""
clear_config_cache()
# Trigger reload of commonly used configs
check_config_files()
print("[SID-Config] All configs reloaded from disk")
def _load_toml(filename: str, force_reload: bool = False) -> Dict[str, Any]:
"""Load a TOML file from the config directory with caching.
Args:
filename: Name of the TOML file to load
force_reload: If True, bypass cache and reload from disk
"""
if not force_reload and filename in _config_cache:
return _config_cache[filename]
if tomllib is None:
print(f"[SID-Config] Warning: tomllib/tomli not available, using defaults")
return {}
filepath = _CONFIG_DIR / filename
if not filepath.exists():
print(f"[SID-Config] Warning: Config file not found: {filepath}")
return {}
try:
with open(filepath, "rb") as f:
data = tomllib.load(f)
_config_cache[filename] = data
return data
except Exception as e:
print(f"[SID-Config] Error loading {filename}: {e}")
return {}
def _deep_merge(base: Dict, override: Dict) -> Dict:
"""Deep merge two dictionaries, with override taking precedence."""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _deep_merge(result[key], value)
else:
result[key] = value
return result
def _load_modular_prompts(force_reload: bool = False) -> Dict[str, Any]:
"""
Load prompts from modular structure in config/prompts/ directory.
Falls back to single prompts.toml if modular structure doesn't exist.
Args:
force_reload: If True, bypass cache and reload from disk
"""
cache_key = "_modular_prompts"
if not force_reload and cache_key in _config_cache:
return _config_cache[cache_key]
if tomllib is None:
return {}
merged_config: Dict[str, Any] = {}
# Check if modular structure exists
if _PROMPTS_DIR.exists() and _PROMPTS_DIR.is_dir():
# Load in order: _base.toml first, then others
toml_files = []
# Base config first
base_file = _PROMPTS_DIR / "_base.toml"
if base_file.exists():
toml_files.append(base_file)
# Main prompt files (portrait, scene, vocabulary, classification)
for filename in ["portrait.toml", "scene.toml", "vocabulary.toml", "classification.toml"]:
filepath = _PROMPTS_DIR / filename
if filepath.exists():
toml_files.append(filepath)
# Style-specific files from styles/ subdirectory
styles_dir = _PROMPTS_DIR / "styles"
if styles_dir.exists() and styles_dir.is_dir():
for filepath in sorted(styles_dir.glob("*.toml")):
toml_files.append(filepath)
# Load and merge all files
for filepath in toml_files:
try:
with open(filepath, "rb") as f:
data = tomllib.load(f)
merged_config = _deep_merge(merged_config, data)
except Exception as e:
print(f"[SID-Config] Error loading {filepath.name}: {e}")
if merged_config:
_config_cache[cache_key] = merged_config
return merged_config
# Fallback to single prompts.toml
fallback = _load_toml("prompts.toml")
_config_cache[cache_key] = fallback
return fallback
def _get_prompts_config() -> Dict[str, Any]:
"""
Get prompts configuration - uses modular structure if available,
otherwise falls back to prompts.toml.
"""
# Try modular structure first
modular = _load_modular_prompts()
if modular:
return modular
# Fallback to single file
return _load_toml("prompts.toml")
def reload_config():
"""Clear cache and reload all configs (useful for development)."""
global _config_cache
_config_cache = {}
# =============================================================================
# Provider Configuration
# =============================================================================
def get_provider_tier(provider: str) -> str:
"""Get the tier for a provider (advanced/standard/basic)."""
config = _load_toml("providers.toml")
tiers = config.get("tiers", {})
return tiers.get(provider.lower(), "standard")
def get_stop_strings(provider: str) -> List[str]:
"""Get stop strings for a provider."""
config = _load_toml("providers.toml")
stop_strings = config.get("stop_strings", {})
return stop_strings.get(provider.lower(), stop_strings.get("default", []))
def get_provider_capabilities(tier: str) -> Dict[str, Any]:
"""Get capabilities for a tier."""
config = _load_toml("providers.toml")
capabilities = config.get("capabilities", {})
return capabilities.get(tier, capabilities.get("standard", {}))
def get_image_limit(provider: str) -> int:
"""Get max image size for a provider."""
config = _load_toml("providers.toml")
limits = config.get("image_limits", {})
return limits.get(provider.lower(), limits.get("default", 1024))
# =============================================================================
# Prompt Configuration
# =============================================================================
def get_system_prompt(tier: str) -> str:
"""Get the base system prompt for a tier."""
config = _get_prompts_config()
system = config.get("system", {})
tier_config = system.get(tier, system.get("standard", {}))
return tier_config.get("base", "Describe this image for AI image generation.")
def get_style_addon(style: str, addon_type: str = "system_addon") -> str:
"""Get the style addon for system or user prompt."""
config = _get_prompts_config()
styles = config.get("styles", {})
# Normalize style name
style_key = style.lower().replace(" & ", "_").replace(" ", "_").replace("-", "_")
style_map = {
"auto_detect": "auto",
"portrait": "portrait",
"fashion_outfit": "fashion",
"fashion": "fashion",
"artistic_style": "artistic",
"artistic": "artistic",
"nsfw_detailed": "nsfw",
"nsfw": "nsfw",
}
style_key = style_map.get(style_key, "auto")
style_config = styles.get(style_key, {})
return style_config.get(addon_type, "")
def get_user_prompt(tier: str, mode: str) -> str:
"""Get the user prompt for a tier and mode."""
config = _get_prompts_config()
user = config.get("user", {})
tier_config = user.get(tier, user.get("standard", {}))
mode_config = tier_config.get(mode.lower(), {})
return mode_config.get("prompt", "Describe this image.")
def get_length_constraint(prompt_length: int) -> str:
"""
Get the length constraint instruction for a prompt_length setting.
Args:
prompt_length: Target word count (0 = no limit, optimal Z-Image range is 80-250)
Returns:
Length constraint instruction string (empty for 0/unlimited)
"""
if prompt_length <= 0:
return "" # No constraint
return f"OUTPUT LENGTH: Write a prompt of approximately {prompt_length} words. Adjust detail level to match this target."
# =============================================================================
# Component Configuration
# =============================================================================
def get_component_prompt(component: str, tier: str) -> str:
"""Get the prompt for a specific component and tier."""
config = _load_toml("components.toml")
comp_config = config.get(component, {})
tier_config = comp_config.get(tier, comp_config.get("standard", {}))
return tier_config.get("prompt", "")
def get_component_name(component: str) -> str:
"""Get the display name for a component."""
config = _load_toml("components.toml")
comp_config = config.get(component, {})
return comp_config.get("name", component.replace("_", " ").title())
def get_mode_config(mode: str) -> Dict[str, Any]:
"""Get the configuration for an analysis mode."""
config = _load_toml("components.toml")
modes = config.get("modes", {})
return modes.get(mode.lower(), modes.get("standard", {}))
def get_mode_components(mode: str) -> List[str]:
"""Get the list of components for an analysis mode."""
mode_config = get_mode_config(mode)
return mode_config.get("components", ["framing", "ethnicity", "clothing"])
def get_all_components() -> List[str]:
"""Get list of all available component keys."""
config = _load_toml("components.toml")
# Filter out the 'modes' section
return [k for k in config.keys() if k != "modes"]
# =============================================================================
# Filter Configuration
# =============================================================================
def get_example_patterns() -> List[str]:
"""Get regex patterns for example text to remove."""
config = _load_toml("filters.toml")
example = config.get("example_patterns", {})
return example.get("patterns", [])
def get_markdown_patterns() -> List[str]:
"""Get regex patterns for markdown cleanup."""
config = _load_toml("filters.toml")
markdown = config.get("markdown_patterns", {})
return markdown.get("patterns", [])
def get_whitespace_patterns() -> Dict[str, str]:
"""Get whitespace cleanup patterns."""
config = _load_toml("filters.toml")
return config.get("whitespace", {})
def get_provider_cleanup_patterns(provider: str) -> List[str]:
"""Get provider-specific cleanup patterns."""
config = _load_toml("filters.toml")
cleanup = config.get("provider_cleanup", {})
provider_config = cleanup.get(provider.lower(), cleanup.get("default", {}))
return provider_config.get("patterns", [])
# =============================================================================
# Output Cleaning Utility
# =============================================================================
def clean_output(text: str, provider: str = "default") -> str:
"""
Clean LLM output using patterns from filters.toml.
Args:
text: Raw LLM output
provider: Provider name for provider-specific cleanup
Returns:
Cleaned text
"""
if not text:
return ""
# Remove example text patterns
for pattern in get_example_patterns():
try:
text = re.sub(pattern, "", text, flags=re.IGNORECASE)
except re.error:
pass
# Remove markdown patterns
for pattern in get_markdown_patterns():
try:
# Handle patterns with capture groups (like bold/italic)
if "(" in pattern and ")" in pattern:
text = re.sub(pattern, r"\1", text, flags=re.MULTILINE)
else:
text = re.sub(pattern, "", text, flags=re.MULTILINE)
except re.error:
pass
# Provider-specific cleanup
for pattern in get_provider_cleanup_patterns(provider):
try:
text = re.sub(pattern, "", text, flags=re.MULTILINE)
except re.error:
pass
# Whitespace cleanup
ws = get_whitespace_patterns()
if ws.get("collapse_newlines"):
text = re.sub(ws["collapse_newlines"], " ", text)
if ws.get("collapse_spaces"):
text = re.sub(ws["collapse_spaces"], " ", text)
if ws.get("double_commas"):
text = re.sub(ws["double_commas"], ",", text)
if ws.get("leading_comma"):
text = re.sub(ws["leading_comma"], "", text)
if ws.get("trailing_comma"):
text = re.sub(ws["trailing_comma"], "", text)
if ws.get("double_periods"):
text = re.sub(ws["double_periods"], ".", text)
return text.strip()
# =============================================================================
# Convenience Functions
# =============================================================================
def build_system_prompt(provider: str, preset_style: str, user_guidance: str = "", prompt_length: int = 150) -> str:
"""
Build complete system prompt based on provider tier and style.
Args:
provider: LLM provider name
preset_style: Selected style preset
user_guidance: Optional user guidance text
prompt_length: Target word count (0 = no limit, default 150 for optimal Z-Image)
Returns:
Complete system prompt string
"""
tier = get_provider_tier(provider)
base = get_system_prompt(tier)
style_addon = get_style_addon(preset_style, "system_addon")
if style_addon:
base += f"\n\n{style_addon}"
# Add length constraint
length_constraint = get_length_constraint(prompt_length)
if length_constraint:
base += f"\n\n{length_constraint}"
if user_guidance and user_guidance.strip():
base += f"""
USER MODIFICATION REQUEST (CRITICAL - MUST APPLY):
"{user_guidance.strip()}"
This is a MODIFICATION directive. You MUST:
1. Apply this modification to your description, overriding what's literally visible
2. Describe the subject AS IF this modification is true
3. For clothing/nudity changes: describe the MODIFIED state (e.g., if "make topless" and image shows bikini, describe as topless)
4. Do NOT describe the original state that contradicts the user request
5. Start your description with the modified element"""
return base
def build_user_prompt(provider: str, analysis_mode: str, preset_style: str, prompt_length: int = 150) -> str:
"""
Build user prompt based on provider tier and analysis mode.
Args:
provider: LLM provider name
analysis_mode: Analysis mode (Standard/Detailed)
preset_style: Selected style preset
prompt_length: Target word count (0 = no limit, default 150 for optimal Z-Image)
Returns:
Complete user prompt string
"""
tier = get_provider_tier(provider)
prompt = get_user_prompt(tier, analysis_mode)
style_addon = get_style_addon(preset_style, "user_addon")
if style_addon:
prompt += f"\n\n{style_addon}"
# Reinforce length constraint in user prompt (already in system, but reinforce)
length_constraint = get_length_constraint(prompt_length)
if length_constraint:
prompt += f"\n\nREMEMBER: {length_constraint}"
return prompt
# =============================================================================
# Scene-Only Prompts (No Human Keywords - Prevents Hallucination)
# =============================================================================
def get_scene_only_system_prompt() -> str:
"""Get the scene-only system prompt (no human/person keywords)."""
config = _get_prompts_config()
system = config.get("system", {})
scene_config = system.get("scene_only", {})
return scene_config.get("base", "Describe this scene for AI image generation. There are no people in this image.")
def get_scene_only_user_prompt(mode: str) -> str:
"""Get the scene-only user prompt for a mode (no human/person keywords)."""
config = _get_prompts_config()
user = config.get("user", {})
scene_config = user.get("scene_only", {})
mode_config = scene_config.get(mode.lower(), {})
return mode_config.get("prompt", "Describe this scene/landscape/object.")
def build_scene_only_system_prompt(prompt_length: int = 150) -> str:
"""
Build complete scene-only system prompt (no human/person keywords).
Used when human detection returns NO to prevent hallucinating people.
Args:
prompt_length: Target word count (0 = no limit)
Returns:
Complete scene-only system prompt string
"""
base = get_scene_only_system_prompt()
# Add length constraint
length_constraint = get_length_constraint(prompt_length)
if length_constraint:
base += f"\n\n{length_constraint}"
return base
def build_scene_only_user_prompt(analysis_mode: str, prompt_length: int = 150) -> str:
"""
Build complete scene-only user prompt (no human/person keywords).
Used when human detection returns NO to prevent hallucinating people.
Args:
analysis_mode: Analysis mode (Standard/Detailed)
prompt_length: Target word count (0 = no limit)
Returns:
Complete scene-only user prompt string
"""
prompt = get_scene_only_user_prompt(analysis_mode)
# Reinforce length constraint
length_constraint = get_length_constraint(prompt_length)
if length_constraint:
prompt += f"\n\nREMEMBER: {length_constraint}"
return prompt
# =============================================================================
# Human With Subject Prompts (Balanced - Human AND Pet/Vehicle/Landscape)
# =============================================================================
def get_human_with_subject_system_prompt() -> str:
"""Get the human_with_subject system prompt (balanced human + subject)."""
config = _get_prompts_config()
system = config.get("system", {})
hws_config = system.get("human_with_subject", {})
return hws_config.get("base", "Describe both the human and the other subject in this image with equal detail.")
def get_human_with_subject_user_prompt(mode: str) -> str:
"""Get the human_with_subject user prompt for a mode."""
config = _get_prompts_config()
user = config.get("user", {})
hws_config = user.get("human_with_subject", {})
mode_config = hws_config.get(mode.lower(), {})
return mode_config.get("prompt", "Describe both the human and the other subject equally.")
def build_human_with_subject_system_prompt(prompt_length: int = 150) -> str:
"""
Build complete human_with_subject system prompt.
Used when image has both a human AND another subject (pet, vehicle, landscape).
Args:
prompt_length: Target word count (0 = no limit)
Returns:
Complete human_with_subject system prompt string
"""
base = get_human_with_subject_system_prompt()
# Add length constraint
length_constraint = get_length_constraint(prompt_length)
if length_constraint:
base += f"\n\n{length_constraint}"
return base
def build_human_with_subject_user_prompt(analysis_mode: str, prompt_length: int = 150) -> str:
"""
Build complete human_with_subject user prompt.
Used when image has both a human AND another subject.
Args:
analysis_mode: Analysis mode (Standard/Detailed)
prompt_length: Target word count (0 = no limit)
Returns:
Complete human_with_subject user prompt string
"""
prompt = get_human_with_subject_user_prompt(analysis_mode)
# Reinforce length constraint
length_constraint = get_length_constraint(prompt_length)
if length_constraint:
prompt += f"\n\nREMEMBER: {length_constraint}"
return prompt
# =============================================================================
# Initialization Check
# =============================================================================
def check_config_files() -> Dict[str, bool]:
"""Check which config files exist and are loadable."""
files = ["providers.toml", "prompts.toml", "components.toml", "filters.toml"]
status = {}
for f in files:
filepath = _CONFIG_DIR / f
status[f] = filepath.exists()
if status[f]:
try:
_load_toml(f)
except Exception:
status[f] = False
# Check modular prompts directory
if _PROMPTS_DIR.exists():
status["prompts/ (modular)"] = True
modular_files = list(_PROMPTS_DIR.glob("*.toml"))
styles_dir = _PROMPTS_DIR / "styles"
if styles_dir.exists():
modular_files.extend(styles_dir.glob("*.toml"))
status["prompts/ file count"] = len(modular_files)
else:
status["prompts/ (modular)"] = False
return status
def get_loaded_prompt_files() -> List[str]:
"""Get list of prompt files that were loaded (for debugging)."""
files = []
if _PROMPTS_DIR.exists() and _PROMPTS_DIR.is_dir():
# Base config first
base_file = _PROMPTS_DIR / "_base.toml"
if base_file.exists():
files.append(base_file.name)
# Main prompt files
for filename in ["portrait.toml", "scene.toml", "vocabulary.toml", "classification.toml"]:
filepath = _PROMPTS_DIR / filename
if filepath.exists():
files.append(filename)
# Style-specific files
styles_dir = _PROMPTS_DIR / "styles"
if styles_dir.exists() and styles_dir.is_dir():
for filepath in sorted(styles_dir.glob("*.toml")):
files.append(f"styles/{filepath.name}")
if not files:
files.append("prompts.toml (fallback)")
return files
# Print status on import (for debugging)
if __name__ == "__main__":
print("Config file status:")
for file, value in check_config_files().items():
if isinstance(value, bool):
status = "OK" if value else "MISSING"
else:
status = str(value)
print(f" {file}: {status}")
print("\nLoaded prompt files:")
for f in get_loaded_prompt_files():
print(f" - {f}")
print("\nProvider tiers:")
for p in ["anthropic", "openai", "ollama", "lmstudio"]:
print(f" {p}: {get_provider_tier(p)}")
print("\nSample system prompt (basic tier):")
prompt = get_system_prompt("basic")
print(prompt[:200] + "..." if len(prompt) > 200 else prompt)
print("\nSample scene-only system prompt:")
scene_prompt = get_scene_only_system_prompt()
print(scene_prompt[:200] + "..." if len(scene_prompt) > 200 else scene_prompt)