Skip to main content

Architecture

hns is a single-module CLI tool. The entire implementation lives in hns/cli.py. This page covers the internal architecture to help contributors get up to speed quickly.

hns architecture diagram showing the flow from CLI entry to audio recording, transcription, and output.hns architecture diagram showing the flow from CLI entry to audio recording, transcription, and output.

High-Level Flow

  1. CLI entry. Click parses arguments and dispatches to one of three paths: hns (record + transcribe), hns --last (transcribe cached audio), or hns --list-models (print models and exit).
  2. Record. AudioRecorder captures microphone input via sounddevice, streams it to a WAV file in the platform cache directory.
  3. Transcribe. WhisperTranscriber loads a faster-whisper model and transcribes the WAV file locally. VAD filters silence. Segments are joined into a single string.
  4. Output. Transcription is printed to stdout and copied to the clipboard via pyperclip. All progress and error messages go to stderr.

This stdout/stderr separation is what makes hns composable. stdout carries only the transcription, so it can be piped or captured with $(hns) without interference from status messages.

Core Components

AudioRecorder

Records microphone audio and writes it to disk.

ResponsibilityDetail
Audio capturesounddevice.InputStream at 16kHz mono, float32 samples converted to int16
File outputWAV written to platform-specific cache directory (last_recording.wav)
Live feedbackBackground thread prints elapsed time to stderr while recording
Stop signalBlocks on input(). User presses Enter to stop

Cache paths:

PlatformPath
macOS~/Library/Caches/hns/
Linux~/.cache/hns/
Windows~/AppData/Local/hns/Cache/

The cached WAV file is what --last reads from to re-transcribe without re-recording.

WhisperTranscriber

Loads a Whisper model and transcribes audio.

ResponsibilityDetail
Model loadingfaster-whisper with CPU / int8 quantization. Model name from HNS_WHISPER_MODEL env var, defaults to base
VADBuilt-in VAD filter (min_silence_duration_ms=500, speech_pad_ms=400, threshold=0.5) strips silence before transcription
TranscriptionBeam search (beam_size=5), optional language forcing via HNS_LANG
ProgressBackground thread shows elapsed time on stderr during transcription
OutputSegments joined with spaces into a single string

Invalid model names fall back to base with a warning.

Output Layer

After transcription:

  1. pyperclip.copy() copies the text to the system clipboard.
  2. print() writes the text to stdout (via a separate Console instance bound to stdout).

Clipboard errors are caught and warned, but they don't abort the process.

Dependencies

PackageRole
clickCLI argument parsing
sounddeviceMicrophone audio capture
numpyAudio sample conversion (float32 to int16)
faster-whisperLocal Whisper inference (CTranslate2 backend)
pyperclipCross-platform clipboard access
richStyled stderr output

Threading Model

hns uses threads in two places:

  1. Recording timer. A daemon thread updates the elapsed time display on stderr while sounddevice.InputStream captures audio on its own callback thread. Recording stops when input() returns (Enter key).
  2. Transcription progress. Transcription runs in a background thread so the main thread can display an elapsed time counter. Results pass back via a queue.Queue.

Both use threading.Event for clean shutdown coordination.

Entry Point

The Click command main() in cli.py is registered as the hns console script in pyproject.toml:

[project.scripts]
hns = "hns.cli:main"

This is what makes hns available as a command after installation with uv, pipx, or pip.