A lightweight Windows push-to-talk voice assistant that runs silently in the system tray. Hold a key → speak → release → command executes. No wake word. No continuous listening. Near-zero CPU when idle.
| Layer | Library | Why |
|---|---|---|
| Global hotkey | pynput | Event-driven OS hook — zero polling cost |
| Audio capture | sounddevice | Thin PortAudio wrapper; stream only open while key held |
| Speech-to-text | faster-whisper (tiny) | Local, ~75 MB, int8-quantised, ~1-2 s on CPU |
| System tray | pystray + Pillow | Minimal; no Electron, no heavy GUI |
| Config | PyYAML | Human-readable, easy to edit |
| Smart home | requests (+ HA REST) | Single HTTP call, no persistent connection |
- Private — audio never leaves the machine
- No API key or billing — works offline
- Fast enough — tiny model transcribes a 3-second command in ~1-2 s on a modern CPU
- Low idle cost — model sits in RAM; CPU usage returns to 0 % between commands
PushAssistant/
├── run.py ← entry point
├── run.bat ← double-click launcher (Windows)
├── requirements.txt
│
├── config/
│ └── config.yaml ← all user settings
│
├── assistant/ ← core pipeline
│ ├── main.py ← app bootstrap + system tray
│ ├── hotkey_listener.py ← global key press/release hook
│ ├── audio_recorder.py ← microphone capture
│ ├── speech_to_text.py ← faster-whisper wrapper
│ ├── command_parser.py ← regex-based intent extraction
│ └── action_router.py ← routes intents to action modules
│
├── actions/ ← one file per action category
│ ├── web_search.py ← open browser with search URL
│ ├── open_app.py ← launch apps / open websites
│ └── smart_home.py ← delegate to active provider
│
└── providers/ ← smart home back-ends
├── base_provider.py ← abstract interface + NoOp default
└── home_assistant_provider.py
- Python 3.10+ (
python --version) - Windows 10 / 11
- A working microphone
cd C:\Users\SESA667337\dev\PushAssistant
python -m venv .venv
.venv\Scripts\activatepip install -r requirements.txtFirst-run note:
faster-whisperdownloads the Whisper tiny model (~75 MB) the first time it loads. Subsequent starts are instant. The model is cached in%USERPROFILE%\.cache\huggingface\hub\.
Open config\config.yaml and adjust:
hotkey: "scroll_lock" # key to hold while speaking
speech_to_text:
model: "base" # tiny / base / small
language: "en"
apps:
discord: "discord" # add any app or website here
youtube: "https://youtube.com"
smart_home:
provider: "none" # change to "home_assistant" when readypython run.pyOr double-click run.bat.
The app starts silently in the system tray (look for the green microphone icon near the clock). Right-click the icon → Quit to exit.
Usage: hold Scroll Lock (default) → speak your command → release.
Once you are happy with the setup, run with:
pythonw run.pyor edit run.bat to use pythonw instead of python.
| You say | What happens |
|---|---|
search google for how to reverse a linked list |
Opens Google search |
search youtube for lofi music |
Opens YouTube search |
open discord |
Launches Discord |
open youtube |
Opens youtube.com |
open reddit.com |
Opens reddit.com |
turn off the desk lamp |
Calls provider turn_off |
turn on bedroom light |
Calls provider turn_on |
what is the capital of France |
Googles the question |
Hold hotkey
│
▼
AudioRecorder.start() ← sounddevice InputStream opens
│
Hold key while speaking…
│
Release hotkey
│
▼
AudioRecorder.stop() ← returns float32 numpy array
│
▼
SpeechToText.transcribe()
│ "turn off desk lamp"
▼
CommandParser.parse()
│ {intent: device_control, action: off, device: desk lamp}
▼
ActionRouter.execute()
│
▼
SmartHomeProvider.turn_off("desk lamp")
| State | CPU | RAM |
|---|---|---|
| Idle (tray running) | ~0 % | ~150 MB (Whisper model loaded) |
| Recording (key held) | ~1–2 % | no change |
| Transcribing (~1-2 s) | 50–100 % briefly | no change |
All timing is event-driven — no polling, no wake word, no background audio.
Edit config/config.yaml:
apps:
slack: "slack"
whatsapp: "C:\\Users\\YourName\\AppData\\Local\\WhatsApp\\WhatsApp.exe"
hacker news: "https://news.ycombinator.com"Say "open slack" or "open hacker news".
Edit config/config.yaml (Home Assistant entities):
devices:
"kitchen light": "light.kitchen_ceiling"
"tv": "media_player.living_room_tv"- Add a regex rule in
assistant/command_parser.py:
# Inside the _RULES list, before the catch-all open_app rules:
(
re.compile(r"^\s*(?:set timer for|timer)\s+(\d+)\s+(second|minute|hour)s?\s*$", re.IGNORECASE),
lambda m: {"intent": "timer", "amount": int(m.group(1)), "unit": m.group(2).lower()},
),- Create an action file
actions/timer.py:
def execute(intent: dict) -> None:
# your logic here
pass- Register it in
assistant/action_router.py:
elif kind == "timer":
from actions.timer import execute
execute(intent)- Create
providers/myprovider_provider.py:
from providers.base_provider import SmartHomeProvider
class MyProvider(SmartHomeProvider):
def __init__(self, config): ...
def turn_on(self, device): ...
def turn_off(self, device): ...- Register in
assistant/action_router.pyinside_load_provider():
if name == "myprovider":
from providers.myprovider_provider import MyProvider
return MyProvider(config)- Set
smart_home.provider: myproviderinconfig.yaml.
pip install pyinstaller
pyinstaller --onefile --windowed --name PushAssistant --add-data ".venv\Lib\site-packages\faster_whisper\assets;faster_whisper\assets" run.py
xcopy /E /I config dist\config| Flag | Purpose |
|---|---|
--onefile |
Single .exe |
--windowed |
No console window |
--name PushAssistant |
Output filename |
The .exe is created in dist\.
Model note: PyInstaller does not bundle the Whisper model. The first run of the packaged
.exewill still download it (~75 MB). To pre-bundle it, add the model cache directory with--add-data.
- Press
Win + R→shell:startup - Drop a shortcut to
PushAssistant.exein the folder that opens
| Symptom | Fix |
|---|---|
| Hotkey not detected in some apps | Run as administrator (runas /user:Administrator python run.py) |
sounddevice error on start |
Check microphone permissions in Windows Settings → Privacy |
Whisper says int8 not supported |
Change compute_type: "float32" in config.yaml |
| App won't open | Add an explicit mapping under apps: in config.yaml |
| No icon in tray | Right-click taskbar → Taskbar Settings → enable system tray icons |