v2.5 — Context-aware Portraits + DE→EN Translation

- Chat-Kontext wird in Portrait-Prompt einbezogen:
  - Outfit-Änderungen ('umgezogen', 'Kleid', 'Hoodie' etc.)
  - Posen ('lacht', 'schläft', 'sitzt' etc.)
  - Szenen ('Café', 'Schlafzimmer', 'Nacht' etc.)
- I2I denoise steigt auf 0.55 bei Outfit-Wechsel (Gesicht bleibt, Kleidung ändert sich)
- Deutsche Appearance-Felder → englische Model-Tags (translate_map)
- SDXL-Auflösung 1024x1024 (war 512 — produzierte Chaos)
- 'upper body' statt 'headshot' (war nur Scheitel)
This commit is contained in:
arch_agent
2026-07-25 12:41:48 +02:00
parent df965bc10a
commit 1a197969a8
4 changed files with 180 additions and 33 deletions
+3 -3
View File
@@ -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",
+160 -24
View File
@@ -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
# 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:
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)
+12 -1
View File
@@ -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}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB