본문으로 건너뛰기Hwanseok Sim

← 홈으로

  • 운영 중

Gem3Park — 주차장 인터폰 음성 AI

주차장 인터폰으로 걸려 온 방문 차량의 전화를 알아듣고, 동·호수와 방문 목적을 확인해 출입 명령이나 상담원 인계까지 처리하는 음성 AI 시스템.

기간
2026.02 – 운영 중
역할
단독 설계·개발·배포·운영
  • TypeScript
  • Node.js (Express)
  • Asterisk (PJSIP · AudioSocket · AMI)
  • Google Cloud STT · TTS
  • Gemini
  • SSE
  • PM2 · systemd

주차장 인터폰으로 걸려 온 방문 차량의 전화를 AI가 받아, 동·호수와 방문 목적을 확인하고 주차 관제 시스템에 출입 명령을 보낸다. 확신할 수 없으면 차단기를 열지 않고 상담원에게 넘긴다.

전화망(Asterisk PBX)부터 실시간 오디오 처리, STT·LLM·TTS 연동, 상태 기반 대화 제어, 주차 관제 API 연동, 운영 모니터링 웹, 장애 복구 자동화까지 혼자 설계·개발·배포·운영했다. 2026년 2월 첫 커밋 이후 실제 현장 통화를 받으며, 운영 로그와 녹음에서 드러난 인식 오류를 하나씩 구조로 막아 왔다.

참고

마스킹 안내. 현장 단지 이름, 상호명, 실제 통화의 동·호수와 세션 ID, 서버·네트워크 주소, 계정 정보는 싣지 않았다. 동·호수는 유형 설명용 예시(106호↔1006호 같은 0 하나 차이 쌍)만 썼다. 모든 수치는 Gem3Park 저장소의 운영 로그 집계 문서에서 옮겼고, 수치마다 기간과 측정 대상을 함께 적었다. 사람이 검수한 정답과 비교한 값이 아닌 경우 그렇다고 밝혔다.

항목내용
서비스아파트 주차장 인터폰 음성 응대 · 방문 차량 출입 명령 · 상담원 인계
사용자방문 차량 운전자(통화), 운영 담당자·개발자(모니터링 웹)
역할1인 E2E: 전화망 연동, 오디오 파이프라인, AI 연동, 대화 정책, 관제 API 연동, 모니터링 웹, 배포·장애 대응
기간2026-02-07 첫 커밋 → 2026-02-22부터 운영 로그 보존 → 2026-10-16 최근 커밋 (커밋 232개)
운영 규모저장된 통화 20,568건(2026-09-30 문서 기준), 재생 가능한 녹음 12,002건 인덱싱(2026-09-12 모니터 운영 반영 시점)
코드TypeScript Relay·AI 서비스 약 3.2만 줄 + 테스트 약 2.6만 줄, 모니터링 웹 약 6천 줄, 운영 스크립트 약 2.9천 줄
테스트npm test 781개(2026-11-05 집계, 2026-08-05에는 501개), 모니터링 웹 단위 21개 · Playwright E2E 5개(axe 접근성 검사 포함) · HTTPS 통합 1개

목차: 1. 개요 · 2. 만든 것 · 3. 시스템 구성 · 4. 통화 처리 흐름 · 5. 운영 지표 · 6. 기술적 도전 · 7. 연혁 · 8. 배포·운영 · 9. 품질 · 10. 결정 기록 · 11. 한계 · 12. 역량


1. 서비스 개요

1.1 풀어야 했던 문제

문제왜 어려운가Gem3Park의 접근
전화 음질인터폰 회선은 8kHz 또는 16kHz PCM이고 잡음과 작은 입력 레벨이 섞인다코덱 감지, 16kHz 변환, 필터·게인, 클라이언트 VAD, 20ms 단위 재생 페이싱
숫자 오인식106호와 1006호처럼 0 하나 차이인 호수가 둘 다 실제로 있다STT N-best 보존, 위험 쌍 분류, 같은 턴 원음을 Gemini로 다시 전사, 현장 허용 목록 검증
방문 목적의 다양성"택배요", "○○상가 가요", "엄마 집이요"처럼 표현이 끝없이 다르다키워드 규칙 → LLM 분류 → 의미 폴백 → 원음 판정의 단계별 판정
출입 안전잘못 들은 값으로 차단기를 열면 안 된다모르면 열지 않는다. 재질문 한도를 넘거나 근거가 없으면 상담원 인계
운영 관측한 통화의 원인이 SIP·Relay·인식·대화 제어·외부 명령 중 어디인지 찾기 어렵다통화 종료 요약(JSON), 녹음 재생·전사 비교·검수를 한 화면에 모은 모니터링 웹

1.2 통화 한 건에서 일어나는 일

  1. 운전자가 인터폰을 누르면 SIP 통화가 Asterisk에 들어오고, AudioSocket으로 TypeScript Relay에 음성이 흐른다.
  2. 안내 음성이 나간 뒤 Relay가 필요한 정보(동·호수, 방문 목적)를 하나씩 묻는다.
  3. 발화마다 Google STT가 전사하고, 같은 청취 창에서 Gemini 3.5 Transcribe Live가 병렬로 듣는다.
  4. 동·호수는 현장 허용 목록과 0 하나 차이 위험 쌍 검사를 통과해야 채택된다. 방문 목적은 규칙과 LLM 판정 체인을 거친다.
  5. 정보가 확정되면 주차 관제 API에 출입 명령을 보내고, 끝까지 확정되지 않으면 상담원에게 인계한다.
  6. 통화가 끝나면 명령·대화·토큰·오디오 처리·검증 결과를 구조화된 요약으로 남기고, 모니터링 웹이 이를 실시간으로 보여 준다.

2. 만든 것

구성요소내용
전화망 연동Asterisk PJSIP 통화 수신, AudioSocket PCM 스트리밍, AMI로 상담원 인계 변수 설정 후 dialplan에 제어 반환
오디오 파이프라인8/16kHz 코덱 감지, 업·다운샘플링, high-pass filter·gain, VAD, TTS 24kHz → 통화 코덱 변환, 20ms 청크 큐와 소켓 backpressure 처리
대화 제어guided 상태 기계: 슬롯 수집, 누락 항목만 묻는 결정론적 재질문, 재질문 한도, 실패 시 상담원 인계
음성 인식Google Cloud STT(telephony_short) 주 경로 + Gemini 3.5 Transcribe Live 병렬 보조 + 누락 슬롯만 같은 턴 WAV로 Gemini 재전사
판정동·호수 허용 목록·N-best 모호성 가드, 방문 목적 판정 체인(키워드 → Jev 결정 모델(shadow) → Gemini 3.6 텍스트 → 의미 폴백 → 원음 판정), 긴급차량·출입제한 차량·중고거래 방문 정책
외부 연동주차 관제 API 출입 명령, 방문증 발급 요청, 상담원 인계 요청 builder
모니터링 웹통화 검색, 녹음 재생(구간 반복·배속), 전사 비교, 검수 기록, 사이트 설정 편집, SSE 실시간 갱신, 역할별 권한
운영 도구PM2 프로세스 관리, systemd timer 기반 Asterisk·PJSIP watchdog, 오디오 replay·shadow 비교 스크립트, 재부팅 runbook

3. 시스템 구성

flowchart LR
    Caller["운전자 / 인터폰"] -->|SIP| PBX["Asterisk PBX"]
    PBX -->|AudioSocket PCM| Relay["TypeScript Relay"]
    Relay --> STT["Google Cloud STT<br/>(주 경로)"]
    Relay --> Live["Gemini 3.5 Transcribe Live<br/>(병렬 보조)"]
    STT --> Gate["guided 상태<br/>슬롯 충족 판정"]
    Live --> Gate
    Gate -->|누락 시 같은 턴 WAV| Assist["Gemini 오디오 재전사"]
    Assist --> Validate["파서 · 허용 목록 · 모호성 가드<br/>· 방문 목적 판정"]
    Gate --> Validate
    Validate -->|검증된 슬롯만| Flow["상태 기반 대화 제어"]
    Flow --> TTS["Google Cloud TTS"]
    TTS --> Relay
    Flow -->|출입 명령| API["주차 관제 API"]
    Flow -->|상담원 인계| PBX
    Relay --> Summary["통화 종료 요약 · 로그 · 녹음"]
    Summary --> Monitor["모니터링 웹<br/>(사설망 HTTPS)"]
  • Relay가 통화 하나의 모든 단계를 소유한다. 모델 호출은 단계별로 분리해 지연·오류·비용을 따로 관측하고 교체한다.
  • STT는 Google이 주 경로다. Gemini 3.5 Transcribe Live는 같은 청취 창에서 병렬로 돌며, Google 결과에서 빠진 정보만 표준 라벨로 보충한다.
  • LLM은 판단을 넘겨받지 않는다. 출입 여부는 상태 기계와 허용 목록이 정하고, LLM은 표현 이해와 누락 보완에만 쓴다.
  • 모니터링 웹은 공개 인터넷에 열지 않고 운영자의 사설망(tailnet) 안에서만 HTTPS로 연다.

4. 통화 처리 흐름

4.1 동·호수 확정

flowchart TD
    A["발화"] --> B["Google STT 전사 + N-best"]
    B --> C{"허용 목록에 있나"}
    C -->|없음| D["같은 턴 WAV를 Gemini가 재전사"]
    D --> C2{"허용 목록에 있나"}
    C2 -->|없음| R["재질문<br/>(미스 1회는 한도에서 제외)"]
    C -->|있음| E{"0 하나 차이 위험 쌍인가"}
    C2 -->|있음| E
    E -->|아님| OK["채택"]
    E -->|위험| F["Gemini 같은 턴 전사 우선"]
    F --> G{"두 결과가 같은가"}
    G -->|같음| OK
    G -->|다름| H["호출자에게 확인"]
    H -->|확인 실패| R
    R -->|한도 초과| AG["상담원 인계"]
  • 출입 명령을 만들기 직전에 같은 허용 목록 검사를 한 번 더 한다. 중간 경로에서 섞인 값도 여기서 막힌다.
  • 모호성이 풀리지 않은 상태에서는 출입 명령을 만들지 않는다(fail-closed).

4.2 방문 목적 판정

단계하는 일상태
1. 키워드 규칙·guided 캐시자주 나오는 표현을 표준 라벨로 바로 매핑하고, 이미 확인한 값은 다시 묻지 않음운영
2. Jev 결정 모델텍스트만 받아 정해진 선택지와 확률만 돌려주는 외부 결정 모델. 모든 임계값을 넘을 때만 수락shadow(통화 결과에 영향 없음)
3. Gemini 3.6 텍스트 분류짧은 출력, thinking 토큰 여유분 512운영
4. 의미 폴백재질문 한도 직전, 통화 전체 사용자 발화를 모아 한 번 더 분류운영
5. 원음 판정통화 전체 WAV(없으면 최근 12초 rolling WAV)를 오디오로 분류운영
실패끝까지 unknown이면 상담원 인계운영

모든 단계는 같은 표준 라벨 9개(택배·배달, 픽업, 가족·지인, 상가 방문, 업무, 공사, 응급, 아파트 시설, 일반 방문) 중 하나만 돌려줄 수 있고, 그 밖의 답은 알 수 없음으로 처리한다.


5. 운영 지표

아래 표는 기간과 측정 목적이 서로 달라 합산하지 않는다.

5.1 외부 명령 응답 (2026-02-22 ~ 06-04 보존 로그)

지표결과
분석 통화 세션5,593건
외부 명령 시도 / 성공 응답3,491건 / 3,461건
외부 명령 응답 성공률99.14%

성공률은 주차 관제 API의 응답 기준이며, 차단기가 물리적으로 열렸는지 잰 값이 아니다.

5.2 누락 정보 보조 전사 (2026-07-31 ~ 08-05, 통화 종료 요약 640건)

지표결과
보조 조건이 생긴 통화408건 (63.8%)
보조 전사 시도574회
빈 슬롯에 실제로 반영139회 (24.2%) — 동·호수 106회, 방문 목적 33회
정상 응답 지연중앙값 3.86초, p95 5.09초
오류62회 (timeout 30, abort 32)

24.2%는 보조 결과가 빈 슬롯에 들어간 적용률이다. 전사 정확도나 최종 성공률 향상 폭이 아니다.

5.3 최근 처리 결과 (2026-09-28 ~ 10-01, 426통화)

구분건수
완료(방문증 발급)345
실패(상담원 인계)20
제외(소켓 연결 종료 56, 출입제한 차량 안내 3, 소켓 오류 2)61
완료율 (완료 + 실패 기준)94.5%
완료율 (전체 426건 기준)81.0%
통화 시간 중앙값완료 20.6초, 실패 46.6초

실패 20건 중 16건이 "동호수 재질문 횟수 초과"였다. 이 결과가 6.10의 자기 정정 복구 작업으로 이어졌다.

5.4 방문 목적 텍스트 모델 비교 (9월 실제 발화 300건, 호출 545회)

항목Gemini 3.7-flash (+여유분 512)Gemini 3.6-flash
호출 지연 p50 / p90 / p992,043 / 3,295 / 8,345ms1,274 / 1,646 / 3,023ms
2.0초 초과 호출56.9%5.7%
기준값 일치 (시간 제한 없음)133건 (74.7%)133건 (76.9%)
기준값 일치 (운영 timeout 적용)112건133건
판정 체인 지연 p50 (운영 timeout 적용)3,797ms1,370ms

기준값은 통화가 최종 전송한 방문 목적이며 사람이 검수한 정답이 아니다. 정확도는 비슷하고 지연이 크게 짧아서, 방문 목적 텍스트 호출만 3.6-flash로 옮겼다.


6. 기술적 도전과 해결

각 항목은 문제 → 원인 → 해결 → 검증 → 결과 순서로 정리했다. 결과는 코드와 로그가 뒷받침하는 범위까지만 적었다.

6.1 전화망 음질을 AI가 처리할 수 있는 스트림으로

  • 문제: 통화마다 8kHz 또는 16kHz로 들어오는 PCM과 24kHz TTS 출력을 그대로 이으면 STT 입력 규격이 흔들리고 재생 속도가 통화 시간축과 어긋난다.
  • 해결: 수신 코덱을 감지해 8kHz는 16kHz로 올리고 필터·게인을 건 뒤 STT에 넘긴다. TTS 출력은 통화 코덱에 맞게 내리고 20ms 청크로 나눠 큐에 넣는다. 소켓 backpressure가 생기면 drain까지 멈추고 큐 상한을 넘은 오래된 청크는 버린다.
  • 검증: 8kHz 320바이트·16kHz 640바이트 청크가 각각 20ms인지, 리샘플링 길이·출력 코덱·첫 청크 전송을 로그로 확인한다.
  • 결과: STT에는 일관된 16kHz를, Asterisk에는 실시간 속도의 프레임을 돌려주고, 느린 소켓 때문에 재생 지연이 끝없이 쌓이지 않게 했다.

6.2 방문 목적 인식 실패를 단계별로 복구

  • 문제: 짧거나 간접적인 방문 목적이 한 번의 STT 결과에서 빠지면 정상 방문도 unknown이 됐다. 그렇다고 불확실한 목적을 자동 승인할 수는 없다.
  • 해결: 4.2의 판정 체인을 만들었다. 재질문 한도 직전에는 통화 전체 발화로 의미 판정을 한 번 더 하고, 그래도 없으면 원음을 오디오로 판정한다. 끝까지 unknown이면 상담원에게 넘긴다.
  • 검증: 판정마다 시도 여부, 고른 WAV, 모델, 라벨, 오류를 통화 요약에 남긴다. 단위 테스트가 텍스트 판정 helper와 full-call WAV 우선·rolling WAV 폴백 선택을 검증한다.

6.3 STT가 놓친 정보만 같은 턴 원음으로 다시 듣기

  • 문제: 첫 발화뿐 아니라 재질문·확인 턴에서도 STT가 동·호수나 방문 목적을 비우거나 잘못 들었다.
  • 해결: 모든 guided 턴에서 STT와 현재 상태를 먼저 검사하고, 필요한 정보가 남았을 때만 같은 턴 WAV를 Gemini에 한 번 보내 전사만 받는다. 반환된 전사문도 기존 파서·허용 목록을 그대로 통과해야 하고, 이미 확정된 값은 덮어쓰지 않는다. 턴 단위 single-flight, timeout·abort, 늦게 온 결과 폐기를 관리한다.
  • 검증: 완전한 STT일 때 호출 생략, 중복 요청 방지, timeout·세션 종료, 슬롯 충돌, 허용 목록 미스를 테스트한다. 관측 기록에는 전사 원문 없이 trigger·적용 슬롯·지연·오류 코드만 남긴다.
  • 결과: 5.2의 지표. Gemini가 실패해도 기존 재질문·상담원 인계 흐름은 막히지 않는다.

6.4 잘못 들은 동·호수가 출입 명령으로 이어지지 않게

  • 해결: SIP별 phrase hint와 숫자 보정은 인식 후보를 고르는 데만 쓰고, 실제 출입 가능 여부는 현장 허용 목록이 정한다. 텍스트에서 못 찾으면 최근 발화 WAV를 다시 전사하고, 출입 명령 직전에 같은 검사를 한 번 더 한다.
  • 검증: 판정을 text_allowlist_match/miss, audio_allowlist_match/miss로 구분해 기록한다. 단위 테스트가 비허용 동·호수가 든 출입 명령 생성을 거부하는지 검증한다.

6.5 생성형 응답에 기대던 재질문을 상태 기계로

  • 문제: LLM이 재질문의 내용과 시점을 모두 정하면 이미 받은 정보를 다시 묻거나 매번 다른 흐름이 나왔다.
  • 해결: 동·호수·방문 목적 슬롯을 상태로 두고, 비어 있는 항목만 묻는 결정론적 질문을 LLM보다 먼저 고른다. 재질문 횟수를 제한하고, 한도를 넘거나 Gemini가 429·503을 돌려주면 상담원 인계로 바꾼다.

6.6 프로세스는 살아 있는데 전화가 끊길 때

  • 문제: Asterisk와 Relay가 모두 떠 있어도 SIP 등록이 Rejected에 머물면 인입 전화가 바로 끊겼다. 일시적인 401 인증 실패 뒤 재등록 시도가 멈춘 것이 원인이었다.
  • 해결: watchdog을 프로세스 → Asterisk CLI 응답 → PJSIP 등록 3단계로 만들었다. 등록 이상이면 전체 재시작보다 pjsip reload를 먼저 하고, 복구되지 않을 때만 Asterisk를 재시작한다. systemd timer가 40초마다 실행하고, 재시작은 5분 창 안에서 최대 3회로 제한한다.
  • 검증: Rejected·Unregistered·No response·Maximum retries를 비정상으로 판정하는 parser를 별도 스크립트로 테스트하고, 복구 절차를 runbook으로 남겼다.

6.7 0 하나 차이인 호수의 오승인 막기 (2026-08)

  • 문제: 호출자는 1006호라고 했는데 106호로 확정된 통화가 있었다.
  • 원인: Google N-best 후보 중 하나만 쓰고 나머지를 버렸고, 106호도 허용 목록에 있어서 그대로 통과해 Gemini 검증을 건너뛰었다. 이 단지 허용 목록에는 0 하나를 넣거나 빼면 서로 바뀌는 쌍이 15개 동에 75개 있었다(101↔1001부터 106↔1006까지 여섯 유형).
  • 해결: 턴마다 N-best를 비공개로 보존하고, 위험 쌍이 걸린 턴은 Gemini 같은 턴 전사를 우선한다. 두 결과가 다르면 호출자에게 확인받고, 확인이 안 되면 재질문한다. 모호성이 풀리지 않으면 출입 명령을 막는다.
  • 검증: 당시 npm test 614개 통과, 배포 전 시나리오 게이트를 runbook으로 정리했다.
  • 남은 것: 공백 없이 붙은 STT 결과(101호출입 형태)가 가드를 우회하는 경로가 남아 있다(11장).

6.8 Gemini 3.5 Transcribe Live: 관측 → 보조로 단계 승격 (2026-08 ~ 09)

  • 배경: Google STT를 바꾸지 않고 두 번째 귀를 붙일 수 있는지 확인하고 싶었다. 3.5 Live는 N-best를 주지 않아서 주 경로로는 쓸 수 없었다.
  • 순서: PoC → 통화 결과에 영향 없는 관측 전용 shadow → 동·호수 전용 보조 → 모든 청취 창에서 Google과 병렬 실행. Google에서 방문 목적을 못 찾으면 VAD 종료 시점부터 최대 3초 기다렸다가 표준 라벨만 보충한다.
  • 측정:
    • 사전 replay(사람이 확인한 감사 통화 3건, 발화 창 5개): 문장 일치 4/5, 동·호수 3/3, 목적 3/3. 재생 종료 후 지연 122~225ms(중앙값 204ms, 3개 창)
    • 운영 반영 직후(통화 5건, 청취 창 8개): 성공 7/8, timeout 1, 목적 일치 7/7, 성공 결과 지연 평균 1.02초
  • 해석: 표본이 작아 정확도를 주장하지 않는다. 테스트 계획의 다음 합격 단계(offline 300턴, shadow 1,000창·7일)는 아직 기록이 없다.

6.9 분류 호출의 48%가 timeout이던 문제 (2026-09)

  • 문제: 9월 28일 실측에서 방문 목적 분류 호출 77회 중 37회(48%)가 2.0초 timeout으로 버려졌다.
  • 원인: thinking 토큰이 작은 출력 한도(maxOutputTokens 32)를 먼저 다 써 버려 응답이 비거나 잘렸다.
  • 해결: thinking 토큰 여유분(512)을 별도로 두고, 5.4의 비교를 근거로 방문 목적 텍스트 호출만 Gemini 3.6-flash로 옮겼다. 대화 LLM 기본값은 3.7-flash로 두고, 정확히 HTTP 503일 때만 3.6으로 한 번 폴백한다.
  • 함께 한 것: 외부 결정 모델 Jev를 판정 체인 앞에 shadow로 붙였다. 키워드로 풀리지 않은 9월 발화 442건 offline 재생에서 67건(15.2%)을 확정했고, 그중 63건(94.0%)이 통화의 최종 목적과 같았다(지연 p50 205ms). 기준값이 사람 검수 정답이 아니어서 active 전환은 보류했다.

6.10 자기 정정한 동·호수를 복구하고, 한 번의 미스로 끊기지 않게 (2026-10)

  • 문제: 호출자가 동·호수를 말하다 고쳐 말한 통화에서 Gemini는 정정까지 맞게 들었지만, 파서가 첫 매치만 써서 허용 목록에서 거절됐다. 그 미스가 재질문 한도를 다 써 상담원으로 넘어갔다. 5.3의 실패 20건 중 16건이 이 유형(동호수 재질문 초과)이었다.

  • 해결: 발화 안의 허용 후보가 하나뿐일 때만 채택하는 추출기와 세 가지 복구 금지 규칙을 넣었다. 허용 목록 미스 턴 1회는 재질문 한도에서 빼는 설정(GUIDED_ALLOWLIST_MISS_GRACE_TURNS, 기본 1)을 추가했다.

  • 검증: npm test 771개 통과(변경 전 736개). 07-30 이후 저장 발화 11,738건(미스 460건)에 새 파서를 재생했다.

    복구최종 발급과 일치불일치확인 불가
    복구 금지 규칙 없이16844
    복구 금지 규칙 적용 (배포본)9801

    금지 규칙이 없으면 잘못된 복구 4건이 생겨서 규칙을 넣은 판을 배포했다. 미스 턴 면제는 통화 흐름 단위라 재생으로 잴 수 없어 단위·브리지 테스트로만 검증했다.

6.11 운영 모니터링 웹을 다시 만들기 (2026-09)

  • 문제: 한 통화를 보려면 Relay 로그, Asterisk 로그, 통화 요약 파일, 녹음을 따로 찾아야 했고, 프로그램이 채택한 전사·공급자 원문·사람이 들은 내용을 구별할 화면이 없었다.
  • 해결:
    • 통화 검색: 기간·결과·녹음 유무·검수 상태·전사 내용으로 찾는다. 날짜별 영속 인덱스와 커서 페이지네이션을 쓴다.
    • 녹음 재생: HTTP Range, ±5초 이동, 배속, A–B 반복. 청취 창 단위로 "이 구간 듣기"를 제공한다.
    • 검수 기록: revision을 별도 파일에 원자적으로 저장하고, 동시 수정은 409로 막는다.
    • 공급자 비교: 녹음과 전사 쌍의 해시가 맞을 때만 보여 준다.
    • 사이트 설정 편집, SSE 증분 갱신.
  • 보안: 서버 사용자 파일 + scrypt 해시, 8시간 세션, HttpOnly·Secure·SameSite=Strict 쿠키, 쓰기 요청 CSRF 검사, viewer·reviewer·admin 역할과 장소 범위, 로그인 시도 제한. 공개 터널 없이 운영자 사설망 안에서만 TLS 1.3 HTTPS로 연다.
  • 검증: 단위 21개, Playwright E2E 5개(axe로 WCAG 2.2 AA 자동 검사), HTTPS 통합 1개. 합성 인덱스 10만 건에서 목록 API p95 14.45ms(warm 캐시, 단일 클라이언트이므로 부하 성능은 아님).
  • 결과: 2026-09-12 운영 반영, 녹음 12,002건 인덱싱, 실제 HTTPS 브라우저 접속 확인.

7. 개발 연혁

시기한 일
2026-02AudioSocket ↔ Gemini Live API 릴레이로 시작. 통화 종료 요약, 상담원 인계 안전장치, 동·호수 미확인 3회 인계, 실시간 모니터링 웹 1차, 운영 로그 보존 시작(02-22)
2026-03guided 모드 단순화, 재질문 한도 초과 시 상담원 자동 연결
2026-04멀티사이트 분기 구조, 상가·시설 방문 유형, 방문증 세부 사유 추출, AI REST API·SIP 레지스트리, PM2 기동 체계
2026-05소음 환경 STT 보정, 방문 목적 규칙 보강과 인계 전 최종 판정
2026-06출입제한(블랙리스트) 차량 안내, systemd timer watchdog, 통화 전체 오디오 최종 판정
2026-07동·호수 허용 목록과 음성 재전사, 첫 발화 오디오 폴백, 대화 LLM Gemini 3.6 전환, 오디오 replay·shadow 도구, 모든 guided 턴 전사 보조, 단독 119 긴급차량 처리
2026-080 하나 차이 동·호수 모호성 가드, 기본 LLM Gemini 3.7 + 503 폴백, Gemini 3.5 Transcribe Live PoC와 shadow
2026-093.5 Live 병렬 보조 운영, 모니터링 웹 재구성과 사설망 HTTPS 운영 반영, 방문 목적 텍스트 3.6 전환과 thinking 여유분, Jev shadow, 상가 키워드·STT 힌트 보강
2026-10자기 정정 동·호수 복구와 미스 면제 턴, 중고거래 방문 정책(허용 목록 동·호수가 있을 때만 입차), 재부팅 runbook, STT 모델 검토

8. 배포와 운영

8.1 운영 방식

  • 프로세스: Relay와 모니터링 웹을 PM2로 관리한다. graceful shutdown으로 진행 중인 통화 처리를 정리한다.
  • 전화망 감시: systemd timer watchdog이 40초마다 Asterisk와 PJSIP 등록을 확인하고, reload → 제한된 restart 순서로 복구한다.
  • 기능 단계 배포: 새 인식·판정 경로는 observe/shadow → assist → active 순서로 올린다. 통화 결과에 영향을 주기 전에 관측 필드로 효과와 실패를 먼저 본다.
  • 모델 전환: 모델과 경로는 환경 변수로 바꾼다. 대화 LLM, 방문 목적 텍스트 모델, 오디오 전사 모델을 따로 정할 수 있다.
  • 재현 도구: 저장된 발화와 WAV로 파서·판정을 다시 돌리는 replay 스크립트로 배포 전 효과와 부작용을 잰다(6.10).

8.2 운영하며 배운 것

  • "살아 있음"과 "전화를 받을 수 있음"은 다르다. 프로세스 생존만 보던 health check를 SIP 등록까지 넓혔다.
  • 허용 목록에 있다고 맞는 값은 아니다. 실제로 있는 다른 세대로 잘못 들을 수 있어서 위험 쌍을 따로 다룬다.
  • 정확도만큼 지연이 결과를 바꾼다. 같은 정확도라도 timeout 안에 오지 않으면 운영에서는 틀린 것과 같았다(5.4).
  • 복구 규칙은 복구 수보다 오복구 0이 먼저다. 금지 규칙 없이 16건을 살리는 판보다 9건을 살리고 오복구가 없는 판을 배포했다.
  • 수치는 측정 대상과 함께 적는다. 적용률·응답 성공률·기준값 일치율을 정확도처럼 말하지 않는다.

9. 품질과 테스트

범위내용
npm test (781개)방문 목적 판정, 허용 목록·모호성 가드, 동·호수 추출과 복구 금지 규칙, 오디오 버퍼·rolling WAV, guided 상태 전이·재질문 한도, 전사 보조 수명·timeout·폐기, 인계·방문증 요청 builder, STT VAD·힌트, 모델 설정과 503 폴백
모니터링 웹 단위 (21개)인덱스 갱신·페이지네이션, 녹음 전송·경로 보호·인증, 검수 원자 저장·동시 수정, 사이트 설정 검증, 역할·장소 범위·CSRF, 사용자 관리와 세션 폐기
모니터링 웹 E2E (5개)Playwright 화면 시나리오와 axe 접근성(WCAG 2.2 AA) 자동 검사
HTTPS 통합 (1개)배포 설정의 TLS 경로
watchdogPJSIP 등록 상태 parser를 scripts/test-asterisk-watchdog.sh로 별도 검증
운영 재생저장 발화·WAV로 파서와 판정을 재생해 배포 전 효과와 오복구를 측정

아직 통합 테스트로 묶지 못한 경로: 텍스트·오디오 판정이 모두 실패한 뒤 실제 상담원 연결, 외부 명령 전송 직전 허용 목록 차단, 블랙리스트 통화 분기, VAD의 실제 발화 종료.


10. 기술 스택과 결정 기록

영역사용 기술
전화망Asterisk, PJSIP, AudioSocket, AMI
서버Node.js, TypeScript, Express, SSE
음성·AIGoogle Cloud STT(telephony_short)·TTS, Gemini 3.7-flash / 3.6-flash / 3.5 Transcribe Live, Jev 결정 모델(shadow)
모니터링 웹TypeScript 서버·클라이언트, Playwright, axe-core
운영PM2, systemd timer, Tailscale(사설망 HTTPS)
결정이유감수한 비용
STT·LLM·TTS 분리단계별 모델·오류·지연을 따로 보고 교체연결 코드와 상태 관리 증가
결정론적 게이트 + LLM 보조출입 제어의 예측 가능성과 표현 다양성을 함께 확보규칙과 모델 경로를 함께 유지
자동 허용보다 unknown·상담원 인계오인식이 실제 출입으로 이어지는 위험 축소자동 처리율보다 안전 우선
Google STT 주 경로 + 3.5 Live 병렬 보조N-best와 전화 특화 모델은 유지하고 빠진 정보만 보충청취 창마다 두 번째 호출 비용
위험 쌍은 fail-closed실제로 있는 다른 세대로 여는 오승인을 막음확인 질문만큼 통화가 길어짐
방문 목적 텍스트는 3.6-flash같은 수준의 일치율에서 지연 p50 2,043ms → 1,274ms모델별 설정이 늘어남
shadow 먼저, active는 근거가 쌓인 뒤운영 통화에 영향 없이 효과와 실패를 측정기능이 운영에 반영되기까지 시간이 걸림
모니터링 웹은 사설망 전용녹음·전사라는 민감 데이터를 공개 인터넷에 두지 않음외부에서 접속하려면 사설망 참여가 필요

11. 한계와 다음 단계

과제현재 상태
실제 차단기 transport와 물리 상태 확인미해결. 레거시 명령 경로는 네트워크로 보내지 않고 대기 결과를 돌려준다. API 성공 응답과 차단기의 실제 개방을 구분해 받아야 한다
최종 사용자 확인 단계운영에서는 꺼 둔 상태로 코드에 고정했다. 동·호수 모호성 확인만 따로 동작한다. 어떤 조건에서 확인을 필수로 할지 정책과 테스트로 만들어야 한다
사이트 설정 원천 통합모니터링 웹이 실제 설정 파일을 검증·revision·원자 저장으로 편집하지만, Relay는 시작할 때 한 번만 읽어 재시작해야 반영된다
자격증명·개인정보 수명주기모니터 인증·권한·접근 기록, 원문 없는 관측 필드, 차량번호 마스킹은 갖췄다. 로컬 자격증명 폴백 제거와 녹음·전사 보존 기간·파기 자동화는 남았다. Jev에 보내는 발화는 동·호수를 가리지 않으므로 active 전에 전송 범위를 정해야 한다
보조 전사 지연과 블라인드 감사thinking 여유분을 적용했지만 효과 재측정과 오디오 tail·timeout·모델 지연 분리 측정은 남았다. 원음을 독립 판독하는 블라인드 감사는 아직 하지 않았다
모호성 가드 우회 경로공백 없이 붙은 STT 결과(101호출입 형태)가 가드를 우회하는 경로를 막아야 한다
3.5 Live와 Jev 다음 단계3.5 Live의 offline 300턴·shadow 1,000창 합격 단계, Jev의 active 전환 여부가 남았다
STT 모델 검토telephony_short 유지가 합리적이라고 판단했다. V2 chirp_3는 기존 실패 WAV로 shadow 비교할 후보로 남겼고, 현재 phrase hint가 실제로 적용되는지 근거를 확인해야 한다

재현성 제약: Asterisk dialplan과 PJSIP 설정은 저장소 밖에서 관리되므로 저장소만으로는 SIP 수신부터 상담원 인계까지의 전체 환경을 재현할 수 없다.


12. 이 프로젝트로 증명한 역량

역량근거
실시간 미디어 엔지니어링전화 코덱 변환, 오디오 전처리, VAD, 20ms 페이싱과 backpressure 제어
틀려도 안전한 AI 설계결정론적 게이트, 조건부 재전사, 위험 쌍 fail-closed, 단계별 판정 체인, 사람 인계
근거로 모델을 고르기3.7 vs 3.6 지연·일치율 비교, timeout 원인 분석(thinking 토큰), shadow → assist 단계 승격
전화망부터 운영 화면까지 통합Asterisk·AudioSocket, TypeScript Relay, Google·Gemini, 관제 API, 모니터링 웹
운영 자동화와 장애 대응통화 종료 요약, PJSIP 장애 원인 분석, systemd watchdog, replay 기반 배포 전 검증
민감 데이터를 다루는 운영 도구역할·장소 범위 권한, CSRF·세션 관리, 사설망 HTTPS, 접근성 자동 검사, 원문 없는 관측 기록