be0ac039) 접근이 규칙 우선순위를 손상시켜 전체 롤백 후 "각 규칙 독립 + miss tolerance 2회"(7008720c) 접근으로 안정화됨.
초기(PPI-847/858/859)에는 Transformers.js Whisper tiny/base 모델로 영어 발화를 감지했습니다.
| 강점 | 설명 |
|---|---|
| 경량 특징 추출 | 13개 MFCC 계수만 사용. Whisper 수천 차원 임베딩 대비 ms 단위 처리 |
| 패턴 기반 매칭 | 23개 사전 등록 발음 템플릿의 시퀀스로 감지 → 정확한 제어 |
| 오탐 감소 | 특정 발음 시퀀스만 감지 → 일반 음성에 반응 안 함 |
| 실시간 처리 | 150ms hop, 300ms 분석 윈도우 슬라이딩 |
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 리셋
apps/web/lib/mfcc.ts - MFCC 추출 (순수 TypeScript)apps/web/hooks/use-ai-english-guard.ts - 패턴 매칭 메인 루프apps/web/hooks/use-mfcc-log-collector.ts - 턴별 로그 수집 + S3 업로드apps/web/stores/use-language-guard-settings-store.ts - 규칙 저장소apps/web/components/sections/mfcc-pattern-section.tsx - 규칙 등록 UIapps/web/app/main/debug/english-guard/page.tsx - 템플릿 23개 정의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)
}
use-host-ai-language-monitor.ts 전량 제거 (Whisper 훅 656줄)language-detection.worker.ts 제거 (Whisper 워커)use-ai-english-guard.ts에 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
기존 AI_ENGLISH_DETECTED와 별도로 MFCC_PATTERN_MATCHING alert 타입 추가. 모니터링 페이지에서 두 가지 감지 방식을 분리해서 통계 추적 가능.
패턴 시퀀스 매칭이 거의 작동 안 함. 같은 발화도 감지 실패.
// 패턴 매처가 allSims[expectedGlobalTIdx]로 직접 인덱싱하는데
allSims.sort((a, b) => b.sim - a.sim); // ← 원본 배열 정렬!
// 정렬 후 allSims[5]는 더 이상 tldx=5의 sim이 아님
발화 중 자연스러운 음절 사이 짧은 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 무음 시에만 리셋
}
phrase 수동 select → tldx 입력 후 derivePhraseFromTldx로 자동 유추.
"패턴 시퀀스는 연속 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);
}
summarizeTurnMfcc가 pattern_step 이벤트들을 ruleId별로 수집해 완성된 시퀀스(matchedPatternSequence)를 추출. 세션 로그에 [12 → 14 → 20 → 19] 형태로 표시되어 디버깅 편의성 향상.
sessionId가 전파되기 전 도착한 MFCC 이벤트가 손실되어 분석 시 비어있는 구간 발생.
// pendingMfccQueue로 임시 저장 (최대 50개, ~7.5초)
if (!sessionId) {
pendingMfccQueueRef.current.push(event);
return;
}
// sessionId 전파 시 일괄 emit
// pendingOrphanBuffer로 2초간 보류
pendingOrphanBufferRef.current = {
buffer: currentTurnBufferRef.current,
ownerUserId, receivedAt: Date.now(),
};
// turn_end가 2초 내 도착하면 해당 turnId로 flush, 초과 시 폐기
같은 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 무시
mfcc_sample 로그 제외 (볼륨 40% 감소)1cd0bb20)
실제 동작 테스트에서 예상과 다른 결과 (시퀀스 완성 실패 등)가 발생. ⑥~⑧ 3개 커밋을 한꺼번에 revert.
주된 의심 원인. be0ac039의 "sim 최고 1건"이 규칙 우선순위를 손상시켜 엄격한 규칙이 더 느슨한 규칙에 밀려 cursor 진행이 막히는 케이스 발생.
"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);
}
}
}
response.done status가 cancelled 또는 incomplete일 때 turn_end 미전송// 기존. 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;
}
| 항목 | 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 기준으로 독립 평가되어야 함.
| 커밋 | 방향 | 조정 | 효과 |
|---|---|---|---|
| 422be027 | FP↓ | allSims mutation 버그 수정 + 무음 5hop 허용 | 시퀀스 미완성 개선 |
| 6bba93a9 | FP↓↓ | hop 미스 즉시 리셋 (tolerance 없음) | FP 극감, FN 증가 우려 |
| be0ac039 | FP↓ | hop 당 sim 최고 1건 (의도) | 규칙 간섭 부작용 → 롤백 |
| 7008720c | 균형 | 각 규칙 독립 + miss tolerance 2회 | 밸런스 회복, 안정화 |
| 파라미터 | 값 | 의미 |
|---|---|---|
WINDOW_SAMPLES_16K | 4800 | 분석 윈도우 (300ms @ 16kHz) |
HOP_SAMPLES_16K | 2400 | 슬라이딩 간격 (150ms) |
ENGLISH_GUARD_SIMILARITY_THRESHOLD | 0.90 | 기본 코사인 유사도 임계값 |
MAX_SILENCE_HOPS_FOR_RESET | 5 | ~750ms 무음까지 cursor 유지 |
PATTERN_RULE_MISS_TOLERANCE | 2 | 패턴 규칙. tIdx 불일치 2회까지 허용 |
DEFAULT_MISS_TOLERANCE | 1 | 연속 매칭 모드. threshold 미달 1회 허용 |
GRACE_PERIOD_MS | 500 | AI 발화 시작 후 유예 구간 |
COOLDOWN_MS | 10000 | 감지 후 재감지 쿨다운 |
MFCC_ORPHAN_TURN_WAIT_MS | 2000 | 이벤트 순서 역전 보정 시간 |
MIN_RMS_ENERGY | 0.005 | 무음 판정 에너지 임계값 |
PENDING_MFCC_QUEUE_MAX | 50 | sessionId 전파 전 이벤트 큐 크기 |
TEMPLATES_COUNT | 23 | 사전 등록 발음 템플릿 수 |
// 패턴 시퀀스 모드
ruleCursorsRef // Map<ruleId, cursor> - 규칙별 현재 스텝
ruleMissStreakRef // Map<ruleId, count> - 규칙별 miss 횟수
// 무음 처리
silenceStreakHopsRef // 무음 연속 hop 수
// 기존 연속 매칭 모드 (fallback)
consecutiveMatchCountRef // 연속 매칭 횟수
consecutivePhraseGroupRef // 현재 phrase group
lastMatchedOffsetRef // 마지막 매칭 offset
lastMatchedTIdxRef // 마지막 매칭 tIdx
missStreakRef // threshold 미달 연속 횟수
| 로그 | 의미 |
|---|---|
MFCC 패턴 스텝 매칭 | cursor 정상 진행 (성공) |
MFCC 패턴 시퀀스 완성 | 시퀀스 완성 → 영어 감지 (성공) |
MFCC 패턴 hop 미스 — cursor 리셋 | miss streak ≥ 2 → cursor 0으로 리셋 (정상) |
Flushing pending MFCC events | sessionId 전파 후 큐된 이벤트 일괄 emit |
flushing orphan buffer on late turn_end | 이벤트 순서 역전 보정 |
discarding orphan buffer — wait window exceeded | 2초 이상 대기 → 폐기 |
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 전파 지연 시간 측정 |
mfcc_sample 로그의 topN sim 분포 수집. 정상 발화의 최저 sim과 오탐의 최고 sim 중간값으로 설정.기존 알고리즘 분석은 기계음 영어 발화 감지 로직 분석 문서 참조. 단, 그 문서는 Whisper LID 시점이므로 본 문서가 최신 상태(MFCC 기반)를 반영합니다.