birdiee / docs /CONTRACT.md
Affaad's picture
Deploy Birdie 6ce7570+worktree
ea874ba verified
|
Raw History Blame Contribute Delete
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.