diff --git a/docs/luke-scribe-v0.1-design.md b/docs/luke-scribe-v0.1-design.md new file mode 100644 index 0000000..9c03912 --- /dev/null +++ b/docs/luke-scribe-v0.1-design.md @@ -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와 실시간 기능이 의존할 계약을 정확히 만드는 데 집중한다.