From 9996466363a7def4cd2e80ffc8cb37b2a6d8f439 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 26 Mar 2026 16:08:16 -0400 Subject: [PATCH] First --- .gitignore | 4 + AGENTS.md | 14 + Makefile | 59 ++++ README.md | 76 +++++ build.sh | 167 +++++++++++ config.ini | 30 ++ plans/always_on_plan.md | 182 ++++++++++++ plans/architecture.md | 322 ++++++++++++++++++++ src/audio.c | 532 +++++++++++++++++++++++++++++++++ src/audio.h | 44 +++ src/config.c | 99 +++++++ src/config.h | 28 ++ src/gui_main.c | 636 ++++++++++++++++++++++++++++++++++++++++ src/hotkey.c | 107 +++++++ src/hotkey.h | 18 ++ src/main.c | 185 ++++++++++++ src/transcribe.c | 102 +++++++ src/transcribe.h | 17 ++ src/typer.c | 95 ++++++ src/typer.h | 8 + voice_linux_gui | Bin 0 -> 127760 bytes 21 files changed, 2725 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Makefile create mode 100644 README.md create mode 100755 build.sh create mode 100644 config.ini create mode 100644 plans/always_on_plan.md create mode 100644 plans/architecture.md create mode 100644 src/audio.c create mode 100644 src/audio.h create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/gui_main.c create mode 100644 src/hotkey.c create mode 100644 src/hotkey.h create mode 100644 src/main.c create mode 100644 src/transcribe.c create mode 100644 src/transcribe.h create mode 100644 src/typer.c create mode 100644 src/typer.h create mode 100755 voice_linux_gui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12a24fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.o +voice_linux +models/*.bin +vendor/whisper.cpp/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a1b8fe4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,14 @@ +# Agent Build Policy + +This repository uses [`build.sh`](build.sh) as the single supported top-level build entrypoint. + +## Rules + +1. Always build with: + - `bash ./build.sh` +2. Do **not** use raw `gcc` or `make` directly for top-level project builds unless the user explicitly asks for it. +3. If `WITH_WHISPER`, `WHISPER_CUDA`, or `BUILD_GUI` behavior is needed, pass them as environment variables to [`build.sh`](build.sh), for example: + - `WITH_WHISPER=1 bash ./build.sh` + - `WHISPER_CUDA=1 bash ./build.sh` + - `BUILD_GUI=0 bash ./build.sh` +4. For any user-facing build instructions, prefer referencing [`build.sh`](build.sh) over direct compiler or make commands. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7cd9ba6 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +CC ?= gcc + +CFLAGS_COMMON := -std=c99 -Wall -Wextra -Wpedantic -O2 -g -D_POSIX_C_SOURCE=200809L +CFLAGS := $(CFLAGS_COMMON) +LDFLAGS := +LIBS := -lpulse-simple -lpulse -lX11 -lXtst -lpthread +GTK_CFLAGS := $(shell pkg-config --cflags gtk+-3.0 2>/dev/null) +GTK_LIBS := $(shell pkg-config --libs gtk+-3.0 2>/dev/null) + +WITH_WHISPER ?= 0 +WHISPER_DIR ?= ./vendor/whisper.cpp + +ifeq ($(WITH_WHISPER),1) +CFLAGS += -DWITH_WHISPER=1 -isystem $(WHISPER_DIR)/include -isystem $(WHISPER_DIR)/ggml/include +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 +LIBS += -lwhisper -lggml +endif + +SRC := \ + ./src/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 + +all: $(TARGET) $(GUI_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 + $(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) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6549b85 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# voice_linux (C99) + +Local hotkey dictation for Linux/X11 using: +- PulseAudio capture +- whisper.cpp transcription +- X11/XTest text injection + +## Build deps (Debian/Qubes AppVM) + +```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 +``` + +## Build app + +```bash +# builds CLI + GUI by default +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 +``` + +## Run + +```bash +# CLI hotkey mode (uses ./config.ini by default) +./voice_linux + +# GTK3 window mode (uses ./config.ini by default) +./voice_linux_gui +``` + +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: + +```bash +pactl list short sources +``` + +Then set `audio_device=` in `./config.ini` to the source you want. + +## Notes + +- This implementation targets **X11** (not native Wayland). +- In template-based AppVMs, system packages may not persist unless installed in the template. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..99a6958 --- /dev/null +++ b/build.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +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" +MODEL_PATH="./models/ggml-base.en.bin" + +need_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "[build] error: required command '$1' not found" >&2 + exit 2 + fi +} + +have_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +CFLAGS_COMMON=( + -std=c99 -Wall -Wextra -Wpedantic -O2 -g + -D_POSIX_C_SOURCE=200809L +) + +LDFLAGS_COMMON=( + -lpulse-simple -lpulse -lX11 -lXtst -lpthread +) + +echo "[build] root: $ROOT_DIR" +echo "[build] WITH_WHISPER=$WITH_WHISPER WHISPER_CUDA=$WHISPER_CUDA BUILD_GUI=$BUILD_GUI JOBS=$JOBS" + +need_cmd gcc + +if [[ "$WITH_WHISPER" == "1" ]]; then + if ! have_cmd cmake; then + echo "[build] warning: cmake not found; forcing WITH_WHISPER=0" >&2 + WITH_WHISPER=0 + fi +fi + +if [[ "$WITH_WHISPER" == "1" ]]; then + if [[ ! -d "$WHISPER_DIR" ]] && ! have_cmd git; then + echo "[build] warning: git not found and whisper.cpp is missing; forcing WITH_WHISPER=0" >&2 + WITH_WHISPER=0 + fi +fi + +if [[ "$WITH_WHISPER" == "1" ]]; then + if [[ ! -f "$MODEL_PATH" ]] && ! have_cmd wget; then + echo "[build] warning: wget not found and model is missing; forcing WITH_WHISPER=0" >&2 + WITH_WHISPER=0 + 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" + +if [[ "$WITH_WHISPER" == "1" ]]; then + if [[ ! -d "$WHISPER_DIR" ]]; then + echo "[build] cloning whisper.cpp..." + mkdir -p ./vendor + git clone https://github.com/ggerganov/whisper.cpp.git "$WHISPER_DIR" + fi + + echo "[build] configuring whisper.cpp..." + if [[ "$WHISPER_CUDA" == "1" ]]; then + cmake -S "$WHISPER_DIR" -B "$WHISPER_DIR/build" -DWHISPER_CUDA=ON + else + cmake -S "$WHISPER_DIR" -B "$WHISPER_DIR/build" + fi + + echo "[build] building whisper.cpp..." + cmake --build "$WHISPER_DIR/build" -j"$JOBS" + + mkdir -p ./models + if [[ ! -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 CLI binary..." +CLI_CFLAGS=("${CFLAGS_COMMON[@]}") +CLI_LDFLAGS=("${LDFLAGS_COMMON[@]}") + +if [[ "$WITH_WHISPER" == "1" ]]; then + CLI_CFLAGS+=( + -DWITH_WHISPER=1 + -isystem "$WHISPER_DIR/include" + -isystem "$WHISPER_DIR/ggml/include" + ) + CLI_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 "${CLI_CFLAGS[@]}" \ + ./src/main.c ./src/config.c ./src/hotkey.c ./src/audio.c ./src/transcribe.c ./src/typer.c \ + -o ./voice_linux \ + "${CLI_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" diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..bb87898 --- /dev/null +++ b/config.ini @@ -0,0 +1,30 @@ +# voice_linux runtime configuration +# Hotkey format supports Ctrl, Alt, Shift, Super + final key symbol +# Example: Ctrl+Alt+V + +model_path=./models/ggml-base.en.bin +audio_device=alsa_input.usb-Shure_Inc_Shure_MV6_MV6_5-ed2d37fa14d3e65d8d477cacf910d93c-01.mono-fallback +sample_rate=16000 +language=en +hotkey=Ctrl+Alt+V +type_delay_us=8000 +max_record_seconds=30 +# always-on VAD controls +always_on=1 +# legacy window size (unused by new VAD pipeline, kept for compatibility) +always_on_window_ms=1400 + +# live level threshold (0.0 - 1.0) +vad_peak_threshold=0.006 +# level must stay above threshold this long before speech starts +vad_trigger_ms=100 +# level must stay below threshold this long before speech ends +vad_release_ms=300 +# include this much pre-trigger audio in transcript segment +vad_preroll_ms=500 +# discard segments shorter than this +vad_min_speech_ms=250 +# hard cap for one speech segment +vad_max_speech_ms=15000 + +autotype_enabled=0 diff --git a/plans/always_on_plan.md b/plans/always_on_plan.md new file mode 100644 index 0000000..f786b0c --- /dev/null +++ b/plans/always_on_plan.md @@ -0,0 +1,182 @@ +# Always-On Dictation Plan (Qubes + Linux/X11) + +## Goal +Build an **always-running** speech pipeline that is safe, low-friction, and works with your mic’s physical mute button. + +## Design Principles +1. **Mic hardware mute is king** (privacy first). +2. **Continuous capture, segmented transcription** (VAD/endpointer). +3. **Conservative text injection defaults** (avoid accidental typing). +4. **Dedicated StandaloneVM deployment** for long-term reliability. + +--- + +## Phase A — Stabilize Current UX (1–2 sessions) + +### A1. Finalize GUI feedback states +- Keep current button states: + - idle + - recording + - transcribing +- Add explicit status line values: + - `Mic active, waiting for speech` + - `Speech detected` + - `Segment ended, transcribing` + - `Mic muted or near-silent` + +### A2. Add explicit mode selector in GUI +- Modes: + - **Push-to-talk** (existing) + - **Always-on preview** (new) + - **Always-on auto-type** (advanced) + +### A3. Add per-mode safety defaults +- Default to **Always-on preview** (no typing). +- Require explicit checkbox to enable auto-typing. + +--- + +## Phase B — Always-On Audio Engine (core) + +### B1. Ring buffer capture loop +Refactor audio path to run continuously: +- Thread captures audio at fixed chunk size (e.g., 20 ms frames). +- Store in ring buffer (N seconds history, e.g., 30s). +- Expose non-blocking read windows for VAD and segment extraction. + +### B2. Voice activity detection (VAD) +Implement lightweight VAD first: +- RMS/energy threshold + zero crossing heuristic. +- Track states: + - silence + - speech_started + - speech_active + - speech_ended +- Tunables in config: + - `vad_energy_threshold` + - `vad_start_ms` + - `vad_end_silence_ms` + +(Optionally later: replace with Silero VAD or WebRTC VAD wrapper.) + +### B3. Segment endpointer +When speech ends: +- Cut a segment with small pre-roll/post-roll (e.g., 250 ms / 200 ms). +- Dispatch to transcription worker queue. +- Continue capturing while worker runs. + +### B4. Transcription worker thread +- Single worker initially (simple, deterministic). +- Queue segment jobs FIFO. +- Return: + - text + - confidence surrogate (optional) + - latency metrics + +--- + +## Phase C — Hardware Mute Integration + +### C1. Software mute inference (must-have) +Even without HID events: +- Detect sustained near-zero energy (e.g., >800ms). +- Mark state as `muted_or_silent`. +- In GUI show muted icon/state. + +### C2. HID/evdev mute button (nice-to-have) +If the mic exposes events: +- Open relevant `/dev/input/event*`. +- Parse key/switch event for mute toggle. +- Map to explicit app state `hardware_muted=true/false`. + +Fallback remains C1 inference so behavior is robust across devices. + +--- + +## Phase D — Output Policy for Continuous Mode + +### D1. Preview-first pipeline +- Continuous transcript appears in GUI text area. +- Nothing typed automatically by default. + +### D2. Commit model +Two safe options: +1. **Manual commit** button: insert selected transcript into focused app. +2. **Auto-commit finals**: type only finalized segments (not partials). + +### D3. Guard rails +- Minimum confidence/length filter before typing. +- Ignore common noise artifacts (`[BLANK_AUDIO]`, empty, punctuation-only). +- Cooldown between auto-typed segments (e.g., 300 ms). + +--- + +## Phase E — Observability and Tuning + +### E1. Metrics +Track and expose: +- capture RMS +- VAD state transitions +- segment durations +- transcription latency +- typed character count + +### E2. Debug panel in GUI +Small collapsible panel with: +- live level meter +- current VAD state +- current device name +- queue length / worker busy + +### E3. Config knobs +Add to config: +- `mode=push_to_talk|always_on_preview|always_on_autotype` +- `vad_energy_threshold` +- `vad_end_silence_ms` +- `autotype_enabled` +- `autotype_min_chars` +- `autotype_cooldown_ms` + +--- + +## Phase F — Qubes Deployment Model + +### F1. Dedicated StandaloneVM +Deploy final app in its own StandaloneVM: +- stable dependencies +- controlled attack surface +- predictable startup behavior + +### F2. Device routing +- Attach USB mic to this VM via sys-usb. +- Keep always-on transcription isolated from high-trust VMs. + +### F3. Startup behavior +- `systemd --user` service for auto-launch on login. +- GUI starts minimized/normal per config. + +--- + +## Implementation Order (recommended) +1. Always-on preview mode (B + D1) +2. VAD + endpoint tuning UI (E) +3. Manual commit + guarded auto-commit (D2/D3) +4. Hardware mute event support if available (C2) +5. StandaloneVM packaging + autostart (F) + +--- + +## Risks and Mitigations +- **False triggers / noise typing** → preview-first default, confidence/length filters. +- **Long transcription latency** → worker queue + clear transcribing state. +- **Mic mute not exposed via HID** → software mute inference fallback. +- **Qubes audio routing quirks** → device selector + live level meter + status diagnostics. + +--- + +## Definition of Done (MVP Always-On) +- App runs continuously with no manual start/stop needed. +- Speech automatically segmented and transcribed. +- GUI clearly indicates idle/speech/transcribing/muted. +- Preview mode stable for long sessions. +- Optional typed output only for finalized segments with safety guards. diff --git a/plans/architecture.md b/plans/architecture.md new file mode 100644 index 0000000..7486bd1 --- /dev/null +++ b/plans/architecture.md @@ -0,0 +1,322 @@ +# Voice Linux — Speech-to-Text Dictation System + +## Overview + +A local, GPU-accelerated speech-to-text dictation tool for Qubes OS. Written in C99, using `whisper.cpp` with CUDA for transcription, PulseAudio for audio capture, and Xlib/XTest for global hotkey and text injection. + +**Press a hotkey → speak → text appears in the focused window.** + +## System Environment + +| Property | Value | +|---|---| +| OS | Qubes OS (AppVM) | +| Session | X11 / XFCE | +| Audio | PipeWire 1.4.2 with PulseAudio compat | +| Audio source | `qubes-source` (virtual PipeWire device) | +| Python | 3.13.5 (not used for main app — C99 instead) | +| Display GPU | AMD (dom0) | +| Compute GPU | NVIDIA GeForce GTX (to be passed through) | + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ voice_linux │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ +│ │ hotkey │──▶│ audio │──▶│ transcribe │ │ +│ │ (Xlib │ │ (Pulse │ │ (whisper.cpp │ │ +│ │ XGrabKey)│ │ Simple │ │ CUDA-accel) │ │ +│ └──────────┘ │ API) │ └────────┬─────────┘ │ +│ └──────────┘ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ typer │ │ +│ │ (XTest fake │ │ +│ │ key events) │ │ +│ └──────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +### Components + +| Module | File | C API Used | Purpose | +|---|---|---|---| +| Hotkey | `src/hotkey.c` | Xlib `XGrabKey` | Global push-to-talk hotkey on X11 | +| Audio | `src/audio.c` | PulseAudio Simple API | Record from `qubes-source` into PCM buffer | +| Transcribe | `src/transcribe.c` | whisper.cpp C API | Feed PCM to Whisper, get text back | +| Typer | `src/typer.c` | XTest `XTestFakeKeyEvent` | Type transcribed text into focused window | +| Config | `src/config.c` | stdio | Load YAML/INI config for hotkey, model, device | +| Main | `src/main.c` | — | Wire everything together | + +### Data Flow + +1. `main()` initializes all modules, loads config +2. `hotkey_listen()` blocks on X11 event loop +3. On hotkey press → `audio_start_recording()` begins capturing from PulseAudio +4. On hotkey release (or second press) → `audio_stop_recording()` returns PCM buffer +5. `transcribe()` feeds PCM buffer to whisper.cpp → returns `char *text` +6. `typer_type_text(text)` sends fake key events via XTest to focused window +7. Loop back to step 2 + +## Project Structure + +``` +voice_linux/ +├── Makefile # Build system +├── README.md # Setup and usage +├── config.ini # User config (hotkey, model, audio device) +├── plans/ +│ └── architecture.md # This document +├── src/ +│ ├── main.c # Entry point and orchestrator +│ ├── hotkey.h / hotkey.c # Global X11 hotkey via XGrabKey +│ ├── audio.h / audio.c # PulseAudio Simple API recording +│ ├── transcribe.h / transcribe.c # whisper.cpp integration +│ ├── typer.h / typer.c # XTest text injection +│ └── config.h / config.c # Config file parser +├── models/ # Downloaded whisper models (gitignored) +│ └── ggml-base.en.bin +└── vendor/ + └── whisper.cpp/ # Git submodule or local clone +``` + +## Phase 0: GPU Passthrough (Qubes) + +### Prerequisites +- AMD GPU handles dom0 display (confirmed) +- NVIDIA GTX is idle and available for passthrough (confirmed) +- IOMMU/VT-d enabled in BIOS (likely, since Qubes requires it) + +### Detected Hardware + +``` +17:00.0 VGA compatible controller: NVIDIA Corporation GA107 [GeForce RTX 3050 6GB] (rev a1) +17:00.1 Audio device: NVIDIA Corporation GA107 High Definition Audio Controller (rev a1) +65:00.0 VGA compatible controller: NVIDIA Corporation GP102 [GeForce GTX 1080 Ti] (rev a1) +65:00.1 Audio device: NVIDIA Corporation GP102 HDMI Audio Controller (rev a1) +``` + +**Target GPU**: GTX 1080 Ti (11GB VRAM, CUDA compute 6.1) at BDF `65:00.0` / `65:00.1` + +### AppVM Details + +- **Name**: `ai` +- **Type**: AppVM (template: `debian-13-xfce`) +- **Network**: `sys-vpn-mullvad-app` + +> **Important**: Since `ai` is a template-based AppVM, NVIDIA drivers installed inside it +> will be **lost on reboot** (only `/home` persists). Two options: +> - **Option A**: Install drivers in the `debian-13-xfce` template (affects all AppVMs using it) +> - **Option B**: Convert `ai` to a StandaloneVM: `qvm-clone --class StandaloneVM ai ai-standalone` +> - **Option C**: Use a bind-dirs mechanism to persist `/usr/lib/nvidia` and related paths + +### Steps (run in dom0 terminal) + +#### 0.1 — Hide the GTX 1080 Ti from dom0 + +Edit `/etc/default/grub` in dom0: + +```bash +sudo nano /etc/default/grub +``` + +Find the **first** `GRUB_CMDLINE_LINUX` line: +``` +GRUB_CMDLINE_LINUX="rd.luks.uuid=luks-ebcd163d-dcf3-47bf-8da1-f7c13d82304b rd.lvm.lv=qubes_dom0/root rd.lvm.lv=qubes_dom0/swap plymouth.ignore-serial-consoles rhgb quiet" +``` + +Add `rd.qubes.hide_pci=65:00.0,65:00.1` to the end (inside the quotes): +``` +GRUB_CMDLINE_LINUX="rd.luks.uuid=luks-ebcd163d-dcf3-47bf-8da1-f7c13d82304b rd.lvm.lv=qubes_dom0/root rd.lvm.lv=qubes_dom0/swap plymouth.ignore-serial-consoles rhgb quiet rd.qubes.hide_pci=65:00.0,65:00.1" +``` + +#### 0.2 — Regenerate GRUB and reboot + +```bash +# Try EFI path first (most common on modern Qubes): +sudo grub2-mkconfig -o /boot/efi/EFI/qubes/grub.cfg + +# If that path doesn't exist, try: +# sudo grub2-mkconfig -o /boot/grub2/grub.cfg + +# Reboot dom0: +sudo reboot +``` + +#### 0.3 — Verify GPU is assignable (after reboot) + +```bash +xl pci-assignable-list +# Should show: +# 0000:65:00.0 +# 0000:65:00.1 +``` + +#### 0.4 — Attach GPU to the 'ai' AppVM + +```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 +``` + +#### 0.5 — Install NVIDIA drivers inside the AppVM + +Start the `ai` VM, then inside it: + +```bash +# Inside the ai AppVM: +sudo apt update +sudo apt install -y build-essential linux-headers-$(uname -r) + +# For GTX 1080 Ti (Pascal/GP102), use the 550.x driver branch: +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 +``` + +> `--no-opengl-files` is critical in Qubes — we don't want to replace the VM's display GL, just get CUDA compute. + +#### 0.6 — Install CUDA Toolkit + +```bash +# Inside the ai AppVM: +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 +echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrc +echo 'export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc +source ~/.bashrc +``` + +#### 0.7 — Verify + +```bash +nvidia-smi +nvcc --version +``` + +Expected output: GTX 1080 Ti with 11GB VRAM, CUDA 12.6. + +## Phase 1: Build and Test Core Application + +### 1.1 — Install build dependencies + +```bash +sudo apt install -y \ + build-essential \ + cmake \ + git \ + libpulse-dev \ + libx11-dev \ + libxtst-dev \ + xdotool +``` + +### 1.2 — Clone and build whisper.cpp with CUDA + +```bash +cd vendor/ +git clone https://github.com/ggerganov/whisper.cpp.git +cd whisper.cpp +cmake -B build -DWHISPER_CUDA=ON +cmake --build build --config Release -j$(nproc) +``` + +### 1.3 — Download whisper model + +```bash +cd models/ +# base.en — fast, good for English dictation (~150MB) +wget https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin + +# Or medium.en for better accuracy (~1.5GB, still fast with GPU): +# wget https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.en.bin +``` + +### 1.4 — Implement C99 modules + +Each module is a small, focused C file with a clean header: + +**`src/audio.h`** — Audio capture +- `int audio_init(const char *device_name)` — connect to PulseAudio +- `int audio_start_recording(void)` — begin capturing PCM +- `int audio_stop_recording(float **samples, size_t *n_samples)` — stop and return buffer +- `void audio_cleanup(void)` — disconnect + +**`src/transcribe.h`** — Whisper integration +- `int transcribe_init(const char *model_path)` — load model (CUDA auto-detected) +- `char *transcribe_buffer(const float *samples, size_t n_samples)` — run inference +- `void transcribe_cleanup(void)` — free model + +**`src/typer.h`** — Text injection +- `int typer_init(void)` — open X11 display +- `int typer_type_text(const char *text)` — send fake key events +- `void typer_cleanup(void)` — close display + +**`src/hotkey.h`** — Global hotkey +- `int hotkey_init(unsigned int keycode, unsigned int modifiers)` — grab key +- `int hotkey_wait_press(void)` — block until hotkey pressed +- `int hotkey_wait_release(void)` — block until hotkey released +- `void hotkey_cleanup(void)` — ungrab and close + +**`src/config.h`** — Configuration +- `typedef struct { ... } config_t` +- `int config_load(const char *path, config_t *cfg)` — parse INI file +- Default values for hotkey (Ctrl+Alt+V), model path, audio device + +### 1.5 — Build with Makefile + +```makefile +CC = gcc +CFLAGS = -std=c99 -Wall -Wextra -O2 +LDFLAGS = -lpulse-simple -lpulse -lX11 -lXtst -lwhisper -L./vendor/whisper.cpp/build/src +INCLUDES = -I./vendor/whisper.cpp/include + +voice_linux: src/main.c src/audio.c src/transcribe.c src/typer.c src/hotkey.c src/config.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS) +``` + +### 1.6 — Test end-to-end + +```bash +./voice_linux --model models/ggml-base.en.bin --device qubes-source +# Press Ctrl+Alt+V, speak, press again, text appears in focused window +``` + +## Phase 2: Polish + +- **Audio feedback**: Play a short beep via PulseAudio when recording starts/stops +- **Config file**: `config.ini` with hotkey, model path, audio device, typing delay +- **Systemd user service**: `~/.config/systemd/user/voice-linux.service` for auto-start +- **Logging**: Optional debug log to `~/.local/share/voice_linux/voice.log` + +## Phase 3: Hardware + +- **USB microphone**: Attach via Qubes device manager (`qvm-usb attach`) from sys-usb +- **Physical button**: Read HID events via `/dev/input/eventN` using `evdev` in C +- **Model tuning**: Try `medium.en` or `large-v3` models with GPU — should still be fast + +## Key Risks and Mitigations + +| Risk | Mitigation | +|---|---| +| GPU passthrough fails (IOMMU grouping) | Fall back to CPU-only whisper.cpp (still works, just slower) | +| Older GTX lacks CUDA compute capability | whisper.cpp supports compute 5.0+; most GTX 900+ series work | +| PulseAudio `qubes-source` has no audio | Need mic attached + Qubes audio policy allowing input | +| XGrabKey conflicts with desktop shortcuts | Use an uncommon hotkey combo; make it configurable | +| whisper.cpp API changes | Pin to a specific release tag | + +## Model Size vs Speed Reference (GPU) + +| Model | Size | VRAM | Speed (10s audio) | Accuracy | +|---|---|---|---|---| +| tiny.en | 75MB | ~1GB | <0.5s | Fair | +| base.en | 150MB | ~1GB | <1s | Good | +| small.en | 500MB | ~2GB | ~1-2s | Very good | +| medium.en | 1.5GB | ~5GB | ~2-3s | Excellent | +| large-v3 | 3GB | ~10GB | ~4-6s | Best | + +For dictation, `base.en` is the sweet spot to start. Upgrade to `medium.en` if accuracy isn't sufficient. diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..4c9e292 --- /dev/null +++ b/src/audio.c @@ -0,0 +1,532 @@ +#include "audio.h" + +#include +#include + +#include +#include +#include +#include +#include + +typedef enum { + VAD_IDLE = 0, + VAD_ONSET, + VAD_SPEECH, + VAD_TRAILING, +} vad_state_t; + +typedef struct { + pa_simple *pa; + audio_params_t params; + + pthread_t monitor_thread; + bool monitor_running; + bool stop_requested; + + pthread_mutex_t lock; + + float peak_level; + + bool manual_recording; + float *manual_buffer; + size_t manual_count; + size_t manual_cap; + + bool always_on_enabled; + audio_vad_params_t vad; + vad_state_t vad_state; + int onset_count; + int trailing_count; + int speech_count; + + float *ring_buffer; + size_t ring_cap; + size_t ring_head; + size_t ring_count; + + float *segment_buffer; + size_t segment_count; + size_t segment_cap; + + float *ready_segment; + size_t ready_count; +} audio_state_t; + +static audio_state_t g_audio; + +static const char *vad_state_name(vad_state_t s) { + switch (s) { + case VAD_IDLE: return "IDLE"; + case VAD_ONSET: return "ONSET"; + case VAD_SPEECH: return "SPEECH"; + case VAD_TRAILING: return "TRAILING"; + default: return "?"; + } +} + +static void set_vad_state_locked(vad_state_t next, const char *reason, float level) { + if (g_audio.vad_state == next) return; + fprintf(stderr, "[vad] %s -> %s (%s, level=%.4f thr=%.4f)\n", + vad_state_name(g_audio.vad_state), vad_state_name(next), reason, level, g_audio.vad.threshold); + g_audio.vad_state = next; +} + +static size_t ms_to_samples(int sample_rate, int ms) { + if (sample_rate <= 0 || ms <= 0) return 1; + long long n = (long long)sample_rate * (long long)ms / 1000LL; + if (n < 1) n = 1; + return (size_t)n; +} + +static int reserve_float_buffer(float **buffer, size_t *cap, size_t needed) { + if (needed <= *cap) return 0; + size_t new_cap = *cap ? (*cap * 2) : 4096; + while (new_cap < needed) { + if (new_cap > ((size_t)-1) / 2) { + return -1; + } + new_cap *= 2; + } + float *p = (float *)realloc(*buffer, new_cap * sizeof(float)); + if (!p) return -1; + *buffer = p; + *cap = new_cap; + return 0; +} + +static void ring_push_sample(float s) { + if (!g_audio.ring_buffer || g_audio.ring_cap == 0) return; + + if (g_audio.ring_count < g_audio.ring_cap) { + size_t idx = (g_audio.ring_head + g_audio.ring_count) % g_audio.ring_cap; + g_audio.ring_buffer[idx] = s; + g_audio.ring_count++; + } else { + g_audio.ring_buffer[g_audio.ring_head] = s; + g_audio.ring_head = (g_audio.ring_head + 1) % g_audio.ring_cap; + } +} + +static int ring_copy_last(size_t n, float **dst, size_t *dst_count) { + if (!dst || !dst_count) return -1; + *dst = NULL; + *dst_count = 0; + + if (!g_audio.ring_buffer || g_audio.ring_count == 0 || n == 0) { + return 0; + } + + if (n > g_audio.ring_count) n = g_audio.ring_count; + float *out = (float *)malloc(n * sizeof(float)); + if (!out) return -1; + + size_t start = (g_audio.ring_head + g_audio.ring_count - n) % g_audio.ring_cap; + for (size_t i = 0; i < n; ++i) { + out[i] = g_audio.ring_buffer[(start + i) % g_audio.ring_cap]; + } + + *dst = out; + *dst_count = n; + return 0; +} + +static void queue_ready_segment_locked(void) { + if (g_audio.segment_count == 0) return; + + float *copy = (float *)malloc(g_audio.segment_count * sizeof(float)); + if (!copy) return; + memcpy(copy, g_audio.segment_buffer, g_audio.segment_count * sizeof(float)); + + free(g_audio.ready_segment); + g_audio.ready_segment = copy; + g_audio.ready_count = g_audio.segment_count; +} + +static void reset_segment_locked(void) { + g_audio.segment_count = 0; + g_audio.speech_count = 0; + g_audio.onset_count = 0; + g_audio.trailing_count = 0; +} + +static int append_segment_block_locked(const float *samples, size_t n) { + if (!samples || n == 0) return 0; + if (reserve_float_buffer(&g_audio.segment_buffer, &g_audio.segment_cap, g_audio.segment_count + n) != 0) { + return -1; + } + memcpy(&g_audio.segment_buffer[g_audio.segment_count], samples, n * sizeof(float)); + g_audio.segment_count += n; + return 0; +} + +static void vad_begin_speech_locked(void) { + size_t preroll_samples = ms_to_samples(g_audio.params.sample_rate, g_audio.vad.preroll_ms); + float *pre = NULL; + size_t pre_n = 0; + if (ring_copy_last(preroll_samples, &pre, &pre_n) == 0 && pre && pre_n > 0) { + if (reserve_float_buffer(&g_audio.segment_buffer, &g_audio.segment_cap, pre_n) == 0) { + memcpy(g_audio.segment_buffer, pre, pre_n * sizeof(float)); + g_audio.segment_count = pre_n; + g_audio.speech_count = (int)pre_n; + } + free(pre); + } +} + +static void vad_finish_segment_locked(void) { + size_t min_speech = ms_to_samples(g_audio.params.sample_rate, g_audio.vad.min_speech_ms); + if (g_audio.segment_count >= min_speech) { + queue_ready_segment_locked(); + fprintf(stderr, "[vad] segment ready: %zu samples (%.2fs)\n", + g_audio.ready_count, + (double)g_audio.ready_count / (double)g_audio.params.sample_rate); + } else { + fprintf(stderr, "[vad] segment dropped: too short (%zu < %zu samples)\n", + g_audio.segment_count, min_speech); + } + + set_vad_state_locked(VAD_IDLE, "finish", 0.0f); + reset_segment_locked(); +} + +static void process_vad_chunk_locked(const float *chunk, size_t n, float chunk_peak) { + if (!chunk || n == 0) return; + + const float threshold = g_audio.vad.threshold; + const int above = chunk_peak >= threshold; + + size_t trigger_n = ms_to_samples(g_audio.params.sample_rate, g_audio.vad.trigger_ms); + size_t release_n = ms_to_samples(g_audio.params.sample_rate, g_audio.vad.release_ms); + size_t max_speech_n = ms_to_samples(g_audio.params.sample_rate, g_audio.vad.max_speech_ms); + + switch (g_audio.vad_state) { + case VAD_IDLE: + if (above) { + g_audio.onset_count = (int)n; + set_vad_state_locked(VAD_ONSET, "above threshold", chunk_peak); + } + break; + + case VAD_ONSET: + if (above) { + g_audio.onset_count += (int)n; + if ((size_t)g_audio.onset_count >= trigger_n) { + reset_segment_locked(); + vad_begin_speech_locked(); + if (append_segment_block_locked(chunk, n) != 0) { + set_vad_state_locked(VAD_IDLE, "segment alloc failure", chunk_peak); + reset_segment_locked(); + break; + } + g_audio.speech_count += (int)n; + g_audio.trailing_count = 0; + set_vad_state_locked(VAD_SPEECH, "trigger reached", chunk_peak); + } + } else { + set_vad_state_locked(VAD_IDLE, "onset fell below threshold", chunk_peak); + g_audio.onset_count = 0; + } + break; + + case VAD_SPEECH: + if (append_segment_block_locked(chunk, n) != 0) { + set_vad_state_locked(VAD_IDLE, "segment alloc failure", chunk_peak); + reset_segment_locked(); + break; + } + g_audio.speech_count += (int)n; + + if ((size_t)g_audio.speech_count >= max_speech_n) { + vad_finish_segment_locked(); + break; + } + + if (!above) { + g_audio.trailing_count = (int)n; + set_vad_state_locked(VAD_TRAILING, "below threshold", chunk_peak); + } + break; + + case VAD_TRAILING: + if (append_segment_block_locked(chunk, n) != 0) { + set_vad_state_locked(VAD_IDLE, "segment alloc failure", chunk_peak); + reset_segment_locked(); + break; + } + g_audio.speech_count += (int)n; + + if ((size_t)g_audio.speech_count >= max_speech_n) { + vad_finish_segment_locked(); + break; + } + + if (above) { + set_vad_state_locked(VAD_SPEECH, "voice resumed", chunk_peak); + g_audio.trailing_count = 0; + } else { + g_audio.trailing_count += (int)n; + if ((size_t)g_audio.trailing_count >= release_n) { + vad_finish_segment_locked(); + } + } + break; + } +} + +static void *monitor_thread_fn(void *unused) { + (void)unused; + + const size_t chunk = 1024; + float tmp[1024]; + int err = 0; + + while (1) { + pthread_mutex_lock(&g_audio.lock); + bool should_stop = g_audio.stop_requested; + pthread_mutex_unlock(&g_audio.lock); + if (should_stop) break; + + if (pa_simple_read(g_audio.pa, tmp, sizeof(tmp), &err) < 0) { + fprintf(stderr, "audio: pa_simple_read failed: %s\n", pa_strerror(err)); + break; + } + + float chunk_peak = 0.0f; + for (size_t i = 0; i < chunk; ++i) { + float a = tmp[i] < 0.0f ? -tmp[i] : tmp[i]; + if (a > chunk_peak) chunk_peak = a; + } + + pthread_mutex_lock(&g_audio.lock); + + g_audio.peak_level = chunk_peak; + + size_t max_manual = (size_t)g_audio.params.sample_rate * (size_t)g_audio.params.max_seconds; + for (size_t i = 0; i < chunk; ++i) { + float s = tmp[i]; + ring_push_sample(s); + + if (g_audio.manual_recording) { + if (g_audio.manual_count < max_manual && + reserve_float_buffer(&g_audio.manual_buffer, &g_audio.manual_cap, g_audio.manual_count + 1) == 0) { + g_audio.manual_buffer[g_audio.manual_count++] = s; + } + } + } + + if (g_audio.always_on_enabled) { + process_vad_chunk_locked(tmp, chunk, chunk_peak); + } + + pthread_mutex_unlock(&g_audio.lock); + } + + pthread_mutex_lock(&g_audio.lock); + g_audio.monitor_running = false; + pthread_mutex_unlock(&g_audio.lock); + + return NULL; +} + +int audio_init(const audio_params_t *params) { + if (!params) return -1; + + memset(&g_audio, 0, sizeof(g_audio)); + g_audio.params = *params; + + pthread_mutex_init(&g_audio.lock, NULL); + + g_audio.vad.threshold = 0.006f; + g_audio.vad.trigger_ms = 100; + g_audio.vad.release_ms = 300; + g_audio.vad.preroll_ms = 500; + g_audio.vad.min_speech_ms = 250; + g_audio.vad.max_speech_ms = 15000; + + pa_sample_spec ss = { + .format = PA_SAMPLE_FLOAT32LE, + .rate = (uint32_t)g_audio.params.sample_rate, + .channels = 1, + }; + + int err = 0; + g_audio.pa = pa_simple_new( + NULL, + "voice_linux", + PA_STREAM_RECORD, + g_audio.params.device[0] ? g_audio.params.device : NULL, + "dictation", + &ss, + NULL, + NULL, + &err); + + if (!g_audio.pa) { + fprintf(stderr, "audio: pa_simple_new failed: %s\n", pa_strerror(err)); + pthread_mutex_destroy(&g_audio.lock); + return -2; + } + + size_t ring_cap = ms_to_samples(g_audio.params.sample_rate, 6000); + g_audio.ring_buffer = (float *)malloc(ring_cap * sizeof(float)); + if (!g_audio.ring_buffer) { + pa_simple_free(g_audio.pa); + g_audio.pa = NULL; + pthread_mutex_destroy(&g_audio.lock); + return -3; + } + g_audio.ring_cap = ring_cap; + + if (pthread_create(&g_audio.monitor_thread, NULL, monitor_thread_fn, NULL) != 0) { + free(g_audio.ring_buffer); + g_audio.ring_buffer = NULL; + g_audio.ring_cap = 0; + pa_simple_free(g_audio.pa); + g_audio.pa = NULL; + pthread_mutex_destroy(&g_audio.lock); + return -4; + } + + g_audio.monitor_running = true; + return 0; +} + +int audio_start_recording(void) { + pthread_mutex_lock(&g_audio.lock); + g_audio.manual_count = 0; + g_audio.manual_recording = true; + pthread_mutex_unlock(&g_audio.lock); + return 0; +} + +int audio_stop_recording(float **samples_out, size_t *count_out) { + if (!samples_out || !count_out) return -1; + + pthread_mutex_lock(&g_audio.lock); + g_audio.manual_recording = false; + + size_t n = g_audio.manual_count; + float *out = NULL; + if (n > 0) { + out = (float *)malloc(n * sizeof(float)); + if (out) { + memcpy(out, g_audio.manual_buffer, n * sizeof(float)); + } + } + pthread_mutex_unlock(&g_audio.lock); + + *samples_out = out; + *count_out = n; + return (out || n == 0) ? 0 : -2; +} + +int audio_set_vad_params(const audio_vad_params_t *params) { + if (!params) return -1; + + pthread_mutex_lock(&g_audio.lock); + g_audio.vad = *params; + + if (g_audio.vad.threshold < 0.0f) g_audio.vad.threshold = 0.0f; + if (g_audio.vad.threshold > 1.0f) g_audio.vad.threshold = 1.0f; + if (g_audio.vad.trigger_ms < 10) g_audio.vad.trigger_ms = 10; + if (g_audio.vad.release_ms < 10) g_audio.vad.release_ms = 10; + if (g_audio.vad.preroll_ms < 0) g_audio.vad.preroll_ms = 0; + if (g_audio.vad.min_speech_ms < 50) g_audio.vad.min_speech_ms = 50; + if (g_audio.vad.max_speech_ms < g_audio.vad.min_speech_ms) g_audio.vad.max_speech_ms = g_audio.vad.min_speech_ms; + pthread_mutex_unlock(&g_audio.lock); + + return 0; +} + +int audio_set_always_on_enabled(int enabled) { + pthread_mutex_lock(&g_audio.lock); + g_audio.always_on_enabled = enabled ? true : false; + if (!g_audio.always_on_enabled) { + set_vad_state_locked(VAD_IDLE, "always-on disabled", 0.0f); + reset_segment_locked(); + } + pthread_mutex_unlock(&g_audio.lock); + return 0; +} + +float audio_get_peak_level(void) { + pthread_mutex_lock(&g_audio.lock); + float p = g_audio.peak_level; + pthread_mutex_unlock(&g_audio.lock); + return p; +} + +int audio_get_speech_segment(float **samples_out, size_t *count_out) { + if (!samples_out || !count_out) return -1; + + pthread_mutex_lock(&g_audio.lock); + if (!g_audio.ready_segment || g_audio.ready_count == 0) { + pthread_mutex_unlock(&g_audio.lock); + *samples_out = NULL; + *count_out = 0; + return 1; + } + + *samples_out = g_audio.ready_segment; + *count_out = g_audio.ready_count; + g_audio.ready_segment = NULL; + g_audio.ready_count = 0; + pthread_mutex_unlock(&g_audio.lock); + + return 0; +} + +int audio_get_debug_info(audio_debug_info_t *out) { + if (!out) return -1; + + pthread_mutex_lock(&g_audio.lock); + out->always_on_enabled = g_audio.always_on_enabled ? 1 : 0; + out->vad_state = (int)g_audio.vad_state; + out->onset_samples = g_audio.onset_count; + out->trailing_samples = g_audio.trailing_count; + out->speech_samples = g_audio.speech_count; + out->ready_samples = g_audio.ready_count; + out->peak_level = g_audio.peak_level; + out->threshold = g_audio.vad.threshold; + pthread_mutex_unlock(&g_audio.lock); + + return 0; +} + +void audio_cleanup(void) { + pthread_mutex_lock(&g_audio.lock); + g_audio.stop_requested = true; + pthread_mutex_unlock(&g_audio.lock); + + if (g_audio.monitor_running) { + pthread_join(g_audio.monitor_thread, NULL); + } + + if (g_audio.pa) { + pa_simple_free(g_audio.pa); + g_audio.pa = NULL; + } + + free(g_audio.manual_buffer); + g_audio.manual_buffer = NULL; + g_audio.manual_cap = 0; + g_audio.manual_count = 0; + + free(g_audio.ring_buffer); + g_audio.ring_buffer = NULL; + g_audio.ring_cap = 0; + g_audio.ring_count = 0; + g_audio.ring_head = 0; + + free(g_audio.segment_buffer); + g_audio.segment_buffer = NULL; + g_audio.segment_cap = 0; + g_audio.segment_count = 0; + + free(g_audio.ready_segment); + g_audio.ready_segment = NULL; + g_audio.ready_count = 0; + + pthread_mutex_destroy(&g_audio.lock); +} diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..1a909a3 --- /dev/null +++ b/src/audio.h @@ -0,0 +1,44 @@ +#ifndef VOICE_AUDIO_H +#define VOICE_AUDIO_H + +#include + +typedef struct { + char device[256]; + int sample_rate; + int max_seconds; +} audio_params_t; + +typedef struct { + float threshold; + int trigger_ms; + int release_ms; + int preroll_ms; + int min_speech_ms; + int max_speech_ms; +} audio_vad_params_t; + +typedef struct { + int always_on_enabled; + int vad_state; /* 0=IDLE,1=ONSET,2=SPEECH,3=TRAILING */ + int onset_samples; + int trailing_samples; + int speech_samples; + size_t ready_samples; + float peak_level; + float threshold; +} audio_debug_info_t; + +int audio_init(const audio_params_t *params); +int audio_start_recording(void); +int audio_stop_recording(float **samples_out, size_t *count_out); + +int audio_set_vad_params(const audio_vad_params_t *params); +int audio_set_always_on_enabled(int enabled); +float audio_get_peak_level(void); +int audio_get_speech_segment(float **samples_out, size_t *count_out); +int audio_get_debug_info(audio_debug_info_t *out); + +void audio_cleanup(void); + +#endif diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..12aa0f5 --- /dev/null +++ b/src/config.c @@ -0,0 +1,99 @@ +#include "config.h" + +#include +#include +#include +#include + +static void trim(char *s) { + size_t len = strlen(s); + while (len > 0 && isspace((unsigned char)s[len - 1])) { + s[--len] = '\0'; + } + char *p = s; + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (p != s) { + memmove(s, p, strlen(p) + 1); + } +} + +void config_set_defaults(voice_config_t *cfg) { + if (!cfg) return; + snprintf(cfg->model_path, sizeof(cfg->model_path), "./models/ggml-base.en.bin"); + 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->sample_rate = 16000; + cfg->type_delay_us = 8000; + cfg->max_record_seconds = 30; + cfg->always_on = 1; + cfg->always_on_window_ms = 1400; + cfg->vad_peak_threshold = 0.006f; + cfg->vad_trigger_ms = 100; + cfg->vad_release_ms = 300; + cfg->vad_preroll_ms = 500; + cfg->vad_min_speech_ms = 250; + cfg->vad_max_speech_ms = 15000; + cfg->autotype_enabled = 0; +} + +int config_load_file(const char *path, voice_config_t *cfg) { + if (!path || !cfg) return -1; + + FILE *f = fopen(path, "r"); + if (!f) return -2; + + char line[1024]; + while (fgets(line, sizeof(line), f)) { + trim(line); + if (line[0] == '\0' || line[0] == '#') continue; + + char *eq = strchr(line, '='); + if (!eq) continue; + *eq = '\0'; + + char *key = line; + char *value = eq + 1; + trim(key); + trim(value); + + if (strcmp(key, "model_path") == 0) { + snprintf(cfg->model_path, sizeof(cfg->model_path), "%s", value); + } else if (strcmp(key, "audio_device") == 0) { + snprintf(cfg->audio_device, sizeof(cfg->audio_device), "%s", value); + } else if (strcmp(key, "language") == 0) { + 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, "sample_rate") == 0) { + cfg->sample_rate = atoi(value); + } else if (strcmp(key, "type_delay_us") == 0) { + cfg->type_delay_us = atoi(value); + } else if (strcmp(key, "max_record_seconds") == 0) { + cfg->max_record_seconds = atoi(value); + } else if (strcmp(key, "always_on") == 0) { + cfg->always_on = atoi(value); + } else if (strcmp(key, "always_on_window_ms") == 0) { + cfg->always_on_window_ms = atoi(value); + } else if (strcmp(key, "vad_peak_threshold") == 0) { + cfg->vad_peak_threshold = (float)atof(value); + } else if (strcmp(key, "vad_trigger_ms") == 0) { + cfg->vad_trigger_ms = atoi(value); + } else if (strcmp(key, "vad_release_ms") == 0) { + cfg->vad_release_ms = atoi(value); + } else if (strcmp(key, "vad_preroll_ms") == 0) { + cfg->vad_preroll_ms = atoi(value); + } else if (strcmp(key, "vad_min_speech_ms") == 0) { + cfg->vad_min_speech_ms = atoi(value); + } else if (strcmp(key, "vad_max_speech_ms") == 0) { + cfg->vad_max_speech_ms = atoi(value); + } else if (strcmp(key, "autotype_enabled") == 0) { + cfg->autotype_enabled = atoi(value); + } + } + + fclose(f); + return 0; +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..1a80a4c --- /dev/null +++ b/src/config.h @@ -0,0 +1,28 @@ +#ifndef VOICE_CONFIG_H +#define VOICE_CONFIG_H + +#include + +typedef struct { + char model_path[512]; + char audio_device[256]; + char language[32]; + char hotkey[64]; + int sample_rate; + int type_delay_us; + int max_record_seconds; + int always_on; + int always_on_window_ms; + float vad_peak_threshold; + int vad_trigger_ms; + int vad_release_ms; + int vad_preroll_ms; + int vad_min_speech_ms; + int vad_max_speech_ms; + int autotype_enabled; +} voice_config_t; + +void config_set_defaults(voice_config_t *cfg); +int config_load_file(const char *path, voice_config_t *cfg); + +#endif diff --git a/src/gui_main.c b/src/gui_main.c new file mode 100644 index 0000000..3fe0f99 --- /dev/null +++ b/src/gui_main.c @@ -0,0 +1,636 @@ +#include "audio.h" +#include "config.h" +#include "transcribe.h" +#include "typer.h" + +#include + +#include +#include +#include +#include + +typedef struct { + GtkApplication *app; + GtkWidget *window; + GtkWidget *status_label; + GtkWidget *record_btn; + GtkWidget *always_on_btn; + GtkWidget *type_check; + GtkWidget *transcript_view; + GtkWidget *record_light; + GtkWidget *transcribe_light; + + GtkWidget *level_area; + GtkWidget *threshold_scale; + GtkWidget *trigger_scale; + GtkWidget *release_scale; + GtkWidget *preroll_scale; + GtkWidget *min_speech_scale; + GtkWidget *max_speech_scale; + GtkWidget *debug_label; + + voice_config_t cfg; + int use_gpu; + int recording; + int transcribing; + int always_on_running; + + unsigned int meter_timer_id; + float live_peak; +} gui_state_t; + +static float clampf(float v, float lo, float hi) { + if (v < lo) return lo; + if (v > hi) return hi; + return v; +} + +static void set_status(gui_state_t *s, const char *text) { + gtk_label_set_text(GTK_LABEL(s->status_label), text); +} + +static void append_transcript(gui_state_t *s, const char *text) { + if (!text || text[0] == '\0') return; + + GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(s->transcript_view)); + GtkTextIter end; + gtk_text_buffer_get_end_iter(buf, &end); + + int has_text = gtk_text_buffer_get_char_count(buf) > 0; + if (has_text) { + gtk_text_buffer_insert(buf, &end, "\n", -1); + } + gtk_text_buffer_insert(buf, &end, text, -1); +} + +static void update_activity_lights(gui_state_t *s) { + if (!s->record_light || !s->transcribe_light) return; + + const int rec_on = (s->recording || s->always_on_running) ? 1 : 0; + const char *rec_markup = rec_on + ? "● Recording" + : "● Recording"; + + const char *tr_markup = s->transcribing + ? "● Transcribing" + : "● Transcribing"; + + gtk_label_set_markup(GTK_LABEL(s->record_light), rec_markup); + gtk_label_set_markup(GTK_LABEL(s->transcribe_light), tr_markup); +} + +static void update_record_button(gui_state_t *s) { + GtkStyleContext *ctx = gtk_widget_get_style_context(s->record_btn); + gtk_style_context_remove_class(ctx, "idle"); + gtk_style_context_remove_class(ctx, "recording"); + gtk_style_context_remove_class(ctx, "busy"); + + if (s->transcribing) { + gtk_widget_set_sensitive(s->record_btn, FALSE); + gtk_button_set_label(GTK_BUTTON(s->record_btn), "Transcribing..."); + gtk_style_context_add_class(ctx, "busy"); + update_activity_lights(s); + return; + } + + if (s->always_on_running) { + gtk_widget_set_sensitive(s->record_btn, FALSE); + gtk_button_set_label(GTK_BUTTON(s->record_btn), "Manual Disabled (Always-On)"); + gtk_style_context_add_class(ctx, "busy"); + update_activity_lights(s); + return; + } + + gtk_widget_set_sensitive(s->record_btn, TRUE); + if (s->recording) { + gtk_button_set_label(GTK_BUTTON(s->record_btn), "Stop Recording"); + gtk_style_context_add_class(ctx, "recording"); + } else { + gtk_button_set_label(GTK_BUTTON(s->record_btn), "Start Recording"); + gtk_style_context_add_class(ctx, "idle"); + } + + update_activity_lights(s); +} + +static void update_always_on_button(gui_state_t *s) { + if (!s->always_on_btn) return; + gtk_button_set_label(GTK_BUTTON(s->always_on_btn), s->always_on_running ? "Stop Always-On" : "Start Always-On"); +} + +static void log_audio_stats(const float *samples, size_t count, int sample_rate, const char *device_name, char *out, size_t out_sz) { + if (!samples || count == 0 || sample_rate <= 0) { + snprintf(out, out_sz, "Empty capture"); + return; + } + + float peak_abs = 0.0f; + double sum_abs = 0.0; + + for (size_t i = 0; i < count; ++i) { + float v = samples[i]; + float a = v < 0.0f ? -v : v; + if (a > peak_abs) peak_abs = a; + sum_abs += (double)a; + } + + double mean_abs = sum_abs / (double)count; + double seconds = (double)count / (double)sample_rate; + + if (peak_abs < 0.005f) { + snprintf(out, out_sz, + "Near-silent audio (%.2fs, peak %.6f) from '%.120s'", + seconds, peak_abs, device_name ? device_name : "(default)"); + } else { + snprintf(out, out_sz, + "Audio OK (%.2fs, peak %.6f, mean %.6f)", + seconds, peak_abs, mean_abs); + } +} + +static int peak_is_speech(const float *samples, size_t count, float threshold) { + float peak_abs = 0.0f; + for (size_t i = 0; i < count; ++i) { + float v = samples[i]; + float a = v < 0.0f ? -v : v; + if (a > peak_abs) peak_abs = a; + } + return peak_abs >= threshold; +} + +static void process_segment(gui_state_t *s, float *samples, size_t count) { + if (!samples || count == 0) { + free(samples); + return; + } + + char audio_msg[256]; + log_audio_stats(samples, count, s->cfg.sample_rate, s->cfg.audio_device, audio_msg, sizeof(audio_msg)); + + if (!peak_is_speech(samples, count, s->cfg.vad_peak_threshold)) { + set_status(s, "Mic muted or near-silent"); + free(samples); + return; + } + + s->transcribing = 1; + update_record_button(s); + set_status(s, "Transcribing... (please wait)"); + while (gtk_events_pending()) gtk_main_iteration(); + + char *text = transcribe_buffer(samples, count); + free(samples); + + s->transcribing = 0; + update_record_button(s); + + if (!text) { + set_status(s, "Transcription failed"); + return; + } + + append_transcript(s, text); + + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(s->type_check))) { + typer_type_text(text); + } + + set_status(s, audio_msg); + free(text); +} + +static int read_scale_int(GtkWidget *scale) { + return (int)gtk_range_get_value(GTK_RANGE(scale)); +} + +static float read_scale_float(GtkWidget *scale) { + return (float)gtk_range_get_value(GTK_RANGE(scale)); +} + +static void apply_vad_params(gui_state_t *s) { + audio_vad_params_t v; + v.threshold = read_scale_float(s->threshold_scale); + v.trigger_ms = read_scale_int(s->trigger_scale); + v.release_ms = read_scale_int(s->release_scale); + v.preroll_ms = read_scale_int(s->preroll_scale); + v.min_speech_ms = read_scale_int(s->min_speech_scale); + v.max_speech_ms = read_scale_int(s->max_speech_scale); + + if (v.max_speech_ms < v.min_speech_ms) { + v.max_speech_ms = v.min_speech_ms; + gtk_range_set_value(GTK_RANGE(s->max_speech_scale), (double)v.max_speech_ms); + } + + s->cfg.vad_peak_threshold = v.threshold; + s->cfg.vad_trigger_ms = v.trigger_ms; + s->cfg.vad_release_ms = v.release_ms; + s->cfg.vad_preroll_ms = v.preroll_ms; + s->cfg.vad_min_speech_ms = v.min_speech_ms; + s->cfg.vad_max_speech_ms = v.max_speech_ms; + + audio_set_vad_params(&v); + gtk_widget_queue_draw(s->level_area); +} + +static void on_vad_value_changed(GtkRange *range, gpointer user_data) { + (void)range; + gui_state_t *s = (gui_state_t *)user_data; + apply_vad_params(s); +} + +static gboolean on_level_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data) { + gui_state_t *s = (gui_state_t *)user_data; + + GtkAllocation a; + gtk_widget_get_allocation(widget, &a); + + const double w = (double)a.width; + const double h = (double)a.height; + + cairo_set_source_rgb(cr, 0.10, 0.10, 0.10); + cairo_rectangle(cr, 0.0, 0.0, w, h); + cairo_fill(cr); + + double peak = (double)clampf(s->live_peak, 0.0f, 1.0f); + double threshold = (double)clampf(s->cfg.vad_peak_threshold, 0.0f, 1.0f); + + double fill_w = peak * w; + if (fill_w > 0.0) { + if (peak >= threshold) { + cairo_set_source_rgb(cr, 0.18, 0.72, 0.22); + } else { + cairo_set_source_rgb(cr, 0.20, 0.52, 0.85); + } + cairo_rectangle(cr, 0.0, 0.0, fill_w, h); + cairo_fill(cr); + } + + double tx = threshold * w; + cairo_set_source_rgb(cr, 0.95, 0.25, 0.25); + cairo_set_line_width(cr, 2.0); + cairo_move_to(cr, tx, 0.0); + cairo_line_to(cr, tx, h); + cairo_stroke(cr); + + cairo_set_source_rgb(cr, 0.75, 0.75, 0.75); + cairo_set_line_width(cr, 1.0); + cairo_rectangle(cr, 0.5, 0.5, w - 1.0, h - 1.0); + cairo_stroke(cr); + + return FALSE; +} + +static const char *vad_state_text(int s) { + switch (s) { + case 0: return "IDLE"; + case 1: return "ONSET"; + case 2: return "SPEECH"; + case 3: return "TRAILING"; + default: return "?"; + } +} + +static gboolean meter_tick_cb(gpointer user_data) { + gui_state_t *s = (gui_state_t *)user_data; + + audio_debug_info_t di; + if (audio_get_debug_info(&di) == 0) { + s->live_peak = di.peak_level; + + char dbg[256]; + snprintf(dbg, sizeof(dbg), + "Debug: always_on=%d state=%s peak=%.4f thr=%.4f onset=%d trail=%d speech=%d ready=%zu", + di.always_on_enabled, + vad_state_text(di.vad_state), + di.peak_level, + di.threshold, + di.onset_samples, + di.trailing_samples, + di.speech_samples, + di.ready_samples); + gtk_label_set_text(GTK_LABEL(s->debug_label), dbg); + } else { + s->live_peak = audio_get_peak_level(); + } + + gtk_widget_queue_draw(s->level_area); + + if (s->always_on_running && !s->transcribing) { + float *samples = NULL; + size_t count = 0; + int rc = audio_get_speech_segment(&samples, &count); + if (rc == 0) { + set_status(s, "Speech segment detected, transcribing..."); + process_segment(s, samples, count); + } + } + + return G_SOURCE_CONTINUE; +} + +static void on_record_clicked(GtkButton *btn, gpointer user_data) { + (void)btn; + gui_state_t *s = (gui_state_t *)user_data; + + if (s->always_on_running || s->transcribing) { + return; + } + + if (!s->recording) { + if (audio_start_recording() != 0) { + set_status(s, "Failed to start recording"); + return; + } + + s->recording = 1; + update_record_button(s); + set_status(s, "Recording..."); + return; + } + + float *samples = NULL; + size_t count = 0; + if (audio_stop_recording(&samples, &count) != 0) { + set_status(s, "Failed to stop recording"); + return; + } + + s->recording = 0; + update_record_button(s); + + process_segment(s, samples, count); +} + +static void on_always_on_clicked(GtkButton *btn, gpointer user_data) { + (void)btn; + gui_state_t *s = (gui_state_t *)user_data; + + if (s->transcribing || s->recording) return; + + if (s->always_on_running) { + audio_set_always_on_enabled(0); + s->always_on_running = 0; + update_always_on_button(s); + update_record_button(s); + set_status(s, "Always-on stopped"); + return; + } + + apply_vad_params(s); + audio_set_always_on_enabled(1); + s->always_on_running = 1; + update_always_on_button(s); + update_record_button(s); + set_status(s, "Always-on active (continuous monitor)"); +} + +static GtkWidget *add_scale_row(GtkWidget *grid, + int row, + const char *label_text, + double min, + double max, + double step, + double value, + int digits, + gui_state_t *s, + GtkWidget **out_scale) { + GtkWidget *label = gtk_label_new(label_text); + gtk_label_set_xalign(GTK_LABEL(label), 0.0f); + gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1); + + GtkWidget *scale = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, min, max, step); + gtk_scale_set_digits(GTK_SCALE(scale), digits); + gtk_scale_set_draw_value(GTK_SCALE(scale), TRUE); + gtk_range_set_value(GTK_RANGE(scale), value); + gtk_widget_set_hexpand(scale, TRUE); + gtk_grid_attach(GTK_GRID(grid), scale, 1, row, 1, 1); + + g_signal_connect(scale, "value-changed", G_CALLBACK(on_vad_value_changed), s); + *out_scale = scale; + + return scale; +} + +static void on_app_activate(GtkApplication *app, gpointer user_data) { + gui_state_t *s = (gui_state_t *)user_data; + + 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); + + GtkWidget *outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_container_set_border_width(GTK_CONTAINER(outer), 12); + gtk_container_add(GTK_CONTAINER(s->window), outer); + + char info[1024]; + snprintf(info, sizeof(info), "Device: %s\nModel: %s\nLanguage: %s\nGPU: %s", + s->cfg.audio_device, s->cfg.model_path, s->cfg.language, s->use_gpu ? "on" : "off"); + GtkWidget *info_label = gtk_label_new(info); + gtk_label_set_xalign(GTK_LABEL(info_label), 0.0f); + gtk_box_pack_start(GTK_BOX(outer), info_label, FALSE, FALSE, 0); + + s->status_label = gtk_label_new("Idle"); + gtk_label_set_xalign(GTK_LABEL(s->status_label), 0.0f); + gtk_box_pack_start(GTK_BOX(outer), s->status_label, FALSE, FALSE, 0); + + GtkWidget *lights_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14); + s->record_light = gtk_label_new(NULL); + s->transcribe_light = gtk_label_new(NULL); + gtk_label_set_use_markup(GTK_LABEL(s->record_light), TRUE); + gtk_label_set_use_markup(GTK_LABEL(s->transcribe_light), TRUE); + gtk_box_pack_start(GTK_BOX(lights_row), s->record_light, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(lights_row), s->transcribe_light, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(outer), lights_row, FALSE, FALSE, 0); + + s->record_btn = gtk_button_new_with_label("Start Recording"); + gtk_widget_set_name(s->record_btn, "record-btn"); + + GtkCssProvider *provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data( + provider, + "#record-btn { font-weight: 700; color: #ffffff; }" + "#record-btn.idle { background-image: none; background-color: #c62828; }" + "#record-btn.recording { background-image: none; background-color: #2e7d32; }" + "#record-btn.busy { background-image: none; background-color: #ef6c00; }" + "#record-btn:backdrop { opacity: 0.92; }", + -1, + NULL); + gtk_style_context_add_provider_for_screen( + gdk_screen_get_default(), + GTK_STYLE_PROVIDER(provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref(provider); + + s->recording = 0; + update_record_button(s); + + g_signal_connect(s->record_btn, "clicked", G_CALLBACK(on_record_clicked), s); + gtk_box_pack_start(GTK_BOX(outer), s->record_btn, FALSE, FALSE, 0); + update_activity_lights(s); + + s->always_on_btn = gtk_button_new_with_label("Start Always-On"); + g_signal_connect(s->always_on_btn, "clicked", G_CALLBACK(on_always_on_clicked), s); + gtk_box_pack_start(GTK_BOX(outer), s->always_on_btn, FALSE, FALSE, 0); + + s->type_check = gtk_check_button_new_with_label("Type transcript into focused window"); + 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); + + GtkWidget *meter_frame = gtk_frame_new("Live Microphone Level"); + gtk_box_pack_start(GTK_BOX(outer), meter_frame, FALSE, FALSE, 0); + + GtkWidget *meter_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_container_set_border_width(GTK_CONTAINER(meter_box), 8); + gtk_container_add(GTK_CONTAINER(meter_frame), meter_box); + + s->level_area = gtk_drawing_area_new(); + gtk_widget_set_size_request(s->level_area, -1, 30); + g_signal_connect(s->level_area, "draw", G_CALLBACK(on_level_draw), s); + gtk_box_pack_start(GTK_BOX(meter_box), s->level_area, FALSE, FALSE, 0); + + GtkWidget *grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 6); + gtk_grid_set_column_spacing(GTK_GRID(grid), 10); + gtk_box_pack_start(GTK_BOX(meter_box), grid, FALSE, FALSE, 0); + + add_scale_row(grid, 0, "Threshold (0.0-1.0)", 0.0, 1.0, 0.001, + s->cfg.vad_peak_threshold, 3, s, &s->threshold_scale); + add_scale_row(grid, 1, "Trigger ms (above threshold)", 10.0, 2000.0, 10.0, + (double)s->cfg.vad_trigger_ms, 0, s, &s->trigger_scale); + add_scale_row(grid, 2, "Release ms (below threshold)", 10.0, 4000.0, 10.0, + (double)s->cfg.vad_release_ms, 0, s, &s->release_scale); + add_scale_row(grid, 3, "Preroll ms (audio before trigger)", 0.0, 3000.0, 10.0, + (double)s->cfg.vad_preroll_ms, 0, s, &s->preroll_scale); + add_scale_row(grid, 4, "Min speech ms", 50.0, 5000.0, 10.0, + (double)s->cfg.vad_min_speech_ms, 0, s, &s->min_speech_scale); + add_scale_row(grid, 5, "Max speech ms", 200.0, 60000.0, 100.0, + (double)s->cfg.vad_max_speech_ms, 0, s, &s->max_speech_scale); + + GtkWidget *hint = gtk_label_new( + "Green bar means level >= threshold. Red line is threshold.\n" + "Always-On uses continuous capture + preroll so word starts are not clipped."); + gtk_label_set_xalign(GTK_LABEL(hint), 0.0f); + gtk_box_pack_start(GTK_BOX(meter_box), hint, FALSE, FALSE, 0); + + s->debug_label = gtk_label_new("Debug: waiting for audio..."); + gtk_label_set_xalign(GTK_LABEL(s->debug_label), 0.0f); + gtk_box_pack_start(GTK_BOX(meter_box), s->debug_label, FALSE, FALSE, 0); + + GtkWidget *frame = gtk_frame_new("Transcript Log"); + gtk_box_pack_start(GTK_BOX(outer), frame, TRUE, TRUE, 0); + + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(frame), scroll); + + s->transcript_view = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(s->transcript_view), GTK_WRAP_WORD_CHAR); + gtk_container_add(GTK_CONTAINER(scroll), s->transcript_view); + + apply_vad_params(s); + update_always_on_button(s); + gtk_widget_show_all(s->window); + + s->meter_timer_id = g_timeout_add(60, meter_tick_cb, s); + + if (s->cfg.always_on) { + audio_set_always_on_enabled(1); + s->always_on_running = 1; + update_always_on_button(s); + update_record_button(s); + set_status(s, "Always-on active (continuous monitor)"); + } +} + +static const char *arg_value(int argc, char **argv, const char *name) { + for (int i = 1; i + 1 < argc; ++i) { + if (strcmp(argv[i], name) == 0) { + return argv[i + 1]; + } + } + return NULL; +} + +static int arg_flag(int argc, char **argv, const char *name) { + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], name) == 0) { + return 1; + } + } + return 0; +} + +int main(int argc, char **argv) { + gui_state_t st; + memset(&st, 0, sizeof(st)); + + config_set_defaults(&st.cfg); + + const char *cfg_path = arg_value(argc, argv, "--config"); + if (!cfg_path) cfg_path = "./config.ini"; + if (config_load_file(cfg_path, &st.cfg) != 0) { + fprintf(stderr, "warning: failed to load %s, using defaults\n", cfg_path); + } + + st.use_gpu = arg_flag(argc, argv, "--gpu") ? 1 : 0; + if (arg_flag(argc, argv, "--cpu")) st.use_gpu = 0; + + audio_params_t ap = {0}; + snprintf(ap.device, sizeof(ap.device), "%s", st.cfg.audio_device); + 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; + } + + audio_vad_params_t vp = { + .threshold = st.cfg.vad_peak_threshold, + .trigger_ms = st.cfg.vad_trigger_ms, + .release_ms = st.cfg.vad_release_ms, + .preroll_ms = st.cfg.vad_preroll_ms, + .min_speech_ms = st.cfg.vad_min_speech_ms, + .max_speech_ms = st.cfg.vad_max_speech_ms, + }; + 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); + + int gtk_argc = 1; + char *gtk_argv[] = { argv[0], NULL }; + int rc = g_application_run(G_APPLICATION(st.app), gtk_argc, gtk_argv); + + if (st.meter_timer_id != 0) { + g_source_remove(st.meter_timer_id); + st.meter_timer_id = 0; + } + + audio_set_always_on_enabled(0); + + g_object_unref(st.app); + typer_cleanup(); + transcribe_cleanup(); + audio_cleanup(); + + return rc; +} diff --git a/src/hotkey.c b/src/hotkey.c new file mode 100644 index 0000000..c1d930d --- /dev/null +++ b/src/hotkey.c @@ -0,0 +1,107 @@ +#include "hotkey.h" + +#include +#include +#include +#include +#include + +static unsigned int parse_mod_token(const char *tok) { + if (strcasecmp(tok, "Ctrl") == 0 || strcasecmp(tok, "Control") == 0) return ControlMask; + if (strcasecmp(tok, "Alt") == 0) return Mod1Mask; + if (strcasecmp(tok, "Shift") == 0) return ShiftMask; + if (strcasecmp(tok, "Super") == 0 || strcasecmp(tok, "Win") == 0) return Mod4Mask; + return 0; +} + +static int parse_combo(Display *dpy, const char *combo, KeyCode *kc, unsigned int *mods) { + if (!combo || !kc || !mods) return -1; + + char buf[128]; + snprintf(buf, sizeof(buf), "%s", combo); + + char *save = NULL; + char *tok = strtok_r(buf, "+", &save); + char *parts[8]; + size_t n = 0; + while (tok && n < 8) { + parts[n++] = tok; + tok = strtok_r(NULL, "+", &save); + } + if (n == 0) return -2; + + *mods = 0; + for (size_t i = 0; i + 1 < n; ++i) { + *mods |= parse_mod_token(parts[i]); + } + + KeySym ks = XStringToKeysym(parts[n - 1]); + if (ks == NoSymbol && strlen(parts[n - 1]) == 1) { + char tmp[2] = {parts[n - 1][0], '\0'}; + ks = XStringToKeysym(tmp); + } + if (ks == NoSymbol) return -3; + + *kc = XKeysymToKeycode(dpy, ks); + return *kc ? 0 : -4; +} + +int hotkey_init(hotkey_ctx_t *ctx, const char *combo) { + if (!ctx) return -1; + memset(ctx, 0, sizeof(*ctx)); + + ctx->display = XOpenDisplay(NULL); + if (!ctx->display) return -2; + + ctx->root = DefaultRootWindow(ctx->display); + + if (parse_combo(ctx->display, combo, &ctx->keycode, &ctx->modifiers) != 0) { + return -3; + } + + const unsigned int extra_masks[] = {0, LockMask, Mod2Mask, LockMask | Mod2Mask}; + for (size_t i = 0; i < sizeof(extra_masks) / sizeof(extra_masks[0]); ++i) { + XGrabKey(ctx->display, (int)ctx->keycode, ctx->modifiers | extra_masks[i], ctx->root, + True, GrabModeAsync, GrabModeAsync); + } + + XSelectInput(ctx->display, ctx->root, KeyPressMask | KeyReleaseMask); + XSync(ctx->display, False); + return 0; +} + +int hotkey_wait_press(hotkey_ctx_t *ctx) { + if (!ctx || !ctx->display) return -1; + + for (;;) { + XEvent ev; + XNextEvent(ctx->display, &ev); + if (ev.type == KeyPress && ev.xkey.keycode == ctx->keycode) { + return 0; + } + } +} + +int hotkey_wait_release(hotkey_ctx_t *ctx) { + if (!ctx || !ctx->display) return -1; + + for (;;) { + XEvent ev; + XNextEvent(ctx->display, &ev); + if (ev.type == KeyRelease && ev.xkey.keycode == ctx->keycode) { + return 0; + } + } +} + +void hotkey_cleanup(hotkey_ctx_t *ctx) { + if (!ctx || !ctx->display) return; + + const unsigned int extra_masks[] = {0, LockMask, Mod2Mask, LockMask | Mod2Mask}; + for (size_t i = 0; i < sizeof(extra_masks) / sizeof(extra_masks[0]); ++i) { + XUngrabKey(ctx->display, (int)ctx->keycode, ctx->modifiers | extra_masks[i], ctx->root); + } + + XCloseDisplay(ctx->display); + ctx->display = NULL; +} diff --git a/src/hotkey.h b/src/hotkey.h new file mode 100644 index 0000000..a3520ae --- /dev/null +++ b/src/hotkey.h @@ -0,0 +1,18 @@ +#ifndef VOICE_HOTKEY_H +#define VOICE_HOTKEY_H + +#include + +typedef struct { + Display *display; + Window root; + KeyCode keycode; + unsigned int modifiers; +} hotkey_ctx_t; + +int hotkey_init(hotkey_ctx_t *ctx, const char *combo); +int hotkey_wait_press(hotkey_ctx_t *ctx); +int hotkey_wait_release(hotkey_ctx_t *ctx); +void hotkey_cleanup(hotkey_ctx_t *ctx); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..e479eec --- /dev/null +++ b/src/main.c @@ -0,0 +1,185 @@ +#include "audio.h" +#include "config.h" +#include "hotkey.h" +#include "transcribe.h" +#include "typer.h" + +#include +#include +#include +#include +#include + +static const char *arg_value(int argc, char **argv, const char *name) { + for (int i = 1; i + 1 < argc; ++i) { + if (strcmp(argv[i], name) == 0) { + return argv[i + 1]; + } + } + return NULL; +} + +static int arg_flag(int argc, char **argv, const char *name) { + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], name) == 0) { + return 1; + } + } + return 0; +} + +static void log_audio_stats(const float *samples, size_t count, int sample_rate, const char *device_name) { + if (!samples || count == 0 || sample_rate <= 0) { + fprintf(stderr, "[audio] empty capture\n"); + return; + } + + float peak_abs = 0.0f; + double sum_abs = 0.0; + + for (size_t i = 0; i < count; ++i) { + float v = samples[i]; + float a = v < 0.0f ? -v : v; + if (a > peak_abs) peak_abs = a; + sum_abs += (double)a; + } + + double mean_abs = sum_abs / (double)count; + double seconds = (double)count / (double)sample_rate; + + fprintf(stderr, "[audio] duration=%.2fs samples=%zu peak=%.6f mean_abs=%.6f\n", + seconds, count, peak_abs, mean_abs); + + if (peak_abs < 0.005f) { + fprintf(stderr, "[audio] warning: signal is extremely low/near-silent. Check mic routing and selected source '%s'.\n", + device_name ? device_name : "(default)"); + } +} + +static double monotonic_seconds(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; +} + +int main(int argc, char **argv) { + voice_config_t cfg; + config_set_defaults(&cfg); + + const char *cfg_path = arg_value(argc, argv, "--config"); + if (!cfg_path) cfg_path = "./config.ini"; + + if (config_load_file(cfg_path, &cfg) != 0) { + fprintf(stderr, "warning: failed to load %s, using defaults\n", cfg_path); + } + + int use_gpu = arg_flag(argc, argv, "--gpu") ? 1 : 0; + if (arg_flag(argc, argv, "--cpu")) use_gpu = 0; + + audio_params_t ap = {0}; + snprintf(ap.device, sizeof(ap.device), "%s", cfg.audio_device); + ap.sample_rate = cfg.sample_rate; + ap.max_seconds = cfg.max_record_seconds; + + transcribe_params_t tp = {0}; + snprintf(tp.model_path, sizeof(tp.model_path), "%s", cfg.model_path); + snprintf(tp.language, sizeof(tp.language), "%s", cfg.language); + tp.use_gpu = use_gpu; + tp.n_threads = (int)sysconf(_SC_NPROCESSORS_ONLN); + + hotkey_ctx_t hk; + + if (audio_init(&ap) != 0) { + fprintf(stderr, "fatal: audio init failed\n"); + return 1; + } + if (transcribe_init(&tp) != 0) { + fprintf(stderr, "fatal: transcribe init failed\n"); + audio_cleanup(); + return 1; + } + if (typer_init(cfg.type_delay_us) != 0) { + fprintf(stderr, "fatal: typer init failed\n"); + transcribe_cleanup(); + audio_cleanup(); + return 1; + } + if (hotkey_init(&hk, cfg.hotkey) != 0) { + fprintf(stderr, "fatal: hotkey init failed for '%s'\n", cfg.hotkey); + typer_cleanup(); + transcribe_cleanup(); + audio_cleanup(); + return 1; + } + + fprintf(stderr, "voice_linux ready\n"); + fprintf(stderr, "hotkey toggle mode: press %s to START recording, press %s again to STOP and transcribe\n", cfg.hotkey, cfg.hotkey); + fprintf(stderr, "device: %s | model: %s | language: %s | gpu: %s\n", + cfg.audio_device, cfg.model_path, cfg.language, use_gpu ? "on" : "off"); + + for (;;) { + if (hotkey_wait_press(&hk) != 0) break; + if (hotkey_wait_release(&hk) != 0) break; + + fprintf(stderr, "[rec] start\n"); + if (audio_start_recording() != 0) { + fprintf(stderr, "audio_start_recording failed\n"); + continue; + } + + const double rec_start = monotonic_seconds(); + for (;;) { + if (hotkey_wait_press(&hk) != 0) { + fprintf(stderr, "hotkey second press wait failed\n"); + goto done; + } + if (hotkey_wait_release(&hk) != 0) { + fprintf(stderr, "hotkey second release wait failed\n"); + goto done; + } + + const double elapsed = monotonic_seconds() - rec_start; + if (elapsed < 0.35) { + fprintf(stderr, "[hotkey] ignoring early stop event (%.2f s)\n", elapsed); + continue; + } + break; + } + + float *samples = NULL; + size_t count = 0; + if (audio_stop_recording(&samples, &count) != 0) { + fprintf(stderr, "audio_stop_recording failed\n"); + continue; + } + + fprintf(stderr, "[rec] stop, samples=%zu\n", count); + if (!samples || count == 0) { + free(samples); + continue; + } + + log_audio_stats(samples, count, cfg.sample_rate, cfg.audio_device); + + fprintf(stderr, "[stt] transcribing...\n"); + char *text = transcribe_buffer(samples, count); + free(samples); + + if (!text) { + fprintf(stderr, "[stt] no transcription\n"); + continue; + } + + fprintf(stderr, "[stt] %s\n", text); + typer_type_text(text); + free(text); + } + +done: + hotkey_cleanup(&hk); + typer_cleanup(); + transcribe_cleanup(); + audio_cleanup(); + + return 0; +} diff --git a/src/transcribe.c b/src/transcribe.c new file mode 100644 index 0000000..c7c9b8d --- /dev/null +++ b/src/transcribe.c @@ -0,0 +1,102 @@ +#include "transcribe.h" + +#include +#include +#include + +#ifndef WITH_WHISPER +#define WITH_WHISPER 0 +#endif + +#if WITH_WHISPER +#include + +static struct whisper_context *g_ctx = NULL; +static transcribe_params_t g_params; + +int transcribe_init(const transcribe_params_t *params) { + if (!params) return -1; + g_params = *params; + + struct whisper_context_params cparams = whisper_context_default_params(); + cparams.use_gpu = g_params.use_gpu ? true : false; + g_ctx = whisper_init_from_file_with_params(g_params.model_path, cparams); + if (!g_ctx) { + fprintf(stderr, "transcribe: failed to load model: %s\n", g_params.model_path); + return -2; + } + return 0; +} + +char *transcribe_buffer(const float *samples, size_t count) { + if (!g_ctx || !samples || count == 0) return NULL; + + struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); + params.print_progress = false; + params.print_special = false; + params.print_realtime = false; + params.print_timestamps = false; + params.translate = false; + params.n_threads = g_params.n_threads > 0 ? g_params.n_threads : 4; + params.language = g_params.language[0] ? g_params.language : "en"; + + if (whisper_full(g_ctx, params, samples, (int)count) != 0) { + fprintf(stderr, "transcribe: whisper_full failed\n"); + return NULL; + } + + int nseg = whisper_full_n_segments(g_ctx); + size_t total = 1; + for (int i = 0; i < nseg; ++i) { + const char *seg = whisper_full_get_segment_text(g_ctx, i); + if (seg) total += strlen(seg) + 1; + } + + char *out = (char *)malloc(total); + if (!out) return NULL; + out[0] = '\0'; + + for (int i = 0; i < nseg; ++i) { + const char *seg = whisper_full_get_segment_text(g_ctx, i); + if (!seg) continue; + strcat(out, seg); + if (i + 1 < nseg) strcat(out, " "); + } + + return out; +} + +void transcribe_cleanup(void) { + if (g_ctx) { + whisper_free(g_ctx); + g_ctx = NULL; + } +} + +#else + +static transcribe_params_t g_params; + +int transcribe_init(const transcribe_params_t *params) { + if (!params) return -1; + g_params = *params; + fprintf(stderr, "transcribe: built without whisper support. Rebuild with WITH_WHISPER=1 bash ./build.sh\n"); + return 0; +} + +char *transcribe_buffer(const float *samples, size_t count) { + (void)samples; + (void)count; + + const char *msg = "[transcription unavailable: rebuild with WITH_WHISPER=1 and install whisper.cpp]"; + char *out = (char *)malloc(strlen(msg) + 1); + if (!out) return NULL; + strcpy(out, msg); + return out; +} + +void transcribe_cleanup(void) { + memset(&g_params, 0, sizeof(g_params)); +} + +#endif diff --git a/src/transcribe.h b/src/transcribe.h new file mode 100644 index 0000000..df66c2a --- /dev/null +++ b/src/transcribe.h @@ -0,0 +1,17 @@ +#ifndef VOICE_TRANSCRIBE_H +#define VOICE_TRANSCRIBE_H + +#include + +typedef struct { + char model_path[512]; + char language[32]; + int use_gpu; + int n_threads; +} transcribe_params_t; + +int transcribe_init(const transcribe_params_t *params); +char *transcribe_buffer(const float *samples, size_t count); +void transcribe_cleanup(void); + +#endif diff --git a/src/typer.c b/src/typer.c new file mode 100644 index 0000000..ca25b85 --- /dev/null +++ b/src/typer.c @@ -0,0 +1,95 @@ +#include "typer.h" + +#include +#include +#include + +#include +#include +#include +#include + +static Display *g_dpy = NULL; +static int g_delay_us = 8000; + +static int type_keysym(KeySym ks, int need_shift) { + KeyCode kc = XKeysymToKeycode(g_dpy, ks); + if (!kc) return -1; + + KeyCode shift_kc = XKeysymToKeycode(g_dpy, XK_Shift_L); + + if (need_shift && shift_kc) { + XTestFakeKeyEvent(g_dpy, shift_kc, True, CurrentTime); + } + + XTestFakeKeyEvent(g_dpy, kc, True, CurrentTime); + XTestFakeKeyEvent(g_dpy, kc, False, CurrentTime); + + if (need_shift && shift_kc) { + XTestFakeKeyEvent(g_dpy, shift_kc, False, CurrentTime); + } + + 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); + return 0; +} + +static int type_char(char c) { + if (c >= 'a' && c <= 'z') { + return type_keysym((KeySym)(XK_a + (c - 'a')), 0); + } + if (c >= 'A' && c <= 'Z') { + return type_keysym((KeySym)(XK_a + (c - 'A')), 1); + } + if (c >= '0' && c <= '9') { + return type_keysym((KeySym)(XK_0 + (c - '0')), 0); + } + + switch (c) { + case ' ': return type_keysym(XK_space, 0); + case '\n': return type_keysym(XK_Return, 0); + case '.': return type_keysym(XK_period, 0); + case ',': return type_keysym(XK_comma, 0); + case '!': return type_keysym(XK_1, 1); + case '?': return type_keysym(XK_slash, 1); + case ':': return type_keysym(XK_semicolon, 1); + case ';': return type_keysym(XK_semicolon, 0); + case '\'': return type_keysym(XK_apostrophe, 0); + case '"': return type_keysym(XK_apostrophe, 1); + 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_9, 1); + case ')': return type_keysym(XK_0, 1); + default: return 0; + } +} + +int typer_init(int type_delay_us) { + g_dpy = XOpenDisplay(NULL); + if (!g_dpy) return -1; + g_delay_us = type_delay_us > 0 ? type_delay_us : 8000; + return 0; +} + +int typer_type_text(const char *text) { + if (!g_dpy || !text) return -1; + + for (size_t i = 0; i < strlen(text); ++i) { + if (type_char(text[i]) != 0) { + fprintf(stderr, "typer: failed at char '%c'\n", text[i]); + } + } + + return 0; +} + +void typer_cleanup(void) { + if (g_dpy) { + XCloseDisplay(g_dpy); + g_dpy = NULL; + } +} diff --git a/src/typer.h b/src/typer.h new file mode 100644 index 0000000..a6eaf43 --- /dev/null +++ b/src/typer.h @@ -0,0 +1,8 @@ +#ifndef VOICE_TYPER_H +#define VOICE_TYPER_H + +int typer_init(int type_delay_us); +int typer_type_text(const char *text); +void typer_cleanup(void); + +#endif diff --git a/voice_linux_gui b/voice_linux_gui new file mode 100755 index 0000000000000000000000000000000000000000..80edb42b6436ab5ddbe52a5e824862823804c556 GIT binary patch literal 127760 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