aa
This commit is contained in:
14
.junie/models/local-devstral24b.json
Normal file
14
.junie/models/local-devstral24b.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:11434/v1/responses",
|
||||
"id": "qwen3-coder:30b",
|
||||
"id": "qwen2.5-coder:32b",
|
||||
"apiType": "OpenAIResponses",
|
||||
"temperature": 0.3,
|
||||
"temperature": 0.1,
|
||||
"primaryModel": {
|
||||
"id": "qwen3-coder:30b",
|
||||
"temperature": 0.3
|
||||
"id": "qwen2.5-coder:32b",
|
||||
"temperature": 0.1
|
||||
},
|
||||
"fasterModel": {
|
||||
"id": "qwen2.5-coder:1.5b"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:11434/v1/responses",
|
||||
"id": "qwen25-coder-32b-64k:latest",
|
||||
"baseUrl": "http://localhost:11440/v1/responses",
|
||||
"id": "qwen3-coder:30b",
|
||||
"apiType": "OpenAIResponses",
|
||||
"temperature": 0.3,
|
||||
"primaryModel": {
|
||||
"id": "qwen25-coder-32b-64k:latest",
|
||||
"temperature": 0.2
|
||||
"id": "qwen3-coder:30b",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"fasterModel": {
|
||||
"id": "qwen25-coder-32b-64k:latest",
|
||||
"temperature": 0.2
|
||||
"id": "qwen2.5-coder:1.5b"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
152
optimize_clips.py
Normal 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
59
position.md
Normal 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
|
||||
|
||||
That’s usually called a front-facing portrait or frontal portrait.
|
||||
|
||||
More specific terms:
|
||||
|
||||
Head-on portrait: person faces directly toward the camera.
|
||||
Straight-on portrait: neutral, camera directly in front.
|
||||
Mugshot-style photo: front-facing, often neutral expression, but has a police/ID connotation.
|
||||
Passport photo or ID photo: front-facing with a plain background and strict framing.
|
||||
|
||||
For normal use, I’d say: a straight-on portrait.
|
||||
|
||||
and with full top till down
|
||||
Een seconde nagedacht
|
||||
|
||||
That’s called a front-facing full-body portrait.
|
||||
|
||||
Also common:
|
||||
|
||||
Full-length portrait: shows the person from head to toe.
|
||||
Straight-on full-body shot: person faces the camera directly.
|
||||
Head-to-toe portrait: informal but clear.
|
||||
|
||||
Best phrase: a straight-on full-body portrait.
|
||||
|
||||
and when isotopic ( i think is the word) facing?
|
||||
|
||||
I think you mean either isometric or three-quarter view. I’ll pin down the naming so you can use the right prompt/photo term.
|
||||
|
||||
Nagedacht gedurende 4s
|
||||
|
||||
You probably mean isometric, but for a person/photo that is usually not the right word.
|
||||
|
||||
Use these terms:
|
||||
|
||||
Three-quarter view
|
||||
Person is turned partly sideways, often about 45°, but still visible from the front. This is the normal portrait term.
|
||||
|
||||
Profile view
|
||||
Person is fully sideways, seen from the side.
|
||||
|
||||
Frontal view / straight-on view
|
||||
Person faces camera directly.
|
||||
|
||||
Isometric view
|
||||
Mostly used for objects, architecture, games, diagrams: a 3D-looking view with equal axes/angles, not a normal human portrait pose.
|
||||
|
||||
For your case, say:
|
||||
|
||||
a full-body three-quarter portrait
|
||||
or
|
||||
a straight-on full-body portrait in three-quarter view if the body is angled but the person still looks at the camera.
|
||||
|
||||
Head-on a full-body three-quarter full-nude-body portrait transparent background
|
||||
Head-on straight-on full-nude-body portrait transparent background
|
||||
Head-on straight-on full-body portrait no background
|
||||
@@ -7,4 +7,5 @@ sentencepiece
|
||||
dashscope
|
||||
torchvision
|
||||
peft
|
||||
torchao==0.11.0
|
||||
torchao==0.11.0
|
||||
rembg[cpu]
|
||||
@@ -1,15 +1,23 @@
|
||||
{
|
||||
"api_url": "http://127.0.0.1:8500/edit",
|
||||
"prompt": "high quality, detailed, female nude",
|
||||
"prompt": "high quality. detailed, female nude, high quality. realistic. detailed. female nude",
|
||||
"base_prompts": [
|
||||
"Head-on a full-body three-quarter full-nude-body female portrait, realistic, transparent background",
|
||||
"Head-on straight-on full-nude-body female portrait, realistic, transparent background",
|
||||
"Head-on straight-on full-body female portrait, realistic, no background",
|
||||
"high quality, full-nude-body, female, masterpiece, realistic, photo, looking at viewer",
|
||||
"high quality, full-nude-body, female, masterpiece, realistic, photo, detailed skin, professional lighting, transparent background",
|
||||
"high quality, full-nude-body, female, masterpiece, realistic, photo, cinematic lighting, dramatic shadows, sharp focus, transparent background"
|
||||
],
|
||||
"seed": -1,
|
||||
"max_area": 655360,
|
||||
"margin": 10,
|
||||
"top_margin": 20,
|
||||
"headroom": 0.05,
|
||||
"poll_interval": 2,
|
||||
"stage_dir": "./tour-comfy/stage",
|
||||
"output_dir": "./tour-comfy/output",
|
||||
"failed_dir": "./tour-comfy/failed",
|
||||
"stage_dir": "/mnt/zim/tour-comfy/stage",
|
||||
"output_dir": "/mnt/zim/tour-comfy/output",
|
||||
"failed_dir": "/mnt/zim/tour-comfy/failed",
|
||||
"processed_file": "./tour-comfy/processed.json",
|
||||
"log_file": "./tour-comfy/watcher.log"
|
||||
}
|
||||
|
||||
101
tour-comfy/database.py
Normal file
101
tour-comfy/database.py
Normal 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()
|
||||
@@ -16,16 +16,31 @@ import uuid
|
||||
import random
|
||||
import copy
|
||||
import threading
|
||||
import csv
|
||||
|
||||
try:
|
||||
from . import database
|
||||
from . import embeddings
|
||||
from . import naming
|
||||
except ImportError:
|
||||
import database
|
||||
import embeddings
|
||||
import naming
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
|
||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import Response
|
||||
from pydantic import BaseModel
|
||||
import shutil
|
||||
import re
|
||||
|
||||
# --- config -----------------------------------------------------------------
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
|
||||
NAMES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "names.json")
|
||||
GROUPS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "groups.json")
|
||||
WD_MODEL = os.environ.get("WD_MODEL", "SmilingWolf/wd-vit-tagger-v3")
|
||||
COMFY = os.environ.get("COMFY_URL", "http://127.0.0.1:8188").rstrip("/")
|
||||
WORKFLOW_PATH = os.environ.get(
|
||||
"WORKFLOW_PATH",
|
||||
@@ -39,6 +54,7 @@ GEN_TIMEOUT = int(os.environ.get("GEN_TIMEOUT", "600")) # seconds per request
|
||||
# Node ids in workflow_qwen_edit.json (kept stable on purpose).
|
||||
NODE_LOADIMAGE = "4"
|
||||
NODE_POSITIVE = "5"
|
||||
NODE_NEGATIVE = "6"
|
||||
NODE_LATENT = "7"
|
||||
NODE_KSAMPLER = "8"
|
||||
NODE_SAVE = "10"
|
||||
@@ -52,7 +68,7 @@ app = FastAPI(title="Qwen-Image-Edit Rapid-AIO API", version="1.0")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["GET", "POST"],
|
||||
allow_methods=["GET", "POST", "DELETE"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@@ -167,8 +183,122 @@ def _comfy_fetch_image(outputs: dict) -> bytes:
|
||||
return r.content
|
||||
|
||||
|
||||
# --- WD tagger (lazy) -------------------------------------------------------
|
||||
|
||||
_tagger = None # (model, transform, labels) once loaded
|
||||
_tagger_lock = threading.Lock()
|
||||
|
||||
|
||||
def _load_tagger():
|
||||
global _tagger
|
||||
if _tagger is not None:
|
||||
return _tagger
|
||||
with _tagger_lock:
|
||||
if _tagger is not None:
|
||||
return _tagger
|
||||
import torch
|
||||
import timm
|
||||
from timm.data import create_transform, resolve_data_config
|
||||
import huggingface_hub
|
||||
|
||||
model = timm.create_model(f"hf_hub:{WD_MODEL}", pretrained=True).eval()
|
||||
if torch.cuda.is_available():
|
||||
model = model.cuda()
|
||||
|
||||
cfg = resolve_data_config(model.pretrained_cfg, model=model)
|
||||
transform = create_transform(**cfg)
|
||||
|
||||
lpath = huggingface_hub.hf_hub_download(WD_MODEL, "selected_tags.csv")
|
||||
with open(lpath, newline="") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
# category 0=general 4=character 9=rating
|
||||
labels = [(r["name"], int(r.get("category", 9))) for r in rows]
|
||||
|
||||
_tagger = (model, transform, labels)
|
||||
return _tagger
|
||||
|
||||
|
||||
def _run_tagger(pil_img: Image.Image, threshold: float = 0.35):
|
||||
import torch
|
||||
model, transform, labels = _load_tagger()
|
||||
tensor = transform(pil_img.convert("RGB")).unsqueeze(0)
|
||||
if torch.cuda.is_available():
|
||||
tensor = tensor.cuda()
|
||||
with torch.no_grad():
|
||||
scores = torch.sigmoid(model(tensor))[0].cpu().tolist()
|
||||
tags = [
|
||||
{"tag": name, "score": round(score, 3), "cat": cat}
|
||||
for (name, cat), score in zip(labels, scores)
|
||||
if score >= threshold
|
||||
]
|
||||
tags.sort(key=lambda x: -x["score"])
|
||||
return tags
|
||||
|
||||
|
||||
def _tags_to_name(tags: list, max_tags: int = 8) -> str:
|
||||
content = [t["tag"] for t in tags if t["cat"] in (0, 4)][:max_tags]
|
||||
return " ".join(content).replace("_", " ")
|
||||
|
||||
|
||||
def _load_json(path: str) -> dict:
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def _save_json(path: str, data: dict):
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def _apply_transparency(png_bytes: bytes) -> bytes:
|
||||
"""Use rembg to remove background and return PNG bytes with Alpha channel."""
|
||||
try:
|
||||
from rembg import remove
|
||||
import io
|
||||
from PIL import Image
|
||||
img = Image.open(io.BytesIO(png_bytes))
|
||||
# rembg works best on RGB
|
||||
if img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
out = remove(img)
|
||||
buf = io.BytesIO()
|
||||
out.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
except Exception as e:
|
||||
print(f"Error in transparency post-processing: {e}")
|
||||
return png_bytes
|
||||
|
||||
|
||||
# --- pipeline helper ---------------------------------------------------------
|
||||
|
||||
def _load_poses():
|
||||
poses_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "poses.md")
|
||||
if not os.path.exists(poses_path):
|
||||
return {}
|
||||
|
||||
poses = {}
|
||||
current_pose = None
|
||||
current_desc = []
|
||||
|
||||
with open(poses_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("# "):
|
||||
if current_pose:
|
||||
poses[current_pose] = " ".join(current_desc).strip()
|
||||
current_pose = line[2:].rstrip(":").strip()
|
||||
current_desc = []
|
||||
elif line and current_pose:
|
||||
current_desc.append(line)
|
||||
|
||||
if current_pose:
|
||||
poses[current_pose] = " ".join(current_desc).strip()
|
||||
|
||||
return poses
|
||||
|
||||
|
||||
def _run_pipeline(
|
||||
pil: Image.Image,
|
||||
prompt: str,
|
||||
@@ -189,6 +319,12 @@ def _run_pipeline(
|
||||
graph = copy.deepcopy(BASE_WORKFLOW)
|
||||
graph[NODE_LOADIMAGE]["inputs"]["image"] = stored
|
||||
graph[NODE_POSITIVE]["inputs"]["prompt"] = prompt
|
||||
|
||||
# Transparency detection
|
||||
is_transparent = any(kw in prompt.lower() for kw in ["transparent", "no background", "remove background", "alpha channel"])
|
||||
if is_transparent:
|
||||
graph[NODE_NEGATIVE]["inputs"]["prompt"] = "checkerboard, grid, pattern, texture, background details"
|
||||
|
||||
graph[NODE_LATENT]["inputs"]["width"] = w
|
||||
graph[NODE_LATENT]["inputs"]["height"] = h
|
||||
ks = graph[NODE_KSAMPLER]["inputs"]
|
||||
@@ -196,7 +332,12 @@ def _run_pipeline(
|
||||
client_id = uuid.uuid4().hex
|
||||
prompt_id = _comfy_queue(graph, client_id)
|
||||
outputs = _comfy_wait(prompt_id, time.time() + GEN_TIMEOUT)
|
||||
return _comfy_fetch_image(outputs)
|
||||
png_bytes = _comfy_fetch_image(outputs)
|
||||
|
||||
if is_transparent:
|
||||
png_bytes = _apply_transparency(png_bytes)
|
||||
|
||||
return png_bytes
|
||||
|
||||
|
||||
# --- batch state -------------------------------------------------------------
|
||||
@@ -213,20 +354,76 @@ def _load_output_dir() -> str:
|
||||
return d
|
||||
|
||||
|
||||
def _batch_worker(job_id: str, filenames: list, prompt: str, seed: int, max_area: int):
|
||||
def _move_to_trash(filepath: str):
|
||||
if not filepath or not os.path.exists(filepath):
|
||||
return
|
||||
output_dir = _load_output_dir()
|
||||
trash_dir = os.path.join(output_dir, ".trash")
|
||||
os.makedirs(trash_dir, exist_ok=True)
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
trash_path = os.path.join(trash_dir, f"{ts}_{filename}")
|
||||
|
||||
try:
|
||||
shutil.move(filepath, trash_path)
|
||||
except Exception as e:
|
||||
print(f"Error moving {filepath} to trash: {e}")
|
||||
|
||||
|
||||
def _batch_worker(job_id: str, filenames: list, prompts: list[str], seed: int, max_area: int, group_id: str | None = None):
|
||||
output_dir = _load_output_dir()
|
||||
for fname in filenames:
|
||||
# If no group_id provided, try to inherit from source
|
||||
actual_gid = group_id
|
||||
if not actual_gid:
|
||||
try:
|
||||
person = database.get_person(fname)
|
||||
if person and person[1]: # group_id is at index 1
|
||||
actual_gid = person[1]
|
||||
else:
|
||||
# Create a new group for this standalone image
|
||||
actual_gid = naming.get_base_name(fname)
|
||||
# Update source image to join this group
|
||||
database.upsert_person(fname, group_id=actual_gid)
|
||||
except Exception as e:
|
||||
print(f"Error determining group for {fname}: {e}")
|
||||
|
||||
fpath = os.path.join(output_dir, fname)
|
||||
if not os.path.exists(fpath):
|
||||
jobs[job_id]["failed"] += len(prompts)
|
||||
continue
|
||||
|
||||
try:
|
||||
pil = Image.open(fpath).convert("RGB")
|
||||
png = _run_pipeline(pil, prompt, seed, max_area)
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
out_name = f"{ts}_{fname}"
|
||||
with open(os.path.join(output_dir, out_name), "wb") as f:
|
||||
f.write(png)
|
||||
jobs[job_id]["done"] += 1
|
||||
for prompt in prompts:
|
||||
try:
|
||||
png = _run_pipeline(pil, prompt, seed, max_area)
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# Clean filename to avoid nested timestamps
|
||||
clean_fname = naming.get_base_name(fname)
|
||||
out_name = f"{ts}_{clean_fname}"
|
||||
out_path = os.path.join(output_dir, out_name)
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(png)
|
||||
|
||||
# Register in DB
|
||||
try:
|
||||
embedding = embeddings.generate_embedding(out_path)
|
||||
database.upsert_person(out_name, filepath=out_path, embedding=embedding, group_id=actual_gid)
|
||||
except Exception as db_err:
|
||||
print(f"Database error in batch worker: {db_err}")
|
||||
|
||||
jobs[job_id]["done"] += 1
|
||||
except Exception as e:
|
||||
print(f"Error in batch for {fname} with prompt '{prompt}': {e}")
|
||||
jobs[job_id]["failed"] += 1
|
||||
except Exception as e:
|
||||
jobs[job_id]["failed"] += 1
|
||||
print(f"Error opening {fname}: {e}")
|
||||
jobs[job_id]["failed"] += len(prompts)
|
||||
|
||||
jobs[job_id]["status"] = "done"
|
||||
|
||||
|
||||
@@ -258,22 +455,31 @@ def update_config(update: ConfigUpdate):
|
||||
|
||||
class BatchRequest(BaseModel):
|
||||
filenames: list[str]
|
||||
prompt: str
|
||||
prompt: str | list[str]
|
||||
seed: int = -1
|
||||
max_area: int = 0
|
||||
group_id: str | None = None
|
||||
|
||||
|
||||
@app.post("/batch")
|
||||
def start_batch(req: BatchRequest):
|
||||
prompts = [req.prompt] if isinstance(req.prompt, str) else req.prompt
|
||||
total_tasks = len(req.filenames) * len(prompts)
|
||||
|
||||
job_id = uuid.uuid4().hex[:8]
|
||||
jobs[job_id] = {"status": "running", "total": len(req.filenames), "done": 0, "failed": 0}
|
||||
jobs[job_id] = {"status": "running", "total": total_tasks, "done": 0, "failed": 0}
|
||||
t = threading.Thread(
|
||||
target=_batch_worker,
|
||||
args=(job_id, req.filenames, req.prompt, req.seed, req.max_area),
|
||||
args=(job_id, req.filenames, prompts, req.seed, req.max_area, req.group_id),
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
return {"job_id": job_id, "total": len(req.filenames)}
|
||||
return {"job_id": job_id, "total": total_tasks}
|
||||
|
||||
|
||||
@app.get("/poses")
|
||||
def get_poses():
|
||||
return _load_poses()
|
||||
|
||||
|
||||
@app.get("/batch/{job_id}")
|
||||
@@ -283,6 +489,186 @@ def get_batch(job_id: str):
|
||||
return jobs[job_id]
|
||||
|
||||
|
||||
@app.get("/images")
|
||||
def list_images():
|
||||
output_dir = _load_output_dir()
|
||||
extensions = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg')
|
||||
try:
|
||||
# Try to get from DB first
|
||||
try:
|
||||
persons = database.list_persons()
|
||||
# persons is (filename, name, group_id, clip_description)
|
||||
db_images = []
|
||||
for p in persons:
|
||||
db_images.append({
|
||||
"filename": p[0],
|
||||
"name": p[1],
|
||||
"group_id": p[2],
|
||||
"clip_description": p[3]
|
||||
})
|
||||
# Still sort by mtime for consistency with filesystem
|
||||
db_images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])) if os.path.exists(os.path.join(output_dir, x["filename"])) else 0, reverse=True)
|
||||
return {"images": db_images}
|
||||
except Exception as db_err:
|
||||
print(f"DB error in list_images: {db_err}")
|
||||
# Fallback to filesystem
|
||||
files = [f for f in os.listdir(output_dir) if f.lower().endswith(extensions)]
|
||||
files.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
||||
return {"images": [{"filename": f} for f in files]}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# --- tagging routes ----------------------------------------------------------
|
||||
|
||||
class TagRequest(BaseModel):
|
||||
filename: str
|
||||
threshold: float = 0.35
|
||||
max_tags: int = 8
|
||||
group_id: str | None = None
|
||||
|
||||
|
||||
@app.post("/tag")
|
||||
def tag_image(req: TagRequest):
|
||||
output_dir = _load_output_dir()
|
||||
fpath = os.path.join(output_dir, req.filename)
|
||||
if not os.path.exists(fpath):
|
||||
raise HTTPException(404, "File not found in output dir")
|
||||
try:
|
||||
pil = Image.open(fpath)
|
||||
tags = _run_tagger(pil, req.threshold)
|
||||
clip_desc = _tags_to_name(tags, req.max_tags)
|
||||
auto_name = naming.generate_associative_name(tags)
|
||||
|
||||
# Save to DB
|
||||
try:
|
||||
embedding = embeddings.generate_embedding(fpath)
|
||||
database.upsert_person(req.filename, filepath=fpath, name=auto_name, clip_description=clip_desc, tags=tags, embedding=embedding, group_id=req.group_id)
|
||||
except Exception as db_err:
|
||||
print(f"Database error during tag: {db_err}")
|
||||
|
||||
# Legacy fallback
|
||||
try:
|
||||
names = _load_json(NAMES_PATH)
|
||||
names[req.filename] = clip_desc
|
||||
_save_json(NAMES_PATH, names)
|
||||
except: pass
|
||||
|
||||
return {"filename": req.filename, "clip_description": clip_desc, "tags": tags[:30]}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.get("/names")
|
||||
def get_names():
|
||||
try:
|
||||
persons = database.list_persons()
|
||||
return {p[0]: p[1] for p in persons if p[1]}
|
||||
except:
|
||||
return _load_json(NAMES_PATH)
|
||||
|
||||
|
||||
@app.post("/names/{filename}")
|
||||
def set_name(filename: str, body: dict):
|
||||
name = body.get("name", "")
|
||||
try:
|
||||
database.upsert_person(filename, name=name)
|
||||
except Exception as db_err:
|
||||
print(f"Database error in set_name: {db_err}")
|
||||
|
||||
# Legacy fallback
|
||||
try:
|
||||
names = _load_json(NAMES_PATH)
|
||||
names[filename] = name
|
||||
_save_json(NAMES_PATH, names)
|
||||
except: pass
|
||||
|
||||
return {"filename": filename, "name": name}
|
||||
|
||||
|
||||
# --- group routes ------------------------------------------------------------
|
||||
|
||||
@app.get("/groups")
|
||||
def get_groups():
|
||||
try:
|
||||
persons = database.list_persons()
|
||||
return {p[0]: p[2] for p in persons if p[2]}
|
||||
except:
|
||||
return _load_json(GROUPS_PATH)
|
||||
|
||||
|
||||
class MergeRequest(BaseModel):
|
||||
filenames: list[str]
|
||||
group_id: str | None = None
|
||||
|
||||
|
||||
@app.post("/groups/merge")
|
||||
def merge_groups(req: MergeRequest):
|
||||
gid = req.group_id or f"cg_{uuid.uuid4().hex[:8]}"
|
||||
for fname in req.filenames:
|
||||
try:
|
||||
database.upsert_person(fname, group_id=gid)
|
||||
except Exception as db_err:
|
||||
print(f"Database error in merge: {db_err}")
|
||||
|
||||
# Legacy fallback
|
||||
try:
|
||||
groups = _load_json(GROUPS_PATH)
|
||||
for fname in req.filenames:
|
||||
groups[fname] = gid
|
||||
_save_json(GROUPS_PATH, groups)
|
||||
except: pass
|
||||
|
||||
return {"group_id": gid, "files": req.filenames}
|
||||
|
||||
|
||||
class ExtractRequest(BaseModel):
|
||||
filename: str
|
||||
|
||||
|
||||
@app.post("/groups/extract")
|
||||
def extract_from_group(req: ExtractRequest):
|
||||
gid = f"solo:{req.filename}"
|
||||
try:
|
||||
database.upsert_person(req.filename, group_id=gid)
|
||||
except Exception as db_err:
|
||||
print(f"Database error in extract: {db_err}")
|
||||
|
||||
# Legacy fallback
|
||||
try:
|
||||
groups = _load_json(GROUPS_PATH)
|
||||
groups[req.filename] = gid
|
||||
_save_json(GROUPS_PATH, groups)
|
||||
except: pass
|
||||
|
||||
return {"filename": req.filename}
|
||||
|
||||
|
||||
@app.get("/similar/{filename}")
|
||||
def get_similar(filename: str, limit: int = 10):
|
||||
person = database.get_person(filename)
|
||||
if not person or person[3] is None:
|
||||
raise HTTPException(404, "Image or embedding not found")
|
||||
|
||||
embedding = person[3]
|
||||
results = database.search_similar(embedding, limit=limit)
|
||||
|
||||
similar = []
|
||||
for r in results:
|
||||
# Avoid returning the same image as the most similar
|
||||
if r[0] == filename:
|
||||
continue
|
||||
similar.append({
|
||||
"filename": r[0],
|
||||
"name": r[1],
|
||||
"group_id": r[2],
|
||||
"clip_description": r[3],
|
||||
"distance": float(r[4])
|
||||
})
|
||||
|
||||
return {"filename": filename, "similar": similar}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
try:
|
||||
@@ -292,6 +678,119 @@ def health():
|
||||
raise HTTPException(503, f"ComfyUI unreachable at {COMFY}: {e}")
|
||||
|
||||
|
||||
def _crop_to_bbox(pil_img: Image.Image, margin: int = 20, top_margin: int = 20, headroom: float = 0.05) -> Image.Image:
|
||||
if pil_img.mode != 'RGBA':
|
||||
return pil_img
|
||||
|
||||
alpha = pil_img.split()[-1]
|
||||
bbox = alpha.getbbox()
|
||||
if not bbox:
|
||||
return pil_img
|
||||
|
||||
left, upper, right, lower = bbox
|
||||
left = max(0, left - margin)
|
||||
upper = max(0, upper - top_margin)
|
||||
right = min(pil_img.width, right + margin)
|
||||
lower = min(pil_img.height, lower + margin)
|
||||
|
||||
cropped = pil_img.crop((left, upper, right, lower))
|
||||
if headroom > 0:
|
||||
h_px = int(cropped.height * headroom)
|
||||
if h_px > 0:
|
||||
new_img = Image.new("RGBA", (cropped.width, cropped.height + h_px), (0, 0, 0, 0))
|
||||
new_img.paste(cropped, (0, h_px))
|
||||
return new_img
|
||||
return cropped
|
||||
|
||||
|
||||
def _process_upload(file_path: str, filename: str, prompts: list[str], name: str | None = None, group_id: str | None = None):
|
||||
output_dir = _load_output_dir()
|
||||
try:
|
||||
pil = Image.open(file_path)
|
||||
|
||||
# 1. CLIP tag the source
|
||||
tags = _run_tagger(pil.convert("RGB"))
|
||||
clip_desc = _tags_to_name(tags)
|
||||
auto_name = name or naming.generate_associative_name(tags)
|
||||
|
||||
# 2. Embedding for source
|
||||
embedding = embeddings.generate_embedding(file_path)
|
||||
|
||||
# 3. Register source in DB (optional, but good for tracking)
|
||||
# We'll use the original filename or a timestamped one
|
||||
database.upsert_person(filename, filepath=file_path, name=auto_name, clip_description=clip_desc, tags=tags, embedding=embedding, group_id=group_id)
|
||||
|
||||
# 4. Default behavior: Crop if needed
|
||||
# We'll use default values from watcher
|
||||
cropped_pil = _crop_to_bbox(pil)
|
||||
|
||||
# 5. Run prompts
|
||||
for i, prompt in enumerate(prompts):
|
||||
try:
|
||||
# Use RGB for pipeline
|
||||
png = _run_pipeline(cropped_pil.convert("RGB"), prompt)
|
||||
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
out_name = f"{ts}_{i}_{filename}"
|
||||
if not out_name.lower().endswith(".png"):
|
||||
out_name += ".png"
|
||||
out_path = os.path.join(output_dir, out_name)
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(png)
|
||||
|
||||
# Register output in DB
|
||||
out_embedding = embeddings.generate_embedding(out_path)
|
||||
database.upsert_person(out_name, filepath=out_path, name=auto_name, clip_description=clip_desc, embedding=out_embedding, group_id=group_id)
|
||||
except Exception as e:
|
||||
print(f"Error processing prompt '{prompt}' for {filename}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in _process_upload for {filename}: {e}")
|
||||
|
||||
|
||||
@app.post("/upload")
|
||||
def upload_image(
|
||||
background_tasks: BackgroundTasks,
|
||||
image: UploadFile = File(...),
|
||||
prompts: str = Form(""),
|
||||
name: str = Form(None),
|
||||
):
|
||||
# Load config to get output_dir (we use output_dir for UI uploads to avoid watcher conflict)
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
conf = json.load(f)
|
||||
output_dir = _load_output_dir()
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||
safe_filename = re.sub(r'[^a-zA-Z0-9_.-]', '_', image.filename)
|
||||
# Ensure extension
|
||||
if not safe_filename.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
|
||||
safe_filename += ".png"
|
||||
|
||||
filename = f"{ts}_{safe_filename}"
|
||||
file_path = os.path.join(output_dir, filename)
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
shutil.copyfileobj(image.file, f)
|
||||
|
||||
prompt_list = [p.strip() for p in prompts.split(",") if p.strip()]
|
||||
|
||||
# Add base-set prompts if defined in config
|
||||
base_prompts = conf.get("base_prompts", [])
|
||||
if isinstance(base_prompts, list):
|
||||
prompt_list.extend(base_prompts)
|
||||
|
||||
if not prompt_list:
|
||||
# Use default prompt from config
|
||||
prompt_list = [conf.get("prompt", "high quality, masterpiece")]
|
||||
|
||||
group_id = naming.get_base_name(filename)
|
||||
background_tasks.add_task(_process_upload, file_path, filename, prompt_list, name, group_id)
|
||||
|
||||
return {"status": "processing", "filename": filename, "group_id": group_id, "prompts": prompt_list}
|
||||
|
||||
|
||||
@app.post("/edit")
|
||||
async def edit(
|
||||
image: UploadFile = File(...),
|
||||
@@ -313,6 +812,64 @@ async def edit(
|
||||
return Response(content=png, media_type="image/png")
|
||||
|
||||
|
||||
@app.delete("/images/{filename}")
|
||||
def delete_image(filename: str):
|
||||
person = database.get_person(filename)
|
||||
if person and person[5] and os.path.exists(person[5]):
|
||||
_move_to_trash(person[5])
|
||||
|
||||
database.delete_person(filename)
|
||||
return {"status": "deleted", "filename": filename}
|
||||
|
||||
|
||||
@app.delete("/groups/{group_id}")
|
||||
def delete_group(group_id: str):
|
||||
files = database.get_group_files(group_id)
|
||||
for filename, filepath in files:
|
||||
if filepath and os.path.exists(filepath):
|
||||
_move_to_trash(filepath)
|
||||
|
||||
database.delete_group(group_id)
|
||||
return {"status": "deleted", "group_id": group_id}
|
||||
|
||||
|
||||
@app.post("/remove-background/{filename}")
|
||||
def remove_background(filename: str):
|
||||
person = database.get_person(filename)
|
||||
if not person or not person[5] or not os.path.exists(person[5]):
|
||||
raise HTTPException(404, "Image file not found")
|
||||
|
||||
path = person[5]
|
||||
with open(path, "rb") as f:
|
||||
png_bytes = f.read()
|
||||
|
||||
transparent_png = _apply_transparency(png_bytes)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(transparent_png)
|
||||
|
||||
return {"status": "success", "filename": filename}
|
||||
|
||||
|
||||
@app.post("/remove-background/group/{group_id}")
|
||||
def remove_background_group(group_id: str, background_tasks: BackgroundTasks):
|
||||
def _bg_task():
|
||||
files = database.get_group_files(group_id)
|
||||
for filename, filepath in files:
|
||||
if filepath and os.path.exists(filepath):
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
png_bytes = f.read()
|
||||
transparent_png = _apply_transparency(png_bytes)
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(transparent_png)
|
||||
except Exception as e:
|
||||
print(f"Error removing background for {filename}: {e}")
|
||||
|
||||
background_tasks.add_task(_bg_task)
|
||||
return {"status": "processing", "group_id": group_id}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host=os.environ.get("HOST", "0.0.0.0"),
|
||||
|
||||
30
tour-comfy/embeddings.py
Normal file
30
tour-comfy/embeddings.py
Normal 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
|
||||
@@ -19,7 +19,12 @@ COMFY="$BASE/ComfyUI"
|
||||
_basefs="$(stat -f -c %T "$BASE" 2>/dev/null || echo unknown)"
|
||||
case "$_basefs" in
|
||||
fuseblk|ntfs|ntfs3|exfat|vfat|msdos|9p|cifs|smb*)
|
||||
VENV="${COMFY_VENV:-$HOME/comfyui-venv}" ;; # NTFS-ish BASE -> venv on home
|
||||
VENV="${COMFY_VENV:-/home/mike/comfyui/venv}" ;; # NTFS-ish BASE -> venv on home
|
||||
*)
|
||||
VENV="${COMFY_VENV:-$BASE/venv}" ;; # native fs -> venv beside code
|
||||
if [ -d "/home/mike/comfyui/venv" ]; then
|
||||
VENV="${COMFY_VENV:-/home/mike/comfyui/venv}"
|
||||
else
|
||||
VENV="${COMFY_VENV:-$BASE/venv}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
99
tour-comfy/groups.json
Normal file
99
tour-comfy/groups.json
Normal 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
10
tour-comfy/names.json
Normal 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
96
tour-comfy/naming.py
Normal 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)
|
||||
@@ -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, '"')}"></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
65
tour-comfy/poses.md
Normal 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
|
||||
@@ -104,5 +104,12 @@
|
||||
"img_85.png": "f48059d59efd33ec8cce4daa44bbd46d",
|
||||
"img_83.png": "501207e02d72776b65d705db9f28a179",
|
||||
"img_86.png": "9e831c994a69ee1e18a6d279d69c072f",
|
||||
"img_87.png": "4ce165b53df962d9e371124bfdec64bd"
|
||||
"img_87.png": "4ce165b53df962d9e371124bfdec64bd",
|
||||
"img_88.png": "7a22effc45850ecd53670827e0608e97",
|
||||
"20260618_045234_test_clipboard.png": "9cd53a2f405bebbac4a21dded0f12e34",
|
||||
"20260618_045450_test_clipboard.png": "9cd53a2f405bebbac4a21dded0f12e34",
|
||||
"20260618_045549_test_clipboard.png": "9cd53a2f405bebbac4a21dded0f12e34",
|
||||
"20260618_050846_img_4.png": "b18d89c79c74df97f1a7f106ece9d946",
|
||||
"20260618_051435_test_group.png": "bb550daac9e09ead5487e984b18d4b13",
|
||||
"20260618_051426_test_group.png": "bb550daac9e09ead5487e984b18d4b13"
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Launch the folder watcher service.
|
||||
set -e
|
||||
API_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
BASE="$( cd "$API_DIR/.." && pwd )"
|
||||
|
||||
# Try to activate venv if it exists, otherwise use system python
|
||||
if [ -d "$BASE/venv" ]; then
|
||||
source "$BASE/venv/bin/activate"
|
||||
fi
|
||||
|
||||
# env.sh resolves API_DIR/VENV (and keeps the venv off NTFS).
|
||||
source "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env.sh"
|
||||
source "$VENV/bin/activate"
|
||||
cd "$API_DIR"
|
||||
|
||||
exec python3 watcher.py
|
||||
|
||||
@@ -1137,3 +1137,215 @@ requests.exceptions.ConnectionError: HTTPConnectionPool(host='192.168.1.171', po
|
||||
2026-06-18 00:02:13,359 - INFO - Calling API for img_88.png -> 20260618_000213_img_88.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 00:02:13,515 - ERROR - API failed for img_88.png: 500 - Internal Server Error
|
||||
2026-06-18 00:02:13,516 - INFO - Flagging image img_88.png (moving to failed directory as 20260618_000213_img_88.png)
|
||||
2026-06-18 00:09:14,677 - INFO - Starting processing for img_88.png...
|
||||
2026-06-18 00:09:14,679 - INFO - Cropping /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage/img_88.png to (23, 31, 142, 284) (margin=10, top_margin=20)
|
||||
2026-06-18 00:09:14,679 - INFO - Adding 12px headroom to /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage/img_88.png
|
||||
2026-06-18 00:09:14,681 - INFO - Calling API for img_88.png -> 20260618_000914_img_88.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 00:10:38,474 - INFO - Successfully processed img_88.png -> /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output/20260618_000914_img_88.png
|
||||
2026-06-18 00:10:38,482 - INFO - Updated /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output/car.html with 128 images
|
||||
2026-06-18 00:19:43,853 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage...
|
||||
2026-06-18 00:19:43,853 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output
|
||||
2026-06-18 00:19:43,853 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 00:55:00,980 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage...
|
||||
2026-06-18 00:55:00,980 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output
|
||||
2026-06-18 00:55:00,980 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 01:26:22,512 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage...
|
||||
2026-06-18 01:26:22,513 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output
|
||||
2026-06-18 01:26:22,513 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 01:41:54,677 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage...
|
||||
2026-06-18 01:41:54,677 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output
|
||||
2026-06-18 01:41:54,677 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 02:47:35,417 - INFO - Watcher started. Monitoring /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/stage...
|
||||
2026-06-18 02:47:35,417 - INFO - Output directory: /home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/output
|
||||
2026-06-18 02:47:35,417 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 04:44:15,381 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 04:44:15,381 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 04:44:15,381 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 04:45:02,791 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 04:45:02,791 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 04:45:02,791 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 04:52:24,025 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 04:52:24,025 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 04:52:24,025 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 04:54:50,113 - INFO - Starting processing for 20260618_045234_test_clipboard.png...
|
||||
2026-06-18 04:54:50,157 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045234_test_clipboard.png is mode RGB, not RGBA. Skipping crop.
|
||||
2026-06-18 04:54:50,191 - INFO - Calling API for 20260618_045234_test_clipboard.png -> 20260618_045450_20260618_045234_test_clipboard.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 04:55:49,039 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 04:55:49,039 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 04:55:49,039 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 04:56:29,059 - INFO - Starting processing for 20260618_045234_test_clipboard.png...
|
||||
2026-06-18 04:56:29,061 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045234_test_clipboard.png is mode RGB, not RGBA. Skipping crop.
|
||||
2026-06-18 04:56:29,063 - INFO - Calling API for 20260618_045234_test_clipboard.png -> 20260618_045629_20260618_045234_test_clipboard.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 04:56:40,714 - INFO - Successfully processed 20260618_045234_test_clipboard.png -> /mnt/zim/tour-comfy/output/20260618_045629_20260618_045234_test_clipboard.png
|
||||
2026-06-18 04:56:40,749 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14
|
||||
2026-06-18 04:56:40,749 - INFO - Loaded built-in ViT-H-14 model config.
|
||||
2026-06-18 04:56:40,963 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found"
|
||||
2026-06-18 04:56:40,964 - INFO - Instantiating model architecture: CLIP
|
||||
2026-06-18 04:56:44,506 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors
|
||||
2026-06-18 04:56:44,708 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0}
|
||||
2026-06-18 04:56:44,708 - INFO - Model ViT-H-14 creation process complete.
|
||||
2026-06-18 04:56:46,626 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 203 images
|
||||
2026-06-18 04:56:56,628 - INFO - Starting processing for 20260618_045450_test_clipboard.png...
|
||||
2026-06-18 04:56:56,628 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045450_test_clipboard.png is mode RGB, not RGBA. Skipping crop.
|
||||
2026-06-18 04:56:56,628 - INFO - Calling API for 20260618_045450_test_clipboard.png -> 20260618_045656_20260618_045450_test_clipboard.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 04:57:12,385 - INFO - Successfully processed 20260618_045450_test_clipboard.png -> /mnt/zim/tour-comfy/output/20260618_045656_20260618_045450_test_clipboard.png
|
||||
2026-06-18 04:57:12,648 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 206 images
|
||||
2026-06-18 04:58:31,683 - INFO - Starting processing for 20260618_045549_test_clipboard.png...
|
||||
2026-06-18 04:58:31,683 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_045549_test_clipboard.png is mode RGB, not RGBA. Skipping crop.
|
||||
2026-06-18 04:58:31,683 - INFO - Calling API for 20260618_045549_test_clipboard.png -> 20260618_045831_20260618_045549_test_clipboard.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 04:58:40,721 - INFO - Successfully processed 20260618_045549_test_clipboard.png -> /mnt/zim/tour-comfy/output/20260618_045831_20260618_045549_test_clipboard.png
|
||||
2026-06-18 04:58:40,879 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 210 images
|
||||
2026-06-18 05:00:23,081 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 05:00:23,082 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 05:00:23,082 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 05:09:29,285 - INFO - Starting processing for 20260618_050846_img_4.png...
|
||||
2026-06-18 05:09:29,287 - INFO - Cropping /mnt/zim/tour-comfy/stage/20260618_050846_img_4.png to (20, 51, 138, 345) (margin=10, top_margin=20)
|
||||
2026-06-18 05:09:29,288 - INFO - Adding 14px headroom to /mnt/zim/tour-comfy/stage/20260618_050846_img_4.png
|
||||
2026-06-18 05:09:29,292 - INFO - Calling API for 20260618_050846_img_4.png -> 20260618_050929_20260618_050846_img_4.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 05:09:43,897 - INFO - Successfully processed 20260618_050846_img_4.png -> /mnt/zim/tour-comfy/output/20260618_050929_20260618_050846_img_4.png
|
||||
2026-06-18 05:09:43,934 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14
|
||||
2026-06-18 05:09:43,934 - INFO - Loaded built-in ViT-H-14 model config.
|
||||
2026-06-18 05:09:44,116 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found"
|
||||
2026-06-18 05:09:44,117 - INFO - Instantiating model architecture: CLIP
|
||||
2026-06-18 05:09:47,742 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors
|
||||
2026-06-18 05:09:48,828 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0}
|
||||
2026-06-18 05:09:48,828 - INFO - Model ViT-H-14 creation process complete.
|
||||
2026-06-18 05:09:50,791 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 196 images
|
||||
2026-06-18 05:14:14,718 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 05:14:14,718 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 05:14:14,718 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 05:16:29,765 - INFO - Starting processing for 20260618_051435_test_group.png...
|
||||
2026-06-18 05:16:29,766 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_051435_test_group.png is mode RGB, not RGBA. Skipping crop.
|
||||
2026-06-18 05:16:29,768 - INFO - Calling API for 20260618_051435_test_group.png -> 20260618_051629_20260618_051435_test_group.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 05:16:58,905 - INFO - Successfully processed 20260618_051435_test_group.png -> /mnt/zim/tour-comfy/output/20260618_051629_20260618_051435_test_group.png
|
||||
2026-06-18 05:16:58,941 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14
|
||||
2026-06-18 05:16:58,942 - INFO - Loaded built-in ViT-H-14 model config.
|
||||
2026-06-18 05:16:59,127 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found"
|
||||
2026-06-18 05:16:59,128 - INFO - Instantiating model architecture: CLIP
|
||||
2026-06-18 05:17:02,756 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors
|
||||
2026-06-18 05:17:02,958 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0}
|
||||
2026-06-18 05:17:02,958 - INFO - Model ViT-H-14 creation process complete.
|
||||
2026-06-18 05:17:04,921 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 216 images
|
||||
2026-06-18 05:17:31,937 - INFO - Starting processing for 20260618_051415_test_group.png...
|
||||
2026-06-18 05:17:31,937 - ERROR - Failed to crop /mnt/zim/tour-comfy/stage/20260618_051415_test_group.png: broken PNG file (chunk b'END\xae')
|
||||
2026-06-18 05:17:31,937 - ERROR - Error processing 20260618_051415_test_group.png: broken PNG file (chunk b'END\xae')
|
||||
Traceback (most recent call last):
|
||||
File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 154, in process_image
|
||||
cropped_img = crop_to_bbox(
|
||||
input_path,
|
||||
...<2 lines>...
|
||||
headroom=CONF.get("headroom", 0.0)
|
||||
)
|
||||
File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 85, in crop_to_bbox
|
||||
alpha = img.split()[-1]
|
||||
~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/Image.py", line 2793, in split
|
||||
self.load()
|
||||
~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/ImageFile.py", line 392, in load
|
||||
s = read(read_bytes)
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 999, in load_read
|
||||
cid, pos, length = self.png.read()
|
||||
~~~~~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 184, in read
|
||||
raise SyntaxError(msg)
|
||||
SyntaxError: broken PNG file (chunk b'END\xae')
|
||||
2026-06-18 05:17:31,942 - ERROR - Main loop error: cannot access local variable 'temp_path' where it is not associated with a value
|
||||
2026-06-18 05:18:28,964 - INFO - Starting processing for 20260618_051415_test_group.png...
|
||||
2026-06-18 05:18:28,964 - ERROR - Failed to crop /mnt/zim/tour-comfy/stage/20260618_051415_test_group.png: broken PNG file (chunk b'END\xae')
|
||||
2026-06-18 05:18:28,964 - ERROR - Error processing 20260618_051415_test_group.png: broken PNG file (chunk b'END\xae')
|
||||
Traceback (most recent call last):
|
||||
File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 154, in process_image
|
||||
cropped_img = crop_to_bbox(
|
||||
input_path,
|
||||
...<2 lines>...
|
||||
headroom=CONF.get("headroom", 0.0)
|
||||
)
|
||||
File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 85, in crop_to_bbox
|
||||
alpha = img.split()[-1]
|
||||
~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/Image.py", line 2793, in split
|
||||
self.load()
|
||||
~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/ImageFile.py", line 392, in load
|
||||
s = read(read_bytes)
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 999, in load_read
|
||||
cid, pos, length = self.png.read()
|
||||
~~~~~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 184, in read
|
||||
raise SyntaxError(msg)
|
||||
SyntaxError: broken PNG file (chunk b'END\xae')
|
||||
2026-06-18 05:18:28,965 - ERROR - Main loop error: cannot access local variable 'temp_path' where it is not associated with a value
|
||||
2026-06-18 05:19:00,707 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 05:19:00,707 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 05:19:00,707 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 05:19:12,911 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 05:19:12,911 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 05:19:12,911 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 05:20:07,936 - INFO - Starting processing for 20260618_051415_test_group.png...
|
||||
2026-06-18 05:20:07,937 - ERROR - Failed to crop /mnt/zim/tour-comfy/stage/20260618_051415_test_group.png: broken PNG file (chunk b'END\xae')
|
||||
2026-06-18 05:20:07,937 - ERROR - Error processing 20260618_051415_test_group.png: broken PNG file (chunk b'END\xae')
|
||||
Traceback (most recent call last):
|
||||
File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 155, in process_image
|
||||
cropped_img = crop_to_bbox(
|
||||
input_path,
|
||||
...<2 lines>...
|
||||
headroom=CONF.get("headroom", 0.0)
|
||||
)
|
||||
File "/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/tour-comfy/watcher.py", line 85, in crop_to_bbox
|
||||
alpha = img.split()[-1]
|
||||
~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/Image.py", line 2793, in split
|
||||
self.load()
|
||||
~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/ImageFile.py", line 392, in load
|
||||
s = read(read_bytes)
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 999, in load_read
|
||||
cid, pos, length = self.png.read()
|
||||
~~~~~~~~~~~~~^^
|
||||
File "/home/mike/comfyui/venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 184, in read
|
||||
raise SyntaxError(msg)
|
||||
SyntaxError: broken PNG file (chunk b'END\xae')
|
||||
2026-06-18 05:20:07,938 - INFO - Flagging image 20260618_051415_test_group.png (moving to failed directory as 20260618_052007_20260618_051415_test_group.png)
|
||||
2026-06-18 05:20:08,939 - INFO - Starting processing for 20260618_051426_test_group.png...
|
||||
2026-06-18 05:20:08,939 - INFO - Image /mnt/zim/tour-comfy/stage/20260618_051426_test_group.png is mode RGB, not RGBA. Skipping crop.
|
||||
2026-06-18 05:20:08,942 - INFO - Calling API for 20260618_051426_test_group.png -> 20260618_052008_20260618_051426_test_group.png with prompt: high quality, detailed, female nude
|
||||
2026-06-18 05:20:17,982 - INFO - Successfully processed 20260618_051426_test_group.png -> /mnt/zim/tour-comfy/output/20260618_052008_20260618_051426_test_group.png
|
||||
2026-06-18 05:20:18,011 - INFO - Parsing model identifier. Schema: None, Identifier: ViT-H-14
|
||||
2026-06-18 05:20:18,011 - INFO - Loaded built-in ViT-H-14 model config.
|
||||
2026-06-18 05:20:18,198 - INFO - HTTP Request: HEAD https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors "HTTP/1.1 302 Found"
|
||||
2026-06-18 05:20:18,198 - INFO - Instantiating model architecture: CLIP
|
||||
2026-06-18 05:20:21,797 - INFO - Loading full pretrained weights from: /home/mike/.cache/huggingface/hub/models--laion--CLIP-ViT-H-14-laion2B-s32B-b79K/snapshots/1c2b8495b28150b8a4922ee1c8edee224c284c0c/open_clip_model.safetensors
|
||||
2026-06-18 05:20:22,087 - INFO - Final image preprocessing configuration set: {'size': (224, 224), 'mode': 'RGB', 'mean': (0.48145466, 0.4578275, 0.40821073), 'std': (0.26862954, 0.26130258, 0.27577711), 'interpolation': 'bicubic', 'resize_mode': 'shortest', 'fill_color': 0}
|
||||
2026-06-18 05:20:22,087 - INFO - Model ViT-H-14 creation process complete.
|
||||
2026-06-18 05:20:24,035 - INFO - Updated /mnt/zim/tour-comfy/output/car.html with 227 images
|
||||
2026-06-18 12:28:33,669 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 12:28:33,669 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 12:28:33,669 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 12:39:41,444 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 12:39:41,444 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 12:39:41,444 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 13:15:51,389 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 13:15:51,389 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 13:15:51,389 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 13:26:24,286 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 13:26:24,286 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 13:26:24,286 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 13:33:50,546 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 13:33:50,546 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 13:33:50,546 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 17:14:37,020 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 17:14:37,020 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 17:14:37,020 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 20:51:01,612 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 20:51:01,613 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 20:51:01,613 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 22:02:15,432 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 22:02:15,432 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 22:02:15,432 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 22:22:53,496 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 22:22:53,496 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 22:22:53,496 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
2026-06-18 22:31:18,891 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage...
|
||||
2026-06-18 22:31:18,891 - INFO - Output directory: /mnt/zim/tour-comfy/output
|
||||
2026-06-18 22:31:18,891 - INFO - API URL: http://127.0.0.1:8500/edit
|
||||
|
||||
@@ -10,6 +10,13 @@ import sys
|
||||
import fcntl
|
||||
import re
|
||||
|
||||
try:
|
||||
from . import database
|
||||
from . import embeddings
|
||||
except ImportError:
|
||||
import database
|
||||
import embeddings
|
||||
|
||||
# Load configuration
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
|
||||
|
||||
@@ -142,6 +149,7 @@ def process_image(filename):
|
||||
output_filename = f"{timestamp}_{filename}"
|
||||
output_path = os.path.join(CONF["output_dir"], output_filename)
|
||||
|
||||
temp_path = input_path + ".tmp.png"
|
||||
try:
|
||||
logging.info(f"Starting processing for {filename}...")
|
||||
cropped_img = crop_to_bbox(
|
||||
@@ -152,7 +160,6 @@ def process_image(filename):
|
||||
)
|
||||
|
||||
# Save temporary cropped image for upload
|
||||
temp_path = input_path + ".tmp.png"
|
||||
cropped_img.save(temp_path, format="PNG")
|
||||
|
||||
with open(temp_path, 'rb') as f:
|
||||
@@ -169,6 +176,22 @@ def process_image(filename):
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
logging.info(f"Successfully processed {filename} -> {output_path}")
|
||||
|
||||
# Register in DB
|
||||
try:
|
||||
embedding = embeddings.generate_embedding(output_path)
|
||||
gid = filename
|
||||
database.upsert_person(output_filename, filepath=output_path, embedding=embedding, group_id=gid)
|
||||
|
||||
# Also trigger tagging to get auto-name and clip description
|
||||
tag_url = CONF["api_url"].replace("/edit", "/tag")
|
||||
try:
|
||||
requests.post(tag_url, json={"filename": output_filename, "group_id": gid}, timeout=30)
|
||||
except Exception as tag_err:
|
||||
logging.error(f"Error triggering tagging for {output_filename}: {tag_err}")
|
||||
except Exception as db_err:
|
||||
logging.error(f"Database error registering {output_filename}: {db_err}")
|
||||
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user