Enterprise Meeting Knowledge Management System with: Backend (FastAPI): - Authentication proxy with JWT (pj-auth-api integration) - MySQL database with 4 tables (users, meetings, conclusions, actions) - Meeting CRUD with system code generation (C-YYYYMMDD-XX, A-YYYYMMDD-XX) - Dify LLM integration for AI summarization - Excel export with openpyxl - 20 unit tests (all passing) Client (Electron): - Login page with company auth - Meeting list with create/delete - Meeting detail with real-time transcription - Editable transcript textarea (single block, easy editing) - AI summarization with conclusions/action items - 5-second segment recording (efficient for long meetings) Sidecar (Python): - faster-whisper medium model with int8 quantization - ONNX Runtime VAD (lightweight, ~20MB vs PyTorch ~2GB) - Chinese punctuation processing - OpenCC for Traditional Chinese conversion - Anti-hallucination parameters - Auto-cleanup of temp audio files OpenSpec: - add-meeting-assistant-mvp (47 tasks, archived) - add-realtime-transcription (29 tasks, archived) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
from fastapi import APIRouter, HTTPException, Depends
|
|
import httpx
|
|
import json
|
|
|
|
from ..config import settings
|
|
from ..models import SummarizeRequest, SummarizeResponse, ActionItemCreate, TokenPayload
|
|
from .auth import get_current_user
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/ai/summarize", response_model=SummarizeResponse)
|
|
async def summarize_transcript(
|
|
request: SummarizeRequest, current_user: TokenPayload = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Send transcript to Dify for AI summarization.
|
|
Returns structured conclusions and action items.
|
|
"""
|
|
if not settings.DIFY_API_KEY:
|
|
raise HTTPException(status_code=503, detail="Dify API not configured")
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.post(
|
|
f"{settings.DIFY_API_URL}/chat-messages",
|
|
headers={
|
|
"Authorization": f"Bearer {settings.DIFY_API_KEY}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
json={
|
|
"inputs": {},
|
|
"query": request.transcript,
|
|
"response_mode": "blocking",
|
|
"user": current_user.email,
|
|
},
|
|
timeout=120.0, # Long timeout for LLM processing
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
raise HTTPException(
|
|
status_code=response.status_code,
|
|
detail=f"Dify API error: {response.text}",
|
|
)
|
|
|
|
data = response.json()
|
|
answer = data.get("answer", "")
|
|
|
|
# Try to parse structured JSON from Dify response
|
|
parsed = parse_dify_response(answer)
|
|
|
|
return SummarizeResponse(
|
|
conclusions=parsed["conclusions"],
|
|
action_items=[
|
|
ActionItemCreate(
|
|
content=item.get("content", ""),
|
|
owner=item.get("owner", ""),
|
|
due_date=item.get("due_date"),
|
|
)
|
|
for item in parsed["action_items"]
|
|
],
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
raise HTTPException(
|
|
status_code=504, detail="Dify API timeout - transcript may be too long"
|
|
)
|
|
except httpx.RequestError as e:
|
|
raise HTTPException(status_code=503, detail=f"Dify API unavailable: {str(e)}")
|
|
|
|
|
|
def parse_dify_response(answer: str) -> dict:
|
|
"""
|
|
Parse Dify response to extract conclusions and action items.
|
|
Attempts JSON parsing first, then falls back to text parsing.
|
|
"""
|
|
# Try to find JSON in the response
|
|
try:
|
|
# Look for JSON block
|
|
if "```json" in answer:
|
|
json_start = answer.index("```json") + 7
|
|
json_end = answer.index("```", json_start)
|
|
json_str = answer[json_start:json_end].strip()
|
|
elif "{" in answer and "}" in answer:
|
|
# Try to find JSON object
|
|
json_start = answer.index("{")
|
|
json_end = answer.rindex("}") + 1
|
|
json_str = answer[json_start:json_end]
|
|
else:
|
|
raise ValueError("No JSON found")
|
|
|
|
data = json.loads(json_str)
|
|
return {
|
|
"conclusions": data.get("conclusions", []),
|
|
"action_items": data.get("action_items", []),
|
|
}
|
|
except (ValueError, json.JSONDecodeError):
|
|
# Fallback: return raw answer as single conclusion
|
|
return {
|
|
"conclusions": [answer] if answer else [],
|
|
"action_items": [],
|
|
}
|