Spaces:
Runtime error
Runtime error
| """Seed target identity sightings from `seed_data/targets/{identity}/*.jpg`. | |
| For each identity folder, the script holds out one photo into | |
| `seed_data/holdout/{identity}.{ext}` (so you can use it as a query) and | |
| inserts the rest into the database, marked source='target' identity='{name}'. | |
| The identity column is for evaluation only — the search API never reads it. | |
| Run inside the backend container: | |
| python -m scripts.seed_targets | |
| """ | |
| import argparse | |
| import logging | |
| import random | |
| import shutil | |
| from pathlib import Path | |
| from PIL import Image | |
| from sqlalchemy import select | |
| from app.config import get_settings | |
| from app.models import LostDog, Sighting | |
| from scripts._common import ( | |
| insert_sighting, | |
| log, | |
| offset_within_meters, | |
| open_session, | |
| random_city_point, | |
| random_recent_timestamp, | |
| ) | |
| VALID_EXTS = {".jpg", ".jpeg", ".png", ".webp"} | |
| def _is_image(p: Path) -> bool: | |
| return p.is_file() and p.suffix.lower() in VALID_EXTS | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| "--force", | |
| action="store_true", | |
| help="Re-seed even if target rows already exist.", | |
| ) | |
| parser.add_argument("--seed", type=int, default=7) | |
| parser.add_argument( | |
| "--root", | |
| type=Path, | |
| default=Path("/seed_data"), | |
| help="Root containing targets/ and holdout/ subdirs.", | |
| ) | |
| args = parser.parse_args() | |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") | |
| rng = random.Random(args.seed) | |
| targets_root: Path = args.root / "targets" | |
| holdout_root: Path = args.root / "holdout" | |
| holdout_root.mkdir(parents=True, exist_ok=True) | |
| if not targets_root.exists(): | |
| log.info( | |
| "No targets directory at %s — drop dogs into seed_data/targets/{name}/ " | |
| "and re-run. Skipping.", | |
| targets_root, | |
| ) | |
| return | |
| identity_dirs = [d for d in sorted(targets_root.iterdir()) if d.is_dir()] | |
| if not identity_dirs: | |
| log.info( | |
| "targets/ is empty — drop ~5 dogs with 2-3 photos each into " | |
| "seed_data/targets/{name}/ and re-run. Skipping." | |
| ) | |
| return | |
| session = open_session() | |
| try: | |
| existing = session.scalar( | |
| select(Sighting).where(Sighting.source == "target").limit(1) | |
| ) | |
| if existing and not args.force: | |
| log.info("Target rows already present — skipping (pass --force to re-seed).") | |
| return | |
| if existing and args.force: | |
| # Cascade-delete the LostDog rows; FK ondelete=CASCADE will remove | |
| # the linked sightings too. | |
| session.query(LostDog).filter( | |
| LostDog.id.in_( | |
| select(Sighting.lost_dog_id) | |
| .where(Sighting.source == "target") | |
| .where(Sighting.lost_dog_id.is_not(None)) | |
| ) | |
| ).delete(synchronize_session=False) | |
| # Catch any orphan target sightings (no lost_dog_id) too. | |
| session.query(Sighting).filter(Sighting.source == "target").delete() | |
| session.commit() | |
| settings = get_settings() | |
| total_dogs = 0 | |
| total_photos = 0 | |
| for ident_dir in identity_dirs: | |
| identity = ident_dir.name | |
| photos = sorted([p for p in ident_dir.iterdir() if _is_image(p)]) | |
| if len(photos) < 2: | |
| log.warning( | |
| "Identity '%s' has %d photo(s); need at least 2 (one for index, " | |
| "one as holdout query). Skipping.", | |
| identity, | |
| len(photos), | |
| ) | |
| continue | |
| # One "home turf" point per identity — every photo of this dog is placed | |
| # within seed_target_radius_m (default 100 m) of this center, so the | |
| # matched sightings cluster tightly on the map. | |
| home_lat, home_lng = random_city_point(rng) | |
| # Hold out one photo deterministically (last one alphabetically). | |
| holdout_src = photos[-1] | |
| index_photos = photos[:-1] | |
| holdout_dst = holdout_root / f"{identity}{holdout_src.suffix.lower()}" | |
| shutil.copyfile(holdout_src, holdout_dst) | |
| log.info( | |
| "Identity '%s': home=(%.5f, %.5f), indexing %d photo(s), holdout=%s", | |
| identity, | |
| home_lat, | |
| home_lng, | |
| len(index_photos), | |
| holdout_dst.name, | |
| ) | |
| # Create the LostDog cluster row first so FKs are valid. | |
| lost_dog = LostDog( | |
| name=identity.title(), | |
| last_seen_at=random_recent_timestamp(rng), | |
| last_seen_lat=home_lat, | |
| last_seen_lng=home_lng, | |
| contact_name=f"{identity.title()}'s Owner", | |
| contact_email="koenig.andres@gmail.com", | |
| contact_phone=None, | |
| ) | |
| session.add(lost_dog) | |
| session.flush() # populate lost_dog.id | |
| total_dogs += 1 | |
| for photo in index_photos: | |
| try: | |
| image = Image.open(photo) | |
| image.load() | |
| except Exception as exc: # noqa: BLE001 | |
| log.warning("Could not open %s: %s", photo, exc) | |
| continue | |
| coords = offset_within_meters( | |
| rng, home_lat, home_lng, settings.seed_target_radius_m | |
| ) | |
| row = insert_sighting( | |
| session, | |
| image, | |
| source="target", | |
| identity=identity, | |
| coords=coords, | |
| rng=rng, | |
| prefix="target", | |
| lost_dog_id=lost_dog.id, | |
| ) | |
| if row is not None: | |
| total_photos += 1 | |
| session.commit() | |
| log.info( | |
| "Seeded %d lost-dog clusters with %d total reference photos.", | |
| total_dogs, | |
| total_photos, | |
| ) | |
| finally: | |
| session.close() | |
| if __name__ == "__main__": | |
| main() | |