Skip to content

Recording and transcription services

  • recorder in the base compose file
  • draft-asr in the base compose file
  • egress opt-in, the only part that saves media files
  • diarizer optional, external, not in compose

Two layers decide whether anything is captured. Services (this page) are the machinery you run. Org policy is permission. A service that is running does nothing for an org whose policy is off, and an org that is allowed gets nothing if the service behind that feature is missing.

A hidden, subscribe-only participant in the media server. It joins only rooms with an active recording, transcribes only participants who consented, and never publishes media. It is small: about 1% of a core per recorded track. It is in the base compose file.

  • SERVER_URL (or RECORDER_SERVER_URL) is http://server:3001; RECORDER_TOKEN authenticates it to the server. Without it, recordings capture nothing and the UI says so. It is never given to a speech host: draft-asr has its own key, DRAFTS_API_KEY (below), and the server, recorder and draft-asr refuse to start if the two are equal.
  • Transcription runs at a provider: TRANSCRIPTION_PROVIDER (deepinfra or openai-compatible), or an org’s own setting. With none configured, recording still works and transcripts say so.
  • DRAFTS_URL (default http://draft-asr:3003) sends audio for word-by-word caption drafts. An empty value turns drafts off; captions then arrive per sentence. A comma list names several speech hosts: a host that refuses a stream because it is full sits out a few seconds, one that is down sits out about 30 s (longer if it keeps failing), and the sentence moves to the next host. SPEECH_HOSTS_MODE=failover uses them in list order; the default picks the one with most room.
  • DRAFTS_API_KEY is the speech hosts’ key (draft-asr’s DRAFT_TOKEN gets the same value), at least 16 characters. gen-keys.mjs and first boot generate it.
  • DIARIZER_URL is optional (see below).

Advanced recorder settings. The defaults suit most servers; change one only for a measured reason.

Setting Default Meaning
RECORDER_POLL_MS 2000 How often the recorder asks the server which rooms to be in.
DRAFTS_MAX_SESSIONS 16 Live draft streams this recorder opens at once, across all rooms. Keep it at or below the speech hosts’ DRAFT_MAX_STREAMS.
RECORDER_MAX_IN_FLIGHT 3 Sentences one room may have waiting at the transcription provider at once.
RECORDER_HEDGE_MS 1200 Send a sentence again if a cloud provider has not answered in this long (0 = off). Never used for your own speech hosts, where a duplicate only doubles the load.
RECORDER_VAD_PAD_SEC 0.25 Audio kept either side of each stretch of speech sent for transcription.
RECORDER_CROSSTALK_GATE off 1 marks sentences that are mostly another person’s voice leaking into a microphone.
RECORDER_REFINE_RAM_MB / _PROCESS 64 / 512 Memory for audio held until the DeepInfra re-check, per recording and for the whole recorder.
RECORDER_HEALTH_PORT 3002 The recorder’s own /healthz.
VAD_MODEL_PATH in the image The speech-detection model; checked against a pinned checksum on every start.

Live caption drafts. A local Kroko Zipformer model through sherpa-onnx, many streams per process, internal network only, and per its compose comment it never stores audio or logs text. It is in the base compose file.

Setting Default Meaning
DRAFT_WORKERS 2 Worker processes
DRAFT_MAX_STREAMS 16 Concurrent speaker streams. Past it, streams are refused and those sentences fall back to per-sentence captions, so the media server on the same box keeps its CPU.
DRAFT_FINAL_MODEL parakeet-v2 Second-pass model for voice commands. off means drafts only.
DRAFT_FINAL_WORKERS / _THREADS / _QUEUE 1 / unset / 4 Tuning for the second pass.
DRAFT_TOKEN / DRAFT_TOKENS DRAFTS_API_KEY The keys draft-asr accepts. DRAFT_TOKENS is a comma list; an entry name=secret (secret at least 16 characters) makes name a tenant with its own stream cap and audio quota.
DRAFT_TENANT_MAX_STREAMS half of DRAFT_MAX_STREAMS with two or more tenants Streams one tenant may hold, so one tenant cannot fill the box.
DRAFT_METRICS_TOKEN unset Optional operator key: /healthz with it lists every tenant’s streams and refusals by name. It cannot open a stream and must differ from every DRAFT_TOKENS secret.
DRAFT_DRAIN_MS 20000 On shutdown, how long sentences already in flight may finish. The compose file gives the container 30 s to stop, so the drain is never cut short.

Advanced draft-asr settings:

Setting Default Meaning
DRAFT_HOST / DRAFT_PORT 0.0.0.0 / 3003 Where it listens inside its container. On a separate speech box, the compose file publishes the port on the WireGuard address only.
DRAFT_MODEL kroko Draft model; zipformer-2023 is the other choice.
DRAFT_MODELS_DIR in the image Folder holding the models. Every file is checked against a pinned checksum before a decoder starts.
DRAFT_MODEL_UNPINNED off 1 allows a model with no pinned checksum (zipformer-2023, a DRAFT_FINAL_MODEL_DIR export); logged at start.
DRAFT_FINAL_MODEL_DIR unset Load the second-pass model from another folder under DRAFT_MODELS_DIR.
DRAFT_IDLE_MS 10000 A stream that sends nothing for this long is closed and its slot freed.
DRAFT_MAX_STREAM_SEC 60 Longest one stream (one sentence) may stay open.
DRAFT_AUDIO_RATE_X 2 A stream sending audio faster than this multiple of real time is closed (0 = off).
DRAFT_TENANT_AUDIO_SEC_PER_MIN 120 x the tenant’s stream cap Audio seconds per minute one tenant may send (0 = off).
DRAFT_ALLOW_QUERY_TOKEN off 1 also accepts the key as ?api_key= in the URL. Off keeps keys out of proxy logs.
DRAFT_WORKER_PROCESS / DRAFT_FINAL_PROCESS off 1 runs each decoder as a child process instead of a thread, so a restart gives its memory back. A separate speech box turns both on.
DRAFT_RESTART_AUDIO_SEC / DRAFT_RESTART_RSS_MB 600 / 1024 Restart a draft decoder after this much audio or memory (0 = never).
DRAFT_CPU_BUDGET off CPU seconds per second the box may spend (auto = 80% of its cores). Past it, finals are answered with the draft text and new drafts are refused. A separate speech box sets 24.
DRAFT_COST_KROKO / _110M / _V2 0.073 / 0.038 / 0.095 Starting CPU cost per audio second of each model; the service corrects them from what it measures.
DRAFT_GATE_MODEL / DRAFT_GATE_WORKERS off / 1 Two-step second pass: a smaller model on every sentence, the large one only where it was unsure.
DRAFT_GATE_U_LO / DRAFT_GATE_U_HI 0.6 / 0.85 Below the low mark every sentence goes straight to the large model; across the band its share falls from 65% to 30% of sentences; above 90% load, none.
DRAFT_DRAFT_SHARE 0.6 Share of DRAFT_CPU_BUDGET live drafts may use before new ones are refused.

Resources, from the compose comments: about 7.5% of a Zen 1 core per active speaker for drafts. The second pass costs about 1.5 GB RAM per worker; about 4 finals in flight per 6 cores keeps p95 under 3 s. Past the queue limit or DRAFT_FINAL_TIMEOUT_MS, the draft text answers at once. To move the load, point DRAFTS_URL at another machine.

Without a credential, /healthz answers liveness only (status, healthy); a tenant key shows that tenant’s own figures, and DRAFT_METRICS_TOKEN shows every tenant.

An Org owner or Org admin can let live caption drafts use a TeamHUD-run speech host. It is off until they turn it on, and everyone in the call is told. The server and recorder need both settings, or shared hosts are never used:

  • DRAFTS_SHARED_URL: the shared hosts. Each must be https:///wss://, or http:///ws:// to a private-range address reached over WireGuard; anything else stops the server at start.
  • DRAFTS_SHARED_API_KEY: the key for those hosts only. The org’s own DRAFTS_API_KEY is never sent to them.

The Org owner or Org admin also chooses which goes first: the shared host (its own server as the backup) or the organization’s own server (the shared host as the backup). The call notice names the one in use, and also says where finished captions and the transcript are made: on this server, at DeepInfra (a third-party service in the United States), or at the organization’s own transcription service.

Per-organization limits on a shared host (tenant claims)

Section titled “Per-organization limits on a shared host (tenant claims)”

A server presents one DRAFTS_SHARED_API_KEY for all of its organizations, so on its own the shared host cannot tell them apart. A server that hosts many organizations signs a short-lived claim for each recording that uses the shared host, naming the organization by a pseudonym (never its name or id). The recorder sends it with each stream, to shared hosts only. The shared host checks it with the server’s public key, then applies a stream cap and an audio quota per organization, inside the server’s own.

A self-hosted server that uses a shared host needs none of this: its own key is its identity, and the shared host’s operator lists it as optional. Do not set a signing key unless the shared host’s operator asks for it: once the server sends claims, the shared host refuses any it cannot check.

Server settings (set together; unset means no claims):

  • DRAFTS_CLAIM_SIGNING_KEY: a 32-byte Ed25519 seed in base64 (openssl rand -base64 32). Never leaves the server. At start the server logs the line to give the shared host’s operator.
  • DRAFTS_CLAIM_SUB_KEY: the key for the organization pseudonym, at least 16 characters and not the signing key (openssl rand -hex 32). Changing it gives every organization a new pseudonym.
  • DRAFTS_CLAIM_ISSUER (default teamhud) and DRAFTS_CLAIM_AUDIENCE (default teamhud-cloud-asr, the shared host’s DRAFT_POOL_ID).

Shared host settings:

Setting Default Meaning
DRAFT_CLAIM_KEYS unset (off) <tenant>:<issuer>=<public key>, comma list. A key counts only for the DRAFT_TOKENS name it is listed under; two keys for one name is a rotation. Once set, every tenant needs a claim unless listed in DRAFT_CLAIM_OPTIONAL, and a claim that does not check out is always refused.
DRAFT_CLAIM_OPTIONAL unset Tenants that may stream without a claim (self-hosted servers).
DRAFT_CLAIM_MODE enforce log while rolling out: a missing claim is counted on the operator /healthz, not refused. Switch back once claim_missing stays at 0.
DRAFT_POOL_ID teamhud-cloud-asr The audience a claim must name.
DRAFT_ORG_MAX_STREAMS half the tenant’s Streams one organization may hold inside its server’s tenant.
DRAFT_ORG_AUDIO_SEC_PER_MIN 120 x DRAFT_ORG_MAX_STREAMS Audio seconds per minute per organization.

A refusal for one organization (no claim, a bad claim, its stream cap or quota) only costs that organization’s sentence its live drafts; its finished captions still arrive, and the host stays in use for everyone else.

Opt-in, and the only part that writes media files. LiveKit Track Egress in passthrough: each consented participant’s track is written as it was published (Opus to .ogg, VP8 to .webm, H.264 to .mp4), with no transcode and no headless Chrome. The server starts and stops it per track, and retention deletes the files.

Enable with three settings together in .env:

Terminal window
COMPOSE_PROFILES=egress
EGRESS_OUTPUT_DIR=/recordings
LIVEKIT_REDIS_ADDRESS=redis:6379

This adds redis (the media server hands egress jobs over it; no persistence), a one-shot recording-media-perms step that makes the recording-media volume writable, and egress (image livekit/egress:v1.10.0 unless EGRESS_IMAGE is set). The server and egress share recording-media at /recordings. Without these, transcription still works but no media is saved.

Files are deleted after the org’s media retention, 30 days by default. Allowing audio or video capture also needs the recording feature on the org’s plan. backup.sh tars the recording-media volume beside each database dump, but the copies share the box’s disk, so machine-level backups still matter.

There is no diarizer service in the compose file. DIARIZER_URL points at one you run yourself: a NeMo-Speech.cpp serve with Sortformer, used to split meeting-room microphones into speakers after the meeting. The compose comment puts it at about 3 Zen 1 cores while running and says to run it on a separate box. Unset means one speaker per microphone.

  1. Confirm RECORDER_TOKEN is set (firstboot.sh generates it).
  2. Choose a transcription provider: TRANSCRIPTION_PROVIDER for the deployment, or each org’s own.
  3. Optionally enable egress, and size the box (Sizing).
  4. Have an Org owner or Org admin allow recording and the specific capture modes under Recording policy.
  5. Everyone in the call is told, and the recording and caption indicators are visible while on. Guests, and anyone who has not accepted the terms, are asked to consent.

Other variables are in Configuration.