This commit is contained in:
mike
2026-06-18 23:26:30 +02:00
parent 1dead1c666
commit 045b9b6458
21 changed files with 1477 additions and 979 deletions

View File

@@ -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
}
}

View File

@@ -1,11 +1,11 @@
{ {
"baseUrl": "http://localhost:11434/v1/responses", "baseUrl": "http://localhost:11434/v1/responses",
"id": "qwen3-coder:30b", "id": "qwen2.5-coder:32b",
"apiType": "OpenAIResponses", "apiType": "OpenAIResponses",
"temperature": 0.3, "temperature": 0.1,
"primaryModel": { "primaryModel": {
"id": "qwen3-coder:30b", "id": "qwen2.5-coder:32b",
"temperature": 0.3 "temperature": 0.1
}, },
"fasterModel": { "fasterModel": {
"id": "qwen2.5-coder:1.5b" "id": "qwen2.5-coder:1.5b"

View File

@@ -1,14 +1,13 @@
{ {
"baseUrl": "http://localhost:11434/v1/responses", "baseUrl": "http://localhost:11440/v1/responses",
"id": "qwen25-coder-32b-64k:latest", "id": "qwen3-coder:30b",
"apiType": "OpenAIResponses", "apiType": "OpenAIResponses",
"temperature": 0.3, "temperature": 0.3,
"primaryModel": { "primaryModel": {
"id": "qwen25-coder-32b-64k:latest", "id": "qwen3-coder:30b",
"temperature": 0.2 "temperature": 0.3
}, },
"fasterModel": { "fasterModel": {
"id": "qwen25-coder-32b-64k:latest", "id": "qwen2.5-coder:1.5b"
"temperature": 0.2
} }
} }

View File

@@ -1,5 +0,0 @@
FROM qwen2.5-coder:32b
PARAMETER num_ctx 64000
PARAMETER temperature 0.15
PARAMETER top_p 0.9

152
optimize_clips.py Normal file
View File

@@ -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")

59
position.md Normal file
View File

@@ -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
Thats 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, Id say: a straight-on portrait.
and with full top till down
Een seconde nagedacht
Thats 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. Ill 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

View File

@@ -8,3 +8,4 @@ dashscope
torchvision torchvision
peft peft
torchao==0.11.0 torchao==0.11.0
rembg[cpu]

View File

@@ -1,15 +1,23 @@
{ {
"api_url": "http://127.0.0.1:8500/edit", "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, "seed": -1,
"max_area": 655360, "max_area": 655360,
"margin": 10, "margin": 10,
"top_margin": 20, "top_margin": 20,
"headroom": 0.05, "headroom": 0.05,
"poll_interval": 2, "poll_interval": 2,
"stage_dir": "./tour-comfy/stage", "stage_dir": "/mnt/zim/tour-comfy/stage",
"output_dir": "./tour-comfy/output", "output_dir": "/mnt/zim/tour-comfy/output",
"failed_dir": "./tour-comfy/failed", "failed_dir": "/mnt/zim/tour-comfy/failed",
"processed_file": "./tour-comfy/processed.json", "processed_file": "./tour-comfy/processed.json",
"log_file": "./tour-comfy/watcher.log" "log_file": "./tour-comfy/watcher.log"
} }

101
tour-comfy/database.py Normal file
View File

@@ -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()

View File

@@ -16,16 +16,31 @@ import uuid
import random import random
import copy import copy
import threading import threading
import csv
try:
from . import database
from . import embeddings
from . import naming
except ImportError:
import database
import embeddings
import naming
import requests import requests
from PIL import Image 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.middleware.cors import CORSMiddleware
from fastapi.responses import Response from fastapi.responses import Response
from pydantic import BaseModel from pydantic import BaseModel
import shutil
import re
# --- config ----------------------------------------------------------------- # --- 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("/") COMFY = os.environ.get("COMFY_URL", "http://127.0.0.1:8188").rstrip("/")
WORKFLOW_PATH = os.environ.get( WORKFLOW_PATH = os.environ.get(
"WORKFLOW_PATH", "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 ids in workflow_qwen_edit.json (kept stable on purpose).
NODE_LOADIMAGE = "4" NODE_LOADIMAGE = "4"
NODE_POSITIVE = "5" NODE_POSITIVE = "5"
NODE_NEGATIVE = "6"
NODE_LATENT = "7" NODE_LATENT = "7"
NODE_KSAMPLER = "8" NODE_KSAMPLER = "8"
NODE_SAVE = "10" NODE_SAVE = "10"
@@ -52,7 +68,7 @@ app = FastAPI(title="Qwen-Image-Edit Rapid-AIO API", version="1.0")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
allow_methods=["GET", "POST"], allow_methods=["GET", "POST", "DELETE"],
allow_headers=["*"], allow_headers=["*"],
) )
@@ -167,8 +183,122 @@ def _comfy_fetch_image(outputs: dict) -> bytes:
return r.content 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 --------------------------------------------------------- # --- 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( def _run_pipeline(
pil: Image.Image, pil: Image.Image,
prompt: str, prompt: str,
@@ -189,6 +319,12 @@ def _run_pipeline(
graph = copy.deepcopy(BASE_WORKFLOW) graph = copy.deepcopy(BASE_WORKFLOW)
graph[NODE_LOADIMAGE]["inputs"]["image"] = stored graph[NODE_LOADIMAGE]["inputs"]["image"] = stored
graph[NODE_POSITIVE]["inputs"]["prompt"] = prompt 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"]["width"] = w
graph[NODE_LATENT]["inputs"]["height"] = h graph[NODE_LATENT]["inputs"]["height"] = h
ks = graph[NODE_KSAMPLER]["inputs"] ks = graph[NODE_KSAMPLER]["inputs"]
@@ -196,7 +332,12 @@ def _run_pipeline(
client_id = uuid.uuid4().hex client_id = uuid.uuid4().hex
prompt_id = _comfy_queue(graph, client_id) prompt_id = _comfy_queue(graph, client_id)
outputs = _comfy_wait(prompt_id, time.time() + GEN_TIMEOUT) 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 ------------------------------------------------------------- # --- batch state -------------------------------------------------------------
@@ -213,20 +354,76 @@ def _load_output_dir() -> str:
return d 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() output_dir = _load_output_dir()
for fname in filenames: 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) fpath = os.path.join(output_dir, fname)
if not os.path.exists(fpath):
jobs[job_id]["failed"] += len(prompts)
continue
try: try:
pil = Image.open(fpath).convert("RGB") pil = Image.open(fpath).convert("RGB")
for prompt in prompts:
try:
png = _run_pipeline(pil, prompt, seed, max_area) png = _run_pipeline(pil, prompt, seed, max_area)
ts = time.strftime("%Y%m%d_%H%M%S") 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: # 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) 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 jobs[job_id]["done"] += 1
except Exception as e: except Exception as e:
print(f"Error in batch for {fname} with prompt '{prompt}': {e}")
jobs[job_id]["failed"] += 1 jobs[job_id]["failed"] += 1
except Exception as e:
print(f"Error opening {fname}: {e}")
jobs[job_id]["failed"] += len(prompts)
jobs[job_id]["status"] = "done" jobs[job_id]["status"] = "done"
@@ -258,22 +455,31 @@ def update_config(update: ConfigUpdate):
class BatchRequest(BaseModel): class BatchRequest(BaseModel):
filenames: list[str] filenames: list[str]
prompt: str prompt: str | list[str]
seed: int = -1 seed: int = -1
max_area: int = 0 max_area: int = 0
group_id: str | None = None
@app.post("/batch") @app.post("/batch")
def start_batch(req: BatchRequest): 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] 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( t = threading.Thread(
target=_batch_worker, 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, daemon=True,
) )
t.start() 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}") @app.get("/batch/{job_id}")
@@ -283,6 +489,186 @@ def get_batch(job_id: str):
return jobs[job_id] 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") @app.get("/health")
def health(): def health():
try: try:
@@ -292,6 +678,119 @@ def health():
raise HTTPException(503, f"ComfyUI unreachable at {COMFY}: {e}") 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") @app.post("/edit")
async def edit( async def edit(
image: UploadFile = File(...), image: UploadFile = File(...),
@@ -313,6 +812,64 @@ async def edit(
return Response(content=png, media_type="image/png") 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__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host=os.environ.get("HOST", "0.0.0.0"), uvicorn.run(app, host=os.environ.get("HOST", "0.0.0.0"),

30
tour-comfy/embeddings.py Normal file
View File

@@ -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

View File

@@ -19,7 +19,12 @@ COMFY="$BASE/ComfyUI"
_basefs="$(stat -f -c %T "$BASE" 2>/dev/null || echo unknown)" _basefs="$(stat -f -c %T "$BASE" 2>/dev/null || echo unknown)"
case "$_basefs" in case "$_basefs" in
fuseblk|ntfs|ntfs3|exfat|vfat|msdos|9p|cifs|smb*) 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 esac

99
tour-comfy/groups.json Normal file
View File

@@ -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"
}

10
tour-comfy/names.json Normal file
View File

@@ -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"
}

96
tour-comfy/naming.py Normal file
View File

@@ -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)

View File

@@ -1,931 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Image Monitor</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #e0e0e0;
min-height: 100vh;
}
.header {
position: fixed;
top: 0; left: 0; right: 0;
background: rgba(15, 15, 15, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid #333;
padding: 16px 24px;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 18px;
font-weight: 600;
color: #fff;
display: flex;
align-items: center;
gap: 10px;
}
.status {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #888;
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #22c55e;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-dot.updating {
background: #f59e0b;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.controls {
display: flex;
gap: 12px;
align-items: center;
}
.btn {
background: #1a1a1a;
border: 1px solid #333;
color: #ccc;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn:hover {
background: #252525;
border-color: #444;
color: #fff;
}
.btn.primary {
background: #2563eb;
border-color: #2563eb;
color: #fff;
}
.btn.primary:hover {
background: #3b82f6;
}
.prompt-bar {
display: flex;
gap: 8px;
align-items: center;
}
.prompt-input {
background: #1a1a1a;
border: 1px solid #333;
color: #e0e0e0;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
width: 320px;
outline: none;
transition: border-color 0.2s;
}
.prompt-input:focus {
border-color: #2563eb;
}
.prompt-input::placeholder {
color: #555;
}
.count {
font-size: 13px;
color: #666;
font-variant-numeric: tabular-nums;
}
.gallery {
padding: 100px 24px 40px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
max-width: 1800px;
margin: 0 auto;
}
.image-card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s, border-color 0.2s;
position: relative;
}
.image-card:hover {
transform: translateY(-2px);
border-color: #444;
}
.image-card.new {
border-color: #2563eb;
animation: highlight 1s ease;
}
@keyframes highlight {
from { box-shadow: 0 0 0 2px #2563eb; }
to { box-shadow: none; }
}
.image-wrapper {
position: relative;
padding-top: 300%; /* 1:3 aspect ratio (3x taller than wide) */
background: #111;
overflow: hidden;
}
.image-wrapper img {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.image-card:hover .image-wrapper img {
transform: scale(1.03);
}
.image-info {
padding: 14px 16px;
}
.image-name {
font-size: 14px;
font-weight: 500;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 6px;
}
.image-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #666;
}
.image-time {
display: flex;
align-items: center;
gap: 4px;
}
.badge {
background: #2563eb;
color: #fff;
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge.recent {
background: #22c55e;
}
.empty-state {
text-align: center;
padding: 120px 20px;
color: #555;
}
.empty-state svg {
width: 64px; height: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.empty-state h2 {
font-size: 20px;
color: #777;
margin-bottom: 8px;
}
.empty-state p {
font-size: 14px;
}
.toast {
position: fixed;
bottom: 24px; right: 24px;
background: #1a1a1a;
border: 1px solid #333;
padding: 12px 20px;
border-radius: 8px;
font-size: 13px;
color: #ccc;
transform: translateY(100px);
opacity: 0;
transition: all 0.3s;
z-index: 200;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
.toast.success { border-left: 3px solid #22c55e; }
.toast.info { border-left: 3px solid #2563eb; }
.btn.active {
background: #2563eb;
border-color: #2563eb;
color: #fff;
}
.image-card.selectable {
cursor: pointer;
user-select: none;
}
.image-card.selected {
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37,99,235,0.35);
}
.select-indicator {
position: absolute;
top: 8px; right: 8px;
width: 22px; height: 22px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.5);
background: rgba(0,0,0,0.45);
display: none;
align-items: center;
justify-content: center;
z-index: 10;
font-size: 13px;
font-weight: 700;
color: #fff;
transition: background 0.15s, border-color 0.15s;
}
.selectable .select-indicator { display: flex; }
.selected .select-indicator {
background: #2563eb;
border-color: #2563eb;
}
.selected .select-indicator::after { content: '✓'; }
.batch-bar {
position: fixed;
bottom: 0; left: 0; right: 0;
background: rgba(12,12,12,0.97);
backdrop-filter: blur(12px);
border-top: 1px solid #2a2a2a;
padding: 14px 24px;
display: flex;
align-items: center;
gap: 12px;
z-index: 100;
transform: translateY(100%);
transition: transform 0.25s ease;
}
.batch-bar.visible { transform: translateY(0); }
.batch-count {
font-size: 13px;
color: #888;
min-width: 80px;
white-space: nowrap;
}
.batch-prompt-wrap { flex: 1; position: relative; }
.batch-prompt {
width: 100%;
background: #1a1a1a;
border: 1px solid #333;
color: #e0e0e0;
padding: 7px 12px;
border-radius: 6px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.batch-prompt:focus { border-color: #2563eb; }
.batch-prompt::placeholder { color: #555; }
.batch-progress {
font-size: 12px;
color: #666;
min-width: 90px;
text-align: right;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="header">
<h1>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
Live Image Monitor
</h1>
<div class="prompt-bar">
<input class="prompt-input" id="promptInput" type="text" placeholder="prompt..." onkeydown="if(event.key==='Enter')setPrompt()" />
<button class="btn primary" onclick="setPrompt()">Set Prompt</button>
</div>
<div class="controls">
<span class="count" id="count">0 images</span>
<button class="btn" onclick="setIntervalTime(30)">30s</button>
<button class="btn" onclick="setIntervalTime(120)">2m</button>
<button class="btn primary" onclick="refreshNow()">Refresh Now</button>
<button class="btn" id="selectBtn" onclick="toggleSelectMode()">Select</button>
</div>
<div class="status">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">Auto-refresh: 2m</span>
</div>
</div>
<div class="gallery" id="gallery">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<h2>No images found</h2>
<p>Place this HTML file in your image folder, or configure the path below.</p>
</div>
</div>
<div class="toast" id="toast"></div>
<div class="batch-bar" id="batchBar">
<span class="batch-count" id="batchCount">0 selected</span>
<div class="batch-prompt-wrap">
<input class="batch-prompt" id="batchPromptInput" list="promptHistory"
placeholder="temporal prompt..."
onkeydown="if(event.key==='Enter')processSelected()" />
<datalist id="promptHistory"></datalist>
</div>
<button class="btn" onclick="selectAll()">All</button>
<button class="btn" onclick="deselectAll()">None</button>
<button class="btn primary" id="processBtn" onclick="processSelected()">Process</button>
<span class="batch-progress" id="batchProgress"></span>
<button class="btn" onclick="toggleSelectMode()">Done</button>
</div>
<script>
// ============================================
// CONFIGURATION - EDIT THIS SECTION
// ============================================
// Set this to your image folder path relative to this HTML file
// Examples: './' (same folder), './images/', '../screenshots/'
const IMAGE_FOLDER = './';
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"20260617_215245_img_87.png",
"20260617_134615_img_86.png",
"20260617_134229_img_83.png",
"20260617_134119_img_85.png",
"20260617_134041_img_84.png",
"20260617_133917_img_82.png",
"20260617_133832_img_81.png",
"20260617_133519_img_80.png",
"20260617_133411_img_79.png",
"20260617_133154_img_78.png",
"20260617_133129_img_77.png",
"20260617_132851_img_76.png",
"20260617_074613_img_75.png",
"20260617_074556_img_74.png",
"20260617_074413_img_73.png",
"20260617_074223_img_72.png",
"20260617_015946_img_71.png",
"20260617_015728_img_70.png",
"20260617_015611_img_69.png",
"20260617_015007_img_68.png",
"20260617_014351_img_66.png",
"20260617_013327_img_67.png",
"20260617_013231_img_68.png",
"20260617_013211_img_65.png",
"20260617_013150_img_66.png",
"20260617_013111_img_63.png",
"20260617_013035_img_64.png",
"20260617_012709_img_62.png",
"20260617_011132_img_61.png",
"20260617_005512_img_60.png",
"20260617_005200_img_59.png",
"20260617_005040_img_56.png",
"20260617_005026_img_55.png",
"20260617_005008_img_54.png",
"20260617_004942_img_53.png",
"20260617_004814_img_57.png",
"20260616_023306_img_52.png",
"20260616_023209_img_51.png",
"20260616_022543_img_50.png",
"20260616_022349_img_48.png",
"20260616_021938_20160903_200935.jpg",
"20260616_021235_img_47.png",
"20260616_021214_img_46.png",
"20260616_021150_img_44.png",
"20260616_021116_img_43.png",
"20260616_021056_img_45.png",
"20260616_021003_img_41.png",
"20260616_020908_img_40.png",
"20260616_020403_img_39.png",
"20260616_020059_img_38.png",
"20260616_020035_img_36.png",
"20260616_020020_img_35.png",
"20260616_015949_img_37.png",
"20260616_015919_img_33.png",
"20260616_015850_img_34.png",
"20260616_015341_img_32.png",
"20260616_014757_img_31.png",
"20260616_014225_img_30.png",
"20260616_014057_img_29.png",
"20260616_013755_img_28.png",
"20260616_013603_img_27.png",
"20260616_013013_img_26.png",
"20260616_012939_img_25.png",
"20260616_011823_imgxxxx.png",
"20260616_011447_img_24.png",
"20260616_010228_img_22.png",
"20260616_005752_img_21.png",
"20260616_005727_img_19.png",
"20260616_005202_img_20.png",
"20260616_004250_img_18.png",
"20260616_004220_img_17.png",
"20260616_004001_img_16.png",
"20260616_003916_img_15.png",
"20260616_003803_img_14.png",
"20260616_003629_img_13.png",
"20260616_003548_img.png",
"20260616_002456_test123.jpeg",
"20260616_002302_image.png",
"20260615_155756_img_6v1.png",
"20260615_155354_others.jpeg",
"20260615_154852_other.jpeg",
"20260615_154333_other.jpeg",
"20260615_153749_img_11.png",
"20260615_153426_img_12.png",
"20260615_153125_img_10.png",
"20260615_152826_img.png",
"20260615_152252_imgxxx.png",
"20260615_151829_img_92.png",
"20260615_151614_img_93.png",
"20260615_150812_img_19_2.png",
"20260615_150340_test.png",
"20260615_145017_img_93.png",
"img_9.png",
"b1.png",
"jb3.png",
"jb.png",
"jb1.png",
"img.png",
"out7.png",
"out5.png",
"out4.png",
"out3.png",
"out2.png",
"out.png",
"bb_01.png",
"tp236b.png",
"pass-1.png",
"t159zr-1.png",
"kbk99v.png",
"kk563t.png",
"Pasted image (9).png",
"Pasted image (7).png",
"Pasted image (5).png",
"Pasted image (4).png",
"Pasted image (3).png",
"Pasted image (2).png",
"Pasted image.png",
"pa01.png",
"pa0.png",
"p13.png",
"p12-2.png",
"p12.png",
"p11.png",
"p10.png",
"p1.png",
"img_2.png",
"img_3.png"
];
// --- HYDRATION_END ---
// Supported image extensions
const EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
// Auto-refresh interval in seconds (default: 120 = 2 minutes)
let REFRESH_INTERVAL = 120;
// ============================================
let autoRefreshTimer = null;
let knownFiles = new Set();
let currentFiles = [];
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => toast.classList.remove('show'), 3000);
}
function formatTime(date) {
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return date.toLocaleDateString();
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// Try to get file info using HEAD requests (works with some local servers)
async function getFileInfo(filename) {
try {
const response = await fetch(IMAGE_FOLDER + filename, { method: 'HEAD' });
const lastMod = response.headers.get('last-modified');
const size = response.headers.get('content-length');
return {
modified: lastMod ? new Date(lastMod) : new Date(),
size: size ? parseInt(size) : 0
};
} catch {
return { modified: new Date(), size: 0 };
}
}
async function scanFolder() {
// For local file:// protocol, we can't list directories.
// This works when served via HTTP or you can manually list files.
// METHOD 1: If you have a simple file server, it might return directory listing
try {
const response = await fetch(IMAGE_FOLDER);
const text = await response.text();
// Try to parse common directory listing formats
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const links = Array.from(doc.querySelectorAll('a'))
.map(a => a.getAttribute('href'))
.filter(href => href && EXTENSIONS.some(ext => href.toLowerCase().endsWith(ext)))
.map(href => href.split('/').pop()); // Just the filename
if (links.length > 0) return links;
} catch (e) {
// Directory listing not available
}
// METHOD 2: Manual file list (fallback)
// If you can't use a server, list your files here or use the manual input below
return null;
}
async function loadImages() {
const gallery = document.getElementById('gallery');
const statusDot = document.getElementById('statusDot');
statusDot.classList.add('updating');
let files = null;
// Use preloaded images if available
if (typeof PRELOADED_IMAGES !== 'undefined' && PRELOADED_IMAGES.length > 0) {
files = PRELOADED_IMAGES;
} else {
files = await scanFolder();
}
// FALLBACK: If auto-scan doesn't work, use this manual approach
if (!files) {
files = await discoverImages();
}
// Sort by newest first (we use a cache-busting parameter to force reload)
const fileObjects = files.map(name => ({
name: name,
url: IMAGE_FOLDER + name + '?t=' + Date.now(),
cleanName: name.split('?')[0]
}));
// Check for new files
const newFiles = fileObjects.filter(f => !knownFiles.has(f.cleanName));
newFiles.forEach(f => knownFiles.add(f.cleanName));
currentFiles = fileObjects;
if (fileObjects.length === 0) {
gallery.innerHTML = `
<div class="empty-state" style="grid-column: 1 / -1;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
<h2>No images found in "${IMAGE_FOLDER}"</h2>
<p>Make sure this HTML file is in the same folder as your images,<br>or update the IMAGE_FOLDER path in the script.</p>
</div>
`;
statusDot.classList.remove('updating');
return;
}
// Render gallery
gallery.innerHTML = fileObjects.map((file, index) => {
const isNew = index < newFiles.length;
const isRecent = index < 3;
return `
<div class="image-card ${isNew ? 'new' : ''}" data-name="${file.cleanName}">
<div class="image-wrapper">
<div class="select-indicator"></div>
<img src="${file.url}"
alt="${file.cleanName}"
loading="lazy"
onerror="this.parentElement.innerHTML='<div style=\\'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#444;font-size:13px;\\'>Failed to load</div>'">
</div>
<div class="image-info">
<div class="image-name" title="${file.cleanName}">${file.cleanName}</div>
<div class="image-meta">
<span class="image-time">#${String(fileObjects.length - index).padStart(3, '0')}</span>
${isRecent ? '<span class="badge recent">Latest</span>' : ''}
</div>
</div>
</div>
`;
}).join('');
updateCardSelection();
document.getElementById('count').textContent = `${fileObjects.length} image${fileObjects.length !== 1 ? 's' : ''}`;
if (newFiles.length > 0) {
showToast(`${newFiles.length} new image${newFiles.length !== 1 ? 's' : ''} detected`, 'success');
}
statusDot.classList.remove('updating');
}
// Discover images by trying common patterns (fallback for file:// protocol)
async function discoverImages() {
const found = [];
// Try to find images by testing if they exist
// This is a best-effort approach for local file access
// If you know your naming pattern, add it here:
const patterns = [];
// Try to extract from page if there are any references
const images = document.querySelectorAll('img[src]');
images.forEach(img => {
const src = img.getAttribute('src');
if (src && EXTENSIONS.some(ext => src.toLowerCase().includes(ext))) {
found.push(src.split('/').pop().split('?')[0]);
}
});
// Remove duplicates
return [...new Set(found)];
}
function setIntervalTime(seconds) {
REFRESH_INTERVAL = seconds;
document.getElementById('statusText').textContent = `Auto-refresh: ${seconds < 60 ? seconds + 's' : (seconds / 60) + 'm'}`;
clearInterval(autoRefreshTimer);
autoRefreshTimer = setInterval(refreshNow, REFRESH_INTERVAL * 1000);
showToast(`Refresh interval set to ${seconds < 60 ? seconds + ' seconds' : (seconds / 60) + ' minutes'}`, 'info');
}
function refreshNow() {
loadImages();
}
const API = 'http://127.0.0.1:8500';
// --- selection state ---
let selectionMode = false;
const selectedFiles = new Set();
function toggleSelectMode() {
selectionMode = !selectionMode;
if (!selectionMode) selectedFiles.clear();
document.getElementById('selectBtn').classList.toggle('active', selectionMode);
const bar = document.getElementById('batchBar');
bar.classList.toggle('visible', selectionMode);
document.querySelector('.gallery').style.paddingBottom = selectionMode ? '90px' : '40px';
updateCardSelection();
updateBatchBar();
}
function toggleFile(name) {
if (selectedFiles.has(name)) selectedFiles.delete(name);
else selectedFiles.add(name);
updateCardSelection();
updateBatchBar();
}
function updateCardSelection() {
document.querySelectorAll('.image-card').forEach(card => {
const name = card.dataset.name;
card.classList.toggle('selectable', selectionMode);
card.classList.toggle('selected', selectionMode && selectedFiles.has(name));
});
}
function updateBatchBar() {
const n = selectedFiles.size;
document.getElementById('batchCount').textContent = `${n} selected`;
document.getElementById('processBtn').textContent = n > 0 ? `Process (${n})` : 'Process';
}
function selectAll() {
currentFiles.forEach(f => selectedFiles.add(f.cleanName));
updateCardSelection();
updateBatchBar();
}
function deselectAll() {
selectedFiles.clear();
updateCardSelection();
updateBatchBar();
}
// --- batch processing ---
let pollTimer = null;
async function processSelected() {
const prompt = document.getElementById('batchPromptInput').value.trim();
if (!prompt) { showToast('Enter a prompt first', 'info'); return; }
if (selectedFiles.size === 0) { showToast('No images selected', 'info'); return; }
savePromptHistory(prompt);
const filenames = [...selectedFiles];
document.getElementById('batchProgress').textContent = 'Submitting…';
try {
const r = await fetch(`${API}/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames, prompt, seed: -1, max_area: 0 }),
});
if (!r.ok) { showToast('Batch failed to start', 'info'); document.getElementById('batchProgress').textContent = ''; return; }
const { job_id, total } = await r.json();
showToast(`Processing ${total} image${total !== 1 ? 's' : ''}`, 'info');
pollJob(job_id, total);
} catch (e) {
showToast('API not reachable', 'info');
document.getElementById('batchProgress').textContent = '';
}
}
function pollJob(job_id, total) {
clearTimeout(pollTimer);
pollTimer = setTimeout(async () => {
try {
const r = await fetch(`${API}/batch/${job_id}`);
if (!r.ok) return;
const job = await r.json();
const done = job.done + job.failed;
document.getElementById('batchProgress').textContent = `${done}/${total}`;
if (job.status === 'running') {
pollJob(job_id, total);
} else {
const msg = job.failed > 0
? `Done: ${job.done} ok, ${job.failed} failed`
: `Done: ${job.done} processed`;
document.getElementById('batchProgress').textContent = msg;
showToast(msg, 'success');
loadImages();
}
} catch (e) { /* ignore transient errors */ pollJob(job_id, total); }
}, 2000);
}
// --- prompt history (localStorage) ---
function loadPromptHistory() {
const hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
const dl = document.getElementById('promptHistory');
dl.innerHTML = hist.map(p => `<option value="${p.replace(/"/g, '&quot;')}"></option>`).join('');
}
function savePromptHistory(prompt) {
let hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
hist = [prompt, ...hist.filter(p => p !== prompt)].slice(0, 20);
localStorage.setItem('batchPromptHistory', JSON.stringify(hist));
loadPromptHistory();
}
async function loadCurrentPrompt() {
try {
const r = await fetch(`${API}/config`);
if (r.ok) {
const conf = await r.json();
document.getElementById('promptInput').value = conf.prompt || '';
}
} catch (e) {
// API not reachable yet
}
}
async function setPrompt() {
const prompt = document.getElementById('promptInput').value.trim();
if (!prompt) return;
try {
const r = await fetch(`${API}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
if (r.ok) {
showToast('Prompt updated', 'success');
} else {
showToast('Failed to update prompt', 'info');
}
} catch (e) {
showToast('API not reachable', 'info');
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadCurrentPrompt();
loadPromptHistory();
loadImages();
autoRefreshTimer = setInterval(refreshNow, REFRESH_INTERVAL * 1000);
// Selection click delegation
document.getElementById('gallery').addEventListener('click', e => {
if (!selectionMode) return;
const card = e.target.closest('.image-card');
if (card) toggleFile(card.dataset.name);
});
});
// Handle visibility change - refresh when tab becomes active
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
loadImages();
}
});
</script>
</body>
</html>

65
tour-comfy/poses.md Normal file
View File

@@ -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

View File

@@ -104,5 +104,12 @@
"img_85.png": "f48059d59efd33ec8cce4daa44bbd46d", "img_85.png": "f48059d59efd33ec8cce4daa44bbd46d",
"img_83.png": "501207e02d72776b65d705db9f28a179", "img_83.png": "501207e02d72776b65d705db9f28a179",
"img_86.png": "9e831c994a69ee1e18a6d279d69c072f", "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"
} }

View File

@@ -1,13 +1,9 @@
#!/bin/bash #!/bin/bash
# Launch the folder watcher service. # Launch the folder watcher service.
set -e set -e
API_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # env.sh resolves API_DIR/VENV (and keeps the venv off NTFS).
BASE="$( cd "$API_DIR/.." && pwd )" source "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env.sh"
source "$VENV/bin/activate"
# Try to activate venv if it exists, otherwise use system python
if [ -d "$BASE/venv" ]; then
source "$BASE/venv/bin/activate"
fi
cd "$API_DIR" cd "$API_DIR"
exec python3 watcher.py exec python3 watcher.py

View File

@@ -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,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,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: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

View File

@@ -10,6 +10,13 @@ import sys
import fcntl import fcntl
import re import re
try:
from . import database
from . import embeddings
except ImportError:
import database
import embeddings
# Load configuration # Load configuration
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json") 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_filename = f"{timestamp}_{filename}"
output_path = os.path.join(CONF["output_dir"], output_filename) output_path = os.path.join(CONF["output_dir"], output_filename)
temp_path = input_path + ".tmp.png"
try: try:
logging.info(f"Starting processing for {filename}...") logging.info(f"Starting processing for {filename}...")
cropped_img = crop_to_bbox( cropped_img = crop_to_bbox(
@@ -152,7 +160,6 @@ def process_image(filename):
) )
# Save temporary cropped image for upload # Save temporary cropped image for upload
temp_path = input_path + ".tmp.png"
cropped_img.save(temp_path, format="PNG") cropped_img.save(temp_path, format="PNG")
with open(temp_path, 'rb') as f: with open(temp_path, 'rb') as f:
@@ -169,6 +176,22 @@ def process_image(filename):
with open(output_path, 'wb') as f: with open(output_path, 'wb') as f:
f.write(response.content) f.write(response.content)
logging.info(f"Successfully processed {filename} -> {output_path}") 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): if os.path.exists(temp_path):
os.remove(temp_path) os.remove(temp_path)
return True return True