Get started

Engineering NotesVoice and meetings

Choose meeting speech recognition by speaker turns and retention

Choose a meeting speech recognition service from a single error-rate table and two things go wrong. One of the services we compared and did not adopt led on character error rate alone, yet when it was sent a two-person meeting in five-minute windows, it merged the two people into one in 12 of the 22 windows. On screen, a question and its answer arrive glued into a single speech bubble (the box that groups what one person said in a row). A managed service can also keep the transcript on its side. AssemblyAI's public documentation says that unless the account is configured otherwise, transcripts stay for 30 days.

OwlAI transcribes meetings along two paths. The on-premises default is MOSS, a self-hosted model running on a GPU inside the customer network; OwlAI SaaS, which has no GPU, uses AssemblyAI, a managed service. This post walks through the criteria behind that choice in order: first the shape of the result, then quality measured under identical conditions, then the window setting chosen for latency and cost, and last, where audio goes and how long it stays. It also covers how the product deletes what remains on the managed service.

Speaker separation has to come in the same response as the transcript

A meeting is transcribed one window (the stretch of audio transcribed at once) at a time on a fixed cycle. The on-premises default is a 90-second window every 60 seconds, overlapping the previous one by 30 seconds (“Overlapping meetings stall transcripts, so split queues by resource”). Splitting speech bubbles means knowing where the voice changes inside a window, so the recognition result has to carry a timestamp and a speaker label for each utterance. Calling transcription and diarization separately adds a step that lines up the two results' timestamps.

Speaker labels do not point to people. The model numbers its labels afresh in every window, so "speaker 1" in one window and "speaker 1" in the next may be different people. Labels are therefore compared only within a window. Whether the first newly committed utterance starts a new bubble is decided by whether its label matches the utterance just before it in the same window's overlap. What gets stored is one marker per utterance meaning "a different voice starts here." The same function builds both the bubbles on screen and the transcript fed to the minutes model from that marker.

This shrinks what the service has to provide. There is no need for it to give the same person the same label across the whole meeting; telling voices apart within one request is enough. That separation does have to arrive in the same response as the transcript.

Configuration checks this at startup. Meeting transcription (meetingStt) accepts only self-hosted models that return speakers and timestamps per utterance (transcriptFormat: moss) and AssemblyAI; any other combination is rejected at startup. Microphone input, where a short clip comes back as one line of text, has different requirements and is chosen separately in the stt section. Meetings have no language setting. Meetings mix languages, and fixing one makes the model read every utterance as that language, mangling the others.

Accuracy is measured with the product's windows and judged on meaning

Managed services were evaluated with audio cut into the same windows as the product and joined with the same rule. The self-hosted model's figures come from the same conditions. If one side is stitched windows and the other is a whole meeting fed in one pass, utterances lost at window seams count against only one side, and the conclusion flips.

Scoring went by meaning rather than exact characters. Korean meetings have many spots that are ambiguous character by character even to a human listener, so engines often agree on meaning while their character error rates differ by 14 to 27%. Transcripts were therefore scored 0 to 4 per block of about 30 seconds on how well the meaning survived, weighted by duration, and speakers were scored on whether they matched human-corrected bubble boundaries within ±2 seconds. The only internal meeting recordings used were two cleared for sending outside; the rest came from AMI, a public meeting speech corpus, and a Korean read-speech corpus.

Once speaker boundaries were scored, the ranking by character error rate changed. Services that produced any of the following were not adopted:

  • Merging two people into one in a two-person meeting (12 of the 22 five-minute windows above)
  • Splitting a four-person meeting recording into 12 to 19 speakers
  • Returning a contentless success response on degraded audio instead of an error (no retry runs, and the window is saved as silence)
  • Transcribing into another language when Korean was specified

Quality is equal across window settings, so latency and cost decide

Longer windows might seem to help quality, but from a 60-second cycle up there was no difference. On AMI, the self-hosted model's word error rate (WER) was 23.8% with a 60-second cycle and 90-second window and 23.5% with a 300-second cycle and 330-second window, a difference that was not significant (p=0.72). Feeding the whole meeting at once gave 22.8% (measured September 2026). Shortening the cycle goes the other way: 30 seconds raised WER by 6 points and 15 seconds by 17 points, and giving the model a longer window for more context did not recover it, because short pieces cut the model off before it finishes a sentence. So 60 seconds is the floor for the cycle, and configuration validation enforces at least 30 seconds of overlap between window and cycle. With a 6-second overlap, 33 of the 106 utterances handed to the next window lost their beginning; with 30 seconds, one did.

With quality equal, the two settings come down to latency and cost.

Setting (cycle/window) Until bubbles appear on screen Audio sent (vs. recording length) GPU time (per meeting hour) Used by
60/90 usually 1–2 minutes 1.48× 86 seconds on-premises (self-hosted)
300/330 usually up to about five and a half minutes 1.07× 79 seconds OwlAI SaaS (managed)

A managed service that bills by audio length turns that multiple straight into the bill, so OwlAI SaaS uses 300/330. On-premises installations run their own GPU, where the difference is only 7 seconds of GPU time, so they use 60/90 for the shorter delay. There is one mechanism and one codebase; only the settings (meeting.transcribeStrideSeconds, meeting.transcribeWindowSeconds) differ.

A managed service keeps audio and transcripts on its side

The self-hosted model returns its result in one request, and nothing is left outside the customer network. A managed service runs an asynchronous job: upload the audio file, create a transcription job, ask whether it has finished, and fetch the result. In the meantime the uploaded audio and the transcript sit on the service's side. That is why the configuration example (config.example.yaml) states that turning on AssemblyAI sends meeting audio outside, and advises against it for on-premises installations where audio must stay in house.

With a managed service, the service's retention policy becomes the retention period for meeting audio. AssemblyAI's public documentation says the following (checked November 2026). A separate agreement, such as a business associate agreement (BAA) for health information, changes the defaults, so confirm them against the documentation and contract terms current at adoption time.

  • Uploaded audio files: deletion begins 24 hours after upload and finishes within 48 hours at most.
  • Transcripts: if no data retention period (TTL) is set on the account, deletion begins after 30 days.
  • Account TTL: when set, deletion of both audio and transcripts begins at that time, and it can go as low as 1 hour.
  • Model training: only certain files, as the contract permits, are used for training; files from accounts on European servers, under a BAA, or opted out of training are not.
  • There is no API that deletes an uploaded file on its own; deleting a transcription job also deletes the file it used.

OwlAI SaaS sends requests to the European endpoint, and its production account has a short TTL and training opt-out turned on. Those settings are safeguards on the account side; separately, the product deletes what it uses right away.

Delete right after transcribing, and record what to delete again if missed

Transcribing one window with the managed service goes in this order. Right after the audio is uploaded, and before the transcription job is created, a row for that window goes into the deletion record. Right after the job is created, the job ID is filled into that row. Once the result is fetched and saved, the job on the service side is deleted at once, which deletes the uploaded file with it.

Transcribing one window with a managed service: the deletion record and three cleanup steps
Figure 1. Transcribing one window with a managed service: the deletion record and three cleanup steps

The row comes first because a worker can die while waiting for the result, or a delete request can fail. The row is still there, so a cleanup job that runs every 10 minutes deletes what is left through the service that created the row. It uses that service rather than the one currently in the configuration, because objects may still sit on the old service after a switch. Failed rows are retried at intervals that start at 10 minutes and double up to 6 hours, and they are never abandoned. Giving up would turn an outage of a few hours (a key rotation, a suspended account) into permanent retention on the service side. Rows whose interval has reached 6 hours are reported in an error log once a day.

Retries never upload the audio again. If a job has not been deleted yet, the retry keeps waiting on it; if only the audio exists, it creates a job from the same upload URL. This keeps the same window from being billed twice. For the same reason, the cleanup job leaves a row alone while its window is still unfinished, until the retry takes it over.

A row with no job ID isn't taken at face value. If the job creation request timed out, the service may hold a job the record has no ID for. Reading that row as "never created" would leave the job undeleted forever, and the retry would create a second one and bill twice. So the service's recent job list is searched for a job that used that upload URL; the retry takes it over and the cleanup job deletes it. If nothing turns up within the set range, nothing new is created and the attempt ends as a failure, because creating one without knowing would lose the earlier job.

One case remains. If the audio was uploaded but job creation keeps failing, there is no API to delete that file, so it stays until the service's retention deadline. Retries reuse the same audio, so there is at most one such file per window, and the cleanup job logs it as a warning. If the worker dies after the upload response and before the row is written, there is no record at all. The account TTL is what deletes even those files.

Inside the product, the original meeting audio is deleted after the minutes are produced (“Ending a meeting drops the last remarks; audio waits for transcription”), and once the meeting ends only the transcript and minutes remain.

Design choices

We use turn changes within a window instead of stitching speakers across the whole meeting. Stitching would make it possible to let people attach names to "speaker A," but that requires storing voice features or putting who said what into the meeting record. Transcript rows hold only the turn-change marker, with no speaker field, and no voice features are kept. The price is that the minutes cannot say who said something, and owners and due dates for action items are filled in only when someone actually said them in the meeting. It is also this choice that reduces what the service must provide to "tell voices apart within one request."

OwlAI SaaS chooses cost over latency and uses 300/330. At equal quality, the audio sent drops from 1.48× to 1.07×, and in exchange bubbles can appear up to about five and a half minutes late. The screen fills that gap with "Transcribing…".

Instead of leaving it to the service's retention policy or the account TTL alone, the product deletes as soon as it uses something and deletes again from the record whatever was missed. The TTL can be changed only from the account dashboard, a new value applies only to later requests, and the service can change its retention policy. The price is a deletion record table, a periodic cleanup job, and code that pages through job lists to find lost jobs.

If your product connects a managed speech recognition service, it is faster to check two things before you look at accuracy tables: whether speaker separation arrives in the same response as the transcript, and how long uploaded audio and results stay on the service and what you can delete them with.