8.2 KiB
Voice Commands Tab — Implementation Plan
Overview
Add a third GUI tab ("Commands") that lets users define trigger phrases from whisper output and map them to either text replacements or keyboard combinations. This enables meta-speech like saying "period" to insert ., or "over" to send Return.
Architecture
Data Model
typedef enum {
VOICE_CMD_TEXT, // replace trigger with literal text
VOICE_CMD_KEYCOMBO // send a key combination via XTest
} voice_cmd_type_t;
typedef struct {
char trigger[128]; // spoken phrase, case-insensitive
voice_cmd_type_t type;
char text_value[256]; // for TEXT type: replacement string
unsigned int keyval; // for KEYCOMBO type: GDK keyval
unsigned int modifiers; // for KEYCOMBO type: modifier mask
char display_key[128]; // human-readable key description
} voice_cmd_t;
typedef struct {
voice_cmd_t *cmds;
int count;
int capacity;
int enabled; // global on/off toggle
} voice_cmd_list_t;
Processing Pipeline
transcribe_buffer() → raw text
↓
voice_cmd_apply(text, list) → action sequence
↓
For each action:
- TEXT_CHUNK: accumulate into output string
- KEY_EVENT: call typer_send_keycombo()
↓
Final text → append_transcript() + typer_type_text()
The apply function returns a sequence of interleaved actions because a single transcript may contain multiple triggers mixed with regular text. Example: "hello over goodbye over" → text:"hello " → key:Return → text:"goodbye " → key:Return.
Matching Strategy
- Case-insensitive comparison
- Whole-word boundary matching to avoid false positives
- Longest-match-first: sort triggers by length descending before scanning
- Left-to-right scan through transcript text
- Matched trigger phrases are consumed and not included in output text
New Files
src/voice_cmd.h
voice_cmd_tandvoice_cmd_list_tstructsvoid voice_cmd_init(voice_cmd_list_t *list)void voice_cmd_free(voice_cmd_list_t *list)int voice_cmd_add(voice_cmd_list_t *list, const voice_cmd_t *cmd)int voice_cmd_remove(voice_cmd_list_t *list, int index)int voice_cmd_load(const char *path, voice_cmd_list_t *list)int voice_cmd_save(const char *path, const voice_cmd_list_t *list)
src/voice_cmd.c
- Load/save from
voice_commands.ini voice_cmd_apply()— the core text processing function
voice_commands.ini
Persisted command rules, separate from main config.ini.
Format:
# trigger|type|value
# type: text or key
# for text: value is the replacement string (supports \n \t escapes)
# for key: value is keyval:modifiers (decimal GDK values)
period|text|.
question mark|text|?
comma|text|,
exclamation mark|text|!
new line|text|\n
new paragraph|text|\n\n
tab|text|\t
over|key|65293:0
end of command|key|65293:5
Modified Files
src/typer.h / src/typer.c
- Add:
int typer_send_keycombo(unsigned int keyval, unsigned int modifiers) - Uses XTest to send arbitrary key combinations with modifier state
- Reuses existing X11 display connection from typer_init()
src/gui_main.c
- Add third notebook tab in on_app_activate()
- Tab contains:
- GtkTreeView showing existing commands (trigger, type, action display)
- Delete button per row or for selected row
- Add-new-command form:
- GtkEntry for trigger phrase
- GtkRadioButton pair: Text / Key Combo
- GtkEntry for text replacement value
- GtkButton "Capture Key" + GtkLabel showing captured key
- GtkButton "Add Command"
- GtkCheckButton "Enable voice commands" toggle
- Key capture workflow:
- User clicks Capture
- Button label changes to "Press key combo now..."
- Window key-press-event handler captures next keypress
- Records keyval + modifier state
- Formats display string like "Ctrl+Shift+Return"
- Disconnects capture handler, restores button label
src/gui_main.c — process_segment()
- After transcribe_buffer() returns text, call voice_cmd_apply()
- Process returned action sequence: text chunks go to transcript/autotype, key events go to typer_send_keycombo()
Makefile
- Add voice_cmd.o to build targets
build.sh
- Add src/voice_cmd.c to compilation
GUI Tab Layout
┌─ Voice Commands ──────────────────────────────────────────┐
│ │
│ ☑ Enable voice commands │
│ │
│ ┌──────────────┬──────┬─────────────────┬───────────┐ │
│ │ Trigger │ Type │ Action │ │ │
│ ├──────────────┼──────┼─────────────────┼───────────┤ │
│ │ period │ text │ . │ [Delete] │ │
│ │ question mark│ text │ ? │ [Delete] │ │
│ │ new line │ text │ \n │ [Delete] │ │
│ │ over │ key │ Return │ [Delete] │ │
│ │ end of cmd │ key │ Ctrl+Shift+Ret │ [Delete] │ │
│ └──────────────┴──────┴─────────────────┴───────────┘ │
│ │
│ ── Add New Command ── │
│ Trigger: [____________] │
│ ○ Text replacement ● Key combination │
│ Text: [____________] │
│ Key: [Press keys...] [Capture] │
│ [Add Command] │
│ │
│ Help: [hover text area] │
└────────────────────────────────────────────────────────────┘
Default Commands (shipped with first install)
| Trigger | Type | Action |
|---|---|---|
| period | text | . |
| question mark | text | ? |
| exclamation mark | text | ! |
| comma | text | , |
| colon | text | : |
| semicolon | text | ; |
| new line | text | \n |
| new paragraph | text | \n\n |
| tab | text | \t |
| open paren | text | ( |
| close paren | text | ) |
| open bracket | text | [ |
| close bracket | text | ] |
| open brace | text | { |
| close brace | text | } |
Edge Cases
-
Substring false positives: "I need a period of time" — whole-word boundary matching prevents "period" from matching inside "periodical" but this phrase would still trigger. Could add a "require end-of-phrase position" option per command, but start simple with whole-word matching.
-
Case sensitivity: Whisper may output "Period" or "period" depending on sentence position. All matching is case-insensitive.
-
Multi-word triggers: "end of command" requires multi-word scanning. Sort triggers longest-first so "end of command" matches before "end".
-
Escape sequences in text values: Support \n, \t, \ in the text replacement field. Parse on load, display escaped in UI.
-
Key capture conflicts: While capturing, consume the event so it doesn't trigger other handlers. Disconnect capture handler after first keypress.
-
Empty transcript after replacements: If the entire transcript was voice commands with no remaining text, skip autotype and transcript append.
Implementation Order
- Create src/voice_cmd.h and src/voice_cmd.c with data structures and load/save
- Add typer_send_keycombo() to src/typer.h / src/typer.c
- Implement voice_cmd_apply() core matching logic
- Build the Commands tab UI in src/gui_main.c
- Wire key capture handler
- Hook voice_cmd_apply() into process_segment()
- Create default voice_commands.ini
- Update Makefile and build.sh
- Build and test