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
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 inapp.pyandbirdie/ui/.- Every timestamp is a timezone-aware
datetimein Doha time (UTC+3,birdie.clock.DOHA_TZ). - Everything time-based takes a
clock(RealClockin the app,FakeClockin tests). Nothing sleeps in tests. - Timers and thresholds come from
birdie.config.TIMERS(demo or production, behindDEMO_MODE). - Seat polygons are normalised
(x, y)corners in0..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
- Convert your seat and venue objects to the models above (
Model.model_validate(dict)). - Anything that observes seats should end in
VenueTracker.process({seat_id: Observation}), so smoothing, hold timers and SeatEvents stay identical. - Anything that reads availability should go through
LiveData+birdie.decision.