← 음성 어시스턴트로 돌아가기

OpenAI Realtime API 구현 가이드

WebRTC 기반 실시간 음성 대화 시스템

push_pin 핵심 개념

WebRTC 방식의 장점
이 애플리케이션은 WebSocket 방식이 아닌 WebRTC 방식을 사용합니다. 음성 데이터는 RTCPeerConnection을 통해 직접 전송되고, 제어 명령만 Data Channel로 전송되어 더 효율적입니다.
1
Ephemeral Token 방식
서버가 OpenAI에서 임시 토큰을 받아 클라이언트에 전달. API 키는 서버에만 존재하여 보안 유지
2
RTCPeerConnection
브라우저와 OpenAI 서버 간 직접 P2P 연결. 오디오 스트림을 저지연으로 전송
3
Data Channel (oai-events)
세션 설정, 이벤트 수신 등 제어 메시지를 주고받는 양방향 채널

sync 연결 프로세스

computer 브라우저
settings 우리 서버
smart_toy OpenAI
연결 시작
GET /api/token
POST /session
세션 생성
ephemeral token
토큰 받음
SDP Offer 생성
WebRTC 연결
연결 수락
check_circle WebRTC P2P 연결 완료 (음성 스트림 활성화)
session.update
설정 적용
mic 음성 대화 시작

chat 대화 흐름

사용자 입력

  1. input_audio_buffer.speech_started - 사용자가 말하기 시작
  2. UI에 "듣고 있습니다..." placeholder 표시
  3. input_audio_buffer.speech_stopped - 사용자 말하기 종료
  4. conversation.item.input_audio_transcription.completed - 텍스트 변환 완료
  5. UI에서 placeholder를 실제 텍스트로 업데이트

어시스턴트 응답

  1. response.created - 새 응답 시작 (isNewResponse = true)
  2. response.output_audio_transcript.delta - 텍스트 조각 수신 (여러 번 반복)
  3. 첫 delta에서 새 메시지 박스 생성, 이후는 같은 박스에 추가
  4. response.done - 응답 완료 (토큰 사용량 포함)
warning 중요한 구현 포인트

1. session.update 시 반드시 type 포함:
type: 'realtime'을 빼먹으면 "Missing required parameter" 에러 발생

2. 메시지 순서 보장:
비동기 transcription 때문에 순서가 엉킬 수 있음. speech_started에서 즉시 placeholder를 생성하여 해결

3. 응답 분리:
isNewResponse 플래그로 여러 응답이 하나의 메시지 박스에 섞이는 것을 방지

edit_note 주요 코드 구현

1. 토큰 발급 (서버)

// 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 });

2. WebRTC 연결 (클라이언트)

// 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
});

3. 세션 설정 업데이트

this.dataChannel.onopen = () => {
  this.sendEvent({
    type: 'session.update',
    session: {
      type: 'realtime',  // 이거 빼먹지 마세요!
      instructions: '짧고 간결하게 답변하기',
      audio: {
        input: {
          transcription: {
            model: 'whisper-1'
          }
        }
      }
    }
  });
};

4. 메시지 순서 보장 로직

// 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;
  }
});

5. 응답 분리 로직

// 새 응답 시작
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;
  }
});

track_changes 주요 이벤트 목록

radio_button_checked 클라이언트 → OpenAI

  • session.update
    세션 설정 변경
  • conversation.item.create
    새 대화 항목 생성
  • response.create
    응답 생성 요청

radio_button_checked OpenAI → 클라이언트

  • session.created
    세션 생성 완료
  • input_audio_buffer.speech_started
    사용자 말하기 시작
  • conversation.item.input_audio_transcription.completed
    사용자 음성 → 텍스트 변환
  • response.created
    응답 시작
  • response.output_audio_transcript.delta
    응답 텍스트 조각
  • response.done
    응답 완료 + 토큰 사용량

flash_on 성능 최적화

연결 시간 측정

search 문제 해결

자주 발생하는 에러

1. "Missing required parameter: 'session.type'"
→ session.update에 type: 'realtime' 추가

2. "Unknown parameter: 'session.modalities'"
→ 토큰 생성 시에는 최소한의 설정만. 세션 설정은 session.update에서

3. CSP 에러로 OpenAI 연결 차단
→ helmet 설정에 connectSrc: ["'self'", "https://api.openai.com"] 추가

4. 메시지가 엉켜서 표시됨
→ placeholder 시스템과 isNewResponse 플래그로 해결