Spaces:
Running
Running
| """ | |
| One-time migration: upload local Location/<gender>/<n. name>/<image> | |
| into Supabase (Storage bucket + locations table), preserving the folder number | |
| as sort_order and using the clean label as the name. | |
| Prereqs: | |
| 1. Run supabase_schema.sql once in the Supabase SQL editor. | |
| 2. Set SUPABASE_URL + SUPABASE_SERVICE_KEY in .env. | |
| Run: python scripts/seed_supabase_locations.py | |
| Re-runnable (idempotent): re-uploading just overwrites the image + row. | |
| """ | |
| import os | |
| import re | |
| import io | |
| import sys | |
| # project root on path so `core` imports work when run from anywhere | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| except Exception: | |
| pass | |
| from PIL import Image, ImageOps # noqa: E402 | |
| from core import supabase_store # noqa: E402 | |
| LOCATIONS_DIR = "Location" | |
| IMG_EXTS = (".jpg", ".jpeg", ".png", ".webp", ".bmp") | |
| def clean_label(name: str) -> str: | |
| return re.sub(r"^\s*\d+\.\s*", "", name).strip() | |
| def folder_number(name: str) -> int: | |
| m = re.match(r"^\s*(\d+)", name) | |
| return int(m.group(1)) if m else 9999 | |
| def to_jpeg_bytes(path: str) -> bytes: | |
| pil = Image.open(path) | |
| pil = ImageOps.exif_transpose(pil) or pil # honour phone EXIF rotation | |
| buf = io.BytesIO() | |
| pil.convert("RGB").save(buf, format="JPEG", quality=92) | |
| return buf.getvalue() | |
| def main(): | |
| if not supabase_store.is_enabled(): | |
| print("β Supabase is not configured.") | |
| print(" Set SUPABASE_URL and SUPABASE_SERVICE_KEY in .env, then retry.") | |
| sys.exit(1) | |
| print(f"β Supabase: {supabase_store.SUPABASE_URL} bucket={supabase_store.BUCKET}\n") | |
| uploaded = empty = errors = removed = 0 | |
| for gender in ("Male", "Female"): | |
| gdir = os.path.join(LOCATIONS_DIR, gender) | |
| if not os.path.isdir(gdir): | |
| continue | |
| print(f"== {gender} ==") | |
| # Only folders that actually contain a photo, in the original 1β19 order. | |
| with_photos = [] | |
| for folder in sorted(os.listdir(gdir), key=folder_number): | |
| fpath = os.path.join(gdir, folder) | |
| if not os.path.isdir(fpath): | |
| continue | |
| imgs = [f for f in sorted(os.listdir(fpath)) if f.lower().endswith(IMG_EXTS)] | |
| if imgs: | |
| with_photos.append((clean_label(folder), os.path.join(fpath, imgs[0]))) | |
| else: | |
| print(f" Β· {clean_label(folder):<28} β no photo (skipped)") | |
| empty += 1 | |
| # Upload, RESEQUENCED 1..N (gaps from skipped folders removed). | |
| kept = set() | |
| for seq, (label, img_path) in enumerate(with_photos, start=1): | |
| try: | |
| data = to_jpeg_bytes(img_path) | |
| supabase_store.upsert_location(gender, label, data, sort_order=seq) | |
| kept.add(label) | |
| print(f" β {seq:>2}. {label:<28} β {os.path.basename(img_path)}") | |
| uploaded += 1 | |
| except Exception as e: | |
| print(f" β {seq:>2}. {label:<28} ERROR: {e}") | |
| errors += 1 | |
| # Remove any leftover Supabase rows for this gender that we didn't just | |
| # upload (e.g. empty seeded rows, or locations whose photo was removed). | |
| for row in supabase_store.list_locations(gender): | |
| if row["label"] not in kept: | |
| try: | |
| supabase_store.delete_location(gender, row["folder"]) | |
| print(f" β removed stale: {row['label']}") | |
| removed += 1 | |
| except Exception as e: | |
| print(f" ! could not remove {row['label']}: {e}") | |
| print() | |
| print(f"Done β {uploaded} uploaded, {empty} empty (skipped), " | |
| f"{removed} stale removed, {errors} errors.") | |
| if __name__ == "__main__": | |
| main() | |