External Platforms
OpenAI Realtime API
Integrate Trulience avatars with OpenAI's Realtime voice API using WebRTCOverview
OpenAI’s Realtime API provides low-latency voice-to-voice conversations using WebRTC.
Working example: OpenAI integration example
How It Works
- OpenAI Realtime API handles speech recognition, conversation logic, and voice synthesis
- Trulience renders the avatar and synchronizes lip movements with OpenAI’s audio output
- Audio is delivered via standard WebRTC
RTCPeerConnection- no SDK required
Prerequisites
- OpenAI API key with Realtime API access
- A Trulience avatar configured for External Voice Platforms
- Basic knowledge of React and WebRTC
Dashboard Configuration
- Open your avatar’s settings in the Trulience dashboard
- Navigate to the BRAIN tab
- Select ‘3rd Party AI’ mode
- Set ‘Service provider or framework’ to ‘External Voice Platforms’
This configuration disables Trulience’s built-in STT, LLM, and TTS, allowing OpenAI to handle these components.
Integration Steps
1. Install Dependencies
npm install @trulience/react-sdk2. Get Ephemeral Key
OpenAI Realtime API requires an ephemeral key from your backend. Create an API endpoint:
// app/api/token/route.ts
export async function GET() {
const response = await fetch('https://api.openai.com/v1/realtime/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o-realtime-preview-2024-12-17',
voice: 'verse'
})
});
const data = await response.json();
return Response.json(data);
}Then fetch it from the frontend:
const tokenResponse = await fetch('/api/token');
const data = await tokenResponse.json();
const ephemeralKey = data.client_secret.value;3. Create RTCPeerConnection
const pc = new RTCPeerConnection();
// Add microphone input
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
pc.addTrack(micStream.getTracks()[0]);4. Capture Remote Audio Track
This is where the Trulience integration happens:
// Listen for OpenAI's audio track
pc.ontrack = (event) => {
const remoteStream = event.streams[0];
// Route audio to Trulience avatar
trulienceRef.current.setMediaStream(remoteStream);
trulienceRef.current.getTrulienceObject().setSpeakerEnabled(true);
// Mute any auto-created audio elements to prevent double audio
const audioElement = document.querySelector('audio');
if (audioElement) audioElement.muted = true;
};5. Exchange SDP with OpenAI
// Create offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send offer to OpenAI
const sdpResponse = await fetch(
'https://api.openai.com/v1/realtime/calls?model=gpt-4o-realtime-preview-2024-12-17',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${ephemeralKey}`,
'Content-Type': 'application/sdp'
},
body: offer.sdp
}
);
// Set remote description
const answer = {
type: 'answer',
sdp: await sdpResponse.text()
};
await pc.setRemoteDescription(answer);Complete React Example
Here’s a full working implementation:
import React, { useRef, useState } from 'react';
import { TrulienceAvatar } from '@trulience/react-sdk';
function OpenAIRealtimeIntegration() {
const trulienceRef = useRef(null);
const audioRef = useRef(null);
const [pc, setPc] = useState(null);
const [connected, setConnected] = useState(false);
const startSession = async () => {
try {
// Get ephemeral key from backend
const tokenResponse = await fetch('/api/token');
const data = await tokenResponse.json();
const ephemeralKey = data?.client_secret?.value;
if (!ephemeralKey) {
throw new Error('Failed to get ephemeral key');
}
// Create peer connection
const newPc = new RTCPeerConnection();
// Set up audio playback (muted to avoid duplicate audio)
if (audioRef.current) {
audioRef.current.autoplay = true;
audioRef.current.muted = true;
}
// Capture remote audio track
newPc.ontrack = (e) => {
if (audioRef.current) {
audioRef.current.srcObject = e.streams[0];
}
// Route to Trulience
if (trulienceRef.current) {
trulienceRef.current.setMediaStream(e.streams[0]);
trulienceRef.current.getTrulienceObject().setSpeakerEnabled(true);
}
};
// Add microphone input
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
newPc.addTrack(micStream.getTracks()[0]);
// Create offer
const offer = await newPc.createOffer();
await newPc.setLocalDescription(offer);
// Send offer to OpenAI Realtime API
const sdpResponse = await fetch(
'https://api.openai.com/v1/realtime/calls?model=gpt-4o-realtime-preview-2024-12-17',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${ephemeralKey}`,
'Content-Type': 'application/sdp'
},
body: offer.sdp
}
);
const answer = {
type: 'answer',
sdp: await sdpResponse.text()
};
await newPc.setRemoteDescription(answer);
setPc(newPc);
setConnected(true);
} catch (error) {
console.error('Failed to start session:', error);
}
};
const endSession = () => {
if (pc) {
pc.getSenders().forEach(sender => sender.track?.stop());
pc.close();
setPc(null);
setConnected(false);
}
};
return (
<div className="relative min-h-screen">
<audio ref={audioRef} style={{ display: 'none' }} />
<div className="absolute inset-0">
<TrulienceAvatar
ref={trulienceRef}
url={process.env.NEXT_PUBLIC_TRULIENCE_SDK_URL}
avatarId="your-avatar-id"
token="your-trulience-token"
width="100%"
height="100%"
/>
</div>
<button
onClick={connected ? endSession : startSession}
className={`absolute bottom-6 left-1/2 -translate-x-1/2 px-6 py-3 rounded-lg text-white font-semibold ${
connected ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'
}`}
>
{connected ? 'Disconnect' : 'Connect'}
</button>
</div>
);
}
export default OpenAIRealtimeIntegration;Key Integration Points
Preventing Double Audio
OpenAI’s audio arrives via WebRTC track. To prevent hearing both OpenAI’s audio and the avatar’s audio:
- Create a muted
<audio>element to receive the WebRTC stream - Route the same stream to Trulience via
setMediaStream() - Enable the avatar’s speaker with
setSpeakerEnabled(true)
// Muted audio element prevents browser from playing directly
<audio ref={audioRef} muted autoplay style={{ display: 'none' }} />
// In ontrack handler
audioRef.current.srcObject = e.streams[0]; // Muted playback
trulienceRef.current.setMediaStream(e.streams[0]); // Avatar playback
trulienceRef.current.getTrulienceObject().setSpeakerEnabled(true);Using Data Channels
You can also send events to OpenAI via a data channel:
const channel = pc.createDataChannel('oai-events');
channel.addEventListener('message', (e) => {
const event = JSON.parse(e.data);
console.log('OpenAI event:', event);
});
channel.onopen = () => {
// Send initial instruction
channel.send(JSON.stringify({
type: 'response.create',
response: {
instructions: 'Introduce yourself as a Trulience AI avatar.'
}
}));
};Troubleshooting
Issue: No audio from avatar
- Verify
setSpeakerEnabled(true)is called - Check that remote stream has audio tracks:
e.streams[0].getAudioTracks().length > 0 - Ensure the audio element is muted (not the stream)
Issue: Connection fails
- Ensure ephemeral key is valid and not expired
- Check browser console for ICE connection errors
- Verify HTTPS is used (WebRTC requires secure context)
Issue: Avatar not lip-syncing
- Check that
setMediaStream()is called with a valid MediaStream - Ensure the avatar is configured for ‘External Voice Platforms’ in the dashboard
- Verify the audio track is active:
stream.getAudioTracks()[0].enabled === true
Example Code Repository
See our OpenAI integration example for a complete working implementation with error handling, state management, and additional features.
Next Steps
- Review the Integration Guide for general audio routing patterns
- Explore OpenAI’s Realtime API documentation
- Check out our Javascript SDK documentation for more avatar controls