Laryn is a Windows-first Electron speech-to-text app. Press Ctrl + Win to start recording, press it again to stop, and the app sends the audio through a Cloudflare Worker that transcribes with Whisper via Workers AI behind AI Gateway. The Worker then lightly cleans the transcript with a conservative text model pass before the desktop app pastes it into the currently focused application.
apps/desktop: Electron + Vite desktop client.apps/worker: Cloudflare Worker transcription API.packages/shared: Shared API types.
pnpm install
Copy-Item apps\desktop\.env.example apps\desktop\.env
Copy-Item apps\worker\.dev.vars.example apps\worker\.dev.varsThe desktop defaults to the deployed Worker:
https://laryn.mineperial.comOnly start a local Worker when you are actively developing Worker code:
pnpm worker:devFor normal desktop development, run:
pnpm desktop:devFor hot renderer development, run pnpm desktop:dev:renderer in one terminal and pnpm desktop:dev:hot in another.
Create an AI Gateway in Cloudflare and a D1 database named laryn, then apply the Worker migrations:
pnpm --filter @laryn/worker exec wrangler d1 migrations apply laryn --local
pnpm --filter @laryn/worker exec wrangler d1 migrations apply laryn --remoteUpdate apps/worker/wrangler.jsonc with the real D1 database_id, then provide:
CLOUDFLARE_ACCOUNT_IDAI_GATEWAY_IDGROQ_API_KEYwhenTRANSCRIPTION_PROVIDER=groqBETTER_AUTH_SECRETBETTER_AUTH_URLGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETPOLAR_ACCESS_TOKENPOLAR_WEBHOOK_SECRETPOLAR_PRO_PRODUCT_IDPUBLIC_APP_URL
The Worker uses Cloudflare AI Gateway for Groq speech-to-text when TRANSCRIPTION_PROVIDER=groq. Cleanup runs through Cloudflare-hosted text generation models using Workers AI neurons pricing.
Google OAuth callback URLs:
- Local:
http://localhost:8787/api/auth/callback/google - Production:
https://laryn.mineperial.com/api/auth/callback/google
Polar is configured through the Better Auth plugin. The webhook endpoint is:
https://laryn.mineperial.com/api/auth/polar/webhooks
Desktop clients now pair with an account using a device-code flow from the Settings drawer. The old shared LARYN_DESKTOP_TOKEN flow has been replaced by per-device tokens stored by Electron safe storage.
The Worker uses these model defaults:
- Speech-to-text provider:
groq - Groq speech-to-text:
whisper-large-v3-turbo - Workers AI speech-to-text fallback:
@cf/openai/whisper-large-v3-turbo - Speech-to-text language:
en - Speech-to-text hints: configurable with
TRANSCRIPTION_HINTSfor names, product terms, acronyms, and project vocabulary that are often misheard - Cheap cleanup:
@cf/meta/llama-3.2-1b-instruct - Standard cleanup:
@cf/meta/llama-3.2-3b-instruct - Premium cleanup:
@cf/google/gemma-4-26b-a4b-it - Cleanup fallback:
@cf/meta/llama-3.1-8b-instruct-fast - Default cleanup tier:
off - Cleanup timeout:
3500ms, then paste the raw transcript - Groq transcription is routed through Cloudflare AI Gateway and metered with
LARYN_GROQ_WHISPER_MICRO_USD_PER_AUDIO_MINUTEplusLARYN_GROQ_MIN_BILLABLE_AUDIO_MS. - Pre-AI usage controls are configured with
LARYN_MONTHLY_USAGE_CAP_UNITS,LARYN_RATE_LIMIT_MAX_REQUESTS,LARYN_RATE_LIMIT_WINDOW_SECONDS, andLARYN_MAX_CONCURRENT_TRANSCRIPTIONS_PER_USER.
For the lowest-cost setup, keep cleanup set to off. That uses only the speech-to-text call and skips the second text-generation cleanup call. Switch to cheap only when you want punctuation/capitalization cleanup.
Deploy with:
pnpm worker:deployValidate the Worker bundle without deploying:
pnpm worker:dry-runGitHub Actions are split by purpose:
CI: runs on push and PR, then typechecks, lints, and runswrangler deploy --dry-run.Worker Deploy: manual production Worker deploy with the checked-in Wrangler CLI.Desktop Release: manual Windows installer and R2 auto-update publication.
Cloudflare Git-connected Worker build settings:
- Root directory:
/ - Build command:
pnpm --filter @laryn/worker build - Deploy command:
pnpm --filter @laryn/worker run deploy - Version command:
pnpm --filter @laryn/worker run versions:upload
Keep the root directory at / so pnpm can resolve the workspace package @laryn/shared. The filtered pnpm commands execute inside apps/worker, where wrangler.jsonc lives.
Desktop builds are published manually from .github/workflows/desktop-release.yml. Open the workflow in GitHub Actions, choose Run workflow, and enter the semver version to release.
The workflow expects these GitHub repository variables:
LARYN_R2_BUCKET: the R2 bucket used for auto-update assetsLARYN_UPDATE_BASE_URL: the public base URL for that bucketCLOUDFLARE_ACCOUNT_ID: the Cloudflare account id that owns the bucket
It also needs R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY repository secrets with R2 object write access. Each manual run builds the Windows NSIS installer and the macOS Apple Silicon DMG/ZIP, generates a friendly two-word update name, uploads the desktop assets to the GitHub Release, verifies latest.yml and latest-mac.yml, and publishes the update feeds plus .exe, .dmg, .zip, and .blockmap files to R2.
The hosted download page redirects through these update feeds:
- Windows:
/downloads/laryn-windows-latest.exe - macOS:
/downloads/laryn-mac-latest.dmg
The macOS build is currently ad-hoc signed and not notarized. It is useful for early testing, but macOS users may need to approve the app in Privacy & Security on first launch.
Installed desktop builds check the R2 update feed after startup, download updates in the background, and install the downloaded update the next time Laryn restarts. When an update is ready, Settings shows an install button that restarts Laryn and applies it. The app version and generated update name are visible in Settings for debug purposes.
The requested binding is Ctrl + Win. Windows and Electron can be inconsistent with modifier-only global shortcuts, so the app tries Control+Super first and falls back to Control+Super+Space. The active binding is shown in the app.
Laryn is licensed under the Apache License, Version 2.0. See LICENSE.
The source license does not grant permission to use Laryn names, logos, domains, hosted services, production infrastructure, API credentials, signing keys, or private configuration. See NOTICE and TRADEMARKS.md.
Keep generated secrets in ignored files only, especially apps/worker/.dev.vars and apps/desktop/.env.
Run the redacted secret scanner before publishing or merging release changes:
pnpm security:scan -- --history
pnpm security:scan -- --include-ignoredThe ignored-file scan will fail when local secret files contain real values. That is expected; rotate any value that was ever committed and do not paste scanner output publicly.