reorder
This commit is contained in:
@@ -1,64 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# One-time (idempotent) host setup for the Qwen-Image-Edit service.
|
|
||||||
# Runs as the service user (NO sudo). Safe to re-run: existing pieces are skipped.
|
|
||||||
#
|
|
||||||
# Builds, under the project BASE (the parent of this api/ dir):
|
|
||||||
# venv/ torch 2.3.1+rocm5.7 + ComfyUI deps (gfx906 / ROCm 5.7)
|
|
||||||
# ComfyUI/ pinned to v0.3.77 + ComfyUI-GGUF custom node
|
|
||||||
# ComfyUI/models/{unet,text_encoders,vae}/ the v23 Q8 GGUF + encoder + vae
|
|
||||||
set -e
|
|
||||||
|
|
||||||
source "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env.sh"
|
|
||||||
GGUF_NODE="$COMFY/custom_nodes/ComfyUI-GGUF"
|
|
||||||
COMFY_TAG="v0.3.77" # newest tag that runs on torch 2.3.1 (no comfy_kitchen)
|
|
||||||
|
|
||||||
echo "[bootstrap] BASE=$BASE VENV=$VENV"
|
|
||||||
|
|
||||||
# --- ComfyUI (pinned) -------------------------------------------------------
|
|
||||||
if [ ! -d "$COMFY/.git" ]; then
|
|
||||||
echo "[bootstrap] cloning ComfyUI @ $COMFY_TAG ..."
|
|
||||||
git clone --depth 1 --branch "$COMFY_TAG" \
|
|
||||||
https://github.com/comfyanonymous/ComfyUI.git "$COMFY"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- venv + python deps -----------------------------------------------------
|
|
||||||
if [ ! -d "$VENV" ]; then
|
|
||||||
echo "[bootstrap] creating venv at $VENV ..."
|
|
||||||
python3 -m venv "$VENV"
|
|
||||||
fi
|
|
||||||
source "$VENV/bin/activate"
|
|
||||||
python -m pip install --upgrade pip wheel
|
|
||||||
echo "[bootstrap] installing torch (rocm5.7) ..."
|
|
||||||
pip install torch==2.3.1+rocm5.7 torchvision==0.18.1+rocm5.7 \
|
|
||||||
--index-url https://download.pytorch.org/whl/rocm5.7
|
|
||||||
echo "[bootstrap] installing ComfyUI requirements ..."
|
|
||||||
pip install -r "$COMFY/requirements.txt"
|
|
||||||
|
|
||||||
# --- ComfyUI-GGUF custom node ----------------------------------------------
|
|
||||||
if [ ! -d "$GGUF_NODE" ]; then
|
|
||||||
echo "[bootstrap] cloning ComfyUI-GGUF ..."
|
|
||||||
git clone --depth 1 https://github.com/city96/ComfyUI-GGUF.git "$GGUF_NODE"
|
|
||||||
fi
|
|
||||||
pip install -r "$GGUF_NODE/requirements.txt" || pip install gguf
|
|
||||||
|
|
||||||
# --- API deps ---------------------------------------------------------------
|
|
||||||
pip install fastapi "uvicorn[standard]" websocket-client python-multipart pillow requests
|
|
||||||
|
|
||||||
# --- models (resume-safe; skipped if already complete) ----------------------
|
|
||||||
M="$COMFY/models"
|
|
||||||
mkdir -p "$M/unet" "$M/text_encoders" "$M/vae"
|
|
||||||
dl() { # url dest
|
|
||||||
if [ -s "$2" ]; then echo "[bootstrap] have $(basename "$2")"; return; fi
|
|
||||||
echo "[bootstrap] downloading $(basename "$2") ..."
|
|
||||||
wget -c -q -O "$2" "$1"
|
|
||||||
}
|
|
||||||
dl "https://huggingface.co/Novice25/Qwen-Image-Edit-Rapid-AIO-GGUF/resolve/main/v23/Qwen-Rapid-NSFW-v23_Q8_0.gguf" \
|
|
||||||
"$M/unet/Qwen-Rapid-NSFW-v23_Q8_0.gguf"
|
|
||||||
dl "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors" \
|
|
||||||
"$M/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors"
|
|
||||||
dl "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors" \
|
|
||||||
"$M/vae/qwen_image_vae.safetensors"
|
|
||||||
|
|
||||||
echo "[bootstrap] verifying torch + GPU ..."
|
|
||||||
python -c "import torch; print('torch', torch.__version__, 'cuda', torch.cuda.is_available(), torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'NO GPU')"
|
|
||||||
echo "[bootstrap] BOOTSTRAP_DONE"
|
|
||||||
7241
tour-comfy/car.html
7241
tour-comfy/car.html
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"api_url": "http://127.0.0.1:8500/edit",
|
|
||||||
"prompt": "high quality. hyper realistic. detailed, detailed skin, detailed female nude. realistic, high quality. realistic. detailed. female nude. realistic",
|
|
||||||
"base_prompts": [
|
|
||||||
"Head-on full-nude-body three-quarter female portrait, realistic, black void background",
|
|
||||||
"Head-on straight-on full-nude-body female portrait, realistic, black void background",
|
|
||||||
"Head-on straight-on full-body female portrait, realistic, black void 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, black void background",
|
|
||||||
"high quality, full-nude-body, female, masterpiece, realistic, photo, cinematic lighting, dramatic shadows, sharp focus, black void background"
|
|
||||||
],
|
|
||||||
"seed": -1,
|
|
||||||
"max_area": 655360,
|
|
||||||
"margin": 10,
|
|
||||||
"top_margin": 20,
|
|
||||||
"headroom": 0.05,
|
|
||||||
"poll_interval": 2,
|
|
||||||
"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",
|
|
||||||
"wireframe_dir": "/mnt/zim/tour-comfy/wireframe",
|
|
||||||
"faceswap_model": "~/.insightface/models/inswapper_128.onnx",
|
|
||||||
"facefusion_dir": "~/facefusion",
|
|
||||||
"facefusion_venv": "~/facefusion-venv",
|
|
||||||
"sam2_checkpoint": "~/.sam/sam2.1_hiera_base_plus.pt",
|
|
||||||
"sam2_config": "configs/sam2.1/sam2.1_hiera_b+.yaml",
|
|
||||||
"bg_removal": "sam2"
|
|
||||||
}
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
import psycopg2
|
|
||||||
from psycopg2 import pool as _pgpool
|
|
||||||
import threading
|
|
||||||
import json
|
|
||||||
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "192.168.1.160",
|
|
||||||
"port": 5433,
|
|
||||||
"dbname": "dv",
|
|
||||||
"user": "dev",
|
|
||||||
"password": "dev"
|
|
||||||
}
|
|
||||||
|
|
||||||
# A pooled connection is reused across requests instead of paying a fresh TCP +
|
|
||||||
# auth round-trip (~40 ms) on every single query. Under load (a generation
|
|
||||||
# running, a burst of reorders) the old open-per-call design starved the web
|
|
||||||
# threadpool and occasionally tripped Postgres' connection ceiling → 500s.
|
|
||||||
# The pool bounds connections and keeps them warm. If the pool can't be
|
|
||||||
# created or is momentarily exhausted, get_db_connection() falls back to a
|
|
||||||
# direct connect so callers never block or fail.
|
|
||||||
# Cover the web server's sync threadpool (uvicorn/anyio default = 40 workers)
|
|
||||||
# with headroom for background workers, while staying well under Postgres'
|
|
||||||
# max_connections (100).
|
|
||||||
_POOL_MIN = 2
|
|
||||||
_POOL_MAX = 48
|
|
||||||
_pool = None
|
|
||||||
_pool_lock = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pool():
|
|
||||||
global _pool
|
|
||||||
if _pool is not None:
|
|
||||||
return _pool
|
|
||||||
with _pool_lock:
|
|
||||||
if _pool is None:
|
|
||||||
try:
|
|
||||||
_pool = _pgpool.ThreadedConnectionPool(
|
|
||||||
_POOL_MIN, _POOL_MAX, **DB_CONFIG)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[db] pool init failed, using direct connections: {e}")
|
|
||||||
_pool = False # sentinel: don't retry on every call
|
|
||||||
return _pool
|
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection():
|
|
||||||
"""Return a live DB connection, preferring the pool.
|
|
||||||
|
|
||||||
Always pair with _put_db_connection() (the existing finally: conn.close()
|
|
||||||
callsites are rewritten to call it) so pooled connections are returned
|
|
||||||
rather than dropped.
|
|
||||||
"""
|
|
||||||
p = _get_pool()
|
|
||||||
if p:
|
|
||||||
try:
|
|
||||||
conn = p.getconn()
|
|
||||||
# Guard against a stale/dead pooled connection.
|
|
||||||
if getattr(conn, "closed", 0):
|
|
||||||
try:
|
|
||||||
p.putconn(conn, close=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return conn
|
|
||||||
except _pgpool.PoolError:
|
|
||||||
# Pool momentarily exhausted — expected under burst; fall back to a
|
|
||||||
# direct connection silently rather than blocking or failing.
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[db] pool getconn failed, direct connect: {e}")
|
|
||||||
return psycopg2.connect(**DB_CONFIG)
|
|
||||||
|
|
||||||
|
|
||||||
def _put_db_connection(conn):
|
|
||||||
"""Return a connection to the pool (rolling back any open txn) or close it.
|
|
||||||
|
|
||||||
Safe for both pooled and direct/fallback connections: putconn raises for a
|
|
||||||
connection the pool doesn't own, in which case we just close it.
|
|
||||||
"""
|
|
||||||
if conn is None:
|
|
||||||
return
|
|
||||||
p = _pool
|
|
||||||
try:
|
|
||||||
if p:
|
|
||||||
try:
|
|
||||||
conn.rollback() # clear any aborted/idle-in-txn state before reuse
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
p.putconn(conn)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
conn.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def migrate_schema():
|
|
||||||
"""Add new columns to person table if they don't exist. Safe to call repeatedly."""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
for sql in [
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS prompt TEXT",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS pose TEXT",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS sort_order INTEGER",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS group_name TEXT",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS hidden BOOLEAN DEFAULT FALSE",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS has_background BOOLEAN DEFAULT TRUE",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS source_refs TEXT",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS has_clothing BOOLEAN DEFAULT NULL",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS content_type TEXT DEFAULT 'image'",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS faceswap_source_video TEXT",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS archived BOOLEAN DEFAULT FALSE",
|
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS face_embedding vector(512)",
|
|
||||||
]:
|
|
||||||
cur.execute(sql)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
|
||||||
embedding=None, clip_description=None, prompt=None, pose=None,
|
|
||||||
sort_order=None, group_name=None, hidden=None,
|
|
||||||
has_background=None, source_refs=None, has_clothing=None,
|
|
||||||
content_type=None, faceswap_source_video=None, archived=None,
|
|
||||||
face_embedding=None):
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
face_embedding_str = ("[" + ",".join(map(str, face_embedding)) + "]") if face_embedding is not None else None
|
|
||||||
try:
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO person (filename, filepath, name, group_id, tags, embedding,
|
|
||||||
clip_description, prompt, pose, sort_order, group_name, hidden,
|
|
||||||
has_background, source_refs, has_clothing,
|
|
||||||
content_type, faceswap_source_video, archived, face_embedding)
|
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %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),
|
|
||||||
prompt = COALESCE(EXCLUDED.prompt, person.prompt),
|
|
||||||
pose = COALESCE(EXCLUDED.pose, person.pose),
|
|
||||||
sort_order = COALESCE(EXCLUDED.sort_order, person.sort_order),
|
|
||||||
group_name = COALESCE(EXCLUDED.group_name, person.group_name),
|
|
||||||
hidden = COALESCE(EXCLUDED.hidden, person.hidden),
|
|
||||||
has_background = COALESCE(EXCLUDED.has_background, person.has_background),
|
|
||||||
source_refs = COALESCE(EXCLUDED.source_refs, person.source_refs),
|
|
||||||
has_clothing = COALESCE(EXCLUDED.has_clothing, person.has_clothing),
|
|
||||||
content_type = COALESCE(EXCLUDED.content_type, person.content_type),
|
|
||||||
faceswap_source_video = COALESCE(EXCLUDED.faceswap_source_video, person.faceswap_source_video),
|
|
||||||
archived = COALESCE(EXCLUDED.archived, person.archived),
|
|
||||||
face_embedding = COALESCE(EXCLUDED.face_embedding, person.face_embedding);
|
|
||||||
""", (filename, filepath, name, group_id,
|
|
||||||
json.dumps(tags) if tags else None,
|
|
||||||
embedding, clip_description, prompt, pose, sort_order, group_name, hidden,
|
|
||||||
has_background, source_refs, has_clothing,
|
|
||||||
content_type, faceswap_source_video, archived, face_embedding_str))
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def set_archived(filename, archived: bool):
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute("UPDATE person SET archived = %s WHERE filename = %s", (archived, filename))
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def set_hidden(filename, hidden: bool):
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute("UPDATE person SET hidden = %s WHERE filename = %s", (hidden, filename))
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def get_person(filename):
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute("""
|
|
||||||
SELECT name, group_id, tags, embedding, clip_description, filepath,
|
|
||||||
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
|
||||||
has_clothing
|
|
||||||
FROM person WHERE filename = %s
|
|
||||||
""", (filename,))
|
|
||||||
return cur.fetchone()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def list_persons(include_archived=False):
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
where = "" if include_archived else "WHERE archived IS NOT TRUE"
|
|
||||||
cur.execute(f"""
|
|
||||||
SELECT filename, name, group_id, clip_description,
|
|
||||||
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
|
||||||
has_clothing, content_type, faceswap_source_video, archived
|
|
||||||
FROM person
|
|
||||||
{where}
|
|
||||||
""")
|
|
||||||
return cur.fetchall()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def search_similar(embedding, limit=10):
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
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()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
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()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
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()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
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()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def set_group_order(group_id, ordered_filenames):
|
|
||||||
"""Assign sort_order 0,1,2,... to filenames in the given order."""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
for idx, fname in enumerate(ordered_filenames):
|
|
||||||
cur.execute(
|
|
||||||
"UPDATE person SET sort_order = %s WHERE filename = %s",
|
|
||||||
(idx, fname)
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def get_group_order(group_id):
|
|
||||||
"""Return [(filename, sort_order), ...] sorted by sort_order NULLS LAST."""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute("""
|
|
||||||
SELECT filename, sort_order
|
|
||||||
FROM person
|
|
||||||
WHERE group_id = %s
|
|
||||||
ORDER BY sort_order NULLS LAST, filename
|
|
||||||
""", (group_id,))
|
|
||||||
return cur.fetchall()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def set_group_name(group_id, name):
|
|
||||||
"""Set group_name for every file in the group."""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute("UPDATE person SET group_name = %s WHERE group_id = %s", (name, group_id))
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
def get_next_sort_order(group_id):
|
|
||||||
"""Return max(sort_order)+1 for the group, or 1 if no sorted members exist."""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT COALESCE(MAX(sort_order), 0) + 1 FROM person WHERE group_id = %s",
|
|
||||||
(group_id,)
|
|
||||||
)
|
|
||||||
return cur.fetchone()[0]
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_group_names():
|
|
||||||
"""Return {group_id: group_name} for groups that have a name set."""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute("""
|
|
||||||
SELECT DISTINCT ON (group_id) group_id, group_name
|
|
||||||
FROM person
|
|
||||||
WHERE group_id IS NOT NULL AND group_name IS NOT NULL
|
|
||||||
""")
|
|
||||||
return {row[0]: row[1] for row in cur.fetchall()}
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
|
|
||||||
def get_face_embedding(filename):
|
|
||||||
"""Return the face_embedding as a list of floats for a filename, or None."""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute("SELECT face_embedding FROM person WHERE filename = %s", (filename,))
|
|
||||||
row = cur.fetchone()
|
|
||||||
if row and row[0] is not None:
|
|
||||||
val = row[0]
|
|
||||||
# psycopg2 without a pgvector adapter returns vectors as plain strings "[f,f,...]"
|
|
||||||
if isinstance(val, str):
|
|
||||||
return [float(x) for x in val.strip("[]").split(",")]
|
|
||||||
return list(val)
|
|
||||||
return None
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
|
|
||||||
|
|
||||||
def search_similar_face(embedding, limit=12, exclude_group_id=None):
|
|
||||||
"""Cosine search on face_embedding (stored only for *_face.png rows).
|
|
||||||
|
|
||||||
Returns [(filename, group_id, distance), ...] sorted ascending by distance.
|
|
||||||
Rows belonging to exclude_group_id are skipped so a group doesn't match itself.
|
|
||||||
"""
|
|
||||||
conn = get_db_connection()
|
|
||||||
cur = conn.cursor()
|
|
||||||
try:
|
|
||||||
embedding_str = "[" + ",".join(map(str, embedding)) + "]"
|
|
||||||
if exclude_group_id:
|
|
||||||
cur.execute("""
|
|
||||||
SELECT filename, group_id, face_embedding <=> %s AS distance
|
|
||||||
FROM person
|
|
||||||
WHERE face_embedding IS NOT NULL
|
|
||||||
AND (group_id IS NULL OR group_id != %s)
|
|
||||||
ORDER BY distance ASC
|
|
||||||
LIMIT %s
|
|
||||||
""", (embedding_str, exclude_group_id, limit))
|
|
||||||
else:
|
|
||||||
cur.execute("""
|
|
||||||
SELECT filename, group_id, face_embedding <=> %s AS distance
|
|
||||||
FROM person
|
|
||||||
WHERE face_embedding IS NOT NULL
|
|
||||||
ORDER BY distance ASC
|
|
||||||
LIMIT %s
|
|
||||||
""", (embedding_str, limit))
|
|
||||||
return cur.fetchall()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_put_db_connection(conn)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Install/refresh the systemd services for Qwen-Image-Edit on THIS host.
|
|
||||||
# Host-agnostic: the service user, group and install path are derived at run
|
|
||||||
# time, so the same file works on tour, hubby, etc.
|
|
||||||
#
|
|
||||||
# Run with sudo (needs to write /etc/systemd/system). Assumes bootstrap.sh has
|
|
||||||
# already created venv/, ComfyUI/ and the models under BASE.
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
echo "This script must be run as root (use sudo)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # .../comfyui/api
|
|
||||||
BASE="$( cd "$SCRIPT_DIR/.." && pwd )" # .../comfyui
|
|
||||||
TEMPLATES="$SCRIPT_DIR/systemd"
|
|
||||||
|
|
||||||
# The service should run as the owner of the project, not root.
|
|
||||||
SVC_USER="${SUDO_USER:-$(stat -c '%U' "$SCRIPT_DIR")}"
|
|
||||||
SVC_GROUP="$(id -gn "$SVC_USER")"
|
|
||||||
|
|
||||||
echo "Installing services: user=$SVC_USER group=$SVC_GROUP base=$BASE"
|
|
||||||
|
|
||||||
for unit in comfyui-backend comfyui-api; do
|
|
||||||
sed -e "s|__USER__|$SVC_USER|g" \
|
|
||||||
-e "s|__GROUP__|$SVC_GROUP|g" \
|
|
||||||
-e "s|__BASE__|$BASE|g" \
|
|
||||||
"$TEMPLATES/$unit.service" > "/etc/systemd/system/$unit.service"
|
|
||||||
echo " wrote /etc/systemd/system/$unit.service"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Reloading systemd daemon..."
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
echo "Enabling + (re)starting services..."
|
|
||||||
systemctl enable comfyui-backend.service comfyui-api.service
|
|
||||||
systemctl restart comfyui-backend.service comfyui-api.service
|
|
||||||
|
|
||||||
echo "Deployment complete."
|
|
||||||
echo "Check status with: systemctl status comfyui-backend comfyui-api"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Shared path resolver for the Qwen-Image-Edit service scripts.
|
|
||||||
# Sourced by bootstrap.sh / run_comfyui.sh / start_api.sh.
|
|
||||||
#
|
|
||||||
# Why this exists: a Python venv CANNOT live on the NTFS (fuseblk) mount used
|
|
||||||
# on tour (/media/tour/APPS). Its interpreter symlinks turn into
|
|
||||||
# "unsupported reparse tag 0x..." after a reboot/remount, so `python`
|
|
||||||
# vanishes and every service dies. ComfyUI code and the model files are plain
|
|
||||||
# files and are fine on NTFS -- only the venv must be on a native fs.
|
|
||||||
#
|
|
||||||
# So: if BASE is on a non-native filesystem, the venv goes under $HOME (ext4);
|
|
||||||
# otherwise it stays at $BASE/venv. Override explicitly with COMFY_VENV.
|
|
||||||
|
|
||||||
ENV_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # .../comfyui/api
|
|
||||||
API_DIR="$ENV_DIR"
|
|
||||||
BASE="$( cd "$ENV_DIR/.." && pwd )" # .../comfyui
|
|
||||||
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/mike/comfyui/venv}" ;; # NTFS-ish BASE -> venv on home
|
|
||||||
*)
|
|
||||||
if [ -d "/home/mike/comfyui/venv" ]; then
|
|
||||||
VENV="${COMFY_VENV:-/home/mike/comfyui/venv}"
|
|
||||||
else
|
|
||||||
VENV="${COMFY_VENV:-$BASE/venv}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
{
|
|
||||||
"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_a5a45c98",
|
|
||||||
"kbk99v.png": "cg_a5a45c98",
|
|
||||||
"20260618_004941_out7.png": "cg_a5a45c98",
|
|
||||||
"out7.png": "cg_a5a45c98",
|
|
||||||
"20260618_004334_Pasted image (3).png": "cg_0290aa0c",
|
|
||||||
"Pasted image (3).png": "cg_0290aa0c",
|
|
||||||
"20260618_002025_20260616_020020_img_35.png": "cg_0290aa0c",
|
|
||||||
"20260616_020020_img_35.png": "cg_0290aa0c",
|
|
||||||
"20260618_004428_20260616_015949_img_37.png": "cg_0290aa0c",
|
|
||||||
"20260618_002036_20260616_015949_img_37.png": "cg_0290aa0c",
|
|
||||||
"20260616_015949_img_37.png": "cg_0290aa0c",
|
|
||||||
"20260616_020059_img_38.png": "cg_0290aa0c",
|
|
||||||
"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_569ddd5e",
|
|
||||||
"20260615_154852_other.jpeg": "cg_569ddd5e",
|
|
||||||
"20260615_154333_other.jpeg": "cg_1c0c5074",
|
|
||||||
"20260618_004407_20260616_002456_test123.jpeg": "cg_569ddd5e",
|
|
||||||
"20260616_002456_test123.jpeg": "cg_569ddd5e",
|
|
||||||
"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": "cg_bc58b958",
|
|
||||||
"20260618_220856_2_20260618_053458_image.png": "cg_bc58b958",
|
|
||||||
"20260618_220907_2_20260618_053458_image.png": "cg_bc58b958",
|
|
||||||
"20260618_220918_2_20260618_053458_image.png": "cg_bc58b958",
|
|
||||||
"Pasted image (7).png": "cg_0290aa0c",
|
|
||||||
"20260616_021235_img_47.png": "cg_0290aa0c",
|
|
||||||
"20260616_020035_img_36.png": "cg_0290aa0c",
|
|
||||||
"20260618_002014_20260616_020035_img_36.png": "cg_0290aa0c",
|
|
||||||
"20260618_011517_20260618_002014_20260616_020035_img_36.png": "cg_0290aa0c",
|
|
||||||
"Pasted image (4).png": "cg_0290aa0c",
|
|
||||||
"20260618_010700_Pasted image (4).png": "cg_0290aa0c",
|
|
||||||
"20260618_015156_20260618_010700_Pasted image (4).png": "cg_0290aa0c",
|
|
||||||
"20260616_023306_img_52.png": "cg_571ceb34",
|
|
||||||
"20260616_022349_img_48.png": "cg_571ceb34",
|
|
||||||
"20260616_022543_img_50.png": "cg_571ceb34",
|
|
||||||
"20260618_015102_20260616_022543_img_50.png": "cg_571ceb34",
|
|
||||||
"20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_124440_6_20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_124424_5_20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_124412_4_20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_124355_3_20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_124334_2_20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_124316_1_20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_124256_0_20260618_124203_image.png": "cg_f1e85987",
|
|
||||||
"20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123315_8_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123257_7_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123244_6_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123227_5_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123210_4_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123153_3_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123132_2_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123119_1_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_123104_0_20260618_122931_image.png": "cg_f1e85987",
|
|
||||||
"20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_133011_9_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132956_8_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132941_7_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132923_6_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132906_5_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132848_4_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132831_3_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132817_2_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132806_1_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132755_0_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_133023_10_20260618_132718_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132450_8_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132432_7_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132414_6_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132357_5_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132340_4_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132323_3_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132305_2_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132248_1_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132232_0_20260618_132215_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132152_8_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132134_7_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132114_6_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132054_5_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132035_4_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_132017_3_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131959_2_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131943_1_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131926_0_20260618_131909_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131822_8_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131811_7_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131759_6_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131747_5_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131736_4_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131725_3_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131713_2_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131702_1_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_131651_0_20260618_131624_image.png": "cg_7e0e0569",
|
|
||||||
"20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172641_8_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172630_7_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172618_6_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172607_5_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172555_4_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172544_3_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172532_2_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172521_1_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_172510_0_20260618_172500_image.png": "cg_08203e88",
|
|
||||||
"20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_172015_8_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171939_7_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171906_6_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171834_5_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171802_4_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171732_3_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171706_2_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171639_1_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171624_0_20260618_171448_image.png": "cg_08203e88",
|
|
||||||
"20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_172048_8_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_172032_7_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_171956_6_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_171923_5_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_171851_4_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_171819_3_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_171746_2_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_171717_1_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260618_171655_0_20260618_171628_image.png": "cg_08203e88",
|
|
||||||
"20260617_015728_img_70.png": "cg_f6e02c38",
|
|
||||||
"20260617_015611_img_69.png": "cg_f6e02c38",
|
|
||||||
"20260617_133154_img_78.png": "cg_75340e8e",
|
|
||||||
"20260617_133411_img_79.png": "cg_75340e8e",
|
|
||||||
"p1.png": "cg_a5a45c98",
|
|
||||||
"20260618_004909_p1.png": "cg_a5a45c98",
|
|
||||||
"20260618_004345_p1.png": "cg_a5a45c98",
|
|
||||||
"20260615_150812_img_19_2.png": "cg_a5a45c98",
|
|
||||||
"20260618_004847_20260615_150812_img_19_2.png": "cg_a5a45c98",
|
|
||||||
"20260618_011434_20260618_004847_20260615_150812_img_19_2.png": "cg_a5a45c98",
|
|
||||||
"20260618_015052_20260618_004407_20260616_002456_test123.jpeg": "cg_569ddd5e",
|
|
||||||
"20260618_223501_image.png": "solo:20260618_223501_image.png",
|
|
||||||
"20260618_223557_2_20260618_223501_image.png": "solo:20260618_223557_2_20260618_223501_image.png",
|
|
||||||
"20260618_223541_1_20260618_223501_image.png": "solo:20260618_223541_1_20260618_223501_image.png",
|
|
||||||
"20260618_223525_0_20260618_223501_image.png": "solo:20260618_223525_0_20260618_223501_image.png",
|
|
||||||
"20260619_040307_7_20260619_040135_image.png": "solo:20260619_040307_7_20260619_040135_image.png",
|
|
||||||
"20260619_040319_8_20260619_040135_image.png": "solo:20260619_040319_8_20260619_040135_image.png",
|
|
||||||
"20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041924_8_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041902_7_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041839_6_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041734_3_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041818_5_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041756_4_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041711_2_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041650_1_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_041628_0_20260619_041612_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_040221_3_20260619_040135_image.png": "cg_8c8d8ec9",
|
|
||||||
"20260619_040158_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_043405_1_20260619_040135_image.png": "cg_d193128f",
|
|
||||||
"20260619_043433_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041913_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041850_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041829_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041806_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041744_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041722_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041701_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041639_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041618_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041555_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041544_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041533_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041522_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041511_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041500_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041449_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041438_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041427_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041417_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041406_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041355_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041334_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041323_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041041_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041030_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_041020_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_040147_0_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_040209_2_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_040255_6_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_040244_5_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260619_040233_4_20260619_040135_image.png": "solo:20260619_040233_4_20260619_040135_image.png",
|
|
||||||
"20260619_043740_1_20260619_040135_image.png": "cg_84349b43",
|
|
||||||
"20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052708_8_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052657_7_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052645_6_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052634_5_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052622_4_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052611_3_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052559_2_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052548_1_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260618_052537_0_20260618_052526_image.png": "cg_7ec17537",
|
|
||||||
"20260619_052235_mr_1_20260619_040135_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_052030_mr_mr_mr_1_20260619_040135_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_052351_mr_mr_mr_mr_1_20260619_040135_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_052514_mr_1_20260618_052526_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_051326_mr_1_20260619_040135_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_051905_mr_mr_1_20260619_040135_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_050945_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_125445_mr_7_20260619_124038_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_125434_mr_mr_1_20260619_040135_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_125654_mr_mr_7_20260619_124038_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_130043_mr_1_20260619_124038_image.png": "cg_ed2e43d1",
|
|
||||||
"20260619_130001_mr_1_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124635_mr_7_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124529_8_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124508_7_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124446_6_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124421_5_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124351_4_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124316_3_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124242_2_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124210_1_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124139_0_20260619_123958_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124540_8_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124518_7_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124456_6_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124435_5_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124407_4_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124333_3_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124258_2_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124226_1_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_124154_0_20260619_124038_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184116_image.png": "cg_6f321af3",<
|
|
||||||
"20260619_184259_8_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184248_7_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184236_6_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184225_5_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184214_4_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184202_3_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184150_2_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184139_1_20260619_184116_image.png": "cg_6f321af3",
|
|
||||||
"20260619_184128_0_20260619_184116_image.png": "cg_6f321af3"
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Install FaceFusion 3.x for high-quality face+hair swap.
|
|
||||||
# Clones into ~/facefusion and creates a dedicated venv at ~/facefusion-venv.
|
|
||||||
# Usage: bash tour-comfy/install_facefusion.sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
FF_DIR="${FACEFUSION_DIR:-$HOME/facefusion}"
|
|
||||||
FF_VENV="${FACEFUSION_VENV:-$HOME/facefusion-venv}"
|
|
||||||
FF_REPO="https://github.com/facefusion/facefusion"
|
|
||||||
|
|
||||||
echo "[facefusion] Installing to $FF_DIR (venv: $FF_VENV)"
|
|
||||||
|
|
||||||
# 1. Clone or update
|
|
||||||
if [ -d "$FF_DIR/.git" ]; then
|
|
||||||
echo "[facefusion] Updating existing clone ..."
|
|
||||||
git -C "$FF_DIR" pull --ff-only
|
|
||||||
else
|
|
||||||
echo "[facefusion] Cloning $FF_REPO ..."
|
|
||||||
git clone "$FF_REPO" "$FF_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. Create dedicated venv (avoids dependency conflicts with ComfyUI)
|
|
||||||
if [ ! -d "$FF_VENV" ]; then
|
|
||||||
echo "[facefusion] Creating venv at $FF_VENV ..."
|
|
||||||
python3 -m venv "$FF_VENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PIP="$FF_VENV/bin/pip"
|
|
||||||
PY="$FF_VENV/bin/python"
|
|
||||||
|
|
||||||
"$PIP" install --upgrade pip wheel
|
|
||||||
|
|
||||||
# 3. Install FaceFusion requirements
|
|
||||||
cd "$FF_DIR"
|
|
||||||
"$PIP" install -r requirements.txt \
|
|
||||||
--extra-index-url https://download.pytorch.org/whl/cu124
|
|
||||||
|
|
||||||
# 4. Download base models (ghost_3_1_256 + gfpgan_1.4 for enhance)
|
|
||||||
echo "[facefusion] Downloading default models via FaceFusion model manager ..."
|
|
||||||
"$PY" facefusion.py \
|
|
||||||
--processors face_swapper hair_swapper face_enhancer \
|
|
||||||
--face-swapper-model ghost_3_1_256 \
|
|
||||||
--face-enhancer-model gfpgan_1.4 \
|
|
||||||
--execution-providers cpu \
|
|
||||||
download-models 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[facefusion] Installation complete."
|
|
||||||
echo " Binary: $PY $FF_DIR/facefusion.py"
|
|
||||||
echo " Config: set facefusion_dir/facefusion_venv in tour-comfy/config.json"
|
|
||||||
echo ""
|
|
||||||
echo "Restart the API (start_api.sh) and the 'Hair swap' toggle will activate."
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Install GFPGAN face enhancement into the ComfyUI venv.
|
|
||||||
# Applies two patches for Python 3.13 + newer torchvision compatibility.
|
|
||||||
# Usage: bash tour-comfy/install_gfpgan.sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
source "$SCRIPT_DIR/env.sh"
|
|
||||||
source "$VENV/bin/activate"
|
|
||||||
|
|
||||||
PYTHON="$VENV/bin/python"
|
|
||||||
PIP="$VENV/bin/pip"
|
|
||||||
|
|
||||||
echo "[gfpgan] Step 1 — install basicsr (with Python 3.13 patch) ..."
|
|
||||||
TMPDIR=$(mktemp -d)
|
|
||||||
curl -sL "https://pypi.io/packages/source/b/basicsr/basicsr-1.4.2.tar.gz" -o "$TMPDIR/basicsr-1.4.2.tar.gz"
|
|
||||||
tar -xzf "$TMPDIR/basicsr-1.4.2.tar.gz" -C "$TMPDIR"
|
|
||||||
|
|
||||||
# Patch 1: fix get_version() — exec() doesn't update locals() in Python 3
|
|
||||||
"$PYTHON" - <<'PYPATCH'
|
|
||||||
import sys, re
|
|
||||||
setup = sys.argv[1]
|
|
||||||
with open(setup) as f:
|
|
||||||
content = f.read()
|
|
||||||
old = "def get_version():\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'))\n return locals()['__version__']"
|
|
||||||
new = "def get_version():\n if not os.path.exists(version_file):\n write_version_py()\n globs = {}\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'), globs)\n return globs['__version__']"
|
|
||||||
if old in content:
|
|
||||||
content = content.replace(old, new)
|
|
||||||
with open(setup, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
print(' Patched setup.py get_version()')
|
|
||||||
else:
|
|
||||||
print(' setup.py pattern not found, skipping patch')
|
|
||||||
PYPATCH
|
|
||||||
"$TMPDIR/basicsr-1.4.2/setup.py" -- "$TMPDIR/basicsr-1.4.2/setup.py" 2>/dev/null || true
|
|
||||||
"$PYTHON" - "$TMPDIR/basicsr-1.4.2/setup.py" <<'PYPATCH'
|
|
||||||
import sys, re, os
|
|
||||||
setup = sys.argv[1]
|
|
||||||
with open(setup) as f:
|
|
||||||
content = f.read()
|
|
||||||
old = "def get_version():\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'))\n return locals()['__version__']"
|
|
||||||
new = "def get_version():\n if not os.path.exists(version_file):\n write_version_py()\n globs = {}\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'), globs)\n return globs['__version__']"
|
|
||||||
if old in content:
|
|
||||||
content = content.replace(old, new)
|
|
||||||
with open(setup, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
print(' Patched setup.py get_version()')
|
|
||||||
else:
|
|
||||||
print(' setup.py already patched or pattern changed, skipping')
|
|
||||||
PYPATCH
|
|
||||||
|
|
||||||
"$PIP" install "$TMPDIR/basicsr-1.4.2/" --no-build-isolation --no-deps -q
|
|
||||||
rm -rf "$TMPDIR"
|
|
||||||
|
|
||||||
echo "[gfpgan] Step 2 — install facexlib and gfpgan ..."
|
|
||||||
"$PIP" install facexlib gfpgan -q
|
|
||||||
|
|
||||||
# Patch 2: fix torchvision functional_tensor import (removed in newer torchvision)
|
|
||||||
DEGR_PY="$VENV/lib/python3.13/site-packages/basicsr/data/degradations.py"
|
|
||||||
if [ -f "$DEGR_PY" ]; then
|
|
||||||
if grep -q "functional_tensor" "$DEGR_PY"; then
|
|
||||||
sed -i 's/from torchvision.transforms.functional_tensor import rgb_to_grayscale/from torchvision.transforms.functional import rgb_to_grayscale/' "$DEGR_PY"
|
|
||||||
echo "[gfpgan] Patched degradations.py functional_tensor import"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Pre-download the model
|
|
||||||
MODEL_DIR="$HOME/.gfpgan/weights"
|
|
||||||
MODEL_PATH="$MODEL_DIR/GFPGANv1.4.pth"
|
|
||||||
mkdir -p "$MODEL_DIR"
|
|
||||||
if [ ! -f "$MODEL_PATH" ]; then
|
|
||||||
echo "[gfpgan] Downloading GFPGANv1.4.pth (~333 MB) ..."
|
|
||||||
wget -q --show-progress \
|
|
||||||
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth" \
|
|
||||||
-O "$MODEL_PATH.tmp"
|
|
||||||
mv "$MODEL_PATH.tmp" "$MODEL_PATH"
|
|
||||||
echo "[gfpgan] Model saved to $MODEL_PATH"
|
|
||||||
else
|
|
||||||
echo "[gfpgan] Model already present: $MODEL_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "[gfpgan] Done. Restart the API (start_api.sh) to enable face enhancement."
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
_NAMES_USA = [
|
|
||||||
"Aiko", "Alara", "Amara", "Amira", "Andromeda", "Aoife", "Ara", "Aria",
|
|
||||||
"Ariel", "Arya", "Asel", "Ashira", "Astrid", "Aurora", "Aya", "Ayame",
|
|
||||||
"Bree", "Briar", "Calla", "Calypso", "Celeste", "Clio", "Cora", "Dalia",
|
|
||||||
"Dawn", "Daya", "Delia", "Deva", "Eila", "Eira", "Elara", "Elene",
|
|
||||||
"Elin", "Elira", "Ember", "Era", "Eris", "Estelle", "Eve", "Faye",
|
|
||||||
"Fern", "Fiora", "Fleur", "Flora", "Gaia", "Greer", "Gwen", "Halo",
|
|
||||||
"Haven", "Hera", "Ida", "Indra", "Io", "Iris", "Iva", "Jade",
|
|
||||||
"Jaya", "Juno", "Kai", "Kaia", "Kaya", "Kessa", "Kira", "Laia",
|
|
||||||
"Leda", "Lila", "Lira", "Lirien", "Luma", "Luna", "Lyra", "Maia",
|
|
||||||
"Mara", "Marit", "Maya", "Meira", "Mira", "Miya", "Nadia", "Naia",
|
|
||||||
"Nara", "Neva", "Nia", "Nike", "Nina", "Noor", "Nora", "Nova",
|
|
||||||
"Nyx", "Ora", "Orla", "Petra", "Quinn", "Ren", "Rhea", "Riona",
|
|
||||||
"Rue", "Saga", "Sage", "Sarai", "Selene", "Sera", "Silva", "Sol",
|
|
||||||
"Sora", "Sylva", "Tala", "Tara", "Theia", "Tova", "Ula", "Uma",
|
|
||||||
"Una", "Vega", "Vela", "Vera", "Vesper", "Vira", "Vivi", "Wren",
|
|
||||||
"Xara", "Xena", "Yael", "Yuki", "Zara", "Zita", "Zoe", "Zora",
|
|
||||||
]
|
|
||||||
|
|
||||||
_NAMES = [
|
|
||||||
"Aaf", "Aafje", "Aafke", "Aagje", "Aaltje", "Abby", "Abigail", "Ada",
|
|
||||||
"Adèle", "Adriana", "Agnes", "Aisha", "Ayla", "Alana", "Aletta", "Alexandra",
|
|
||||||
"Alice", "Alicia", "Alida", "Alina", "Alise", "Aliza", "Alma", "Amalia",
|
|
||||||
"Amanda", "Amber", "Amelie", "Amélie", "Amira", "Amy", "An", "Ana",
|
|
||||||
"Anabel", "Anastasia", "Andrea", "Angela", "Angelina", "Aniek", "Anika",
|
|
||||||
"Anissa", "Anja", "Anke", "Anna", "Annabel", "Anne", "Annebel",
|
|
||||||
"Annemarie", "Annet", "Annette", "Annie", "Anouk", "Ans", "Antje",
|
|
||||||
"Antoinette", "Ariane", "Arianna", "Ariana", "Arwen", "Ashley", "Astrid",
|
|
||||||
"Aukje", "Aurora", "Avalon", "Aya",
|
|
||||||
|
|
||||||
"Babette", "Barbara", "Bea", "Beatrix", "Beau", "Bente", "Berber",
|
|
||||||
"Bernadette", "Bertje", "Betsy", "Bianca", "Bibi", "Bodil", "Bo",
|
|
||||||
"Bobbi", "Bregje", "Brechtje", "Brenda", "Brit", "Britt",
|
|
||||||
|
|
||||||
"Cato", "Catharina", "Celine", "Céline", "Cemre", "Chantal", "Charlotte",
|
|
||||||
"Chelsey", "Chiara", "Chloë", "Christa", "Christel", "Christina", "Cindy",
|
|
||||||
"Claire", "Clara", "Clarissa", "Claudia", "Cleo", "Coby", "Conny", "Cora",
|
|
||||||
"Cornelia", "Cynthia",
|
|
||||||
|
|
||||||
"Daantje", "Dafne", "Daisy", "Dana", "Daniëlle", "Daphne", "Debbie",
|
|
||||||
"Demi", "Denise", "Desi", "Diana", "Dide", "Diede", "Dieuwertje", "Dina",
|
|
||||||
"Dinja", "Dionne", "Do", "Dominique", "Doortje", "Dora", "Doris", "Doutzen",
|
|
||||||
|
|
||||||
"Eef", "Eefje", "Eeke", "Eline", "Elisa", "Elise", "Elisabeth", "Ella",
|
|
||||||
"Elle", "Ellen", "Ellemijn", "Ellis", "Els", "Elsa", "Else", "Elvira",
|
|
||||||
"Emilia", "Emma", "Emmy", "Eva", "Evelien", "Eveline", "Evi",
|
|
||||||
|
|
||||||
"Fabiënne", "Famke", "Fay", "Faye", "Fem", "Femke", "Fenna", "Fenne",
|
|
||||||
"Fien", "Fiene", "Fieke", "Fleur", "Fleurtje", "Floor", "Floortje",
|
|
||||||
"Flore", "Florianne", "Froukje",
|
|
||||||
|
|
||||||
"Gabriëlle", "Gea", "Geertje", "Geertruida", "Geke", "Gerda", "Gerdi",
|
|
||||||
"Gerdien", "Gertrude", "Ghislaine", "Gina", "Gitte", "Greet", "Greetje",
|
|
||||||
"Greta", "Grietje", "Guusje", "Gwen",
|
|
||||||
|
|
||||||
"Hanna", "Hannah", "Hanne", "Hanneke", "Hannelore", "Harriët", "Hedwig",
|
|
||||||
"Heleen", "Helena", "Helene", "Henriëtte", "Hester", "Hilde", "Hinke",
|
|
||||||
|
|
||||||
"Ilona", "Ilse", "Imke", "Imme", "Ina", "Indy", "Ineke", "Ines", "Inge",
|
|
||||||
"Ingrid", "Iris", "Isa", "Isabel", "Isabella", "Isabelle", "Ise", "Iva",
|
|
||||||
"Ivana",
|
|
||||||
|
|
||||||
"Jacoba", "Jacqueline", "Jada", "Jade", "Janna", "Janne", "Janneke",
|
|
||||||
"Jantien", "Jasmijn", "Jasmine", "Jayda", "Jelka", "Jelke", "Jente",
|
|
||||||
"Jenthe", "Jet", "Jette", "Jill", "Jo", "Joan", "Joanna", "Johanna",
|
|
||||||
"Jolanda", "Jolie", "Josephine", "Josje", "Judith", "Julia", "Julie",
|
|
||||||
"Juliette", "Juna", "Juni", "Juul", "Juut",
|
|
||||||
|
|
||||||
"Kaat", "Kaatje", "Karen", "Karin", "Karlijn", "Kiki", "Kim", "Kirsten",
|
|
||||||
"Klara", "Klasina", "Kris", "Kristel",
|
|
||||||
|
|
||||||
"Lana", "Lara", "Laura", "Laurien", "Lena", "Lene", "Leni", "Lenie",
|
|
||||||
"Lenthe", "Lieke", "Lien", "Lieve", "Linde", "Lindsey", "Lisa", "Lisanne",
|
|
||||||
"Liset", "Liselotte", "Livia", "Liz", "Liza", "Lizzy", "Loïs", "Lola",
|
|
||||||
"Lonneke", "Lotte", "Lou", "Louise", "Lova", "Luca", "Lucia", "Lucie",
|
|
||||||
"Luna",
|
|
||||||
|
|
||||||
"Maaike", "Maan", "Maartje", "Maayke", "Madeleine", "Madelief", "Maike",
|
|
||||||
"Maja", "Malou", "Manon", "Mara", "Mare", "Margot", "Margriet", "Maria",
|
|
||||||
"Marieke", "Marije", "Marijne", "Marit", "Marja", "Marjan", "Marjolein",
|
|
||||||
"Marleen", "Marloes", "Marlot", "Marly", "Martha", "Marthe", "Mathilde",
|
|
||||||
"Maud", "Mees", "Meike", "Melanie", "Melissa", "Merel", "Mette", "Mia",
|
|
||||||
"Mieke", "Mila", "Milou", "Minke", "Mira", "Mirjam", "Myrthe",
|
|
||||||
|
|
||||||
"Naomi", "Natasja", "Nathalie", "Nena", "Nienke", "Nina", "Noa", "Noëlle",
|
|
||||||
"Noor", "Nora", "Nova",
|
|
||||||
|
|
||||||
"Olivia", "Olga",
|
|
||||||
|
|
||||||
"Pien", "Pleun",
|
|
||||||
|
|
||||||
"Quinty",
|
|
||||||
|
|
||||||
"Renske", "Renée", "Rianne", "Riek", "Rika", "Rina", "Rinske", "Rivka",
|
|
||||||
"Robin", "Romy", "Roos", "Roosmarijn", "Rosa", "Rosalie", "Rosanne",
|
|
||||||
"Rose", "Rowena", "Ruby",
|
|
||||||
|
|
||||||
"Sabine", "Sanne", "Sara", "Sarah", "Sarina", "Saskia", "Selma", "Senna",
|
|
||||||
"Sera", "Sharon", "Silke", "Sjoukje", "Sofie", "Sophie", "Soraya",
|
|
||||||
"Sterre", "Susan", "Susanne", "Suzan", "Suzanne", "Suze", "Sylvia",
|
|
||||||
|
|
||||||
"Tamar", "Tara", "Tess", "Tessa", "Teuntje", "Thea", "Theodora", "Thera",
|
|
||||||
"Theresa", "Thirza", "Tine", "Tineke", "Tirza", "Tjitske", "Toos",
|
|
||||||
"Trijntje", "Truus",
|
|
||||||
|
|
||||||
"Valerie", "Veerle", "Vera", "Vera-Lynn", "Veronique", "Victoria", "Vienna",
|
|
||||||
"Vieve", "Vita", "Vivian", "Vivienne",
|
|
||||||
|
|
||||||
"Wende", "Wendy", "Wiep", "Wietske", "Wies", "Wiesje", "Willeke", "Wilma",
|
|
||||||
|
|
||||||
"Yara", "Yfke", "Ymkje", "Yolanda", "Yuna",
|
|
||||||
|
|
||||||
"Zara", "Zoë", "Zofia", "Zonne", "Zwaantje",
|
|
||||||
]
|
|
||||||
|
|
||||||
def generate_associative_name(tags=None):
|
|
||||||
"""Return a random evocative name for a new group."""
|
|
||||||
return random.choice(_NAMES)
|
|
||||||
|
|
||||||
|
|
||||||
def clean_tag(tag):
|
|
||||||
return tag.replace("_", " ").strip()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_associative_description(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."""
|
|
||||||
return re.sub(r'^(\d{8}_\d{6}_)+', '', name)
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
"""
|
|
||||||
orbit_module.py — 2.5D actor orbit preview via depth-card parallax.
|
|
||||||
|
|
||||||
Pipeline:
|
|
||||||
1. load actor image — use provided path directly (selection is caller's responsibility)
|
|
||||||
2. create_depth_map — fake depth from alpha mask distance transform
|
|
||||||
3. find_bg_plate — static background for compositing (original for nobg; blurred for opaque)
|
|
||||||
4. render_orbit — per-frame: parallax-warp actor, composite over static bg
|
|
||||||
5. save_orbit_output — write RGBA PNGs + MP4
|
|
||||||
|
|
||||||
The "orbit" illusion requires a static reference (background plate).
|
|
||||||
Without it the viewer has nothing to anchor on and it reads as a side-slide.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from orbit_module import run_orbit_pipeline
|
|
||||||
result = run_orbit_pipeline(image_path, output_dir)
|
|
||||||
|
|
||||||
CLI: see orbit_poc.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import math
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
from scipy.ndimage import distance_transform_edt
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"create_depth_map",
|
|
||||||
"render_orbit_frame",
|
|
||||||
"render_orbit",
|
|
||||||
"save_orbit_output",
|
|
||||||
"run_orbit_pipeline",
|
|
||||||
]
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Image loading helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _load_rgba(path: str) -> np.ndarray:
|
|
||||||
"""Load any image as RGBA uint8 H×W×4."""
|
|
||||||
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
|
||||||
if img is None:
|
|
||||||
raise FileNotFoundError(f"Cannot read image: {path}")
|
|
||||||
if img.ndim == 2:
|
|
||||||
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGRA)
|
|
||||||
elif img.shape[2] == 3:
|
|
||||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
|
||||||
return cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
|
|
||||||
|
|
||||||
|
|
||||||
def _has_real_alpha(rgba: np.ndarray) -> bool:
|
|
||||||
"""True if the image contains meaningful transparency (not just all-255)."""
|
|
||||||
alpha = rgba[:, :, 3]
|
|
||||||
transparent_pct = float((alpha < 32).mean())
|
|
||||||
return transparent_pct > 0.05 # >5% transparent pixels = real alpha
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Background plate
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _find_original_for_nobg(actor_path: str) -> str | None:
|
|
||||||
"""
|
|
||||||
Given a .nobg.png sidecar path, find the original opaque image.
|
|
||||||
e.g. foo.nobg.png → foo.png or foo.jpg
|
|
||||||
"""
|
|
||||||
root, _ = os.path.splitext(actor_path)
|
|
||||||
if not root.endswith(".nobg"):
|
|
||||||
return None
|
|
||||||
base = root[: -len(".nobg")]
|
|
||||||
for ext in (".png", ".jpg", ".jpeg", ".webp"):
|
|
||||||
p = base + ext
|
|
||||||
if os.path.exists(p):
|
|
||||||
return p
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _make_bg_plate(actor_rgba: np.ndarray) -> np.ndarray:
|
|
||||||
"""
|
|
||||||
Build a static background plate for opaque images (no nobg available).
|
|
||||||
|
|
||||||
Strategy: blur the source image with a large kernel. The blurred copy stays
|
|
||||||
fixed while the sharp actor layer shifts — creates subtle depth separation.
|
|
||||||
"""
|
|
||||||
H, W = actor_rgba.shape[:2]
|
|
||||||
# Large blur: simulates out-of-focus background
|
|
||||||
blurred_rgb = cv2.GaussianBlur(actor_rgba[:, :, :3], (0, 0), max(H, W) * 0.04)
|
|
||||||
plate = np.dstack([blurred_rgb, np.full((H, W), 255, dtype=np.uint8)])
|
|
||||||
return plate.astype(np.uint8)
|
|
||||||
|
|
||||||
|
|
||||||
def get_bg_plate(actor_path: str, actor_rgba: np.ndarray) -> np.ndarray:
|
|
||||||
"""
|
|
||||||
Return the background plate for the orbit:
|
|
||||||
- .nobg.png: load the matching original opaque image (best result)
|
|
||||||
- Opaque image: return blurred copy (subtle but functional)
|
|
||||||
Plate is resized to match actor_rgba dimensions.
|
|
||||||
"""
|
|
||||||
H, W = actor_rgba.shape[:2]
|
|
||||||
|
|
||||||
orig = _find_original_for_nobg(actor_path)
|
|
||||||
if orig:
|
|
||||||
bg = _load_rgba(orig)
|
|
||||||
if bg.shape[:2] != (H, W):
|
|
||||||
bg = cv2.resize(bg, (W, H), interpolation=cv2.INTER_AREA)
|
|
||||||
bg[:, :, 3] = 255 # ensure fully opaque background
|
|
||||||
return bg
|
|
||||||
|
|
||||||
return _make_bg_plate(actor_rgba)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Depth map
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def create_depth_map(image_rgba: np.ndarray) -> np.ndarray:
|
|
||||||
"""
|
|
||||||
Float32 H×W depth in [0,1]. 1 = closest (subject centre), 0 = far/background.
|
|
||||||
|
|
||||||
Uses the alpha mask distance transform:
|
|
||||||
- For transparent-bg images: EDT of the foreground mask (subject body)
|
|
||||||
- For opaque images: EDT from image edges (assumes subject is centred)
|
|
||||||
|
|
||||||
Power-law shaping (^0.5) keeps the gradient gradual near the centre.
|
|
||||||
"""
|
|
||||||
alpha = image_rgba[:, :, 3]
|
|
||||||
mask = (alpha > 32).astype(np.uint8)
|
|
||||||
|
|
||||||
if mask.sum() == 0:
|
|
||||||
return np.zeros(alpha.shape, dtype=np.float32)
|
|
||||||
|
|
||||||
dist = distance_transform_edt(mask).astype(np.float32)
|
|
||||||
max_d = dist.max()
|
|
||||||
if max_d > 0:
|
|
||||||
dist /= max_d
|
|
||||||
return np.sqrt(dist)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Orbit rendering
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _alpha_composite(fg: np.ndarray, bg: np.ndarray) -> np.ndarray:
|
|
||||||
"""Alpha-composite RGBA fg over RGBA bg. Returns RGBA uint8."""
|
|
||||||
a = fg[:, :, 3:4].astype(np.float32) / 255.0
|
|
||||||
out_rgb = fg[:, :, :3].astype(np.float32) * a + bg[:, :, :3].astype(np.float32) * (1.0 - a)
|
|
||||||
out_a = fg[:, :, 3:4].astype(np.float32) + bg[:, :, 3:4].astype(np.float32) * (1.0 - a)
|
|
||||||
return np.dstack([out_rgb.clip(0, 255), out_a.clip(0, 255)]).astype(np.uint8)
|
|
||||||
|
|
||||||
|
|
||||||
def render_orbit_frame(
|
|
||||||
actor_rgba: np.ndarray,
|
|
||||||
depth: np.ndarray,
|
|
||||||
theta: float,
|
|
||||||
parallax_strength: float = 0.08,
|
|
||||||
bg_rgba: np.ndarray | None = None,
|
|
||||||
) -> np.ndarray:
|
|
||||||
"""
|
|
||||||
Swing-mode frame: depth-based parallax shift only.
|
|
||||||
Closer pixels (depth≈1) shift more than far pixels (depth≈0).
|
|
||||||
Composited over static bg plate for perceivable depth.
|
|
||||||
Returns RGBA uint8 H×W×4.
|
|
||||||
"""
|
|
||||||
H, W = actor_rgba.shape[:2]
|
|
||||||
|
|
||||||
shift_x = depth * (W * parallax_strength * math.sin(theta))
|
|
||||||
shift_y = depth * (H * parallax_strength * 0.03 * -math.cos(theta))
|
|
||||||
|
|
||||||
yc, xc = np.mgrid[0:H, 0:W].astype(np.float32)
|
|
||||||
map_x = (xc - shift_x).astype(np.float32)
|
|
||||||
map_y = (yc - shift_y).astype(np.float32)
|
|
||||||
|
|
||||||
bgra = cv2.cvtColor(actor_rgba, cv2.COLOR_RGBA2BGRA)
|
|
||||||
warped_bgra = cv2.remap(bgra, map_x, map_y,
|
|
||||||
interpolation=cv2.INTER_LINEAR,
|
|
||||||
borderMode=cv2.BORDER_CONSTANT,
|
|
||||||
borderValue=(0, 0, 0, 0))
|
|
||||||
warped = cv2.cvtColor(warped_bgra, cv2.COLOR_BGRA2RGBA)
|
|
||||||
|
|
||||||
if bg_rgba is None:
|
|
||||||
return warped
|
|
||||||
return _alpha_composite(warped, bg_rgba)
|
|
||||||
|
|
||||||
|
|
||||||
def _perspective_card_frame(
|
|
||||||
bgra_src: np.ndarray,
|
|
||||||
theta: float,
|
|
||||||
) -> np.ndarray:
|
|
||||||
"""
|
|
||||||
Orbit-mode frame: simulate a flat card rotating around its vertical axis.
|
|
||||||
|
|
||||||
- Squishes width by |cos(θ)|, centred on canvas
|
|
||||||
- Mirrors source for the back half (cos < 0) so the "back" is visible
|
|
||||||
- Returns BGRA with transparent borders (ready for bg composite)
|
|
||||||
|
|
||||||
At θ=0° → full width, unmirrored (front face)
|
|
||||||
At θ=90° → hair-thin line
|
|
||||||
At θ=180°→ full width, mirrored (back face)
|
|
||||||
"""
|
|
||||||
H, W = bgra_src.shape[:2]
|
|
||||||
cos_t = math.cos(theta)
|
|
||||||
compress = abs(cos_t)
|
|
||||||
is_back = cos_t < 0
|
|
||||||
|
|
||||||
if compress < 0.025:
|
|
||||||
# Near-edge-on: return a one-pixel-wide vertical strip to avoid singularity
|
|
||||||
out = np.zeros_like(bgra_src)
|
|
||||||
mid = W // 2
|
|
||||||
out[:, mid:mid+1] = bgra_src[:, mid:mid+1]
|
|
||||||
return out
|
|
||||||
|
|
||||||
new_w = max(int(round(W * compress)), 2)
|
|
||||||
x0 = (W - new_w) // 2
|
|
||||||
x1 = x0 + new_w
|
|
||||||
|
|
||||||
# Source corners: mirror left↔right for the back face
|
|
||||||
if is_back:
|
|
||||||
src = np.float32([[W, 0], [0, 0], [0, H], [W, H]])
|
|
||||||
else:
|
|
||||||
src = np.float32([[0, 0], [W, 0], [W, H], [0, H]])
|
|
||||||
|
|
||||||
dst = np.float32([[x0, 0], [x1, 0], [x1, H], [x0, H]])
|
|
||||||
|
|
||||||
M = cv2.getPerspectiveTransform(src, dst)
|
|
||||||
return cv2.warpPerspective(bgra_src, M, (W, H),
|
|
||||||
flags=cv2.INTER_LINEAR,
|
|
||||||
borderMode=cv2.BORDER_CONSTANT,
|
|
||||||
borderValue=(0, 0, 0, 0))
|
|
||||||
|
|
||||||
|
|
||||||
def render_orbit(
|
|
||||||
actor_rgba: np.ndarray,
|
|
||||||
depth: np.ndarray,
|
|
||||||
n_frames: int = 36,
|
|
||||||
parallax_strength: float = 0.08,
|
|
||||||
mode: str = "swing",
|
|
||||||
max_angle_deg: float = 35.0,
|
|
||||||
bg_rgba: np.ndarray | None = None,
|
|
||||||
) -> list:
|
|
||||||
"""
|
|
||||||
Render all orbit frames.
|
|
||||||
|
|
||||||
mode='swing' — sinusoidal ±max_angle_deg depth-parallax, loops cleanly
|
|
||||||
mode='orbit' — full 360° perspective card rotation (compress + mirror)
|
|
||||||
|
|
||||||
Returns list of RGBA uint8 frames.
|
|
||||||
"""
|
|
||||||
if mode == "swing":
|
|
||||||
max_rad = math.radians(max_angle_deg)
|
|
||||||
angles = [max_rad * math.sin(2 * math.pi * i / n_frames) for i in range(n_frames)]
|
|
||||||
return [render_orbit_frame(actor_rgba, depth, theta, parallax_strength, bg_rgba)
|
|
||||||
for theta in angles]
|
|
||||||
|
|
||||||
elif mode == "orbit":
|
|
||||||
# Don't use the photo bg plate — the transparent areas should stay transparent
|
|
||||||
# so the perspective compression (card getting thin at 90°, mirrored at 180°)
|
|
||||||
# is clearly visible. Solid bg is added at MP4 write time.
|
|
||||||
angles = [2 * math.pi * i / n_frames for i in range(n_frames)]
|
|
||||||
bgra_src = cv2.cvtColor(actor_rgba, cv2.COLOR_RGBA2BGRA)
|
|
||||||
return [
|
|
||||||
cv2.cvtColor(_perspective_card_frame(bgra_src, theta), cv2.COLOR_BGRA2RGBA)
|
|
||||||
for theta in angles
|
|
||||||
]
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown mode: {mode!r}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Output saving
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _composite_over_solid(frame_rgba: np.ndarray, bg: tuple = (18, 18, 18)) -> np.ndarray:
|
|
||||||
"""Alpha-composite RGBA over a solid colour; return BGR uint8 for ffmpeg."""
|
|
||||||
rgb = frame_rgba[:, :, :3].astype(np.float32)
|
|
||||||
a = frame_rgba[:, :, 3:4].astype(np.float32) / 255.0
|
|
||||||
bg_f = np.array(bg, dtype=np.float32)
|
|
||||||
out = (rgb * a + bg_f * (1.0 - a)).clip(0, 255).astype(np.uint8)
|
|
||||||
return cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
|
|
||||||
|
|
||||||
|
|
||||||
def save_orbit_output(
|
|
||||||
frames: list,
|
|
||||||
output_dir: str,
|
|
||||||
fps: int = 24,
|
|
||||||
bg_color: tuple = (18, 18, 18),
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
Write orbit_frames/frame_NNN.png (RGBA) and orbit_preview.mp4.
|
|
||||||
Returns dict with paths.
|
|
||||||
"""
|
|
||||||
frames_dir = os.path.join(output_dir, "orbit_frames")
|
|
||||||
os.makedirs(frames_dir, exist_ok=True)
|
|
||||||
|
|
||||||
frame_paths = []
|
|
||||||
for i, frame in enumerate(frames):
|
|
||||||
path = os.path.join(frames_dir, f"frame_{i:03d}.png")
|
|
||||||
cv2.imwrite(path, cv2.cvtColor(frame, cv2.COLOR_RGBA2BGRA))
|
|
||||||
frame_paths.append(path)
|
|
||||||
|
|
||||||
video_path = os.path.join(output_dir, "orbit_preview.mp4")
|
|
||||||
_frames_to_mp4(frames, video_path, fps=fps, bg_color=bg_color)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"frames_dir": frames_dir,
|
|
||||||
"n_frames": len(frames),
|
|
||||||
"video_path": video_path,
|
|
||||||
"frame_paths": frame_paths,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _frames_to_mp4(
|
|
||||||
frames: list, output_path: str, fps: int = 24, bg_color: tuple = (18, 18, 18)
|
|
||||||
) -> None:
|
|
||||||
"""Composite frames over solid bg, write MP4 via ffmpeg."""
|
|
||||||
if not frames:
|
|
||||||
return
|
|
||||||
with tempfile.TemporaryDirectory(prefix="orbit_mp4_") as tmpdir:
|
|
||||||
for i, frame in enumerate(frames):
|
|
||||||
bgr = _composite_over_solid(frame, bg_color)
|
|
||||||
cv2.imwrite(
|
|
||||||
os.path.join(tmpdir, f"frame_{i:04d}.jpg"), bgr,
|
|
||||||
[cv2.IMWRITE_JPEG_QUALITY, 95],
|
|
||||||
)
|
|
||||||
H, W = frames[0].shape[:2]
|
|
||||||
W2, H2 = W - (W % 2), H - (H % 2)
|
|
||||||
cmd = [
|
|
||||||
"ffmpeg", "-y",
|
|
||||||
"-framerate", str(fps),
|
|
||||||
"-i", os.path.join(tmpdir, "frame_%04d.jpg"),
|
|
||||||
"-vf", f"crop={W2}:{H2}:0:0",
|
|
||||||
"-c:v", "libx264", "-pix_fmt", "yuv420p",
|
|
||||||
"-crf", "18", "-movflags", "+faststart",
|
|
||||||
output_path,
|
|
||||||
]
|
|
||||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
if r.returncode != 0:
|
|
||||||
raise RuntimeError(f"ffmpeg failed: {r.stderr[-600:]}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Debug helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _save_debug(actor_rgba, actor_path, bg_rgba, debug_dir):
|
|
||||||
os.makedirs(debug_dir, exist_ok=True)
|
|
||||||
if os.path.exists(actor_path):
|
|
||||||
shutil.copy2(actor_path, os.path.join(debug_dir, "selected_frame.png"))
|
|
||||||
cv2.imwrite(os.path.join(debug_dir, "actor_rgba.png"),
|
|
||||||
cv2.cvtColor(actor_rgba, cv2.COLOR_RGBA2BGRA))
|
|
||||||
cv2.imwrite(os.path.join(debug_dir, "mask.png"), actor_rgba[:, :, 3])
|
|
||||||
if bg_rgba is not None:
|
|
||||||
cv2.imwrite(os.path.join(debug_dir, "bg_plate.png"),
|
|
||||||
cv2.cvtColor(bg_rgba, cv2.COLOR_RGBA2BGRA))
|
|
||||||
|
|
||||||
|
|
||||||
def _save_depth_debug(depth, debug_dir):
|
|
||||||
os.makedirs(debug_dir, exist_ok=True)
|
|
||||||
d8 = (depth * 255).astype(np.uint8)
|
|
||||||
cv2.imwrite(os.path.join(debug_dir, "depth.png"), d8)
|
|
||||||
cv2.imwrite(os.path.join(debug_dir, "depth_colorized.png"),
|
|
||||||
cv2.applyColorMap(d8, cv2.COLORMAP_MAGMA))
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Full pipeline
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def run_orbit_pipeline(
|
|
||||||
image_path: str,
|
|
||||||
output_dir: str,
|
|
||||||
n_frames: int = 36,
|
|
||||||
parallax_strength: float = 0.08,
|
|
||||||
mode: str = "swing",
|
|
||||||
fps: int = 24,
|
|
||||||
max_angle_deg: float = 35.0,
|
|
||||||
debug: bool = True,
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
Full pipeline: load → bg-plate → depth → render → save.
|
|
||||||
|
|
||||||
image_path: the specific image to orbit (caller selects; no sharpness heuristic)
|
|
||||||
Returns dict: actor_path, frames_dir, video_path, n_frames, debug_dir, has_alpha, has_bg
|
|
||||||
"""
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
debug_dir = os.path.join(output_dir, "debug")
|
|
||||||
|
|
||||||
# 1. Load actor — prefer nobg sidecar for cleaner depth
|
|
||||||
actor_path = image_path
|
|
||||||
root, _ = os.path.splitext(image_path)
|
|
||||||
nobg_candidate = root + ".nobg.png"
|
|
||||||
if not root.endswith(".nobg") and os.path.exists(nobg_candidate):
|
|
||||||
actor_path = nobg_candidate
|
|
||||||
|
|
||||||
actor_rgba = _load_rgba(actor_path)
|
|
||||||
has_alpha = _has_real_alpha(actor_rgba)
|
|
||||||
|
|
||||||
# 2. Background plate (static reference — essential for perceivable depth)
|
|
||||||
bg_rgba = get_bg_plate(actor_path, actor_rgba)
|
|
||||||
has_bg = bg_rgba is not None
|
|
||||||
|
|
||||||
if debug:
|
|
||||||
_save_debug(actor_rgba, actor_path, bg_rgba, debug_dir)
|
|
||||||
|
|
||||||
# 3. Depth map
|
|
||||||
depth = create_depth_map(actor_rgba)
|
|
||||||
|
|
||||||
if debug:
|
|
||||||
_save_depth_debug(depth, debug_dir)
|
|
||||||
|
|
||||||
# 4. Render
|
|
||||||
frames = render_orbit(
|
|
||||||
actor_rgba, depth,
|
|
||||||
n_frames=n_frames,
|
|
||||||
parallax_strength=parallax_strength,
|
|
||||||
mode=mode,
|
|
||||||
max_angle_deg=max_angle_deg,
|
|
||||||
bg_rgba=bg_rgba,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. Save
|
|
||||||
result = save_orbit_output(frames, output_dir, fps=fps)
|
|
||||||
result.update({
|
|
||||||
"actor_path": actor_path,
|
|
||||||
"debug_dir": debug_dir,
|
|
||||||
"has_alpha": has_alpha,
|
|
||||||
"has_bg": has_bg,
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
orbit_poc.py — 2.5D actor orbit preview proof-of-concept.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python orbit_poc.py --input img1.png img2.png ... --output ./output
|
|
||||||
python orbit_poc.py --input ./filmstrip_images/ --output ./output --mode swing --frames 36
|
|
||||||
|
|
||||||
Output:
|
|
||||||
./output/orbit_frames/frame_NNN.png
|
|
||||||
./output/orbit_preview.mp4
|
|
||||||
./output/debug/selected_frame.png
|
|
||||||
./output/debug/actor_rgba.png
|
|
||||||
./output/debug/mask.png
|
|
||||||
./output/debug/depth.png
|
|
||||||
./output/debug/depth_colorized.png
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
# Ensure tour-comfy is on the path when running from project root
|
|
||||||
_here = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
if _here not in sys.path:
|
|
||||||
sys.path.insert(0, _here)
|
|
||||||
|
|
||||||
from orbit_module import run_orbit_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff", ".tif"}
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_inputs(raw_inputs: list) -> list:
|
|
||||||
"""Expand dirs and glob patterns; return sorted list of image paths."""
|
|
||||||
paths = []
|
|
||||||
for item in raw_inputs:
|
|
||||||
if os.path.isdir(item):
|
|
||||||
for fname in sorted(os.listdir(item)):
|
|
||||||
if os.path.splitext(fname)[1].lower() in _IMAGE_EXTS:
|
|
||||||
paths.append(os.path.join(item, fname))
|
|
||||||
elif "*" in item or "?" in item:
|
|
||||||
paths.extend(sorted(glob.glob(item)))
|
|
||||||
elif os.path.isfile(item):
|
|
||||||
paths.append(item)
|
|
||||||
else:
|
|
||||||
print(f"[warn] not found: {item}", file=sys.stderr)
|
|
||||||
return paths
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Generate a 2.5D orbit preview (depth-card parallax) from actor images."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--input", "-i", nargs="+", required=True,
|
|
||||||
metavar="PATH",
|
|
||||||
help="Input image paths, glob patterns, or directories",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--output", "-o", default="./output",
|
|
||||||
metavar="DIR",
|
|
||||||
help="Output directory (default: ./output)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--frames", "-f", type=int, default=36,
|
|
||||||
help="Number of animation frames (default: 36)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--fps", type=int, default=24,
|
|
||||||
help="Output video framerate (default: 24)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--parallax", "-p", type=float, default=0.08,
|
|
||||||
help="Parallax strength 0–1, fraction of image width (default: 0.08)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--angle", "-a", type=float, default=35.0,
|
|
||||||
help="Maximum orbit angle in degrees for swing mode (default: 35)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--mode", "-m", choices=["swing", "orbit"], default="swing",
|
|
||||||
help="'swing' = left↔right loop (default), 'orbit' = full 360°",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--no-debug", action="store_true",
|
|
||||||
help="Skip writing debug output files",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
image_paths = _collect_inputs(args.input)
|
|
||||||
if not image_paths:
|
|
||||||
print("[error] No input images found.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(f"[orbit] {len(image_paths)} input image(s)")
|
|
||||||
for p in image_paths[:5]:
|
|
||||||
print(f" {p}")
|
|
||||||
if len(image_paths) > 5:
|
|
||||||
print(f" ... ({len(image_paths) - 5} more)")
|
|
||||||
|
|
||||||
print(f"[orbit] output dir : {os.path.abspath(args.output)}")
|
|
||||||
print(f"[orbit] frames={args.frames} fps={args.fps} mode={args.mode} "
|
|
||||||
f"parallax={args.parallax} angle±{args.angle}°")
|
|
||||||
|
|
||||||
# Use the first image as the primary input (CLI can pass multiple; first = best choice)
|
|
||||||
primary = image_paths[0]
|
|
||||||
if len(image_paths) > 1:
|
|
||||||
print(f"[orbit] using first image as primary: {primary}")
|
|
||||||
print(f" (pass a single image or the specific frame you want)")
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
result = run_orbit_pipeline(
|
|
||||||
image_path=primary,
|
|
||||||
output_dir=args.output,
|
|
||||||
n_frames=args.frames,
|
|
||||||
parallax_strength=args.parallax,
|
|
||||||
mode=args.mode,
|
|
||||||
fps=args.fps,
|
|
||||||
max_angle_deg=args.angle,
|
|
||||||
debug=not args.no_debug,
|
|
||||||
)
|
|
||||||
elapsed = time.perf_counter() - t0
|
|
||||||
|
|
||||||
print(f"\n[orbit] done in {elapsed:.1f}s")
|
|
||||||
print(f" actor : {result['actor_path']}")
|
|
||||||
print(f" has alpha : {result['has_alpha']}")
|
|
||||||
print(f" has bg plate : {result['has_bg']}")
|
|
||||||
print(f" frames dir : {result['frames_dir']} ({result['n_frames']} PNGs)")
|
|
||||||
print(f" video : {result['video_path']}")
|
|
||||||
if not args.no_debug:
|
|
||||||
print(f" debug dir : {result['debug_dir']}")
|
|
||||||
if not result['has_alpha']:
|
|
||||||
print(f"\n TIP: Use 'No BG' on this image first for a much better orbit effect.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
"""
|
|
||||||
orbit_qwen.py — near-real actor turntable using Qwen-Image-Edit.
|
|
||||||
|
|
||||||
Unlike orbit_module.py (fake 2.5D depth-card parallax), this actually asks the
|
|
||||||
generative model to RE-RENDER the subject at each yaw angle. Each view is
|
|
||||||
anchored to the original front image with a fixed seed so identity, body, hair
|
|
||||||
and lighting stay consistent while only the viewpoint rotates.
|
|
||||||
|
|
||||||
Pipeline:
|
|
||||||
1. build a yaw-angle prompt per frame (turntable or swing)
|
|
||||||
2. _run_pipeline (Qwen via ComfyUI) → one re-rendered view per angle
|
|
||||||
3. bottom-center align onto a common canvas
|
|
||||||
4. stitch to a looping MP4
|
|
||||||
|
|
||||||
Validated finding (2026-06-25): 2D blending between independently-generated
|
|
||||||
views (optical-flow morph OR crossfade) always ghosts — the bodies don't
|
|
||||||
overlap, so any in-between frame shows a double exposure. The cure is DENSITY,
|
|
||||||
not blending: ~24 crisp keyframes (15° steps) played with NO interpolation at
|
|
||||||
~12fps reads as a smooth turntable, exactly like classic 3D turntable GIFs.
|
|
||||||
Interpolation is kept available (interp_factor>1) but defaults OFF.
|
|
||||||
|
|
||||||
Reuses edit_api._run_pipeline, so it talks to the same running ComfyUI server.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from orbit_qwen import run_qwen_orbit
|
|
||||||
result = run_qwen_orbit("/path/to/front.png", "/out/dir", n_views=12)
|
|
||||||
|
|
||||||
CLI: see orbit_qwen_poc.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import io
|
|
||||||
import sys
|
|
||||||
import math
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
# Reuse the real Qwen pipeline from the API service (no server round-trip needed;
|
|
||||||
# _run_pipeline queues directly to ComfyUI). Import is cheap — only loads the
|
|
||||||
# workflow JSON; models load lazily and the uvicorn startup hook does not fire.
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
if _HERE not in sys.path:
|
|
||||||
sys.path.insert(0, _HERE)
|
|
||||||
|
|
||||||
from edit_api import _run_pipeline, _load_output_dir, MAX_AREA # noqa: E402
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"yaw_prompt",
|
|
||||||
"generate_views",
|
|
||||||
"interpolate_views",
|
|
||||||
"build_video",
|
|
||||||
"run_qwen_orbit",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 1. Prompt construction
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Identity lock appended to every angle — this is what keeps it "the same person".
|
|
||||||
_IDENTITY = (
|
|
||||||
"exactly the same woman, identical face, identical body shape and proportions, "
|
|
||||||
"same hair, same skin tone, same lighting, photorealistic, sharp focus, "
|
|
||||||
"full body visible head to feet, centered, transparent background"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _angle_phrase(deg: float) -> str:
|
|
||||||
"""
|
|
||||||
Natural-language viewpoint for a yaw angle (turntable; subject rotates
|
|
||||||
clockwise as deg increases). 0 = facing camera, 180 = facing away.
|
|
||||||
"""
|
|
||||||
d = deg % 360
|
|
||||||
# Bucket to the nearest named viewpoint for the clearest model instruction,
|
|
||||||
# then add the precise degree as reinforcement.
|
|
||||||
if d < 22.5 or d >= 337.5:
|
|
||||||
view = "facing the camera directly, front view"
|
|
||||||
elif d < 67.5:
|
|
||||||
view = "turned slightly to her right, three-quarter front-right view"
|
|
||||||
elif d < 112.5:
|
|
||||||
view = "full right-side profile, body turned 90 degrees"
|
|
||||||
elif d < 157.5:
|
|
||||||
view = "three-quarter rear view from behind-right, back partially visible"
|
|
||||||
elif d < 202.5:
|
|
||||||
view = "facing directly away from the camera, full back view, back of head and back visible"
|
|
||||||
elif d < 247.5:
|
|
||||||
view = "three-quarter rear view from behind-left, back partially visible"
|
|
||||||
elif d < 292.5:
|
|
||||||
view = "full left-side profile, body turned 90 degrees"
|
|
||||||
else:
|
|
||||||
view = "turned slightly to her left, three-quarter front-left view"
|
|
||||||
return view
|
|
||||||
|
|
||||||
|
|
||||||
def yaw_prompt(deg: float) -> str:
|
|
||||||
"""Full prompt for one turntable angle."""
|
|
||||||
view = _angle_phrase(deg)
|
|
||||||
return (
|
|
||||||
f"Rotate the camera around the subject to a {int(deg % 360)} degree turntable angle: "
|
|
||||||
f"{view}. The subject stands still in a neutral standing pose; only the viewing "
|
|
||||||
f"angle changes, like a 3D turntable. {_IDENTITY}."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _angles_for(mode: str, n_views: int, sweep_deg: float) -> list:
|
|
||||||
"""Return the list of yaw angles to render."""
|
|
||||||
if mode == "turntable":
|
|
||||||
# Full 360, evenly spaced, loops cleanly
|
|
||||||
return [360.0 * i / n_views for i in range(n_views)]
|
|
||||||
elif mode == "swing":
|
|
||||||
# -sweep/2 .. +sweep/2 .. back (front-facing arc only — most reliable)
|
|
||||||
half = sweep_deg / 2.0
|
|
||||||
fwd = [(-half + sweep_deg * i / (n_views - 1)) for i in range(n_views)]
|
|
||||||
# map negatives into 0..360 turntable space (e.g. -45 -> 315)
|
|
||||||
return [a % 360 for a in fwd]
|
|
||||||
raise ValueError(f"Unknown mode: {mode!r}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 2. View generation (Qwen)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _autocrop_alpha(pil: Image.Image, pad: int = 8) -> Image.Image:
|
|
||||||
"""Crop to the alpha bounding box (+pad) so every view is framed on the body."""
|
|
||||||
if pil.mode != "RGBA":
|
|
||||||
return pil
|
|
||||||
alpha = np.array(pil)[:, :, 3]
|
|
||||||
ys, xs = np.where(alpha > 16)
|
|
||||||
if len(xs) == 0:
|
|
||||||
return pil
|
|
||||||
x0, x1 = max(0, xs.min() - pad), min(pil.width, xs.max() + pad)
|
|
||||||
y0, y1 = max(0, ys.min() - pad), min(pil.height, ys.max() + pad)
|
|
||||||
return pil.crop((x0, y0, x1, y1))
|
|
||||||
|
|
||||||
|
|
||||||
def generate_views(
|
|
||||||
image_path: str,
|
|
||||||
output_dir: str,
|
|
||||||
n_views: int = 12,
|
|
||||||
seed: int = 42,
|
|
||||||
mode: str = "turntable",
|
|
||||||
sweep_deg: float = 180.0,
|
|
||||||
anchor: str = "original",
|
|
||||||
max_area: int = 0,
|
|
||||||
steps: int = 8,
|
|
||||||
on_progress=None,
|
|
||||||
) -> list:
|
|
||||||
"""
|
|
||||||
Render one Qwen view per yaw angle.
|
|
||||||
|
|
||||||
anchor='original' — every view edits the SAME front image (stable identity)
|
|
||||||
anchor='chain' — each view edits the previous result (smoother transitions,
|
|
||||||
but identity can drift over a full turn)
|
|
||||||
|
|
||||||
Returns list of dicts: {deg, path, pil}.
|
|
||||||
"""
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
views_dir = os.path.join(output_dir, "views")
|
|
||||||
os.makedirs(views_dir, exist_ok=True)
|
|
||||||
|
|
||||||
base_pil = Image.open(image_path).convert("RGB")
|
|
||||||
angles = _angles_for(mode, n_views, sweep_deg)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
prev_pil = None
|
|
||||||
for i, deg in enumerate(angles):
|
|
||||||
src_pil = base_pil if anchor == "original" or prev_pil is None else prev_pil
|
|
||||||
prompt = yaw_prompt(deg)
|
|
||||||
if on_progress:
|
|
||||||
on_progress(i, len(angles), deg)
|
|
||||||
|
|
||||||
png = _run_pipeline(
|
|
||||||
src_pil, prompt, seed,
|
|
||||||
max_area or MAX_AREA,
|
|
||||||
steps=steps,
|
|
||||||
)
|
|
||||||
view_pil = Image.open(io.BytesIO(png)).convert("RGBA")
|
|
||||||
view_pil = _autocrop_alpha(view_pil)
|
|
||||||
|
|
||||||
path = os.path.join(views_dir, f"view_{i:03d}_{int(deg):03d}deg.png")
|
|
||||||
view_pil.save(path)
|
|
||||||
results.append({"deg": deg, "path": path, "pil": view_pil})
|
|
||||||
|
|
||||||
if anchor == "chain":
|
|
||||||
# Feed an RGB version forward (pipeline wants RGB anyway)
|
|
||||||
prev_pil = view_pil.convert("RGB")
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 3. Smoothing — canvas-align + optical-flow interpolation
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _to_common_canvas(views: list, pad_frac: float = 0.12) -> list:
|
|
||||||
"""
|
|
||||||
Place every view on one fixed-size RGBA canvas, bottom-centered (feet anchored),
|
|
||||||
so the body doesn't jump frame-to-frame. Returns list of HxWx4 uint8 arrays.
|
|
||||||
"""
|
|
||||||
H = max(v["pil"].height for v in views)
|
|
||||||
W = max(v["pil"].width for v in views)
|
|
||||||
padH, padW = int(H * pad_frac), int(W * pad_frac)
|
|
||||||
CH, CW = H + 2 * padH, W + 2 * padW
|
|
||||||
|
|
||||||
out = []
|
|
||||||
for v in views:
|
|
||||||
p = v["pil"]
|
|
||||||
canvas = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
|
||||||
# bottom-centered: feet sit on a common baseline
|
|
||||||
x = (CW - p.width) // 2
|
|
||||||
y = CH - padH - p.height
|
|
||||||
canvas.paste(p, (x, y), p)
|
|
||||||
out.append(np.array(canvas))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _flow_morph_rgb(a: np.ndarray, b: np.ndarray, t: float) -> np.ndarray:
|
|
||||||
"""
|
|
||||||
Optical-flow morph between two SOLID RGB frames (3-channel) at fraction t.
|
|
||||||
Operates on composited-over-bg images so there is no alpha halo/ghost.
|
|
||||||
Warps a→mid and b→mid, then blends.
|
|
||||||
"""
|
|
||||||
ag = cv2.cvtColor(a, cv2.COLOR_RGB2GRAY)
|
|
||||||
bg = cv2.cvtColor(b, cv2.COLOR_RGB2GRAY)
|
|
||||||
flow_ab = cv2.calcOpticalFlowFarneback(ag, bg, None, 0.5, 5, 31, 5, 7, 1.5, 0)
|
|
||||||
flow_ba = cv2.calcOpticalFlowFarneback(bg, ag, None, 0.5, 5, 31, 5, 7, 1.5, 0)
|
|
||||||
|
|
||||||
H, W = ag.shape
|
|
||||||
yc, xc = np.mgrid[0:H, 0:W].astype(np.float32)
|
|
||||||
wa = cv2.remap(a, (xc + flow_ab[..., 0] * t), (yc + flow_ab[..., 1] * t),
|
|
||||||
cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
|
||||||
wb = cv2.remap(b, (xc + flow_ba[..., 0] * (1 - t)), (yc + flow_ba[..., 1] * (1 - t)),
|
|
||||||
cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
|
||||||
return (wa.astype(np.float32) * (1 - t) + wb.astype(np.float32) * t).clip(0, 255).astype(np.uint8)
|
|
||||||
|
|
||||||
|
|
||||||
def interpolate_views(
|
|
||||||
views: list,
|
|
||||||
factor: int = 4,
|
|
||||||
loop: bool = True,
|
|
||||||
smooth: bool = True,
|
|
||||||
bg: tuple = (18, 18, 18),
|
|
||||||
) -> list:
|
|
||||||
"""
|
|
||||||
Expand keyframes into a smooth sequence.
|
|
||||||
|
|
||||||
Keyframes are first composited over the solid bg, so all blending happens
|
|
||||||
in opaque RGB space — this removes the transparent-alpha ghosting that
|
|
||||||
plagued earlier flow morphs.
|
|
||||||
|
|
||||||
factor — intermediate frames per keyframe pair (1 = keyframes only)
|
|
||||||
loop — also blend last→first (seamless turntable)
|
|
||||||
smooth — optical-flow morph (True) vs simple crossfade (False)
|
|
||||||
|
|
||||||
Returns list of HxWx3 uint8 RGB frames.
|
|
||||||
"""
|
|
||||||
canvases = _to_common_canvas(views)
|
|
||||||
bg_arr = np.array(bg, dtype=np.float32)
|
|
||||||
|
|
||||||
def _flatten(rgba):
|
|
||||||
a = rgba[:, :, 3:4].astype(np.float32) / 255.0
|
|
||||||
return (rgba[:, :, :3].astype(np.float32) * a + bg_arr * (1 - a)).clip(0, 255).astype(np.uint8)
|
|
||||||
|
|
||||||
solid = [_flatten(c) for c in canvases]
|
|
||||||
if factor <= 1:
|
|
||||||
return solid
|
|
||||||
|
|
||||||
n = len(solid)
|
|
||||||
pairs = n if loop else n - 1
|
|
||||||
frames = []
|
|
||||||
for i in range(pairs):
|
|
||||||
a, b = solid[i], solid[(i + 1) % n]
|
|
||||||
frames.append(a)
|
|
||||||
for k in range(1, factor):
|
|
||||||
t = k / factor
|
|
||||||
if smooth:
|
|
||||||
frames.append(_flow_morph_rgb(a, b, t))
|
|
||||||
else:
|
|
||||||
frames.append((a.astype(np.float32) * (1 - t) +
|
|
||||||
b.astype(np.float32) * t).astype(np.uint8))
|
|
||||||
if not loop:
|
|
||||||
frames.append(solid[-1])
|
|
||||||
return frames
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 4. Video
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _composite_solid(frame: np.ndarray, bg=(18, 18, 18)) -> np.ndarray:
|
|
||||||
"""Accept RGB (already flattened) or RGBA; return BGR for ffmpeg."""
|
|
||||||
if frame.shape[2] == 3:
|
|
||||||
return cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
|
||||||
rgb = frame[:, :, :3].astype(np.float32)
|
|
||||||
a = frame[:, :, 3:4].astype(np.float32) / 255.0
|
|
||||||
bg_f = np.array(bg, dtype=np.float32)
|
|
||||||
out = (rgb * a + bg_f * (1 - a)).clip(0, 255).astype(np.uint8)
|
|
||||||
return cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
|
|
||||||
|
|
||||||
|
|
||||||
def build_video(frames: list, output_path: str, fps: int = 24, bg=(18, 18, 18)) -> None:
|
|
||||||
if not frames:
|
|
||||||
return
|
|
||||||
with tempfile.TemporaryDirectory(prefix="orbit_qwen_") as tmp:
|
|
||||||
for i, fr in enumerate(frames):
|
|
||||||
cv2.imwrite(os.path.join(tmp, f"f_{i:04d}.jpg"),
|
|
||||||
_composite_solid(fr, bg), [cv2.IMWRITE_JPEG_QUALITY, 95])
|
|
||||||
H, W = frames[0].shape[:2]
|
|
||||||
W2, H2 = W - (W % 2), H - (H % 2)
|
|
||||||
cmd = [
|
|
||||||
"ffmpeg", "-y", "-framerate", str(fps),
|
|
||||||
"-i", os.path.join(tmp, "f_%04d.jpg"),
|
|
||||||
"-vf", f"crop={W2}:{H2}:0:0",
|
|
||||||
"-c:v", "libx264", "-pix_fmt", "yuv420p",
|
|
||||||
"-crf", "18", "-movflags", "+faststart", output_path,
|
|
||||||
]
|
|
||||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
if r.returncode != 0:
|
|
||||||
raise RuntimeError(f"ffmpeg failed: {r.stderr[-600:]}")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 5. Orchestration
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def run_qwen_orbit(
|
|
||||||
image_path: str,
|
|
||||||
output_dir: str,
|
|
||||||
n_views: int = 24,
|
|
||||||
seed: int = 42,
|
|
||||||
mode: str = "turntable",
|
|
||||||
sweep_deg: float = 180.0,
|
|
||||||
anchor: str = "original",
|
|
||||||
interp_factor: int = 1,
|
|
||||||
smooth: bool = False,
|
|
||||||
fps: int = 12,
|
|
||||||
max_area: int = 0,
|
|
||||||
steps: int = 8,
|
|
||||||
on_progress=None,
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
Full near-real turntable: generate Qwen views → align → MP4.
|
|
||||||
|
|
||||||
Defaults reflect the validated recipe: 24 crisp keyframes, NO blending,
|
|
||||||
12fps. Raise interp_factor only if you accept morph ghosting.
|
|
||||||
|
|
||||||
Returns dict: views (list), n_views, n_frames, video_path, views_dir.
|
|
||||||
"""
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
|
|
||||||
views = generate_views(
|
|
||||||
image_path, output_dir,
|
|
||||||
n_views=n_views, seed=seed, mode=mode, sweep_deg=sweep_deg,
|
|
||||||
anchor=anchor, max_area=max_area, steps=steps, on_progress=on_progress,
|
|
||||||
)
|
|
||||||
|
|
||||||
loop = (mode == "turntable")
|
|
||||||
frames = interpolate_views(views, factor=interp_factor, loop=loop, smooth=smooth)
|
|
||||||
|
|
||||||
video_path = os.path.join(output_dir, "turntable.mp4")
|
|
||||||
build_video(frames, video_path, fps=fps)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"views": [{"deg": v["deg"], "path": v["path"]} for v in views],
|
|
||||||
"n_views": len(views),
|
|
||||||
"n_frames": len(frames),
|
|
||||||
"video_path": video_path,
|
|
||||||
"views_dir": os.path.join(output_dir, "views"),
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
orbit_qwen_poc.py — near-real turntable test via Qwen-Image-Edit.
|
|
||||||
|
|
||||||
python orbit_qwen_poc.py --input front.png --output ./out \
|
|
||||||
--views 12 --mode turntable --interp 4 --seed 42
|
|
||||||
|
|
||||||
Generates one re-rendered view per yaw angle (anchored to the input, fixed seed),
|
|
||||||
flow-interpolates for smoothness, and stitches a looping MP4.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
if _HERE not in sys.path:
|
|
||||||
sys.path.insert(0, _HERE)
|
|
||||||
|
|
||||||
from orbit_qwen import run_qwen_orbit
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ap = argparse.ArgumentParser(description="Qwen turntable (near-real subject turning).")
|
|
||||||
ap.add_argument("--input", "-i", required=True, help="Front-facing source image")
|
|
||||||
ap.add_argument("--output", "-o", default="./out_turntable", help="Output directory")
|
|
||||||
ap.add_argument("--views", "-v", type=int, default=24, help="Qwen keyframes (yaw steps)")
|
|
||||||
ap.add_argument("--mode", "-m", choices=["turntable", "swing"], default="turntable",
|
|
||||||
help="turntable=full 360 loop, swing=front-facing arc only")
|
|
||||||
ap.add_argument("--sweep", type=float, default=180.0, help="swing arc width in degrees")
|
|
||||||
ap.add_argument("--anchor", choices=["original", "chain"], default="original",
|
|
||||||
help="original=each view from source (stable), chain=from previous (smoother)")
|
|
||||||
ap.add_argument("--interp", type=int, default=1,
|
|
||||||
help="interpolated frames per keyframe gap (1=none; >1 ghosts, not advised)")
|
|
||||||
ap.add_argument("--no-smooth", action="store_true", help="crossfade instead of optical-flow morph")
|
|
||||||
ap.add_argument("--fps", type=int, default=12)
|
|
||||||
ap.add_argument("--seed", type=int, default=42)
|
|
||||||
ap.add_argument("--steps", type=int, default=8, help="Qwen sampler steps (4 fast, 8 nicer)")
|
|
||||||
ap.add_argument("--max-area", type=int, default=0, help="output pixel budget (0=API default)")
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
if not os.path.exists(args.input):
|
|
||||||
print(f"[error] input not found: {args.input}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def prog(i, n, deg):
|
|
||||||
print(f" [{i+1}/{n}] rendering {int(deg):3d}°…", flush=True)
|
|
||||||
|
|
||||||
print(f"[turntable] input : {args.input}")
|
|
||||||
print(f"[turntable] mode={args.mode} views={args.views} anchor={args.anchor} "
|
|
||||||
f"interp×{args.interp} seed={args.seed} steps={args.steps}")
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
res = run_qwen_orbit(
|
|
||||||
image_path=args.input, output_dir=args.output,
|
|
||||||
n_views=args.views, seed=args.seed, mode=args.mode, sweep_deg=args.sweep,
|
|
||||||
anchor=args.anchor, interp_factor=args.interp, smooth=not args.no_smooth,
|
|
||||||
fps=args.fps, max_area=args.max_area, steps=args.steps, on_progress=prog,
|
|
||||||
)
|
|
||||||
dt = time.perf_counter() - t0
|
|
||||||
|
|
||||||
print(f"\n[turntable] done in {dt:.1f}s "
|
|
||||||
f"({dt/max(1,res['n_views']):.1f}s/view)")
|
|
||||||
print(f" keyframes : {res['n_views']} → {res['views_dir']}")
|
|
||||||
print(f" frames : {res['n_frames']} (after interpolation)")
|
|
||||||
print(f" video : {res['video_path']}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
4546
tour-comfy/poses.md
4546
tour-comfy/poses.md
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# privacy-lock-watcher.sh
|
|
||||||
# Listens for systemd-logind "Session locked" signals via D-Bus and minimises
|
|
||||||
# any browser window showing the gallery (127.0.0.1:8500 or localhost:8500).
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./privacy-lock-watcher.sh &
|
|
||||||
#
|
|
||||||
# Requirements: dbus-monitor (dbus-tools), wmctrl or xdotool
|
|
||||||
#
|
|
||||||
# Home Assistant integration idea:
|
|
||||||
# HA → shell_command: ssh user@machine "loginctl lock-session"
|
|
||||||
# That triggers the D-Bus event below, which minimises the browser.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
GALLERY_PATTERN="127.0.0.1:8500\|localhost:8500\|tour-comfy"
|
|
||||||
|
|
||||||
minimize_gallery() {
|
|
||||||
# Try xdotool first (works with most WMs)
|
|
||||||
if command -v xdotool &>/dev/null; then
|
|
||||||
xdotool search --name "$GALLERY_PATTERN" windowminimize --sync 2>/dev/null || true
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
# Fallback: wmctrl
|
|
||||||
if command -v wmctrl &>/dev/null; then
|
|
||||||
wmctrl -r "$GALLERY_PATTERN" -b add,hidden 2>/dev/null || true
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo "[privacy-lock-watcher] No window manager tool found (install xdotool or wmctrl)" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "[privacy-lock-watcher] Listening for lock events on D-Bus..."
|
|
||||||
|
|
||||||
dbus-monitor --system "type='signal',interface='org.freedesktop.login1.Session'" 2>/dev/null \
|
|
||||||
| while IFS= read -r line; do
|
|
||||||
if echo "$line" | grep -q '"Lock"'; then
|
|
||||||
echo "[privacy-lock-watcher] Lock detected — minimising gallery window"
|
|
||||||
minimize_gallery
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
{
|
|
||||||
"img_32.png": "4d5ca98a255d51d5775725136f13a162",
|
|
||||||
"img_24.png": "942d5409a0e4c70b4f960bc6468269e2",
|
|
||||||
"img_78.png": "15e83866d2f2afb2ee1e8d2a06a70b9d",
|
|
||||||
"img_44.png": "6196e5ba2bb6fb46459c44543e0eeb64",
|
|
||||||
"img_41.png": "8871248c6da117c9b4318bcc6942c17a",
|
|
||||||
"img_55.png": "ce7e90270592666da95b0c638d10d745",
|
|
||||||
"img_23.png": "2ab193e0c19cec124764f9f54383ef2d",
|
|
||||||
"img_53.png": "ce7e90270592666da95b0c638d10d745",
|
|
||||||
"img_56.png": "1501a3128f19392124e727121a8f2bd4",
|
|
||||||
"img_30.png": "d907cf83d022af0545b55842f2573581",
|
|
||||||
"img_58.png": "4fae0cf0017c9be9a550792167d60ce3",
|
|
||||||
"img_79.png": "ab18eb4ce243db0f81f608d744596885",
|
|
||||||
"imgxx.png": "861b244b0aa205b8fdff4015a128a5a4",
|
|
||||||
"img_80.png": "de8dfcb0d6eeda8187783e8417f04b8b",
|
|
||||||
"img_21.png": "82660461b10e0545165fe8dade87d4d2",
|
|
||||||
"img_72.png": "ebd8cd52977b7fccc6ecff7ee2f392b5",
|
|
||||||
"img_28.png": "37a7ac25c45ec63a87e04c9fe3b73aab",
|
|
||||||
"img_73.png": "28c91d6bc883ed6de89b62755d5417b9",
|
|
||||||
"img_88.png": "7a22effc45850ecd53670827e0608e97",
|
|
||||||
"img_16.png": "9c76b232aacf1f18739b323f5b6887d7",
|
|
||||||
"img_63.png": "b454d1869a8fc62ed1dc988467b541db",
|
|
||||||
"img_3.png": "3c87ca4219c2ed6f275c044fa888afc0",
|
|
||||||
"img_18.png": "5a50839c5d5a726b5b0e770bb266f6d6",
|
|
||||||
"img_22.png": "4c9adb619b5f191d9b76d1493df211c6",
|
|
||||||
"img_77.png": "6db31602d0ed36abc2d92a88fdcc21b0",
|
|
||||||
"image.png": "7ad3913d8fea47ea120a8e958dd2fc62",
|
|
||||||
"img_71.png": "b3e79f4ff2882360b24f77a295f16650",
|
|
||||||
"20150913_211324.jpg": "bb75b92835207e81287a392f17f88eaf",
|
|
||||||
"img_52.png": "935946efb74fc333f41146691a61cc8f",
|
|
||||||
"img_54.png": "0d70e4782c2823f2f8d2b4c149a38e0b",
|
|
||||||
"img_2.png": "2a721fdedc31f4ee11fd7c8ab85a4b33",
|
|
||||||
"img_6.png": "7ad3913d8fea47ea120a8e958dd2fc62",
|
|
||||||
"imgxxxx.png": "8965337a6abd7bfec6cb774978b4198b",
|
|
||||||
"img_48.png": "b6daebc286bc1c22a586ee1233c5b420",
|
|
||||||
"img_66.png": "59e227fc6b06a2cd27659a9facf43c0d",
|
|
||||||
"img_50.png": "6f8b764e0973cb5c34b0c02f79b202ca",
|
|
||||||
"20160903_200935.jpg": "1cf5a582c8a40610640898ebaee2ade3",
|
|
||||||
"img_62.png": "c2c444a31001421a69dbc9ecf8588149",
|
|
||||||
"img_82.png": "724fcb641da5f3294aa800ce0a9b93e4",
|
|
||||||
"img_57.png": "514f89464e3c79bba7928b69ed01650e",
|
|
||||||
"img_7.png": "2ad545906c6b99cac1a78305a37a5eed",
|
|
||||||
"img_67.png": "1970b9952e14688c22f8f54ea7d7b4ec",
|
|
||||||
"img_37.png": "fefa5a1bb755fb0d4f03d966ac04dae3",
|
|
||||||
"img_6v1.png": "7ad3913d8fea47ea120a8e958dd2fc62",
|
|
||||||
"img_26.png": "23ef7f416f21ca3d5d237ecfdc88833e",
|
|
||||||
"img_75.png": "7b405fc4a4e022a272e5f09c7c485712",
|
|
||||||
"img_64.png": "d5b235b57e6ec07e790f35b9cda399fb",
|
|
||||||
"img_31.png": "312874252c58cbb89c8cad46bc17e7e7",
|
|
||||||
"img_45.png": "2329402c6d8c5b7555a7432eb1aeccd9",
|
|
||||||
"img_68.png": "4c7fbe72509ab5b08ccdab726b5ff035",
|
|
||||||
"img_29.png": "5ed1e8acd413beb7165da9880d6b052d",
|
|
||||||
"img_1.png": "4c09437df56a8beb2a620f420a4d4d5a",
|
|
||||||
"img_39.png": "732cda29c3f658a9847239d23cbe759b",
|
|
||||||
"img_87.png": "4ce165b53df962d9e371124bfdec64bd",
|
|
||||||
"20160903_200728.jpg": "7c93ad8f71045b07aeb390e0c906d5f2",
|
|
||||||
"img_61.png": "68b1c7a9596ab2f0ff038bb585d7c4d4",
|
|
||||||
"img_12.png": "3b126df55d41bb48829650a766e3c6f3",
|
|
||||||
"img_85.png": "f48059d59efd33ec8cce4daa44bbd46d",
|
|
||||||
"img_69.png": "0bcf667d5603a157159052baddbb6e50",
|
|
||||||
"img_8.png": "4c453e8e332e92478bf0d49e663dedc9",
|
|
||||||
"img_92.png": "7e35f295100f51a9a58533c8d1f1fc80",
|
|
||||||
"img_35.png": "ed1e95446696a97f46d84b246d01e0f7",
|
|
||||||
"img_81.png": "072948a36c4ba79f4e761a9491f3d8eb",
|
|
||||||
"img_27.png": "9bb13a4ca7292c964fedd91065af64a0",
|
|
||||||
"img_33.png": "c5f814c539acc7cdef0da3279544e55f",
|
|
||||||
"img_47.png": "2fb600fb8f3717bd79a9f56fa32efb0b",
|
|
||||||
"img_5.png": "3c87ca4219c2ed6f275c044fa888afc0",
|
|
||||||
"img_46.png": "3c73192c04ca22cc4ecb450565315798",
|
|
||||||
"img_25.png": "1d51cc605017423c0ae114f0b883bfea",
|
|
||||||
"img_14.png": "759b2e7532f7bf3a0d956a7f1c4cb7af",
|
|
||||||
"img_93.png": "ab83477ac7782d7a7b38369ccfa7df80",
|
|
||||||
"img_10.png": "10635b82a1cfcefb2c97af2ebd61d889",
|
|
||||||
"img_13.png": "5fb9fc29ca1d32c4ab6816160fcfdfe1",
|
|
||||||
"img_65.png": "c442724f5a29468eed4e9563e25e06e8",
|
|
||||||
"img_36.png": "ed1e95446696a97f46d84b246d01e0f7",
|
|
||||||
"img_19.png": "82660461b10e0545165fe8dade87d4d2",
|
|
||||||
"img_51.png": "851612780583d06942c119b2c99b7b06",
|
|
||||||
"img_38.png": "dad813a6e3e919454ba1ff0fb3f8df22",
|
|
||||||
"img_20.png": "29d28ef9f53d71c066798c46341d30a9",
|
|
||||||
"img_34.png": "c5f814c539acc7cdef0da3279544e55f",
|
|
||||||
"img_19_2.png": "e745ea158afbf416df6c53835c11f4c7",
|
|
||||||
"img_91.png": "7e35f295100f51a9a58533c8d1f1fc80",
|
|
||||||
"test123.jpeg": "b89b5886901ba89c5d3fcd97430904e8",
|
|
||||||
"img_49.png": "bd72afa8e00ffff04dc8e860af058d0d",
|
|
||||||
"test.png": "e569b50c015080e05e36f21307550e1a",
|
|
||||||
"img_40.png": "6022fd6b8a4580ef4451c3c87e469b85",
|
|
||||||
"img_83.png": "501207e02d72776b65d705db9f28a179",
|
|
||||||
"img_70.png": "0bcf667d5603a157159052baddbb6e50",
|
|
||||||
"img_15.png": "cbd90cb2e2edf2e4eff746c586657ad5",
|
|
||||||
"imgxxx.png": "8965337a6abd7bfec6cb774978b4198b",
|
|
||||||
"img_86.png": "9e831c994a69ee1e18a6d279d69c072f",
|
|
||||||
"img_11.png": "c5e036773ead71f40a1f1966f74abc2b",
|
|
||||||
"img_4.png": "3c87ca4219c2ed6f275c044fa888afc0",
|
|
||||||
"img_9.png": "7e35f295100f51a9a58533c8d1f1fc80",
|
|
||||||
"img_76.png": "29b9c219a777155d576b9f35a3f41cca",
|
|
||||||
"img_run.png": "72ddfe64a1cea5611ea61425f4f61fd2",
|
|
||||||
"img_59.png": "4fae0cf0017c9be9a550792167d60ce3",
|
|
||||||
"img_74.png": "b7c41d0f4c062cc7d7da240173a1f075",
|
|
||||||
"img.png": "6023644b2237fe301e43431e28a9d2e9",
|
|
||||||
"img_17.png": "9955e08008340d6b2acd945cc4d9505a",
|
|
||||||
"img_60.png": "2c31cb016cad120daaf4e441a720a56c",
|
|
||||||
"img_84.png": "f5dea766c46abfa4011c23d3c466d8dc",
|
|
||||||
"img_43.png": "e1d75f3326dd89c39fa91b9aaa0b54f9",
|
|
||||||
"img_42.png": "a4578fb780c2424581d72c862e14af6c"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Launch the ComfyUI backend (headless) for the Qwen-Image-Edit API.
|
|
||||||
# gfx906 (MI50) has no flash-attention, so use the pytorch cross-attention path.
|
|
||||||
set -e
|
|
||||||
# env.sh resolves BASE/COMFY/VENV (and keeps the venv off NTFS). Portable
|
|
||||||
# across hosts (tour: /media/tour/APPS/comfyui, hubby: /home/hubby/comfyui).
|
|
||||||
source "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env.sh"
|
|
||||||
cd "$COMFY"
|
|
||||||
source "$VENV/bin/activate"
|
|
||||||
|
|
||||||
# MI50 / Vega20 is happiest in fp16; avoid bf16 emulation.
|
|
||||||
export PYTORCH_HIP_ALLOC_CONF="expandable_segments:True,garbage_collection_threshold:0.8"
|
|
||||||
export HSA_ENABLE_SDMA=0
|
|
||||||
|
|
||||||
# Split cross-attention chunks the attention matmul -> much lower peak VRAM,
|
|
||||||
# which is what lets the 20B Q8 edit model + reference-image sequence fit in 32GB.
|
|
||||||
# --lowvram offloads models to CPU RAM when not in use, preventing OOM.
|
|
||||||
exec python main.py \
|
|
||||||
--listen 127.0.0.1 \
|
|
||||||
--port 8188 \
|
|
||||||
--use-split-cross-attention \
|
|
||||||
--lowvram \
|
|
||||||
"$@"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Launch the FastAPI edit service (talks to the local ComfyUI on :8188).
|
|
||||||
set -e
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Add all nvidia CUDA library paths bundled with the venv (needed by onnxruntime-gpu / insightface)
|
|
||||||
_NV_BASE="$VENV/lib/python3.13/site-packages/nvidia"
|
|
||||||
_NV_LIBPATH="$_NV_BASE/cuda_runtime/lib:$_NV_BASE/cublas/lib:$_NV_BASE/cudnn/lib:$_NV_BASE/curand/lib:$_NV_BASE/cufft/lib:$_NV_BASE/cusolver/lib:$_NV_BASE/cusparse/lib:$_NV_BASE/nvjitlink/lib:$_NV_BASE/cuda_nvrtc/lib"
|
|
||||||
export LD_LIBRARY_PATH="${_NV_LIBPATH}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
|
||||||
|
|
||||||
export COMFY_URL="http://127.0.0.1:8188"
|
|
||||||
export HOST="0.0.0.0"
|
|
||||||
export PORT="8500"
|
|
||||||
# Output pixel budget. MI50 is compute-bound on this 20B model:
|
|
||||||
# ~0.59MP -> ~110s ~0.79MP -> ~140s ~1.0MP -> ~180s (4 steps)
|
|
||||||
# 0.79MP is a sane speed/quality default; raise for bigger output.
|
|
||||||
# Lowered to 0.65MP to help prevent GPU OOM on MI50.
|
|
||||||
export MAX_AREA="${MAX_AREA:-655360}"
|
|
||||||
|
|
||||||
exec python edit_api.py
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Launch the folder watcher service.
|
|
||||||
set -e
|
|
||||||
# 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
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Stop and disable systemd services for Qwen-Image-Edit
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
echo "This script must be run as root (use sudo)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Stopping services..."
|
|
||||||
systemctl stop comfyui-api.service
|
|
||||||
systemctl stop comfyui-backend.service
|
|
||||||
|
|
||||||
echo "Disabling services..."
|
|
||||||
systemctl disable comfyui-api.service
|
|
||||||
systemctl disable comfyui-backend.service
|
|
||||||
|
|
||||||
echo "Services stopped and disabled."
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Qwen-Image-Edit ComfyUI Services
|
|
||||||
Documentation=man:systemd.special(7)
|
|
||||||
Requires=comfyui-backend.service comfyui-api.service
|
|
||||||
After=comfyui-backend.service comfyui-api.service
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Validate background removal strategies.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python test_transparency.py [image.png ...]
|
|
||||||
|
|
||||||
Writes comparison files next to each input:
|
|
||||||
*_rembg.png — pure rembg (bg_removal=rembg path)
|
|
||||||
*_blackbg.png — simulated black-bg composite (what Qwen renders in sam2 mode)
|
|
||||||
*_thresh.png — threshold mask only (non-black pixels → person)
|
|
||||||
*_thresh_sam2.png — threshold bbox → SAM2 edge refinement (new sam2 mode path)
|
|
||||||
"""
|
|
||||||
import io, sys, os
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image, ImageFilter
|
|
||||||
|
|
||||||
OUTPUT_DIR = "/mnt/zim/tour-comfy/output"
|
|
||||||
VENV_SITE = "/home/mike/comfyui/venv/lib/python3.13/site-packages"
|
|
||||||
SAM2_CKPT = os.path.expanduser("~/.sam/sam2.1_hiera_base_plus.pt")
|
|
||||||
SAM2_CFG = "configs/sam2.1/sam2.1_hiera_b+.yaml"
|
|
||||||
|
|
||||||
|
|
||||||
# ── rembg ──────────────────────────────────────────────────────────────────────
|
|
||||||
def apply_rembg(png_bytes: bytes) -> bytes:
|
|
||||||
from rembg import remove
|
|
||||||
return remove(png_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
# ── SAM2 loader ────────────────────────────────────────────────────────────────
|
|
||||||
_predictor = None
|
|
||||||
def load_sam2():
|
|
||||||
global _predictor
|
|
||||||
if _predictor is not None:
|
|
||||||
return _predictor
|
|
||||||
try:
|
|
||||||
import torch
|
|
||||||
from sam2.build_sam import build_sam2
|
|
||||||
from sam2.sam2_image_predictor import SAM2ImagePredictor
|
|
||||||
model = build_sam2(SAM2_CFG, SAM2_CKPT, device="cuda")
|
|
||||||
_predictor = SAM2ImagePredictor(model)
|
|
||||||
print("[sam2] loaded")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[sam2] FAILED: {e}")
|
|
||||||
_predictor = False
|
|
||||||
return _predictor
|
|
||||||
|
|
||||||
|
|
||||||
# ── Simulate black-bg Qwen output ─────────────────────────────────────────────
|
|
||||||
def make_black_bg(png_bytes: bytes) -> bytes:
|
|
||||||
"""Composite a rembg cutout onto pure black — simulates Qwen 'black background' output."""
|
|
||||||
rgba = Image.open(io.BytesIO(apply_rembg(png_bytes))).convert("RGBA")
|
|
||||||
bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255))
|
|
||||||
bg.paste(rgba, mask=rgba.split()[3])
|
|
||||||
out = bg.convert("RGB")
|
|
||||||
buf = io.BytesIO(); out.save(buf, "PNG"); return buf.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
# ── Threshold-only mask ────────────────────────────────────────────────────────
|
|
||||||
def apply_threshold_mask(png_bytes: bytes, threshold: int = 25) -> bytes:
|
|
||||||
"""Find non-black pixels → person mask. No SAM2 needed."""
|
|
||||||
img = Image.open(io.BytesIO(png_bytes)).convert("RGB")
|
|
||||||
arr = np.array(img)
|
|
||||||
h, w = arr.shape[:2]
|
|
||||||
|
|
||||||
is_person = np.max(arr, axis=2) > threshold
|
|
||||||
coverage = is_person.sum() / (h * w)
|
|
||||||
print(f" [threshold] person coverage: {coverage:.1%}")
|
|
||||||
|
|
||||||
if not is_person.any():
|
|
||||||
print(" [threshold] all-black image — no person found")
|
|
||||||
return png_bytes
|
|
||||||
|
|
||||||
mask_np = is_person.astype(np.uint8) * 255
|
|
||||||
alpha_img = Image.fromarray(mask_np, "L").filter(ImageFilter.GaussianBlur(radius=2))
|
|
||||||
rgba = img.convert("RGBA"); r, g, b, _ = rgba.split()
|
|
||||||
out = Image.merge("RGBA", (r, g, b, alpha_img))
|
|
||||||
buf = io.BytesIO(); out.save(buf, "PNG"); return buf.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
# ── NEW: Threshold bbox → SAM2 refinement (sam2 mode path) ────────────────────
|
|
||||||
def apply_thresh_sam2(png_bytes: bytes, threshold: int = 25) -> bytes:
|
|
||||||
"""
|
|
||||||
For black-background Qwen output:
|
|
||||||
1. Threshold to find person bbox (non-black pixels)
|
|
||||||
2. Run SAM2 with that tight bbox for clean edge refinement
|
|
||||||
3. Fallback to threshold mask if SAM2 unavailable or mask looks wrong
|
|
||||||
"""
|
|
||||||
import torch
|
|
||||||
|
|
||||||
img = Image.open(io.BytesIO(png_bytes)).convert("RGB")
|
|
||||||
arr = np.array(img)
|
|
||||||
h, w = arr.shape[:2]
|
|
||||||
|
|
||||||
# Step 1 — threshold
|
|
||||||
is_person = np.max(arr, axis=2) > threshold
|
|
||||||
thresh_cov = is_person.sum() / (h * w)
|
|
||||||
print(f" [thresh_sam2] threshold person coverage: {thresh_cov:.1%}")
|
|
||||||
|
|
||||||
if not is_person.any():
|
|
||||||
print(" [thresh_sam2] all-black — fallback to rembg")
|
|
||||||
return apply_rembg(png_bytes)
|
|
||||||
|
|
||||||
rows = np.any(is_person, axis=1)
|
|
||||||
cols = np.any(is_person, axis=0)
|
|
||||||
rmin = int(np.where(rows)[0][0]); rmax = int(np.where(rows)[0][-1])
|
|
||||||
cmin = int(np.where(cols)[0][0]); cmax = int(np.where(cols)[0][-1])
|
|
||||||
|
|
||||||
margin = int(min(h, w) * 0.02)
|
|
||||||
y1 = max(0, rmin - margin); y2 = min(h, rmax + margin)
|
|
||||||
x1 = max(0, cmin - margin); x2 = min(w, cmax + margin)
|
|
||||||
print(f" [thresh_sam2] person bbox (+margin): ({x1},{y1})-({x2},{y2})")
|
|
||||||
|
|
||||||
# Step 2 — SAM2 with person-specific bbox
|
|
||||||
predictor = load_sam2()
|
|
||||||
if predictor is not False:
|
|
||||||
box = np.array([[x1, y1, x2, y2]], dtype=np.float32)
|
|
||||||
try:
|
|
||||||
with torch.inference_mode():
|
|
||||||
predictor.set_image(arr)
|
|
||||||
masks, scores, _ = predictor.predict(box=box, multimask_output=True)
|
|
||||||
|
|
||||||
if masks is not None and len(masks) > 0:
|
|
||||||
best = masks[int(np.argmax(scores))]
|
|
||||||
sam_cov = float(best.sum()) / (h * w)
|
|
||||||
print(f" [thresh_sam2] SAM2 coverage: {sam_cov:.1%} (threshold was {thresh_cov:.1%})")
|
|
||||||
|
|
||||||
# Accept SAM2 result if coverage is within reasonable range of threshold
|
|
||||||
if 0.03 < sam_cov < 0.95 and abs(sam_cov - thresh_cov) < 0.30:
|
|
||||||
mask_np = best.astype(np.uint8) * 255
|
|
||||||
alpha_img = Image.fromarray(mask_np, "L").filter(ImageFilter.GaussianBlur(radius=1))
|
|
||||||
rgba = img.convert("RGBA"); r, g, b, _ = rgba.split()
|
|
||||||
out = Image.merge("RGBA", (r, g, b, alpha_img))
|
|
||||||
buf = io.BytesIO(); out.save(buf, "PNG")
|
|
||||||
print(" [thresh_sam2] SAM2 result accepted ✓")
|
|
||||||
return buf.getvalue()
|
|
||||||
else:
|
|
||||||
print(f" [thresh_sam2] SAM2 coverage diverged from threshold — using threshold mask")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" [thresh_sam2] SAM2 error: {e} — using threshold mask")
|
|
||||||
else:
|
|
||||||
print(" [thresh_sam2] SAM2 not available — using threshold mask")
|
|
||||||
|
|
||||||
# Step 3 — fallback: threshold mask with soft edges
|
|
||||||
mask_np = is_person.astype(np.uint8) * 255
|
|
||||||
alpha_img = Image.fromarray(mask_np, "L").filter(ImageFilter.GaussianBlur(radius=2))
|
|
||||||
rgba = img.convert("RGBA"); r, g, b, _ = rgba.split()
|
|
||||||
out = Image.merge("RGBA", (r, g, b, alpha_img))
|
|
||||||
buf = io.BytesIO(); out.save(buf, "PNG")
|
|
||||||
print(" [thresh_sam2] threshold mask used as fallback")
|
|
||||||
return buf.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────────
|
|
||||||
if __name__ == "__main__":
|
|
||||||
paths = sys.argv[1:] if len(sys.argv) > 1 else [
|
|
||||||
os.path.join(OUTPUT_DIR, "20260622_181910_0_20260619_124038_image.png"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
if not os.path.exists(path):
|
|
||||||
print(f"SKIP (not found): {path}"); continue
|
|
||||||
stem = os.path.splitext(path)[0]
|
|
||||||
print(f"\n══ {os.path.basename(path)} ══")
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
raw = f.read()
|
|
||||||
|
|
||||||
print("1. rembg (bg_removal=rembg path)...")
|
|
||||||
rb = apply_rembg(raw)
|
|
||||||
with open(stem + "_rembg.png", "wb") as f: f.write(rb)
|
|
||||||
print(f" → {os.path.basename(stem)}_rembg.png")
|
|
||||||
|
|
||||||
print("2. Simulate black-bg Qwen output...")
|
|
||||||
bb = make_black_bg(raw)
|
|
||||||
with open(stem + "_blackbg.png", "wb") as f: f.write(bb)
|
|
||||||
print(f" → {os.path.basename(stem)}_blackbg.png")
|
|
||||||
|
|
||||||
print("3. Threshold-only mask on black-bg image...")
|
|
||||||
tm = apply_threshold_mask(bb)
|
|
||||||
with open(stem + "_thresh.png", "wb") as f: f.write(tm)
|
|
||||||
print(f" → {os.path.basename(stem)}_thresh.png")
|
|
||||||
|
|
||||||
print("4. Threshold bbox → SAM2 refinement on black-bg image (NEW sam2 mode path)...")
|
|
||||||
ts = apply_thresh_sam2(bb)
|
|
||||||
with open(stem + "_thresh_sam2.png", "wb") as f: f.write(ts)
|
|
||||||
print(f" → {os.path.basename(stem)}_thresh_sam2.png")
|
|
||||||
|
|
||||||
print("\n── Done ──")
|
|
||||||
print(" *_rembg.png rembg on real background (bg_removal=rembg path)")
|
|
||||||
print(" *_thresh.png threshold-only on black bg")
|
|
||||||
print(" *_thresh_sam2.png threshold-bbox → SAM2 on black bg (NEW sam2 mode path)")
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
"""
|
|
||||||
turntable_cache.py — persistent state for Qwen turntable generation.
|
|
||||||
|
|
||||||
State stored as JSON: {output_dir}/_turntable/{group_id}/state.json
|
|
||||||
Views stored alongside: {output_dir}/_turntable/{group_id}/views/view_NNN_DDDdeg.png
|
|
||||||
Final video: {output_dir}/_turntable/{group_id}/turntable.mp4
|
|
||||||
|
|
||||||
One state file per group tracks completed angles so background generation can
|
|
||||||
resume after restart without re-rendering anything that's already on disk.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def cache_dir(output_dir: str, group_id: str) -> str:
|
|
||||||
return os.path.join(output_dir, "_turntable", str(group_id))
|
|
||||||
|
|
||||||
|
|
||||||
def state_path(output_dir: str, group_id: str) -> str:
|
|
||||||
return os.path.join(cache_dir(output_dir, group_id), "state.json")
|
|
||||||
|
|
||||||
|
|
||||||
def load_state(output_dir: str, group_id: str) -> Optional[dict]:
|
|
||||||
p = state_path(output_dir, group_id)
|
|
||||||
if not os.path.exists(p):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
with open(p) as f:
|
|
||||||
return json.load(f)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def save_state(output_dir: str, group_id: str, state: dict):
|
|
||||||
os.makedirs(cache_dir(output_dir, group_id), exist_ok=True)
|
|
||||||
p = state_path(output_dir, group_id)
|
|
||||||
tmp = p + ".tmp"
|
|
||||||
with open(tmp, "w") as f:
|
|
||||||
json.dump(state, f, indent=2)
|
|
||||||
os.replace(tmp, p) # atomic
|
|
||||||
|
|
||||||
|
|
||||||
def init_state(
|
|
||||||
output_dir: str,
|
|
||||||
group_id: str,
|
|
||||||
source_image: str,
|
|
||||||
preferred_filename: str,
|
|
||||||
n_views: int = 24,
|
|
||||||
seed: int = 42,
|
|
||||||
steps: int = 8,
|
|
||||||
) -> dict:
|
|
||||||
"""Create a fresh state dict and save it. Wipes any existing partial state."""
|
|
||||||
import sys
|
|
||||||
if _HERE not in sys.path:
|
|
||||||
sys.path.insert(0, _HERE)
|
|
||||||
from orbit_qwen import _angles_for
|
|
||||||
|
|
||||||
angles = _angles_for("turntable", n_views, 180.0)
|
|
||||||
state = {
|
|
||||||
"group_id": str(group_id),
|
|
||||||
"preferred_filename": preferred_filename,
|
|
||||||
"source_image": source_image,
|
|
||||||
"seed": seed,
|
|
||||||
"n_views": n_views,
|
|
||||||
"steps": steps,
|
|
||||||
"angles": angles,
|
|
||||||
"views": {}, # deg_key (str) -> abs path
|
|
||||||
"video_path": None,
|
|
||||||
"completed": False,
|
|
||||||
"started_at": time.time(),
|
|
||||||
"completed_at": None,
|
|
||||||
}
|
|
||||||
save_state(output_dir, group_id, state)
|
|
||||||
return state
|
|
||||||
|
|
||||||
|
|
||||||
def deg_key(deg: float) -> str:
|
|
||||||
return f"{deg:.1f}"
|
|
||||||
|
|
||||||
|
|
||||||
def mark_view_done(output_dir: str, group_id: str, state: dict, deg: float, path: str):
|
|
||||||
state["views"][deg_key(deg)] = path
|
|
||||||
save_state(output_dir, group_id, state)
|
|
||||||
|
|
||||||
|
|
||||||
def mark_completed(output_dir: str, group_id: str, state: dict, video_path: str):
|
|
||||||
state["completed"] = True
|
|
||||||
state["video_path"] = video_path
|
|
||||||
state["completed_at"] = time.time()
|
|
||||||
save_state(output_dir, group_id, state)
|
|
||||||
|
|
||||||
|
|
||||||
def next_missing_angle(state: dict) -> Optional[float]:
|
|
||||||
"""Return first angle not yet in state['views'], or None if all done."""
|
|
||||||
done = state.get("views", {})
|
|
||||||
for deg in state.get("angles", []):
|
|
||||||
if deg_key(deg) not in done:
|
|
||||||
return deg
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def list_cached_group_ids(output_dir: str) -> list:
|
|
||||||
td = os.path.join(output_dir, "_turntable")
|
|
||||||
if not os.path.isdir(td):
|
|
||||||
return []
|
|
||||||
return [
|
|
||||||
d for d in os.listdir(td)
|
|
||||||
if os.path.isfile(os.path.join(td, d, "state.json"))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_status_summary(output_dir: str) -> dict:
|
|
||||||
"""Return {group_id: status_dict} for all groups that have a state file."""
|
|
||||||
result = {}
|
|
||||||
for gid in list_cached_group_ids(output_dir):
|
|
||||||
st = load_state(output_dir, gid)
|
|
||||||
if st:
|
|
||||||
result[gid] = {
|
|
||||||
"completed": st.get("completed", False),
|
|
||||||
"n_done": len(st.get("views", {})),
|
|
||||||
"n_total": st.get("n_views", 24),
|
|
||||||
"video_path": st.get("video_path"),
|
|
||||||
"preferred_filename": st.get("preferred_filename"),
|
|
||||||
"started_at": st.get("started_at"),
|
|
||||||
"completed_at": st.get("completed_at"),
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def delete_state(output_dir: str, group_id: str):
|
|
||||||
"""Wipe all cached views, state, and video for this group."""
|
|
||||||
import shutil
|
|
||||||
d = cache_dir(output_dir, group_id)
|
|
||||||
if os.path.isdir(d):
|
|
||||||
shutil.rmtree(d)
|
|
||||||
|
|
||||||
|
|
||||||
def get_group_video(output_dir: str, group_id: str) -> Optional[str]:
|
|
||||||
"""Return the video path if the turntable is complete and the file exists."""
|
|
||||||
st = load_state(output_dir, group_id)
|
|
||||||
if not st or not st.get("completed"):
|
|
||||||
return None
|
|
||||||
vp = st.get("video_path")
|
|
||||||
if vp and os.path.exists(vp):
|
|
||||||
return vp
|
|
||||||
return None
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,314 +0,0 @@
|
|||||||
import os
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
import requests
|
|
||||||
from PIL import Image
|
|
||||||
import logging
|
|
||||||
import hashlib
|
|
||||||
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")
|
|
||||||
|
|
||||||
def load_config():
|
|
||||||
with open(CONFIG_PATH, 'r') as f:
|
|
||||||
conf = json.load(f)
|
|
||||||
# Resolve relative paths relative to this script's directory
|
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
for key in ["stage_dir", "output_dir", "failed_dir", "processed_file", "log_file"]:
|
|
||||||
if not os.path.isabs(conf[key]):
|
|
||||||
conf[key] = os.path.normpath(os.path.join(base_dir, "..", conf[key]))
|
|
||||||
return conf
|
|
||||||
|
|
||||||
CONF = load_config()
|
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.FileHandler(CONF["log_file"]),
|
|
||||||
logging.StreamHandler()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_processed_files():
|
|
||||||
if os.path.exists(CONF["processed_file"]):
|
|
||||||
try:
|
|
||||||
with open(CONF["processed_file"], 'r') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
if isinstance(data, list):
|
|
||||||
# Migration: convert old list format to dict
|
|
||||||
return {name: None for name in data}
|
|
||||||
return data
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error reading processed file: {e}")
|
|
||||||
return {}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def save_processed_files(processed):
|
|
||||||
try:
|
|
||||||
with open(CONF["processed_file"], 'w') as f:
|
|
||||||
json.dump(processed, f, indent=2)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error saving processed file: {e}")
|
|
||||||
|
|
||||||
def calculate_hash(filepath):
|
|
||||||
"""Calculate MD5 hash of a file."""
|
|
||||||
hasher = hashlib.md5()
|
|
||||||
try:
|
|
||||||
with open(filepath, 'rb') as f:
|
|
||||||
for chunk in iter(lambda: f.read(4096), b""):
|
|
||||||
hasher.update(chunk)
|
|
||||||
return hasher.hexdigest()
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error calculating hash for {filepath}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def crop_to_bbox(image_path, margin, top_margin=None, headroom=0.0):
|
|
||||||
try:
|
|
||||||
img = Image.open(image_path)
|
|
||||||
if img.mode != 'RGBA':
|
|
||||||
logging.info(f"Image {image_path} is mode {img.mode}, not RGBA. Skipping crop.")
|
|
||||||
return img
|
|
||||||
|
|
||||||
alpha = img.split()[-1]
|
|
||||||
bbox = alpha.getbbox()
|
|
||||||
if not bbox:
|
|
||||||
logging.info(f"No non-transparent bbox found for {image_path}. Returning original.")
|
|
||||||
return img
|
|
||||||
|
|
||||||
if top_margin is None:
|
|
||||||
top_margin = margin
|
|
||||||
|
|
||||||
# Add margin
|
|
||||||
left, upper, right, lower = bbox
|
|
||||||
left = max(0, left - margin)
|
|
||||||
upper = max(0, upper - top_margin)
|
|
||||||
right = min(img.width, right + margin)
|
|
||||||
lower = min(img.height, lower + margin)
|
|
||||||
|
|
||||||
logging.info(f"Cropping {image_path} to {left, upper, right, lower} (margin={margin}, top_margin={top_margin})")
|
|
||||||
cropped = img.crop((left, upper, right, lower))
|
|
||||||
|
|
||||||
if headroom > 0:
|
|
||||||
h_px = int(cropped.height * headroom)
|
|
||||||
if h_px > 0:
|
|
||||||
logging.info(f"Adding {h_px}px headroom to {image_path}")
|
|
||||||
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
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to crop {image_path}: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def is_file_stable(filepath):
|
|
||||||
"""Check if file size is stable for at least 1 second."""
|
|
||||||
try:
|
|
||||||
size1 = os.path.getsize(filepath)
|
|
||||||
time.sleep(1)
|
|
||||||
size2 = os.path.getsize(filepath)
|
|
||||||
return size1 == size2 and size1 > 0
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def flag_image(filename):
|
|
||||||
input_path = os.path.join(CONF["stage_dir"], filename)
|
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
||||||
failed_filename = f"{timestamp}_{filename}"
|
|
||||||
failed_path = os.path.join(CONF["failed_dir"], failed_filename)
|
|
||||||
try:
|
|
||||||
os.makedirs(CONF["failed_dir"], exist_ok=True)
|
|
||||||
logging.info(f"Flagging image {filename} (moving to failed directory as {failed_filename})")
|
|
||||||
shutil.move(input_path, failed_path)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to move {filename} to failed directory: {e}")
|
|
||||||
|
|
||||||
def process_image(filename):
|
|
||||||
# Reload config in case it changed
|
|
||||||
global CONF
|
|
||||||
try:
|
|
||||||
CONF = load_config()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
input_path = os.path.join(CONF["stage_dir"], filename)
|
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
||||||
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(
|
|
||||||
input_path,
|
|
||||||
CONF["margin"],
|
|
||||||
top_margin=CONF.get("top_margin"),
|
|
||||||
headroom=CONF.get("headroom", 0.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save temporary cropped image for upload
|
|
||||||
cropped_img.save(temp_path, format="PNG")
|
|
||||||
|
|
||||||
with open(temp_path, 'rb') as f:
|
|
||||||
files = {'image': (filename, f, 'image/png')}
|
|
||||||
data = {
|
|
||||||
'prompt': CONF["prompt"],
|
|
||||||
'seed': CONF.get("seed", -1),
|
|
||||||
'max_area': CONF.get("max_area", 0)
|
|
||||||
}
|
|
||||||
logging.info(f"Calling API for {filename} -> {output_filename} with prompt: {CONF['prompt']}")
|
|
||||||
response = requests.post(CONF["api_url"], files=files, data=data, timeout=600)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
logging.error(f"API failed for {filename}: {response.status_code} - {response.text}")
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
return False
|
|
||||||
except requests.exceptions.ConnectionError as e:
|
|
||||||
logging.error(f"Connection error while processing {filename}: {e}")
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error processing {filename}: {str(e)}", exc_info=True)
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update_car_html():
|
|
||||||
output_dir = CONF["output_dir"]
|
|
||||||
car_html_path = os.path.join(output_dir, "car.html")
|
|
||||||
if not os.path.exists(car_html_path):
|
|
||||||
logging.warning(f"car.html not found at {car_html_path}")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# List images in output_dir
|
|
||||||
extensions = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg')
|
|
||||||
images = [f for f in os.listdir(output_dir) if f.lower().endswith(extensions) and f != "car.html"]
|
|
||||||
|
|
||||||
# Sort by mtime, newest first
|
|
||||||
images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
|
||||||
|
|
||||||
with open(car_html_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
images_json = json.dumps(images, indent=12).strip()
|
|
||||||
# Ensure it looks nice in the JS
|
|
||||||
images_json = images_json.replace('\n', '\n ')
|
|
||||||
|
|
||||||
pattern = r'// --- HYDRATION_START ---.*?// --- HYDRATION_END ---'
|
|
||||||
replacement = f'// --- HYDRATION_START ---\n const PRELOADED_IMAGES = {images_json};\n // --- HYDRATION_END ---'
|
|
||||||
|
|
||||||
new_content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
|
||||||
|
|
||||||
with open(car_html_path, 'w') as f:
|
|
||||||
f.write(new_content)
|
|
||||||
logging.info(f"Updated {car_html_path} with {len(images)} images")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to update car.html: {e}")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Prevent multiple instances
|
|
||||||
lock_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "watcher.lock")
|
|
||||||
fp = open(lock_file, 'w')
|
|
||||||
try:
|
|
||||||
fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
except IOError:
|
|
||||||
print("Another instance of watcher.py is already running. Exiting.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
processed = get_processed_files()
|
|
||||||
|
|
||||||
# Ensure directories exist
|
|
||||||
os.makedirs(CONF["stage_dir"], exist_ok=True)
|
|
||||||
os.makedirs(CONF["output_dir"], exist_ok=True)
|
|
||||||
os.makedirs(CONF["failed_dir"], exist_ok=True)
|
|
||||||
|
|
||||||
logging.info(f"Watcher started. Monitoring {CONF['stage_dir']}...")
|
|
||||||
logging.info(f"Output directory: {CONF['output_dir']}")
|
|
||||||
logging.info(f"API URL: {CONF['api_url']}")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
files = [f for f in os.listdir(CONF["stage_dir"])
|
|
||||||
if f.lower().endswith(('.png', '.jpg', '.jpeg'))
|
|
||||||
and not f.endswith('.tmp.png')]
|
|
||||||
|
|
||||||
for f in files:
|
|
||||||
input_path = os.path.join(CONF["stage_dir"], f)
|
|
||||||
|
|
||||||
# Check if file is stable (not still being copied)
|
|
||||||
if not is_file_stable(input_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Calculate current file hash
|
|
||||||
current_hash = calculate_hash(input_path)
|
|
||||||
if not current_hash:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if already processed
|
|
||||||
if f in processed:
|
|
||||||
stored_hash = processed[f]
|
|
||||||
if stored_hash == current_hash:
|
|
||||||
continue
|
|
||||||
if stored_hash is None:
|
|
||||||
# Migration case: filename exists but no hash.
|
|
||||||
# Skip to avoid mass re-processing, but update the hash.
|
|
||||||
logging.info(f"Updating hash for previously processed {f}")
|
|
||||||
processed[f] = current_hash
|
|
||||||
save_processed_files(processed)
|
|
||||||
continue
|
|
||||||
|
|
||||||
res = process_image(f)
|
|
||||||
if res is True:
|
|
||||||
processed[f] = current_hash
|
|
||||||
save_processed_files(processed)
|
|
||||||
update_car_html()
|
|
||||||
elif res is False:
|
|
||||||
flag_image(f)
|
|
||||||
# We don't add to processed here so that if the user
|
|
||||||
# moves the file back to stage, it will be retried.
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Main loop error: {e}")
|
|
||||||
|
|
||||||
time.sleep(CONF["poll_interval"])
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"1": {
|
|
||||||
"class_type": "UnetLoaderGGUF",
|
|
||||||
"inputs": { "unet_name": "Qwen-Rapid-NSFW-v23_Q8_0.gguf" },
|
|
||||||
"_meta": { "title": "unet (gguf)" }
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"class_type": "CLIPLoader",
|
|
||||||
"inputs": {
|
|
||||||
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
|
|
||||||
"type": "qwen_image"
|
|
||||||
},
|
|
||||||
"_meta": { "title": "text encoder" }
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"class_type": "VAELoader",
|
|
||||||
"inputs": { "vae_name": "qwen_image_vae.safetensors" },
|
|
||||||
"_meta": { "title": "vae" }
|
|
||||||
},
|
|
||||||
"4": {
|
|
||||||
"class_type": "LoadImage",
|
|
||||||
"inputs": { "image": "input.png" },
|
|
||||||
"_meta": { "title": "input image" }
|
|
||||||
},
|
|
||||||
"5": {
|
|
||||||
"class_type": "TextEncodeQwenImageEditPlus",
|
|
||||||
"inputs": {
|
|
||||||
"clip": ["2", 0],
|
|
||||||
"vae": ["3", 0],
|
|
||||||
"image1": ["4", 0],
|
|
||||||
"prompt": "edit instruction goes here"
|
|
||||||
},
|
|
||||||
"_meta": { "title": "positive" }
|
|
||||||
},
|
|
||||||
"6": {
|
|
||||||
"class_type": "TextEncodeQwenImageEditPlus",
|
|
||||||
"inputs": {
|
|
||||||
"clip": ["2", 0],
|
|
||||||
"vae": ["3", 0],
|
|
||||||
"prompt": " "
|
|
||||||
},
|
|
||||||
"_meta": { "title": "negative" }
|
|
||||||
},
|
|
||||||
"7": {
|
|
||||||
"class_type": "EmptySD3LatentImage",
|
|
||||||
"inputs": { "width": 1024, "height": 1024, "batch_size": 1 },
|
|
||||||
"_meta": { "title": "latent" }
|
|
||||||
},
|
|
||||||
"8": {
|
|
||||||
"class_type": "KSampler",
|
|
||||||
"inputs": {
|
|
||||||
"model": ["1", 0],
|
|
||||||
"positive": ["5", 0],
|
|
||||||
"negative": ["6", 0],
|
|
||||||
"latent_image": ["7", 0],
|
|
||||||
"seed": 0,
|
|
||||||
"steps": 4,
|
|
||||||
"cfg": 1.0,
|
|
||||||
"sampler_name": "euler_ancestral",
|
|
||||||
"scheduler": "beta",
|
|
||||||
"denoise": 1.0
|
|
||||||
},
|
|
||||||
"_meta": { "title": "sampler" }
|
|
||||||
},
|
|
||||||
"9": {
|
|
||||||
"class_type": "VAEDecode",
|
|
||||||
"inputs": { "samples": ["8", 0], "vae": ["3", 0] },
|
|
||||||
"_meta": { "title": "decode" }
|
|
||||||
},
|
|
||||||
"10": {
|
|
||||||
"class_type": "SaveImage",
|
|
||||||
"inputs": { "images": ["9", 0], "filename_prefix": "qwenedit" },
|
|
||||||
"_meta": { "title": "save" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user