OpenAI Realtime 세션 매니저 — 코드레벨 동작 흐름 P0코드레벨

마지막 업데이트 2026-07-29

OpenAI Realtime 세션 매니저 — 코드레벨 동작 흐름 P0 코드…: 입력: 개요 · 범위, 주요 처리 단계: startSession() 흐름 (L665–978), 결과: 파일 · 라인 레퍼런스 흐름
동작 흐름 요약
  1. 입력: 개요 · 범위
  2. 주요 처리 단계: startSession() 흐름 (L665–978)
  3. 결과: 파일 · 라인 레퍼런스
문서 읽는 법 · 설명식

이 문서는 이렇게 읽으면 됩니다

OpenAI Realtime 세션 매니저 — 코드레벨 동작 흐름의 핵심을 설명식으로 먼저 안내합니다. 기술적 결론과 원문 근거는 아래 본문에 보존되어 있습니다.

핵심 흐름 펼쳐 보기
  1. 비유와 핵심 질문으로 먼저 전체 구조를 잡습니다.
  2. 실제 컴포넌트·파일·데이터 흐름을 따라 내려갑니다.
  3. 코드 라인과 주의사항에서 구현 근거를 확인합니다.
  • 개요 · 범위
  • 아키텍처 한눈에
  • 진입점 · 시그니처
작성일: 2026-06-14 대상: 개발자 — 핑퐁이 AI 세션의 진입점/흐름 파악 핵심 파일: apps/web/hooks/use-session-manager.ts

개요 · 범위

useSessionManager는 브라우저와 OpenAI Realtime API 사이의 WebRTC 세션 1개를 통째로 관리하는 훅이다. 마이크 캡처 → 오디오 가공 → OpenAI 송출, OpenAI 음성 수신 → 스피커 재생, 데이터채널 이벤트(전사·발화 상태) 처리, 끼어들기/침묵/종료까지 담당한다.

사용처(중요): 이 훅은 V1 게스트 릴레이(hooks/use-guest-session-relay.ts:144)와 프롬프트 테스트 페이지(components/pages/prompt-test.tsx:87)에서만 쓰인다.
V2(client-guest/SFU)는 이 훅을 쓰지 않고 별도 구현 entities/guest-session/model/use-ai-session.ts를 사용한다. 두 경로는 OpenAI Realtime 연동 로직이 평행하게 존재하므로, V2 작업 시 이 문서가 아니라 use-ai-session을 봐야 한다.

아키텍처 한눈에

[브라우저] useSessionManager
  ├─ 마이크: getUserMedia → volumeAmplifier(내장 마이크 한정) → audioProcessingChain(Gate/EQ/Comp/Limiter) → pc.addTrack
  ├─ SDP offer → createSession() ──HTTP──▶ /api/realtime/call
  │                                   └─ createCall() ──FormData(sdp+session)──▶ OpenAI /v1/realtime/calls
  │                                                 ◀── answer SDP ──┘
  ├─ pc.setRemoteDescription(answer) → pc.ontrack → audioElement.srcObject (스피커)
  └─ dataChannel "oai-events": 이벤트 송수신 (전사/발화/세션설정)

SDP를 서버로 프록시하는 이유: OPENAI_API_KEY를 브라우저에 노출하지 않기 위해. 클라이언트는 offer SDP만 보내고, Next API 라우트가 키를 붙여 OpenAI에 중계한 뒤 answer SDP를 돌려준다. 세션 instruction/voice/VAD 등 민감·정책 설정도 서버에서 조립된다.

진입점 · 시그니처

훅 옵션 (UseSessionManagerProps, L36–52)

옵션역할
onLogChatLog 생성 콜백 (user/assistant 전사, text/auto/fallback)
rtcConfigurationICE/TURN 서버 설정 (ref로 보관, 런타임 갱신 가능)
onPeerTalkingStatusChanged / onUserTalkingStatusChanged핑퐁이/아동 발화 상태 변화 통지
onNoGuestAudio15초간 입력 무음 시 호출 (침묵 타이머)
onAutoFinish자동 종료 조건 충족 시 (스텝 전환용)
onWebRTCDisconnected / onWebRTCFailed연결 끊김(즉시) / 5초 지속 또는 failed(세션 중단)
onAssistantTurnFinalized핑퐁이 응답 턴의 SessionLog id 확정 시점
enableSocketEmission, roomId게스트 역할일 때 소켓으로 상태 송출(WebRTC 통계·증폭 상태)
initialSilenceDurationMs프로필 저장 VAD 기본값. 없으면 VAD_FALLBACK_MS

StartSessionParams (L54–67) · 반환값

startSession(){ dataChannel, outputAudioStream, inputAudioStream }을 반환한다. outputAudioStream이 곧 핑퐁이 음성 스트림이며, 게스트 릴레이가 이를 호스트에게 그대로 add한다.

startSession() 흐름 (L665–978)

  1. 1동시성 가드 진입. requestingActivityIdRef로 같은/다른 activity 재요청을 구분(로그만). 실제 무효화는 아래 requestId로 처리.
  2. 2세션 파라미터를 ref에 반영. activityRef, turnDetectionRef.interrupt_response, autoFinishConfig, startMent, autoTransition/fallback 활성화.
  3. 3RTCPeerConnection 생성(rtcConfigurationRef). WebRTC 모니터링 상태추적·상태감시 시작.
  4. 4스피커(audioElement) 준비. createAudioElement()<audio autoplay> 생성 + setSinkId로 출력 장치 라우팅. pause/error/playing/ended 리스너로 재생 이상 추적.
  5. 5수신 트랙 바인딩. connection.ontrack = e => audioElement.srcObject = e.streams[0] (핑퐁이 음성 재생).
  6. 6마이크 스트림 구성. createMediaStream() → 내장 마이크면 volumeAmplifier, 항상 audioProcessingChain 적용 → 각 트랙 enabled=enableMicrophoneconnection.addTrack. audioProcessingChain.startProcessing() 시작.
  7. 7데이터채널 생성. createDataChannel("oai-events") + message/open 리스너. (open 시 isActive=true)
  8. 8세션 컨텍스트 조립. avatar/subAvatars/user 조회, voice 결정(avatar.voice || "sage").
  9. 9requestId 발급. requestId=`${Date.now()}-${Math.random()}`tokenRequestIdRef에 저장(최신 요청 식별).
  10. 10SDP 협상. createOffer→setLocalDescriptioncreateSession(offer.sdp,...)로 answer SDP 수신.
  11. 11최신 요청 검증. tokenRequestIdRef !== requestId면 더 새 요청이 시작된 것 → throw START_FAILED(이 결과 폐기).
  12. 12연결 확정. setRemoteDescription(answer)peerConnectionRef 저장 → audioStream 도착까지 100ms 폴링 대기(isActiveRef && audioStream).
  13. 13후속 시작. 볼륨 모니터링·소켓 증폭상태 콜백·STT 폴백·트랙/상태 모니터링 시작. autoTransition이면 AUTO_START_MESSAGE("대화시작") 또는 startMent 송신.
finally 주의: 성공/실패와 무관하게 requestingActivityIdRef=null로 리셋. 실패 시 catch에서 stopSession() 호출 후 SessionError("START_FAILED") 재throw.

세션 생성 (SDP 프록시) createSession → /api/realtime/call → createCall

클라이언트 createSession()(use-session-manager.ts:1036)은 10초 AbortController 타임아웃으로 /api/realtime/call에 POST한다. 라우트(app/api/realtime/call/route.ts)는 createCall()(lib/openai-create-call.ts)을 호출하고, 여기서 실제 OpenAI 요청·세션 config 조립·재시도가 일어난다.

세션 config 구조 (openai-create-call.ts:84–103)

const sessionConfig = {
  type: "realtime",
  model: resolvedModel,            // 모델 버전 매핑(아래)
  instructions: finalInstruction,  // 변수치환된 프롬프트(prompt/preset/user vars)
  audio: {
    input: {
      transcription: { model: "gpt-4o-transcribe", language: "ko" },
      noise_reduction: { type: inputAudioNoiseReduction },  // 기본 far_field
      turn_detection: turnDetection,
    },
    output: { voice: voice },      // avatar.voice || "sage"
  },
};

모델 버전 매핑 (L27–30, packages/shared constants)

realtimeModelVersion실제 모델
v1.0 (DEFAULT_REALTIME_MODEL_VERSION)gpt-realtime-2025-08-28
v1.5gpt-realtime-1.5

turn_detection (VAD) (L43–59)

재시도 · 타임아웃

서버(createCall): 최대 3회 재시도, 각 시도 10초 타임아웃, 실패 간 1초 대기. answer SDP가 "v="로 시작하지 않으면 invalid로 간주.

클라이언트(createSession): 별도 10초 AbortController. 초과 시 SessionError("SESSION_TIMEOUT"). ⚠️ 서버가 3회×10초까지 갈 수 있어 클라 10초가 먼저 끊길 수 있음(타임아웃 계층 불일치는 알아둘 것).

오디오 파이프라인

입력(마이크 → OpenAI) createMediaStream, L996–1034

getUserMedia(autoGainControl:false, echoCancellation:true, noiseSuppression:true)
  └─▶ [내장 마이크면] volumeAmplifier.processMediaStream
       └─▶ audioProcessingChain.processMediaStream (항상)
           └─▶ pc.addTrack

출력(OpenAI → 스피커)

데이터채널 이벤트 처리 handleDataChannelMessage, L332–522

이벤트동작
session.created / session.updated서버가 확정한 turn_detectionturnDetectionRef에 동기화
input_audio_buffer.speech_started침묵타이머 해제, STT폴백 중단, autoFinish 아동발화 시작, isInputAudioBufferPlaying=true, peerTalking=false
input_audio_buffer.speech_stopped(폴백 활성 시 50ms 후 STT폴백 재시작), autoFinish 아동발화 종료, 끼어들기 OFF면 즉시 interruptBlocked=true(잔여음 차단), playing=false
output_audio_buffer.started침묵타이머 해제, STT폴백 중단(에코방지), peerTalking=true, 끼어들기 OFF면 interruptBlocked=true
output_audio_buffer.stopped / .cleared(500ms 후 폴백 재시작), peerTalking=false, interruptBlocked=false, 침묵타이머 시작, 직전 핑퐁이 전사로 autoFinish 판정
...input_audio_transcription.completed아동 음성 전사 → ChatLog(role:user, type:audio) 생성 + createLog(DB) + onLog
conversation.item.added (user/input_text)텍스트 입력 로그. AUTO_START_MESSAGE면 type=auto, 폴백 전사와 동일하면 type=fallback, 아니면 text
response.output_audio_transcript.done핑퐁이 응답 전사 → ChatLog(role:assistant) + onAssistantTurnFinalized(item_id). 자동종료 문구 판정(after-pingpong-speech)

파생 상태 · 끼어들기 제어

발화 상태

마이크 입력 게이팅 (L1233–1237)

// 세 조건이 모두 만족해야 마이크 트랙 enabled
audioInputEnabled = microphoneEnabled && !autoFinishSilence && !interruptBlocked;
끼어들기(interrupt) 동작: interrupt_response=false(기본)면 아동 발화 종료(speech_stopped) 또는 핑퐁이 발화 시작(output started) 즉시 interruptBlocked=true로 마이크를 닫아 핑퐁이 말 도중 아동 음성이 끼어드는 것을 막는다. 핑퐁이 발화 종료 시 해제. 런타임 토글은 toggleInterruptResponse()session.update로 OpenAI에 반영.

응답 취소 cancelResponse, L230

response.cancel + output_audio_buffer.clear 두 메시지를 전송. sendMessage에서 핑퐁이 발화 중 텍스트 입력 시, after-child-speech 자동종료 시 호출된다.

침묵 타이머 · WebRTC 연결 처리

종료 (stopSession) 정리 순서 L592–663

  1. dataChannel 리스너 제거 + close
  2. peerConnection close
  3. mediaStream 트랙 stop
  4. audioElement 정리(srcObject 트랙 stop, 리스너 제거)
  5. 각종 ref/타이머 초기화 + webrtcMonitoring 중지(closed 송출)
  6. sttFallback·volumeAmplifier·audioProcessingChain·autoFinish 정리
  7. isActive=false, requestId/requestingActivityId 리셋(재시도 허용)

통합 서브 훅 (별도 문서 예정)

역할로드맵
useVolumeAmplifier내장 마이크 자동 게인#17
useAudioProcessingChainGate→EQ→Comp→Limiter DSP#7
useSttFallbackWeb Speech API 폴백 전사#18
useSessionAutoFinish자동 종료(문구/침묵 기반)(Activity #12 연계)
useWebRTCMonitoring연결 상태·getStats 통계 송출#19

함정 · 주의

파일 · 라인 레퍼런스

파일역할
apps/web/hooks/use-session-manager.ts훅 본체(세션 라이프사이클·이벤트·상태)
apps/web/app/api/realtime/call/route.tsSDP 프록시 엔드포인트
apps/web/lib/openai-create-call.tsOpenAI 호출·세션 config 조립·재시도
apps/web/lib/constants.tsREALTIME_MODEL, DEFAULT_VOICE("sage"), AUTO_START_MESSAGE("대화시작")
packages/shared/src/utils/constants.tsDEFAULT_REALTIME_MODEL_VERSION("v1.0")
apps/web/hooks/use-guest-session-relay.ts:144소비처(V1 게스트 릴레이)
apps/web/components/pages/prompt-test.tsx:87소비처(프롬프트 테스트)

관련 문서