Skip to main content

Transcription

Transcription is Cortex’s core capability. A headless bot joins an ODIN Voice room, receives a separate audio stream for each speaker, detects when someone is talking, transcribes each utterance, and stores it as a message — attributed to the right person, in real time.

How it works​

The key detail: the bot receives individual per-peer audio, not a mixed stream. That’s what makes attribution accurate — every message is tied to the participant who spoke it.

Which provider turns the audio into text is up to you: by default Cortex transcribes with OpenAI on 4Players’ account, or you can bring your own key or endpoint.

Want to build your own bot instead?

Cortex runs and scales this bot for you. If you'd rather build and host your own, the ODIN Voice Node.js SDK gives you the same per-speaker audio access Cortex relies on — see the Transcribing Audio sample. Cortex is the managed version of that approach.

Sessions and messages​

  • A session is the active transcription of one room. Create it with the externalRoomId of the ODIN room you want to transcribe — the bot joins right away. See Sessions & Gatherings.
  • Each transcribed utterance becomes a message on that session, attributed to a participant.
const session = await project.sessions.create({
title: "Support call #1021",
externalRoomId: "support-1021",
idleTimeout: 90, // optional: auto-end after 90s of silence
});
// The bot starts automatically. Pass `autoStart: false` to create the session
// inactive and call `session.start()` later.

// ...conversation happens...

const messages = await session.getMessages();
await session.stop();

Reading the transcript​

Every message carries a per-session sequence number, seq (1, 2, 3, … without gaps). Use it as a cursor to read only what’s new:

let after = 0;
const page = await session.listMessages(
{ after, limit: 200 },
{ ifNoneMatch: `"${after}"` },
);
if (page) {
render(page.messages);
after = page.nextCursor ?? page.latestSeq; // nextCursor while more pages are waiting
}
// `page` is null when nothing changed since `after` — a very cheap call to repeat.

To process messages as they happen rather than polling, use watchMessages() or subscribe to the message.created event. The event carries the same seq, so after a reconnect you can catch up with listMessages({ after: lastSeenSeq }).

Transcription providers​

Speech-to-text runs through transcription plugins — a special kind of plugin that works on audio inside the bot. Each project has exactly one transcriber:

PluginWho runs the speech-to-textCredentialsBilling
cortex-transcription (default)OpenAI, operated by 4PlayersNone — 4Players’ accountBilled by 4Players per audio minute
openai-transcriptionOpenAI, or any OpenAI-compatible speech-to-text APIYour own API key, optional base URLNot billed by 4Players — your provider bills you

Every project starts on cortex-transcription — nothing to set up. Switch to openai-transcription when you want to:

  • use your own OpenAI account (your contract, your rate limits, your invoice),
  • use another OpenAI-compatible provider such as Groq, or
  • run speech-to-text yourself — a self-hosted, OpenAI-compatible server (for example one based on faster-whisper) keeps transcription on infrastructure you control.

Bring your own key (or endpoint)​

Switching takes three steps: install the plugin with your settings, check the connection, and select the instance as the project’s transcriber.

import { CortexClient } from "@4players/odin-cortex";

const client = new CortexClient({ /* ... */ });
const project = client.project("your-project-id");

// 1. Install openai-transcription with your own key
const plugin = await client.pluginCatalog.get("openai-transcription");
const instance = await project.plugins.create({
pluginId: plugin.id,
settings: {
apiKey: process.env.OPENAI_API_KEY, // stored encrypted, never returned
// baseUrl: "https://stt.example.com/v1", // optional: any OpenAI-compatible API
model: "whisper-1", // any model your provider offers
language: "en", // optional ISO 639-1 hint
},
});

// 2. Test the key and base URL against the provider (lists its models)
const health = await project.plugins.invokeEndpoint("openai-transcription", "health");
if (health.status !== "healthy") throw new Error(String(health.message));

// 3. Make it the project's transcriber
await project.settings.updateTranscription(instance.id);

// Later: go back to the default Cortex transcription
await project.settings.updateTranscription(null);

The same selection is available over REST: PATCH /api/projects/{projectId}/settings/transcription with { "transcriberInstanceId": "<instance id>" } (or null). The current choice is the transcriberInstanceId field of the project settings (project.settings.get()).

Settings of openai-transcription:

SettingRequiredNotes
apiKeyYesYour provider’s API key. Encrypted at rest and returned masked (********).
baseUrlNoEmpty for OpenAI. Otherwise the https base URL of an OpenAI-compatible API, e.g. https://api.groq.com/openai/v1.
modelNoDefault whisper-1. Any transcription model your provider offers (e.g. gpt-4o-transcribe, whisper-large-v3).
languageNoISO 639-1 code of the room language (e.g. de). Improves accuracy and latency; empty = auto-detect.
promptNoNames and game terms the model should recognize.

Good to know:

  • Running sessions switch within about 30 seconds — no need to restart them.
  • No silent fallback. If your provider fails (wrong key, quota exhausted, endpoint down), Cortex does not fall back to the 4Players-billed default — that would bill you and send your audio somewhere you didn’t choose. Those utterances are not transcribed. Test with the health endpoint before selecting an instance, and watch your provider’s dashboard.
  • The selected transcriber is protected. While an instance is the project’s transcriber, it can’t be disabled or deleted (409) — select another one (or null) first.
  • baseUrl must be a public https URL. Private, loopback and link-local addresses are rejected, both when you save the settings and when the bot connects, and redirects are not followed. A self-hosted server therefore needs a public https endpoint.
  • Audio never becomes an event. Raw audio stays inside the bot and goes only to the provider you selected — it is never published to webhooks, SSE or functions.
  • Only the default is billed by 4Players. Minutes transcribed with your own key or endpoint are not billed by 4Players — only by your provider, if at all.
More providers

cortex-transcription and openai-transcription are available today. Further provider types are planned.

Tune the default, too

cortex-transcription has settings as well — model (whisper-1, gpt-4o-transcribe or gpt-4o-mini-transcribe), language and prompt. To change them, install it as a plugin instance with your settings and select that instance. All models are billed per audio minute.

ODIN 1.x and 2.x rooms​

ODIN Voice 1.x and 2.x are not interoperable — they use different protocols and server infrastructure. Tell Cortex which major your clients use so the bot joins with a matching SDK:

await project.settings.update({ odinSdkVersion: "v2" }); // "v1" is the default

Make sure the session’s gateway belongs to the same major. On ODIN 2.x rooms the bot also gets noise suppression before transcription and flushes an utterance as soon as the speaker stops talking, which lowers transcription latency.

Recording announcements​

The bot speaks a short notice into the room when it joins, and again just before it leaves, so everyone knows they are being recorded. In many jurisdictions (including the whole EU) recording a conversation without informing every participant is unlawful, so announcements are on by default. You can change the wording — in any language — or switch them off if your application obtains consent another way:

await project.settings.updateAnnouncements({
joinText: "This conversation is now being recorded and transcribed.",
leaveText: "Recording and transcription have stopped.",
});

Texts are limited to 500 characters and can’t be blank (use enabled: false to turn announcements off). Changes apply to the next session.

End-to-end encrypted rooms​

If your rooms use ODIN end-to-end encryption, the bot needs the same cipher password as your clients. Choose how it gets it:

ModeBot behavior
disabled (default)Joins without a cipher — for unencrypted rooms.
automaticUses the room’s externalRoomId as the cipher password — your clients must do the same.
manualUses a fixed password you configure.
await project.settings.updateE2EEncryption("manual", "the-room-cipher-password");

Summaries​

A session can produce an AI summary of the conversation (recap, action items, a shareable transcript) through the Session Summary plugin. You can regenerate it on demand:

await session.regenerateSummary();

Summaries arrive as session annotations and the session.annotation.created event — handy for emailing a recap when a call ends (see the serverless function guide).

Debug audio (optional)​

For troubleshooting, a project can enable debug audio, which retains the exact audio that was sent to the transcription provider for each message for 24 hours, so you can play back what was heard. It’s off by default because it stores voice data.

await project.settings.updateDebugAudio(true);

// Messages with a recording have `hasDebugAudio: true`
const wav = await session.downloadDebugAudio(messageId);
Handle voice data responsibly

Transcripts — and especially debug audio — can contain personal data. Enable debug audio only when you need it, and make sure your privacy notices cover transcription.

Identifying speakers​

The bot maps each audio stream to a participant using the externalUserId your client passed to ODIN when joining the room. Make sure your clients pass a stable, meaningful userId so transcripts attribute correctly. See Users & Participants.

Players with an active voice mute can also be excluded from transcription on ODIN 2.x rooms — see Moderation.