
회의록 앱을 하루 만에 직접 만든 이야기
요약
기존 회의록 서비스의 비용 부담을 해결하기 위해 Claude Code를 활용하여 맞춤형 회의록 자동화 앱을 하루 만에 구축한 사례를 소개합니다. 로컬 whisper.cpp로 텍스트 변환을 무료화하고 Claude API로 요약하는 효율적인 파이프라인을 구현했습니다.
핵심 포인트
- Claude Code를 활용한 설계서 기반의 빠른 구현 프로세스
- whisper.cpp를 이용한 로컬 텍스트 변환으로 비용 절감
- Claude API를 통한 종량제 방식의 효율적인 요약 시스템
- 녹음 앱과 파이프라인 간의 느슨한 결합(Loosely coupled) 구조
이 기사는 riku-kobayashi.jp에 게재한 기사를 전재한 것입니다.
온라인 회의를 할 때마다 회의록을 수기로 작성하는 것이 번거로워, Notta나 Notion AI와 같은 미팅 노트 서비스도 검토했습니다. 다만, 개인적으로 사용하기에는 요금 플랜이 비싼 데다, 텍스트 변환(Transcription) 시간이나 녹음 횟수에 제한이 있는 경우가 많아 제 사용 방식에는 맞지 않았습니다. 그렇다면 같은 경험을 나만의 전용으로 만들어 버리자고 생각했습니다. 요구사항은 다음 두 가지입니다.
- 텍스트 변환은 무료 · 로컬(Local) 완결
- 요약만 Claude API에 던져서, Markdown과 Google Docs에 회의록으로 남기기
Claude Code에게 설계서와 구현 계획을 쓰게 한 뒤 구현하게 하는, 평소의 방식으로 진행했더니 녹음 · 텍스트 변환 · 요약까지 한 차례 동작하는 단계까지 거의 하루 만에 도달했습니다. 이 기사에서는 그 내용을 되돌아봅니다.
애초의 동기가 기존 서비스는 개인 용도로는 비싸고 제한이 있다는 것이었기에, 실제로 어느 정도 차이가 있는지 비교해 보겠습니다. 월 20회 · 1시간씩 회의가 있다고 가정합니다.
| 서비스 | 요금 | 월 20회 회의(20시간) 시 실질 비용 | 1시간당 |
|---|---|---|---|
| Notta 프리미엄 (연결제) | 월 약 1,185엔 | 정액 1,185엔 (텍스트 변환 월 1,800분 범위 내) | 약 40엔 (30시간 프레임을 다 사용했을 경우) |
| ... | |||
| 텍스트 변환을 로컬의 whisper.cpp로 무료화하고, 비용이 발생하는 요약만 Claude API에 종량제로 맡김으로써, 월 20회 회의라도 수백 엔 이내로 억제하고 있습니다. Notta나 Notion AI는 월정액이므로 사용하지 않는 달에도 동일한 금액이 들지만, 직접 만들면 사용한 만큼만 지불합니다. 개인 용도로 회의가 가끔 있는 달일수록 차이가 벌어지는 계산입니다. |
최종적인 구성은 다음과 같습니다.
녹음 앱(meeting-recorder)과 회의록 파이프라인(minutes-pipeline)은 파일 시스템을 통해 느슨하게 결합(Loosely coupled)되어 있습니다. 먼저 Python 파이프라인 단독을 수동으로 inbox/에 둔 음성 파일로 동작할 수 있는 상태로 만들고, 그것이 완료된 후 Swift 녹음 앱을 만들어 자동화하는 순서로 진행했습니다. 녹음 측의 결함이 회의록 처리에 파급되지 않는다(그 반대도 마찬가지)는 장점도 있습니다.
먼저 설계서를 작성하고, 이를 구현 계획(태스크 분할)으로 떨어뜨린 다음, Claude Code에게 구현을 시켰습니다.
- 설계서 → 구현 계획 작성
- scaffold → 텍스트 변환 → 병합(Merge) → 요약 → Markdown 생성 → 오케스트레이터(Orchestrator) → CLI → Google Docs 연동 → 알림/감시 데몬 구현
- 실제로 자신의 목소리를 녹음하여 파이프라인에 통과시키고, 발견된 결함 수정, README 정비
설계서와 구현 계획이 태스크 단위로 세밀하게 나누어져 있었기에, Claude Code가 망설임 없이 순서대로 구현을 진행할 수 있었던 점이 컸다고 생각합니다. 구현부터 동작 확인 · 버그 수정 · README까지 포함한 실질적인 작업 시간은 거의 하루 분량 안에 들어왔습니다.
패키지 전체는 11개 파일 · 556행이라는 컴팩트한 구성입니다.
pipeline/src/minutes_pipeline/
├── config.py # config.toml 읽기
├── transcribe.py # whisper-cli 호출
...
상대방의 개인 식별(화자 분리, Speaker Diarization)까지는 하지 않고, 「나」와 「상대」의 2구분으로 타협했습니다. 마이크 음성과 시스템 음성을 각각 whisper.cpp에 적용하고, 세그먼트를 타임스탬프 순으로 다시 정렬할 뿐입니다.
def merge(mine: list[Segment], theirs: list[Segment]) -> list[Utterance]:
tagged = [("自分", s) for s in mine] + [("相手", s) for s in theirs]
tagged.sort(key=lambda t: t[1].start)
...
pyannote 등을 이용한 화자 분리도 검토했지만, 개인 용도에서는 나인지 상대인지만 알 수 있으면 충분하므로 설계 단계에서 스코프(Scope) 외로 설정했습니다.
요약 · 결정 사항 · TODO · 상세 내용을 파싱하는 코드를 직접 쓰고 싶지 않았기에, Anthropic SDK의 messages.parse와 Pydantic 모델을 사용하여 그대로 구조화된 출력(Structured Output)을 받도록 했습니다.
class MeetingSummary(BaseModel):
title: str
summary: str
...
시스템 프롬프트에는 TODO의 담당자·기한은 읽어낼 수 있는 경우에만 설정하고, 추측으로 채우지 말 것, 결정 사항은 확정된 것만 작성할 것과 같은 할루시네이션 (Hallucination) 방지 지시를 넣었습니다.
회의 중에는 긴 시간 동안 녹음하기 때문에, 처리 도중에 실패하더라도 녹음을 다시 하게 만들고 싶지 않았습니다. process.py에서는 state.json에 각 단계의 완료 상태를 기록하며 진행하고, 예외 발생 시에는 failed/ 디렉토리로 이동하여 그곳에서 재실행할 수 있도록 했습니다.
if not state["transcript_done"]:
...
state["transcript_done"] = True
...
텍스트 변환 (Transcription)이 완료되었음에도 요약만 실패한 경우, failed/에서 재실행하면 텍스트 변환을 다시 하지 않고 요약부터 리트라이 (Retry)됩니다.
Python 파이프라인의 동작을 수동 음성 파일로 확인할 수 있게 된 후에는, 다음으로 녹음 자체를 자동화하는 메뉴바 상주 앱을 만들었습니다.
Zoom이나 Google Meet를 전용으로 감지하는 API가 있는 것은 아니므로, 마이크가 자신 이외의 프로세스에 사용되기 시작했음을 CoreAudio의 프로세스 단위 API (kAudioHardwarePropertyProcessObjectList / kAudioProcessPropertyIsRunningInput)로 감지합니다. 감지하면 "회의를 감지했습니다"라는 알림을 띄우고, 클릭하면 녹음을 시작합니다. 오감지 (보이스 메모나 음성 입력 등)를 피하기 위해, 감지했을 뿐으로는 자동 녹음을 하지 않습니다.
static func isMicInUseByOtherProcesses() -> Bool {
// ... 프로세스 목록을 가져옴 ...
for process in processes {
...
상대방의 목소리 (시스템 전체의 오디오)를 녹음하는 방법으로, 처음에는 ScreenCaptureKit을 검토했습니다. 하지만 이것은 화면 기록 권한이 필요하며, 화면을 녹화하는 것도 아닌데 매번 그 허가를 요구하는 것은 사용자 입장에서 유쾌하지 않습니다. 그래서 오디오만 추출할 수 있는 Core Audio Process Tap API (AudioHardwareCreateProcessTap, macOS 14.4 이상)로 변경했습니다. 이것은 시스템 오디오 녹음이라는 전용의 가벼운 권한만 있으면 됩니다.
하는 일은 단순합니다. 대상 앱을 한정하지 않는 **글로벌 탭 (Global Tap)**을 만들면, Zoom이든 Meet이든 상관없이 Mac 전체의 시스템 오디오 (=상대방의 목소리를 포함)를 하나의 오디오 스트림으로 받을 수 있습니다. 자신의 목소리는 별도로 AVAudioEngine을 통해 마이크로부터 녹음하며, 최종적으로 두 개의 오디오 파일을 각각 whisper.cpp가 다루기 쉬운 16kHz·모노(Mono) WAV로 변환합니다.
"탭 (Tap)"은 특정 프로세스가 재생하려고 하는 오디오를 가로채서 받는 메커니즘입니다. Zoom 등 특정 앱의 오디오만 노릴 수도 있고, 제외 대상을 지정하지 않음으로써 Mac상의 모든 프로세스의 오디오를 한꺼번에 받을 수도 있습니다. 이번에는 후자인 "글로벌 탭"을 사용함으로써, 회의 앱의 종류를 불문하고 상대방의 목소리를 잡을 수 있도록 했습니다 (코드상으로는 CATapDescription(stereoGlobalTapButExcludeProcesses: [])와 같이 제외 프로세스를 비워두어 생성합니다).
녹음 시작 후에는 마이크 사용 상황을 일정 간격으로 폴링 (Polling)하며, 다른 프로세스가 마이크를 사용하지 않게 되면 자동으로 녹음을 종료합니다 (AutoStopMonitor). 당초에는 디바이스 레벨의 알림만으로 판정하려고 했으나, 자신이 녹음 중일 때는 해당 이벤트가 발생하지 않기 때문에 폴링 방식으로 변경했습니다.
종료 시에는 mic.wav / system.wav / meta.json을 저장 위치 폴더 (기본값은 ~/Meetings) 하위의 inbox/<일시>/로 내보냅니다. 저장 위치는 Settings 화면에서 변경 가능합니다. meta.json을 마지막에 작성함으로써, Python 파이프라인 측에 파일이 모두 갖춰졌다는 신호로 사용하고 있습니다.
수동으로 mic.wav / system.wav / meta.json
에 넣어 minutes process를 실행하면, 몇 분만 기다리면 notes/에 회의록 Markdown 파일이 생성됩니다. 녹음 앱이 완성된 후에는 회의를 감지한 알림을 클릭하는 것만으로 여기까지 자동으로 진행됩니다.
~/Meetings/notes/2026-07-07_定例MTG.md
요약, 결정 사항, TODO, 전체 전사(Transcription) 내용이 한 파일에 모두 담겨 있어, 상상했던 그대로의 경험이 되었습니다.
그 후에도 배포용 dmg 패키지화나 회의록 목록 GUI 등, 사용하면서 신경 쓰이는 부분들을 계속 만들어가고 있지만, 핵심인 녹음 → 전사 (Transcription) → 요약 → 저장이라는 일련의 흐름은 이 하루 만에 일단 작동하는 형태로 완성되었습니다.
최근 "SaaS는 끝났다"라는 담론을 자주 접하곤 하는데, 이번에 그것을 실감하는 경험을 했습니다. Notta나 Notion AI와 같은 SaaS는 개발 비용을 불특정 다수의 사용자가 나누어 부담함으로써 월간 구독료 형태로 유지됩니다. 뒤집어 말하면, 오직 나 한 사람의 요구사항에만 집중하여 AI 에이전트에게 구현하게 할 수 있다면, 그 비용 분담분은 통째로 필요 없어진다는 뜻입니다. 이번 사례의 경우, 화자 분리(Speaker Diarization)도 실시간 표시도 필요 없이, 나와 상대방이라는 2개 구분으로 Markdown에 남길 수 있으면 충분했습니다. 이처럼 요구사항을 좁혔기에 Claude Code를 통해 거의 하루 만에 완성할 수 있었습니다.
물론 유지보수나 에지 케이스(Edge Case) 대응은 본인의 책임이며, 누군가에게 배포하여 사용하게 한다면 이야기는 달라집니다. 그럼에도 개인이 자신만을 위해 사용하는 도구인 한, 원하는 기능만을 최단 거리로 갖춘 소프트웨어를 직접 만드는 것이, 범용 SaaS에 맞춰 사용법을 타협하는 것보다 더 빠르고 저렴한 상황은 앞으로 더욱 많아질 것이라고 느낍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기