PPI · FEATURES · SESSION LIFECYCLE

PPI-1351 첫 영상 AI 세션 사전 준비 — 데이터 흐름과 prepare 상태

마지막 업데이트 2026-10-02

작성 2026-10-02 코드 커밋 cef0b7d7 PR #1151 실기기 검증 미실행
한 줄 요약. 새 활동의 첫 full:video 재생 동안 LiveKit 방 접속과 Agent 준비를 백그라운드로 끝내 둡니다. AI 스텝에 들어가는 순간 준비된 연결을 활성화해 영상→AI 전환의 연결 대기를 줄입니다. 핵심은 “연결 준비”와 “대화 시작”의 분리입니다. 준비 중에는 AI가 듣지도, 말하지도, 메시지를 받지도 않습니다.

1. 코드 경로

파일역할
apps/web/entities/guest-page-session/lib/first-video-ai-prepare.ts (신규)준비 대상 판정 shouldPrepareAiSessionForStep, 롤백 플래그 aiPrepare, stepRequiresSession 이동
apps/web/entities/guest-page-session/model/use-step-transition.ts첫 영상에서 준비 요청, AI 스텝에서 준비 합류·활성화·fallback, 활동 이동 시 준비 세션 정지
apps/web/entities/guest-session/model/use-ai-session.tsstartSession({prepareOnly}), prepareSession·hasPreparedSession·waitForPreparedSession·activatePreparedSession, 텍스트 차단, 준비 무효화
*.test.ts 2개판정 helper 6건, 전환 훅 시나리오 7건 (fake aiSession)

2. 목적과 범위

3. 데이터 흐름 (순서대로)

① 새 활동 진입, 첫 스텝이 full:video
   use-step-transition.ts:111   이전 세션(활성/준비) 있으면 정지
   use-step-transition.ts:141   응답 차단 ON (영상 구간)
   use-step-transition.ts:149   판정: 새 활동 + full:video + 뒤에 AI 스텝 + 플래그 ON
   use-step-transition.ts:159   prepareSession() 호출, 결과를 기다리지 않음 → 영상은 그대로 재생
        │
        ▼
② 준비 시작
   use-ai-session.ts:3012       prepareSession: startSession({prepareOnly:true})
                                pendingPrepareRef = 진행 중 Promise (나중에 합류용)
   use-ai-session.ts:1685       preparedSessionRef = {활동ID, 세대, ready:false}
        │
        ▼
③ 런타임 확인 · 입력 차단
   use-ai-session.ts:1877       LiveKit이 아니면 조용히 중단 → AI 스텝에서 기존 시작
   use-ai-session.ts:1897       입력 정책 transition_blocked = true (연결·게시 전)
        │
        ▼
④ LiveKit 연결 · Agent 준비
   use-ai-session.ts:2501       5초 마이크 fallback 예약 안 함
   use-ai-session.ts:2528       waitUntilAgentReady()
   use-ai-session.ts:2564       ready:true → "session_prepared" 로그 후 return
                                (첫 인사 없음, active 아님)
        │   영상 재생 중:
        │   · 진행자/수동 텍스트 → :748 차단
        │   · Agent 응답 소리 → responseBlocked로 출력 차단
        │   · 연결 끊김/실패/ASR 실패 → :1741 준비 무효화
        ▼
⑤ 영상 종료 → 같은 활동 AI 스텝 진입
   use-step-transition.ts:170   응답 차단 OFF
   use-step-transition.ts:173   준비 진행 중이면 같은 시도에 합류해 대기
   use-step-transition.ts:179   응답 차단 OFF 재적용 (준비 실패 정리가 다시 걸었을 경우 대비)
                                듣기 의도가 있으면 마이크 예약 (첫 응답 뒤 켜짐)
   use-step-transition.ts:187   activatePreparedSession()
        │
        ▼
⑥ 활성화
   use-ai-session.ts:3059       재사용 판정: ready + 같은 활동 + 같은 세대 + 연결 있음
   use-ai-session.ts:3078       준비 표시 해제 → 목적 AI 스텝의 stepIndex·startMent·autoFinish 적용
   use-ai-session.ts:3085       입력 차단 해제 (실제 마이크는 첫 응답 뒤 열림)
   use-ai-session.ts:3089       첫 인사 1회 → active = true
        │
        ├─ 재사용 불가 → 기존 handleStartSession (현재와 같은 동작)
        ▼
⑦ 대화 진행 (기존 흐름과 동일)

정리 경로: 다른 활동 이동(:111) · 세션 정지(use-ai-session.ts:1510) · 언마운트
           → 준비 세션도 함께 닫힘

줄 번호는 커밋 cef0b7d7 기준입니다.

4. prepare 상태 정의

prepare 상태는 AI 연결과 Agent 준비는 끝났지만 대화는 아직 허용하지 않은 상태입니다. use-ai-session.ts의 ref 두 개로 표현합니다.

상태pendingPrepareRefpreparedSessionRefisActive의미
idlenullnullfalse준비 없음
preparingPromise{ready:false}false방 접속·Agent 준비 대기 중
preparednull{ready:true}false연결 완료, 대화 금지
activenullnulltrue일반 대화 상태 (기존과 동일)

preparedSessionRef = {activityId, generation, ready}. 이 연결을 어느 활동의 몇 번째 시작 시도가 만들었는지 기록합니다.

전이

idle ──prepareSession()──▶ preparing ──Agent ready──▶ prepared ──activatePreparedSession()──▶ active

preparing·prepared → idle:
  실패 · 비LiveKit 런타임 · 연결 끊김/실패/ASR 실패 · 다른 활동 이동
  · 세션 정지 · 언마운트 · 일반 startSession 호출(새 시작이 덮어씀)

불변 조건 (preparing·prepared 공통)

  1. AI 입력 차단 (transition_blocked = true)
  2. AI 출력 차단 (responseBlocked = true)
  3. 첫 인사·텍스트·스텝 멘트 미전송
  4. 5초 마이크 fallback 미예약
  5. isActive = false: 외부(진행자 화면, 재시작, 자동 듣기 복구)에는 세션이 없는 것으로 보입니다.
  6. LiveKit 방과 Agent job은 실제로 살아 있습니다. worker 자리 1개와 job 수명을 사용합니다.

활성화 조건 (prepared → active)

ready === true, 같은 활동 ID, 같은 세대, LiveKit 세션 존재. 네 가지를 모두 만족해야 재사용하고, 하나라도 어긋나면 기존 방식으로 새로 시작합니다.

5. prepare 상태에서 막히는 것 · 그대로인 것

막히는 것

항목방법
첫 인사 (AUTO_START)준비 모드에서 return, 활성화 때 1회
아동 마이크 → AI 송신입력 정책 transition_blocked
5초 마이크 자동 복구준비 모드에서 예약 안 함
진행자·수동 텍스트sendTextMessage false 반환
스텝 멘트 (startMent)isActive false (기존 가드)
AI 소리 출력responseBlocked
세션 재시작·자동 듣기 복구isSessionActive false (기존 가드)

그대로 동작하는 것

  • LiveKit 방 접속과 Agent job 점유: 이 기능의 목적이라 그대로 둡니다.
  • 진행자 듣기 ON/OFF: 의도값만 바뀌고 입력은 계속 막혀 있습니다. AI 스텝에서 그 의도대로 복원됩니다.
  • 모니터·녹음용 아동 마이크: AI 입력 경로와 별개입니다.
  • 연결 상태 알림: 끊기거나 실패하면 진행자 모니터에 상태가 전송될 수 있습니다.
  • VAD 등 설정 RPC: 설정만 전달되고 응답은 만들지 않습니다.
서버 Agent의 입력·응답 허용값(기본 true)은 바꾸지 않았습니다. 그래도 영상 중에 Agent가 말할 수 없는 이유는 세 경로가 모두 닫혀 있기 때문입니다: 마이크가 송신되지 않고, 텍스트를 보내지 않고, 서버 첫 인사(greeting_enabled)가 꺼져 있습니다.

6. Agent 서버 영향 — worker와 job

worker는 만들어지지 않고, job은 prepare에서 만들어진다

prepare → startSession → /api/livekit/call
  → 토큰에 Agent dispatch 포함 (livekit-token.ts RoomAgentDispatch)
  → 브라우저 방 접속 → LiveKit 서버가 worker에 job 배정
  → ppi_agent 엔트리포인트 (agent.py:5257)
      · 첫 줄에서 job 수명 타이머 시작 (agent.py:5260, 기본 20분)
  → ready 신호 → waitUntilAgentReady 통과 → session_prepared

job 수명 상한은 늘어나지 않는다

기존:  [영상 ─────][AI 대화 ──────────]
                   ↑ job 시작        ↑ 활동 종료 → job 종료
변경:  [영상 ─────][AI 대화 ──────────]
       ↑ job 시작                     ↑ 활동 종료 → job 종료
AI 대화 가능 시간이 줄어듭니다. 20분 상한은 엔트리포인트 진입 시각부터 계산합니다(job_lifetime.py). 예를 들어 영상 5분 뒤에 AI 대화 18분이 이어지는 활동은 기존에는 job이 18분이라 정상이었습니다. 변경 후에는 job이 23분이 되어 AI 대화 15분 지점에서 만료됩니다. 대응은 배포 없이 PPI_JOB_MAX_DURATION_SECONDS로 상한을 늘리거나, 긴 영상은 끝 무렵에 준비를 시작하는 것입니다(후자는 코드 수정 필요).

worker 자리 점유

worker A (최대 2 job, 부하 = active_jobs / 2 × 0.99)
  [job: 아동1 준비 중]  [빈 자리]   → 부하 0.495 → 새 job 받음
  [job: 아동1 준비 중]  [job: 아동2] → 부하 0.99  → 새 job 거부 → 다른 worker로

worker를 늘려야 하나

7. 검증 · 리뷰 · 남은 위험

검증 (자동)

리뷰에서 수정한 P1

실기기 확인 필요 (미실행)

남은 위험 (P2, 미수정): 영상 길이만큼 job과 Soniox 연결을 점유해 Soniox 429 위험이 커지고 job 수명이 줄어듭니다. 준비 실패 시 진행자 모니터에 연결 실패 상태가 잠깐 보일 수 있습니다(미확인). 영상 중 연결이 끊겨 무효화된 뒤에는 텍스트가 죽은 세션으로 보내지려다 실패할 수 있습니다. 소리가 나지는 않습니다.

관련 문서