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

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)