Tại sao tôi bỏ cloud voice API sau sáu tháng sử dụng
Sáu tháng trước, mọi câu hỏi bằng giọng nói của tôi đều phải đi qua cloud API — Google Speech-to-Text để chuyển giọng nói thành văn bản, OpenAI API để lấy câu trả lời. Chi phí lúc đầu còn chấp nhận được. Nhưng độ trễ đã phá hỏng trải nghiệm. Mỗi lần tương tác mất thêm 1,5–2 giây chỉ vì overhead mạng, trước khi model kịp bắt đầu xử lý.
Rồi còn vấn đề bảo mật nữa. Tôi làm việc với tài liệu nội bộ và đôi khi đặt câu hỏi về các hệ thống không được phép rời khỏi mạng nội bộ. Việc gửi audio đó lên dịch vụ bên thứ ba cảm giác rất không ổn — và thực ra là không ổn thật.
Whisper (model nhận dạng giọng nói mã nguồn mở của OpenAI) kết hợp với Ollama (công cụ chạy LLM cục bộ) đã giải quyết cả hai vấn đề cùng lúc. Sáu tháng chạy stack này trên môi trường thực tế: không giới hạn quota, không tốn tiền API, và thời gian phản hồi cạnh tranh được với các giải pháp cloud trên phần cứng tầm trung.
Đây là cách tôi thiết lập từng bước.
Những gì bạn thực sự cần
Hãy nói thẳng về yêu cầu phần cứng ngay từ đầu. Model medium của Whisper chạy ổn trên CPU, nhưng các model lớn hơn sẽ được hưởng lợi đáng kể từ GPU. Với Ollama, model 7B tham số như Mistral hoặc LLaMA 3 cần ít nhất 8GB RAM — 16GB sẽ thoải mái hơn.
Cấu hình của tôi: Ubuntu 22.04, 32GB RAM, NVIDIA RTX 3060 với 12GB VRAM. Dùng macOS với Apple Silicon? Ollama hỗ trợ Metal rất tốt — hiệu năng thực sự ấn tượng trên M2 hoặc M3.
Các gói cần thiết:
- Python 3.10+
- ffmpeg (xử lý audio)
- portaudio (đầu vào microphone)
- Ollama
Cài đặt
Bước 1: Cài đặt Ollama
Ollama cung cấp một lệnh duy nhất xử lý mọi thứ, kể cả service systemd:
curl -fsSL https://ollama.com/install.sh | sh
Tải model bạn muốn dùng. Với phần cứng phổ thông, mistral là điểm cân bằng tốt nhất giữa tốc độ và khả năng. Nếu máy bạn có ít hơn 8GB VRAM, phi3 nhẹ hơn đáng kể:
ollama pull mistral
# Hoặc dùng model nhẹ hơn:
ollama pull phi3
Kiểm tra xem nó có hoạt động thực sự không trước khi tiếp tục:
ollama list
# Sẽ hiển thị các model đã tải về
curl http://localhost:11434/api/generate -d '{
"model": "mistral",
"prompt": "Hello, are you working?",
"stream": false
}'
Bước 2: Thiết lập môi trường Python
python3 -m venv voice-assistant-env
source voice-assistant-env/bin/activate
# Cài đặt các dependency hệ thống trước
sudo apt install -y ffmpeg portaudio19-dev python3-dev
# Cài đặt các gói Python
pip install openai-whisper pyaudio requests numpy
Trên macOS:
brew install ffmpeg portaudio
pip install openai-whisper pyaudio requests numpy
Bước 3: Tải model Whisper
Whisper tự động tải model khi dùng lần đầu. Tải trước sẽ tránh phải chờ trong lần chạy thực tế đầu tiên:
import whisper
# Lệnh này tải về và cache model (~1.5GB cho 'medium')
model = whisper.load_model("medium")
print("Tải model thành công")
Các kích thước model có sẵn: tiny, base, small, medium, large. Cho hội thoại thông thường, small hoặc medium là lựa chọn thực tế nhất — large chỉ cần thiết khi độ chính xác là yêu cầu bắt buộc.
Cấu hình
Script trợ lý giọng nói cốt lõi
Dưới đây là script hoàn chỉnh tôi đang chạy trên môi trường thực tế. Nó ghi âm, chuyển giọng nói thành văn bản bằng Whisper, gửi văn bản đó tới Ollama, và in ra câu trả lời:
import whisper
import pyaudio
import wave
import requests
import json
import os
import tempfile
import numpy as np
# ── Cấu hình ──────────────────────────────────────────────
WHISPER_MODEL = "medium" # Đổi sang "small" để phản hồi nhanh hơn
OLLAMA_MODEL = "mistral" # Phải khớp với model đã tải bằng ollama
OLLAMA_URL = "http://localhost:11434/api/generate"
# Cài đặt ghi âm
SAMPLE_RATE = 16000
CHUNK_SIZE = 1024
CHANNELS = 1
FORMAT = pyaudio.paInt16
RECORD_SECONDS = 5 # Điều chỉnh theo độ dài câu hỏi thông thường của bạn
SILENCE_THRESHOLD = 500 # Điều chỉnh theo độ nhạy microphone của bạn
# ── Tải model Whisper một lần khi khởi động ─────────────────────────
print(f"Đang tải model Whisper: {WHISPER_MODEL}")
whisper_model = whisper.load_model(WHISPER_MODEL)
print("Sẵn sàng. Nhấn Enter để bắt đầu ghi âm.")
def record_audio() -> str:
"""Ghi âm từ microphone và lưu vào file tạm."""
audio = pyaudio.PyAudio()
stream = audio.open(
format=FORMAT,
channels=CHANNELS,
rate=SAMPLE_RATE,
input=True,
frames_per_buffer=CHUNK_SIZE
)
print("Đang ghi âm... (hãy nói)")
frames = []
for _ in range(0, int(SAMPLE_RATE / CHUNK_SIZE * RECORD_SECONDS)):
data = stream.read(CHUNK_SIZE)
frames.append(data)
print("Ghi âm hoàn tất.")
stream.stop_stream()
stream.close()
audio.terminate()
# Lưu vào file WAV tạm
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
with wave.open(tmp.name, 'wb') as wf:
wf.setnchannels(CHANNELS)
wf.setsampwidth(audio.get_sample_size(FORMAT))
wf.setframerate(SAMPLE_RATE)
wf.writeframes(b''.join(frames))
return tmp.name
def transcribe(audio_path: str) -> str:
"""Chuyển file audio thành văn bản bằng Whisper."""
result = whisper_model.transcribe(audio_path, language="en")
os.unlink(audio_path) # Xóa file tạm
return result["text"].strip()
def ask_ollama(prompt: str) -> str:
"""Gửi prompt tới Ollama cục bộ và trả về câu trả lời."""
payload = {
"model": OLLAMA_MODEL,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.7,
"num_predict": 256 # Giữ câu trả lời ngắn gọn cho giọng nói
}
}
response = requests.post(OLLAMA_URL, json=payload, timeout=60)
response.raise_for_status()
return response.json()["response"].strip()
def main():
while True:
input("\nNhấn Enter để nói (Ctrl+C để thoát)...")
audio_path = record_audio()
print("Đang chuyển giọng nói thành văn bản...")
text = transcribe(audio_path)
if not text:
print("Không phát hiện giọng nói. Thử lại.")
continue
print(f"Bạn nói: {text}")
print("Đang xử lý...")
response = ask_ollama(text)
print(f"\nTrợ lý: {response}\n")
if __name__ == "__main__":
main()
Tinh chỉnh theo phần cứng của bạn
Ba cài đặt tạo ra sự khác biệt thực sự trong thực tế:
- Cài đặt ngôn ngữ Whisper: Luôn chỉ định ngôn ngữ khi bạn biết trước (
language="en"). Tự động phát hiện ngôn ngữ làm tăng độ trễ và đôi khi chọn sai ngôn ngữ — tôi đã từng thấy nó chuyển tiếng Anh thành tiếng Pháp khi audio có tiếng nhạc nền. - num_predict: Giới hạn output của Ollama ở 256 token giúp câu trả lời giọng nói ngắn gọn và dễ nghe. Một bài văn 2000 token đọc trên màn hình đã khó chịu; đọc to lên thì không thể chịu được.
- Số giây ghi âm: 5 giây xử lý được hầu hết các lệnh ngắn. Tăng lên 8–10 giây cho các cuộc hội thoại qua lại.
Thêm system prompt để hành vi nhất quán
Một persona hệ thống giúp Ollama đưa ra câu trả lời ngắn gọn, phù hợp với giọng nói. Nếu không có nó, câu trả lời thường dài dòng và chứa markdown trông rất xấu khi hiển thị trên terminal:
SYSTEM_PROMPT = (
"Bạn là trợ lý giọng nói súc tích. Giữ tất cả câu trả lời dưới 3 câu. "
"Không dùng định dạng markdown. Trả lời như thể đang nói trực tiếp."
)
def ask_ollama(user_input: str) -> str:
full_prompt = f"{SYSTEM_PROMPT}\n\nUser: {user_input}\nAssistant:"
payload = {
"model": OLLAMA_MODEL,
"prompt": full_prompt,
"stream": False,
"options": {"temperature": 0.7, "num_predict": 256}
}
response = requests.post(OLLAMA_URL, json=payload, timeout=60)
return response.json()["response"].strip()
Kiểm tra và theo dõi
Kiểm tra từng thành phần độc lập
Hãy kiểm tra từng phần riêng lẻ trước khi chạy toàn bộ pipeline. Làm vậy ngay từ đầu giúp tôi tiết kiệm vài tiếng debug khi lần đầu thiết lập — truy tìm lỗi Whisper trong log của Ollama thực sự rất mệt.
Kiểm tra Whisper riêng lẻ:
# Ghi âm một đoạn 5 giây
python3 -c "
import sounddevice as sd
import scipy.io.wavfile as wav
import numpy as np
fs = 16000
duration = 5
print('Đang ghi âm...')
audio = sd.rec(int(duration * fs), samplerate=fs, channels=1, dtype='int16')
sd.wait()
wav.write('test.wav', fs, audio)
print('Đã lưu test.wav')
"
# Chuyển giọng nói thành văn bản
python3 -c "
import whisper
m = whisper.load_model('medium')
result = m.transcribe('test.wav', language='en')
print(result['text'])
"
Kiểm tra Ollama độc lập:
curl http://localhost:11434/api/generate \
-d '{"model": "mistral", "prompt": "What is 2+2?", "stream": false}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['response'])"
Đo độ trễ
Sáu tháng chạy trên RTX 3060 cho tôi một baseline ổn định:
- Whisper
mediumtrên GPU: ~0,8–1,2 giây cho một đoạn audio 5 giây - Ollama (Mistral 7B, GPU): ~1,5–3 giây cho câu trả lời 256 token
- Tổng thời gian: khoảng 2,5–4 giây
Khi tính cả độ trễ mạng, con số này hoàn toàn cạnh tranh được với cloud API. Và hoàn toàn chạy cục bộ.
Gắn thêm đo thời gian cơ bản để theo dõi theo thời gian:
import time
start = time.time()
text = transcribe(audio_path)
print(f"Chuyển giọng nói: {time.time() - start:.2f}s")
start = time.time()
response = ask_ollama(text)
print(f"Phản hồi LLM: {time.time() - start:.2f}s")
Chạy như một background service
Để chạy thường trực, bọc script Python trong một service systemd. Ollama đã chạy như vậy theo mặc định — bước này chỉ thêm trợ lý của bạn chạy song song:
sudo nano /etc/systemd/system/voice-assistant.service
[Unit]
Description=Trợ lý giọng nói cục bộ
After=ollama.service
[Service]
Type=simple
User=youruser
WorkingDirectory=/home/youruser/voice-assistant
ExecStart=/home/youruser/voice-assistant/voice-assistant-env/bin/python assistant.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now voice-assistant
Những vấn đề cần chú ý
Ba vấn đề hay gặp trong môi trường thực tế:
- Ollama hết VRAM: Chạy các tác vụ GPU khác song song với Ollama có thể đẩy nó ra khỏi GPU hoàn toàn. Kiểm tra bằng
nvidia-smi— bạn muốn thấy model được tải vào bộ nhớ GPU, không phải tràn sang RAM hệ thống. - Whisper nhận sai khi im lặng: Khi không ai nói trong lúc ghi âm, Whisper trả về văn bản vô nghĩa thay vì chuỗi rỗng. Thêm kiểm tra năng lượng RMS đơn giản trước khi gửi bất kỳ thứ gì tới Ollama — bỏ qua request nếu audio im lặng.
- RAM tăng dần trong phiên dài: Tôi khởi động lại service hàng tuần để phòng ngừa. Sau hai tháng, tôi nhận thấy RAM tăng thêm ~800MB so với lúc mới khởi động. Khởi động lại hàng tuần giữ nó ổn định.
Toàn bộ stack — Whisper medium cộng với Mistral 7B — chiếm khoảng 6GB VRAM và 4GB RAM hệ thống khi cả hai đã được tải. Trong sáu tháng chạy thực tế, tôi chỉ có đúng hai lần khởi động lại ngoài kế hoạch, cả hai đều do mất điện. Đó là độ tin cậy tôi hoàn toàn hài lòng.

