Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dafd869d7 | ||
|
|
4d0b638f7d | ||
|
|
e6de105478 | ||
|
|
bc4fc50c7d | ||
|
|
e1cfd7fc98 | ||
|
|
e83da30048 |
@@ -21,7 +21,8 @@ SRC := \
|
||||
./src/config.c \
|
||||
./src/audio.c \
|
||||
./src/transcribe.c \
|
||||
./src/typer.c
|
||||
./src/typer.c \
|
||||
./src/voice_cmd.c
|
||||
|
||||
TARGET := ./voice_linux
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ if [[ "$WITH_WHISPER" == "1" ]]; then
|
||||
fi
|
||||
|
||||
gcc "${APP_CFLAGS[@]}" \
|
||||
./src/gui_main.c ./src/config.c ./src/audio.c ./src/transcribe.c ./src/typer.c \
|
||||
./src/gui_main.c ./src/config.c ./src/audio.c ./src/transcribe.c ./src/typer.c ./src/voice_cmd.c \
|
||||
-o ./voice_linux \
|
||||
"${APP_LDFLAGS[@]}"
|
||||
|
||||
|
||||
+37
-5
@@ -2,7 +2,7 @@
|
||||
# Hotkey format supports Ctrl, Alt, Shift, Super + final key symbol
|
||||
# Example: Ctrl+Alt+V
|
||||
|
||||
model_path=./models/ggml-base.en.bin
|
||||
model_path=./models/ggml-medium.en.bin
|
||||
use_gpu=1
|
||||
audio_device=alsa_input.usb-Shure_Inc_Shure_MV6_MV6_5-ed2d37fa14d3e65d8d477cacf910d93c-01.mono-fallback
|
||||
sample_rate=16000
|
||||
@@ -11,21 +11,53 @@ hotkey=Ctrl+Alt+V
|
||||
type_delay_us=8000
|
||||
max_record_seconds=30
|
||||
# always-on VAD controls
|
||||
always_on=1
|
||||
always_on=0
|
||||
# legacy window size (unused by new VAD pipeline, kept for compatibility)
|
||||
always_on_window_ms=1400
|
||||
|
||||
# live level threshold (0.0 - 1.0)
|
||||
vad_peak_threshold=0.006
|
||||
vad_peak_threshold=0.0460
|
||||
# level must stay above threshold this long before speech starts
|
||||
vad_trigger_ms=100
|
||||
vad_trigger_ms=153
|
||||
# level must stay below threshold this long before speech ends
|
||||
vad_release_ms=300
|
||||
# include this much pre-trigger audio in transcript segment
|
||||
vad_preroll_ms=500
|
||||
# discard segments shorter than this
|
||||
vad_min_speech_ms=250
|
||||
vad_min_speech_ms=692
|
||||
# hard cap for one speech segment
|
||||
vad_max_speech_ms=15000
|
||||
|
||||
autotype_enabled=1
|
||||
filter_bracketed_tags=0
|
||||
|
||||
# whisper decode controls
|
||||
whisper_sampling_strategy=0
|
||||
whisper_n_max_text_ctx=16384
|
||||
whisper_offset_ms=0
|
||||
whisper_duration_ms=0
|
||||
whisper_translate=0
|
||||
whisper_detect_language=0
|
||||
whisper_no_context=1
|
||||
whisper_no_timestamps=1
|
||||
whisper_single_segment=1
|
||||
whisper_token_timestamps=1
|
||||
whisper_thold_pt=0.0100
|
||||
whisper_thold_ptsum=0.0100
|
||||
whisper_max_len=0
|
||||
whisper_split_on_word=0
|
||||
whisper_max_tokens=0
|
||||
whisper_audio_ctx=0
|
||||
whisper_tdrz_enable=0
|
||||
whisper_suppress_blank=1
|
||||
whisper_suppress_nst=1
|
||||
whisper_temperature=0.0000
|
||||
whisper_max_initial_ts=1.0000
|
||||
whisper_length_penalty=-1.0000
|
||||
whisper_temperature_inc=0.2000
|
||||
whisper_entropy_thold=2.4000
|
||||
whisper_logprob_thold=-1.0000
|
||||
whisper_no_speech_thold=0.6000
|
||||
whisper_greedy_best_of=1
|
||||
whisper_beam_size=5
|
||||
whisper_beam_patience=-1.0000
|
||||
|
||||
+34
-5
@@ -37,6 +37,10 @@ EXAMPLES:
|
||||
./increment_and_push.sh -m "Add live debug indicators"
|
||||
./increment_and_push.sh -M "Breaking config format update"
|
||||
./increment_and_push.sh -r -m "Release minor update"
|
||||
|
||||
NOTES:
|
||||
- If the working tree is dirty, the script auto-commits pending changes first
|
||||
as: prep: <commit message>
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -47,11 +51,36 @@ check_git_repo() {
|
||||
fi
|
||||
}
|
||||
|
||||
require_clean_worktree() {
|
||||
if ! git -C "$ROOT_DIR" diff --quiet || ! git -C "$ROOT_DIR" diff --cached --quiet; then
|
||||
print_error "Working tree is not clean. Commit or stash changes before running this script."
|
||||
exit 1
|
||||
has_pending_changes() {
|
||||
if ! git -C "$ROOT_DIR" diff --quiet; then
|
||||
return 0
|
||||
fi
|
||||
if ! git -C "$ROOT_DIR" diff --cached --quiet; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "$(git -C "$ROOT_DIR" ls-files --others --exclude-standard)" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
auto_commit_pending_changes() {
|
||||
local message="$1"
|
||||
|
||||
if ! has_pending_changes; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_warning "Working tree is dirty. Auto-committing pending changes before version bump."
|
||||
git -C "$ROOT_DIR" add -A
|
||||
|
||||
if git -C "$ROOT_DIR" diff --cached --quiet; then
|
||||
print_warning "No staged changes after auto-add; continuing."
|
||||
return 0
|
||||
fi
|
||||
|
||||
git -C "$ROOT_DIR" commit -m "prep: ${message}" >/dev/null
|
||||
print_success "Committed pending changes: prep: ${message}"
|
||||
}
|
||||
|
||||
latest_version_tag() {
|
||||
@@ -221,7 +250,7 @@ fi
|
||||
|
||||
print_status "voice_linux increment and push"
|
||||
check_git_repo
|
||||
require_clean_worktree
|
||||
auto_commit_pending_changes "$COMMIT_MESSAGE"
|
||||
|
||||
LATEST_TAG="$(latest_version_tag || true)"
|
||||
if [[ -z "$LATEST_TAG" ]]; then
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# GPU Passthrough in Qubes OS — Summary & Lessons Learned
|
||||
|
||||
## What We Did
|
||||
|
||||
Passed an NVIDIA GTX 1080 Ti (BDF `65:00.0` / `65:00.1`) through from Qubes dom0 to a StandaloneVM called `ai`, installed NVIDIA drivers and CUDA toolkit, and rebuilt whisper.cpp with CUDA support for GPU-accelerated speech-to-text.
|
||||
|
||||
## Environment
|
||||
|
||||
| Component | Detail |
|
||||
|-----------|--------|
|
||||
| Host OS | Qubes OS with Xen hypervisor |
|
||||
| dom0 display GPU | AMD (stays in dom0) |
|
||||
| Passthrough GPU | NVIDIA GeForce GTX 1080 Ti (Pascal/GP102, 11GB VRAM, compute 6.1) |
|
||||
| Second NVIDIA GPU | RTX 3050 6GB at BDF `17:00.0` (not used for this project) |
|
||||
| Target VM | `ai` — originally an AppVM, converted to StandaloneVM |
|
||||
| VM OS | Debian 13 (bookworm/trixie) with XFCE |
|
||||
| NVIDIA driver | 550.127.05 |
|
||||
| CUDA toolkit | 12.6 |
|
||||
|
||||
## Timeline of Steps
|
||||
|
||||
### Phase 0: dom0 GPU Passthrough
|
||||
|
||||
1. **Identified GPU BDF addresses** — `lspci` in dom0 showed two NVIDIA GPUs. We targeted the GTX 1080 Ti at `65:00.0` (GPU) and `65:00.1` (HDMI audio controller).
|
||||
|
||||
2. **Confirmed boot loader** — Verified Qubes was using GRUB (not systemd-boot) by checking `/etc/default/grub` existed.
|
||||
|
||||
3. **Hid GPU from dom0** — Added `rd.qubes.hide_pci=65:00.0,65:00.1` to `GRUB_CMDLINE_LINUX` in `/etc/default/grub`.
|
||||
|
||||
4. **Regenerated GRUB config** — `sudo grub2-mkconfig -o /boot/efi/EFI/qubes/grub.cfg` (EFI path, not legacy BIOS path).
|
||||
|
||||
5. **Rebooted dom0** — Full system reboot required for PCI hide to take effect.
|
||||
|
||||
6. **Verified GPU was assignable** — `xl pci-assignable-list` showed `0000:65:00.0` and `0000:65:00.1`.
|
||||
|
||||
7. **Attached GPU to VM** — `qvm-pci attach ai dom0:65_00.0 --persistent -o permissive=true` (and same for `65_00.1`).
|
||||
|
||||
### Phase 1: Driver & CUDA Installation (inside ai VM)
|
||||
|
||||
8. **Installed build prerequisites** — `build-essential`, `linux-headers-$(uname -r)`.
|
||||
|
||||
9. **Installed NVIDIA driver** — Downloaded `.run` installer, ran with `--no-opengl-files --dkms`.
|
||||
|
||||
10. **Installed CUDA toolkit** — Via NVIDIA's Debian repo + `cuda-toolkit-12-6`.
|
||||
|
||||
11. **Verified** — `nvidia-smi` showed GTX 1080 Ti, `nvcc --version` showed CUDA 12.6.
|
||||
|
||||
### Phase 2: Application Rebuild
|
||||
|
||||
12. **Rebuilt whisper.cpp with CUDA** — `WHISPER_CUDA=1 bash ./build.sh`.
|
||||
|
||||
13. **Verified CUDA linkage** — `ldd ./voice_linux | grep whisper|ggml` confirmed CUDA libraries linked.
|
||||
|
||||
14. **Tested GPU inference** — Ran with GPU mode, confirmed `nvidia-smi` showed process using GPU memory.
|
||||
|
||||
## Problems Encountered & Solutions
|
||||
|
||||
### Problem 1: AppVM vs StandaloneVM confusion
|
||||
|
||||
**What happened**: The `ai` VM was initially described as an AppVM (template-based). In an AppVM, anything installed outside `/home` is lost on reboot — NVIDIA drivers would vanish.
|
||||
|
||||
**Discovery**: User clarified it was actually a StandaloneVM, not template-based.
|
||||
|
||||
**Lesson**: Always verify VM type before planning driver installation. In Qubes:
|
||||
- **AppVM**: Root filesystem resets to template on reboot. Drivers must go in the template or use bind-dirs.
|
||||
- **StandaloneVM**: Full persistent root filesystem. Drivers persist normally.
|
||||
- Check with: `qvm-prefs ai virt_mode` and `qvm-ls --fields name,klass ai`
|
||||
|
||||
### Problem 2: VM must be shut down before PCI attach
|
||||
|
||||
**What happened**: Attempted to run `qvm-pci attach` while the `ai` VM was running. The attach appeared to succeed but the device wasn't visible inside the VM.
|
||||
|
||||
**Discovery**: `qvm-device pci list ai` showed empty even though attach command ran.
|
||||
|
||||
**Lesson**: The VM must be **shut down** before attaching PCI devices with `--persistent`. The workflow is:
|
||||
1. Shut down VM
|
||||
2. Attach PCI device
|
||||
3. Start VM
|
||||
4. Verify inside VM with `lspci`
|
||||
|
||||
### Problem 3: "Already assigned" error on re-attach
|
||||
|
||||
**What happened**: After a reboot cycle, running the attach command again produced an "already assigned" message.
|
||||
|
||||
**Lesson**: `--persistent` means the attachment survives reboots. Don't re-run the attach command after reboot — it's already configured. Verify with `qvm-pci list ai` from dom0.
|
||||
|
||||
### Problem 4: lspci not found inside VM
|
||||
|
||||
**What happened**: Tried to verify GPU visibility inside the VM but `lspci` wasn't installed.
|
||||
|
||||
**Solution**: `sudo apt install pciutils` then `lspci | grep -i nvidia`.
|
||||
|
||||
**Lesson**: Minimal Debian VMs may not have `pciutils` installed. Include it in prerequisites.
|
||||
|
||||
### Problem 5: Script couldn't be copy-pasted into dom0
|
||||
|
||||
**What happened**: Created a comprehensive dom0 shell script for GPU passthrough, but Qubes security model prevents clipboard paste from other VMs into dom0.
|
||||
|
||||
**Solution**: Provided numbered step-by-step commands that could be typed manually, and also created the script as a file that could be transferred via `qvm-run` or Qubes file copy.
|
||||
|
||||
**Lesson**: When automating dom0 tasks in Qubes:
|
||||
- Dom0 is intentionally isolated — no clipboard sharing
|
||||
- Scripts must be typed manually or transferred via `qvm-copy-to-vm` (from dom0 to VM) or `qvm-run -p` pipes
|
||||
- Keep dom0 scripts short and simple
|
||||
- Always include a revert mechanism
|
||||
|
||||
### Problem 6: Reboot scope confusion
|
||||
|
||||
**What happened**: Unclear whether "reboot" meant just the VM or the entire Qubes system (dom0 + all VMs).
|
||||
|
||||
**Clarification**: GRUB changes require a **full system reboot** (dom0 reboot, which takes down all VMs). PCI attachment changes only require the target VM to be restarted.
|
||||
|
||||
**Lesson**: Be explicit about reboot scope:
|
||||
- `sudo reboot` in dom0 = full system reboot
|
||||
- `qvm-shutdown ai && qvm-start ai` = just the VM
|
||||
|
||||
### Problem 7: Build accidentally ran without whisper support
|
||||
|
||||
**What happened**: After some iteration, a build was accidentally triggered with `WITH_WHISPER=0`, producing a binary that showed "[transcription unavailable: rebuild with WITH_WHISPER=1]".
|
||||
|
||||
**Solution**: Rebuilt with `WITH_WHISPER=1 bash ./build.sh`.
|
||||
|
||||
**Lesson**: The build system defaults matter. Our `build.sh` auto-detects whisper if the vendor directory exists, but explicit `WITH_WHISPER=0` overrides that. Always verify the build output includes whisper support with `ldd ./voice_linux | grep whisper`.
|
||||
|
||||
### Problem 8: Dirty git worktree blocking version increment
|
||||
|
||||
**What happened**: The `increment_and_push.sh` script refused to run because of uncommitted changes in the working tree.
|
||||
|
||||
**Solution**: Committed the intended changes first, then ran the increment script.
|
||||
|
||||
**Lesson**: The increment script enforces a clean worktree policy. Always commit your changes before running it. If there are unrelated/untracked files, `git stash` them first.
|
||||
|
||||
## Key Qubes-Specific Knowledge
|
||||
|
||||
### PCI Passthrough Essentials
|
||||
|
||||
```
|
||||
# Hide device from dom0 (GRUB, requires full reboot):
|
||||
rd.qubes.hide_pci=BDF1,BDF2
|
||||
|
||||
# Attach to VM (VM must be off):
|
||||
qvm-pci attach VMNAME dom0:BDF --persistent -o permissive=true
|
||||
|
||||
# Verify assignment:
|
||||
qvm-pci list VMNAME # from dom0
|
||||
lspci | grep -i nvidia # from inside VM
|
||||
```
|
||||
|
||||
### NVIDIA Driver in Qubes VM
|
||||
|
||||
- **Always use `--no-opengl-files`** — Qubes VMs use a virtual GPU for display. Installing OpenGL files would break the display.
|
||||
- **Use `--dkms`** — Ensures kernel module rebuilds on kernel updates.
|
||||
- **StandaloneVM recommended** — Avoids template pollution and persistence issues.
|
||||
- **`permissive=true`** — Required for NVIDIA GPUs in Qubes due to how they access PCI config space.
|
||||
|
||||
### What Persists Where
|
||||
|
||||
| VM Type | /home | /usr, /lib, /etc | Drivers |
|
||||
|---------|-------|-------------------|---------|
|
||||
| AppVM | ✅ Persists | ❌ Resets to template | ❌ Lost on reboot |
|
||||
| StandaloneVM | ✅ Persists | ✅ Persists | ✅ Persists |
|
||||
| Template | ✅ Persists | ✅ Persists | ✅ Persists (shared to AppVMs) |
|
||||
|
||||
### Dom0 Safety
|
||||
|
||||
- Dom0 has no network access by design
|
||||
- Clipboard is one-way (dom0 → VM, not VM → dom0) and requires explicit Ctrl+Shift+C/V
|
||||
- Always have a revert plan for GRUB changes (keep a backup of `/etc/default/grub`)
|
||||
- Log what you change — our script wrote to `/var/log/gpu_passthrough.log`
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ dom0 (Xen Hypervisor) │
|
||||
│ │
|
||||
│ AMD GPU ──── Display │
|
||||
│ GTX 1080 Ti ──── HIDDEN via rd.qubes.hide_pci │
|
||||
│ │ │
|
||||
│ │ PCI passthrough (qvm-pci attach --persistent) │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ ai StandaloneVM │ │
|
||||
│ │ │ │
|
||||
│ │ NVIDIA Driver 550.x (--no-opengl-files --dkms) │ │
|
||||
│ │ │ │ │
|
||||
│ │ CUDA Toolkit 12.6 │ │
|
||||
│ │ │ │ │
|
||||
│ │ whisper.cpp (GGML_CUDA=ON) │ │
|
||||
│ │ │ │ │
|
||||
│ │ voice_linux (GPU-accelerated transcription) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Performance Results
|
||||
|
||||
| Model | VRAM Used | Decode Time (10s audio) | Quality |
|
||||
|-------|-----------|------------------------|---------|
|
||||
| tiny.en | ~1GB | <0.5s | Fair |
|
||||
| base.en | ~1GB | <1s | Good |
|
||||
| small.en | ~2GB | ~1-2s | Very good |
|
||||
| medium.en | ~5GB | ~2-3s | Excellent |
|
||||
| large-v3-turbo | ~6GB | ~3-4s | Excellent+ |
|
||||
| large-v3 | ~10GB | ~4-6s | Best |
|
||||
|
||||
The GTX 1080 Ti with 11GB VRAM can run all models including large-v3. For real-time dictation, medium.en provides the best accuracy-to-speed tradeoff.
|
||||
|
||||
## Files Created During This Process
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| [`plans/gpu_enablement_plan.md`](gpu_enablement_plan.md) | Step-by-step GPU passthrough and driver installation plan |
|
||||
| [`plans/architecture.md`](architecture.md) | Full system architecture including hardware details |
|
||||
| [`scripts/download_models.sh`](../scripts/download_models.sh) | Downloads all compatible whisper models |
|
||||
|
||||
## If You Had to Do It Again
|
||||
|
||||
1. **Verify VM type first** — `qvm-ls --fields name,klass ai` before planning anything
|
||||
2. **Shut down VM before PCI operations** — always
|
||||
3. **Back up GRUB config** — `sudo cp /etc/default/grub /etc/default/grub.bak` before editing
|
||||
4. **Install pciutils early** — `sudo apt install pciutils` so you can verify GPU visibility
|
||||
5. **Use the .run installer, not apt packages** — NVIDIA's apt packages for Debian can conflict with Qubes' virtual GPU setup; the `.run` installer with `--no-opengl-files` is cleaner
|
||||
6. **Test CPU mode first** — Build and verify voice_linux works on CPU before adding GPU complexity
|
||||
7. **Keep dom0 commands minimal** — Type them manually, don't try to automate complex scripts in dom0
|
||||
@@ -0,0 +1,202 @@
|
||||
# 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
|
||||
|
||||
```c
|
||||
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_t` and `voice_cmd_list_t` structs
|
||||
- `void 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:
|
||||
1. User clicks Capture
|
||||
2. Button label changes to "Press key combo now..."
|
||||
3. Window key-press-event handler captures next keypress
|
||||
4. Records keyval + modifier state
|
||||
5. Formats display string like "Ctrl+Shift+Return"
|
||||
6. 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
|
||||
|
||||
1. **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.
|
||||
|
||||
2. **Case sensitivity**: Whisper may output "Period" or "period" depending on sentence position. All matching is case-insensitive.
|
||||
|
||||
3. **Multi-word triggers**: "end of command" requires multi-word scanning. Sort triggers longest-first so "end of command" matches before "end".
|
||||
|
||||
4. **Escape sequences in text values**: Support \n, \t, \\ in the text replacement field. Parse on load, display escaped in UI.
|
||||
|
||||
5. **Key capture conflicts**: While capturing, consume the event so it doesn't trigger other handlers. Disconnect capture handler after first keypress.
|
||||
|
||||
6. **Empty transcript after replacements**: If the entire transcript was voice commands with no remaining text, skip autotype and transcript append.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Create src/voice_cmd.h and src/voice_cmd.c with data structures and load/save
|
||||
2. Add typer_send_keycombo() to src/typer.h / src/typer.c
|
||||
3. Implement voice_cmd_apply() core matching logic
|
||||
4. Build the Commands tab UI in src/gui_main.c
|
||||
5. Wire key capture handler
|
||||
6. Hook voice_cmd_apply() into process_segment()
|
||||
7. Create default voice_commands.ini
|
||||
8. Update Makefile and build.sh
|
||||
9. Build and test
|
||||
+169
@@ -39,6 +39,43 @@ void config_set_defaults(voice_config_t *cfg) {
|
||||
cfg->vad_max_speech_ms = 15000;
|
||||
cfg->autotype_enabled = 1;
|
||||
cfg->filter_bracketed_tags = 1;
|
||||
|
||||
cfg->whisper_sampling_strategy = 0;
|
||||
cfg->whisper_n_max_text_ctx = 16384;
|
||||
cfg->whisper_offset_ms = 0;
|
||||
cfg->whisper_duration_ms = 0;
|
||||
|
||||
cfg->whisper_translate = 0;
|
||||
cfg->whisper_detect_language = 0;
|
||||
cfg->whisper_no_context = 0;
|
||||
cfg->whisper_no_timestamps = 1;
|
||||
cfg->whisper_single_segment = 0;
|
||||
|
||||
cfg->whisper_token_timestamps = 0;
|
||||
cfg->whisper_thold_pt = 0.01f;
|
||||
cfg->whisper_thold_ptsum = 0.01f;
|
||||
cfg->whisper_max_len = 0;
|
||||
cfg->whisper_split_on_word = 0;
|
||||
cfg->whisper_max_tokens = 0;
|
||||
|
||||
cfg->whisper_audio_ctx = 0;
|
||||
cfg->whisper_tdrz_enable = 0;
|
||||
|
||||
cfg->whisper_suppress_blank = 1;
|
||||
cfg->whisper_suppress_nst = 0;
|
||||
|
||||
cfg->whisper_temperature = 0.0f;
|
||||
cfg->whisper_max_initial_ts = 1.0f;
|
||||
cfg->whisper_length_penalty = -1.0f;
|
||||
|
||||
cfg->whisper_temperature_inc = 0.2f;
|
||||
cfg->whisper_entropy_thold = 2.4f;
|
||||
cfg->whisper_logprob_thold = -1.0f;
|
||||
cfg->whisper_no_speech_thold = 0.6f;
|
||||
|
||||
cfg->whisper_greedy_best_of = 1;
|
||||
cfg->whisper_beam_size = 5;
|
||||
cfg->whisper_beam_patience = -1.0f;
|
||||
}
|
||||
|
||||
int config_load_file(const char *path, voice_config_t *cfg) {
|
||||
@@ -97,9 +134,141 @@ int config_load_file(const char *path, voice_config_t *cfg) {
|
||||
cfg->autotype_enabled = atoi(value);
|
||||
} else if (strcmp(key, "filter_bracketed_tags") == 0) {
|
||||
cfg->filter_bracketed_tags = atoi(value);
|
||||
} else if (strcmp(key, "whisper_sampling_strategy") == 0) {
|
||||
cfg->whisper_sampling_strategy = atoi(value);
|
||||
} else if (strcmp(key, "whisper_n_max_text_ctx") == 0) {
|
||||
cfg->whisper_n_max_text_ctx = atoi(value);
|
||||
} else if (strcmp(key, "whisper_offset_ms") == 0) {
|
||||
cfg->whisper_offset_ms = atoi(value);
|
||||
} else if (strcmp(key, "whisper_duration_ms") == 0) {
|
||||
cfg->whisper_duration_ms = atoi(value);
|
||||
} else if (strcmp(key, "whisper_translate") == 0) {
|
||||
cfg->whisper_translate = atoi(value);
|
||||
} else if (strcmp(key, "whisper_detect_language") == 0) {
|
||||
cfg->whisper_detect_language = atoi(value);
|
||||
} else if (strcmp(key, "whisper_no_context") == 0) {
|
||||
cfg->whisper_no_context = atoi(value);
|
||||
} else if (strcmp(key, "whisper_no_timestamps") == 0) {
|
||||
cfg->whisper_no_timestamps = atoi(value);
|
||||
} else if (strcmp(key, "whisper_single_segment") == 0) {
|
||||
cfg->whisper_single_segment = atoi(value);
|
||||
} else if (strcmp(key, "whisper_token_timestamps") == 0) {
|
||||
cfg->whisper_token_timestamps = atoi(value);
|
||||
} else if (strcmp(key, "whisper_thold_pt") == 0) {
|
||||
cfg->whisper_thold_pt = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_thold_ptsum") == 0) {
|
||||
cfg->whisper_thold_ptsum = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_max_len") == 0) {
|
||||
cfg->whisper_max_len = atoi(value);
|
||||
} else if (strcmp(key, "whisper_split_on_word") == 0) {
|
||||
cfg->whisper_split_on_word = atoi(value);
|
||||
} else if (strcmp(key, "whisper_max_tokens") == 0) {
|
||||
cfg->whisper_max_tokens = atoi(value);
|
||||
} else if (strcmp(key, "whisper_audio_ctx") == 0) {
|
||||
cfg->whisper_audio_ctx = atoi(value);
|
||||
} else if (strcmp(key, "whisper_tdrz_enable") == 0) {
|
||||
cfg->whisper_tdrz_enable = atoi(value);
|
||||
} else if (strcmp(key, "whisper_suppress_blank") == 0) {
|
||||
cfg->whisper_suppress_blank = atoi(value);
|
||||
} else if (strcmp(key, "whisper_suppress_nst") == 0) {
|
||||
cfg->whisper_suppress_nst = atoi(value);
|
||||
} else if (strcmp(key, "whisper_temperature") == 0) {
|
||||
cfg->whisper_temperature = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_max_initial_ts") == 0) {
|
||||
cfg->whisper_max_initial_ts = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_length_penalty") == 0) {
|
||||
cfg->whisper_length_penalty = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_temperature_inc") == 0) {
|
||||
cfg->whisper_temperature_inc = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_entropy_thold") == 0) {
|
||||
cfg->whisper_entropy_thold = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_logprob_thold") == 0) {
|
||||
cfg->whisper_logprob_thold = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_no_speech_thold") == 0) {
|
||||
cfg->whisper_no_speech_thold = (float)atof(value);
|
||||
} else if (strcmp(key, "whisper_greedy_best_of") == 0) {
|
||||
cfg->whisper_greedy_best_of = atoi(value);
|
||||
} else if (strcmp(key, "whisper_beam_size") == 0) {
|
||||
cfg->whisper_beam_size = atoi(value);
|
||||
} else if (strcmp(key, "whisper_beam_patience") == 0) {
|
||||
cfg->whisper_beam_patience = (float)atof(value);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_save_file(const char *path, const voice_config_t *cfg) {
|
||||
if (!path || !cfg) return -1;
|
||||
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) return -2;
|
||||
|
||||
fprintf(f, "# voice_linux runtime configuration\n");
|
||||
fprintf(f, "# Hotkey format supports Ctrl, Alt, Shift, Super + final key symbol\n");
|
||||
fprintf(f, "# Example: Ctrl+Alt+V\n\n");
|
||||
|
||||
fprintf(f, "model_path=%s\n", cfg->model_path);
|
||||
fprintf(f, "use_gpu=%d\n", cfg->use_gpu ? 1 : 0);
|
||||
fprintf(f, "audio_device=%s\n", cfg->audio_device);
|
||||
fprintf(f, "sample_rate=%d\n", cfg->sample_rate);
|
||||
fprintf(f, "language=%s\n", cfg->language);
|
||||
fprintf(f, "hotkey=%s\n", cfg->hotkey);
|
||||
fprintf(f, "type_delay_us=%d\n", cfg->type_delay_us);
|
||||
fprintf(f, "max_record_seconds=%d\n", cfg->max_record_seconds);
|
||||
fprintf(f, "# always-on VAD controls\n");
|
||||
fprintf(f, "always_on=%d\n", cfg->always_on ? 1 : 0);
|
||||
fprintf(f, "# legacy window size (unused by new VAD pipeline, kept for compatibility)\n");
|
||||
fprintf(f, "always_on_window_ms=%d\n\n", cfg->always_on_window_ms);
|
||||
|
||||
fprintf(f, "# live level threshold (0.0 - 1.0)\n");
|
||||
fprintf(f, "vad_peak_threshold=%.4f\n", cfg->vad_peak_threshold);
|
||||
fprintf(f, "# level must stay above threshold this long before speech starts\n");
|
||||
fprintf(f, "vad_trigger_ms=%d\n", cfg->vad_trigger_ms);
|
||||
fprintf(f, "# level must stay below threshold this long before speech ends\n");
|
||||
fprintf(f, "vad_release_ms=%d\n", cfg->vad_release_ms);
|
||||
fprintf(f, "# include this much pre-trigger audio in transcript segment\n");
|
||||
fprintf(f, "vad_preroll_ms=%d\n", cfg->vad_preroll_ms);
|
||||
fprintf(f, "# discard segments shorter than this\n");
|
||||
fprintf(f, "vad_min_speech_ms=%d\n", cfg->vad_min_speech_ms);
|
||||
fprintf(f, "# hard cap for one speech segment\n");
|
||||
fprintf(f, "vad_max_speech_ms=%d\n\n", cfg->vad_max_speech_ms);
|
||||
|
||||
fprintf(f, "autotype_enabled=%d\n", cfg->autotype_enabled ? 1 : 0);
|
||||
fprintf(f, "filter_bracketed_tags=%d\n\n", cfg->filter_bracketed_tags ? 1 : 0);
|
||||
|
||||
fprintf(f, "# whisper decode controls\n");
|
||||
fprintf(f, "whisper_sampling_strategy=%d\n", cfg->whisper_sampling_strategy);
|
||||
fprintf(f, "whisper_n_max_text_ctx=%d\n", cfg->whisper_n_max_text_ctx);
|
||||
fprintf(f, "whisper_offset_ms=%d\n", cfg->whisper_offset_ms);
|
||||
fprintf(f, "whisper_duration_ms=%d\n", cfg->whisper_duration_ms);
|
||||
fprintf(f, "whisper_translate=%d\n", cfg->whisper_translate ? 1 : 0);
|
||||
fprintf(f, "whisper_detect_language=%d\n", cfg->whisper_detect_language ? 1 : 0);
|
||||
fprintf(f, "whisper_no_context=%d\n", cfg->whisper_no_context ? 1 : 0);
|
||||
fprintf(f, "whisper_no_timestamps=%d\n", cfg->whisper_no_timestamps ? 1 : 0);
|
||||
fprintf(f, "whisper_single_segment=%d\n", cfg->whisper_single_segment ? 1 : 0);
|
||||
fprintf(f, "whisper_token_timestamps=%d\n", cfg->whisper_token_timestamps ? 1 : 0);
|
||||
fprintf(f, "whisper_thold_pt=%.4f\n", cfg->whisper_thold_pt);
|
||||
fprintf(f, "whisper_thold_ptsum=%.4f\n", cfg->whisper_thold_ptsum);
|
||||
fprintf(f, "whisper_max_len=%d\n", cfg->whisper_max_len);
|
||||
fprintf(f, "whisper_split_on_word=%d\n", cfg->whisper_split_on_word ? 1 : 0);
|
||||
fprintf(f, "whisper_max_tokens=%d\n", cfg->whisper_max_tokens);
|
||||
fprintf(f, "whisper_audio_ctx=%d\n", cfg->whisper_audio_ctx);
|
||||
fprintf(f, "whisper_tdrz_enable=%d\n", cfg->whisper_tdrz_enable ? 1 : 0);
|
||||
fprintf(f, "whisper_suppress_blank=%d\n", cfg->whisper_suppress_blank ? 1 : 0);
|
||||
fprintf(f, "whisper_suppress_nst=%d\n", cfg->whisper_suppress_nst ? 1 : 0);
|
||||
fprintf(f, "whisper_temperature=%.4f\n", cfg->whisper_temperature);
|
||||
fprintf(f, "whisper_max_initial_ts=%.4f\n", cfg->whisper_max_initial_ts);
|
||||
fprintf(f, "whisper_length_penalty=%.4f\n", cfg->whisper_length_penalty);
|
||||
fprintf(f, "whisper_temperature_inc=%.4f\n", cfg->whisper_temperature_inc);
|
||||
fprintf(f, "whisper_entropy_thold=%.4f\n", cfg->whisper_entropy_thold);
|
||||
fprintf(f, "whisper_logprob_thold=%.4f\n", cfg->whisper_logprob_thold);
|
||||
fprintf(f, "whisper_no_speech_thold=%.4f\n", cfg->whisper_no_speech_thold);
|
||||
fprintf(f, "whisper_greedy_best_of=%d\n", cfg->whisper_greedy_best_of);
|
||||
fprintf(f, "whisper_beam_size=%d\n", cfg->whisper_beam_size);
|
||||
fprintf(f, "whisper_beam_patience=%.4f\n", cfg->whisper_beam_patience);
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -22,9 +22,47 @@ typedef struct {
|
||||
int vad_max_speech_ms;
|
||||
int autotype_enabled;
|
||||
int filter_bracketed_tags;
|
||||
|
||||
int whisper_sampling_strategy; // 0=greedy, 1=beam search
|
||||
int whisper_n_max_text_ctx;
|
||||
int whisper_offset_ms;
|
||||
int whisper_duration_ms;
|
||||
|
||||
int whisper_translate;
|
||||
int whisper_detect_language;
|
||||
int whisper_no_context;
|
||||
int whisper_no_timestamps;
|
||||
int whisper_single_segment;
|
||||
|
||||
int whisper_token_timestamps;
|
||||
float whisper_thold_pt;
|
||||
float whisper_thold_ptsum;
|
||||
int whisper_max_len;
|
||||
int whisper_split_on_word;
|
||||
int whisper_max_tokens;
|
||||
|
||||
int whisper_audio_ctx;
|
||||
int whisper_tdrz_enable;
|
||||
|
||||
int whisper_suppress_blank;
|
||||
int whisper_suppress_nst;
|
||||
|
||||
float whisper_temperature;
|
||||
float whisper_max_initial_ts;
|
||||
float whisper_length_penalty;
|
||||
|
||||
float whisper_temperature_inc;
|
||||
float whisper_entropy_thold;
|
||||
float whisper_logprob_thold;
|
||||
float whisper_no_speech_thold;
|
||||
|
||||
int whisper_greedy_best_of;
|
||||
int whisper_beam_size;
|
||||
float whisper_beam_patience;
|
||||
} voice_config_t;
|
||||
|
||||
void config_set_defaults(voice_config_t *cfg);
|
||||
int config_load_file(const char *path, voice_config_t *cfg);
|
||||
int config_save_file(const char *path, const voice_config_t *cfg);
|
||||
|
||||
#endif
|
||||
|
||||
+915
-40
File diff suppressed because it is too large
Load Diff
+57
-4
@@ -80,6 +80,49 @@ void transcribe_set_filter_bracketed_tags(int enabled) {
|
||||
static struct whisper_context *g_ctx = NULL;
|
||||
static transcribe_params_t g_params;
|
||||
|
||||
static void apply_full_params_from_config(struct whisper_full_params *dst, const transcribe_params_t *src) {
|
||||
if (!dst || !src) return;
|
||||
|
||||
dst->n_threads = src->n_threads > 0 ? src->n_threads : 4;
|
||||
dst->n_max_text_ctx = src->n_max_text_ctx;
|
||||
dst->offset_ms = src->offset_ms;
|
||||
dst->duration_ms = src->duration_ms;
|
||||
|
||||
dst->translate = src->translate ? true : false;
|
||||
dst->detect_language = src->detect_language ? true : false;
|
||||
dst->no_context = src->no_context ? true : false;
|
||||
dst->no_timestamps = src->no_timestamps ? true : false;
|
||||
dst->single_segment = src->single_segment ? true : false;
|
||||
|
||||
dst->token_timestamps = src->token_timestamps ? true : false;
|
||||
dst->thold_pt = src->thold_pt;
|
||||
dst->thold_ptsum = src->thold_ptsum;
|
||||
dst->max_len = src->max_len;
|
||||
dst->split_on_word = src->split_on_word ? true : false;
|
||||
dst->max_tokens = src->max_tokens;
|
||||
|
||||
dst->audio_ctx = src->audio_ctx;
|
||||
dst->tdrz_enable = src->tdrz_enable ? true : false;
|
||||
|
||||
dst->suppress_blank = src->suppress_blank ? true : false;
|
||||
dst->suppress_nst = src->suppress_nst ? true : false;
|
||||
|
||||
dst->temperature = src->temperature;
|
||||
dst->max_initial_ts = src->max_initial_ts;
|
||||
dst->length_penalty = src->length_penalty;
|
||||
|
||||
dst->temperature_inc = src->temperature_inc;
|
||||
dst->entropy_thold = src->entropy_thold;
|
||||
dst->logprob_thold = src->logprob_thold;
|
||||
dst->no_speech_thold = src->no_speech_thold;
|
||||
|
||||
dst->greedy.best_of = src->greedy_best_of;
|
||||
dst->beam_search.beam_size = src->beam_size;
|
||||
dst->beam_search.patience = src->beam_patience;
|
||||
|
||||
dst->language = src->language[0] ? src->language : "en";
|
||||
}
|
||||
|
||||
int transcribe_init(const transcribe_params_t *params) {
|
||||
if (!params) return -1;
|
||||
g_params = *params;
|
||||
@@ -95,17 +138,22 @@ int transcribe_init(const transcribe_params_t *params) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void transcribe_update_params(const transcribe_params_t *params) {
|
||||
if (!params) return;
|
||||
g_params = *params;
|
||||
transcribe_set_filter_bracketed_tags(g_params.filter_bracketed_tags);
|
||||
}
|
||||
|
||||
char *transcribe_buffer(const float *samples, size_t count) {
|
||||
if (!g_ctx || !samples || count == 0) return NULL;
|
||||
|
||||
struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
|
||||
enum whisper_sampling_strategy strategy = g_params.sampling_strategy ? WHISPER_SAMPLING_BEAM_SEARCH : WHISPER_SAMPLING_GREEDY;
|
||||
struct whisper_full_params params = whisper_full_default_params(strategy);
|
||||
params.print_progress = false;
|
||||
params.print_special = false;
|
||||
params.print_realtime = false;
|
||||
params.print_timestamps = false;
|
||||
params.translate = false;
|
||||
params.n_threads = g_params.n_threads > 0 ? g_params.n_threads : 4;
|
||||
params.language = g_params.language[0] ? g_params.language : "en";
|
||||
apply_full_params_from_config(¶ms, &g_params);
|
||||
|
||||
if (whisper_full(g_ctx, params, samples, (int)count) != 0) {
|
||||
fprintf(stderr, "transcribe: whisper_full failed\n");
|
||||
@@ -161,6 +209,11 @@ int transcribe_init(const transcribe_params_t *params) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void transcribe_update_params(const transcribe_params_t *params) {
|
||||
if (!params) return;
|
||||
g_params = *params;
|
||||
}
|
||||
|
||||
char *transcribe_buffer(const float *samples, size_t count) {
|
||||
(void)samples;
|
||||
(void)count;
|
||||
|
||||
@@ -9,9 +9,47 @@ typedef struct {
|
||||
int use_gpu;
|
||||
int n_threads;
|
||||
int filter_bracketed_tags;
|
||||
|
||||
int sampling_strategy; // 0=greedy, 1=beam search
|
||||
int n_max_text_ctx;
|
||||
int offset_ms;
|
||||
int duration_ms;
|
||||
|
||||
int translate;
|
||||
int detect_language;
|
||||
int no_context;
|
||||
int no_timestamps;
|
||||
int single_segment;
|
||||
|
||||
int token_timestamps;
|
||||
float thold_pt;
|
||||
float thold_ptsum;
|
||||
int max_len;
|
||||
int split_on_word;
|
||||
int max_tokens;
|
||||
|
||||
int audio_ctx;
|
||||
int tdrz_enable;
|
||||
|
||||
int suppress_blank;
|
||||
int suppress_nst;
|
||||
|
||||
float temperature;
|
||||
float max_initial_ts;
|
||||
float length_penalty;
|
||||
|
||||
float temperature_inc;
|
||||
float entropy_thold;
|
||||
float logprob_thold;
|
||||
float no_speech_thold;
|
||||
|
||||
int greedy_best_of;
|
||||
int beam_size;
|
||||
float beam_patience;
|
||||
} transcribe_params_t;
|
||||
|
||||
int transcribe_init(const transcribe_params_t *params);
|
||||
void transcribe_update_params(const transcribe_params_t *params);
|
||||
void transcribe_set_filter_bracketed_tags(int enabled);
|
||||
char *transcribe_buffer(const float *samples, size_t count);
|
||||
void transcribe_cleanup(void);
|
||||
|
||||
+47
@@ -182,6 +182,53 @@ int typer_type_text(const char *text) {
|
||||
return failures == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
int typer_send_keycombo(unsigned int keyval, unsigned int modifiers) {
|
||||
if (!g_dpy || keyval == 0) return -1;
|
||||
|
||||
KeyCode kc = XKeysymToKeycode(g_dpy, (KeySym)keyval);
|
||||
if (!kc) return -1;
|
||||
|
||||
KeyCode ctrl_kc = XKeysymToKeycode(g_dpy, XK_Control_L);
|
||||
KeyCode shift_kc = XKeysymToKeycode(g_dpy, XK_Shift_L);
|
||||
KeyCode alt_kc = XKeysymToKeycode(g_dpy, XK_Alt_L);
|
||||
KeyCode super_kc = XKeysymToKeycode(g_dpy, XK_Super_L);
|
||||
|
||||
if ((modifiers & (1u << 1)) && ctrl_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, ctrl_kc, True, CurrentTime)) return -1;
|
||||
}
|
||||
if ((modifiers & (1u << 0)) && shift_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, shift_kc, True, CurrentTime)) return -1;
|
||||
}
|
||||
if ((modifiers & (1u << 2)) && alt_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, alt_kc, True, CurrentTime)) return -1;
|
||||
}
|
||||
if ((modifiers & (1u << 3)) && super_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, super_kc, True, CurrentTime)) return -1;
|
||||
}
|
||||
|
||||
if (!XTestFakeKeyEvent(g_dpy, kc, True, CurrentTime)) return -1;
|
||||
XSync(g_dpy, False);
|
||||
sleep_us(g_key_hold_us);
|
||||
if (!XTestFakeKeyEvent(g_dpy, kc, False, CurrentTime)) return -1;
|
||||
|
||||
if ((modifiers & (1u << 3)) && super_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, super_kc, False, CurrentTime)) return -1;
|
||||
}
|
||||
if ((modifiers & (1u << 2)) && alt_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, alt_kc, False, CurrentTime)) return -1;
|
||||
}
|
||||
if ((modifiers & (1u << 0)) && shift_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, shift_kc, False, CurrentTime)) return -1;
|
||||
}
|
||||
if ((modifiers & (1u << 1)) && ctrl_kc) {
|
||||
if (!XTestFakeKeyEvent(g_dpy, ctrl_kc, False, CurrentTime)) return -1;
|
||||
}
|
||||
|
||||
XSync(g_dpy, False);
|
||||
sleep_us(g_delay_us);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void typer_cleanup(void) {
|
||||
g_target_window = None;
|
||||
if (g_dpy) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
int typer_init(int type_delay_us);
|
||||
int typer_capture_focus(void);
|
||||
int typer_type_text(const char *text);
|
||||
int typer_send_keycombo(unsigned int keyval, unsigned int modifiers);
|
||||
void typer_cleanup(void);
|
||||
|
||||
#endif
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
#ifndef VOICE_VERSION_H
|
||||
#define VOICE_VERSION_H
|
||||
|
||||
#define VOICE_LINUX_VERSION "v0.0.6"
|
||||
#define VOICE_LINUX_VERSION "v0.0.9"
|
||||
#define VOICE_LINUX_VERSION_MAJOR 0
|
||||
#define VOICE_LINUX_VERSION_MINOR 0
|
||||
#define VOICE_LINUX_VERSION_PATCH 6
|
||||
#define VOICE_LINUX_VERSION_PATCH 9
|
||||
|
||||
#endif
|
||||
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
#include "voice_cmd.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void trim(char *s) {
|
||||
if (!s) return;
|
||||
size_t len = strlen(s);
|
||||
while (len > 0 && isspace((unsigned char)s[len - 1])) {
|
||||
s[--len] = '\0';
|
||||
}
|
||||
char *p = s;
|
||||
while (*p && isspace((unsigned char)*p)) p++;
|
||||
if (p != s) {
|
||||
memmove(s, p, strlen(p) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
static int str_ieq_n(const char *a, const char *b, size_t n) {
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
unsigned char ca = (unsigned char)a[i];
|
||||
unsigned char cb = (unsigned char)b[i];
|
||||
if (tolower(ca) != tolower(cb)) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int is_word_char(unsigned char c) {
|
||||
return isalnum(c) || c == '_';
|
||||
}
|
||||
|
||||
static int matches_whole_word_ci(const char *text, size_t pos, const char *trig, size_t trig_len) {
|
||||
if (!text || !trig || trig_len == 0) return 0;
|
||||
if (!str_ieq_n(text + pos, trig, trig_len)) return 0;
|
||||
|
||||
unsigned char left = (pos == 0) ? 0 : (unsigned char)text[pos - 1];
|
||||
unsigned char right = (unsigned char)text[pos + trig_len];
|
||||
|
||||
if (pos > 0 && is_word_char(left)) return 0;
|
||||
if (right != '\0' && is_word_char(right)) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void unescape_copy(const char *src, char *dst, size_t dst_sz) {
|
||||
if (!dst || dst_sz == 0) return;
|
||||
dst[0] = '\0';
|
||||
if (!src) return;
|
||||
|
||||
size_t w = 0;
|
||||
for (size_t i = 0; src[i] != '\0' && w + 1 < dst_sz; ++i) {
|
||||
if (src[i] == '\\' && src[i + 1] != '\0') {
|
||||
i++;
|
||||
char c = src[i];
|
||||
if (c == 'n') dst[w++] = '\n';
|
||||
else if (c == 't') dst[w++] = '\t';
|
||||
else if (c == 'r') dst[w++] = '\r';
|
||||
else dst[w++] = c;
|
||||
} else {
|
||||
dst[w++] = src[i];
|
||||
}
|
||||
}
|
||||
dst[w] = '\0';
|
||||
}
|
||||
|
||||
static void escape_copy(const char *src, char *dst, size_t dst_sz) {
|
||||
if (!dst || dst_sz == 0) return;
|
||||
dst[0] = '\0';
|
||||
if (!src) return;
|
||||
|
||||
size_t w = 0;
|
||||
for (size_t i = 0; src[i] != '\0' && w + 1 < dst_sz; ++i) {
|
||||
char c = src[i];
|
||||
const char *rep = NULL;
|
||||
if (c == '\n') rep = "\\n";
|
||||
else if (c == '\t') rep = "\\t";
|
||||
else if (c == '\r') rep = "\\r";
|
||||
else if (c == '\\') rep = "\\\\";
|
||||
|
||||
if (rep) {
|
||||
for (size_t j = 0; rep[j] != '\0' && w + 1 < dst_sz; ++j) {
|
||||
dst[w++] = rep[j];
|
||||
}
|
||||
} else {
|
||||
dst[w++] = c;
|
||||
}
|
||||
}
|
||||
dst[w] = '\0';
|
||||
}
|
||||
|
||||
void voice_cmd_format_key_combo(unsigned int keyval, unsigned int modifiers, char *out, size_t out_sz) {
|
||||
if (!out || out_sz == 0) return;
|
||||
|
||||
char keyname[64];
|
||||
if (keyval >= 32 && keyval < 127) {
|
||||
snprintf(keyname, sizeof(keyname), "%c", (char)keyval);
|
||||
} else {
|
||||
switch (keyval) {
|
||||
case 65293: snprintf(keyname, sizeof(keyname), "Return"); break;
|
||||
case 65289: snprintf(keyname, sizeof(keyname), "Tab"); break;
|
||||
case 65307: snprintf(keyname, sizeof(keyname), "Escape"); break;
|
||||
case 65535: snprintf(keyname, sizeof(keyname), "Delete"); break;
|
||||
case 65288: snprintf(keyname, sizeof(keyname), "BackSpace"); break;
|
||||
default: snprintf(keyname, sizeof(keyname), "Key%u", keyval); break;
|
||||
}
|
||||
}
|
||||
|
||||
char pref[96];
|
||||
pref[0] = '\0';
|
||||
if (modifiers & VOICE_CMD_MOD_CTRL) strncat(pref, "Ctrl+", sizeof(pref) - strlen(pref) - 1);
|
||||
if (modifiers & VOICE_CMD_MOD_SHIFT) strncat(pref, "Shift+", sizeof(pref) - strlen(pref) - 1);
|
||||
if (modifiers & VOICE_CMD_MOD_ALT) strncat(pref, "Alt+", sizeof(pref) - strlen(pref) - 1);
|
||||
if (modifiers & VOICE_CMD_MOD_SUPER) strncat(pref, "Super+", sizeof(pref) - strlen(pref) - 1);
|
||||
|
||||
snprintf(out, out_sz, "%s%s", pref, keyname);
|
||||
}
|
||||
|
||||
void voice_cmd_init(voice_cmd_list_t *list) {
|
||||
if (!list) return;
|
||||
memset(list, 0, sizeof(*list));
|
||||
list->enabled = 1;
|
||||
}
|
||||
|
||||
void voice_cmd_free(voice_cmd_list_t *list) {
|
||||
if (!list) return;
|
||||
free(list->cmds);
|
||||
memset(list, 0, sizeof(*list));
|
||||
list->enabled = 1;
|
||||
}
|
||||
|
||||
static int ensure_capacity(voice_cmd_list_t *list, int min_cap) {
|
||||
if (!list) return -1;
|
||||
if (list->capacity >= min_cap) return 0;
|
||||
|
||||
int next = list->capacity > 0 ? list->capacity * 2 : 16;
|
||||
if (next < min_cap) next = min_cap;
|
||||
|
||||
void *p = realloc(list->cmds, (size_t)next * sizeof(voice_cmd_t));
|
||||
if (!p) return -1;
|
||||
list->cmds = (voice_cmd_t *)p;
|
||||
list->capacity = next;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int voice_cmd_add(voice_cmd_list_t *list, const voice_cmd_t *cmd) {
|
||||
if (!list || !cmd) return -1;
|
||||
if (cmd->trigger[0] == '\0') return -2;
|
||||
if (ensure_capacity(list, list->count + 1) != 0) return -3;
|
||||
list->cmds[list->count++] = *cmd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int voice_cmd_remove(voice_cmd_list_t *list, int index) {
|
||||
if (!list) return -1;
|
||||
if (index < 0 || index >= list->count) return -2;
|
||||
for (int i = index; i + 1 < list->count; ++i) {
|
||||
list->cmds[i] = list->cmds[i + 1];
|
||||
}
|
||||
list->count--;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int voice_cmd_seed_defaults(voice_cmd_list_t *list) {
|
||||
if (!list) return -1;
|
||||
|
||||
static const struct {
|
||||
const char *trigger;
|
||||
const char *text;
|
||||
} defs[] = {
|
||||
{"period", "."},
|
||||
{"question mark", "?"},
|
||||
{"exclamation mark", "!"},
|
||||
{"comma", ","},
|
||||
{"colon", ":"},
|
||||
{"semicolon", ";"},
|
||||
{"new line", "\n"},
|
||||
{"new paragraph", "\n\n"},
|
||||
{"tab", "\t"},
|
||||
{"open paren", "("},
|
||||
{"close paren", ")"},
|
||||
{"open bracket", "["},
|
||||
{"close bracket", "]"},
|
||||
{"open brace", "{"},
|
||||
{"close brace", "}"},
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(defs) / sizeof(defs[0]); ++i) {
|
||||
voice_cmd_t c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
snprintf(c.trigger, sizeof(c.trigger), "%s", defs[i].trigger);
|
||||
c.type = VOICE_CMD_TEXT;
|
||||
snprintf(c.text_value, sizeof(c.text_value), "%s", defs[i].text);
|
||||
if (voice_cmd_add(list, &c) != 0) return -2;
|
||||
}
|
||||
|
||||
voice_cmd_t over;
|
||||
memset(&over, 0, sizeof(over));
|
||||
snprintf(over.trigger, sizeof(over.trigger), "over");
|
||||
over.type = VOICE_CMD_KEYCOMBO;
|
||||
over.keyval = 65293;
|
||||
over.modifiers = 0;
|
||||
voice_cmd_format_key_combo(over.keyval, over.modifiers, over.display_key, sizeof(over.display_key));
|
||||
if (voice_cmd_add(list, &over) != 0) return -3;
|
||||
|
||||
voice_cmd_t eoc;
|
||||
memset(&eoc, 0, sizeof(eoc));
|
||||
snprintf(eoc.trigger, sizeof(eoc.trigger), "end of command");
|
||||
eoc.type = VOICE_CMD_KEYCOMBO;
|
||||
eoc.keyval = 65293;
|
||||
eoc.modifiers = VOICE_CMD_MOD_CTRL | VOICE_CMD_MOD_SHIFT;
|
||||
voice_cmd_format_key_combo(eoc.keyval, eoc.modifiers, eoc.display_key, sizeof(eoc.display_key));
|
||||
if (voice_cmd_add(list, &eoc) != 0) return -4;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int voice_cmd_load(const char *path, voice_cmd_list_t *list) {
|
||||
if (!path || !list) return -1;
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return -2;
|
||||
|
||||
voice_cmd_free(list);
|
||||
voice_cmd_init(list);
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
trim(line);
|
||||
if (line[0] == '\0' || line[0] == '#') continue;
|
||||
|
||||
if (strncmp(line, "@enabled=", 9) == 0) {
|
||||
list->enabled = (atoi(line + 9) != 0) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
char *p1 = strchr(line, '|');
|
||||
if (!p1) continue;
|
||||
*p1 = '\0';
|
||||
char *p2 = strchr(p1 + 1, '|');
|
||||
if (!p2) continue;
|
||||
*p2 = '\0';
|
||||
|
||||
char *trigger = line;
|
||||
char *type = p1 + 1;
|
||||
char *value = p2 + 1;
|
||||
trim(trigger);
|
||||
trim(type);
|
||||
trim(value);
|
||||
if (trigger[0] == '\0') continue;
|
||||
|
||||
voice_cmd_t c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
snprintf(c.trigger, sizeof(c.trigger), "%.127s", trigger);
|
||||
|
||||
if (strcmp(type, "text") == 0) {
|
||||
c.type = VOICE_CMD_TEXT;
|
||||
unescape_copy(value, c.text_value, sizeof(c.text_value));
|
||||
} else if (strcmp(type, "key") == 0) {
|
||||
c.type = VOICE_CMD_KEYCOMBO;
|
||||
unsigned int k = 0, m = 0;
|
||||
if (sscanf(value, "%u:%u", &k, &m) != 2) continue;
|
||||
c.keyval = k;
|
||||
c.modifiers = m;
|
||||
voice_cmd_format_key_combo(c.keyval, c.modifiers, c.display_key, sizeof(c.display_key));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (voice_cmd_add(list, &c) != 0) {
|
||||
fclose(f);
|
||||
return -3;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int voice_cmd_save(const char *path, const voice_cmd_list_t *list) {
|
||||
if (!path || !list) return -1;
|
||||
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) return -2;
|
||||
|
||||
fprintf(f, "# voice command rules\n");
|
||||
fprintf(f, "# @enabled=1 or @enabled=0 controls global enable state\n");
|
||||
fprintf(f, "# trigger|type|value\n");
|
||||
fprintf(f, "# type=text => value supports escaped \\n, \\t, \\\\ \n");
|
||||
fprintf(f, "# type=key => value format keyval:modifiers\n\n");
|
||||
fprintf(f, "@enabled=%d\n\n", list->enabled ? 1 : 0);
|
||||
|
||||
for (int i = 0; i < list->count; ++i) {
|
||||
const voice_cmd_t *c = &list->cmds[i];
|
||||
if (c->type == VOICE_CMD_TEXT) {
|
||||
char esc[512];
|
||||
escape_copy(c->text_value, esc, sizeof(esc));
|
||||
fprintf(f, "%s|text|%s\n", c->trigger, esc);
|
||||
} else {
|
||||
fprintf(f, "%s|key|%u:%u\n", c->trigger, c->keyval, c->modifiers);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *voice_cmd_apply(const char *input,
|
||||
const voice_cmd_list_t *list,
|
||||
voice_cmd_key_callback_t key_cb,
|
||||
void *user_data) {
|
||||
if (!input) return NULL;
|
||||
|
||||
size_t in_len = strlen(input);
|
||||
size_t cap = in_len * 2 + 64;
|
||||
char *out = (char *)malloc(cap);
|
||||
if (!out) return NULL;
|
||||
|
||||
size_t w = 0;
|
||||
size_t i = 0;
|
||||
|
||||
while (i < in_len) {
|
||||
int best_idx = -1;
|
||||
size_t best_len = 0;
|
||||
|
||||
if (list && list->enabled) {
|
||||
for (int ci = 0; ci < list->count; ++ci) {
|
||||
const voice_cmd_t *c = &list->cmds[ci];
|
||||
size_t tl = strlen(c->trigger);
|
||||
if (tl == 0 || i + tl > in_len) continue;
|
||||
if (!matches_whole_word_ci(input, i, c->trigger, tl)) continue;
|
||||
if (tl > best_len) {
|
||||
best_len = tl;
|
||||
best_idx = ci;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best_idx >= 0) {
|
||||
const voice_cmd_t *c = &list->cmds[best_idx];
|
||||
if (c->type == VOICE_CMD_TEXT) {
|
||||
size_t repl = strlen(c->text_value);
|
||||
if (w + repl + 1 > cap) {
|
||||
size_t next = (cap * 2) + repl + 32;
|
||||
char *np = (char *)realloc(out, next);
|
||||
if (!np) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = np;
|
||||
cap = next;
|
||||
}
|
||||
memcpy(out + w, c->text_value, repl);
|
||||
w += repl;
|
||||
} else if (key_cb) {
|
||||
key_cb(c->keyval, c->modifiers, user_data);
|
||||
}
|
||||
|
||||
i += best_len;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (w + 2 > cap) {
|
||||
size_t next = cap * 2;
|
||||
char *np = (char *)realloc(out, next);
|
||||
if (!np) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
out = np;
|
||||
cap = next;
|
||||
}
|
||||
|
||||
out[w++] = input[i++];
|
||||
}
|
||||
|
||||
out[w] = '\0';
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef VOICE_VOICE_CMD_H
|
||||
#define VOICE_VOICE_CMD_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
VOICE_CMD_TEXT = 0,
|
||||
VOICE_CMD_KEYCOMBO = 1,
|
||||
} voice_cmd_type_t;
|
||||
|
||||
enum {
|
||||
VOICE_CMD_MOD_SHIFT = 1 << 0,
|
||||
VOICE_CMD_MOD_CTRL = 1 << 1,
|
||||
VOICE_CMD_MOD_ALT = 1 << 2,
|
||||
VOICE_CMD_MOD_SUPER = 1 << 3,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
char trigger[128];
|
||||
voice_cmd_type_t type;
|
||||
char text_value[256];
|
||||
unsigned int keyval;
|
||||
unsigned int modifiers;
|
||||
char display_key[128];
|
||||
} voice_cmd_t;
|
||||
|
||||
typedef struct {
|
||||
voice_cmd_t *cmds;
|
||||
int count;
|
||||
int capacity;
|
||||
int enabled;
|
||||
} voice_cmd_list_t;
|
||||
|
||||
typedef int (*voice_cmd_key_callback_t)(unsigned int keyval, unsigned int modifiers, void *user_data);
|
||||
|
||||
void 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);
|
||||
int voice_cmd_seed_defaults(voice_cmd_list_t *list);
|
||||
char *voice_cmd_apply(const char *input,
|
||||
const voice_cmd_list_t *list,
|
||||
voice_cmd_key_callback_t key_cb,
|
||||
void *user_data);
|
||||
|
||||
void voice_cmd_format_key_combo(unsigned int keyval,
|
||||
unsigned int modifiers,
|
||||
char *out,
|
||||
size_t out_sz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
# voice command rules
|
||||
# @enabled=1 or @enabled=0 controls global enable state
|
||||
# trigger|type|value
|
||||
# type=text => value supports escaped \n, \t, \\
|
||||
# type=key => value format keyval:modifiers
|
||||
|
||||
@enabled=1
|
||||
|
||||
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|}
|
||||
end of command|key|65293:3
|
||||
over.|key|65293:2
|
||||
Reference in New Issue
Block a user