from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, File, UploadFile, Form, WebSocket, WebSocketDisconnect, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session from sqlalchemy import or_, and_, func, desc from app.core.db import get_db, SessionLocal from app.core.deps import get_current_user from app.core.security import decode_token from app.models import Meeting, MeetingAttendee, User, TranscriptSegment, SummarizeTask, TranscriptTask, MeetingAudio, AIModel, Hotword from app.schemas.meeting import MeetingOut, MeetingDetailOut, MeetingUpdate, MeetingListOut, MeetingCreate, AttendeeOut, MeetingAudioOut from app.services.meeting_service import MeetingService from typing import List, Optional import shutil from pathlib import Path import uuid from datetime import datetime import asyncio import websockets import json import logging from redis import Redis from app.core.redis import get_redis from app.core.config import get_settings from app.models import PromptTemplate logger = logging.getLogger(__name__) router = APIRouter(prefix="/meetings", tags=["meetings"]) async def run_transcript_worker(task_id: str): db = SessionLocal() try: await MeetingService.process_transcript_task(db, task_id) finally: db.close() @router.post("/upload_temp") async def upload_temp_file( file: UploadFile = File(...), current_user: User = Depends(get_current_user), ): # Dynamically resolve storage path relative to project root (backend) base_dir = Path(__file__).resolve().parents[4] storage_dir = base_dir / "storage" / "uploads" / "temp" storage_dir.mkdir(parents=True, exist_ok=True) file_ext = Path(file.filename).suffix unique_filename = f"{uuid.uuid4()}{file_ext}" file_path = storage_dir / unique_filename with file_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) return {"file_path": str(file_path), "file_name": file.filename} @router.post("", response_model=MeetingOut) async def create_meeting( meeting_in: MeetingCreate, background_tasks: BackgroundTasks, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): meeting = Meeting( title=meeting_in.title, tags=meeting_in.tags, meeting_time=meeting_in.meeting_time, status="uploaded" if meeting_in.file_path else meeting_in.status, type=meeting_in.type, user_id=current_user.user_id, asr_model_id=meeting_in.asr_model_id, summary_model_id=meeting_in.summary_model_id, summary_prompt_id=meeting_in.prompt_id, ) db.add(meeting) db.flush() if meeting_in.participants: # Deduplicate participants to avoid UniqueViolation unique_participants = list(set(meeting_in.participants)) for user_id in unique_participants: attendee = MeetingAttendee(meeting_id=meeting.meeting_id, user_id=user_id) db.add(attendee) if meeting_in.file_path: p = Path(meeting_in.file_path) if p.exists(): audio = MeetingAudio( meeting_id=meeting.meeting_id, file_path=str(p), file_name=p.name, file_size=p.stat().st_size, processing_status="uploaded" ) db.add(audio) db.flush() # Ensure audio ID is generated and accessible final_model_id = meeting.asr_model_id if not final_model_id: default_model = db.query(AIModel).filter(AIModel.model_type == 'asr').first() if default_model: final_model_id = default_model.model_id meeting.asr_model_id = final_model_id try: print(f"[DEBUG] Creating transcript task for meeting {meeting.meeting_id} with model {final_model_id}") task = await MeetingService.create_transcript_task( db, meeting_id=meeting.meeting_id, model_id=final_model_id ) print(f"[DEBUG] Task created: {task.task_id}, scheduling background worker") background_tasks.add_task(run_transcript_worker, task.task_id) except Exception as e: print(f"[ERROR] Failed to start transcription task: {e}") db.commit() db.refresh(meeting) return meeting @router.post("/{meeting_id}/upload_audio") async def upload_meeting_audio( meeting_id: int, background_tasks: BackgroundTasks, file: UploadFile = File(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() if not meeting: raise HTTPException(status_code=404, detail="Meeting not found") # Save File base_dir = Path(__file__).resolve().parents[4] storage_dir = base_dir / "storage" / "uploads" / str(meeting_id) storage_dir.mkdir(parents=True, exist_ok=True) file_ext = Path(file.filename).suffix unique_filename = f"{uuid.uuid4()}{file_ext}" file_path = storage_dir / unique_filename with file_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) # Create MeetingAudio record audio = MeetingAudio( meeting_id=meeting.meeting_id, file_path=str(file_path), file_name=file.filename, file_size=file_path.stat().st_size, processing_status="uploaded" ) db.add(audio) db.commit() db.refresh(audio) # Trigger Transcription Task # For live meetings, the audio upload is just a backup/archive. # Transcription is handled in real-time via WebSocket, so we don't trigger it here # to avoid overwriting the status or duplicating work. if meeting.type != 'live': final_model_id = meeting.asr_model_id if not final_model_id: default_model = db.query(AIModel).filter(AIModel.model_type == 'asr').first() if default_model: final_model_id = default_model.model_id meeting.asr_model_id = final_model_id db.commit() try: print(f"[DEBUG] Triggering transcript task for uploaded audio: meeting {meeting_id}") task = await MeetingService.create_transcript_task( db, meeting_id=meeting.meeting_id, model_id=final_model_id ) background_tasks.add_task(run_transcript_worker, task.task_id) except Exception as e: print(f"[ERROR] Failed to start transcription task for upload: {e}") else: print(f"[DEBUG] Skipping transcript task for live meeting audio upload: meeting {meeting_id}") return {"status": "success", "file_path": str(file_path)} @router.post("/upload", response_model=MeetingOut) async def create_meeting_with_upload( background_tasks: BackgroundTasks, file: UploadFile = File(...), title: str = Form(...), participants: Optional[List[int]] = Form(None), prompt_id: Optional[int] = Form(None), asr_model_id: Optional[int] = Form(None), summary_model_id: Optional[int] = Form(None), tags: Optional[List[str]] = Form(None), meeting_time: Optional[str] = Form(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): # Process meeting time mt = datetime.utcnow() if meeting_time: try: # Attempt to parse timestamp (ms) mt = datetime.fromtimestamp(int(meeting_time) / 1000.0) except: pass # Create Meeting meeting = Meeting( title=title, tags=",".join(tags) if tags else None, # Store as comma separated string if that's the convention, or modify model meeting_time=mt, status="uploaded", # Initial status for uploaded meeting user_id=current_user.user_id, asr_model_id=asr_model_id, summary_model_id=summary_model_id, ) db.add(meeting) db.flush() # Get meeting_id # Handle Participants if participants: for user_id in participants: attendee = MeetingAttendee(meeting_id=meeting.meeting_id, user_id=user_id) db.add(attendee) # Save File base_dir = Path(__file__).resolve().parents[4] storage_dir = base_dir / "storage" / "uploads" storage_dir.mkdir(parents=True, exist_ok=True) file_ext = Path(file.filename).suffix unique_filename = f"{uuid.uuid4()}{file_ext}" file_path = storage_dir / unique_filename with file_path.open("wb") as buffer: shutil.copyfileobj(file.file, buffer) # Create MeetingAudio record audio = MeetingAudio( meeting_id=meeting.meeting_id, file_path=str(file_path), file_name=file.filename, file_size=file_path.stat().st_size, processing_status="uploaded" ) db.add(audio) db.commit() db.refresh(meeting) # Trigger Transcription Task # If no model provided, try to find default final_model_id = asr_model_id if not final_model_id: default_model = db.query(AIModel).filter(AIModel.model_type == 'asr').first() if default_model: final_model_id = default_model.model_id # Update meeting with default model meeting.asr_model_id = final_model_id db.commit() # Only create task if we have a model (or let service handle it if we want to support auto-detect later) # But for now, let's just pass what we have. # Service create_transcript_task expects model_id. # If we pass None, it will be stored as None in TranscriptTask. try: task = await MeetingService.create_transcript_task( db, meeting_id=meeting.meeting_id, model_id=final_model_id ) background_tasks.add_task(run_transcript_worker, task.task_id) except Exception as e: # Log error but don't fail the upload response print(f"Failed to start transcription task: {e}") return meeting async def run_summarize_worker(task_id: str): db = SessionLocal() try: await MeetingService.process_summarize_task(db, task_id) finally: db.close() @router.post("/{meeting_id}/transcript") async def create_transcript_task( meeting_id: int, background_tasks: BackgroundTasks, model_id: int = Query(..., description="ASR Model ID"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Manually trigger a transcript task for a meeting. Useful for retrying failed tasks or re-transcribing with a different model. """ meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() if not meeting: raise HTTPException(status_code=404, detail="Meeting not found") try: task = await MeetingService.create_transcript_task( db, meeting_id=meeting_id, model_id=model_id ) background_tasks.add_task(run_transcript_worker, task.task_id) return task except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/{meeting_id}/summarize") async def start_summarize( meeting_id: int, payload: dict, background_tasks: BackgroundTasks, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): try: task = await MeetingService.create_summarize_task( db, meeting_id, payload.get("prompt_id"), payload.get("model_id"), payload.get("extra_prompt", "") ) background_tasks.add_task(run_summarize_worker, task.task_id) return {"task_id": task.task_id, "status": task.status} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/tasks", response_model=dict) def list_all_tasks( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), task_type: Optional[str] = Query(None), # summarize, transcript status: Optional[str] = Query(None), ): """ 统一任务监控接口,合并转译和总结任务 """ tasks = [] # 1. 获取总结任务 sum_query = db.query(SummarizeTask, Meeting.title, User.username).join( Meeting, SummarizeTask.meeting_id == Meeting.meeting_id ).join( User, Meeting.user_id == User.user_id ) if status: sum_query = sum_query.filter(SummarizeTask.status == status) for t, m_title, username in sum_query.order_by(desc(SummarizeTask.created_at)).limit(50).all(): tasks.append({ "task_id": t.task_id, "type": "总结", "meeting_title": m_title, "creator": username, "status": t.status, "progress": t.progress, "created_at": t.created_at, "meeting_id": t.meeting_id }) # 2. 获取转译任务 trans_query = db.query(TranscriptTask, Meeting.title, User.username).join( Meeting, TranscriptTask.meeting_id == Meeting.meeting_id ).join( User, Meeting.user_id == User.user_id ) if status: trans_query = trans_query.filter(TranscriptTask.status == status) for t, m_title, username in trans_query.order_by(desc(TranscriptTask.created_at)).limit(50).all(): tasks.append({ "task_id": t.task_id, "type": "转译", "meeting_title": m_title, "creator": username, "status": t.status, "progress": t.progress, "created_at": t.created_at, "meeting_id": t.meeting_id }) # 按时间全局排序 tasks.sort(key=lambda x: x["created_at"], reverse=True) return {"items": tasks} @router.websocket("/{meeting_id}/ws") async def websocket_meeting( websocket: WebSocket, meeting_id: int, token: str = Query(...), db: Session = Depends(get_db) ): """ Real-time meeting WebSocket endpoint. Proxies audio to local ASR service and saves transcripts to DB. """ # 1. Authenticate try: payload = decode_token(token) user_id = payload.get("sub") if not user_id: await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return user = db.query(User).filter(User.user_id == user_id).first() if not user: await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return # Check meeting access meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() if not meeting: await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return # Check if user is creator or attendee is_attendee = any(a.user_id == user.user_id for a in meeting.attendees) if meeting.user_id != user.user_id and not is_attendee: # Check admin role_codes = [ur.role.role_code for ur in user.roles] if "admin" not in role_codes and "superuser" not in role_codes: await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return except Exception as e: logger.error(f"WebSocket auth failed: {e}") await websocket.close(code=status.WS_1008_POLICY_VIOLATION) return await websocket.accept() if meeting and meeting.type == "live" and meeting.status != "transcribing": meeting.status = "transcribing" db.commit() # 2. Connect to ASR Service asr_ws_url = "ws://127.0.0.1:3050" # Determine ASR Model URL based on meeting configuration model_id = meeting.asr_model_id if not model_id: # Fallback to default model default_model = db.query(AIModel).filter(AIModel.model_type == 'asr', AIModel.is_default == 1).first() if not default_model: default_model = db.query(AIModel).filter(AIModel.model_type == 'asr').first() if default_model: model_id = default_model.model_id if model_id: model = db.query(AIModel).filter(AIModel.model_id == model_id).first() if model and model.base_url: base = model.base_url.rstrip('/') if base.startswith("https://"): asr_ws_url = base.replace("https://", "wss://") elif base.startswith("http://"): asr_ws_url = base.replace("http://", "ws://") else: # If no protocol specified, assume ws:// if "://" not in base: asr_ws_url = f"ws://{base}" else: asr_ws_url = base logger.info(f"Connecting to ASR at {asr_ws_url} for meeting {meeting_id}") try: async with websockets.connect(asr_ws_url) as asr_ws: # Send initial configuration to ASR if needed # Model-Kf expects configuration first usually, but defaults might work. # Let's send a default config based on schema hotword_filters = [Hotword.scope.in_(["public", "global"])] if meeting.user_id: hotword_filters.append( (Hotword.scope == "personal") & (Hotword.user_id == meeting.user_id) ) hotwords = db.query(Hotword).filter( (hotword_filters[0]) if len(hotword_filters) == 1 else (hotword_filters[0] | hotword_filters[1]) ).all() hotword_list = [] hotword_string_parts = [] for hw in hotwords: token = hw.word if token: hotword_list.append(token) hotword_string_parts.append(token) hotword_string = " ".join([p for p in hotword_string_parts if p]) init_msg = { "mode": "2pass", "wav_name": "microphone", "is_speaking": True, "chunk_size": [5, 10, 5], "chunk_interval": 10, "itn": False, "hotwords": hotword_list, "hotword": hotword_string } await asr_ws.send(json.dumps(init_msg)) # Create tasks for bidirectional forwarding async def forward_client_to_asr(): try: while True: data = await websocket.receive() if "bytes" in data: await asr_ws.send(data["bytes"]) elif "text" in data: # Forward control messages (JSON) await asr_ws.send(data["text"]) except WebSocketDisconnect: logger.info("Client disconnected") except Exception as e: logger.error(f"Client->ASR error: {e}") async def forward_asr_to_client(): try: last_saved_end_ms = 0 def parse_number(v): if isinstance(v, (int, float)): return float(v) if isinstance(v, str): try: return float(v.strip()) except Exception: return None return None def parse_bool(v): if isinstance(v, bool): return v if isinstance(v, str): return v.strip().lower() in ("true", "1", "yes", "y", "t") if isinstance(v, (int, float)): return int(v) == 1 return False def extract_text(d: dict) -> str: t = d.get("text") if isinstance(t, str): return t result = d.get("result") if isinstance(result, dict): rt = result.get("text") if isinstance(rt, str): return rt return "" async for message in asr_ws: # Parse message try: msg_data = json.loads(message) # Forward to client await websocket.send_text(message) raw_is_final = msg_data.get("is_final") raw_final = msg_data.get("final") raw_status = msg_data.get("status") is_final = parse_bool(raw_is_final) or parse_bool(raw_final) if not is_final: if isinstance(raw_status, str): is_final = raw_status.strip().lower() in ("final", "finished", "done", "completed", "complete") elif isinstance(raw_status, (int, float)): is_final = int(raw_status) in (1, 2) text = extract_text(msg_data).strip() if is_final is True and text: speaker_tag = msg_data.get("speaker", "Unknown") start_ms = None end_ms = None timestamp = msg_data.get("timestamp") if isinstance(timestamp, list) and len(timestamp) > 0: try: starts = [] ends = [] for pair in timestamp: if not isinstance(pair, (list, tuple)) or len(pair) < 2: continue s = parse_number(pair[0]) e = parse_number(pair[1]) if s is not None and e is not None: starts.append(s) ends.append(e) if starts and ends: raw_start = min(starts) raw_end = max(ends) if raw_end < raw_start: raise ValueError("timestamp end < start") # Heuristic: if ASR returns seconds, convert to ms if raw_end < 1000: raw_start *= 1000.0 raw_end *= 1000.0 start_ms = int(raw_start) end_ms = int(raw_end) except Exception: start_ms = None end_ms = None if start_ms is None or end_ms is None: bt = msg_data.get("begin_time") et = msg_data.get("end_time") bt_num = parse_number(bt) et_num = parse_number(et) if bt_num is not None and et_num is not None: if et_num < 1000 and bt_num < 1000: bt_num *= 1000.0 et_num *= 1000.0 start_ms = int(bt_num) end_ms = int(et_num) if start_ms is None or end_ms is None or end_ms < start_ms: start_ms = last_saved_end_ms + 1 end_ms = start_ms + 1000 last_saved_end_ms = max(last_saved_end_ms, end_ms) with SessionLocal() as local_db: existing = local_db.query(TranscriptSegment).filter( TranscriptSegment.meeting_id == meeting_id, TranscriptSegment.speaker_tag == speaker_tag, TranscriptSegment.start_time_ms == start_ms, TranscriptSegment.end_time_ms == end_ms, ).order_by(TranscriptSegment.segment_id.desc()).first() if existing: existing.text_content = text local_db.commit() else: segment = TranscriptSegment( meeting_id=meeting_id, speaker_id=0, speaker_tag=speaker_tag, start_time_ms=start_ms, end_time_ms=end_ms, text_content=text, ) local_db.add(segment) local_db.commit() except json.JSONDecodeError: pass except Exception as e: logger.error(f"ASR->Client error: {e}") # Run both tasks done, pending = await asyncio.wait( [asyncio.create_task(forward_client_to_asr()), asyncio.create_task(forward_asr_to_client())], return_when=asyncio.FIRST_COMPLETED ) for task in pending: task.cancel() # 3. Post-Meeting Processing (Auto-Summary) logger.info(f"WebSocket session ended for meeting {meeting_id}. Checking for auto-summary...") # Use a new session for post-processing to ensure clean state with SessionLocal() as local_db: # Update meeting status to 'transcribed' m = local_db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() if m and m.status == 'transcribing': m.status = 'transcribed' local_db.commit() logger.info(f"Meeting {meeting_id} status updated to 'transcribed'") # Check Auto-Summary Setting settings = get_settings() if getattr(settings, "AUTO_SUMMARIZE", True): # Default to True if not set try: # Find default LLM Model llm_model = None if m.summary_model_id: llm_model = local_db.query(AIModel).filter( AIModel.model_id == m.summary_model_id, AIModel.status == 1 ).first() if not llm_model: llm_model = local_db.query(AIModel).filter( AIModel.model_type == 'llm', AIModel.is_default == 1, AIModel.status == 1 ).first() # Find Prompt (Use meeting-specific or default) prompt_tmpl = None if m.summary_prompt_id: prompt_tmpl = local_db.query(PromptTemplate).filter( PromptTemplate.id == m.summary_prompt_id ).first() if not prompt_tmpl: prompt_tmpl = local_db.query(PromptTemplate).filter( PromptTemplate.status == 1 ).order_by(PromptTemplate.is_system.desc(), PromptTemplate.id.asc()).first() if llm_model and prompt_tmpl: logger.info(f"Auto-triggering summary for meeting {meeting_id}") # Create summarize task # Note: create_summarize_task is async and commits to DB # We need to await it new_sum_task = await MeetingService.create_summarize_task( local_db, meeting_id=meeting_id, prompt_id=prompt_tmpl.id, model_id=llm_model.model_id ) # Launch worker in background asyncio.create_task(MeetingService.run_summarize_worker(new_sum_task.task_id)) # Update meeting status to 'summarizing' m.status = 'summarizing' local_db.commit() logger.info(f"Meeting {meeting_id} status updated to 'summarizing'") else: logger.warning(f"Skipping auto-summary: No default LLM or Prompt found for meeting {meeting_id}") except Exception as sum_e: logger.error(f"Failed to auto-trigger summary for meeting {meeting_id}: {sum_e}") else: logger.info(f"Auto-summary disabled for meeting {meeting_id}") except Exception as e: logger.error(f"Failed to connect to ASR service: {e}") await websocket.close(code=status.WS_1011_INTERNAL_ERROR) @router.get("/tasks/{task_id}") async def get_task_progress( task_id: str, db: Session = Depends(get_db), redis: Redis = Depends(get_redis), current_user: User = Depends(get_current_user), ): task = MeetingService.get_task_status(db, task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") # Fetch real-time status message from Redis message = None try: msg_bytes = await redis.get(f"task:status:{task_id}") if msg_bytes: # redis.get in async mode (aioredis) returns bytes or str depending on decode_responses # If decode_responses=True (which is common in FastAPI setups), it's already str. # If it's bytes, we decode. If it's str, we use it directly. if isinstance(msg_bytes, bytes): message = msg_bytes.decode("utf-8") else: message = str(msg_bytes) except Exception as e: # Use logging.warning instead of logger.warning if logger is not defined globally logging.warning(f"Failed to get task status message from Redis: {e}") return { "task_id": task.task_id, "status": task.status, "progress": task.progress, "result": getattr(task, "result", None), "error": task.error_message, "message": message } # ... 其余接口保持不变 @router.get("", response_model=MeetingListOut) def list_meetings( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), scope: str = Query("all", description="all, created, joined"), keyword: Optional[str] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), ): id_query = db.query(Meeting.meeting_id, Meeting.meeting_time) if scope == "created": id_query = id_query.filter(Meeting.user_id == current_user.user_id) elif scope == "joined": id_query = id_query.join(MeetingAttendee).filter(MeetingAttendee.user_id == current_user.user_id) else: id_query = id_query.outerjoin(MeetingAttendee).filter( or_(Meeting.user_id == current_user.user_id, MeetingAttendee.user_id == current_user.user_id) ) if keyword: id_query = id_query.filter(Meeting.title.contains(keyword)) id_query = id_query.distinct() total = id_query.count() paged_ids = id_query.order_by( Meeting.meeting_time.desc(), Meeting.meeting_id.desc() ).offset((page - 1) * page_size).limit(page_size).subquery() results = db.query(Meeting).join( paged_ids, Meeting.meeting_id == paged_ids.c.meeting_id ).order_by( paged_ids.c.meeting_time.desc(), paged_ids.c.meeting_id.desc() ).all() items = [] for m in results: item = MeetingOut.model_validate(m) item.creator_name = m.creator.display_name if m.creator else "Unknown" item.creator_avatar = m.creator.avatar if m.creator else None items.append(item) return {"items": items, "total": total} @router.get("/{meeting_id}/audio/{audio_id}") def get_meeting_audio( meeting_id: int, audio_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() if not meeting: raise HTTPException(status_code=404, detail="Meeting not found") is_attendee = any(a.user_id == current_user.user_id for a in meeting.attendees) if meeting.user_id != current_user.user_id and not is_attendee: role_codes = [ur.role.role_code for ur in current_user.roles] if "admin" not in role_codes and "superuser" not in role_codes: raise HTTPException(status_code=403, detail="Not authorized") audio = db.query(MeetingAudio).filter( MeetingAudio.audio_id == audio_id, MeetingAudio.meeting_id == meeting_id ).first() if not audio: raise HTTPException(status_code=404, detail="Audio not found") file_path = Path(audio.file_path) if not file_path.exists(): raise HTTPException(status_code=404, detail="Audio file missing") return FileResponse(path=str(file_path), filename=audio.file_name or file_path.name) @router.get("/{meeting_id}", response_model=MeetingDetailOut) def get_meeting_detail( meeting_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() if not meeting: raise HTTPException(status_code=404, detail="Meeting not found") is_attendee = any(a.user_id == current_user.user_id for a in meeting.attendees) if meeting.user_id != current_user.user_id and not is_attendee: # Check if admin role_codes = [ur.role.role_code for ur in current_user.roles] if "admin" not in role_codes and "superuser" not in role_codes: raise HTTPException(status_code=403, detail="Not authorized") item = MeetingDetailOut.model_validate(meeting) item.creator_name = meeting.creator.display_name if meeting.creator else "Unknown" item.creator_avatar = meeting.creator.avatar if meeting.creator else None # Keep ASR arrival order: segment_id reflects insertion order if item.segments: item.segments = sorted(item.segments, key=lambda x: x.segment_id) # Fetch latest transcript task latest_trans = db.query(TranscriptTask).filter( TranscriptTask.meeting_id == meeting_id ).order_by(desc(TranscriptTask.created_at)).first() if latest_trans: item.latest_transcript_task = latest_trans # Fetch latest summarize task latest_sum = db.query(SummarizeTask).filter( SummarizeTask.meeting_id == meeting_id ).order_by(desc(SummarizeTask.created_at)).first() if latest_sum: item.latest_summarize_task = latest_sum settings = get_settings() audios = db.query(MeetingAudio).filter( MeetingAudio.meeting_id == meeting_id ).order_by(desc(MeetingAudio.upload_time)).all() item.audio_files = [] for audio in audios: audio_item = MeetingAudioOut.model_validate(audio) audio_item.audio_url = f"{settings.api_v1_prefix}/meetings/{meeting_id}/audio/{audio.audio_id}" item.audio_files.append(audio_item) return item @router.delete("/{meeting_id}") def delete_meeting( meeting_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): meeting = db.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() if not meeting: raise HTTPException(status_code=404, detail="Meeting not found") if meeting.user_id != current_user.user_id: role_codes = [ur.role.role_code for ur in current_user.roles] if "admin" not in role_codes and "superuser" not in role_codes: raise HTTPException(status_code=403, detail="Forbidden") db.delete(meeting) db.commit() return {"status": "ok"}