<p align="center">
  <img src="logo.png" alt="Decktation Logo" width="400"/>
</p>

# Decktation - Push-to-Talk Dictation for Steam Deck

Voice dictation plugin for Steam Deck using faster-whisper with context-aware transcription.

## Features

- **Push-to-Talk**: Hold button combo (default: L1+R1) to record
- **Configurable buttons**: Choose 1-5 button combinations from any Steam Deck controller buttons
- **Game presets**: Switch between game profiles with different chat systems (WoW, Guild Wars 2, Generic)
- **Channel-aware**: Speak channel prefixes like "party pull boss" and it routes to the right chat channel
- **Test Recording**: Built-in 3-second test with automatic transcription display
- **Context-aware**: Optional WoW addon integration for better accuracy with zone, boss, and party context
- **Fast transcription**: Uses faster-whisper for efficient CPU-based speech recognition
- **Auto-type**: Automatically types transcribed text into active window via ydotool
- **Toast notifications**: Optional notifications when recording starts/stops

## Installation

> [!IMPORTANT]
> Use a direct ZIP URL that points to the packaged plugin artifact.
> Recommended stable URL:
> **[`https://silverfoxy.github.io/decktation/releases/latest/decktation.zip`](https://silverfoxy.github.io/decktation/releases/latest/decktation.zip)**
> Branch build URL pattern:
> **`https://silverfoxy.github.io/decktation/branches/<url-encoded-branch-name>/decktation.zip`**
> GitHub release assets also work:
> **[`https://github.com/silverfoxy/decktation/releases/latest/download/decktation.zip`](https://github.com/silverfoxy/decktation/releases/latest/download/decktation.zip)**
>
> Do **not** use GitHub's **Source code (zip)** or **Source code (tar.gz)** archives.
> Those do not contain Decktation's bundled Python dependencies.

1. Open **Decky Settings**, enable developer options if necessary, and select
   **Install Plugin from URL** or **Install Plugin from ZIP**.

2. Paste the direct ZIP URL above, or download `decktation.zip` first and then
   choose the file locally. Decky imports the archive and reloads the plugin
   automatically.

For example, branch `feature/chat polish` becomes
`feature%2Fchat%20polish` in the URL path.

All dependencies and the private keyboard helper are pre-bundled in the
release. No system packages or one-time `sudo` setup are required.

## Usage

1. Open Quick Access Menu (... button on Steam Deck)
2. Navigate to Decktation plugin
3. Select your game from the **Game** dropdown
4. Enable the plugin (waits for Whisper model to load)
5. (Optional) Change the button combination in the plugin UI
6. In any app/game: hold **[button1]+[button2]** together to record, release to transcribe and type

## Game Presets

Decktation ships with three presets configured in `defaults/game_presets.json`:

| Preset | Behavior |
|--------|----------|
| **World of Warcraft** | Presses Enter to open chat, types channel prefix + message, presses Enter to send |
| **Guild Wars 2** | Presses Enter to focus chat, types the Guild Wars 2 channel command + message, presses Enter to send |
| **Generic** | Types text directly into whatever window is focused — no Enter presses |

### WoW Channel Prefixes

In WoW mode, start your message with a channel name to route it to the right channel:

```
"party let's go"          →  /p let's go
"raid: focus adds first"  →  /raid focus adds first
"say, hello everyone"     →  /s hello everyone
"guild heads up"          →  /g heads up
"type search term"        →  types directly without opening chat
```

Separators supported: space, colon, comma, or period after the channel name. Case-insensitive.

Available WoW channels: `say`, `party`, `raid`, `guild`, `officer`, `yell`, `instance`, `whisper`, `type`

### Guild Wars 2 Channel Prefixes

Guild Wars 2 mode uses the same voice-first flow. For example:

```
"map meta starts in five"       →  /m meta starts in five
"party stack on me"             →  /p stack on me
"squad: ready check"            →  /d ready check
"team push mid"                 →  /t push mid
"guild one hello everyone"      →  /g1 hello everyone
"whisper Player Name hello"     →  /w Player Name hello
```

Available Guild Wars 2 channels: `say`, `map`, `party`, `squad`, `raid` (an alias for squad), `team`, `guild`, `guild one` through `guild six`, `whisper`, `type`.

### Adding Custom Channels

You can add channels beyond the built-in ones (e.g. WoW's numbered channels `/1`, `/2`) by editing two config files.

**Step 1 — add the channel command to `defaults/game_presets.json`** under the preset's `channels` map:

```json
"channels": {
  "say": "/s ",
  "party": "/p ",
  "one": "/1 ",
  "two": "/2 "
}
```

**Step 2 — add the spoken trigger words to `defaults/channel_languages.json`** under each language's `channels` map:

```json
"en": {
  "channels": {
    "one": ["one", "channel one"],
    "two": ["two", "channel two"]
  }
}
```

After saving both files, saying `"one hello"` or `"channel one hello"` will send `/1 hello`.

You can add as many trigger words per channel as you like (e.g., aliases in multiple languages). The key in `defaults/channel_languages.json` must match the key in `defaults/game_presets.json`.

### Adding More Presets

Edit `defaults/game_presets.json` to add new games — no code changes needed. Each preset specifies:
- `chat_open_key` — key to open the chat box (`"enter"` or `null`)
- `chat_send_key` — key to send the message (`"enter"` or `null`)
- `default_channel` — channel to use when no prefix is spoken
- `channels` — map of spoken words to slash-command prefixes
- `whisper_prompt` — vocabulary hint for the Whisper model

## Button Configuration

The plugin uses a configurable button combo for push-to-talk. You can set 1–5 buttons from the plugin UI.

Available buttons:

- **L1, R1** (bumpers) — _Default combo_
- **L2, R2** (triggers)
- **L4, R4, L5, R5** (Steam Deck back grips, detected through raw HID)
- **A, B, X, Y** (face buttons)

## Use Cases

### Gaming

- **WoW chat**: Dictate messages with correct channel routing (party, raid, guild, etc.)
- **Generic text input**: Type player names, search terms, chat in any game without a keyboard

### General

- **Web browsing**: Fill forms, search, comment
- **Discord/messaging**: Send messages hands-free
- **Any text input**: Works in any active window using the Generic preset

## WoW Context Support (Optional)

For improved accuracy with WoW-specific vocabulary, install the included WoW addon and run `convert_wow_context.py --watch`. It reads your current zone, target, party members, and class/spec from the game and feeds them to Whisper as context.

See `doc/TESTING_GUIDE.md` for setup instructions.

## Troubleshooting

### Plugin not showing up

- Check Decky Loader logs: `/tmp/decky-*.log`
- Ensure all Python dependencies are installed
- Restart Decky Loader

### Recording not working

- Ensure the plugin is enabled
- Check that Steam Deck mic is working (test in another app)
- Check `/tmp/decktation.log` for `ydotoold ready`
- Check logs: `/tmp/decktation.log`

### Button combo not detected

- Try a different button combination in the plugin UI
- Rear grip buttons are supported on Steam Deck hardware through raw HID
- Check `/tmp/decktation.log` for controller listener errors
- Verify controller listener is running: `pgrep -f controller_listener`

### Performance on Steam Deck

- Default `base` model is recommended (good balance)
- For faster: use `tiny` model (edit wow_voice_chat.py line 28)
- For accuracy: use `small` model (slower, needs more resources)

## Technical Details

- **Speech recognition**: faster-whisper (CTranslate2 backend)
- **Model**: base (150MB, ~2-4s transcription time)
- **Input**: Steam Deck microphone or connected headset
- **Controller input**: Steam Deck raw HID reports (independent of per-game Steam Input layouts)
- **Output**: Keyboard simulation via ydotool (bundled)
- **Game presets**: `defaults/game_presets.json` — data-driven, no code changes needed to add games
- **Dependencies**: Pre-bundled Python 3.11 libraries in `lib/` folder
- **Architecture**: TypeScript frontend + Python backend + separate controller listener process

## Development

```bash
npm install           # Install Node dependencies (package-lock.json is ignored)
npm run build         # Compile TypeScript to dist/index.js
npm run watch         # Watch mode for development

# Unit tests (no hardware required)
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/pytest tests/ -v
```

CI runs unit tests and the TypeScript build on every push via GitHub Actions.

## Privacy

- All processing happens locally on your Steam Deck
- No data sent to external servers
- Whisper model downloads from HuggingFace (one-time)

## Credits

Built with:

- [faster-whisper](https://github.com/guillaumekln/faster-whisper) - Efficient Whisper implementation
- [Decky Loader](https://github.com/SteamDeckHomebrew/decky-loader) - Steam Deck plugin framework

## License

MIT
