CoDNA Request access
Contents

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 as https://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 when retained is 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-Type is what CoDNA sniffed from the bytes, not what the file was called.
  • filename is 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 answers 413 with the limit in error.
  • Idempotency-Key is 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 t1 to tN in order, with a filename and the track's Content-Type.
  • speaker is the name the track's words carry in the transcript. Several tracks may share one: someone who left the call and came back.
  • offset is 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: 202 with 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, done or failed.
  • progress: optional, 0 to 1, shown to the person waiting.
  • error: when failed, why, in words for the person who uploaded the recording ("The recording has no sound in it."). Not a stack trace.
  • result: when done, 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 in segments, 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 of start. Times are seconds from the start of the recording. speaker is one of speakers, or empty. text is 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 other 4xx: the service will not take this recording. CoDNA shows error to the uploader and does not try again by itself.
  • 429, 503, other 5xx, 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. A Retry-After longer 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:

  1. Check the bearer token.
  2. POST /v1/jobs: store the body, answer an id, and transcribe in the background.
  3. GET /v1/jobs/{id}: say how it stands, with the result once there is one.
  4. 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.