For the complete documentation index, see llms.txt. This page is also available as Markdown.

API reference

Most builders should use the Python SDK or Pipecat — they implement this protocol, buffering, and audio/video sync for you. Read on only if you need low-level WebSocket control.

Overview

Real-time talking head synthesis API. Send speech audio, receive synchronized video and audio frames.

After connecting and receiving SessionReady, you should send one initial audio message with silence (one frame worth of silent audio) to start the interaction on the server. The server then immediately begins streaming video and audio frames at 25fps. When no speech audio has been sent, the server generates silence frames (persona at rest with idle animation). When you send speech audio, the server generates speech frames with lip-synced animation synchronized to your audio.

After the initial silence frame, you only need to send speech audio — no additional silence, padding, or keep-alive messages are required.

Production deployments: This WebSocket API is intended for server-to-server use over a stable connection. Connect from a backend server rather than a front-end client — to keep your API key secure and because the raw WebSocket isn't built for flaky end-user networks. Run the backend in US East, close to Ojin's inference, for the lowest latency, and deliver the final media stream to end users over a transport built for varying network conditions — typically WebRTC — for smooth, reliable, low-latency playback.


How It Works

  1. Connect to the WebSocket endpoint with your API key and config ID

  2. Receive SessionReady — the server has allocated inference resources for your session

  3. Send initial audio message with silence for one frame.

  4. The server starts streaming frames immediately — silence frames with idle animation

  5. Send speech audio whenever it becomes available (e.g., TTS output from your language model) — also buffer it locally for playback

  6. Receive speech frames — the server transitions to lip-synced animation and returns to silence frames when audio runs out

  7. Render video frames at 25fps, keeping a small jitter buffer (trim idle frames only if they back up after a stall)

  8. Start playing your buffered TTS audio when the first speech frame (frame_type 1 or 3) arrives from Ojin — stop when speech ends

Frame Types

Every frame arrives as a binary InteractionResponse containing both a JPEG image and a PCM audio chunk. Frames are always delivered in order. The frame_type field classifies each frame:

frame_type

Description

0

Idle — persona at rest with idle animation. Generated automatically when no speech audio is queued

1

Speech — lip-synced animation generated from your audio input

2

Fade-out — post-cancel ramp back toward idle after an interruption

3

Start of speech — first speech frame of a turn resuming after an interruption/cancel. Natural (uninterrupted) turns start with 1, not 3

Buffering for Network Jitter

The server delivers frames at realtime 25 fps, so your buffer does not grow on its own. Keep a small client-side buffer — a few frames — to absorb network jitter and prevent stuttering during speech, and start playback once it's filled.

The right buffer size depends on your network conditions and latency requirements: keep it as low as possible to minimize latency, but high enough to absorb jitter without starving playback.

If frames ever back up — for example a brief network stall followed by a burst of queued frames arriving at once — you can recover by trimming idle frames (frame_type == 0). Only idle frames are safe to drop — never drop speech (1), start-of-speech (3), or fade-out (2) frames.


Connection Flow


WebSocket Handshake

Open WebSocket connection

get

Connect to the WebSocket endpoint providing an API key in the Authorization header and a config_id query parameter. The server upgrades the connection to WebSocket and immediately begins streaming frames after sending SessionReady.

Recommended WebSocket settings:

  • ping_interval: 30 seconds

  • ping_timeout: 10 seconds

Authorizations
AuthorizationstringRequired

Raw API key (no Bearer prefix).

Query parameters
config_idstringRequired

Configuration ID for the persona, created in the Oris Portrait tab of the dashboard.

Header parameters
AuthorizationstringRequired

Your raw API key. No Bearer prefix.

Example: your-api-key
Responses
101

WebSocket upgrade successful. After the upgrade, the server sends a SessionReady JSON message and begins streaming binary InteractionResponse frames immediately.

application/json

Sent once by the server after the WebSocket connection is established and inference resources are allocated. The server begins streaming frames immediately after this message.

Format: JSON text frame.

typestring · enumRequiredPossible values:
get/

Message Format

Mixed message types: Both JSON (text) and binary messages are exchanged on the same WebSocket connection. Your client must check the WebSocket frame type to distinguish them:

  • Text frames (JSON): SessionReady, ErrorResponse (server → client), CancelInteraction (client → server)

  • Binary frames: InteractionResponse (server → client), InteractionInput (client → server)

Byte order: All multi-byte integer fields in binary messages use network byte order (big-endian).


Messages Reference

Server → Client Messages

Client → Server Messages


Message Details

InteractionInput (Client → Server, Binary)

Binary message for sending speech audio to the server. Only send speech audio — do not send silence or padding.

Binary structure:

Header fields use big-endian byte order. The PCM audio samples in the payload use little-endian (standard for PCM int16). In Python: struct.pack('!BQI', payload_type, timestamp, params_size).

Audio requirements:

Property
Value

Format

PCM signed 16-bit integers (little-endian samples)

Sample rate

16,000 Hz

Channels

1 (mono)

Max message size

512 KB (entire binary message including header)

Recommended streaming pattern:

The server runs a 25 fps virtual timeline and needs your audio input to stay slightly ahead of it. Forwarding tiny TTS fragments one at a time (e.g. 40 ms chunks) makes input rate match output rate, so the server starves and emits idle frames between speech — lip-sync skips or drifts. Instead, feed audio with a cushion, not per-fragment:

  1. Prime ~1 second of speech audio at the start of a turn.

  2. Then send the largest chunks you can — coalesce queued fragments into ~400 ms sends (under the 512 KB cap).

  3. Stay realtime: never wait for the whole utterance before sending.


InteractionResponse (Server → Client, Binary)

Binary message containing a video frame and synchronized audio. The server streams these continuously after SessionReady. Frames always arrive in order.

Binary structure:

All multi-byte integers are big-endian. In Python: struct.unpack('!B16sQIII', header_bytes) for the main header, struct.unpack('!IB', entry_bytes) for each payload entry. The Frame type byte is a single trailing uint8 after the last payload entry and is the authoritative frame classifier.

Frame type:

Frame type
Meaning

0

Idle — persona at rest with idle animation

1

Speech — lip-synced animation from your audio

2

Fade-out — post-cancel ramp toward idle

3

Start of speech — first speech frame of a turn resuming after an interruption/cancel (natural turns start with 1)

Payload types:

Type
Format
Typical size per frame

1 (audio)

PCM int16, 16kHz mono

1,280 bytes (640 samples = 40ms at 25fps)

2 (image)

JPEG-encoded image

Variable (resolution depends on config, e.g. 1280×720)

Parsing example:


CancelInteraction

Message
Purpose
Server behavior
Use case

CancelInteraction

Immediate stop

Stops processing, discards remaining frames

User interruption


ErrorResponse (Server → Client, JSON)

Error codes:

Code
Description

AUTH_FAILED

Invalid API key

UNAUTHORIZED

Caller lacks permission

MISSING_CONFIG_ID

config_id query parameter not provided

INVALID_MESSAGE

Malformed or unsupported message payload

INVALID_HEADERS

Missing or invalid headers

MODEL_NOT_FOUND

Config ID not found or invalid

BACKEND_UNAVAILABLE

No healthy inference backend available

RATE_LIMITED

Too many requests

TIMEOUT

Operation exceeded processing time

CANCELLED

Interaction cancelled by client

INTERNAL_ERROR

Unexpected server error

FRAME_SIZE_EXCEEDED

Message exceeded 512KB limit


Rate Limits & Constraints

Constraint
Value

Rate limit

6 requests per second

Max message size

512 KB per message

Video output

25 fps (realtime delivery)

Exceeding limits results in an ErrorResponse with code RATE_LIMITED.


Best Practices

Audio Input

  • Send one silence frame first to start the conversation, then send speech audio when available

  • Prime ~1 s of audio, then send the largest chunks you can (~400 ms). The server needs your input to stay slightly ahead of its 25 fps timeline. Forwarding tiny fragments one at a time starves it and produces idle frames between speech — buffer locally for playback, but coalesce what you send. See the InteractionInput message details above and Optimizing Performance.

  • The Python SDK and Pipecat handle this input shaping for you — this only matters when driving the WebSocket API directly.

Buffer Management

  • Play frames at 25 fps (40ms per frame)

  • The server delivers at realtime 25 fps — keep a small jitter buffer (a few frames); it won't grow on its own

  • Recovery: if frames back up after a network stall, skip 1 out of every 2 idle frames (frame_type == 0) until the buffer shrinks back

  • Never drop speech (1), start-of-speech (3), or fade-out (2) frames

  • Tune your target buffer size based on your network conditions — keep it as low as possible for minimal latency

Audio and Video Synchronization

Each InteractionResponse contains both a JPEG image and a PCM audio chunk. However, the recommended approach for audio playback is:

  1. Buffer your TTS/source audio locally as it arrives from your speech service (for later playback only. You do not need to buffer it to send it to Ojin)

  2. Forward TTS audio to Ojin immediately as it arrives from your speech service

  3. Wait for the first speech frame (frame_type 1 or 3) to arrive from Ojin

  4. Start playing your buffered TTS audio at that moment

  5. Stop audio playback when speech ends — the first idle or fade-out frame (frame_type 0 or 2) after speech

  6. Render video from every frame regardless of type

Sending is immediate, but playback is gated on the first speech frame. This ensures audio and video stay in sync.

Error Handling

  • Handle both JSON ErrorResponse messages and plain text error strings

  • Implement exponential backoff for reconnection

  • Monitor server load in the SessionReady message

Interruption Handling

  • Use CancelInteraction for immediate stops (e.g., user interrupts the bot)

  • Clear your frame buffer on interruption


Complete Example


Troubleshooting

Symptoms, causes, and fixes — connection and auth, a starved/idle mouth, choppy playback, latency, and frame lag (speech_filter_amount) — live in the global Troubleshooting guide, where each entry is tagged 🟢 SDK-handled or 🟠 your setup.


Example Implementation

A complete working Python example integrating Ojin Oris Portrait with a speech-to-speech service (Hume EVI) is available here:

github.com/journee-live/speech-to-video-samples/tree/main/samples (includes Hume STS → Oris Portrait walkthroughs)

The repository demonstrates the full integration pattern: microphone capture → STS service → TTS audio → Ojin lip-sync → synchronized video and audio playback at 25fps. It includes the buffer management and frame handling approach described in Best Practices above.


Last updated

Was this helpful?