Lokalny asystent głosowy z Ollamą
Mikrofon → rozpoznawanie mowy → lokalny LLM → głos. Cały asystent na własnym komputerze, bez wysyłania niczego do chmury.
W tym wpisie
Asystent głosowy to cztery klocki: słuchanie, rozumienie, myślenie, mówienie. Każdy z nich da się dziś uruchomić lokalnie — na laptopie, bez klucza API i bez wysyłania nagrań na cudze serwery. Ten wpis składa te klocki w działający prototyp w Pythonie. Z tego samego pnia wyrósł później nasz Jarvis.
Klocki
| Etap | Narzędzie | Dlaczego |
|---|---|---|
| Słuchanie | sounddevice + prosty VAD | zero zależności natywnych poza PortAudio |
| Rozumienie (STT) | faster-whisper | Whisper na CPU/GPU, model small wystarcza do rozmowy |
| Myślenie (LLM) | Ollama | jeden ollama run, lokalne API na :11434 |
| Mówienie (TTS) | piper | szybkie, lekkie głosy, także polskie |
1. Słuchanie: kiedy użytkownik skończył mówić?
Najprostszy detektor ciszy: nagrywaj w blokach po 30 ms, licz energię (RMS) i uznaj koniec wypowiedzi po ~800 ms poniżej progu.
import numpy as np, sounddevice as sd
SR = 16000
def record_utterance(threshold=0.01, silence_ms=800):
chunks, silent = [], 0
with sd.InputStream(samplerate=SR, channels=1, dtype="float32") as stream:
while True:
block, _ = stream.read(int(SR * 0.03))
chunks.append(block)
rms = float(np.sqrt(np.mean(block ** 2)))
silent = silent + 30 if rms < threshold else 0
if chunks and silent >= silence_ms and len(chunks) > 20:
break
return np.concatenate(chunks).flatten()
Ten sam pomiar RMS przydaje się później do animowania „ust” postaci — tak działa lipsync u Simlokatorów.
2. Rozumienie: faster-whisper
from faster_whisper import WhisperModel
stt = WhisperModel("small", device="cpu", compute_type="int8")
def transcribe(audio):
segments, _ = stt.transcribe(audio, language="pl", vad_filter=True)
return " ".join(s.text for s in segments).strip()
Model small w int8 transkrybuje zdanie w ok. sekundę na zwykłym laptopie. Dla angielskiego można zejść do base.
3. Myślenie: Ollama
Uruchom raz ollama pull llama3.1 (lub dowolny model, który mieści się w Twojej pamięci). Ollama wystawia lokalne HTTP API, do którego wystarczy requests:
import requests
SYSTEM = "Jesteś zwięzłym asystentem. Odpowiadasz po polsku, maksymalnie dwoma zdaniami."
history = [{"role": "system", "content": SYSTEM}]
def think(user_text):
history.append({"role": "user", "content": user_text})
r = requests.post("http://localhost:11434/api/chat", json={
"model": "llama3.1", "messages": history, "stream": False,
})
answer = r.json()["message"]["content"]
history.append({"role": "assistant", "content": answer})
return answer
Lista history to cała „pamięć” rozmowy. Przy dłuższych sesjach przycinaj ją do ostatnich N wiadomości albo streszczaj — model ma ograniczone okno kontekstu.
4. Mówienie: piper
Piper generuje mowę z pliku modelu .onnx, bez internetu:
import subprocess
def speak(text, voice="pl_PL-gosia-medium.onnx"):
subprocess.run(
["piper", "--model", voice, "--output-raw"],
input=text.encode(), stdout=subprocess.PIPE, check=True,
)
Surowe PCM z --output-raw możesz od razu odtworzyć przez sounddevice, bez zapisu do pliku — mniejsze opóźnienie.
Pętla
while True:
audio = record_utterance()
text = transcribe(audio)
if not text:
continue
print("Ty:", text)
reply = think(text)
print("Asystent:", reply)
speak(reply)
Cały cykl (od końca mówienia do początku odpowiedzi) na laptopie bez GPU to 3–5 sekund. Największy koszt to LLM; mniejszy model albo streaming odpowiedzi z Ollamy ("stream": True i podawanie zdań do TTS na bieżąco) skraca to wyraźnie.
Co dalej
- Słowo wybudzające: uruchamiaj
record_utterance()dopiero po „hej, Jarvis” (np.openwakeword). - Narzędzia: zamiast zwykłej odpowiedzi poproś model o JSON
{"action": "...", "args": {...}}i wykonuj akcje — to pierwszy krok od czatu do agenta. - Przerywanie: nasłuchuj mikrofonu także podczas mówienia i ucinaj TTS, gdy użytkownik wejdzie w słowo.
Wszystko powyższe działa offline. Twoje nagrania nie opuszczają komputera — a to dziś rzadki luksus.