핸드폰으로 녹음한 회의 음성 파일을 업로드하면, AI가 화자 구분·텍스트 변환·요약을 도와 회의록 초안을 만들고, 필요하면 게시판 등록까지 보조하는 도구입니다.
모든 처리는 로컬 PC에서 일어납니다 — 음성/텍스트가 외부 클라우드로 나가지 않고, 비용도 0원.
Windows에서 처음 설치한다면 아래 절차를 먼저 권장합니다. .env 생성, 전용 venv 생성, Python 패키지 설치, ffmpeg/Ollama 확인, 모델 다운로드, doctor.py 진단까지 순서대로 안내합니다.
git clone https://github.com/thisgun/meeting_record.git
cd meeting_record
setup.bat모델 다운로드를 나중에 하고 싶으면 setup.bat --skip-models를 사용하세요. 이미 직접 설치하고 싶거나 macOS/Linux라면 아래 수동 절차를 그대로 따라가면 됩니다.
git clone https://github.com/thisgun/meeting_record.git
cd meeting_record
# 전용 가상환경 생성·활성화 (권장 — 전역 파이썬 오염/버전 충돌 방지)
python -m venv .venv-meetingrec
.\.venv-meetingrec\Scripts\Activate.ps1 # PowerShell
# .venv-meetingrec\Scripts\activate.bat # cmd(명령 프롬프트)일 경우 이 줄
Copy-Item .env.example .env # 환경 변수 파일 생성 (cmd: copy / macOS·Linux: cp .env.example .env)
pip install -r requirements.txt
pip install -e . --no-deps # 선택: meeting-record 같은 CLI 명령 등록
ollama pull gemma4:e2b-it-qat # 기본 요약 모델 (Q4, 4.3GB · 8GB GPU 100% 적재)
python scripts/download_models.py # AI 모델 사전 다운로드 (~6GB)
python doctor.py # 시스템 진단
python main.py "회의.mp3" --no-upload # 첫 실행: G5 없이 로컬 저장만💡 가상환경을 꼭 쓰세요. 이 프로젝트는
torch·faster-whisper·pyannote.audio등 무거운 패키지를 최신 버전으로 끌어옵니다. 전역 파이썬에 바로 설치하면open-webui처럼 버전을 고정해 쓰는 다른 앱과 충돌(numpy,faster-whisper등)이 납니다. 전용 venv로 격리하면 안전합니다. 이후 작업할 때마다 새 터미널에서 먼저 활성화하세요.셸별 활성화 명령 (사용하는 터미널에 맞게):
OS / 셸 활성화 Windows · PowerShell .\.venv-meetingrec\Scripts\Activate.ps1Windows · cmd(명령 프롬프트) .venv-meetingrec\Scripts\activate.batWindows · Git Bash source .venv-meetingrec/Scripts/activatemacOS · Linux (bash/zsh) source .venv-meetingrec/bin/activate활성화되면 프롬프트 앞에
(.venv-meetingrec)가 붙습니다. 위 빠른 시작 블록은 Windows(PowerShell) 기준입니다 — macOS·Linux에서는 venv 경로가Scripts/가 아니라bin/이고, 파일 복사는cp .env.example .env를 쓰세요. 나머지 (python -m venv·pip install·ollama pull·python ...)는 모든 OS에서 동일합니다.
- cmd에서
Activate.ps1을 실행하면 메모장이 열립니다 —.ps1은 PowerShell 전용이라 cmd에선 파일이 "열기"만 됩니다. cmd에선 위activate.bat을 쓰세요.- PowerShell에서 "실행 정책" 오류가 나면 한 번만:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
Releases 페이지에서 g5_meeting_api-vX.Y.Z.zip 다운로드 → 압축 풀어서 안의 plugin/meeting_api/ 폴더를 그누보드5의 plugin/ 안에 FTP 업로드. 자세한 절차는 아래 그누보드5 plugin 표준 배치 섹션.
python -m streamlit run app.py브라우저에서 http://localhost:8501 — 회의 목록/검색/비교/편집/Export 모두 GUI.
-
Python 3.10 ~ 3.14 (python.org) — 3.9 이하 불가. 최신 3.14까지 동작합니다.
-
ffmpeg — 오디오 변환에 필수. OS별 설치:
OS 설치 명령 Windows winget install Gyan.FFmpeg --source wingetmacOS brew install ffmpegLinux sudo apt install ffmpeg(또는 배포판 패키지 매니저)⚠️ 설치 후 반드시 새 터미널을 여세요. PATH는 새 셸부터 반영되므로, 설치한 같은 창에서 바로 실행하면FFmpegNotFoundError가 그대로 납니다. (winget 사용자가 가장 자주 겪는 함정) -
Ollama + 모델 (예:
ollama pull gemma4:e2b-it-qat) -
그누보드5 + XAMPP/cafe24 (로컬 또는 원격 호스팅)
python doctor.py 로 모든 의존성을 한 번에 점검 가능. (ffmpeg가 PATH에 없어도 winget/choco/scoop 표준 설치 위치를 자동 탐색하므로, 새 터미널을 못 연 경우에도 대개 동작합니다.)
상세 매뉴얼
- 이게 뭐 하는 프로그램인가요? — 사용 시나리오 · 제작 배경
- 전체 시스템 구조
- 디렉토리 구조 상세 — 모듈 3계층 import 규칙
- 데이터 흐름 (실행 순서)
- 핵심 용어 정리
- 환경 구성 — 요구사항 · AI 모델 ·
.env환경변수 - 실행 방법 — CLI 옵션 · pyannote 화자분리 · 품질 게이트 · 처리 시간 · GPU(CUDA) 가속
- 자주 발생하는 문제 (트러블슈팅) — ffmpeg · Ollama · 요약 잘림 · 업로드 등 11건
- 운영 시 점검 사항
회의를 마치고 이런 반복 작업을 줄이고 싶다고 상상해보세요:
- 회의 중에 핸드폰으로 녹음 버튼만 누름
- 녹음 끝나고 PC에 음성 파일을 옮김
- 명령어 한 번 실행
- 도구가 회의록 초안 작성을 보조:
- 누가 어떤 말을 했는지 화자별로 구분
- 음성을 한국어 텍스트로 변환
- 회의 제목, 개요, 결정 사항, 액션 아이템으로 요약
- 로컬 데이터베이스에 저장
- 설정한 경우 사내 게시판(그누보드5)에 등록
이 모든 과정을 한 번의 명령으로 처리하되, 결과는 검수 가능한 회의록 초안으로 보는 것을 권장합니다.
python main.py "산업안전_회의.mp3" 한 줄을 실행하면, 이런 회의록 초안이 생성되어 SQLite에
저장되고, 업로드 설정이 되어 있으면 그누보드5 게시판 등록까지 진행됩니다 (실제 출력 형식):
# 산업안전 강화 및 중대재해 감축 방안 논의
## 개요
대규모 행사 안전, 중대재해 감축, 산재 은폐 의혹 조사를 의제로 진행. 태양광 설치 추락·
지게차 사고 사례를 분석하고 재발 방지 대책(AI 예방 시스템·드론 모니터링·다국어 교육)을 논의.
## 주요 논의
- 위험 격차 해소를 위한 소규모 사업장 밀착 점검 강화
- 지붕·태양광 작업 시 견고한 덮개 설치 의무, 개·보수 단계 중점 관리
- 안전한 일터 지킴이 인력 활용한 상시 순찰 체계
## 결정 사항
- 이동통로와 지게차 주행로를 난간으로 물리 분리, 건널목 설치
- 다국적 언어 교육 교재 번역·더빙 완료, 국적별 안전 리더 지정
---
*오디오 1시간 46분 50초 · 처리 10분 8초 · 화자 2명 · 발화 125건 · 모델 Whisper large-v3*발화 하나하나는 게시글 댓글로 화자·시간과 함께 등록됩니다 (예: [02:41] 사용자1: 지금부터 회의를 시작하겠습니다…). 본문 맨 아래 한 줄은 처리 정보 푸터로, .env의
POST_PROCESSING_FOOTER=0으로 끌 수 있습니다.
📷 스크린샷을 추가하려면: 웹 UI나 그누보드5 게시글 화면을 캡처해
docs/img/에 넣고 여기에으로 링크하세요. (이 저장소엔 기본 이미지를 포함하지 않습니다.)
- 회의록 작성 시간 절약: 초안을 먼저 만들고 사람이 검수·보완
- 검색 가능한 기록: 모든 회의가 DB와 게시판에 누적됨
- 프라이버시: 모든 처리가 로컬 PC에서 수행됨 (음성/텍스트가 외부 클라우드로 안 나감)
- 비용 0원: 오픈소스 + 로컬 LLM (Ollama)만 사용
이 프로젝트는 3개의 독립된 구성 요소가 협력합니다.
┌────────────────────────────────────────────────────────────────┐
│ ① Python 파이프라인 (meeting_record/) │
│ │
│ 음성파일.mp3 → ffmpeg → faster-whisper → speechbrain → Ollama│
│ ↓ ↓ │
│ SQLite 저장 ───────────────────────────────────────► │
│ │ │
└──────────────────────────────────────────────────────┼─────────┘
│ HTTP
▼
┌──────────────────────────────────────────────────────┬─────────┐
│ ② PHP REST API (g5_meeting_api/plugin/meeting_api) │ │
│ │ │
│ /health.php /post.php /comment.php /update_*.php │
│ /list_comments.php /delete_post.php ◄────────────┘ │
│ │ │
│ │ 그누보드5 common.php 로드 │
│ ▼ │
├────────────────────────────────────────────────────────────────┤
│ ③ 그누보드5 (<그누보드5루트>/) ← 원본 무수정 │
│ │
│ PHP 게시판 엔진 + MariaDB (XAMPP) │
│ "meeting" 게시판에 게시글 + 댓글 등록 │
└────────────────────────────────────────────────────────────────┘
- ① Python 파이프라인 — AI/ML 라이브러리(PyTorch, Whisper)를 쓰려면 Python이 필수
- ② PHP REST API — 그누보드5는 PHP 기반이라 PHP에서 호출하는 게 가장 안전. 원본을 건드리지 않고 별도 폴더에서 외부 API 제공
- ③ 그누보드5 원본 — 사용자가 원본 코드를 수정하지 않기로 요구. 향후 그누보드5 업그레이드 시 충돌 없음
각 구성 요소는 독립적으로 교체 가능합니다. 예: 그누보드5 대신 워드프레스를 쓰고 싶으면 ②만 다시 만들면 됨.
meeting_record/
│
├── main.py # CLI 진입점 (그 외: doctor.py, app.py, watcher.py, export.py, enroll.py)
├── setup.bat # Windows 초보자용 설치 도우미
├── config.py # .env 파일 로드 및 검증
├── meeting_record/ # 공통 유틸 패키지 (console UTF-8 stdio, cli)
├── requirements.txt # Python 패키지 목록
├── .env # 환경 변수 (사용자 비밀값, git 무시)
├── .env.example # .env 작성 가이드
│
├── src/ # 핵심 모듈
│ ├── audio.py # 오디오 파일을 16kHz WAV로 변환
│ ├── transcriber.py # 음성 → 텍스트 + 화자 분리
│ ├── diarizer_local.py # 로컬 화자 분리 (HF 토큰 불필요)
│ ├── summarizer/ # 요약 패키지 (prompts/parsing/chunking/ollama_io/sections)
│ ├── storage/ # SQLite 저장/조회 패키지 (db/schema/meetings/sync/search)
│ └── g5_client.py # 그누보드5 API 호출
│
├── scripts/
│ ├── setup_windows.ps1 # setup.bat 내부 설치 로직
│ └── download_models.py # AI 모델 사전 다운로드
│
├── data/ # 데이터 저장소 (git 무시)
│ ├── meetings.db # SQLite 데이터베이스
│ ├── uploads/ # 원본 음성 파일 백업
│ ├── work/ # 변환된 WAV + 발화 캐시(JSON)
│ └── models/ # 다운로드한 AI 모델
│
├── g5_meeting_api/ # ② PHP REST API 배포 패키지
│ ├── README.md
│ └── plugin/meeting_api/
│ ├── _bootstrap.php # 공통 헬퍼: 인증, JSON 응답
│ ├── _load_gnuboard5.php # 그누보드5 common.php 로드 (트릭 포함)
│ ├── config.php # API 토큰, bo_table 설정
│ ├── health.php # GET: 헬스 체크
│ ├── post.php # POST: 회의 요약을 게시글로 등록
│ ├── comment.php # POST: 발화 1건을 댓글로 등록
│ ├── update_post.php # POST: 게시글 제목/본문 수정
│ ├── list_comments.php # POST: 게시글 댓글 목록 조회
│ ├── update_comment.php # POST: 댓글 본문/작성자 수정
│ ├── delete_post.php # POST: 게시글과 댓글 삭제
│ ├── cleanup_tests.php # POST: 오래된 연결 테스트 글 정리
│ ├── setup_board.php # POST: 게시판 자동 생성
│ └── setup_member.php # POST: 회의록봇 회원 계정 자동 생성
│
└── <그누보드5루트>/ # ③ 별도 설치 위치. 저장소에는 포함되지 않음
└── plugin/meeting_api/ # 위 배포 패키지를 복사해 배치
이 저장소는 루트 진입점 + src 구현 패키지 + meeting_record 공통 패키지의 3계층을 의도적으로 유지합니다. (전면 패키지화는 현재 목표가 아니며 보류 상태 — 진입점을 python main.py로 단순 실행하기 위함)
| 계층 | 위치 | import | 역할 |
|---|---|---|---|
| 진입점 | 루트 (main.py·doctor.py·app.py·watcher.py·export.py·enroll.py) |
python <파일>.py 로 실행 |
CLI·웹UI·폴더감시 |
| 설정 | 루트 config.py |
from config import load_config |
.env 로드·검증 |
| 공통 유틸 | meeting_record/ |
from meeting_record.X import ... |
console UTF-8, cli 등 횡단 관심사 |
| 기능 모듈 | src/ |
from src.X import ... |
audio·transcriber·summarizer·storage·g5_client 등 |
규칙: 새 코드는 위 패턴을 따릅니다.
src모듈끼리도from src.X import ...(절대 import) 사용.- 서브패키지 내부(예:
src/summarizer/)에서만 상대 import(from .parsing import ...). config·meeting_record는 진입점에서만 직접 import (기능 모듈은 설정을 인자로 받음).
XAMPP Apache는 기본적으로 C:\xampp\htdocs\ 폴더를 서비스합니다. 로컬 테스트에서 그누보드5를 C:\xampp\htdocs\gnuboard5\에 설치했다면, 이 저장소의 g5_meeting_api\plugin\meeting_api\ 폴더를 아래 위치로 복사합니다:
C:\xampp\htdocs\gnuboard5\plugin\meeting_api\
원격 서버에서도 같은 원칙입니다. ZIP 안의 plugin/meeting_api/ 폴더를 그누보드5의 plugin/ 폴더 안에 업로드하면 됩니다.
python main.py "회의.mp3" 명령을 실행하면 다음 단계로 흘러갑니다:
┌─────────────────────────────────────────────────────────────────┐
│ [1/6] 오디오 변환 │
│ audio.py가 ffmpeg를 호출 │
│ 회의.mp3 → 16kHz mono WAV (음성인식에 최적화된 포맷) │
│ data\uploads\ 에 원본 백업, data\work\<uuid>.wav 생성 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ [2/6] 음성 인식 + 화자 분리 │
│ transcriber.py: faster-whisper(small) → 한국어 텍스트 변환 │
│ + 내장 word_timestamps로 단어별 시작/끝 시간 산출 │
│ + diarizer_local.py: speechbrain으로 화자 임베딩 추출 │
│ + scikit-learn 클러스터링으로 화자 N명 자동 추정 │
│ │
│ 결과: [{"start":0.5, "end":2.3, "speaker":"사용자1", │
│ "text":"안녕하세요"}, ...] │
│ │
│ ★ 이 결과를 data\work\회의.<파일지문>.segments.json 에 캐시 저장 │
│ (다음 실행 시 STT 안 다시 돌림) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ [3/6] 회의 요약 │
│ summarizer/: Ollama gemma4:e2b-it-qat 호출 │
│ 프롬프트: "다음 회의를 정리해주세요" + 발화 전체 │
│ 출력: JSON {"title": "...", "summary_md": "## 개요 ..."} │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ [4/6] 로컬 DB 저장 │
│ storage/: SQLite (data\meetings.db) │
│ - meetings 테이블: 회의 제목, 요약, 길이, 동기화 상태 │
│ - utterances 테이블: 발화 23건 각각 (화자, 시간, 텍스트) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ [5/6] 요약 미리보기 (화면 출력) │
│ 제목, 마크다운 요약을 콘솔에 표시 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ [6/6] 그누보드5 업로드 │
│ g5_client.py가 HTTP POST │
│ → /plugin/meeting_api/post.php │
│ (게시글 1개 = 회의 요약) │
│ → /plugin/meeting_api/comment.php │
│ (댓글 23개 = 발화 각각 "[03:15] 사용자1: ...") │
│ │
│ PHP API → common.php 로드 → MariaDB에 직접 INSERT │
│ → g5_board / g5_write_meeting 테이블 갱신 │
└─────────────────────────────────────────────────────────────────┘
↓
✓ 완료
http://127.0.0.1/gnuboard5/bbs/board.php?bo_table=meeting
에서 결과 확인
| 용어 | 설명 |
|---|---|
| STT | Speech-to-Text. 음성을 텍스트로 변환하는 기술 |
| Whisper | OpenAI가 공개한 다국어 음성 인식 모델. 한국어 정확도 우수 |
| faster-whisper | Whisper를 CTranslate2로 가속한 구현. 단어 단위 타임스탬프 제공 (Python 3.9~3.14) |
| Diarization | 화자 분리. "누가 언제 말했는지" 구분하는 기술 |
| pyannote | 화자 분리의 사실상 표준 라이브러리. HuggingFace 토큰 필요 |
| speechbrain | 음성 임베딩 라이브러리. 우리는 ECAPA-TDNN 모델로 화자 임베딩 추출 |
| ECAPA-TDNN | 사람 목소리를 192차원 벡터로 변환하는 신경망. 비슷한 목소리는 비슷한 벡터 |
| 클러스터링 | 비슷한 벡터끼리 묶기. 발화별 음성 벡터를 묶어 화자 그룹 형성 |
| LLM | Large Language Model. 텍스트 입출력 AI. 우리는 요약에 사용 |
| Ollama | 로컬에서 LLM을 실행하는 도구. 외부 API 호출 없이 PC 안에서 돌아감 |
| gemma4:e2b-it-qat | Google Gemma 4 효율 모델의 Q4 양자화판. 4.3GB. 8GB GPU에 100% 적재, 한국어 요약 품질·속도 균형 (기본값) |
| 그누보드5 | 한국에서 인기 있는 무료 PHP 게시판 엔진 |
| XAMPP | Apache + MariaDB + PHP를 한 번에 설치해주는 패키지 |
| MariaDB | MySQL의 오픈소스 포크. 호환됨 |
| bo_table | 그누보드5에서 게시판 한 개를 식별하는 코드. 우리는 "meeting" 사용 |
| wr_id | 그누보드5 게시글 또는 댓글의 고유 ID |
| wr_is_comment | 0이면 게시글, 1이면 댓글 (같은 테이블 사용) |
| 구성 요소 | 요구 / 권장 |
|---|---|
| OS | Windows 10·11 · macOS · Linux (모두 지원) |
| Python | 3.10 이상 (3.11~3.13 권장) |
| ffmpeg | 필수 — Windows winget install Gyan.FFmpeg · macOS brew install ffmpeg · Linux apt install ffmpeg |
| Ollama | 최신 버전 권장 (로컬 요약 LLM 실행) |
| 요약 모델 | gemma4:e2b-it-qat (4.3GB, 기본) — ollama pull gemma4:e2b-it-qat |
| GPU (선택) | NVIDIA + CUDA면 수십 배 가속. VRAM 8GB면 기본 모델이 100% GPU 적재 |
| 그누보드5 (선택) | 게시판 연동/등록 보조 시. XAMPP·cafe24 등 PHP 호스팅 (없으면 --no-upload) |
📌 위 표는 요구사항이며 특정 버전 강제가 아닙니다. 개발·검증 환경(참고): Windows 11 · Python 3.11 · RTX 4060 8GB(CUDA 12.8) · XAMPP(PHP 8.2, MariaDB 10.4). 다른 OS·GPU·CPU 전용 환경에서도 동작하도록 설계됐습니다(
python doctor.py로 내 환경을 진단하세요).
faster-whisper # Whisper STT 엔진 (단어 타임스탬프 포함, Python 3.9~3.14)
pyannote.audio # 고정밀 화자 분리 (선택, HF 토큰 필요 / Python 3.13+는 자동 설치 제외)
torch # 딥러닝 프레임워크 (CPU 버전)
torchaudio # 오디오 처리
speechbrain # 로컬 화자 임베딩 (HF 토큰 불필요)
scikit-learn # 클러스터링
soundfile # WAV 파일 입출력
ollama # Ollama Python 클라이언트
requests # HTTP 호출 (그누보드5 API)
python-dotenv # .env 파일 로드
pydub # 오디오 메타데이터
psutil # RAM/VRAM 진단 및 저메모리 안전장치
설치 명령:
cd meeting_record
# 전용 가상환경 생성·활성화 (최초 1회)
python -m venv .venv-meetingrec
.\.venv-meetingrec\Scripts\Activate.ps1 # PowerShell
# .venv-meetingrec\Scripts\activate.bat # cmd일 경우 이 줄
pip install -r requirements.txt
pip install -e . --no-deps # 선택: meeting-record 같은 CLI 명령 등록활성화된 venv 안에서는 프롬프트 앞에
(.venv-meetingrec)가 표시됩니다. 새 터미널을 열 때마다 다시 활성화해야 합니다 — 셸별 명령은 위 빠른 시작의 활성화 표를 참고하세요. (cmd에서Activate.ps1을 실행하면 메모장이 열리니activate.bat을 쓰고, PowerShell 실행정책 오류가 나면Set-ExecutionPolicy -Scope CurrentUser RemoteSigned를 1회 실행)
| 모델 | 용도 | 크기 | 다운로드 위치 |
|---|---|---|---|
| faster-whisper-small | 한국어 음성 → 텍스트 (단어 타임스탬프 포함) | 500MB | data\models\faster-whisper-small\ |
| spkrec-ecapa-voxceleb | 화자 임베딩 | 85MB | data\models\spkrec-ecapa-voxceleb\ |
왜 사전 다운로드? Windows에서 HuggingFace는 기본적으로 캐시에 심볼릭 링크를 만드는데, 일반 사용자 권한으로는 실패합니다. 그래서
scripts/download_models.py가 우리 프로젝트 폴더에 직접 다운로드하도록 해두었습니다.
다운로드 명령:
python scripts\download_models.py# HuggingFace 토큰 (선택, 무료 — 결제 아닌 인증용)
# 비워두면 로컬 화자 분리 (speechbrain) 자동 사용
# 채우면 더 정확한 pyannote 사용. 발급/약관동의 방법은 위 "정밀 화자 분리 — pyannote" 절 참고.
HUGGINGFACE_TOKEN=
# Ollama 설정
OLLAMA_HOST=http://127.0.0.1:11434
OLLAMA_MODEL=gemma4:e2b-it-qat
OLLAMA_KEEP_ALIVE=0
OLLAMA_NUM_CTX_MAX=32768
OLLAMA_NUM_PREDICT=8192
OLLAMA_NUM_GPU=
OLLAMA_TIMEOUT_SEC=300
OLLAMA_SUMMARY_CHUNK_SEC=900
OLLAMA_MIN_FREE_RAM_GB=4
OLLAMA_MEMORY_WAIT_SEC=30
# Whisper 설정
WHISPER_MODEL=small # tiny/base/small/medium/large-v3
WHISPER_COMPUTE_TYPE=int8 # CPU에서는 int8 권장
WHISPER_LANGUAGE=ko
TYPO_CORRECTION=1 # STT 후 사전/.env 오타 보정 적용
TYPO_CORRECTION_RULES= # 예: 지개차=>지게차;태양관=>태양광
TYPO_CORRECTION_AI=0 # 1이면 로컬 Ollama로 명백한 STT 오타만 추가 검사
# 품질 게이트
QUALITY_CHECK=1 # STT/화자 분리 위험 신호를 회의록 상단에 표시
QUALITY_BLOCK_UPLOAD=1 # 품질이 낮으면 G5 자동 업로드 차단
# 본문 하단 처리 정보 푸터 (오디오 길이·처리 시간·화자 수·모델). 끄려면 0
POST_PROCESSING_FOOTER=1
# 그누보드5 API
G5_API_BASE=http://127.0.0.1/gnuboard5/plugin/meeting_api
G5_API_TOKEN=
G5_BO_TABLE=meeting
# 경로 (보통 변경 안 함)
DB_PATH=./data/meetings.db
WORK_DIR=./data/work
UPLOAD_DIR=./data/uploads중요:
config.py는.env값이 시스템 환경변수보다 우선 적용되도록load_dotenv(override=True)로 설정되어 있습니다. 시스템에OLLAMA_HOST=0.0.0.0같은 잘못된 값이 있어도.env가 이깁니다.
cd meeting_record
python main.py "회의녹음.mp3" --no-upload진행 단계가 화면에 출력됩니다:
[1/6] 오디오 변환 (ffmpeg)
[2/6] 음성 인식 + 화자 분리 (faster-whisper small, 분리: 로컬 (speechbrain))
[3/5] 회의 요약 (Ollama gemma4:e2b-it-qat)
[4/5] SQLite 저장
[5/5] 요약 미리보기
✓ 완료 (--no-upload: 원격 업로드 생략)
meeting_id=1
그누보드5 플러그인과 .env의 G5_API_BASE/G5_API_TOKEN을 설정한 뒤에는 --no-upload 없이 실행하면 게시판까지 업로드됩니다.
pip install -e . --no-deps를 실행했다면 같은 명령을 짧게 쓸 수도 있습니다:
meeting-record "회의녹음.mp3" --no-upload
meeting-doctor
meeting-export 5 --format html# 화자 수를 강제 지정 (자동 추정 끄기)
python main.py "회의.mp3" --speakers 3
# 다인원 회의가 1~2명으로 뭉치면 실제 예상 화자 수로 다시 분리
# STT 캐시가 있어도 화자 라벨만 다시 계산해서 캐시를 갱신합니다.
python main.py "회의.mp3" --speakers 6
# STT는 그대로 두고 새 자동 화자 분리 로직만 다시 적용
python main.py "회의.mp3" --rediarize
# 그누보드5 업로드 생략 (로컬 DB에만 저장)
python main.py "회의.mp3" --no-upload
# 품질 게이트가 업로드를 막아도 꼭 올려야 할 때만 강행
python main.py "회의.mp3" --force-upload
# DB에 저장된 회의 조회
python main.py --show 1
# 업로드 실패한 회의 재전송
python main.py --resync
# 품질 게이트로 차단된 회의를 확인 후 승인 → 저장된 내용 그대로 업로드 (파일 재처리·DB 중복 없음)
python main.py --approve 1
# 도움말
python main.py --help기본 자동 화자 분리는 배포용 사용성을 우선해 적응형으로 동작합니다. 로컬 speechbrain
분리 결과가 1인/소수 발화처럼 애매하면 짧거나 비슷한 클러스터를 병합해 한 사람을
사용자1, 사용자2로 쪼개는 오류를 줄입니다. 반대로 다인원 회의 신호가 뚜렷하면
긴 발화를 짧은 창으로 나눠 임베딩을 뽑고, 높은 화자 수 후보를 보존하며 병합을 약하게
적용합니다. 실제 화자 수를 알고 있고 더 세밀한 분리가 필요할 때만 --speakers N 또는
WATCH_SPEAKERS=N을 사용하세요.
기본 로컬 speechbrain 화자 분리가 여러 사람을 1명으로 뭉치거나 서로 다른 사람을 같은
사용자N으로 묶을 때, pyannote로 바꾸면 화자 구분 정확도가 올라갑니다. (특히 발화가
겹치거나 배경음이 있는 녹음)
💸 유료가 아닙니다.
HUGGINGFACE_TOKEN은 결제가 아니라 무료 인증 토큰입니다. 화자 분리는 전부 이 PC(로컬)에서 실행되어 사용량 과금이 없습니다.
pyannote.audio(라이브러리): 무료·오픈소스 — 이미 설치돼 있음- 화자 분리 모델: HuggingFace에서 무료. 단 약관 동의 + 무료 토큰 필요(gated 모델)
⚠️ 별개로 pyannoteAI라는 유료 클라우드 API가 있지만, 이 프로젝트는 그게 아니라 로컬 pyannote.audio를 씁니다 — 무관, 무료.
🪟 Windows 사용자 — 먼저 읽으세요: 현재
pyannote.audio 4.x+speechbrain 1.x조합은 Windows에서k2의존성 문제로 대체로 동작하지 않습니다(자세한 내용은 아래 토큰 권한 / 의존성 주의). 토큰을 발급·설정해도 실행 시 자동으로 speechbrain으로 fallback되므로, Windows라면HUGGINGFACE_TOKEN을 비워두고 speechbrain을 그대로 쓰는 것을 권장합니다(헛시도 없이 더 빠름). pyannote 정확도가 꼭 필요하면 Linux 환경에서 쓰세요. macOS/Linux 사용자라면 아래 설정대로 진행하면 됩니다.
설정 (한 번만):
- 무료 토큰 발급 — https://hf.co/settings/tokens 에서 가입 후
read권한 토큰 생성 - 모델 약관 동의 — 아래 두 페이지에 접속해 각각 Agree / Accept 클릭 (무료):
.env에 토큰 추가:HUGGINGFACE_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
적용 후 동작:
- 토큰이 있으면 자동으로 pyannote 사용, 비어 있으면 speechbrain으로 자동 fallback.
- 모델 약관 미동의(403)이거나 pyannote 로드 실패 시에도 speechbrain으로 안전하게 fallback되므로 파이프라인이 멈추지 않습니다.
- pyannote 모델은 첫 실행 때 1회 다운로드됩니다(수백 MB).
화자만 다시 분리 (STT 캐시 재사용, 빠름):
python main.py "회의.mp3" --speakers 5 # 화자 수 강제
python main.py "회의.mp3" --rediarize # 화자 수 자동 추정으로 재분리
⚠️ --speakers/--rediarize(캐시 재분리)는 로컬 speechbrain을 씁니다. pyannote는 신규 STT(캐시 없는 전체 실행) 경로에서만 적용됩니다. pyannote로 처음부터 분리하려면data/work/의 해당*.segments.json을 지우고 전체 실행하세요.
- 토큰은 classic
read토큰을 쓰거나, fine-grained 토큰이면 "public gated repositories 접근" 권한을 켜야 합니다 (없으면403 enable access to public gated repositories). - 모델 2개 모두 약관 동의 필수:
speaker-diarization-3.1그리고segmentation-3.0(하나라도 빠지면403 GatedRepoError). - Windows +
pyannote.audio 4.x+speechbrain 1.x조합은k2의존성으로 막힐 공산이 큽니다. pyannote 로드 시 speechbrain의k2_fsalazy import가 실행되는데,k2는 Windows 공식 휠이 없어 (직접 빌드도 까다로움) 이 import가 실패합니다(ImportError: Lazy import of ...k2_fsa failed). 단 모든 Windows가 무조건 실패한다는 뜻은 아니며,pyannote.audio==3.1을 쓰거나 k2를 직접 빌드하면 동작할 수 있습니다. 확실한 방법은 Linux(k2 설치 쉬움)이고, 실패해도 자동으로 speechbrain으로 fallback됩니다. - 어떤 이유로든 pyannote 로드 실패 시 speechbrain으로 안전 fallback되어 파이프라인은 멈추지 않습니다.
🔐 토큰은 비밀값입니다.
.env에만 두고(git 제외) 채팅·문서·코드에 노출하지 마세요. 노출됐다면 HuggingFace에서 재발급(rotate)하세요.
이 도구는 회의록을 자동으로 “확정”하지 않고, STT/화자 분리 결과가 위험해 보이면 회의록 상단에 품질 경고를 붙입니다.
다음 신호를 조합해 품질을 판단합니다. 내용 품질 신호가 강하게 겹치면 낮음으로 판정하고, 기본값에서는 G5 자동 업로드를 차단합니다.
- 긴 녹음인데 발화가 거의 감지되지 않음
- 같거나 비슷한 문장이 반복됨
- Whisper 신뢰도 지표가 낮거나 무음 구간이 텍스트화됨
- 긴 회의인데 화자가 1명으로만 인식됨 또는 한 화자에 몰림
→ 이 항목은 1인 강의·인터뷰일 수 있어 단독으로는 차단하지 않고 경고로만 봅니다.
차단된 회의록은 로컬 SQLite DB에는 저장됩니다. 원문을 확인한 뒤 정말 올려야 하면 python main.py --approve <meeting_id> 로 저장된 내용을 그대로 업로드하세요(파일을 다시 처리하지 않으므로 DB 중복이 생기지 않습니다). 처음부터 강행하려면 python main.py "회의.mp3" --force-upload, 게이트 자체를 끄려면 .env에서 QUALITY_BLOCK_UPLOAD=0을 씁니다.
| 모델 | 크기 | 107분 오디오 STT 예상 | 한국어 정확도 |
|---|---|---|---|
tiny |
75MB | ~15분 | 낮음 (회의록 부적합) |
base |
150MB | ~25분 | 보통 |
small |
500MB | ~40분 | 양호 (기본값, 권장) |
medium |
1.5GB | ~115분 | 매우 좋음 |
large-v3 |
3GB | ~250분 | 최고 |
| 모델 | 크기 | 171발화 요약 예상 | 품질 |
|---|---|---|---|
qwen3:4b |
2.5GB | ~10분 | 양호 |
gemma3:4b |
3.3GB | ~12분 | 양호 |
exaone3.5:7.8b |
4.8GB | ~20분 | 한국어 특화, 좋음 |
gemma4:e2b-it-qat |
4.3GB | ~30분 | 매우 좋음 (기본값·8GB GPU 100% 적재) |
gemma4:e4b-it-qat |
6.1GB | ~30분 | 더 우수, 8GB에도 적재됨 |
gemma4:e2b (풀프리시전) |
7.7GB | ~30분 | 매우 좋음, 단 8GB GPU엔 안 들어감 |
gemma4:e4b |
9.6GB | 30분+ (긴 context 불가) | 짧은 회의만 |
gemma4:e2b-it-qat(Q4 양자화, 4.3GB)가 기본값입니다. 풀프리시전 gemma4:e2b(7.7GB)는
8GB VRAM에 다 안 올라가 CPU 분할 → 느리거나 요약이 실패할 수 있어, QAT(양자화) 변형을
기본으로 합니다(ollama ps에서 100% GPU 확인됨). 다만 RTX 3050 Laptop 4GB처럼 VRAM이 더 작은 환경에서는
모델 전체를 GPU에 올릴 수 없고, CPU fallback도 free RAM이 부족하면 멈춘 것처럼 보일 수
있습니다. 이 프로젝트는 STT/화자분리 직후 faster-whisper, pyannote, speechbrain 캐시를 정리한
뒤 free RAM이 OLLAMA_MIN_FREE_RAM_GB보다 낮으면 Ollama 호출을 시작하지 않습니다. 이때
STT 캐시는 남으므로 메모리를 비우고 다시 실행하면 요약 단계부터 이어갈 수 있습니다.
저메모리 환경 권장값:
OLLAMA_MODEL=gemma4:e2b-it-qat
OLLAMA_KEEP_ALIVE=0
OLLAMA_NUM_CTX_MAX=32768 # 100분 안팎 긴 회의 권장. 짧은 회의만 8192~16384 가능
OLLAMA_NUM_PREDICT=8192
OLLAMA_SUMMARY_CHUNK_SEC=900 # 긴 회의는 15분 단위 상세 요약 후 최종 통합
OLLAMA_MIN_FREE_RAM_GB=6 # QAT(4.3GB)는 여유 있음. 풀프리시전 gemma4:e2b면 6~8GB 권장
# GPU offload가 계속 실패하면 CPU만 사용. 단, free RAM이 충분해야 합니다.
# OLLAMA_NUM_GPU=0
# 4GB VRAM에서는 OLLAMA_NUM_GPU=999 같은 전체 GPU 강제 값을 쓰지 마세요.
WHISPER_BATCH_SIZE=4긴 회의는 기본적으로 OLLAMA_SUMMARY_CHUNK_SEC 기준으로 구간별 상세 요약을 만든 뒤 최종
회의록으로 통합합니다. OLLAMA_NUM_CTX_MAX를 너무 낮추면 구간 요약 또는 최종 통합 단계에서
JSON 지시가 context 밖으로 밀려 ## 같은 조각 응답이 나올 수 있습니다. 요약 품질보다
안정성이 더 중요하거나 free RAM 확보가 어렵다면 qwen3:4b 같은 더 작은 모델을 먼저 사용해 보세요.
| 우선순위 | Whisper | LLM | 총 예상 시간 |
|---|---|---|---|
| 속도 최우선 | small |
qwen3:4b |
~50분 |
| 균형 (권장) | small |
gemma4:e2b-it-qat |
~70분 |
| 정확도 우선 | medium |
gemma4:e2b-it-qat |
~145분 |
| 최고 품질 | large-v3 |
exaone3.5:7.8b |
~270분 |
변경 후 모델 다운로드: 새 Whisper 모델 사용 시
python scripts/download_models.py실행 (또는WHISPER_DOWNLOAD_ALL=1환경변수로 전체 받기)
.env에서 설정:
WHISPER_VAD_FILTER=1 # 무음 구간 사전 필터링 (5~15% 단축)
WHISPER_CPU_THREADS=0 # 0=모든 코어 자동 사용
WHISPER_BATCH_SIZE=8 # 메모리 여유 있으면 16으로
NVIDIA GPU가 있으면 STT가 10~30배 빨라집니다. 107분 회의가 5~10분에 끝납니다.
1단계: 시스템 진단
python doctor.py출력에서 다음 확인:
✓ CUDA available이면 바로 사용 가능 (3단계로 점프)! PyTorch는 CPU 전용 빌드입니다이면 2단계 실행 필요! nvidia-smi 없음이면 NVIDIA GPU 또는 드라이버 없음 → 하드웨어 추가 필요
2단계: PyTorch GPU 빌드 재설치 (venv 활성화 상태에서)
# 기존 CPU 버전 제거 (torchvision도 반드시 함께 — 안 그러면 STT/pyannote가 깨질 수 있습니다)
pip uninstall -y torch torchaudio torchvision
# CUDA 빌드 설치 (최신 드라이버면 cu128 권장)
pip install torch torchaudio torchvision --index-url https://download.pytorch.org/whl/cu128
⚠️ CUDA 버전은 환경마다 다릅니다. 위cu128은 비교적 최신 드라이버 기준입니다. 구형 GPU·드라이버이거나 Linux/다른 CUDA 버전이라면, OS·CUDA를 선택해 정확한 명령을 생성해 주는 PyTorch 공식 설치 셀렉터 를 사용하세요. 지원 CUDA 버전은nvidia-smi우측 상단의CUDA Version으로 확인합니다.NVIDIA 드라이버는 NVIDIA 공식 사이트에서 받으세요. CUDA Toolkit은 PyTorch가 번들 라이브러리를 쓰므로 별도 설치가 필요 없습니다.
3단계: .env에서 활성화
DEVICE=auto # CUDA 자동 감지 (권장)
# 또는 명시적으로
# DEVICE=cuda
WHISPER_MODEL=medium # GPU에서는 medium/large-v3도 충분히 빠름
WHISPER_COMPUTE_TYPE= # 빈 값 = 자동 (cuda면 float16)
WHISPER_BATCH_SIZE=16 # VRAM 8GB+ 권장, 12GB+면 32검증
python doctor.py # DEVICE=cuda 표시 확인
python main.py "회의.mp3" # [info] DEVICE=cuda, compute_type=float16 출력GPU 권장 사양
| GPU | VRAM | 권장 Whisper | 처리 시간 (107분 회의) |
|---|---|---|---|
| RTX 3050 Laptop | 4GB | small, batch 4~8 | STT는 가능, LLM은 free RAM 확보 필요 |
| GTX 1660 / RTX 2060 | 6GB | small | ~10분 |
| RTX 3060 12GB | 12GB | medium | ~7분 |
| RTX 4070 / 3080 | 12GB | large-v3 | ~6분 |
| RTX 4090 / A100 | 24GB+ | large-v3 + batch_size 64 | ~3분 |
Fallback 동작
DEVICE=cuda지정했으나 PyTorch GPU 빌드 없음 → 자동으로 CPU fallback + 경고DEVICE=auto(기본) → CUDA 가용 시 자동 사용, 아니면 CPU- 어떤 경우든 동작은 항상 보장됨
첫 실행은 느림: 모델 다운로드 (~6GB) + 모델 메모리 로딩으로 추가 시간 필요. 두 번째부터는 위 표 적용.
# 1) 로컬 SQLite
python main.py --show 1
# 2) 그누보드5 게시판 (브라우저)
start http://127.0.0.1/gnuboard5/bbs/board.php?bo_table=meeting
# 3) 특정 회의 직접 보기
start http://127.0.0.1/gnuboard5/bbs/board.php?bo_table=meeting&wr_id=8CLI 대신 브라우저에서 회의 목록 조회/편집/검색/export까지 모두 가능.
cd meeting_record
# 최초 1회: .env에 STREAMLIT_ACCESS_PASSWORD=강력한-비밀번호 설정
python -m streamlit run app.py브라우저 자동으로 열리거나 http://localhost:8501 접속 후 비밀번호로 로그인.
핸드폰/다른 PC에서 접속 (같은 Wi-Fi):
python -m streamlit run app.py --server.address 0.0.0.0→ http://<PC-IP>:8501 로 접속 (예: http://192.168.0.10:8501 — <PC-IP>는 ipconfig/ifconfig로 확인)
외부 접속을 열 때는
STREAMLIT_ACCESS_PASSWORD를 반드시 설정하세요. 기본값은 12시간 세션 만료, 5회 실패 시 300초 잠금이며 실패/잠금 상태는data/app_auth_state.json에 저장됩니다. 필요하면.env에서STREAMLIT_SESSION_TTL_SEC,STREAMLIT_AUTH_MAX_ATTEMPTS,STREAMLIT_AUTH_LOCKOUT_SEC,STREAMLIT_AUTH_STATE_PATH를 조정하세요. 로컬 테스트에서만 인증을 끄려면.env에STREAMLIT_ALLOW_NO_AUTH=1을 명시합니다. 인터넷에 공개해야 한다면 앱 비밀번호만 믿지 말고 VPN, Cloudflare Access, Nginx/Apache Basic Auth 같은 앞단 인증을 함께 두는 것을 권장합니다.
페이지 구성:
| 페이지 | 기능 |
|---|---|
| 📋 회의 목록 | 전체 회의 + 상세 보기 (요약/발화/통계/편집/Export 탭) |
| 🔍 검색 | FTS5 회의 + 발화 검색 |
| 📚 사전 관리 | 도메인 사전 추가/삭제 + Whisper 프롬프트 미리보기 |
| 👤 화자 등록 | 등록된 화자 목록 + 관리 |
회의 상세 편집 탭에서 가능한 작업:
- 화자 라벨 일괄 변경 (
사용자3→장관님) - 발화 텍스트 인라인 수정 (검색 → 수정 → 저장)
- 요약 본문 마크다운 직접 편집
- 회의 삭제 (확인 입력 필수)
Export 탭에서 가능한 작업:
.docx다운로드.html다운로드 (브라우저 Ctrl+P로 PDF 변환 가능)- 그누보드5 게시글 바로 이동 링크
참고: 모든 변경 사항은 즉시 SQLite에 반영되고 FTS 인덱스도 자동 동기화됩니다. 그누보드5는 별도 sync가 필요한 부분 (현재 발화 텍스트는 자동 sync, 화자명/요약은 수동 갱신).
같은 Wi-Fi의 핸드폰/노트북에서:
http://<PC의-LAN-IP>/gnuboard5/bbs/board.php?bo_table=meeting
PC의 IP 확인: ipconfig 명령 또는 Get-NetIPAddress
회의 중 의도치 않게 노출되는 개인정보(주민번호/휴대폰/카드/계좌/이메일/일반전화)를 자동으로 가립니다.
.env 설정 (.env.example 기본값은 partial — 관공서/민감 회의 권장):
PII_MASK_LEVEL=partial # 일부 가림: 901234-1****67 ← 권장
# PII_MASK_LEVEL=full # 라벨 대체: [주민번호]
# PII_MASK_LEVEL=off # 마스킹 안 함 (코드 미설정 시 기본)
⚠️ 회의 중 언급된 긴 숫자(예산·통계 등)가 드물게 오탐 마스킹될 수 있습니다. 정확한 숫자가 더 중요하면off로 두되, 게시판 공개 전 직접 검수하세요. 실행 시 현재 마스킹 레벨이 콘솔에 표시됩니다(off면 경고).
처리 시점: STT 후 + 요약 후 자동 적용 (발화와 요약 본문 모두). 그누보드5에 등록되는 댓글/게시글도 자동으로 마스킹된 결과가 올라갑니다.
예시 (partial):
010-1234-5678→010-****-**78hong.gildong@example.com→h**********g@example.com901234-1234567→901234-1****671234-5678-9012-3456→1234-****-****-3456
# 수동 실행
python scripts/backup.py # ./data/backups/<날짜>/ 로 백업
python scripts/backup.py --out D:\backup # 출력 폴더 지정
python scripts/backup.py --keep 14 # 14일 초과 백업 자동 삭제
python scripts/backup.py --no-mysql # SQLite만MariaDB 위치나 계정이 기본값과 다르면 .env에 설정합니다:
MYSQLDUMP_PATH=C:\xampp\mysql\bin\mysqldump.exe
MYSQL_DATABASE=meeting
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=Windows 작업 스케줄러로 자동화 (관리자 PowerShell):
$project = (Resolve-Path .).Path
schtasks /create /tn "MeetingRecordBackup" /sc DAILY /st 03:00 `
/tr "cmd /c `"cd /d $project && python scripts\backup.py --keep 30`""매일 새벽 3시에 자동 백업 + 30일 초과 자동 정리.
백업 내용:
meetings.db(SQLite, sqlite3.backup() API로 안전한 복사)meeting.sql(MariaDB mysqldump, 트리거/이벤트/루틴 포함)
python export.py 5 # meeting_id=5 → ./data/exports/<title>.docx
python export.py 5 --format html # HTML로
python export.py 5 --format all # docx + html 둘 다
python export.py 5 --no-transcript # 발화 전문 제외 (요약만)
python export.py --all # 모든 회의 일괄PDF가 필요하면: HTML로 export 후 브라우저에서 Ctrl+P → "PDF로 저장"이 가장 깨끗합니다. 한국어 폰트 문제 없고, 표/리스트도 그대로 보입니다.
생성된 docx 포함 내용:
- 표지 + 메타정보 표 (작성일시/원본/길이/화자수)
- 마크다운 요약 본문 (헤더/리스트/굵게 등 변환)
- 발화 전문 표 (시간/화자/내용) —
--no-transcript로 제외 가능
python stats.py 5 # 화자별 발언 횟수/시간/비율
python stats.py 5 --time # 시간 구간별 분포 추가
python stats.py 5 --json # JSON 출력 (다른 도구 연동용)예시 출력:
화자 발언 횟수 총 발언 시간 평균 발언 비율
사용자4 44 30분 55초 42초 32.7%
사용자3 (장관) 62 28분 6초 27초 29.7%
회의 처리 완료/실패 시 자동으로 통보합니다. 두 채널 모두 옵션이고, 설정된 채널만 작동합니다.
.env 설정 예시
NOTIFY_LEVEL=all # all=성공/실패, fail=실패만, off=알림 끔
# Slack
NOTIFY_SLACK_WEBHOOK=https://hooks.slack.com/services/T.../B.../...
# 이메일 (Gmail 예시: 앱 비밀번호 사용)
NOTIFY_EMAIL_HOST=smtp.gmail.com
NOTIFY_EMAIL_PORT=587
NOTIFY_EMAIL_USER=sender@gmail.com
NOTIFY_EMAIL_PASS=앱비밀번호
NOTIFY_EMAIL_FROM=sender@gmail.com
NOTIFY_EMAIL_TO=receiver1@example.com,receiver2@example.comSlack 메시지 예시:
✅ 회의록 처리 완료
제목: 산업안전 강화 기관장 회의...
원본: 회의.mp3
길이: 1시간 46분 / 발화 171건 / 화자 6명
처리 시간: 33분 12초
게시글: wr_id=548
watcher.py로 무인 운영 시에도 자동 적용됩니다.
회의에 자주 등장하는 고유명사/전문용어를 사전에 등록해두면 STT가 더 정확하게 인식하고, 자주 발생하는 오인식은 자동으로 교정됩니다.
파일명 힌트 (등록 불필요·가장 간단)
파일명에 든 작품명·인명·기관명은 자동으로 추출되어 Whisper 힌트(initial_prompt)로 전달됩니다. 일회성 회의라 사전 등록까지 하긴 번거로울 때 가장 빠른 방법입니다.
와일드씽_무대인사_트라이앵글.mp3 → "와일드씽", "트라이앵글" 을 힌트로 사용
고유명사가 깨져 나올 때(예: 와일드씽이 월드에스칭으로) 먼저 파일명부터 또렷하게 지어 보세요.
음악·잡음이 많은 음원 — 환각 억제
배경음악·노래·랩·함성이 섞인 음원은 Whisper가 직전 문맥에 끌려 엉뚱한 말이나 외국어를
지어내기도 합니다(예: outras, 반복). 이럴 때만 아래를 끄면 줄어듭니다.
WHISPER_CONDITION_ON_PREVIOUS_TEXT=0 # 기본 1. 또렷한 회의는 1이 보통 유리또렷한 회의 음성에서는 문맥(=1)이 정확도에 도움이 되므로 기본값 1을 유지하고, 음악/잡음이 심한 음원에서만 0으로 바꾸는 것을 권장합니다.
.env에서 간단 오타 보정
몇 개만 빠르게 고치고 싶다면 DB 사전에 등록하지 않고 .env에 문자 그대로 치환 규칙을 넣을 수 있습니다.
TYPO_CORRECTION=1
TYPO_CORRECTION_RULES=중대재수자=>중대재해;태양관=>태양광;지개차=>지게차- 여러 규칙은
;로 구분합니다. TYPO_CORRECTION_RULES는 정규식이 아니라 단순 문자열 치환입니다.- 회의 처리 시 캐시를 쓰더라도 발화 텍스트에 다시 적용되고, 보정이 있으면
.segments.json캐시도 갱신됩니다. - 복잡한 정규식이나 많은 용어는 아래
dict.py사전 관리 기능을 쓰는 편이 좋습니다.
로컬 AI 오타 보정 (선택)
규칙으로 등록하지 않은 명백한 STT 오인식까지 문맥으로 잡고 싶다면 로컬 Ollama 보정을 켤 수 있습니다.
TYPO_CORRECTION=1
TYPO_CORRECTION_AI=1
TYPO_CORRECTION_AI_MODEL= # 빈 값이면 OLLAMA_MODEL 사용
TYPO_CORRECTION_AI_CHUNK_SIZE=30 # 한 번에 검사할 발화 수AI 보정은 발화의 text만 고치며 화자, 시간, 발화 순서는 바꾸지 않습니다. 프롬프트는 요약/문체 개선/순화/재작성을 금지하고 “명백한 STT 오타만” 고치도록 제한되어 있습니다. 그래도 고유명사나 농담 표현을 잘못 고칠 수 있으므로 중요한 회의에서는 TYPO_CORRECTION_RULES 또는 dict.py 사전 규칙을 우선 권장합니다.
기본 사용
# 1) 회의에 등장할 용어 등록 (Whisper에게 컨텍스트 제공)
python dict.py add 산업안전감독관
python dict.py add 노동부
# 2) 자주 발생하는 오인식 교정 패턴 등록
python dict.py add 산업안전 --pattern 산업안정 # "산업안정"으로 인식되면 "산업안전"으로
python dict.py add 이민재 --pattern "(?:이민자|이민제)" # 정규식 매칭
python dict.py add 중대재해 --pattern 중대제해
# 3) 미리보기
python dict.py test "산업안정 강화 회의" # 어떻게 치환되는지 확인
python dict.py prompt # Whisper에게 전달될 컨텍스트 확인작동 방식
- 회의 처리 시 자동 적용 (
main.py또는watcher.py어느 쪽이든):- Whisper의
initial_prompt로 등록된 용어들 전달 → 처음부터 정확한 인식 - STT 결과에 후처리 치환 적용 → Whisper가 놓친 오류 교정
- Whisper의
- 기존 회의에 소급 적용:
python dict.py apply-to-meeting 5 # meeting_id=5 의 발화 + 요약 + 그누보드5 일괄 갱신 python dict.py apply-to-meeting 5 --skip-remote # 그누보드5는 건드리지 않고 로컬만
CSV 일괄 등록
term,pattern,replacement,notes
산업안전,산업안정,,STT 흔한 오류
중대재해,중대제해,,
이민재,(?:이민자|이민제),,정책실장python dict.py import-csv dictionary.csv관리
python dict.py list # 전체 목록
python dict.py disable 3 # id=3 비활성화 (삭제는 아님)
python dict.py enable 3
python dict.py delete 3 # 완전 삭제예시: 정부 회의 도메인
python dict.py add 산업안전강화 --pattern 산업안정강화
python dict.py add 기관장 --pattern 기간장
python dict.py add 중대재해
python dict.py add 노동부
python dict.py add 안전공단
python dict.py add 갈매기특공대 --pattern "갈매기 특공대"권장: 첫 회의 처리 후 STT 결과를 훑어보며 흔한 오류 단어를 등록하세요. 두 번째 회의부터 같은 오류가 자동 교정됩니다.
회의록 등록 PHP API는 그누보드5의 표준 plugin/ 디렉토리 안에 meeting_api/ 폴더로 설치합니다. 그누보드5 원본 코드는 한 줄도 수정하지 않고 새 폴더만 추가하는 방식.
디렉토리 구조:
gnuboard5/ ← 원본 (수정 안 함)
└── plugin/
└── meeting_api/ ← 추가하는 폴더
├── _bootstrap.php
├── _load_gnuboard5.php
├── config.php ← G5 경로 자동 (../../로 그누보드5 루트)
├── config.local.php ← 운영 토큰 (git 제외)
├── health.php
├── post.php
├── comment.php
├── update_post.php
├── list_comments.php
├── update_comment.php
├── delete_post.php
└── setup_board.php ← 게시판 자동 생성 (1회 후 삭제)
호출 URL: https://YOUR-DOMAIN/<gnuboard폴더>/plugin/meeting_api/health.php
로컬 그누보드5를 쓰는 경우 <그누보드5루트>\plugin\meeting_api\에 설치합니다.
curl -H "X-API-Token: YOUR_TOKEN" http://127.0.0.1/gnuboard5/plugin/meeting_api/health.php
python doctor.py배포 패키지: GitHub Releases의 g5_meeting_api-vX.Y.Z.zip
-
FTP 업로드 — ZIP 풀면 안에
plugin/meeting_api/폴더가 그대로 있음. 이 폴더를 원격 그누보드5의plugin/안에 업로드:/home/<user>/www/ └── gnu5615/ └── plugin/ └── meeting_api/ ← 업로드 -
운영 토큰 설정 —
config.local.php.example→config.local.php복사 후 토큰/디버그 변경:define('meeting_API_TOKEN', '강력한_랜덤_48자_이상'); define('meeting_API_DEBUG', false); // 선택: 호출 가능한 IP/CIDR 제한. 비워두면 IP 제한 없음. // define('meeting_API_ALLOWED_IPS', '203.0.113.10,203.0.113.0/24'); // setup_board.php 실행 시에만 true. 성공 후 false로 되돌리거나 파일 삭제. define('meeting_API_ALLOW_SETUP', true); // 선택: 전체 JSON/게시글/댓글 본문 크기 제한 조정 // define('meeting_API_MAX_BODY_BYTES', 3145728); // define('meeting_API_MAX_POST_CONTENT_BYTES', 2097152); // define('meeting_API_MAX_COMMENT_CONTENT_BYTES', 262144);
-
게시판 자동 생성 (1회):
curl -X POST -H "X-API-Token: YOUR_TOKEN" \ https://YOUR-DOMAIN/gnu5615/plugin/meeting_api/setup_board.php성공 후
meeting_API_ALLOW_SETUP을false로 되돌리거나 setup_board.php를 즉시 삭제.기존 버전에서 생성된 marker 없는 글을 임시로 수정/삭제해야 한다면
config.local.php에define('meeting_API_ALLOW_UNMARKED_WRITES', true);를 잠깐 추가할 수 있습니다. 작업 후에는 다시 제거하거나false로 되돌리세요.
3-1. (권장) 회의록봇 회원 계정 — 비공개/회원제 게시판이면 글이 로그인한 회원 글로
등록되도록 봇 계정을 사용하세요. meeting_MB_ID를 비워두면 기존처럼 비회원(게스트)
글로 등록됩니다(하위 호환).
// config.local.php
define('meeting_MB_ID', 'meetingbot'); // 봇 회원 아이디
define('meeting_WR_NAME', '회의록봇'); // 회원 닉네임/작성자명
// define('meeting_MB_PASSWORD', ''); // 비우면 setup 시 임의 생성(1회 응답에 표시)
// define('meeting_MB_LEVEL', 2); // 기본 2(일반 회원). 게시판 글쓰기 레벨 이상으로계정 자동 생성(그누보드 관리자에서 직접 만들었다면 생략):
curl -X POST -H "X-API-Token: YOUR_TOKEN" \
https://YOUR-DOMAIN/gnu5615/plugin/meeting_api/setup_member.php응답의 report.member_created가 true면 성공. 임의 생성 비밀번호는 generated_password에
1회만 표시되니 필요하면 보관하세요. 작업 후 meeting_API_ALLOW_SETUP을 false로 되돌리거나
setup_member.php를 삭제합니다. health.php 응답의 author_mode가 member,
member_account_exists가 true인지 확인하세요.
- Python
.env:G5_API_BASE=https://YOUR-DOMAIN/gnu5615/plugin/meeting_api G5_API_TOKEN=원격_토큰
G5_TARGETS=local,remote
G5_API_BASE_LOCAL=http://127.0.0.1/gnuboard5/plugin/meeting_api
G5_API_TOKEN_LOCAL=로컬_토큰
G5_API_BASE_REMOTE=https://YOUR-DOMAIN/gnu5615/plugin/meeting_api
G5_API_TOKEN_REMOTE=원격_토큰
# 기존 단일 설정(default)이 원격 게시판이었다면 remote로 승계
G5_LEGACY_TARGET=remote회의 처리 시 두 게시판에 동시 등록하며, python main.py --resync는 타겟별 누락분을 다시 전송합니다.
python doctor.py # G5 타겟 섹션
python scripts/check_g5_remote.py # 전체 타겟 health + 쓰기/수정/삭제 테스트
python scripts/check_g5_remote.py --target remote
python scripts/check_g5_remote.py --cleanup-stale --cleanup-minutes 60Python 코드 변경 후에는 빠른 단위 테스트도 실행할 수 있습니다:
python -m pip install pytest
python -m pytest -q- ✅ 그누보드5 원본 무수정 —
plugin/표준 디렉토리에 새 폴더만 추가 - ✅ 그누보드5 업그레이드 안전 — 충돌 없음
- ✅ G5 경로 자동 —
__DIR__ . '/../..'로 루트 인식 - ✅ common.php 안전 로드 — plugin 안에서는 SCRIPT_FILENAME 위장 없이 정상 동작
이전 방식(그누보드5 옆 별도 폴더) 잔재. 선택사항:
Remove-Item C:\xampp\htdocs\g5_meeting_api # junction 정리
# Remove-Item <이전-g5_meeting_api-폴더> -Recurse # 원본 폴더 (백업 후)config.local.php절대 git/FTP에서 공유 안 함 (토큰 노출)setup_board.php/setup_member.php작업 후meeting_API_ALLOW_SETUP=false로 되돌리거나 파일 즉시 삭제- 가능하면
meeting_API_ALLOWED_IPS로 Python 실행 PC/서버 IP만 허용 - API 본문 크기 제한 유지 (
meeting_API_MAX_*_BYTES) — 기본값을 크게 올릴수록 서버 부하 위험 증가 - 그누보드5
install/폴더 운영 시 삭제 - HTTPS 사용 (토큰 평문 전송 보호)
한 번 사람의 음성을 등록해두면 이후 회의에서 자동으로 실제 이름이 매핑됩니다.
등록 방법 1: 외부 wav 파일로 등록
# 10초 이상의 깨끗한 음성 (한 사람만, 잡음 적게)
python enroll.py add "장관님" samples/장관님.wav
python enroll.py add "김부장님" samples/김부장.wav등록 방법 2: 기존 회의에서 자동 추출
이미 처리한 회의가 있다면 거기서 화자 샘플을 뽑을 수 있습니다:
# meeting_id=5 의 "사용자3"의 가장 긴 단일 발화 → wav로 추출
python enroll.py extract-from 5 사용자3 --out data/samples/장관님.wav --target 30
# 그 wav로 등록
python enroll.py add "장관님" data/samples/장관님.wav확인 / 관리
python enroll.py list # 등록된 화자 목록
python enroll.py test data/samples/test.wav # 입력 음성이 누구인지 매칭 테스트
python enroll.py delete "장관님" # 삭제자동 매칭
화자가 등록되어 있으면 python main.py audio.mp3 실행 시 다음과 같이 동작:
- 평소처럼 STT + 화자 분리 (clustering)
- 각 클러스터의 중심 임베딩을 등록된 화자들과 cosine 유사도 비교
- 임계값(기본 0.75) 이상 매칭되는 클러스터 → 실제 이름 사용
- 매칭 안 되는 클러스터 →
SPEAKER_00→사용자1폴백
콘솔에 매칭 결과가 표시됩니다:
[info] 등록 화자 매칭: {0: '장관님', 2: '사회자'}
그누보드5 댓글에도 회의_장관님, 회의_사회자 같은 작성자명으로 등록됩니다.
⚠️ 한 회의에 한 사람을 매번 정확히 같은 라벨로 매핑하려면 30초 이상의 깨끗한 단일 발화 샘플이 좋습니다.
여러 회의 간 통계/키워드 비교 및 시간대별 추이 분석.
CLI:
# 두 회의 직접 비교 (메타/화자/공통 키워드/A에만/B에만)
python compare.py 3 5
# 월별 회의 통계 (몇 건, 평균 길이, 총 발화)
python compare.py --timeline
python compare.py --timeline --since 2026-01-01 --until 2026-12-31
# 특정 키워드의 월별 등장 빈도
python compare.py --keyword-trend "산업재해"
# 특정 화자의 월별 발언 추이
python compare.py --speaker-trend 사용자3
# 특정 회의의 상위 키워드 (한국어 빈도 분석)
python compare.py --top-keywords 5 --top 30웹 UI (📊 비교 페이지) 5개 탭:
- 두 회의 비교 — A/B 선택 → 메타 차이 + 화자 (공통/A만/B만) + 키워드 (공통/A만/B만)
- 월별 통계 — 월별 회의 수/총 길이/평균/발화 수 표 + 차트
- 키워드 추이 — 특정 단어가 언제 많이 다뤄졌는지
- 화자 추이 — 한 사람의 월별 발언량/회의 참여 추이
- 상위 키워드 — 회의별 가장 자주 나오는 단어 + 차트
한국어 키워드 추출:
kiwipiepy(한국어 형태소 분석기)가 설치되어 있으면 자동으로 사용 — 명사 위주 정확한 추출 (NNG/NNP 태그)- 미설치 시 정규식 기반 폴백 (부사/조사 결합형이 일부 섞일 수 있음)
- 인접 명사는 자동으로 결합 (예: "산업/안전/감독관" → "산업안전감독관")
- 현재 사용 중인 방식:
python doctor.py의 "키워드 추출" 섹션에 표시
설치:
pip install "kiwipiepy>=0.20.0" # 형태소 분석 활성화 (선택)누적된 회의록을 키워드로 검색:
python search.py "산업재해" # 회의 제목/요약 + 발화 동시 검색
python search.py "지게차" --speaker 사용자3 # 특정 화자의 발화만
python search.py "AI 도입" --meetings-only # 회의 요약만
python search.py "안전" --since 2026-01-01 # 이 날짜 이후
python search.py "산업재해 OR 지게차" --advanced # FTS5 문법 직접 사용
python search.py --rebuild # FTS 인덱스 재구축 (스키마 변경 시)
python search.py --recreate-fts --tokenizer trigram # FTS 테이블을 trigram으로 재생성기본 검색은 일반 검색어를 안전하게 처리합니다. 아래 FTS5 문법을 직접 쓰려면 --advanced를 붙이세요.
FTS5 고급 쿼리 문법:
"단어 단어"— AND (기본)"단어 OR 단어"— OR"정확한 구문"— 따옴표로 묶기"단어1 NEAR/5 단어2"— 5단어 이내 근접"산업*"— 접두사 와일드카드
한국어 검색 팁: 기본은 trigram 토크나이저라 최소 3글자 검색이 가능합니다. 2글자(예: "산재")는 결과가 안 나올 수 있으니 "산업재해", "산재 사망" 처럼 longer 검색을 사용하세요. SQLite 빌드에 trigram이 없으면 자동으로 unicode61로 fallback되어 검색 품질이 다소 낮아질 수 있습니다. 현재 tokenizer는 python doctor.py의 "SQLite 검색" 섹션에서 확인할 수 있습니다. 나중에 trigram 지원 환경으로 옮겼다면 python search.py --recreate-fts --tokenizer trigram으로 FTS 테이블을 다시 만들 수 있습니다.
CLI 명령을 매번 치는 대신, 특정 폴더에 음성 파일을 떨어뜨리기만 하면 자동 처리됩니다.
cd meeting_record
python watcher.pyG5 설정 전에는 로컬 DB 저장만 먼저 테스트할 수 있습니다:
python watcher.py --no-upload실행 후 data\watch\ 폴더에 .mp3, .m4a, .mp4 등 음성/동영상 파일을 드롭하면:
- 파일 크기 변화 감지 (5초간 안정 = 복사 완료로 판단)
main.py자동 실행 (.env의WATCH_SPEAKERS적용)- STT → 화자 분리 → 품질 점검 → 요약 → DB → 그누보드5 업로드까지 자동 처리
- 처리 결과는 콘솔 +
data\watch.log에 기록
품질이 낮음으로 판정되면 기본값에서는 G5 업로드가 차단되고 로컬 DB에만 저장됩니다. 원문을 확인한 뒤 올리려면 python main.py --approve <meeting_id>를 사용하세요. watcher에서 처음부터 강행 업로드가 필요하면 .env의 QUALITY_BLOCK_UPLOAD=0을 사용하세요.
Ollama 대기 동작
- watcher를 다시 켠 직후 첫 요약은
gemma4:e2b-it-qat콜드 로딩 때문에 step 3에서 60~90초 동안 출력이 없을 수 있습니다. 이후생성 중... N청크가 보이면 정상입니다. OLLAMA_TIMEOUT_SEC동안 첫 청크가 전혀 없으면 실패로 처리합니다. 이전 stuck 이후 Ollama 서버 스케줄러가 좀비 상태일 수 있으므로ollama stop gemma4:e2b-it-qat또는 Ollama 재시작 후python watcher.py를 다시 실행하세요.- 메모리 여유가 충분하고 연속 파일 처리 속도가 중요하면
OLLAMA_KEEP_ALIVE=10m처럼 모델을 잠시 유지할 수 있습니다. RTX 3050 4GB/저메모리 환경에서는 기본값0이 더 안전합니다.
핵심 설정 (.env):
# 지원 확장자: mp3, m4a, wav, amr, aac, ogg, flac, wma, mp4
WATCH_DIR=./data/watch # 감시할 폴더
WATCH_SPEAKERS=6 # 화자 수 (빈 값이면 자동 추정)
WATCH_STABILITY_SEC=5 # 파일 안정 확인 시간
WATCH_NO_UPLOAD=0 # 1이면 G5 업로드 생략
WATCH_LOG=./data/watch.log # 처리 이력
다인원 회의인데 자동 추정 결과가 사용자1, 사용자2 정도로 뭉치면 WATCH_SPEAKERS=6처럼 실제 예상 화자 수를 지정하세요. 기존 .segments.json 캐시가 있어도 watcher가 main.py --speakers 6으로 실행하므로 STT는 재사용하고 화자 라벨만 다시 분리합니다.
활용 예시:
- 핸드폰 SMB 공유로
data\watch\폴더에 직접 업로드 → 자동 처리 - 회의 끝나고 PC에 파일 옮기기만 하면 됨 (CLI 명령 안 적어도 됨)
- Windows 시작 시 watcher 자동 실행: 작업 스케줄러 등록
옵션 명령:
python watcher.py --scan-now # 폴더의 기존 파일만 처리 후 종료원인: ffmpeg가 설치되지 않았거나 PATH에 없음.
해결 (OS별 설치):
# Windows
winget install Gyan.FFmpeg --source winget
# macOS: brew install ffmpeg
# Linux: sudo apt install ffmpeg (또는 배포판 패키지 매니저)
# 설치 후 새 터미널에서 확인:
ffmpeg -version원인: XAMPP Control Panel이 외부에서 시작된 mysqld를 인식하지 못해 stale 상태로 표시하고, 사용자가 Start 버튼을 누르면 중복 실행을 시도해 첫 번째 인스턴스가 ibdata1 파일 잠금을 가진 채로 두 번째 인스턴스가 즉시 죽음.
증상:
- XAMPP Control Panel에 "Stopped" 표시
- Start 누르면 잠깐 켜졌다가 즉시 죽음
- 에러 로그에
[ERROR] InnoDB: The innodb_system data file 'ibdata1' must be writable
해결:
- 현재 실행 중인지 확인:
Get-Process mysqld - 살아있으면 Start 버튼 누르지 말 것
- 영구 해결책: my.ini 의 pid_file을 절대 경로로 변경 (이미 적용됨):
[mysqld] pid_file="C:/xampp/mysql/data/mysql.pid"
- 가장 안정적: Windows 서비스로 등록 (관리자 권한 필요)
원인 A: 시스템 환경변수 OLLAMA_HOST=0.0.0.0이 설정되어 있는데 Python이 이걸 가져다가 사용. 0.0.0.0은 서버 바인드 주소이지 클라이언트 접속 주소가 아니므로 연결 실패.
해결 (이미 적용됨):
config.py의load_dotenv(override=True)로.env우선_validated_ollama_host()함수가0.0.0.0→127.0.0.1자동 변환
원인 B: STT 처리 중 CPU 100% 점유로 Ollama가 일시적으로 응답 못함.
해결:
summarizer.py가OLLAMA_TIMEOUT_SEC동안 첫 토큰/다음 청크를 기다린 뒤 실패 처리- step 3에서 처음 60~90초 출력이 없는 것은 모델 콜드 로딩이면 정상
- 발화 캐시(
*.{파일지문}.segments.json) 덕분에 실패해도 재실행 시 STT 안 다시 함
원인: HuggingFace가 모델 다운로드 시 캐시에 심볼릭 링크를 만드는데, Windows 일반 사용자는 권한 없음.
해결 (이미 적용됨): scripts/download_models.py가 우리 프로젝트 폴더에 직접 다운로드 (심볼릭 링크 없이). transcriber.py는 로컬 폴더를 우선 사용.
만약 다시 발생하면:
python scripts\download_models.py원인: 그누보드5 common.php는 $_SERVER['SCRIPT_FILENAME']을 기준으로 자기 경로를 계산하므로, 외부 폴더에서 require하면 경로 계산이 깨짐 → dbconfig.php를 못 찾음.
해결 (이미 적용됨): _load_gnuboard5.php가 $_SERVER['SCRIPT_FILENAME']을 임시로 그누보드5의 index.php 경로로 위장하고 require.
원인 A: meeting 게시판이 만들어지지 않음.
해결: API 헬스 체크로 확인
curl -H "X-API-Token: YOUR_TOKEN" http://127.0.0.1/gnuboard5/plugin/meeting_api/health.php
# board_exists: true 인지 확인false면 SQL로 생성:
SET SESSION sql_mode='';
INSERT INTO g5_board SELECT * FROM g5_board WHERE bo_table='free';
UPDATE g5_board SET bo_table = 'meeting', bo_subject='회의록',
bo_count_write=0, bo_count_comment=0 WHERE bo_table='free' LIMIT 1;
CREATE TABLE g5_write_meeting LIKE g5_write_free;원인 B: 비회원 글 작성이 막혀있음.
해결: 그누보드5 관리자(http://127.0.0.1/gnuboard5/adm/)에서 meeting 게시판의 글쓰기 권한을 "모두"로 변경.
원인: 기본 Ollama context window가 4096 토큰. 긴 회의는 입력이 잘림.
해결 (이미 적용됨): summarizer.py가 transcript 길이를 보고 동적으로 num_ctx를
4096~OLLAMA_NUM_CTX_MAX 범위에서 자동 조정합니다. 기본값은 32768이며, 메모리가 부족한
PC에서는 .env에서 낮출 수 있습니다.
OLLAMA_NUM_CTX_MAX=8192
OLLAMA_NUM_PREDICT=4096원인: faster-whisper, pyannote, speechbrain 모델이 같은 Python 프로세스에서 RAM/VRAM을
붙잡고 있는 상태에서 Ollama가 gemma4:e2b-it-qat 같은 모델을 로드하려고 하면 GPU 적재 실패
후 CPU fallback도 메모리 부족으로 진행이 멈출 수 있습니다. RTX 3050 Laptop 4GB 환경에서
특히 자주 발생합니다.
해결 (이미 적용됨):
- STT/화자분리 직후 faster-whisper, pyannote, speechbrain 캐시와 CUDA 캐시를 정리합니다.
- Ollama 호출 전 free RAM이
OLLAMA_MIN_FREE_RAM_GB보다 낮으면 요약을 시작하지 않고 STT 캐시만 남긴 뒤 종료합니다. .env에서OLLAMA_KEEP_ALIVE=0으로 요약 후 Ollama 모델을 바로 해제합니다.
권장 조치:
python doctor.pyOllama 실행 전 여유 RAM 부족이 보이면 브라우저, IDE, 다른 Python 프로세스, 기존
Ollama 모델을 종료한 뒤 다시 실행하세요.
ollama stop gemma4:e2b-it-qat그래도 부족하면 .env에서 다음처럼 낮춥니다.
OLLAMA_NUM_CTX_MAX=8192
OLLAMA_NUM_PREDICT=4096
WHISPER_BATCH_SIZE=4
# 마지막 수단: 더 작은 LLM 사용
# OLLAMA_MODEL=qwen3:4b증상: 메모리 정리 로그는 정상 출력되고, Python 워커도 chat() 호출까지 갔지만
생성 중... 청크가 전혀 나오지 않습니다. 첫 stuck 이후 Ollama 서버 내부 스케줄러에
잔존 상태가 남아 새 요청이 계속 대기하는 좀비 상태일 수 있습니다.
정상 대기와 구분:
- 정상:
python watcher.py재실행 후 첫 요약에서 60~90초 정도 조용하다가생성 중... N청크가 출력됨 - 비정상:
OLLAMA_TIMEOUT_SEC가 지날 때까지 첫 청크가 전혀 없고 요약 실패로 종료됨
해결:
ollama stop gemma4:e2b-it-qat
# 그래도 안 풀리면 Ollama 앱/서비스 재시작 후
python watcher.py이미 STT 캐시가 저장된 파일은 다음 실행 때 발화 캐시를 사용하므로 faster-whisper를 다시 돌리지
않고 요약 단계부터 이어갈 수 있습니다. 메모리 여유가 충분한 PC에서 연속 파일 처리 속도가
중요하면 OLLAMA_KEEP_ALIVE=10m처럼 모델을 잠시 유지할 수 있지만, 저메모리 환경에서는
OLLAMA_KEEP_ALIVE=0을 유지하세요.
원인: Ollama가 정상 연결은 됐지만 JSON 대신 ## 같은 마크다운 조각만 반환한 경우입니다.
긴 회의에서 OLLAMA_NUM_CTX_MAX가 부족해 JSON 출력 지시가 context 밖으로 잘렸거나, 모델이
JSON 출력 지시를 놓쳤거나, 이전 요청 실패 뒤 모델 상태가 불안정할 때 발생할 수 있습니다.
해결 (이미 적용됨):
- JSON 파싱 실패 시 즉시 성공 처리하지 않고 재시도합니다.
- 재시도 후에도 JSON이 아니면 DB 저장과 그누보드5 업로드 전에 실패로 종료합니다.
- 긴 회의는
OLLAMA_SUMMARY_CHUNK_SEC기준으로 구간별 상세 요약을 만든 뒤 최종 통합합니다. - 최종 요약에 6개 필수 섹션이 없거나 본문이 너무 짧으면 업로드하지 않습니다.
- transcript가 현재
OLLAMA_NUM_CTX_MAX보다 크면 Ollama 호출 전에 필요한 권장 context를 안내하고 중단합니다. - STT 캐시는 남아 있으므로 다음 실행은 음성 인식부터 다시 하지 않습니다.
이미 실패 요약이 올라간 게시글은 그누보드5 관리자에서 삭제한 뒤 다시 실행하세요. 계속 반복되면 다음처럼 context와 출력 길이를 조정하거나 Ollama 모델을 재시작합니다.
ollama stop gemma4:e2b-it-qat
python watcher.pyOLLAMA_NUM_CTX_MAX=32768
OLLAMA_NUM_PREDICT=8192
OLLAMA_SUMMARY_CHUNK_SEC=900증상: 업로드는 성공했지만 URL이
/gnuboard5/index.php/.../bbs/board.php?...처럼 실제 그누보드5 공개 경로와 다르게 출력됩니다.
원인: 일부 호스팅 환경에서 그누보드5의 G5_BBS_URL 상수가 서버 내부 경로나
SCRIPT_NAME 보정값을 섞어 계산하는 경우가 있습니다.
해결 (이미 적용됨):
- Python 클라이언트가
G5_API_BASE에서 공개 그누보드5 루트를 계산해 게시글 URL을 보정합니다. - PHP 플러그인도
G5_BBS_URL에 의존하지 않고/plugin/meeting_api앞 경로를 기준으로 URL을 만듭니다. - 그래도 특수 호스팅에서 어긋나면
config.local.php에 공개 루트를 직접 지정하세요.
define('meeting_PUBLIC_BASE_URL', 'https://YOUR-DOMAIN/gnu5624');-
API 토큰 강화:
gnuboard5\plugin\meeting_api\config.local.phpdefine('meeting_API_TOKEN', '강력한-48자-이상-랜덤-문자열'); define('meeting_API_DEBUG', false); // 디버그 정보 노출 차단 define('meeting_API_ALLOW_SETUP', false); // 설치 후 비활성화 // define('meeting_API_ALLOWED_IPS', '203.0.113.10,203.0.113.0/24');
동일 토큰을 프로젝트 폴더의
.env에 있는G5_API_TOKEN에도 설정.토큰 생성:
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))
-
그누보드5 관리자 계정: 첫 가입 회원이 자동 최고관리자. 가입:
http://127.0.0.1/gnuboard5/bbs/register.php -
MariaDB root 비밀번호: XAMPP 기본은 빈 값. 변경 권장.
ALTER USER 'root'@'localhost' IDENTIFIED BY '새비밀번호';
회의 데이터를 잃지 않으려면 정기 백업:
| 항목 | 경로 | 주기 |
|---|---|---|
| 로컬 DB | .\data\meetings.db |
매주 |
| 원본 음성 | .\data\uploads\ |
매월 |
| 그누보드5 DB | meeting (MariaDB) |
매주 |
| 그누보드5 첨부파일 | <그누보드5루트>\data\file\meeting\ |
매월 |
MariaDB 백업:
& "C:\xampp\mysql\bin\mysqldump.exe" -u root meeting > backup.sql장기 운영 시 디스크 사용 추정:
| 항목 | 1회 평균 | 100회 누적 |
|---|---|---|
| AI 모델 (고정) | ~6GB | 6GB |
| 원본 음성 (1시간) | ~60MB | 6GB |
| WAV (1시간) | ~115MB | 11.5GB |
| 캐시 JSON | ~50KB | 5MB |
| DB | ~수십KB | ~수MB |
→ 100회 회의 기준 약 24GB. 정기적으로 data\work\*.wav 정리 권장 (캐시 JSON은 유지).
# 1. 모든 서비스 상태
Get-Process mysqld, httpd, ollama -ErrorAction SilentlyContinue
Invoke-WebRequest http://127.0.0.1:11434/api/version
Invoke-WebRequest http://127.0.0.1/gnuboard5/plugin/meeting_api/health.php `
-Headers @{ "X-API-Token" = "YOUR_TOKEN" }
# 2. 디스크 사용량
Get-ChildItem .\data -Recurse |
Measure-Object -Property Length -Sum
# 3. 미동기화 회의 확인
python main.py --resync[사용자] [PC] [브라우저]
│ │ │
│ 회의 녹음 │ │
├──────────────────────────────────►│ │
│ │ python main.py meeting.mp3 │
│ ├─►[1] ffmpeg: mp3→wav │
│ ├─►[2] faster-whisper: 음성→텍스트 │
│ │ speechbrain: 화자 분리 │
│ ├─►[3] Ollama gemma4:e2b-it-qat: 요약 │
│ ├─►[4] SQLite 저장 │
│ ├─►[5] 콘솔에 미리보기 출력 │
│ ├─►[6] HTTP POST │
│ │ ↓ │
│ │ PHP API → MariaDB INSERT │
│ │ │
│ "완료. wr_id=8" │ │
│◄──────────────────────────────────┤ │
│ │ │
│ 브라우저에서 결과 확인 │ │
├───────────────────────────────────────────────────────────────────────►│
│ │ ↓
│ │ 그누보드5 게시판
│ │ 회의록 게시글 + 댓글