Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
API en temps réel Azure OpenAI GPT pour la voix et l’audio fait partie de la famille des modèles GPT-4o qui prend en charge les interactions conversationnelles à faible latence, « voix entrante, voix sortante ».
Vous pouvez utiliser l’API Realtime via WebRTC, SIP ou WebSocket pour envoyer une entrée audio au modèle et recevoir des réponses audio en temps réel. Suivez les instructions de cet article pour commencer à utiliser l’API Realtime via WebRTC.
Dans la plupart des cas, utilisez l’API WebRTC pour la diffusion en continu audio en temps réel. L’API WebRTC est une norme web qui permet une communication en temps réel (RTC) entre les navigateurs et les applications mobiles. Voici quelques raisons pour lesquelles WebRTC est préféré pour le streaming audio en temps réel :
- Latence inférieure : WebRTC est conçu pour réduire le délai, ce qui le rend plus adapté à la communication audio et vidéo où la faible latence est essentielle pour maintenir la qualité et la synchronisation.
- Gestion des médias : WebRTC prend en charge les codecs audio et vidéo intégrés, fournissant une gestion optimisée des flux multimédias.
- Correction d’erreur : WebRTC inclut des mécanismes de gestion de la perte de paquets et de la gigue, qui sont essentiels pour maintenir la qualité des flux audio sur des réseaux imprévisibles.
- Communication d’égal à égal : WebRTC permet une communication directe entre les clients, ce qui réduit la nécessité d’un serveur central pour relayer les données audio, ce qui peut réduire davantage la latence.
Utilisez l’API Realtime via WebSockets si vous devez :
- Diffuser en continu des données audio d’un serveur vers un client.
- Envoyer et recevoir des données en temps réel entre un client et un serveur.
Les WebSockets ne sont pas recommandés pour la diffusion en continu audio en temps réel, car ils ont une latence plus élevée que WebRTC.
Modèles pris en charge
Vous pouvez accéder aux modèles GPT en temps réel pour les déploiements globaux dans les régions USA Est 2 et Suède Centre.
-
gpt-4o-mini-realtime-preview(2024-12-17) -
gpt-4o-realtime-preview(2024-12-17) -
gpt-realtime(version 2025-08-28) -
gpt-realtime-mini(version 2025-10-06) -
gpt-realtime-mini(version 2025-12-15) -
gpt-realtime-1.5(version 2026-02-23) -
gpt-realtime-2(version 2026-05-07) -
gpt-realtime-2.1(version 2026-07-07) -
gpt-realtime-2.1-mini(version 2026-07-07) -
gpt-realtime-translate(version 2026-05-06) -
gpt-realtime-whisper(version 2026-05-06) -
gpt-live-transcribe(version 2026-07-29)
Utilisez le chemin d’accès /openai/v1 dans l’URL de requête pour appeler l’API Realtime.
Pour plus d’informations sur les modèles pris en charge, consultez la documentation sur les modèles et versions.
Important
Utilisez le protocole GA pour WebRTC.
Les points de terminaison en disponibilité générale décrits dans cet article utilisent des URL différentes de celles de l’API preview :
- GA utilise
/openai/v1/realtime/client_secretset/openai/v1/realtime/calls - Aperçu (obsolète) utilisait
/openai/realtimeapi/sessionset des URL régionales
Si vous êtes un client actuel utilisant des points de terminaison en préversion, migrez vers le protocole GA.
Nous conservons ici la documentation du protocole hérité.
Conditions préalables
Avant de pouvoir utiliser l’audio en temps réel GPT, vous avez besoin des éléments suivants :
- Un abonnement Azure - Create one gratuitement.
- Ressource Microsoft Foundry - Créer une ressource Microsoft Foundry dans l’une des régions prises en charge.
- Déploiement d’un modèle en temps réel GPT dans une région prise en charge, comme décrit dans la section modèles pris en charge dans cet article.
- Dans le portail Foundry, chargez votre projet. Sélectionnez Générer dans le menu supérieur droit, puis sélectionnez l’onglet Modèles dans le volet gauche, puis sélectionnez Déployer un modèle de base. Recherchez le modèle souhaité, puis sélectionnez Déployer dans la page du modèle.
Configurer WebRTC
Pour utiliser WebRTC, vous avez besoin de deux éléments de code :
- Application de navigateur web.
- Service où votre navigateur web peut récupérer un jeton éphémère.
Autres options :
- Mettez sous proxy la négociation de session du navigateur web via le protocole protocole SDP (Session Description Protocol) par le biais du même service qui récupère le jeton éphémère. Ce scénario est plus sécurisé, car le navigateur web n’a pas accès au jeton éphémère.
- Filtrez les messages qui vont dans le navigateur web à l’aide d’un paramètre de requête.
- Créez une connexion WebSocket d’observateur pour écouter ou enregistrer la session.
Étapes
Étape 1 : Configurer le service pour obtenir un jeton éphémère
La clé permettant de générer un jeton éphémère est l’API REST à l’aide de
url = https://{your azure resource}.openai.azure.com/openai/v1/realtime/client_secrets
Note
Il s’agit du point de terminaison GA pour WebRTC. Si vous effectuez une migration à partir de l’API en préversion, le point de terminaison a changé :
-
Préversion (déconseillée) :
/openai/realtimeapi/sessions?api-version=2025-04-01-preview -
Disponibilité générale (actuelle) :
/openai/v1/realtime/client_secrets(aucun paramètre de version de l’API n’est nécessaire)
Pour plus d’informations sur la migration, consultez Migration de la préversion vers la version ga de l’API En temps réel.
Vous utilisez cette URL avec une clé API ou un jeton Microsoft Entra ID. Cette requête récupère un jeton éphémère et configure la configuration de session que vous souhaitez que le navigateur web utilise, y compris les instructions d’invite et la voix de sortie.
Voici un exemple de code Python pour un service de jeton. L’application de navigateur web peut appeler ce service à l’aide du point de terminaison /token pour récupérer un jeton éphémère. Cet exemple de code utilise DefaultAzureCredential pour s’authentifier auprès de RealtimeAPI générant des jetons éphémères.
Note
Remplacez les valeurs des espaces réservés dans les exemples de code :
-
<your azure resource>ou<YOUR AZURE RESOURCE>- Nom de la ressource OpenAI Azure -
<your model deployment name>ou<YOUR MODEL DEPLOYMENT NAME>- Nom de déploiement de votre modèle en temps réel
La configuration de session inclut les éléments suivants :
| Champ | Obligatoire | Description |
|---|---|---|
session.type |
Oui | Doit être realtime |
session.model |
Oui | Nom du déploiement de votre modèle |
session.instructions |
Non | Requête système destinée à l’assistant |
session.audio.output.voice |
Non | Voix pour la sortie audio : alloy, , balladash, cedarsageechocoralmarin, ou shimmerverse |
from flask import Flask, jsonify
import os
import requests
import time
import threading
from azure.identity import DefaultAzureCredential
app = Flask(__name__)
# Session configuration
session_config = {
"session": {
"type": "realtime",
"model": "<your model deployment name>",
"instructions": "You are a helpful assistant.",
"audio": {
"output": {
"voice": "marin",
},
},
},
}
# Get configuration from environment variables
azure_resource = os.getenv('AZURE_RESOURCE') # e.g., 'your-azure-resource'
# Token caching variables
cached_token = None
token_expiry = 0
token_lock = threading.Lock()
def get_bearer_token(resource_scope: str) -> str:
"""Get a bearer token using DefaultAzureCredential with caching."""
global cached_token, token_expiry
current_time = time.time()
# Check if we have a valid cached token (with 5 minute buffer before expiry)
with token_lock:
if cached_token and current_time < (token_expiry - 300):
return cached_token
# Get a new token
try:
credential = DefaultAzureCredential()
token = credential.get_token(resource_scope)
with token_lock:
cached_token = token.token
token_expiry = token.expires_on
print(f"Acquired new bearer token, expires at: {time.ctime(token_expiry)}")
return cached_token
except Exception as e:
print(f"Failed to acquire bearer token: {e}")
raise
@app.route('/token', methods=['GET'])
def get_token():
"""
An endpoint which returns the contents of a REST API request to the protected endpoint.
Uses DefaultAzureCredential for authentication with token caching.
"""
try:
# Get bearer token using DefaultAzureCredential
bearer_token = get_bearer_token("https://ai.azure.com/.default")
# Construct the Azure OpenAI endpoint URL
url = f"https://{azure_resource}.openai.azure.com/openai/v1/realtime/client_secrets"
headers = {
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
}
# Make the request to Azure OpenAI
response = requests.post(
url,
headers=headers,
json=session_config,
timeout=30
)
# Check if the request was successful
if response.status_code != 200:
print(f"Request failed with status {response.status_code}: {response.reason}")
print(f"Response headers: {dict(response.headers)}")
print(f"Response content: {response.text}")
response.raise_for_status()
# Parse the JSON response and extract the ephemeral token
data = response.json()
ephemeral_token = data.get('value', '')
if not ephemeral_token:
print(f"No ephemeral token found in response: {data}")
return jsonify({"error": "No ephemeral token available"}), 500
# Return the ephemeral token as JSON
return jsonify({"token": ephemeral_token})
except requests.exceptions.RequestException as e:
print(f"Token generation error: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response status: {e.response.status_code}")
print(f"Response reason: {e.response.reason}")
print(f"Response content: {e.response.text}")
return jsonify({"error": "Failed to generate token"}), 500
except Exception as e:
print(f"Unexpected error: {e}")
return jsonify({"error": "Failed to generate token"}), 500
if __name__ == '__main__':
if not azure_resource:
print("Error: AZURE_RESOURCE environment variable is required")
exit(1)
print(f"Starting token service for Azure resource: {azure_resource}")
print("Using DefaultAzureCredential for authentication")
print("Production mode - use gunicorn to run this service:")
port = int(os.getenv('PORT', 5000))
print(f" gunicorn -w 4 -b 0.0.0.0:{port} --timeout 30 token-service:app")
Étape 2 : Configurer votre application de navigateur
Votre application de navigateur appelle votre service de jetons pour obtenir le jeton, puis lance une connexion WebRTC avec RealtimeAPI. Pour lancer la connexion WebRTC, utilisez l’URL suivante avec le jeton éphémère pour l’authentification.
https://<your azure resource>.openai.azure.com/openai/v1/realtime/calls
Note
Il s’agit du point de terminaison ga pour les connexions WebRTC. Si vous effectuez une migration à partir de l’API en préversion :
-
Préversion (déconseillée) :
https://<region>.realtimeapi-preview.ai.azure.com/v1/realtimertc -
Disponibilité générale actuelle :
https://<your azure resource>.openai.azure.com/openai/v1/realtime/calls
Une fois connecté, l’application de navigateur envoie du texte sur le canal de données et l’audio sur le canal multimédia. Voici un exemple de document HTML pour commencer.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Azure OpenAI Realtime Session</title>
</head>
<body>
<h1>Azure OpenAI Realtime Session</h1>
<button onclick="StartSession()">Start Session</button>
<!-- Log container for API messages -->
<div id="logContainer"></div>
<script>
const AZURE_RESOURCE = "<your azure resource>"
const WEBRTC_URL= `https://${AZURE_RESOURCE}.openai.azure.com/openai/v1/realtime/calls?webrtcfilter=on`
async function StartSession() {
try {
// Call our token service to get the ephemeral key
const tokenResponse = await fetch("/token");
if (!tokenResponse.ok) {
throw new Error(`Token service request failed: ${tokenResponse.status}`);
}
const tokenData = await tokenResponse.json();
const ephemeralKey = tokenData.token;
console.log("Ephemeral key received from token service");
// Mask the ephemeral key in the log message.
logMessage("Ephemeral Key Received from Token Service: " + "***");
// Set up the WebRTC connection using the ephemeral key.
init(ephemeralKey);
} catch (error) {
console.error("Error fetching ephemeral key:", error);
logMessage("Error fetching ephemeral key: " + error.message);
}
}
async function init(ephemeralKey) {
logMessage("🚀 Starting WebRTC initialization...");
let peerConnection = new RTCPeerConnection();
logMessage("✅ RTCPeerConnection created");
// Set up to play remote audio from the model.
const audioElement = document.createElement('audio');
audioElement.autoplay = true;
document.body.appendChild(audioElement);
logMessage("🔊 Audio element created and added to page");
peerConnection.ontrack = (event) => {
logMessage("🎵 Remote track received! Type: " + event.track.kind);
logMessage("📊 Number of streams: " + event.streams.length);
if (event.streams.length > 0) {
audioElement.srcObject = event.streams[0];
logMessage("✅ Audio stream assigned to audio element");
// Add event listeners to audio element for debugging
audioElement.onloadstart = () => logMessage("🔄 Audio loading started");
audioElement.oncanplay = () => logMessage("▶️ Audio can start playing");
audioElement.onplay = () => logMessage("🎵 Audio playback started");
audioElement.onerror = (e) => logMessage("❌ Audio error: " + e.message);
} else {
logMessage("⚠️ No streams in track event");
}
};
// Set up data channel for sending and receiving events
logMessage("🎤 Requesting microphone access...");
try {
const clientMedia = await navigator.mediaDevices.getUserMedia({ audio: true });
logMessage("✅ Microphone access granted");
const audioTrack = clientMedia.getAudioTracks()[0];
logMessage("🎤 Audio track obtained: " + audioTrack.label);
peerConnection.addTrack(audioTrack);
logMessage("✅ Audio track added to peer connection");
} catch (error) {
logMessage("❌ Failed to get microphone access: " + error.message);
return;
}
const dataChannel = peerConnection.createDataChannel('realtime-channel');
logMessage("📡 Data channel created");
dataChannel.addEventListener('open', () => {
logMessage('✅ Data channel is open - ready to send messages');
// Send client events to start the conversation
logMessage("📝 Preparing to send text input message...");
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "hello there! Can you give me some vacation options?",
},
],
},
};
logMessage("📤 Sending conversation.item.create event...");
logMessage("💬 Text content: " + event.item.content[0].text);
try {
dataChannel.send(JSON.stringify(event));
logMessage("✅ Text input sent successfully!");
// Now send response.create to trigger the AI response
const responseEvent = {
type: "response.create"
};
logMessage("📤 Sending response.create event to trigger AI response...");
dataChannel.send(JSON.stringify(responseEvent));
logMessage("✅ Response.create sent successfully!");
} catch (error) {
logMessage("❌ Failed to send text input: " + error.message);
}
}); dataChannel.addEventListener('message', (event) => {
const realtimeEvent = JSON.parse(event.data);
console.log(realtimeEvent);
logMessage("Received server event: " + JSON.stringify(realtimeEvent, null, 2));
if (realtimeEvent.type === "session.update") {
const instructions = realtimeEvent.session.instructions;
logMessage("Instructions: " + instructions);
} else if (realtimeEvent.type === "session.error") {
logMessage("Error: " + realtimeEvent.error.message);
} else if (realtimeEvent.type === "session.end") {
logMessage("Session ended.");
}
});
dataChannel.addEventListener('close', () => {
logMessage('Data channel is closed');
});
// Start the session using the Session Description Protocol (SDP)
logMessage("🤝 Creating WebRTC offer...");
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
logMessage("✅ Local description set");
logMessage("📡 Sending SDP offer to: " + WEBRTC_URL);
const sdpResponse = await fetch(`${WEBRTC_URL}`, {
method: "POST",
body: offer.sdp,
headers: {
Authorization: `Bearer ${ephemeralKey}`,
"Content-Type": "application/sdp",
},
});
logMessage("📥 Received SDP response, status: " + sdpResponse.status);
if (!sdpResponse.ok) {
logMessage("❌ SDP exchange failed: " + sdpResponse.statusText);
return;
}
const answerSdp = await sdpResponse.text();
logMessage("✅ Got SDP answer, length: " + answerSdp.length + " chars");
const answer = { type: "answer", sdp: answerSdp };
await peerConnection.setRemoteDescription(answer);
logMessage("✅ Remote description set - WebRTC connection should be establishing...");
// Add connection state logging
peerConnection.onconnectionstatechange = () => {
logMessage("🔗 Connection state: " + peerConnection.connectionState);
};
peerConnection.oniceconnectionstatechange = () => {
logMessage("🧊 ICE connection state: " + peerConnection.iceConnectionState);
}; const button = document.createElement('button');
button.innerText = 'Close Session';
button.onclick = stopSession;
document.body.appendChild(button);
function stopSession() {
if (dataChannel) dataChannel.close();
if (peerConnection) peerConnection.close();
peerConnection = null;
logMessage("Session closed.");
}
}
function logMessage(message) {
const logContainer = document.getElementById("logContainer");
const p = document.createElement("p");
p.textContent = message;
logContainer.appendChild(p);
}
</script>
</body>
</html>
Dans l’exemple, nous utilisons le paramètre de requête webrtcfilter=on. Ce paramètre de requête limite les messages de canal de données envoyés au navigateur pour conserver vos instructions du prompt privées. Lorsque le filtre est activé, seuls les messages suivants sont retournés au navigateur sur le canal de données :
- input_audio_buffer.speech_started
- input_audio_buffer.speech_stopped
- output_audio_buffer.started
- output_audio_buffer.stopped
- conversation.item.input_audio_transcription.completed
- conversation.item.added
- conversation.élément.créé
- réponse.texte_de_sortie.delta
- response.output_text.done
- response.output_audio_transcript.delta
- response.output_audio_transcript.done
Conseil
Pour obtenir la liste complète des événements d’API en temps réel, consultez la référence de l’API.
Une fois la connexion établie, vous devez voir les messages de console suivants :
✅ RTCPeerConnection created✅ Microphone access granted✅ Data channel is open🎵 Audio playback started
Si l’IA répond, vous verrez des événements response.output_audio_transcript.done contenant la réponse transcrite.
Reference :RTCPeerConnection, événements d’API en temps réel
Étape 3 (facultatif) : Créer un observateur/contrôleur websocket
Si vous proxyez la négociation de session via votre application de service, vous pouvez analyser l’en-tête Location retourné et l’utiliser pour créer une connexion websocket à l’appel WebRTC. Cette connexion peut enregistrer l’appel WebRTC et même le contrôler en émettant directement des événements session.update et d’autres commandes.
Voici une version mise à jour du token_service indiqué précédemment, maintenant avec un point de terminaison /connect que vous pouvez utiliser pour obtenir le jeton éphémère et négocier l’initiation de session. Il inclut également une connexion websocket qui écoute la session WebRTC.
from flask import Flask, jsonify, request
#from flask_cors import CORS
import os
import requests
import time
import threading
import asyncio
import json
import websockets
from azure.identity import DefaultAzureCredential
app = Flask(__name__)
# CORS(app) # Enable CORS for all routes when running locally for testing
# Session configuration
session_config = {
"session": {
"type": "realtime",
"model": "<YOUR MODEL DEPLOYMENT NAME>",
"instructions": "You are a helpful assistant.",
"audio": {
"output": {
"voice": "marin",
},
},
},
}
# Get configuration from environment variables
azure_resource = os.getenv('AZURE_RESOURCE') # e.g., 'your-azure-resource'
# Token caching variables
cached_token = None
token_expiry = 0
token_lock = threading.Lock()
def get_bearer_token(resource_scope: str) -> str:
"""Get a bearer token using DefaultAzureCredential with caching."""
global cached_token, token_expiry
current_time = time.time()
# Check if we have a valid cached token (with 5 minute buffer before expiry)
with token_lock:
if cached_token and current_time < (token_expiry - 300):
return cached_token
# Get a new token
try:
credential = DefaultAzureCredential()
token = credential.get_token(resource_scope)
with token_lock:
cached_token = token.token
token_expiry = token.expires_on
print(f"Acquired new bearer token, expires at: {time.ctime(token_expiry)}")
return cached_token
except Exception as e:
print(f"Failed to acquire bearer token: {e}")
raise
def get_ephemeral_token():
"""
Generate an ephemeral token from Azure OpenAI.
Returns:
str: The ephemeral token
Raises:
Exception: If token generation fails
"""
# Get bearer token using DefaultAzureCredential
bearer_token = get_bearer_token("https://ai.azure.com/.default")
# Construct the Azure OpenAI endpoint URL
url = f"https://{azure_resource}.openai.azure.com/openai/v1/realtime/client_secrets"
headers = {
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
}
# Make the request to Azure OpenAI
response = requests.post(
url,
headers=headers,
json=session_config,
timeout=30
)
# Check if the request was successful
if response.status_code != 200:
print(f"Request failed with status {response.status_code}: {response.reason}")
print(f"Response headers: {dict(response.headers)}")
print(f"Response content: {response.text}")
response.raise_for_status()
# Parse the JSON response and extract the ephemeral token
data = response.json()
ephemeral_token = data.get('value', '')
if not ephemeral_token:
print(f"No ephemeral token found in response: {data}")
raise Exception("No ephemeral token available")
return ephemeral_token
def perform_sdp_negotiation(ephemeral_token, sdp_offer):
"""
Perform SDP negotiation with the Azure OpenAI Realtime API.
Args:
ephemeral_token (str): The ephemeral token for authentication
sdp_offer (str): The SDP offer to send
Returns:
tuple: (sdp_answer, location_header) - The SDP answer from the server and Location header for WebSocket
Raises:
Exception: If SDP negotiation fails
"""
# Construct the realtime endpoint URL - matching the v1transceiver_test pattern
realtime_url = f"https://{azure_resource}.openai.azure.com/openai/v1/realtime/calls"
headers = {
'Authorization': f'Bearer {ephemeral_token}',
'Content-Type': 'application/sdp' # Azure OpenAI expects application/sdp, not form data
}
print(f"Sending SDP offer to: {realtime_url}")
# Send the SDP offer as raw body data (not form data)
response = requests.post(realtime_url, data=sdp_offer, headers=headers, timeout=30)
if response.status_code == 201: # Changed from 200 to 201 to match the test expectation
sdp_answer = response.text
location_header = response.headers.get('Location', '')
print(f"Received SDP answer: {sdp_answer[:100]}...")
if location_header:
print(f"Captured Location header: {location_header}")
else:
print("Warning: No Location header found in response")
return sdp_answer, location_header
else:
error_msg = f"SDP negotiation failed: {response.status_code} - {response.text}"
print(error_msg)
raise Exception(error_msg)
@app.route('/token', methods=['GET'])
def get_token():
"""
An endpoint which returns an ephemeral token for Azure OpenAI Realtime API.
Uses DefaultAzureCredential for authentication with token caching.
"""
try:
ephemeral_token = get_ephemeral_token()
return jsonify({
"token": ephemeral_token,
"endpoint": f"https://{azure_resource}.openai.azure.com",
"deployment": "gpt-4o-realtime-preview"
})
except requests.exceptions.RequestException as e:
print(f"Token generation error: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response status: {e.response.status_code}")
print(f"Response reason: {e.response.reason}")
print(f"Response content: {e.response.text}")
return jsonify({"error": "Failed to generate token"}), 500
except Exception as e:
print(f"Unexpected error: {e}")
return jsonify({"error": "Failed to generate token"}), 500
async def connect_websocket(location_header, bearer_token=None, api_key=None):
"""
Connect to the WebSocket endpoint using the Location header.
Similar to the _connect_websocket function in run_v1transceiver_test.py
Args:
location_header (str): The Location header from the SDP negotiation response
bearer_token (str, optional): Bearer token for authentication
api_key (str, optional): API key for authentication (fallback)
Returns:
None: Just logs messages, doesn't store them
"""
# Extract call_id from location header
# Example: /v1/realtime/calls/rtc_abc123 -> rtc_abc123
call_id = location_header.split('/')[-1]
print(f"Extracted call_id: {call_id}")
# Construct WebSocket URL: wss://<resource>.openai.azure.com/openai/v1/realtime?call_id=<call_id>
ws_url = f"wss://{azure_resource}.openai.azure.com/openai/v1/realtime?call_id={call_id}"
print(f"Connecting to WebSocket: {ws_url}")
message_count = 0
try:
# WebSocket headers - use proper authentication
headers = {}
if bearer_token is not None:
print("Using Bearer token for WebSocket authentication")
headers["Authorization"] = f"Bearer {bearer_token}"
elif api_key is not None:
print("Using API key for WebSocket authentication")
headers["api-key"] = api_key
else:
print("Warning: No authentication provided for WebSocket")
async with websockets.connect(ws_url, additional_headers=headers) as websocket:
print("WebSocket connection established")
# Listen for messages
try:
async for message in websocket:
try:
# Parse JSON message
json_data = json.loads(message)
msg_type = json_data.get('type', 'unknown')
message_count += 1
print(f"WebSocket [{message_count}]: {msg_type}")
# Handle specific message types with additional details
if msg_type == 'response.done':
session_status = json_data['response'].get('status', 'unknown')
session_details = json_data['response'].get('details', 'No details provided')
print(f" -> Response status: {session_status}, Details: {session_details}")
# Continue listening instead of breaking
elif msg_type == 'session.created':
session_id = json_data.get('session', {}).get('id', 'unknown')
print(f" -> Session created: {session_id}")
elif msg_type == 'error':
error_message = json_data.get('error', {}).get('message', 'No error message')
print(f" -> Error: {error_message}")
except json.JSONDecodeError:
message_count += 1
print(f"WebSocket [{message_count}]: Non-JSON message: {message[:100]}...")
except Exception as e:
print(f"Error processing WebSocket message: {e}")
except websockets.exceptions.ConnectionClosed:
print(f"WebSocket connection closed by remote (processed {message_count} messages)")
except Exception as e:
print(f"WebSocket message loop error: {e}")
except Exception as e:
print(f"WebSocket connection error: {e}")
print(f"WebSocket monitoring completed. Total messages processed: {message_count}")
def start_websocket_background(location_header, bearer_token):
"""
Start WebSocket connection in background thread to monitor/record the call.
"""
def run_websocket():
try:
print(f"Starting background WebSocket monitoring for: {location_header}")
# Create new event loop for this thread
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# Run the WebSocket connection (now just logs, doesn't return messages)
loop.run_until_complete(
connect_websocket(location_header, bearer_token)
)
print("Background WebSocket monitoring completed.")
except Exception as e:
print(f"Background WebSocket error: {e}")
finally:
loop.close()
except Exception as e:
print(f"Failed to start background WebSocket: {e}")
# Start the WebSocket in a background thread
websocket_thread = threading.Thread(target=run_websocket, daemon=True)
websocket_thread.start()
print("Background WebSocket thread started")
@app.route('/connect', methods=['POST'])
def connect_and_negotiate():
"""
Get token and perform SDP negotiation.
Expects multipart form data with 'sdp' field containing the SDP offer.
Returns SDP answer as plain text (matching the v1transceiver_test behavior).
Automatically starts WebSocket connection in background to monitor/record the call.
"""
try:
# Get the SDP offer from multipart form data
if 'sdp' not in request.form:
return jsonify({"error": "Missing 'sdp' field in multipart form data"}), 400
sdp_offer = request.form['sdp']
print(f"Received SDP offer: {sdp_offer[:100]}...")
# Get ephemeral token using shared function
ephemeral_token = get_ephemeral_token()
print(f"Got ephemeral token for SDP negotiation: {ephemeral_token[:20]}...")
# Perform SDP negotiation using shared function
sdp_answer, location_header = perform_sdp_negotiation(ephemeral_token, sdp_offer)
# Create response headers
response_headers = {'Content-Type': 'application/sdp'}
# If we have a location header, start WebSocket connection in background to monitor/record the call
if location_header:
try:
# Get a bearer token for WebSocket authentication
bearer_token = get_bearer_token("https://ai.azure.com/.default")
start_websocket_background(location_header, bearer_token)
except Exception as e:
print(f"Failed to start background WebSocket monitoring: {e}")
# Don't fail the main request if WebSocket setup fails
# Return SDP answer as plain text, just like the v1transceiver_test expects
return sdp_answer, 201, response_headers
except Exception as e:
error_msg = f"Error in SDP negotiation: {e}"
print(error_msg)
return jsonify({"error": error_msg}), 500
if __name__ == '__main__':
if not azure_resource:
print("Error: AZURE_RESOURCE environment variable is required")
exit(1)
print(f"Starting token service for Azure resource: {azure_resource}")
print("Using DefaultAzureCredential for authentication")
port = int(os.getenv('PORT', 5000))
print(f" gunicorn -w 4 -b 0.0.0.0:{port} --timeout 30 token-service:app")
Les modifications de navigateur associées sont affichées ici.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Azure OpenAI Realtime Session - Connect Endpoint</title>
</head>
<body>
<h1>Azure OpenAI Realtime Session - Using /connect Endpoint</h1>
<button onclick="StartSession()">Start Session</button>
<!-- Log container for API messages -->
<div id="logContainer"></div>
<script>
const AZURE_RESOURCE = "YOUR AZURE RESOURCE NAME"
async function StartSession() {
try {
logMessage("🚀 Starting session with /connect endpoint...");
// Set up the WebRTC connection first
const peerConnection = new RTCPeerConnection();
logMessage("✅ RTCPeerConnection created");
// Get microphone access and add audio track BEFORE creating offer
logMessage("🎤 Requesting microphone access...");
try {
const clientMedia = await navigator.mediaDevices.getUserMedia({ audio: true });
logMessage("✅ Microphone access granted");
const audioTrack = clientMedia.getAudioTracks()[0];
logMessage("🎤 Audio track obtained: " + audioTrack.label);
peerConnection.addTrack(audioTrack);
logMessage("✅ Audio track added to peer connection");
} catch (error) {
logMessage("❌ Failed to get microphone access: " + error.message);
return;
}
// Set up audio playback
const audioElement = document.createElement('audio');
audioElement.autoplay = true;
document.body.appendChild(audioElement);
logMessage("🔊 Audio element created and added to page");
peerConnection.ontrack = (event) => {
logMessage("🎵 Remote track received! Type: " + event.track.kind);
logMessage("📊 Number of streams: " + event.streams.length);
if (event.streams.length > 0) {
audioElement.srcObject = event.streams[0];
logMessage("✅ Audio stream assigned to audio element");
// Add event listeners to audio element for debugging
audioElement.onloadstart = () => logMessage("🔄 Audio loading started");
audioElement.oncanplay = () => logMessage("▶️ Audio can start playing");
audioElement.onplay = () => logMessage("🎵 Audio playback started");
audioElement.onerror = (e) => logMessage("❌ Audio error: " + e.message);
} else {
logMessage("⚠️ No streams in track event");
}
};
// Set up data channel BEFORE SDP exchange
const dataChannel = peerConnection.createDataChannel('realtime-channel');
logMessage("📡 Data channel created");
dataChannel.addEventListener('open', () => {
logMessage('✅ Data channel is open - ready to send messages');
// Send client events to start the conversation
logMessage("📝 Preparing to send text input message...");
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "hello there! Can you give me some vacation options?",
},
],
},
};
logMessage("📤 Sending conversation.item.create event...");
logMessage("💬 Text content: " + event.item.content[0].text);
try {
dataChannel.send(JSON.stringify(event));
logMessage("✅ Text input sent successfully!");
// Now send response.create to trigger the AI response
const responseEvent = {
type: "response.create"
};
logMessage("📤 Sending response.create event to trigger AI response...");
dataChannel.send(JSON.stringify(responseEvent));
logMessage("✅ Response.create sent successfully!");
} catch (error) {
logMessage("❌ Failed to send text input: " + error.message);
}
});
dataChannel.addEventListener('message', (event) => {
const realtimeEvent = JSON.parse(event.data);
console.log(realtimeEvent);
logMessage("📥 Received server event: " + realtimeEvent.type);
// Log more detail for important events
if (realtimeEvent.type === "error") {
logMessage("❌ Error: " + realtimeEvent.error.message);
} else if (realtimeEvent.type === "session.created") {
logMessage("🎉 Session created successfully");
} else if (realtimeEvent.type === "response.output_audio_transcript.done") {
logMessage("📝 AI transcript complete: " + (realtimeEvent.transcript || ""));
} else if (realtimeEvent.type === "response.done") {
logMessage("✅ Response completed");
}
});
dataChannel.addEventListener('close', () => {
logMessage('❌ Data channel is closed');
});
dataChannel.addEventListener('error', (error) => {
logMessage('❌ Data channel error: ' + error);
});
// Add connection state logging
peerConnection.onconnectionstatechange = () => {
logMessage("🔗 Connection state: " + peerConnection.connectionState);
};
peerConnection.oniceconnectionstatechange = () => {
logMessage("🧊 ICE connection state: " + peerConnection.iceConnectionState);
};
// Create offer AFTER setting up data channel
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
logMessage("🤝 WebRTC offer created with audio track");
// Prepare multipart form data for /connect endpoint
const formData = new FormData();
formData.append('sdp', offer.sdp);
logMessage("📤 Sending SDP via multipart form to /connect endpoint...");
// Call our /connect endpoint with multipart form data
const connectResponse = await fetch("/connect", {
method: "POST",
body: formData // FormData automatically sets correct Content-Type
});
if (!connectResponse.ok) {
throw new Error(`Connect service request failed: ${connectResponse.status}`);
}
// Get the SDP answer directly as text (not JSON)
const answerSdp = await connectResponse.text();
logMessage("✅ Got SDP answer from /connect endpoint, length: " + answerSdp.length + " chars");
// Set up the WebRTC connection using the SDP answer
const answer = { type: "answer", sdp: answerSdp };
await peerConnection.setRemoteDescription(answer);
logMessage("✅ Remote description set");
// Add close session button
const button = document.createElement('button');
button.innerText = 'Close Session';
button.onclick = () => stopSession(dataChannel, peerConnection);
document.body.appendChild(button);
logMessage("🔴 Close session button added");
function stopSession(dataChannel, peerConnection) {
if (dataChannel) dataChannel.close();
if (peerConnection) peerConnection.close();
logMessage("Session closed.");
}
} catch (error) {
console.error("Error in StartSession:", error);
logMessage("Error in StartSession: " + error.message);
}
}
function logMessage(message) {
const logContainer = document.getElementById("logContainer");
const p = document.createElement("p");
p.textContent = message;
logContainer.appendChild(p);
}
async function init(peerConnection) {
logMessage("� Continuing WebRTC setup with existing peer connection...");
// Set up to play remote audio from the model.
const audioElement = document.createElement('audio');
audioElement.autoplay = true;
document.body.appendChild(audioElement);
logMessage("🔊 Audio element created and added to page");
peerConnection.ontrack = (event) => {
logMessage("🎵 Remote track received! Type: " + event.track.kind);
logMessage("📊 Number of streams: " + event.streams.length);
if (event.streams.length > 0) {
audioElement.srcObject = event.streams[0];
logMessage("✅ Audio stream assigned to audio element");
// Add event listeners to audio element for debugging
audioElement.onloadstart = () => logMessage("🔄 Audio loading started");
audioElement.oncanplay = () => logMessage("▶️ Audio can start playing");
audioElement.onplay = () => logMessage("🎵 Audio playback started");
audioElement.onerror = (e) => logMessage("❌ Audio error: " + e.message);
} else {
logMessage("⚠️ No streams in track event");
}
};
const dataChannel = peerConnection.createDataChannel('realtime-channel');
logMessage("📡 Data channel created");
dataChannel.addEventListener('open', () => {
logMessage('✅ Data channel is open - ready to send messages');
// Send client events to start the conversation
logMessage("📝 Preparing to send text input message...");
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [
{
type: "input_text",
text: "hello there! Can you give me some vacation options?",
},
],
},
};
logMessage("📤 Sending conversation.item.create event...");
logMessage("💬 Text content: " + event.item.content[0].text);
try {
dataChannel.send(JSON.stringify(event));
logMessage("✅ Text input sent successfully!");
// Now send response.create to trigger the AI response
const responseEvent = {
type: "response.create"
};
logMessage("📤 Sending response.create event to trigger AI response...");
dataChannel.send(JSON.stringify(responseEvent));
logMessage("✅ Response.create sent successfully!");
} catch (error) {
logMessage("❌ Failed to send text input: " + error.message);
}
});
dataChannel.addEventListener('close', () => {
logMessage('❌ Data channel is closed');
});
dataChannel.addEventListener('error', (error) => {
logMessage('❌ Data channel error: ' + error);
}); // Add connection state logging
peerConnection.onconnectionstatechange = () => {
logMessage("� Connection state: " + peerConnection.connectionState);
};
peerConnection.oniceconnectionstatechange = () => {
logMessage("🧊 ICE connection state: " + peerConnection.iceConnectionState);
};
// Add close session button
const button = document.createElement('button');
button.innerText = 'Close Session';
button.onclick = () => stopSession(dataChannel, peerConnection);
document.body.appendChild(button);
logMessage("🔴 Close session button added");
function stopSession(dataChannel, peerConnection) {
if (dataChannel) dataChannel.close();
if (peerConnection) peerConnection.close();
logMessage("Session closed.");
}
}
function logMessage(message) {
const logContainer = document.getElementById("logContainer");
const p = document.createElement("p");
p.textContent = message;
logContainer.appendChild(p);
}
</script>
</body>
</html>
Reference :DefaultAzureCredential, documentation de Flask
Étape 4 (facultatif) : Configurer le pare-feu réseau
Si vous utilisez un pare-feu réseau, l’API Realtime nécessite la règle suivante Allow côté client.
| Port | Protocoles | Règle | Plage d’adresses IP |
|---|---|---|---|
| 3478 | UDP, TCP | Permettre | Tous les sous-réseaux appartenant au AzureCloud.<Your_Microsoft_Foundry_Resource_Azure_Region>Service Tag. Exemple AzureCloud.eastus2.
Affichez la liste complète des plages d’adresses IP et des étiquettes de service Azure. |
Dépannage
Erreurs d’authentification
- 401 Non autorisé : vérifiez que votre clé API ou jeton Microsoft Entra ID est valide. Vérifiez que l’identité a le rôle Cognitive Services User attribué sur la ressource OpenAI Azure.
- 403 Interdit : vérifiez que votre ressource est déployée dans une région prise en charge (USA Est 2 ou Suède Centre).
Erreurs de connexion
-
Échec de la connexion WebRTC :
- Vérifiez que votre navigateur prend en charge WebRTC et autorise l’accès au microphone. Vérifiez que vous utilisez HTTPS (requis pour
getUserMedia). - Si vous utilisez le pare-feu réseau, vérifiez les paramètres du pare-feu.
- Vérifiez que votre navigateur prend en charge WebRTC et autorise l’accès au microphone. Vérifiez que vous utilisez HTTPS (requis pour
- Canal de données non ouvert : vérifiez les erreurs d’état de connexion ICE dans la console du navigateur. Vérifiez que le jeton éphémère n’a pas expiré.
- Échec de l’échange SDP : vérifiez que l’URL du point de terminaison WebRTC est correcte et que le jeton éphémère est valide.
Erreurs de modèle
-
Modèle introuvable : vérifiez que votre nom de déploiement correspond exactement (respectant la casse). Vérifiez que vous avez déployé un modèle en temps réel (
gpt-4o-realtime-preview,gpt-realtimeetc.). - Quota dépassé : vérifiez votre quota OpenAI Azure dans le portail Azure. L'API en temps réel dispose d'un quota distinct des finalisations de chat.
Problèmes audio
-
Aucune sortie audio : vérifier que
audioElement.autoplay = trueest défini et que les stratégies de lecture automatique du navigateur ne bloquent pas la lecture. Essayez d’abord de cliquer sur la page pour activer l’audio. - Qualité audio médiocre : WebRTC s’ajuste automatiquement pour les conditions réseau. Vérifiez votre connexion réseau et essayez de réduire le trafic réseau.
Contenu connexe
- Essayez le guide de démarrage rapide audio en temps réel.
- Consultez la référence de l’API Realtime
- En savoir plus sur Azure OpenAI quotas et limites