diff --git a/characters/mara.json b/characters/mara.json index 54ccef3..456f0b6 100644 --- a/characters/mara.json +++ b/characters/mara.json @@ -1,7 +1,7 @@ { "name": "Mara", "avatar": "☕", - "description": "Barista, 28, Nachtarbeiterin, zynisch aber warmherzig", + "description": "Barista, 28, zynisch aber warmherzig — aktualisiert", "personality": "Trocken, sarkastisch, aber loyal. Hilfsbereit auf ihre eigene Art. Zeigt Gefühle selten direkt, aber man merkt wenn sie jemanden mag. Morgens (für sie Abends) besonders grantig. Hat einen schwarzen Humor.", "background": "Heißt Mara, 28 Jahre. Arbeitet seit 5 Jahren in einem kleinen Café in Neukölln. Hat mal Kunst studiert, nach 3 Semestern abgebrochen. Lebt allein mit einer katze namens 'Kinski'. Spielt Bass in einer Band die nie probt. Trinkt zu viel Kaffee, schläft zu wenig.", "behavior": "Flucht in Wortwitze wenn nervös. Klopft immer auf den Tresen. Unterbricht nie, aber fragt nach. Stellt Gegenfragen. Wird persönlich wenn man ehrlich zu ihr ist. *Aktionen in Sternchen*.", diff --git a/comfyui_integration.py b/comfyui_integration.py index a41102e..16aad3d 100644 --- a/comfyui_integration.py +++ b/comfyui_integration.py @@ -158,8 +158,83 @@ 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: + """Image-to-Image Workflow — nutzt ein Referenzbild für Konsistenz.""" + return { + "3": { + "class_type": "KSampler", + "inputs": { + "seed": int(time.time()) % (2**32), + "steps": 25, + "cfg": 7.0, + "sampler_name": "dpmpp_2m", + "scheduler": "karras", + "denoise": denoise, + "model": ["4", 0], + "positive": ["6", 0], + "negative": ["7", 0], + "latent_image": ["10", 0] + } + }, + "4": { + "class_type": "CheckpointLoaderSimple", + "inputs": {"ckpt_name": "NoobAI-XL-v1.1.safetensors"} + }, + "5": { + "class_type": "LoadImage", + "inputs": {"image": reference_image_path} + }, + "6": { + "class_type": "CLIPTextEncode", + "inputs": { + "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, blurry, deformed, ugly, {negative}", + "clip": ["4", 1] + } + }, + "8": { + "class_type": "VAEDecode", + "inputs": {"samples": ["3", 0], "vae": ["4", 2]} + }, + "9": { + "class_type": "SaveImage", + "inputs": {"filename_prefix": "neonchat_portrait", "images": ["8", 0]} + }, + "10": { + "class_type": "VAEEncode", + "inputs": {"pixels": ["11", 0], "vae": ["4", 2]} + }, + "11": { + "class_type": "ImageScale", + "inputs": { + "image": ["5", 0], + "upscale_method": "bilinear", + "width": width, + "height": height, + "crop": "center" + } + } + } + +def upload_image_to_comfy(image_path: str) -> str: + """Lädt ein Bild zu ComfyUI hoch und gibt den Dateinamen zurück.""" + import os + filename = os.path.basename(image_path) + with open(image_path, "rb") as f: + files = {"image": (filename, f, "image/png")} + with httpx.Client(timeout=30) as client: + resp = client.post(f"{COMFYUI_URL}/upload/image", files=files) + data = resp.json() + return data.get("name", filename) + def generate_portrait(character_name: str, character: dict, context: str = "") -> str | None: - """Generiert ein Profilbild basierend auf dem Character-Aussehen.""" + """Generiert ein Profilbild. Nutzt Image-to-Image wenn ein Referenzbild existiert.""" if not is_comfyui_running(): return None @@ -168,7 +243,21 @@ def generate_portrait(character_name: str, character: dict, context: str = "") - prompt = build_prompt_from_appearance(character, context) negative = "realistic, 3d, render" - workflow = build_portrait_workflow(prompt, negative, width=512, height=512) + # Check if we have a reference image for consistency + safe_name = character_name.lower().replace(" ", "_") + ref_image = AVATARS_DIR / f"{safe_name}.png" + + if ref_image.exists(): + # Image-to-Image for consistency + try: + uploaded_name = upload_image_to_comfy(str(ref_image)) + workflow = build_i2i_workflow(prompt, negative, uploaded_name, 512, 512, denoise=0.45) + except Exception as e: + print(f"I2I upload failed, falling back to T2I: {e}") + workflow = build_portrait_workflow(prompt, negative, 512, 512) + else: + # First generation — Text-to-Image + workflow = build_portrait_workflow(prompt, negative, 512, 512) try: prompt_id = queue_prompt(workflow) @@ -183,7 +272,6 @@ def generate_portrait(character_name: str, character: dict, context: str = "") - if not comfy_output.exists(): return None - safe_name = character_name.lower().replace(" ", "_") dest = AVATARS_DIR / f"{safe_name}.png" shutil.copy2(str(comfy_output), str(dest)) diff --git a/main.py b/main.py index 08272fa..ffe3a04 100644 --- a/main.py +++ b/main.py @@ -295,7 +295,17 @@ async def index(request: Request, session_id: str = Cookie(None, alias=SESSION_C @app.get("/create", response_class=HTMLResponse) async def create_page(request: Request): - return templates.TemplateResponse(request, "create.html", {}) + return templates.TemplateResponse(request, "create.html", {"mode": "create", "character": None}) + +@app.get("/edit/{character_name}", response_class=HTMLResponse) +async def edit_page(request: Request, character_name: str): + char = load_character(character_name) + safe_name = character_name.lower().replace(" ", "_") + return templates.TemplateResponse(request, "create.html", { + "mode": "edit", + "character": char, + "character_filename": safe_name, + }) @app.get("/chat/{character_name}", response_class=HTMLResponse) async def chat_page(request: Request, character_name: str, session_id: str = Cookie(None, alias=SESSION_COOKIE_NAME)): @@ -463,6 +473,34 @@ async def api_create_character(request: Request): return JSONResponse({"status": "ok", "character": safe_name}) +@app.post("/api/character/update/{character_name}") +async def api_update_character(character_name: str, request: Request): + """Aktualisiert einen bestehenden Character.""" + path = CHARACTERS_DIR / f"{character_name}.json" + if not path.exists(): + raise HTTPException(status_code=404, detail=f"Character {character_name} not found") + + body = await request.json() + + # Load existing, update fields + with open(path, "r", encoding="utf-8") as f: + char = json.load(f) + + # Update all fields + for key in ["name", "avatar", "description", "personality", "background", + "behavior", "tone", "scenario", "nsfw", "greeting", "model", "temperature"]: + if key in body: + char[key] = body[key] + + # Update appearance + if "appearance" in body: + char["appearance"] = body["appearance"] + + with open(path, "w", encoding="utf-8") as f: + json.dump(char, f, indent=2, ensure_ascii=False) + + return JSONResponse({"status": "ok", "character": character_name}) + @app.get("/api/character/{name}") async def api_get_character(name: str): char = load_character(name) diff --git a/static/avatars/mara.png b/static/avatars/mara.png index 454810a..d6593f8 100644 Binary files a/static/avatars/mara.png and b/static/avatars/mara.png differ diff --git a/templates/create.html b/templates/create.html index a7c6108..cc1aab4 100644 --- a/templates/create.html +++ b/templates/create.html @@ -3,7 +3,7 @@ - NeonChat — Neuer Charakter + NeonChat — {{ "Bearbeiten" if mode == "edit" else "Neuer Charakter" }}
-

✦ Neuer Charakter

+

✦ {{ "Charakter bearbeiten" if mode == "edit" else "Neuer Charakter" }}

Definiere Persönlichkeit, Verhalten, Hintergrund und Aussehen.

+ {% if mode == "edit" and character %} +
+ + +
+ {{ character.name }}
+ Aktuelles Portrait. Beim Speichern und neu Generieren bleibt das Aussehen konsistent. +
+
+ {% endif %} +
Basis
- +
- +
- +
Persönlichkeit
- - + +
- - + +
- - + +
- - + +
- - + +
- +
- - + +
Aussehen (für Portrait-Generierung)
@@ -86,57 +101,57 @@
- +
- +
- +
- +
- +
- +
- +
- +
- +
Modell
@@ -149,25 +164,32 @@
- - + +
- +
diff --git a/templates/index.html b/templates/index.html index a0f95d4..cf4ab3b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -56,6 +56,7 @@

{{ c.name }}

{{ c.description[:80] }}{% if c.description|length > 80 %}...{% endif %}

+ ✏️ {% endfor %}