Docs How it works
Transcription
CoDNA turns a recording into a transcript in raw. It does not transcribe anything itself.
It hands the recording to a transcription service at CODNA_TRANSCRIBE_URL and writes
the page from what the service answers. Speech-to-text is conversion, like reading a PDF's
text: the core calls no language model and knows nothing about engines or what a service
keeps (adr/0010, adr/0017).
This page is the contract between the two. It is small on purpose, so that anyone can put their own server behind it: a Whisper on a GPU box, a wrapper around a cloud API. The hosted service at codna.me is one implementation of it.
Settings
CODNA_TRANSCRIBE_URL: the service's base URL, such ashttps://scribe.example.com. CoDNA appends the paths below to it. Unset, recordings are kept and their pages say they were not transcribed.CODNA_TRANSCRIBE_TOKEN: the bearer token CoDNA sends. One per instance: the service knows which instance is calling by its token.
Version 1
Every path starts with /v1. Every request carries Authorization: Bearer <token>.
Within v1, either side may add optional fields, and both ignore fields they do not know.
Anything else (a field removed or renamed, a meaning changed, a new required field) is a
/v2, and a service keeps answering /v1 until the cores calling it have moved.
Read the policy
GET /v1/policy
{
"retained": true,
"notice": "Recordings on this workspace are kept to improve speech recognition for Georgian."
}
retained: whether the service keeps a copy of recordings it is sent, for any purpose beyond answering the job.notice: words for the people being recorded. Required whenretainedis true; may be empty when it is false.tracks: optional; true when the service takes a meeting as several tracks, one per person (below). Absent or false, CoDNA sends a meeting's mix as an ordinary recording.
CoDNA reads this before someone records or uploads, and shows the notice beside Record and Upload while it is not empty. It caches the answer for a minute. A service that cannot say should answer an error rather than guess: CoDNA then tells people it could not check.
Submit a recording
POST /v1/jobs?filename=board-meeting.mp4
Content-Type: video/mp4
Content-Length: 1834127360
Idempotency-Key: 0192a4c6-6f1e-7c3a-9e55-3c1d2f0a9b11
<the recording's bytes>
- The body is the recording exactly as CoDNA holds it: audio or video, in whatever container it arrived in (WebM, Ogg, MP4, M4A, MP3, WAV, MOV, Matroska…). Turning video into audio, and splitting long recordings to an engine's limits, is the service's job.
Content-Typeis what CoDNA sniffed from the bytes, not what the file was called.filenameis the name it was uploaded under, for the service's logs. It may be absent.- The body can be large: an hour of video is a gigabyte or two, and CoDNA takes recordings up
to
CODNA_MAX_RECORDING(4 GiB unless set). It is streamed, with its length given. A service that takes less answers413with the limit inerror. Idempotency-Keyis CoDNA's id for this attempt. CoDNA can stop after sending and before hearing the answer (a restart, a lost connection), and then sends the same recording again with the same key. The service answers that with the job it already has, rather than transcribing, and charging for, the same recording twice. A retry a person asks for is a new attempt with a new key.
202 Accepted
{ "id": "j_7HxK2", "status": "queued" }
The job id is the service's, opaque to CoDNA, at most 200 characters.
Send a meeting as tracks
A meeting is recorded with each person on their own track, so that its transcript can
name real people instead of guessing them apart. A service whose policy says "tracks": true takes
those tracks as one job, on the same path, as a multipart form:
POST /v1/jobs?filename=weekly-sync.ogg
Content-Type: multipart/form-data; boundary=…
Content-Length: 14358167
Idempotency-Key: 0192a4c6-6f1e-7c3a-9e55-3c1d2f0a9b11
part "tracks" (application/json), first:
[{"part": "t1", "speaker": "Nino Beridze", "offset": 0.0},
{"part": "t2", "speaker": "Levan (guest)", "offset": 63.4}]
part "t1" (filename "weekly-sync-t1.ogg", Content-Type audio/ogg): the track's bytes
part "t2" (filename "weekly-sync-t2.ogg", Content-Type audio/ogg): …
- The list comes first, then each track as its own part, named
t1totNin order, with afilenameand the track'sContent-Type. speakeris the name the track's words carry in the transcript. Several tracks may share one: someone who left the call and came back.offsetis where the track starts, in seconds on the meeting's clock.- Up to 50 tracks. The form's length is given and its bytes are streamed from disk, so a service
should read it part by part rather than hold it in memory. Their bytes together count against the
service's size limit (
413). - Answers, idempotency and errors are as for one recording:
202with the job.
The result is as for one recording, except that speakers are the names given, in the order each
first speaks (one who never speaks is left out), every segment's speaker is one of them, and
duration is the meeting's: the latest track's offset plus its length.
Ask how a job stands
GET /v1/jobs/{id}
{ "id": "j_7HxK2", "status": "running", "progress": 0.4 }
status:queued,running,doneorfailed.progress: optional, 0 to 1, shown to the person waiting.error: whenfailed, why, in words for the person who uploaded the recording ("The recording has no sound in it."). Not a stack trace.result: whendone, the transcript (below).
There is no callback. CoDNA asks every few seconds at first and every half minute after that, so the service never has to reach the instance, which may well sit behind a firewall.
The result
{
"id": "j_7HxK2",
"status": "done",
"result": {
"language": "ka",
"duration": 3754.2,
"engine": "gemini-3.5-transcribe",
"speakers": ["Speaker 1", "Speaker 2"],
"segments": [
{ "start": 0.0, "end": 4.8, "speaker": "Speaker 1", "text": "Let's start with the budget." },
{ "start": 5.1, "end": 12.6, "speaker": "Speaker 2", "text": "I sent the numbers on Friday." }
],
"retained": true,
"notice": "A copy of this recording was kept to improve speech recognition for Georgian."
}
}
language: the language spoken, as a BCP 47 tag (ka,en,en-GB). For a meeting in two languages, the main one.duration: the recording's length in seconds.engine: what transcribed it, as the service names it. CoDNA shows it; it does not interpret it.speakers: every label used insegments, in the order each first speaks. Empty when the engine does not tell speakers apart. Labels are the engine's (Speaker 1); naming people is for whoever reads the transcript.segments: in order ofstart. Times are seconds from the start of the recording.speakeris one ofspeakers, or empty.textis what was said, as plain text.retained: whether a copy of this recording was kept. It can differ from the policy read earlier, when staff changed it in between; this answer is the one the page records.notice: optional words to show with the transcript.
Let go of a job
DELETE /v1/jobs/{id}
CoDNA calls this once it has written the transcript, or has given up on the job. The
service may then drop the recording and the result, unless it is keeping them under its
policy. 204, or 404 for a job it no longer has. A service that keeps nothing beyond its
jobs' lifetimes can answer 204 and do nothing.
Errors
An error is a status and, where there is something to say, a body:
{ "error": "This recording is longer than 6 hours." }
401: the token is wrong. CoDNA stops and says the service refused this instance.413,415, and other4xx: the service will not take this recording. CoDNA showserrorto the uploader and does not try again by itself.429,503, other5xx, and a connection that fails: try again later. CoDNA backs off, from ten seconds doubling to ten minutes, and gives up after twelve such answers in a row: about an hour. ARetry-Afterlonger than its own wait is honoured, up to an hour a time, so a service that says "an hour" is asked again hourly for twelve hours.
A job the service lost (404 from GET /v1/jobs/{id}) is submitted again, under the same
idempotency key.
What CoDNA does with it
A recording is uploaded whole or in pieces, recorded in the browser, or held as a meeting, and kept byte for byte as the original of a raw page. That page appears at once, saying the recording is being transcribed. When the service answers, the transcript becomes the page's next version:
---
title: Board meeting
original_filename: board-meeting.mp4
mime_type: video/mp4
source: upload
transcription: done
length: "1:02:34"
length_seconds: 3754
language: ka
engine: gemini-3.5-transcribe
speakers: [Speaker 1, Speaker 2]
kept_for_training: true
---
**Speaker 1** (0:00) Let's start with the budget. It is over by a tenth.
**Speaker 2** (0:05) I sent the numbers on Friday.
Each turn is a paragraph that opens with who spoke and when. Consecutive segments by one speaker join, until two minutes have passed. A job that fails gives the page a version saying why, and whoever uploaded the recording can ask for it to be transcribed again. See Ingestion.
Writing a service
The least a service has to do:
- Check the bearer token.
POST /v1/jobs: store the body, answer an id, and transcribe in the background.GET /v1/jobs/{id}: say how it stands, with the result once there is one.GET /v1/policy:{"retained": false, "notice": ""}, if it keeps nothing.
DELETE may do nothing, and idempotency keys may be ignored by a service that does not mind
the odd recording transcribed twice. A Whisper server that runs one job at a time and keeps
everything in a directory is a page of code.