# Spec — Module B: Web App Trợ lý ảo Xe thông minh **Người phụ trách:** Hiếu | **Đề 3 — Secure Virtual Assistant with Speaker Recognition** **Nhận bàn giao từ:** Module A (Khoa) — `model/moduleA_artifacts_final/` --- ## 1. Phạm vi & ranh giới với Module A Module B **kế thừa nguyên** `speaker_model.py` và `checkpoints/best.pt` từ Module A, không sửa code hay threshold mặc định (`DEFAULT_THRESHOLD_SV = 0.35`, `DEFAULT_THRESHOLD_SID = 0.20`). Việc của Module B: - Xây web app hoàn chỉnh: ghi âm → ASR → orchestrator → 3 usecase (general/SV/SID) → TTS. - Component enrollment & quản lý thành viên trong xe (thu giọng, lưu DB). - `check_audio_quality()` cho luồng enrollment (Module A chỉ định hướng, không code). - Convert audio `.webm` (MediaRecorder trình duyệt) → `.wav` 16kHz mono trước khi gọi `get_embedding()` — **trách nhiệm của Module B**, dùng `pydub`/`ffmpeg`. - Đảm bảo logic gating SV/SID nằm cứng trong code Python (mục 3), không giao cho LLM quyết định. ## 2. Tech stack đã chốt | Thành phần | Lựa chọn | Ghi chú | |---|---|---| | Backend | FastAPI (Python) | Chạy trực tiếp `speaker_model.py` (torch/speechbrain) trong cùng process, load model 1 lần lúc startup | | Frontend | React (Vite) hoặc HTML/JS thuần | Gộp chung 1 Space với backend — FastAPI serve luôn static build, tránh CORS/multi-deploy | | ASR | PhoWhisper (`transformers`) | Chạy CPU trong backend, đủ cho câu ngắn 2-6 giây | | Orchestrator | Gemini API (key đã có trong `.env.txt`) | Ép **structured/JSON output** `{intent, entities}` — Gemini chỉ classify, KHÔNG tự quyết định có cần SV hay không | | TTS | gTTS | Free, không cần key riêng | | Speaker model | `speaker_model.py` (Module A) | Không đổi interface/threshold | | Database | SQLite (SQLAlchemy) | Local trong container HF Space — **chấp nhận reset khi Space sleep/restart**, đủ cho demo, không cần vector DB (identify() đã dot-product thuần Python) | | Nhạc (SID) | Deezer API | Free, không cần key cho search; **giới hạn: chỉ preview 30s/bài**, chấp nhận cho demo | | Thời tiết/định vị (General) | OpenWeatherMap | Free tier; vị trí dùng **toạ độ cố định demo** (không có GPS xe thật) | | Deploy | Hugging Face Spaces (Docker SDK, free CPU tier) | ~16GB RAM free, đủ chạy đồng thời PhoWhisper + speechbrain | **Rủi ro đã biết, chấp nhận:** DB mất dữ liệu khi HF Space sleep (free tier không có persistent disk) → cần re-enroll sau mỗi lần Space ngủ dậy nếu demo cách nhau lâu. Nếu cần bền hơn, cân nhắc chuyển sang Supabase (free Postgres) sau — không bắt buộc cho demo báo cáo. ## 3. Kiến trúc pipeline tổng thể ``` [Browser: ghi âm .webm] │ ▼ convert .webm → .wav 16kHz mono (pydub/ffmpeg) │ ▼ ASR: PhoWhisper → text │ ▼ Orchestrator: Gemini API (JSON mode) input: text output: { "intent": "...", "entities": {...} } │ ▼ [GATE CỨNG - code Python, KHÔNG phải LLM] bảng mapping intent → usecase → có cần SV không │ ┌────┼─────────────┬─────────────┐ ▼ ▼ ▼ General SV SID (không auth) (verify() trước) (identify()) │ │ │ ▼ ▼ ▼ Action Handler tương ứng (mục 4) │ ▼ TTS: gTTS → audio response → trả về Browser ``` ## 4. Orchestrator (Gemini) — hợp đồng I/O - **Input:** text đã ASR (tiếng Việt). - **Output bắt buộc dạng JSON có schema cố định**, ví dụ: ```json { "intent": "sv_unlock_door" | "sv_start_engine" | "sid_play_playlist" | "general_weather" | "general_location" | "unknown", "entities": {} } ``` - Gemini **chỉ làm nhiệm vụ hiểu intent/entity**, không quyết định pass/fail xác thực. - Code Python có 1 bảng mapping tĩnh (hardcode), ví dụ: | intent | usecase | cần SV? | |---|---|---| | `general_weather`, `general_location` | General | Không | | `sv_unlock_door`, `sv_start_engine` | SV | **Có** | | `sid_play_playlist` | SID | Không (nhưng cần `identify()`) | Bảng này quyết định route, **không phải Gemini quyết định**. ## 5. Ba usecase — chi tiết ### 5.1 General — thời tiết / nhiệt độ / định vị - Không cần xác thực giọng nói. - Gọi OpenWeatherMap current weather API với toạ độ cố định demo (vd TP.HCM). - Trả lời qua TTS: "Hiện tại trời [mô tả], nhiệt độ [x]°C." - Định vị: trả toạ độ/tên địa điểm demo cố định (do không có GPS xe thật) — cần nêu rõ giới hạn này trong báo cáo. ### 5.2 SV — mở khoá cửa / khởi động xe 1. Ghi âm runtime → `get_embedding()`. 2. So khớp với embedding đã enroll của user hiện tại (hoặc quét toàn bộ DB nếu chưa xác định ai đang nói — cần chốt thêm ở bước code: có yêu cầu chọn "tôi là ai" trước hay auto quét). 3. `verify(emb_enrolled, emb_runtime)` — threshold mặc định 0.35. 4. Nếu `True`: thực hiện action (mock: đổi trạng thái "đã mở khoang"/"đã khởi động"), ghi vào bảng `action_log` (timestamp, user_id, action, verified=True). 5. Nếu `False`: cho phép **retry 1-2 lần** trước khi báo "không xác thực được" (theo khuyến nghị README Module A, vì FRR thực nghiệm ~37.5%) — không chặn ngay từ lần đầu. 6. Log cả các lần verify thất bại (verified=False) để phục vụ báo cáo/demo. ### 5.3 SID — phát playlist cá nhân hoá 1. Ghi âm runtime → `get_embedding()`. 2. `identify(emb, db_embeddings)` — threshold mặc định 0.20 — trả về `user_id` hoặc `None`. 3. Nếu có `user_id`: lấy danh sách bài hát yêu thích của user từ bảng `preferences` → gọi Deezer API lấy preview URL từng bài → trả về frontend hiển thị playlist thật + phát qua `