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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.)
|
||||
@@ -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: <tempdir>/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()
|
||||
Reference in New Issue
Block a user