From b296491703321616050ff53ca69b17162720b802 Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Fri, 6 Feb 2026 18:53:02 -0300 Subject: [PATCH] Switch config to JSON --- Makefile | 16 ++++++++ README.md | 63 ++++++++++++++++++++++--------- go.mod | 1 - go.sum | 2 - internal/config/config.go | 79 +++++++++++++++++++++++++++++++-------- systemd/lel.service | 2 +- 6 files changed, 127 insertions(+), 36 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b832d37 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +BIN_DIR := . +LELD := $(BIN_DIR)/leld +LELCTL := $(BIN_DIR)/lelctl +CONFIG := $(HOME)/.config/lel/config.json + +.PHONY: build run clean + +build: + go build -o $(LELD) ./cmd/leld + go build -o $(LELCTL) ./cmd/lelctl + +run: + $(LELD) --config $(CONFIG) + +clean: + rm -f $(LELD) $(LELCTL) diff --git a/README.md b/README.md index e39b579..e1acc4c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lel -X11 transcription daemon that records audio and runs Whisper, logging the transcript. +X11 transcription daemon that records audio, runs Whisper, logs the transcript, and can optionally run AI post-processing before injecting text. ## Requirements @@ -13,26 +13,37 @@ X11 transcription daemon that records audio and runs Whisper, logging the transc ## Build ```bash -go build -o leld ./cmd/leld -go build -o lelctl ./cmd/lelctl +make build ``` ## Config -Create `~/.config/lel/config.toml`: +Create `~/.config/lel/config.json`: -```toml -hotkey = "Cmd+m" -ffmpeg_input = "pulse:default" -whisper_model = "base" -whisper_lang = "en" -whisper_device = "cpu" -whisper_extra_args = "" -record_timeout_sec = 120 -whisper_timeout_sec = 300 -segment_sec = 5 -streaming = false -injection_backend = "clipboard" +```json +{ + "hotkey": "Cmd+m", + "ffmpeg_input": "pulse:default", + "ffmpeg_path": "", + "whisper_model": "base", + "whisper_lang": "en", + "whisper_device": "cpu", + "whisper_extra_args": "", + "record_timeout_sec": 120, + "whisper_timeout_sec": 300, + "segment_sec": 5, + "streaming": false, + "injection_backend": "clipboard", + + "ai_enabled": true, + "ai_provider": "ollama", + "ai_model": "llama3.2:3b", + "ai_temperature": 0.0, + "ai_system_prompt_file": "", + "ai_base_url": "http://localhost:11434", + "ai_api_key": "", + "ai_timeout_sec": 20 +} ``` Env overrides: @@ -41,11 +52,14 @@ Env overrides: - `WHISPER_FFMPEG_IN` - `WHISPER_STREAM`, `WHISPER_SEGMENT_SEC`, `WHISPER_TIMEOUT_SEC` - `LEL_RECORD_TIMEOUT_SEC`, `LEL_HOTKEY`, `LEL_INJECTION_BACKEND` +- `LEL_FFMPEG_PATH` +- `LEL_AI_ENABLED`, `LEL_AI_PROVIDER`, `LEL_AI_MODEL`, `LEL_AI_TEMPERATURE`, `LEL_AI_SYSTEM_PROMPT_FILE` +- `LEL_AI_BASE_URL`, `LEL_AI_API_KEY`, `LEL_AI_TIMEOUT_SEC` ## Run manually ```bash -./leld --config ~/.config/lel/config.toml +./leld --config ~/.config/lel/config.json ``` Disable the tray icon: @@ -70,11 +84,26 @@ systemctl --user enable --now lel - Press it again to stop and transcribe. - The transcript is logged to stderr. +Execution flow (single in-flight state machine): + +- `recording` -> `transcribing` -> `processing` (optional) -> `outputting` -> `idle` + Injection backends: - `clipboard`: copy to clipboard and inject via Ctrl+V (requires `xclip` + `xdotool`) - `injection`: type the text with simulated keypresses (requires `xdotool`) +AI providers: + +- `ollama`: calls the local Ollama HTTP API (`/api/generate`) +- `openai_compat`: calls a chat-completions compatible API (`/v1/chat/completions`) + +Dependency checks: + +- Recording requires `ffmpeg` (or set `ffmpeg_path`) +- Transcribing uses the `whisper` CLI +- Outputting requires `xclip` (and `xdotool` for injection backends) + Control: ```bash diff --git a/go.mod b/go.mod index 2551fa4..b7623ad 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module lel go 1.25.5 require ( - github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc github.com/getlantern/systray v1.2.2 ) diff --git a/go.sum b/go.sum index dbc8f19..a8d1893 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= diff --git a/internal/config/config.go b/internal/config/config.go index 9c036d3..801a59e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,32 +1,40 @@ package config import ( + "encoding/json" "errors" "os" "path/filepath" "strconv" "strings" - - "github.com/BurntSushi/toml" ) type Config struct { - Hotkey string `toml:"hotkey"` - FfmpegInput string `toml:"ffmpeg_input"` - WhisperModel string `toml:"whisper_model"` - WhisperLang string `toml:"whisper_lang"` - WhisperDevice string `toml:"whisper_device"` - WhisperExtraArgs string `toml:"whisper_extra_args"` - RecordTimeoutSec int `toml:"record_timeout_sec"` - WhisperTimeoutSec int `toml:"whisper_timeout_sec"` - SegmentSec int `toml:"segment_sec"` - Streaming bool `toml:"streaming"` - InjectionBackend string `toml:"injection_backend"` + Hotkey string `json:"hotkey"` + FfmpegInput string `json:"ffmpeg_input"` + WhisperModel string `json:"whisper_model"` + WhisperLang string `json:"whisper_lang"` + WhisperDevice string `json:"whisper_device"` + WhisperExtraArgs string `json:"whisper_extra_args"` + RecordTimeoutSec int `json:"record_timeout_sec"` + WhisperTimeoutSec int `json:"whisper_timeout_sec"` + SegmentSec int `json:"segment_sec"` + Streaming bool `json:"streaming"` + InjectionBackend string `json:"injection_backend"` + + AIEnabled bool `json:"ai_enabled"` + AIProvider string `json:"ai_provider"` + AIModel string `json:"ai_model"` + AITemperature float64 `json:"ai_temperature"` + AISystemPromptFile string `json:"ai_system_prompt_file"` + AIBaseURL string `json:"ai_base_url"` + AIAPIKey string `json:"ai_api_key"` + AITimeoutSec int `json:"ai_timeout_sec"` } func DefaultPath() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".config", "lel", "config.toml") + return filepath.Join(home, ".config", "lel", "config.json") } func Defaults() Config { @@ -42,6 +50,15 @@ func Defaults() Config { SegmentSec: 5, Streaming: false, InjectionBackend: "clipboard", + + AIEnabled: false, + AIProvider: "ollama", + AIModel: "llama3.2:3b", + AITemperature: 0.0, + AISystemPromptFile: "", + AIBaseURL: "http://localhost:11434", + AIAPIKey: "", + AITimeoutSec: 20, } } @@ -53,7 +70,11 @@ func Load(path string) (Config, error) { } if _, err := os.Stat(path); err == nil { - if _, err := toml.DecodeFile(path, &cfg); err != nil { + data, err := os.ReadFile(path) + if err != nil { + return cfg, err + } + if err := json.Unmarshal(data, &cfg); err != nil { return cfg, err } } @@ -113,6 +134,34 @@ func applyEnv(cfg *Config) { if v := os.Getenv("LEL_INJECTION_BACKEND"); v != "" { cfg.InjectionBackend = v } + if v := os.Getenv("LEL_AI_ENABLED"); v != "" { + cfg.AIEnabled = parseBool(v) + } + if v := os.Getenv("LEL_AI_PROVIDER"); v != "" { + cfg.AIProvider = v + } + if v := os.Getenv("LEL_AI_MODEL"); v != "" { + cfg.AIModel = v + } + if v := os.Getenv("LEL_AI_TEMPERATURE"); v != "" { + if n, err := strconv.ParseFloat(v, 64); err == nil { + cfg.AITemperature = n + } + } + if v := os.Getenv("LEL_AI_SYSTEM_PROMPT_FILE"); v != "" { + cfg.AISystemPromptFile = v + } + if v := os.Getenv("LEL_AI_BASE_URL"); v != "" { + cfg.AIBaseURL = v + } + if v := os.Getenv("LEL_AI_API_KEY"); v != "" { + cfg.AIAPIKey = v + } + if v := os.Getenv("LEL_AI_TIMEOUT_SEC"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + cfg.AITimeoutSec = n + } + } } func parseBool(v string) bool { diff --git a/systemd/lel.service b/systemd/lel.service index 21c8657..6530e80 100644 --- a/systemd/lel.service +++ b/systemd/lel.service @@ -4,7 +4,7 @@ After=default.target [Service] Type=simple -ExecStart=%h/.local/bin/leld --config %h/.config/lel/config.toml +ExecStart=%h/.local/bin/leld --config %h/.config/lel/config.json Restart=on-failure RestartSec=2