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:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user