WebRTC 기반 실시간 음성 대화 시스템
input_audio_buffer.speech_started - 사용자가 말하기 시작input_audio_buffer.speech_stopped - 사용자 말하기 종료conversation.item.input_audio_transcription.completed - 텍스트 변환 완료response.created - 새 응답 시작 (isNewResponse = true)response.output_audio_transcript.delta - 텍스트 조각 수신 (여러 번 반복)response.done - 응답 완료 (토큰 사용량 포함)type: 'realtime'을 빼먹으면 "Missing required parameter" 에러 발생// routes/api.js
const sessionConfig = {
session: {
type: 'realtime', // 필수!
model: 'gpt-realtime',
audio: {
output: {
voice: 'alloy'
}
}
}
};
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(sessionConfig)
});
const data = await response.json();
res.json({ value: data.client_secret.value });
// 1. Token 받기
const tokenResponse = await fetch('/api/token');
const tokenData = await tokenResponse.json();
const ephemeralToken = tokenData.value;
// 2. WebRTC 설정
this.peerConnection = new RTCPeerConnection();
// 3. 마이크 추가
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
stream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, stream);
});
// 4. Data Channel 생성
this.dataChannel = this.peerConnection.createDataChannel('oai-events');
// 5. SDP Offer 생성 및 전송
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
const sdpResponse = await fetch(
'https://api.openai.com/v1/realtime/calls?model=gpt-realtime',
{
method: 'POST',
body: offer.sdp,
headers: {
'Authorization': `Bearer ${ephemeralToken}`,
'Content-Type': 'application/sdp'
}
}
);
const answerSdp = await sdpResponse.text();
await this.peerConnection.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
this.dataChannel.onopen = () => {
this.sendEvent({
type: 'session.update',
session: {
type: 'realtime', // 이거 빼먹지 마세요!
instructions: '짧고 간결하게 답변하기',
audio: {
input: {
transcription: {
model: 'whisper-1'
}
}
}
}
});
};
// speech_started에서 즉시 placeholder 생성
client.on('speech.started', () => {
this.pendingUserMessage = this.addMessage('user', '듣고 있습니다...');
});
// 실제 텍스트로 업데이트
client.on('user.transcript', ({transcript}) => {
if (this.pendingUserMessage) {
const textEl = this.pendingUserMessage.querySelector('.message-text');
textEl.textContent = transcript;
this.pendingUserMessage = null;
}
});
// 새 응답 시작
client.on('response.created', () => {
this.isNewResponse = true;
});
// Delta 처리
client.on('assistant.transcript.delta', ({delta}) => {
if (this.isNewResponse) {
// 새 메시지 박스 생성
this.addMessage('assistant', delta);
this.isNewResponse = false;
} else {
// 기존 박스에 추가
const lastMessage = document.querySelector('.message-assistant:last-child');
const textEl = lastMessage.querySelector('.message-text');
textEl.textContent += delta;
}
});
session.updateconversation.item.createresponse.createsession.createdinput_audio_buffer.speech_startedconversation.item.input_audio_transcription.completedresponse.createdresponse.output_audio_transcript.deltaresponse.donetype: 'realtime' 추가connectSrc: ["'self'", "https://api.openai.com"] 추가