11-voice-assistant

Example of the component nguyenduchoai/innoedge v0.1.4
<a id="top"></a>
# 11 — Voice Assistant: Trợ lý giọng nói thông minh (Xiaozhi Voice Assistant)

> 🇻🇳 **Tài liệu Tiếng Việt** (toàn bộ nội dung bên dưới) | [🇬🇧 English Documentation](#english)

Kiểu Xiaozhi, trên InnoEdge core. Giữ nút để nói, thả ra máy trả lời.

```
   giữ BOOT                                   thả BOOT
      │                                          │
      ▼  listen start                            ▼  listen stop
   ESP32 ──PCM16 16 kHz, 20 ms/khung──▶  mock-cloud -ai -voice
                                              │ ASR (Whisper)   → "bật đèn"
                                              │ Claude          → led(on) → máy → ack
                                              │ TTS             → PCM
   loa   ◀──tts start · PCM đúng nhịp · tts stop──┘
   LED sáng khi máy đang nói
```

## Phần cứng — bộ rẻ nhất mua được (~120.000đ)

| Linh kiện | Chân ESP32-S3 | Ghi chú |
|---|---|---|
| **INMP441** (mic I2S) SCK | GPIO4 | |
| INMP441 WS | GPIO5 | |
| INMP441 SD | GPIO6 | |
| INMP441 L/R | GND | kênh trái |
| **MAX98357A** (amp I2S) BCLK | GPIO15 | |
| MAX98357A LRC | GPIO16 | |
| MAX98357A DIN | GPIO7 | |
| Loa 4Ω 3W | MAX98357A +/− | |
| Cả hai VDD | 3V3 | |

Đổi chân trong `sdkconfig.defaults` (rồi `idf.py fullclean`). Chân `-1` = tắt
kênh — không có mic vẫn build và boot được.

> Bo có codec chip (ES8311, ES8388 — như ESP32-S3-BOX hay VIMATE) **không** dùng
> driver này: chúng cần I2C cấu hình codec. Đó là driver riêng theo bo.

## Chạy

Mặc định dùng **Qwen3-ASR** (nghe) và **VieNeu TTS** (nói) — hai provider tiếng
Việt đang chạy thật trên VIMATE Edu, **cài trên server local** của bạn. Claude là
bộ não.

```bash
# Terminal 1 — voice stack (một lần; lần đầu tải model, xem tools/voice-stack)
cd tools/voice-stack && docker compose --profile gpu up -d      # Qwen3 cần NVIDIA
#   không GPU:  docker compose up -d vieneu-tts   rồi dùng -asr dashscope hoặc -asr whisper

# Terminal 2 — cloud giả có AI + giọng nói (mặc định đã trỏ localhost:8000/8080)
export ANTHROPIC_API_KEY=sk-ant-...
go run ./tools/mock-cloud -ai -voice

# Terminal 3 — thiết bị
cd examples/11-voice-assistant
idf.py set-target esp32s3 && idf.py menuconfig   # cloud base URL → http://<IP mock>:8080
idf.py flash monitor
```

Giữ BOOT, nói *"bật đèn"*, thả ra.

Voice stack chạy trên máy khác (server có GPU): `-asr-url http://<ip>:8000/v1
-tts-url http://<ip>:8080/v1`. Chi tiết, yêu cầu phần cứng, bật auth:
[`tools/voice-stack/README.md`](../../tools/voice-stack/README.md).

### Hợp đồng hai provider (lấy từ Edu, không đoán)

| | Endpoint | Điểm dễ sai |
|---|---|---|
| Qwen3-ASR | `POST /v1/chat/completions`, audio là content-part `audio_url` (vLLM) / `input_audio` (DashScope) | **không** phải `/audio/transcriptions`; kết quả có thể bọc `<asr_text>` hoặc lọt chữ Hán — mock dọn như VIMATE |
| VieNeu TTS | `POST /v1/audio/speech` `{model, input, voice, style, response_format:"pcm", sample_rate}` | nhận `sample_rate` → mock **xin thẳng 16 kHz**, không resample; kiểm header `X-Audio-Sample-Rate`, sai thì từ chối |

## Kết quả mong đợi

Serial monitor:
```
I voice: sẵn sàng — chạy `mock-cloud -ai -voice`, GIỮ nút BOOT để nói
I ie.audio: listen start
I ie.audio: listen stop
I voice: 🎤 nghe được: "bật đèn"
I voice: 🔊 máy đang nói…
I voice: 🔇 xong
```
mock-cloud:
```
  🎤 AABB… bắt đầu nói
  🎤 AABB… nói xong: 1.4s
  🎤 AABB…: "bật đèn"
  ← {"type":"stt","text":"bật đèn"}
🤖 Đã bật đèn cho bạn.
  🔊 → AABB…: 1.2s "Đã bật đèn cho bạn."
```
Persona mặc định là `device` (example 09) nên nếu máy cũng chạy handler `led`
thì đèn sáng thật — ở example này chỉ có LED báo "đang nói"; ghép với 09 nếu
muốn AI điều khiển thật qua giọng.

## Không có GPU / không muốn tự host

| Muốn | Lệnh |
|---|---|
| Qwen3 cloud (DashScope, Alibaba) | `export DASHSCOPE_API_KEY=...` · `-asr dashscope` |
| Whisper (OpenAI, hoặc faster-whisper-server local CPU) | `-asr whisper -asr-key ... [-asr-url http://...]` |
| TTS OpenAI thay VieNeu | `-tts openai -tts-key ...` (24 kHz, mock tự resample) |

VieNeu CPU là đủ và không có bản cloud — laptop thường vẫn chạy được TTS.

## SDK core thêm gì để có giọng nói

Đúng **hai thứ**, tổng ~40 dòng:

| | |
|---|---|
| `innoedge_send_binary()` | gửi khung binary (opcode 0x02) |
| `innoedge_events_t.on_binary` / `.on_frame` | nhận binary, và frame text SDK không biết (`tts`, `stt`) |

Toàn bộ phần còn lại — I2S, đệm phát, push-to-talk, giao thức listen/tts — nằm
trong `components-hw/innoedge_audio` (250 dòng, driver mẫu). Đây là cách nền
tảng "đa năng" giữ lõi nhỏ: tính năng mới là component cắm vào, không phải SDK
phình ra. Thanh toán (06), AI agent (09), gia sư (10), giọng nói (11) — cùng
một SDK core, cùng một kênh WebSocket.

## Ba quyết định cố ý

**PCM thô, không Opus.** 16 kHz × 16 bit = 32 KB/s. Trên WiFi LAN đó là chuyện
nhỏ. Opus tốn CPU và thêm dependency; thêm khi thiết bị đi qua internet 4G.

**Push-to-talk, không wake word.** Wake word (ESP-SR WakeNet) là 200 KB model +
AFE + cấu hình theo mic. Nút bấm dạy được cùng kiến trúc mà không cần tuần
hiệu chỉnh. VIMATE có wake word — đó là lớp sản phẩm.

**Cloud gửi PCM đúng nhịp thật** (20 ms mỗi 20 ms). Đệm phát trên máy chỉ 1,5 s;
bắn cả clip một lúc là tràn và rơi tiếng. Test `TestVoiceVongTronDayDu` chốt
điều này.

## Chưa kiểm chứng trên phần cứng

Build OK, test phía server 4/4 với ASR/TTS/thiết bị giả. **Chưa cắm mic thật.**
Ba tham số gần như chắc chắn phải chỉnh khi có bo:

| Tham số | Vì sao |
|---|---|
| `IE_AUDIO_MIC_GAIN_SHIFT` (4) | INMP441 biên độ nhỏ; quá nhỏ → ASR không nghe, quá lớn → méo |
| `slot_mask` trái/phải | tuỳ chân L/R của mic nối GND hay 3V3 |
| Chiều dịch bit 24→16 | nếu tiếng rè toàn bộ, thử `>> 14` thay vì `<< 4 >> 16` |

Đây là loại việc không mô phỏng được — cần bo thật, mic thật, tai thật.

## Troubleshooting

| Triệu chứng | Nguyên nhân |
|---|---|
| `Qwen3-ASR ... connection refused :8000` | Voice stack chưa lên, hoặc chưa `--profile gpu`; xem `docker compose ps` |
| `VieNeu: ... sidecar có chạy ở localhost:8080 không?` | `cd tools/voice-stack && docker compose up -d vieneu-tts`, chờ `health/ready` |
| `VieNeu HTTP 401` | Sidecar bật auth: `export VIENEU_TTS_API_KEY=` đúng key |
| `VieNeu trả 24000 Hz, đã xin 16000` | Sidecar bản cũ không tôn trọng `sample_rate` — cập nhật sidecar |
| Giữ BOOT không thấy `listen start` | Máy chưa online, hoặc mic `-1` |
| `clip quá ngắn, bỏ` | Bấm dưới 200 ms |
| ASR trả chữ vô nghĩa | Gain sai, hoặc L/R sai kênh → mic thu toàn 0 |
| Tiếng đứt quãng | Mạng chậm hơn 32 KB/s; hoặc `đệm phát đầy` trong log → cloud gửi nhanh hơn nhịp |
| Không có tiếng | Amp DIN sai chân; MAX98357A cần SD nối 3V3 để bật |

---

<a id="english"></a>
## 🇬🇧 English Documentation

> [🇻🇳 Quay lại Tiếng Việt](#top)

A Xiaozhi-style voice assistant built on the InnoEdge core. Hold the button to speak; release to hear the spoken response.

```
   Hold BOOT                                  Release BOOT
      │                                            │
      ▼  listen start                              ▼  listen stop
   ESP32 ──PCM16 16 kHz, 20 ms/frame──▶  mock-cloud -ai -voice
                                               │ ASR (Qwen3/Whisper) → "turn on the light"
                                               │ Claude              → led(on) → Device → ACK
                                               │ TTS (VieNeu)        → PCM audio stream
   Speaker ◀──tts start · streamed PCM frames · tts stop──┘
   LED lights up while the machine is speaking
```

## Hardware Setup (~$5 total)

| Component | ESP32-S3 Pin | Notes |
|---|---|---|
| **INMP441** (I2S Microphone) SCK | GPIO4 | I2S Clock |
| INMP441 WS | GPIO5 | Word Select |
| INMP441 SD | GPIO6 | Serial Data |
| INMP441 L/R | GND | Left channel |
| **MAX98357A** (I2S Amplifier) BCLK | GPIO15 | Bit Clock |
| MAX98357A LRC | GPIO16 | Left/Right Clock |
| MAX98357A DIN | GPIO7 | Data In |
| 4Ω 3W Speaker | MAX98357A +/− | Terminal |
| VDD (both modules) | 3V3 | Regulated 3.3V |

Configure GPIO pins in `sdkconfig.defaults`. Setting pins to `-1` disables the channel, allowing the project to build and run without audio hardware.

## How to Run

### 1. Launch Voice Pipeline Stack (Terminal 1)
```bash
# In tools/voice-stack (Docker Compose with GPU ASR and CPU TTS)
cd tools/voice-stack && docker compose up -d
```

### 2. Start AI Voice Mock-Cloud (Terminal 2)
```bash
export ANTHROPIC_API_KEY=sk-ant-...
go run ./tools/mock-cloud -ai -voice
```

### 3. Flash Firmware (Terminal 3)
```bash
cd examples/11-voice-assistant
idf.py set-target esp32s3 && idf.py menuconfig
idf.py flash monitor
```

Hold the BOOT button, say *"turn on the light"*, and release. The LED turns on and the speaker announces the confirmation!

To create a project from this example, run:

idf.py create-project-from-example "nguyenduchoai/innoedge=0.1.4:11-voice-assistant"

or download archive (~11.04 KB)