|
Download docs/CONTRACT.md from Affaad/birdiee: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/Affaad/birdiee/resolve/main/docs/CONTRACT.md
- Command line
-
hf download hf://spaces/Affaad/birdiee/docs/CONTRACT.md
-
curl -L -o CONTRACT.md https://huggingface.co/spaces/Affaad/birdiee/resolve/main/docs/CONTRACT.md
10.3 kB
| # Birdie data contract | |
| This file is the merge contract between the two backend repos. Anything that produces or consumes seat data must use | |
| the models below (they live in `birdie/models.py`). Map your code onto the public functions listed per module; private | |
| helpers (leading underscore) are free to change. | |
| Rules: | |
| - `birdie/` never imports Gradio. UI code lives only in `app.py` and `birdie/ui/`. | |
| - Every timestamp is a timezone-aware `datetime` in Doha time (UTC+3, `birdie.clock.DOHA_TZ`). | |
| - Everything time-based takes a `clock` (`RealClock` in the app, `FakeClock` in tests). Nothing sleeps in tests. | |
| - Timers and thresholds come from `birdie.config.TIMERS` (demo or production, behind `DEMO_MODE`). | |
| - Seat polygons are normalised `(x, y)` corners in `0..1`, so they work at any camera resolution. | |
| - The app does not persist source frames. Hosted Space logging and retention must be checked separately. The app stores seat states and aggregate counts. | |
| ## Models (`birdie/models.py`) | |
| | Model | Fields | | |
| |---|---| | |
| | `Venue` | `venue_id: str`, `name: str`, `area: str`, `type: VenueType` (`cafe`, `library`, `cowork`, `study_room`), `lat: float`, `lon: float`, `zones: list[str]`, `source: VenueSource` (`live`, `recorded`, `simulated`), optional `floor_plan` and display metadata | | |
| | `Seat` | `seat_id: str`, `venue_id: str`, `zone: str`, optional `table_id`, optional customer-map coordinates, `seat_polygon` and `workspace_polygon` (normalised camera regions), `has_outlet`, `has_window` | | |
| | `StudyTable` | `table_id`, `table_type` (`shared`, `private`, `single_person_desk`), `capacity`, `seat_ids`, normalised `table_polygon`, `is_shareable`, `is_available_for_studying` | | |
| | `SeatSnapshot` | `venue_id`, `seat_id`, `zone`, `has_outlet`, `state: SeatState` (`UNKNOWN`, `FREE`, `OCCUPIED`, `HELD`, `REVIEW_REQUIRED`, `UNAVAILABLE`), timestamps, detector evidence, grace-period countdown, review and override flags | | |
| | `SeatEvent` | `ts: datetime`, `venue_id`, `seat_id`, `old_state: SeatState \| None`, `new_state: SeatState` | | |
| | `Query` | `area: str \| None`, `party_size: int`, `needs_outlet: bool`, `quiet: bool`, `zone: str \| None`, `venue_type: VenueType \| None`, `arrival_minutes: int` | | |
| | `VenueConfig` | `venue`, `seats`, `tables`, `unmapped_seat_ids`, private mirror-exclusion regions, legacy `camera_table_regions` fallback, and optional `recording_sha256` tying calibration to one clip | | |
| With an explicit `tables` layout, each configured seat must link to exactly one table and appear in that table's `seat_ids`. Known IDs awaiting calibration live in `unmapped_seat_ids`; they are excluded from capacity and availability. Camera coordinates stay private and are not returned by the public venue API. | |
| An API seat is a verified study space only when its state is `FREE`, its configured chair is detected above the detector confidence threshold, and its assigned workspace is clear. A missing chair, uncertain/shared item, missing polygon, unknown seat state, or disabled table is never counted as usable. Shared tables are evaluated seat by seat. Private or non-shareable tables are one group space and require every seat to be verified free. A belongings timeout only raises staff review; it never frees the blocked workspace. `counts.usable_study_spaces` is the customer-facing total; `counts.free` remains the lower-level occupancy count for compatibility. | |
| Zone ids used by the demo venues: `quiet`, `open`, `group`, `family`, `women`, `terrace`, `indoor` | |
| (labels in `birdie.venues.ZONE_LABELS`). | |
| ## Storage (`birdie/storage.py`) | |
| SQLite at `data/birdie.db` (gitignored). `Repository(path)`; use `Repository(":memory:")` in tests. | |
| | Function | Purpose | | |
| |---|---| | |
| | `add_seat_event(event, synthetic=False)`, `add_seat_events(events, synthetic=False)` | Append to the seat event log. `synthetic=True` marks back-filled history for simulated venues | | |
| | `seat_events(venue_id=None, since=None, until=None, include_synthetic=True) -> list[SeatEvent]` | Read the event log in time order | | |
| | `record_occupancy(venue_id, ts, free, occupied, held)`, `occupancy_buckets(venue_id, since=None)` | Live occupancy in 15-minute buckets | | |
| | `add_alert(...)`, `alerts(venue_id=None, unresolved_only=False)`, `resolve_alerts(venue_id, seat_id, kind=None)` | Staff alerts | | |
| | `save_seat_snapshots(snapshots)`, `seat_snapshots(venue_id)` | Persist current state and unattended timer timestamps across process restarts | | |
| ## Venues (`birdie/venues.py`) | |
| `load_all() -> dict[str, VenueConfig]`, `load_venue(venue_id)`, `save_venue(config)`, `zone_label(zone)`. | |
| ## Vision (`birdie/vision/`) | |
| | Function | Purpose | | |
| |---|---| | |
| | `detector.detect(frame, threshold=0.4) -> list[{label, score, box}]` | RT-DETR r18 (`PekingU/rtdetr_r18vd`). Keeps COCO classes `chair`, `person`, `backpack, handbag, suitcase, laptop, cup, bottle, book, cell phone`. `box = [x1, y1, x2, y2]` in pixels of the frame passed in. Resizes to long side <= 640 internally | | |
| | `detector.resize_long_side(frame, 640) -> PIL.Image`, `detector.load()` | Helpers. `_infer` is the pure (frame in, detections out) function wrapped with `@spaces.GPU` when `spaces` is importable | | |
| | `seats.assign_detections(detections, seats, (w, h), min_overlap=0.2, table_regions=None) -> {seat_id: [det]}` | Assign person/item evidence to configured seat/workspace regions; ambiguous shared-table items remain unknown | | |
| | `seats.observe_evidence(assigned)` | Confidence-aware `PERSON`, `ITEMS`, `EMPTY`, or conservative `UNKNOWN` evidence | | |
| | `seats.workspace_evidence(detections, seats, (w, h))` | Verify a visible chair and clear/blocked/uncertain desk workspace for each calibrated seat | | |
| | `state.SeatStateMachine(venue_id, seat_id, clock, k=3, hold_seconds=...)` | Smooths evidence; belongings become `HELD`, then `REVIEW_REQUIRED` after the real-time grace period | | |
| | `state.VenueTracker(venue_id, seats, clock, on_event)` | `.process({seat_id: Observation}) -> [SeatEvent]` (missing seats = UNKNOWN), `.tick()` (hold timers and stale-feed expiry), `.snapshots()`, `.counts()`, `.force(states)`, `.set_seats(seats)` | | |
| | `render.annotate(frame, detections)` | RGB frame with detector boxes only; no static seat regions and no blurring | | |
| | `render.to_data_uri(frame)` | In-memory JPEG data URL (frames never touch disk) | | |
| | `clip.iter_clip_frames(path, fps=1)` | Sample a recorded clip at 1 fps, in memory | | |
| | `demo.replay_recording(svc, venue_id, path, stop)` | Repeatedly process a configured clip and invalidate seat states on failure | | |
| ## Forecast (`birdie/forecast/`) | |
| | Function | Purpose | | |
| |---|---| | |
| | `calendar.is_weekend(dt)` (Friday + Saturday), `day_type(dt)` (`weekday`/`friday`/`saturday`), `is_friday_prayer_quiet(dt)`, `is_ramadan(dt)`, `is_exam_season(dt)`, `is_terrace_season(dt)`, `slot_of(dt)`, `features(dt)` | Qatar calendar. `RAMADAN_RANGES`, `EXAM_RANGES` are editable | | |
| | `synthetic.profile(venue_type, ts)`, `synthetic.generate_history(venue, n_seats, end, weeks=4) -> [{ts, occupied, free, total, synthetic}]` | Synthetic history, always labelled synthetic | | |
| | `model.BaselineForecaster(venue_type).fit(history).predict_ratio(ts)` | Default forecaster (occupancy ratio 0..1) | | |
| | `model.ChronosForecaster().predict(contexts, steps)`, `model.load_chronos()` | Optional `amazon/chronos-bolt-small`; raises `ForecasterUnavailable` / returns None if it can't load | | |
| | `model.ForecastEngine(configs, clock, repo).ratio_at(venue_id, ts)` | What the app uses (`BIRDIE_USE_CHRONOS=1` to try Chronos) | | |
| ## Decision (`birdie/decision.py`) | |
| Functions take a `LiveData` object: anything with `now()`, `venues()`, `venue(id)`, `seats(id)`, `snapshots(id)`, | |
| and `forecast_ratio(id, ts)`. `BirdieService` implements it. | |
| | Function | Purpose | | |
| |---|---| | |
| | `availability_at(data, venue_id, arrival_minutes) -> Availability` | `expected_free`, `low`, `high`, `live_free`, `forecast_free`, `live_weight`; `.summary()` | | |
| | `blend_availability(...)` | The pure blending rule (live weight `0.5 ** (arrival / 20 min)`) | | |
| | `focus_score(data, venue_id, arrival_minutes) -> FocusScore` | `score` 0..100 and `parts` (free 40, noise 25, outlets 20, trend 15) | | |
| | `recommend(data, query, top=3) -> [Recommendation]` | Filters by the `Query`, ranks by focus score; each has `reasons` | | |
| ## Simulator (`birdie/simulator.py`) | |
| `Simulator(configs, trackers, ratio_at, clock)`: `.step()` feeds observations to each simulated venue's | |
| `VenueTracker.process` (the camera's code path); `.backfill(repo, venue_id, hours=24)` writes a synthetic event log. | |
| ## Analytics (`birdie/analytics.py`) | |
| `venue_dashboard(repo, venue_id, seat_ids, now, hours=24, observed_since=None) -> Dashboard` with | |
| `occupancy_by_hour`, `peak_hours`, `held_minutes_total`, `held_minutes_by_seat`. Building blocks: `seat_intervals`, | |
| `occupancy_by_hour`, `held_minutes`. | |
| ## Service (`birdie/service.py`) | |
| `BirdieService(repo=None, clock=None, venues=None, timers=TIMERS, detector=None)` is the one object the UI talks to: | |
| `process_frame(venue_id, frame)` associates detections and updates seat state; `snapshots(venue_id)`, `zone_counts(venue_id)`, `upsert_seat(seat)`, | |
| `delete_seat(venue_id, seat_id)`, `study_availability(venue_id)`, `set_table_available_for_studying(...)`, `availability_at(venue_id, minutes)`, | |
| `forecast` (ForecastEngine), `simulator`, `tick()`, `start()` (back-fill + 2 s scheduler), `stop()`. | |
| `GET /api/venues/{venue_id}` returns table-level and seat-level `usable_for_studying`, `workspace_clear`, and `availability_reason`. Staff may disable/re-enable a seat or table through the bearer-token protected maintenance endpoints. Venue geometry changes are stored in `venues/<venue_id>.json` and require a fresh camera sample before availability returns. | |
| ## Assistant (`birdie/assistant/`, stub) | |
| `answer(data, question) -> AssistantAnswer` = `parse_query(question) -> Query` (Fanar, JSON) -> | |
| `recommend()` in Python -> `phrase_reply(...)` (Fanar, user's language). No tool calling. | |
| ## Mapping a second repo onto this contract | |
| 1. Convert your seat and venue objects to the models above (`Model.model_validate(dict)`). | |
| 2. Anything that observes seats should end in `VenueTracker.process({seat_id: Observation})`, so smoothing, | |
| hold timers and SeatEvents stay identical. | |
| 3. Anything that reads availability should go through `LiveData` + `birdie.decision`. | |