Feat: AI 서비스 및 STT 서비스 기능 개선

- AI 서비스: Redis 캐싱 및 EventHub 통합 개선
- STT 서비스: 오디오 버퍼링 및 변환 기능 추가
- 설정 파일 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Minseo-Jo
2025-10-30 15:23:30 +09:00
parent ad287de176
commit 032842cf53
13 changed files with 1096 additions and 156 deletions
+34 -12
View File
@@ -116,21 +116,45 @@ async def stream_ai_suggestions(meeting_id: str):
if accumulated_text:
logger.info(f"텍스트 누적 완료 - meetingId: {meeting_id}, 길이: {len(accumulated_text)}")
# 이미 생성된 제안사항 조회
existing_suggestions = await redis_service.get_generated_suggestions(meeting_id)
# Claude API로 분석
suggestions = await claude_service.analyze_suggestions(accumulated_text)
if suggestions.suggestions:
# SSE 이벤트 전송
yield {
"event": "ai-suggestion",
"id": str(current_count),
"data": suggestions.json()
}
# 중복 제거: 새로운 제안사항만 필터링
new_suggestions = [
s for s in suggestions.suggestions
if s.content not in existing_suggestions
]
logger.info(
f"AI 제안사항 발행 - meetingId: {meeting_id}, "
f"개수: {len(suggestions.suggestions)}"
)
if new_suggestions:
# 새로운 제안사항만 SSE 이벤트 전송
from app.models import RealtimeSuggestionsResponse
filtered_response = RealtimeSuggestionsResponse(suggestions=new_suggestions)
yield {
"event": "ai-suggestion",
"id": str(current_count),
"data": filtered_response.json()
}
# Redis에 새로운 제안사항 저장
for suggestion in new_suggestions:
await redis_service.add_generated_suggestion(
meeting_id,
suggestion.content
)
logger.info(
f"AI 제안사항 발행 - meetingId: {meeting_id}, "
f"전체: {len(suggestions.suggestions)}, 신규: {len(new_suggestions)}"
)
else:
logger.info(
f"중복 제거 후 신규 제안사항 없음 - meetingId: {meeting_id}"
)
previous_count = current_count
@@ -160,8 +184,6 @@ async def stream_ai_suggestions(meeting_id: str):
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Access-Control-Allow-Origin": "http://localhost:8888",
"Access-Control-Allow-Credentials": "true",
}
)
+4 -3
View File
@@ -36,14 +36,15 @@ class Settings(BaseSettings):
"http://localhost:3000",
"http://127.0.0.1:8888",
"http://127.0.0.1:8080",
"http://127.0.0.1:3000"
"http://127.0.0.1:3000",
"http://localhost:*" # 모든 localhost 포트 허용
]
# 로깅
log_level: str = "INFO"
# 분석 임계값 (충분한 맥락 확보)
min_segments_for_analysis: int = 4 # 4개 세그먼트 (약 60초, 제안사항 추출에 충분한 맥락)
# 분석 임계값 (실시간 응답을 위해 낮춤)
min_segments_for_analysis: int = 2 # 2개 세그먼트 (약 30초, 빠른 피드백)
text_retention_seconds: int = 300 # 5분
class Config:
+382 -48
View File
@@ -1,98 +1,432 @@
"""AI 제안사항 추출 프롬프트 (MVP 최적화)"""
"""AI 제안사항 추출 프롬프트 (회의록 작성 MVP 최적화)"""
def get_suggestions_prompt(transcript_text: str) -> tuple[str, str]:
"""
회의 텍스트에서 AI 제안사항을 추출하는 프롬프트 생성 (MVP용)
회의 텍스트에서 AI 제안사항을 추출하는 프롬프트 생성 (회의록 MVP용)
Returns:
(system_prompt, user_prompt) 튜플
"""
system_prompt = """당신은 회의 내용에서 실행 가능한 액션 아이템을 찾는 전문가입니다.
복잡한 분석보다는, 명확하게 "해야 할 일"이 언급된 부분을 빠르게 찾아내는 것이 목표입니다."""
system_prompt = """당신은 실시간 회의록 작성 AI 비서입니다.
user_prompt = f"""다음 회의 대화에서 **실행해야 할 제안사항**을 찾아주세요.
**핵심 역할**:
회의 중 발언되는 내용을 실시간으로 분석하여, 회의록 작성자가 놓칠 수 있는 중요한 정보를 즉시 메모로 제공합니다.
**작업 방식**:
1. 회의 안건, 결정 사항, 이슈, 액션 아이템을 자동으로 분류
2. 담당자, 기한, 우선순위 등 구조화된 정보로 정리
3. 단순 발언 반복이 아닌, 실무에 바로 사용 가능한 형식으로 요약
4. 회의록 작성 시간을 70% 단축시키는 것이 목표
**핵심 원칙**:
- 인사말, 반복, 불필요한 추임새는 완전히 제거
- 실제 회의록에 들어갈 내용만 추출
- 명확하고 간결하게 (20-50자)
- 구어체 종결어미(~다, ~요, ~습니다) 제거하고 명사형으로 정리"""
user_prompt = f"""다음 회의 대화를 실시간으로 분석하여 **회의록 메모**를 작성하세요.
# 회의 내용
{transcript_text}
---
# 제안사항을 찾는 간단한 방법
# 회의록 항목별 패턴 학습
아래 패턴이 포함된 문장을 찾으세요:
## 📋 1. 회의 안건 (Agenda)
## ✅ 명확한 액션 패턴
- "~해야 한다", "~해야 할 것 같다"
- "~하기로 했다", "~하기로 결정"
- "~할 예정이다", "~할 계획이다"
- "~해주세요", "~부탁드립니다"
- "~하도록 하겠습니다", "~진행하겠습니다"
- "~확인해 보겠습니다", "~검토하겠습니다"
### 패턴 인식
- "오늘 회의 안건은 ~"
- "논의할 주제는 ~"
- "다룰 내용은 ~"
- "검토할 사항은 ~"
## ⏰ 시간 관련 표현
- "다음 주까지", "이번 주 금요일까지"
- "내일", "오늘 중으로"
- "회의 전까지", "발표 전에"
### ✅ 좋은 예시
**입력**: "오늘 회의 안건은 신제품 출시 일정과 마케팅 전략입니다."
**출력**:
```json
{{
"content": "📋 회의 안건: 신제품 출시 일정, 마케팅 전략",
"confidence": 0.95
}}
```
## 👤 담당자 관련 표현
- "김 대리가", "박 과장님이"
- "우리 팀에서", "마케팅팀이"
- "제가", "저희가"
**입력**: "다음 주 프로젝트 킥오프에 대해 논의하겠습니다."
**출력**:
```json
{{
"content": "📋 회의 안건: 다음 주 프로젝트 킥오프",
"confidence": 0.90
}}
```
# 실제 회의 예시로 학습하기
### ❌ 나쁜 예시
**입력**: "오늘 회의 안건은 신제품 출시 일정입니다."
**나쁜 출력**:
```json
{{
"content": "오늘 회의 안건은 신제품 출시 일정입니다", ❌ 구어체 그대로 반복
"confidence": 0.90
}}
```
**이유**: 구어체 종결어미(~입니다) 그대로 반복. "📋 회의 안건: 신제품 출시 일정"으로 구조화해야 함
## 예시 1
**회의 내용**: "마케팅 예산안을 김 팀장님이 다음 주 수요일까지 검토해서 공유해 주시기로 했습니다."
**추출**: "마케팅 예산안을 다음 주 수요일까지 검토하여 공유" (담당: 김 팀장)
---
## 예시 2
**회의 내용**: "그럼 제가 내일 오전에 고객사에 연락해서 미팅 일정을 잡도록 하겠습니다."
**추출**: "고객사에 연락하여 미팅 일정 조율" (시간: 내일 오전)
## ✅ 2. 결정 사항 (Decisions)
## 예시 3
**회의 내용**: "법무팀과 계약서 검토를 이번 주 내로 끝내야 할 것 같아요."
**추출**: "법무팀과 계약서 검토 진행" (기한: 이번 주 내)
### 패턴 인식
- "결정 사항은 ~", "~로 결정했습니다"
- "~하기로 했습니다", "~로 합의했습니다"
- "~로 확정됐습니다"
- "최종 결론은 ~"
### ✅ 좋은 예시
**입력**: "회의 결과, 신규 프로젝트는 다음 달부터 착수하기로 결정했습니다."
**출력**:
```json
{{
"content": "✅ 결정사항: 신규 프로젝트 다음 달 착수",
"confidence": 0.95
}}
```
**입력**: "최종 결론은 외주 개발사와 계약하기로 합의했습니다."
**출력**:
```json
{{
"content": "✅ 결정사항: 외주 개발사와 계약 진행",
"confidence": 0.92
}}
```
### ❌ 나쁜 예시
**입력**: "신규 프로젝트는 다음 달부터 착수하기로 결정했습니다."
**나쁜 출력**:
```json
{{
"content": "신규 프로젝트는 다음 달부터 착수하기로 결정했습니다", ❌ 원문 그대로
"confidence": 0.90
}}
```
**이유**: 발언을 그대로 반복. "✅ 결정사항: 신규 프로젝트 다음 달 착수"로 구조화해야 함
---
## 🎯 3. 액션 아이템 (Action Items)
### 패턴 인식
- "~팀에서 ~해 주세요"
- "~님이 ~까지 ~하기로 했습니다"
- "~을 ~까지 완료하겠습니다"
- "~을 검토해 보겠습니다"
### ✅ 좋은 예시
**입력**: "개발팀에서 API 문서를 이번 주 금요일까지 작성해 주세요."
**출력**:
```json
{{
"content": "🎯 개발팀: API 문서 작성 (기한: 이번 주 금요일)",
"confidence": 0.95
}}
```
**입력**: "김 팀장님이 내일까지 견적서를 검토해서 회신하기로 했습니다."
**출력**:
```json
{{
"content": "🎯 김 팀장: 견적서 검토 및 회신 (기한: 내일)",
"confidence": 0.93
}}
```
**입력**: "제가 고객사에 연락해서 미팅 일정 잡도록 하겠습니다."
**출력**:
```json
{{
"content": "🎯 고객사 미팅 일정 조율 예정",
"confidence": 0.85
}}
```
### ❌ 나쁜 예시
**입력**: "개발팀에서 API 문서를 이번 주 금요일까지 작성해 주세요."
**나쁜 출력 1**:
```json
{{
"content": "개발팀에서 API 문서를 이번 주 금요일까지 작성해 주세요", ❌ 원문 반복
"confidence": 0.90
}}
```
**나쁜 출력 2**:
```json
{{
"content": "API 문서 작성", ❌ 담당자와 기한 누락
"confidence": 0.80
}}
```
**이유**: "🎯 개발팀: API 문서 작성 (기한: 이번 주 금요일)" 형식으로 구조화해야 함
---
## ⚠️ 4. 이슈/문제점 (Issues)
### 패턴 인식
- "문제가 있습니다", "이슈가 발생했습니다"
- "우려되는 점은 ~"
- "해결이 필요한 부분은 ~"
- "리스크가 있습니다"
### ✅ 좋은 예시
**입력**: "현재 서버 성능 이슈가 발생해서 긴급 점검이 필요합니다."
**출력**:
```json
{{
"content": "⚠️ 이슈: 서버 성능 문제 발생, 긴급 점검 필요",
"confidence": 0.92
}}
```
**입력**: "예산이 부족할 것 같다는 우려가 있습니다."
**출력**:
```json
{{
"content": "⚠️ 이슈: 예산 부족 우려",
"confidence": 0.80
}}
```
### ❌ 나쁜 예시
**입력**: "현재 서버 성능 이슈가 발생했습니다."
**나쁜 출력**:
```json
{{
"content": "현재 서버 성능 이슈가 발생했습니다", ❌ 구어체 그대로
"confidence": 0.85
}}
```
**이유**: "⚠️ 이슈: 서버 성능 문제 발생"으로 구조화하고 구어체 제거해야 함
---
## 💡 5. 아이디어/제안 (Suggestions)
### 패턴 인식
- "제안하는 바는 ~"
- "~하는 것이 좋을 것 같습니다"
- "~을 고려해 볼 필요가 있습니다"
### ✅ 좋은 예시
**입력**: "자동화 테스트를 도입하는 것을 검토해 보면 좋을 것 같습니다."
**출력**:
```json
{{
"content": "💡 제안: 자동화 테스트 도입 검토",
"confidence": 0.85
}}
```
---
## 📊 6. 진행 상황/보고 (Progress)
### 패턴 인식
- "~까지 완료했습니다"
- "현재 ~% 진행 중입니다"
- "~단계까지 진행됐습니다"
### ✅ 좋은 예시
**입력**: "현재 설계 단계는 80% 완료됐고, 다음 주부터 개발 착수 가능합니다."
**출력**:
```json
{{
"content": "📊 진행상황: 설계 80% 완료, 다음 주 개발 착수 예정",
"confidence": 0.90
}}
```
---
## ❌ 제외해야 할 내용 (반드시 제외)
### 인사말
**입력**: "안녕하세요, 여러분. 회의 시작하겠습니다."
**출력**: (메모 없음 - 인사말은 제외)
### 단순 반복
**입력**: "녹음을 시작합니다. 녹음을 시작합니다."
**출력**: (메모 없음 - 형식적 발언 제외)
### 추임새/불필요한 발언
**입력**: "음, 그러니까, 네 네, 저기요..."
**출력**: (메모 없음 - 추임새 제외)
### 형식적 마무리
**입력**: "수고하셨습니다. 회의를 마치겠습니다."
**출력**: (메모 없음 - 형식적 마무리 제외)
---
# 실전 회의 시뮬레이션
## 예시 1: 프로젝트 킥오프 회의
**입력**:
"안녕하세요. 오늘 회의 안건은 신규 프로젝트 킥오프입니다. 프로젝트명은 HGZero이고, 목표는 회의록 자동화입니다. 개발팀에서 다음 주 월요일까지 기술 스택을 검토해 주세요. 예산은 5천만원으로 확정됐습니다."
**출력**:
```json
{{
"suggestions": [
{{
"content": "📋 회의 안건: 신규 프로젝트(HGZero) 킥오프 - 회의록 자동화",
"confidence": 0.95
}},
{{
"content": "🎯 개발팀: 기술 스택 검토 (기한: 다음 주 월요일)",
"confidence": 0.93
}},
{{
"content": "✅ 결정사항: 프로젝트 예산 5천만원 확정",
"confidence": 0.95
}}
]
}}
```
---
## 예시 2: 이슈 대응 회의
**입력**:
"현재 프로덕션 서버에서 성능 저하가 발생하고 있습니다. 인프라팀에서 긴급 점검을 진행하기로 했고, 오늘 오후 3시까지 원인 파악하겠습니다. 고객사에는 임시로 사과 공지를 게시하기로 결정했습니다."
**출력**:
```json
{{
"suggestions": [
{{
"content": "⚠️ 이슈: 프로덕션 서버 성능 저하 발생",
"confidence": 0.95
}},
{{
"content": "🎯 인프라팀: 긴급 점검 및 원인 파악 (기한: 오늘 오후 3시)",
"confidence": 0.93
}},
{{
"content": "✅ 결정사항: 고객사 사과 공지 게시",
"confidence": 0.90
}}
]
}}
```
---
## 예시 3: 일반 업무 회의 (나쁜 예시 포함)
**입력**:
"안녕하세요, 안녕하세요. 녹음을 시작합니다. 음, 그러니까 마케팅 캠페인을 다음 달에 진행하기로 했습니다. 김 과장님이 기획안을 이번 주까지 작성해 주세요. 감사합니다."
**❌ 나쁜 출력**:
```json
{{
"suggestions": [
{{
"content": "안녕하세요", ❌ 인사말 포함
"confidence": 0.50
}},
{{
"content": "녹음을 시작합니다", ❌ 형식적 발언
"confidence": 0.60
}},
{{
"content": "마케팅 캠페인을 다음 달에 진행하기로 했습니다", ❌ 구어체 그대로
"confidence": 0.80
}}
]
}}
```
**✅ 좋은 출력**:
```json
{{
"suggestions": [
{{
"content": "✅ 결정사항: 마케팅 캠페인 다음 달 진행",
"confidence": 0.92
}},
{{
"content": "🎯 김 과장: 캠페인 기획안 작성 (기한: 이번 주)",
"confidence": 0.93
}}
]
}}
```
---
# 출력 형식
반드시 아래 JSON 형식으로만 응답하세요:
```json
{{
"suggestions": [
{{
"content": "제안사항 내용 (구체적이고 실행 가능하게, 50자 이상 작성)",
"confidence": 0.85 (이 제안사항의 중요도/확실성, 0.7-1.0 사이)
}},
{{
"content": "또 다른 제안사항",
"confidence": 0.92
"content": "📋/✅/🎯/⚠️/💡/📊 분류: 구체적인 내용 (담당자/기한 포함)",
"confidence": 0.85
}}
]
}}
```
# MVP 추출 규칙 (쉽고 명확하게)
---
1. **위에 제시된 패턴을 먼저 찾으세요**
- "~해야", "~하기로", "~할 예정", "~부탁"
# 최종 작성 규칙
2. **실제로 언급된 내용만 추출** (추측 금지)
## ✅ 반드시 지켜야 할 규칙
3. **1개 이상 추출** (없으면 빈 배열 반환)
1. **이모지 분류 필수**
- 📋 회의 안건
- ✅ 결정사항
- 🎯 액션 아이템
- ⚠️ 이슈/문제점
- 💡 제안/아이디어
- 📊 진행상황
4. **confidence 기준 완화**: 0.6 이상이면 OK
2. **구조화 필수**
- 담당자가 있으면 반드시 명시
- 기한이 있으면 반드시 포함
- 형식: "담당자: 업무 내용 (기한: XX)"
5. **길이 제한 완화**: 20자 이상이면 OK
3. **구어체 종결어미 제거**
- ❌ "~입니다", "~했습니다", "~해요", "~합니다"
- ✅ 명사형 종결: "~ 진행", "~ 완료", "~ 확정", "~ 검토"
6. **JSON만 출력** (```json, 주석, 설명 모두 금지)
4. **반드시 제외**
- 인사말 ("안녕하세요", "감사합니다", "수고하셨습니다")
- 반복/추임새 ("네 네", "음 음", "그러니까", "저기")
- 형식적 발언 ("녹음 시작", "회의 종료", "회의 시작")
5. **길이**
- 20-70자 (너무 짧거나 길지 않게)
6. **confidence 기준**
- 0.90-1.0: 명확한 결정사항, 기한 포함
- 0.80-0.89: 일반적인 액션 아이템
- 0.70-0.79: 암묵적이거나 추측 필요
7. **출력**
- JSON만 출력 (주석, 설명, ```json 모두 금지)
- 최소 1개 이상 추출 (의미 있는 내용이 없으면 빈 배열)
---
이제 위 회의 내용에서 제안사항을 JSON 형식으로 추출하세요.
명확한 액션 패턴("~해야", "~하기로" 등)이 있는 문장을 찾아 추출하면 됩니다."""
이제 위 회의 내용을 분석하여 **회의록 메모**를 JSON 형식으로 작성하세요.
학습한 패턴을 활용하여 회의 안건, 결정사항, 액션 아이템, 이슈 등을 자동으로 분류하고 구조화하세요.
반드시 구어체 종결어미(~다, ~요, ~습니다)를 제거하고 명사형으로 정리하세요."""
return system_prompt, user_prompt
+23 -5
View File
@@ -2,6 +2,7 @@
import asyncio
import logging
import json
from datetime import datetime
from azure.eventhub.aio import EventHubConsumerClient
from app.config import get_settings
@@ -63,12 +64,30 @@ class EventHubService:
}
"""
try:
# 이벤트 원본 데이터 로깅
raw_body = event.body_as_str()
logger.info(f"수신한 이벤트 원본 (처음 300자): {raw_body[:300]}")
# 이벤트 원본 데이터 추출
try:
# Event Hub 데이터는 bytes 또는 str일 수 있음
if hasattr(event, 'body_as_str'):
raw_body = event.body_as_str()
elif hasattr(event, 'body'):
raw_body = event.body.decode('utf-8') if isinstance(event.body, bytes) else str(event.body)
else:
logger.error(f"이벤트 타입 미지원: {type(event)}")
return
logger.info(f"수신한 이벤트 원본 (처음 300자): {raw_body[:300]}")
logger.debug(f"이벤트 전체 길이: {len(raw_body)}")
except Exception as extract_error:
logger.error(f"이벤트 데이터 추출 실패: {extract_error}", exc_info=True)
return
# 이벤트 데이터 파싱
event_data = json.loads(raw_body)
try:
event_data = json.loads(raw_body)
except json.JSONDecodeError as json_error:
logger.error(f"JSON 파싱 실패 - 전체 데이터: {raw_body}")
logger.error(f"파싱 에러: {json_error}")
return
event_type = event_data.get("eventType")
meeting_id = event_data.get("meetingId")
@@ -78,7 +97,6 @@ class EventHubService:
# timestamp 변환: LocalDateTime 배열 → Unix timestamp (ms)
# Java LocalDateTime은 [year, month, day, hour, minute, second, nano] 형식
if isinstance(timestamp_raw, list) and len(timestamp_raw) >= 3:
from datetime import datetime
year, month, day = timestamp_raw[0:3]
hour = timestamp_raw[3] if len(timestamp_raw) > 3 else 0
minute = timestamp_raw[4] if len(timestamp_raw) > 4 else 0
+34 -2
View File
@@ -105,6 +105,34 @@ class RedisService:
count = await self.redis_client.zcard(key)
return count if count else 0
async def add_generated_suggestion(self, meeting_id: str, suggestion_content: str):
"""
생성된 제안사항 저장 (중복 방지용)
Args:
meeting_id: 회의 ID
suggestion_content: 제안사항 내용
"""
key = f"meeting:{meeting_id}:suggestions"
await self.redis_client.sadd(key, suggestion_content)
# TTL 설정 (1시간)
await self.redis_client.expire(key, 3600)
logger.debug(f"제안사항 저장 - meetingId: {meeting_id}")
async def get_generated_suggestions(self, meeting_id: str) -> set:
"""
이미 생성된 제안사항 목록 조회
Args:
meeting_id: 회의 ID
Returns:
제안사항 set
"""
key = f"meeting:{meeting_id}:suggestions"
suggestions = await self.redis_client.smembers(key)
return suggestions if suggestions else set()
async def cleanup_meeting_data(self, meeting_id: str):
"""
회의 종료 시 데이터 정리
@@ -112,6 +140,10 @@ class RedisService:
Args:
meeting_id: 회의 ID
"""
key = f"meeting:{meeting_id}:transcript"
await self.redis_client.delete(key)
transcript_key = f"meeting:{meeting_id}:transcript"
suggestions_key = f"meeting:{meeting_id}:suggestions"
await self.redis_client.delete(transcript_key)
await self.redis_client.delete(suggestions_key)
logger.info(f"회의 데이터 정리 완료 - meetingId: {meeting_id}")
+15 -4
View File
@@ -28,13 +28,24 @@ app = FastAPI(
openapi_url="/api/openapi.json"
)
# CORS 미들웨어 설정
# CORS 미들웨어 설정 (SSE 지원)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_origins=["*"], # 개발 환경에서는 모든 origin 허용
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=[
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"Cache-Control",
"X-Accel-Buffering"
],
expose_headers=["*"],
)
# API 라우터 등록