From 2d379bece755443ad8d9ddc0da526a69552bcd73 Mon Sep 17 00:00:00 2001 From: default Date: Sun, 12 Apr 2026 03:27:52 +0000 Subject: [PATCH] Backend integration with the website UI --- .gitignore | 6 +- Dockerfile | 32 +-- README.md | 6 +- backend/Dockerfile | 2 +- backend/README.md | 24 ++ backend/requirements.txt | 2 + backend/server.py | 100 ++++++++ docker-compose.yml | 17 +- scripts/generate_fake_data.py | 105 ++++++++ website/bun.lock | 3 + website/package.json | 1 + website/server/app.ts | 9 - .../src/lib/components/InteractiveMap.svelte | 234 +++++++++++++++++- website/src/routes/+layout.ts | 2 + website/src/routes/history/+page.svelte | 80 +++++- website/svelte.config.js | 12 +- 16 files changed, 568 insertions(+), 67 deletions(-) create mode 100644 scripts/generate_fake_data.py delete mode 100644 website/server/app.ts create mode 100644 website/src/routes/+layout.ts diff --git a/.gitignore b/.gitignore index d274230..d9bc459 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,8 @@ build/ venv/ -__pycache__/ \ No newline at end of file +__pycache__/ + +*.pt + +static/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f87c93a..2684887 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,30 @@ +# Build frontend FROM oven/bun:latest AS builder -WORKDIR /app - -# Install dependencies +WORKDIR /project/website COPY website/package.json website/bun.lock ./ RUN bun install --frozen-lockfile -# Build the SvelteKit application COPY website/ . -RUN bun run build +RUN mkdir -p ../backend && bun run build -# Setup the production environment -FROM oven/bun:latest +# Backend and final image +FROM python:3.9-slim + +RUN apt-get update && \ + apt-get install -y ffmpeg libgl1 libglib2.0-0 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* WORKDIR /app -# Copy production dependencies configuration and install -COPY --from=builder /app/package.json /app/bun.lock ./ -RUN bun install --production --frozen-lockfile +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -# Copy the build output and the custom Bun server -COPY --from=builder /app/build ./build -COPY --from=builder /app/server ./server +COPY backend/ . +# Copy static build from builder +COPY --from=builder /project/backend/static /app/static -EXPOSE 3000 +EXPOSE 8000 -CMD ["bun", "run", "server/app.ts"] +CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md index 2b2fa87..e193104 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,13 @@ MicroPython scripts for the M5Stack M5GO device. ## Docker Setup -The entire stack can be run via Docker Compose, which builds both the Svelte website and the Python AI Backend. +The entire stack can be run via Docker Compose, which builds both the Svelte website and the Python AI Backend into a single seamless container. ```bash docker-compose up --build ``` -- **Web Frontend**: Runs on port `3000` -- **AI Backend**: Runs on port `8000` +- **App (Frontend + Backend)**: Runs on port `8000` ## Configuration @@ -56,6 +55,7 @@ ELEVENLABS_API_KEY=your_elevenlabs_api_key_here ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb TERP_AI_BEARER_TOKEN=your_jwt_token_here TERP_AI_CONVERSATION_ID=5e752e56-06c6-ec73-1f13-456029ce1299 +MONGODB_URI=mongodb_url_here ``` Update the `/m5go/main.py` file to include your Wi-Fi credentials and the correct local IP for the WebSocket (`WS_URL`). \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 1c09e8c..d3f2ba4 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.9-slim # Install ffmpeg and other necessary packages RUN apt-get update && \ - apt-get install -y ffmpeg libgl1-mesa-glx libglib2.0-0 && \ + apt-get install -y ffmpeg libgl1 libglib2.0-0 && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/backend/README.md b/backend/README.md index efe6378..9062ceb 100644 --- a/backend/README.md +++ b/backend/README.md @@ -100,6 +100,30 @@ Returns a JSON object detailing the room status, counts, and pairings. } ``` +### `/api/study-rooms` (GET) + +Returns a list of all recorded study room data. + +### `/api/study-rooms/history` (GET) + +Returns a list of all recorded study room data from the last 24 hours, sorted by most recent first. + +**Response:** +```json +{ + "data": [ + { + "location": { + "type": "Point", + "coordinates": [-77.3079, 38.8315] + }, + "db": 65.2, + "date": "2026-04-12T14:30:00.000Z" + } + ] +} +``` + ## Client Integration Notes For the ESP32/M5GO client (`m5go/main.py`), ensure you update the `WS_URL` variable to point to the correct local IP address of the machine running this backend server. diff --git a/backend/requirements.txt b/backend/requirements.txt index e0b9c8d..5e2ca77 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,3 +8,5 @@ python-multipart ultralytics opencv-python-headless scipy +pymongo +pydantic diff --git a/backend/server.py b/backend/server.py index e3ec33a..a9b9251 100644 --- a/backend/server.py +++ b/backend/server.py @@ -8,13 +8,46 @@ import json import base64 import urllib.request from fastapi import FastAPI, WebSocket, WebSocketDisconnect, File, UploadFile +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime, timedelta +from pymongo import MongoClient from dotenv import load_dotenv from vision import analyze_room_image +from fastapi.middleware.cors import CORSMiddleware + load_dotenv() app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# MongoDB Setup +MONGO_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/") +mongo_client = MongoClient(MONGO_URI) +db = mongo_client.study_buddy_db +study_rooms_collection = db.study_rooms + +# Pydantic models for Study Room Data +class GeoJSONPoint(BaseModel): + type: str = "Point" + coordinates: List[float] + +class StudyRoomData(BaseModel): + room_id: Optional[str] = None + location: GeoJSONPoint + db: float + date: Optional[datetime] = None + SAMPLE_RATE = 16000 BITS_PER_SAMPLE = 16 NUM_CHANNELS = 1 @@ -253,3 +286,70 @@ async def check_room_status(file: UploadFile = File(...)): contents = await file.read() result = analyze_room_image(contents) return result + +UMD_LOCATIONS = [ + { "id": 'esj', "name": 'Edward St. John (ESJ)', "lng": -76.94209511596014, "lat": 38.987133359608755 }, + { "id": 'mckeldin', "name": 'McKeldin Library', "lng": -76.94494907523277, "lat": 38.986021017749366 }, + { "id": 'hornbake', "name": 'Hornbake Library', "lng": -76.94161787005467, "lat": 38.988233373664826 }, + { "id": 'stem', "name": 'STEM Library', "lng": -76.93942003731279, "lat": 38.988991437126195 }, + { "id": 'clarice', "name": 'Clarice Library', "lng": -76.9500912552473, "lat": 38.990547823732285 }, + { "id": 'yahentamitsi', "name": 'Yahentamitsi', "lng": -76.9448027183373, "lat": 38.99108961575231 }, + { "id": 'iribe', "name": 'Iribe', "lng": -76.93643838603555, "lat": 38.98933701397555 }, + { "id": 'reckord', "name": 'Reckord Armory', "lng": -76.93897470250619, "lat": 38.98609556181066 }, + { "id": 'stamp', "name": 'Stamp Student Union', "lng": -76.94473083972326, "lat": 38.988130238874874 } +] + +@app.post("/api/study-rooms") +async def create_study_room_data(data: StudyRoomData): + # Check if the coordinates match one of the known locations (with small tolerance) + is_valid_location = False + req_lng, req_lat = data.location.coordinates[0], data.location.coordinates[1] + + for loc in UMD_LOCATIONS: + if abs(loc["lng"] - req_lng) < 0.0001 and abs(loc["lat"] - req_lat) < 0.0001: + is_valid_location = True + # Override coordinates to exactly match known location for consistency + data.location.coordinates = [loc["lng"], loc["lat"]] + data.room_id = loc["id"] + break + + if not is_valid_location: + from fastapi import HTTPException + raise HTTPException(status_code=400, detail="Invalid location. Coordinates must correspond to a known UMD location.") + + if not data.date: + data.date = datetime.utcnow() + doc = data.dict() + result = study_rooms_collection.insert_one(doc) + return {"id": str(result.inserted_id), "room_id": data.room_id, "status": "success"} + +@app.get("/api/study-rooms") +async def get_study_room_data(): + rooms = list(study_rooms_collection.find({}, {"_id": 0})) + return {"data": rooms} + +@app.get("/api/study-rooms/history") +async def get_study_room_history(): + """Get all study room data from the last 24 hours.""" + twenty_four_hours_ago = datetime.utcnow() - timedelta(hours=24) + rooms = list(study_rooms_collection.find( + {"date": {"$gte": twenty_four_hours_ago}}, + {"_id": 0} + ).sort("date", -1)) + return {"data": rooms} + +@app.get("/{full_path:path}") +async def serve_spa(full_path: str): + static_dir = "static" + if not os.path.exists(static_dir): + return {"error": "Static directory not found. Please build the frontend."} + + static_path = os.path.join(static_dir, full_path) + if os.path.isfile(static_path): + return FileResponse(static_path) + + index_path = os.path.join(static_dir, "index.html") + if os.path.exists(index_path): + return FileResponse(index_path) + + return {"error": "index.html not found in static directory"} diff --git a/docker-compose.yml b/docker-compose.yml index 2e103ca..1e0eba7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,9 @@ -version: '3.8' - services: - web: + app: build: context: . dockerfile: Dockerfile - ports: - - "3000:3000" - env_file: - - .env - environment: - - NODE_ENV=production - - ai_backend: - build: - context: ./backend - dockerfile: Dockerfile ports: - "8000:8000" env_file: - - ./backend/.env + - ./backend/.env \ No newline at end of file diff --git a/scripts/generate_fake_data.py b/scripts/generate_fake_data.py new file mode 100644 index 0000000..e1c0b69 --- /dev/null +++ b/scripts/generate_fake_data.py @@ -0,0 +1,105 @@ +import random +from datetime import datetime, timedelta +from pymongo import MongoClient + +# MongoDB Setup +MONGO_URI = "mongodb+srv://SarayuJ:SarayuJ123@cluster0.xjy5c.mongodb.net/testing" +client = MongoClient(MONGO_URI) +db = client.study_buddy_db +collection = db.study_rooms + +UMD_LOCATIONS = [ + { "id": 'esj', "name": 'Edward St. John (ESJ)', "lng": -76.94209511596014, "lat": 38.987133359608755 }, + { "id": 'mckeldin', "name": 'McKeldin Library', "lng": -76.94494907523277, "lat": 38.986021017749366 }, + { "id": 'hornbake', "name": 'Hornbake Library', "lng": -76.94161787005467, "lat": 38.988233373664826 }, + { "id": 'stem', "name": 'STEM Library', "lng": -76.93942003731279, "lat": 38.988991437126195 }, + { "id": 'clarice', "name": 'Clarice Library', "lng": -76.9500912552473, "lat": 38.990547823732285 }, + { "id": 'yahentamitsi', "name": 'Yahentamitsi', "lng": -76.9448027183373, "lat": 38.99108961575231 }, + { "id": 'iribe', "name": 'Iribe', "lng": -76.93643838603555, "lat": 38.98933701397555 }, + { "id": 'reckord', "name": 'Reckord Armory', "lng": -76.93897470250619, "lat": 38.98609556181066 }, + { "id": 'stamp', "name": 'Stamp Student Union', "lng": -76.94473083972326, "lat": 38.988130238874874 } +] + +def get_db_for_time_and_location(hour, loc_id): + """ + Generate a dB level based on the hour of the day and the location. + This creates a recognizable pattern for AI analysis. + """ + base_db = 40.0 # Ambient noise + + if loc_id in ['mckeldin', 'esj']: + # Busy during the day (10am - 4pm) + if 10 <= hour <= 16: + base_db = 75.0 + elif 17 <= hour <= 22: + base_db = 60.0 + else: + base_db = 45.0 + + elif loc_id in ['stem', 'iribe']: + # Busy in the afternoon/evening (2pm - 8pm) + if 14 <= hour <= 20: + base_db = 70.0 + elif 9 <= hour <= 13: + base_db = 55.0 + else: + base_db = 42.0 + + elif loc_id == 'stamp': + # Busy during lunch (12pm - 2pm) and dinner (5pm - 7pm) + if 12 <= hour <= 14 or 17 <= hour <= 19: + base_db = 85.0 + elif 10 <= hour <= 21: + base_db = 65.0 + else: + base_db = 50.0 + + else: + # General locations (Clarice, Yahentamitsi, Reckord, Hornbake) + # Moderate noise during the day + if 9 <= hour <= 18: + base_db = 60.0 + else: + base_db = 45.0 + + # Add random noise to make it look realistic (+/- 5 dB) + noise = random.uniform(-5.0, 5.0) + return max(30.0, min(100.0, base_db + noise)) + +def generate_fake_data(): + print("Clearing existing study room data...") + collection.delete_many({}) + + now = datetime.utcnow() + start_time = now - timedelta(hours=24) + + docs_to_insert = [] + + print("Generating 24 hours of fake data with patterns...") + # Generate data points every 15 minutes for the last 24 hours + current_time = start_time + while current_time <= now: + hour = current_time.hour + + for loc in UMD_LOCATIONS: + db_level = get_db_for_time_and_location(hour, loc["id"]) + + doc = { + "room_id": loc["id"], + "location": { + "type": "Point", + "coordinates": [loc["lng"], loc["lat"]] + }, + "db": round(db_level, 2), + "date": current_time + } + docs_to_insert.append(doc) + + current_time += timedelta(minutes=15) + + print(f"Inserting {len(docs_to_insert)} records into MongoDB...") + collection.insert_many(docs_to_insert) + print("Done!") + +if __name__ == "__main__": + generate_fake_data() diff --git a/website/bun.lock b/website/bun.lock index 95d43f4..d26e7d3 100644 --- a/website/bun.lock +++ b/website/bun.lock @@ -11,6 +11,7 @@ "devDependencies": { "@iconify/svelte": "^5.2.1", "@sveltejs/adapter-node": "^5.2.10", + "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.57.1", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/forms": "^0.5.11", @@ -367,6 +368,8 @@ "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ=="], + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="], + "@sveltejs/kit": ["@sveltejs/kit@2.57.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw=="], "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.0.0", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g=="], diff --git a/website/package.json b/website/package.json index d648a51..9c9f6e6 100644 --- a/website/package.json +++ b/website/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@iconify/svelte": "^5.2.1", "@sveltejs/adapter-node": "^5.2.10", + "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.57.1", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/forms": "^0.5.11", diff --git a/website/server/app.ts b/website/server/app.ts deleted file mode 100644 index 33e9185..0000000 --- a/website/server/app.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createServer } from 'node:http'; -import { handler } from '../build/handler.js'; - -const port = process.env.PORT || 3000; -const server = createServer(handler as any); - -server.listen(port, () => { - console.log(`Bun Server is listening on http://localhost:${port}`); -}); diff --git a/website/src/lib/components/InteractiveMap.svelte b/website/src/lib/components/InteractiveMap.svelte index a4694bd..01d9047 100644 --- a/website/src/lib/components/InteractiveMap.svelte +++ b/website/src/lib/components/InteractiveMap.svelte @@ -1,21 +1,150 @@
@@ -132,4 +341,13 @@ :global(.maplibregl-ctrl-group button:hover) { background: rgba(255, 255, 255, 0.1) !important; } + :global(.custom-map-popup .maplibregl-popup-content) { + background: transparent !important; + padding: 0 !important; + box-shadow: none !important; + border-radius: 12px; + } + :global(.custom-map-popup .maplibregl-popup-tip) { + border-top-color: rgba(24, 24, 37, 0.9) !important; /* matches bg-crust */ + } diff --git a/website/src/routes/+layout.ts b/website/src/routes/+layout.ts new file mode 100644 index 0000000..d2c0be2 --- /dev/null +++ b/website/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const prerender = true; +export const ssr = false; \ No newline at end of file diff --git a/website/src/routes/history/+page.svelte b/website/src/routes/history/+page.svelte index 88b6301..0725674 100644 --- a/website/src/routes/history/+page.svelte +++ b/website/src/routes/history/+page.svelte @@ -2,6 +2,56 @@ import Icon from '@iconify/svelte'; import MapControls from '$lib/components/MapControls.svelte'; import InteractiveMap from '$lib/components/InteractiveMap.svelte'; + import { onDestroy, onMount } from 'svelte'; + + // Time state + const NOW = Date.now(); + const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; + let playbackTime = $state(NOW); + + let isPlaying = $state(false); + let playInterval: any; + + function togglePlay() { + isPlaying = !isPlaying; + if (isPlaying) { + // Auto replay from beginning if at the end + if (playbackTime >= NOW) { + playbackTime = NOW - TWENTY_FOUR_HOURS; + } + playInterval = setInterval(() => { + // Advance 15 minutes per tick + playbackTime += 15 * 60 * 1000; + if (playbackTime >= NOW) { + playbackTime = NOW; + isPlaying = false; + clearInterval(playInterval); + } + }, 200); // Ticks every 200ms + } else { + clearInterval(playInterval); + } + } + + onDestroy(() => { + if (playInterval) clearInterval(playInterval); + }); + + // Derived values for the UI + let progressPercent = $derived(((playbackTime - (NOW - TWENTY_FOUR_HOURS)) / TWENTY_FOUR_HOURS) * 100); + + let formattedTime = $derived.by(() => { + const d = new Date(playbackTime); + return d.toLocaleString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZone: 'America/New_York' + }) + ' EST'; + }); + @@ -10,7 +60,7 @@
- + @@ -32,21 +82,32 @@

Playback Controls

- Tue, Oct 14 - 14:00 + {formattedTime}
- -
-
-
+
+
+
- -
+ + + { if (isPlaying) togglePlay(); }} + class="w-full absolute opacity-0 cursor-pointer h-full z-20" + /> + + +
@@ -55,5 +116,4 @@
-
diff --git a/website/svelte.config.js b/website/svelte.config.js index b0d0789..58b6ada 100644 --- a/website/svelte.config.js +++ b/website/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from '@sveltejs/adapter-node'; +import adapter from '@sveltejs/adapter-static'; /** @type {import('@sveltejs/kit').Config} */ const config = { @@ -7,10 +7,12 @@ const config = { runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) }, kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter() + adapter: adapter({ + pages: '../backend/static', + assets: '../backend/static', + fallback: 'index.html', + strict: false + }) } };