From 903274a2494b285a66ed6d786718a35accb37b80 Mon Sep 17 00:00:00 2001 From: LeonardoTrapani Date: Thu, 7 Aug 2025 23:10:01 +0200 Subject: [PATCH] Rename HotkeyDaemon -> Daemon update readme --- README.md | 178 +++++++++--------- cmd/hyprvoice/main.go | 8 +- internal/{hotkeydaemon => daemon}/daemon.go | 15 +- .../{hotkeydaemon => daemon}/daemon_test.go | 2 +- 4 files changed, 100 insertions(+), 103 deletions(-) rename internal/{hotkeydaemon => daemon}/daemon.go (90%) rename internal/{hotkeydaemon => daemon}/daemon_test.go (98%) diff --git a/README.md b/README.md index b2e92d1..48153ee 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,135 @@ # Hyprvoice -> **Voice‑powered typing for Wayland/Hyprland desktops** — press a global shortcut, speak, and watch words appear in whatever window you're focused on. +> **Voice‑powered typing for Wayland/Hyprland — press to toggle, speak, instant paste.** +> Streams audio while you talk and **pastes the final text the moment you toggle off** → aims to be the **fastest feel** on Wayland. -**🚧 Early Development Status:** This project is currently in early development. Not ready for production use yet. +**Status:** Early development (expect rough edges) --- -## Why does this hyprvoice exist? +## TL;DR -Typing is slow, repetitive strain injuries are real. This project aims to give Wayland users a fast, privacy‑respecting alternative that works entirely on their own hardware _or_ any cloud ASR they trust. +- **Toggle workflow** (Hyprland‑friendly): press to start, press to stop. +- **Cloud streaming ASR** (MVP) → **single final paste** into the focused window. +- **Daemon with clear states & events**; desktop notifications. +- **Clipboard‑based injection** (save/restore) with **`wtype`** +- **Unixy pipeline** (small pieces, bounded channels). --- -## Current Implementation Status +## Requirements -| Component | Status | Description | -| ------------------------- | ------- | ------------------------------------------------------------ | -| **Hot‑key daemon** | ✅ Done | Background service with Unix socket IPC for command handling | -| **Desktop notifications** | ✅ Done | Recording state changes via `notify-send` | -| **Service management** | ✅ Done | Install/remove systemd user service with embedded unit file | -| **Live audio capture** | 🔄 TODO | PipeWire input with VAD/noise gate | -| **ASR backends** | 🔄 TODO | Local Whisper or cloud APIs (OpenAI, Deepgram, AssemblyAI) | -| **Text injection** | 🔄 TODO | `wtype`/`ydotool` keystrokes to focused window | -| **Configuration** | 🔄 TODO | TOML config file with runtime reload | +- Wayland + **Hyprland** +- **PipeWire** (audio capture) +- **systemd --user** (service) +- **wl-clipboard** (clipboard save/restore) +- **libnotify**/`notify-send` (optional notifications) +- `wtype` or `ydotool` (optional text injection fallback) + +> Other distros may work, but Arch/Hyprland is the primary target for now. --- -## Quick Start (Arch / Hyprland) +## Install (Arch / Hyprland) ```bash -# 1. Install from AUR (source build) -yay -S hyprvoice -#   or pre‑built binary -yay -S hyprvoice-bin +# AUR +yay -S hyprvoice # or: yay -S hyprvoice-bin -# 2. Run the interactive setup helper -hyprvoice-install run --backend whispercpp - -# 3. Enable the user service +# Enable user service systemctl --user enable --now hyprvoice.service -# 4. Add a key binding in Hyprland conf +# Hyprland keybind (toggle) bind = SUPER, R, exec, hyprvoice toggle ``` --- -## Configuration file (`~/.config/hyprvoice/config.toml`) +## Usage -**Note:** Configuration file support is not yet implemented. This shows the planned format: - -```toml -[asr] -backend = "whispercpp" # whispercpp | openai -model = "medium.en.bin" # used if backend = whispercpp -api_key = "" # used if backend = openai - -[vad] -threshold = -45 # dBFS -hang_ms = 300 - -[inject] -method = "wtype" # wtype | ydotool | clipboard - -[keybind] -# If you don't use Hyprland, set an XDG desktop accelerator here -shortcut = "CTRL+ALT+SPACE" -``` - -All options are documented in the sample config generated by `--init`. +- Press your **toggle** key to start; press again to stop. +- Audio streams to the cloud ASR while you speak. +- On stop (or VAD endpoint), Hyprvoice **pastes once** into the focused window. +- Injection flow: **save clipboard → copy final text → send Ctrl+V → restore clipboard**. --- -## Architecture +## Status -``` -┌──────────────┐ Unix Socket ┌──────────────┐ PCM ┌──────────┐ -│ CLI Client │◄────────────────│ Hot‑key │────────▶│ Audio │ -│ (toggle/stop)│ │ Daemon │ │ Capture │ -└──────────────┘ │ (hotkeydaemon│ │ (pipewire│ - │ + bus) │ │ + VAD) │ - └──────────────┘ └──────────┘ - │ │ - │ notify-send │ chunks - ▼ ▼ - ┌──────────────┐ ┌──────────┐ - │ Desktop │ │ ASR │ - │ Notification │ │ Adapter │ - └──────────────┘ │(whisper/ │ - │ openai) │ - └──────────┘ - │ - │ text - ▼ - ┌──────────┐ - │ Inject │ - │ (wtype/ │ - │ ydotool) │ - └──────────┘ -``` +| Component | State | Notes | +| -------------------------- | ----- | ------------------------------------------- | +| **Daemon (control plane)** | ✅ | State, IPC, worker orchestration | +| **Recording control** | ✅ | `hyprvoice toggle` | +| **Desktop notifications** | ✅ | `notify-send` (logs fallback) | +| **Audio capture** | 🔄 | PipeWire + VAD | +| **ASR backends** | 🔄 | Cloud **streaming** now; local Whisper next | +| **Text injection** | 🔄 | Clipboard paste → `wtype` → `ydotool` | +| **Service management** | 🔄 | `systemd --user` | -**Planned Components:** - -- `cmd/hyprvoice`: CLI with Cobra commands (existing: `serve`, `toggle`, `stop`) -- `internal/bus`: Unix socket IPC for daemon communication (✅ implemented) -- `internal/hotkeydaemon`: Background service managing recording state (✅ implemented) -- `internal/notify`: Desktop notifications via `notify-send` (✅ implemented) -- `internal/audiocapture`: PipeWire input with VAD/noise gate (🔄 TODO) -- `internal/asr`: ASR backends (Whisper, OpenAI, etc.) (🔄 TODO) -- `internal/inject`: Text injection via wtype/ydotool (🔄 TODO) -- `internal/config`: TOML configuration with runtime reload (🔄 TODO) - -Each box will be a Go package inside `internal/` so you can swap implementations without touching public APIs. +Legend: ✅ done · 🔄 in progress · ⏳ planned --- -## Building from Source +## How it works + +- **Model:** pipeline + central state (daemon = control plane). +- **State machine:** `idle → recording → transcribing → injecting → idle`. +- **Rule:** switch to **`transcribing`**\*\* as soon as the first audio frame is sent\*\* to the ASR. + +### ASCII diagram + +``` + +-------------------+ Unix socket IPC +-----------+ +CLI cmd → | Control Daemon | <---------------------------- | CLI/Tool | + |-------------------| +-----------+ + | State: idle/rec/ | + | transcribing/... | events events + | Event bus (chan) | -----> [Notifications] -----> notify-send/log + | | + | frames finals | + +--+-----------+----+ + | | + Audio | | Final Text + Frames v v + +--------+ +--------+ text +-----------+ + | Audio |-->| ASR | -------------->| Injection | + | Capture| | Stream | | Worker | + +--------+ +--------+ +-----------+ + | ^ + +--------------+ + backpressure via bounded channels + +State (daemon): +idle --toggle--> recording --first frame--> transcribing --final--> injecting --done--> idle +``` + +### Data flow + +1. `toggle` → **recording** +2. First frame sent → **transcribing** +3. Cloud ASR returns **final** → **injecting** +4. Paste once → **idle** +5. Notifications at each transition + +--- + +## Build from source ```bash git clone https://github.com/leonardotrapani/hyprvoice.git cd hyprvoice -# Compile CGO_ENABLED=1 go build -o hyprvoice ./cmd/hyprvoice -# Run tests go test ./... ``` -**Requires** Go 1.22+, a C compiler, and `pkg-config` with PipeWire headers. - --- -Contributions welcome! Check the [issues](https://github.com/leonardotrapani/hyprvoice/issues) for good first tasks. +## Contributing + +- All PRs and issues welcome. --- ## License -This project is licensed under the MIT License. See the [LICENSE.md](LICENSE.md) file for details. +MIT — see [LICENSE.md](LICENSE.md) diff --git a/cmd/hyprvoice/main.go b/cmd/hyprvoice/main.go index 8a04d1f..38ec386 100644 --- a/cmd/hyprvoice/main.go +++ b/cmd/hyprvoice/main.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/leonardotrapani/hyprvoice/internal/bus" - "github.com/leonardotrapani/hyprvoice/internal/hotkeydaemon" + "github.com/leonardotrapani/hyprvoice/internal/daemon" "github.com/leonardotrapani/hyprvoice/internal/notify" "github.com/spf13/cobra" ) @@ -31,9 +31,9 @@ func init() { func serveCmd() *cobra.Command { return &cobra.Command{ Use: "serve", - Short: "Run the hotkey daemon", + Short: "Run the daemon", RunE: func(cmd *cobra.Command, args []string) error { - return (hotkeydaemon.New(notify.Desktop{})).Run() + return (daemon.New(notify.Desktop{})).Run() }, } } @@ -86,7 +86,7 @@ func versionCmd() *cobra.Command { func stopCmd() *cobra.Command { return &cobra.Command{ Use: "stop", - Short: "Stop the hotkey daemon", + Short: "Stop the daemon", RunE: func(cmd *cobra.Command, args []string) error { resp, err := bus.SendCommand('q') if err != nil { diff --git a/internal/hotkeydaemon/daemon.go b/internal/daemon/daemon.go similarity index 90% rename from internal/hotkeydaemon/daemon.go rename to internal/daemon/daemon.go index b330a18..d8a10f4 100644 --- a/internal/hotkeydaemon/daemon.go +++ b/internal/daemon/daemon.go @@ -1,4 +1,4 @@ -package hotkeydaemon +package daemon import ( "bufio" @@ -121,18 +121,19 @@ func (d *Daemon) handle(c net.Conn) { case 't': // toggle d.mu.Lock() d.recording = !d.recording - recording := d.recording + isRecording := d.recording d.mu.Unlock() - d.notifier.RecordingChanged(recording) - log.Printf("Recording toggled: %t", recording) - fmt.Fprintf(c, "STATUS recording=%t\n", recording) + d.notifier.RecordingChanged(isRecording) + log.Printf("Recording toggled: %t", isRecording) + + fmt.Fprintf(c, "STATUS recording=%t\n", isRecording) case 's': // status d.mu.Lock() - recording := d.recording + isRecording := d.recording d.mu.Unlock() - fmt.Fprintf(c, "STATUS recording=%t\n", recording) + fmt.Fprintf(c, "STATUS recording=%t\n", isRecording) case 'v': // protocol version fmt.Fprintf(c, "STATUS proto=%s\n", bus.ProtoVer) case 'q': // quit daemon diff --git a/internal/hotkeydaemon/daemon_test.go b/internal/daemon/daemon_test.go similarity index 98% rename from internal/hotkeydaemon/daemon_test.go rename to internal/daemon/daemon_test.go index c4e07b7..5d654f0 100644 --- a/internal/hotkeydaemon/daemon_test.go +++ b/internal/daemon/daemon_test.go @@ -1,4 +1,4 @@ -package hotkeydaemon +package daemon import ( "testing"