v2.4 — Character Editor + Image-to-Image Portrait Consistency
Features:
- /edit/{name} route: Character nachträglich bearbeiten
- /api/character/update/{name}: API zum Aktualisieren
- create.html dient als Editor und Creator (mode=edit/create)
- Index-Seite: ✏️ Button pro Character
- Portrait-Vorschau im Editor
- Image-to-Image: Folge-Portraits nutzen bestehendes Bild als Referenz
- LoadImage → ImageScale → VAEEncode → KSampler (denoise=0.45)
- Aussehen bleibt konsistent über mehrere Generierungen
- Fallback auf Text-to-Image wenn kein Referenzbild existiert
- ComfyUI Image Upload via /upload/image API
- Jinja2 if-Expressions für vorausgefüllte Formularfelder
This commit is contained in:
+91
-3
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user