ok-voice is a Babashka (native Clojure) application for voice-to-text transcription using OpenAI's Realtime API. It captures audio from PulseAudio, streams it to OpenAI, and types transcribed text into the active window via xdotool.
bb ok-voice # Run the main application
bb run ok-voice # Alternative task syntaxbb -e "(require '[ok-voice.core :as core])" # Quick REPL evaluation
bb -f src/ok_voice/core.clj # Run a specific filebb print-deps # Print classpath and dependencies
bb tasks # List available tasksCurrently no test suite is configured. When adding tests:
bb test # Run all tests (when configured)clj-kondo --lint src # Lint all source files
clj-kondo --lint src/ok_voice/core.clj # Lint single file- Namespace names use lowercase with hyphens:
ok-voice.core,ok-voice.transcription - Directory structure mirrors namespace:
src/ok_voice/core.clj - All source files live in
src/ok_voice/
(ns ok-voice.example
(:require [babashka.process :as p] ; alias is shortened name
[clojure.string :as str] ; common abbreviations: str, io
[clojure.java.io :as io]
[ok-voice.notify :as notify])) ; local namespaces use full name
(:import [java.util Base64])) ; Java interop imports- Group requires alphabetically within each category
- Use
:asfor aliases consistently - Use
:refersparingly, only for very common functions (e.g.,[sh]) - Import Java classes at the end with
:import
| Type | Convention | Example |
|---|---|---|
| Functions | kebab-case | start-audio!, read-chunk |
| Side-effecting functions | trailing ! |
write-pid!, stop!, connect! |
| Predicates | trailing ? |
process-alive?, ready? |
| Private vars | ^:private metadata |
(def ^:private sample-rate 24000) |
| Constants | ^:private with descriptive names |
(def ^:private api-url "...") |
| Atoms/refs | descriptive names | text-buffer, pipeline |
- Indent with 2 spaces
- Function arguments aligned when breaking lines:
(defn start!
[api-key opts]
(let [on-text (:on-text opts)
websocket (ws/connect! api-key
{:on-ready (fn [] ...)
:on-delta (fn [delta] ...)})]
...))- Collection literals: align map keys, one pair per line for 3+ entries
- Thread-first
->for data transformations - Thread-last
->>for sequence operations
Use type hints for performance-critical Java interop:
(defn- encode-base64 [^bytes ba]
(.encodeToString encoder ba))
(defn read-chunk [audio-proc]
(let [in ^java.io.InputStream (:out audio-proc)]
...))- Fatal errors: Use
System/exit 1with user notification:
(when error-condition
(notify/error "ok-voice" "Error message")
(System/exit 1))- Recoverable errors: Return nil or use try/catch:
(try
(let [result (sh "which" cmd)]
(zero? (:exit result)))
(catch Exception _
false))- Silent failures: Use catch-all for cleanup operations:
(catch java.io.IOException _
nil)- Debug logging: Write to stderr with binding:
(binding [*out* *err*]
(println "[error]" msg))- Add docstrings for public functions:
(defn start!
"Connects to OpenAI, starts audio capture, and begins streaming transcription.
opts: {:on-text (fn [delta] ...)} — called with each transcription delta.
Returns pipeline map on success, nil on failure."
[api-key opts]
...)- Include parameter descriptions for complex functions
- Document return values and nil handling
- Prefer
reset!for complete replacement,swap!for transformations - Use atoms with descriptive names for pipeline state
- Clean up state in shutdown hooks:
(.addShutdownHook (Runtime/getRuntime)
(Thread. (fn []
(toggle/remove-pid!)
(transcription/stop! pipeline))))- Use
babashka.process/shfor synchronous commands - Use
babashka.process/processfor async/streaming processes - Always clean up child processes:
p/destroy-tree proc - Check exit codes:
(zero? (:exit result))
Each module should have a single responsibility:
core.clj- Main entry point and orchestrationconfig.clj- Configuration loading and validationaudio.clj- Audio capture via parecordwebsocket.clj- OpenAI WebSocket communicationtranscription.clj- Transcription pipeline coordinationtext.clj- Text output via xdotoolnotify.clj- Desktop notificationstoggle.clj- Process management (single instance)deps.clj- Dependency checking
babashka.process- Shell command executionbabashka.http-client.websocket- WebSocket clientclj-yaml.core- YAML parsing for configcheshire.core- JSON encoding/decodingclojure.java.io- File operationsclojure.string- String utilities