# AI Voice

> Nhận diện giọng nói thành văn bản (Speech-to-Text) qua OpenRouter + Gemini, hỗ trợ wake word và ghi mic

| | |
|---|---|
| Version | 1.0.0 |
| Category | AI & Agents |
| Permissions | network.remote, process.spawn, filesystem.read, filesystem.write |
| Depends on | ai-chat |

# AI Voice

Nhận diện giọng nói thành văn bản (Speech-to-Text) qua OpenRouter + Gemini, hỗ trợ wake word và ghi mic.

Đây là bản port của app `stt` (OpenRouter + Gemini) thành plugin Kazuo. Toàn bộ xử lý audio
(ghi mic, VU meter, waveform, wake word bằng openWakeWord/ONNX, gọi OpenRouter) do **backend Python**
đảm nhiệm — plugin bundle sẵn code Python trong `python/` và `main/index.ts` spawn nó khi enable.

**Type:** `backend` — see `.claude/skills/kazuo-create-plugin/references/plugin-examples.md`

## Yêu cầu (bắt buộc)

- **Python 3.10+** cài sẵn và có trong PATH (main thử `python` → `py` → `python3`).
- Cài thư viện Python 1 lần:
  ```bash
  pip install -r plugins-library/ai-voice/python/requirements.txt
  ```
  (khi cài plugin, thư mục được copy sang `userData/plugins/ai-voice/python/` — nếu chưa cài deps ở
  môi trường Python đang dùng thì backend sẽ thoát và widget hiện thông báo `pip install ...`).
- Nhập **OpenRouter API Key** trong Settings của plugin (lấy tại openrouter.ai/keys).

## Cách hoạt động

- `main/index.ts` (main process): khi `activate` → spawn `python backend.py` (cwd = `python/`), truyền
  `OPENROUTER_API_KEY` từ settings qua env. `deactivate` → kill tiến trình. Kênh IPC:
  - `plugin:ai-voice:status` — widget hỏi trạng thái backend (đang chạy? lỗi gì?) để hiện thông báo.
  - `plugin:ai-voice:restartBackend` — respawn để nhận API key mới sau khi lưu Settings.
  - `plugin:ai-voice:writeText` — ghi transcript ra file người dùng chọn (sandbox không tự ghi được).
- `python/backend.py`: mở WebSocket `ws://127.0.0.1:8765`, xử lý lệnh phiên âm/ghi/wake word.
- `js/widget.js` (webview): kết nối thẳng WebSocket đó (không qua IPC), vẽ UI. Chỉ dùng
  `window['kazuo-ai']` cho 3 việc: `openFileDialog` (chọn file), `serveLocalFile` (nghe lại bản ghi),
  `saveFileDialog` + `invoke writeText` (lưu .txt).

## VAD endpointing (cắt đoạn giọng nói)

Thay vì luôn ghi cứng đủ số giây rồi gửi Gemini, plugin dùng **Silero VAD** (bundle sẵn trong
`openwakeword` — 0 dependency mới) để: chờ tới khi bạn **bắt đầu nói** → ghi → **dừng khi im lặng** →
cắt đúng đoạn giọng nói (kèm đệm) rồi mới gửi. Nếu không nói trong thời gian chờ → **hủy, không gửi**
(tránh gửi audio rỗng/noise, đỡ tốn token, bỏ transcript rác). Áp cho cả wake-word tự kích hoạt lẫn
nút "▶ Phiên âm".

### Profile môi trường (switch nhanh)

Dropdown **"Môi trường"** trên widget: mỗi profile (Yên tĩnh / Văn phòng / Ngoài trời / Trên xe /
Máy lạnh·quạt, hoặc do bạn **tạo thêm**) giữ **bộ tham số VAD riêng**. **Switch profile → áp ngay** tham số
của môi trường đó (đi đâu chọn đó). Trong wizard có thể **➕ Tạo** profile mới (đặt tên) và **🗑 Xóa**.

### Hiệu chỉnh tự động (wizard)

VAD phụ thuộc mic/giọng/môi trường. Có **wizard hiệu chỉnh theo bộ mẫu nhiều môi trường**: tự mở lần đầu
(hoặc bấm **"🎚 Chạy hiệu chỉnh VAD"** trong ⚙ Settings của widget). **Thu tự do**: nhập **tên môi trường**
rồi bấm **"🤫 Thu tiếng nền (2s)"** hoặc **"🗣 Thu giọng nói (4s)"** — thu bao nhiêu mẫu ở bao nhiêu môi
trường tùy ý; mỗi lần lưu 1 bản ghi WAV ở `vad-samples/`. **Danh sách bản ghi** cho chọn nhiều (checkbox),
**nghe lại (▶)**, **Chọn tất cả / Xóa đã chọn / Mở thư mục** (xóa & mở thư mục dùng plugin **File Explorer**,
cần bấm Grant 1 lần). Mẫu thu + danh sách gắn theo **profile đang chọn**. Bấm **"⚙ Tính tham số từ bộ
mẫu"** → gộp bản ghi nền/giọng của **profile đó** → tự tính
**Khuếch đại + Ngưỡng âm lượng + Ngưỡng VAD** + hiện sóng giọng-gộp so với vạch ngưỡng đề xuất →
**"Áp dụng & xác nhận"**. Bản ghi **chỉ mất khi bạn tự xóa**. Cờ `vad_calibrated` đánh dấu đã hiệu chỉnh.

**Học môi trường một chạm** (nhạc nền, quạt, tiếng ồn liên tục): bấm **"🎵 Học môi trường (8s)"** trong
wizard khi đang **im lặng** → thu 8 giây tiếng nền, **không cần mẫu giọng nói và không lưu bản ghi**. Giữ
nguyên Khuếch đại hiện tại, đặt **Ngưỡng âm lượng** = mức nền (phân vị 95%) × 1.3 + 0.03 (kẹp 0.02–0.5) và
**Ngưỡng VAD** = max(0.5, xác suất Silero nền + 0.15) (tối đa 0.9). Kết quả hiện ở khu "Tham số đề xuất" →
bấm **"Áp dụng & xác nhận"**. Nếu nền quá to (ngưỡng chạm trần 0.5) thì nên giảm âm lượng nhạc.

- Bật/tắt bằng ô **"Bỏ qua khi không có giọng nói"**. **Tắt → về hành vi ghi cứng đủ ô "Giây" như cũ.**
- Slider **"Ngưỡng VAD"** (cao = chặt hơn, ít bắt nhầm noise). Nên để ~0.5; quá thấp (vd 0.05) khiến
  VAD luôn tưởng có giọng → không bao giờ dừng/bỏ qua.
- **Ô "Giây" KHÔNG áp cho chế độ VAD** — khi bật VAD, cơ chế dừng là im lặng, còn trần an toàn là
  `vad_max_record` (mặc định **30s**, chỉnh trong `python/config.json5`). Ô "Giây" chỉ dùng khi VAD tắt
  (ghi cứng). Trần đếm từ lúc bắt đầu nói.
- **5 tham số VAD chỉnh trực tiếp trên widget** (tự lưu vào `config.json5`):
  - **Chờ tối đa** (`vad_max_wait`, 3.0s) — quá hạn chưa nghe thấy giọng → bỏ qua.
  - **Im lặng dừng** (`vad_silence`, 0.8s) — im liên tục bấy nhiêu sau khi nói → dừng & gửi. Bị cắt cụt
    khi nói ngập ngừng → **tăng** giá trị này.
  - **Giọng tối thiểu** (`vad_min_speech`, 0.2s) — thời lượng giọng nói để coi là "bắt đầu".
  - **Đệm** (`vad_pad`, 0.2s) — thêm trước/sau đoạn cắt; hụt đầu/cuối câu → **tăng**.
  - **Trần ghi** (`vad_max_record`, 30s) — chốt chặn an toàn, đếm từ lúc bắt đầu nói.
  - Không bắt được giọng / cắt sớm → **giảm Ngưỡng VAD** trước, rồi tăng Im lặng / Chờ tối đa.
- **Sóng âm + VU meter** hiển thị mức tín hiệu khi nghe wake và khi đang ghi (giúp căn micro/độ to).
  Trên sóng còn có **đường ngưỡng** (vàng, di chuyển live khi kéo slider) + **đường xác suất giọng nói**
  (xanh, chỉ khi đang ghi): bấm "▶ Phiên âm" nói thử, chỉnh slider sao cho lúc nói đường xanh vượt đường
  vàng, lúc im thì dưới.
- **Hạn chế:** VAD chỉ phân biệt "có/không có giọng nói", **không lọc giọng người khác** — ai nói trong
  khoảng ghi đều bị thu. Nói ngập ngừng quá `vad_silence` có thể bị cắt cụt (tăng `vad_silence` nếu cần).
  Sóng âm hiển thị **biên độ** (độ to), không trực tiếp bằng ngưỡng VAD (ngưỡng so với xác suất giọng nói).

## STT cục bộ (on-device) + chữ tạm

Ngoài OpenRouter (cloud), backend chạy được 3 engine **ngay trên máy**: `faster-whisper`
(CPU/GPU), `whisper.cpp` (`pywhispercpp`) và `sherpa-onnx` (model tiếng Việt 30M, rất nhẹ).
Thư viện + model **tự cài khi chọn lần đầu** (pip + tải về `stt-models/`, đã `.gitignore`), có tiến
trình trong widget. Cấu hình (`config.json5`): `stt_live`, `stt_final` (`cloud`|`local`),
`stt_priority` (`speed`|`accuracy`|`light`|`custom`), `stt_order`, `stt_models`, `stt_device`.

- **Chữ tạm (`stt_live`)**: khi ghi có VAD, mỗi vài trăm ms engine đầu tiên *đã sẵn sàng* trong
  plan phiên âm lại đoạn đã nói và phát event `partial {text}` (rỗng = xoá). ai-chat hiện chữ này mờ
  trong ô nhập. Không tải model giữa chừng: engine chưa có model bị bỏ qua.
- **Chữ cuối (`stt_final=local`)**: thử engine theo thứ tự plan; lỗi → engine kế tiếp; tới
  `openrouter` thì dùng cloud. Nếu plan KHÔNG có `openrouter` mà không engine nào dùng được → báo lỗi,
  không tự gửi audio lên cloud. Chỉ áp cho WAV 16kHz mono (file khác → cloud).
- Lệnh WS mới: `stt_list` → `stt_engines`; `stt_install {engine, model}` → `stt_install` (tiến trình/xong).

## Files

- `manifest.json` — `type=backend`, `mainEntry: main/index.js`, `defaultSettings: { apiKey, keyRef }`, size `460×680`.
- `widget.html` / `js/widget.js` / `css/widget.css` — UI **rút gọn**: chọn mic (+ số giây), model,
  ngôn ngữ, prompt, khối wake word, nút "▶ Phiên âm" (ghi mic rồi phiên âm), dòng trạng thái và ô kết
  quả (tích lũy transcript theo giờ — hiện cả khi wake word tự kích hoạt rồi phiên âm). Có nút "⚙" +
  overlay settings (chỉ API key) ngay trong widget (host bỏ nút gear per-widget — xem
  references/plugin-runtime-api.md 1b).
- `settings.html` / `js/settings.js` / `css/settings.css` — cùng field `apiKey` (dùng bởi Plugin Manager
  + cửa sổ pop-out); lưu xong tự gọi `restartBackend`.
- `locales/en.json5` / `locales/vi.json5` — chuỗi UI, namespace `widget.*`/`settings.*`.
- `main/index.ts` (+ `main/index.js` đã compile) — supervisor tiến trình Python + IPC.
- `python/` — backend STT bundle từ `stt`: `backend.py`, `stt_core.py`, `config.py`,
  `download_wakewords.py`, `requirements.txt`, `wakewords/Hey_Nova.onnx` (1 model wake word custom
  duy nhất — builtin đã tắt trong `stt_core.WAKEWORD_CHOICES`).

## Default settings

- `keyRef` — default: `""` (tên key trong Settings của Kazuo AI > API Keys; có giá trị thì dùng key đó thay cho `apiKey`).
- `apiKey` — default: `""` (OpenRouter API key; các cấu hình khác lưu trong `python/config.json5` do
  backend tự quản qua WebSocket, chỉnh trực tiếp trên widget).

## Hạn chế đã biết

- Cổng WebSocket cố định `8765` — không chạy song song với app `stt` gốc (trùng cổng).
- Ghi mic/wake word cần thư viện âm thanh gốc (`sounddevice`, `openwakeword`, `onnxruntime`) — phải
  `pip install` trước; thiếu sẽ báo lỗi trên widget.

## Test

Mở Kazuo AI → Plugin Manager — plugin xuất hiện tự động (host tự quét `plugins-library/`) → Install →
Enable. Sau khi sửa `main/index.ts`, biên dịch lại: `npx tsc -p tsconfig.plugins.json` (chạy ở project root).
