Unify voice_linux into single GUI binary with startup CPU/GPU model selection and multi-model download support
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Summary
|
||||
|
||||
Enable CUDA-accelerated whisper.cpp inference for the voice_linux project by passing the GTX 1080 Ti through to the Qubes `ai` AppVM, installing NVIDIA drivers and CUDA toolkit, and rebuilding the application with `WHISPER_CUDA=1`.
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ What's missing is the **hardware and driver stack** underneath.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[dom0 - Xen Hypervisor] -->|PCI passthrough| B[ai AppVM]
|
||||
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]
|
||||
@@ -71,7 +71,7 @@ Expected output should include:
|
||||
|
||||
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 AppVM
|
||||
### Step 0.4 — Attach GPU to the ai StandaloneVM
|
||||
|
||||
```bash
|
||||
qvm-pci attach ai dom0:65_00.0 --persistent -o permissive=true
|
||||
@@ -80,28 +80,15 @@ 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 — Decide on driver persistence
|
||||
### Step 0.5 — Persistence model (already satisfied)
|
||||
|
||||
Since `ai` is a template-based AppVM using `debian-13-xfce`, anything installed outside `/home` is lost on reboot. Three options:
|
||||
Your `ai` VM is already a **StandaloneVM**, so driver and CUDA installs persist normally across reboots.
|
||||
|
||||
| Option | Pros | Cons |
|
||||
|--------|------|------|
|
||||
| **A: Install in template** | Persists automatically, simple | Affects ALL AppVMs using that template |
|
||||
| **B: Convert to StandaloneVM** | Full persistence, isolated | Uses more disk, no template updates |
|
||||
| **C: bind-dirs** | Keeps template-based, selective persistence | More complex setup |
|
||||
|
||||
**Recommendation**: Option B (StandaloneVM) is simplest for a dedicated AI workload. Run in dom0:
|
||||
|
||||
```bash
|
||||
qvm-clone --class StandaloneVM ai ai-standalone
|
||||
# Then attach GPU to ai-standalone instead
|
||||
qvm-pci attach ai-standalone dom0:65_00.0 --persistent -o permissive=true
|
||||
qvm-pci attach ai-standalone dom0:65_00.1 --persistent -o permissive=true
|
||||
```
|
||||
No template/bind-dirs workaround is required.
|
||||
|
||||
## Phase 1: NVIDIA Driver Installation
|
||||
|
||||
> **All commands from here run inside the ai AppVM (or ai-standalone).**
|
||||
> **All commands from here run inside the ai StandaloneVM.**
|
||||
|
||||
### Step 1.1 — Install build prerequisites
|
||||
|
||||
|
||||
@@ -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.
|
||||
Executable
+45
@@ -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"
|
||||
@@ -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;
|
||||
@@ -68,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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+171
-20
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -40,6 +41,7 @@ typedef struct {
|
||||
|
||||
unsigned int meter_timer_id;
|
||||
float live_peak;
|
||||
int runtime_ready;
|
||||
} gui_state_t;
|
||||
|
||||
static float clampf(float v, float lo, float hi) {
|
||||
@@ -432,9 +434,176 @@ static void on_filter_tags_toggled(GtkToggleButton *btn, gpointer user_data) {
|
||||
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);
|
||||
@@ -597,7 +766,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};
|
||||
@@ -605,13 +775,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);
|
||||
tp.filter_bracketed_tags = st.cfg.filter_bracketed_tags;
|
||||
|
||||
if (audio_init(&ap) != 0) {
|
||||
fprintf(stderr, "fatal: audio init failed\n");
|
||||
return 1;
|
||||
@@ -627,18 +790,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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user