마지막 업데이트 2026-07-22
useSessionManager는 브라우저와 OpenAI Realtime API 사이의 WebRTC 세션 1개를 통째로 관리하는 훅이다. 마이크 캡처 → 오디오 가공 → OpenAI 송출, OpenAI 음성 수신 → 스피커 재생, 데이터채널 이벤트(전사·발화 상태) 처리, 끼어들기/침묵/종료까지 담당한다.
hooks/use-guest-session-relay.ts:144)와 프롬프트 테스트 페이지(components/pages/prompt-test.tsx:87)에서만 쓰인다.
entities/guest-session/model/use-ai-session.ts를 사용한다. 두 경로는 OpenAI Realtime 연동 로직이 평행하게 존재하므로, V2 작업 시 이 문서가 아니라 use-ai-session을 봐야 한다.
SDP를 서버로 프록시하는 이유: OPENAI_API_KEY를 브라우저에 노출하지 않기 위해. 클라이언트는 offer SDP만 보내고, Next API 라우트가 키를 붙여 OpenAI에 중계한 뒤 answer SDP를 돌려준다. 세션 instruction/voice/VAD 등 민감·정책 설정도 서버에서 조립된다.
| 옵션 | 역할 |
|---|---|
onLog | ChatLog 생성 콜백 (user/assistant 전사, text/auto/fallback) |
rtcConfiguration | ICE/TURN 서버 설정 (ref로 보관, 런타임 갱신 가능) |
onPeerTalkingStatusChanged / onUserTalkingStatusChanged | 핑퐁이/아동 발화 상태 변화 통지 |
onNoGuestAudio | 15초간 입력 무음 시 호출 (침묵 타이머) |
onAutoFinish | 자동 종료 조건 충족 시 (스텝 전환용) |
onWebRTCDisconnected / onWebRTCFailed | 연결 끊김(즉시) / 5초 지속 또는 failed(세션 중단) |
onAssistantTurnFinalized | 핑퐁이 응답 턴의 SessionLog id 확정 시점 |
enableSocketEmission, roomId | 게스트 역할일 때 소켓으로 상태 송출(WebRTC 통계·증폭 상태) |
initialSilenceDurationMs | 프로필 저장 VAD 기본값. 없으면 VAD_FALLBACK_MS |
startSession()는 { dataChannel, outputAudioStream, inputAudioStream }을 반환한다. outputAudioStream이 곧 핑퐁이 음성 스트림이며, 게스트 릴레이가 이를 호스트에게 그대로 add한다.
requestingActivityIdRef로 같은/다른 activity 재요청을 구분(로그만). 실제 무효화는 아래 requestId로 처리.activityRef, turnDetectionRef.interrupt_response, autoFinishConfig, startMent, autoTransition/fallback 활성화.rtcConfigurationRef). WebRTC 모니터링 상태추적·상태감시 시작.createAudioElement()로 <audio autoplay> 생성 + setSinkId로 출력 장치 라우팅. pause/error/playing/ended 리스너로 재생 이상 추적.connection.ontrack = e => audioElement.srcObject = e.streams[0] (핑퐁이 음성 재생).createMediaStream() → 내장 마이크면 volumeAmplifier, 항상 audioProcessingChain 적용 → 각 트랙 enabled=enableMicrophone 후 connection.addTrack. audioProcessingChain.startProcessing() 시작.createDataChannel("oai-events") + message/open 리스너. (open 시 isActive=true)avatar.voice || "sage").requestId=`${Date.now()}-${Math.random()}` → tokenRequestIdRef에 저장(최신 요청 식별).createOffer→setLocalDescription → createSession(offer.sdp,...)로 answer SDP 수신.tokenRequestIdRef !== requestId면 더 새 요청이 시작된 것 → throw START_FAILED(이 결과 폐기).setRemoteDescription(answer) → peerConnectionRef 저장 → audioStream 도착까지 100ms 폴링 대기(isActiveRef && audioStream).AUTO_START_MESSAGE("대화시작") 또는 startMent 송신.requestingActivityIdRef=null로 리셋. 실패 시 catch에서 stopSession() 호출 후 SessionError("START_FAILED") 재throw.클라이언트 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 조립·재시도가 일어난다.
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" }, };
| realtimeModelVersion | 실제 모델 |
|---|---|
v1.0 (DEFAULT_REALTIME_MODEL_VERSION) | gpt-realtime-2025-08-28 |
v1.5 | gpt-realtime-1.5 |
server_vad · threshold 0.35 · prefix_padding 500ms · silence_duration silenceDurationMs ?? 2000 · create_response true · interrupt_response falseactivity.semanticVad가 "none"이 아니면 → semantic_vad로 교체(eagerness=low|medium|high|auto). server_vad의 threshold/padding/silence는 무시되고 create/interrupt만 승계.서버(createCall): 최대 3회 재시도, 각 시도 10초 타임아웃, 실패 간 1초 대기. answer SDP가 "v="로 시작하지 않으면 invalid로 간주.
클라이언트(createSession): 별도 10초 AbortController. 초과 시 SessionError("SESSION_TIMEOUT"). ⚠️ 서버가 3회×10초까지 갈 수 있어 클라 10초가 먼저 끊길 수 있음(타임아웃 계층 불일치는 알아둘 것).
audioDeviceIdOrStream이 이미 MediaStream이면 getUserMedia 생략(외부에서 준비한 스트림 사용).!audioInputDevice || isBuiltInDevice(...)). 오디오 처리 체인은 항상 적용(초기화가 startProcessing 전제).connection.ontrack의 event.streams[0]을 audioElement.srcObject로 연결. 같은 스트림이 outputAudioStream으로 반환되어 릴레이/립싱크/노이즈가드가 공유한다.createAudioElement의 setSinkId(미지원 브라우저는 무시).| 이벤트 | 동작 |
|---|---|
session.created / session.updated | 서버가 확정한 turn_detection을 turnDetectionRef에 동기화 |
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) |
output_audio_buffer.started/stopped로 직접 토글.isInputAudioBufferPlaying || sttFallback.isPlaying || isTextareaTyping 합성(L1254).// 세 조건이 모두 만족해야 마이크 트랙 enabled
audioInputEnabled = microphoneEnabled && !autoFinishSilence && !interruptBlocked;
interrupt_response=false(기본)면 아동 발화 종료(speech_stopped) 또는 핑퐁이 발화 시작(output started) 즉시 interruptBlocked=true로 마이크를 닫아 핑퐁이 말 도중 아동 음성이 끼어드는 것을 막는다. 핑퐁이 발화 종료 시 해제. 런타임 토글은 toggleInterruptResponse() → session.update로 OpenAI에 반영.response.cancel + output_audio_buffer.clear 두 메시지를 전송. sendMessage에서 핑퐁이 발화 중 텍스트 입력 시, after-child-speech 자동종료 시 호출된다.
startSilenceTimer, L319): 세션 활성+입력 가능 상태에서 발화가 없으면 15초 뒤 onNoGuestAudio(). speech/output 이벤트마다 해제·재설정.handleWebRTCConnectionStateChange, L160): disconnected→즉시 onWebRTCDisconnected() + 5초 타이머 후 onWebRTCFailed(). failed→즉시 onWebRTCFailed(). connected→타이머 해제.isActive=false, requestId/requestingActivityId 리셋(재시도 허용)| 훅 | 역할 | 로드맵 |
|---|---|---|
useVolumeAmplifier | 내장 마이크 자동 게인 | #17 |
useAudioProcessingChain | Gate→EQ→Comp→Limiter DSP | #7 |
useSttFallback | Web Speech API 폴백 전사 | #18 |
useSessionAutoFinish | 자동 종료(문구/침묵 기반) | (Activity #12 연계) |
useWebRTCMonitoring | 연결 상태·getStats 통계 송출 | #19 |
state + ref 쌍으로 존재(isActive/isActiveRef, audioInputEnabled/audioInputEnabledRef 등). 수정 시 양쪽 동기화 필수.createSession 호출부(L1057)가 "far_field"를 하드코딩 전송. 런타임 변경은 updateInputAudioNoiseReduction/toggleNoiseReduction의 session.update로만.use-ai-session.ts. 동일 버그를 두 곳에 각각 고쳐야 할 수 있음.tokenRequestIdRef로 이전 응답을 폐기. 이 가드를 건드리면 race로 유령 세션이 생길 수 있음.| 파일 | 역할 |
|---|---|
| apps/web/hooks/use-session-manager.ts | 훅 본체(세션 라이프사이클·이벤트·상태) |
| apps/web/app/api/realtime/call/route.ts | SDP 프록시 엔드포인트 |
| apps/web/lib/openai-create-call.ts | OpenAI 호출·세션 config 조립·재시도 |
| apps/web/lib/constants.ts | REALTIME_MODEL, DEFAULT_VOICE("sage"), AUTO_START_MESSAGE("대화시작") |
| packages/shared/src/utils/constants.ts | DEFAULT_REALTIME_MODEL_VERSION("v1.0") |
| apps/web/hooks/use-guest-session-relay.ts:144 | 소비처(V1 게스트 릴레이) |
| apps/web/components/pages/prompt-test.tsx:87 | 소비처(프롬프트 테스트) |