From 5864d78081df12f5c93ba1e8c0c7b24d821436a5 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Sun, 31 May 2026 09:03:43 -0400 Subject: [PATCH] Add README and cross-platform export tool README documents minimal setup, run, and config for Linux and Windows. export.py bundles just the source into a zip (no .venv, caches, or API key files) for moving to another machine; it's pure stdlib run via 'uv run export.py', so it behaves identically on Linux and Windows with no shell required. Co-Authored-By: Claude Opus 4.8 --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ export.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 README.md create mode 100644 export.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..25c6d89 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Blurt + +Tray-based push-to-talk voice-to-text for Linux and Windows. Toggle recording +with a hotkey, speak, toggle off — Blurt transcribes the clip and copies the +text to your clipboard. + +If a transcript starts with `to my agent`, the rest is dispatched as an agent +task instead of copied. + +## Setup + +Requires [uv](https://docs.astral.sh/uv/) and Python 3.11+. + +```sh +uv sync +``` + +### API key + +Blurt reads the key for its current model's provider in this order: +environment variable → OS keyring → local file. + +| Provider | Env var | Keyring (`service` / `user`) | File | +|------------|----------------------|---------------------------------|----------------------| +| OpenAI | `OPENAI_API_KEY` | `openai-api-key` / `felixm` | `api_key.txt` | +| OpenRouter | `OPENROUTER_API_KEY` | `openrouter-api-key` / `felixm` | `openrouter_key.txt` | + +Key files and runtime config stay local (gitignored). + +## Running + +**Linux** — `uv run blurt` starts the tray icon. Bind a hotkey to +`./blurt-toggle`, which signals the running instance to start/stop recording. + +**Windows** — launch `blurt.vbs` (runs Blurt with no console window). The +global hotkey is **F20**. + +## Configuration + +Set from the tray menu, or via files / env vars: + +- **Model** — `model.txt` / `BLURT_TRANSCRIPTION_MODEL`. For lowest latency use + `openai/whisper-large-v3-turbo` (OpenRouter, served by Groq). +- **Language** — `language.txt` / `BLURT_TRANSCRIPTION_LANGUAGE` (`en`, `de`). +- **Input device** — tray menu. + +Logs are written to `blurt.log` in your temp directory (openable from the tray +menu). + +## Moving to another machine + +`uv run export.py` writes a zip of just the source (no 200 MB+ `.venv`, no +caches, no key files) to your temp directory. It's pure Python, so it runs the +same on Linux and Windows — no shell needed — and it only reads, so it's safe +while Blurt is recording. Unzip on the other machine and `uv sync` to rebuild +`.venv` for that OS. (Or simply `git clone` the repo fresh.) diff --git a/export.py b/export.py new file mode 100644 index 0000000..94923e5 --- /dev/null +++ b/export.py @@ -0,0 +1,63 @@ +"""Bundle Blurt's source into a portable zip — e.g. to move it to another +machine — leaving out the 200 MB+ .venv, caches, logs, and API key files. + +Cross-platform and dependency-free (stdlib only, no shell, no git), so it runs +the same way on Linux and Windows: + + uv run export.py [output.zip] # default: /blurt-export.zip + +It only reads, never modifying this directory, so it is safe to run while Blurt +is recording. On the other machine: unzip, then `uv sync` to rebuild .venv for +that OS. +""" + +import os +import sys +import tempfile +import zipfile +from pathlib import Path + +PROJECT_DIR = Path(__file__).resolve().parent +ARCHIVE_ROOT = "blurt" # top-level folder inside the zip + +# Never worth copying. API key files are excluded by name so secrets are never +# swept in, even though they sit right next to the source. +EXCLUDE_DIRS = { + ".git", ".venv", "__pycache__", ".claude", + "dist", "build", ".ruff_cache", ".pytest_cache", +} +EXCLUDE_NAMES = {"api_key.txt", "openrouter_key.txt"} +EXCLUDE_SUFFIXES = {".pyc", ".pyo"} + + +def _included_files(): + for root, dirs, files in os.walk(PROJECT_DIR): + dirs[:] = [ + d for d in dirs + if d not in EXCLUDE_DIRS and not d.endswith(".egg-info") + ] + for name in files: + if name in EXCLUDE_NAMES or Path(name).suffix in EXCLUDE_SUFFIXES: + continue + yield Path(root) / name + + +def main(): + default_out = Path(tempfile.gettempdir()) / "blurt-export.zip" + out = (Path(sys.argv[1]) if len(sys.argv) > 1 else default_out).resolve() + out.parent.mkdir(parents=True, exist_ok=True) + + count = 0 + with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf: + for path in _included_files(): + if path.resolve() == out: # don't zip the output into itself + continue + arcname = Path(ARCHIVE_ROOT) / path.relative_to(PROJECT_DIR) + zf.write(path, arcname) + count += 1 + + print(f"Wrote {out} ({count} files, {out.stat().st_size // 1024} KB)") + + +if __name__ == "__main__": + main()