Local, privacy-first voice assistant with faster-whisper, Ollama and Piper
Solution for the use case Voice Assistant for Elderly Care.
- AI tools
- faster-whisper, Whisper, Ollama, Llama 3.2, Piper TTS, OpenAI Python SDK
- Language
- Python
- Published
- 2026-10-26
Approach
A voice assistant for an older person living at home that runs entirely on a small local computer: no audio or transcript leaves the device unless an emergency message is sent to a contact the user chose.
- Speech-to-text: faster-whisper (Whisper model small, int8 on CPU) with voice-activity filtering.
- Conversation: a local LLM (default llama3.2:3b) through Ollama's OpenAI-compatible API, called with the official openai Python SDK. A system prompt keeps answers short and forbids medical advice.
- Text-to-speech: Piper with a local voice, spoken a little slower than the default.
- Reminders: daily medication and one-off appointment reminders, created by voice through LLM tool calls, stored in a JSON file and repeated until the user says "done".
- Emergency-contact escalation: a fixed phrase check ("I fell", "help me", "chest pain") runs before the LLM and sends a webhook message to a family member or carer. The assistant always tells the user to call the local emergency number. It does not call emergency services and is not a replacement for them or for a medical alarm.
This is not a medical device and does not monitor health.
Implementation steps
- Install Ollama on the device and pull a small model (ollama pull llama3.2:3b); download a Piper voice with python -m piper.downloadvoices enUS-lessac-medium.
- Transcribe each spoken turn locally with faster-whisper (model small, int8 on CPU, VAD filter on).
- Check the transcript against fixed emergency phrases first; on a match, POST a message to the emergency contact's webhook and tell the user to call the local emergency number, without waiting for the LLM.
- If the user confirms a reminder ("done", "I took them"), mark today's open reminders as acknowledged.
- Otherwise send the conversation to the local LLM through the OpenAI-compatible API with two tools, addreminder and listreminders, and execute the tool calls against the reminder store.
- Validate tool arguments (24-hour time, ISO date) and return errors to the model so it can ask the user again.
- Run the reminder check regularly: announce due reminders and repeat unconfirmed ones after 30 minutes.
- Speak every reply with Piper at a slightly slower speaking rate and play it on the device speaker.
Code (Python)
"""The conversation loop: emergency check first, then reminders, then the local LLM with tools."""
from __future__ import annotations
import json
import re
from datetime import datetime
from .emergency import EmergencyEscalation, is_emergency
from .reminders import ReminderStore
SYSTEM_PROMPT = """You are a friendly voice assistant for an older person living at home.
Current local time: {now}.
Rules:
- Answer in one to three short, simple sentences. Ask at most one question at a time.
- You can set and list reminders with the tools. Confirm the time back to the user.
- Do not give medical advice and never suggest changing a medication or dose; suggest asking
the doctor or pharmacist instead.
- You cannot call emergency services. If the user may be in danger, tell them to call {number}."""
TOOLS = [
{"type": "function", "function": {
"name": "add_reminder",
"description": "Create a reminder, daily unless a date is given.",
"parameters": {"type": "object", "properties": {
"text": {"type": "string", "description": "what to remind about, e.g. 'take the blood pressure tablet'"},
"at": {"type": "string", "description": "24h local time HH:MM"},
"kind": {"type": "string", "enum": ["medication", "appointment", "other"]},
"on_date": {"type": "string", "description": "YYYY-MM-DD for a one-off reminder"},
}, "required": ["text", "at", "kind"]},
}},
{"type": "function", "function": {
"name": "list_reminders",
"description": "List today's reminders.",
"parameters": {"type": "object", "properties": {}},
}},
]
_DONE = re.compile(r"\b(done|i took (them|it|my)|i have taken|taken (them|it))\b", re.IGNORECASE)
class Assistant:
def __init__(self, llm, model: str, reminders: ReminderStore, escalation: EmergencyEscalation,
transcriber=None, speaker=None, max_history: int = 10):
self.llm, self.model = llm, model # llm: an openai.OpenAI client (pointed at Ollama)
self.reminders, self.escalation = reminders, escalation
self.transcriber, self.speaker = transcriber, speaker
self.history: list[dict] = []
self.max_history = max_history
def handle_text(self, text: str, now: datetime) -> str:
if is_emergency(text): # deterministic, never waits for the model
return self.escalation.escalate(text, now)
if _DONE.search(text) and (done := self.reminders.acknowledge_open(now)):
return "Thank you. I have noted it: " + ", ".join(r.text for r in done) + "."
reply = self._chat(text, now)
self.history = (self.history + [{"role": "user", "content": text},
{"role": "assistant", "content": reply}])[-self.max_history:]
return reply
def handle_audio(self, wav_in: str, wav_out: str, now: datetime) -> tuple[str, str]:
heard = self.transcriber.transcribe(wav_in)
if not heard:
reply = "Sorry, I did not catch that. Could you say it again?"
else:
reply = self.handle_text(heard, now)
self.speaker.speak(reply, wav_out)
return heard, reply
def reminder_announcements(self, now: datetime) -> list[str]:
"""Sentences to say now: new reminders and repeats of unconfirmed ones."""
out = [f"It is {r.at}. Time to {r.text}. Say 'done' when you have." for r in self.reminders.due(now)]
out += [f"Just a reminder: {r.text}. Have you done it?" for r in self.reminders.repeat_unacknowledged(now)]
return out
def _chat(self, text: str, now: datetime) -> str:
system = SYSTEM_PROMPT.format(now=now.strftime("%A %Y-%m-%d %H:%M"),
number=self.escalation.emergency_number)
messages = [{"role": "system", "content": system}, *self.history, {"role": "user", "content": text}]
for _ in range(3): # at most a few tool rounds per turn
msg
…
Prompts
You are a friendly voice assistant for an older person living at home.
Current local time: {now}.
Rules:
- Answer in one to three short, simple sentences. Ask at most one question at a time.
- You can set and list reminders with the tools. Confirm the time back to the user.
- Do not give medical advice and never suggest changing a medication or dose; suggest asking
the doctor or pharmacist instead.
- You cannot call emergency services. If the user may be in danger, tell them to call {number}.
Performance
Targets, not measured results.
- Target: reply starts within about 3 seconds of the end of speech on a mini PC; measure on your device, since Whisper and the LLM both depend heavily on CPU or GPU.
- Emergency phrases are matched in code, so escalation does not depend on the LLM and takes as long as the webhook call (timeout 10 s).
- In a manual check with Ollama and llama3.2:3b, the model created a daily 20:00 reminder from "remind me every day at 8 in the evening" via the tool call. Small models do not always call tools correctly; test your phrasing and model.
- Speech recognition quality for older voices, dialects and background noise varies; test with the actual users and microphone.
Lessons learned
The user speaks normally ("Please remind me to take my heart tablet at 8 in the evening") and hears a short spoken confirmation. At 20:00 the assistant says "Time to take the heart tablet. Say 'done' when you have" and repeats it after 30 minutes if there was no confirmation. If the user says "I fell and can't get up", the assistant replies "I have sent a message to Anna. If you are hurt or in danger, call 112 now." A text mode (--text) lets carers test the setup by typing.