마지막 업데이트 2026-10-07
신고: "대화시작" 텍스트가 전송된 뒤 AI 발화가 생성되지 않고 무응답. 진행자가 "답답함"을 입력하자 그제야 대화시작 멘트를 말함.
결론: 서버나 AI 모델 문제가 아니라 아동 화면 코드가 시작 응답을 스스로 취소했다. 영상 스텝이 끝나 AI 스텝으로 넘어간 직후, 숨겨진 영상 요소가 끝난 영상을 0초부터 다시 재생했다. 이 재생 이벤트를 "영상 스텝 시작"으로 처리해 cancelResponse()가 불렸고, 방금 보낸 "대화시작"의 응답이 LiveKit interrupt로 끊겼다.
원인 층: ① 직접 원인 — handleVideoStart가 현재 스텝이 영상 스텝인지 확인하지 않고 응답을 취소한다. ② 계기 — 아동 화면의 isPlaying이 항상 true라 끝나서 멈춘 영상이 비영상 스텝에서도 다시 재생된다. ③ 드러난 이유(추정) — PPI-1351 이후 시작 메시지가 잘못된 취소보다 먼저 나가 실제 응답에 걸린다(5절).
같은 회기에서 같은 일이 3번 일어났다 (18:41:24, 18:46:07, 18:47:52). "영상 → AI 스텝" 전환 4번 중 3번이라 드문 경쟁 상태가 아니라 이 구조에서 반복되는 버그다.
"대화시작"은 대화 기록에 남아 있었다. 그래서 진행자의 다음 텍스트("답답함")에 대한 첫 응답(response-1)이 대화시작 멘트가 됐다.
"늦게 도착한 ended 이벤트 때문"이 아니다. 영상이 near-end/watchdog으로 일찍 종료 처리돼 ended가 스텝 전환 뒤에 오는 경우가 많지만 필수 조건이 아니다. 18:33 건은 ended가 시작 메시지보다 먼저 왔는데도 똑같이 다시 재생됐고, 18:47 건은 ended 이벤트 처리 전에 재생 요청이 먼저 나갔다(4절). 다시 재생은 "영상이 끝나 멈춘 상태 + isPlaying 항상 true"면 스텝 전환 렌더 때 언제든 일어난다.
interrupt 이유가 host_cancel_ai_response로 찍혔지만 진행자가 누른 것이 아니다. 진행자 로그 5,022줄에 취소·interrupt 조작은 0건이다. buildLiveKitManualInterruptPayload()의 기본 이유값이 그대로 나간 것이다.
이슈 분류기(ppi-issue-classifier)는 "네트워크 × 연결 60%"로 오분류했다. 분류 체계에 "클라이언트가 AI 응답을 잘못 취소함"을 담을 유형이 없고, 시작 메시지 직후 interrupt 패턴을 보는 규칙도 없다.
18:41:24.469 "상황 영상" 87.912초 / 87.936초에서 near-end fallback → onEnd() (ended:false, 실제 끝 전) → handleVideoEnd → goToNextStep() 18:41:24.474 Moving to next step 5/0 → 5/1 "도원 감정파악" (split:image, AI 스텝) 18:41:24.488 Sending start message "대화시작" 4자 → LiveKit user text 18:41:24.489 prepared_session_activated (PPI-1351, 영상 중 미리 붙여둔 세션 활성화) 18:41:24.516 Video ended event fired (장면은 이미 5/1, paused:true, ended:true) ← 순서상 끼어 있을 뿐 인과 아님 18:41:24.537 Attempting programmatic play from playback effect ← isPlaying 항상 true + 영상이 멈춘 상태 → play() 18:41:24.562 Video play event fired currentTime 0 (끝난 영상이 처음부터 재생, 화면엔 숨김) 18:41:24.563 Notifying video play started ← handleVideoStart 18:41:24.700 Sent LiveKit manual interrupt reason: host_cancel_ai_response 18:41:29.509 No response from OpenAI within 5 seconds … 32초 무응답 … 18:41:56.522 진행자 텍스트 3자("답답함") 전송 18:41:57.837 AI response-1 "리호야, 방금 단톡방에서 친구들이랑 대화를 나눴는데…" (대화시작 멘트)
① 영상 조기 종료
apps/web/shared/ui/meet-video.tsx:779 currentTime ≥ duration−0.1 → onEndCalledRef=true, props.onEnd() (near-end fallback)
apps/web/shared/ui/meet-video.tsx:189 watchdog onEnd (watchdog backstop)
└ guest-layout-content.tsx handleVideoEnd → session.goToNextStep()
② 다음 AI 스텝에서 시작 메시지
entities/guest-page-session/model/use-step-transition.ts prepared_session_activate (PPI-1351)
└ entities/guest-session/model/use-ai-session.ts:1427 "Sending start message"
③ 영상이 끝나 멈춘 상태로 남음 (ended 이벤트 도착 시점과 무관)
meet-video.tsx:650 handleEnded — onEndCalledRef 이미 true라 onEnd 재호출은 없음
요소는 paused/ended 상태로 숨겨진 채 유지 (스텝 전환 전에 끝났어도 같음, 18:33 건)
④ 끝난 영상 재생
guest-layout-content.tsx:222 <MeetVideo key="meet-video"> 항상 마운트, 비영상 스텝에선 숨김만
guest-layout-content.tsx:236 isPlaying={isSessionProp ? true : …} ← 아동 화면은 항상 true
meet-video.tsx:471 if (props.isPlaying && video.readyState >= 3 && video.paused) video.play() ← ended도 다시 틂
⑤ 재생 = 영상 스텝 시작으로 처리
meet-video.tsx:532 handlePlay → :563 props.onStart()
└ guest-layout-content.tsx:153 handleVideoStart
if (isSessionProp && !hasNotifiedPlayRef.current) ← videoResource 바뀌면 false로 초기화
notifyVideoPlayStarted(...)
cancelResponseForVideoStepStart(props.session) ← 현재 스텝 종류 확인 없음
⑥ 응답 취소
shared/lib/video-step-response.ts:149 controller.cancelResponse() (#885 2026-07-16 도입: "LiveKit 영상 step 시작 시 응답 취소")
└ use-ai-session.ts:840 cancelResponse → :877 liveKitSession.interrupt()
└ lib/voice-agent/livekit-client-session.ts:737 reason 기본값 "host_cancel_ai_response"
원본: ppi-issue-classifier/inputs/김리호_18회기/session-20261006.jsonl.gz (guest 10,209줄 + monitor 5,022줄). 시각은 KST.
| 전환 | 영상 종료 방식 | 시작 메시지 | 0초 재생 | 영상 시작 알림 | interrupt | 결과 |
|---|---|---|---|---|---|---|
| 18:33:25 잘듣기 대회 영상 → 질문 | near-end fallback | .921 | .938 | 없음 | 없음 | 정상 (1.5초 뒤 response-1) |
| 18:41:24 상황 영상 → 도원 감정파악 | near-end fallback | .488 | .562 | .563 | .700 (+0.21초) | 5초 무응답 |
| 18:46:07 도원 등장 → 도원시비 | watchdog backstop | .051 | .113 | .113 | .411 (+0.36초) | 5초 무응답 |
| 18:47:52 소율 등장 → 소율의 고민상담 1 | watchdog backstop | .172 | .232 | .232 | .555 (+0.38초) | 5초 무응답 |
| 전환 | Video ended event fired | 다시 재생 요청 | 결과 |
|---|---|---|---|
| 18:33 | .893 — 시작 메시지(.921)보다 먼저 | .896 | 정상 (알림 없음) |
| 18:41 | .516 — 시작 메시지 뒤 | .537 | 취소 |
| 18:46 | .083 — 시작 메시지 뒤 | .088 | 취소 |
| 18:47 | .220 — 재생 요청(.205)보다 늦음 | .205 (이미 ended:true) | 취소 |
결과를 가른 것은 ended 도착 순서가 아니라 Notifying video play started(→ 응답 취소) 발생 여부다.
cancelResponseForVideoStepStart는 2026-07-16(#885)부터 있었고, 끝난 영상의 재생도 그 전부터 가능했던 동작이다. 달라진 것은 시작 메시지가 나가는 시점이다.
PPI-1351 이전 — 영상 스텝 다음 AI 스텝에서 세션을 새로 연결했다. 연결에 약 1초가 걸려 시작 메시지는 잘못된 취소(+0.1초 안팎)보다 뒤에 나갔다. 취소할 응답이 없어 영향이 없었을 것이다.
PPI-1351 이후 (2026-10-02 병합, ppi-web@3.4.6~) — 영상 중 미리 붙여둔 세션을 쓰므로 시작 메시지가 스텝 전환 14ms 뒤 나간다. 그 뒤 0.1~0.2초에 오는 잘못된 취소가 시작 응답을 정확히 끊는다.
이 수업 당시 클라이언트 버전은 로그에 남지 않아 3.4.6 이상이었는지는 확인하지 못했다. 다만 4건 모두 prepared 경로를 탔으므로 PPI-1351 코드가 실행된 것은 확실하다. 관련 설계: PPI-1351 첫 영상 AI 세션 사전 준비
guest-layout-content.tsx handleVideoStart에서 현재 레이아웃이 영상 스텝일 때만 취소한다. #885의 원래 목적(영상 스텝 위로 AI 발화가 흐르는 것 방지)은 그대로 유지된다.
const handleVideoStart = useCallback(() => {
if (isSessionProp && !hasNotifiedPlayRef.current) {
hasNotifiedPlayRef.current = true;
props.session.notifyVideoPlayStarted(...);
+ const isVideoStep = layoutMode === "full:video" || layoutMode === "split:video";
+ if (isVideoStep) cancelResponseForVideoStepStart(props.session);
- cancelResponseForVideoStepStart(props.session);
}
}, [isSessionProp, props, layoutMode]);
비영상 스텝에서 끝나서 멈춘 영상을 다시 재생하지 않게 한다. ended 도착 시점을 고치는 것(조기 종료 판정 제거 등)은 해결책이 아니다. 두 가지 방법 중 하나.
cancelResponse에 이유를 받아 liveKitSession.interrupt(reason)로 넘긴다 (video_step_start, host_cancel, non_ai_step, noise_guard 등). 이번처럼 진행자가 누르지 않은 취소가 host_cancel_ai_response로 찍혀 원인을 헷갈리게 하는 일을 막는다.
A + C를 먼저 적용해 재발을 막고 원인 추적을 쉽게 한다. B는 영상 재생 상태 전체에 영향이 있으므로 실기기 확인을 거쳐 별도로 진행한다.
| 항목 | 방법 |
|---|---|
| 단위 테스트 | 비영상 레이아웃에서 onStart가 와도 cancelResponse가 불리지 않음 / 영상 레이아웃에서는 불림 |
| 재현 | 첫 스텝이 full:video이고 다음 스텝이 AI인 활동 (예: (S2)주도16_3a 상황파악, 16_4 시비거는 도원, 16_5a 고민상담 소율). 영상 끝 → 시작 멘트가 1~2초 안에 나오는지 |
| 회귀 (#885·PPI-1233·PPI-1252) | AI 발화 중 영상 스텝으로 넘어갈 때 AI 소리가 영상 위로 흐르지 않는지 |
| 실기기 | Windows Chrome, iPad Safari, Android Chrome에서 영상 → AI 전환 각 3회. near-end·watchdog 두 종료 경로 모두 |
| 로그 확인 | 전환 직후 Sent LiveKit manual interrupt와 No response from OpenAI within 5 seconds가 없을 것 |
시작 메시지 후 interrupt가 없는데 무응답이면 다른 원인이다. agent 쪽 턴 폐기는 이현우 18회기 AI 무응답 (Half Cascade 턴 폐기) 참고.