feat: full-platform STT API (v2.3 consensus plan) #1

Open
lukehemmin wants to merge 21 commits from feat/full-platform into main
Showing only changes of commit b7c30f8b71 - Show all commits
+866
View File
@@ -0,0 +1,866 @@
# Luke Scribe v0.1 설계
- 작성일: 2026-08-10
- 저장소: `lukehemmin/luke_scribe`
- 브랜치: `main`
- 상태: **v0.1 범위 승인**
- 모드: Builder / Research
- 상위 문서:
- `.omc/specs/deep-interview-luke-scribe-stt-api.md`
- `.omc/plans/consensus-luke-scribe-stt-api.md`
## 1. 결정 요약
Luke Scribe의 최종 방향은 파일 전사와 실시간 전사를 제공하는 self-hosted STT API다.
하지만 v0.1에서는 플랫폼 전체를 만들지 않는다. 먼저 아래 질문에 측정값으로 답한다.
> 한국어와 영문 기술용어가 섞인 음성을 우리 하드웨어에서 어느 모델과 설정으로 전사해야 하는가?
v0.1은 `WAV/MP3 -> TranscriptResult JSON` 전사 코어와 모델 벤치마크만 완성한다.
REST API, Redis 큐, WebSocket, 화자 분리, LLM 후처리는 이후 버전으로 미룬다.
단, v0.1을 폐기형 프로토타입으로 만들지는 않는다. 다음 다섯 가지 계약은 이후 버전에서도 유지한다.
1. 정규화된 오디오 입력 계약
2. 전사 엔진 인터페이스
3. `TranscriptResult` 결과 스키마
4. 장치 탐지 및 실행 프로파일
5. 단일 전사를 조율하는 `TranscriptionService`
이 경계 덕분에 v0.2의 FastAPI와 v0.3 이후의 큐·실시간 처리는 전사 코어를 수정하지 않고 호출 방식만 추가할 수 있다.
## 2. 제품 정의
> **Luke Scribe는 한국어와 영문 기술용어가 섞인 음성을 외부 STT API에 보내지 않고 통제된 실행 환경에서 전사하는 엔진이자 API다.**
### 첫 사용자
Luke Scribe를 호출할 Luke의 내부 서비스와 개발 도구다.
### 첫 번째 성공 장면
개발자가 실제 회의 또는 기술 설명이 담긴 WAV/MP3 파일을 CLI에 전달한다. Luke Scribe는 다음 정보를 포함한 JSON을 생성한다.
- 전체 전사문
- 구간별 시작·종료 시각과 텍스트
- 감지 언어
- 실제 사용한 모델, 장치, 정밀도
- 전사 시간과 RTF(real-time factor)
- 경고 및 강등 내역
결과에서 `API`, `vLLM`, `FastAPI`, `Kubernetes`, `GPU` 같은 핵심 기술용어가 원문 표기로 보존되어야 한다.
## 3. v0.1 범위
### 포함
- Python 3.11+ 프로젝트 구조와 CLI
- `faster-whisper` 기반 전사 엔진
- `large-v3-turbo``large-v3` 비교
- CPU 및 CUDA 장치 탐지
- 실행 장치와 `compute_type` 자동 선택 및 명시적 override
- WAV/MP3 입력
- ffmpeg 기반 16 kHz mono 오디오 정규화
- 전체 텍스트와 세그먼트 타임스탬프를 포함한 JSON 출력
- 한국어·영문 기술용어 혼용 벤치마크
- WER, K-CER, entity 보존율, 처리 시간, RTF, VRAM/RSS 측정
- 임시 파생 오디오 삭제
- 단위·통합·스모크 테스트
### 제외
- FastAPI와 HTTP 엔드포인트
- Redis/RQ와 비동기 Job 큐
- WebSocket 실시간 전사
- 화자 분리
- SRT/VTT 출력
- glossary/rules/LLM 후처리
- Docker Compose 운영 구성
- Colab 터널
- 4시간/2GB 운영 보장
- 다중 GPU 및 다중 워커
제외 항목은 포기한 기능이 아니다. 모델과 결과 계약이 검증된 뒤 순서대로 연결한다.
### 런타임 기준선
구현 시작 시 `pyproject.toml`과 lockfile에서 정확한 버전을 고정한다. 문서 수준의 지원 범위는 다음과 같다.
| 항목 | v0.1 기준 |
|---|---|
| Python | 3.11 이상, 구현 시 선택한 minor 버전 고정 |
| faster-whisper | 구현 시 검증한 단일 버전 고정 |
| CTranslate2 | faster-whisper 및 CUDA와 함께 검증한 버전 고정 |
| ffmpeg / ffprobe | 시스템 실행 파일 필수, 시작 전 존재 및 버전 확인 |
| 모델 | `large-v3-turbo`, `large-v3` |
| 모델 획득 | Hugging Face에서 최초 1회 다운로드 후 로컬 캐시 |
| 네트워크 없음 | 캐시된 모델만 사용; 없으면 `model_unavailable_offline`로 실패 |
| 디스크 | 두 모델 캐시와 임시 WAV를 저장할 여유 공간을 preflight에서 검사 |
| 필수 GPU | **Colab T4 16GB를 v0.1 기준 GPU로 우선 확정** |
| 필수 CPU | x86_64, RAM 16GB 이상을 qualification 기준으로 사용 |
GTX 1050, L4, A100, H100은 추가 관측 대상이지 v0.1 완료를 막는 필수 환경이 아니다. Colab T4를 사용할 수 없게 되면 구현 전에 동급 이상의 NVIDIA GPU 한 종을 명시적으로 대체 지정한다.
Colab T4는 공개·합성·익명화한 benchmark 자료의 GPU qualification에만 사용한다. 실제 내부 음성은 자체 통제 GPU에서만 처리하며 Colab으로 업로드하지 않는다.
### v0.1 지원 입력 범위
- 컨테이너: WAV, MP3
- 오디오 코덱: ffmpeg가 PCM WAV 또는 MP3로 식별한 입력
- 검증 방식: 파일 확장자가 아니라 ffprobe 결과 사용
- 검증 보장 범위: 파일당 60분 이하, 1GB 이하
- 손상·부분 다운로드 파일: `audio_probe_failed`
- ffprobe/ffmpeg timeout: 기본 60초, CLI 옵션으로만 상향 가능
- 더 긴 파일: 명시적으로 `unsupported_input_envelope` 반환
## 4. 핵심 전제
1. 현재 가장 큰 불확실성은 API 구조가 아니라 실제 KO+EN 혼용 전사 품질이다.
2. `large-v3-turbo`를 기본 후보로 두되, 벤치마크를 통과하지 못하면 `large-v3`를 기본으로 채택한다.
3. v0.1은 한 프로세스에서 한 파일을 처리한다. 동시성은 아직 제품 가치 검증에 필요하지 않다.
4. 모든 상위 계층은 동일한 `TranscriptionService``TranscriptResult`를 사용한다.
5. 약한 하드웨어에서 조용히 품질을 낮추지 않는다. 실제 선택된 모델과 정밀도를 결과에 기록한다.
6. 외부 STT API로 음성이나 전사문을 전송하지 않는다.
## 5. 실행 흐름
```text
WAV / MP3
|
v
Audio Ingest
- 형식/크기 검사
- duration probe
- ffmpeg -> 16 kHz mono WAV
|
v
Device Resolver
- CPU/CUDA 탐지
- compute_type 결정
- 사용자 override 검증
|
v
Transcription Engine
- faster-whisper
- turbo 또는 large-v3
- VAD / language / hotwords 옵션
|
v
TranscriptResult
- text / segments
- model/device/precision
- timings/warnings
|
+-----------------> JSON 파일
|
+-----------------> Benchmark evaluator
- K-CER
- entity retention
- RTF
- VRAM/RSS
```
## 6. 확장 가능한 경계
확장성을 위해 모든 기능을 미리 추상화하지 않는다. 아래 다섯 경계만 안정적인 계약으로 만든다.
### 6.1 `AudioIngestor`
책임:
- 입력 파일 검증
- ffprobe로 메타데이터 확인
- ffmpeg로 정규화
- 임시파일 수명 관리
v0.1에서는 로컬 파일만 받는다. 이후 HTTP 업로드와 영상 파일은 이 계층 앞에 입력 어댑터로 추가한다.
`NormalizedAudio` 계약:
| 필드 | 형식 | 필수 | 규칙 |
|---|---|---:|---|
| `path` | path | 예 | Luke Scribe가 소유한 임시 정규화 파일 |
| `duration_sec` | float | 예 | `> 0`, ffprobe 측정값 |
| `sample_rate` | int | 예 | 항상 `16000` |
| `channels` | int | 예 | 항상 `1` |
| `sample_format` | enum | 예 | v0.1은 `s16`만 |
| `source_codec` | string | 예 | ffprobe가 보고한 원본 코덱 |
| `source_size_bytes` | int | 예 | `> 0` |
### 6.2 `TranscriptionEngine`
개념적 인터페이스:
```python
class TranscriptionEngine(Protocol):
def transcribe(
self,
audio: NormalizedAudio,
options: TranscriptionOptions,
) -> TranscriptResult: ...
```
v0.1 구현은 `FasterWhisperEngine` 하나만 둔다. 멀티 엔진 플러그인 시스템은 만들지 않는다.
`TranscriptionOptions` 계약:
| 필드 | 형식 | 기본값 | 제약 |
|---|---|---|---|
| `model` | enum | benchmark 전 `large-v3-turbo` | `large-v3-turbo` 또는 `large-v3` |
| `language` | string/null | `ko` | `null`이면 자동 감지 |
| `device` | string | `auto` | `auto`, `cpu`, `cuda`, `cuda:N` |
| `compute_type` | string/null | `null` | `null`이면 resolver가 결정 |
| `vad` | bool | `true` | faster-whisper VAD 사용 여부 |
| `hotwords` | list[string] | `[]` | 빈 문자열 제거, 중복 제거 |
### 6.3 `TranscriptResult`
CLI, 향후 REST API, 큐 워커, WebSocket 최종 결과가 공유하는 정규 형태다. 전송 형식과 내부 모델을 분리해 향후 필드를 추가해도 기존 소비자가 깨지지 않게 한다.
### 6.4 `DeviceProfile`
장치 탐지 결과와 실행 결정을 분리한다.
- 탐지값: CPU, GPU 이름, compute capability, 총/가용 VRAM
- 결정값: device, compute type, model
- 출처: `auto` 또는 사용자 override
- 경고: CPU fallback, 정밀도 변경, 모델 미지원
v0.1에서는 한 장치와 한 실행만 선택한다. 워커 수 계산은 v0.2 이후에 추가한다.
장치 resolver는 다음 순서로만 결정한다.
| 요청 | 탐지 조건 | 결과 |
|---|---|---|
| `device=cpu` | CPU 사용 가능 | CPU + `int8` |
| `device=cuda[:N]` | 지정 GPU 존재, 모델 적재 가능 | 지정 GPU + 요청/자동 compute type |
| `device=cuda[:N]` | GPU 없음·인덱스 오류·적재 불가 | fallback 없이 실패 |
| `device=auto` | CUDA GPU에서 모델 적재 가능 | CUDA 사용; cc>=7.0이면 `float16` 우선 |
| `device=auto` | CUDA는 있으나 float16 부적합 | 지원되는 `int8_float16` 또는 `int8` |
| `device=auto` | GPU 없음 또는 모델 적재 불가 | CPU `int8`, warning 기록 |
| 명시 `compute_type` | 런타임이 지원하지 않음 | fallback 없이 실패 |
자동 모델 추천값은 benchmark 전에는 `unresolved`다. `detect`는 하드웨어상 적재 가능한 모델 목록만 보여준다. benchmark가 승인한 결정 파일이 있으면 그때부터 `recommended_model`을 출력한다.
`DeviceProfile` 필수 필드:
- `requested_device`, `selected_device`, `selection_source`
- `device_name`, `compute_capability`
- `vram_total_mb`, `vram_free_mb`
- `requested_compute_type`, `selected_compute_type`
- `loadable_models`, `recommended_model`
- `warnings`
### 6.5 `TranscriptionService`
`TranscriptionService`는 안정적인 애플리케이션 경계다. 입력 수명 관리, 장치 선택, 엔진 호출, timing 수집, 결과 조립을 담당한다. CLI는 이 서비스만 호출한다. 이후 API와 큐 워커도 동일한 서비스를 호출한다.
```python
class TranscriptionService:
def transcribe_file(
self,
source: Path,
options: TranscriptionOptions,
) -> TranscriptResult: ...
```
서비스 내부의 클래스 구성과 orchestration 순서는 변경할 수 있지만, 위 호출 의미와 `TranscriptResult` 계약은 v1 동안 유지한다.
## 7. 권장 프로젝트 구조
```text
luke_scribe/
├── docs/
│ └── luke-scribe-v0.1-design.md
├── benchmarks/
│ ├── README.md
│ ├── manifest.yaml
│ └── references/
├── src/luke_scribe/
│ ├── __init__.py
│ ├── cli.py
│ ├── config.py
│ ├── audio/
│ │ ├── ingest.py
│ │ └── models.py
│ ├── devices/
│ │ ├── detect.py
│ │ └── profile.py
│ ├── engine/
│ │ ├── base.py
│ │ └── faster_whisper.py
│ ├── pipeline/
│ │ └── transcribe.py
│ ├── results/
│ │ ├── models.py
│ │ └── json_writer.py
│ └── benchmark/
│ ├── evaluator.py
│ ├── metrics.py
│ └── report.py
├── tests/
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── pyproject.toml
└── README.md
```
`api/`, `jobqueue/`, `realtime/`, `postprocess/` 디렉터리는 실제 기능을 구현하는 버전에서 추가한다. 빈 디렉터리나 가짜 인터페이스를 미리 만들지 않는다.
## 8. CLI 계약
### 장치 탐지
```bash
luke-scribe detect
```
출력 예시:
```json
{
"requested_device": "auto",
"selected_device": "cuda:0",
"selection_source": "auto",
"device_name": "NVIDIA T4",
"compute_capability": "7.5",
"vram_total_mb": 15360,
"vram_free_mb": 14820,
"requested_compute_type": null,
"selected_compute_type": "float16",
"loadable_models": ["large-v3-turbo", "large-v3"],
"recommended_model": null,
"warnings": []
}
```
`recommended_model=null`은 benchmark 결정 파일이 아직 없다는 뜻이다.
### 단일 파일 전사
```bash
luke-scribe transcribe samples/meeting.mp3 \
--language ko \
--model large-v3-turbo \
--device auto \
--output result.json
```
필수 동작:
- 성공 시 exit code `0`
- 입력 오류 시 exit code `2`
- 모델/장치 초기화 실패 시 exit code `3`
- 전사 실패 시 exit code `4`
- 오류는 stderr, 결과 JSON은 지정 파일 또는 stdout으로 출력
- override를 적용할 수 없으면 조용히 변경하지 않고 명확하게 실패
### 벤치마크
```bash
luke-scribe bench benchmarks/manifest.yaml \
--models large-v3-turbo,large-v3 \
--output benchmark-report.json
```
벤치마크는 같은 정규화 오디오와 같은 옵션을 사용해 모델만 바꿔 비교한다.
### 전체 옵션 계약
| 명령 | 옵션 | 기본값 | 규칙 |
|---|---|---|---|
| `detect` | `--json` | `true` | v0.1은 JSON 출력만 보장 |
| `transcribe` | `--language` | `ko` | `auto`이면 `null`로 전달 |
| `transcribe` | `--model` | 결정 파일 또는 turbo | 지원 enum만 허용 |
| `transcribe` | `--device` | `auto` | `cpu`, `cuda`, `cuda:N` |
| `transcribe` | `--compute-type` | `auto` | 명시값은 fallback 금지 |
| `transcribe` | `--vad/--no-vad` | `--vad` | 상호 배타적 |
| `transcribe` | `--hotword` | 없음 | 반복 가능 |
| `transcribe` | `--output` | stdout | `-`도 stdout 의미 |
| `transcribe` | `--force` | `false` | 기존 출력 파일 overwrite 허용 |
| `transcribe` | `--log-level` | `INFO` | 로그는 stderr |
| `bench` | `--models` | 두 모델 모두 | 한 모델만 지정 가능 |
| `bench` | `--device` | `auto` | 한 run 안에서는 고정 |
| `bench` | `--compute-type` | `auto` | 모델 간 동일 정책 적용 |
| `bench` | `--hotword-set` | `none,domain` | 반복 가능; 정의된 실험군만 허용 |
| `bench` | `--repeats` | `3` | warm-up 이후 측정 반복 수 |
| `bench` | `--output` | 필수 | JSON report 경로 |
기존 파일이 있고 `--force`가 없으면 모델을 로드하기 전에 실패한다. stdout에 성공 JSON을 쓰는 동안에는 progress bar를 출력하지 않는다.
`bench --models`에 한 모델만 지정하면 report만 생성한다. 모델 결정에는 두 모델의 동일 실행 환경 비교가 필요하므로 단일 모델 run은 decision artifact를 생성하거나 기존 artifact를 변경할 수 없다.
## 9. 결과 스키마 v1
```json
{
"schema_version": "1.0",
"status": "completed",
"source": {
"name": "meeting.mp3",
"codec": "mp3",
"size_bytes": 804231
},
"normalized_audio": {
"duration_sec": 42.8,
"audio_format": "pcm_s16le",
"sample_rate": 16000,
"channels": 1
},
"execution": {
"model": "large-v3-turbo",
"device": "cuda:0",
"compute_type": "float16",
"language_requested": "ko",
"language_detected": null,
"language_detection_confidence": null
},
"timings": {
"model_load_sec": 4.21,
"transcription_sec": 6.32,
"rtf": 0.148
},
"text": "오늘 API 서버에서 vLLM을 사용해 보겠습니다.",
"segments": [
{
"index": 0,
"start": 0.52,
"end": 4.84,
"text": "오늘 API 서버에서 vLLM을 사용해 보겠습니다.",
"avg_logprob": -0.21,
"no_speech_prob": 0.01
}
],
"warnings": []
}
```
### 스키마 규칙
- `schema_version`은 필수다.
- 시간은 초 단위 실수다.
- `text`는 세그먼트를 읽기 순서대로 합친 최종 문자열이다.
- 모델·장치·정밀도는 요청값이 아니라 실제 적용값을 기록한다.
- 확장 필드는 추가할 수 있지만 기존 필드의 의미와 자료형은 v1 안에서 바꾸지 않는다.
- 실패 결과는 동일한 envelope에서 `status="failed"`, `error.code`, `error.message`를 제공한다.
- `language_requested`가 언어를 강제하면 `language_detected`와 confidence는 `null`이다.
- 자동 감지일 때만 감지 언어와 confidence를 기록한다.
- 세그먼트는 `index` 오름차순이며 서로 역행할 수 없다.
- 초 단위 시간은 JSON number로 기록하고 최소 millisecond 정밀도를 보존한다.
- `text`는 각 세그먼트의 trim된 텍스트를 단일 공백으로 연결한 값이다.
- 소비자는 알 수 없는 추가 필드를 무시해야 한다.
구현 시 이 계약을 `docs/schemas/transcript-result-v1.schema.json`으로 옮기고 fixture를 JSON Schema validator로 검증한다.
실패 envelope 예시:
```json
{
"schema_version": "1.0",
"status": "failed",
"source": {"name": "broken.mp3"},
"error": {
"code": "audio_probe_failed",
"message": "입력 오디오를 해석할 수 없습니다.",
"retryable": false
},
"warnings": []
}
```
## 10. 벤치마크 설계
### 데이터셋 구성
v0.1 판단용 데이터셋은 최소 다음 조건을 충족한다.
- 총 30개 이상 클립
- 총 음성 길이 60분 이상
- 실제 사용 환경에서 수집한 한국어 중심 음성
- 조용한 녹음, 생활 소음, 마이크 거리 차이를 모두 포함
- 단독 화자와 두 명 이상 대화 포함
- 영문 기술용어 entity 최소 100회 등장
- 각 클립에 사람이 검수한 정답 전사문과 entity 목록 제공
개인정보가 있는 원본은 저장소에 커밋하지 않는다. `manifest.yaml`에는 로컬 경로 또는 익명화된 fixture만 기록한다.
`manifest.yaml` 필수 필드:
```yaml
dataset_version: "1.0"
hotword_sets:
none: []
domain: [API, vLLM, FastAPI, Kubernetes, LLM, GPU]
clips:
- id: ko-en-001
audio_path: /secure/local/path/ko-en-001.wav
reference_path: references/ko-en-001.txt
language: ko
entities:
- canonical: API
surface: API
start_char: 3
end_char: 6
- canonical: vLLM
surface: vLLM
start_char: 12
end_char: 16
tags: [clean, single-speaker]
```
entity annotation의 `start_char`/`end_char`는 UTF-8 byte가 아니라 reference Unicode code point index다. 같은 entity가 두 번 나오면 occurrence를 두 개 기록한다.
`domain` hotword set은 평가 전 고정한 공통 용어집이다. 클립별 정답 entity를 실행 시 주입하지 않는다. 이렇게 해야 benchmark 정답을 모델 입력으로 누출하지 않는다.
hotword set은 `benchmarks/hotwords/domain-v1.json`처럼 별도 artifact로 저장한다. artifact에는 `version`, 정렬된 `terms`, `sha256`을 포함한다. manifest와 model decision은 이름뿐 아니라 artifact 경로·버전·hash를 참조한다.
### 필수 지표
| 지표 | 의미 | v0.1 사용 목적 |
|---|---|---|
| WER | 전체 단어 오류율 | 일반적인 전사 품질 비교 |
| K-CER | 한국어 정규화 후 문자 오류율 | 한국어 띄어쓰기 차이 영향 완화 |
| Entity 보존율 | 기술용어가 원형대로 남은 비율 | 핵심 제품 품질 판정 |
| RTF | 처리 시간 / 오디오 길이 | 장치별 처리 속도 판정 |
| Peak VRAM/RSS | 최대 메모리 사용량 | 배포 가능 장치 판정 |
| Failure rate | 실패 클립 / 전체 클립 | 안정성 판정 |
### 지표 계산 규칙
K-CER용 정규화는 다음 순서로 고정한다.
1. Unicode NFKC 정규화
2. 영문은 lowercase로 변환하되 entity 평가는 원래 대소문자를 유지
3. 문장부호 제거
4. 모든 whitespace 제거
5. 숫자는 아라비아 숫자로 통일하는 별도 mapping file 적용
6. Unicode code point 단위로 Levenshtein distance 계산
K-CER는 전체 reference 문자 수를 분모로 substitution + deletion + insertion을 합산한 corpus-level micro average다. WER는 일반 비교를 위한 보조 지표로 원래 whitespace token을 사용한다. 클립별 지표는 별도로 보존하되 모델 선택에는 corpus aggregate K-CER를 사용한다.
entity 보존은 manifest의 occurrence annotation 수를 분모로 한다. K-CER 계산에서 생성한 reference-hypothesis 문자 alignment를 이용해 각 reference entity span을 hypothesis 위치에 투영한다. 투영된 구간의 앞뒤 8 Unicode code point window 안에 canonical 표기가 정확히 존재할 때만 성공이다. 하나의 hypothesis occurrence는 한 annotation에만 매칭한다. 따라서 다른 문장에 우연히 생성된 entity가 실제 누락을 상쇄하지 못한다.
### 벤치마크 실행 규칙
1. 모델·장치·compute type 조합마다 별도 프로세스를 사용한다.
2. 측정 전에 비평가용 warm-up 클립 하나를 1회 처리한다.
3. 각 평가 클립을 기본 3회 실행한다.
4. 품질 지표는 아래 deterministic decode 설정의 첫 결과를 사용한다.
5. 시간과 메모리는 3회 min, median, max를 보고한다.
6. `model_load_sec`는 별도 측정하고 RTF에서는 제외한다.
7. RTF는 순수 transcription wall time / audio duration으로 계산한다.
8. RSS는 `psutil`로 해당 benchmark process의 RSS를 100ms 간격 sampling한다.
9. VRAM은 NVML의 해당 PID 사용량을 100ms 간격 sampling한다.
10. 환경 정보에는 OS, Python, ffmpeg, faster-whisper, CTranslate2, CUDA, cuDNN, GPU driver 버전을 기록한다.
고정 decode 설정:
```yaml
beam_size: 5
temperature: 0.0
condition_on_previous_text: true
vad_filter: true
vad_parameters:
min_silence_duration_ms: 500
speech_pad_ms: 200
word_timestamps: false
without_timestamps: false
```
`language`, `model`, `device`, `compute_type`, `hotword_artifact`는 실험 변수로 report `run_config`에 기록한다. 그 밖의 faster-whisper decode 기본값도 실제 해석값을 모두 report에 직렬화한다.
benchmark report 필수 top-level 필드:
- `report_version`, `dataset_version`, `generated_at`
- `environment`
- `run_config`
- `models[]`
- 모델·hotword variant별 `wer`, `k_cer`, `entity_retention`, `failure_rate`
- 모델·hotword variant별 `rtf_min`, `rtf_median`, `rtf_max`
- 모델·hotword variant별 `peak_rss_min_mb`, `peak_rss_median_mb`, `peak_rss_max_mb`
- 모델·hotword variant별 `peak_vram_min_mb`, `peak_vram_median_mb`, `peak_vram_max_mb`
- `decision.status`, `decision.default_model`, `decision.reasons[]`
### 모델 선택 게이트
`large-v3`를 정확도 기준선으로 사용한다. 먼저 각 모델이 절대 품질 기준을 통과하는지 검사한다.
절대 품질 기준:
1. entity 보존율 `>= 95%`
2. K-CER `<= 15%`
3. 실패율 `0%`
어느 모델도 절대 기준을 통과하지 못하면 `decision.status="no_acceptable_model"`로 끝낸다. 이 결과도 유효한 v0.1 연구 결론이지만, 제품 기본 모델 승인과 v0.2 진입은 차단한다.
각 모델은 먼저 자신의 최종 배포 variant를 정한다. `none`이 절대 기준을 통과하면 `none`을 선택한다. `none`은 실패하고 `domain`이 통과하면 version/hash가 고정된 `domain`을 선택한다. 둘 다 실패하면 해당 모델은 실패다. 이후 모델 비교에는 각 모델의 최종 배포 variant 지표만 사용한다. 따라서 평가 구성과 실제 기본 실행 구성이 달라지지 않는다.
완전한 모델 결정표:
| turbo | large-v3 | 결정 |
|---|---|---|
| 실패 | 실패 | `no_acceptable_model` |
| 통과 | 실패 | turbo 기본 |
| 실패 | 통과 | large-v3 기본 |
| 통과 | 통과 | 아래 상대 비교 적용 |
두 모델이 모두 절대 기준을 통과하면, `large-v3-turbo`가 아래 조건을 모두 만족할 때 turbo를 기본 모델로 선택한다.
1. entity 보존율 `>= 95%`
2. turbo의 K-CER가 large-v3보다 상대적으로 15% 넘게 나쁘지 않음
3. 실패율 `0%`
4. 대상 GPU에서 turbo의 median RTF가 large-v3보다 최소 10% 작음
하나라도 실패하면 `large-v3`를 기본 모델로 선택한다. 두 모델은 benchmark와 명시적 CLI override를 위해 계속 지원한다. 실시간 기본 모델은 WebSocket 프로토타입 단계에서 별도로 판단한다.
hotwords 적용 전·후 결과는 독립 run으로 기록한다. 결정 artifact에는 선택된 hotword set도 함께 저장한다.
### 모델 결정 artifact
`bench`는 report와 별도로 `benchmarks/decisions/default-model-v1.json`을 atomic write한다.
```json
{
"decision_version": "1.0",
"status": "approved",
"default_model": "large-v3-turbo",
"default_hotword_set": "none",
"hotword_artifact": null,
"dataset_version": "1.0",
"report_sha256": "...",
"model_variants": {
"large-v3-turbo": {"hotword_set": "none", "hotword_artifact": null},
"large-v3": {"hotword_set": "domain", "hotword_artifact": "benchmarks/hotwords/domain-v1.json", "hotword_sha256": "..."}
},
"decided_at": "2026-08-10T00:00:00Z"
}
```
`status``approved` 또는 `no_acceptable_model`이다. artifact의 schema, dataset version, report hash 검증이 실패하면 무시하지 않고 `invalid_model_decision`으로 실패한다.
`transcribe`의 모델 선택 순서:
1. 명시적 `--model`이 있으면 해당 모델 사용
2. 유효한 `approved` artifact가 있으면 default model/hotword set 사용
3. artifact가 없으면 연구 bootstrap으로 turbo를 사용하고 `model_decision_missing` warning 기록
4. `no_acceptable_model`이면 명시적 `--model` 없는 실행은 `model_decision_blocked`로 실패
hotword 선택은 모델 선택과 독립적으로 다음 우선순위를 따른다.
1. 하나 이상의 명시적 `--hotword`가 있으면 그 목록이 전부를 override
2. 그렇지 않고 decision artifact에 선택 모델의 `model_variants`가 있으면 해당 hotword artifact 사용
3. artifact가 없거나 해당 모델 기록이 없으면 hotwords 비활성화
명시적 `--model`로 기본 모델을 바꿔도 유효한 decision artifact 안에 해당 모델 variant가 있으면 그 모델의 검증된 hotword 설정을 사용한다.
## 11. 오류 및 안전 계약
| 코드 | 상황 | 동작 |
|---|---|---|
| `invalid_input` | 파일 없음, 지원하지 않는 형식 | 모델을 로드하기 전에 실패 |
| `audio_probe_failed` | ffprobe가 입력을 해석하지 못함 | 원인과 stderr 요약 반환 |
| `unsupported_input_envelope` | 60분 또는 1GB 지원 범위 초과 | 처리 시작 전에 실패 |
| `model_download_failed` | 모델 다운로드 실패 | 재시도 가능 여부 표시 |
| `model_unavailable_offline` | 네트워크와 캐시 모델 모두 없음 | 필요한 모델과 캐시 위치 표시 |
| `model_load_failed` | 모델 또는 CT2 초기화 실패 | 장치·정밀도·런타임 정보 포함 |
| `device_unavailable` | 요청한 CUDA 장치 없음 | 자동 CPU 변경 없이 실패 |
| `out_of_memory` | 모델 적재 또는 추론 OOM | 실패 시점과 실제 설정 기록 |
| `transcription_failed` | 추론 중 예외 | 원본 예외를 감싼 안정적 오류 코드 반환 |
자동 모드에서만 명시적인 fallback을 허용한다. fallback이 발생하면 `warnings`와 로그에 반드시 남긴다. 사용자가 `--device cuda:0`처럼 강제한 값은 자동으로 바꾸지 않는다.
fallback 및 retry 규칙:
| 실패 시점 | `auto` | 명시 override |
|---|---|---|
| GPU preflight 부적합 | CPU int8로 1회 전환 | 즉시 실패 |
| 모델 load OOM | float16 -> int8_float16 -> CPU int8, 최대 2회 전환 | 즉시 실패 |
| 모델 download 실패 | 네트워크 오류일 때만 1회 재시도 | 동일 |
| 추론 중 OOM | 전체 파일 자동 재실행 없이 실패 | 즉시 실패 |
| ffmpeg/probe 실패 | 재시도 없이 실패 | 동일 |
추론 중 OOM에서 자동 CPU 재실행을 하지 않는 이유는 긴 파일을 사용자 모르게 처음부터 다시 처리하지 않기 위해서다.
임시 디렉터리는 성공·실패·중단 모든 경로에서 정리한다. 원본 파일은 읽기 전용으로 취급하며 삭제하거나 수정하지 않는다.
정리 보장은 정상 종료와 catch 가능한 signal(`SIGINT`, `SIGTERM`)까지다. `SIGKILL`, 전원 장애, 호스트 crash에서는 즉시 삭제를 보장할 수 없다. 각 run은 고유 prefix가 있는 temp directory를 사용하고, 시작 시 TTL이 지난 Luke Scribe orphan directory를 정리한다.
CLI 실패 출력 규칙:
- `--output`이 지정되었고 입력 검증 이후 실패하면 해당 경로에 실패 envelope를 atomic write한다.
- stdout 모드에서는 실패 envelope를 stdout에 한 줄 JSON으로 출력한다.
- stderr에는 사람이 읽는 한 줄 요약과 로그만 출력한다.
- 출력 파일 자체를 쓸 수 없으면 JSON 생성을 시도하지 않고 exit code `5`로 종료한다.
- 부분 JSON 파일은 남기지 않는다. 임시 파일에 쓴 뒤 rename한다.
`TranscriptionService`는 실패 결과를 반환하지 않고 `LukeScribeError`의 typed subclass를 발생시킨다. CLI 어댑터가 이를 exit code와 실패 envelope로 변환한다.
| 오류 범주 | 실패 JSON | stderr | exit code |
|---|---:|---:|---:|
| CLI 문법·알 수 없는 옵션 | 아니요 | usage + 오류 | 2 |
| `invalid_input`, probe, 지원 범위 | 예 | 한 줄 요약 | 2 |
| 모델·장치·decision 오류 | 예 | 한 줄 요약 | 3 |
| 추론·OOM 오류 | 예 | 한 줄 요약 | 4 |
| 결과 파일 write 오류 | 아니요 | 한 줄 요약 | 5 |
## 12. 테스트 전략
### 단위 테스트
- 장치 탐지 결과에서 compute type 결정
- 사용자 override 검증
- ffprobe 결과 파싱
- ffmpeg 명령 구성
- 임시파일 정리
- 결과 스키마 직렬화와 역직렬화
- WER/K-CER/entity 보존율 계산
- 모델 선택 게이트의 경계값
- transcript JSON Schema validation
- manifest와 report schema validation
- orphan temp directory TTL 판정
### 통합 테스트
- 짧은 WAV 파일을 CPU로 전사
- MP3를 정규화한 뒤 전사
- 잘못된 파일과 손상 파일 거부
- 존재하지 않는 CUDA 장치 강제 시 명확한 실패
- 전사 성공·실패 후 임시파일 부재 확인
- 같은 fixture에 대해 두 모델의 benchmark report 생성
- 출력 경로가 이미 존재할 때 `--force` 계약 확인
- 실패 envelope의 stdout/file 동작 확인
### 실제 하드웨어 스모크 테스트
- x86_64/RAM 16GB CPU 환경 1종
- Colab T4 16GB 또는 구현 전에 지정한 동급 이상 NVIDIA GPU 1종
- 각 환경에서 `detect`, `transcribe`, `bench` 실행
- 모델, compute type, RTF, peak memory를 결과물로 보관
GPU가 없는 CI에서는 모델과 ffmpeg 호출을 대체한 계약 테스트까지만 수행한다. CPU의 대형 모델 qualification에는 별도 timeout과 최대 RSS를 기록하며, 너무 느린 실행은 실패가 아니라 `unsupported_for_production` 경고로 분류할 수 있다. 실제 GPU 모델 스모크 테스트는 Colab T4 job 또는 수동 검증으로 수행한다.
## 13. 완료 기준
v0.1은 다음 항목을 모두 만족해야 완료다.
- [ ] `detect`가 CPU/GPU 및 권장 실행값을 JSON으로 출력한다.
- [ ] `transcribe`가 WAV와 MP3를 처리해 schema v1 JSON을 생성한다.
- [ ] CPU와 실제 NVIDIA GPU 각각 한 환경에서 전사에 성공한다.
- [ ] benchmark dataset이 30개·60분·entity 100회 기준을 충족한다.
- [ ] turbo와 large-v3 비교 보고서가 생성된다.
- [ ] WER, K-CER, entity 보존율, failure rate, RTF, VRAM/RSS가 보고서에 포함된다.
- [ ] 정규화·warm-up·3회 반복·min/median/max 규칙으로 같은 데이터의 결과를 재현할 수 있다.
- [ ] 모델 선택 게이트가 `approved` 기본 모델 또는 `no_acceptable_model` 결정을 생성한다.
- [ ] 어느 모델도 절대 품질 기준을 통과하지 못하면 v0.2 진입이 차단된다.
- [ ] 성공·실패 경로에서 임시 파생 오디오가 남지 않는다.
- [ ] catch 가능한 중단과 시작 시 orphan cleanup이 검증된다.
- [ ] 단위·통합 테스트가 통과한다.
- [ ] transcript result, benchmark manifest, benchmark report의 machine-verifiable schema가 존재한다.
- [ ] README에 설치, 모델 다운로드, 3개 CLI 사용법, 알려진 제한을 문서화한다.
완료의 핵심 산출물은 코드 양이 아니라 다음 세 가지다.
1. 재현 가능한 전사 명령
2. 안정적인 결과 스키마
3. 모델 선택을 뒷받침하는 벤치마크 보고서
## 14. 확장 로드맵
### v0.2: Batch API
- FastAPI
- `POST /v1/transcriptions`
- `GET /v1/transcriptions/{id}`
- API Key 인증
- in-process Job 실행
- 결과 보관과 원본/파생 오디오 삭제
- Docker GPU/CPU 이미지
전사 API는 v0.1의 `TranscriptionService`를 호출하고 `TranscriptResult`를 그대로 응답 모델로 변환한다.
### v0.3: Durable Queue
- Redis + RQ no-fork worker
- `queued -> processing -> completed|failed|cancelled` 상태
- 진행률과 queue position
- 협조적 취소
- 재시작 내성
- 공유 입력/결과 스토어
Job은 `TranscriptResult`를 감싸지만 결과 자체의 스키마는 변경하지 않는다.
### v0.4: Realtime
- WebSocket init frame과 API Key 인증
- PCM16/Opus 입력 협상
- VAD와 rolling buffer
- partial/final 결과
- LocalAgreement 기반 확정 규칙
- 세션 수 제한과 backpressure
- 30분 메모리 평탄성 테스트
실시간의 `final` 세그먼트는 v0.1의 세그먼트 모델을 재사용한다. `partial`은 별도의 변경 가능한 이벤트 타입으로 둔다.
### v0.5: Output and Post-processing
- word timestamps
- SRT/VTT
- glossary와 deterministic rules
- confidence flag
- 선택적 diarization
- 선택적 LLM correction
### v1.0: Internal Production
- 운영 대상 하드웨어 프로파일 확정
- 관측 지표와 경보
- 4시간/2GB 처리 보장
- 보관 및 삭제 정책 검증
- 부하·장기 실행·복구 테스트
- Colab 또는 외부 터널은 실제 필요가 확인된 경우에만 포함
## 15. 고려한 접근
### 접근 A: 전사 코어와 벤치마크 우선 — 채택
- 가장 큰 기술 불확실성을 먼저 제거한다.
- 모델 선택을 추측이 아닌 실제 데이터로 결정한다.
- 결과·엔진·장치 경계를 유지해 이후 API 확장이 가능하다.
- 첫 버전에서 API와 실시간 데모가 없다는 단점이 있다.
### 접근 B: 파일 API와 준실시간을 함께 구현
- 최종 제품과 가까운 데모를 빠르게 볼 수 있다.
- 모델 품질, VAD, 버퍼, WebSocket 문제가 동시에 얽힌다.
- 실패 원인을 분리하기 어렵고 전사 코어 계약이 흔들릴 수 있다.
### 접근 C: 기존 P1~P5 플랫폼 전체 구현
- 처음부터 운영 기능을 모두 고려한다.
- 사용자와 모델 품질이 검증되기 전에 큐·스토리지·동시성 복잡도가 생긴다.
- 구현량이 아니라 검증 순서가 잘못될 가능성이 높다.
## 16. 열린 질문
다음 질문은 문서 작성만으로 정하지 않고 v0.1의 데이터 또는 실제 사용 조건으로 닫는다.
1. 벤치마크 음성은 어떤 실제 업무 환경에서 수집할 것인가?
2. Colab T4 외에 첫 번째 추가 GPU 관측 대상은 GTX 1050, L4, 또는 별도 서버 중 무엇인가?
3. entity 목록의 초기 도메인은 AI/개발 용어만 포함할 것인가, 다른 전문 분야도 포함할 것인가?
4. K-CER 15%라는 절대 기준이 10개 pilot 결과에서도 현실적이고 충분히 엄격한가?
5. v0.2 Batch API에서 처음부터 비동기 Job을 제공할지, 동기 호출로 시작할지?
## 17. 다음 작업
1. 10개 대표 클립으로 소규모 spike dataset을 먼저 만든다.
2. `large-v3-turbo``large-v3`를 동일 조건으로 수동 실행한다.
3. 결과 스키마와 지표 계산이 유효한지 확인한다.
4. 문제가 없으면 30개·60분 기준 데이터셋으로 확장한다.
5. 벤치마크 결과로 기본 모델을 결정한 뒤에만 v0.2 API 설계를 시작한다.
## 18. 이번 결정에서 확인된 사용자 의도
- “일단 A 먼저 가도 되긴한데”라는 선택으로 모델 품질을 먼저 검증하는 순서에 동의했다.
- “어차피 확장해야 하는 거면”이라는 조건 때문에 v0.1도 확장 경계를 가진 실제 코어로 설계했다.
- 따라서 이번 범위는 기능을 많이 넣는 대신, 나중에 API와 실시간 기능이 의존할 계약을 정확히 만드는 데 집중한다.