feat: Meeting Assistant MVP - Complete implementation

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>
This commit is contained in:
egg
2025-12-10 20:17:44 +08:00
commit 8b6184ecc5
65 changed files with 10510 additions and 0 deletions

102
backend/app/routers/ai.py Normal file
View File

@@ -0,0 +1,102 @@
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": [],
}