Skip to content

Latest commit

 

History

History
185 lines (150 loc) · 5.51 KB

File metadata and controls

185 lines (150 loc) · 5.51 KB

AGENTS.md - ok-voice Codebase Guide

Project Overview

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.

Build/Lint/Test Commands

Running the Application

bb ok-voice          # Run the main application
bb run ok-voice      # Alternative task syntax

Development

bb -e "(require '[ok-voice.core :as core])"   # Quick REPL evaluation
bb -f src/ok_voice/core.clj                   # Run a specific file

Checking Dependencies

bb print-deps                                 # Print classpath and dependencies
bb tasks                                      # List available tasks

Testing

Currently no test suite is configured. When adding tests:

bb test                                       # Run all tests (when configured)

Linting (if clj-kondo is available)

clj-kondo --lint src                          # Lint all source files
clj-kondo --lint src/ok_voice/core.clj        # Lint single file

Code Style Guidelines

Namespace Structure

  • 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/

Imports/Requires

(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 :as for aliases consistently
  • Use :refer sparingly, only for very common functions (e.g., [sh])
  • Import Java classes at the end with :import

Naming Conventions

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

Formatting

  • 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

Type Hints

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)]
    ...))

Error Handling

  1. Fatal errors: Use System/exit 1 with user notification:
(when error-condition
  (notify/error "ok-voice" "Error message")
  (System/exit 1))
  1. Recoverable errors: Return nil or use try/catch:
(try
  (let [result (sh "which" cmd)]
    (zero? (:exit result)))
  (catch Exception _
    false))
  1. Silent failures: Use catch-all for cleanup operations:
(catch java.io.IOException _
  nil)
  1. Debug logging: Write to stderr with binding:
(binding [*out* *err*]
  (println "[error]" msg))

Documentation

  • 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

Atoms and State

  • 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))))

Process/External Command Handling

  • Use babashka.process/sh for synchronous commands
  • Use babashka.process/process for async/streaming processes
  • Always clean up child processes: p/destroy-tree proc
  • Check exit codes: (zero? (:exit result))

File Organization

Each module should have a single responsibility:

  • core.clj - Main entry point and orchestration
  • config.clj - Configuration loading and validation
  • audio.clj - Audio capture via parecord
  • websocket.clj - OpenAI WebSocket communication
  • transcription.clj - Transcription pipeline coordination
  • text.clj - Text output via xdotool
  • notify.clj - Desktop notifications
  • toggle.clj - Process management (single instance)
  • deps.clj - Dependency checking

Key Dependencies

  • babashka.process - Shell command execution
  • babashka.http-client.websocket - WebSocket client
  • clj-yaml.core - YAML parsing for config
  • cheshire.core - JSON encoding/decoding
  • clojure.java.io - File operations
  • clojure.string - String utilities