마지막 업데이트 2026-10-14
measurement)로 기록합니다.
| 파일 | 역할 |
|---|---|
apps/livekit-agent/agent.py:1686 _sample_tts_audio_outbound | 첫 TTS 프레임부터 1초 동안 로컬 오디오 트랙의 RTP 송출 증가량을 측정합니다. 호출 위치는 tts_node 첫 프레임 :2110 |
apps/livekit-agent/agent.py:1618 _is_audio_publication | publication kind를 rtc.TrackKind.KIND_AUDIO(int)로 비교합니다. 기존 문자열 비교 버그도 이 함수로 함께 고쳤습니다 |
apps/web/lib/voice-agent/livekit-ai-speech-delivery-probe.ts (신규) | Agent가 speaking일 때마다 1초 동안의 수신량·에너지·재생 진행량을 측정합니다. 수신이 0이면 한 번 더 확인합니다 |
apps/web/lib/voice-agent/livekit-playout-drift-monitor.ts:110 | inbound-rtp 통계에 packetsReceived, packetsLost, bytesReceived, totalAudioEnergy를 추가했습니다. drift 판정 로직은 그대로입니다 |
apps/web/lib/voice-agent/livekit-client-session.ts:2302, :3456 | probe를 생성하고 lk.agent.state 속성 변경에 연결합니다. play 결과는 :3376~3399에서 전달하고, 종료·disconnect 시 :3003, :3261에서 probe를 멈춥니다 |
tts_node_audio_output)까지만 근거가 있었습니다. 같은 발화의 송출, 수신, 재생은 이어진 기록이 없었습니다. 중계 송신량 0(Audio producer outbound stalled)만으로는 Agent가 안 보냈는지, 브라우저가 못 받았는지 구분할 수 없습니다. 이 경고는 정상 수업에서도 활동이 바뀔 때 나옵니다.lk.agent.state를 씁니다. 이 속성은 시그널링으로 오기 때문에, 데이터 채널만 멈추는 장애(김민결 8회기)에서도 측정이 끊기지 않습니다.Agent tts_node 첫 프레임 yield
└─ agent.py:2110 _start_outbound_sample(agent_response_id) ← 백그라운드 task, 파이프라인 비차단
get_stats() → 1초 sleep → get_stats()
→ tts_audio_outbound_sample packets_sent_delta / bytes_sent_delta / track_sid
│
▼ (RTP) LiveKit SFU
│
브라우저: Agent 참가자 속성 lk.agent.state = "speaking"
└─ livekit-client-session.ts:3456 onAgentSpeakingChanged(true)
기준값: inbound-rtp + audio element 상태
1초 뒤 재측정
├─ 수신 증가 있음 → AI speech delivery sample (info)
├─ 수신 0 + 아직 speaking → AI speech delivery inbound stalled (warn) → 1초 뒤 재확인
│ → 최종 sample (회복=info / 계속 0=warn, attempt=2)
└─ 1초 전에 speaking 종료 → 종료 시점까지로 정리 (trigger=speech_ended)
tts_audio_outbound_sample| 필드 | 의미 |
|---|---|
agent_response_id, track_sid | 발화와 송출 트랙. 브라우저 쪽 trackSid, responseId와 맞춰 봅니다 |
packets_sent_delta, bytes_sent_delta | 1초(window_ms) 동안의 outbound-rtp 증가량. 0이면 WARN |
measurement | ok / no_audio_track / stats_unavailable(+error_class). ok가 아니면 delta는 null입니다 |
AI speech delivery sample (logger LIVEKIT_AI_SPEECH_DELIVERY)| 필드 | 의미 |
|---|---|
packetsReceivedDelta, bytesReceivedDelta, packetsLostDelta | 소리 데이터가 도착했는가 |
totalAudioEnergyDelta, concealedSamplesDelta | 도착한 데이터가 실제 소리인가, 아니면 무음 샘플이거나 은닉(손실 보정)인가 |
currentTimeDeltaSec, paused, muted, volume | audio element 재생이 진행됐는가 |
outputBlocked, lastPlayResult | 앱이 출력을 막았는가, play()가 resolved / blocked / rejected(+errorName) 중 무엇이었나. 이전에는 dev에서만 기록됐는데 이제 prod에도 남습니다 |
measurement, trigger, attempt, windowMs, wallClockMs | 측정 가능 여부(ok / no_agent_track / stats_unavailable), 측정 계기, 재확인 차수, 실제 측정 길이, 벽시계 시각 |
| 구간 | 판정 조건 | 다음에 볼 곳 |
|---|---|---|
| ① Agent → LiveKit 송출 | 생성 프레임은 있는데 packets_sent_delta = 0 | Agent 출력 큐, AudioSource, 송출 트랙 |
| ② LiveKit → 브라우저 수신 | 송신은 늘었는데 packetsReceivedDelta = 0 | LiveKit 서버 전달, 구독 상태, 네트워크 경로 |
| ②-a 무음 샘플 | 패킷은 늘었는데 totalAudioEnergyDelta ≈ 0 | Agent가 무음을 보냈는지, 디코딩 쪽 문제인지 먼저 구분 |
| ③ 브라우저 재생 | 수신·에너지는 정상인데 play 거부, muted/paused, outputBlocked, 재생시간 정지 중 하나 | 브라우저 재생 정책·상태 |
| ④ 단말 출력 (로그 밖) | 위 조건이 모두 정상 | 기기 볼륨, 블루투스·이어폰 등 출력 장치 |
no_agent_track, stats_unavailable, no_audio_track은 원인이 아니라 측정 공백입니다. 특히 브라우저에서 AI 트랙이 구독되지 않은 상태라면 no_agent_track으로 나옵니다.track_sid, agent_response_id를 우선해 맞춥니다. responseId는 데이터 채널로 받은 최신값이라 비어 있을 수 있습니다.currentTime이 재생 클럭으로 진행해 출력단 정지를 반영하지 않을 수 있습니다.str(kind)에 "AUDIO"가 있는지 검사하던 코드가 SDK의 int enum에서 항상 거짓이었습니다. 그래서 기존 tts_node_audio_output의 audio_publication_count가 운영에서 0으로 기록됐을 가능성이 큽니다. 이번 배포부터 정상값이 나옵니다. 배포 전 로그의 이 필드는 신뢰하지 않습니다.AI speech delivery sample이 발화마다 1건씩 S3 세션 로그에 쌓이는지, lk.agent.state가 실제로 speaking으로 바뀌는지는 아직 확인하지 않았습니다. 바뀌지 않으면 브라우저 로그는 0건입니다.