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)}")