English | 한국어
한국어 회의를 로컬에서 녹음하고, 검색 가능한 Decision Wiki로 남기는 도구 Local Korean meeting recorder and cited Decision Wiki for Apple Silicon.
Recap은 Apple Silicon Mac에서 회의 녹음 → 전사 → 화자 분리 → AI 교정·요약 → 검색·채팅 → Decision Wiki 정리 흐름을 기본적으로 로컬에서 처리하는 프로젝트입니다. 회의가 끝난 뒤 사라지는 대화를 결정사항, 액션아이템, 원문 timestamp 근거와 함께 다시 찾을 수 있게 만드는 것이 목표입니다.
기본 전사 모델은 로컬 whisper-large-v3-turbo이며 외부 전송은 없습니다. 설정에서 OpenAI를 기본값으로 선택하고 한 번 동의하면 로컬로 되돌리기 전까지 이후 새 전사 음성이 OpenAI로 전송됩니다. 회의별 비교는 실행할 때마다 별도 동의를 요구합니다. 교정·요약·검색·채팅은 계속 로컬에서 실행됩니다.
⚠️ Apple Silicon Mac 전용 — 이 프로젝트는 MLX 프레임워크를 사용하며, Apple Silicon(M1/M2/M3/M4) Mac에서만 동작합니다. Intel Mac, Linux, Windows에서는 MLX 기반 STT가 지원되지 않습니다.
| Decision Wiki 현황 | Wiki 검색 |
|---|---|
![]() |
![]() |
긴 전사문은 남겨도 다시 찾기 어렵습니다. Recap은 원문 전사와 RAG 검색을 유지하면서, 회의에서 나온 결정사항과 액션아이템을 별도의 Markdown Wiki 레이어로 정리하는 흐름을 제공합니다.
- 원문 보존: 전체 전사문과 회의별 RAG 인덱스는 그대로 유지합니다.
- 근거 인용: Wiki에 승격되는 결정사항은
[meeting:{id}@HH:MM:SS]형식의 원문 timestamp 근거를 갖도록 설계했습니다. - 하이브리드 검색: Wiki 검색은 BM25/FTS5 키워드 검색과 e5-small 벡터 검색을 함께 사용해, 표현이 조금 달라도 관련 결정을 찾을 수 있게 합니다.
- 업무 현황 다이제스트: 미해결 액션, 최근 결정, 프로젝트별 현재 상태를 LLM 호출 없이 집계합니다.
Decision Wiki 기능은 설정에서 활성화해 사용하는 로컬 LLM 기반 컴파일러와 검색 인덱스를 사용합니다. 자동 생성 결과는 보수적으로 다루며, 원문 근거와 함께 확인할 수 있는 방향을 우선합니다.
- 음성 → 텍스트 변환: 기본은 mlx-whisper 기반 로컬 한국어 STT, 선택적으로 OpenAI
gpt-4o-transcribe-diarize - 전사 모델 선택기: 웹 UI에서 기본 처리 위치와 로컬 음성 인식 모델을 관리
- 회의별 다른 모델 전사: 기존 회의록을 보존한 채 로컬/OpenAI 결과를 비파괴 A/B 작업으로 생성
- 화자 분리: Senko CoreML 기본, community-1 (pyannote CPU) / speakrs CoreML 선택 가능
- AI 교정: Gemma 4 (기본) 또는 EXAONE 3.5 로컬 LLM으로 전사 오류 교정 (MLX 기본, Ollama 선택 가능)
- Decision Wiki: 회의 결정사항과 액션아이템을 원문 timestamp 근거가 있는 Markdown Wiki로 정리
- 하이브리드 검색: 전사문은 ChromaDB + SQLite FTS5 RAG로, Wiki는 BM25/FTS5 + e5-small 벡터 검색으로 탐색
- AI 채팅: 회의 원문과 Wiki 지식을 기반으로 질의응답
- Zoom 자동 녹음: Zoom 회의 감지 시 ffmpeg로 자동 녹음 시작/종료
- BlackHole 지원: 시스템 오디오 캡처 (BlackHole 설치 시 자동 전환, 미설치 시 마이크 사용)
- macOS 메뉴바 앱: rumps 기반 메뉴바 상주, 녹음 상태 실시간 표시
- 웹 UI: macOS 네이티브 스타일 SPA (회의 목록 + 뷰어 + 검색 + Wiki + AI 채팅 + 준비 상태 + 설정)
- 녹취별 폴더 열기: 개별 녹취 페이지의 녹음 폴더 열기로 Finder에서 원본 파일을 선택
- 제목 자동 정리: 개별·일괄 녹취의 내용을 로컬 LLM으로 요약해 날짜를 포함한 제목을 백그라운드에서 적용. 화면을 이동해도 계속 처리하며 작업 내역에서 중단·재시도·이전 제목 복원 가능
- 설정 UI: 웹에서 STT 모델/LLM 모델/Temperature/전사 언어 등 실시간 변경
- Zoom 감지: Zoom 회의 시작/종료 자동 감지 (CptHost 프로세스 모니터링)
- 폴더 감시: 지정 폴더에 파일 추가 시 자동 처리
- 서멀 관리: 팬리스 MacBook Air 대응, 2-job + 쿨다운 패턴
AI 제목은 2026-09-15 · 상담 자동화 도입 일정 확정 형태로 저장합니다. 보존된
녹취 날짜를 우선하고, 없으면 회의 ID의 날짜 또는 확인된 원본 파일 수정 날짜를
사용합니다. 파일 수정 날짜를 사용하면 작업 내역에 확인 안내를 표시하며, 날짜를
확인할 수 없으면 해당 항목을 실패로 표시합니다. 긴 녹취는 시작·중간·끝을 발췌합니다.
전사 작업 중인 녹취는 제외하며, 접수 후 직접 수정한 제목은 덮어쓰지 않습니다.
제목 정리 API는 POST /api/meetings/titles, 원본 위치 열기는
POST /api/meetings/{id}/open-audio-folder입니다. 둘 다 로컬 앱 요청만 허용합니다.
제목 작업과 전사 작업의 진행은 작업 내역에서 확인합니다. 앱 자체를 종료하면
미완료 제목 작업은 중단으로 표시되며 작업 내역에서 다시 실행할 수 있습니다.
기존 title-suggestion은 호환용 미리보기 API로 유지합니다.
llm.title_input_chars, llm.title_max_tokens, llm.title_max_chars로 제목 생성의
입력·출력 상한을 설정할 수 있습니다.
| 항목 | 최소 사양 |
|---|---|
| OS | macOS 14 (Sonoma) 이상 |
| 칩 | Apple Silicon (M1, M2, M3, M4) — Intel Mac 미지원 |
| RAM | 16GB 이상 |
| 디스크 | 20GB 이상 여유 공간 |
| Python | 3.11 또는 3.12 (3.13 이상 미지원) |
| 기타 | ffmpeg |
⚠️ Python 버전 주의: Python 3.13 이상에서는 ChromaDB의 Rust 네이티브 바인딩이 호환되지 않아 크래시가 발생할 수 있습니다. 반드시 Python 3.11 또는 3.12를 사용하세요.
참고: LLM 백엔드로 Ollama 또는 MLX를 선택할 수 있습니다. Ollama 선택 시 별도 Ollama 앱 설치가 필요하고, MLX 선택 시 추가 설치 없이 동작합니다.
# 칩 종류 확인
sysctl -n machdep.cpu.brand_string
# RAM 확인
echo "$(( $(sysctl -n hw.memsize) / 1073741824 ))GB"| 내 Mac | 권장 설정 | 이유 |
|---|---|---|
| M4 + 16GB | MLX + Gemma 4 E4B (기본) | 최적 성능, 멀티모달, Thinking 모드 |
| M3/M4 + 16GB 이상 | MLX + Gemma 4 E4B (기본) | 통합 메모리 네이티브, Ollama 불필요 |
| M1/M2 + 16GB | MLX + Gemma 4 E4B (기본) | 검증된 성능. 한국어 고유명사 정확도 우선 시 EXAONE 으로 전환 |
| M1/M2 + 8GB | MLX + Gemma 4 E2B 또는 Ollama | E2B는 ~3GB로 메모리 절약 |
가장 쉬운 방법: AI 코딩 에이전트가 자동으로 환경을 구성합니다.
git clone https://github.com/notadev-iamaura/meeting-transcriber.git
cd meeting-transcriberClaude Code 사용 시:
claude
# 프롬프트에 "이 프로젝트 셋업해줘" 입력Cursor 사용 시:
- 프로젝트 폴더 열기 → Composer에 "이 프로젝트 셋업해줘" 입력
AI 에이전트가 CLAUDE.md를 읽고 가상환경 생성, 의존성 설치, Ollama 모델 다운로드까지 자동 처리합니다.
HuggingFace 토큰 설정 등 수동 단계는 에이전트가 안내해줍니다.
git clone https://github.com/notadev-iamaura/meeting-transcriber.git
cd meeting-transcriberpython3 -m venv .venv
source .venv/bin/activatepip install -e ".[dev]"bash scripts/install.sh이 스크립트가 자동으로 처리하는 항목:
- Homebrew 확인
- Python 3.11+ 확인
- ffmpeg 설치
- Ollama 확인 (Ollama 백엔드 사용 시)
- EXAONE 3.5 모델 다운로드 (Ollama 백엔드 사용 시)
- 데이터 디렉토리 생성 + 보안 설정
MLX 기본 환경에서는 LLM 모델이 첫 실행 시 HuggingFace 에서 자동 다운로드 되므로
install.sh의 7단계(EXAONE pull) 는 건너뛰어도 됩니다. Ollama 백엔드를 명시적으로 선택한 경우에만 필요합니다.
본인 마이크 + 시스템 오디오(상대방 목소리) 를 하나의 WAV 로 녹음하려면 macOS Aggregate Device 가 필요합니다. 자동 셋업 스크립트:
bash scripts/setup_audio.sh자세한 내용은 BlackHole / Aggregate Device 섹션 을 참조하세요. 단순 자기 녹음만 필요하면 이 단계는 건너뛰어도 됩니다.
기본 설정(MLX + Gemma 4 E4B)은 변경 없이 바로 사용 가능합니다. 최초 실행 시 HuggingFace에서 모델이 자동 다운로드됩니다 (~6GB).
| 모델 | config.yaml 설정 |
크기 | 특징 |
|---|---|---|---|
| Gemma 4 E4B (기본) | mlx-community/gemma-4-e4b-it-4bit |
~6GB | Google, 다국어 140+, Thinking 모드, 벤치마크 기반 기본값 |
| EXAONE 3.5 | mlx-community/EXAONE-3.5-7.8B-Instruct-4bit |
~5GB | LG, 한국어 특화. 한국어 고유명사 정확도 우선 시 권장 |
| Gemma 4 E2B | mlx-community/gemma-4-e2b-it-4bit |
~3GB | 경량, 8GB RAM 가능 |
모델 변경은 config.yaml에서 한 줄만 바꾸면 됩니다:
llm:
mlx_model_name: "mlx-community/EXAONE-3.5-7.8B-Instruct-4bit" # ← 원하는 모델로 변경또는 웹 UI 설정 페이지(http://127.0.0.1:8765/app/settings)에서 드롭다운으로 변경할 수 있습니다.
Ollama 백엔드를 사용하려면 ollama.com에서 앱을 설치한 후:
ollama pull exaone3.5:7.8b-instruct-q4_K_M
config.yaml에서llm.backend: "ollama"로 변경하세요.
community-1 선택 시 사용하는 pyannote 모델은 HuggingFace에서 **게이트 모델(gated model)**로 배포됩니다.
모델은 로컬에서 실행되지만, 최초 다운로드·약관 동의 시 인증이 필요합니다.
이후 로컬 캐시(가중치 포함)가 완전하면 토큰 없이 오프라인 실행이 가능합니다.
설정 절차:
- HuggingFace에 무료 가입
- 아래 두 모델 페이지를 방문하여 각각 "Agree and access repository" 클릭:
- 토큰 발급 페이지에서 Access Token 생성 (Read 권한)
- HuggingFace CLI에 로그인하여 사용자 전용 캐시에 저장:
hf auth login
chmod 600 ~/.cache/huggingface/token참고:
.zshrc의export HUGGINGFACE_TOKEN=...은 현재 터미널 실행에는 사용할 수 있지만 macOS LaunchAgent에는 전달되지 않습니다. 로그인 자동 시작과 새벽 자동 처리를 사용하려면 위 CLI 캐시가 필요합니다. 토큰 값은 plist, YAML, 로그에 저장하지 않습니다. 토큰 설정 후 최초 실행 시 모델이 자동 다운로드되며 (~/.cache/huggingface/에 캐시), 기본 로컬 전사 모드는 이후 인터넷 없이 동작합니다. 선택적 OpenAI 전사를 실행할 때는 해당 음성 업로드를 위한 인터넷 연결이 필요합니다.
# 메뉴바 + 웹 서버 실행 (기본)
python main.py
# 헤드리스 모드 (서버만)
python main.py --no-menubar
# 포트 변경
python main.py --port 9000
# 디버그 로깅
python main.py --log-level debug
# 콜드 스타트 측정 (임시 데이터 디렉토리/포트 사용, 3초 초과 시 실패)
python scripts/measure_startup.py --python .venv/bin/python --max-seconds 3
# 최초 설정 마법사용 로컬 준비 상태 확인
curl -s http://127.0.0.1:8765/api/setup/readiness | jq
# 같은 정보를 웹 UI에서 확인
open http://127.0.0.1:8765/app/setup
# 경량 .app 런처용 read-only 실행 계약 확인 (서버 시작 전)
.venv/bin/python -m ui.launcher --project-dir "$PWD"
# unsigned local .app 번들 생성 (실행하지 않고 dist/ 아래 산출물만 생성)
.venv/bin/python scripts/build_launcher_app.py --output-dir dist --project-dir "$PWD" --force
# 런타임 소스 스냅샷을 .app 안에 포함해 생성 (기본은 off)
.venv/bin/python scripts/build_launcher_app.py --output-dir dist --project-dir "$PWD" --bundle-source --force
# 생성된 .app 구조/서명 readiness read-only 검증 (앱 실행/서명/공증 없음)
.venv/bin/python scripts/validate_launcher_app.py "dist/Recap.app" --json
# unsigned local DMG 생성 (local_ready .app만 허용, 서명/공증/앱 실행 없음)
.venv/bin/python scripts/build_launcher_dmg.py --app-path "dist/Recap.app" --output-dir dist --force --json
# unsigned local release manifest 생성 (hash/size/readiness 기록, mount/서명/공증 없음)
.venv/bin/python scripts/build_release_manifest.py --app-path "dist/Recap.app" --dmg-path "dist/Recap.dmg" --json
# unsigned local release 산출물 일괄 생성 (.app + .dmg + manifest, 서명/공증 없음)
.venv/bin/python scripts/build_unsigned_release.py --output-dir dist --project-dir "$PWD" --force --json/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"brew install python@3.11brew install ffmpegollama.com에서 macOS 앱을 다운로드하여 설치합니다.
# EXAONE 3.5 모델 다운로드 (약 5GB)
ollama pull exaone3.5:7.8b-instruct-q4_K_Mbash scripts/install.sh --check회사·학교·일부 국가 네트워크에서 HuggingFace 자동 다운로드가 SSL 인증서 오류, 방화벽 차단, 또는 게이트웨이 검사 때문에 실패할 수 있습니다. 앱 안전성 보호를 위해 SSL 검증 우회(verify=False, --trusted-host, PYTHONHTTPSVERIFY=0 등) 는 절대 사용하지 마세요. 대신 아래 절차로 브라우저를 통해 직접 받으면 됩니다.
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
huggingface_hub.utils._errors.LocalEntryNotFoundError: ...
ConnectionError: HTTPSConnectionPool(host='huggingface.co', port=443) ...
앱 안에 수동 다운로드 도우미가 내장되어 있습니다.
GUI 방법 (권장):
http://127.0.0.1:8765/app/settings→ "음성 인식 모델 (STT)" 섹션- 받고 싶은 모델 카드의 "▸ 브라우저로 직접 받기" 펼침
- 표시된 HuggingFace 직접 URL (
config.json,weights.safetensors등) 을 일반 브라우저로 열어 다운로드 - 같은 폴더(예:
~/Downloads/whisper-turbo)에 저장한 후, 카드의 "가져오기" 버튼 클릭 → 폴더 경로 입력 - 자동 검증 후
~/.meeting-transcriber/stt_models/{id}-manual/에 배치되며 활성화 가능 상태로 전환
CLI 방법 (자동화 스크립트용):
# 1) 수동 다운로드 정보 (URL + 타깃 폴더) 조회
curl -s http://127.0.0.1:8765/api/stt-models/seastar-medium-4bit/manual-download-info | jq
# 2) 사용자가 브라우저로 받은 폴더 경로를 임포트
curl -X POST http://127.0.0.1:8765/api/stt-models/seastar-medium-4bit/import-manual \
-H "Content-Type: application/json" \
-d '{"source_dir": "/Users/me/Downloads/seastar"}'
# 3) 활성화
curl -X POST http://127.0.0.1:8765/api/stt-models/seastar-medium-4bit/activate
⚠️ ~/.meeting-transcriber/stt_models/아래에 직접 파일을 복사하지 마세요. 반드시import-manualAPI 또는 GUI 의 "가져오기" 를 통해 배치해야 앱이 올바른 위치({id}-manual/) 와 상태를 관리합니다.
MLX LLM 은 ~/.cache/huggingface/hub/ 에 캐시됩니다. SSL 이슈가 있다면 동일한 위치에 직접 받아 두면 첫 실행 시 자동 인식됩니다.
브라우저 다운로드 절차:
- HuggingFace 모델 페이지 방문 후 "Files and versions" 탭
- 각 파일을 클릭하여 우측 "download" 버튼으로 받음 (
config.json,tokenizer.json,tokenizer_config.json,model.safetensors또는model-00001-of-XXX.safetensors일체,model.safetensors.index.json등 모든 파일) - 아래 경로에 동일한 구조로 배치:
# 예: Gemma 4 E4B (기본)
mkdir -p ~/.cache/huggingface/hub/models--mlx-community--gemma-4-e4b-it-4bit/snapshots/main
mv ~/Downloads/gemma-4-e4b/* ~/.cache/huggingface/hub/models--mlx-community--gemma-4-e4b-it-4bit/snapshots/main/
# refs 디렉토리 생성 (HuggingFace 캐시 규약)
mkdir -p ~/.cache/huggingface/hub/models--mlx-community--gemma-4-e4b-it-4bit/refs
echo "main" > ~/.cache/huggingface/hub/models--mlx-community--gemma-4-e4b-it-4bit/refs/mainpython main.py실행 → 첫 추론 시 캐시에서 로드 (인터넷 연결 시도 없음)
더 간단한 대안 — Ollama 백엔드로 전환:
Ollama 는 자체 다운로드 채널을 사용하므로 HuggingFace SSL 이슈를 우회할 수 있습니다.
# 1) ollama.com 에서 macOS 앱 설치 (브라우저 다운로드)
ollama pull exaone3.5:7.8b-instruct-q4_K_M
# 2) config.yaml 변경
# llm:
# backend: "ollama"설정 → 일반 → 화자분리 엔진에서 senko (기본), community-1, speakrs를
선택합니다. API는 GET/PUT /api/settings의 diarization_engine 필드입니다.
diarization.engine이 없는 기존 설정도 senko를 사용합니다. 기존 model_name으로
community-1을 추론하지 않습니다. 명시된 engine과 model_name 값은 보존하며 설정 파일을
자동 덮어쓰지 않습니다. 기존 pyannote 동작을 유지하려면 engine: "community-1"을 저장하세요.
변경은 이후 화자분리 실행에 적용되며 이미 저장된 diarize.json은 재사용됩니다.
OpenAI 단일 업로드의 화자 구간 재사용 경로는 그대로 유지됩니다.
Senko 설치 (macOS 14+ / Apple Silicon, Xcode Command Line Tools 필요):
source .venv/bin/activate
python -m pip install "git+https://github.com/narcotic-sh/senko.git@1bcb09041bfa170b4a5a4b0af92f66eff76204d5"
python -m pip checkSenko upstream의 Python API
senko.Diarizer(device="coreml", warmup=False, quiet=True).diarize(...)를 앱의 Python
worker에서 호출합니다. CoreML 패키지/모델은 upstream 설치·최초 실행 과정에서 준비됩니다.
전사 병합에는 짧은 발화를 보존하는 raw_segments를 사용합니다. upstream 패키지는
앱 기본 의존성에 포함되지 않아 위 설치가 필요합니다.
speakrs 설치 (Apple Silicon Mac, Rust/Cargo 1.88+ 및 C toolchain 필요):
brew install openblas pkg-config
PKG_CONFIG_PATH="$(brew --prefix openblas)/lib/pkgconfig" \
cargo install --path tools/speakrs-sidecar --locked
# config.yaml의 diarization.speakrs_binary에 실제 설치 경로 지정:
# 예: /Users/<계정>/.cargo/bin/recap-speakrs저장소의 recap-speakrs sidecar는 speakrs 0.5.0
OwnedDiarizationPipeline을 ExecutionMode::CoreMl로 실행합니다. 최초 실행 시 upstream
모델 번들을 다운로드하며 SPEAKRS_MODELS_DIR로 로컬 번들을 지정할 수 있습니다.
LaunchAgent에서는 셸 PATH와 다를 수 있으므로 바이너리 절대 경로를 권장합니다.
Python worker를 exec로 교체하므로 같은 PID에 모델 잠금·Zoom pause·취소·타임아웃이 적용됩니다.
두 CoreML 엔진은 플랫폼/의존성이 없으면 명확한 오류로 중단하며 자동 fallback은 없습니다.
설정에서 community-1을 직접 선택할 수 있습니다. community-1은 CPU 강제이며 기존 HF 토큰
검사 및 완전한 로컬 캐시의 토큰 생략(#76)을 유지합니다. HF 캐시 검사는 이 엔진에만 적용합니다.
model_name, device, min_speakers, max_speakers, output_mode는 community-1 전용이며,
Senko/speakrs는 자체 화자 수 추정을 사용합니다. 다운로드 실패 시 SSL·게이트를 우회하지 않습니다.
준비 상태 검사는 패키지/바이너리 존재까지 확인하며 모델 다운로드·실제 추론 성공을 보증하지 않습니다.
ETA는 엔진별 통계를 분리하고 초기 RTF 힌트(Senko 0.01, speakrs 0.05, community-1 0.25)를 사용합니다. 실제 관측값으로 갱신하며 최초 모델 다운로드 시간은 포함하지 않습니다. 2026-09-29 M4 의사 GT A/B에서 Senko가 short/long DER·JER·속도 1위였으며 제품 결정으로 기본값을 변경했습니다. 사람 RTTM 검증은 대기 중입니다. Linux 테스트는 CoreML을 모킹합니다.
pyannote 모델은 HuggingFace 게이트 모델이라 최초 다운로드·약관 동의에는 토큰이 필요합니다. 사용자가 게이트 페이지에서 직접 받아 둔 로컬 HF 캐시(가중치 포함)가 완전하면, 앱은 토큰 없이 오프라인 worker로 화자분리를 실행할 수 있습니다. 캐시를 삭제하거나 모델을 바꾸면 다시 토큰이 필요합니다. 에이전트가 대신 동의하거나 공개 미러·비공식 재배포본으로 우회하면 안 됩니다.
- https://huggingface.co/pyannote/speaker-diarization-community-1 방문 → "Agree and access repository" 클릭
- https://huggingface.co/pyannote/segmentation-3.0 방문 → 동일하게 동의
- https://huggingface.co/settings/tokens 에서 Read 권한 토큰 발급
- 사용자 전용 CLI 캐시에 저장:
hf auth login
chmod 600 ~/.cache/huggingface/token- SSL 인증서 자체가 깨진 환경이라면 위 1~3 은 일반 브라우저에서 진행하되, 모델 파일은 동일한 게이트 페이지 → "Files and versions" → 각 파일 다운로드 →
~/.cache/huggingface/hub/models--pyannote--speaker-diarization-community-1/에 위와 동일한 캐시 구조로 배치
pip install 의 SSL 검증을 우회하지 마세요. 다음 순서로 해결:
- 회사 네트워크: IT 팀에
pypi.org,files.pythonhosted.org,huggingface.co화이트리스트 요청 - 개인 네트워크: 모바일 핫스팟 / 다른 네트워크에서 시도
- 대체 패키지 매니저:
uv사용
해결되지 않으면 진행을 중단하고 사용자가 환경 문제를 먼저 해결한 뒤 셋업을 재개해야 합니다.
더 자세한 운영 원칙(에이전트가 절대 시도하지 말아야 할 우회 행동 9가지) 은
CLAUDE.md의 "AI 에이전트용: 네트워크·다운로드 장애 처리 원칙" 섹션을 참조하세요.
# 메뉴바 + 웹 서버 (기본)
python main.py
# 헤드리스 모드 (서버만, SSH/서비스용)
python main.py --no-menubar실행 후 http://127.0.0.1:8765/app 으로 접속합니다.
3-Column macOS 네이티브 스타일 인터페이스:
┌──────────┬────────────────┬──────────────────────────────┐
│ Nav Bar │ 회의 목록 │ 콘텐츠 영역 │
│ │ │ │
│ 📋 회의록 │ 2026-03-10 ● │ 회의 제목 / 전사문 / 요약 │
│ 🔍 검색 │ 2026-03-09 ● │ 또는 검색 결과 / AI 채팅 │
│ 💬 채팅 │ ... │ │
│ 준비 │ │ │
│ ⚙ 설정 │ │ │
│ │ │ ☀/🌙 │
│ 상태표시 │ │ │
└──────────┴────────────────┴──────────────────────────────┘
회의 목록: 좌측 패널에 날짜별 회의 목록. 상태 도트로 완료(초록)/처리중(파랑)/실패(빨강) 표시.
전사문 뷰어: 회의 선택 시 참석자별 번호 배지 + 타임스탬프로 발화 표시. 전사문 내 검색 지원.
회의록 (AI 요약): 탭 전환으로 AI가 생성한 회의록을 확인하고 직접 편집할 수 있습니다.
기존 산출물을 지우는 강제 재생성은 파일 경쟁 시 데이터 손실을 막기 위해 산출물 존재 여부와
무관하게 서버가 409 SECURITY_BLOCKED로 거부합니다.
검색: 전체 회의 내용에서 키워드 검색. 날짜/화자 필터. 결과 클릭 시 해당 발화로 이동.
AI 채팅: 회의 내용 기반 질의응답. "지난 회의에서 결정된 일정이 뭐야?" 같은 질문 가능.
준비 상태: /app/setup에서 데이터 디렉토리, ffmpeg, HuggingFace 토큰, 오디오 장치, STT 모델 상태를 읽기 전용으로 확인. 설치나 권한 변경은 실행하지 않음.
설정: 로컬/OpenAI 기본 전사 선택, OpenAI API 키 등록, 로컬 STT 모델 선택, LLM 모델 변경, Temperature 조절, LLM 스킵 토글, 전사 언어 변경 — 모두 웹에서 적용.
다크/라이트 모드: 우측 상단 토글로 전환. 시스템 설정 자동 감지 + 수동 오버라이드 가능.
기본 STT 모델은 whisper-large-v3-turbo 입니다 (6 회의 벤치마크 1위, komixv2 대비 CER −16%p).
설정 페이지의 "음성 인식 모델 (STT)" 섹션에서 한국어 fine-tune 모델 3종도 GUI로 다운로드/활성화할 수 있습니다.
설정의 기본 전사 모델에서 이 Mac에서 처리 또는 OpenAI 서버에서 처리를 선택할 수 있습니다. OpenAI를 선택하려면 같은 화면에서 API 키를 macOS Keychain에 등록하고 외부 업로드에 한 번 명시적으로 동의해야 합니다. 이 선택을 유지하는 동안 이후 새 전사는 OpenAI로 처리됩니다. 새 설치의 초기값은 로컬이며 자동 cloud fallback은 없습니다.
gpt-4o-transcribe-diarize는 API의 language 힌트를 지원하지 않아 언어를 자동 감지하며, 설정의 전사 언어 값은 로컬 STT 경로에 적용됩니다.
아직 전사를 시작하지 않은 녹음 완료 회의에서는 전사 시작을 눌러 이번 회의에만 사용할 모델을 고를 수 있습니다. 전역 기본값이 로컬이어도 한 회의만 OpenAI로 보낼 수 있고, 반대로 전역 기본값이 OpenAI여도 한 회의만 로컬로 처리할 수 있습니다. 이 선택은 작업 큐에 해당 회의의 snapshot으로만 저장되어 설정의 기본값을 바꾸지 않습니다. OpenAI를 고르면 그 파일에 대해 외부 전송 동의를 매번 다시 받습니다.
전사가 완료된 회의의 다른 모델로 텍스트 변환하기… 버튼은 현재 회의록을 지우지 않습니다. 현재 로컬 모델과 OpenAI 모델의 결과를 별도 A/B 작업으로 저장해 비교하며, 실행할 때마다 해당 파일의 외부 전송 동의를 다시 받습니다. 녹음 완료·변환 전 실패 파일은 먼저 해당 회의의 첫 전사를 완료해야 합니다.
| 모델 | 베이스 | Zeroth CER | 회의 음성 | RAM | 디스크 | HuggingFace |
|---|---|---|---|---|---|---|
| whisper-large-v3-turbo ⭐ (기본) | Large-v3 Turbo | — | 회의 벤치 1위 | ~2GB | ~1.6GB | mlx-community/whisper-large-v3-turbo |
| komixv2 | Medium fp16 | 11.88% | 환각 최소, 가독성 양호 | 1.88GB | 1.5GB | youngouk/whisper-medium-komixv2-mlx |
| seastar (4bit) | Medium + Zeroth | 1.25% | 무음 환각 위험 | 1.26GB | 420MB | youngouk/seastar-medium-ko-4bit-mlx |
| ghost613 (4bit) | Large-v3-turbo + Zeroth | 1.60% | 대량 환각 (실사용 부적합) | 1.31GB | 442MB | youngouk/ghost613-turbo-korean-4bit-mlx |
벤치마크 출처:
- Zeroth CER/WER: Zeroth Korean test set 30 샘플 (깨끗한 읽기 음성)
- 회의 음성 평가: 6 회의 A/B 테스트 (
docs/BENCHMARK.md §1) — 잡음·에코·원거리 마이크 포함Zeroth 점수만으로 4bit 모델을 채택하면 실제 회의에서 무음 구간 환각("ohn ohn", "네 네 네")이 빈번해 가독성이 떨어집니다. 따라서 회의 환경 안정성이 입증된
whisper-large-v3-turbo가 기본값입니다. 모든 모델은 사전 양자화된 형태로 HuggingFace 에 배포되어 다운로드 1회로 끝납니다.
사용법:
- 설정 페이지 (
/app/settings) → "음성 인식 모델 (STT)" 섹션으로 스크롤 - 원하는 모델의
[다운로드]버튼 클릭 (HuggingFace 에서 사전 양자화된 모델을 직접 다운로드) - 다운로드 완료 후
[활성화]클릭 → config.yaml 자동 갱신 - 다음 전사부터 새 모델 적용 (재시작 불필요)
자동 다운로드가 SSL/방화벽 등 네트워크 이슈로 실패하면 수동 다운로드 가이드 섹션을 참고하세요. 카드의 "▸ 브라우저로 직접 받기" 섹션을 열어 URL을 복사해 브라우저로 받은 뒤 "가져오기" 버튼으로 임포트할 수 있습니다.
# 또는 config.yaml 에서 직접 변경 (HuggingFace repo ID 사용)
stt:
provider: "local" # 기본. "openai"는 명시적 외부 전송
model_name: "mlx-community/whisper-large-v3-turbo" # 기본값
openai_model: "gpt-4o-transcribe-diarize" # 화자/시간 세그먼트 지원
# model_name: "youngouk/seastar-medium-ko-4bit-mlx" # 다른 모델로 변경 시
# 수동으로 가져온 경우에는 로컬 경로 사용 (예: ~/.meeting-transcriber/stt_models/seastar-medium-4bit-manual)| 단계 | 설명 | 소요 시간 (1시간 회의) |
|---|---|---|
| 변환 | ffmpeg → 16kHz mono WAV | ~3초 |
| 전사 | mlx-whisper (GPU) | ~3분 |
| 화자분리 | Senko (CoreML 기본), community-1 / speakrs 선택 | 엔진·길이·최초 로드에 따라 다름 |
| 병합 | 전사+화자 매칭 | ~1초 |
| LLM 보정 | EXAONE/Gemma 4 | ~2분 |
| 요약 | AI 회의록 생성 | ~30초 |
총 ~11분 (1시간 회의 기준, M4 16GB). LLM 스킵 시 ~8분.
Zoom 회의를 감지하면 자동으로 녹음을 시작하고, 회의 종료 시 전사 파이프라인까지 자동 실행합니다.
Zoom 회의 시작 감지 → ffmpeg 녹음 시작 (recordings_temp/)
→ 메뉴바 🔴 녹음 표시
→ WebSocket "recording_started" 이벤트
Zoom 회의 종료 감지 → ffmpeg 녹음 정지 (stdin 'q' → graceful 종료)
→ 녹음 파일을 audio_input/으로 이동
→ FolderWatcher 감지 → 전사 파이프라인 자동 시작
오디오 캡처 방식 — 3가지 옵션:
| 설정 | 녹음 내용 | 용도 |
|---|---|---|
| 마이크만 (기본) | 본인 목소리 + 공기 중 상대방 소리 (에코 위험) | 단순 자기 녹음 |
| BlackHole 2ch | 시스템 오디오 출력만 (Zoom 상대방 목소리) | 본인 마이크 입력은 안 들어감 |
| Aggregate Device ⭐ 권장 | 본인 마이크 + 시스템 오디오 동시 | Zoom·Teams 양방향 회의 녹음 |
본인 + 상대방을 모두 한 WAV 파일로 녹음하려면 macOS Aggregate Device 를 만들어야 합니다. 자동 셋업 스크립트를 제공합니다:
# 1) 상태 점검 (BlackHole 설치 여부 + Aggregate 존재 여부)
bash scripts/setup_audio.sh --check
# 2) 미구성이면 자동 셋업 (BlackHole 설치 안내 + Aggregate Device 자동 생성)
bash scripts/setup_audio.sh스크립트가 자동으로 처리하는 항목:
- BlackHole 2ch 설치 여부 검사 → 없으면
brew install blackhole-2ch안내 후 종료 (사용자 직접 실행 필요) Meeting Transcriber Aggregate장치 존재 여부 확인 → 있으면 skip- CoreAudio API (
AudioHardwareCreateAggregateDevice) 로기본 입력 장치 + BlackHole 2ch를 묶은 Aggregate 생성 (Swift 스크립트 실행) ffmpeg -list_devices로 최종 등록 검증
Zoom 등 화상 앱 설정 (사용자 직접):
- 스피커:
BlackHole 2ch선택 → 상대방 목소리가 BlackHole 로 흐름 - 마이크: 평소 쓰던 마이크 (예:
MacBook Air Microphone) 그대로 유지 ⚠️ Zoom 마이크를Meeting Transcriber Aggregate로 잡으면 하울링 발생합니다- 본인이 상대방 목소리를 듣지 못하는 문제가 있다면 Multi-Output Device 로 BlackHole + 이어폰 동시 출력 구성 권장
본인 목소리 없이 시스템 오디오만 녹음하면 충분한 경우:
brew install blackhole-2ch자세한 절차 (Audio MIDI 설정, 채널별 볼륨 검증, 트러블슈팅) 는
docs/AGGREGATE_DEVICE_SETUP.md를 참조하세요.
수동 녹음 제어 (API):
# 녹음 시작
curl -X POST http://127.0.0.1:8765/api/recording/start
# 녹음 상태 확인
curl http://127.0.0.1:8765/api/recording/status
# 녹음 정지
curl -X POST http://127.0.0.1:8765/api/recording/stop
# 오디오 장치 목록
curl http://127.0.0.1:8765/api/recording/devices~/.meeting-transcriber/audio_input/에 오디오 파일을 넣으면 안전 검사를 거쳐
recorded 회의로 등록됩니다. auto_processing.enabled: false이면 UI의 전사 시작을
눌러야 하며, 이는 파일이 DB에 등록되지 않은 상태와 구분됩니다.
자동 처리는 명시적으로 활성화한 뒤 앱이 실행 중일 때 동작합니다. 앱 시작과
startup 감사가 예약 시각을 넘겨 끝나면 당일 누락분을 한 번 바로 처리하는 것이
기본입니다. 설정 화면에서 최근 시간 범위와 1회 처리 상한을 조정할 수 있으며,
기본 0은 누락분 전체를 순차 큐에 등록한다는 뜻입니다. 전사 항목의 실행 결과는 실제
완료가 아니라 **큐 등록(대기 중)**으로 표시되고, 요약처럼 동기 완료된 항목만 완료로 표시됩니다.
같은 회의의 전체 전사, 지연 요약, 검색 재색인, 재전사, 삭제, 회의록·전사문 편집은 한 번에 하나씩
실행됩니다. 지연 요약과 수동 편집은 DB 상태가 completed인 회의에서만 시작되므로,
처리 중이거나 재전사 대기 중인 회의는 완료 후 다시 시도해야 합니다.
# 1. 모델 목록 + 상태 조회
curl http://127.0.0.1:8765/api/stt-models | python -m json.tool
# 2. 모델 다운로드 시작 (백그라운드, 사전 양자화된 HF repo 에서 snapshot_download)
curl -X POST http://127.0.0.1:8765/api/stt-models/seastar-medium-4bit/download
# 2-b. 자동 다운로드가 SSL/방화벽으로 실패할 때 — HTTP 직접 GET 폴백
curl -X POST http://127.0.0.1:8765/api/stt-models/seastar-medium-4bit/download-direct
# 3. 다운로드 진행률 확인 (3초 간격 폴링 권장)
curl http://127.0.0.1:8765/api/stt-models/seastar-medium-4bit/download-status
# 4. 활성 모델 변경 (config.yaml 자동 갱신)
curl -X POST http://127.0.0.1:8765/api/stt-models/seastar-medium-4bit/activate
# 로컬/OpenAI 통합 전사 카탈로그와 API 키 등록 상태(키 값은 반환하지 않음)
curl http://127.0.0.1:8765/api/transcription-models | python -m json.tool
# 한 회의만 OpenAI로 전사 (전역 기본 설정은 변경하지 않음)
curl -X POST http://127.0.0.1:8765/api/meetings/{meeting_id}/transcribe \
-H "Content-Type: application/json" \
-d '{"model_id":"openai:gpt-4o-transcribe-diarize","external_upload_confirmed":true}'
# audio_input에는 있지만 DB/UI에 없는 파일만 비파괴 복구
# auto_processing.enabled=false인 로컬 앱에서만 실행 가능
curl -X POST 'http://127.0.0.1:8765/api/system/audio-input/recover?recent_days=7' \
| python -m json.tool
# 위 응답의 recent_registered_meeting_ids에만 명시적으로 로컬 전사 요청
curl -X POST http://127.0.0.1:8765/api/meetings/{meeting_id}/transcribe \
-H "Content-Type: application/json" \
-d '{"model_id":"local","external_upload_confirmed":false}'복구 API는 audio_input 직접 자식 중 DB 미등록 파일만 검사하며 기존 row, 원본,
quarantine을 변경하지 않습니다. 최근 7일 목록은 DB 등록 시각이 아니라 검증된 원본
mtime 기준입니다. 진행/완료 집계는 GET /api/status의 audio_input_scan에서 확인합니다.
OpenAI API 키는 CLI 인자나 config.yaml에 넣지 말고 설정 화면에서 등록하는 것을 권장합니다. 앱은 키를 macOS Keychain에 저장하고 등록 여부만 API에 반환합니다.
웹 UI에서 과거 회의 내용을 검색하거나, AI 채팅으로 질의할 수 있습니다.
bash scripts/setup_launchagent.shconfig.yaml 파일에서 모든 설정을 관리합니다. 주요 항목:
| 설정 | 설명 | 기본값 |
|---|---|---|
paths.base_dir |
데이터 디렉토리 | ~/.meeting-transcriber |
stt.provider |
기본 전사 처리 위치 (local 또는 명시적 openai) |
local |
stt.model_name |
로컬 Whisper 모델 (HuggingFace ID 또는 로컬 경로) | mlx-community/whisper-large-v3-turbo |
stt.openai_model |
외부 전사 선택 시 사용하는 화자분리 모델 | gpt-4o-transcribe-diarize |
diarization.engine |
화자분리 엔진: senko / community-1 / speakrs |
senko |
diarization.speakrs_binary |
speakrs sidecar 실행 파일 (PATH 또는 절대 경로) | recap-speakrs |
diarization.timeout_seconds |
화자분리 실제 실행 타임아웃 하한 | 1800초 |
diarization.dynamic_timeout_enabled |
긴 오디오의 화자분리 실행 예산을 길이에 비례해 확대 | true |
diarization.dynamic_timeout_multiplier |
화자분리 동적 타임아웃 길이 배수 | 1.25 |
diarization.dynamic_timeout_max_seconds |
화자분리 동적 연장분의 타임아웃 상한 | 10800초 |
llm.backend |
LLM 백엔드 | "mlx" (기본) 또는 "ollama" |
llm.mlx_model_name |
MLX 모델명 | mlx-community/gemma-4-e4b-it-4bit |
llm.mlx_max_tokens |
MLX 최대 생성 토큰 | 2000 |
pipeline.skip_llm_steps |
LLM 보정과 의존 요약/검색 산출물 생성을 보류 | false (기본: 전체 8단계 실행) |
server.port |
웹 서버 포트 | 8765 |
server.startup_timeout_seconds |
메뉴바 실행 전 FastAPI HTTP readiness 최대 대기 | 10.0초 |
thermal.batch_size |
연속 처리 건수 | 2 |
thermal.cooldown_seconds |
쿨다운 시간 | 180 (3분) |
recording.enabled |
녹음 기능 활성화 | true |
recording.auto_record_on_zoom |
Zoom 자동 녹음 | true |
recording.prefer_system_audio |
BlackHole 우선 사용 | true |
recording.sample_rate |
샘플레이트 | 16000 |
recording.max_duration_seconds |
최대 녹음 시간 | 14400 (4시간) |
recording.min_duration_seconds |
앱 녹음 파일 조기 파기 기준 | 30초 |
audio_quality.min_duration_seconds |
전사 큐 진입 최소 실제 재생 시간 | 30.0초 |
audio_quality.decode_timeout_base_seconds |
ffmpeg full-decode 최소 timeout | 60.0초 |
audio_quality.decode_timeout_factor |
음성 길이 비례 timeout 계수 | 0.25 |
audio_quality.decode_timeout_cap_seconds |
ffmpeg full-decode timeout 상한 | 900.0초 |
watcher.file_ready_timeout_seconds |
쓰기 중인 입력 파일의 readiness 최대 대기 | 30.0초 |
watcher.writer_probe_timeout_seconds |
macOS system lsof 단일 writable-open 검사 제한 |
2.0초 |
watcher.startup_probe_concurrency |
재시작 시 기존 파일 writable-open(lsof) 확인 최대 동시성 |
8 |
watcher.startup_writer_attestation_max_age_seconds |
기존 DB 작업이 없는 startup 신규 ACCEPT의 final writer 확인 재사용 상한 | 0.25초 |
pipeline.skip_llm_steps=true이면 correct뿐 아니라 이에 의존하는
summarize/chunk/embed도 보류합니다. merge.json과 회의 ID에 결합된 no-replace
llm_deferred.json marker가 후속 의도를 보존합니다. 나중의 LLM 실행은 canonical
산출물이 모두 비어 있을 때만 최종 이름을 O_EXCL로 직접 생성합니다. 이전 실행의
부분 산출물이나 임의의 generation hard-link는 자동 복구 근거로 사용하지 않으며,
기존 파일을 그대로 보존하고 SECURITY_BLOCKED로 중단합니다.
로컬 저장소는 현재 macOS 사용자 계정이 소유하는 신뢰 경계입니다. no-follow/pinned-FD 검사는 symlink 탈출과 관측된 namespace 경쟁에서 외부 파일을 덮어쓰거나 지우지 않기 위한 것이며, 같은 사용자 권한의 악성 프로세스가 파일·DB·코드를 변조하는 상황의 authenticity를 보장하지는 않습니다.
화자분리의 실제 실행 제한은 diarization.*에서 관리합니다. 기본 공식은
max(timeout_seconds, min(dynamic_timeout_max_seconds, ceil(오디오 길이 × dynamic_timeout_multiplier)))입니다.
따라서 명시적으로 더 크게 설정한 고정 하한은 낮추지 않습니다. Zoom 보호로 worker가 일시정지된 시간은
실행 제한에서 제외됩니다. 화자분리 timeout이 발생해도 원본·변환 WAV·전사 체크포인트는
보존되며 재시도는 마지막 성공 단계 다음인 화자분리부터 재개합니다.
audio_quality.enabled: true일 때 길이는 ffmpeg 16 kHz mono full-decode
sample count와 성공한 ffprobe duration 중 더 짧은 값으로 판정합니다.
이로써 잘린 파일과 codec padding을 둘 다 보수적으로 처리합니다. 저장된 미디어 기준
30초 미만이거나 파일 자체의 손상이 확정된 음성은 전사 큐에 등록하지 않고
~/.meeting-transcriber/audio_quarantine/으로 이동합니다. 도구 부재·timeout·source busy·
보안 차단처럼 파일 결함을 확정할 수 없는 경우에는 원본을 보존하고 큐/STT 진입만
차단합니다. 정확히 30초인 파일은 볼륨 조건을 만족하면 통과합니다.
입력 파일이나 audio_input/audio_quarantine 등 설정 경로의 symlink는 지원하지
않으며 외부 target을 읽거나 이동하지 않습니다. 쓰기 중인 파일은 readiness timeout 뒤에도
원본을 보존하고 후속 변경 또는 재시작 때 다시 검사합니다. audio_quality.enabled: false는
길이·볼륨 full-decode 정책만 끄며, 경로·일반 파일·쓰기 완료 안전 검사는 유지합니다.
브라우저 업로드도 raw 설정 경로를 no-follow로 검증하고 완성된 임시 inode를 원자적으로
무덮어쓰기 publish한 뒤, watcher의 같은 품질 gate를 거쳐야만 queue에 등록됩니다.
기존 DB 작업을 격리할 때는 source identity와 예약 목적지를 journal에 먼저 기록하므로,
격리 이동과 DB 정리 사이 앱이 종료돼도 다음 시작에서 안전하게 이어서 완료합니다.
재시작 감사의 최초·최종 lsof 확인은 설정된 동시성 안에서 실행됩니다. 기존 DB 작업이
없는 startup 신규 ACCEPT만 짧은 final 확인 결과를 재사용하며, 그 사이 fingerprint가
바뀌거나 허용 시간이 지나면 적용 직전에 다시 확인합니다. 원본 격리와 기존 큐 복원은
파괴적 또는 실행 의도를 바꾸는 작업이므로 항상 적용 직전에 직렬로 writer 상태를
확인합니다. writer가 없는 정상 조회에서 macOS가 대상과 무관한 임시 HFS/APFS mount
warning만 출력한 경우에는 이를 별도 경고로 기록하고 계속 판정하지만, 대상 mount·권한
오류·알 수 없는 stderr·timeout은 계속 보류합니다.
재시작 시 기존 오디오 감사는 FastAPI HTTP startup과 분리됩니다.
writable-open 확인은 설정된 동시성 경계 안에서 실행하고, ffmpeg 품질 검증과
DB/quarantine 변경은 순차 처리합니다. 감사 중에도 /api/health와
/api/status는 응답하지만, JobProcessor·Lifecycle·AutoProcessing은 감사가
끝난 뒤에만 시작합니다. 메뉴바는 설정된 timeout 동안 서버 readiness를
확인하고, 포트 바인드나 lifespan 시작이 실패하면 실행되지 않습니다.
환경변수로 오버라이드 가능:
| 환경변수 | 설명 |
|---|---|
MT_BASE_DIR |
데이터 디렉토리 |
MT_SERVER_PORT |
서버 포트 |
MT_LLM_BACKEND |
LLM 백엔드 (mlx 또는 ollama) |
MT_LLM_MODEL |
MLX 모델명 오버라이드 |
MT_LLM_HOST |
Ollama 호스트 (Ollama 사용 시) |
HUGGINGFACE_TOKEN |
현재 프로세스용 HuggingFace 토큰 폴백. LaunchAgent는 CLI 캐시 사용 |
OPENAI_API_KEY |
개발/CI용 OpenAI 키 폴백. 일반 사용은 macOS Keychain 권장 |
config.yaml의 pipeline.bulk_stage_batching: true를 켜면 같은 로컬 STT 모델의
전체 처리 대기 회의 두 건을 전사부터 묶어서 처리합니다. Whisper와 LLM을 묶음 안에서
재사용하고, 저장한 단계 다음부터 재개합니다. 한 번에 하나의 대형 모델만 사용하며
기존 2건 후 쿨다운을 유지합니다. 기본값은 false입니다.
전체 묶음의 모델 전환은 줄지만 첫 회의의 교정본이 나오는 시점은 늦어질 수 있습니다. 설정·취소·복구 계약과 검증 범위는 단계별 일괄 처리를 참고하세요.
meeting-transcriber/
├── main.py # 앱 진입점 (rumps + FastAPI)
├── config.py # 설정 관리 (Pydantic + YAML)
├── config.yaml # 설정 파일
├── core/ # 핵심 엔진
│ ├── pipeline.py # 전사 파이프라인 (11단계 순차 처리)
│ ├── model_manager.py # 모델 순차 로드 (RAM 9.5GB 제한)
│ ├── job_queue.py # 작업 큐 관리
│ ├── thermal_manager.py # 서멀 관리 (2-job + 쿨다운)
│ ├── watcher.py # 폴더 감시
│ ├── orchestrator.py # 파이프라인 오케스트레이터
│ ├── transcription_models.py # 로컬/OpenAI 전사 선택 화이트리스트
│ ├── llm_backend.py # LLM 백엔드 프로토콜 (Ollama/MLX)
│ ├── ollama_client.py # Ollama API 클라이언트
│ ├── mlx_client.py # MLX in-process LLM 백엔드
│ └── chipset_detector.py # Apple Silicon 칩셋 감지
├── steps/ # 파이프라인 단계
│ ├── audio_converter.py # 오디오 → WAV 변환
│ ├── transcriber.py # STT (mlx-whisper)
│ ├── openai_transcriber.py # 명시적 선택 시 OpenAI 화자분리 전사
│ ├── vad_detector.py # 음성 구간 감지 (Silero VAD v5)
│ ├── hallucination_filter.py # 환각 필터링 (4중 기준)
│ ├── text_postprocessor.py # 텍스트 정규화 (NFC, 공백)
│ ├── number_normalizer.py # 숫자 표현 정규화
│ ├── diarizer.py # 화자 분리 (pyannote)
│ ├── merger.py # 전사 + 화자 병합
│ ├── corrector.py # 설정된 로컬 LLM 교정 (Gemma 4 기본 / EXAONE 선택)
│ ├── chunker.py # 텍스트 청크 분할
│ ├── embedder.py # 벡터 임베딩
│ ├── summarizer.py # AI 요약
│ ├── zoom_detector.py # Zoom 회의 감지 (CptHost 프로세스)
│ └── recorder.py # 오디오 녹음 (ffmpeg AVFoundation)
├── search/ # 검색 엔진
│ ├── hybrid_search.py # 하이브리드 검색 (Vector + FTS5)
│ └── chat.py # AI 채팅 (RAG)
├── api/ # REST API
│ ├── server.py # FastAPI 서버
│ ├── routes.py # 하위 호환 shim
│ ├── routers/ # 기능별 API 라우터
│ └── websocket.py # WebSocket 실시간 통신
├── ui/ # 사용자 인터페이스
│ ├── menubar.py # macOS 메뉴바 (rumps)
│ ├── native_window.py # PyWebView 네이티브 창
│ ├── launcher.py # 경량 .app 런처용 read-only preflight/command 계약
│ └── web/ # 웹 UI (SPA, 순수 HTML/CSS/JS)
│ ├── index.html # 3-Column SPA 셸
│ ├── style.css # 공통 레이아웃/디자인 시스템
│ ├── *-view.js # 기능별 SPA view/controller
│ ├── *.css # 공통/기능별 component CSS
│ ├── app.js # 공통 유틸리티 (API, WebSocket)
│ └── spa.js # SPA 라우터 + 뷰 (Home/Viewer/Search/Chat/Settings)
├── security/ # 보안
│ ├── secure_dir.py # 디렉토리 보안 설정
│ ├── lifecycle.py # 데이터 수명주기 관리
│ ├── health_check.py # 시스템 상태 점검
│ ├── openai_keychain.py # OpenAI 키 Keychain 저장/조회
│ └── setup_readiness.py # 최초 설정 마법사용 read-only 준비 상태
├── scripts/ # 스크립트
│ ├── install.sh # 설치 스크립트
│ ├── build_launcher_app.py # unsigned local .app 런처 번들 생성
│ ├── validate_launcher_app.py # .app 구조/서명 readiness read-only 검증
│ ├── build_launcher_dmg.py # unsigned local DMG 패키징
│ ├── build_release_manifest.py # unsigned local 산출물 manifest 생성
│ ├── build_unsigned_release.py # unsigned local release 산출물 일괄 생성
│ ├── setup_launchagent.sh # 자동 시작 설정
│ ├── benchmark_ab_test.py # STT A/B 벤치마크
│ └── convert_whisper_mlx.py # Whisper 모델 MLX 변환
└── tests/ # 단위·통합·UI·하네스 테스트
| 영역 | 기술 |
|---|---|
| STT | mlx-whisper whisper-large-v3-turbo (기본·로컬), 선택적 OpenAI gpt-4o-transcribe-diarize |
| 화자 분리 | pyannote-audio 3.1 (CPU) |
| LLM | Gemma 4 E4B (기본) 또는 EXAONE 3.5 7.8B / Gemma 4 E2B via MLX |
| 임베딩 | multilingual-e5-small (MPS) |
| 벡터 DB | ChromaDB |
| 키워드 검색 | SQLite FTS5 |
| API | FastAPI + WebSocket |
| macOS UI | rumps |
- 로컬 우선: 기본 동작은 오프라인. 기본 OpenAI 선택은 한 번 동의 후 이후 새 전사에 적용되고, 회의별 비교는 매번 별도 동의
- MLX 기본 백엔드: Gemma 4 E4B (기본) / EXAONE 3.5 / Gemma 4 E2B 중 선택, Ollama도 지원
- 웹 UI 설정 변경: 기본 전사 위치/API 키, LLM 모델/Temperature/전사 언어를 브라우저에서 변경
- Zoom 자동 녹음: 회의 감지 → 녹음 → 전사까지 완전 자동화
- 순차 모델 로드: RAM 16GB 제한 내에서 피크 9.5GB 유지
- 서멀 관리: 팬리스 MacBook Air에서도 안정적 실행 (2-job 배치 + 3분 쿨다운)
- 체크포인트 복구: 파이프라인 중단 시 마지막 단계부터 재개
- 데이터 보안: chmod 700, Spotlight 제외, localhost only
- 파일 스테이징: 녹음 중 파일은
recordings_temp/에 격리, 완료 후audio_input/으로 이동 - STT 품질 강화: VAD 전처리 + 4중 환각 필터링 + 텍스트 정규화
- 데이터 라이프사이클: Hot/Warm/Cold 분류와 보존 상태 점검(자동 압축·삭제 없음)
- Graceful Degradation: 개별 단계 실패 시 다음 단계로 폴백, 부분 결과 유지
정확한 테스트 수와 커버리지는 코드 변경에 따라 달라지므로 고정 수치로 게시하지 않습니다.
현재 소스의 회귀 검증은 pytest tests/ -x -q와 docs/STATUS.md의 릴리스 게이트를
기준으로 합니다.
오디오 입력 (.wav/.m4a/.mp3)
→ [1] 오디오 변환 (ffmpeg → 16kHz mono WAV)
→ [2] STT 전사
├─ 로컬: 필요 시 VAD → mlx-whisper → 환각 필터/텍스트 후처리
└─ OpenAI: 명시적 동의 후 diarized transcription
→ [3] 화자 분리 (OpenAI 단일 청크의 화자 구간 재사용 / 그 외 pyannote community-1, CPU)
→ [4] 세그먼트 병합 (STT + 화자 시간 매칭)
→ [5] LLM 교정 (Gemma 4 기본, EXAONE 선택 가능)
→ [6] AI 요약 생성
→ [7] 스마트 청킹 (토픽/시간 기반, 300토큰)
→ [8] 벡터 임베딩 (ChromaDB + SQLite FTS5 이중 저장)
→ 검색 가능한 회의록 완성
| 지표 | 목표 | 비고 |
|---|---|---|
| 피크 RAM | 9.5GB / 16GB | ModelLoadManager 뮤텍스로 강제 |
| 배치 처리 | 2건 + 3분 쿨다운 | 팬리스 MacBook Air 서멀 관리 |
| 체크포인트 | 단계별 JSON 저장 | 중단 시 마지막 성공 단계부터 재개 |
| 동시 모델 | 최대 1개 | STT→화자분리→LLM 순차 로드/언로드 |
| 처리 단계 | 설명 |
|---|---|
| 한국어 STT 모델 | whisper-large-v3-turbo (기본) — 6 회의 벤치마크 1위 (docs/BENCHMARK.md §1). komixv2 대비 CER −16%p. 한국어 fine-tune 모델은 GUI 에서 선택 가능 |
| 환각 필터링 | 4단 (avg_logprob, no_speech_prob, 세그먼트 내부 반복, 크로스 세그먼트 반복) |
| 텍스트 정규화 | NFC 유니코드 정규화, 공백/줄바꿈 정리 |
| 숫자 정규화 | 한국어 숫자 표현 통일 |
| VAD | 기본 OFF — 이 환경에서 VAD ON 시 실행시간 3배 증가·커버리지 저하 관찰됨. 필요 시 vad.enabled: true 로 전환 |
기본값(STT 모델, VAD, LLM, 필터 임계값 등)은 회의 오디오를 대상으로 한
실험 결과에 근거해 선택했습니다. 표본이 작고 단일 하드웨어(M4 16GB)에서의
측정이라 일반화에 한계가 있습니다. 상세 데이터·한계·재현 방법은
docs/BENCHMARK.md 참조.
요약:
| 영역 | 기본값 | 관찰 |
|---|---|---|
| STT 모델 | whisper-large-v3-turbo |
6개 실제 회의 비교에서 komixv2보다 안정적 (§1.1) |
| VAD | OFF | ON 시 실행 3.1배·커버리지 -13.2%p (이 환경) |
| LLM 모델 | gemma-4-e4b-it-4bit |
정답지 44발화 대비 유사도 92.9% vs EXAONE 47.5% |
| LLM temperature | 0.0 | MLX 4bit에서 0.0~0.5 결과 동일 관찰 |
| 교정 batch_size | 5 | 파싱 100%, 원문 변형 최소 |
주요 한계 (자세한 내용은 docs/BENCHMARK.md#한계):
- 단일 하드웨어 측정 (M4 16GB)
- 정답지 44 발화(2 샘플) — 통계적 유의성 확보엔 부족
- 정답지는 Claude 가 수동 작성한 것으로, 편집 스타일 편향 가능성 있음
- LLM 결과는 "회의록 교정" 태스크 한정. 다른 태스크(예: 한국어 QA)에서는 EXAONE 이 우수하다는 공개 벤치마크가 있음
재현:
# LLM 파라미터 스윕 (temperature × batch_size)
python scripts/benchmark_llm_correct.py
# 설정 재검증 (3 샘플로 동일 설정 재적용)
python scripts/validate_settings.py# 기본 안정 게이트: e2e/ui/native 마커는 pyproject.toml 정책에 따라 제외
pytest tests/ -v --tb=short
# 빠른 실행
pytest tests/ -q
# 핵심 unit/search/queue 스모크
pytest tests/test_config.py tests/test_job_queue.py tests/test_hybrid_search.py -q
# 주요 route 스모크
pytest tests/test_routes_home_dashboard.py tests/test_routes.py tests/test_routes_meetings_batch.py -q
# UI 하네스와 bulk actions 품질 게이트는 명시 실행
pytest -m harness -q
pytest -m ui tests/ui/behavior/test_bulk_actions_behavior.py -q
pytest -m ui tests/ui/a11y/test_bulk_actions_a11y.py -q
pytest -m ui tests/ui/visual/test_bulk_actions_visual.py -q
# MLX/Metal 등 native 런타임 테스트는 기본 게이트에서 제외한다.
# GitHub Actions에서는 workflow_dispatch/주간 schedule diagnostic gate로 실행한다.
pytest -m native tests/ -v
# 특정 모듈 테스트
pytest tests/test_transcriber.py -v
pytest tests/test_hallucination_filter.py -v
# 커버리지 리포트
pytest tests/ --cov=core --cov=steps --cov=search --cov=api --cov=security --cov=ui --cov-report=term
# 커버리지 HTML 리포트
pytest tests/ --cov=core --cov=steps --cov=search --cov=api --cov=security --cov=ui --cov-report=html
# open htmlcov/index.html# 린트
ruff check .
# 포맷 검사
ruff format --check .
# 포맷 적용
ruff format .
# 타입 체크
mypy config.py api core steps search ui security --no-error-summaryRAG 검색 인덱스(ChromaDB + SQLite FTS5) 가 누락된 회의가 있는 경우 사용.
다음 상황 중 하나라도 해당되면 일부 또는 전체 회의가 채팅에서 "회의 전사문이 제공되지 않았습니다" 와 같이 응답할 수 있다.
- 2026-04 이전 (chunk/embed 단계가 메인 파이프라인에 추가되기 전) 에 완료된 회의
- 임베딩 단계 실행 중 ChromaDB / FTS5 저장이 실패한 적이 있는 회의
~/.meeting-transcriber/chroma_db/또는meetings.db를 수동으로 삭제한 경우
신규 회의는 자동으로 인덱싱되므로 별도 조치 불필요.
전사문 저장·모두 바꾸기는 수정 버전을 원문과 함께 저장하고 자동 재색인을 예약합니다. 반영 대기·실패 중인 회의의 이전 청크는 검색 답변에서 제외하며, 앱을 다시 시작하면 완료되지 않은 편집본의 재색인을 재등록합니다. 전사문 위에 검색 반영 상태와 요약 확인 필요 안내가 표시됩니다. 기존 요약은 자동으로 바꾸지 않습니다. 편집된 회의를 인용하는 Wiki 페이지는 버전 연결이 마련되기 전까지 답변 근거에서 보수적으로 제외합니다.
설정의 복구 필요는 벡터 인덱스뿐 아니라 키워드 인덱스, 기대 청크 ID, 편집 버전을 함께 검사한 결과입니다. 개별 회의 옆에서 반영 대기·실패·일부 누락· 검증 정보 없음 등의 원인을 확인하고 재색인할 수 있습니다. 원본 날짜를 확인할 수 없는 재색인은 오늘 날짜로 추측하지 않고 검색 날짜를 빈 값으로 둡니다.
회의·날짜·화자를 지정한 채팅은 해당 범위를 보존하는 RAG 경로를 사용합니다. WebSocket 클라이언트는 앱과 동일한 loopback Host·포트·Origin으로 연결해야 하며, Origin이 없는 연결도 거부됩니다. 일반 웹 UI는 브라우저가 Origin을 자동으로 보냅니다.
검색 갱신은 대기와 작업 중을 구분하며 작업 중 종료된 기록도 재시작 시 복구합니다.
최초 인덱싱에서 결정한 날짜와 출처는 회의별 meeting_date.json에 보존하고 이후
파이프라인·재색인에서 함께 사용합니다. 날짜 미확정 회의의 Wiki 생성은 이유를
기록하고 보류합니다.
검색 API의 source_errors는 벡터·키워드 검색 장애를 구분합니다. 검색 화면은
정상 저장소에서 찾은 결과를 유지하면서 오류를 안내하며, 두 저장소가 실패한 경우
‘검색 결과 없음’ 대신 복구 안내를 표시합니다. 채팅은 검색 일부가 실패한 경우에도
불완전한 근거로 생성하지 않고 검색 오류와 복구 안내를 반환합니다.
- 메뉴바 → 웹 UI 열기 → 설정 → "검색 인덱스" 탭
- "전체 누락분 백필 시작" 버튼 클릭
- 진행 상황은 progress bar 로 표시 (백그라운드 실행, 창을 닫아도 계속됨)
- 누락 회의 목록에서 개별 회의만 재색인할 수도 있음
# 누락 회의 현황 확인
curl -s http://127.0.0.1:8765/api/reindex/status | jq
# 단일 회의 재색인 (correct.json 또는 merge.json 체크포인트 필요)
curl -X POST http://127.0.0.1:8765/api/meetings/<meeting_id>/reindex
# 일괄 백필 시작 (백그라운드)
curl -X POST http://127.0.0.1:8765/api/reindex/all진행 상황은 WebSocket reindex_progress 이벤트로 실시간 broadcast 된다.
일괄 백필은 글로벌 lock 으로 단일 동시 실행만 허용하며 (메모리 / DB 충돌 방지),
오디오 재처리 없이 LLM/STT 결과를 재사용하므로 빠르게 복구 가능.
CONTRIBUTING.md를 참고하세요.


