500 lines
19 KiB
Python
500 lines
19 KiB
Python
|
|
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
|
from app.models.models import Meeting, TranscriptSegment, CreateMeetingRequest, UpdateMeetingRequest
|
|
from app.core.database import get_db_connection
|
|
from app.core.config import UPLOAD_DIR, AUDIO_DIR, MARKDOWN_DIR, ALLOWED_EXTENSIONS, ALLOWED_IMAGE_EXTENSIONS, MAX_FILE_SIZE, MAX_IMAGE_SIZE
|
|
from app.services.qiniu_service import qiniu_service
|
|
from typing import Optional
|
|
import os
|
|
import uuid
|
|
import shutil
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/meetings", response_model=list[Meeting])
|
|
def get_meetings(user_id: Optional[int] = None):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
base_query = '''
|
|
SELECT
|
|
m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at,
|
|
m.user_id as creator_id, u.caption as creator_username
|
|
FROM meetings m
|
|
JOIN users u ON m.user_id = u.user_id
|
|
'''
|
|
|
|
if user_id:
|
|
query = f'''
|
|
{base_query}
|
|
LEFT JOIN attendees a ON m.meeting_id = a.meeting_id
|
|
WHERE m.user_id = %s OR a.user_id = %s
|
|
GROUP BY m.meeting_id
|
|
ORDER BY m.meeting_time DESC, m.created_at DESC
|
|
'''
|
|
cursor.execute(query, (user_id, user_id))
|
|
else:
|
|
query = f" {base_query} ORDER BY m.meeting_time DESC, m.created_at DESC"
|
|
cursor.execute(query)
|
|
|
|
meetings = cursor.fetchall()
|
|
|
|
meeting_list = []
|
|
for meeting in meetings:
|
|
attendees_query = '''
|
|
SELECT u.user_id, u.caption
|
|
FROM attendees a
|
|
JOIN users u ON a.user_id = u.user_id
|
|
WHERE a.meeting_id = %s
|
|
'''
|
|
cursor.execute(attendees_query, (meeting['meeting_id'],))
|
|
attendees_data = cursor.fetchall()
|
|
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
|
|
|
meeting_list.append(Meeting(
|
|
meeting_id=meeting['meeting_id'],
|
|
title=meeting['title'],
|
|
meeting_time=meeting['meeting_time'],
|
|
summary=meeting['summary'],
|
|
created_at=meeting['created_at'],
|
|
attendees=attendees,
|
|
creator_id=meeting['creator_id'],
|
|
creator_username=meeting['creator_username']
|
|
))
|
|
|
|
return meeting_list
|
|
|
|
@router.get("/meetings/{meeting_id}", response_model=Meeting)
|
|
def get_meeting_details(meeting_id: int):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
query = '''
|
|
SELECT
|
|
m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at,
|
|
m.user_id as creator_id, u.caption as creator_username,
|
|
af.file_path as audio_file_path
|
|
FROM meetings m
|
|
JOIN users u ON m.user_id = u.user_id
|
|
LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id
|
|
WHERE m.meeting_id = %s
|
|
'''
|
|
cursor.execute(query, (meeting_id,))
|
|
meeting = cursor.fetchone()
|
|
|
|
if not meeting:
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
attendees_query = '''
|
|
SELECT u.user_id, u.caption
|
|
FROM attendees a
|
|
JOIN users u ON a.user_id = u.user_id
|
|
WHERE a.meeting_id = %s
|
|
'''
|
|
cursor.execute(attendees_query, (meeting['meeting_id'],))
|
|
attendees_data = cursor.fetchall()
|
|
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
|
|
|
meeting_data = Meeting(
|
|
meeting_id=meeting['meeting_id'],
|
|
title=meeting['title'],
|
|
meeting_time=meeting['meeting_time'],
|
|
summary=meeting['summary'],
|
|
created_at=meeting['created_at'],
|
|
attendees=attendees,
|
|
creator_id=meeting['creator_id'],
|
|
creator_username=meeting['creator_username']
|
|
)
|
|
|
|
# Add audio file path if exists
|
|
if meeting['audio_file_path']:
|
|
meeting_data.audio_file_path = meeting['audio_file_path']
|
|
|
|
return meeting_data
|
|
|
|
@router.get("/meetings/{meeting_id}/transcript", response_model=list[TranscriptSegment])
|
|
def get_meeting_transcript(meeting_id: int):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
# First check if meeting exists
|
|
meeting_query = "SELECT meeting_id FROM meetings WHERE meeting_id = %s"
|
|
cursor.execute(meeting_query, (meeting_id,))
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
# Get transcript segments
|
|
transcript_query = '''
|
|
SELECT segment_id, meeting_id, speaker_tag, start_time_ms, end_time_ms, text_content
|
|
FROM transcript_segments
|
|
WHERE meeting_id = %s
|
|
ORDER BY start_time_ms ASC
|
|
'''
|
|
cursor.execute(transcript_query, (meeting_id,))
|
|
segments = cursor.fetchall()
|
|
|
|
return [TranscriptSegment(**segment) for segment in segments]
|
|
|
|
@router.post("/meetings")
|
|
def create_meeting(meeting_request: CreateMeetingRequest):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
# Create meeting
|
|
meeting_query = '''
|
|
INSERT INTO meetings (user_id, title, meeting_time, summary)
|
|
VALUES (%s, %s, %s, %s)
|
|
'''
|
|
# Note: You'll need to pass user_id, for now using hardcoded value
|
|
cursor.execute(meeting_query, (1, meeting_request.title, meeting_request.meeting_time, None))
|
|
meeting_id = cursor.lastrowid
|
|
|
|
# Add attendees
|
|
for attendee_id in meeting_request.attendee_ids:
|
|
attendee_query = '''
|
|
INSERT INTO attendees (meeting_id, user_id)
|
|
VALUES (%s, %s)
|
|
ON DUPLICATE KEY UPDATE meeting_id = meeting_id
|
|
'''
|
|
cursor.execute(attendee_query, (meeting_id, attendee_id))
|
|
|
|
connection.commit()
|
|
return {"meeting_id": meeting_id, "message": "Meeting created successfully"}
|
|
|
|
@router.put("/meetings/{meeting_id}")
|
|
def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
# Check if meeting exists
|
|
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
# Update meeting
|
|
update_query = '''
|
|
UPDATE meetings
|
|
SET title = %s, meeting_time = %s, summary = %s
|
|
WHERE meeting_id = %s
|
|
'''
|
|
cursor.execute(update_query, (
|
|
meeting_request.title,
|
|
meeting_request.meeting_time,
|
|
meeting_request.summary,
|
|
meeting_id
|
|
))
|
|
|
|
# Update attendees - remove existing ones and add new ones
|
|
cursor.execute("DELETE FROM attendees WHERE meeting_id = %s", (meeting_id,))
|
|
|
|
for attendee_id in meeting_request.attendee_ids:
|
|
attendee_query = '''
|
|
INSERT INTO attendees (meeting_id, user_id)
|
|
VALUES (%s, %s)
|
|
'''
|
|
cursor.execute(attendee_query, (meeting_id, attendee_id))
|
|
|
|
connection.commit()
|
|
return {"message": "Meeting updated successfully"}
|
|
|
|
@router.delete("/meetings/{meeting_id}")
|
|
def delete_meeting(meeting_id: int):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
# Check if meeting exists
|
|
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
# Delete related records first (foreign key constraints)
|
|
cursor.execute("DELETE FROM transcript_segments WHERE meeting_id = %s", (meeting_id,))
|
|
cursor.execute("DELETE FROM audio_files WHERE meeting_id = %s", (meeting_id,))
|
|
cursor.execute("DELETE FROM attachments WHERE meeting_id = %s", (meeting_id,))
|
|
cursor.execute("DELETE FROM attendees WHERE meeting_id = %s", (meeting_id,))
|
|
|
|
# Delete meeting
|
|
cursor.execute("DELETE FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
|
|
|
connection.commit()
|
|
return {"message": "Meeting deleted successfully"}
|
|
|
|
@router.post("/meetings/{meeting_id}/regenerate-summary")
|
|
def regenerate_summary(meeting_id: int):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
# Check if meeting exists
|
|
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
# For now, return a mock summary
|
|
# In a real implementation, this would call an AI service
|
|
mock_summary = """# AI 生成摘要
|
|
|
|
## 主要议题
|
|
- 项目进度回顾
|
|
- 技术方案讨论
|
|
- 下阶段规划
|
|
|
|
## 关键决策
|
|
- 采用新的技术架构
|
|
- 调整项目时间节点
|
|
- 分配任务责任
|
|
|
|
## 后续行动
|
|
- [ ] 完成技术方案文档
|
|
- [ ] 安排下次会议时间
|
|
- [ ] 跟进项目进度"""
|
|
|
|
# Update meeting summary
|
|
cursor.execute(
|
|
"UPDATE meetings SET summary = %s WHERE meeting_id = %s",
|
|
(mock_summary, meeting_id)
|
|
)
|
|
connection.commit()
|
|
|
|
return {"summary": mock_summary}
|
|
|
|
@router.get("/meetings/{meeting_id}/edit", response_model=Meeting)
|
|
def get_meeting_for_edit(meeting_id: int):
|
|
"""Get meeting details with full attendee information for editing"""
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
query = '''
|
|
SELECT
|
|
m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at,
|
|
m.user_id as creator_id, u.caption as creator_username,
|
|
af.file_path as audio_file_path
|
|
FROM meetings m
|
|
JOIN users u ON m.user_id = u.user_id
|
|
LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id
|
|
WHERE m.meeting_id = %s
|
|
'''
|
|
cursor.execute(query, (meeting_id,))
|
|
meeting = cursor.fetchone()
|
|
|
|
if not meeting:
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
# Get attendees with full info for editing
|
|
attendees_query = '''
|
|
SELECT u.user_id, u.caption
|
|
FROM attendees a
|
|
JOIN users u ON a.user_id = u.user_id
|
|
WHERE a.meeting_id = %s
|
|
'''
|
|
cursor.execute(attendees_query, (meeting['meeting_id'],))
|
|
attendees_data = cursor.fetchall()
|
|
attendees = [{'user_id': row['user_id'], 'caption': row['caption']} for row in attendees_data]
|
|
|
|
meeting_data = Meeting(
|
|
meeting_id=meeting['meeting_id'],
|
|
title=meeting['title'],
|
|
meeting_time=meeting['meeting_time'],
|
|
summary=meeting['summary'],
|
|
created_at=meeting['created_at'],
|
|
attendees=attendees,
|
|
creator_id=meeting['creator_id'],
|
|
creator_username=meeting['creator_username']
|
|
)
|
|
|
|
# Add audio file path if exists
|
|
if meeting['audio_file_path']:
|
|
meeting_data.audio_file_path = meeting['audio_file_path']
|
|
|
|
return meeting_data
|
|
|
|
@router.post("/meetings/upload-audio")
|
|
async def upload_audio(
|
|
audio_file: UploadFile = File(...),
|
|
meeting_id: int = Form(...)
|
|
):
|
|
# Validate file extension
|
|
file_extension = os.path.splitext(audio_file.filename)[1].lower()
|
|
if file_extension not in ALLOWED_EXTENSIONS:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Unsupported file type. Allowed types: {', '.join(ALLOWED_EXTENSIONS)}"
|
|
)
|
|
|
|
# Check file size
|
|
if audio_file.size > MAX_FILE_SIZE:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="File size exceeds 100MB limit"
|
|
)
|
|
|
|
# Check if meeting exists
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
# TEMP: Use existing file to test Qiniu upload instead of client file
|
|
# This bypasses potential client file processing issues
|
|
existing_file = AUDIO_DIR / "31ce039a-f619-4869-91c8-eab934bbd1d4.m4a"
|
|
if not existing_file.exists():
|
|
raise HTTPException(status_code=500, detail="Test file not found")
|
|
|
|
temp_path = existing_file
|
|
print(f"DEBUG: Using existing test file: {temp_path}")
|
|
print(f"DEBUG: Test file exists: {temp_path.exists()}")
|
|
print(f"DEBUG: Test file size: {temp_path.stat().st_size}")
|
|
|
|
# Upload to Qiniu
|
|
try:
|
|
print(f"DEBUG: Attempting to upload audio to Qiniu - meeting_id: {meeting_id}, filename: {audio_file.filename}")
|
|
print(f"DEBUG: Temp file path: {temp_path}")
|
|
print(f"DEBUG: Temp file exists: {temp_path.exists()}")
|
|
|
|
success, qiniu_url, error_msg = qiniu_service.upload_audio_file(
|
|
str(temp_path), meeting_id, audio_file.filename
|
|
)
|
|
|
|
print(f"DEBUG: Qiniu upload result - success: {success}, url: {qiniu_url}, error: {error_msg}")
|
|
|
|
# TEMP: Don't delete existing test file
|
|
# if temp_path.exists():
|
|
# temp_path.unlink()
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail=f"Failed to upload to Qiniu: {error_msg}")
|
|
|
|
# Save file info to database with Qiniu URL
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
# Insert audio file record with Qiniu URL
|
|
insert_query = '''
|
|
INSERT INTO audio_files (meeting_id, file_name, file_path, file_size, upload_time)
|
|
VALUES (%s, %s, %s, %s, NOW())
|
|
ON DUPLICATE KEY UPDATE
|
|
file_name = VALUES(file_name),
|
|
file_path = VALUES(file_path),
|
|
file_size = VALUES(file_size),
|
|
upload_time = VALUES(upload_time)
|
|
'''
|
|
cursor.execute(insert_query, (meeting_id, audio_file.filename, qiniu_url, audio_file.size))
|
|
connection.commit()
|
|
|
|
return {
|
|
"message": "Audio file uploaded successfully to Qiniu",
|
|
"file_name": audio_file.filename,
|
|
"file_path": qiniu_url,
|
|
"qiniu_url": qiniu_url
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"DEBUG: Exception in audio upload: {str(e)}")
|
|
print(f"DEBUG: Exception type: {type(e)}")
|
|
import traceback
|
|
print(f"DEBUG: Traceback: {traceback.format_exc()}")
|
|
# TEMP: Don't delete existing test file in case of error
|
|
# if temp_path.exists():
|
|
# temp_path.unlink()
|
|
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
|
|
|
@router.get("/meetings/{meeting_id}/audio")
|
|
def get_audio_file(meeting_id: int):
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
|
|
query = '''
|
|
SELECT file_name, file_path, file_size, upload_time
|
|
FROM audio_files
|
|
WHERE meeting_id = %s
|
|
'''
|
|
cursor.execute(query, (meeting_id,))
|
|
audio_file = cursor.fetchone()
|
|
|
|
if not audio_file:
|
|
raise HTTPException(status_code=404, detail="Audio file not found for this meeting")
|
|
|
|
return {
|
|
"file_name": audio_file['file_name'],
|
|
"file_path": audio_file['file_path'],
|
|
"file_size": audio_file['file_size'],
|
|
"upload_time": audio_file['upload_time']
|
|
}
|
|
|
|
@router.post("/meetings/{meeting_id}/upload-image")
|
|
async def upload_image(
|
|
meeting_id: int,
|
|
image_file: UploadFile = File(...)
|
|
):
|
|
# Validate file extension
|
|
file_extension = os.path.splitext(image_file.filename)[1].lower()
|
|
if file_extension not in ALLOWED_IMAGE_EXTENSIONS:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Unsupported image type. Allowed types: {', '.join(ALLOWED_IMAGE_EXTENSIONS)}"
|
|
)
|
|
|
|
# Check file size
|
|
if image_file.size > MAX_IMAGE_SIZE:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Image size exceeds 10MB limit"
|
|
)
|
|
|
|
# Check if meeting exists
|
|
with get_db_connection() as connection:
|
|
cursor = connection.cursor(dictionary=True)
|
|
cursor.execute("SELECT meeting_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
|
if not cursor.fetchone():
|
|
raise HTTPException(status_code=404, detail="Meeting not found")
|
|
|
|
# Create temporary file for upload
|
|
temp_filename = f"{uuid.uuid4()}{file_extension}"
|
|
temp_path = MARKDOWN_DIR / temp_filename
|
|
|
|
# Save file temporarily
|
|
# Save file temporarily
|
|
try:
|
|
contents = await image_file.read()
|
|
with open(temp_path, "wb") as buffer:
|
|
buffer.write(contents)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to save temporary image: {str(e)}")
|
|
|
|
# Upload to Qiniu
|
|
try:
|
|
print(f"DEBUG: Attempting to upload image to Qiniu - meeting_id: {meeting_id}, filename: {image_file.filename}")
|
|
print(f"DEBUG: Temp file path: {temp_path}")
|
|
print(f"DEBUG: Temp file exists: {temp_path.exists()}")
|
|
|
|
success, qiniu_url, error_msg = qiniu_service.upload_markdown_image(
|
|
str(temp_path), meeting_id, image_file.filename
|
|
)
|
|
|
|
print(f"DEBUG: Qiniu upload result - success: {success}, url: {qiniu_url}, error: {error_msg}")
|
|
|
|
# Clean up temporary file
|
|
if temp_path.exists():
|
|
temp_path.unlink()
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=500, detail=f"Failed to upload image to Qiniu: {error_msg}")
|
|
|
|
return {
|
|
"message": "Image uploaded successfully to Qiniu",
|
|
"file_name": image_file.filename,
|
|
"file_path": qiniu_url,
|
|
"url": qiniu_url,
|
|
"qiniu_url": qiniu_url
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"DEBUG: Exception in image upload: {str(e)}")
|
|
print(f"DEBUG: Exception type: {type(e)}")
|
|
import traceback
|
|
print(f"DEBUG: Traceback: {traceback.format_exc()}")
|
|
# Clean up temporary file in case of error
|
|
if temp_path.exists():
|
|
temp_path.unlink()
|
|
raise HTTPException(status_code=500, detail=f"Image upload failed: {str(e)}")
|