This commit is contained in:
mike
2026-06-27 20:23:14 +02:00
parent 35306327f7
commit 52ae51fef3
7 changed files with 4493 additions and 134 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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