Quality scan v4: calibrated PIL verdicts (dark/blurry, 300px downscale, threshold 200); persist status+flag_reason; lazy thumbnails (11.5s→0.08s page load)

This commit is contained in:
2026-08-09 14:11:54 +10:00
parent 03f8af42c5
commit 2945b1c0f6
4 changed files with 126 additions and 138 deletions

View File

@@ -45,20 +45,11 @@ def _is_image(p: Path) -> bool:
def _thumb(path: str):
"""Return thumbnail URL WITHOUT generating (lazy — /thumbs/ generates on first hit)."""
src = Path(path)
if not src.exists():
return None
key = src.stem + "_" + str(abs(hash(str(src))))[:8] + ".jpg"
THUMB_DIR.mkdir(parents=True, exist_ok=True)
dest = THUMB_DIR / key
if not dest.exists():
try:
with Image.open(src) as im:
im.convert("RGB")
im.thumbnail(THUMB_SIZE)
im.save(dest, "JPEG", quality=70)
except Exception:
return None
return f"/thumbs/{key}"
@@ -73,6 +64,7 @@ def _load_item(row: sqlite3.Row) -> dict:
"thumb": _thumb(row["path"]) if _is_image(p) else None,
"exists": p.exists(),
"size_mb": round(p.stat().st_size / 1e6, 1) if p.exists() else None,
"flag_reason": row["flag_reason"] if "flag_reason" in row.keys() else None,
}
@@ -99,7 +91,7 @@ def index(request: Request):
def review(request: Request, source: str = None, status: str = None, page: int = 1):
conn = _conn()
per_page = 200
q = "SELECT sha256, path, status, source FROM image_hashes WHERE 1=1"
q = "SELECT sha256, path, status, source, flag_reason FROM image_hashes WHERE 1=1"
count_q = "SELECT COUNT(*) FROM image_hashes WHERE 1=1"
params = []
if source:
@@ -111,8 +103,9 @@ def review(request: Request, source: str = None, status: str = None, page: int =
count_q += " AND status=?"
params.append(status)
else:
q += " AND status IN ('scanned','review')"
count_q += " AND status IN ('scanned','review')"
# default review queue: flagged items first, then keep, then scanned
q += " AND status IN ('review','delete_candidate','keep','scanned')"
count_q += " AND status IN ('review','delete_candidate','keep','scanned')"
total = conn.execute(count_q, params).fetchone()[0]
pages = max(1, (total + per_page - 1) // per_page)
page = max(1, min(page, pages))
@@ -234,12 +227,39 @@ def full_file(sha: str):
@app.get("/thumbs/{name}")
def thumb_file(name: str):
"""Serve thumbnail; generate on first request (cached after)."""
f = THUMB_DIR / name
if not f.exists():
raise HTTPException(404)
# lazy generate — reconstruct source path from key (stem is orig filename)
THUMB_DIR.mkdir(parents=True, exist_ok=True)
# find the source image: key = <stem>_<hash8>.jpg
stem = name.rsplit("_", 1)[0]
src = _find_source(stem)
if not src:
raise HTTPException(404)
try:
with Image.open(src) as im:
im.convert("RGB")
im.thumbnail(THUMB_SIZE)
im.save(f, "JPEG", quality=70)
except Exception:
raise HTTPException(404)
return FileResponse(f)
def _find_source(stem: str):
"""Find the original image for a thumbnail key (by filename stem)."""
import os
for root_dir in ("/mnt/ubuntu_storage_3TB/archive/03_photos",
"/mnt/data/01_keep", "/mnt/data/02_review", "/mnt/data/03_delete"):
for dirpath, _, files in os.walk(root_dir):
for fn in files:
if fn.rsplit(".", 1)[0] == stem:
return Path(dirpath) / fn
return None
@app.get("/upload", response_class=HTMLResponse)
def upload_page(request: Request):
"""Upload page — drop files/archives into the incoming folder."""