Skip to content

About

Gradium x Active Builders challenge, Oct 2026 - judging app: record your voice once, then hear Levelang's translations in French and German versions of it - 🥇 1st Place

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

levelang-voice

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.

What a judge does

  1. Open the page and enter the passcode from the submission.
  2. Read the consent notes, then read a short passage aloud (10–60 s).
  3. Listen to the candidates (one per accent Gradium offers, each with a preview line) and pick one voice per language.
  4. Play a test phrase in your voice or a stock voice, with each word lit as it is spoken, and choose the playback speed.
  5. 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.

How it fits together

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 with preservesPitch, and highlights words from the player's currentTime (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.

The relay's routes

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 passcode

The judging account has an unlimited voice grant, so the passcode is what protects the Gradium credits.

  • POST /api/session compares the passcode with PASSCODE in constant time (both sides hashed with SHA-256, then timingSafeEqual).
  • A right passcode sets a cookie: HttpOnly, SameSite=Strict, Secure over https, valid 7 days. Its value is an expiry time and an HMAC-SHA256 of it, keyed by SESSION_SECRET and the passcode, so changing PASSCODE signs everyone out.
  • Every /api/voices* and /api/speak call needs that cookie. Without it the relay answers 401 passcode_required and never calls the backend.
  • Per-IP rate limits, in memory: 10 passcode tries per 15 minutes, 5 recordings per hour, 60 speak calls per 10 minutes. Over the limit is 429 rate_limited with Retry-After.
  • Unsafe requests must come from the page itself (Sec-Fetch-Site or Origin), and the page sends a strict CSP and Permissions-Policy: microphone=(self).

The code is in server/gate.ts, server/rateLimit.ts and server/app.ts.

Where Gradium is used

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:

  1. 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.
  2. List the accents for each target language: GET /voice-generator/available-accents (list_accents), cached for an hour.
  3. 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 with GET /voice-generator/embeddings?embedding_id=… until the candidate is ready (_wait_ready). A candidate that never gets ready is deleted at once.
  4. Speak, with word timestamps: POST /post/speech/tts with only_audio: false, which streams newline-delimited JSON: audio chunks (base64 PCM, 24 kHz) and text messages with each segment's start and stop times (synthesize, _synthesize_once). The candidate previews and every speak call use it. Gradium merges the opening words into one segment, so the backend spreads that segment across its words by length (spread_words).
  5. 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.
  6. 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 preservesPitch and playbackRate (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).

Changes in Levelang's private repositories

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.

Run it locally

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 :8080
npm run dev          # page on :5173, /api proxied to :8080

Checks:

npm run typecheck
npm run test:unit                                  # WAV, word alignment, gate and relay
uv run --project provider pytest provider/tests    # the Gradium layer, mocked, no key

Deploy (Fly.io)

It 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-demo
fly 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=false

The 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-demo

To take it down after judging:

fly apps destroy levelang-voice-demo

License

Copyright © 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.

About

Gradium x Active Builders challenge, Oct 2026 - judging app: record your voice once, then hear Levelang's translations in French and German versions of it - 🥇 1st Place

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages