From 5889c1f1e12a9dae41975fe84f1cabff5c96f633 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 26 Mar 2026 20:41:14 -0400 Subject: [PATCH] Unify voice_linux into single GUI binary with startup CPU/GPU model selection and multi-model download support --- Makefile | 30 +----- README.md | 66 ++++++------ build.sh | 103 +++++++------------ config.ini | 1 + plans/gpu_enablement_plan.md | 27 ++--- plans/unified_binary_plan.md | 142 ++++++++++++++++++++++++++ scripts/download_models.sh | 45 +++++++++ src/config.c | 3 + src/config.h | 1 + src/gui_main.c | 191 +++++++++++++++++++++++++++++++---- 10 files changed, 442 insertions(+), 167 deletions(-) create mode 100644 plans/unified_binary_plan.md create mode 100755 scripts/download_models.sh diff --git a/Makefile b/Makefile index 7cd9ba6..2f883ab 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index 1043835..24de576 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.sh b/build.sh index 99a6958..ad1a479 100755 --- a/build.sh +++ b/build.sh @@ -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 \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 \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" diff --git a/config.ini b/config.ini index 3829b2f..42a4145 100644 --- a/config.ini +++ b/config.ini @@ -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 diff --git a/plans/gpu_enablement_plan.md b/plans/gpu_enablement_plan.md index dd42143..e3d2908 100644 --- a/plans/gpu_enablement_plan.md +++ b/plans/gpu_enablement_plan.md @@ -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 diff --git a/plans/unified_binary_plan.md b/plans/unified_binary_plan.md new file mode 100644 index 0000000..e6d2d0f --- /dev/null +++ b/plans/unified_binary_plan.md @@ -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. diff --git a/scripts/download_models.sh b/scripts/download_models.sh new file mode 100755 index 0000000..f41d92c --- /dev/null +++ b/scripts/download_models.sh @@ -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" diff --git a/src/config.c b/src/config.c index feccd35..a132092 100644 --- a/src/config.c +++ b/src/config.c @@ -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) { diff --git a/src/config.h b/src/config.h index 1e29e3f..37d6ba3 100644 --- a/src/config.h +++ b/src/config.h @@ -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; diff --git a/src/gui_main.c b/src/gui_main.c index b7a4410..41fcdb4 100644 --- a/src/gui_main.c +++ b/src/gui_main.c @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -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);