39장. 실전 ③ 회의록 요약 파이프라인
이 장의 목표 회의 녹음 파일을 던지면 자동으로 받아쓰기 → 요약 → 액션 아이템 까지 나오는 1인용 파이프라인을 만듭니다.
사내망 어디서도 실행 가능합니다.
39.1 파이프라인 흐름
[회의 녹음 .m4a / .mp3 / .wav]
↓
[Whisper STT — 받아쓰기]
↓
[화자 분리 (옵션)]
↓
[청킹 — 30분 단위로 자르기]
↓
[LLM 요약 — 단계별 요약 + 액션 추출]
↓
[Markdown 보고서 출력]
39.2 도구
| 역할 | 도구 |
|---|---|
| STT | Whisper (대용량, 31장) |
| LLM | Ollama + qwen3:32b |
| 화자 분리 (옵션) | pyannote.audio |
| 글루 | Python 스크립트 |
39.3 1단계 — Whisper 받아쓰기
whisper-cpp 또는 mlx-whisper 사용.
$ whisper-cli \
-m models/ggml-large-v3.bin \
-f meeting.m4a \
-l ko \
-ot \ # 텍스트만
-of result.txt
MLX 버전 (맥에서 더 빠름):
$ mlx_whisper meeting.m4a \
--model mlx-community/whisper-large-v3-turbo \
--language ko \
--output-format txt
결과 텍스트:
회의 시작합니다. 오늘은 다음 분기 로드맵에 대해 ...
지난 분기 성과는 ...
39.4 2단계 — 화자 분리 (선택)
여러 명이 발언했을 때 “누가 무엇을 말했는가” 추출.
$ pip install pyannote.audio
Hugging Face 로그인 + 토큰 필요 (모델이 게이트됨).
from pyannote.audio import Pipeline
pipe = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token="hf_..."
)
result = pipe("meeting.m4a")
for segment, _, speaker in result.itertracks(yield_label=True):
print(f"{segment.start:.1f}~{segment.end:.1f} {speaker}")
Whisper 결과와 시간을 맞춰서 합치면:
[김부장] 회의 시작합니다.
[박과장] 지난 분기 성과는...
간단 버전: 화자 분리 없이 그냥 텍스트만 써도 요약은 됩니다.
39.5 3단계 — 긴 회의는 청킹
1시간 회의는 약 10K 토큰. 한 번에 LLM에 넣어도 되지만, 더 긴 회의 는 청킹.
def chunk_text(text, chunk_size=3000, overlap=200):
chunks = []
i = 0
while i < len(text):
chunks.append(text[i:i+chunk_size])
i += chunk_size - overlap
return chunks
각 청크를 따로 요약 후 요약된 청크들을 다시 한 번 통합 (Map-Reduce 패턴).
39.6 4단계 — LLM 요약 프롬프트
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
SYSTEM = """너는 임원 보고용 회의록 요약 비서야.
규칙:
- 한국어 존댓말
- 추측 금지. 명시되지 않은 건 "미정"
- 출처는 회의록 인용에 한정
- 5줄 이내 핵심 요약 + 상세 섹션
"""
USER_TEMPLATE = """다음 회의 받아쓰기를 정리해.
[형식]
## 한 줄 요약
...
## 결정 사항
- ...
## 액션 아이템
- [담당자] 무엇을 / 언제까지
## 보류 사항
- ...
## 다음 회의 안건
- ...
[회의 받아쓰기]
{transcript}
"""
def summarize(transcript):
resp = client.chat.completions.create(
model="qwen3:32b",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": USER_TEMPLATE.format(transcript=transcript)},
],
temperature=0.2,
max_tokens=2000,
)
return resp.choices[0].message.content
39.7 Map-Reduce 요약 (긴 회의)
def map_reduce_summary(transcript):
chunks = chunk_text(transcript)
partials = [summarize(c) for c in chunks]
merged = "\n\n---\n\n".join(partials)
final = summarize_final(merged)
return final
def summarize_final(merged):
# 부분 요약들을 합쳐 최종 임원 보고용
resp = client.chat.completions.create(
model="qwen3:32b",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"다음은 회의의 부분 요약들이야. 이를 종합해서 임원 보고용 최종 요약을 만들어.\n\n{merged}"},
],
temperature=0.2,
)
return resp.choices[0].message.content
39.8 전체 스크립트 (Python)
#!/usr/bin/env python3
"""
meeting2md.py — 회의 녹음을 Markdown 보고서로 변환
"""
import subprocess, sys, os
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
def transcribe(audio_path):
out = audio_path.rsplit(".", 1)[0] + ".txt"
subprocess.run([
"mlx_whisper", audio_path,
"--model", "mlx-community/whisper-large-v3-turbo",
"--language", "ko",
"--output-format", "txt",
"--output-dir", os.path.dirname(out) or ".",
], check=True)
return open(out).read()
def chunk_text(text, size=3000, overlap=200):
chunks, i = [], 0
while i < len(text):
chunks.append(text[i:i+size])
i += size - overlap
return chunks
SYSTEM = """너는 임원 보고용 회의록 요약 비서야.
- 한국어 존댓말
- 추측 금지
- 형식 엄수"""
USER_TPL = """다음 회의 받아쓰기를 정리해.
[형식]
## 한 줄 요약
## 결정 사항
## 액션 아이템 (담당자/기한)
## 보류 사항
## 다음 안건
[받아쓰기]
{t}
"""
def summarize(t):
r = client.chat.completions.create(
model="qwen3:32b",
messages=[
{"role":"system","content":SYSTEM},
{"role":"user","content":USER_TPL.format(t=t)},
],
temperature=0.2,
max_tokens=2000,
)
return r.choices[0].message.content
def main(audio_path):
print(f"[1/3] 받아쓰기: {audio_path}")
text = transcribe(audio_path)
print(f"[2/3] 요약 (LLM)")
if len(text) < 10000:
summary = summarize(text)
else:
partials = [summarize(c) for c in chunk_text(text)]
summary = summarize("\n\n---\n\n".join(partials))
print(f"[3/3] 저장")
out_path = audio_path.rsplit(".", 1)[0] + ".md"
with open(out_path, "w") as f:
f.write(summary)
print(f"완료: {out_path}")
if __name__ == "__main__":
main(sys.argv[1])
사용:
$ python meeting2md.py meeting_2026-05-16.m4a
5분 안에 meeting_2026-05-16.md 가 만들어집니다.
39.9 품질 끌어올리기
화자 정보 포함
요약 프롬프트에 “[김부장], [박과장] 같은 화자 발언을 인용해” 추가.
액션 아이템 강조
액션 아이템은 다음 형식으로:
- [담당자] 액션 (기한: YYYY-MM-DD)
결정 vs 의견 분리
결정 사항: 회의에서 확정된 것만
의견·제안: 논의됐지만 결정 안 된 것
회사 용어 사전
자주 쓰는 약어를 시스템 프롬프트에 미리:
용어:
- B2B: 기업 간 거래
- KPI: 핵심성과지표
- ...
39.10 자동화·통합
매일 자동 처리
launchd 또는 cron 으로
회의 폴더를 감시 → 새 파일 → 자동 변환.
Slack에 자동 게시
요약 완료 시 Slack incoming webhook 호출.
Notion / Confluence에 자동 저장
해당 API로 페이지 생성.
사내 RAG에 자동 추가
38장의 챗봇에 결과를 인덱싱 → 회의 검색 가능.
39.11 보안
녹음 파일은 매우 민감합니다.
- 로컬에서 처리되도록 Whisper도 로컬 모델 사용 ✅
- 외부 API 호출 0
- 결과 파일은 사내 공유 위치에 권한 관리 후 저장
- 원본 음성·텍스트의 보관 기한 정책 설정
이 장에서 기억할 한 가지
회의록 자동화 = Whisper + LLM + Map-Reduce.
1시간 회의 → 5분 안에 보고서. 다 로컬에서, 데이터 한 톨도 외부로 안 나갑니다.
손으로 해볼 것
1. 39.8 절 스크립트 그대로 실행
본인의 5~10분 음성 메모로 테스트.
2. 회의록 형식 커스터마이즈
회사 보고 양식에 맞게 USER_TPL을 수정하고 실제 회의 1건으로 결과 비교.
다음 장에서는 모델 테스트 루틴 만들기 — 새 모델이 나올 때마다 흔들리지 않게 비교하는 법을 정리합니다.