MFCC 패턴 감지 튜닝 변천사

기간. 2026-05-07 ~ 2026-05-21 티켓. PPI-904 / 926 / 967 커밋. 11개 (revert 1회 포함) 작성. 2026-05-24
한 줄 요약. Whisper LID 기반 영어 감지(메모리 부담 + 정확도 변동)를 폐기하고, MFCC 13계수 코사인 유사도 + 패턴 시퀀스 매칭으로 전환. 2주에 걸친 11개 커밋 동안 FP↔FN 트레이드오프를 둘러싼 튜닝이 반복되었고, "hop 당 sim 최고 1건"(be0ac039) 접근이 규칙 우선순위를 손상시켜 전체 롤백 후 "각 규칙 독립 + miss tolerance 2회"(7008720c) 접근으로 안정화됨.

목차

  1. 1. 배경 - 왜 MFCC인가
  2. 2. 알고리즘 핵심 컴포넌트
  3. 3. PPI-904 - 기초 확립 (5/7~5/8)
  4. 4. PPI-926 - 강화 시도 + 전체 롤백 (5/12~5/13)
  5. 5. PPI-967 - 최종 로깅 보정 (5/21)
  6. 6. 롤백의 핵심 분석
  7. 7. 반복된 튜닝 패턴
  8. 8. 현재 최종 알고리즘 상태
  9. 9. 재발 시 디버깅 단서

🎯 1. 배경 - 왜 MFCC인가

이전 방식. Whisper LID의 한계

초기(PPI-847/858/859)에는 Transformers.js Whisper tiny/base 모델로 영어 발화를 감지했습니다.

MFCC 채택 동기 (PPI-904)

강점설명
경량 특징 추출13개 MFCC 계수만 사용. Whisper 수천 차원 임베딩 대비 ms 단위 처리
패턴 기반 매칭23개 사전 등록 발음 템플릿의 시퀀스로 감지 → 정확한 제어
오탐 감소특정 발음 시퀀스만 감지 → 일반 음성에 반응 안 함
실시간 처리150ms hop, 300ms 분석 윈도우 슬라이딩

🧮 2. 알고리즘 핵심 컴포넌트

처리 파이프라인

AI 음성 (16kHz)
  ↓
1. MFCC 추출 (apps/web/lib/mfcc.ts)
   300ms 윈도우, 10ms hop frame, Hamming → FFT → Mel(26ch) → DCT → 13계수
  ↓
2. 23개 템플릿 코사인 유사도 계산
   각 template_mfcc와 cos similarity → maxSimilarity, bestTemplateIdx 추출
  ↓
3. 패턴 규칙 매칭 (use-ai-english-guard.ts)
   각 규칙별 cursor 유지
   - 매 hop. bestTemplateIdx == rule.steps[cursor].tldx && sim >= threshold
     → cursor++
   - cursor == rule.steps.length → 영어 감지, 응답 즉시 중단
  ↓
4. 무음 처리. 5 hop(750ms) 연속 무음 → 모든 cursor 리셋

핵심 파일

주요 데이터 구조

interface MfccPatternRule {
  id: string;              // UUID
  name: string;            // "Negative Feedback" 등
  enabled: boolean;
  stopSpeech: boolean;     // 감지 시 응답 즉시 중단 여부
  steps: MfccPatternStep[]; // 시퀀스 스텝 배열
}

interface MfccPatternStep {
  tldx: number;            // 글로벌 템플릿 인덱스 (0~22)
  phrase: MfccPhraseType;  // "I_JUST_GOT_PROMOTED" 등
  sim?: number;            // 선택적 step별 threshold (없으면 기본 0.90)
}

🌱 3. PPI-904 - 기초 확립 (5/7~5/8)

① Whisper LID 제거 + MFCC 패턴 시퀀스 도입

5/7 3a5b9795 FEAT
변경 핵심
  • use-host-ai-language-monitor.ts 전량 제거 (Whisper 훅 656줄)
  • language-detection.worker.ts 제거 (Whisper 워커)
  • use-ai-english-guard.ts에 MFCC 패턴 매칭 로직 추가
  • UI. Whisper 설정 → MFCC 패턴 등록 (mfcc-pattern-section.tsx)
초기 파라미터
WINDOW_SAMPLES_16K = 4800           // 300ms
HOP_SAMPLES_16K = 2400              // 150ms
MAX_SILENCE_HOPS_FOR_RESET = 5      // 750ms 무음까지 cursor 유지
ENGLISH_GUARD_SIMILARITY_THRESHOLD = 0.90

② Alert 타입 분리

5/7 4f8530b9 FIX

기존 AI_ENGLISH_DETECTED와 별도로 MFCC_PATTERN_MATCHING alert 타입 추가. 모니터링 페이지에서 두 가지 감지 방식을 분리해서 통계 추적 가능.

③ allSims 정렬 mutation 버그 + 무음 cursor 즉시 리셋

5/8 422be027 FIX
증상

패턴 시퀀스 매칭이 거의 작동 안 함. 같은 발화도 감지 실패.

원인 1. allSims.sort() in-place mutation
// 패턴 매처가 allSims[expectedGlobalTIdx]로 직접 인덱싱하는데
allSims.sort((a, b) => b.sim - a.sim);  // ← 원본 배열 정렬!
// 정렬 후 allSims[5]는 더 이상 tldx=5의 sim이 아님
원인 2. 음절 경계 무음 1 hop만으로 cursor 리셋

발화 중 자연스러운 음절 사이 짧은 dip(150ms)에도 cursor가 즉시 0으로 → 시퀀스 미완성.

해결
// 1. 정렬 전 복사본 생성
const topN = allSims.slice().sort(...).slice(0, TOP_N_TEMPLATES);
// 원본 allSims는 그대로 → tldx 인덱싱 유효

// 2. 무음 임계 도입
if (silenceStreakHopsRef.current >= MAX_SILENCE_HOPS_FOR_RESET) {
  ruleCursorsRef.current.clear();  // 750ms 무음 시에만 리셋
}
추가. UI 통일

phrase 수동 select → tldx 입력 후 derivePhraseFromTldx로 자동 유추.

④ Hop 미스 시 cursor 즉시 리셋 - 엄격 모드

5/8 6bba93a9 PRECISION ↑
정책 명확화

"패턴 시퀀스는 연속 hop에서만 완성되어야 한다." 이전: threshold 미달 시 cursor 유지. 변경: 미스 즉시 cursor → 0 (무음 5hop 예외).

if (stepSim >= stepThreshold) {
  // 진행
} else if (cursor > 0) {
  // hop 미스 시 cursor 즉시 리셋
  logger.info("MFCC 패턴 hop 미스 — cursor 리셋");
  ruleCursorsRef.current.set(rule.id, 0);
}
튜닝 방향. Precision 극대화 (FP 감소). FN 증가 가능성은 추후 확인 후 보정 예정.

⑤ 세션 로그에 매칭 tldx 시퀀스 표시

5/8 de1eb174 FEAT

summarizeTurnMfcc가 pattern_step 이벤트들을 ruleId별로 수집해 완성된 시퀀스(matchedPatternSequence)를 추출. 세션 로그에 [12 → 14 → 20 → 19] 형태로 표시되어 디버깅 편의성 향상.

🔄 4. PPI-926 - 강화 시도 + 전체 롤백 (5/12~5/13)

⑥ 로그 누락 + 이벤트 순서 역전 보정

5/12 907b9c77 FIX
문제 1. 초기 로그 누락

sessionId가 전파되기 전 도착한 MFCC 이벤트가 손실되어 분석 시 비어있는 구간 발생.

// pendingMfccQueue로 임시 저장 (최대 50개, ~7.5초)
if (!sessionId) {
  pendingMfccQueueRef.current.push(event);
  return;
}
// sessionId 전파 시 일괄 emit
문제 2. turn_end가 ai_speaking_start보다 먼저 도착
// pendingOrphanBuffer로 2초간 보류
pendingOrphanBufferRef.current = {
  buffer: currentTurnBufferRef.current,
  ownerUserId, receivedAt: Date.now(),
};
// turn_end가 2초 내 도착하면 해당 turnId로 flush, 초과 시 폐기

⑦ Hop 당 sim 최고 1건만 채택 - 문제의 시도

5/12 be0ac039 PRECISION ↑
의도

같은 hop에서 여러 규칙이 동시 매칭 시 pattern_step 로그 중복 → 어느 시퀀스가 완성될지 모호. sim 최고 1개만 진행으로 명확화 시도.

// 1차: 후보 수집
const matches = [];
for (const rule of mfccPatternRulesRef.current) {
  if (stepSim >= stepThreshold) matches.push({ rule, stepSim, ... });
}
// 2차: winner 선택 (sim 최고)
const winner = matches.reduce((best, cur) =>
  cur.stepSim > best.stepSim ? cur : best
);
// winner의 cursor만 전진, 나머지는 이 hop 무시
잠재 위험. 규칙 정의 순서나 threshold 강도를 무시하고 sim만으로 우선순위 결정. 엄격한 threshold 규칙이 느슨한 규칙에 밀려날 가능성.

⑧ 검증 로그 축소 + UI 필터 단순화

5/12 2aa75c7d REFACTOR
  • 패턴 모드에서 mfcc_sample 로그 제외 (볼륨 40% 감소)
  • 4개 필터(samples/english/lid/pattern) → 2개로 단순화
  • transcript 델타 로깅 제거
⚠️ 5/13. PPI-926 전체 롤백 (1cd0bb20)

실제 동작 테스트에서 예상과 다른 결과 (시퀀스 완성 실패 등)가 발생. ⑥~⑧ 3개 커밋을 한꺼번에 revert.

주된 의심 원인. be0ac039의 "sim 최고 1건"이 규칙 우선순위를 손상시켜 엄격한 규칙이 더 느슨한 규칙에 밀려 cursor 진행이 막히는 케이스 발생.

⑨ 재시도 - 각 규칙 독립 + miss tolerance 2회

5/13 7008720c BALANCE
접근 변경

"sim 최고 1건"의 규칙 간섭 문제를 해결하기 위해 각 규칙을 완전히 독립적으로 평가. 대신 miss tolerance 2회를 추가해 음절 경계 변화에 대응.

const PATTERN_RULE_MISS_TOLERANCE = 2;
const ruleMissStreakRef = useRef<Map<string, number>>(new Map());

// 각 규칙 독립 평가
for (const rule of mfccPatternRulesRef.current) {
  if (bestTemplateIdx === expectedTIdx && maxSimilarity >= stepThreshold) {
    ruleMissStreakRef.current.set(rule.id, 0);
    cursor++;  // 스텝 진행
  } else if (cursor > 0) {
    const missStreak = (ruleMissStreakRef.current.get(rule.id) ?? 0) + 1;
    if (missStreak >= PATTERN_RULE_MISS_TOLERANCE) {
      ruleCursorsRef.current.set(rule.id, 0);  // 2회 허용 후 리셋
      ruleMissStreakRef.current.set(rule.id, 0);
    } else {
      ruleMissStreakRef.current.set(rule.id, missStreak);
    }
  }
}
튜닝 방향. Recall 강화 + 신뢰도 회복. 6bba93a9의 엄격함(즉시 리셋)을 완화해 음절 경계 dip 대응.

🪛 5. PPI-967 - 최종 로깅 보정 (5/21)

⑩ turn_end 미전송 3가지 케이스 보정

5/21 969c853e FIX
발견된 누락 패턴 3가지
  1. response.done status가 cancelled 또는 incomplete일 때 turn_end 미전송
  2. DataChannel close 시 진행 중 턴 미종료
  3. 소켓 재연결 중 grace window의 MFCC 데이터 미처리
추가. 합성 ID 버그
// 기존. event.item_id 없을 때 합성 ID 사용
aiTranscript.id = `assistant-${Date.now()}`;
lastFinalizedItemIdRef.current = aiTranscript.id;  // ← 합성 ID 저장
// → 이후 같은 itemId 비교가 실패해 dedup 실패

// 수정. 실제 item_id 존재 시에만 갱신
if (event.item_id) {
  lastFinalizedItemIdRef.current = event.item_id;
}
교훈. 비동기 이벤트 흐름은 종료 케이스가 다양함. 정상 경로 외 cancelled/incomplete/disconnect 등 모든 종료 시그널에서 cleanup 실행 보장.

🔍 6. 롤백의 핵심 분석

be0ac039 vs 7008720c 비교

항목be0ac039 (롤백됨)7008720c (현재)
방식 모든 규칙 후보 수집 → sim 최고 1개만 진행 각 규칙 독립 평가, bestTemplateIdx 기준
Miss 처리 즉시 cursor 리셋 (6bba93a9 정책 유지) miss 2회까지 허용 (PATTERN_RULE_MISS_TOLERANCE)
멀티 규칙 같은 hop에서 1개만 진행 각각 독립 진행 가능
로그 깔끔 (winner만 pattern_step) 복잡 (여러 규칙이 동시 step 로그 남길 수 있음)
규칙 우선순위 sim 크기에 의존 (위반) 정의된 threshold 존중

롤백을 부른 시나리오

Rule A. [12, 14, 20] threshold 0.92  (엄격)
Rule B. [16, 18, 22] threshold 0.90  (느슨)

hop N. tldx=12 매칭, simA=0.95, simB=0.88
  → be0ac039. winner=A, cursor_A=1, cursor_B=0
  → 7008720c. cursor_A=1 진행, B는 영향 없음

hop N+1. tldx=16 매칭, simA=0.85, simB=0.94
  → be0ac039. simA가 threshold(0.92) 미달이지만 winner=B (simB 더 높음)
            → cursor_B=1, 그런데 cursor_A는 이 hop에서 miss 처리됨
            → 다음 hop에서 cursor_A 리셋 (정책상)
            → Rule A 시퀀스 미완성
  → 7008720c. cursor_A는 miss(missStreak=1), cursor_B=1 독립 진행
            → 추후 Rule A의 step2 매칭 시 cursor_A 계속 진행 가능
            → 두 규칙 모두 정상 진행

핵심. "sim이 더 높다"가 "이 규칙이 더 중요하다"를 의미하지 않음. 각 규칙은 자신의 threshold 기준으로 독립 평가되어야 함.

📊 7. 반복된 튜닝 패턴

FP ↔ FN 트레이드오프

커밋방향조정효과
422be027 FP↓ allSims mutation 버그 수정 + 무음 5hop 허용 시퀀스 미완성 개선
6bba93a9 FP↓↓ hop 미스 즉시 리셋 (tolerance 없음) FP 극감, FN 증가 우려
be0ac039 FP↓ hop 당 sim 최고 1건 (의도) 규칙 간섭 부작용 → 롤백
7008720c 균형 각 규칙 독립 + miss tolerance 2회 밸런스 회복, 안정화

로깅/시각화 보강 패턴

핵심 교훈 3가지

  1. "비교 기준"을 흐리지 말 것. sim 값 비교는 같은 threshold 안에서만 유효. 다른 threshold 규칙끼리는 sim 비교 무의미.
  2. 음절 경계를 잊지 말 것. 사람 발화는 hop 단위로 깨끗하지 않음. miss tolerance / 무음 tolerance 필수.
  3. 비동기 이벤트의 종료 다양성. 정상 종료 외 cancelled, incomplete, disconnect 모든 경로에서 cleanup 보장.

📌 8. 현재 최종 알고리즘 상태

핵심 파라미터 (7008720c + 969c853e 기준)

파라미터의미
WINDOW_SAMPLES_16K4800분석 윈도우 (300ms @ 16kHz)
HOP_SAMPLES_16K2400슬라이딩 간격 (150ms)
ENGLISH_GUARD_SIMILARITY_THRESHOLD0.90기본 코사인 유사도 임계값
MAX_SILENCE_HOPS_FOR_RESET5~750ms 무음까지 cursor 유지
PATTERN_RULE_MISS_TOLERANCE2패턴 규칙. tIdx 불일치 2회까지 허용
DEFAULT_MISS_TOLERANCE1연속 매칭 모드. threshold 미달 1회 허용
GRACE_PERIOD_MS500AI 발화 시작 후 유예 구간
COOLDOWN_MS10000감지 후 재감지 쿨다운
MFCC_ORPHAN_TURN_WAIT_MS2000이벤트 순서 역전 보정 시간
MIN_RMS_ENERGY0.005무음 판정 에너지 임계값
PENDING_MFCC_QUEUE_MAX50sessionId 전파 전 이벤트 큐 크기
TEMPLATES_COUNT23사전 등록 발음 템플릿 수

상태 변수 추적 (Refs)

// 패턴 시퀀스 모드
ruleCursorsRef     // Map<ruleId, cursor>  - 규칙별 현재 스텝
ruleMissStreakRef  // Map<ruleId, count>   - 규칙별 miss 횟수

// 무음 처리
silenceStreakHopsRef  // 무음 연속 hop 수

// 기존 연속 매칭 모드 (fallback)
consecutiveMatchCountRef    // 연속 매칭 횟수
consecutivePhraseGroupRef   // 현재 phrase group
lastMatchedOffsetRef        // 마지막 매칭 offset
lastMatchedTIdxRef          // 마지막 매칭 tIdx
missStreakRef               // threshold 미달 연속 횟수

🩺 9. 재발 시 디버깅 단서

로그 키워드

로그의미
MFCC 패턴 스텝 매칭cursor 정상 진행 (성공)
MFCC 패턴 시퀀스 완성시퀀스 완성 → 영어 감지 (성공)
MFCC 패턴 hop 미스 — cursor 리셋miss streak ≥ 2 → cursor 0으로 리셋 (정상)
Flushing pending MFCC eventssessionId 전파 후 큐된 이벤트 일괄 emit
flushing orphan buffer on late turn_end이벤트 순서 역전 보정
discarding orphan buffer — wait window exceeded2초 이상 대기 → 폐기
phrase group changed — reset consecutive연속 매칭 모드. 다른 phrase 그룹 → 초기화
threshold miss — reset consecutive연속 매칭 모드. threshold 미달 → 초기화

증상별 진단 매트릭스

증상의심 원인확인 사항
영어 패턴 감지 안 됨 (FN) threshold 미달 or miss streak 초과 mfcc_sample 로그의 sim 분포 vs threshold(0.90)
특정 규칙만 작동 안 함 step별 sim threshold가 너무 높음 MfccPatternStep.sim 값과 실제 sim 비교
거짓 감지 빈발 (FP) threshold 낮거나 시퀀스 짧음 알림의 tldx 시퀀스가 의도와 일치하는지
시퀀스 미완성 음절 경계 dip이 5 hop 초과 silence streak이 5 도달했는지, miss streak 추이
S3 로그 비어있음 turn_end 미전송 response.done status 확인 (cancelled/incomplete)
초기 구간 로그 누락 pendingMfccQueue 오버플로우 sessionId 전파 지연 시간 측정

알고리즘 튜닝 가이드

  1. Threshold 조정. mfcc_sample 로그의 topN sim 분포 수집. 정상 발화의 최저 sim과 오탐의 최고 sim 중간값으로 설정.
  2. Miss Tolerance 조정. 음절 경계에서 다른 bestTIdx 나오는 빈도 측정. 1회 이상이면 tolerance=1, 2회 이상이면 2, 3회 이상이면 3.
  3. Max Silence Hops 조정. 발화 중 자연스러운 dip 지속 시간 측정. 보통 100~300ms이면 현재 5(750ms)는 보수적 설정.
  4. Step 시퀀스 설계. 최소 3스텝 권장. 1~2스텝은 FP 위험. offset 변화도 고려해 발음 순서 자연스러운 시퀀스 선호.

기존 알고리즘 분석은 기계음 영어 발화 감지 로직 분석 문서 참조. 단, 그 문서는 Whisper LID 시점이므로 본 문서가 최신 상태(MFCC 기반)를 반영합니다.