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 @@
Definiere Persönlichkeit, Verhalten und Hintergrund. Der Bot bleibt immer in Rolle.
+Definiere Persönlichkeit, Verhalten, Hintergrund und Aussehen.