From bc90b37ea24b5bc1b413715046ca487d33266521 Mon Sep 17 00:00:00 2001 From: PXL8D Date: Tue, 12 Aug 2025 16:37:02 -0700 Subject: [PATCH] Implement Universal Dynamic Metadata Extraction System - Session 5 Priority 1 --- metaman.py | 445 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 383 insertions(+), 62 deletions(-) diff --git a/metaman.py b/metaman.py index 8981cb5..ef81c95 100644 --- a/metaman.py +++ b/metaman.py @@ -682,74 +682,395 @@ class MetaManLoadImage: return params def _extract_params_from_comfyui_prompt(self, prompt_data: dict) -> dict: - """Extract generation parameters from ComfyUI prompt data""" + """Universal dynamic extraction from ComfyUI prompt data - works with any node ecosystem""" params = {} - try: - for node_id, node_data in prompt_data.items(): - if not isinstance(node_data, dict): - continue - - class_type = node_data.get('class_type', '') - inputs = node_data.get('inputs', {}) - - # Extract from KSampler nodes - if class_type == 'KSampler': - if 'steps' in inputs: - params['steps'] = inputs['steps'] - if 'cfg' in inputs: - params['cfg_scale'] = inputs['cfg'] - if 'sampler_name' in inputs: - params['sampler'] = inputs['sampler_name'] - if 'scheduler' in inputs: - params['scheduler'] = inputs['scheduler'] - if 'seed' in inputs: - params['seed'] = inputs['seed'] - if 'denoise' in inputs: - params['denoising_strength'] = inputs['denoise'] - - # Extract from CheckpointLoaderSimple - elif class_type == 'CheckpointLoaderSimple': - if 'ckpt_name' in inputs: - params['model_name'] = inputs['ckpt_name'] - - # Extract from CLIPTextEncode (prompts) - elif class_type == 'CLIPTextEncode': - if 'text' in inputs: - # First positive prompt we find - if 'prompt' not in params and inputs['text'].strip(): - params['prompt'] = inputs['text'] - # If we already have a prompt, this might be negative - elif 'negative_prompt' not in params and inputs['text'].strip(): - # Simple heuristic: if it contains common negative words - negative_indicators = ['worst', 'low quality', 'blurry', 'bad', 'ugly', 'deformed'] - if any(indicator in inputs['text'].lower() for indicator in negative_indicators): - params['negative_prompt'] = inputs['text'] - - # Extract from EmptyLatentImage (dimensions) - elif class_type == 'EmptyLatentImage': - if 'width' in inputs: - params['width'] = inputs['width'] - if 'height' in inputs: - params['height'] = inputs['height'] - - # Extract from LoraLoader nodes - elif class_type == 'LoraLoader': - if 'lora_name' in inputs and 'strength_model' in inputs: - if 'loras' not in params: - params['loras'] = [] - lora_info = { - 'name': inputs['lora_name'], - 'weight': inputs['strength_model'] - } - if 'strength_clip' in inputs: - lora_info['clip_weight'] = inputs['strength_clip'] - params['loras'].append(lora_info) + print(f"MetaMan Universal: Starting extraction from {len(prompt_data)} nodes") + try: + # Phase 1: Extract core parameters universally + self._extract_core_parameters_universal(prompt_data, params) + + # Phase 2: Collect ALL text content from all nodes + all_text_content = self._scan_all_text_content_universal(prompt_data) + print(f"MetaMan Universal: Found {len(all_text_content)} text candidates") + + # Phase 3: Classify text content using improved heuristics + classified_prompts = self._classify_prompts_intelligent(all_text_content) + + # Phase 4: Extract LoRAs from all text content + all_loras = self._extract_loras_universal(all_text_content) + + # Phase 5: Compile final results + if classified_prompts['positive']: + params['prompt'] = classified_prompts['positive']['text'] + print(f"MetaMan Universal: POSITIVE from {classified_prompts['positive']['source']}: {params['prompt'][:100]}...") + + if classified_prompts['negative']: + params['negative_prompt'] = classified_prompts['negative']['text'] + print(f"MetaMan Universal: NEGATIVE from {classified_prompts['negative']['source']}: {params['negative_prompt'][:100]}...") + + if all_loras: + params['loras'] = all_loras + print(f"MetaMan Universal: Extracted {len(all_loras)} LoRAs: {[lora['name'] for lora in all_loras]}") + + print(f"MetaMan Universal: Extraction complete: {list(params.keys())}") + except Exception as e: - print(f"MetaMan Load Image: Error extracting ComfyUI params: {e}") + print(f"MetaMan Universal Extraction Error: {e}") + import traceback + traceback.print_exc() return params + + def _extract_core_parameters_universal(self, prompt_data: dict, params: dict): + """Extract core parameters from all nodes universally""" + print(f"MetaMan Universal: Extracting core parameters...") + + for node_id, node_data in prompt_data.items(): + if not isinstance(node_data, dict): + continue + + class_type = node_data.get('class_type', '') + inputs = node_data.get('inputs', {}) + + self._extract_core_parameters_from_node(inputs, class_type, params, node_id) + + def _extract_core_parameters_from_node(self, inputs: dict, class_type: str, params: dict, node_id: str): + """Extract core parameters from any node type""" + # Universal parameter mapping with broader coverage + param_mappings = { + 'steps': ['steps', 'sampling_steps', 'num_steps', 'inference_steps'], + 'cfg_scale': ['cfg', 'cfg_scale', 'guidance_scale', 'guidance'], + 'sampler': ['sampler_name', 'sampler', 'sampling_method', 'sampler_type'], + 'scheduler': ['scheduler', 'scheduler_name', 'noise_schedule', 'scheduler_type'], + 'seed': ['seed', 'noise_seed', 'random_seed', 'generator_seed'], + 'denoising_strength': ['denoise', 'denoising_strength', 'strength', 'denoise_strength'], + 'model_name': ['ckpt_name', 'model_name', 'checkpoint_name', 'model', 'checkpoint'], + 'width': ['width', 'image_width', 'w', 'img_width'], + 'height': ['height', 'image_height', 'h', 'img_height'] + } + + for param_key, possible_fields in param_mappings.items(): + if param_key not in params: # Don't overwrite existing values + for field in possible_fields: + if field in inputs and inputs[field] is not None: + params[param_key] = inputs[field] + print(f"MetaMan Universal: Found {param_key} = {inputs[field]} in {node_id} ({class_type})") + break + + def _find_text_fields(self, inputs: dict) -> list: + """Find all text fields in node inputs""" + text_fields = [] + text_field_names = [ + 'text', 'prompt', 'positive', 'negative', + 'wildcard_text', 'populated_text', 'formatted_text', + 'content', 'description', 'caption' + ] + + for field_name, value in inputs.items(): + if isinstance(value, str) and len(value.strip()) > 0: + # Direct text fields + if any(text_name in field_name.lower() for text_name in text_field_names): + text_fields.append((field_name, value)) + # Check for any other string fields that might contain prompts + elif len(value) > 50: # Likely to be prompt text + text_fields.append((field_name, value)) + + return text_fields + + def _analyze_text_content(self, text: str, field_name: str) -> str: + """Analyze text content to determine if it's positive, negative, or general""" + text_lower = text.lower() + field_lower = field_name.lower() + + # Field name indicators + if 'negative' in field_lower or 'bad' in field_lower: + return 'negative' + elif 'positive' in field_lower or 'prompt' in field_lower: + return 'positive' + + # Content analysis + negative_indicators = [ + 'worst', 'low quality', 'blurry', 'bad', 'ugly', 'deformed', + 'distorted', 'mutation', 'error', 'artifact', 'disfigured', + 'extra limbs', 'missing', 'cropped', 'watermark', 'text' + ] + + positive_indicators = [ + 'masterpiece', 'best quality', 'high resolution', 'detailed', + 'beautiful', 'professional', 'sharp focus', 'cinematic', + 'score_9', 'score_8', 'photorealistic', 'ultra' + ] + + negative_count = sum(1 for indicator in negative_indicators if indicator in text_lower) + positive_count = sum(1 for indicator in positive_indicators if indicator in text_lower) + + if negative_count > positive_count and negative_count > 2: + return 'negative' + elif positive_count > 0 or len(text) > 100: # Longer text usually positive + return 'positive' + else: + return 'general' + + def _score_text_relevance(self, text: str, field_name: str, category: str) -> float: + """Score text relevance for prompt selection""" + score = 0.0 + + # Field name scoring + field_lower = field_name.lower() + if 'populated' in field_lower or 'formatted' in field_lower: + score += 10.0 # Processed text is usually better + elif 'text' in field_lower or 'prompt' in field_lower: + score += 5.0 + + # Content scoring + if category == 'positive': + score += len(text) * 0.01 # Longer positive prompts often better + if ' list: + """Extract LoRA information from text using regex""" + import re + + loras = [] + + # Pattern for or + lora_pattern = r':]+)(?::([0-9.]+))?[^>]*>' + matches = re.findall(lora_pattern, text, re.IGNORECASE) + + for match in matches: + name = match[0].strip() + weight = float(match[1]) if match[1] else 1.0 + + loras.append({ + 'name': name, + 'weight': weight + }) + + return loras + + def _scan_all_text_content_universal(self, prompt_data: dict) -> list: + """Scan ALL nodes for text content with enhanced detection""" + text_candidates = [] + + for node_id, node_data in prompt_data.items(): + if not isinstance(node_data, dict): + continue + + class_type = node_data.get('class_type', '') + inputs = node_data.get('inputs', {}) + + # Enhanced text field detection + for field_name, field_value in inputs.items(): + if isinstance(field_value, str) and len(field_value.strip()) > 5: + text_candidates.append({ + 'text': field_value, + 'source': f"{node_id}.{field_name}", + 'node_id': node_id, + 'field_name': field_name, + 'node_type': class_type, + 'length': len(field_value), + 'is_text_field': self._is_likely_text_field(field_name), + 'is_processed_field': self._is_processed_text_field(field_name) + }) + + print(f"MetaMan Universal: Found text in {node_id}.{field_name} ({class_type}): {len(field_value)} chars") + + return text_candidates + + def _is_likely_text_field(self, field_name: str) -> bool: + """Check if field name indicates text content""" + text_indicators = [ + 'text', 'prompt', 'positive', 'negative', 'content', 'description', + 'caption', 'wildcard', 'populated', 'formatted', 'input', 'output' + ] + field_lower = field_name.lower() + return any(indicator in field_lower for indicator in text_indicators) + + def _is_processed_text_field(self, field_name: str) -> bool: + """Check if field name indicates processed/formatted text""" + processed_indicators = ['populated', 'formatted', 'processed', 'final', 'output', 'result'] + field_lower = field_name.lower() + return any(indicator in field_lower for indicator in processed_indicators) + + def _classify_prompts_intelligent(self, text_candidates: list) -> dict: + """Intelligent classification of text content into positive/negative prompts""" + classified = {'positive': None, 'negative': None} + + # Score all candidates for positive/negative likelihood + positive_candidates = [] + negative_candidates = [] + + for candidate in text_candidates: + if len(candidate['text']) < 20: # Skip very short text + continue + + pos_score = self._score_as_positive_prompt(candidate) + neg_score = self._score_as_negative_prompt(candidate) + + candidate['positive_score'] = pos_score + candidate['negative_score'] = neg_score + + print(f"MetaMan Universal: {candidate['source']} - Pos: {pos_score:.1f}, Neg: {neg_score:.1f}") + + if pos_score > 0.5: + positive_candidates.append(candidate) + if neg_score > 0.5: + negative_candidates.append(candidate) + + # Select best positive prompt + if positive_candidates: + positive_candidates.sort(key=lambda x: x['positive_score'], reverse=True) + classified['positive'] = positive_candidates[0] + print(f"MetaMan Universal: Best positive: {classified['positive']['source']} (score: {classified['positive']['positive_score']:.1f})") + + # Select best negative prompt + if negative_candidates: + negative_candidates.sort(key=lambda x: x['negative_score'], reverse=True) + classified['negative'] = negative_candidates[0] + print(f"MetaMan Universal: Best negative: {classified['negative']['source']} (score: {classified['negative']['negative_score']:.1f})") + + return classified + + def _score_as_positive_prompt(self, candidate: dict) -> float: + """Score text as likely positive prompt""" + score = 0.0 + text = candidate['text'] + text_lower = text.lower() + field_name = candidate['field_name'].lower() + + # Field name indicators (strong signals) + if 'positive' in field_name or 'prompt' in field_name: + score += 3.0 + if 'populated' in field_name or 'formatted' in field_name: + score += 2.0 # Processed text usually main prompt + if 'text' in field_name: + score += 1.0 + + # Content indicators + positive_keywords = [ + 'score_9', 'score_8', 'masterpiece', 'best quality', 'high resolution', + 'detailed', 'beautiful', 'professional', 'cinematic', 'photorealistic', + 'ultra', 'highly detailed', 'intricate', 'sharp focus', 'realistic' + ] + + keyword_count = sum(1 for keyword in positive_keywords if keyword in text_lower) + score += keyword_count * 0.5 + + # LoRA presence (strong indicator of main prompt) + if ' 200: + score += 2.0 + elif len(text) > 100: + score += 1.0 + elif len(text) > 50: + score += 0.5 + + # Negative indicators (reduce score) + negative_keywords = ['worst', 'low quality', 'bad', 'ugly', 'blurry', 'deformed'] + negative_count = sum(1 for keyword in negative_keywords if keyword in text_lower) + if negative_count > 2: + score -= 2.0 # Likely negative prompt + + return max(0.0, score) + + def _score_as_negative_prompt(self, candidate: dict) -> float: + """Score text as likely negative prompt""" + score = 0.0 + text = candidate['text'] + text_lower = text.lower() + field_name = candidate['field_name'].lower() + + # Field name indicators + if 'negative' in field_name: + score += 5.0 # Very strong indicator + elif 'bad' in field_name or 'unwanted' in field_name: + score += 3.0 + + # Content analysis - negative keywords + negative_keywords = [ + 'worst quality', 'low quality', 'bad', 'ugly', 'blurry', 'deformed', + 'distorted', 'mutation', 'error', 'artifact', 'disfigured', + 'extra limbs', 'missing', 'cropped', 'watermark', 'text', 'signature', + 'username', 'lowres', 'jpeg artifacts', 'duplicate', 'morbid', + 'mutilated', 'poorly drawn', 'extra fingers', 'fused fingers', + 'too many fingers', 'unclear eyes', 'lowers', 'bad anatomy', + 'bad hands', 'missing fingers', 'extra digit', 'fewer digits' + ] + + keyword_count = sum(1 for keyword in negative_keywords if keyword in text_lower) + score += keyword_count * 0.7 + + # Pattern analysis - negative prompts often have parentheses with weights + parentheses_content = re.findall(r'\([^)]+:[0-9.]+\)', text) + if parentheses_content: + score += len(parentheses_content) * 0.5 + + # Short text with negative words likely negative prompt + if len(text) < 200 and keyword_count > 1: + score += 1.0 + + # Positive indicators (reduce score) + positive_keywords = ['masterpiece', 'best quality', 'detailed', 'beautiful'] + positive_count = sum(1 for keyword in positive_keywords if keyword in text_lower) + if positive_count > 1: + score -= 1.5 # Likely positive prompt + + # LoRA presence (usually in positive prompts) + if ' list: + """Extract LoRAs from all text content""" + all_loras = [] + seen_loras = set() + + for candidate in text_candidates: + loras_in_text = self._extract_loras_from_text(candidate['text']) + for lora in loras_in_text: + lora_key = lora['name'].lower() + if lora_key not in seen_loras: + seen_loras.add(lora_key) + all_loras.append(lora) + print(f"MetaMan Universal: Found LoRA '{lora['name']}' (weight: {lora['weight']}) in {candidate['source']}") + + return all_loras class MetaManUniversalNodeV2: