TCM ↔ AMD 라벨 연동 명세서
TAD 아웃바운드 오토콜이 남기는 발신 통화 녹취를 TCM이 STT+LLM으로 8클래스 분류하여 같은 키로 결과를 써넣는 연동 규약입니다. 이 라벨은 우리 AMD(Answering Machine Detection) 모델의 학습/자기강화 데이터가 됩니다. 본 문서 하나로 TCM 팀이 곧바로 구현에 착수할 수 있도록 계약을 정확히 기술합니다.
TAD 아웃바운드 오토콜은 모든 발신 통화의 녹취를 MinIO S3에 남깁니다. 여기에는 상대가 받는 응답 순간과 early-media(연결 전 음성)까지 포함됩니다 — 예: "연결이 되지 않아 소리샘으로 연결됩니다" 같은 통신사·사서함 안내 멘트까지 캡처됩니다.
TCM은 그 wav를 가져가(read) STT+LLM으로 8클래스 분류한 뒤, 같은 키(decision_id)로 결과를 써넣습니다(write). 우리는 이 라벨을 모아 AMD 모델의 학습 및 자기강화(self-reinforcing) 데이터셋으로 사용합니다.
key =
decision_id8클래스 분류
같은
decision_idAMD 학습셋 생성
① 우리 wav를 decision_id 키로 읽고 → ② STT+LLM 분류 → ③ 같은 decision_id 키로 결과를 써넣기. 그게 전부입니다. 콜백/통지 불필요, 우리 쪽 호출 불필요.
decision_id모든 객체(오디오 · 컨텍스트 라벨 · 정정 결과)는 콜 단위 고유 ID인 decision_id로 키링됩니다. 따라서 전화번호나 통화 시각으로 별도 매칭을 할 필요가 전혀 없습니다.
TCM은 우리 wav를 그 키(decision_id)로 읽고, 분석 결과를 같은 키로 써넣기만 하면 됩니다. 키만 보존하면 우리 쪽 export가 알아서 라벨과 정정을 결합합니다.
decision_id는 UUID 형태의 불투명(opaque) 문자열입니다. 의미를 해석하지 말고 그대로 전달·보존하세요.
| 항목 | 값 | 비고 |
|---|---|---|
| endpoint | https://s3intra.tlabs.team:9000 | 정식 TLS |
| bucket | tad | — |
| forcePathStyle | true | 필수 (MinIO) |
| region | us-east-1 | 임의값 가능 (MinIO는 무시) |
MinIO는 가상 호스트 스타일이 아닌 path-style로 동작합니다. AWS SDK v3 사용 시 반드시 forcePathStyle: true를 설정하세요. 누락하면 DNS/SSL 단계에서 실패합니다.
| 액션 | 접두사 (prefix) | 용도 |
|---|---|---|
| read | amd/audio/* | 분석 대상 통화 녹취 wav |
| read | amd/labels/* | (선택) 분석 힌트용 컨텍스트 |
| write | amd/corrections/* | 분류 결과 써넣기 (append) |
위 스코프로 제한된 전용 액세스 키를 별도의 안전한 채널로 전달합니다. 우리 마스터 키는 공유하지 않습니다. (자격증명 값은 본 문서에 포함하지 않습니다.)
ListObjectsV2(prefix="amd/audio/")키에서 decision_id 추출
corrections/{id}/ 중method=="tcm" 있으면 스킵GET →STT + LLM 8클래스
PUT같은 decision_id
- (1) 나열:
ListObjectsV2(prefix="amd/audio/"). 키 형식은amd/audio/YYYY/MM/DD/{decision_id}.wav— 파일명(확장자 제외)이 decision_id. - (2) 멱등 체크:
ListObjectsV2(prefix="amd/corrections/{decision_id}/")결과 객체들을 보고, 그중method=="tcm"인 정정이 이미 있으면 재처리하지 않고 스킵. - (3) 분석: wav를
GET으로 받아 STT+LLM으로 8클래스 분류. - (4) write: 결과를 correction 객체로
PUT(다음 섹션 출력 스펙 참조).
// npm i @aws-sdk/client-s3 import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; const s3 = new S3Client({ endpoint: "https://s3intra.tlabs.team:9000", region: "us-east-1", forcePathStyle: true, // ★ MinIO 필수 credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY } // 안전채널로 수령 }); const BUCKET = "tad"; // 1) 오디오 나열 → decision_id 추출 async function listDecisionIds(token) { const r = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: "amd/audio/", ContinuationToken: token })); return { ids: (r.Contents || []) .filter(o => o.Key.endsWith(".wav")) .map(o => ({ key: o.Key, id: o.Key.split("/").pop().replace(".wav", "") })), next: r.IsTruncated ? r.NextContinuationToken : null }; } // 2) 멱등: 이미 TCM이 처리했나? async function alreadyDone(id) { const r = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: `amd/corrections/${id}/` })); for (const o of (r.Contents || [])) { const body = await getJson(o.Key); if (body && body.method === "tcm") return true; // 스킵 } return false; } // 3) wav 분석 → 4) 결과 write async function processOne({ key, id }) { if (await alreadyDone(id)) return; const wav = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key })); const { klass, transcript, confidence } = await sttLlmClassify(wav.Body); // TCM 내부 const result = { schemaVersion: 1, correction_of: id, "class": klass, // 8클래스 중 하나 method: "tcm", confidence, // 0.0 ~ 1.0 transcript, // STT 텍스트 model: "tcm-stt-llm-v1", at: new Date().toISOString() // ISO8601 UTC }; await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: `amd/corrections/${id}/${Date.now()}.json`, Body: JSON.stringify(result), ContentType: "application/json" })); }
class 필드에 들어갈 수 있는 값은 아래 8개뿐| 클래스 | 설명 | 한국 통신 예시 |
|---|---|---|
| human | 사람이 응대 가능한 상태 (실제 통화 상대) | "여보세요?", "네 누구세요" |
| voicemail_greeting | 음성사서함 안내 멘트 (메시지 남기라는 안내) | "소리샘입니다", "삐 소리 후 메시지를 남겨주세요" |
| voicemail_beep | 사서함 녹음 시작 비프음 | 안내 멘트 뒤의 "삐—" 신호음 |
| carrier_announcement | 통신사 시스템 안내 (연결 불가/상태 안내) | "고객님이 전화를 받을 수 없습니다", 결번/정지/없는 번호 안내 |
| ringback | 일반 통화 연결음 (응답 전 기본 신호) | "뚜루루… 뚜루루…" |
| coloring | 통화 연결음 대체 음악·멘트 (가입자 설정 컬러링) | 음악 컬러링, "지금은 통화 연결 중입니다" 류 멘트 |
| fax_tone | 팩스/모뎀 톤 (데이터 신호) | "삐∼익" 고음 캐리어 톤 |
| silence_noise_unknown | 무음 · 잡음 · 위 어디에도 해당 없거나 판별 불가 | 무음, 잡음만, 판단 불가 |
ringback은 통신사 기본 연결음(뚜루루)이고, coloring은 가입자가 설정한 음악·멘트형 컬러링입니다. 컬러링을 사람/안내로 오분류하지 않도록 주의하세요.
| 항목 | 값 |
|---|---|
| 키 패턴 | amd/audio/YYYY/MM/DD/{decision_id}.wav |
| 포맷 | 16kHz · mono · WAV (PCM) |
| 구간 | 발신~종료 통화 녹취 (응답 순간 + early-media 안내 포함) |
YYYY/MM/DD는 콜이 기록된 날짜 파티션이며, 파일명(확장자 제외)이 decision_id입니다.
| 항목 | 값 |
|---|---|
| 키 패턴 | amd/labels/YYYY/MM/DD/{decision_id}.json |
| 주요 필드 | amd.verdict · amd.confidence · amd.features (우리 엔진의 1차 판정/신뢰도/추출 피처) |
이 JSON은 분석 정확도를 높이기 위한 힌트로만 쓰세요. 읽지 않아도 무방하며, 어떤 경우에도 수정 금지입니다(섹션 7 참조).
컨텍스트 라벨에는 원문 전화번호나 상담원 ID가 들어 있지 않습니다. PII는 HMAC 해시(number_hash, agent_id_hash)로만 존재합니다.
| 항목 | 값 |
|---|---|
| 키 패턴 | amd/corrections/{decision_id}/{epoch_ms}.json |
| ContentType | application/json |
{epoch_ms}는 쓰기 시각의 epoch 밀리초(예: Date.now()). 같은 decision_id 폴더에 객체를 append하는 구조입니다.
| 필드 | 타입 | 설명 |
|---|---|---|
| schemaVersion | int | 고정 1 |
| correction_of | string | 대상 decision_id (조인 키) |
| class | string | 8클래스 중 하나 (섹션 5) |
| method | string | 고정 "tcm" — 멱등/우선순위 판별의 근거 |
| confidence | number | 0.0 ~ 1.0 |
| transcript | string | STT 텍스트 |
| model | string | 모델/버전 식별자 (예: tcm-stt-llm-v1) |
| at | string | ISO8601 UTC 시각 |
{
"schemaVersion": 1,
"correction_of": "3f2a7c9e-1b4d-4a6f-8c11-9d0e2f5a7b33",
"class": "voicemail_greeting",
"method": "tcm",
"confidence": 0.94,
"transcript": "연결이 되지 않아 소리샘으로 연결됩니다. 삐 소리 후...",
"model": "tcm-stt-llm-v1",
"at": "2026-05-27T08:21:33.512Z"
}
amd/labels/...의 원본 라벨은 절대 수정/삭제하지 마세요. 결과는 오직 amd/corrections/에만 append합니다. 원본 불변성이 전체 감사추적(audit trail)의 근간입니다.
같은 decision_id에 이미 method=="tcm" 정정이 존재하면 재처리 금지. 신규(미처리) 콜만 처리하세요. (섹션 4 STEP 2)
S3에 직접 객체를 쓰는 대신 HTTP API로 결과를 제출하고 싶다면 아래 엔드포인트를 사용할 수 있습니다.
POST https://auto.tla.so/api/amd/label/{decision_id} Content-Type: application/json { "class": "<8클래스>", "method": "tcm", "confidence": 0.0, "transcript": "STT 텍스트" }
다만 S3 직접 쓰기를 권장합니다 — 더 단순하고 디커플드(decoupled)하며, 우리 API 가용성에 의존하지 않고 배치로 대량 처리하기에 유리합니다.
- 배치 주기: 예시로 5~15분 간격 폴링. 신규 오디오만 집어 처리하므로 주기는 처리량에 맞춰 조정.
- 신규분만 처리(멱등): 매 배치마다 섹션 4 STEP 2의 멱등 체크로 이미 처리한 콜은 건너뜀. 재실행해도 안전.
- 동시성 / 레이트: 적정 동시 처리 수와 요청 레이트를 설정해 MinIO/네트워크에 과부하를 주지 않도록. (점진적으로 올려가며 조정 권장)
- 실패 시: 일시 실패는 지수 백오프 재시도, 반복 실패 건은 격리(quarantine)해 두고 다음 배치에서 다시 시도. 한 건 실패가 배치 전체를 멈추지 않도록.
- 로깅: 처리량 · 지연(latency) · 스킵/성공/실패 카운트를 로깅해 추이를 관찰.
모든 동작은 멱등 + 신규분만입니다. 중복 PUT을 피하고(같은 decision_id에 tcm 정정 1건), 실패는 재시도/격리로 흡수하면 됩니다.
오디오 객체에는 실제 통화 음성이 담겨 있습니다. 접근통제 · 전송/저장 암호화 · 보존기간 · 파기정책을 반드시 준수하세요. 분석 후 불필요한 원본/사본을 보관하지 마세요.
한국 통신비밀보호법 · 개인정보보호법을 고려해야 하며, 운영 전 법무 검토가 필요합니다.
우리 라벨 JSON에는 전화번호 원문이 없고 해시(HMAC)만 저장됩니다. TCM이 생성하는 correction에도 원문 PII(번호/이름 등)를 넣지 마세요. transcript에 PII가 섞일 수 있으니 내부 보존·취급 정책에 따라 관리하세요.
우리 export 단계가 decision_id별로 라벨과 정정들을 모아 우선순위 규칙으로 최종 effective_class를 산출합니다. 우선순위는 다음과 같습니다.
TCM은 결과를 써넣기만 하면 됩니다. 병합·우선순위·학습셋 생성은 전부 우리 export가 처리합니다. 콜백/통지 불필요.