Download docs/CONTRACT.md from Affaad/birdie: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/spaces/Affaad/birdie/resolve/main/docs/CONTRACT.md
- Command line
-
hf download hf://spaces/Affaad/birdie/docs/CONTRACT.md
-
curl -L -o CONTRACT.md https://huggingface.co/spaces/Affaad/birdie/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.
The customer API groups detailed backend states into three availability statuses: free, taken (OCCUPIED or UNAVAILABLE), and held (HELD or REVIEW_REQUIRED). UNKNOWN is returned as checking, a camera-evidence indicator rather than an availability state. counts.free, counts.taken, counts.held_or_review, and counts.checking partition configured seats; legacy detailed state counters and the raw state/seat_state fields remain available for diagnostics. A 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.
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.