A small web app for the Gradium × Active Builders challenge. You record your voice once, in English. Gradium turns that recording into French and German versions of your voice, which Levelang then uses to read its translations aloud.
Live at voice.levelang.app; the passcode is in the submission.
This app is throwaway. It exists for the judging and comes down
afterwards. The product it demonstrates is Levelang, whose backend is
private. The Gradium code that backend runs is copied into
provider/ so you can read it here.
- Open the page and enter the passcode from the submission.
- Read the consent notes, then read a short passage aloud (10–60 s).
- Listen to the candidates (one per accent Gradium offers, each with a preview line) and pick one voice per language.
- Play a test phrase in your voice or a stock voice, with each word lit as it is spoken, and choose the playback speed.
- Part 2: sign in at levelang.app with the
judging account (its details are in the submission). On
/voice, hear your voice read one translation at every level. On/translate, play any translation, and switch between your voice and a stock one. On/chat, ask for translations and play each quoted one. French and German play in your voice; other languages use the browser's own speech.
Everyone shares one judging account. Recording replaces the voices already on it.
browser ──/api──▶ this relay ──Bearer JWT──▶ Levelang backend ──▶ Gradium (clone, localize, TTS)
(page + passcode (Hono, Node 24) (FastAPI, hosted └──▶ Supabase (voices, cache,
cookie only) staging) grants, limits)
then: levelang.app ──signs in as the same account──▶ Levelang backend ──▶ the same voices
- The page (
src/, Vite + React) records the microphone with an AudioWorklet, downsamples to 24 kHz mono and encodes a 16-bit WAV in the browser, because the backend accepts WAV only and a minute at 24 kHz stays under its 4 MB cap (src/lib/recorder.ts,src/lib/wav.ts). It plays speech through one<audio>element withpreservesPitch, and highlights words from the player'scurrentTime(src/lib/playback.ts,src/lib/wordAlign.ts). - The relay (
server/, Hono on Node) serves the built page and passes/api/*through to the backend's/api/v1/*. It holds the judging account's Supabase session: it signs in with the account's password, refreshes the session before it expires, and calls the backend server to server with that JWT. The browser never sees the JWT or the credentials, and the backend's CORS list doesn't change. - The Levelang backend owns the voices. It checks the account's voice
grant, calls Gradium through
provider/, stores one active voice per language per account, caches audio, and meters use.
Each is a thin pass-through. Status codes, Retry-After and the backend's
{"detail": {"error_code", "message"}} error bodies come back unchanged.
| Relay route | Backend route (/api/v1) |
What it does |
|---|---|---|
GET /api/voices/me |
GET /voices/me |
Grant, voices, limits and playback settings. |
POST /api/voices |
POST /voices |
Upload the WAV (≤ 4 MB), get candidates + previews. |
POST /api/voices/me |
POST /voices/me |
Keep the picked candidates, one per language. |
DELETE /api/voices/me/{id} |
DELETE /voices/me/{id} |
Remove one voice. |
DELETE /api/voices/me |
DELETE /voices/me |
Remove all voices. |
PUT /api/voices/me/settings |
PUT /voices/me/settings |
Save the playback speed (1, 0.85, 0.75, 0.65). |
POST /api/speak |
POST /speak |
A phrase as WAV plus one timing per word. |
The relay rebuilds a recording upload from its known fields only, and refuses one over 4 MB before it leaves the server. JSON bodies pass through as sent, and the backend validates them. It logs the method, path, status and duration only: never audio, bodies, cookies or tokens.
The judging account has an unlimited voice grant, so the passcode is what protects the Gradium credits.
POST /api/sessioncompares the passcode withPASSCODEin constant time (both sides hashed with SHA-256, thentimingSafeEqual).- A right passcode sets a cookie:
HttpOnly,SameSite=Strict,Secureover https, valid 7 days. Its value is an expiry time and an HMAC-SHA256 of it, keyed bySESSION_SECRETand the passcode, so changingPASSCODEsigns everyone out. - Every
/api/voices*and/api/speakcall needs that cookie. Without it the relay answers401 passcode_requiredand never calls the backend. - Per-IP rate limits, in memory: 10 passcode tries per 15 minutes, 5
recordings per hour, 60
speakcalls per 10 minutes. Over the limit is429 rate_limitedwithRetry-After. - Unsafe requests must come from the page itself (
Sec-Fetch-SiteorOrigin), and the page sends a strict CSP andPermissions-Policy: microphone=(self).
The code is in server/gate.ts,
server/rateLimit.ts and
server/app.ts.
Every Gradium call is in provider/gradium.py, over
REST with the account's x-api-key. In the order a judge's recording meets
them:
- Clone the recording:
POST /voices/with the WAV (create_clone). The reply can come before Gradium finishes processing, with the voice id set and a "please wait" note; the voice exists, so the next step retries until it's ready. - List the accents for each target language:
GET /voice-generator/available-accents(list_accents), cached for an hour. - Localize the clone into each language and accent:
POST /voice-generator/localize(localize), retried while a fresh clone still answers 404 or 409, then polled withGET /voice-generator/embeddings?embedding_id=…until the candidate is ready (_wait_ready). A candidate that never gets ready is deleted at once. - Speak, with word timestamps:
POST /post/speech/ttswithonly_audio: false, which streams newline-delimited JSON:audiochunks (base64 PCM, 24 kHz) andtextmessages with each segment's start and stop times (synthesize,_synthesize_once). The candidate previews and everyspeakcall use it. Gradium merges the opening words into one segment, so the backend spreads that segment across its words by length (spread_words). - Delete:
DELETE /voices/{id}for a clone,DELETE /voice-generator/embeddings/{id}for a localized voice (delete_voice). An id that is already gone is not an error. - Credits:
GET /usages/credits(remaining_credits), cached for a minute.
How the backend uses them:
- The natural clone is never kept. It exists only while one recording's localized versions are made. Its deletion is queued the moment it exists and run as soon as localization ends, whatever the outcome. Localized voices keep speaking after their source clone is gone. Levelang doesn't store the recording either.
- Voices belong to the account, not to a device or a page: one active voice per language, chosen from the candidates, so the app and levelang.app play the same voice this page made. Unpicked candidates are deleted, and picking a new voice for a language deletes the old one.
- Deletions go through a queue. Every provider deletion is written to a queue before Levelang's own rows go, tried at once, and retried with backoff until Gradium confirms it; repeated failures alert. A crash midway leaves a queue entry, never an untracked voice at Gradium.
- Speed is applied in the player. Gradium synthesizes at natural speed,
because its own pacing setting barely changes short phrases. The page
slows playback with
preservesPitchandplaybackRate(src/lib/playback.ts) at the account's saved speed, so every surface plays at the same speed. - Credits are guarded. Below a credit floor, new Gradium work answers 503. Audio is cached, so a repeated phrase costs nothing. Simultaneous TTS
sessions are held within the plan's account-wide cap across machines
(
provider/slots.py).
PRIVATE_CHANGES.md lists every commit made for this
entry in Levelang's private backend, website and app repositories, with the
files each one touched.
Needs Node 24 or later. To run the provider tests, you also need uv.
npm install
cp .env.example .env.local # then fill it in, see below
npm run build
npm start # http://localhost:8080.env.local (gitignored) holds PASSCODE, DEMO_EMAIL, DEMO_PASSWORD,
SUPABASE_URL and SUPABASE_ANON_KEY (the judging account on the Levelang
Supabase project), plus an optional SESSION_SECRET and BACKEND_URL
(default: Levelang's hosted staging).
For live reload, run the relay and the Vite dev server side by side:
npm run dev:server # relay on :8080npm run dev # page on :5173, /api proxied to :8080Checks:
npm run typechecknpm run test:unit # WAV, word alignment, gate and relayuv run --project provider pytest provider/tests # the Gradium layer, mocked, no keyIt runs as the Fly app levelang-voice-demo in cdg, served at
voice.levelang.app. Keep it to one machine: the rate limits and the
account's session live in memory. To set it up from scratch:
fly apps create levelang-voice-demofly secrets set --app levelang-voice-demo PASSCODE=… SESSION_SECRET="$(openssl rand -hex 32)" DEMO_EMAIL=… DEMO_PASSWORD=… SUPABASE_URL=… SUPABASE_ANON_KEY=…fly deploy --ha=falseThe relay refuses to start on Fly without SESSION_SECRET. For the custom
domain, add a certificate, then a CNAME from voice to
levelang-voice-demo.fly.dev at the DNS host. On Cloudflare, set the record
to DNS only: proxied, it answers with Cloudflare's addresses instead of
Fly's, and Fly won't issue the certificate.
fly certs add voice.levelang.app --app levelang-voice-demoTo take it down after judging:
fly apps destroy levelang-voice-demoCopyright © 2026 Alex Beverage. All rights reserved. The code is published
for the challenge's judges to read, and no license to reuse it is granted.
See LICENSE.