8 Commits
17 changed files with 941 additions and 168 deletions
+5 -25
View File
@@ -17,43 +17,23 @@ LIBS += -lwhisper -lggml
endif
SRC := \
./src/main.c \
./src/gui_main.c \
./src/config.c \
./src/hotkey.c \
./src/audio.c \
./src/transcribe.c \
./src/typer.c
OBJ := $(SRC:.c=.o)
TARGET := ./voice_linux
GUI_TARGET := ./voice_linux_gui
.PHONY: all clean run run-gui cpu gpu
.PHONY: all clean run
all: $(TARGET) $(GUI_TARGET)
all: $(TARGET)
$(TARGET): $(OBJ)
$(CC) $(OBJ) -o $@ $(LDFLAGS) $(LIBS)
./src/%.o: ./src/%.c
$(CC) $(CFLAGS) -c $< -o $@
$(GUI_TARGET): ./src/gui_main.c ./src/config.c ./src/audio.c ./src/transcribe.c ./src/typer.c
$(TARGET): $(SRC)
$(CC) $(CFLAGS) $(GTK_CFLAGS) $^ -o $@ $(LDFLAGS) $(LIBS) $(GTK_LIBS)
cpu:
$(MAKE) clean
$(MAKE) WITH_WHISPER=1
gpu:
$(MAKE) clean
$(MAKE) WITH_WHISPER=1
run: $(TARGET)
./voice_linux
run-gui: $(GUI_TARGET)
./voice_linux_gui
clean:
rm -f $(OBJ) $(TARGET) $(GUI_TARGET)
rm -f $(TARGET)
+32 -34
View File
@@ -9,58 +9,56 @@ Local hotkey dictation for Linux/X11 using:
```bash
sudo apt update
sudo apt install -y build-essential cmake git libpulse-dev libx11-dev libxtst-dev
```
## Build whisper.cpp
```bash
mkdir -p ./vendor
cd ./vendor
git clone https://github.com/ggerganov/whisper.cpp.git
cd ./whisper.cpp
cmake -B build -DWHISPER_CUDA=ON
cmake --build build -j"$(nproc)"
```
If CUDA is not ready yet, you can still build whisper.cpp CPU-only by omitting `-DWHISPER_CUDA=ON`.
## Download a model
```bash
mkdir -p ./models
wget -O ./models/ggml-base.en.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin
sudo apt install -y build-essential cmake git wget libpulse-dev libx11-dev libxtst-dev libgtk-3-dev pkg-config
```
## Build app
Use [`build.sh`](build.sh):
```bash
# builds CLI + GUI by default
# CPU whisper build
bash ./build.sh
# optional switches:
WHISPER_CUDA=1 bash ./build.sh # enable CUDA whisper.cpp build
BUILD_GUI=0 bash ./build.sh # build CLI only
WITH_WHISPER=0 bash ./build.sh # build without whisper integration
# CUDA whisper build
WHISPER_CUDA=1 bash ./build.sh
# build without whisper integration
WITH_WHISPER=0 bash ./build.sh
```
`./voice_linux` is the single app binary and always launches the GTK window.
## Download whisper models
Download all supported test models:
```bash
bash ./scripts/download_models.sh
```
This fetches:
- `ggml-tiny.en.bin`
- `ggml-base.en.bin`
- `ggml-small.en.bin`
- `ggml-medium.en.bin`
- `ggml-large-v3-turbo.bin`
- `ggml-large-v3.bin`
## Run
```bash
# CLI hotkey mode (uses ./config.ini by default)
./voice_linux
# GTK3 window mode (uses ./config.ini by default)
./voice_linux_gui
```
On startup, the app opens a dialog where you choose:
- CPU or GPU mode
- Whisper model from `./models/`
Default hotkey: `Ctrl+Alt+V`
- Press once to **start** recording
- Press again to **stop** recording, transcribe, and type into the focused window
If you see `samples=0`, that was an immediate start/stop event.
If you see `[stt] [BLANK_AUDIO]`, recording worked but audio energy was near silent.
Check your input source with:
@@ -68,7 +66,7 @@ Check your input source with:
pactl list short sources
```
Then set `audio_device=` in `./config.ini` to the source you want.
Then set `audio_device=` in [`config.ini`](config.ini).
## Versioning workflow
+35 -68
View File
@@ -6,7 +6,6 @@ cd "$ROOT_DIR"
WITH_WHISPER="${WITH_WHISPER:-1}"
WHISPER_CUDA="${WHISPER_CUDA:-0}"
BUILD_GUI="${BUILD_GUI:-1}"
JOBS="${JOBS:-$(nproc)}"
WHISPER_DIR="./vendor/whisper.cpp"
@@ -33,9 +32,24 @@ LDFLAGS_COMMON=(
)
echo "[build] root: $ROOT_DIR"
echo "[build] WITH_WHISPER=$WITH_WHISPER WHISPER_CUDA=$WHISPER_CUDA BUILD_GUI=$BUILD_GUI JOBS=$JOBS"
echo "[build] WITH_WHISPER=$WITH_WHISPER WHISPER_CUDA=$WHISPER_CUDA JOBS=$JOBS"
need_cmd gcc
need_cmd pkg-config
if ! pkg-config --exists gtk+-3.0; then
echo "[build] error: gtk+-3.0 dev files not found" >&2
exit 2
fi
GTK_CFLAGS_STR="$(pkg-config --cflags gtk+-3.0 2>/dev/null || true)"
if [[ -z "$GTK_CFLAGS_STR" ]]; then
echo "[build] error: gtk+-3.0 cflags are unavailable" >&2
exit 2
fi
if ! printf '#include <gtk/gtk.h>\n' | gcc -x c -fsyntax-only $GTK_CFLAGS_STR - >/dev/null 2>&1; then
echo "[build] error: gtk headers are not usable (gtk/gtk.h check failed)" >&2
exit 2
fi
if [[ "$WITH_WHISPER" == "1" ]]; then
if ! have_cmd cmake; then
@@ -58,28 +72,7 @@ if [[ "$WITH_WHISPER" == "1" ]]; then
fi
fi
if [[ "$BUILD_GUI" == "1" ]]; then
if ! have_cmd pkg-config; then
echo "[build] warning: pkg-config not found; forcing BUILD_GUI=0" >&2
BUILD_GUI=0
elif ! pkg-config --exists gtk+-3.0; then
echo "[build] warning: gtk+-3.0 dev files not found; forcing BUILD_GUI=0" >&2
BUILD_GUI=0
else
GTK_CFLAGS_STR="$(pkg-config --cflags gtk+-3.0 2>/dev/null || true)"
if [[ -z "$GTK_CFLAGS_STR" ]]; then
echo "[build] warning: gtk+-3.0 cflags are unavailable; forcing BUILD_GUI=0" >&2
BUILD_GUI=0
else
if ! printf '#include <gtk/gtk.h>\n' | gcc -x c -fsyntax-only $GTK_CFLAGS_STR - >/dev/null 2>&1; then
echo "[build] warning: gtk headers are not usable (gtk/gtk.h check failed); forcing BUILD_GUI=0" >&2
BUILD_GUI=0
fi
fi
fi
fi
echo "[build] effective: WITH_WHISPER=$WITH_WHISPER BUILD_GUI=$BUILD_GUI"
echo "[build] effective: WITH_WHISPER=$WITH_WHISPER"
if [[ "$WITH_WHISPER" == "1" ]]; then
if [[ ! -d "$WHISPER_DIR" ]]; then
@@ -90,7 +83,7 @@ if [[ "$WITH_WHISPER" == "1" ]]; then
echo "[build] configuring whisper.cpp..."
if [[ "$WHISPER_CUDA" == "1" ]]; then
cmake -S "$WHISPER_DIR" -B "$WHISPER_DIR/build" -DWHISPER_CUDA=ON
cmake -S "$WHISPER_DIR" -B "$WHISPER_DIR/build" -DGGML_CUDA=ON
else
cmake -S "$WHISPER_DIR" -B "$WHISPER_DIR/build"
fi
@@ -99,26 +92,31 @@ if [[ "$WITH_WHISPER" == "1" ]]; then
cmake --build "$WHISPER_DIR/build" -j"$JOBS"
mkdir -p ./models
if [[ ! -f "$MODEL_PATH" ]]; then
if [[ -x ./scripts/download_models.sh ]]; then
echo "[build] ensuring whisper models are downloaded..."
bash ./scripts/download_models.sh
elif [[ ! -f "$MODEL_PATH" ]]; then
echo "[build] downloading model $MODEL_PATH..."
wget -O "$MODEL_PATH" \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin
fi
fi
mkdir -p ./src
echo "[build] building GTK3 app binary..."
echo "[build] building CLI binary..."
CLI_CFLAGS=("${CFLAGS_COMMON[@]}")
CLI_LDFLAGS=("${LDFLAGS_COMMON[@]}")
read -r -a GTK_CFLAGS <<< "$(pkg-config --cflags gtk+-3.0)"
read -r -a GTK_LIBS <<< "$(pkg-config --libs gtk+-3.0)"
APP_CFLAGS=("${CFLAGS_COMMON[@]}" "${GTK_CFLAGS[@]}")
APP_LDFLAGS=("${LDFLAGS_COMMON[@]}" "${GTK_LIBS[@]}")
if [[ "$WITH_WHISPER" == "1" ]]; then
CLI_CFLAGS+=(
APP_CFLAGS+=(
-DWITH_WHISPER=1
-isystem "$WHISPER_DIR/include"
-isystem "$WHISPER_DIR/ggml/include"
)
CLI_LDFLAGS+=(
APP_LDFLAGS+=(
-L"$WHISPER_DIR/build/src"
-L"$WHISPER_DIR/build/ggml/src"
-Wl,-rpath,"$WHISPER_DIR/build/src"
@@ -127,41 +125,10 @@ if [[ "$WITH_WHISPER" == "1" ]]; then
)
fi
gcc "${CLI_CFLAGS[@]}" \
./src/main.c ./src/config.c ./src/hotkey.c ./src/audio.c ./src/transcribe.c ./src/typer.c \
gcc "${APP_CFLAGS[@]}" \
./src/gui_main.c ./src/config.c ./src/audio.c ./src/transcribe.c ./src/typer.c \
-o ./voice_linux \
"${CLI_LDFLAGS[@]}"
"${APP_LDFLAGS[@]}"
if [[ "$BUILD_GUI" == "1" ]]; then
echo "[build] building GTK3 GUI binary..."
read -r -a GTK_CFLAGS <<< "$(pkg-config --cflags gtk+-3.0)"
read -r -a GTK_LIBS <<< "$(pkg-config --libs gtk+-3.0)"
GUI_CFLAGS=("${CFLAGS_COMMON[@]}" "${GTK_CFLAGS[@]}")
GUI_LDFLAGS=("${LDFLAGS_COMMON[@]}" "${GTK_LIBS[@]}")
if [[ "$WITH_WHISPER" == "1" ]]; then
GUI_CFLAGS+=(
-DWITH_WHISPER=1
-isystem "$WHISPER_DIR/include"
-isystem "$WHISPER_DIR/ggml/include"
)
GUI_LDFLAGS+=(
-L"$WHISPER_DIR/build/src"
-L"$WHISPER_DIR/build/ggml/src"
-Wl,-rpath,"$WHISPER_DIR/build/src"
-Wl,-rpath,"$WHISPER_DIR/build/ggml/src"
-lwhisper -lggml
)
fi
gcc "${GUI_CFLAGS[@]}" \
./src/gui_main.c ./src/config.c ./src/audio.c ./src/transcribe.c ./src/typer.c \
-o ./voice_linux_gui \
"${GUI_LDFLAGS[@]}"
fi
echo "[build] done: ./voice_linux (and ./voice_linux_gui if BUILD_GUI=1)"
echo "[build] run CLI: ./voice_linux"
echo "[build] run GUI: ./voice_linux_gui"
echo "[build] done: ./voice_linux"
echo "[build] run: ./voice_linux"
+2 -1
View File
@@ -3,6 +3,7 @@
# Example: Ctrl+Alt+V
model_path=./models/ggml-base.en.bin
use_gpu=1
audio_device=alsa_input.usb-Shure_Inc_Shure_MV6_MV6_5-ed2d37fa14d3e65d8d477cacf910d93c-01.mono-fallback
sample_rate=16000
language=en
@@ -27,4 +28,4 @@ vad_min_speech_ms=250
# hard cap for one speech segment
vad_max_speech_ms=15000
autotype_enabled=0
autotype_enabled=1
+240
View File
@@ -0,0 +1,240 @@
# GPU Enablement Plan — voice_linux on NVIDIA GTX 1080 Ti
## Summary
Enable CUDA-accelerated whisper.cpp inference for the voice_linux project by passing the GTX 1080 Ti through to the Qubes `ai` **StandaloneVM**, installing NVIDIA drivers and CUDA toolkit, and rebuilding the application with `WHISPER_CUDA=1`.
## Current State
The codebase is **already GPU-ready**:
- [`build.sh`](../build.sh) accepts `WHISPER_CUDA=1` to build whisper.cpp with CUDA
- [`src/transcribe.c:22`](../src/transcribe.c:22) sets `cparams.use_gpu` from the transcribe params
- [`src/main.c:77`](../src/main.c:77) supports `--gpu` / `--cpu` CLI flags
- [`src/transcribe.h:9`](../src/transcribe.h:9) has `use_gpu` in the params struct
What's missing is the **hardware and driver stack** underneath.
## Architecture
```mermaid
graph TD
A[dom0 - Xen Hypervisor] -->|PCI passthrough| B[ai StandaloneVM]
B --> C[NVIDIA Driver 550.x]
C --> D[CUDA Toolkit 12.6]
D --> E[whisper.cpp built with WHISPER_CUDA=ON]
E --> F[voice_linux --gpu]
style A fill:#f9f,stroke:#333
style F fill:#9f9,stroke:#333
```
## Phase 0: GPU Passthrough in dom0
> **All commands in this phase run in a dom0 terminal.**
### Step 0.1 — Hide the GTX 1080 Ti from dom0
Edit `/etc/default/grub` in dom0 and add `rd.qubes.hide_pci=65:00.0,65:00.1` to the end of the **first** `GRUB_CMDLINE_LINUX` line (inside the quotes):
```bash
sudo nano /etc/default/grub
```
The line should look like:
```
GRUB_CMDLINE_LINUX="...existing options... rd.qubes.hide_pci=65:00.0,65:00.1"
```
The BDF addresses `65:00.0` (GPU) and `65:00.1` (HDMI audio) come from the `lspci` output documented in [`plans/architecture.md:97`](../plans/architecture.md:97).
### Step 0.2 — Regenerate GRUB and reboot
```bash
sudo grub2-mkconfig -o /boot/efi/EFI/qubes/grub.cfg
# If that path doesn't exist: sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo reboot
```
### Step 0.3 — Verify GPU is assignable
After reboot, in dom0:
```bash
xl pci-assignable-list
```
Expected output should include:
```
0000:65:00.0
0000:65:00.1
```
If these don't appear, check IOMMU grouping with `xl pci-assignable-list -l` and verify the GRUB change took effect with `cat /proc/cmdline`.
### Step 0.4 — Attach GPU to the ai StandaloneVM
```bash
qvm-pci attach ai dom0:65_00.0 --persistent -o permissive=true
qvm-pci attach ai dom0:65_00.1 --persistent -o permissive=true
```
The `--persistent` flag ensures the GPU is attached on every boot. The `permissive=true` option is needed for NVIDIA GPUs in Qubes.
### Step 0.5 — Persistence model (already satisfied)
Your `ai` VM is already a **StandaloneVM**, so driver and CUDA installs persist normally across reboots.
No template/bind-dirs workaround is required.
## Phase 1: NVIDIA Driver Installation
> **All commands from here run inside the ai StandaloneVM.**
### Step 1.1 — Install build prerequisites
```bash
sudo apt update
sudo apt install -y build-essential linux-headers-$(uname -r)
```
### Step 1.2 — Install NVIDIA driver
For the GTX 1080 Ti (Pascal/GP102, compute capability 6.1), use the 550.x branch:
```bash
wget https://us.download.nvidia.com/XFree86/Linux-x86_64/550.127.05/NVIDIA-Linux-x86_64-550.127.05.run
chmod +x NVIDIA-Linux-x86_64-550.127.05.run
sudo ./NVIDIA-Linux-x86_64-550.127.05.run --no-opengl-files --dkms
```
> **Critical**: `--no-opengl-files` prevents replacing the VM's display GL stack. We only need CUDA compute, not display rendering.
### Step 1.3 — Verify driver
```bash
nvidia-smi
```
Expected: Shows GTX 1080 Ti with 11GB VRAM and driver version 550.127.05.
## Phase 2: CUDA Toolkit Installation
### Step 2.1 — Install CUDA toolkit
```bash
wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt update
sudo apt install -y cuda-toolkit-12-6
```
### Step 2.2 — Configure environment
Add to `~/.bashrc`:
```bash
echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
source ~/.bashrc
```
### Step 2.3 — Verify CUDA
```bash
nvcc --version
```
Expected: CUDA 12.6.
## Phase 3: Rebuild with CUDA
### Step 3.1 — Clean and rebuild
From the project directory, run:
```bash
WHISPER_CUDA=1 bash ./build.sh
```
This will:
1. Clone whisper.cpp into `vendor/whisper.cpp` (if not already present)
2. Run `cmake -DWHISPER_CUDA=ON` to configure whisper.cpp with CUDA support
3. Build whisper.cpp with CUDA kernels
4. Download the `ggml-base.en.bin` model (if not present)
5. Compile voice_linux linking against the CUDA-enabled whisper library
### Step 3.2 — Verify CUDA linkage
```bash
ldd ./voice_linux | grep -i cuda
```
Should show linkage to CUDA libraries (indirectly through libwhisper).
Also check:
```bash
ldd ./vendor/whisper.cpp/build/src/libwhisper.so | grep -i cuda
```
Should show `libcudart.so`, `libcublas.so`, etc.
## Phase 4: Run with GPU
### Step 4.1 — Launch with GPU flag
```bash
./voice_linux --gpu
```
The startup log at [`src/main.c:119`](../src/main.c:119) will print `gpu: on` confirming GPU mode. Watch for whisper.cpp log lines mentioning CUDA device initialization.
### Step 4.2 — Verify GPU utilization
In a separate terminal while transcribing:
```bash
nvidia-smi
```
Should show `voice_linux` process using GPU memory.
### Step 4.3 — Optional: Try a larger model
With 11GB VRAM on the 1080 Ti, you can comfortably run `medium.en` for much better accuracy:
```bash
cd models/
wget https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.en.bin
```
Then update [`config.ini`](../config.ini) line 5:
```ini
model_path=./models/ggml-medium.en.bin
```
| Model | VRAM Usage | Speed (10s audio) | Accuracy |
|-------|-----------|-------------------|----------|
| base.en | ~1GB | <1s | Good |
| medium.en | ~5GB | ~2-3s | Excellent |
| large-v3 | ~10GB | ~4-6s | Best |
## Troubleshooting
| Problem | Solution |
|---------|----------|
| `xl pci-assignable-list` empty | Check GRUB cmdline with `cat /proc/cmdline`, verify `rd.qubes.hide_pci` is present |
| `nvidia-smi` not found | Driver install failed; check `dmesg` for NVIDIA errors |
| `nvcc` not found | CUDA PATH not set; run `source ~/.bashrc` or check install |
| whisper.cpp cmake fails with CUDA | Ensure `nvcc` is in PATH before running `build.sh` |
| `voice_linux` crashes on `--gpu` | Run without `--gpu` first to verify CPU mode works, then check CUDA libs with `ldd` |
| GPU passthrough causes VM crash | Try without `permissive=true` first, or check IOMMU groups in dom0 |
## No Code Changes Required
The existing codebase handles everything:
- [`build.sh:8`](../build.sh:8) — `WHISPER_CUDA` env var controls CUDA build
- [`build.sh:92-93`](../build.sh:92) — passes `-DWHISPER_CUDA=ON` to cmake
- [`src/transcribe.c:21-22`](../src/transcribe.c:21) — `whisper_context_default_params()` + `use_gpu` flag
- [`src/main.c:77-78`](../src/main.c:77) — `--gpu` / `--cpu` CLI argument parsing
The only change is the build command: `WHISPER_CUDA=1 bash ./build.sh` instead of `bash ./build.sh`.
+142
View File
@@ -0,0 +1,142 @@
# Unified Binary + Startup Dialog Plan
## Summary
Merge the CLI and GUI into a single `voice_linux` binary that always opens the GTK3 window. On launch, a **startup dialog** appears letting the user choose CPU vs GPU and select which whisper model to use. All available ggml models are downloaded to `./models/` so the user can test them.
## Current State
| Item | Status |
|------|--------|
| Two binaries: `voice_linux` (CLI) and `voice_linux_gui` (GTK3) | Active |
| GPU passthrough + NVIDIA driver + CUDA toolkit | Working |
| whisper.cpp built with `GGML_CUDA=ON` | Working |
| `--gpu` / `--cpu` flags parsed in both binaries | Working |
| Model path hardcoded in `config.ini` as `model_path=` | Working |
| `transcribe_init()` called once at startup before GTK loop | Working |
## Architecture Changes
```mermaid
flowchart TD
A[./voice_linux launched] --> B[Load config.ini]
B --> C[audio_init]
C --> D{Show Startup Dialog}
D --> |User picks CPU/GPU + model| E[transcribe_init with selections]
E --> F[typer_init]
F --> G[Main GTK window + event loop]
G --> H[On window close: cleanup]
```
Key change: **`transcribe_init()` is deferred** until after the startup dialog returns the user's selections. Audio init happens first so the mic meter can work, but model loading waits for the dialog.
## Whisper Models to Download
All English-optimized ggml models from the whisper.cpp HuggingFace repo. The GTX 1080 Ti has 11 GB VRAM, so all models up to `large-v3` fit comfortably.
| Model | File | Size | VRAM est. |
|-------|------|------|-----------|
| tiny.en | `ggml-tiny.en.bin` | ~75 MB | ~200 MB |
| base.en | `ggml-base.en.bin` | ~142 MB | ~350 MB |
| small.en | `ggml-small.en.bin` | ~466 MB | ~1 GB |
| medium.en | `ggml-medium.en.bin` | ~1.5 GB | ~3 GB |
| large-v3-turbo | `ggml-large-v3-turbo.bin` | ~1.6 GB | ~3.5 GB |
| large-v3 | `ggml-large-v3.bin` | ~3.1 GB | ~6 GB |
Base URL: `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/`
## Detailed Steps
### Step 1: Download all whisper models
Create a `scripts/download_models.sh` that downloads each model to `./models/` if not already present. Also callable from `build.sh`.
### Step 2: Add startup dialog to gui_main.c
Insert a modal `GtkDialog` that runs **before** `transcribe_init()`:
- **GPU toggle**: `GtkComboBoxText` with entries `CPU` and `GPU`. Default to `GPU` if CUDA device is detected at runtime, otherwise `CPU`.
- **Model picker**: `GtkComboBoxText` populated by scanning `./models/ggml-*.bin` files. Display friendly names derived from filenames. Pre-select the model from `config.ini`.
- **OK button**: closes dialog, returns selections to `main()`.
The dialog is a transient child of a hidden parent window so it appears centered on screen.
GPU detection at dialog time: check if `nvidia-smi` exits 0, or check if the whisper.cpp CUDA backend reports a device. Simplest approach: try `access /dev/nvidia0` — if it exists, default to GPU.
### Step 3: Defer transcribe_init
Current flow in `gui_main.c` [`main()`](src/gui_main.c:588):
```
config_load -> use_gpu from argv -> audio_init -> transcribe_init -> typer_init -> gtk loop
```
New flow:
```
config_load -> audio_init -> show_startup_dialog -> transcribe_init(dialog selections) -> typer_init -> gtk loop
```
The startup dialog returns:
- `int use_gpu` — 0 or 1
- `char model_path[512]` — full path to selected model file
These override whatever was in config.ini / argv.
### Step 4: Remove CLI binary from default build
- Keep `src/main.c` in the repo for reference or future headless use, but do not build it by default.
- In [`build.sh`](build.sh): remove the separate CLI gcc invocation. The GUI gcc line outputs `./voice_linux` instead of `./voice_linux_gui`.
- In [`Makefile`](Makefile): change `TARGET` to build from `gui_main.c` sources. Remove `voice_linux_gui` target. Optionally add a `cli` target for headless builds.
- The `BUILD_GUI` env var is no longer needed; remove it.
### Step 5: Add use_gpu to config.ini and config.c
Add a `use_gpu=1` line to [`config.ini`](config.ini). Parse it in [`config_load_file()`](src/config.c:43). This becomes the default for the startup dialog, overridable by the dialog selection. The `--gpu` / `--cpu` CLI flags still work as overrides.
Add `int use_gpu;` to [`voice_config_t`](src/config.h:6).
### Step 6: Update README.md
Simplify run instructions to just `./voice_linux`. Remove `voice_linux_gui` references. Document the startup dialog behavior.
### Step 7: Build and test
```bash
bash ./build.sh
./voice_linux
```
Verify:
- Startup dialog appears with CPU/GPU toggle and model list
- Selecting GPU + medium.en loads correctly with CUDA backend
- Main window works as before after dialog closes
## Files Modified
| File | Change |
|------|--------|
| `src/gui_main.c` | Add startup dialog function; defer `transcribe_init`; update `main()` flow |
| `src/config.h` | Add `use_gpu` field to `voice_config_t` |
| `src/config.c` | Parse `use_gpu` from config file |
| `config.ini` | Add `use_gpu=1` |
| `build.sh` | Single binary output `./voice_linux`; remove `BUILD_GUI` toggle; remove CLI build |
| `Makefile` | Single target `./voice_linux` from gui sources |
| `README.md` | Update run instructions |
| `scripts/download_models.sh` | New script to download all models |
## Files NOT Modified
| File | Reason |
|------|--------|
| `src/main.c` | Kept for optional headless builds; not built by default |
| `src/transcribe.c` | API unchanged — `transcribe_init` / `transcribe_cleanup` already support the needed flow |
| `src/transcribe.h` | No changes needed |
| `src/audio.c` / `src/audio.h` | No changes needed |
| `src/hotkey.c` / `src/hotkey.h` | Only used by CLI; not linked into GUI binary |
## Risk Notes
- The startup dialog adds a brief pause before the main window. Model loading (especially large-v3 at 3 GB) takes several seconds — a progress indicator or status label in the main window info area will show loading state.
- If no models are found in `./models/`, the dialog should show an error message and offer to run the download script.
- The `transcribe_init` call blocks the GTK main thread during model load. For large models this could be 5-10 seconds. An improvement would be async loading with a progress bar, but for v1 a simple "Loading model..." label update before the blocking call is sufficient.
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
mkdir -p ./models
BASE_URL="https://huggingface.co/ggerganov/whisper.cpp/resolve/main"
MODELS=(
"ggml-tiny.en.bin"
"ggml-base.en.bin"
"ggml-small.en.bin"
"ggml-medium.en.bin"
"ggml-large-v3-turbo.bin"
"ggml-large-v3.bin"
)
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "[models] error: required command '$1' not found" >&2
exit 2
fi
}
need_cmd wget
echo "[models] target dir: $ROOT_DIR/models"
for model in "${MODELS[@]}"; do
out="./models/$model"
url="$BASE_URL/$model"
if [[ -f "$out" ]]; then
size="$(stat -c%s "$out" 2>/dev/null || echo 0)"
echo "[models] found local copy for $model (${size} bytes), resuming/verifying with wget -c"
else
echo "[models] downloading: $model"
fi
wget -c -O "$out" "$url"
echo "[models] ready: $model"
done
echo "[models] complete"
+7 -1
View File
@@ -25,6 +25,7 @@ void config_set_defaults(voice_config_t *cfg) {
snprintf(cfg->audio_device, sizeof(cfg->audio_device), "qubes-source");
snprintf(cfg->language, sizeof(cfg->language), "en");
snprintf(cfg->hotkey, sizeof(cfg->hotkey), "Ctrl+Alt+V");
cfg->use_gpu = 1;
cfg->sample_rate = 16000;
cfg->type_delay_us = 8000;
cfg->max_record_seconds = 30;
@@ -36,7 +37,8 @@ void config_set_defaults(voice_config_t *cfg) {
cfg->vad_preroll_ms = 500;
cfg->vad_min_speech_ms = 250;
cfg->vad_max_speech_ms = 15000;
cfg->autotype_enabled = 0;
cfg->autotype_enabled = 1;
cfg->filter_bracketed_tags = 1;
}
int config_load_file(const char *path, voice_config_t *cfg) {
@@ -67,6 +69,8 @@ int config_load_file(const char *path, voice_config_t *cfg) {
snprintf(cfg->language, sizeof(cfg->language), "%s", value);
} else if (strcmp(key, "hotkey") == 0) {
snprintf(cfg->hotkey, sizeof(cfg->hotkey), "%s", value);
} else if (strcmp(key, "use_gpu") == 0) {
cfg->use_gpu = atoi(value);
} else if (strcmp(key, "sample_rate") == 0) {
cfg->sample_rate = atoi(value);
} else if (strcmp(key, "type_delay_us") == 0) {
@@ -91,6 +95,8 @@ int config_load_file(const char *path, voice_config_t *cfg) {
cfg->vad_max_speech_ms = atoi(value);
} else if (strcmp(key, "autotype_enabled") == 0) {
cfg->autotype_enabled = atoi(value);
} else if (strcmp(key, "filter_bracketed_tags") == 0) {
cfg->filter_bracketed_tags = atoi(value);
}
}
+2
View File
@@ -8,6 +8,7 @@ typedef struct {
char audio_device[256];
char language[32];
char hotkey[64];
int use_gpu;
int sample_rate;
int type_delay_us;
int max_record_seconds;
@@ -20,6 +21,7 @@ typedef struct {
int vad_min_speech_ms;
int vad_max_speech_ms;
int autotype_enabled;
int filter_bracketed_tags;
} voice_config_t;
void config_set_defaults(voice_config_t *cfg);
+238 -23
View File
@@ -6,10 +6,12 @@
#include <gtk/gtk.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
typedef struct {
GtkApplication *app;
@@ -18,6 +20,7 @@ typedef struct {
GtkWidget *record_btn;
GtkWidget *always_on_btn;
GtkWidget *type_check;
GtkWidget *filter_tags_check;
GtkWidget *transcript_view;
GtkWidget *record_light;
GtkWidget *transcribe_light;
@@ -39,6 +42,9 @@ typedef struct {
unsigned int meter_timer_id;
float live_peak;
int runtime_ready;
char last_transcript[4096];
double last_transcript_time;
} gui_state_t;
static float clampf(float v, float lo, float hi) {
@@ -160,6 +166,28 @@ static int peak_is_speech(const float *samples, size_t count, float threshold) {
return peak_abs >= threshold;
}
static double monotonic_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0;
}
static int is_duplicate_transcript(gui_state_t *s, const char *text) {
if (!s || !text || text[0] == '\0') return 0;
if (s->last_transcript[0] == '\0') return 0;
const double dt = monotonic_seconds() - s->last_transcript_time;
if (dt < 0.0 || dt > 1.0) return 0;
return strcmp(s->last_transcript, text) == 0;
}
static void remember_transcript(gui_state_t *s, const char *text) {
if (!s || !text || text[0] == '\0') return;
snprintf(s->last_transcript, sizeof(s->last_transcript), "%s", text);
s->last_transcript_time = monotonic_seconds();
}
static void process_segment(gui_state_t *s, float *samples, size_t count) {
if (!samples || count == 0) {
free(samples);
@@ -175,6 +203,18 @@ static void process_segment(gui_state_t *s, float *samples, size_t count) {
return;
}
int should_autotype = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->type_check)) ? 1 : 0;
if (should_autotype && gtk_window_is_active(GTK_WINDOW(s->window))) {
should_autotype = 0;
fprintf(stderr, "gui: autotype skipped because voice_linux window is focused\n");
}
if (should_autotype) {
if (typer_capture_focus() != 0) {
fprintf(stderr, "gui: failed to capture focus target for autotype\n");
}
}
s->transcribing = 1;
update_record_button(s);
set_status(s, "Transcribing... (please wait)");
@@ -191,12 +231,25 @@ static void process_segment(gui_state_t *s, float *samples, size_t count) {
return;
}
append_transcript(s, text);
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->type_check))) {
typer_type_text(text);
if (is_duplicate_transcript(s, text)) {
fprintf(stderr, "gui: suppressed duplicate transcript within 1s window\n");
set_status(s, "Duplicate segment suppressed");
free(text);
return;
}
append_transcript(s, text);
if (should_autotype) {
if (typer_type_text(text) != 0) {
fprintf(stderr, "gui: autotype injection failed\n");
set_status(s, "Autotype failed (see terminal logs)");
free(text);
return;
}
}
remember_transcript(s, text);
set_status(s, audio_msg);
free(text);
}
@@ -413,9 +466,183 @@ static GtkWidget *add_scale_row(GtkWidget *grid,
return scale;
}
static void on_filter_tags_toggled(GtkToggleButton *btn, gpointer user_data) {
gui_state_t *s = (gui_state_t *)user_data;
int enabled = gtk_toggle_button_get_active(btn) ? 1 : 0;
s->cfg.filter_bracketed_tags = enabled;
transcribe_set_filter_bracketed_tags(enabled);
}
static int cmp_str_ptr(const void *a, const void *b) {
const char *sa = *(const char * const *)a;
const char *sb = *(const char * const *)b;
return strcmp(sa, sb);
}
static int scan_model_paths(char out[][512], int max_out) {
DIR *d = opendir("./models");
if (!d) return 0;
int n = 0;
struct dirent *ent;
while ((ent = readdir(d)) != NULL && n < max_out) {
const char *name = ent->d_name;
if (strncmp(name, "ggml-", 5) != 0) continue;
size_t len = strlen(name);
if (len < 5 || strcmp(name + len - 4, ".bin") != 0) continue;
snprintf(out[n], 512, "./models/%s", name);
n++;
}
closedir(d);
if (n > 1) {
char *ptrs[64];
for (int i = 0; i < n; ++i) ptrs[i] = out[i];
qsort(ptrs, (size_t)n, sizeof(ptrs[0]), cmp_str_ptr);
char sorted[64][512];
for (int i = 0; i < n; ++i) {
snprintf(sorted[i], sizeof(sorted[i]), "%s", ptrs[i]);
}
for (int i = 0; i < n; ++i) {
snprintf(out[i], 512, "%s", sorted[i]);
}
}
return n;
}
static const char *base_name(const char *path) {
const char *slash = strrchr(path, '/');
return slash ? (slash + 1) : path;
}
static int run_startup_dialog(gui_state_t *s, GtkWindow *parent) {
char model_paths[64][512];
int model_count = scan_model_paths(model_paths, 64);
if (model_count <= 0) {
GtkWidget *msg = gtk_message_dialog_new(
parent,
GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
"No whisper models found in ./models\nRun scripts/download_models.sh first.");
gtk_dialog_run(GTK_DIALOG(msg));
gtk_widget_destroy(msg);
return -1;
}
GtkWidget *dialog = gtk_dialog_new_with_buttons(
"Startup Options",
parent,
GTK_DIALOG_MODAL,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Start", GTK_RESPONSE_OK,
NULL);
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
GtkWidget *grid = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
gtk_container_set_border_width(GTK_CONTAINER(grid), 10);
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
GtkWidget *gpu_label = gtk_label_new("Compute mode:");
gtk_label_set_xalign(GTK_LABEL(gpu_label), 0.0f);
gtk_grid_attach(GTK_GRID(grid), gpu_label, 0, 0, 1, 1);
GtkWidget *gpu_combo = gtk_combo_box_text_new();
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gpu_combo), "CPU");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gpu_combo), "GPU");
int gpu_default = s->use_gpu;
if (gpu_default == 0 && access("/dev/nvidia0", F_OK) == 0) {
gpu_default = 1;
}
gtk_combo_box_set_active(GTK_COMBO_BOX(gpu_combo), gpu_default ? 1 : 0);
gtk_grid_attach(GTK_GRID(grid), gpu_combo, 1, 0, 1, 1);
GtkWidget *model_label = gtk_label_new("Whisper model:");
gtk_label_set_xalign(GTK_LABEL(model_label), 0.0f);
gtk_grid_attach(GTK_GRID(grid), model_label, 0, 1, 1, 1);
GtkWidget *model_combo = gtk_combo_box_text_new();
int active_model_index = 0;
for (int i = 0; i < model_count; ++i) {
const char *path = model_paths[i];
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(model_combo), path, base_name(path));
if (strcmp(path, s->cfg.model_path) == 0) {
active_model_index = i;
}
}
gtk_combo_box_set_active(GTK_COMBO_BOX(model_combo), active_model_index);
gtk_grid_attach(GTK_GRID(grid), model_combo, 1, 1, 1, 1);
gtk_widget_show_all(dialog);
int rc = gtk_dialog_run(GTK_DIALOG(dialog));
if (rc == GTK_RESPONSE_OK) {
const gchar *active_model = gtk_combo_box_get_active_id(GTK_COMBO_BOX(model_combo));
int gpu_active = gtk_combo_box_get_active(GTK_COMBO_BOX(gpu_combo));
s->use_gpu = (gpu_active == 1) ? 1 : 0;
if (active_model && active_model[0] != '\0') {
snprintf(s->cfg.model_path, sizeof(s->cfg.model_path), "%s", active_model);
}
s->cfg.use_gpu = s->use_gpu;
gtk_widget_destroy(dialog);
return 0;
}
gtk_widget_destroy(dialog);
return -1;
}
static void on_app_activate(GtkApplication *app, gpointer user_data) {
gui_state_t *s = (gui_state_t *)user_data;
if (!s->runtime_ready) {
if (run_startup_dialog(s, NULL) != 0) {
g_application_quit(G_APPLICATION(app));
return;
}
transcribe_params_t tp = {0};
snprintf(tp.model_path, sizeof(tp.model_path), "%s", s->cfg.model_path);
snprintf(tp.language, sizeof(tp.language), "%s", s->cfg.language);
tp.use_gpu = s->use_gpu;
tp.n_threads = (int)sysconf(_SC_NPROCESSORS_ONLN);
tp.filter_bracketed_tags = s->cfg.filter_bracketed_tags;
if (transcribe_init(&tp) != 0) {
GtkWidget *msg = gtk_message_dialog_new(
NULL,
GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
"Failed to initialize model:\n%s",
s->cfg.model_path);
gtk_dialog_run(GTK_DIALOG(msg));
gtk_widget_destroy(msg);
g_application_quit(G_APPLICATION(app));
return;
}
if (typer_init(s->cfg.type_delay_us) != 0) {
GtkWidget *msg = gtk_message_dialog_new(
NULL,
GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
"Failed to initialize X11 typing backend.");
gtk_dialog_run(GTK_DIALOG(msg));
gtk_widget_destroy(msg);
g_application_quit(G_APPLICATION(app));
return;
}
s->runtime_ready = 1;
}
s->window = gtk_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(s->window), "voice_linux");
gtk_window_set_default_size(GTK_WINDOW(s->window), 760, 640);
@@ -478,6 +705,11 @@ static void on_app_activate(GtkApplication *app, gpointer user_data) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->type_check), s->cfg.autotype_enabled ? TRUE : FALSE);
gtk_box_pack_start(GTK_BOX(outer), s->type_check, FALSE, FALSE, 0);
s->filter_tags_check = gtk_check_button_new_with_label("Filter bracketed tags like [laughs]");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->filter_tags_check), s->cfg.filter_bracketed_tags ? TRUE : FALSE);
g_signal_connect(s->filter_tags_check, "toggled", G_CALLBACK(on_filter_tags_toggled), s);
gtk_box_pack_start(GTK_BOX(outer), s->filter_tags_check, FALSE, FALSE, 0);
GtkWidget *meter_frame = gtk_frame_new("Live Microphone Level");
gtk_box_pack_start(GTK_BOX(outer), meter_frame, FALSE, FALSE, 0);
@@ -573,7 +805,8 @@ int main(int argc, char **argv) {
fprintf(stderr, "warning: failed to load %s, using defaults\n", cfg_path);
}
st.use_gpu = arg_flag(argc, argv, "--gpu") ? 1 : 0;
st.use_gpu = st.cfg.use_gpu ? 1 : 0;
if (arg_flag(argc, argv, "--gpu")) st.use_gpu = 1;
if (arg_flag(argc, argv, "--cpu")) st.use_gpu = 0;
audio_params_t ap = {0};
@@ -581,12 +814,6 @@ int main(int argc, char **argv) {
ap.sample_rate = st.cfg.sample_rate;
ap.max_seconds = st.cfg.max_record_seconds;
transcribe_params_t tp = {0};
snprintf(tp.model_path, sizeof(tp.model_path), "%s", st.cfg.model_path);
snprintf(tp.language, sizeof(tp.language), "%s", st.cfg.language);
tp.use_gpu = st.use_gpu;
tp.n_threads = (int)sysconf(_SC_NPROCESSORS_ONLN);
if (audio_init(&ap) != 0) {
fprintf(stderr, "fatal: audio init failed\n");
return 1;
@@ -602,18 +829,6 @@ int main(int argc, char **argv) {
};
audio_set_vad_params(&vp);
if (transcribe_init(&tp) != 0) {
fprintf(stderr, "fatal: transcribe init failed\n");
audio_cleanup();
return 1;
}
if (typer_init(st.cfg.type_delay_us) != 0) {
fprintf(stderr, "fatal: typer init failed\n");
transcribe_cleanup();
audio_cleanup();
return 1;
}
st.app = gtk_application_new("local.voice_linux", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(st.app, "activate", G_CALLBACK(on_app_activate), &st);
+1
View File
@@ -87,6 +87,7 @@ int main(int argc, char **argv) {
snprintf(tp.language, sizeof(tp.language), "%s", cfg.language);
tp.use_gpu = use_gpu;
tp.n_threads = (int)sysconf(_SC_NPROCESSORS_ONLN);
tp.filter_bracketed_tags = cfg.filter_bracketed_tags;
hotkey_ctx_t hk;
+78 -1
View File
@@ -1,5 +1,6 @@
#include "transcribe.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -8,6 +9,71 @@
#define WITH_WHISPER 0
#endif
static int g_filter_bracketed_tags = 1;
static char *filter_bracketed_tags_copy(const char *input) {
if (!input) return NULL;
const size_t len = strlen(input);
char *no_tags = (char *)malloc(len + 1);
if (!no_tags) return NULL;
size_t w = 0;
int bracket_depth = 0;
for (size_t i = 0; i < len; ++i) {
unsigned char ch = (unsigned char)input[i];
if (ch == '[') {
bracket_depth++;
continue;
}
if (ch == ']' && bracket_depth > 0) {
bracket_depth--;
continue;
}
if (bracket_depth > 0) {
continue;
}
no_tags[w++] = (char)ch;
}
no_tags[w] = '\0';
char *out = (char *)malloc(w + 1);
if (!out) {
free(no_tags);
return NULL;
}
size_t j = 0;
int in_space = 1;
for (size_t i = 0; i < w; ++i) {
unsigned char ch = (unsigned char)no_tags[i];
if (isspace(ch)) {
if (!in_space) {
out[j++] = ' ';
in_space = 1;
}
} else {
out[j++] = (char)ch;
in_space = 0;
}
}
if (j > 0 && out[j - 1] == ' ') {
j--;
}
out[j] = '\0';
free(no_tags);
return out;
}
void transcribe_set_filter_bracketed_tags(int enabled) {
g_filter_bracketed_tags = enabled ? 1 : 0;
}
#if WITH_WHISPER
#include <whisper.h>
@@ -17,6 +83,7 @@ static transcribe_params_t g_params;
int transcribe_init(const transcribe_params_t *params) {
if (!params) return -1;
g_params = *params;
transcribe_set_filter_bracketed_tags(g_params.filter_bracketed_tags);
struct whisper_context_params cparams = whisper_context_default_params();
cparams.use_gpu = g_params.use_gpu ? true : false;
@@ -63,7 +130,17 @@ char *transcribe_buffer(const float *samples, size_t count) {
if (i + 1 < nseg) strcat(out, " ");
}
return out;
if (!g_filter_bracketed_tags) {
return out;
}
char *filtered = filter_bracketed_tags_copy(out);
if (!filtered) {
return out;
}
free(out);
return filtered;
}
void transcribe_cleanup(void) {
+2
View File
@@ -8,9 +8,11 @@ typedef struct {
char language[32];
int use_gpu;
int n_threads;
int filter_bracketed_tags;
} transcribe_params_t;
int transcribe_init(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);
+109 -13
View File
@@ -11,6 +11,16 @@
static Display *g_dpy = NULL;
static int g_delay_us = 8000;
static int g_key_hold_us = 4000;
static Window g_target_window = None;
static void sleep_us(int us) {
if (us <= 0) return;
struct timespec ts;
ts.tv_sec = us / 1000000;
ts.tv_nsec = (long)(us % 1000000) * 1000L;
nanosleep(&ts, NULL);
}
static int type_keysym(KeySym ks, int need_shift) {
KeyCode kc = XKeysymToKeycode(g_dpy, ks);
@@ -19,21 +29,21 @@ static int type_keysym(KeySym ks, int need_shift) {
KeyCode shift_kc = XKeysymToKeycode(g_dpy, XK_Shift_L);
if (need_shift && shift_kc) {
XTestFakeKeyEvent(g_dpy, shift_kc, True, CurrentTime);
if (!XTestFakeKeyEvent(g_dpy, shift_kc, True, CurrentTime)) return -1;
}
XTestFakeKeyEvent(g_dpy, kc, True, CurrentTime);
XTestFakeKeyEvent(g_dpy, kc, False, CurrentTime);
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 (need_shift && shift_kc) {
XTestFakeKeyEvent(g_dpy, shift_kc, False, CurrentTime);
if (!XTestFakeKeyEvent(g_dpy, shift_kc, False, CurrentTime)) return -1;
}
XFlush(g_dpy);
struct timespec ts;
ts.tv_sec = g_delay_us / 1000000;
ts.tv_nsec = (long)(g_delay_us % 1000000) * 1000L;
nanosleep(&ts, NULL);
XSync(g_dpy, False);
sleep_us(g_delay_us);
return 0;
}
@@ -62,32 +72,118 @@ static int type_char(char c) {
case '-': return type_keysym(XK_minus, 0);
case '_': return type_keysym(XK_minus, 1);
case '/': return type_keysym(XK_slash, 0);
case '\\': return type_keysym(XK_backslash, 0);
case '|': return type_keysym(XK_backslash, 1);
case '=': return type_keysym(XK_equal, 0);
case '+': return type_keysym(XK_equal, 1);
case '[': return type_keysym(XK_bracketleft, 0);
case '{': return type_keysym(XK_bracketleft, 1);
case ']': return type_keysym(XK_bracketright, 0);
case '}': return type_keysym(XK_bracketright, 1);
case '`': return type_keysym(XK_grave, 0);
case '~': return type_keysym(XK_grave, 1);
case '@': return type_keysym(XK_2, 1);
case '#': return type_keysym(XK_3, 1);
case '$': return type_keysym(XK_4, 1);
case '%': return type_keysym(XK_5, 1);
case '^': return type_keysym(XK_6, 1);
case '&': return type_keysym(XK_7, 1);
case '*': return type_keysym(XK_8, 1);
case '(': return type_keysym(XK_9, 1);
case ')': return type_keysym(XK_0, 1);
case '<': return type_keysym(XK_comma, 1);
case '>': return type_keysym(XK_period, 1);
default: return 0;
}
}
int typer_init(int type_delay_us) {
int ev_base = 0;
int err_base = 0;
int major = 0;
int minor = 0;
g_dpy = XOpenDisplay(NULL);
if (!g_dpy) return -1;
if (!g_dpy) {
fprintf(stderr, "typer: XOpenDisplay failed\n");
return -1;
}
if (!XTestQueryExtension(g_dpy, &ev_base, &err_base, &major, &minor)) {
fprintf(stderr, "typer: XTEST extension unavailable\n");
XCloseDisplay(g_dpy);
g_dpy = NULL;
return -1;
}
g_delay_us = type_delay_us > 0 ? type_delay_us : 8000;
g_key_hold_us = g_delay_us / 2;
if (g_key_hold_us < 4000) g_key_hold_us = 4000;
if (g_key_hold_us > 20000) g_key_hold_us = 20000;
g_target_window = None;
return 0;
}
int typer_capture_focus(void) {
if (!g_dpy) return -1;
Window focused = None;
int revert = 0;
XGetInputFocus(g_dpy, &focused, &revert);
if (focused == None || focused == PointerRoot) {
return -1;
}
g_target_window = focused;
return 0;
}
int typer_type_text(const char *text) {
if (!g_dpy || !text) return -1;
for (size_t i = 0; i < strlen(text); ++i) {
Window previous_focus = None;
int previous_revert = 0;
XGetInputFocus(g_dpy, &previous_focus, &previous_revert);
Window target = g_target_window;
if (target == None || target == PointerRoot) {
target = previous_focus;
}
if (target == None || target == PointerRoot) {
fprintf(stderr, "typer: no valid target window for autotype\n");
return -1;
}
if (previous_focus != target) {
XSetInputFocus(g_dpy, target, RevertToParent, CurrentTime);
XSync(g_dpy, False);
sleep_us(20000);
}
int failures = 0;
const size_t n = strlen(text);
for (size_t i = 0; i < n; ++i) {
if (type_char(text[i]) != 0) {
fprintf(stderr, "typer: failed at char '%c'\n", text[i]);
fprintf(stderr, "typer: failed at char 0x%02x ('%c')\n",
(unsigned char)text[i], isprint((unsigned char)text[i]) ? text[i] : '?');
failures++;
}
}
return 0;
if (previous_focus != None && previous_focus != PointerRoot && previous_focus != target) {
XSetInputFocus(g_dpy, previous_focus, RevertToParent, CurrentTime);
XSync(g_dpy, False);
}
if (failures > 0) {
fprintf(stderr, "typer: %d/%zu characters failed to inject\n", failures, n);
}
return failures == 0 ? 0 : -1;
}
void typer_cleanup(void) {
g_target_window = None;
if (g_dpy) {
XCloseDisplay(g_dpy);
g_dpy = NULL;
+1
View File
@@ -2,6 +2,7 @@
#define VOICE_TYPER_H
int typer_init(int type_delay_us);
int typer_capture_focus(void);
int typer_type_text(const char *text);
void typer_cleanup(void);
+2 -2
View File
@@ -1,9 +1,9 @@
#ifndef VOICE_VERSION_H
#define VOICE_VERSION_H
#define VOICE_LINUX_VERSION "v0.0.2"
#define VOICE_LINUX_VERSION "v0.0.6"
#define VOICE_LINUX_VERSION_MAJOR 0
#define VOICE_LINUX_VERSION_MINOR 0
#define VOICE_LINUX_VERSION_PATCH 2
#define VOICE_LINUX_VERSION_PATCH 6
#endif
BIN
View File
Binary file not shown.