reorder
This commit is contained in:
4410
tour-comfy/car.html
4410
tour-comfy/car.html
File diff suppressed because it is too large
Load Diff
@@ -174,6 +174,21 @@ def set_archived(filename, archived: bool):
|
||||
cur.close()
|
||||
_put_db_connection(conn)
|
||||
|
||||
def set_filenames_archived(filenames, archived: bool):
|
||||
if not filenames:
|
||||
return []
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("UPDATE person SET archived = %s WHERE filename = ANY(%s) RETURNING filename", (archived, list(filenames)))
|
||||
rows = cur.fetchall()
|
||||
updated = [r[0] for r in rows]
|
||||
conn.commit()
|
||||
return updated
|
||||
finally:
|
||||
cur.close()
|
||||
_put_db_connection(conn)
|
||||
|
||||
def set_hidden(filename, hidden: bool):
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
@@ -120,6 +120,39 @@ async def _track_activity(request, call_next):
|
||||
_last_request_time = time.time()
|
||||
return await call_next(request)
|
||||
|
||||
def _sync_preloaded_images():
|
||||
"""Update PRELOADED_IMAGES in car.html (both source and output) to match current DB state."""
|
||||
try:
|
||||
output_dir = _load_output_dir()
|
||||
paths = [
|
||||
os.path.join(_HERE, "car.html"),
|
||||
os.path.join(output_dir, "car.html")
|
||||
]
|
||||
|
||||
persons = database.list_persons(include_archived=False)
|
||||
# Only include if file actually exists on disk
|
||||
db_images = [p[0] for p in persons if os.path.exists(os.path.join(output_dir, p[0]))]
|
||||
|
||||
# Sort by mtime, newest first
|
||||
db_images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
||||
|
||||
images_json = json.dumps(db_images, indent=12).strip()
|
||||
# Format for JS insertion
|
||||
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 ---'
|
||||
|
||||
for p in paths:
|
||||
if not os.path.exists(p): continue
|
||||
with open(p, 'r') as f:
|
||||
content = f.read()
|
||||
new_content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
||||
with open(p, 'w') as f:
|
||||
f.write(new_content)
|
||||
print(f"[static] Updated {p} with {len(db_images)} preloaded images")
|
||||
except Exception as e:
|
||||
print(f"[static] Failed to update car.html hydration: {e}")
|
||||
|
||||
def _sync_frontend():
|
||||
for name in ["car.html", "trash.html"]:
|
||||
src = os.path.join(_HERE, name)
|
||||
@@ -168,7 +201,7 @@ def _load_faceswap_model_path() -> str:
|
||||
|
||||
|
||||
def _run_consistency_check():
|
||||
"""Identifies DB records with missing files and files on disk with no DB record."""
|
||||
"""Identifies DB records with missing files, files on disk with no DB record, and archived items."""
|
||||
try:
|
||||
output_dir = _load_output_dir()
|
||||
data_dir = os.path.join(output_dir, "_data")
|
||||
@@ -176,8 +209,17 @@ def _run_consistency_check():
|
||||
|
||||
persons = database.list_persons(include_archived=True)
|
||||
missing_files = []
|
||||
archived_items = []
|
||||
for p in persons:
|
||||
filename = p[0]
|
||||
is_archived = bool(p[14]) if p[14] else False
|
||||
if is_archived:
|
||||
archived_items.append({
|
||||
"filename": filename,
|
||||
"group_id": p[2],
|
||||
"name": p[1]
|
||||
})
|
||||
|
||||
fpath = os.path.join(output_dir, filename)
|
||||
if not os.path.exists(fpath):
|
||||
missing_files.append({
|
||||
@@ -201,10 +243,11 @@ def _run_consistency_check():
|
||||
report = {
|
||||
"timestamp": time.time(),
|
||||
"missing_files": missing_files,
|
||||
"untracked_files": untracked_files
|
||||
"untracked_files": untracked_files,
|
||||
"archived_items": archived_items
|
||||
}
|
||||
_write_json(os.path.join(data_dir, "inconsistencies.json"), report)
|
||||
print(f"[consistency] check complete: {len(missing_files)} missing, {len(untracked_files)} untracked")
|
||||
print(f"[consistency] check complete: {len(missing_files)} missing, {len(untracked_files)} untracked, {len(archived_items)} archived")
|
||||
return report
|
||||
except Exception as e:
|
||||
print(f"[consistency] check failed: {e}")
|
||||
@@ -1236,10 +1279,16 @@ def _write_all_static() -> None:
|
||||
|
||||
# images.json — all images (frontend filters by .archived field)
|
||||
db_images = []
|
||||
archived_count = 0
|
||||
for p in persons:
|
||||
fpath = os.path.join(output_dir, p[0])
|
||||
if not os.path.exists(fpath):
|
||||
continue
|
||||
|
||||
is_archived = bool(p[14]) if p[14] else False
|
||||
if is_archived:
|
||||
archived_count += 1
|
||||
|
||||
db_images.append({
|
||||
"filename": p[0],
|
||||
"name": p[1],
|
||||
@@ -1255,8 +1304,9 @@ def _write_all_static() -> None:
|
||||
"has_clothing": p[11],
|
||||
"content_type": p[12] or "image",
|
||||
"faceswap_source_video": p[13],
|
||||
"archived": bool(p[14]) if p[14] else False,
|
||||
"archived": is_archived,
|
||||
})
|
||||
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
|
||||
try:
|
||||
db_images.sort(
|
||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
||||
@@ -1293,6 +1343,9 @@ def _write_all_static() -> None:
|
||||
_write_json(os.path.join(data_dir, "config.json"), json.load(_f))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Update car.html PRELOADED_IMAGES
|
||||
_sync_preloaded_images()
|
||||
|
||||
except Exception as e:
|
||||
print(f"[static] write_all error: {e}")
|
||||
@@ -1617,8 +1670,11 @@ def update_config(update: ConfigUpdate):
|
||||
return {"seed": conf["seed"]}
|
||||
|
||||
|
||||
class GroupArchiveRequest(BaseModel):
|
||||
filenames: list[str]
|
||||
|
||||
class RepairRequest(BaseModel):
|
||||
action: str # "delete_record", "import_file"
|
||||
action: str # "delete_record", "import_file", "restore", "delete_permanently"
|
||||
filename: str
|
||||
|
||||
class BatchRequest(BaseModel):
|
||||
@@ -2309,7 +2365,7 @@ def get_inconsistencies(run_now: bool = False):
|
||||
|
||||
@app.post("/db/repair")
|
||||
def db_repair(req: RepairRequest):
|
||||
"""Repair a specific inconsistency."""
|
||||
"""Repair a specific inconsistency or manage archived items."""
|
||||
output_dir = _load_output_dir()
|
||||
if req.action == "delete_record":
|
||||
database.delete_person(req.filename)
|
||||
@@ -2325,6 +2381,30 @@ def db_repair(req: RepairRequest):
|
||||
_invalidate_static()
|
||||
_run_consistency_check()
|
||||
return {"status": "imported", "filename": req.filename}
|
||||
elif req.action == "restore":
|
||||
database.set_archived(req.filename, False)
|
||||
_invalidate_static()
|
||||
_run_consistency_check()
|
||||
return {"status": "restored", "filename": req.filename}
|
||||
elif req.action == "delete_permanently":
|
||||
person = database.get_person(req.filename)
|
||||
# In person table, p[5] is filepath.
|
||||
# Wait, let me check the column order in database.py
|
||||
# list_persons: 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, face_embedding
|
||||
# get_person: returns same?
|
||||
if person:
|
||||
# We need the absolute path. get_person returns relative path usually or we construct it.
|
||||
# Actually database.py upsert_person stores filepath if provided.
|
||||
# Let's check database.get_person.
|
||||
pass
|
||||
|
||||
fpath = os.path.join(output_dir, req.filename)
|
||||
if os.path.exists(fpath):
|
||||
_move_to_trash(fpath)
|
||||
database.delete_person(req.filename)
|
||||
_invalidate_static()
|
||||
_run_consistency_check()
|
||||
return {"status": "deleted_permanently", "filename": req.filename}
|
||||
else:
|
||||
raise HTTPException(400, "Invalid action")
|
||||
|
||||
@@ -2574,6 +2654,7 @@ def archive_image(filename: str):
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
_invalidate_static()
|
||||
_run_consistency_check()
|
||||
return {"filename": filename, "archived": True}
|
||||
|
||||
|
||||
@@ -2584,6 +2665,7 @@ def unarchive_image(filename: str):
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
_invalidate_static()
|
||||
_run_consistency_check()
|
||||
return {"filename": filename, "archived": False}
|
||||
|
||||
|
||||
@@ -2744,6 +2826,53 @@ def delete_image(filename: str):
|
||||
return {"status": "deleted", "filename": filename}
|
||||
|
||||
|
||||
@app.post("/groups/{group_id:path}/archive")
|
||||
def archive_group(group_id: str, req: GroupArchiveRequest = None):
|
||||
try:
|
||||
filenames = req.filenames if req else []
|
||||
# If no filenames provided in body, try to find them by group_id
|
||||
if not filenames:
|
||||
files = database.get_group_files(group_id)
|
||||
filenames = [f[0] for f in files]
|
||||
|
||||
# Still nothing? Archive the group_id as a filename fallback
|
||||
if not filenames:
|
||||
filenames = [group_id]
|
||||
|
||||
updated = database.set_filenames_archived(filenames, True)
|
||||
count = len(updated)
|
||||
print(f"[archive] Archived group {group_id} ({count} items): {updated}")
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
_invalidate_static()
|
||||
_run_consistency_check()
|
||||
return {"status": "archived", "group_id": group_id, "count": count, "filenames": updated}
|
||||
|
||||
|
||||
@app.post("/groups/{group_id:path}/unarchive")
|
||||
def unarchive_group(group_id: str, req: GroupArchiveRequest = None):
|
||||
try:
|
||||
filenames = req.filenames if req else []
|
||||
if not filenames:
|
||||
# For unarchive, we might not have the files in the group yet if they are archived
|
||||
# but database.list_persons(include_archived=True) would find them.
|
||||
# Actually database.get_group_files(group_id) also finds them as it doesn't filter.
|
||||
files = database.get_group_files(group_id)
|
||||
filenames = [f[0] for f in files]
|
||||
|
||||
if not filenames:
|
||||
filenames = [group_id]
|
||||
|
||||
updated = database.set_filenames_archived(filenames, False)
|
||||
count = len(updated)
|
||||
print(f"[unarchive] Unarchived group {group_id} ({count} items): {updated}")
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
_invalidate_static()
|
||||
_run_consistency_check()
|
||||
return {"status": "unarchived", "group_id": group_id, "count": count, "filenames": updated}
|
||||
|
||||
|
||||
@app.delete("/groups/{group_id:path}")
|
||||
def delete_group(group_id: str):
|
||||
files = database.get_group_files(group_id)
|
||||
|
||||
@@ -131,13 +131,18 @@ Anatomically precise, Use the exact characteristics teenage face of Image 1.
|
||||
|
||||
# Labial Inspect
|
||||
|
||||
Recline on your back with legs spread wide apart and knees bent at a 90-degree angle, feet firmly planted on the floor.
|
||||
Place your hands behind your head with fingers interlaced, arching your back to accentuate your torso and pelvis.
|
||||
Tilt your head back slightly, ensuring your face remains visible to the camera.
|
||||
Close your eyes and slightly part your lips in a relaxed, open position.
|
||||
Ensure your body is both tense and yielding to touch and examination, with a hint of arousal subtle evident in your moist, glistening labials.
|
||||
Recline on your back with legs spread wide apart and knees bent at a 90-degree angle, feet firmly planted on the floor.
|
||||
Place your hands behind your head with fingers interlaced, arching your back to accentuate your torso and pelvis.
|
||||
Tilt your head back slightly, ensuring your face remains visible to the camera.
|
||||
Close your eyes and slightly part your lips in a relaxed, open position.
|
||||
Ensure your body is both tense and yielding to touch and examination, with a hint of arousal subtle evident in your moist, glistening labials.
|
||||
Perfect anatomy, photo realistic.
|
||||
|
||||
# Labial Instpec (lust)
|
||||
|
||||
Recline on your back with legs spread wide apart and knees bent at a 90-degree angle, feet firmly planted on the floor. Place your hands behind your head with fingers interlaced, arching your back to accentuate your torso and pelvis. Tilt your head forward towards the viewer, looking into the viewer with luster. Keep your eyes open and slightly part your lips in a relaxed, open position. Ensure your body is both tense and yielding to touch and examination, with a hint of arousal subtle evident in your moist, glistening labials. Perfect anatomy, photo realistic. Keep the identical same female
|
||||
as in the reference image
|
||||
|
||||
# Bride:
|
||||
|
||||
Lying flat on your back, bending one leg and bringing it up to the chest, the other leg stretched out.
|
||||
|
||||
@@ -75,6 +75,11 @@
|
||||
|
||||
<div id="loading" style="display:none">Running consistency check...</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Archived Items <span class="info">(Manually archived, recoverable)</span></h2>
|
||||
<div id="archivedList">No archived items found.</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Orphaned Records <span class="info">(In DB, but file missing on disk)</span></h2>
|
||||
<div id="missingList">No orphaned records found.</div>
|
||||
@@ -108,11 +113,17 @@
|
||||
body: JSON.stringify({ filename, action })
|
||||
});
|
||||
const d = await r.json();
|
||||
showToast(d.status === 'deleted' ? 'Record deleted' : 'File imported');
|
||||
let msg = 'Done';
|
||||
if (d.status === 'deleted') msg = 'Record deleted';
|
||||
else if (d.status === 'imported') msg = 'File imported';
|
||||
else if (d.status === 'restored') msg = 'Item restored';
|
||||
else if (d.status === 'deleted_permanently') msg = 'Permanently deleted';
|
||||
|
||||
showToast(msg);
|
||||
loadInconsistencies();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast('Repair failed');
|
||||
showToast('Action failed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +134,7 @@
|
||||
const r = await fetch(url);
|
||||
const d = await r.json();
|
||||
|
||||
renderArchived(d.archived_items || []);
|
||||
renderMissing(d.missing_files || []);
|
||||
renderUntracked(d.untracked_files || []);
|
||||
|
||||
@@ -138,6 +150,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderArchived(files) {
|
||||
const container = document.getElementById('archivedList');
|
||||
if (files.length === 0) {
|
||||
container.innerHTML = 'No archived items found.';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = files.map(f => `
|
||||
<div class="item">
|
||||
<div>
|
||||
<div class="filename">${f.filename}</div>
|
||||
<div class="info">Group: ${f.group_id || 'none'} | Name: ${f.name || 'none'}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn success" onclick="repair('${f.filename}', 'restore')">Restore</button>
|
||||
<button class="btn danger" onclick="if(confirm('Permanently delete ${f.filename}?')) repair('${f.filename}', 'delete_permanently')">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderMissing(files) {
|
||||
const container = document.getElementById('missingList');
|
||||
if (files.length === 0) {
|
||||
|
||||
@@ -227,9 +227,14 @@ def update_car_html():
|
||||
return
|
||||
|
||||
try:
|
||||
# Use database to list only non-archived images
|
||||
persons = database.list_persons(include_archived=False)
|
||||
db_images = {p[0] for p in persons}
|
||||
|
||||
# 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"]
|
||||
images = [f for f in os.listdir(output_dir)
|
||||
if f.lower().endswith(extensions) and f != "car.html" and f in db_images]
|
||||
|
||||
# Sort by mtime, newest first
|
||||
images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
||||
|
||||
Reference in New Issue
Block a user