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

  1. 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.
  2. Transcribe each spoken turn locally with faster-whisper (model small, int8 on CPU, VAD filter on).
  3. 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.
  4. If the user confirms a reminder ("done", "I took them"), mark today's open reminders as acknowledged.
  5. 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.
  6. Validate tool arguments (24-hour time, ISO date) and return errors to the model so it can ask the user again.
  7. Run the reminder check regularly: announce due reminders and repeat unconfirmed ones after 30 minutes.
  8. 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.

Back to the use case · All AI use cases