diff --git a/characters/mara.json b/characters/mara.json index 456f0b6..b358ac6 100644 --- a/characters/mara.json +++ b/characters/mara.json @@ -9,12 +9,12 @@ "scenario": "Du sitzt in einem kleinen Café. Es regnet draußen. Mara steht hinter der Theke.", "nsfw": "Keine Einschränkungen. Kann über alles sprechen. Flirten und Anzügliches sind ok — Mara ist nicht prüde, aber auch nicht leicht zu kriegen.", "greeting": "*wischt den Tresen ab, schickt kurz hoch* Na. Der übliche? Oder willst du mich heute überraschen?", - "model": "", - "temperature": 0.85, + "model": "glm4:latest", + "temperature": 0.8, "appearance": { "style": "anime", "gender": "female", - "age": "28", + "age": "24", "height": "168cm", "build": "schlank", "hair_color": "dunkelbraun", diff --git a/comfyui_integration.py b/comfyui_integration.py index 16aad3d..cd4e429 100644 --- a/comfyui_integration.py +++ b/comfyui_integration.py @@ -19,6 +19,77 @@ def is_comfyui_running() -> bool: except: return False +def build_context_from_messages(messages: list, character_name: str) -> str: + """Extrahiert visuelle Hinweise aus den letzten Chat-Nachrichten.""" + if not messages: + return "" + + # Nehme die letzten 10 Nachrichten + recent = messages[-10:] + + # Sammle nur Bot-Nachrichten — die beschreiben was sie trägt/macht + bot_msgs = [m["content"] for m in recent if m["role"] == "assistant"] + if not bot_msgs: + return "" + + # Keywords die das Aussehen verändern könnten + outfit_keywords = { + "umgezogen": "outfit change", + "Kleid": "dress", "Jacke": "jacket", "Mantel": "coat", "Hoodie": "hoodie", + "Bademant": "bathrobe", "Nachthemd": "nightgown", "Pyjama": "pajamas", + "Bikini": "bikini", "Unterwäsch": "lingerie", "nackt": "nude", + "Schwimmanzug": "swimsuit", "Sport": "sportswear", "Leggings": "leggings", + "Krawatte": "necktie", "Anzug": "suit", "Uniform": "uniform", + "Schürze": "apron", "Tank Top" : "tank top", "Crop-Top": "crop top", + "Pullover": "sweater", "Bluse": "blouse", "Hemd": "shirt", + } + + pose_keywords = { + "lacht": "smiling", "weint": "crying", "lächelt": "smiling", + "schläft": "sleeping", "schläfrig": "sleepy", + "sitzt": "sitting", "steht": "standing", "liegt": "lying down", + "fließt": "leaning forward", "geht": "walking", + "schaut": "looking at viewer", "dreht": "turning away", + "streckt": "stretching", "gähnt": "yawning", + "wütend": "angry", "traurig": "sad", "überrascht": "surprised", + "verlegen": "embarrassed", "errötet": "blushing", + } + + scene_keywords = { + "Café": "cafe interior", "Bar": "bar", "Küche": "kitchen", + "Schlafzimmer": "bedroom", "Badezimmer": "bathroom", + "Bett": "on bed", "Sofa": "on sofa", "Badewanne": "in bathtub", + "Draußen": "outdoors", "Park": "park", "Straße": "street", + "Regen": "rainy", "Sonne": "sunny", "Nacht": "night", + } + + found_outfits = set() + found_poses = set() + found_scenes = set() + + combined = " ".join(bot_msgs[-3:]) # Letzte 3 Bot-Nachrichten + + for de, en in outfit_keywords.items(): + if de.lower() in combined.lower(): + found_outfits.add(en) + for de, en in pose_keywords.items(): + if de.lower() in combined.lower(): + found_poses.add(en) + for de, en in scene_keywords.items(): + if de.lower() in combined.lower(): + found_scenes.add(en) + + parts = [] + if found_outfits: + parts.append(f"wearing {', '.join(found_outfits)}") + if found_poses: + parts.append(", ".join(found_poses)) + if found_scenes: + parts.append(", ".join(found_scenes)) + + return ", ".join(parts) if parts else "" + + def build_prompt_from_appearance(character: dict, context: str = "") -> str: """Baut einen ComfyUI-Prompt aus dem Character-Aussehen.""" app = character.get("appearance", {}) @@ -36,59 +107,116 @@ def build_prompt_from_appearance(character: dict, context: str = "") -> str: distinctive = app.get("distinctive", "") # Style prefix + # Style prefix and gender tags — adapt to style style_map = { - "anime": "anime style, anime art, ", - "realistic": "photorealistic, realistic, ", - "comic": "comic book style, western comic, ", - "cartoon": "cartoon style, ", - "pixel": "pixel art, ", + "anime": ("anime style, anime art, ", True), + "realistic": ("photorealistic, realistic, detailed skin texture, ", False), + "comic": ("comic book style, western comic, ", False), + "cartoon": ("cartoon style, ", False), + "pixel": ("pixel art, ", False), } - style_prefix = style_map.get(style, "anime style, ") + style_prefix, use_danbooru = style_map.get(style, ("anime style, ", True)) - # Gender - if gender == "female": - gender_tag = "1girl, woman, " - elif gender == "male": - gender_tag = "1boy, man, " + # Gender — Danbooru tags for anime, natural language for others + if use_danbooru: + if gender == "female": + gender_tag = "1girl, woman, " + elif gender == "male": + gender_tag = "1boy, man, " + else: + gender_tag = "person, " else: - gender_tag = "person, " + if gender == "female": + gender_tag = "young woman, " + elif gender == "male": + gender_tag = "young man, " + else: + gender_tag = "person, " + + # Translation map: German appearance values → English tags for the model + translate_map = { + # Hair colors + "dunkelbraun": "dark brown", "hellbraun": "light brown", "braun": "brown", + "schwarz": "black", "blond": "blonde", "blonde": "blonde", + "rot": "red", "rotbraun": "auburn", "weiß": "white", "grau": "gray", + "blau": "blue", "grün": "green", "pink": "pink", "lila": "purple", + "silber": "silver", "feuerrot": "fiery red", "platinum": "platinum blonde", + # Hair styles + "kurz": "short", "kurz, ungeordnet": "short messy", "lang": "long", + "ungeordnet": "messy", "pony": "ponytail", "zöpfe": "twin braids", + "hochsteckfrisur": "updo", "gelockt": "curly", "glatt": "straight", + "wellig": "wavy", "kurz, gepflegt": "short neat", "schulterlang": "shoulder length", + # Eye colors + "braun": "brown", "blau": "blue", "grün": "green", "grau": "gray", + "bernstein": "amber", "haselnuss": "hazel", "violett": "violet", + # Skin + "hell": "fair", "dunkel": "dark", "mittel": "medium", "oliv": "olive", + "blass": "pale", "gebräunt": "tanned", + # Build + "schlank": "slim", "zierlich": "petite", "athletisch": "athletic", + "muskulös": "muscular", "kurvig": "curvy", "kräftig": "stocky", + "dünn": "thin", "groß": "tall", "klein": "short", + # Clothing keywords + "Café-Schürze über schwarzem T-Shirt": "cafe apron over black t-shirt", + "Café-Schürze über schwarzem T-Shirt, Jeans": "cafe apron over black t-shirt, jeans", + "Café-Schürze": "cafe apron", "Schürze": "apron", "T-Shirt": "t-shirt", + "Jeans": "jeans", "Kleid": "dress", "Anzug": "suit", "Hoodie": "hoodie", + "Jacke": "jacket", "Mantel": "coat", "Uniform": "uniform", + "über": "over", "schwarzem": "black", "weißem": "white", + "rotem": "red", "blauem": "blue", "grünem": "green", + # Distinctive features + "müde Augen": "tired eyes", "Augenringe": "dark circles under eyes", + "Kaugummi": "chewing gum", "Brille": "glasses", "Tattoo": "tattoo", + "Narben": "scars", "Sommersprossen": "freckles", + "leichte Augenringe": "slight dark circles under eyes", + "immer ein Kaugummi im Mund": "chewing gum", + } + + def tr(val: str) -> str: + """Übersetzt deutsche Wörter zu englischen Model-Tags.""" + if not val: + return val + result = val + for de, en in sorted(translate_map.items(), key=lambda x: -len(x[0])): + result = result.replace(de, en) + return result # Build prompt parts = [ - f"portrait, headshot, {style_prefix}{gender_tag}", + f"upper body, {style_prefix}{gender_tag}", f"{age} years old", ] if build: - parts.append(build) + parts.append(tr(build)) if hair_color and hair_style: - parts.append(f"{hair_color} hair, {hair_style}") + parts.append(f"{tr(hair_color)} hair, {tr(hair_style)}") elif hair_color: - parts.append(f"{hair_color} hair") + parts.append(f"{tr(hair_color)} hair") elif hair_style: - parts.append(hair_style) + parts.append(tr(hair_style)) if eye_color: - parts.append(f"{eye_color} eyes") + parts.append(f"{tr(eye_color)} eyes") if skin: - parts.append(f"{skin} skin") + parts.append(f"{tr(skin)} skin") if clothing: - parts.append(f"wearing {clothing}") + parts.append(f"wearing {tr(clothing)}") if distinctive: - parts.append(distinctive) + parts.append(tr(distinctive)) if context: parts.append(context) prompt = ", ".join(parts) - prompt += ", detailed face, high quality, masterpiece, best quality, simple background" + prompt += ", looking at viewer, detailed face, high quality, masterpiece, best quality, simple background" return prompt -def build_portrait_workflow(prompt: str, negative: str = "", width: int = 512, height: int = 512) -> dict: +def build_portrait_workflow(prompt: str, negative: str = "", width: int = 1024, height: int = 1024) -> dict: return { "3": { "class_type": "KSampler", "inputs": { "seed": int(time.time()) % (2**32), - "steps": 25, + "steps": 30, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", @@ -158,7 +286,7 @@ def wait_for_image(prompt_id: str, timeout: int = 180) -> str | None: time.sleep(3) return None -def build_i2i_workflow(prompt: str, negative: str, reference_image_path: str, width: int = 512, height: int = 512, denoise: float = 0.45) -> dict: +def build_i2i_workflow(prompt: str, negative: str, reference_image_path: str, width: int = 1024, height: int = 1024, denoise: float = 0.45) -> dict: """Image-to-Image Workflow — nutzt ein Referenzbild für Konsistenz.""" return { "3": { @@ -241,7 +369,13 @@ def generate_portrait(character_name: str, character: dict, context: str = "") - AVATARS_DIR.mkdir(parents=True, exist_ok=True) prompt = build_prompt_from_appearance(character, context) - negative = "realistic, 3d, render" + style = character.get("appearance", {}).get("style", "anime") + # Negative prompt depends on style + if style == "anime": + negative = "realistic, 3d, render, photorealistic, " + else: + negative = "anime, 3d, render, " + negative += "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, blurry, deformed, ugly" # Check if we have a reference image for consistency safe_name = character_name.lower().replace(" ", "_") @@ -249,15 +383,17 @@ def generate_portrait(character_name: str, character: dict, context: str = "") - if ref_image.exists(): # Image-to-Image for consistency + # Higher denoise if context mentions outfit change + denoise = 0.55 if any(kw in (context or "").lower() for kw in ["outfit change", "wearing ", "nude", "lingerie", "bikini", "dress", "hoodie", "coat", "jacket"]) else 0.45 try: uploaded_name = upload_image_to_comfy(str(ref_image)) - workflow = build_i2i_workflow(prompt, negative, uploaded_name, 512, 512, denoise=0.45) + workflow = build_i2i_workflow(prompt, negative, uploaded_name, 1024, 1024, denoise=denoise) except Exception as e: print(f"I2I upload failed, falling back to T2I: {e}") - workflow = build_portrait_workflow(prompt, negative, 512, 512) + workflow = build_portrait_workflow(prompt, negative, 1024, 1024) else: # First generation — Text-to-Image - workflow = build_portrait_workflow(prompt, negative, 512, 512) + workflow = build_portrait_workflow(prompt, negative, 1024, 1024) try: prompt_id = queue_prompt(workflow) diff --git a/main.py b/main.py index ffe3a04..35c9f47 100644 --- a/main.py +++ b/main.py @@ -15,7 +15,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel -from comfyui_integration import is_comfyui_running, generate_portrait +from comfyui_integration import is_comfyui_running, generate_portrait, build_context_from_messages # === Konfiguration === BASE_DIR = Path(__file__).parent @@ -553,6 +553,17 @@ async def api_generate_portrait(name: str, request: Request): body = await request.json() if request.headers.get("content-type") == "application/json" else {} custom_prompt = body.get("prompt", "") + # Lese letzte Chat-Nachrichten für Kontext (Outfit-Änderungen etc.) + session_id = request.cookies.get(SESSION_COOKIE_NAME, "") + try: + if session_id: + recent_msgs = get_short_term_memory(session_id, name, limit=10) + chat_context = build_context_from_messages(recent_msgs, name) + if chat_context: + custom_prompt = f"{custom_prompt}, {chat_context}" if custom_prompt else chat_context + except Exception as e: + print(f"Context extraction failed (non-fatal): {e}") + import uuid job_id = str(uuid.uuid4())[:8] _portrait_jobs[job_id] = {"status": "generating", "image": None, "error": None} diff --git a/static/avatars/mara.png b/static/avatars/mara.png index d6593f8..c7d0651 100644 Binary files a/static/avatars/mara.png and b/static/avatars/mara.png differ