Rename HotkeyDaemon -> Daemon
update readme
This commit is contained in:
@@ -1,139 +1,135 @@
|
|||||||
# Hyprvoice
|
# 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 |
|
- Wayland + **Hyprland**
|
||||||
| ------------------------- | ------- | ------------------------------------------------------------ |
|
- **PipeWire** (audio capture)
|
||||||
| **Hot‑key daemon** | ✅ Done | Background service with Unix socket IPC for command handling |
|
- **systemd --user** (service)
|
||||||
| **Desktop notifications** | ✅ Done | Recording state changes via `notify-send` |
|
- **wl-clipboard** (clipboard save/restore)
|
||||||
| **Service management** | ✅ Done | Install/remove systemd user service with embedded unit file |
|
- **libnotify**/`notify-send` (optional notifications)
|
||||||
| **Live audio capture** | 🔄 TODO | PipeWire input with VAD/noise gate |
|
- `wtype` or `ydotool` (optional text injection fallback)
|
||||||
| **ASR backends** | 🔄 TODO | Local Whisper or cloud APIs (OpenAI, Deepgram, AssemblyAI) |
|
|
||||||
| **Text injection** | 🔄 TODO | `wtype`/`ydotool` keystrokes to focused window |
|
> Other distros may work, but Arch/Hyprland is the primary target for now.
|
||||||
| **Configuration** | 🔄 TODO | TOML config file with runtime reload |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start (Arch / Hyprland)
|
## Install (Arch / Hyprland)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Install from AUR (source build)
|
# AUR
|
||||||
yay -S hyprvoice
|
yay -S hyprvoice # or: yay -S hyprvoice-bin
|
||||||
# or pre‑built binary
|
|
||||||
yay -S hyprvoice-bin
|
|
||||||
|
|
||||||
# 2. Run the interactive setup helper
|
# Enable user service
|
||||||
hyprvoice-install run --backend whispercpp
|
|
||||||
|
|
||||||
# 3. Enable the user service
|
|
||||||
systemctl --user enable --now hyprvoice.service
|
systemctl --user enable --now hyprvoice.service
|
||||||
|
|
||||||
# 4. Add a key binding in Hyprland conf
|
# Hyprland keybind (toggle)
|
||||||
bind = SUPER, R, exec, hyprvoice 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:
|
- Press your **toggle** key to start; press again to stop.
|
||||||
|
- Audio streams to the cloud ASR while you speak.
|
||||||
```toml
|
- On stop (or VAD endpoint), Hyprvoice **pastes once** into the focused window.
|
||||||
[asr]
|
- Injection flow: **save clipboard → copy final text → send Ctrl+V → restore clipboard**.
|
||||||
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`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## Status
|
||||||
|
|
||||||
```
|
| Component | State | Notes |
|
||||||
┌──────────────┐ Unix Socket ┌──────────────┐ PCM ┌──────────┐
|
| -------------------------- | ----- | ------------------------------------------- |
|
||||||
│ CLI Client │◄────────────────│ Hot‑key │────────▶│ Audio │
|
| **Daemon (control plane)** | ✅ | State, IPC, worker orchestration |
|
||||||
│ (toggle/stop)│ │ Daemon │ │ Capture │
|
| **Recording control** | ✅ | `hyprvoice toggle` |
|
||||||
└──────────────┘ │ (hotkeydaemon│ │ (pipewire│
|
| **Desktop notifications** | ✅ | `notify-send` (logs fallback) |
|
||||||
│ + bus) │ │ + VAD) │
|
| **Audio capture** | 🔄 | PipeWire + VAD |
|
||||||
└──────────────┘ └──────────┘
|
| **ASR backends** | 🔄 | Cloud **streaming** now; local Whisper next |
|
||||||
│ │
|
| **Text injection** | 🔄 | Clipboard paste → `wtype` → `ydotool` |
|
||||||
│ notify-send │ chunks
|
| **Service management** | 🔄 | `systemd --user` |
|
||||||
▼ ▼
|
|
||||||
┌──────────────┐ ┌──────────┐
|
|
||||||
│ Desktop │ │ ASR │
|
|
||||||
│ Notification │ │ Adapter │
|
|
||||||
└──────────────┘ │(whisper/ │
|
|
||||||
│ openai) │
|
|
||||||
└──────────┘
|
|
||||||
│
|
|
||||||
│ text
|
|
||||||
▼
|
|
||||||
┌──────────┐
|
|
||||||
│ Inject │
|
|
||||||
│ (wtype/ │
|
|
||||||
│ ydotool) │
|
|
||||||
└──────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Planned Components:**
|
Legend: ✅ done · 🔄 in progress · ⏳ planned
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
```bash
|
||||||
git clone https://github.com/leonardotrapani/hyprvoice.git
|
git clone https://github.com/leonardotrapani/hyprvoice.git
|
||||||
cd hyprvoice
|
cd hyprvoice
|
||||||
# Compile
|
|
||||||
CGO_ENABLED=1 go build -o hyprvoice ./cmd/hyprvoice
|
CGO_ENABLED=1 go build -o hyprvoice ./cmd/hyprvoice
|
||||||
# Run tests
|
|
||||||
go test ./...
|
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
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License. See the [LICENSE.md](LICENSE.md) file for details.
|
MIT — see [LICENSE.md](LICENSE.md)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/leonardotrapani/hyprvoice/internal/bus"
|
"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/leonardotrapani/hyprvoice/internal/notify"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -31,9 +31,9 @@ func init() {
|
|||||||
func serveCmd() *cobra.Command {
|
func serveCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Run the hotkey daemon",
|
Short: "Run the daemon",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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 {
|
func stopCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "stop",
|
Use: "stop",
|
||||||
Short: "Stop the hotkey daemon",
|
Short: "Stop the daemon",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
resp, err := bus.SendCommand('q')
|
resp, err := bus.SendCommand('q')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package hotkeydaemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -121,18 +121,19 @@ func (d *Daemon) handle(c net.Conn) {
|
|||||||
case 't': // toggle
|
case 't': // toggle
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
d.recording = !d.recording
|
d.recording = !d.recording
|
||||||
recording := d.recording
|
isRecording := d.recording
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
d.notifier.RecordingChanged(recording)
|
d.notifier.RecordingChanged(isRecording)
|
||||||
log.Printf("Recording toggled: %t", recording)
|
log.Printf("Recording toggled: %t", isRecording)
|
||||||
fmt.Fprintf(c, "STATUS recording=%t\n", recording)
|
|
||||||
|
fmt.Fprintf(c, "STATUS recording=%t\n", isRecording)
|
||||||
case 's': // status
|
case 's': // status
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
recording := d.recording
|
isRecording := d.recording
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
fmt.Fprintf(c, "STATUS recording=%t\n", recording)
|
fmt.Fprintf(c, "STATUS recording=%t\n", isRecording)
|
||||||
case 'v': // protocol version
|
case 'v': // protocol version
|
||||||
fmt.Fprintf(c, "STATUS proto=%s\n", bus.ProtoVer)
|
fmt.Fprintf(c, "STATUS proto=%s\n", bus.ProtoVer)
|
||||||
case 'q': // quit daemon
|
case 'q': // quit daemon
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package hotkeydaemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
Reference in New Issue
Block a user