diff --git a/characters/mara.json b/characters/mara.json index 8ad595e..54ccef3 100644 --- a/characters/mara.json +++ b/characters/mara.json @@ -10,5 +10,18 @@ "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 + "temperature": 0.85, + "appearance": { + "style": "anime", + "gender": "female", + "age": "28", + "height": "168cm", + "build": "schlank", + "hair_color": "dunkelbraun", + "hair_style": "kurz, ungeordnet", + "eye_color": "braun", + "skin": "hell", + "clothing": "Café-Schürze über schwarzem T-Shirt, Jeans", + "distinctive": "müde Augen, leichte Augenringe, immer ein Kaugummi im Mund" + } } \ No newline at end of file diff --git a/comfyui_integration.py b/comfyui_integration.py index 3e65d17..a41102e 100644 --- a/comfyui_integration.py +++ b/comfyui_integration.py @@ -1,5 +1,6 @@ """ NeonChat ComfyUI Integration — Profilbild-Generierung für Characters +Basiert auf dem Character-Aussehen (appearance) """ import json, os, time, shutil @@ -8,7 +9,6 @@ import httpx COMFYUI_URL = os.environ.get("COMFYUI_URL", "http://localhost:8188") COMFYUI_OUTPUT_DIR = Path(os.environ.get("COMFYUI_OUTPUT", str(Path.home() / "comfy" / "ComfyUI" / "output"))) -CHARACTERS_DIR = Path(__file__).parent / "characters" AVATARS_DIR = Path(__file__).parent / "static" / "avatars" def is_comfyui_running() -> bool: @@ -19,6 +19,69 @@ def is_comfyui_running() -> bool: except: return False +def build_prompt_from_appearance(character: dict, context: str = "") -> str: + """Baut einen ComfyUI-Prompt aus dem Character-Aussehen.""" + app = character.get("appearance", {}) + + style = app.get("style", "anime") + gender = app.get("gender", "female") + age = app.get("age", "25") + height = app.get("height", "") + build = app.get("build", "") + hair_color = app.get("hair_color", "") + hair_style = app.get("hair_style", "") + eye_color = app.get("eye_color", "") + skin = app.get("skin", "") + clothing = app.get("clothing", "") + distinctive = app.get("distinctive", "") + + # Style prefix + style_map = { + "anime": "anime style, anime art, ", + "realistic": "photorealistic, realistic, ", + "comic": "comic book style, western comic, ", + "cartoon": "cartoon style, ", + "pixel": "pixel art, ", + } + style_prefix = style_map.get(style, "anime style, ") + + # Gender + if gender == "female": + gender_tag = "1girl, woman, " + elif gender == "male": + gender_tag = "1boy, man, " + else: + gender_tag = "person, " + + # Build prompt + parts = [ + f"portrait, headshot, {style_prefix}{gender_tag}", + f"{age} years old", + ] + if build: + parts.append(build) + if hair_color and hair_style: + parts.append(f"{hair_color} hair, {hair_style}") + elif hair_color: + parts.append(f"{hair_color} hair") + elif hair_style: + parts.append(hair_style) + if eye_color: + parts.append(f"{eye_color} eyes") + if skin: + parts.append(f"{skin} skin") + if clothing: + parts.append(f"wearing {clothing}") + if distinctive: + parts.append(distinctive) + if context: + parts.append(context) + + prompt = ", ".join(parts) + prompt += ", 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: return { "3": { @@ -47,14 +110,14 @@ def build_portrait_workflow(prompt: str, negative: str = "", width: int = 512, h "6": { "class_type": "CLIPTextEncode", "inputs": { - "text": f"portrait, {prompt}, detailed face, high quality, masterpiece, best quality, 1girl, solo", + "text": prompt, "clip": ["4", 1] } }, "7": { "class_type": "CLIPTextEncode", "inputs": { - "text": f"lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, {negative}", + "text": f"lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, blurry, deformed, ugly, {negative}", "clip": ["4", 1] } }, @@ -95,14 +158,15 @@ def wait_for_image(prompt_id: str, timeout: int = 180) -> str | None: time.sleep(3) return None -def generate_portrait(character_name: str, description: str) -> str | None: +def generate_portrait(character_name: str, character: dict, context: str = "") -> str | None: + """Generiert ein Profilbild basierend auf dem Character-Aussehen.""" if not is_comfyui_running(): return None AVATARS_DIR.mkdir(parents=True, exist_ok=True) - prompt = f"anime style, {description}, simple background, headshot" - negative = "blurry, deformed, ugly, realistic" + prompt = build_prompt_from_appearance(character, context) + negative = "realistic, 3d, render" workflow = build_portrait_workflow(prompt, negative, width=512, height=512) @@ -126,4 +190,4 @@ def generate_portrait(character_name: str, description: str) -> str | None: return f"/static/avatars/{safe_name}.png" except Exception as e: print(f"ComfyUI error: {e}") - return None + return None \ No newline at end of file diff --git a/main.py b/main.py index 3bd7405..d292de5 100644 --- a/main.py +++ b/main.py @@ -454,6 +454,7 @@ async def api_create_character(request: Request): "greeting": body.get("greeting", ""), "model": body.get("model", ""), "temperature": body.get("temperature", 0.8), + "appearance": body.get("appearance", {}), } with open(path, "w", encoding="utf-8") as f: @@ -499,9 +500,12 @@ async def api_list_models(): async def api_comfyui_status(): return JSONResponse({"running": is_comfyui_running()}) +# In-memory tracking for portrait generation +_portrait_jobs = {} + @app.post("/api/character/{name}/portrait") async def api_generate_portrait(name: str, request: Request): - """Generiert ein Profilbild via ComfyUI.""" + """Startet Portrait-Generierung — gibt sofort eine Job-ID zurück.""" char = load_character(name) if not is_comfyui_running(): @@ -510,14 +514,27 @@ 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", "") - # Build description from character data - desc = custom_prompt or f"{char.get('personality', '')}, {char.get('background', '')}" + import uuid + job_id = str(uuid.uuid4())[:8] + _portrait_jobs[job_id] = {"status": "generating", "image": None, "error": None} - image_path = generate_portrait(name, desc) - if image_path: - return JSONResponse({"status": "ok", "image": image_path}) - else: - return JSONResponse({"error": "Bildgenerierung fehlgeschlagen"}, status_code=500) + async def run_generation(): + image_path = generate_portrait(name, char, custom_prompt) + if image_path: + _portrait_jobs[job_id] = {"status": "done", "image": image_path, "error": None} + else: + _portrait_jobs[job_id] = {"status": "error", "image": None, "error": "Generierung fehlgeschlagen"} + + asyncio.create_task(run_generation()) + + return JSONResponse({"status": "started", "job_id": job_id}) + +@app.get("/api/portrait/status/{job_id}") +async def api_portrait_status(job_id: str): + """Fragt den Status einer Portrait-Generierung ab.""" + if job_id not in _portrait_jobs: + return JSONResponse({"status": "not_found"}, status_code=404) + return JSONResponse(_portrait_jobs[job_id]) if __name__ == "__main__": import uvicorn diff --git a/static/avatars/mara.png b/static/avatars/mara.png index 3a092ae..47f2f61 100644 Binary files a/static/avatars/mara.png and b/static/avatars/mara.png differ diff --git a/templates/chat.html b/templates/chat.html index 67cb464..f1fc542 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -315,16 +315,48 @@ body: JSON.stringify({}) }); const data = await resp.json(); - if (resp.ok && data.image) { - // Update avatar in header - const avatarEl = document.querySelector('.avatar'); - avatarEl.innerHTML = ``; - btn.textContent = '✅ Fertig!'; - setTimeout(() => { btn.textContent = oldText; btn.disabled = false; }, 2000); - } else { + + if (!resp.ok) { alert(data.error || 'Generierung fehlgeschlagen'); btn.textContent = oldText; btn.disabled = false; + return; + } + + if (data.job_id) { + const jobId = data.job_id; + const pollInterval = setInterval(async () => { + try { + const statusResp = await fetch(`/api/portrait/status/${jobId}`); + const statusData = await statusResp.json(); + + if (statusData.status === 'done' && statusData.image) { + clearInterval(pollInterval); + const avatarEl = document.querySelector('.avatar'); + avatarEl.innerHTML = ``; + btn.textContent = '✅ Fertig!'; + setTimeout(() => { btn.textContent = oldText; btn.disabled = false; }, 2000); + } else if (statusData.status === 'error') { + clearInterval(pollInterval); + alert(statusData.error || 'Generierung fehlgeschlagen'); + btn.textContent = oldText; + btn.disabled = false; + } + } catch(e) { + // ignore poll errors, keep trying + } + }, 3000); + + // Timeout after 5 minutes + setTimeout(() => { + clearInterval(pollInterval); + btn.textContent = oldText; + btn.disabled = false; + }, 300000); + } else if (data.error) { + alert(data.error); + btn.textContent = oldText; + btn.disabled = false; } } catch(e) { alert('Verbindungsfehler: ' + e.message); diff --git a/templates/create.html b/templates/create.html index 78969b5..a7c6108 100644 --- a/templates/create.html +++ b/templates/create.html @@ -7,7 +7,7 @@

✦ Neuer Charakter

-

Definiere Persönlichkeit, Verhalten und Hintergrund. Der Bot bleibt immer in Rolle.

+

Definiere Persönlichkeit, Verhalten, Hintergrund und Aussehen.

+
Basis
@@ -40,50 +46,103 @@
-
+
Persönlichkeit
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
+
Aussehen (für Portrait-Generierung)
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ +
Modell
- + @@ -103,7 +162,6 @@