Skip to content
MuizzyrankingPublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

GallerAI

Event photo and video sharing powered by face recognition. Find yourself in every moment.

Organizers create events, bulk-upload photos/videos, and share a link. Attendees open the link (entering a passcode if the event is private), browse the gallery with All / Photos / Videos tabs, take a selfie to find their own photos, and download singles or zips. Attendees never need an account — only organizers log in.

Interactive docs: GET /docs.


How it works

Upload (organizer + approved attendee uploads)

Frontend splits 1000 files into chunks of ~25, POSTs chunks in parallel
        ↓
FastAPI streams each file to TempStorage, hashing while writing (one I/O pass)
        ↓
Per chunk: one dedupe query on (event_id, file_hash), one bulk insert
(conflict → row-by-row retry, race duplicates dropped, temp cleaned)
        ↓
Celery tasks dispatched immediately per chunk (images + videos split)
        ↓
Image: RetinaFace detect (downscaled to 1600px) → ArcFace 512-d embeddings
       → face_embeddings rows (pgvector) → promote via save_from_path()
Video: promote only (no face pipeline)
        ↓
Redis event:{id}:progress + pub/sub → SSE GET /media/{id}/upload-progress

Attendee uploads with require_upload_approval=true land as PENDING_APPROVAL (hidden) until POST .../approve. Rejected items go to the bin.

Face search (always anonymous)

Selfie → extract_single_embedding (largest face) → pgvector cosine search
  SQL: MAX(1 - (embedding <=> :q)) per media, threshold 0.6, limit 1000
        ↓
Matches + embedding stored in Redis under scan_token (TTL 2h, default 7200s)
Embedding never touches the users table
        ↓
GET /gallery/anonymous?token=&type=&page= → paginated MediaCards with match_score

Flags (feedback only)

POST /gallery/flag {media_id, reason, scan_token?, note?} stores a row in media_flags (no user). A flag means "bad match", never deletes the photo. Organizers read GET /gallery/flags.

Deletes = soft + bin

DELETE /media/{id}/{media_id} sets is_deleted + deleted_at. Bin: GET /media/{id}/bin/events (sidebar) → GET /media/{id}/bin → restore / hard-delete / empty.


Project structure

app/
  api/router/      auth.py event.py media.py faces.py gallery.py downloads.py admin.py
  repositories/    media_repo.py flag_repo.py event_repo.py admin_repo.py  # pure DB, no logic
  services/        media_service.py (ingest) face_service.py search_service.py
                   gallery_service.py (anon store + flags) download_service.py
                   event_service.py auth/ storage_service/
  models/          user.py event.py media.py media_flag.py face_embedding.py
  schemas/         event.py media.py face.py gallery.py
  core/            pagination.py (Pydantic inherit) schemas.py (ApiResponse[T] + meta)
  workers/         media_tasks.py celery_app.py
alembic/versions/  0001_enable_extensions  2f1be46638be_models  3a1b2c4d5e6f_soft_delete_flags
API_CONTRACT.md    # hand this to the frontend agent

Key patterns:

  • Pagination: inherit PaginationParams (page, page_size, offset property, build_meta(total)). Router takes Annotated[MyParams, Query()] → ?page=&page_size=&type= in one object. Lists return ApiResponse[data, meta].
  • ApiResponse: ApiResponse[T] (PEP 695) with message, data, meta: PaginationMeta | None.
  • Repos vs services: repos do SELECT ... WHERE event + not deleted + type + offset/limit; services do ingest, face ML, Redis, storage, zip streaming.
  • Storage: TempStorage.stage_upload always first; BaseStorage.save_from_path promotes to local now, Cloudflare R2 later (same call shape).

Getting started

cp .env.example .env
uv sync
docker compose up -d              # postgres + redis (see docker-compose.yml)
uv run alembic upgrade head
uv run uvicorn app.main:app --reload --port 8000
uv run celery -A app.workers.celery_app worker --loglevel=info --concurrency=2

Docs: http://localhost:8000/docs · Health: GET /health.

Upload at scale (1000 files)

  • Frontend: chunks of 25, 4–6 parallel POSTs, one SSE stream for progress + ETA.
  • Backend: INGEST_CHUNK_SIZE=25, INGEST_CONCURRENCY=8; Celery --concurrency = CPU cores (face detection is CPU-bound, model warmed once per worker).
  • Deduped by SHA256 per event; response reports accepted / skipped / errors.

Tabs

GET /media/{id}/counts → {total, images, videos} for badges. GET /media/{id}?type=all|image|video&page=&page_size= for each tab.


Environment

Var Default Purpose
SECRET_KEY — JWT signing (set a strong value)
POSTGRES_HOST/PORT/DB/USER/PASSWORD localhost/5432/gallerai asyncpg + pgvector
REDIS_HOST/PORT/DB localhost/6379/0 Celery broker, progress pub/sub, scan tokens, cache
STORAGE_BACKEND local local now, cloudflare later
TEMP_STORAGE_ROOT / LOCAL_STORAGE_FINAL_ROOT ./storage/tmp, ./storage/final staging vs permanent (separate dirs)
FACE_SIMILARITY_THRESHOLD 0.6 stricter = fewer false positives
FACE_DETECTION_CONFIDENCE / FACE_MIN_SIZE 0.9 / 80 quality filters
ANONYMOUS_SCAN_TTL_SECONDS 7200 scan_token lifetime
GOOGLE_CLIENT_ID/SECRET/REDIRECT_URI — creator Google login
SMTP_* / FRONTEND_URL — transactional email + links

Design decisions

  • No attendee accounts. Earlier drafts had UserEventGallery, claim-into-account, User.face_embedding. Deleted in 3a1b2c4d5e6f: attendees are anonymous + passcode cookie + scan_token. Only organizers have User rows (plus co-organizers via event_members).
  • Nullable storage_backend: NULL = not promoted yet; local is a real permanent backend, not scratch.
  • Temp vs final split: every upload stages first, detection runs on temp, promotion copies to final, temp deleted by one path.
  • Event-scoped progress: keyed by event_id, not upload id — reconnect-safe, aggregates concurrent uploaders.
  • Model warmup once per worker (worker_process_init), Celery concurrency = parallelism (no nested process pools).
  • is_private legacy: column kept but ignored for visibility; everything shows unless soft-deleted.
  • Admin: is_admin Bearer gates /api/v1/admin/* (users, events, flags, settings, stats). Archive/restore are reversible; hard deletes wipe storage too.
  • Serve through app: GET .../serve/{key} checks access + deleted, proxies local bytes, 302s to CDN once cloud is configured.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages