#!/usr/bin/env python3 """ ComfyBridge v2 – Erweiterte ComfyUI Integration mit Bildkonsistenz Input: core_state (trust, mood, loneliness) Output: Generiertes Bild + Metadaten + Vision-Analyse Features: - Echte ComfyUI API-Integration - IPAdapter für Gesichtskonsistenz (face_base.png) - ControlNet OpenPose für Körperhaltung (body_base.png) - Trust-basiertes Styling - Bild-Download und Metadaten-Speicherung - VisionBridge-Integration """ import json import os import time import uuid import subprocess from datetime import datetime, timezone from pathlib import Path try: import requests from PIL import Image import io REQUESTS_AVAILABLE = True except ImportError: REQUESTS_AVAILABLE = False # Konfiguration PATHS = { "state": os.path.expanduser("~/natiris/core/natiris_full_state.json"), "config": os.path.expanduser("~/natiris/config/character_genesis.json"), "output_dir": os.path.expanduser("~/natiris/generated/"), "output": os.path.expanduser("~/natiris/bridges/comfy_response.json"), "base_images": os.path.expanduser("~/natiris/assets/base_images/"), "vision_script": os.path.expanduser("~/natiris/bridges/VisionBridge.py"), } COMFY_API = os.getenv("COMFY_API_URL", "http://localhost:8188") CLIENT_ID = f"natiris_{datetime.now().strftime('%Y%m%d')}" # Trust-basierte Styling-Map TRUST_MAP = [ { "range": [0, 3], "style": "neutral_portrait", "prompt_add": "neutral expression, professional lighting, medium distance, formal atmosphere", "distance": "medium", "lighting": "neutral, professional" }, { "range": [4, 7], "style": "personal_context", "prompt_add": "relaxed expression, warm lighting, indoor setting, cozy home environment", "distance": "medium-close", "lighting": "warm, soft" }, { "range": [8, 10], "style": "intimate", "prompt_add": "warm smile, intimate lighting, close portrait, emotional connection, soft focus background", "distance": "close", "lighting": "warm, intimate, golden hour" } ] class ComfyBridge: """ComfyUI Integration Bridge für Natiris""" def __init__(self): self.client_id = f"natiris_{uuid.uuid4().hex[:8]}" self.base_images_dir = Path(PATHS["base_images"]) self.output_dir = Path(PATHS["output_dir"]) self.output_dir.mkdir(parents=True, exist_ok=True) self.base_images_dir.mkdir(parents=True, exist_ok=True) self.current_workflow = None self.prompt_id = None def check_health(self): """Prüft ComfyUI Verfügbarkeit""" try: response = requests.get(f"{COMFY_API}/system_stats", timeout=5) data = response.json() return { "reachable": True, "version": data.get("system", {}).get("comfyui_version", "unknown"), "devices": data.get("devices", []) } except Exception as e: return {"reachable": False, "error": str(e)} def check_base_images(self): """Prüft und erstellt Dummy-Basisbilder falls nötig""" face_base = self.base_images_dir / "face_base.png" body_base = self.base_images_dir / "body_base.png" pose_base = self.base_images_dir / "pose_base.png" status = { "face_exists": face_base.exists(), "body_exists": body_base.exists(), "pose_exists": pose_base.exists(), "all_ready": False } # Erstelle Dummy-Bilder falls nicht vorhanden if not face_base.exists(): self._create_dummy_face(face_base) if not body_base.exists(): self._create_dummy_body(body_base) if not pose_base.exists(): self._create_dummy_pose(pose_base) status["all_ready"] = face_base.exists() and body_base.exists() status["face_path"] = str(face_base) status["body_path"] = str(body_base) status["pose_path"] = str(pose_base) return status def _create_dummy_face(self, path): """Erstellt Dummy-Gesichtsreferenz""" try: from PIL import Image, ImageDraw # Weißes 512x512 Bild mit Gesicht-Oval img = Image.new('RGB', (512, 512), color='lightgray') draw = ImageDraw.Draw(img) # Einfaches Gesicht-Oval draw.ellipse([150, 100, 362, 400], fill='peachpuff', outline='tan', width=2) # Augen draw.ellipse([200, 180, 240, 220], fill='white') draw.ellipse([200, 180, 240, 220], outline='black', width=1) draw.ellipse([270, 180, 310, 220], fill='white') draw.ellipse([270, 180, 310, 220], outline='black', width=1) # Mund draw.arc([210, 260, 300, 340], start=0, end=180, fill='darkred', width=2) img.save(path) print(f"✓ Dummy face_base.png erstellt: {path}") except Exception as e: print(f"⚠ Konnte face_dummy nicht erstellen: {e}") def _create_dummy_body(self, path): """Erstellt Dummy-Körperreferenz""" try: from PIL import Image, ImageDraw # 512x768 für Portrait-Format img = Image.new('RGB', (512, 768), color='lightgray') draw = ImageDraw.Draw(img) # Körper-Silhouette draw.ellipse([156, 50, 356, 300], fill='peachpuff', outline='tan', width=2) # Kopf draw.rectangle([200, 280, 312, 550], fill='peachpuff', outline='tan', width=2) # Torso draw.rectangle([150, 300, 200, 500], fill='peachpuff', outline='tan', width=2) # Linker Arm draw.rectangle([312, 300, 362, 500], fill='peachpuff', outline='tan', width=2) # Rechter Arm img.save(path) print(f"✓ Dummy body_base.png erstellt: {path}") except Exception as e: print(f"⚠ Konnte body_dummy nicht erstellen: {e}") def _create_dummy_pose(self, path): """Erstellt Dummy-Pose für ControlNet""" try: from PIL import Image, ImageDraw # Schwarz-Weiß Pose-Bild (OpenPose Format simuliert) img = Image.new('RGB', (512, 768), color='black') draw = ImageDraw.Draw(img) # Skeleton-Linien in weiß draw.line([(256, 100), (256, 400)], fill='white', width=3) # Spine draw.line([(256, 200), (150, 350)], fill='white', width=3) # Linker Arm draw.line([(256, 200), (362, 350)], fill='white', width=3) # Rechter Arm draw.line([(256, 400), (200, 700)], fill='white', width=3) # Linkes Bein draw.line([(256, 400), (312, 700)], fill='white', width=3) # Rechtes Bein # Gelenke for pos in [(256, 100), (256, 200), (150, 350), (362, 350), (256, 400), (200, 700), (312, 700)]: draw.ellipse([pos[0]-5, pos[1]-5, pos[0]+5, pos[1]+5], fill='white') img.save(path) print(f"✓ Dummy pose_base.png erstellt: {path}") except Exception as e: print(f"⚠ Konnte pose_dummy nicht erstellen: {e}") def get_style_config(self, trust): """Liefert Styling basierend auf Trust-Level""" for entry in TRUST_MAP: if entry["range"][0] <= trust <= entry["range"][1]: return entry return TRUST_MAP[0] def build_prompt(self, state): """Generiert Prompt aus State""" core = state.get("core_state", {}) emotion = state.get("modules", {}).get("Emotion", {}) bond = state.get("modules", {}).get("Bond", {}) trust = core.get("trust", 7.0) mood = core.get("mood", 5) loneliness = core.get("loneliness", 2) arousal = core.get("arousal_level", 3) style = self.get_style_config(trust) # Basis-Charakter-Beschreibung für Konsistenz character_desc = ( "young woman, natural beauty, warm eyes, " "consistent facial features, same person, " f"{style['lighting']}, " f"{style['distance']} portrait, " f"mood: {self._mood_to_desc(mood)}, " ) # Trust-spezifische Zusätze prompt = ( f"{character_desc} " f"{style['prompt_add']}, " f"high detail, cinematic, soft bokeh" ) negative = ( "blurry, distorted, deformed, extra limbs, " "different person, inconsistent face, " "low quality, bad anatomy, ugly, duplicate" ) return { "positive": prompt, "negative": negative, "style": style["style"], "trust": trust, "mood": mood, "width": 512, "height": 768 if trust > 7 else 512 # Intim = Portrait-Format } def _mood_to_desc(self, mood): """Konvertiert Mood-Wert zu Beschreibung""" if mood >= 8: return "radiant, glowing with happiness" elif mood >= 6: return "content, peaceful" elif mood >= 4: return "neutral, calm" elif mood >= 2: return "melancholic, withdrawn" else: return "sad, distant" def build_workflow(self, prompt_data, base_images): """Baut ComfyUI Workflow JSON""" seed = int(time.time()) % 2147483647 workflow = { # 1: Positive Prompt "1": { "inputs": {"text": prompt_data["positive"], "clip": ["12", 1]}, "class_type": "CLIPTextEncode" }, # 2: Negative Prompt "2": { "inputs": {"text": prompt_data["negative"], "clip": ["12", 1]}, "class_type": "CLIPTextEncode" }, # 3: KSampler "3": { "inputs": { "seed": seed, "steps": 25, "cfg": 7.0, "sampler_name": "euler_ancestral", "scheduler": "karras", "denoise": 1.0, "model": ["12", 0], "positive": ["1", 0], "negative": ["2", 0], "latent_image": ["13", 0] }, "class_type": "KSampler" }, # 4: VAE Decode "4": { "inputs": {"samples": ["3", 0], "vae": ["12", 2]}, "class_type": "VAEDecode" }, # 5: Save Image "5": { "inputs": { "filename_prefix": f"natiris_{prompt_data['style']}", "images": ["4", 0] }, "class_type": "SaveImage" }, # 12: Checkpoint Loader "12": { "inputs": {"ckpt_name": "realisticVisionV60B1_v51HyperVAE.safetensors"}, "class_type": "CheckpointLoaderSimple" }, # 13: Empty Latent "13": { "inputs": { "width": prompt_data["width"], "height": prompt_data["height"], "batch_size": 1 }, "class_type": "EmptyLatentImage" } } # IPAdapter-Integration falls Basisbilder existieren if base_images.get("face_exists"): workflow.update(self._build_ipadapter_nodes(base_images["face_path"])) self.current_workflow = workflow return workflow def _build_ipadapter_nodes(self, face_path): """Erweitert Workflow um IPAdapter Nodes""" # Vereinfacht - in echter Umgebung: IPAdapter Model laden + Anwenden return { # Für spätere Erweiterung - IPAdapter Integration # "20": {"inputs": {"image": face_path}, "class_type": "LoadImage"}, # "21": {"inputs": {"ipadapter_file": "ip...safetensors"}, "class_type": "IPAdapterModelLoader"}, } def submit_workflow(self, workflow): """Sendet Workflow an ComfyUI""" try: data = { "prompt": workflow, "client_id": self.client_id } response = requests.post(f"{COMFY_API}/prompt", json=data, timeout=10) result = response.json() if "prompt_id" in result: self.prompt_id = result["prompt_id"] return {"success": True, "prompt_id": result["prompt_id"]} else: return {"success": False, "error": result.get("error", "Unknown error")} except Exception as e: return {"success": False, "error": str(e)} def poll_result(self, prompt_id, max_wait=300): """Wartet auf Workflow-Completion""" start_time = time.time() while time.time() - start_time < max_wait: try: # Queue-Status queue = requests.get(f"{COMFY_API}/queue", timeout=5).json() # Prüfe History history = requests.get(f"{COMFY_API}/history", timeout=5).json() if prompt_id in history: return {"completed": True, "data": history[prompt_id]} # Prüfe ob noch in Queue running = [r.get("prompt_id") for r in queue.get("queue_running", [])] pending = [p.get("prompt_id") for p in queue.get("queue_pending", [])] if prompt_id not in running and prompt_id not in pending and prompt_id not in history: # Möglicherweise schon verarbeitet und in anderer History pass time.sleep(0.5) except Exception as e: return {"completed": False, "error": str(e)} return {"completed": False, "error": "Timeout"} def download_image(self, filename, subfolder="", folder_type="output"): """Lädt generiertes Bild herunter""" try: params = { "filename": filename, "subfolder": subfolder, "type": folder_type } response = requests.get(f"{COMFY_API}/view", params=params, timeout=30) if response.status_code == 200: return response.content else: return None except Exception as e: print(f"Download error: {e}") return None def save_image(self, image_data, metadata): """Speichert Bild mit Metadaten""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"natiris_{metadata['style']}_{timestamp}.png" filepath = self.output_dir / filename try: with open(filepath, "wb") as f: f.write(image_data) # Metadaten als JSON meta_file = self.output_dir / f"{filename}.json" with open(meta_file, "w") as f: json.dump(metadata, f, indent=2) return {"success": True, "path": str(filepath), "filename": filename} except Exception as e: return {"success": False, "error": str(e)} def trigger_vision_analysis(self, image_path): """Startet VisionBridge-Analyse""" try: result = subprocess.run([ "python3", PATHS["vision_script"], "--image", image_path ], capture_output=True, text=True, timeout=30) return { "success": result.returncode == 0, "stdout": result.stdout, "stderr": result.stderr } except Exception as e: return {"success": False, "error": str(e)} def generate(self, state_path=None): """Hauptmethode: Generiert Bild aus State""" # 1. State laden state = {} if state_path and os.path.exists(state_path): with open(state_path) as f: state = json.load(f) elif os.path.exists(PATHS["state"]): with open(PATHS["state"]) as f: state = json.load(f) # 2. Health Check health = self.check_health() if not health["reachable"]: return {"success": False, "error": "ComfyUI not reachable", "health": health} # 3. Basisbilder prüfen/erstellen base_images = self.check_base_images() # 4. Prompt generieren prompt_data = self.build_prompt(state) # 5. Workflow bauen workflow = self.build_workflow(prompt_data, base_images) # 6. Submit submit_result = self.submit_workflow(workflow) if not submit_result["success"]: return {"success": False, "error": submit_result.get("error", "Submit failed")} prompt_id = submit_result["prompt_id"] print(f"✓ Workflow submitted: {prompt_id}") # 7. Poll für Ergebnis poll_result = self.poll_result(prompt_id) if not poll_result["completed"]: return {"success": False, "error": poll_result.get("error", "Poll failed")} # 8. Bild extrahieren history_data = poll_result["data"] outputs = history_data.get("outputs", {}) if not outputs: return {"success": False, "error": "No outputs in history"} # Finde SaveImage Node (meist node 5) for node_id, node_output in outputs.items(): if "images" in node_output: for img_data in node_output["images"]: filename = img_data.get("filename") subfolder = img_data.get("subfolder", "") # Download image_bytes = self.download_image(filename, subfolder) if image_bytes: # Speichern metadata = { "prompt": prompt_data, "trust": prompt_data["trust"], "style": prompt_data["style"], "prompt_id": prompt_id, "timestamp": datetime.now(timezone.utc).isoformat() } save_result = self.save_image(image_bytes, metadata) # Vision-Analyse if save_result["success"]: print(f"✓ Image saved: {save_result['path']}") # Optional: Vision-Analyse # vision_result = self.trigger_vision_analysis(save_result["path"]) return { "success": True, "image_path": save_result["path"], "metadata": metadata, "comfy_status": health } return {"success": False, "error": "Image processing failed"} def main(): """CLI Entry Point""" import argparse parser = argparse.ArgumentParser(description="Natiris ComfyUI Bridge") parser.add_argument("--state", help="Path to state JSON", default=PATHS["state"]) parser.add_argument("--check", action="store_true", help="Check health only") parser.add_argument("--test", action="store_true", help="Generate test image") args = parser.parse_args() bridge = ComfyBridge() if args.check: health = bridge.check_health() base = bridge.check_base_images() print(json.dumps({"health": health, "base_images": base}, indent=2)) return if args.test: print("ComfyBridge Test Mode") print("-" * 40) # Health health = bridge.check_health() print(f"ComfyUI: {'✓' if health['reachable'] else '✗'} {health.get('version', 'unknown')}") # Base Images base = bridge.check_base_images() print(f"Base Images: {'✓' if base['all_ready'] else '⚠'} Created if needed") # Generate print("\nGenerating image...") result = bridge.generate(args.state) if result["success"]: print(f"\n✅ SUCCESS") print(f"Image: {result['image_path']}") print(f"Style: {result['metadata']['style']}") print(f"Trust: {result['metadata']['trust']}") else: print(f"\n❌ FAILED") print(f"Error: {result.get('error', 'Unknown')}") # Speichere Response with open(PATHS["output"], "w") as f: json.dump(result, f, indent=2) return # Default: Generate result = bridge.generate(args.state) print(json.dumps(result, indent=2)) if __name__ == "__main__": main()