Code comments Removed Update

This commit is contained in:
2026-04-12 08:49:36 -04:00
parent e6c7ed230b
commit 96272aedb7
22 changed files with 677 additions and 433 deletions
+47 -31
View File
@@ -1,15 +1,29 @@
# HUSHMAP - AI Study Buddy & Room Monitor <div align="center">
<h1>HushMap</h1>
<h3>AI Intelligent Room Monitor</h3>
<br />
<p>
<a href="https://svelte.dev"><img src="https://img.shields.io/badge/SvelteKit-FF3E00?style=for-the-badge&logo=svelte&logoColor=white" alt="SvelteKit"></a>
<a href="https://fastapi.tiangolo.com/"><img src="https://img.shields.io/badge/FastAPI-005571?style=for-the-badge&logo=fastapi" alt="FastAPI"></a>
<a href="https://www.mongodb.com/"><img src="https://img.shields.io/badge/MongoDB-4EA94B?style=for-the-badge&logo=mongodb&logoColor=white" alt="MongoDB"></a>
<a href="https://m5stack.com/"><img src="https://img.shields.io/badge/IoT-M5GO-blue?style=for-the-badge&logo=microchip&logoColor=white" alt="M5GO"></a>
<a href="https://docker.com"><img src="https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"></a>
</p>
</div>
This project is a comprehensive solution featuring an M5GO smart device integration, an AI Voice and Vision Backend, and a Svelte frontend dashboard. It connects physical hardware to advanced AI models (Terp AI, ElevenLabs, YOLOv8) to provide a real-time study buddy experience and a study room occupancy monitor. <br/>
## Project Architecture HushMap bridges the gap between hardware sensors and top-tier artificial intelligence pipelines (e.g. **Terp AI**, **ElevenLabs**, **YOLOv8**), delivering a seamless real-time learning assistant combined with live noise and occupancy metrics.
### 1. Website Frontend (`/website` & Root) ---
A SvelteKit application providing the user interface for our system.
- Powered by `sv` (Svelte CLI) and Bun.
- Configured for production deployment via Docker.
**Developing:** ## System Architecture
### 1. Web Dashboard (`/website`)
A responsive, high-fidelity PWA frontend written in Svelte 5 and styled seamlessly with Catppuccin color guidelines.
* **Powered By**: SvelteKit, Vite, and Bun.
* **Features**: Live interactive map tracking, responsive UI, persistent theming, and an autonomous browser-based Voice Agent calling modal interface.
* **Setup**:
```bash ```bash
cd website cd website
bun install bun install
@@ -17,48 +31,50 @@ bun run dev --open
``` ```
### 2. AI Backend Services (`/backend`) ### 2. AI Backend Services (`/backend`)
A FastAPI backend providing two core capabilities: A blazing fast asynchronous HTTP server facilitating audio chunking and sensor metrics logic over full-duplex sockets.
- **Real-time Voice WebSockets (`/ws/voice`)**: Connects the M5GO device AND the web dashboard to STT (faster-whisper), LLMs (Terp AI), and TTS (ElevenLabs). It streams 16-bit PCM audio bytes natively over WebSockets in full-duplex. * **Core Capabilities**:
- **Context DB Aggregation**: TerpAI automatically queries the MongoDB `study_rooms_collection` to gather live hardware decibel readings globally before answering your prompt. * **Voice Socket Pipelining**: WebSockets (`/ws/voice`) that hook incoming 16-bit PCM arrays into `faster-whisper`.
- **Vision Occupancy API (`/api/vision/room-status`)**: Uses YOLOv8 object detection to identify people and chairs in a room image, determining if a study room is fully occupied and pairing the closest person to an available chair. * **LLM Context Augmentation**: Seamlessly aggregates live MongoDB noise statistics (Decibel levels per location) to feed contextual history to the TerpAI engine!
* **Computer Vision Endpoint**: Exposes a `YOLOv8` tensor API (`/api/vision/room-status`) to parse webcam imagery, pinpoint seating capacities, and locate available chairs algorithmically.
**Developing:** * **Setup**:
```bash ```bash
cd backend cd backend
pip install -r requirements.txt pip install -r requirements.txt
uvicorn server:app --host 0.0.0.0 --port 8000 uvicorn server:app --host 0.0.0.0 --port 8000
``` ```
*(Requires `ffmpeg`, `libgl1-mesa-glx`, and `libglib2.0-0` installed on your system)* > [!WARNING]
> Host devices must have `ffmpeg`, `libgl1-mesa-glx`, and `libglib2.0-0` binaries natively installed to encode audio buffers and execute OpenCV rendering.
### 3. M5GO Device (`/m5go`) ### 3. M5GO Hardware Node (`/m5go`)
MicroPython scripts for the M5Stack M5GO device. C-based MicroPython binaries tailored strictly for the IoT edge nodes traversing the physical campus.
- Uses `uwebsockets` to connect to the backend. * **Features**: Connects internally wired I2S Microphone blocks to route direct byte arrays securely out across WPA/WPA2 networks into the main API gateway using minimal payload overhead. Push-button PTT interfaces built directly into the screen chassis.
- High-quality audio I2S configuration for the internal microphone and speaker.
- Push-to-talk integration: Hold Button A to talk to the AI, release to get an audio response back.
## Docker Setup ---
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. ## Docker Production Setup
The entire monolithic architecture cleanly orchestrates via docker compose. Frontends compile out via SSR, and Python APIs wire natively within a segregated container network loop.
```bash ```bash
docker-compose up --build docker-compose up --build
``` ```
> The global deployment interface listens on port `8000`.
- **App (Frontend + Backend)**: Runs on port `8000` ---
## Configuration ## Core Configuration
Make sure you set up your `.env` variables before running the Docker containers or local servers. Before starting services, strictly adhere to configuring your environment files (`.env`) within `/backend`:
Create a `.env` in the `/backend` folder:
```ini ```ini
ELEVENLABS_API_KEY=sk_... ELEVENLABS_API_KEY=sk_...
ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb
TERP_AI_BEARER_TOKEN=eyJhbGciOiJ... TERP_AI_BEARER_TOKEN=eyJhbGciOiJSUz...
TERP_AI_CONVERSATION_ID=37fa27cc-542a-c8a8-9c31-9d1954fdc1d2 TERP_AI_CONVERSATION_ID=3a150d8e-bb12-...
MONGODB_URI=mongodb+srv://... MONGODB_URI=mongodb+srv://user:pass@cluster0...
``` ```
Update your `.env` to match the exact `authorization: Bearer` and `parentSegmentId` context from TerpAI if timeouts occur. > [!TIP]
> Ensure the `.env` mirrors your authentic `x-cosmos-session` headers and bearer tokens exported from a live browser session to prevent immediate 401 Unauthorized timeouts in the Terp AI pipeline.
Update the `/m5go/main.py` file to include your Wi-Fi credentials and the correct local IP for the WebSocket (`WS_URL`). For IoT clients, update `/m5go/main.py` explicitly to broadcast to your running router IP namespace matching your specific VLAN.
+43 -91
View File
@@ -1,93 +1,74 @@
# AI Voice Services Backend <div align="center">
<h1>HushMap: AI Services API</h1>
<p>
<a href="https://fastapi.tiangolo.com/"><img src="https://img.shields.io/badge/FastAPI-009688?style=for-the-badge&logo=fastapi&logoColor=white" alt="FastAPI"></a>
<a href="https://python.org"><img src="https://img.shields.io/badge/Python_3.9+-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python"></a>
<img src="https://img.shields.io/badge/Ultralytics-YOLOv8-FF0000?style=for-the-badge" alt="YOLOv8 Vision">
<img src="https://img.shields.io/badge/Whisper-STT-4A90E2?style=for-the-badge" alt="Whisper">
</p>
<p><i>The central nervous system linking physical M5GO devices, external Computer Vision tensors, and Conversational NLP APIs synchronously.</i></p>
</div>
This directory contains the FastAPI backend for the AI Voice Agent, facilitating communication between the M5GO device, Terp AI, and ElevenLabs. ---
## Setup Instructions ## Setup Instructions
### Prerequisites ### Prerequisites
1. **Python 3.9+** is recommended. 1. **Python 3.9+** is strictly recommended to support asynchronous typing paradigms.
2. **FFmpeg** must be installed on the system to handle audio format conversions (MP3 to 16-bit 16kHz PCM). 2. **FFmpeg** must be successfully registered onto your OS PATH environments. This engine handles the core conversions decoding MP3 output arrays into 16-bit, 16kHz Mono arrays natively required for browser contexts:
- On Ubuntu/Debian: `sudo apt install ffmpeg` - **Ubuntu/Debian**: `sudo apt install ffmpeg`
- On macOS: `brew install ffmpeg` - **macOS**: `brew install ffmpeg`
- On Windows: Download from the [FFmpeg website](https://ffmpeg.org/download.html) and add to PATH. - **Windows**: Install globally via the [FFmpeg website](https://ffmpeg.org/download.html).
### Installation ### Environment Initialization
Bootstrap the virtual environment and initialize project dependencies:
1. Navigate to the `ai_services` directory.
2. (Optional but recommended) Create a virtual environment:
```bash ```bash
cd backend
python -m venv venv python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate source venv/bin/activate # Windows: .\venv\Scripts\activate
```
3. Install the required Python packages:
```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
### Configuration ### Configuration Tokens
Update the `.env` file in this directory with your credentials: Provide runtime keys securely targeting TerpAI context queues and ElevenLabs synthesized avatars within a `.env` dotfile:
```ini ```ini
ELEVENLABS_API_KEY=sk_... ELEVENLABS_API_KEY=sk_...
ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb
TERP_AI_BEARER_TOKEN=eyJhbGciOiJSUz... TERP_AI_BEARER_TOKEN=eyJhbGciOiJSUz...
TERP_AI_CONVERSATION_ID=37fa27cc-542a-c8a8-9c31-9d1954fdc1d2 TERP_AI_CONVERSATION_ID=37fa27cc-...
MONGODB_URI=mongodb+srv://... MONGODB_URI=mongodb+srv://...
``` ```
## Running the Server To invoke the engine, simply execute Uvicorn across your `0.0.0.0` loopback:
Start the FastAPI application using Uvicorn:
```bash ```bash
uvicorn server:app --host 0.0.0.0 --port 8000 uvicorn server:app --host 0.0.0.0 --port 8000
``` ```
This will start the server and make it accessible on your local network on port 8000.
## WebSocket Endpoints ---
### `/ws/voice` ## Gateway Pipelines
This is the primary WebSocket endpoint used by the M5GO device for real-time voice communication. ### Full-Duplex Subroutines (`/ws/voice`)
**Protocol Flow:** This WebSocket proxy establishes a fully integrated multi-turn communication bridge seamlessly interacting between Edge node Hardware APIs (ESP32/M5GO/Browsers) and NLP architectures.
1. **Connection:** The client establishes a WebSocket connection to `ws://<server_ip>:8000/ws/voice`. 1. **Int16 Byte Array Exchange**: Devices connect to `ws://<server_ip>:8000/ws/voice` and push raw binary frames asynchronously over the socket.
2. **Streaming Audio (Client -> Server):** While the user holds the record button, the client continuously sends binary frames containing raw audio data. 2. **Contextual Augmentation**: The server waits for the `"stop_listening"` payload event to signify a completed audio snippet. That float array is cast through `faster-whisper` and combined seamlessly with real-time `MongoDB` decibel tracking telemetry parameters natively attached into the `TerpAI` user conversation chunk.
- **Expected Format:** 16-bit signed integer, 16 kHz, Mono PCM. 3. **TTS Pipeline Rendering**: Output predictions are caught instantly, forwarded natively into the `ElevenLabs` TTS interface rendering `pcm_16000` wav codecs, and alerted back down to clients using a `tts_ready` dispatcher.
3. **End of Audio Signal (Client -> Server):** When the user releases the button, the client sends a JSON text frame to signal the end of the recording:
```json
{
"event": "stop_listening"
}
```
4. **Processing (Server):** Upon receiving the `stop_listening` event, the server executes the AI pipeline:
- Transcribes the accumulated Int16 PCM audio organically using `faster-whisper`.
- Injects a MongoDB aggregate map of the latest 24hr Campus Location noise levels seamlessly into the LLM system prompt.
- Sends the transcribed text & location context to the Terp AI conversational endpoint and waits for the full response.
- Streams the Terp AI response text directly to ElevenLabs TTS and demands `pcm_16000` via URL flags natively!
5. **TTS Endpoint Notification**: The server saves the TTS audio buffer and pushes a JSON:
```json
{
"event": "tts_ready",
"size": 105000
}
```
6. **Audio Callback**: Client queries `GET /api/tts-audio` to play the binary wav response.
## REST Endpoints ### Tensor Vision Endpoints (`/api/vision/room-status`)
### `/api/vision/room-status` (POST) Leveraging OpenCV bindings layered beneath a YOLOv8-driven bounding box topology detector, this `POST` API analyzes raw camera image buffers returning capacity logic natively.
This endpoint uses a YOLO object detection model to detect people and chairs in a room image, determining if the room is full and pairing the closest chairs to people. > [!NOTE]
> This API calculates euclidean distances algorithmically detecting adjacent proximities between "person" classifiers and untaken "chair" bounding frames to accurately diagnose available seats inside crowded architectures!
**Request:**
- `file`: (Required) The image file to analyze (e.g., JPEG, PNG) sent as multipart form-data.
**Response:**
Returns a JSON object detailing the room status, counts, and pairings.
**Response Output Protocol:**
```json ```json
{ {
"room_status": "full", "room_status": "full",
@@ -101,45 +82,16 @@ Returns a JSON object detailing the room status, counts, and pairings.
"chair_index": 1, "chair_index": 1,
"distance": 150.5 "distance": 150.5
} }
],
"details": {
"people": [ ... ],
"chairs": [ ... ]
}
}
```
### `/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 hardware client (`m5go/main.py`), ensure you update the `WS_URL` variable to point to the correct internal server IP. ## Database Registries
For the Web Frontend (`VoiceButton.svelte`), it uses standard Web Audio API's `ScriptProcessorNode` to bridge the Float32 arrays strictly into 16-Bit Mono over a dynamic WebSocket tunnel automatically. * `GET /api/study-rooms/history`: Pulls the active global repository of logged architectural noise measurements captured universally within the preceding 24 hours. Data payloads correspond geographically mapping `GeoJSON` nodes to front-end Mapbox topologies.
* `GET /api/study-rooms`: Pulls generic unstructured noise lists directly unfiltered from Cosmos bounds.
```python > [!IMPORTANT]
# In m5go/main.py > The browser frontend strictly configures standard Web Audio API's `ScriptProcessorNode` interfaces routing data synchronously to this backend! Wait to close down pipelines until *after* all WS queues have successfully been delivered.
WS_URL = "ws://192.168.1.100:8000/ws/voice"
```
+276
View File
@@ -0,0 +1,276 @@
"""Data containing all current emoji
Extracted from https://unicode.org/Public/emoji/latest/emoji-test.txt
and https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-variation-sequences.txt
See utils/generate_emoji.py
+----------------+-------------+------------------+-------------------+
| Emoji Version | Date | Unicode Version | Data File Comment |
+----------------+-------------+------------------+-------------------+
| N/A | 2010-10-11 | Unicode 6.0 | E0.6 |
| N/A | 2014-06-16 | Unicode 7.0 | E0.7 |
| Emoji 1.0 | 2015-06-09 | Unicode 8.0 | E1.0 |
| Emoji 2.0 | 2015-11-12 | Unicode 8.0 | E2.0 |
| Emoji 3.0 | 2016-06-03 | Unicode 9.0 | E3.0 |
| Emoji 4.0 | 2016-11-22 | Unicode 9.0 | E4.0 |
| Emoji 5.0 | 2017-06-20 | Unicode 10.0 | E5.0 |
| Emoji 11.0 | 2018-05-21 | Unicode 11.0 | E11.0 |
| Emoji 12.0 | 2019-03-05 | Unicode 12.0 | E12.0 |
| Emoji 12.1 | 2019-10-21 | Unicode 12.1 | E12.1 |
| Emoji 13.0 | 2020-03-10 | Unicode 13.0 | E13.0 |
| Emoji 13.1 | 2020-09-15 | Unicode 13.0 | E13.1 |
| Emoji 14.0 | 2021-09-14 | Unicode 14.0 | E14.0 |
| Emoji 15.0 | 2022-09-13 | Unicode 15.0 | E15.0 |
| Emoji 15.1 | 2023-09-12 | Unicode 15.1 | E15.1 |
| Emoji 16.0 | 2024-09-10 | Unicode 16.0 | E16.0 |
http://www.unicode.org/reports/tr51/
"""
__all__ = ['STATUS', 'LANGUAGES']
from typing import Any, Dict, List
component = 1
fully_qualified = 2
minimally_qualified = 3
unqualified = 4
STATUS: Dict[str, int] = {
'component': component,
'fully_qualified': fully_qualified,
'minimally_qualified': minimally_qualified,
'unqualified': unqualified,
}
LANGUAGES: List[str] = [
'en',
'es',
'ja',
'ko',
'pt',
'it',
'fr',
'de',
'fa',
'id',
'zh',
'ru',
'tr',
'ar',
]
EMOJI_DATA: Dict[str, Dict[str, Any]] = {
'\U0001f947': {
'en': ':1st_place_medal:',
'status': fully_qualified,
'E': 3,
'de': ':goldmedaille:',
'es': ':medalla_de_oro:',
'fr': ':médaille_dor:',
'ja': ':金メダル:',
'ko': ':금메달:',
'pt': ':medalha_de_ouro:',
'it': ':medaglia_doro:',
'fa': ':مدال_طلا:',
'id': ':medali_emas:',
'zh': ':金牌:',
'ru': ':золотая_медаль:',
'tr': ':birincilik_madalyası:',
'ar': ':ميدالية_مركز_أول:',
},
'\U0001f948': {
'en': ':2nd_place_medal:',
'status': fully_qualified,
'E': 3,
'de': ':silbermedaille:',
'es': ':medalla_de_plata:',
'fr': ':médaille_dargent:',
'ja': ':銀メダル:',
'ko': ':은메달:',
'pt': ':medalha_de_prata:',
'it': ':medaglia_dargento:',
'fa': ':مدال_نقره:',
'id': ':medali_perak:',
'zh': ':银牌:',
'ru': ':серебряная_медаль:',
'tr': ':ikincilik_madalyası:',
'ar': ':ميدالية_مركز_ثان:',
},
'\U0001f949': {
'en': ':3rd_place_medal:',
'status': fully_qualified,
'E': 3,
'de': ':bronzemedaille:',
'es': ':medalla_de_bronce:',
'fr': ':médaille_de_bronze:',
'ja': ':銅メダル:',
'ko': ':동메달:',
'pt': ':medalha_de_bronze:',
'it': ':medaglia_di_bronzo:',
'fa': ':مدال_برنز:',
'id': ':medali_perunggu:',
'zh': ':铜牌:',
'ru': ':бронзовая_медаль:',
'tr': ':üçüncülük_madalyası:',
'ar': ':ميدالية_مركز_ثالث:',
},
'\U0001f18e': {
'en': ':AB_button_(blood_type):',
'status': fully_qualified,
'E': 0.6,
'alias': [':ab:', ':ab_button_blood_type:'],
'de': ':großbuchstaben_ab_in_rotem_quadrat:',
'es': ':grupo_sanguíneo_ab:',
'fr': ':groupe_sanguin_ab:',
'ja': ':血液型ab型:',
'ko': ':에이비형:',
'pt': ':botão_ab_(tipo_sanguíneo):',
'it': ':gruppo_sanguigno_ab:',
'fa': ':دکمه_آ_ب_(گروه_خونی):',
'id': ':tombol_ab_(golongan_darah):',
'zh': ':AB型血:',
'ru': ':IV_группа_крови:',
'tr': ':ab_düğmesi_(kan_grubu):',
'ar': ':زر_ab_(فئة_الدم):',
},
'\U0001f3e7': {
'en': ':ATM_sign:',
'status': fully_qualified,
'E': 0.6,
'alias': [':atm:', ':atm_sign:'],
'de': ':symbol_geldautomat:',
'es': ':señal_de_cajero_automático:',
'fr': ':distributeur_de_billets:',
'ja': ':atm:',
'ko': ':에이티엠:',
'pt': ':símbolo_de_caixa_automático:',
'it': ':simbolo_dello_sportello_bancomat:',
'fa': ':نشان_عابربانک:',
'id': ':tanda_atm:',
'zh': ':取款机:',
'ru': ':значок_банкомата:',
'tr': ':atm_işareti:',
'ar': ':علامة_ماكينة_صرف_آلي:',
},
'\U0001f170\U0000fe0f': {
'en': ':A_button_(blood_type):',
'status': fully_qualified,
'E': 0.6,
'alias': [':a:', ':a_button_blood_type:'],
'variant': True,
'de': ':großbuchstabe_a_in_rotem_quadrat:',
'es': ':grupo_sanguíneo_a:',
'fr': ':groupe_sanguin_a:',
'ja': ':血液型a型:',
'ko': ':에이형:',
'pt': ':botão_a_(tipo_sanguíneo):',
'it': ':gruppo_sanguigno_a:',
'fa': ':دکمه_آ_(گروه_خونی):',
'id': ':tombol_a_(golongan_darah):',
'zh': ':A型血:',
'ru': ':ii_группа_крови:',
'tr': ':a_düğmesi_(kan_grubu):',
'ar': ':زر_a:',
},
'\U0001f170': {
'en': ':A_button_(blood_type):',
'status': unqualified,
'E': 0.6,
'alias': [':a:', ':a_button_blood_type:'],
'variant': True,
'de': ':großbuchstabe_a_in_rotem_quadrat:',
'es': ':grupo_sanguíneo_a:',
'fr': ':groupe_sanguin_a:',
'ja': ':血液型a型:',
'ko': ':에이형:',
'pt': ':botão_a_(tipo_sanguíneo):',
'it': ':gruppo_sanguigno_a:',
'fa': ':دکمه_آ_(گروه_خونی):',
'id': ':tombol_a_(golongan_darah):',
'zh': ':A型血:',
'ru': ':II_группа_крови:',
'tr': ':a_düğmesi_(kan_grubu):',
'ar': ':زر_a:',
},
'\U0001f1e6\U0001f1eb': {
'en': ':Afghanistan:',
'status': fully_qualified,
'E': 2,
'alias': [':flag_for_Afghanistan:', ':afghanistan:'],
'de': ':flagge_afghanistan:',
'es': ':bandera_afganistán:',
'fr': ':drapeau_afghanistan:',
'ja': ':旗_アフガニスタン:',
'ko': ':깃발_아프가니스탄:',
'pt': ':bandeira_afeganistão:',
'it': ':bandiera_afghanistan:',
'fa': ':پرچم_افغانستان:',
'id': ':bendera_afganistan:',
'zh': ':阿富汗:',
'ru': ':флаг_Афганистан:',
'tr': ':bayrak_afganistan:',
'ar': ':علم_أفغانستان:',
},
'\U0001f1e6\U0001f1f1': {
'en': ':Albania:',
'status': fully_qualified,
'E': 2,
'alias': [':flag_for_Albania:', ':albania:'],
'de': ':flagge_albanien:',
'es': ':bandera_albania:',
'fr': ':drapeau_albanie:',
'ja': ':旗_アルバニア:',
'ko': ':깃발_알바니아:',
'pt': ':bandeira_albânia:',
'it': ':bandiera_albania:',
'fa': ':پرچم_آلبانی:',
'id': ':bendera_albania:',
'zh': ':阿尔巴尼亚:',
'ru': ':флаг_Албания:',
'tr': ':bayrak_arnavutluk:',
'ar': ':علم_ألبانيا:',
},
'\U0001f1e9\U0001f1ff': {
'en': ':Algeria:',
'status': fully_qualified,
'E': 2,
'alias': [':flag_for_Algeria:', ':algeria:'],
'de': ':flagge_algerien:',
'es': ':bandera_argelia:',
'fr': ':drapeau_algérie:',
'ja': ':旗_アルジェリア:',
'ko': ':깃발_알제리:',
'pt': ':bandeira_argélia:',
'it': ':bandiera_algeria:',
'fa': ':پرچم_الجزایر:',
'id': ':bendera_aljazair:',
'zh': ':阿尔及利亚:',
'ru': ':флаг_Алжир:',
'tr': ':bayrak_cezayir:',
'ar': ':علم_الجزائر:',
},
'\U0001f1e6\U0001f1f8': {
'en': ':American_Samoa:',
'status': fully_qualified,
'E': 2,
'alias': [':flag_for_American_Samoa:', ':american_samoa:'],
'de': ':flagge_amerikanisch-samoa:',
'es': ':bandera_samoa_americana:',
'fr': ':drapeau_samoa_américaines:',
'ja': ':旗_米領サモア:',
'ko': ':깃발_아메리칸_사모아:',
'pt': ':bandeira_samoa_americana:',
'it': ':bandiera_samoa_americane:',
'fa': ':پرچم_ساموآی_امریکا:',
'id': ':bendera_samoa_amerika:',
'zh': ':美属萨摩亚:',
'ru': ':флаг_Американское_Самоа:',
'tr': ':bayrak_amerikan_samoası:',
'ar': ':علم_ساموا_الأمريكية:',
},
}
+24 -23
View File
@@ -8,6 +8,7 @@ import requests
import json import json
import base64 import base64
import urllib.request import urllib.request
# import ssl # import ssl
# ssl._create_default_https_context = ssl._create_unverified_context # ssl._create_default_https_context = ssl._create_unverified_context
@@ -35,14 +36,14 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# MongoDB Setup
import certifi import certifi
MONGO_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/") MONGO_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
mongo_client = MongoClient(MONGO_URI, tlsCAFile=certifi.where()) mongo_client = MongoClient(MONGO_URI, tlsCAFile=certifi.where())
db = mongo_client.study_buddy_db db = mongo_client.study_buddy_db
study_rooms_collection = db.study_rooms study_rooms_collection = db.study_rooms
# Pydantic models for Study Room Data
class GeoJSONPoint(BaseModel): class GeoJSONPoint(BaseModel):
type: str = "Point" type: str = "Point"
coordinates: List[float] coordinates: List[float]
@@ -75,12 +76,12 @@ HEADERS = {
"sec-fetch-site": "same-origin", "sec-fetch-site": "same-origin",
"sentry-trace": "250c82a03041415b99422d838ccc7003-9a9ec11d7fd0293b-0", "sentry-trace": "250c82a03041415b99422d838ccc7003-9a9ec11d7fd0293b-0",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
"x-cosmos-session-281286": "0:-1#10931", "x-cosmos-session-281286": "0:-1",
"x-cosmos-session-295334": "0:-1#749854", "x-cosmos-session-295334": "0:-1",
"x-cosmos-session-317755": "0:-1#191601", "x-cosmos-session-317755": "0:-1",
"x-cosmos-session-382299": "0:-1#265024", "x-cosmos-session-382299": "0:-1",
"x-cosmos-session-418988": "0:-1#4058856", "x-cosmos-session-418988": "0:-1",
"x-cosmos-session-793952": "0:-1#14004", "x-cosmos-session-793952": "0:-1",
"x-request-id": "6a128b8a-7f63-4f97-a40b-bfd31b4a376e", "x-request-id": "6a128b8a-7f63-4f97-a40b-bfd31b4a376e",
"x-timezone": "America/New_York", "x-timezone": "America/New_York",
} }
@@ -97,7 +98,7 @@ def _write_wav_to_buffer(pcm_data: bytes, sample_rate: int = SAMPLE_RATE) -> byt
buf.write(b"WAVE") buf.write(b"WAVE")
buf.write(b"fmt ") buf.write(b"fmt ")
buf.write(struct.pack("<I", 16)) buf.write(struct.pack("<I", 16))
buf.write(struct.pack("<H", 1)) # PCM buf.write(struct.pack("<H", 1))
buf.write(struct.pack("<H", NUM_CHANNELS)) buf.write(struct.pack("<H", NUM_CHANNELS))
buf.write(struct.pack("<I", sample_rate)) buf.write(struct.pack("<I", sample_rate))
buf.write(struct.pack("<I", byte_rate)) buf.write(struct.pack("<I", byte_rate))
@@ -113,7 +114,7 @@ def _transcribe_pcm(pcm_data: bytes, sample_rate: int = SAMPLE_RATE) -> str:
from faster_whisper import WhisperModel from faster_whisper import WhisperModel
print(f" Using sample rate: {sample_rate} Hz") print(f" Using sample rate: {sample_rate} Hz")
# Debug: analyze PCM audio quality
num_samples = len(pcm_data) // 2 num_samples = len(pcm_data) // 2
if num_samples > 0: if num_samples > 0:
samples = list(struct.unpack(f"<{num_samples}h", pcm_data[:num_samples * 2])) samples = list(struct.unpack(f"<{num_samples}h", pcm_data[:num_samples * 2]))
@@ -122,12 +123,12 @@ def _transcribe_pcm(pcm_data: bytes, sample_rate: int = SAMPLE_RATE) -> str:
rms = (sum(s * s for s in samples) / num_samples) ** 0.5 rms = (sum(s * s for s in samples) / num_samples) ** 0.5
print(f" PCM stats (raw): {num_samples} samples, min={min_s}, max={max_s}, mean={mean_s:.1f}, RMS={rms:.1f}") print(f" PCM stats (raw): {num_samples} samples, min={min_s}, max={max_s}, mean={mean_s:.1f}, RMS={rms:.1f}")
# Remove DC offset (center audio at 0)
dc_offset = int(round(mean_s)) dc_offset = int(round(mean_s))
samples = [max(-32768, min(32767, s - dc_offset)) for s in samples] samples = [max(-32768, min(32767, s - dc_offset)) for s in samples]
pcm_data = struct.pack(f"<{num_samples}h", *samples) pcm_data = struct.pack(f"<{num_samples}h", *samples)
# Stats after correction
rms_fixed = (sum(s * s for s in samples) / num_samples) ** 0.5 rms_fixed = (sum(s * s for s in samples) / num_samples) ** 0.5
print(f" PCM stats (fixed): DC offset removed={dc_offset}, RMS={rms_fixed:.1f}") print(f" PCM stats (fixed): DC offset removed={dc_offset}, RMS={rms_fixed:.1f}")
@@ -138,13 +139,13 @@ def _transcribe_pcm(pcm_data: bytes, sample_rate: int = SAMPLE_RATE) -> str:
with os.fdopen(tmp_fd, "wb") as f: with os.fdopen(tmp_fd, "wb") as f:
f.write(wav_data) f.write(wav_data)
# Save a debug copy so we can listen
debug_path = os.path.join(os.path.dirname(__file__), "debug_audio.wav") debug_path = os.path.join(os.path.dirname(__file__), "debug_audio.wav")
with open(debug_path, "wb") as df: with open(debug_path, "wb") as df:
df.write(wav_data) df.write(wav_data)
print(f" Debug WAV saved to: {debug_path}") print(f" Debug WAV saved to: {debug_path}")
# Initialize the model (using base model for speed)
model = WhisperModel("base", device="cpu", compute_type="int8") model = WhisperModel("base", device="cpu", compute_type="int8")
segments, info = model.transcribe(tmp_path, beam_size=5) segments, info = model.transcribe(tmp_path, beam_size=5)
seg_list = list(segments) seg_list = list(segments)
@@ -232,7 +233,7 @@ def _generate_tts(text: str) -> bytes | None:
print("ElevenLabs API key not configured") print("ElevenLabs API key not configured")
return None return None
# Request PCM directly — no ffmpeg needed
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}?output_format=pcm_16000" url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}?output_format=pcm_16000"
headers = { headers = {
@@ -267,7 +268,7 @@ def _generate_tts(text: str) -> bytes | None:
print(f"ElevenLabs TTS error: {e}") print(f"ElevenLabs TTS error: {e}")
return None return None
# Latest TTS WAV stored in memory for HTTP download by M5GO
_latest_tts_wav = None _latest_tts_wav = None
@app.get("/api/tts-audio") @app.get("/api/tts-audio")
@@ -295,7 +296,7 @@ async def websocket_voice(websocket: WebSocket):
msg = json.loads(data["text"]) msg = json.loads(data["text"])
if msg.get("event") == "stop_listening": if msg.get("event") == "stop_listening":
pcm_data = bytes(audio_buffer) pcm_data = bytes(audio_buffer)
audio_buffer = bytearray() # Reset for next time audio_buffer = bytearray()
device_sample_rate = msg.get("sample_rate", SAMPLE_RATE) device_sample_rate = msg.get("sample_rate", SAMPLE_RATE)
print(f"Received stop_listening event. Buffer size: {len(pcm_data)} bytes, sample_rate: {device_sample_rate} Hz") print(f"Received stop_listening event. Buffer size: {len(pcm_data)} bytes, sample_rate: {device_sample_rate} Hz")
@@ -305,7 +306,7 @@ async def websocket_voice(websocket: WebSocket):
await websocket.send_bytes(b"") await websocket.send_bytes(b"")
continue continue
# Step 1: Speech to Text
print("Transcribing...") print("Transcribing...")
user_text = _transcribe_pcm(pcm_data, sample_rate=device_sample_rate) user_text = _transcribe_pcm(pcm_data, sample_rate=device_sample_rate)
if not user_text: if not user_text:
@@ -315,7 +316,7 @@ async def websocket_voice(websocket: WebSocket):
print(f"User said: {user_text}") print(f"User said: {user_text}")
# Step 2: Terp AI
print("Sending to Terp AI...") print("Sending to Terp AI...")
context_str = get_latest_locations_context() context_str = get_latest_locations_context()
augmented_prompt = f"USER ASKS: {user_text}\n\n[SYSTEM CONTEXT - LATEST UMD ROOM STATS TO HELP YOU ANSWER IF ASKED]:\n{context_str}" augmented_prompt = f"USER ASKS: {user_text}\n\n[SYSTEM CONTEXT - LATEST UMD ROOM STATS TO HELP YOU ANSWER IF ASKED]:\n{context_str}"
@@ -327,12 +328,12 @@ async def websocket_voice(websocket: WebSocket):
print(f"Terp AI response: {ai_response_text}") print(f"Terp AI response: {ai_response_text}")
# Step 3: Text to Speech
print("Generating TTS...") print("Generating TTS...")
tts_pcm = _generate_tts(ai_response_text) tts_pcm = _generate_tts(ai_response_text)
if tts_pcm: if tts_pcm:
# Save as WAV for HTTP download by M5GO
global _latest_tts_wav global _latest_tts_wav
_latest_tts_wav = _write_wav_to_buffer(tts_pcm) _latest_tts_wav = _write_wav_to_buffer(tts_pcm)
print(f"TTS WAV ready: {len(_latest_tts_wav)} bytes, serving via /api/tts-audio") print(f"TTS WAV ready: {len(_latest_tts_wav)} bytes, serving via /api/tts-audio")
@@ -406,14 +407,14 @@ def get_latest_locations_context() -> str:
@app.post("/api/study-rooms") @app.post("/api/study-rooms")
async def create_study_room_data(data: StudyRoomData): 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 is_valid_location = False
req_lng, req_lat = data.location.coordinates[0], data.location.coordinates[1] req_lng, req_lat = data.location.coordinates[0], data.location.coordinates[1]
for loc in UMD_LOCATIONS: for loc in UMD_LOCATIONS:
if abs(loc["lng"] - req_lng) < 0.0001 and abs(loc["lat"] - req_lat) < 0.0001: if abs(loc["lng"] - req_lng) < 0.0001 and abs(loc["lat"] - req_lat) < 0.0001:
is_valid_location = True is_valid_location = True
# Override coordinates to exactly match known location for consistency
data.location.coordinates = [loc["lng"], loc["lat"]] data.location.coordinates = [loc["lng"], loc["lat"]]
data.room_id = loc["id"] data.room_id = loc["id"]
break break
+12 -12
View File
@@ -4,15 +4,15 @@ from ultralytics import YOLO
from scipy.spatial.distance import cdist from scipy.spatial.distance import cdist
from scipy.optimize import linear_sum_assignment from scipy.optimize import linear_sum_assignment
# Load YOLOv8 nano model (downloads automatically if not found)
# 'yolov8n.pt' is lightweight and fast for this purpose
try: try:
model = YOLO("yolov8n.pt") model = YOLO("yolov8n.pt")
except Exception as e: except Exception as e:
print(f"Error loading YOLO model: {e}") print(f"Error loading YOLO model: {e}")
model = None model = None
# COCO Class IDs
PERSON_CLASS_ID = 0 PERSON_CLASS_ID = 0
CHAIR_CLASS_ID = 56 CHAIR_CLASS_ID = 56
@@ -20,14 +20,14 @@ def analyze_room_image(image_bytes: bytes):
if not model: if not model:
return {"error": "Vision model is not loaded"} return {"error": "Vision model is not loaded"}
# Convert bytes to numpy array then to cv2 image
nparr = np.frombuffer(image_bytes, np.uint8) nparr = np.frombuffer(image_bytes, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is None: if img is None:
return {"error": "Invalid image format"} return {"error": "Invalid image format"}
# Run inference
results = model(img) results = model(img)
people = [] people = []
@@ -39,13 +39,13 @@ def analyze_room_image(image_bytes: bytes):
cls_id = int(box.cls[0]) cls_id = int(box.cls[0])
conf = float(box.conf[0]) conf = float(box.conf[0])
# Extract center of bounding box
x1, y1, x2, y2 = box.xyxy[0] x1, y1, x2, y2 = box.xyxy[0]
cx = (x1 + x2) / 2.0 cx = (x1 + x2) / 2.0
cy = (y1 + y2) / 2.0 cy = (y1 + y2) / 2.0
centroid = [float(cx), float(cy)] centroid = [float(cx), float(cy)]
# Only consider detections with confidence > 0.3
if conf > 0.3: if conf > 0.3:
if cls_id == PERSON_CLASS_ID: if cls_id == PERSON_CLASS_ID:
people.append({ people.append({
@@ -65,15 +65,15 @@ def analyze_room_image(image_bytes: bytes):
pairs = [] pairs = []
# Bipartite matching if both people and chairs exist
if num_people > 0 and num_chairs > 0: if num_people > 0 and num_chairs > 0:
people_coords = [p["centroid"] for p in people] people_coords = [p["centroid"] for p in people]
chairs_coords = [c["centroid"] for c in chairs] chairs_coords = [c["centroid"] for c in chairs]
# Distance matrix (Euclidean distances)
dist_matrix = cdist(people_coords, chairs_coords, metric='euclidean') dist_matrix = cdist(people_coords, chairs_coords, metric='euclidean')
# Hungarian algorithm to minimize total distance for pairings
row_ind, col_ind = linear_sum_assignment(dist_matrix) row_ind, col_ind = linear_sum_assignment(dist_matrix)
for person_idx, chair_idx in zip(row_ind, col_ind): for person_idx, chair_idx in zip(row_ind, col_ind):
@@ -84,8 +84,8 @@ def analyze_room_image(image_bytes: bytes):
"distance": distance "distance": distance
}) })
# Basic logic: room is full if there are at least as many people as chairs.
# Can be adjusted based on specific room definitions
is_full = num_people >= num_chairs if num_chairs > 0 else False is_full = num_people >= num_chairs if num_chairs > 0 else False
return { return {
+35 -35
View File
@@ -34,26 +34,26 @@ def d_s(lcd, f, s_x, s_y, p_s):
setScreenColor(0x222222) setScreenColor(0x222222)
# ==========================================
# CONFIGURATION
# ==========================================
WIFI_SSID = "Blobby" WIFI_SSID = "Blobby"
WIFI_PASS = "73556088" WIFI_PASS = "73556088"
WS_URL = "ws://192.168.137.1:8000/ws/voice" WS_URL = "ws://192.168.137.1:8000/ws/voice"
# Location Settings for Noise Monitoring
CURRENT_ROOM_ID = "mckeldin" CURRENT_ROOM_ID = "mckeldin"
CURRENT_LAT = 38.986021 CURRENT_LAT = 38.986021
CURRENT_LNG = -76.944949 CURRENT_LNG = -76.944949
# Audio Settings
TARGET_SAMPLE_RATE = 8000 # Voice recording sample rate TARGET_SAMPLE_RATE = 8000
AUDIO_CHUNK_SIZE = 2048 AUDIO_CHUNK_SIZE = 2048
# ==========================================
# WEBSOCKET CLIENT
# ==========================================
class WSClient: class WSClient:
def __init__(self, sock): def __init__(self, sock):
@@ -200,9 +200,9 @@ def ws_connect(url):
return WSClient(sock) return WSClient(sock)
# ==========================================
# UI & UTILITY FUNCTIONS
# ==========================================
_cur_face = None _cur_face = None
def set_face(face): def set_face(face):
@@ -243,9 +243,9 @@ def connect_wifi():
lcd.clear() lcd.clear()
return wlan.isconnected() return wlan.isconnected()
# ==========================================
# AUDIO I/O
# ==========================================
def get_adc(): def get_adc():
try: try:
@@ -283,13 +283,13 @@ def get_db(adc_obj):
amp = math.sqrt(variance) amp = math.sqrt(variance)
if amp <= 1: return 30 if amp <= 1: return 30
# Scale to human DB
return 20 * math.log10(amp) + 25 return 20 * math.log10(amp) + 25
def init_manual_spk(): def init_manual_spk():
try: try:
# M5GO Core speaker sits on DAC 1 (Pin 25)
# Therefore we MUST strictly use DAC_BUILT_IN. Digital I2S on this pin is not supported!
if hasattr(machine.I2S, "MODE_DAC_BUILT_IN"): if hasattr(machine.I2S, "MODE_DAC_BUILT_IN"):
mode = getattr(machine.I2S, "MODE_MASTER", 1) | getattr(machine.I2S, "MODE_TX", 2) | getattr(machine.I2S, "MODE_DAC_BUILT_IN", 0) mode = getattr(machine.I2S, "MODE_MASTER", 1) | getattr(machine.I2S, "MODE_TX", 2) | getattr(machine.I2S, "MODE_DAC_BUILT_IN", 0)
cfmt = getattr(machine.I2S, "CHANNEL_FMT_RIGHT_LEFT", 1) cfmt = getattr(machine.I2S, "CHANNEL_FMT_RIGHT_LEFT", 1)
@@ -297,7 +297,7 @@ def init_manual_spk():
i2s_id = getattr(machine.I2S, "NUM0", 0) i2s_id = getattr(machine.I2S, "NUM0", 0)
# Different MicroPython versions vary wildly on kwarg vs positional I2S init structure
try_sigs = [ try_sigs = [
([i2s_id], {"mode": mode, "rate": 16000, "bits": 16, "format": cfmt, "ibuf": 2048}), ([i2s_id], {"mode": mode, "rate": 16000, "bits": 16, "format": cfmt, "ibuf": 2048}),
([i2s_id, mode, 16000, 16, cfmt, dfmt], {}), ([i2s_id, mode, 16000, 16, cfmt, dfmt], {}),
@@ -366,7 +366,7 @@ def stream_http_audio(url):
return False return False
hdr += b hdr += b
# Read WAV header
header_left = 44 header_left = 44
while header_left > 0: while header_left > 0:
chunk = sock.recv(header_left) chunk = sock.recv(header_left)
@@ -389,16 +389,16 @@ def stream_http_audio(url):
samples = n // 2 samples = n // 2
for j in range(samples): for j in range(samples):
idx = j * 2 idx = j * 2
# Read 16-bit Signed LE
s = in_buf[idx] | (in_buf[idx + 1] << 8) s = in_buf[idx] | (in_buf[idx + 1] << 8)
if s >= 32768: s -= 65536 if s >= 32768: s -= 65536
# Convert to Unsigned + Center Offset for DAC
u = (s + 32768) & 0xFFFF u = (s + 32768) & 0xFFFF
u_lo = u & 0xFF u_lo = u & 0xFF
u_hi = u >> 8 u_hi = u >> 8
# Map Stereo for built-in MSB
o_idx = j * 4 o_idx = j * 4
out_buf[o_idx] = u_lo out_buf[o_idx] = u_lo
out_buf[o_idx + 1] = u_hi out_buf[o_idx + 1] = u_hi
@@ -415,9 +415,9 @@ def stream_http_audio(url):
if hasattr(audio_out, 'deinit'): audio_out.deinit() if hasattr(audio_out, 'deinit'): audio_out.deinit()
return True return True
# ==========================================
# MAIN LOOP
# ==========================================
def run_main(): def run_main():
if not connect_wifi(): if not connect_wifi():
@@ -445,7 +445,7 @@ def run_main():
while True: while True:
gc.collect() gc.collect()
# --- Voice Interaction ---
if btnA.isPressed(): if btnA.isPressed():
if maintain_ws(): if maintain_ws():
draw_status("Listening...", 0x0000FF, f_listen) draw_status("Listening...", 0x0000FF, f_listen)
@@ -456,12 +456,12 @@ def run_main():
rec_start_us = time.ticks_us() rec_start_us = time.ticks_us()
# Fastest possible analog capture loop
while btnA.isPressed(): while btnA.isPressed():
try: try:
raw = adc.read() if adc else 2048 raw = adc.read() if adc else 2048
sample = (raw - 2048) * 16 sample = (raw - 2048) * 16
# Fast clamp
if sample > 32767: sample = 32767 if sample > 32767: sample = 32767
elif sample < -32768: sample = -32768 elif sample < -32768: sample = -32768
@@ -490,12 +490,12 @@ def run_main():
try: try:
ws.send(json.dumps({"event": "stop_listening", "sample_rate": actual_rate})) ws.send(json.dumps({"event": "stop_listening", "sample_rate": actual_rate}))
# Wait for TTS ready
resp = ws.recv() resp = ws.recv()
if resp and isinstance(resp, str): if resp and isinstance(resp, str):
msg = json.loads(resp) msg = json.loads(resp)
if msg.get("event") == "tts_ready": if msg.get("event") == "tts_ready":
# Start direct TCP stream immediately
draw_status("Speaking...", 0x00FF00, f_speak) draw_status("Speaking...", 0x00FF00, f_speak)
http_base = WS_URL.replace("ws://", "http://").replace("/ws/voice", "") http_base = WS_URL.replace("ws://", "http://").replace("/ws/voice", "")
audio_url = http_base + "/api/tts-audio" audio_url = http_base + "/api/tts-audio"
@@ -518,7 +518,7 @@ def run_main():
draw_status("Hold Button A to Talk", 0xFFFFFF, f_s_o) draw_status("Hold Button A to Talk", 0xFFFFFF, f_s_o)
# --- Noise Monitoring ---
r_db = get_db(adc) r_db = get_db(adc)
s_db = (s_db * 0.8) + (r_db * 0.2) s_db = (s_db * 0.8) + (r_db * 0.2)
db_val = int(s_db) db_val = int(s_db)
@@ -531,7 +531,7 @@ def run_main():
lcd.print(db_s, 5, 4, i_c) lcd.print(db_s, 5, 4, i_c)
l_db_s = db_s l_db_s = db_s
# Periodic DB Posting
try: try:
now_ms = time.ticks_ms() now_ms = time.ticks_ms()
if time.ticks_diff(now_ms, last_db_post_time) > 15000: if time.ticks_diff(now_ms, last_db_post_time) > 15000:
@@ -539,7 +539,7 @@ def run_main():
payload_str = '{"room_id":"%s","location":{"type":"Point","coordinates":[%s,%s]},"db":%s}' % (CURRENT_ROOM_ID, CURRENT_LNG, CURRENT_LAT, db_val) payload_str = '{"room_id":"%s","location":{"type":"Point","coordinates":[%s,%s]},"db":%s}' % (CURRENT_ROOM_ID, CURRENT_LNG, CURRENT_LAT, db_val)
http_url = WS_URL.replace("ws://", "http://").replace("/ws/voice", "/api/study-rooms") http_url = WS_URL.replace("ws://", "http://").replace("/ws/voice", "/api/study-rooms")
# Raw socket post
h_p = http_url.split("://")[1].split("/")[0] h_p = http_url.split("://")[1].split("/")[0]
p_th = "/" + http_url.split("://")[1].split("/", 1)[1] if "/" in http_url.split("://")[1] else "/" p_th = "/" + http_url.split("://")[1].split("/", 1)[1] if "/" in http_url.split("://")[1] else "/"
h_b = h_p.split(":")[0] h_b = h_p.split(":")[0]
@@ -555,7 +555,7 @@ def run_main():
except: except:
pass pass
# Idle Face Blinking logic
if db_val > 67: if db_val > 67:
set_face(f_angry) set_face(f_angry)
else: else:
+10 -10
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python3
import base64 import base64
import urllib.request import urllib.request
import urllib.error import urllib.error
@@ -28,16 +28,16 @@ HEADERS = {
"sec-fetch-mode": "cors", "sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin", "sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"x-cosmos-session-281286": "0:-1#10931", "x-cosmos-session-281286": "0:-1
"x-cosmos-session-295334": "0:-1#745295", "x-cosmos-session-295334": "0:-1
"x-cosmos-session-317755": "0:-1#190559", "x-cosmos-session-317755": "0:-1
"x-cosmos-session-382299": "0:-1#264026", "x-cosmos-session-382299": "0:-1
"x-cosmos-session-418988": "0:-1#4052014", "x-cosmos-session-418988": "0:-1
"x-cosmos-session-793952": "0:-1#13832", "x-cosmos-session-793952": "0:-1
"x-timezone": "America/New_York", "x-timezone": "America/New_York",
} }
# The parent segment ID to chain from — update this to your latest segment
INITIAL_PARENT_SEGMENT_ID = "d7765a66-a24b-4620-b003-11b4c1ed2c36" INITIAL_PARENT_SEGMENT_ID = "d7765a66-a24b-4620-b003-11b4c1ed2c36"
@@ -59,7 +59,7 @@ def send_message(message: str, conversation_id: str = CONVERSATION_ID, parent_se
payload = json.dumps(body).encode("utf-8") payload = json.dumps(body).encode("utf-8")
# Generate fresh per-request headers
sentry_trace_id = uuid.uuid4().hex sentry_trace_id = uuid.uuid4().hex
sentry_span_id = uuid.uuid4().hex[:16] sentry_span_id = uuid.uuid4().hex[:16]
headers = dict(HEADERS) headers = dict(HEADERS)
@@ -105,7 +105,7 @@ def send_message(message: str, conversation_id: str = CONVERSATION_ID, parent_se
print(f"\n[IDs: {decoded}]", file=sys.stderr) print(f"\n[IDs: {decoded}]", file=sys.stderr)
try: try:
ids = json.loads(decoded) ids = json.loads(decoded)
# Try common key names for the segment ID
new_segment_id = ( new_segment_id = (
ids.get("segmentId") ids.get("segmentId")
or ids.get("id") or ids.get("id")
+9 -9
View File
@@ -2,7 +2,7 @@ import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pymongo import MongoClient from pymongo import MongoClient
# MongoDB Setup
MONGO_URI = "mongodb+srv://SarayuJ:[email protected]/testing" MONGO_URI = "mongodb+srv://SarayuJ:[email protected]/testing"
client = MongoClient(MONGO_URI) client = MongoClient(MONGO_URI)
db = client.study_buddy_db db = client.study_buddy_db
@@ -25,10 +25,10 @@ def get_db_for_time_and_location(hour, loc_id):
Generate a dB level based on the hour of the day and the location. Generate a dB level based on the hour of the day and the location.
This creates a recognizable pattern for AI analysis. This creates a recognizable pattern for AI analysis.
""" """
base_db = 40.0 # Ambient noise base_db = 40.0
if loc_id in ['mckeldin', 'esj']: if loc_id in ['mckeldin', 'esj']:
# Busy during the day (10am - 4pm)
if 10 <= hour <= 16: if 10 <= hour <= 16:
base_db = 75.0 base_db = 75.0
elif 17 <= hour <= 22: elif 17 <= hour <= 22:
@@ -37,7 +37,7 @@ def get_db_for_time_and_location(hour, loc_id):
base_db = 45.0 base_db = 45.0
elif loc_id in ['stem', 'iribe']: elif loc_id in ['stem', 'iribe']:
# Busy in the afternoon/evening (2pm - 8pm)
if 14 <= hour <= 20: if 14 <= hour <= 20:
base_db = 70.0 base_db = 70.0
elif 9 <= hour <= 13: elif 9 <= hour <= 13:
@@ -46,7 +46,7 @@ def get_db_for_time_and_location(hour, loc_id):
base_db = 42.0 base_db = 42.0
elif loc_id == 'stamp': elif loc_id == 'stamp':
# Busy during lunch (12pm - 2pm) and dinner (5pm - 7pm)
if 12 <= hour <= 14 or 17 <= hour <= 19: if 12 <= hour <= 14 or 17 <= hour <= 19:
base_db = 85.0 base_db = 85.0
elif 10 <= hour <= 21: elif 10 <= hour <= 21:
@@ -55,14 +55,14 @@ def get_db_for_time_and_location(hour, loc_id):
base_db = 50.0 base_db = 50.0
else: else:
# General locations (Clarice, Yahentamitsi, Reckord, Hornbake)
# Moderate noise during the day
if 9 <= hour <= 18: if 9 <= hour <= 18:
base_db = 60.0 base_db = 60.0
else: else:
base_db = 45.0 base_db = 45.0
# Add random noise to make it look realistic (+/- 5 dB)
noise = random.uniform(-5.0, 5.0) noise = random.uniform(-5.0, 5.0)
return max(30.0, min(100.0, base_db + noise)) return max(30.0, min(100.0, base_db + noise))
@@ -76,7 +76,7 @@ def generate_fake_data():
docs_to_insert = [] docs_to_insert = []
print("Generating 24 hours of fake data with patterns...") print("Generating 24 hours of fake data with patterns...")
# Generate data points every 15 minutes for the last 24 hours
current_time = start_time current_time = start_time
while current_time <= now: while current_time <= now:
hour = current_time.hour hour = current_time.hour
+6 -6
View File
@@ -1,12 +1,12 @@
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
} }
} }
+11 -12
View File
@@ -17,7 +17,7 @@
let errorMessage = $state<string>(''); let errorMessage = $state<string>('');
let hardwareSampleRate = 16000; let hardwareSampleRate = 16000;
// Visualizer data
let currentRms = $state<number>(0); let currentRms = $state<number>(0);
function cleanupMic() { function cleanupMic() {
@@ -44,15 +44,15 @@
function playTTS(url: string) { function playTTS(url: string) {
callState = 'speaking'; callState = 'speaking';
currentAudio = new Audio(url + "?t=" + Date.now()); currentAudio = new Audio(url + "?t=" + Date.now());
currentRms = 0.06; // Set a much smaller safe static visualizer size currentRms = 0.06;
currentAudio.onended = () => { currentAudio.onended = () => {
currentRms = 0; currentRms = 0;
if (callState === 'speaking') { if (callState === 'speaking') {
// AI is done talking, start listening automatically!
startListeningPhase(); startListeningPhase();
} }
}; };
// Some browsers require explicit play tracking
const playPromise = currentAudio.play(); const playPromise = currentAudio.play();
if (playPromise !== undefined) { if (playPromise !== undefined) {
playPromise.catch(e => { playPromise.catch(e => {
@@ -72,7 +72,7 @@
currentRms = 0; currentRms = 0;
try { try {
// Synchronous audio context resume
if (audioContext && audioContext.state === 'suspended') { if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume(); await audioContext.resume();
} }
@@ -100,7 +100,7 @@
ws.send(int16.buffer); ws.send(int16.buffer);
const rms = Math.sqrt(sumSq / float32.length); const rms = Math.sqrt(sumSq / float32.length);
currentRms = rms; // Drive the visualizer UI currentRms = rms;
if (rms > 0.035) { if (rms > 0.035) {
hasSpoken = true; hasSpoken = true;
@@ -132,15 +132,15 @@
if (ws && ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ event: 'stop_listening', sample_rate: hardwareSampleRate })); ws.send(JSON.stringify({ event: 'stop_listening', sample_rate: hardwareSampleRate }));
} }
// Do NOT cleanup hardware mic here as the session is perfectly continuous!
} }
function startCall() { function startCall() {
if (callState !== 'idle') return; if (callState !== 'idle') return;
callState = 'listening'; // transition state instantly callState = 'listening';
try { try {
// MUST CREATE AUDIO CONTEXT SYNCHRONOUSLY IN CLICK HANDLER
const AC = window.AudioContext || (window as any).webkitAudioContext; const AC = window.AudioContext || (window as any).webkitAudioContext;
audioContext = new AC(); audioContext = new AC();
hardwareSampleRate = audioContext.sampleRate; hardwareSampleRate = audioContext.sampleRate;
@@ -152,7 +152,6 @@
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.hostname; const host = window.location.hostname;
ws = new WebSocket(`${protocol}//${host}:8000/ws/voice`); ws = new WebSocket(`${protocol}//${host}:8000/ws/voice`);
ws.onmessage = (e) => { ws.onmessage = (e) => {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.event === 'tts_ready') { if (data.event === 'tts_ready') {
@@ -189,7 +188,7 @@
if (callState === 'idle') { if (callState === 'idle') {
startCall(); startCall();
} else if (callState === 'listening') { } else if (callState === 'listening') {
// Force manual send
hasSpoken = true; hasSpoken = true;
finishUtterance(); finishUtterance();
} else { } else {
@@ -204,7 +203,7 @@
} }
onMount(() => { onMount(() => {
// Auto-start the call when modal opens
startCall(); startCall();
}); });
@@ -31,19 +31,19 @@
function updateMapData() { function updateMapData() {
if (!mapInstance || !mapInstance.getSource("study-locations")) return; if (!mapInstance || !mapInstance.getSource("study-locations")) return;
// Group history by location using room_id
const latestByLoc = new Map(); const latestByLoc = new Map();
for (const room of studyRoomsData) { for (const room of studyRoomsData) {
// Use room_id as the key, fallback to coordinates if room_id is missing for some reason
const key = room.room_id || room.location.coordinates.join(","); const key = room.room_id || room.location.coordinates.join(",");
// Ensure date is treated as UTC
const roomDateString = room.date.endsWith("Z") const roomDateString = room.date.endsWith("Z")
? room.date ? room.date
: room.date + "Z"; : room.date + "Z";
const roomDate = new Date(roomDateString); const roomDate = new Date(roomDateString);
// Filter out points strictly in the future of our playback time
if (playbackTime && roomDate.getTime() > playbackTime) continue; if (playbackTime && roomDate.getTime() > playbackTime) continue;
if (!latestByLoc.has(key)) { if (!latestByLoc.has(key)) {
@@ -68,7 +68,7 @@
} }
const features = latestRooms.map((room: any) => { const features = latestRooms.map((room: any) => {
// Find the corresponding UMD_LOCATION to get the name
let matchingLoc = null; let matchingLoc = null;
if (room.room_id) { if (room.room_id) {
matchingLoc = UMD_LOCATIONS.find( matchingLoc = UMD_LOCATIONS.find(
@@ -98,7 +98,7 @@
}; };
}); });
// Add features for UMD_LOCATIONS that don't have sensor data yet
UMD_LOCATIONS.forEach((loc) => { UMD_LOCATIONS.forEach((loc) => {
const hasData = features.some( const hasData = features.some(
(f) => f.properties.room_id === loc.id, (f) => f.properties.room_id === loc.id,
@@ -113,7 +113,7 @@
}, },
properties: { properties: {
room_id: loc.id, room_id: loc.id,
db: 0, // 0 db for no data db: 0,
name: loc.name, name: loc.name,
date: new Date().toISOString(), date: new Date().toISOString(),
}, },
@@ -277,7 +277,7 @@
`; `;
} }
// Watch and react to mapState updates using a Svelte 5 $effect
$effect(() => { $effect(() => {
const target = mapState.targetFlyTo; const target = mapState.targetFlyTo;
if (mapInstance && target) { if (mapInstance && target) {
@@ -368,7 +368,7 @@
if (!browser || !mapContainer) return; if (!browser || !mapContainer) return;
fetchStudyRoomData(); fetchStudyRoomData();
refreshInterval = setInterval(fetchStudyRoomData, 10000); // refresh every 10s refreshInterval = setInterval(fetchStudyRoomData, 10000);
let map: any; let map: any;
@@ -468,7 +468,7 @@
:global(.maplibregl-ctrl-group) { :global(.maplibregl-ctrl-group) {
background: var( background: var(
--color-panel-glass --color-panel-glass
) !important; /* already switches per theme */ ) !important;
backdrop-filter: blur(12px) !important; backdrop-filter: blur(12px) !important;
border: 1px solid border: 1px solid
color-mix(in srgb, var(--color-surface1) 30%, transparent) !important; color-mix(in srgb, var(--color-surface1) 30%, transparent) !important;
@@ -502,6 +502,6 @@
24, 24,
37, 37,
0.9 0.9
) !important; /* matches bg-crust */ ) !important;
} }
</style> </style>
@@ -3,7 +3,7 @@
import { themeState } from '$lib/states/theme.svelte'; import { themeState } from '$lib/states/theme.svelte';
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
// Dynamic import for chart.js to avoid SSR issues
let Chart: any; let Chart: any;
let chartCanvas: HTMLCanvasElement; let chartCanvas: HTMLCanvasElement;
@@ -32,7 +32,7 @@
return 'Harmful'; return 'Harmful';
} }
// Calculate metrics
let current2hAvg = $derived.by(() => { let current2hAvg = $derived.by(() => {
if (!mapState.selectedLocation) return 0; if (!mapState.selectedLocation) return 0;
const now = Date.now(); const now = Date.now();
@@ -53,7 +53,7 @@
if (!Chart || !chartCanvas || !mapState.selectedLocation) return; if (!Chart || !chartCanvas || !mapState.selectedLocation) return;
if (chartInstance) chartInstance.destroy(); if (chartInstance) chartInstance.destroy();
// filter past 24h for this loc
const locData = mapState.historyData.filter(d => d.room_id === mapState.selectedLocation?.id) const locData = mapState.historyData.filter(d => d.room_id === mapState.selectedLocation?.id)
.map(d => ({ .map(d => ({
x: new Date(d.date.endsWith('Z') ? d.date : d.date + 'Z'), x: new Date(d.date.endsWith('Z') ? d.date : d.date + 'Z'),
@@ -145,7 +145,7 @@
onMount(async () => { onMount(async () => {
const chartModule = await import('chart.js/auto'); const chartModule = await import('chart.js/auto');
const chartjsAdapter = await import('chartjs-adapter-date-fns'); // Need this for time scaling const chartjsAdapter = await import('chartjs-adapter-date-fns');
Chart = chartModule.default; Chart = chartModule.default;
if (mapState.selectedLocation) drawChart(); if (mapState.selectedLocation) drawChart();
}); });
+1 -1
View File
@@ -1 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+4 -4
View File
@@ -17,15 +17,15 @@ class AlertsState {
const now = Date.now(); const now = Date.now();
for (const room of latestReadings) { for (const room of latestReadings) {
// Only fire if the reading is >= 65dB (Disruptive or Harmful)
if (room.db >= 65) { if (room.db >= 65) {
const roomDate = new Date(room.date.endsWith('Z') ? room.date : room.date + 'Z').getTime(); const roomDate = new Date(room.date.endsWith('Z') ? room.date : room.date + 'Z').getTime();
// Ensure the reading is recent (within 5 minutes, 300000ms), to prevent alerting on stale data on initial load
if (now - roomDate < 300000) { if (now - roomDate < 300000) {
const lastAlert = this.alertHistory.get(room.room_id) || 0; const lastAlert = this.alertHistory.get(room.room_id) || 0;
// Cooldown: Don't alert for the same location within 3 minutes (180000ms)
if (now - lastAlert > 180000) { if (now - lastAlert > 180000) {
const locData = UMD_LOCATIONS.find(l => l.id === room.room_id); const locData = UMD_LOCATIONS.find(l => l.id === room.room_id);
const locName = locData ? locData.name : room.room_id; const locName = locData ? locData.name : room.room_id;
@@ -42,7 +42,7 @@ class AlertsState {
timestamp: now timestamp: now
}); });
// Keep a max of 10 alerts logic
if (this.activeAlerts.length > 20) { if (this.activeAlerts.length > 20) {
this.activeAlerts.pop(); this.activeAlerts.pop();
} }
+4 -4
View File
@@ -22,16 +22,16 @@ export const UMD_LOCATIONS: StudyLocation[] = [
export const DEFAULT_VIEW = { lng: -76.94259561477574, lat: 38.98813763708658, zoom: 15.5 }; export const DEFAULT_VIEW = { lng: -76.94259561477574, lat: 38.98813763708658, zoom: 15.5 };
class MapState { class MapState {
// The target coordinates the map should fly to
targetFlyTo = $state<{ lng: number; lat: number; zoom: number; timestamp: number } | null>(null); targetFlyTo = $state<{ lng: number; lat: number; zoom: number; timestamp: number } | null>(null);
selectedLocation = $state<StudyLocation | null>(null); selectedLocation = $state<StudyLocation | null>(null);
historyData = $state<any[]>([]); historyData = $state<any[]>([]);
historyLoading = $state<boolean>(false); historyLoading = $state<boolean>(false);
historyPlaybackTime = $state<number>(Date.now()); // The current time scrubber for 24h historyPlaybackTime = $state<number>(Date.now());
flyTo(lng: number, lat: number, zoom: number = 18) { flyTo(lng: number, lat: number, zoom: number = 18) {
this.targetFlyTo = { lng, lat, zoom, timestamp: Date.now() }; // timestamp ensures reactivity even if same coords this.targetFlyTo = { lng, lat, zoom, timestamp: Date.now() };
} }
flyHome() { flyHome() {
@@ -41,7 +41,7 @@ class MapState {
async fetchHistoryData() { async fetchHistoryData() {
this.historyLoading = true; this.historyLoading = true;
try { try {
// Fetch from FastAPI backend
const res = await fetch('http://127.0.0.1:8000/api/study-rooms/history'); const res = await fetch('http://127.0.0.1:8000/api/study-rooms/history');
if (res.ok) { if (res.ok) {
const json = await res.json(); const json = await res.json();
+5 -5
View File
@@ -4,7 +4,7 @@
import InteractiveMap from '$lib/components/InteractiveMap.svelte'; import InteractiveMap from '$lib/components/InteractiveMap.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
// Time state
const NOW = Date.now(); const NOW = Date.now();
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
let playbackTime = $state(NOW); let playbackTime = $state(NOW);
@@ -15,19 +15,19 @@
function togglePlay() { function togglePlay() {
isPlaying = !isPlaying; isPlaying = !isPlaying;
if (isPlaying) { if (isPlaying) {
// Auto replay from beginning if at the end
if (playbackTime >= NOW) { if (playbackTime >= NOW) {
playbackTime = NOW - TWENTY_FOUR_HOURS; playbackTime = NOW - TWENTY_FOUR_HOURS;
} }
playInterval = setInterval(() => { playInterval = setInterval(() => {
// Advance 15 minutes per tick
playbackTime += 15 * 60 * 1000; playbackTime += 15 * 60 * 1000;
if (playbackTime >= NOW) { if (playbackTime >= NOW) {
playbackTime = NOW; playbackTime = NOW;
isPlaying = false; isPlaying = false;
clearInterval(playInterval); clearInterval(playInterval);
} }
}, 200); // Ticks every 200ms }, 200);
} else { } else {
clearInterval(playInterval); clearInterval(playInterval);
} }
@@ -37,7 +37,7 @@
if (playInterval) clearInterval(playInterval); if (playInterval) clearInterval(playInterval);
}); });
// Derived values for the UI
let progressPercent = $derived(((playbackTime - (NOW - TWENTY_FOUR_HOURS)) / TWENTY_FOUR_HOURS) * 100); let progressPercent = $derived(((playbackTime - (NOW - TWENTY_FOUR_HOURS)) / TWENTY_FOUR_HOURS) * 100);
let formattedTime = $derived.by(() => { let formattedTime = $derived.by(() => {
+1 -1
View File
@@ -4,7 +4,7 @@
--font-sans: 'Comic Relief', 'Comic Neue', 'Comic Sans MS', cursive, system-ui, sans-serif; --font-sans: 'Comic Relief', 'Comic Neue', 'Comic Sans MS', cursive, system-ui, sans-serif;
--font-display: 'Comic Relief', 'Comic Neue', 'Comic Sans MS', cursive, system-ui, sans-serif; --font-display: 'Comic Relief', 'Comic Neue', 'Comic Sans MS', cursive, system-ui, sans-serif;
/* Catppuccin Theme */
--color-crust: #11111b; --color-crust: #11111b;
--color-mantle: #181825; --color-mantle: #181825;
--color-base: #1e1e2e; --color-base: #1e1e2e;
+1 -1
View File
@@ -46,7 +46,7 @@
<div class="relative w-full h-full bg-crust border-l border-white/5 p-6 md:p-12 overflow-y-auto duration-500 transition-colors"> <div class="relative w-full h-full bg-crust border-l border-white/5 p-6 md:p-12 overflow-y-auto duration-500 transition-colors">
<!-- Background Mesh --> <!-- Background Mesh -->
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-surface0/30 via-crust to-crust z-0 pointer-events-none transition-colors duration-500"></div> <div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,var(--tw-gradient-stops))] from-surface0/30 via-crust to-crust z-0 pointer-events-none transition-colors duration-500"></div>
<div class="relative z-10 max-w-4xl mx-auto"> <div class="relative z-10 max-w-4xl mx-auto">
<header class="mb-10"> <header class="mb-10">
+2 -2
View File
@@ -1,9 +1,9 @@
import adapter from '@sveltejs/adapter-static'; import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
compilerOptions: { compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
}, },
kit: { kit: {
+1 -1
View File
@@ -12,7 +12,7 @@ export default defineConfig({
name: 'HushMap', name: 'HushMap',
short_name: 'HushMap', short_name: 'HushMap',
description: 'Campus noise mapping and intervention.', description: 'Campus noise mapping and intervention.',
theme_color: '#0f172a', /* slate-900 */ theme_color: '#0f172a',
background_color: '#0f172a', background_color: '#0f172a',
display: 'standalone', display: 'standalone',
icons: [ icons: [