Docs Decisions
0017 · Recordings become raw transcripts, through a transcription service
Status: Accepted · 2026-10-19. Builds on 0010 (no language model inside), 0016 (a raw page gains a version only from its source) and 0015.
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:
- 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).
- 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.
- 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): 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 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): 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 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.