From 045b9b64588ada9ca933e8233d828e8ca2a3aaa8 Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 18 Jun 2026 23:26:30 +0200 Subject: [PATCH] aa --- .junie/models/local-devstral24b.json | 14 + .junie/models/local-ollama.json | 8 +- .junie/models/local-qwen25.json | 11 +- Modelfile.qwen25-coder-32b-64k | 5 - optimize_clips.py | 152 +++++ position.md | 59 ++ requirements.txt | 3 +- tour-comfy/config.json | 16 +- tour-comfy/database.py | 101 +++ tour-comfy/edit_api.py | 589 ++++++++++++++++- tour-comfy/embeddings.py | 30 + tour-comfy/env.sh | 9 +- tour-comfy/groups.json | 99 +++ tour-comfy/names.json | 10 + tour-comfy/naming.py | 96 +++ tour-comfy/output/car.html | 931 --------------------------- tour-comfy/poses.md | 65 ++ tour-comfy/processed.json | 9 +- tour-comfy/start_watcher.sh | 12 +- tour-comfy/watcher.log | 212 ++++++ tour-comfy/watcher.py | 25 +- 21 files changed, 1477 insertions(+), 979 deletions(-) create mode 100644 .junie/models/local-devstral24b.json delete mode 100644 Modelfile.qwen25-coder-32b-64k create mode 100644 optimize_clips.py create mode 100644 position.md create mode 100644 tour-comfy/database.py create mode 100644 tour-comfy/embeddings.py create mode 100644 tour-comfy/groups.json create mode 100644 tour-comfy/names.json create mode 100644 tour-comfy/naming.py delete mode 100644 tour-comfy/output/car.html create mode 100644 tour-comfy/poses.md diff --git a/.junie/models/local-devstral24b.json b/.junie/models/local-devstral24b.json new file mode 100644 index 0000000..95e549c --- /dev/null +++ b/.junie/models/local-devstral24b.json @@ -0,0 +1,14 @@ +{ + "id": "devstral24b-junie:latest", + "baseUrl": "http://localhost:11434/v1/chat/completions", + "apiType": "OpenAICompletion", + "temperature": 0.05, + "primaryModel": { + "id": "devstral24b-junie:latest", + "temperature": 0.05 + }, + "fasterModel": { + "id": "devstral24b-junie:latest", + "temperature": 0.05 + } +} diff --git a/.junie/models/local-ollama.json b/.junie/models/local-ollama.json index cbf518a..eb42966 100644 --- a/.junie/models/local-ollama.json +++ b/.junie/models/local-ollama.json @@ -1,11 +1,11 @@ { "baseUrl": "http://localhost:11434/v1/responses", - "id": "qwen3-coder:30b", + "id": "qwen2.5-coder:32b", "apiType": "OpenAIResponses", - "temperature": 0.3, + "temperature": 0.1, "primaryModel": { - "id": "qwen3-coder:30b", - "temperature": 0.3 + "id": "qwen2.5-coder:32b", + "temperature": 0.1 }, "fasterModel": { "id": "qwen2.5-coder:1.5b" diff --git a/.junie/models/local-qwen25.json b/.junie/models/local-qwen25.json index 304bf22..80d70dc 100644 --- a/.junie/models/local-qwen25.json +++ b/.junie/models/local-qwen25.json @@ -1,14 +1,13 @@ { - "baseUrl": "http://localhost:11434/v1/responses", - "id": "qwen25-coder-32b-64k:latest", + "baseUrl": "http://localhost:11440/v1/responses", + "id": "qwen3-coder:30b", "apiType": "OpenAIResponses", "temperature": 0.3, "primaryModel": { - "id": "qwen25-coder-32b-64k:latest", - "temperature": 0.2 + "id": "qwen3-coder:30b", + "temperature": 0.3 }, "fasterModel": { - "id": "qwen25-coder-32b-64k:latest", - "temperature": 0.2 + "id": "qwen2.5-coder:1.5b" } } diff --git a/Modelfile.qwen25-coder-32b-64k b/Modelfile.qwen25-coder-32b-64k deleted file mode 100644 index f63e476..0000000 --- a/Modelfile.qwen25-coder-32b-64k +++ /dev/null @@ -1,5 +0,0 @@ -FROM qwen2.5-coder:32b - -PARAMETER num_ctx 64000 -PARAMETER temperature 0.15 -PARAMETER top_p 0.9 diff --git a/optimize_clips.py b/optimize_clips.py new file mode 100644 index 0000000..e3a912a --- /dev/null +++ b/optimize_clips.py @@ -0,0 +1,152 @@ +import os +import glob +import shutil +import subprocess +import tempfile +import threading +import time +CLIPS_DIR = "/data/events/clips" +ARCHIVE_DIR = "/mnt/tour-big/clips" +SIZE_THRESHOLD_MB = 200 +SIZE_THRESHOLD_BYTES = SIZE_THRESHOLD_MB * 1024 * 1024 +MAX_PER_RUN = 20 + + +def has_nvenc(): + r = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + return b"hevc_nvenc" in r.stdout + + +def _ffmpeg(cmd): + return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + + +def _proxy_cmd(src, dst, hw): + """10fps H.265 proxy for quick local browsing.""" + if hw: + return ["ffmpeg", "-y", + "-hwaccel", "cuda", "-hwaccel_output_format", "cuda", + "-i", src, + "-vcodec", "hevc_nvenc", "-rc", "constqp", "-qp", "28", "-preset", "p4", + "-r", "10", "-tag:v", "hvc1", "-acodec", "copy", dst] + return ["ffmpeg", "-y", "-i", src, + "-vcodec", "libx265", "-crf", "28", "-preset", "medium", + "-r", "10", "-tag:v", "hvc1", "-acodec", "copy", dst] + + +def _archive_cmd(src, dst, hw): + """Full-fps high-quality H.265 for archive — small enough to pull back fast over slow SMB.""" + if hw: + return ["ffmpeg", "-y", + "-hwaccel", "cuda", "-hwaccel_output_format", "cuda", + "-i", src, + "-vcodec", "hevc_nvenc", "-rc", "vbr", "-cq", "19", "-b:v", "0", + "-preset", "p6", "-tune", "hq", + "-tag:v", "hvc1", "-acodec", "copy", dst] + return ["ffmpeg", "-y", "-i", src, + "-vcodec", "libx265", "-crf", "19", "-preset", "slow", + "-tag:v", "hvc1", "-acodec", "copy", dst] + + +def _encode_worker(label, cmd, hw, fallback_cmd, out): + proc = _ffmpeg(cmd) + if proc.returncode != 0 and hw: + print(f" [{label}] NVENC failed, retrying in software...") + proc = _ffmpeg(fallback_cmd) + out[label] = proc + + +def process(src, orig_size, hw): + name = os.path.basename(src) + archive_dst = os.path.join(ARCHIVE_DIR, name) + + if os.path.exists(archive_dst): + print(f" Skip {name} — already archived") + return 0 + + proxy_tmp = src + ".proxy.mp4" + # Encode archive to local /tmp first — never write directly to a slow SMB share + archive_tmp = os.path.join(tempfile.gettempdir(), name + ".archive.mp4") + + print(f"[{name}] {orig_size/(1024*1024):.0f} MB ({'NVENC' if hw else 'software'})") + t0 = time.time() + + results = {} + threads = [ + threading.Thread(target=_encode_worker, args=( + "proxy", + _proxy_cmd(src, proxy_tmp, hw), hw, + _proxy_cmd(src, proxy_tmp, False), + results, + )), + threading.Thread(target=_encode_worker, args=( + "archive", + _archive_cmd(src, archive_tmp, hw), hw, + _archive_cmd(src, archive_tmp, False), + results, + )), + ] + for t in threads: t.start() + for t in threads: t.join() + + failed = {k: v for k, v in results.items() if v.returncode != 0} + if failed: + for label, proc in failed.items(): + print(f" [{label}] error: {proc.stderr.decode()[-300:]}") + for f in (proxy_tmp, archive_tmp): + if os.path.exists(f): os.remove(f) + return 0 + + proxy_mb = os.path.getsize(proxy_tmp) / (1024*1024) + archive_mb = os.path.getsize(archive_tmp) / (1024*1024) + orig_mb = orig_size / (1024*1024) + print(f" proxy {orig_mb:.0f}->{proxy_mb:.0f} MB " + f"archive {orig_mb:.0f}->{archive_mb:.0f} MB " + f"({time.time()-t0:.1f}s) — pushing archive to share...") + + try: + shutil.move(archive_tmp, archive_dst) + except Exception as e: + print(f" Archive move failed: {e} — aborting, original intact") + for f in (proxy_tmp, archive_tmp): + if os.path.exists(f): os.remove(f) + return 0 + + # Atomic swap: original -> proxy (same filesystem) + os.replace(proxy_tmp, src) + freed = orig_size - os.path.getsize(src) + print(f" Done freed locally: {freed/(1024*1024):.0f} MB") + return freed + + +if __name__ == "__main__": + hw = has_nvenc() + print(f"Encoder: {'hevc_nvenc (A6000, 2x parallel)' if hw else 'libx265 (software)'}") + print(f"Clips: {CLIPS_DIR}") + print(f"Archive: {ARCHIVE_DIR}\n") + + if not os.path.isdir(ARCHIVE_DIR): + print(f"ERROR: archive not reachable: {ARCHIVE_DIR}") + raise SystemExit(1) + + files = sorted( + [(fp, os.path.getsize(fp)) + for fp in glob.glob(os.path.join(CLIPS_DIR, "*.mp4")) + if os.path.isfile(fp) and os.path.getsize(fp) > SIZE_THRESHOLD_BYTES], + key=lambda x: x[1], reverse=True, + ) + print(f"Found {len(files)} files > {SIZE_THRESHOLD_MB} MB\n") + + total_freed = 0 + done = 0 + for path, size in files: + freed = process(path, size, hw) + total_freed += freed + if freed: + done += 1 + if done >= MAX_PER_RUN: + print(f"Reached {MAX_PER_RUN}-file limit.") + break + + print(f"\nDone. Processed: {done} Freed locally: {total_freed/(1024*1024):.0f} MB") diff --git a/position.md b/position.md new file mode 100644 index 0000000..b0d114d --- /dev/null +++ b/position.md @@ -0,0 +1,59 @@ +token: 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 +how is a photo called where the person faces front, straight lookign into the camera +Een paar seconden nagedacht + +That’s usually called a front-facing portrait or frontal portrait. + +More specific terms: + +Head-on portrait: person faces directly toward the camera. +Straight-on portrait: neutral, camera directly in front. +Mugshot-style photo: front-facing, often neutral expression, but has a police/ID connotation. +Passport photo or ID photo: front-facing with a plain background and strict framing. + +For normal use, I’d say: a straight-on portrait. + +and with full top till down +Een seconde nagedacht + +That’s called a front-facing full-body portrait. + +Also common: + +Full-length portrait: shows the person from head to toe. +Straight-on full-body shot: person faces the camera directly. +Head-to-toe portrait: informal but clear. + +Best phrase: a straight-on full-body portrait. + +and when isotopic ( i think is the word) facing? + +I think you mean either isometric or three-quarter view. I’ll pin down the naming so you can use the right prompt/photo term. + +Nagedacht gedurende 4s + +You probably mean isometric, but for a person/photo that is usually not the right word. + +Use these terms: + +Three-quarter view +Person is turned partly sideways, often about 45°, but still visible from the front. This is the normal portrait term. + +Profile view +Person is fully sideways, seen from the side. + +Frontal view / straight-on view +Person faces camera directly. + +Isometric view +Mostly used for objects, architecture, games, diagrams: a 3D-looking view with equal axes/angles, not a normal human portrait pose. + +For your case, say: + +a full-body three-quarter portrait +or +a straight-on full-body portrait in three-quarter view if the body is angled but the person still looks at the camera. + +Head-on a full-body three-quarter full-nude-body portrait transparent background +Head-on straight-on full-nude-body portrait transparent background +Head-on straight-on full-body portrait no background \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 67d0fe3..4ce3f21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ sentencepiece dashscope torchvision peft -torchao==0.11.0 \ No newline at end of file +torchao==0.11.0 +rembg[cpu] \ No newline at end of file diff --git a/tour-comfy/config.json b/tour-comfy/config.json index a8553c5..3def106 100644 --- a/tour-comfy/config.json +++ b/tour-comfy/config.json @@ -1,15 +1,23 @@ { "api_url": "http://127.0.0.1:8500/edit", - "prompt": "high quality, detailed, female nude", + "prompt": "high quality. detailed, female nude, high quality. realistic. detailed. female nude", + "base_prompts": [ + "Head-on a full-body three-quarter full-nude-body female portrait, realistic, transparent background", + "Head-on straight-on full-nude-body female portrait, realistic, transparent background", + "Head-on straight-on full-body female portrait, realistic, no background", + "high quality, full-nude-body, female, masterpiece, realistic, photo, looking at viewer", + "high quality, full-nude-body, female, masterpiece, realistic, photo, detailed skin, professional lighting, transparent background", + "high quality, full-nude-body, female, masterpiece, realistic, photo, cinematic lighting, dramatic shadows, sharp focus, transparent background" + ], "seed": -1, "max_area": 655360, "margin": 10, "top_margin": 20, "headroom": 0.05, "poll_interval": 2, - "stage_dir": "./tour-comfy/stage", - "output_dir": "./tour-comfy/output", - "failed_dir": "./tour-comfy/failed", + "stage_dir": "/mnt/zim/tour-comfy/stage", + "output_dir": "/mnt/zim/tour-comfy/output", + "failed_dir": "/mnt/zim/tour-comfy/failed", "processed_file": "./tour-comfy/processed.json", "log_file": "./tour-comfy/watcher.log" } diff --git a/tour-comfy/database.py b/tour-comfy/database.py new file mode 100644 index 0000000..4911af5 --- /dev/null +++ b/tour-comfy/database.py @@ -0,0 +1,101 @@ +import psycopg2 +import json + +DB_CONFIG = { + "host": "192.168.1.160", + "port": 5433, + "dbname": "dv", + "user": "dev", + "password": "dev" +} + +def get_db_connection(): + return psycopg2.connect(**DB_CONFIG) + +def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None, embedding=None, clip_description=None): + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute(""" + INSERT INTO person (filename, filepath, name, group_id, tags, embedding, clip_description) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (filename) DO UPDATE + SET filepath = COALESCE(EXCLUDED.filepath, person.filepath), + name = COALESCE(EXCLUDED.name, person.name), + group_id = COALESCE(EXCLUDED.group_id, person.group_id), + tags = COALESCE(EXCLUDED.tags, person.tags), + embedding = COALESCE(EXCLUDED.embedding, person.embedding), + clip_description = COALESCE(EXCLUDED.clip_description, person.clip_description); + """, (filename, filepath, name, group_id, json.dumps(tags) if tags else None, embedding, clip_description)) + conn.commit() + finally: + cur.close() + conn.close() + +def get_person(filename): + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute("SELECT name, group_id, tags, embedding, clip_description, filepath FROM person WHERE filename = %s", (filename,)) + return cur.fetchone() + finally: + cur.close() + conn.close() + +def list_persons(): + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute("SELECT filename, name, group_id, clip_description FROM person") + return cur.fetchall() + finally: + cur.close() + conn.close() + +def search_similar(embedding, limit=10): + conn = get_db_connection() + cur = conn.cursor() + try: + # Convert embedding to string format for pgvector + embedding_str = "[" + ",".join(map(str, embedding)) + "]" + cur.execute(""" + SELECT filename, name, group_id, clip_description, embedding <=> %s AS distance + FROM person + WHERE embedding IS NOT NULL + ORDER BY distance ASC + LIMIT %s; + """, (embedding_str, limit)) + return cur.fetchall() + finally: + cur.close() + conn.close() + +def delete_person(filename): + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute("DELETE FROM person WHERE filename = %s", (filename,)) + conn.commit() + finally: + cur.close() + conn.close() + +def delete_group(group_id): + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute("DELETE FROM person WHERE group_id = %s", (group_id,)) + conn.commit() + finally: + cur.close() + conn.close() + +def get_group_files(group_id): + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute("SELECT filename, filepath FROM person WHERE group_id = %s", (group_id,)) + return cur.fetchall() + finally: + cur.close() + conn.close() diff --git a/tour-comfy/edit_api.py b/tour-comfy/edit_api.py index 041ed56..0836aab 100644 --- a/tour-comfy/edit_api.py +++ b/tour-comfy/edit_api.py @@ -16,16 +16,31 @@ import uuid import random import copy import threading +import csv + +try: + from . import database + from . import embeddings + from . import naming +except ImportError: + import database + import embeddings + import naming import requests from PIL import Image -from fastapi import FastAPI, UploadFile, File, Form, HTTPException +from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import Response from pydantic import BaseModel +import shutil +import re # --- config ----------------------------------------------------------------- -CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") +CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") +NAMES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "names.json") +GROUPS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "groups.json") +WD_MODEL = os.environ.get("WD_MODEL", "SmilingWolf/wd-vit-tagger-v3") COMFY = os.environ.get("COMFY_URL", "http://127.0.0.1:8188").rstrip("/") WORKFLOW_PATH = os.environ.get( "WORKFLOW_PATH", @@ -39,6 +54,7 @@ GEN_TIMEOUT = int(os.environ.get("GEN_TIMEOUT", "600")) # seconds per request # Node ids in workflow_qwen_edit.json (kept stable on purpose). NODE_LOADIMAGE = "4" NODE_POSITIVE = "5" +NODE_NEGATIVE = "6" NODE_LATENT = "7" NODE_KSAMPLER = "8" NODE_SAVE = "10" @@ -52,7 +68,7 @@ app = FastAPI(title="Qwen-Image-Edit Rapid-AIO API", version="1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], - allow_methods=["GET", "POST"], + allow_methods=["GET", "POST", "DELETE"], allow_headers=["*"], ) @@ -167,8 +183,122 @@ def _comfy_fetch_image(outputs: dict) -> bytes: return r.content +# --- WD tagger (lazy) ------------------------------------------------------- + +_tagger = None # (model, transform, labels) once loaded +_tagger_lock = threading.Lock() + + +def _load_tagger(): + global _tagger + if _tagger is not None: + return _tagger + with _tagger_lock: + if _tagger is not None: + return _tagger + import torch + import timm + from timm.data import create_transform, resolve_data_config + import huggingface_hub + + model = timm.create_model(f"hf_hub:{WD_MODEL}", pretrained=True).eval() + if torch.cuda.is_available(): + model = model.cuda() + + cfg = resolve_data_config(model.pretrained_cfg, model=model) + transform = create_transform(**cfg) + + lpath = huggingface_hub.hf_hub_download(WD_MODEL, "selected_tags.csv") + with open(lpath, newline="") as f: + rows = list(csv.DictReader(f)) + # category 0=general 4=character 9=rating + labels = [(r["name"], int(r.get("category", 9))) for r in rows] + + _tagger = (model, transform, labels) + return _tagger + + +def _run_tagger(pil_img: Image.Image, threshold: float = 0.35): + import torch + model, transform, labels = _load_tagger() + tensor = transform(pil_img.convert("RGB")).unsqueeze(0) + if torch.cuda.is_available(): + tensor = tensor.cuda() + with torch.no_grad(): + scores = torch.sigmoid(model(tensor))[0].cpu().tolist() + tags = [ + {"tag": name, "score": round(score, 3), "cat": cat} + for (name, cat), score in zip(labels, scores) + if score >= threshold + ] + tags.sort(key=lambda x: -x["score"]) + return tags + + +def _tags_to_name(tags: list, max_tags: int = 8) -> str: + content = [t["tag"] for t in tags if t["cat"] in (0, 4)][:max_tags] + return " ".join(content).replace("_", " ") + + +def _load_json(path: str) -> dict: + if os.path.exists(path): + with open(path) as f: + return json.load(f) + return {} + + +def _save_json(path: str, data: dict): + with open(path, "w") as f: + json.dump(data, f, indent=2) + + +def _apply_transparency(png_bytes: bytes) -> bytes: + """Use rembg to remove background and return PNG bytes with Alpha channel.""" + try: + from rembg import remove + import io + from PIL import Image + img = Image.open(io.BytesIO(png_bytes)) + # rembg works best on RGB + if img.mode != "RGB": + img = img.convert("RGB") + out = remove(img) + buf = io.BytesIO() + out.save(buf, format="PNG") + return buf.getvalue() + except Exception as e: + print(f"Error in transparency post-processing: {e}") + return png_bytes + + # --- pipeline helper --------------------------------------------------------- +def _load_poses(): + poses_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "poses.md") + if not os.path.exists(poses_path): + return {} + + poses = {} + current_pose = None + current_desc = [] + + with open(poses_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if line.startswith("# "): + if current_pose: + poses[current_pose] = " ".join(current_desc).strip() + current_pose = line[2:].rstrip(":").strip() + current_desc = [] + elif line and current_pose: + current_desc.append(line) + + if current_pose: + poses[current_pose] = " ".join(current_desc).strip() + + return poses + + def _run_pipeline( pil: Image.Image, prompt: str, @@ -189,6 +319,12 @@ def _run_pipeline( graph = copy.deepcopy(BASE_WORKFLOW) graph[NODE_LOADIMAGE]["inputs"]["image"] = stored graph[NODE_POSITIVE]["inputs"]["prompt"] = prompt + + # Transparency detection + is_transparent = any(kw in prompt.lower() for kw in ["transparent", "no background", "remove background", "alpha channel"]) + if is_transparent: + graph[NODE_NEGATIVE]["inputs"]["prompt"] = "checkerboard, grid, pattern, texture, background details" + graph[NODE_LATENT]["inputs"]["width"] = w graph[NODE_LATENT]["inputs"]["height"] = h ks = graph[NODE_KSAMPLER]["inputs"] @@ -196,7 +332,12 @@ def _run_pipeline( client_id = uuid.uuid4().hex prompt_id = _comfy_queue(graph, client_id) outputs = _comfy_wait(prompt_id, time.time() + GEN_TIMEOUT) - return _comfy_fetch_image(outputs) + png_bytes = _comfy_fetch_image(outputs) + + if is_transparent: + png_bytes = _apply_transparency(png_bytes) + + return png_bytes # --- batch state ------------------------------------------------------------- @@ -213,20 +354,76 @@ def _load_output_dir() -> str: return d -def _batch_worker(job_id: str, filenames: list, prompt: str, seed: int, max_area: int): +def _move_to_trash(filepath: str): + if not filepath or not os.path.exists(filepath): + return + output_dir = _load_output_dir() + trash_dir = os.path.join(output_dir, ".trash") + os.makedirs(trash_dir, exist_ok=True) + + filename = os.path.basename(filepath) + ts = time.strftime("%Y%m%d_%H%M%S") + trash_path = os.path.join(trash_dir, f"{ts}_{filename}") + + try: + shutil.move(filepath, trash_path) + except Exception as e: + print(f"Error moving {filepath} to trash: {e}") + + +def _batch_worker(job_id: str, filenames: list, prompts: list[str], seed: int, max_area: int, group_id: str | None = None): output_dir = _load_output_dir() for fname in filenames: + # If no group_id provided, try to inherit from source + actual_gid = group_id + if not actual_gid: + try: + person = database.get_person(fname) + if person and person[1]: # group_id is at index 1 + actual_gid = person[1] + else: + # Create a new group for this standalone image + actual_gid = naming.get_base_name(fname) + # Update source image to join this group + database.upsert_person(fname, group_id=actual_gid) + except Exception as e: + print(f"Error determining group for {fname}: {e}") + fpath = os.path.join(output_dir, fname) + if not os.path.exists(fpath): + jobs[job_id]["failed"] += len(prompts) + continue + try: pil = Image.open(fpath).convert("RGB") - png = _run_pipeline(pil, prompt, seed, max_area) - ts = time.strftime("%Y%m%d_%H%M%S") - out_name = f"{ts}_{fname}" - with open(os.path.join(output_dir, out_name), "wb") as f: - f.write(png) - jobs[job_id]["done"] += 1 + for prompt in prompts: + try: + png = _run_pipeline(pil, prompt, seed, max_area) + ts = time.strftime("%Y%m%d_%H%M%S") + + # Clean filename to avoid nested timestamps + clean_fname = naming.get_base_name(fname) + out_name = f"{ts}_{clean_fname}" + out_path = os.path.join(output_dir, out_name) + + with open(out_path, "wb") as f: + f.write(png) + + # Register in DB + try: + embedding = embeddings.generate_embedding(out_path) + database.upsert_person(out_name, filepath=out_path, embedding=embedding, group_id=actual_gid) + except Exception as db_err: + print(f"Database error in batch worker: {db_err}") + + jobs[job_id]["done"] += 1 + except Exception as e: + print(f"Error in batch for {fname} with prompt '{prompt}': {e}") + jobs[job_id]["failed"] += 1 except Exception as e: - jobs[job_id]["failed"] += 1 + print(f"Error opening {fname}: {e}") + jobs[job_id]["failed"] += len(prompts) + jobs[job_id]["status"] = "done" @@ -258,22 +455,31 @@ def update_config(update: ConfigUpdate): class BatchRequest(BaseModel): filenames: list[str] - prompt: str + prompt: str | list[str] seed: int = -1 max_area: int = 0 + group_id: str | None = None @app.post("/batch") def start_batch(req: BatchRequest): + prompts = [req.prompt] if isinstance(req.prompt, str) else req.prompt + total_tasks = len(req.filenames) * len(prompts) + job_id = uuid.uuid4().hex[:8] - jobs[job_id] = {"status": "running", "total": len(req.filenames), "done": 0, "failed": 0} + jobs[job_id] = {"status": "running", "total": total_tasks, "done": 0, "failed": 0} t = threading.Thread( target=_batch_worker, - args=(job_id, req.filenames, req.prompt, req.seed, req.max_area), + args=(job_id, req.filenames, prompts, req.seed, req.max_area, req.group_id), daemon=True, ) t.start() - return {"job_id": job_id, "total": len(req.filenames)} + return {"job_id": job_id, "total": total_tasks} + + +@app.get("/poses") +def get_poses(): + return _load_poses() @app.get("/batch/{job_id}") @@ -283,6 +489,186 @@ def get_batch(job_id: str): return jobs[job_id] +@app.get("/images") +def list_images(): + output_dir = _load_output_dir() + extensions = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg') + try: + # Try to get from DB first + try: + persons = database.list_persons() + # persons is (filename, name, group_id, clip_description) + db_images = [] + for p in persons: + db_images.append({ + "filename": p[0], + "name": p[1], + "group_id": p[2], + "clip_description": p[3] + }) + # Still sort by mtime for consistency with filesystem + db_images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])) if os.path.exists(os.path.join(output_dir, x["filename"])) else 0, reverse=True) + return {"images": db_images} + except Exception as db_err: + print(f"DB error in list_images: {db_err}") + # Fallback to filesystem + files = [f for f in os.listdir(output_dir) if f.lower().endswith(extensions)] + files.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True) + return {"images": [{"filename": f} for f in files]} + except Exception as e: + raise HTTPException(500, str(e)) + + +# --- tagging routes ---------------------------------------------------------- + +class TagRequest(BaseModel): + filename: str + threshold: float = 0.35 + max_tags: int = 8 + group_id: str | None = None + + +@app.post("/tag") +def tag_image(req: TagRequest): + output_dir = _load_output_dir() + fpath = os.path.join(output_dir, req.filename) + if not os.path.exists(fpath): + raise HTTPException(404, "File not found in output dir") + try: + pil = Image.open(fpath) + tags = _run_tagger(pil, req.threshold) + clip_desc = _tags_to_name(tags, req.max_tags) + auto_name = naming.generate_associative_name(tags) + + # Save to DB + try: + embedding = embeddings.generate_embedding(fpath) + database.upsert_person(req.filename, filepath=fpath, name=auto_name, clip_description=clip_desc, tags=tags, embedding=embedding, group_id=req.group_id) + except Exception as db_err: + print(f"Database error during tag: {db_err}") + + # Legacy fallback + try: + names = _load_json(NAMES_PATH) + names[req.filename] = clip_desc + _save_json(NAMES_PATH, names) + except: pass + + return {"filename": req.filename, "clip_description": clip_desc, "tags": tags[:30]} + except Exception as e: + raise HTTPException(500, str(e)) + + +@app.get("/names") +def get_names(): + try: + persons = database.list_persons() + return {p[0]: p[1] for p in persons if p[1]} + except: + return _load_json(NAMES_PATH) + + +@app.post("/names/{filename}") +def set_name(filename: str, body: dict): + name = body.get("name", "") + try: + database.upsert_person(filename, name=name) + except Exception as db_err: + print(f"Database error in set_name: {db_err}") + + # Legacy fallback + try: + names = _load_json(NAMES_PATH) + names[filename] = name + _save_json(NAMES_PATH, names) + except: pass + + return {"filename": filename, "name": name} + + +# --- group routes ------------------------------------------------------------ + +@app.get("/groups") +def get_groups(): + try: + persons = database.list_persons() + return {p[0]: p[2] for p in persons if p[2]} + except: + return _load_json(GROUPS_PATH) + + +class MergeRequest(BaseModel): + filenames: list[str] + group_id: str | None = None + + +@app.post("/groups/merge") +def merge_groups(req: MergeRequest): + gid = req.group_id or f"cg_{uuid.uuid4().hex[:8]}" + for fname in req.filenames: + try: + database.upsert_person(fname, group_id=gid) + except Exception as db_err: + print(f"Database error in merge: {db_err}") + + # Legacy fallback + try: + groups = _load_json(GROUPS_PATH) + for fname in req.filenames: + groups[fname] = gid + _save_json(GROUPS_PATH, groups) + except: pass + + return {"group_id": gid, "files": req.filenames} + + +class ExtractRequest(BaseModel): + filename: str + + +@app.post("/groups/extract") +def extract_from_group(req: ExtractRequest): + gid = f"solo:{req.filename}" + try: + database.upsert_person(req.filename, group_id=gid) + except Exception as db_err: + print(f"Database error in extract: {db_err}") + + # Legacy fallback + try: + groups = _load_json(GROUPS_PATH) + groups[req.filename] = gid + _save_json(GROUPS_PATH, groups) + except: pass + + return {"filename": req.filename} + + +@app.get("/similar/{filename}") +def get_similar(filename: str, limit: int = 10): + person = database.get_person(filename) + if not person or person[3] is None: + raise HTTPException(404, "Image or embedding not found") + + embedding = person[3] + results = database.search_similar(embedding, limit=limit) + + similar = [] + for r in results: + # Avoid returning the same image as the most similar + if r[0] == filename: + continue + similar.append({ + "filename": r[0], + "name": r[1], + "group_id": r[2], + "clip_description": r[3], + "distance": float(r[4]) + }) + + return {"filename": filename, "similar": similar} + + @app.get("/health") def health(): try: @@ -292,6 +678,119 @@ def health(): raise HTTPException(503, f"ComfyUI unreachable at {COMFY}: {e}") +def _crop_to_bbox(pil_img: Image.Image, margin: int = 20, top_margin: int = 20, headroom: float = 0.05) -> Image.Image: + if pil_img.mode != 'RGBA': + return pil_img + + alpha = pil_img.split()[-1] + bbox = alpha.getbbox() + if not bbox: + return pil_img + + left, upper, right, lower = bbox + left = max(0, left - margin) + upper = max(0, upper - top_margin) + right = min(pil_img.width, right + margin) + lower = min(pil_img.height, lower + margin) + + cropped = pil_img.crop((left, upper, right, lower)) + if headroom > 0: + h_px = int(cropped.height * headroom) + if h_px > 0: + new_img = Image.new("RGBA", (cropped.width, cropped.height + h_px), (0, 0, 0, 0)) + new_img.paste(cropped, (0, h_px)) + return new_img + return cropped + + +def _process_upload(file_path: str, filename: str, prompts: list[str], name: str | None = None, group_id: str | None = None): + output_dir = _load_output_dir() + try: + pil = Image.open(file_path) + + # 1. CLIP tag the source + tags = _run_tagger(pil.convert("RGB")) + clip_desc = _tags_to_name(tags) + auto_name = name or naming.generate_associative_name(tags) + + # 2. Embedding for source + embedding = embeddings.generate_embedding(file_path) + + # 3. Register source in DB (optional, but good for tracking) + # We'll use the original filename or a timestamped one + database.upsert_person(filename, filepath=file_path, name=auto_name, clip_description=clip_desc, tags=tags, embedding=embedding, group_id=group_id) + + # 4. Default behavior: Crop if needed + # We'll use default values from watcher + cropped_pil = _crop_to_bbox(pil) + + # 5. Run prompts + for i, prompt in enumerate(prompts): + try: + # Use RGB for pipeline + png = _run_pipeline(cropped_pil.convert("RGB"), prompt) + + ts = time.strftime("%Y%m%d_%H%M%S") + out_name = f"{ts}_{i}_{filename}" + if not out_name.lower().endswith(".png"): + out_name += ".png" + out_path = os.path.join(output_dir, out_name) + + with open(out_path, "wb") as f: + f.write(png) + + # Register output in DB + out_embedding = embeddings.generate_embedding(out_path) + database.upsert_person(out_name, filepath=out_path, name=auto_name, clip_description=clip_desc, embedding=out_embedding, group_id=group_id) + except Exception as e: + print(f"Error processing prompt '{prompt}' for {filename}: {e}") + + except Exception as e: + print(f"Error in _process_upload for {filename}: {e}") + + +@app.post("/upload") +def upload_image( + background_tasks: BackgroundTasks, + image: UploadFile = File(...), + prompts: str = Form(""), + name: str = Form(None), +): + # Load config to get output_dir (we use output_dir for UI uploads to avoid watcher conflict) + with open(CONFIG_PATH, "r") as f: + conf = json.load(f) + output_dir = _load_output_dir() + os.makedirs(output_dir, exist_ok=True) + + ts = time.strftime("%Y%m%d_%H%M%S") + safe_filename = re.sub(r'[^a-zA-Z0-9_.-]', '_', image.filename) + # Ensure extension + if not safe_filename.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')): + safe_filename += ".png" + + filename = f"{ts}_{safe_filename}" + file_path = os.path.join(output_dir, filename) + + with open(file_path, "wb") as f: + shutil.copyfileobj(image.file, f) + + prompt_list = [p.strip() for p in prompts.split(",") if p.strip()] + + # Add base-set prompts if defined in config + base_prompts = conf.get("base_prompts", []) + if isinstance(base_prompts, list): + prompt_list.extend(base_prompts) + + if not prompt_list: + # Use default prompt from config + prompt_list = [conf.get("prompt", "high quality, masterpiece")] + + group_id = naming.get_base_name(filename) + background_tasks.add_task(_process_upload, file_path, filename, prompt_list, name, group_id) + + return {"status": "processing", "filename": filename, "group_id": group_id, "prompts": prompt_list} + + @app.post("/edit") async def edit( image: UploadFile = File(...), @@ -313,6 +812,64 @@ async def edit( return Response(content=png, media_type="image/png") +@app.delete("/images/{filename}") +def delete_image(filename: str): + person = database.get_person(filename) + if person and person[5] and os.path.exists(person[5]): + _move_to_trash(person[5]) + + database.delete_person(filename) + return {"status": "deleted", "filename": filename} + + +@app.delete("/groups/{group_id}") +def delete_group(group_id: str): + files = database.get_group_files(group_id) + for filename, filepath in files: + if filepath and os.path.exists(filepath): + _move_to_trash(filepath) + + database.delete_group(group_id) + return {"status": "deleted", "group_id": group_id} + + +@app.post("/remove-background/{filename}") +def remove_background(filename: str): + person = database.get_person(filename) + if not person or not person[5] or not os.path.exists(person[5]): + raise HTTPException(404, "Image file not found") + + path = person[5] + with open(path, "rb") as f: + png_bytes = f.read() + + transparent_png = _apply_transparency(png_bytes) + + with open(path, "wb") as f: + f.write(transparent_png) + + return {"status": "success", "filename": filename} + + +@app.post("/remove-background/group/{group_id}") +def remove_background_group(group_id: str, background_tasks: BackgroundTasks): + def _bg_task(): + files = database.get_group_files(group_id) + for filename, filepath in files: + if filepath and os.path.exists(filepath): + try: + with open(filepath, "rb") as f: + png_bytes = f.read() + transparent_png = _apply_transparency(png_bytes) + with open(filepath, "wb") as f: + f.write(transparent_png) + except Exception as e: + print(f"Error removing background for {filename}: {e}") + + background_tasks.add_task(_bg_task) + return {"status": "processing", "group_id": group_id} + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host=os.environ.get("HOST", "0.0.0.0"), diff --git a/tour-comfy/embeddings.py b/tour-comfy/embeddings.py new file mode 100644 index 0000000..e6551c3 --- /dev/null +++ b/tour-comfy/embeddings.py @@ -0,0 +1,30 @@ +import torch +import open_clip +from PIL import Image +import os + +_model = None +_preprocess = None +_device = None + +def get_model(): + global _model, _preprocess, _device + if _model is None: + _device = "cuda" if torch.cuda.is_available() else "cpu" + # ViT-H-14 is 1024-dim + _model, _, _preprocess = open_clip.create_model_and_transforms('ViT-H-14', pretrained='laion2b_s32b_b79k') + _model = _model.to(_device) + _model.eval() + return _model, _preprocess, _device + +def generate_embedding(image_path): + model, preprocess, device = get_model() + try: + image = preprocess(Image.open(image_path)).unsqueeze(0).to(device) + with torch.no_grad(): + image_features = model.encode_image(image) + image_features /= image_features.norm(dim=-1, keepdim=True) + return image_features.cpu().numpy()[0].tolist() + except Exception as e: + print(f"Error generating embedding for {image_path}: {e}") + return None diff --git a/tour-comfy/env.sh b/tour-comfy/env.sh index c0fdd8a..02c3265 100644 --- a/tour-comfy/env.sh +++ b/tour-comfy/env.sh @@ -19,7 +19,12 @@ COMFY="$BASE/ComfyUI" _basefs="$(stat -f -c %T "$BASE" 2>/dev/null || echo unknown)" case "$_basefs" in fuseblk|ntfs|ntfs3|exfat|vfat|msdos|9p|cifs|smb*) - VENV="${COMFY_VENV:-$HOME/comfyui-venv}" ;; # NTFS-ish BASE -> venv on home + VENV="${COMFY_VENV:-/home/mike/comfyui/venv}" ;; # NTFS-ish BASE -> venv on home *) - VENV="${COMFY_VENV:-$BASE/venv}" ;; # native fs -> venv beside code + if [ -d "/home/mike/comfyui/venv" ]; then + VENV="${COMFY_VENV:-/home/mike/comfyui/venv}" + else + VENV="${COMFY_VENV:-$BASE/venv}" + fi + ;; esac diff --git a/tour-comfy/groups.json b/tour-comfy/groups.json new file mode 100644 index 0000000..c775121 --- /dev/null +++ b/tour-comfy/groups.json @@ -0,0 +1,99 @@ +{ + "20260617_005040_img_56.png": "cg_077c3625", + "20260617_005026_img_55.png": "cg_077c3625", + "20260617_014351_img_66.png": "cg_9be4f76c", + "20260617_013150_img_66.png": "cg_9be4f76c", + "20260617_013327_img_67.png": "cg_9be4f76c", + "20260617_013211_img_65.png": "cg_9be4f76c", + "20260617_013035_img_64.png": "cg_9be4f76c", + "20260617_013111_img_63.png": "cg_9be4f76c", + "20260616_005752_img_21.png": "cg_07d742c0", + "20260616_005727_img_19.png": "cg_07d742c0", + "20260615_151614_img_93.png": "cg_74544975", + "20260615_145017_img_93.png": "cg_74544975", + "20260615_151829_img_92.png": "cg_74544975", + "img_9.png": "cg_74544975", + "20260617_133832_img_81.png": "cg_85873ed2", + "20260617_133917_img_82.png": "cg_85873ed2", + "20260617_134119_img_85.png": "cg_85873ed2", + "20260617_134229_img_83.png": "cg_85873ed2", + "20260618_004930_20260617_134041_img_84.png": "cg_85873ed2", + "20260618_004501_20260617_134041_img_84.png": "cg_85873ed2", + "20260617_134041_img_84.png": "cg_85873ed2", + "20260618_011507_20260617_134615_img_86.png": "cg_85873ed2", + "20260617_134615_img_86.png": "cg_85873ed2", + "20260618_011633_t159zr-1.png": "cg_85873ed2", + "t159zr-1.png": "cg_85873ed2", + "20260618_004919_kbk99v.png": "cg_4004e314", + "kbk99v.png": "cg_4004e314", + "20260618_004941_out7.png": "cg_4004e314", + "out7.png": "cg_4004e314", + "20260618_004334_Pasted image (3).png": "cg_67da7537", + "Pasted image (3).png": "cg_67da7537", + "20260618_002025_20260616_020020_img_35.png": "cg_67da7537", + "20260616_020020_img_35.png": "cg_67da7537", + "20260618_004428_20260616_015949_img_37.png": "cg_67da7537", + "20260618_002036_20260616_015949_img_37.png": "cg_67da7537", + "20260616_015949_img_37.png": "cg_67da7537", + "20260616_020059_img_38.png": "cg_67da7537", + "20260616_015919_img_33.png": "cg_4ae30667", + "20260616_015850_img_34.png": "cg_4ae30667", + "20260616_011823_imgxxxx.png": "cg_800abf94", + "20260615_152252_imgxxx.png": "cg_800abf94", + "tp236b.png": "cg_f55e9e4a", + "out.png": "cg_f55e9e4a", + "out2.png": "cg_f55e9e4a", + "p13.png": "cg_4e575e1d", + "pa0.png": "cg_4e575e1d", + "Pasted image (5).png": "cg_85873ed2", + "img_3.png": "cg_53eda359", + "Pasted image.png": "cg_53eda359", + "out3.png": "cg_53eda359", + "20260615_155354_others.jpeg": "cg_1c0c5074", + "20260615_154852_other.jpeg": "cg_1c0c5074", + "20260615_154333_other.jpeg": "cg_1c0c5074", + "20260618_004407_20260616_002456_test123.jpeg": "cg_1c0c5074", + "20260616_002456_test123.jpeg": "cg_1c0c5074", + "20260618_013512_Pasted image (9).png": "cg_809653a0", + "Pasted image (9).png": "cg_809653a0", + "20260615_155756_img_6v1.png": "cg_2b3ab0b0", + "20260616_002302_image.png": "cg_2b3ab0b0", + "20260618_011622_jb1.png": "cg_ee004a75", + "jb1.png": "cg_ee004a75", + "20260618_010649_20260615_150340_test.png": "cg_ee004a75", + "20260615_150340_test.png": "cg_ee004a75", + "20260618_045745_7_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045734_6_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045723_5_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045656_20260618_045450_test_clipboard.png": "cg_32d91763", + "20260618_045629_20260618_045234_test_clipboard.png": "cg_32d91763", + "20260618_045234_test_clipboard.png": "cg_32d91763", + "20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045450_test_clipboard.png": "cg_32d91763", + "20260618_045703_4_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045652_3_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045631_2_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045620_1_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045608_0_20260618_045549_test_clipboard.png": "cg_32d91763", + "20260618_045539_3_20260618_045450_test_clipboard.png": "cg_32d91763", + "20260618_045528_2_20260618_045450_test_clipboard.png": "cg_32d91763", + "20260618_045500_0_20260618_045450_test_clipboard.png": "cg_32d91763", + "20260618_045450_4_20260618_045234_test_clipboard.png": "cg_32d91763", + "20260618_045439_3_20260618_045234_test_clipboard.png": "cg_32d91763", + "20260618_045428_2_20260618_045234_test_clipboard.png": "cg_32d91763", + "20260618_045418_1_20260618_045234_test_clipboard.png": "cg_32d91763", + "20260618_045407_0_20260618_045234_test_clipboard.png": "cg_32d91763", + "20260618_051052_9_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_051040_8_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_051029_7_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_051017_6_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_051006_5_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_050955_4_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_050929_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_050935_3_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_050902_0_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_050913_1_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_050923_2_20260618_050846_img_4.png": "cg_b5b937c7", + "20260618_053530_2_20260618_053458_image.png": "solo:20260618_053530_2_20260618_053458_image.png" +} \ No newline at end of file diff --git a/tour-comfy/names.json b/tour-comfy/names.json new file mode 100644 index 0000000..a52e546 --- /dev/null +++ b/tour-comfy/names.json @@ -0,0 +1,10 @@ +{ + "Pasted image (9).png": "1girl solo breasts nipples nude long hair blue skin navel", + "20260618_015217_20260618_010710_p12-2.png": "Jane Doe", + "20260618_045629_20260618_045234_test_clipboard.png": "1girl breasts solo nipples nude head out of frame navel pussy", + "20260618_045656_20260618_045450_test_clipboard.png": "1girl breasts solo nipples nude navel head out of frame pussy", + "20260618_045831_20260618_045549_test_clipboard.png": "1girl breasts nipples solo nude blue skin pussy head out of frame", + "20260618_050929_20260618_050846_img_4.png": "1girl solo blue skin nipples nude head out of frame breasts navel", + "20260618_051629_20260618_051435_test_group.png": "1girl breasts solo nipples realistic nude head out of frame navel", + "20260618_052008_20260618_051426_test_group.png": "1girl breasts solo nipples nude head out of frame navel realistic" +} \ No newline at end of file diff --git a/tour-comfy/naming.py b/tour-comfy/naming.py new file mode 100644 index 0000000..0a3c48b --- /dev/null +++ b/tour-comfy/naming.py @@ -0,0 +1,96 @@ +import random +import re + +def clean_tag(tag): + return tag.replace("_", " ").strip() + +def generate_associative_name(tags): + """ + Generate a 'real-alike' associative name based on WD tagger tags. + tags: list of dicts {'tag': str, 'score': float, 'cat': int} + """ + if not tags: + return None + + # Filter by score + high_score_tags = [t for t in tags if t['score'] > 0.4] + if not high_score_tags: + high_score_tags = tags[:5] + + # Categories: 0=general, 4=character + characters = [clean_tag(t['tag']) for t in high_score_tags if t['cat'] == 4] + general = [clean_tag(t['tag']) for t in high_score_tags if t['cat'] == 0] + + # Key attributes + subject = None + if characters: + subject = characters[0].title() + elif "1girl" in [t['tag'] for t in high_score_tags]: + subject = "Maiden" + elif "1boy" in [t['tag'] for t in high_score_tags]: + subject = "Youth" + else: + subject = "Subject" + + # Actions/Poses + actions = ["standing", "sitting", "lying", "running", "walking", "dancing", "sleeping"] + found_action = next((clean_tag(t['tag']) for t in high_score_tags if t['tag'] in actions), None) + + # Setting/Background + settings = ["forest", "beach", "city", "space", "room", "garden", "ocean", "mountain", "sky", "underwater", "street"] + found_setting = next((clean_tag(t['tag']) for t in high_score_tags if t['tag'] in settings), None) + + # Appearance + colors = ["red", "blue", "green", "white", "black", "gold", "silver", "pink", "purple", "yellow"] + found_color = next((clean_tag(t['tag']) for t in high_score_tags if clean_tag(t['tag']).split()[0] in colors), None) + if not found_color: + found_color = next((clean_tag(t['tag']) for t in high_score_tags if any(c in t['tag'] for c in colors)), None) + + # Styles/Atmosphere + styles = ["cyberpunk", "fantasy", "realistic", "ethereal", "dark", "bright", "sketch", "oil painting"] + found_style = next((clean_tag(t['tag']) for t in high_score_tags if t['tag'] in styles), None) + + # Build the name + templates = [] + + if found_style and subject: + templates.append(f"{found_style.title()} {subject}") + + if found_color and subject: + templates.append(f"The {found_color.title()} {subject}") + + if subject and found_action: + if found_action.endswith("ing"): + templates.append(f"{subject} {found_action.title()}") + else: + # Basic attempt at present participle + action_ing = found_action + if action_ing.endswith("e"): + action_ing = action_ing[:-1] + "ing" + else: + action_ing += "ing" + templates.append(f"{subject} {action_ing.title()}") + + if subject and found_setting: + templates.append(f"{subject} in the {found_setting.title()}") + + if found_style and found_setting: + templates.append(f"{found_style.title()} {found_setting.title()}") + + if not templates: + # Fallback: combine two random general tags + if len(general) >= 2: + return f"{general[0].title()} {general[1].title()}" + elif general: + return general[0].title() + else: + return "Untitled Artwork" + + # Return a random template result + return random.choice(templates) + + +def get_base_name(name: str) -> str: + """Remove timestamp prefixes from filename.""" + # Matches YYYYMMDD_HHMMSS_ + return re.sub(r'^(\d{8}_\d{6}_)+', '', name) diff --git a/tour-comfy/output/car.html b/tour-comfy/output/car.html deleted file mode 100644 index 1097258..0000000 --- a/tour-comfy/output/car.html +++ /dev/null @@ -1,931 +0,0 @@ - - - - - - Live Image Monitor - - - -
-

- - - - - - Live Image Monitor -

-
- - -
-
- 0 images - - - - -
-
- - Auto-refresh: 2m -
-
- - - -
- -
- 0 selected -
- - -
- - - - - -
- - - - \ No newline at end of file diff --git a/tour-comfy/poses.md b/tour-comfy/poses.md new file mode 100644 index 0000000..c2dd429 --- /dev/null +++ b/tour-comfy/poses.md @@ -0,0 +1,65 @@ +# The Bride: +Lie flat on your back. +Bend one leg and bring it up to your chest, the other leg stretched out. +Place one arm under your head, and the other arm stretched out to the side. +Capture the curves and smooth lines of the body, focusing on the subtle curves of the back and the stretch of the bent leg. +Realistic, no background + +# The Dragon: +Lie on your back with your arms stretched out to the sides. +Bring your legs up to your chest, and cross one leg over the other. +Lean up to balance on your elbows, showcasing the extended arms and strong legs. +Focus on the elegant twist of the body and the intertwined legs. +Only two feet top, and two arms with hands on head. +Realistic, no background + +# The Butterfly: +Lie on your back with your knees bent and your feet touching. +Clasp your hands around your legs and pull your feet up towards your chest. +Extend one leg over the other, showcasing the bent knees and the exposed stretch of the legs. +Focus on the subtle, sexy stretch of the entire body. +Realistic, no background + +# The Sleeper: +Lie on your side with your upper arm behind your head and your lower arm stretched out. +The upper leg bent, while the lower leg is straight. +Lean slightly into the pose, showcasing the straight line of the lower leg and the bent upper leg. +Highlight the soft, supple curves of the torso and hips. +Realistic, no background + +# The Backbend: +Lie on your back with your knees bent and your feet touching. +Lift your feet off the ground and arch your back. +Look towards the ceiling with a seductive expression, showcasing the arched back and exposed legs. +Focus on the graceful lines of the body and the tension in the back and legs. +Realistic, no background + +# The Lotus: +Lie on your back with your knees bent and your feet touching. +Bring your legs up to your chest and cross them. +Clasp your hands around your feet and pull your legs towards your chest. +Focus on the delicate, intricate pose and the symmetry of the legs. +Realistic, no background + +# The Kneeling: +Kneel on all fours, with your hands under your shoulders and your knees under your hips. +Lift one arm and the opposite leg off the ground. +Lower your hips towards the ground, and hold the pose. +Show the strength and sexuality of the pose, focusing on the lifted arm and leg and the curved lines of the body. +Realistic, no background + +# The Prayer: +Lie on your back with your arms stretched out to the sides. +Bring your legs up to your chest and cross them. +Clasp your hands around your feet and pull your legs towards your chest. +Lean slightly into the pose, showcasing the cross-legged position and the symmetry of the pose. +Highlight the smooth curves of the body and the intricate pose. +Realistic, no background + +# The Celestial: +Lie on your back with your arms stretched out to the sides. +Bring your legs up to your chest and cross them. +Bend your knees and clasp your hands around your feet. +Lean back slightly into the pose, showcasing the bend in the knees and the clasp around the feet. +Focus on the ethereal, celestial quality of the pose and the intricate positioning of the hands and feet. +Realistic, no background \ No newline at end of file diff --git a/tour-comfy/processed.json b/tour-comfy/processed.json index d5f3f06..2e0a96c 100644 --- a/tour-comfy/processed.json +++ b/tour-comfy/processed.json @@ -104,5 +104,12 @@ "img_85.png": "f48059d59efd33ec8cce4daa44bbd46d", "img_83.png": "501207e02d72776b65d705db9f28a179", "img_86.png": "9e831c994a69ee1e18a6d279d69c072f", - "img_87.png": "4ce165b53df962d9e371124bfdec64bd" + "img_87.png": "4ce165b53df962d9e371124bfdec64bd", + "img_88.png": "7a22effc45850ecd53670827e0608e97", + "20260618_045234_test_clipboard.png": "9cd53a2f405bebbac4a21dded0f12e34", + "20260618_045450_test_clipboard.png": "9cd53a2f405bebbac4a21dded0f12e34", + "20260618_045549_test_clipboard.png": "9cd53a2f405bebbac4a21dded0f12e34", + "20260618_050846_img_4.png": "b18d89c79c74df97f1a7f106ece9d946", + "20260618_051435_test_group.png": "bb550daac9e09ead5487e984b18d4b13", + "20260618_051426_test_group.png": "bb550daac9e09ead5487e984b18d4b13" } \ No newline at end of file diff --git a/tour-comfy/start_watcher.sh b/tour-comfy/start_watcher.sh index db27ee1..bfefa40 100755 --- a/tour-comfy/start_watcher.sh +++ b/tour-comfy/start_watcher.sh @@ -1,13 +1,9 @@ #!/bin/bash # Launch the folder watcher service. set -e -API_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -BASE="$( cd "$API_DIR/.." && pwd )" - -# Try to activate venv if it exists, otherwise use system python -if [ -d "$BASE/venv" ]; then - source "$BASE/venv/bin/activate" -fi - +# env.sh resolves API_DIR/VENV (and keeps the venv off NTFS). +source "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env.sh" +source "$VENV/bin/activate" cd "$API_DIR" + exec python3 watcher.py diff --git a/tour-comfy/watcher.log b/tour-comfy/watcher.log index bed37e1..d3a4e3c 100644 --- a/tour-comfy/watcher.log +++ b/tour-comfy/watcher.log @@ -1137,3 +1137,215 @@ requests.exceptions.ConnectionError: HTTPConnectionPool(host='192.168.1.171', po 2026-06-18 00:02:13,359 - INFO - Calling API for img_88.png -> 20260618_000213_img_88.png with prompt: high quality, detailed, female nude 2026-06-18 00:02:13,515 - ERROR - API failed for img_88.png: 500 - Internal Server Error 2026-06-18 00:02:13,516 - INFO - Flagging image img_88.png (moving to failed directory as 20260618_000213_img_88.png) +2026-06-18 00:09:14,677 - INFO - Starting processing for img_88.png... +2026-06-18 00:09:14,679 - INFO - Cropping /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage/img_88.png to (23, 31, 142, 284) (margin=10, top_margin=20) +2026-06-18 00:09:14,679 - INFO - Adding 12px headroom to /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage/img_88.png +2026-06-18 00:09:14,681 - INFO - Calling API for img_88.png -> 20260618_000914_img_88.png with prompt: high quality, detailed, female nude +2026-06-18 00:10:38,474 - INFO - Successfully processed img_88.png -> /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output/20260618_000914_img_88.png +2026-06-18 00:10:38,482 - INFO - Updated /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output/car.html with 128 images +2026-06-18 00:19:43,853 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage... +2026-06-18 00:19:43,853 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output +2026-06-18 00:19:43,853 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 00:55:00,980 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage... +2026-06-18 00:55:00,980 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output +2026-06-18 00:55:00,980 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 01:26:22,512 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage... +2026-06-18 01:26:22,513 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output +2026-06-18 01:26:22,513 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 01:41:54,677 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage... +2026-06-18 01:41:54,677 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output +2026-06-18 01:41:54,677 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 02:47:35,417 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage... +2026-06-18 02:47:35,417 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output +2026-06-18 02:47:35,417 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 04:44:15,381 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 04:44:15,381 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 04:44:15,381 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 04:45:02,791 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 04:45:02,791 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 04:45:02,791 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 04:52:24,025 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 04:52:24,025 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 04:52:24,025 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 04:54:50,113 - INFO - Starting processing for 20260618_045234_test_clipboard.png... +2026-06-18 04:54:50,157 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045234_test_clipboard.png is mode RGB, not RGBA. Skipping crop. +2026-06-18 04:54:50,191 - INFO - Calling API for 20260618_045234_test_clipboard.png -> 20260618_045450_20260618_045234_test_clipboard.png with prompt: high quality, detailed, female nude +2026-06-18 04:55:49,039 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 04:55:49,039 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 04:55:49,039 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 04:56:29,059 - INFO - Starting processing for 20260618_045234_test_clipboard.png... +2026-06-18 04:56:29,061 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045234_test_clipboard.png is mode RGB, not RGBA. Skipping crop. +2026-06-18 04:56:29,063 - INFO - Calling API for 20260618_045234_test_clipboard.png -> 20260618_045629_20260618_045234_test_clipboard.png with prompt: high quality, detailed, female nude +2026-06-18 04:56:40,714 - INFO - Successfully processed 20260618_045234_test_clipboard.png -> /mnt/zim/tour-comfy/output/20260618_045629_20260618_045234_test_clipboard.png +2026-06-18 04:56:40,749 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14 +2026-06-18 04:56:40,749 - INFO - Loaded built-in ViT-H-14 model config. +2026-06-18 04:56:40,963 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found" +2026-06-18 04:56:40,964 - INFO - Instantiating model architecture: CLIP +2026-06-18 04:56:44,506 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors +2026-06-18 04:56:44,708 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0} +2026-06-18 04:56:44,708 - INFO - Model ViT-H-14 creation process complete. +2026-06-18 04:56:46,626 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 203 images +2026-06-18 04:56:56,628 - INFO - Starting processing for 20260618_045450_test_clipboard.png... +2026-06-18 04:56:56,628 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045450_test_clipboard.png is mode RGB, not RGBA. Skipping crop. +2026-06-18 04:56:56,628 - INFO - Calling API for 20260618_045450_test_clipboard.png -> 20260618_045656_20260618_045450_test_clipboard.png with prompt: high quality, detailed, female nude +2026-06-18 04:57:12,385 - INFO - Successfully processed 20260618_045450_test_clipboard.png -> /mnt/zim/tour-comfy/output/20260618_045656_20260618_045450_test_clipboard.png +2026-06-18 04:57:12,648 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 206 images +2026-06-18 04:58:31,683 - INFO - Starting processing for 20260618_045549_test_clipboard.png... +2026-06-18 04:58:31,683 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045549_test_clipboard.png is mode RGB, not RGBA. Skipping crop. +2026-06-18 04:58:31,683 - INFO - Calling API for 20260618_045549_test_clipboard.png -> 20260618_045831_20260618_045549_test_clipboard.png with prompt: high quality, detailed, female nude +2026-06-18 04:58:40,721 - INFO - Successfully processed 20260618_045549_test_clipboard.png -> /mnt/zim/tour-comfy/output/20260618_045831_20260618_045549_test_clipboard.png +2026-06-18 04:58:40,879 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 210 images +2026-06-18 05:00:23,081 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 05:00:23,082 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 05:00:23,082 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 05:09:29,285 - INFO - Starting processing for 20260618_050846_img_4.png... +2026-06-18 05:09:29,287 - INFO - Cropping /mnt/zim/tour-comfy/stage/20260618_050846_img_4.png to (20, 51, 138, 345) (margin=10, top_margin=20) +2026-06-18 05:09:29,288 - INFO - Adding 14px headroom to /mnt/zim/tour-comfy/stage/20260618_050846_img_4.png +2026-06-18 05:09:29,292 - INFO - Calling API for 20260618_050846_img_4.png -> 20260618_050929_20260618_050846_img_4.png with prompt: high quality, detailed, female nude +2026-06-18 05:09:43,897 - INFO - Successfully processed 20260618_050846_img_4.png -> /mnt/zim/tour-comfy/output/20260618_050929_20260618_050846_img_4.png +2026-06-18 05:09:43,934 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14 +2026-06-18 05:09:43,934 - INFO - Loaded built-in ViT-H-14 model config. +2026-06-18 05:09:44,116 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found" +2026-06-18 05:09:44,117 - INFO - Instantiating model architecture: CLIP +2026-06-18 05:09:47,742 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors +2026-06-18 05:09:48,828 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0} +2026-06-18 05:09:48,828 - INFO - Model ViT-H-14 creation process complete. +2026-06-18 05:09:50,791 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 196 images +2026-06-18 05:14:14,718 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 05:14:14,718 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 05:14:14,718 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 05:16:29,765 - INFO - Starting processing for 20260618_051435_test_group.png... +2026-06-18 05:16:29,766 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_051435_test_group.png is mode RGB, not RGBA. Skipping crop. +2026-06-18 05:16:29,768 - INFO - Calling API for 20260618_051435_test_group.png -> 20260618_051629_20260618_051435_test_group.png with prompt: high quality, detailed, female nude +2026-06-18 05:16:58,905 - INFO - Successfully processed 20260618_051435_test_group.png -> /mnt/zim/tour-comfy/output/20260618_051629_20260618_051435_test_group.png +2026-06-18 05:16:58,941 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14 +2026-06-18 05:16:58,942 - INFO - Loaded built-in ViT-H-14 model config. +2026-06-18 05:16:59,127 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found" +2026-06-18 05:16:59,128 - INFO - Instantiating model architecture: CLIP +2026-06-18 05:17:02,756 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors +2026-06-18 05:17:02,958 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0} +2026-06-18 05:17:02,958 - INFO - Model ViT-H-14 creation process complete. +2026-06-18 05:17:04,921 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 216 images +2026-06-18 05:17:31,937 - INFO - Starting processing for 20260618_051415_test_group.png... +2026-06-18 05:17:31,937 - ERROR - Failed to crop /mnt/zim/tour-comfy/stage/20260618_051415_test_group.png: broken PNG file (chunk b'END\xae') +2026-06-18 05:17:31,937 - ERROR - Error processing 20260618_051415_test_group.png: broken PNG file (chunk b'END\xae') +Traceback (most recent call last): + File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 154, in process_image + cropped_img = crop_to_bbox( + input_path, + ...<2 lines>... + headroom=CONF.get("headroom", 0.0) + ) + File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 85, in crop_to_bbox + alpha = img.split()[-1] + ~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/Image.py", line 2793, in split + self.load() + ~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/ImageFile.py", line 392, in load + s = read(read_bytes) + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 999, in load_read + cid, pos, length = self.png.read() + ~~~~~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 184, in read + raise SyntaxError(msg) +SyntaxError: broken PNG file (chunk b'END\xae') +2026-06-18 05:17:31,942 - ERROR - Main loop error: cannot access local variable 'temp_path' where it is not associated with a value +2026-06-18 05:18:28,964 - INFO - Starting processing for 20260618_051415_test_group.png... +2026-06-18 05:18:28,964 - ERROR - Failed to crop /mnt/zim/tour-comfy/stage/20260618_051415_test_group.png: broken PNG file (chunk b'END\xae') +2026-06-18 05:18:28,964 - ERROR - Error processing 20260618_051415_test_group.png: broken PNG file (chunk b'END\xae') +Traceback (most recent call last): + File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 154, in process_image + cropped_img = crop_to_bbox( + input_path, + ...<2 lines>... + headroom=CONF.get("headroom", 0.0) + ) + File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 85, in crop_to_bbox + alpha = img.split()[-1] + ~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/Image.py", line 2793, in split + self.load() + ~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/ImageFile.py", line 392, in load + s = read(read_bytes) + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 999, in load_read + cid, pos, length = self.png.read() + ~~~~~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 184, in read + raise SyntaxError(msg) +SyntaxError: broken PNG file (chunk b'END\xae') +2026-06-18 05:18:28,965 - ERROR - Main loop error: cannot access local variable 'temp_path' where it is not associated with a value +2026-06-18 05:19:00,707 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 05:19:00,707 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 05:19:00,707 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 05:19:12,911 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 05:19:12,911 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 05:19:12,911 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 05:20:07,936 - INFO - Starting processing for 20260618_051415_test_group.png... +2026-06-18 05:20:07,937 - ERROR - Failed to crop /mnt/zim/tour-comfy/stage/20260618_051415_test_group.png: broken PNG file (chunk b'END\xae') +2026-06-18 05:20:07,937 - ERROR - Error processing 20260618_051415_test_group.png: broken PNG file (chunk b'END\xae') +Traceback (most recent call last): + File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 155, in process_image + cropped_img = crop_to_bbox( + input_path, + ...<2 lines>... + headroom=CONF.get("headroom", 0.0) + ) + File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 85, in crop_to_bbox + alpha = img.split()[-1] + ~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/Image.py", line 2793, in split + self.load() + ~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/ImageFile.py", line 392, in load + s = read(read_bytes) + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 999, in load_read + cid, pos, length = self.png.read() + ~~~~~~~~~~~~~^^ + File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 184, in read + raise SyntaxError(msg) +SyntaxError: broken PNG file (chunk b'END\xae') +2026-06-18 05:20:07,938 - INFO - Flagging image 20260618_051415_test_group.png (moving to failed directory as 20260618_052007_20260618_051415_test_group.png) +2026-06-18 05:20:08,939 - INFO - Starting processing for 20260618_051426_test_group.png... +2026-06-18 05:20:08,939 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_051426_test_group.png is mode RGB, not RGBA. Skipping crop. +2026-06-18 05:20:08,942 - INFO - Calling API for 20260618_051426_test_group.png -> 20260618_052008_20260618_051426_test_group.png with prompt: high quality, detailed, female nude +2026-06-18 05:20:17,982 - INFO - Successfully processed 20260618_051426_test_group.png -> /mnt/zim/tour-comfy/output/20260618_052008_20260618_051426_test_group.png +2026-06-18 05:20:18,011 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14 +2026-06-18 05:20:18,011 - INFO - Loaded built-in ViT-H-14 model config. +2026-06-18 05:20:18,198 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found" +2026-06-18 05:20:18,198 - INFO - Instantiating model architecture: CLIP +2026-06-18 05:20:21,797 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors +2026-06-18 05:20:22,087 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0} +2026-06-18 05:20:22,087 - INFO - Model ViT-H-14 creation process complete. +2026-06-18 05:20:24,035 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 227 images +2026-06-18 12:28:33,669 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 12:28:33,669 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 12:28:33,669 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 12:39:41,444 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 12:39:41,444 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 12:39:41,444 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 13:15:51,389 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 13:15:51,389 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 13:15:51,389 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 13:26:24,286 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 13:26:24,286 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 13:26:24,286 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 13:33:50,546 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 13:33:50,546 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 13:33:50,546 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 17:14:37,020 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 17:14:37,020 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 17:14:37,020 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 20:51:01,612 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 20:51:01,613 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 20:51:01,613 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 22:02:15,432 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 22:02:15,432 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 22:02:15,432 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 22:22:53,496 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 22:22:53,496 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 22:22:53,496 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-18 22:31:18,891 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-18 22:31:18,891 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-18 22:31:18,891 - INFO - API URL: http://127.0.0.1:8500/edit diff --git a/tour-comfy/watcher.py b/tour-comfy/watcher.py index a6305c8..1800520 100644 --- a/tour-comfy/watcher.py +++ b/tour-comfy/watcher.py @@ -10,6 +10,13 @@ import sys import fcntl import re +try: + from . import database + from . import embeddings +except ImportError: + import database + import embeddings + # Load configuration CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json") @@ -142,6 +149,7 @@ def process_image(filename): output_filename = f"{timestamp}_{filename}" output_path = os.path.join(CONF["output_dir"], output_filename) + temp_path = input_path + ".tmp.png" try: logging.info(f"Starting processing for {filename}...") cropped_img = crop_to_bbox( @@ -152,7 +160,6 @@ def process_image(filename): ) # Save temporary cropped image for upload - temp_path = input_path + ".tmp.png" cropped_img.save(temp_path, format="PNG") with open(temp_path, 'rb') as f: @@ -169,6 +176,22 @@ def process_image(filename): with open(output_path, 'wb') as f: f.write(response.content) logging.info(f"Successfully processed {filename} -> {output_path}") + + # Register in DB + try: + embedding = embeddings.generate_embedding(output_path) + gid = filename + database.upsert_person(output_filename, filepath=output_path, embedding=embedding, group_id=gid) + + # Also trigger tagging to get auto-name and clip description + tag_url = CONF["api_url"].replace("/edit", "/tag") + try: + requests.post(tag_url, json={"filename": output_filename, "group_id": gid}, timeout=30) + except Exception as tag_err: + logging.error(f"Error triggering tagging for {output_filename}: {tag_err}") + except Exception as db_err: + logging.error(f"Database error registering {output_filename}: {db_err}") + if os.path.exists(temp_path): os.remove(temp_path) return True