# 0017 · Recordings become raw transcripts, through a transcription service

**Status:** Accepted · 2026-10-19. Builds on [0010](https://codna.me/docs/adr/0010-mcp-first-no-llm-inside.md) (no language model
inside), [0016](https://codna.me/docs/adr/0016-imports-into-raw.md) (a raw page gains a version only from its source) and
[0015](https://codna.me/docs/adr/0015-page-owned-files.md).

## Context

A great deal of what a company knows is said in meetings and never written down. A recorded online
meeting, or an in-person one recorded on someone's phone, is evidence like any document: it belongs
in raw, and an agent distils it into wiki. CoDNA had no way to take audio or video.

Three things made this more than another converter:

1. **Speech-to-text is a model.** The good engines for Georgian (Gemini, ElevenLabs, the user's own
   fine-tuned Whisper) are hosted services or need a GPU. The core is one static binary that calls no
   language model ([0010](https://codna.me/docs/adr/0010-mcp-first-no-llm-inside.md)).
2. **Recordings are big and slow.** An hour of video is a gigabyte or two; transcribing it takes
   minutes to an hour. The public load balancer takes 64 MB a request. The outbox's two-minute lease
   is no place to wait for a transcript.
3. **What happens to a recording is not the uploader's decision alone.** The user decided that staff
   choose, per hosted instance, whether recordings may be kept to fine-tune the Georgian model. The
   people recorded are not the ones who chose.

## Decision

**The core sends recordings to a transcription service and writes what comes back.** The service is
named by `CODNA_TRANSCRIBE_URL` and follows a small, versioned contract
([transcription.md](https://codna.me/docs/transcription.md)): read the policy, submit a recording with an idempotency
key, ask how the job stands, read the result, let go of it. Speech-to-text is conversion, as reading a
PDF's text is; the core calls no model, and knows nothing of engines, providers or training. It shows
what the service reports. The hosted service (Scribe) is private and runs beside the console; a
self-hoster can put their own Whisper behind the same contract, or set nothing and keep recordings
untranscribed.

**A recording is the original of a raw page**, kept byte for byte as every upload is. The page is
filed as soon as the recording is whole, saying it is being transcribed. The transcript arrives as the
page's next version: a paragraph per turn, opening with the speaker and the time, and the language,
length, engine, speakers and whether a copy was kept in its frontmatter. This is the same rule
[0016](https://codna.me/docs/adr/0016-imports-into-raw.md) set: raw gains a version only from its source, and the transcript is
the source's own text arriving late, written by ingestion. A failure gets a version saying why, and
whoever made the recording can ask for it to be transcribed again.

**Recordings arrive in pieces.** A file is sent a few megabytes at a time, and a meeting recorded in
the browser a few seconds at a time, each piece numbered and taken in order, the same piece sent again
taken as already here. So any length of recording gets through a proxy that takes 64 MB, an upload cut
off part-way carries on from where it stopped, and the load balancer needed no change. In the browser,
each piece is written to IndexedDB before it is sent and dropped only once the server has it: a closed
tab or a dead phone loses at most the last few seconds. Pieces on the server are put together on disk,
never in memory. A browser recording nobody comes back to finish is finished from what arrived a day
later; an upload left part-sent is thrown away after a week.

**A worker takes recordings one step at a time**, under a row lock and a lease, as imports are taken
([0016](https://codna.me/docs/adr/0016-imports-into-raw.md)): put the pieces together, file the page, send the recording, ask
how it stands. Between steps nobody holds it, so a transcript that takes an hour keeps no worker
waiting, and a worker that dies mid-step leaves the step to the next. A service that is down is
waited out, backing off for about an hour; one that refuses the recording ends it.

**People are told before they are recorded.** While the service's policy says recordings are kept,
Record shows its notice and asks whoever records to confirm they told the room, and Upload shows it
beside audio and video. The transcript records whether that recording was kept.

## Consequences

- The core stays one binary with no audio code: no ffmpeg, no model, no provider keys. Splitting long
  recordings to an engine's limits, turning video into audio, and keeping training samples are the
  service's.
- A self-hosted CoDNA without a service still takes recordings: they are kept, playable from their
  page, exported with it, and transcribable later, once a service is configured, by asking again.
- The recording is served from its page to whoever can read the page, with ranges so a player can seek;
  sharing the page shares the recording. Export writes it beside the page, as it writes every original.
- Recordings are evidence in their own right, and large: an instance's storage grows by the size of
  every meeting recorded. Nothing here deletes an original, as nothing deletes any.
- The contract is the boundary. Changing it is a `/v2`; adding an optional field is not. The core and
  the hosted service agree a change before either ships it.

## Alternatives rejected

**Transcribing in the core,** with a Whisper build or a provider SDK linked in. It breaks
[0010](https://codna.me/docs/adr/0010-mcp-first-no-llm-inside.md) and the single binary, puts provider keys and training
decisions in every self-hoster's configuration, and ties the core to whichever engine is best this
year.

**Leaving transcription to agents,** as with distilling raw into wiki. An agent cannot hear: it would
need the audio in a model that can, which is the same service with a worse interface, and a recording
nobody asked an agent about would never become text anyone could search.

**One large upload, with the load balancer raised to take it.** Simpler for a file, no use for a phone:
a recording made on a phone must survive the phone, which means sending it as it is made. Pieces serve
both, and leave the proxy as it was.

**Writing the page only when the transcript arrives.** The recording would be invisible for as long as
transcribing takes, and a failed one would have nowhere to say so. Filing the page at once gives the
recording an address from the start, to share, play and link to.
