Files
meeting-minutes/src/lib/live-summary.ts
T
nad4 0101e4b0d2 feat: 템플릿(6종) + 강도 조절(3단계) + 커스텀 프롬프트
회의록 외 다양한 용도로 쓸 수 있도록 출력 구조와 요약 깊이를 분리.

템플릿:
- 🗂️ 회의록 (표준, 라이브 O)
- 🎓 강의·세미나 노트 (상세, 라이브 O)
- 🤝 1:1 미팅 (표준, 라이브 O)
- 💡 브레인스토밍 (상세, 라이브 O)
- 🎤 인터뷰 (표준, 라이브 X — Q&A 포맷)
- 📝 원문 정리 (상세 고정, 라이브 X — 요약 없이 문단화만)
- ⚙️ 커스텀 (자유 프롬프트)

강도: 간결 / 표준 / 상세 — 템플릿별 기본값 프리셋, 언제든 override.

변경:
- src/lib/templates.ts 신규 — 프리셋 + 프롬프트 빌더
- minutes-generator / live-summary 가 buildPrompt 공유
- /api/summarize{-live} 가 template/depth/customPrompt 파라미터 수용
- page.tsx UI: 템플릿 픽커, 강도 라디오, 커스텀 textarea
- LiveRecorder 가 설정을 useLiveSummary 로 전달 (ref 기반 최신값)
- 단순 변환 모드는 기존 동작 유지 (템플릿 미적용)
- 프롬프트 빌더 단위 테스트 19개 (총 57 tests)
2026-04-21 19:38:24 +09:00

88 lines
2.3 KiB
TypeScript

import {
buildPrompt,
resolveDepth,
type SummaryDepth,
type TemplateId,
} from './templates'
type LiveSummaryResult =
| { success: true; markdown: string }
| { success: false; error: string; rateLimited?: boolean }
interface LiveSummaryOptions {
apiKey: string
template?: TemplateId
depth?: SummaryDepth
customPrompt?: string
fetchFn?: typeof fetch
}
export async function generateLiveSummary(
transcript: string,
options: LiveSummaryOptions,
): Promise<LiveSummaryResult> {
const { apiKey, fetchFn = fetch } = options
if (!apiKey || apiKey.trim().length === 0) {
return { success: false, error: 'Gemini API 키가 필요합니다.' }
}
const trimmed = transcript.trim()
if (trimmed.length === 0) {
return { success: false, error: '요약할 텍스트가 비어 있습니다.' }
}
const templateId = options.template ?? 'meeting'
const depth = resolveDepth(templateId, options.depth)
const prompt = buildPrompt({
templateId,
depth,
transcript: trimmed,
live: true,
customPrompt: options.customPrompt,
})
const url =
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent'
try {
const response = await fetchFn(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': apiKey,
},
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
}),
})
if (!response.ok) {
if (response.status === 429) {
return {
success: false,
error: 'Gemini 요청 한도 초과. 잠시 후 자동 재시도됩니다.',
rateLimited: true,
}
}
return {
success: false,
error: `Gemini API 호출 실패: ${response.status}`,
}
}
const data = await response.json()
const markdown: string =
data?.candidates?.[0]?.content?.parts?.[0]?.text ?? ''
if (markdown.trim().length === 0) {
return { success: false, error: '요약 결과가 비어 있습니다.' }
}
return { success: true, markdown }
} catch (err) {
const message = err instanceof Error ? err.message : '알 수 없는 오류'
return { success: false, error: `실시간 요약 중 오류: ${message}` }
}
}