이 저장소가 대체 무엇인가.
meetily(제품명 Meetily, 원 저장소명 meeting-minutes)는 마이크와 시스템 소리(상대방 목소리)를 동시에 잡아 실시간으로 자막을 만들고, 회의가 끝나면 로컬 LLM으로 요약본을 생성하는 데스크톱 앱이다. 핵심 원칙은 단 하나 — 데이터가 내 기기를 절대 떠나지 않는다. 음성인식 모델도, 녹음 파일도, 전사문도, 요약도 전부 로컬 디스크에 저장된다.
보통의 AI 회의 서비스는 회의 음성을 외부 속기 회사로 보낸다. 빠르고 편하지만, 내 민감한 대화가 남의 서버에 저장된다. 변호사·의사·국방·기업 임원처럼 대화가 새어나가면 안 되는 사람에게는 치명적이다.
meetily는 그 속기사를 내 방 안에 들여놓은 셈이다. 속기사(Whisper/Parakeet)가 실시간으로 받아쓰고, 비서(로컬 LLM)가 요약을 써 주는데, 둘 다 내 노트북 밖으로 한 발짝도 나가지 않는다. 인터넷을 꺼도 동작한다.
코드상 버전은 0.4.0이며 라이선스는 관대한 MIT(제작사 Zackriya Solutions, 웹사이트 meetily.ai). 지원 대상은 Tauri 데스크톱 앱 하나로 통일됐고 macOS·Windows·Linux를 모두 지원한다. 과거 존재했던 Python/FastAPI 백엔드는 archived(보관·미지원) 상태로, 현재 모든 로직은 Rust 코어가 담당한다. 유료 PRO 버전(화자 분리·고급 내보내기 등)도 있지만, 커뮤니티 에디션은 "영원히 무료·오픈소스"를 표방한다.
회의록이라는 용도는 겉모습이다. 이 레포의 진짜 가치는 실시간 오디오 캡처·믹싱·VAD 파이프라인 · GPU 가속 음성인식 · 여러 LLM 프로바이더 추상화 · llama.cpp 사이드카 · Tauri 커맨드/이벤트 아키텍처 · SQLite 리포지토리 패턴을 한 코드베이스에서 통째로 볼 수 있다는 점이다. "AI를 로컬에서 돌리는 데스크톱 앱"을 만들고 싶은 사람에게 이만한 참고서가 드물다.
트렌딩 이유와, 흔한 "받아쓰기 앱"과의 결정적 차이.
대부분의 회의 AI는 "데이터를 안전하게 다룬다"고 약속하지만, 결국 음성을 자기 서버로 올려 처리한다. meetily는 애초에 서버가 없다. 음성인식은 노트북의 Whisper.cpp / Parakeet가, 요약은 Ollama·llama.cpp가 로컬에서 수행한다. GDPR·의료·법률·국방처럼 데이터 주권(data sovereignty)이 필수인 곳에서 "구조적으로" 안전하다는 것이 최대 강점이다.
클라우드 회의 AI = 편의점 택배함에 회의 녹음을 넣으면 본사가 알아서 처리해 돌려준다(빠르지만 남이 내용물을 본다). meetily = 집에 속기 기계와 요약 기계를 들여놓는다(설치·GPU가 필요하지만 내용물이 집 밖으로 안 나간다).
meetily는 전사 엔진을 TranscriptionProvider라는 하나의 트레이트(인터페이스)로 추상화했다. 그래서 정확도가 좋은 OpenAI Whisper와, NVIDIA가 만든 초고속 Parakeet(ONNX)를 같은 코드 경로로 교체할 수 있다. "정확도 우선 / 속도 우선"을 사용자가 고르게 하는 실전 설계다. 두 엔진 모두 GPU 가속(macOS Metal·CoreML, NVIDIA CUDA, AMD/Intel Vulkan)이 자동으로 켜진다.
요약 단계는 Ollama(로컬)·llama.cpp(내장 사이드카)·Claude·Groq·OpenRouter·OpenAI·커스텀 엔드포인트를 모두 지원한다. 프라이버시가 중요하면 로컬, 품질이 급하면 클라우드 — 한 앱 안에서 상황에 따라 스위칭한다. 내부적으로는 대부분 "OpenAI 호환 API" 형식으로 통일하고, Claude만 별도 포맷으로 처리하는 얇은 어댑터 층을 둔다.
루트의 파일 몇 개가 전부가 아니다. Rust 소스만 130여 개 파일이 audio · audio_v2 · whisper_engine · parakeet_engine · summary · database · 각 LLM 프로바이더 모듈로 정교하게 나뉘고, 프로 오디오 믹싱(RMS 더킹·클리핑 방지)·EBU R128 음량 정규화·RNNoise 잡음제거·VAD·GPU 자동감지·자동 업데이트·트레이 아이콘·크로스플랫폼 빌드 스크립트까지 갖췄다. "로컬 AI 스크립트"를 어떻게 배포 가능한 데스크톱 제품으로 키우는지 보여주는 표본이다.
회의록이라는 용도는 부차적이다. 진짜 가치는 Rust 실시간 오디오 · 크로스플랫폼 시스템 오디오 캡처 · 음성인식/LLM 엔진 추상화 · llama.cpp 프로세스 사이드카 · Tauri IPC · SQLite 리포지토리 · Next.js↔Rust 상태 동기화를 실전 규모의 코드로 통째로 관찰할 수 있다는 것.
무엇으로 만들어졌나 — 계층별로. (Cargo.toml·package.json에서 실제 확인)
| 기술 | 역할 | 왜 이걸 썼나 |
|---|---|---|
| Tauri 2.6 | 데스크톱 셸(창·IPC·배포) | Electron보다 가볍고, 코어를 Rust로 짜서 오디오/AI를 네이티브 속도로 |
| Next.js 14 + React 18 | UI 레이어 | 정적 export 결과물을 Tauri가 로컬에서 서빙(개발 포트 3118) |
| TypeScript 5 | 프런트 언어 | 타입 안전한 UI·Tauri 커맨드 호출 |
| BlockNote / Tiptap / Remirror | 리치 텍스트 에디터 | 요약·노트를 편집 가능한 문서로(블록 기반 에디터) |
| Radix UI + Tailwind 3 | 컴포넌트·스타일 | 접근성 좋은 프리미티브 + 유틸리티 CSS |
| TanStack Virtual | 전사문 가상 스크롤 | 긴 회의 자막을 렌더링 성능 저하 없이 표시 |
그 밖에 framer-motion(애니메이션), react-hook-form+zod(폼·검증), react-markdown+remark-gfm(요약 렌더), sonner(토스트), Tauri 플러그인 fs·store·dialog·notification·updater·process·os·single-instance를 쓴다.
| 기술(크레이트) | 역할 | 메모 |
|---|---|---|
| cpal | 크로스플랫폼 오디오 캡처 | 마이크·시스템 사운드 입력 스트림(패치 버전 사용) |
| whisper-rs 0.13 | Whisper.cpp 바인딩(STT) | Metal·CoreML·CUDA·Vulkan·hipblas·OpenBLAS 피처 |
| ort 2.0 (ONNX Runtime) | Parakeet 전사 엔진 | NVIDIA Parakeet TDT 모델을 ONNX로 초고속 추론 |
| silero-rs | VAD(음성 활동 감지) | 말이 있는 구간만 골라 Whisper에 전달(부하 ~70%↓) |
| ebur128 / nnnoiseless | 음량 정규화 / 잡음제거 | 방송 표준 EBU R128 라우드니스 + RNNoise 신경망 노이즈 억제 |
| rubato / realfft / ringbuf | 리샘플링 / FFT / 링버퍼 | 48kHz↔16kHz 변환, 오디오 분석, 무락 버퍼링 |
| symphonia / ffmpeg-sidecar | 오디오 디코딩·임포트 | mp3·flac·aac·mp4·ogg·wav 파일 임포트 & 재전사 |
| sqlx 0.8 (SQLite) | 로컬 DB | 회의·전사·요약·설정을 파일 DB로(async, 컴파일타임 검증) |
| tokio / crossbeam / dashmap | 비동기·동시성 | 스트리밍 오디오와 전사를 병렬로 안전하게 |
macOS는 시스템 오디오 캡처에 cidre(ScreenCaptureKit 바인딩)와 core-graphics·objc를, 분석에 whatlang(요약 언어 감지)을, 텔레메트리에 posthog-rs를 쓴다.
| 기술 | 역할 | 메모 |
|---|---|---|
| llama-cpp-2 0.1.146 | GGUF 모델 추론 | 별도 Rust 바이너리로 llama.cpp를 감쌈 |
| stdin/stdout JSON 프로토콜 | 메인 앱↔사이드카 통신 | Generate·Ping·Shutdown 요청 / Response·Pong·Goodbye·Error 응답 |
| Metal·CUDA·Vulkan 피처 | GPU 가속 | 요약 생성도 GPU로. opt-level="s"+LTO로 빠른 로딩 |
즉 요약용 로컬 LLM은 메인 프로세스가 아니라 별도 프로세스(사이드카)로 띄운다. 무거운 모델이 죽어도 앱 본체는 살아있게 하는 격리 설계다.
backend/의 FastAPI·Docker·standalone whisper-server는 archived(미지원)다. CLAUDE.md는 "이 코드는 역사·마이그레이션 참고용일 뿐, 새 개발·배포·이슈 트리아지에 쓰지 말라"고 못박는다. 특히 옛 FastAPI의 인증 없는 개발용 CORS 설정은 "지원되는 프로덕션 API가 아니다"라고 명시한다. backend/whisper.cpp는 Zackriya 포크를 서브모듈로 참조한다.
데이터가 마이크에서 요약까지 어떻게 흐르는가.
meetily 아키텍처에서 가장 배울 점이 많은 부분이다. 마이크와 시스템 오디오가 들어오면, 파이프라인은 같은 소리를 목적이 다른 두 경로로 동시에 흘려보낸다.
왜 나누나? 녹음은 "나중에 다시 들을 원음"이라 모든 소리를 곱게 믹싱해 보존해야 한다. 반대로 전사는 "글자로 바꿀 말소리"만 필요하므로, VAD(음성 활동 감지)로 침묵·잡음 구간을 버려 음성인식 연산을 아낀다. 하나의 스트림에서 목적별로 처리를 분기하는 전형적 실전 패턴이다.
silero-rs 신경망 VAD로 말소리 구간만 Whisper에 넘겨, 침묵까지 전사하느라 GPU를 낭비하는 것을 막는다.마이크와 시스템 오디오는 서로 다른 속도로 비동기 도착한다. 그대로 더하면 어긋난다. meetily는 VecDeque 기반 링버퍼에 샘플을 쌓아두고, 두 스트림이 같은 시간창(50ms)만큼 모였을 때 정렬해 섞는다. 이때 RMS 기반 더킹(한쪽이 커지면 다른 쪽을 살짝 눌러 줌)으로 시스템 소리가 사람 목소리를 덮지 않게 한다. 방송 엔지니어링에서 쓰는 기법을 그대로 구현했다.
라디오 DJ가 말할 때 배경음악이 자동으로 작아지는 것 — 그게 더킹(ducking)이다. meetily는 회의 녹음에서 상대방 화면 소리가 내 목소리를 삼키지 않도록 같은 원리를 적용한다.
전사와 요약 모두 "인터페이스로 먼저 정의하고, 구현을 갈아끼우는" 방식이다. 전사 쪽은 TranscriptionProvider 트레이트가 transcribe(audio, language) 한 메서드로 Whisper·Parakeet를 통일한다.
// audio/transcription/provider.rs
#[async_trait]
pub trait TranscriptionProvider: Send + Sync {
async fn transcribe(
&self,
audio: Vec<f32>, // 16kHz 모노 샘플
language: Option<String>, // "en", "ko" …
) -> Result<TranscriptResult, TranscriptionError>;
async fn is_model_loaded(&self) -> bool;
fn provider_name(&self) -> &'static str; // 로깅용
}
요약 쪽도 마찬가지로 LLMProvider 열거형(OpenAI·Claude·Groq·OpenRouter·Ollama…)으로 분기하되, 요청/응답 구조체를 OpenAI 호환 형식으로 통일하고 Claude만 ClaudeRequest/ClaudeChatResponse로 따로 처리한다. 새 프로바이더를 추가할 때 이 얇은 어댑터만 구현하면 된다.
프런트(UI)와 Rust 코어는 두 방향으로 대화한다. UI→Rust는 커맨드(요청/응답), Rust→UI는 이벤트(스트리밍 푸시)다.
// UI → Rust : 커맨드 호출
await invoke('start_recording', {
mic_device_name: "Built-in Microphone",
system_device_name: "BlackHole 2ch",
meeting_name: "팀 스탠드업"
});
// Rust → UI : 실시간 자막 이벤트 방출
app.emit("transcript-update", TranscriptUpdate {
text: "안녕하세요".to_string(),
timestamp: Utc::now(),
});
UI는 listen("transcript-update", …)로 구독해 React 상태에 밀어 넣는다. 무거운 오디오·AI는 Rust가 백그라운드로 돌리고, 결과만 이벤트로 흘려보내 UI가 멈추지 않게 하는 구조다. 공유 상태는 Arc<RwLock<T>>, 단순 플래그는 Arc<AtomicBool>로 관리한다.
요약용 로컬 모델(GGUF)은 메인 앱이 직접 로드하지 않고, llama-helper라는 별도 실행 파일을 띄워 JSON을 stdin/stdout으로 주고받는다. 모델 로딩이 무겁거나 크래시가 나도 본체는 안전하다. 프로토콜은 단순하다 — 요청은 Generate/Ping/Shutdown, 응답은 Response/Pong/Goodbye/Error. 사이드카 바이너리는 opt-level="s"+LTO로 작게 빌드해 기동을 빠르게 했다.
오디오는 목적별 경로 분리, 엔진은 트레이트/열거형 추상화, 무거운 모델은 사이드카 격리, UI-코어는 커맨드/이벤트 분리. 큰 네이티브 앱을 어떻게 모듈로 쪼개 안정적으로 만드는지의 모범 답안이다.
어디에 무엇이 있나 — 실제 트리 기준.
핵심은 실제 로직의 90%가 frontend/src-tauri/src에 있다는 점이다. 이름은 "frontend"지만, 그 안의 src-tauri가 바로 Rust 백엔드 코어다. Tauri 앱의 전형적 구조로, "프런트 폴더 안에 네이티브 코어가 동거"한다.
CLAUDE.md가 아키텍처를 통째로 설명하므로 여기서 시작하는 게 가장 빠르다. 그다음 진입점 lib.rs에서 커맨드 등록을 보고, 오디오→전사→요약 순으로 따라가면 데이터 흐름이 잡힌다.audio/에 파일이 왜 이렇게 많나원래 1028줄짜리 core.rs 한 파일이던 오디오 시스템을, 기능별 모듈로 리팩터링한 결과다(core-old.rs가 흔적으로 남아 있다). 덕분에 "장치 탐지 문제 → devices/discovery.rs", "믹싱 문제 → pipeline.rs"처럼 고장 지점을 파일 이름만 보고 찾을 수 있다. 거대한 파일을 어떻게 유지보수 가능하게 쪼개는지의 실례다.
이 레포로 무엇을, 어떻게 배울 수 있나.
cpal로 마이크·시스템 스트림을 열고, 서로 다른 속도의 스트림을 링버퍼로 동기화하는 법을 배운다. Arc<RwLock<T>>로 async 태스크 간 상태를 공유하고, Arc<AtomicBool>로 녹음 on/off 플래그를 원자적으로 다룬다. 핫패스 로깅은 perf_debug! 매크로로 릴리스 빌드에서 0비용이 되게 컴파일 분기하는 기법도 볼 수 있다.
AudioBufferPool)과 메트릭 배칭(AudioMetricsBatcher)으로 완충하는 실제 코드를 관찰할 수 있다."상대방 목소리(시스템 사운드)"를 잡는 건 OS마다 완전히 다르다. meetily는 devices/platform/ 아래에서 macOS=ScreenCaptureKit(cidre)·Windows=WASAPI 루프백·Linux=ALSA/PulseAudio를 각각 구현하고, 공통 인터페이스로 노출한다. 플랫폼 분기를 어떻게 캡슐화하는지 배우기 좋다.
Whisper.cpp(whisper-rs)와 Parakeet(ONNX ort)를 GPU 자동 감지로 로드하고, VAD로 전처리해 실시간 전사하는 전 과정을 볼 수 있다. 모델은 한 번 로드해 캐시하고, parallel_processor.rs로 배치 작업을 병렬화한다. "정확도 vs 속도"를 모델 크기(base/small/medium/large-v3)로 조절하는 실전 감각도 익힌다.
로컬(Ollama·llama.cpp)과 클라우드(Claude·Groq·OpenRouter·OpenAI)를 하나의 요약 서비스로 묶는 법을 배운다. OpenAI 호환 포맷으로 통일하고 예외(Claude)만 어댑터로 처리, CancellationToken으로 진행 중 요약을 취소, whatlang으로 회의 언어를 감지해 요약 언어를 자동 결정하는 등 방어적 설계가 가득하다.
llama-helper는 "무거운 작업을 별도 프로세스로 격리"하는 사이드카 패턴의 교과서적 예다. JSON-over-stdio 프로토콜을 직접 설계·구현해 보는 실습으로 이어진다.
커맨드/이벤트 IPC, 트레이 아이콘, 자동 업데이터, 단일 인스턴스 가드, 플러그인(fs·store·notification) 사용, 그리고 Next.js 정적 export를 네이티브 셸이 서빙하는 배포 구조까지 — 웹 기술로 진짜 데스크톱 앱을 만드는 전체 흐름을 익힌다.
(1) summary/templates/defaults.rs의 요약 템플릿 문구를 바꿔 빌드해 본다. (2) TranscriptionProvider를 구현한 더미 프로바이더(고정 텍스트 반환)를 추가해 트레이트 추상화를 체험한다. (3) 새 Tauri 커맨드 하나를 추가해 UI 버튼과 연결한다.
돌리려면 무엇이 필요한가.
| 구분 | 내용 |
|---|---|
| OS | macOS(Apple Silicon 권장), Windows 10/11(x64), Linux(소스 빌드) |
| 설치(바이너리) | macOS: meetily_0.4.0_aarch64.dmg / Windows: x64-setup.exe |
| GPU(선택·권장) | macOS Metal+CoreML(자동), NVIDIA CUDA, AMD/Intel Vulkan, AMD ROCm(hipblas). GPU 시 CPU 대비 5~10배 |
| 시스템 오디오 | macOS 13+ (ScreenCaptureKit) + 가상 오디오 장치(BlackHole), Windows는 WASAPI 루프백 |
| 권한 | 마이크 권한 필수. macOS는 시스템 오디오 캡처에 화면 녹화 권한도 필요 |
| 디스크 | Whisper 모델(수백 MB~1.5GB) + GGUF 요약 모델(선택) 저장 공간 |
| 빌드 도구 | Rust 1.77+, Node.js, pnpm. Windows는 Visual Studio C++ 빌드툴, Linux는 cmake·llvm·libomp |
meetily가 상대방 목소리를 잡으려면 시스템 오디오 캡처가 필요한데, 이건 OS 보안 장벽에 걸린다. macOS는 화면 녹화 권한 승인과 BlackHole 같은 가상 오디오 장치 설치가 사실상 필수다. Windows는 WASAPI 루프백이 다른 앱의 배타 모드와 충돌할 수 있다. 마이크만 잡히고 시스템 소리가 안 잡히면 이 권한/장치부터 확인하자.
난이도별로. 손으로 만져야 진짜 배운다.
summary/templates/defaults.rs에서 회의 요약 프롬프트/구획을 원하는 형식(예: "결정사항·액션아이템·리스크" 3단)으로 바꾸고 재빌드하라. LLM 프롬프트가 결과물을 어떻게 좌우하는지 즉시 체감할 수 있다.
transcribe()가 입력 오디오 길이에 비례한 가짜 텍스트를 돌려주는 MockProvider를 만들어 트레이트에 등록하라. 실제 모델 없이도 파이프라인(캡처→전사→저장→UI)을 끝까지 흐르게 해 보는, 테스트 더블의 정석 연습.
"현재 로드된 전사 모델 이름과 GPU 백엔드를 반환하는" 커맨드를 lib.rs에 추가하고 invoke_handler에 등록한 뒤, 설정 화면에 표시하라. Rust↔React 왕복(커맨드→응답→상태 반영)을 온전히 경험한다.
audio/import.rs+symphonia 경로를 읽고, 지원 포맷을 하나 더 추가하거나 "임포트한 파일을 다른 모델로 재전사"하는 옵션을 붙여라. 파일 디코딩→리샘플링(48k↔16k)→전사로 이어지는 흐름을 손보며 오디오 처리를 깊이 이해한다.
llama-helper/src/main.rs의 JSON-over-stdio 프로토콜을 참고해, 스트리밍 토큰(부분 응답)을 지원하도록 Response에 청크 타입을 추가하고 메인 앱이 이를 이벤트로 UI에 흘리게 하라. 프로세스 간 통신·백프레셔·스트리밍을 한 번에 다루는 종합 과제.
이 레포를 축으로 4주 커리큘럼.
| 주차 | 주제 | 배울 것 · 실습 |
|---|---|---|
| 1주차 | Rust 비동기 & 오디오 기초 | tokio 태스크·채널, cpal 스트림, 링버퍼 동기화. audio/pipeline.rs·recording_state.rs 정독 |
| 2주차 | 온디바이스 STT | Whisper.cpp 원리, VAD 전처리, GPU 가속(Metal/CUDA), 모델 크기별 속도-정확도. whisper_engine/·transcription/ |
| 3주차 | LLM 통합 & 사이드카 | OpenAI 호환 API·프로바이더 추상화, CancellationToken, JSON-over-stdio 사이드카. summary/·llama-helper/ |
| 4주차 | Tauri 앱 & 배포 | 커맨드/이벤트 IPC, SQLite 리포지토리, Next.js 정적 export, GPU별 빌드 스크립트로 크로스플랫폼 패키징 |
문서에 나온 용어를 한 번에.
| 용어 | 뜻 |
|---|---|
| Tauri | 웹 프런트(HTML/JS) + Rust 코어로 가벼운 데스크톱 앱을 만드는 프레임워크. Electron 대안 |
| STT / 전사(transcription) | 음성을 글자로 바꾸는 것(Speech-to-Text). meetily는 Whisper·Parakeet로 로컬 수행 |
| Whisper.cpp | OpenAI Whisper를 C/C++로 이식해 CPU·GPU에서 가볍게 돌리는 프로젝트. whisper-rs로 Rust에서 사용 |
| Parakeet | NVIDIA가 만든 고속 음성인식 모델. ONNX로 변환돼 ort(ONNX Runtime)로 추론 |
| VAD | 음성 활동 감지. 말이 있는 구간만 골라 전사 부하를 줄인다(silero-rs) |
| 더킹(ducking) | 한 소리가 커지면 다른 소리를 자동으로 눌러 주는 믹싱 기법. RMS 기준으로 적용 |
| EBU R128 | 방송용 음량(라우드니스) 표준. ebur128로 녹음 음량을 균일하게 정규화 |
| RNNoise | 신경망 기반 실시간 잡음 제거. nnnoiseless 크레이트로 적용 |
| 사이드카(sidecar) | 메인 앱과 별개로 띄우는 보조 프로세스. 여기선 로컬 LLM(llama-helper)을 격리해 실행 |
| Ollama | 로컬에서 LLM을 손쉽게 돌리는 런타임. meetily의 기본 로컬 요약 프로바이더 |
| Tauri 커맨드/이벤트 | UI→Rust는 invoke(커맨드), Rust→UI는 emit(이벤트)로 통신하는 양방향 IPC |
| sqlx | Rust의 async SQL 라이브러리. 여기선 SQLite로 회의·전사·요약을 로컬 저장 |
| 데이터 주권(data sovereignty) | 내 데이터를 내가 통제한다는 원칙. meetily의 "로컬 전용"이 지향하는 가치 |
더 파고들 곳.