Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

39장. 실전 ③ 회의록 요약 파이프라인

이 장의 목표 회의 녹음 파일을 던지면 자동으로 받아쓰기 → 요약 → 액션 아이템 까지 나오는 1인용 파이프라인을 만듭니다.

사내망 어디서도 실행 가능합니다.


39.1 파이프라인 흐름

[회의 녹음 .m4a / .mp3 / .wav]
        ↓
[Whisper STT — 받아쓰기]
        ↓
[화자 분리 (옵션)]
        ↓
[청킹 — 30분 단위로 자르기]
        ↓
[LLM 요약 — 단계별 요약 + 액션 추출]
        ↓
[Markdown 보고서 출력]

39.2 도구

역할도구
STTWhisper (대용량, 31장)
LLMOllama + 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건으로 결과 비교.


다음 장에서는 모델 테스트 루틴 만들기 — 새 모델이 나올 때마다 흔들리지 않게 비교하는 법을 정리합니다.