Skip to content

feat(transcription): OpenAI Realtime transcription service - #643

Open
felipeavilis wants to merge 25 commits into
jitsi:masterfrom
felipeavilis:feat/openai-realtime-transcription
Open

feat(transcription): OpenAI Realtime transcription service#643
felipeavilis wants to merge 25 commits into
jitsi:masterfrom
felipeavilis:feat/openai-realtime-transcription

Conversation

@felipeavilis

@felipeavilis felipeavilis commented Jun 15, 2026

Copy link
Copy Markdown

Summary

Adds OpenAIRealtimeTranscriptionService, a new transcription backend that uses the OpenAI Realtime API via WebSocket.

  • One WebSocket session per participant (matches Oracle/Whisper pattern)
  • PCM 24kHz mono audio (PCMAudioSilence24kMediaDevice) — required by the OpenAI Realtime API minimum sample rate
  • server_vad active by default — API detects speech automatically, no manual buffer commit needed
  • Streaming results: partial transcripts via conversation.item.input_audio_transcription.delta, finals via .completed
  • Exponential-backoff reconnect (3 attempts); fatal errors (invalid_api_key, invalid_model) abort the retry loop immediately
  • Sends a human-readable error message to the conference room on authentication failure

Protocol

WebSocket: wss://api.openai.com/v1/realtime?model=gpt-realtime-2
Authorization: Bearer <apiKey>

On session.created, send session.update:

  • session.type: realtime
  • session.audio.input.transcription.model: gpt-realtime-whisper
  • session.audio.input.transcription.language: BCP-47 subtag (e.g. pt, en)

Audio frames sent as input_audio_buffer.append (base64 PCM16 24kHz mono).

New files

File Purpose
transcription/openai/OpenAIRealtimeClient.java WebSocket client, one per participant
transcription/openai/OpenAIRealtimeClientListener.java Callback interface
transcription/OpenAIRealtimeTranscriptionService.java TranscriptionService implementation
transcription/PCMAudioSilence24kCaptureDevice.java 24kHz PCM capture device
transcription/PCMAudioSilence24kMediaDevice.java 24kHz media device wrapper

Changes to existing files

  • TranscriptionListener.java: adds AUTHENTICATION_FAILED to FailureReason enum (additive, no impact on existing services)
  • TranscriptionGatewaySession.java: failed() now sends a human-readable message to the room before stopping — benefits all services including
    GoogleCloudTranscriptionService which already uses RESOURCES_EXHAUSTED
  • jigasi-home/sip-communicator.properties: adds commented-out config block for OpenAI Realtime

Configuration

org.jitsi.jigasi.transcription.customService=org.jitsi.jigasi.transcription.OpenAIRealtimeTranscriptionService
org.jitsi.jigasi.transcription.openai.apiKey=sk-...
org.jitsi.jigasi.transcription.openai.sessionModel=gpt-realtime-2
# Optional — defaults shown:
org.jitsi.jigasi.transcription.openai.transcriptionModel=gpt-realtime-whisper
org.jitsi.jigasi.transcription.openai.websocketUrl=wss://api.openai.com/v1/realtime

Test plan

  • Build passes: mvn install -DskipTests
  • Valid API key: transcription appears in the conference room in real time
  • Invalid API key: SEVERE: OpenAI API key is invalid logged; error message sent to room; Jigasi leaves
  • Invalid model: SEVERE: OpenAI model error logged; Jigasi does not retry indefinitely
  • Participant joins mid-session: new WebSocket session created independently
  • Session ends cleanly: Closing OpenAI Realtime connection logged, no leftover threads

Felipe Amaral added 25 commits June 15, 2026 17:09
Implements TranscriptionService backed by the OpenAI Realtime API
(wss://api.openai.com/v1/realtime). One WebSocket session per
participant; audio sent as base64-encoded PCM16 16kHz mono via
input_audio_buffer.append events. Session configured in
transcription-only mode (modalities: text, turn_detection: null).
Partial results from transcription.delta events, finals from
transcription.completed. Exponential-backoff retry on connect failure.

Configure via sip-communicator.properties:
  org.jitsi.jigasi.transcription.customService=org.jitsi.jigasi.transcription.OpenAIRealtimeTranscriptionService
  org.jitsi.jigasi.transcription.openai.apiKey=sk-...
  org.jitsi.jigasi.transcription.openai.model=gpt-4o-realtime-preview-2024-12-17
  org.jitsi.jigasi.transcription.openai.websocketUrl=wss://api.openai.com/v1/realtime
- Fix session.update structure: use session.type=transcription and
  audio.input.format/transcription nesting per GA spec
- Declare audio rate as 16000 (Jigasi PCM pipeline is fixed at 16kHz)
- Add input_audio_buffer.commit every 1s (required in manual turn_detection mode)
- Add transcriptionDelay config param (minimal/low/medium/high/xhigh)
OpenAI Realtime API enforces a minimum sample rate of 24000 Hz.
Add PCMAudioSilence24kCaptureDevice and PCMAudioSilence24kMediaDevice
to produce 24kHz PCM; OpenAIRealtimeTranscriptionService now uses
this device instead of the 16kHz one used by other services.
…nscription

Replace dialog session WebSocket approach with the correct two-step
transcription session protocol:
1. POST /v1/realtime/transcription_sessions with API key to obtain
   a short-lived ephemeral client_secret token
2. Connect WebSocket to /v1/realtime?model=gpt-realtime-whisper
   authenticated with the ephemeral token (not the API key directly)

This removes the broken session.update with session.type="transcription"
which caused "Passing a transcription session update to a realtime
session is not allowed" errors. Transcription sessions require a
dedicated REST endpoint; dialog model sessions do not support
session.type="transcription".

Also removes MODEL_CONFIG / DEFAULT_MODEL (gpt-realtime-2) and the
java.net.* wildcard import that caused WebSocket annotation ambiguity.
/v1/realtime/transcription_sessions → /v1/realtime/transcription/sessions
…r transcription

Replace the broken REST-first approach (which hit invalid endpoints) with
the correct protocol per OpenAI documentation:
- Connect WebSocket to wss://api.openai.com/v1/realtime?model=gpt-realtime-2
  authenticated directly with the API key
- On connect, send session.update with session.type="transcription" and
  audio.input.transcription.model="gpt-realtime-whisper"

Splits session model (URL param, gpt-realtime-2) from transcription model
(session.update body, gpt-realtime-whisper) via separate config keys:
  openai.sessionModel    (default: gpt-realtime-2)
  openai.transcriptionModel (default: gpt-realtime-whisper)
gpt-realtime-whisper must be passed as input_audio_transcription.model
inside a standard realtime session (model=gpt-realtime-2 in URL).
Setting session.type="transcription" is rejected by dialog session models.
Use input_audio_transcription.model directly per API error guidance.
Confirmed via wscat: session.update requires session.type field.
Value must be "realtime" (not "transcription") for dialog session models.
Transcription is enabled via audio.input.transcription.model.
… at startup

On isConfiguredProperly(), fetch GET /v1/models with the configured API key
and log all realtime models available on the account. Warn clearly if the
configured sessionModel or transcriptionModel is not in the list.

Also improve the invalid_model WebSocket error with a specific log message
pointing to the config keys to fix.
Keep only the improved invalid_model error message pointing to
the config keys to check.
…odel)

Set fatalError flag on invalid_api_key and invalid_model errors so the
retry loop stops immediately instead of retrying three times uselessly.
Log a clear message pointing to the config key to fix in each case.
…io on closed channel

- connected flag is now set in onConnect (WebSocket callback) instead of
  connectInternal, eliminating the race where connected=true was set before
  a fatal error message arrived and closed the session
- sendAudio checks session.isOpen() in addition to connected flag, dropping
  frames silently when the channel is closed (avoids log spam on auth errors)
- Add AUTHENTICATION_FAILED to TranscriptionListener.FailureReason
- TranscriptionGatewaySession.failed() now sends a human-readable chat
  message to the MUC before stopping, for both AUTHENTICATION_FAILED
  and RESOURCES_EXHAUSTED reasons
- OpenAIRealtimeTranscriptionService.onError() detects invalid API key
  errors and calls listener.failed(AUTHENTICATION_FAILED)
When an API auth error arrives during WebSocket handshake, the error
may fire before the JVB conference is established and the Transcriber
enters TRANSCRIBING state. The previous code silently dropped the
failure in this case, preventing the room message from reaching
participants.

Now stop() handles NOT_STARTED + non-null reason by transitioning to
FINISHED and notifying all listeners, so TranscriptionGatewaySession
can send the error message to the room and stop the conference.
…icator.properties

Replace deprecated openai.model with sessionModel and transcriptionModel.
Fix audio format description: 24kHz (not 16kHz).
@damencho

Copy link
Copy Markdown
Member

@felipeavilis Have you looked at opus-transcriber-proxy?
The preferred way of using AI services for transcribing directly from jvb without using jigasi.

Here are the instructions that we will make part of the handbook soon: https://github.com/jitsi/handbook/pull/645/changes

@felipeavilis

Copy link
Copy Markdown
Author

No. I'll take a look. Thanks for the tip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants