- README.md: 전체 개요, 5개 하위 메뉴 구조, 아키텍처 - construction-photos.md: 공사현장 사진대지 (GCS, 행 구조, 음성입력) - meeting-minutes.md: 회의록 (STT 화자분리, Gemini AI 요약, 오디오 녹음) - planning-views.md: 견적/프로젝트/워크플로우 화면 명세 - INDEX.md: 문서 인덱스에 planning 등록
13 KiB
13 KiB
회의록 작성
작성일: 2026-03-06 상태: 운영 중 라우트:
/juil/meeting-minutes관련: README.md | 사진대지 | 뷰 화면
1. 개요
음성으로 회의 내용을 기록하고, Google STT(화자 분리) + Gemini AI(요약/결정사항/액션아이템) 로 자동 정리하는 회의록 시스템. 브라우저 MediaRecorder로 녹음하고, GCS에 오디오를 저장하며, 세그먼트(화자별 발화)를 관리한다.
2. 라우트
/juil/meeting-minutes
├── GET / → index (목록 페이지)
├── GET /list → list (JSON 목록)
├── POST / → store (새 회의록 생성)
├── POST /log-stt-usage → logSttUsage (STT 시간 기록)
├── GET /{id} → show (상세 조회 + segments)
├── PUT /{id} → update (메타데이터 수정)
├── DELETE /{id} → destroy (삭제)
├── POST /{id}/segments → saveSegments (세그먼트 저장)
├── POST /{id}/upload-audio → uploadAudio (오디오 업로드)
├── POST /{id}/summarize → summarize (AI 요약 생성)
├── POST /{id}/diarize → diarize (자동 화자 분리)
└── GET /{id}/download-audio → downloadAudio (오디오 다운로드)
3. 데이터베이스
3.1 meeting_minutes (회의록)
| 컬럼 | 타입 | 설명 |
|---|---|---|
id |
BIGINT PK | |
tenant_id |
BIGINT FK | 테넌트 격리 |
user_id |
BIGINT FK | 작성자 |
title |
VARCHAR(300) | 제목 (기본: "무제 회의록") |
folder |
VARCHAR(100) NULL | 폴더 분류 |
participants |
JSON NULL | 참여자 목록 배열 |
meeting_date |
DATE | 회의 날짜 |
meeting_time |
TIME NULL | 회의 시작 시간 |
duration_seconds |
INT UNSIGNED | 녹음 총 시간(초) |
audio_file_path |
VARCHAR(500) NULL | 오디오 GCS 경로 |
audio_gcs_uri |
VARCHAR(500) NULL | 오디오 GCS URI |
audio_file_size |
BIGINT UNSIGNED NULL | 오디오 파일 크기 (bytes) |
full_transcript |
LONGTEXT NULL | 전체 트랜스크립트 |
summary |
LONGTEXT NULL | AI 요약 |
decisions |
JSON NULL | 결정사항 배열 |
action_items |
JSON NULL | 액션아이템 배열 |
status |
VARCHAR(20) | 상태 (5가지) |
stt_language |
VARCHAR(10) | STT 언어 (기본: ko-KR) |
deleted_at |
TIMESTAMP NULL | 소프트 삭제 |
인덱스: tenant_id, user_id, (tenant_id, meeting_date), status
3.2 meeting_minute_segments (세그먼트)
| 컬럼 | 타입 | 설명 |
|---|---|---|
id |
BIGINT PK | |
meeting_minute_id |
BIGINT FK | 회의록 (cascade delete) |
segment_order |
INT UNSIGNED | 순서 |
speaker_name |
VARCHAR(100) | 화자 이름 (기본: "화자 1") |
speaker_label |
VARCHAR(20) NULL | 화자 라벨/번호 |
text |
TEXT | 발화 텍스트 |
start_time_ms |
INT UNSIGNED | 시작 시간 (ms, 기본: 0) |
end_time_ms |
INT UNSIGNED NULL | 종료 시간 (ms) |
is_manual_speaker |
BOOLEAN | 수동 화자 전환 여부 (기본: true) |
인덱스: meeting_minute_id, (meeting_minute_id, segment_order)
3.3 테이블 관계
meeting_minutes
│ 1:N
▼
meeting_minute_segments (segment_order ASC)
├── speaker_name (화자명)
├── text (발화 내용)
└── start_time_ms / end_time_ms (타임스탬프)
4. 상태 관리
4.1 상태값
| 상태 | 코드 | 색상 | 설명 |
|---|---|---|---|
| 초안 | DRAFT |
회색 | 생성 직후, 편집 가능 |
| 녹음중 | RECORDING |
빨강 | (클라이언트 상태) |
| 처리중 | PROCESSING |
노랑 | AI 요약/화자분리 처리 중 |
| 완료 | COMPLETED |
초록 | AI 처리 완료 |
| 실패 | FAILED |
빨강 | AI 처리 실패 |
4.2 상태 전이
DRAFT
↓ [오디오 업로드, 세그먼트 추가]
DRAFT (계속 편집)
↓ [summarize() 호출]
PROCESSING
↓
COMPLETED (성공) 또는 FAILED (실패)
DRAFT
↓ [diarize() 호출 → 화자 분리]
DRAFT (세그먼트 갱신, 상태 유지)
5. API 명세
5.1 목록 조회
GET /juil/meeting-minutes/list
| 파라미터 | 타입 | 설명 |
|---|---|---|
search |
string | 제목 검색 |
date_from |
date | 시작일 |
date_to |
date | 종료일 |
status |
string | 상태 필터 |
per_page |
int | 페이지당 건수 |
5.2 생성
POST /juil/meeting-minutes
| 필드 | 규칙 | 설명 |
|---|---|---|
title |
nullable, max:300 | 제목 (미입력 시 "무제 회의록") |
folder |
nullable, max:100 | 폴더 분류 |
participants |
nullable, array | 참여자 목록 |
meeting_date |
required, date | 회의 날짜 |
meeting_time |
nullable | 회의 시간 |
stt_language |
nullable, max:10 | STT 언어 (기본: ko-KR) |
5.3 세그먼트 저장
POST /juil/meeting-minutes/{id}/segments
{
"segments": [
{
"speaker_name": "김과장",
"speaker_label": "1",
"text": "블라인드 납기일 확인 필요합니다.",
"start_time_ms": 0,
"end_time_ms": 5000,
"is_manual_speaker": true
}
]
}
전처리: 빈 텍스트 필터링, 언더스코어 노이즈 제거, 다중 공백 정규화 자동 생성:
full_transcript=[화자명] 발화텍스트\n...형식
5.4 오디오 업로드
POST /juil/meeting-minutes/{id}/upload-audio
| 필드 | 규칙 | 설명 |
|---|---|---|
audio |
required, file | webm/mp3 등 |
duration_seconds |
required, integer, min:1 | 녹음 시간(초) |
5.5 AI 요약 생성
POST /juil/meeting-minutes/{id}/summarize
요청: 없음 (서버에서 full_transcript 사용)
응답 예시:
{
"success": true,
"message": "AI 요약이 완료되었습니다.",
"data": {
"summary": "블라인드 납품 일정과 현장 설치 계획을 논의했습니다...",
"decisions": [
"납품일을 3월 15일로 확정",
"현장 실측은 3월 10일 진행"
],
"action_items": [
{
"assignee": "김과장",
"task": "거래처에 납기 확인 연락",
"deadline": "2026-03-08"
}
],
"status": "COMPLETED"
}
}
5.6 자동 화자 분리
POST /juil/meeting-minutes/{id}/diarize
| 필드 | 설명 | 기본값 |
|---|---|---|
min_speakers |
최소 화자 수 | 2 |
max_speakers |
최대 화자 수 | 6 |
응답:
{
"success": true,
"message": "자동 화자 분리가 완료되었습니다. (3명 감지)",
"data": { /* Meeting with segments */ },
"speaker_count": 3
}
6. AI 통합 상세
6.1 화자 분리 (Diarization) 3단계 폴백
[1단계] Google STT V2 (Chirp2) ← 최우선
│ speechToTextWithDiarizationAuto()
│ 최신 모델, 높은 정확도
│ 도메인 용어 힌트 포함
│
↓ (실패 시)
[2단계] Google STT V1 (latest_long) ← 폴백
│ 안정적이지만 약간 덜 정확
│
↓ (1명만 인식 시)
[3단계] Gemini AI 화자 재분배
splitSpeakersWithGemini()
대화 맥락/호칭/질답 패턴/어투 변화 분석
2명 이상으로 재분배
6.2 요약 생성 (Gemini API)
입력: full_transcript (전체 트랜스크립트)
↓
Gemini API 호출
├── 모드 1: Vertex AI (projectId, region, JWT)
└── 모드 2: Google AI Studio (API key) ← 폴백
│
│ Temperature: 0.3 (결정적)
│ Max tokens: 4096
↓
출력 JSON:
{
"summary": "3-5문장 요약",
"decisions": ["결정사항 1", "..."],
"action_items": [
{ "assignee": "담당자", "task": "할일", "deadline": "기한" }
],
"keywords": ["키워드1", "..."]
}
6.3 Gemini 화자 재분배
Google STT가 1명만 인식할 때 Gemini로 대화 맥락 분석:
입력: 단일 화자 트랜스크립트 + 예상 화자 수
↓
Gemini 프롬프트:
- 대화 맥락 분석 (호칭, 질답, 어투 변화)
- 지정된 수의 화자로 분리
↓
출력: 화자별 세그먼트 배열
→ DB 세그먼트 교체
7. 오디오 관리 (GCS)
7.1 GCS 경로 패턴
meeting-minutes/{tenant_id}/{meeting_id}/{timestamp}.webm
7.2 녹음 흐름
브라우저 MediaRecorder API
├── navigator.mediaDevices.getUserMedia({ audio: true })
├── new MediaRecorder(stream)
├── recorder.ondataavailable → webm 블롭 수집
└── 녹음 종료 → FormData로 업로드
↓
POST /{id}/upload-audio
├── GCS 업로드
├── DB: audio_file_path, audio_gcs_uri, audio_file_size, duration_seconds
└── AiTokenHelper::saveGcsStorageUsage()
7.3 다운로드
GET /{id}/download-audio
→ GCS에서 파일 콘텐츠 다운로드
→ Content-Disposition: attachment; filename="{title}.webm"
8. 세그먼트 처리 로직
8.1 저장 시 전처리
// 1. 빈 텍스트 필터링
trim($segment['text']) !== ''
// 2. 언더스코어 노이즈 제거
str_replace('_', '', $text)
// 3. 다중 공백 정규화
preg_replace('/\s{2,}/', ' ', $text)
8.2 전체 트랜스크립트 자동 생성
[김과장] 블라인드 납기일 확인 필요합니다.
[박부장] 3월 15일로 확정합시다.
[김과장] 네, 거래처에 연락하겠습니다.
8.3 화자 분리 결과 세그먼트 변환
Google STT 결과 → MeetingMinuteSegment 변환:
{
segment_order: 순서,
speaker_name: "화자 N",
speaker_label: "N",
text: 발화 텍스트,
start_time_ms: 시작시간,
end_time_ms: 종료시간,
is_manual_speaker: false // 자동 분리
}
9. UI 구성 (React)
9.1 화자 색상
| 화자 | 배경색 | 뱃지색 |
|---|---|---|
| 화자 1 | bg-blue-50 |
bg-blue-100 text-blue-800 |
| 화자 2 | bg-green-50 |
bg-green-100 text-green-800 |
| 화자 3 | bg-purple-50 |
bg-purple-100 text-purple-800 |
| 화자 4 | bg-orange-50 |
bg-orange-100 text-orange-800 |
9.2 지원 언어
| 코드 | 라벨 |
|---|---|
ko-KR |
한국어 |
en-US |
English |
ja-JP |
日本語 |
zh-CN |
中文 |
10. 사용량 추적
| 추적 항목 | 레이블 | Helper |
|---|---|---|
| Web Speech API 사용 | 회의록-음성인식 |
AiTokenHelper::saveSttUsage() |
| Google STT V1 화자 분리 | 회의록-화자분리 |
AiTokenHelper::saveSttUsage() |
| Google STT V2 화자 분리 | 회의록-화자분리(Chirp2) |
AiTokenHelper::saveSttUsage() |
| GCS 오디오 저장 | 회의록-GCS저장 |
AiTokenHelper::saveGcsStorageUsage() |
| Gemini 요약/분리 | 회의록-AI요약 |
AiTokenHelper::saveGeminiUsage() |
11. 모델 메서드
11.1 MeetingMinute
user() # BelongsTo User
segments() # HasMany Segment (segment_order ASC)
getFormattedDurationAttribute() # "H:MM:SS" 또는 "MM:SS"
Cast: participants, decisions, action_items → array, meeting_date → date
11.2 MeetingMinuteService
# CRUD
getList(array $filters) # 검색/필터 목록
create(array $data) # 생성 (DRAFT)
update(MeetingMinute, array $data) # 수정
delete(MeetingMinute) # GCS 삭제 → soft delete
# 세그먼트
saveSegments(MeetingMinute, array $segments) # 전처리 + 저장 + 트랜스크립트 생성
uploadAudio(MeetingMinute, UploadedFile, int $seconds) # GCS 업로드
logSttUsage(int $seconds) # STT 사용량 기록
# AI
generateSummary(MeetingMinute) # Gemini 요약 생성
processDiarization(MeetingMinute, int $min, int $max) # 3단계 화자 분리
splitSpeakersWithGemini(string $text, int $expected) # Gemini 화자 재분배
관련 문서
- README.md — 기획 메뉴 전체 개요
- 공사현장 사진대지 — GCS 파일 관리, 음성 입력
- 견적/프로젝트/워크플로우 — 화면 명세
최종 업데이트: 2026-03-06