Hướng dẫn kết nối
Mọi API đi qua một địa chỉ. Các ví dụ dưới đây đã điền sẵn địa chỉ bạn đang mở trang
này: mở bằng https thì ví dụ dùng https:// và wss://.
Địa chỉ
| HTTP | {{HTTP}} |
|---|---|
| WebSocket | {{WS}} |
Không cần khoá API. HTTP API trả header CORS nên trang web ở tên miền khác gọi thẳng được.
Danh sách API
| Đường dẫn | Việc | Trả về |
|---|---|---|
POST/api/tts | Đọc một đoạn chữ | File WAV |
POST/api/tts/normalize | Đổi số, ngày giờ thành chữ đọc được | JSON |
WS/ws/tts | Đọc theo từng câu, câu nào xong gửi ngay | JSON + WAV từng câu |
POST/api/stt | Chép lời một file âm thanh | JSON |
WS/ws/stt | Chép lời trực tiếp (Gipformer) | JSON |
WS/ws/stt/gipformer | Như trên, gọi rõ tên model | JSON |
WS/ws/stt/phowhisper | Chép lời trực tiếp (PhoWhisper) | JSON |
POST/api/agent/speak | Tiếng + lịch hình miệng cho mặt vẽ | NDJSON |
POST/api/agent/align | Lịch hình miệng cho file WAV có sẵn | JSON |
GET/api/agent-video/avatars | Danh sách avatar người thật | JSON |
GET/api/agent-video/avatar/{id} | Thông số một avatar | JSON |
GET/api/agent-video/avatar/{id}/frame/{i} | Khung nền thứ i | JPEG |
POST/api/agent-video/speak | Tiếng + mảnh miệng đã vẽ | NDJSON |
WS/ws/device | Thiết bị giọng nói: Opus vào, câu trả lời Opus ra | JSON + Opus |
POST/api/device/ota/ | Thiết bị hỏi địa chỉ WebSocket lúc khởi động | JSON |
POST/api/device/vision/explain | Camera của thiết bị gửi ảnh hỏi | JSON |
POST/api/vision | Gửi một ảnh kèm câu hỏi | JSON |
WS/ws/vision | Gửi ảnh liên tục / hỏi về ảnh | JSON |
GET/api/health/{service} | Service còn sống không | JSON |
Đọc chữ (TTS)
POST/api/tts
Thân request là JSON. Trả về file WAV (PCM 16 bit, mono, 48 kHz).
| Trường | Kiểu | Ý nghĩa |
|---|---|---|
input | string | Chữ cần đọc. Bắt buộc. |
voice | string | Tên giọng, mặc định maichi. Danh sách giọng ở GET /api/health/tts. |
Endpoint này đọc nguyên văn, không tự đổi số thành chữ. Có số, ngày giờ, chữ viết tắt thì gọi
/api/tts/normalize trước, hoặc dùng /ws/tts (tự làm bước này).
curl -X POST {{HTTP}}/api/tts \
-H "Content-Type: application/json" \
-d '{"input": "Xin chào, tôi có thể giúp gì cho bạn?", "voice": "maichi"}' \
-o out.wav
// JavaScript (trình duyệt)
const r = await fetch("{{HTTP}}/api/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: "Xin chào", voice: "maichi" }),
});
new Audio(URL.createObjectURL(await r.blob())).play();
POST/api/tts/normalize
curl -X POST {{HTTP}}/api/tts/normalize \
-H "Content-Type: application/json" \
-d '{"input": "Hẹn 9h30 ngày 15/10, nhiệt độ 26°C"}'
# {"text": "Hẹn chín giờ ba mươi phút ngày mười lăm tháng mười, nhiệt độ hai mươi sáu độ xê"}
TTS qua WebSocket
WS/ws/tts
Gửi chữ, nhận tiếng theo từng câu: câu đầu phát được ngay trong lúc câu sau đang đọc. Server tự đổi số, ngày giờ thành chữ rồi mới tách câu. Một kết nối gửi được nhiều yêu cầu, xử lý lần lượt.
| Chiều | Tin | Ý nghĩa |
|---|---|---|
| server → client | {"type":"ready","voices":[...],"default_voice":"maichi"} | Vừa kết nối xong |
| client → server | {"type":"speak","text":"...","voice":"maichi","id":"abc"} | Yêu cầu đọc. voice, id tuỳ chọn; id được gửi lại trong mọi tin trả lời |
| server → client | {"type":"start","id":"abc","normalized":"...","sentences":[...]} | Chữ sau khi chuẩn hoá và các câu sẽ đọc |
| server → client | {"type":"audio","id":"abc","i":0,"text":"...","bytes":52844,"ms":310} | Tin nhị phân ngay sau tin này là file WAV của câu i |
| server → client | {"type":"done","id":"abc","total_ms":1840} | Đọc xong yêu cầu |
| server → client | {"type":"error","id":"abc","error":"..."} | Lỗi; kết nối vẫn dùng tiếp được |
| client → server | {"type":"ping","t":123} | Kiểm tra kết nối; server trả {"type":"pong","t":123}, t gửi lại nguyên văn |
// JavaScript (trình duyệt): phát lần lượt từng câu
const ws = new WebSocket("{{WS}}/ws/tts");
ws.binaryType = "arraybuffer";
const ctx = new AudioContext();
let at = 0;
ws.onmessage = async (e) => {
if (typeof e.data === "string") {
const msg = JSON.parse(e.data);
if (msg.type === "ready") ws.send(JSON.stringify({ type: "speak", text: "Chào bạn. Hôm nay là 15/10." }));
return;
}
const buf = await ctx.decodeAudioData(e.data); // file WAV của một câu
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
at = Math.max(at, ctx.currentTime);
src.start(at);
at += buf.duration;
};
# Python: pip install websockets
import asyncio, json, websockets
async def main():
async with websockets.connect("{{WS}}/ws/tts", max_size=None) as ws:
await ws.recv() # ready
await ws.send(json.dumps({"type": "speak", "text": "Chào bạn. Hôm nay trời đẹp."}))
n = 0
while True:
msg = await ws.recv()
if isinstance(msg, bytes): # WAV của một câu
open(f"cau_{n}.wav", "wb").write(msg); n += 1
elif json.loads(msg)["type"] in ("done", "error"):
break
asyncio.run(main())
Chép lời theo file
POST/api/stt
Gửi multipart/form-data, trường file là file âm thanh (wav, mp3, m4a, webm,
ogg...). Trả về {"text": "..."}. Model PhoWhisper, tiếng Việt.
curl -X POST {{HTTP}}/api/stt -F "file=@ghi_am.wav"
# {"text": "hôm nay trời đẹp quá"}
# Python
import requests
r = requests.post("{{HTTP}}/api/stt", files={"file": open("ghi_am.wav", "rb")})
print(r.json()["text"])
// JavaScript (trình duyệt): blob lấy từ MediaRecorder hoặc <input type="file">
const fd = new FormData();
fd.append("file", blob, "ghi_am.webm");
const { text } = await (await fetch("{{HTTP}}/api/stt", { method: "POST", body: fd })).json();
Chép lời trực tiếp
WS/ws/stt, /ws/stt/gipformer, /ws/stt/phowhisper
Gửi tiếng liên tục, nhận chữ ngay khi đang nói. Ba đường dẫn cùng một giao thức;
/ws/stt là Gipformer.
| Model | Đặc điểm |
|---|---|
| Gipformer 1.5 | Chính xác hơn trên câu lệnh ngắn. Chữ IN HOA, không dấu câu. |
| PhoWhisper | Chữ thường, có dấu câu. Dễ sót vài chữ đầu câu. |
| Chiều | Tin | Ý nghĩa |
|---|---|---|
| server → client | {"type":"ready","sample_rate":16000} | Vừa kết nối xong |
| client → server | tin nhị phân | Âm thanh PCM 16 bit little-endian, mono, 16000 Hz. Gói dài ngắn tuỳ ý, nên khoảng 100 ms (3200 byte) |
| client → server | {"type":"eof"} | Hết tiếng: chốt nốt câu đang dở |
| client → server | {"type":"ping","t":123} | Kiểm tra kết nối; server trả {"type":"pong","t":123} |
| server → client | {"type":"speech","on":true} | Bắt đầu / thôi nói (on:false) |
| server → client | {"type":"partial","utt":3,"text":"..."} | Chữ đoán trong lúc nói. Thay dòng partial trước |
| server → client | {"type":"final","utt":3,"text":"...","audio_sec":2.1,"decode_sec":0.4} | Câu đã chốt. Nối vào kết quả |
| server → client | {"type":"dropped","utt":3} | Câu quá ngắn, coi là tiếng ồn |
| server → client | {"type":"eof_ack"} | Đã xử lý xong sau eof |
Server tự nhận ra lúc ngắt câu (khoảng 0,6 giây im lặng), không cần gửi gì để báo hết câu.
// JavaScript (trình duyệt): micro -> PCM 16 kHz -> WebSocket
const ws = new WebSocket("{{WS}}/ws/stt");
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "partial") console.log("…", msg.text);
if (msg.type === "final") console.log("✔", msg.text);
};
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext({ sampleRate: 16000 }); // trình duyệt tự đổi tần số mẫu
const src = ctx.createMediaStreamSource(stream);
const proc = ctx.createScriptProcessor(1024, 1, 1);
proc.onaudioprocess = (e) => {
if (ws.readyState !== WebSocket.OPEN) return;
const f32 = e.inputBuffer.getChannelData(0);
const i16 = new Int16Array(f32.length);
for (let i = 0; i < f32.length; i++) i16[i] = Math.max(-1, Math.min(1, f32[i])) * 0x7fff;
ws.send(i16.buffer);
};
src.connect(proc);
proc.connect(ctx.destination);
// Dừng: ws.send(JSON.stringify({ type: "eof" })), chờ "eof_ack" rồi ws.close()
localhost). Ứng dụng chạy trên máy chủ, điện thoại hay thiết bị nhúng thì không bị giới hạn này.# Python: gửi một file WAV như đang nói thật. pip install websockets numpy
import asyncio, json, wave
import numpy as np, websockets
async def main(path):
with wave.open(path) as w: # WAV PCM 16 bit, mono
sr = w.getframerate()
pcm = np.frombuffer(w.readframes(w.getnframes()), "<i2").astype(np.float32)
if sr != 16000: # đổi về 16 kHz
pcm = np.interp(np.linspace(0, len(pcm) - 1, int(len(pcm) * 16000 / sr)), np.arange(len(pcm)), pcm)
data = pcm.astype("<i2").tobytes()
async with websockets.connect("{{WS}}/ws/stt") as ws:
await ws.recv() # ready
async def send():
for i in range(0, len(data), 3200): # 100 ms mỗi gói
await ws.send(data[i:i + 3200])
await asyncio.sleep(0.1)
await ws.send(json.dumps({"type": "eof"}))
asyncio.create_task(send())
while (msg := json.loads(await ws.recv()))["type"] != "eof_ack":
if msg["type"] == "final":
print(msg["text"])
asyncio.run(main("ghi_am.wav"))
Agent: mặt vẽ
POST/api/agent/speak
Thân request {"text": "...", "voice": "maichi"}. Trả về NDJSON: mỗi dòng một JSON,
gửi dần theo từng câu để phát được câu đầu ngay. Tối đa 2000 ký tự.
type | Trường |
|---|---|
meta | normalized chữ đã chuẩn hoá, sentences các câu |
chunk | i, text; audio file WAV mã hoá base64;
duration giây; cues lịch hình miệng; syllables thời điểm từng âm tiết
{t, d, text}; env độ to 50 lần/giây (0–1) |
error | i, error |
done | total_ms |
Mỗi phần tử cues là {"t": giây bắt đầu, "d": độ dài, "v": hình miệng, "k": loại âm}.
v theo bộ viseme của Oculus/Meta (sil PP FF DD nn SS CH kk RR aa E I O U) cộng
UH (ư) và ER (ơ, â), nên gắn thẳng được vào avatar 3D có blendshape cùng tên.
k: C phụ âm đầu, G âm lướt, V nguyên âm, K phụ âm cuối.
// JavaScript: đọc NDJSON theo dòng
const r = await fetch("{{HTTP}}/api/agent/speak", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "Chào bạn. Tôi là trợ lý ảo." }),
});
const reader = r.body.pipeThrough(new TextDecoderStream()).getReader();
let buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += value;
let nl;
while ((nl = buf.indexOf("\n")) >= 0) {
const msg = JSON.parse(buf.slice(0, nl));
buf = buf.slice(nl + 1);
if (msg.type === "chunk") {
// msg.audio: WAV base64. msg.cues: đổi hình miệng tại msg.cues[k].t giây kể từ lúc câu bắt đầu phát
}
}
}
POST/api/agent/align?text=...
Lịch hình miệng cho tiếng có sẵn (giọng thu sẵn, TTS khác). Thân request là file WAV PCM 16 bit,
text là chữ đã đọc, viết ở dạng đọc ra (không còn số). Trả về
{duration, cues, syllables, env} như trên.
curl -X POST "{{HTTP}}/api/agent/align?text=xin%20ch%C3%A0o" \
-H "Content-Type: audio/wav" --data-binary @xin_chao.wav
Agent: mặt người thật
Video gốc của avatar được tải một lần (các khung nền JPEG). Khi nói, server chỉ vẽ lại vùng miệng và gửi các mảnh nhỏ; trình phát dán mảnh lên khung nền. Chạy CPU thì vẽ được khoảng 5 khung/giây, cần GPU để đủ 25 khung/giây.
GET/api/agent-video/avatars
curl {{HTTP}}/api/agent-video/avatars
# {"default": "vn_ao_xanh", "avatars": [{"id": "vn_ao_xanh", "name": "...", "credit": "...", "source": "..."}]}
GET/api/agent-video/avatar/{id}
Trả về fps (25), frames số khung nền, width, height,
rev, và boxes: với mỗi khung nền, vị trí [x, y, w, h] của vùng miệng.
GET/api/agent-video/avatar/{id}/frame/{i}?v={rev}
Khung nền thứ i (JPEG), cache được lâu dài; đổi rev khi avatar được dựng lại.
POST/api/agent-video/speak
Thân request {"text": "...", "voice": "maichi", "avatar": "vn_ao_xanh", "pos": 0}.
pos là số khung trình phát đang hiện lúc gửi (bộ đếm tăng 25 lần/giây, không quay vòng).
Trả về NDJSON:
type | Trường |
|---|---|
meta | normalized, sentences |
audio | i, text, audio (WAV base64), duration;
start vị trí khung mà tiếng phải bắt đầu vang lên; head, pad số khung
chuyển tiếp trước và sau; frames tổng số khung của câu |
frames | i; at chỉ số mảnh đầu tiên trong lô; jpegs các mảnh miệng (JPEG base64) |
rendered | i, fps tốc độ vẽ thực tế |
error / done | như agent mặt vẽ |
Ghép hình: câu bắt đầu ở khung first = start - head. Tại vị trí pos (trong
khoảng first đến first + frames), mảnh pos - first được dán lên khung
nền n = mirror(pos) tại toạ độ boxes[n], trong đó mirror chạy khung
nền tới rồi lui: 0, 1, …, N-1, N-1, …, 0, 0, 1, …. Cách làm đầy đủ xem mã trang
/ui/agent-video/.
Giữ kết nối và đo độ trễ
Cả ba WebSocket (/ws/tts, /ws/stt...) nhận {"type":"ping","t":...} và
trả {"type":"pong","t":...} với đúng giá trị t đã gửi; dùng để đo độ trễ hoặc phát
hiện mất kết nối từ trình duyệt. Ping frame chuẩn của WebSocket cũng được trả lời, và server tự gửi ping
mỗi 20 giây nên kết nối im lặng không bị đóng.
const ws = new WebSocket("{{WS}}/ws/stt");
ws.onopen = () => setInterval(() => ws.send(JSON.stringify({ type: "ping", t: Date.now() })), 10000);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "pong") console.log("trễ", Date.now() - msg.t, "ms");
};
Thiết bị giọng nói
WS/ws/device
Cho phần cứng (loa, mạch ESP32...): thiết bị gửi tiếng nói nén Opus, server chép lời, hỏi mô hình ngôn ngữ
và gửi câu trả lời về bằng Opus, câu nào xong đọc ngay câu đó. Thiết bị là client, mở kết nối tới
{{WS}}/ws/device. Prompt và các câu trả lời cố định chỉnh ở trang
Thiết bị giọng nói.
| Header | Giá trị |
|---|---|
Protocol-Version | 1, 2 hoặc 3: cách đóng gói khung nhị phân (bảng dưới) |
Device-Id | Địa chỉ MAC. Dùng để nhớ hội thoại, gắn prompt riêng và ảnh camera cho từng thiết bị |
Client-Id | UUID của thiết bị |
Authorization | Bearer <token>; chỉ kiểm tra khi server đặt DEVICE_TOKEN, sai thì đóng với mã 4401 |
| Chiều | Tin | Ý nghĩa |
|---|---|---|
| client → server | {"type":"hello","version":1,"features":{"mcp":true},"transport":"websocket","audio_params":{"format":"opus","sample_rate":16000,"channels":1,"frame_duration":60}} | Mở đầu phiên. features.mcp: thiết bị có công cụ MCP (xem dưới) |
| server → client | {"type":"hello","transport":"websocket","session_id":"...","audio_params":{"format":"opus","sample_rate":24000,"channels":1,"frame_duration":60}} | Trả trong vòng 10 giây. audio_params là định dạng tiếng server gửi về |
| client → server | {"type":"listen","state":"start","mode":"auto"} | Bắt đầu thu, sau đó gửi các khung Opus. auto: server tự nhận ra hết câu; manual: thiết bị gửi stop; realtime: nghe liên tục kể cả lúc đang đọc, nói chen vào là ngắt câu đang đọc (thiết bị cần khử tiếng vọng) |
| client → server | tin nhị phân | Một khung Opus (16 kHz, mono, 60 ms) |
| client → server | {"type":"listen","state":"stop"} | Hết câu (mode manual) |
| client → server | {"type":"listen","state":"detect","text":"..."} | Thiết bị vừa nghe thấy từ đánh thức |
| client → server | {"type":"abort","reason":"..."} | Ngắt câu trả lời đang đọc |
| client → server | {"type":"iot","update":true,"descriptors":[...]} / {"states":[...]} | Kiểu khai báo thiết bị cũ (trước MCP): các thứ điều khiển được và trạng thái của chúng |
| hai chiều | {"type":"mcp","payload":{...JSON-RPC 2.0...}} | Công cụ của thiết bị, xem dưới |
| server → client | {"type":"stt","text":"..."} | Câu đã nghe được |
| server → client | {"type":"llm","text":"🙂","emotion":"happy"} | Cảm xúc của câu trả lời, để thiết bị vẽ mặt: neutral happy laughing funny sad angry crying loving embarrassed surprised shocked thinking winking cool relaxed delicious kissy confident sleepy silly confused |
| server → client | {"type":"tts","state":"start"} | Bắt đầu đọc; thiết bị thôi thu âm (trừ mode realtime) |
| server → client | {"type":"tts","state":"sentence_start","text":"..."} | Câu sắp phát, theo sau là các khung Opus của câu đó (24 kHz, mono, 60 ms) |
| server → client | {"type":"tts","state":"stop"} | Đọc xong (gửi khi tiếng đã phát hết); thiết bị quay lại nghe hoặc nghỉ |
| server → client | {"type":"iot","commands":[{"name":"Lamp","method":"TurnOn","parameters":{}}]} | Lệnh cho thiết bị khai báo kiểu cũ |
Protocol-Version | Khung nhị phân (big-endian) |
|---|---|
1 | Chỉ có gói Opus |
2 | Header 16 byte: version u16, type u16 (0 = âm thanh), reserved u32, timestamp u32 (ms), payload_size u32; rồi gói Opus |
3 | Header 4 byte: type u8 (0 = âm thanh), reserved u8, payload_size u16; rồi gói Opus |
Thử khi chưa có phần cứng: tools/fake_device.py trong thư mục device-gateway gửi một file
WAV như đang nói vào micro, giả một công cụ đèn qua MCP và lưu câu trả lời ra file WAV.
pip install websockets numpy opuslib # opuslib cần libopus (apt install libopus0 / brew install opus)
python tools/fake_device.py {{WS}}/ws/device cau_hoi.wav --out tra_loi.wav --version 1 --mode auto
Công cụ của thiết bị (MCP)
Thiết bị gửi features.mcp: true trong hello thì ngay sau đó server hỏi danh sách công cụ. Người dùng
ra lệnh ("bật đèn phòng khách") thì mô hình tự chọn công cụ, server gọi xuống thiết bị, đọc kết quả rồi trả lời.
| Chiều | payload |
|---|---|
| server → client | {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"vision":{"url":"{{HTTP}}/api/device/vision/explain","token":"..."}}}}Địa chỉ camera gửi ảnh lên hỏi (xem Ảnh từ camera) |
| server → client | {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"cursor":""}} |
| client → server | {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"self.light.set_state","description":"...","inputSchema":{...}}],"nextCursor":""}} |
| server → client | {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"self.light.set_state","arguments":{"on":true}}} |
| client → server | {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"true"}],"isError":false}} |
POST/api/device/ota/
Thiết bị gọi lúc khởi động để lấy địa chỉ WebSocket, token và giờ server. Đặt địa chỉ này làm OTA URL của
firmware là thiết bị tự tìm ra /ws/device. Server không cập nhật firmware: luôn trả lại đúng phiên bản thiết bị đang chạy.
curl -X POST {{HTTP}}/api/device/ota/ -H "Content-Type: application/json" -d '{"application":{"version":"1.0.0"}}'
# {"server_time":{"timestamp":1760000000000,"timezone_offset":420},
# "firmware":{"version":"1.0.0","url":""},
# "websocket":{"url":"{{WS}}/ws/device","token":"..."}}
Ảnh từ camera
Ba đường vào, cùng một mô hình đọc ảnh. Ảnh gửi kèm Device-Id được giữ làm "mắt" của thiết bị đó
trong 20 giây: hỏi bằng giọng nói lúc ấy ("cái này là gì?") thì mô hình nhìn được ảnh.
POST/api/device/vision/explain · /api/vision
Multipart: question (chữ) và file (ảnh JPEG/PNG, tối đa 8 MB). Header tuỳ chọn:
Device-Id, Authorization: Bearer <token> (khi server đặt DEVICE_TOKEN).
Camera của thiết bị dùng đường đầu (server báo qua MCP initialize); đường sau cho ứng dụng khác.
curl -F question="Trong ảnh có gì?" -F file=@anh.jpg {{HTTP}}/api/vision
# {"success": true, "action": "RESPONSE", "response": "Một chiếc cốc màu trắng trên bàn gỗ."}
WS/ws/vision
| Chiều | Tin | Ý nghĩa |
|---|---|---|
| server → client | {"type":"ready","model":"...","max_bytes":8388608} | Ngay khi nối |
| client → server | {"type":"image","id":1,"question":"Có ai không?","device_id":"aa:bb:..."} rồi một tin nhị phân là ảnh | Hỏi về ảnh. Có thể gửi ảnh dạng base64 trong trường image thay cho tin nhị phân |
| server → client | {"type":"answer","id":1,"text":"...","ms":1800} | Câu trả lời |
| client → server | {"type":"image","device_id":"..."} + ảnh, hoặc chỉ tin nhị phân | Không có question: chỉ cập nhật "mắt" của thiết bị, server trả {"type":"stored"} |
| client → server | {"type":"ping","t":...} | Server trả {"type":"pong","t":...} |
// Trình duyệt / Node: hỏi về một ảnh
const ws = new WebSocket("{{WS}}/ws/vision");
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.onopen = async () => {
ws.send(JSON.stringify({ type: "image", id: 1, question: "Trong ảnh có gì?" }));
ws.send(await (await fetch("anh.jpg")).arrayBuffer());
};
Mô hình và prompt
Câu hỏi đi qua theo thứ tự: trường hợp đặc biệt (khớp câu thì trả lời đúng câu đã viết) → mô hình ngôn ngữ với prompt đang dùng, lịch sử vài lượt gần nhất, giờ hiện tại, trạng thái và công cụ của thiết bị → nếu mô hình lỗi hoặc chưa cấu hình thì vài luật cứng (chào, giờ, ngày...). Sửa ở trang Thiết bị giọng nói: thêm prompt, chọn prompt riêng cho từng thiết bị, thêm câu trả lời cố định, thử hỏi bằng chữ, cho thiết bị đọc một câu.
Mặc định mô hình chạy ngay trên server (Ollama, qwen3-vl:4b-instruct, đọc được cả ảnh). Đổi sang
mô hình khác kiểu OpenAI API bằng biến LLM_BASE_URL, LLM_MODEL, LLM_API_KEY.
Kiểm tra sống
GET/api/health/{service}
service: tts, stt, stt-gipformer, stt-phowhisper,
agent, agent-video, lab, device. Service chạy thì trả 200 kèm thông tin model;
không chạy thì 502.
curl {{HTTP}}/api/health/tts
# {"status": "ok", "voices": ["maichi", ...]}
Giới hạn và lưu ý
- Không có xác thực: ai vào được địa chỉ này đều gọi được API. Publish ra Internet thì nên đặt sau lớp
đăng nhập hoặc giới hạn IP. Riêng thiết bị và trang quản lý prompt có khoá riêng:
DEVICE_TOKEN(thiết bị gửi trongAuthorization) vàADMIN_TOKEN(nhập trên trang để lưu thay đổi). - Chữ gửi đọc tối đa 2000 ký tự mỗi yêu cầu; file gửi lên tối đa 100 MB; một yêu cầu tối đa 10 phút.
- Model đọc xử lý từng yêu cầu một; nhiều yêu cầu cùng lúc sẽ xếp hàng chờ.
- Tiếng gửi vào chép lời trực tiếp phải đúng PCM 16 bit, mono, 16000 Hz; sai định dạng sẽ ra chữ vô nghĩa chứ không báo lỗi.