A dead-simple desktop app that turns an audio file into text. Pick a file, click Transcribe, then Copy or Save as .txt. Auto-detects the language (Hindi, English, Hinglish, and more), with an optional language selector.
Two ways to transcribe, switchable at any time from the main screen:
| Engine | Needs | Speed & accuracy |
|---|---|---|
Groq · cloud (whisper-large-v3) |
A free API key and an internet connection | Fastest, most accurate |
| Local · on this device (faster-whisper) | Nothing — no key, no internet | Slower, a little less accurate |
Local transcription runs entirely on your own computer: your audio never leaves the machine, and there's no file-size limit. It's also the only way to use the app without signing up for anything.
- Download
SmuggyTranscriber.exefrom the Releases page. - Double-click it.
- Windows may show a SmartScreen warning because the app isn't code-signed. Click More info → Run anyway. (It's safe — it's just unsigned.)
- The first time, it asks how you want to transcribe:
- Use a Groq API key — it walks you through getting a free one (see below). Your key is stored privately on your own machine; it's never shared or uploaded anywhere but to Groq.
- Transcribe on this device — no key, no sign-up. The first run downloads
the speech model (about 148 MB for the default
base), then it works offline forever.
- Click Choose audio file…, pick the Language (or leave on Auto), and click Transcribe.
- Use Copy or Save as .txt for the result.
No file-size limit. Large files are compressed and split automatically for Groq (which rejects uploads over 25 MB); the local engine sends nothing anywhere, so it has no limit at all. Supported formats include
.mp3,.wav,.m4a,.ogg,.webm,.mp4,.flac.
When the local engine is selected, a second dropdown picks the model size.
Bigger is more accurate but slower and a larger one-time download. Models are
stored per-user (%LOCALAPPDATA% on Windows, ~/Library/Application Support
on macOS), never inside the app.
| Model | Download | Notes |
|---|---|---|
tiny |
78 MB | Fastest, roughest |
base |
148 MB | Default — good balance |
small |
486 MB | Noticeably better, ~3× slower |
medium |
1.5 GB | Best local quality, slow on CPU |
Downloading a model needs an internet connection once. If you're offline and haven't downloaded one yet, the app says so rather than failing silently.
Hindi → Hinglish conversion needs a Groq key even on the local engine, because the romanisation step runs on Groq's
llama-3.1-8b-instant. Without a key that toggle is disabled; everything else works. You can add a key later via API key in the top-right of the main screen.
Optional — skip this entirely if you're happy with local transcription. The app shows these same steps when you pick the Groq route. Head to console.groq.com and:
1. Sign in (or create a free account).
2. Open API Keys, then click Create API Key.
3. Give it any name and click Submit.
4. Copy the key — it starts with gsk_ and is shown only once. Paste it into
the app and click Save & Continue.
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linux
pip install -r requirements.txt
python app.pyFor dev convenience, the app also reads GROQ_API_KEY from a .env file
(copy .env.example to .env), so you don't have to paste a key each run.
A real, saved key always takes precedence over the env var.
core.py runs its own assertions — no test framework, no fixtures:
python core.py # split planning, engine ids, the PyAV stub
python core.py some.mp3 # ...plus real splitting and audio windowingCI runs the first form on both runners before packaging.
transcribe.py is a tiny CLI that transcribes a hardcoded file. It shares
its logic with the app via core.py:
python transcribe.py # transcribes ReelAudio-39184.mp3 -> transcription.txt| File | Purpose |
|---|---|
core.py |
GUI-free logic: key validation, Groq + local transcription (self-checking) |
settings.py |
Per-user key, engine and model choice via QSettings |
theme.py |
Green-on-black design language: palette, fonts, Qt stylesheet |
app.py |
PySide6 GUI (onboarding, drop-zone, transcribing, result) |
transcribe.py |
CLI, imports from core.py |
updater.py |
Self-update from GitHub Releases, in packaged builds only (self-checking) |
verify_bundle.py |
CI check that a built artifact really contains the local engine |
assets/ |
Bundled fonts, app icon (.ico/.icns), logo, setup screenshots |
A GitHub Actions workflow (.github/workflows/build.yml)
packages the app with PyInstaller (--onefile --windowed --name SmuggyTranscriber)
on two runners in parallel:
- Windows (
windows-latest) →SmuggyTranscriber.exe - macOS (
macos-latest) →SmuggyTranscriber.app, zipped withdittointoSmuggyTranscriber-macos.zip
The local engine adds faster-whisper and CTranslate2 to the bundle (macOS goes from ~76 MB to ~86 MB zipped). PyAV and onnxruntime are deliberately excluded — faster-whisper imports them but we never reach either, and PyAV alone bundles a whole second copy of FFmpeg. Model weights are never bundled; they download on first use. See the comments in the workflow before changing those flags.
On every push to main each build is uploaded as a workflow artifact
(SmuggyTranscriber-windows / SmuggyTranscriber-macos). Pushing a v* tag
(e.g. git tag v1.0 && git push --tags) also attaches both to a GitHub
Release for users to download. No Windows or Mac machine is needed to
build — the runners do it.
The macOS
.appis unsigned, so the first launch needs right-click → Open (or System Settings → Privacy & Security → Open Anyway) to get past Gatekeeper — the Mac equivalent of the Windows SmartScreen prompt.
Every launch, a packaged build asks GitHub for the latest release
(updater.py). If its tag is newer than the VERSION baked into
the running app, it offers Update now / Later; "Update now" downloads that
release's asset, moves the old app aside as .old, drops the new one in its
place and relaunches. The .old is deleted on the next launch. A dev checkout
never does any of this — it updates with git. Failures are logged and never
shown, so being offline or rate-limited just means no prompt.
Cutting a release: bump VERSION in the same commit as the tag. The app
compares the release tag against its own VERSION, so tagging v1.3.0 while
VERSION still says 1.2.0 ships a build that tells every user there's an
update, forever, and re-downloads itself on every launch. Both CI jobs fail the
build on a mismatch rather than let that reach a release.
Because the download is programmatic it carries no quarantine flag, so an updated app skips the Gatekeeper dance above — that only applies to the first copy, downloaded by hand.




