Legacy Preview Update

This commit is contained in:
2026-04-20 16:16:54 -04:00
parent 8312b5bea6
commit 8be7e1e5f9
10 changed files with 358 additions and 81 deletions
+10 -6
View File
@@ -14,7 +14,7 @@
## Setup Instructions
### Prerequisites
1. **Python 3.9+** is strictly recommended to support asynchronous typing paradigms.
1. **Python 3.10+** is strictly recommended to support asynchronous typing paradigms.
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:
- **Ubuntu/Debian**: `sudo apt install ffmpeg`
- **macOS**: `brew install ffmpeg`
@@ -33,16 +33,20 @@ pip install -r requirements.txt
### Configuration Tokens
Provide runtime keys securely targeting TerpAI context queues and ElevenLabs synthesized avatars within a `.env` dotfile:
Provide runtime keys securely targeting TerpAI context queues, Gemini Fallback, and ElevenLabs synthesized avatars within a `.env` dotfile:
```ini
ELEVENLABS_API_KEY=sk_...
ELEVENLABS_VOICE_ID=JBFqnCBsd6RMkjVDRZzb
TERP_AI_BEARER_TOKEN=eyJhbGciOiJSUz...
TERP_AI_CONVERSATION_ID=37fa27cc-...
GEMINI_API_KEY=AIza...
MONGODB_URI=mongodb+srv://...
USE_DB=false
```
*Note: `USE_DB` controls whether the application connects to MongoDB (`true`) or uses on-the-fly generated in-memory data for demonstrations (`false`).*
To invoke the engine, simply execute Uvicorn across your `0.0.0.0` loopback:
```bash
@@ -58,7 +62,7 @@ uvicorn server:app --host 0.0.0.0 --port 8000
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. **Int16 Byte Array Exchange**: Devices connect to `ws://<server_ip>:8000/ws/voice` and push raw binary frames asynchronously over the socket.
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.
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 decibel tracking telemetry parameters natively attached into the AI user conversation chunk. We utilize **Terp AI** with an automatic, seamless fallback to **Gemini 2.5 Flash** if the primary Terp service is unavailable.
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.
### Tensor Vision Endpoints (`/api/vision/room-status`)
@@ -90,8 +94,8 @@ Leveraging OpenCV bindings layered beneath a YOLOv8-driven bounding box topology
## Database Registries
* `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.
* `GET /api/study-rooms/history`: Pulls the active global repository of logged architectural noise measurements captured universally within the preceding 24 hours. (Uses MongoDB or in-memory generated data based on the `USE_DB` flag).
* `GET /api/study-rooms`: Pulls generic unstructured noise lists.
> [!IMPORTANT]
> 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.
> 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.
+3 -1
View File
@@ -9,4 +9,6 @@ ultralytics
opencv-python-headless
scipy
pymongo
pydantic
certifi
google-genai
pydantic
+98 -33
View File
@@ -8,9 +8,16 @@ import requests
import json
import base64
import urllib.request
import time
import sys
# import ssl
# ssl._create_default_https_context = ssl._create_unverified_context
# Add scripts directory to path to import fake data generator
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'scripts'))
try:
from generate_fake_data import get_fake_data
except ImportError:
print("Warning: Could not import get_fake_data from scripts/generate_fake_data.py")
def get_fake_data(locations): return []
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, File, UploadFile
from fastapi.staticfiles import StaticFiles
@@ -21,6 +28,7 @@ from datetime import datetime, timedelta
from pymongo import MongoClient
from dotenv import load_dotenv
from vision import analyze_room_image
from google import genai
from fastapi.middleware.cors import CORSMiddleware
@@ -37,11 +45,30 @@ app.add_middleware(
)
USE_DB = os.getenv("USE_DB", "false").lower() == "true"
import certifi
MONGO_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
mongo_client = MongoClient(MONGO_URI, tlsCAFile=certifi.where())
db = mongo_client.study_buddy_db
study_rooms_collection = db.study_rooms
if USE_DB:
MONGO_URI = os.getenv("MONGODB_URI", "mongodb://localhost:27017/")
mongo_client = MongoClient(MONGO_URI, tlsCAFile=certifi.where())
db = mongo_client.study_buddy_db
study_rooms_collection = db.study_rooms
else:
print("Running in in-memory mode. MongoDB is disabled. Set USE_DB=true to enable.")
# Fake data cache
_fake_data_cache = None
_fake_data_cache_time = 0
def _get_cached_fake_data():
global _fake_data_cache, _fake_data_cache_time
now = time.time()
# Cache for 5 minutes (300 seconds)
if _fake_data_cache is None or now - _fake_data_cache_time > 300:
# Assuming UMD_LOCATIONS is defined further down, but we can just use the global
_fake_data_cache = get_fake_data(UMD_LOCATIONS)
_fake_data_cache_time = now
return _fake_data_cache
class GeoJSONPoint(BaseModel):
@@ -159,7 +186,7 @@ def _transcribe_pcm(pcm_data: bytes, sample_rate: int = SAMPLE_RATE) -> str:
os.remove(tmp_path)
def get_terp_ai_response(message: str) -> str:
"""Send text to Terp AI and return the full response."""
"""Send text to Terp AI and return the full response. Fallback to Gemini if needed."""
url = f"https://terpai.umd.edu/api/internal/userConversations/{CONVERSATION_ID}/segments"
payload = {
"question": message,
@@ -175,7 +202,7 @@ def get_terp_ai_response(message: str) -> str:
full_response = ""
event = None
try:
resp = requests.post(url, json=payload, headers=HEADERS, stream=True, timeout=30, verify=False)
resp = requests.post(url, json=payload, headers=HEADERS, stream=True, timeout=10, verify=False)
resp.raise_for_status()
for line in resp.iter_lines(decode_unicode=True):
if not line:
@@ -188,13 +215,28 @@ def get_terp_ai_response(message: str) -> str:
if event == "response-updated":
full_response += decoded
resp.close()
if full_response:
return full_response
else:
raise Exception("Empty response from Terp AI")
except Exception as e:
print(f"Terp AI error: {e}")
return "I am sorry, there was an error connecting to Terp AI."
print(f"Terp AI error, falling back to Gemini: {e}")
try:
api_key = os.getenv("GEMINI_API_KEY")
if not api_key or api_key == "your_gemini_api_key":
return "Terp AI is unavailable and Gemini fallback is not configured."
client = genai.Client(api_key=api_key)
response = client.models.generate_content(
model='gemini-2.5-flash',
contents=message
)
return response.text
except Exception as gemini_e:
print(f"Gemini fallback error: {gemini_e}")
return "I am sorry, both Terp AI and the Gemini fallback encountered an error."
return full_response
def _convert_to_pcm(audio_data: bytes, input_format: str = "mp3") -> bytes | None:
def _convert_to_pcm(audio_data: bytes, input_format: str = "mp3") -> Optional[bytes]:
"""Convert audio data to 16-bit 16 kHz mono PCM using ffmpeg."""
try:
result = subprocess.run(
@@ -225,7 +267,7 @@ def _convert_to_pcm(audio_data: bytes, input_format: str = "mp3") -> bytes | Non
print("ffmpeg conversion timed out")
return None
def _generate_tts(text: str) -> bytes | None:
def _generate_tts(text: str) -> Optional[bytes]:
"""Generate speech audio from text using ElevenLabs TTS API."""
api_key = os.getenv("ELEVENLABS_API_KEY")
voice_id = os.getenv("ELEVENLABS_VOICE_ID", "JBFqnCBsd6RMkjVDRZzb")
@@ -373,23 +415,36 @@ UMD_LOCATIONS = [
def get_latest_locations_context() -> str:
"""Fetch the latest stats for each known location to feed as AI context."""
twenty_four_hours_ago = datetime.utcnow() - timedelta(hours=24)
pipeline = [
{"$match": {"date": {"$gte": twenty_four_hours_ago}}},
{"$sort": {"date": -1}},
{"$group": {
"_id": "$room_id",
"latest_db": {"$first": "$db"},
"time": {"$first": "$date"}
}}
]
latest_stats = list(study_rooms_collection.aggregate(pipeline))
room_dict = {loc["id"]: loc["name"] for loc in UMD_LOCATIONS}
if USE_DB:
twenty_four_hours_ago = datetime.utcnow() - timedelta(hours=24)
pipeline = [
{"$match": {"date": {"$gte": twenty_four_hours_ago}}},
{"$sort": {"date": -1}},
{"$group": {
"_id": "$room_id",
"latest_db": {"$first": "$db"},
"time": {"$first": "$date"}
}}
]
latest_stats = list(study_rooms_collection.aggregate(pipeline))
else:
fake_data = _get_cached_fake_data()
latest_stats_map = {}
# Data is naturally sorted chronologically in our generator, so reverse it
for d in reversed(fake_data):
if d["room_id"] not in latest_stats_map:
latest_stats_map[d["room_id"]] = {
"_id": d["room_id"],
"latest_db": d["db"],
"time": d["date"]
}
latest_stats = list(latest_stats_map.values())
if not latest_stats:
return "No recent location noise stats available today."
room_dict = {loc["id"]: loc["name"] for loc in UMD_LOCATIONS}
lines = ["Latest Study Room Stats:"]
for stat in latest_stats:
room_id = stat.get("_id")
@@ -426,8 +481,12 @@ async def create_study_room_data(data: StudyRoomData):
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"}
if USE_DB:
result = study_rooms_collection.insert_one(doc)
return {"id": str(result.inserted_id), "room_id": data.room_id, "status": "success"}
else:
return {"id": "dummy_id", "room_id": data.room_id, "status": "success (in-memory, not saved)"}
@app.get("/api/study-rooms")
async def get_study_room_data():
@@ -437,11 +496,17 @@ async def get_study_room_data():
@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))
if USE_DB:
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))
else:
rooms = _get_cached_fake_data()
# Ensure we don't leak ObjectIds or non-serializable stuff
# Dates are naturally sorted but let's reverse them to match MongoDB behavior (newest first)
rooms = list(reversed(rooms))
return {"data": rooms}
@app.get("/{full_path:path}")