Metadata-Version: 2.4
Name: pc-voice-assistant
Version: 1.1.0
Summary: A local, privacy-focused voice assistant for Windows
Author-email: Emmanuel Nelson <nelsonemmanuel006@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/epicnellson/pc-assistant
Project-URL: Bug Tracker, https://github.com/epicnellson/pc-assistant/issues
Keywords: voice-assistant,speech-recognition,vosk,windows,automation,cli
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: sounddevice>=0.4.6
Requires-Dist: numpy>=1.24.0
Requires-Dist: scipy>=1.10.0
Requires-Dist: vosk>=0.3.45
Requires-Dist: openwakeword>=0.6.0
Requires-Dist: pyttsx3>=2.99
Requires-Dist: PyQt6>=6.6.0
Requires-Dist: pystray>=0.19.0
Requires-Dist: Pillow>=10.0.0
Requires-Dist: pyperclip>=1.8.0
Requires-Dist: mss>=10.0.0
Provides-Extra: notifications
Requires-Dist: plyer; extra == "notifications"
Provides-Extra: online-tts
Requires-Dist: edge-tts; extra == "online-tts"
Provides-Extra: ollama
Requires-Dist: ollama; extra == "ollama"
Provides-Extra: all
Requires-Dist: plyer; extra == "all"
Requires-Dist: edge-tts; extra == "all"
Requires-Dist: ollama; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"

# PC Voice Assistant

A local, privacy-focused voice assistant for Windows that runs entirely on your machine. Speak naturally to control your PC — open apps, set reminders, search the web, and more.

## Features

- **Wake Word Detection**: Say "Alexa" or "Hey Computer" to activate (ML-based openWakeWord with energy fallback)
- **Speech Recognition**: Offline Vosk ASR engine — no internet required
- **Natural Language Understanding**: Rule-based intent matching with optional Ollama LLM integration
- **Text-to-Speech**: pyttsx3 (offline) or edge-tts (online, higher quality)
- **Smart Reminders**: Natural time expressions ("in 2 hours", "next Monday at 3pm")
- **Fuzzy App Matching**: "open chrome" matches "Google Chrome" automatically
- **System Integration**: Open apps, control volume, reminders, screenshots, and more
- **System Tray**: Runs in background with tray icon and context menu
- **Rotating Logs**: All activity logged to `logs/assistant.log`
- **133 Test Suite**: Comprehensive unit tests for all modules

---

## 1. Project Overview

The PC Voice Assistant is a Python CLI application that provides hands-free voice control for Windows PCs. It listens for a wake word, records your voice, transcribes it to text, understands your intent, executes the action, and speaks a response.

**Pipeline Flow:**
```
Wake Word → Record Audio (VAD) → Transcribe → Parse Intent → Execute Action → Speak Response → Wait for Wake Word
```

The assistant is:
- **Local/Offline**: Core functionality works without internet
- **Windows-Focused**: Uses Windows-specific commands (PowerShell, Windows APIs)
- **Privacy-First**: All processing happens on your machine
- **Extensible**: Easy to add new intents and commands

---

## 2. Requirements

### Python Version
- Python 3.10 or higher required (union type hints like `Tuple[...] | None`)

### Operating System
- **Windows** (primary) — fully supported
- Linux/Mac: Wake word and TTS modules may work, but system commands (volume, shutdown, sleep) require Windows

### Hardware
- **Microphone required** for voice input
- Any standard USB or built-in microphone works
- Audio output device required for TTS

### External Tools (Optional)

| Tool | Purpose | Install |
|------|---------|---------|
| [Ollama](https://ollama.ai) | LLM-powered responses | `ollama pull llama3.2` |
| Vosk Model | Offline speech recognition | Download from alphacephei.com |
| [edge-tts](https://pypi.org/project/edge-tts/) | Online TTS (better quality) | `pip install edge-tts` |
| [plyer](https://pypi.org/project/plyer/) | Desktop notifications | `pip install plyer` |

---

## 3. Installation

### Step 1: Clone the Repository
```bash
git clone <repository-url>
cd pc-assistant
```

### Step 2: Install Python Dependencies
```bash
pip install -r requirements.txt
```

### Step 3: Download and Install Vosk Model
1. Download from: https://alphacephei.com/vosk/models
2. Recommended model: `vosk-model-small-en-us-0.15` (~45MB) or `vosk-model-en-us-0.22` (~1.8GB)
3. Extract into the project root directory
4. The folder name should match `asr.model_path` in config.json (default: `models/vosk-model-small-en-us-0.15`)

```
pc-assistant/
├── models/
│   └── vosk-model-small-en-us-0.15/
│       ├── am/
│       ├── conf/
│       ├── graph/
│       └── ivector/
├── main.py
├── config.json
└── ...
```

### Step 4: (Optional) Install and Start Ollama
For LLM-powered natural language understanding:

1. Install Ollama: https://ollama.ai
2. Pull a model:
   ```bash
   ollama pull llama3.2
   ```
3. Start Ollama server:
   ```bash
   ollama serve
   ```
4. Set `nlu.use_ollama` to `true` in config.json

### Step 5: Run the Assistant
```bash
python main.py
```

Press Ctrl+C to exit gracefully.

---

## 4. Configuration (config.json)

All settings are in `config.json`. Never edit while the assistant is running.

### wake_word Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | bool | true | Enable wake word detection |
| engine | string | "openwakeword" | Detection engine: "openwakeword" or "energy" |
| input_device | int | 2 | Audio input device index (null = default) |
| confidence_threshold | float | 0.5 | Wake word sensitivity (0.0-1.0, higher = stricter) |
| inference_interval | int | 10 | Frames between inferences (openWakeWord only) |
| trigger_cooldown_seconds | float | 2.0 | Seconds between wake word triggers |
| custom_keywords | list | ["hey assistant", "computer"] | Custom wake words |
| sample_rate | int | 16000 | Audio sample rate (reserved) |
| energy_fallback.threshold | float | 0.02 | Energy threshold for fallback detection |
| energy_fallback.required_chunks | int | 3 | Consecutive chunks above threshold to trigger |

### asr Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| engine | string | "vosk" | Speech recognition engine |
| model_path | string | "./models/vosk-model-small-en-us-0.15" | Path to Vosk model folder |
| sample_rate | int | 16000 | Audio sample rate (reserved) |
| record_seconds | int | 5 | Fallback recording duration in seconds |
| input_device | int | 2 | Audio input device index |
| **vad_enabled** | bool | true | Enable voice activity detection |
| **vad_silence_duration_ms** | int | 800 | Milliseconds of silence to stop recording |
| **vad_min_record_seconds** | float | 1.0 | Minimum recording duration |
| **vad_max_record_seconds** | float | 10.0 | Maximum recording duration |
| **vad_energy_threshold** | float | 0.01 | Energy threshold for speech detection |
| vad.enabled | bool | true | Legacy VAD setting |
| vad.silence_threshold | float | 0.005 | Legacy silence threshold |
| vad.silence_duration | float | 1.2 | Legacy silence duration |
| vad.max_record_duration | float | 10.0 | Legacy max duration |
| audio_enhancement.enabled | bool | true | Apply audio enhancements |
| audio_enhancement.noise_reduction | bool | true | Apply high-pass filter |
| audio_enhancement.normalization | bool | true | Normalize audio levels |
| audio_enhancement.volume_boost | float | 1.5 | Volume multiplier |

### nlu Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| use_ollama | bool | false | Enable LLM fallback for unknown intents |
| use_keyword_commands | bool | true | Reserved for keyword-only mode |
| ollama_model | string | "llama3.2" | Ollama model name |
| ollama_base_url | string | "http://localhost:11434" | Ollama API endpoint |
| max_history | int | 10 | Conversation history length |
| fallback_to_llm | bool | true | Reserved (fallback always enabled) |
| log_intents | bool | true | Reserved for intent logging |
| **fuzzy_match_threshold** | float | 0.6 | Word overlap threshold for fuzzy app matching |
| **unknown_intent_response** | string | "Sorry, I didn't..." | Response for unrecognized input |
| intent_patterns | dict | {...} | Reserved (patterns hardcoded in handlers) |

### tts Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| engine | string | "pyttsx3" | TTS engine: "pyttsx3" or "edge-tts" |
| edge_voice | string | "en-US-JennyNeural" | edge-tts voice name |
| rate | float | 1.0 | Speech rate multiplier |
| volume | float | 1.0 | Reserved for volume control |
| confirm_reminders | bool | true | Reserved for confirmation UI |
| confirm_actions | bool | true | Reserved for confirmation UI |

### assistant Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| continuous_listening | bool | false | Keep listening after each command |
| audio_feedback | bool | true | Play sounds on wake word detection |
| confirm_destructive | bool | true | Confirm before shutdown/restart |

### actions Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| screenshot_folder | string | "screenshots" | Directory to save screenshots |
| file_search_roots | list | ["C:\\Users"] | Root directories for file search |
| file_search_max_results | int | 5 | Maximum file search results |
| file_search_timeout_seconds | int | 30 | Timeout for file search in seconds |
| weather_api_key | string | "" | OpenWeatherMap API key |
| weather_default_city | string | "New York" | Default city for weather queries |
| weather_units | string | "metric" | Weather units: "metric" or "imperial" |
| allow_sleep | bool | true | Allow putting PC to sleep |
| web_search_url | string | "https://google.com/search?q=" | Search engine URL |
| apps | dict | {...} | App name → command mappings |
| websites | dict | {...} | Site name → URL mappings |

### reminders Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | bool | true | Enable reminder functionality |
| storage_path | string | "reminders.json" | Reminder persistence file |
| check_interval_seconds | int | 30 | Seconds between reminder checks |
| notification_timeout | int | 10 | Desktop notification duration |
| tts_notification | bool | true | Speak reminder via TTS |

### ui Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | bool | false | Enable PyQt6 GUI |
| enable_tray | bool | false | Enable system tray icon |
| show_window_on_start | bool | false | Show window on startup |
| minimize_to_tray | bool | false | Minimize to tray on close |
| start_minimized | bool | false | Start minimized |
| confirm_exit | bool | true | Confirm before exiting |

### tray Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | bool | false | Enable system tray |
| show_icon | bool | false | Show tray icon |
| start_listening_on_start | bool | false | Start listening on launch |
| tooltip | string | "PC Voice Assistant" | Tray icon tooltip |
| icon_path | string | null | Custom tray icon path |

### logging Section

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| level | string | "INFO" | Log level: DEBUG, INFO, WARNING, ERROR |
| file | string | "logs/assistant.log" | Log file path |
| console | bool | true | Also log to console |
| max_bytes | int | 5242880 | Max log file size (5MB) |
| backup_count | int | 5 | Number of backup logs to keep |
| show_logs_on_tray | bool | true | Log viewer in tray menu |

### Reserved/Unused Config Keys

These keys exist in config.json but are not currently read by Python code. They are preserved for future use:

| Section | Key | Planned Use |
|---------|-----|-------------|
| asr | whisper_model | Future Whisper ASR integration |
| asr | sample_rate | Reserved for sample rate configuration |
| nlu | use_keyword_commands | Keyword-only mode flag |
| nlu | fallback_to_llm | LLM fallback control |
| nlu | log_intents | Intent usage logging |
| nlu | intent_patterns | Configurable intent patterns |
| tts | volume | TTS volume control |
| tts | confirm_reminders | Reminder confirmation UI |
| tts | confirm_actions | Action confirmation UI |
| wake_word | custom_model_paths | Custom wake word models |
| wake_word | pronunciation_hints | Pronunciation tuning |
| wake_word | sample_rate | Sample rate configuration |

---

## 5. Voice Commands Reference

### Greetings

| Command | Example |
|---------|---------|
| hello | "Hello" |
| hey | "Hey there" |
| good morning | "Good morning" |

### Time & Date

| Command | Example |
|---------|---------|
| what time is it | "What time is it?" |
| what's the date | "What's today's date?" |
| uptime | "How long has the PC been running?" |

### Volume Control

| Command | Example |
|---------|---------|
| volume up | "Volume up" |
| volume down | "Turn down the volume" |
| mute | "Mute" |
| unmute | "Unmute" |

### Applications & Websites

| Command | Example | Notes |
|---------|---------|-------|
| open notepad | "Open Notepad" | Exact match |
| open calculator | "Launch Calculator" | Exact match |
| open chrome | "Open Chrome" | Fuzzy matched to "google chrome" |
| open vs code | "Open VS Code" | Fuzzy matched to "visual studio code" |
| go to github | "Go to GitHub" | Opens URL from config |

### Web Search

| Command | Example |
|---------|---------|
| search for | "Search for Python tutorials" |
| look up | "Look up the weather" |
| find | "Find recipes for pasta" |

### Reminders

| Command | Example |
|---------|---------|
| in X minutes | "Remind me in 10 minutes" |
| at X time | "Remind me at 5 pm" |
| tomorrow at | "Remind me tomorrow at 9am" |
| in X hours | "Remind me in 2 hours" |
| in X hours and Y minutes | "Remind me in 1 hour and 30 minutes" |
| in X seconds | "Remind me in 90 seconds" |
| next weekday at | "Remind me next Monday at 3pm" |
| at noon | "Remind me at noon" |
| at midnight | "Remind me at midnight" |

### Actions

| Command | Example |
|---------|---------|
| screenshot | "Take a screenshot" |
| search | "Search for [query]" |
| empty trash | "Empty the recycle bin" |
| processes | "Show running processes" |

### Math

| Command | Example |
|---------|---------|
| plus/add | "What is 5 plus 3?" |
| minus/subtract | "Calculate 10 minus 4" |
| times/multiply | "What is 3 times 7?" |

### System Control

| Command | Example |
|---------|---------|
| shutdown | "Shutdown" |
| restart | "Restart" |
| sleep | "Sleep" |

### Conversation

| Command | Example |
|---------|---------|
| thank you | "Thank you" |
| goodbye | "Goodbye" |
| help | "Help" |
| who are you | "Who are you?" |

---

## 6. Architecture Overview

### Pipeline Flow

```
┌─────────────────────────────────────────────────────────────────┐
│                         MAIN LOOP                               │
├─────────────────────────────────────────────────────────────────┤
│  1. WAKE WORD DETECTION (wake_word.py)                         │
│     - Listen for wake word ("alexa", "hey computer")          │
│     - Uses openWakeWord ML model or energy threshold           │
│     - Blocking wait with VAD to detect speech start           │
├─────────────────────────────────────────────────────────────────┤
│  2. RECORD AUDIO (asr.py)                                     │
│     - Record until silence detected (VAD)                       │
│     - Configurable min/max duration                            │
│     - Audio enhancement: noise reduction, normalization         │
├─────────────────────────────────────────────────────────────────┤
│  3. TRANSCRIBE (asr.py)                                       │
│     - Vosk offline ASR → text                                 │
│     - Fallback to keyword detection if model unavailable       │
├─────────────────────────────────────────────────────────────────┤
│  4. PARSE INTENT (nlu.py)                                     │
│     - Keyword-based pattern matching                           │
│     - Optional Ollama LLM for unknown intents                  │
│     - Intent handlers: apps, reminders, volume, etc.           │
├─────────────────────────────────────────────────────────────────┤
│  5. EXECUTE ACTION                                             │
│     - Open apps via subprocess                                 │
│     - Set reminders (persisted to JSON)                        │
│     - Control volume via PowerShell                           │
│     - Screenshot via mss                                       │
├─────────────────────────────────────────────────────────────────┤
│  6. SPEAK RESPONSE (tts.py)                                   │
│     - pyttsx3 (offline Windows SAPI)                          │
│     - edge-tts (online, higher quality)                       │
│     - Falls back gracefully if primary fails                   │
└─────────────────────────────────────────────────────────────────┘
```

### Module Responsibilities

| Module | Purpose |
|--------|---------|
| `main.py` | Entry point, main loop coordination, signal handling |
| `wake_word.py` | Wake word detection (openWakeWord/energy fallback) |
| `asr.py` | Audio recording (VAD), Vosk transcription, audio enhancement |
| `nlu.py` | Intent parsing, action handlers, reminder scheduler |
| `tts.py` | Text-to-speech (pyttsx3/edge-tts) |
| `gui.py` | PyQt6 GUI window |
| `tray.py` | System tray management |
| `config_loader.py` | Configuration file loading (singleton) |
| `config_validator.py` | Configuration validation |
| `logging_config.py` | Logging setup with file rotation |
| `audio_feedback.py` | Wake word beep sounds |
| `performance_metrics.py` | Latency tracking |
| `config.json` | All configuration values |

---

## 7. Troubleshooting

### Wake Word Never Triggers

**Symptoms:** Assistant doesn't respond to wake word.

**Solutions:**
1. Check microphone is working in Windows Sound settings
2. Verify `wake_word.input_device` in config.json is correct (or null for default)
3. Check `wake_word.engine` is set to "openwakeword" or "energy"
4. For openWakeWord: ensure it's installed (`pip install openwakeword`)
5. For energy fallback: lower `wake_word.energy_fallback.threshold` (e.g., 0.005)
6. Lower `wake_word.confidence_threshold` (e.g., 0.3)
7. Speak clearly and closer to the microphone

### ASR Model Not Found Error

**Symptoms:** "Vosk model not found" error on startup.

**Solutions:**
1. Download model from https://alphacephei.com/vosk/models
2. Extract to project root, ensure folder structure:
   ```
   models/vosk-model-small-en-us-0.15/{am,conf,graph,ivector}/
   ```
3. Verify `asr.model_path` in config.json matches folder name
4. Check the path doesn't contain special characters

### TTS Makes No Sound

**Symptoms:** Assistant speaks but you hear nothing.

**Solutions:**
1. Check `tts.engine` in config.json
   - pyttsx3: Uses Windows SAPI, check system volume
   - edge-tts: Requires internet connection
2. Run `python -c "import tts; print(tts.get_engine_info())"` to see active engine
3. Install pyttsx3 if missing: `pip install pyttsx3`
4. For edge-tts: `pip install edge-tts`
5. Check Windows audio device is not muted

### Ollama Intents Not Working

**Symptoms:** Unknown commands return fallback instead of LLM response.

**Solutions:**
1. Confirm Ollama is running:
   ```bash
   ollama serve
   ```
2. Check model is installed:
   ```bash
   ollama list
   ```
3. Pull model if missing:
   ```bash
   ollama pull llama3.2
   ```
4. Verify `nlu.use_ollama` is `true` in config.json
5. Check `nlu.ollama_base_url` is `http://localhost:11434`
6. Test manually:
   ```bash
   curl http://localhost:11434/api/tags
   ```

### Reminder Notification Not Showing

**Symptoms:** Reminder fires but no desktop notification.

**Solutions:**
1. Install plyer for desktop notifications:
   ```bash
   pip install plyer
   ```
2. Check Windows notification settings allow notifications from Python
3. Without plyer, reminders still work via TTS voice notification
4. Check `reminders.enabled` is `true` in config.json

### App Open Command Not Working

**Symptoms:** "I couldn't find an app" response.

**Solutions:**
1. Check the app exists in `actions.apps` in config.json
2. Add the app with full path if needed:
   ```json
   "notepad": "C:\\Windows\\notepad.exe"
   ```
3. Fuzzy matching handles partial names:
   - "open chrome" matches "google chrome"
   - "open vs code" matches "visual studio code"
4. For URLs, add to `actions.websites`:
   ```json
   "youtube": "https://youtube.com"
   ```

---

## 8. Running Tests

```bash
python -m pytest tests/ -v
```

**Expected Result:** 133 tests, all passing.

### Test Coverage

| Test File | Tests | Coverage |
|-----------|-------|----------|
| test_asr.py | 16 | ASR configuration, VAD, transcription |
| test_config.py | 7 | Config loading, validation |
| test_main.py | 7 | Main loop error recovery, pause/resume state |
| test_nlu.py | 77 | Intent handlers, reminders, fuzzy matching, clipboard, file search, weather |
| test_tray.py | 3 | System tray lifecycle |
| test_tts.py | 13 | TTS engines, edge cases |
| test_wake_word.py | 10 | Wake word detection, energy fallback |

---

## 9. Adding New Intents

### Step 1: Add App/Command to Config (if applicable)

For new apps or websites, add to `config.json`:

```json
"actions": {
    "apps": {
        "myapp": "C:\\path\\to\\myapp.exe"
    }
}
```

### Step 2: Create Intent Handler Class

In `nlu.py`, add a new handler class:

```python
class NewIntentHandler(IntentHandler):
    def __init__(self):
        super().__init__("newintent", ["trigger word", "other trigger"])
    
    def handle(self, text: str) -> Optional[str]:
        # Your logic here
        return "Response message"
```

### Step 3: Register the Handler

Add to `INTENT_HANDLERS` list in `nlu.py`:

```python
INTENT_HANDLERS = [
    ...
    NewIntentHandler(),
]
```

### Step 4: Add Tests

In `tests/test_nlu.py`:

```python
def test_new_intent_handler(self):
    handler = nlu.NewIntentHandler()
    assert handler.match("trigger word")
    result = handler.handle("trigger word")
    assert "Response" in result
```

### Step 5: Update README

Add your command to the Voice Commands Reference section.

### Step 6: Run Tests

```bash
python -m pytest tests/ -v
```

---

## 10. Changelog

### Version 1.1.0 — Current Development

#### New Features

- **VAD Silence Detection**: Recording now stops automatically when you stop speaking, instead of fixed duration. Configurable min/max duration and silence threshold.
- **Fuzzy App Matching**: "open chrome" automatically matches "google chrome" in config. Uses word overlap algorithm with configurable threshold.
- **Extended Reminder Parsing**: Natural time expressions now supported:
  - "in 2 hours"
  - "in 1 hour and 30 minutes"
  - "tomorrow at 9am"
  - "next monday at 3pm"
  - "in 90 seconds"
- **Unknown Intent Fallback**: Customizable response when no intent matches. Configurable via `nlu.unknown_intent_response`.

#### Reliability Improvements

- **Config Caching**: Config is now loaded once per module, not on every function call. Significant performance improvement.
- **Atomic Reminder Writes**: Uses temp file + os.replace() to prevent corruption if process is killed mid-write.
- **Wake Word Race Condition Fix**: Added `_model_ready` Event to properly synchronize background model loading.
- **TTS MP3/WAV Fix**: edge-tts generates MP3 but was being played as WAV. Now uses msedge.exe for correct playback.
- **Path Quoting Fix**: PowerShell commands now properly escape paths with spaces.

#### Security Improvements

- **subprocess shell=True Removal**: All subprocess calls now use list-based arguments, eliminating shell injection risk.

#### Testing

- **133 Test Suite**: Comprehensive edge case coverage added:
  - ASR: mic failure, missing model, empty transcription, partial results
  - TTS: missing files, long strings, engine fallback
  - Wake word: timeout fallback, model loading errors
  - Main loop: error recovery from all stages
  - Reminders: concurrent writes, persistence, past time handling

---

## License

MIT
