콘텐츠로 이동

바지인·재생 제어

Gateway는 사용자 발화 후보와 TTS playout을 같은 세션 상태기계에서 조정합니다. 핵심은 speech_candidate는 아직 확정 발화가 아니므로 내용을 즉시 버리지 않고 출력을 낮추거나 일시정지하고, 확정 뒤에만 현재 응답을 취소하는 것입니다.

stateDiagram-v2
    [*] --> Normal
    Normal --> Attack: speech_candidate
    Attack --> Ducked: target gain 도달
    Ducked --> HoldRelease: candidate_cancelled
    HoldRelease --> Release: 최소 hold 소진
    Release --> Normal: 150 ms ramp 완료
    Attack --> Cancelled: 발화 확정
    Ducked --> Cancelled: 발화 확정
    Cancelled --> Normal: generation 증가·queue 무효화

    Normal --> Paused: 실험군 + pause 창 안 candidate
    Paused --> Release: candidate_cancelled
    Paused --> Cancelled: 발화 확정
    Paused --> Ducked: pause 상한 초과

duck 계약

재생 중 speech_candidate가 오면 40 ms attack으로 TTS_DUCK_CANDIDATE_GAIN까지 낮춥니다. 목표 gain이 실제 outbound frame에 적용된 뒤 /sohri로 다음 ACK를 보냅니다.

{
  "type": "tts_duck_applied",
  "activityId": "...",
  "gain": 0.35
}

이 ACK는 단순 관측 이벤트가 아니라 EPD와의 제어 계약입니다. EPD는 후보와 activityId를 연결하고 ACK 시점으로 판정을 진행하므로 불필요한 대기로 늦춰서는 안 됩니다. 연속 후보는 같은 duck 덩어리로 병합하고, 취소 시 최소 hold 후 150 ms release ramp로 원래 음량을 복원합니다. 발화가 확정되면 release하지 않고 응답·재생을 취소합니다.

첫 가청 TTS 직후 INITIAL_PLAYBACK_GRACE 동안의 후보는 AEC 안정화용으로 의도적으로 무시합니다. 기본값은 2초이며, 이 구간의 후보가 duck ACK를 만들지 않는 것은 결함이 아닙니다.

선택적 pause A/B

현재 통합 브랜치는 초기 TTS 구간의 후보에서 duck 대신 재생 위치를 멈추는 기능을 포함합니다. 다만 표준 기본값 TTS_PAUSE_WINDOW=0s에서는 비활성입니다.

  • B군은 설정한 가청 누계 window 안의 후보에서 gain 0으로 정지하며 PCM offset을 전진시키지 않습니다.
  • 오탐 취소 시 정지 위치부터 150 ms ramp로 이어 재생하므로 duck 중 흘러가 버리는 내용을 보존합니다.
  • A군은 기존 duck 동작을 유지합니다.
  • 군은 Gateway session ID hash와 TTS_PAUSE_AB_PERCENT로 결정적으로 배정합니다.
  • TTS_PAUSE_MAX를 넘으면 갇힘 방지를 위해 pause를 풀고 기존 duck으로 강등합니다.
  • pause 중에도 tts_playback_state.active는 true입니다. “소리는 없지만 출력 세션은 진행 중”이라는 EPD 상태를 보존하기 위해서입니다.

pause가 EPD의 후보 검출 지연을 없애지는 않습니다. 실험은 내용 보존과 attack 구간을 개선하는 레버이며, 전체 barge-in latency 개선으로 과대 해석해서는 안 됩니다.

playout generation

모든 TTS job에는 enqueue 당시 generation이 붙습니다. response.cancel, output_audio_buffer.clear, 확정 바지인, provider interrupt에서 generation을 증가시킵니다. 이전에 큐에 들어온 job은 나중에 dequeue되더라도 트랙에 쓰기 직전 stale_generation으로 폐기됩니다.

이 설계는 채널을 비우는 작업과 동시에 backend read가 새 frame을 넣는 race에서 취소된 음성이 다시 재생되는 것을 막습니다. stream sentinel도 같은 큐를 지나가므로 정상 완료와 중단 (sohri.tts_stream_end/aborted)을 실제 playout 결과와 맞춥니다.

tts_playback_state

첫 가청 frame을 실제 트랙에 쓸 때 /sohri로 tts_playback_state{active:true}를 한 번 보내고, 스트림을 소진·중단할 때 active:false를 한 번 보냅니다. raw PCM 청크 경계마다 true/false를 반복하지 않습니다. 완전 무음 스트림은 가청 시작 신호를 만들지 않습니다.

스트리밍 TTS가 completed sentinel까지 정상 소진되면 tts_playback_completed ACK를 보냅니다. 큐 포화로 sentinel이 의도적으로 폐기되면 유휴 시 state는 false로 복구되지만 완료 ACK는 없을 수 있으며, 메트릭과 경고로 드러납니다.

half-duplex 입력 게이트

BARGE_IN_ENABLED=false이면 TTS 출력 동안 마이크 PCM을 /sohri로 보내지 않습니다. 이것은 codec 오류가 아니라 의도된 half-duplex drop입니다.

  • MIC_PREROLL_MS=0: TTS 중 입력을 모두 폐기
  • 양수: 최신 PCM ring buffer만 보존하고 TTS 종료 후 먼저 전송
  • 범위: 0..5000 ms, 100 ms 단위
  • preroll은 barge-in이 비활성일 때만 설정 가능

의도적 drop을 오류와 구분하기

다음은 상태 안전성을 위한 정상 폐기 사유가 될 수 있습니다.

  • 취소 전 generation의 TTS 또는 OpenAI PCM
  • provider interrupt 뒤 늦게 도착한 PCM
  • playout queue 포화 시 새 청크 또는 종료 sentinel
  • 활성 response 없이 도착한 SOHRI TTS
  • half-duplex TTS 구간의 mic PCM
  • 초기 보호 구간의 speech candidate

반복 비율이 높으면 운영 문제이므로 운영·메트릭의 reason label과 테스트·문제 해결을 함께 확인하세요.