from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, BackgroundTasks, Body from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session, joinedload from typing import List, Dict, Any import os import shutil import uuid from datetime import datetime from .config import settings from .database import init_db, get_db, Meeting, TranscriptChunk, AnalysisResult, SessionLocal from .services.orchestrator import process_meeting_task from .services.insights_service import generate_insight # Initialize FastAPI App app = FastAPI( title=settings.APP_NAME, version=settings.VERSION, root_path="/tr" ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.on_event("startup") def startup_event(): init_db() @app.get("/api/health") def health(): return {"status": "ok", "version": settings.VERSION} @app.get("/api/meetings") def list_meetings(db: Session = Depends(get_db)): return db.query(Meeting).order_by(Meeting.created_at.desc()).all() @app.get("/api/meetings/{meeting_id}") def get_meeting(meeting_id: int, db: Session = Depends(get_db)): meeting = db.query(Meeting).options( joinedload(Meeting.chunks), joinedload(Meeting.analysis_results) # Eager load analysis results ).filter(Meeting.id == meeting_id).first() if not meeting: raise HTTPException(404, detail="Meeting not found") # Sort chunks by index meeting.chunks.sort(key=lambda x: x.chunk_index) return meeting @app.put("/api/chunks/{chunk_id}") def update_chunk(chunk_id: int, payload: Dict[str, Any] = Body(...), db: Session = Depends(get_db)): chunk = db.query(TranscriptChunk).filter(TranscriptChunk.id == chunk_id).first() if not chunk: raise HTTPException(404, detail="Chunk not found") # Update JSON content (e.g. after editing/deleting lines) if "json_content" in payload: chunk.json_content = payload["json_content"] db.commit() return {"status": "updated"} @app.post("/api/upload") async def upload_audio( background_tasks: BackgroundTasks, file: UploadFile = File(...), db: Session = Depends(get_db) ): # 1. Save File file_id = str(uuid.uuid4()) ext = os.path.splitext(file.filename)[1] filename = f"{file_id}{ext}" file_path = os.path.join(settings.UPLOAD_DIR, filename) with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) # 2. Create DB Entry meeting = Meeting( title=file.filename, filename=filename, file_path=file_path, status="UPLOADED" ) db.add(meeting) db.commit() db.refresh(meeting) # 3. Trigger Processing in Background background_tasks.add_task(process_meeting_task, meeting.id, SessionLocal) return meeting from pydantic import BaseModel class InsightRequest(BaseModel): insight_type: str @app.post("/api/meetings/{meeting_id}/insights") def create_insight(meeting_id: int, payload: InsightRequest, db: Session = Depends(get_db)): """ Triggers the generation of a specific insight (e.g., meeting minutes, action items). If the insight already exists, it returns the stored result. Otherwise, it generates, stores, and returns the new insight. """ try: insight = generate_insight(db, meeting_id, payload.insight_type) return insight except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: # For unexpected errors, return a generic 500 error print(f"ERROR: Unexpected error in create_insight: {e}") raise HTTPException(status_code=500, detail="An internal error occurred while generating the insight.") class TranslationRequest(BaseModel): target_language: str @app.post("/api/meetings/{meeting_id}/translate") def translate_meeting_transcript(meeting_id: int, payload: TranslationRequest, db: Session = Depends(get_db)): """ Triggers the translation of a meeting's transcript. """ try: # For now, we only support English if payload.target_language.lower() != 'english': raise HTTPException(status_code=400, detail="Currently, only translation to English is supported.") from .services.translation_service import translate_transcript translation = translate_transcript(db, meeting_id, payload.target_language) return translation except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: print(f"ERROR: Unexpected error in translate_meeting_transcript: {e}") raise HTTPException(status_code=500, detail="An internal error occurred during translation.") class RenameRequest(BaseModel): old_name: str new_name: str @app.post("/api/meetings/{meeting_id}/rename_speaker") def rename_speaker_globally(meeting_id: int, payload: RenameRequest, db: Session = Depends(get_db)): meeting = db.query(Meeting).filter(Meeting.id == meeting_id).first() if not meeting: raise HTTPException(404, detail="Meeting not found") count = 0 # Iterate over all chunks directly from DB relation for chunk in meeting.chunks: if not chunk.json_content: continue modified = False new_content = [] for msg in chunk.json_content: if msg.get("speaker") == payload.old_name: msg["speaker"] = payload.new_name modified = True count += 1 new_content.append(msg) if modified: # Force update of the JSON field chunk.json_content = list(new_content) db.add(chunk) db.commit() return {"status": "updated", "rows_affected": count} @app.delete("/api/meetings/{meeting_id}") def delete_meeting(meeting_id: int, db: Session = Depends(get_db)): meeting = db.query(Meeting).filter(Meeting.id == meeting_id).first() if not meeting: raise HTTPException(404, detail="Meeting not found") # 1. Delete Files try: if os.path.exists(meeting.file_path): os.remove(meeting.file_path) # Delete chunks dir chunk_dir = os.path.join(settings.UPLOAD_DIR, "chunks", str(meeting_id)) if os.path.exists(chunk_dir): shutil.rmtree(chunk_dir) except Exception as e: print(f"Error deleting files: {e}") # 2. Delete DB Entry (Cascade deletes chunks/analyses) db.delete(meeting) db.commit() return {"status": "deleted"} # Serve Frontend # This must be the last route definition to avoid catching API routes static_path = "/frontend_static" if not os.path.exists(static_path): # Fallback for local development if not in Docker static_path = os.path.join(os.path.dirname(__file__), "../frontend/dist") if os.path.exists(static_path): app.mount("/", StaticFiles(directory=static_path, html=True), name="static") if __name__ == "__main__": import uvicorn uvicorn.run("backend.app:app", host="0.0.0.0", port=8001, reload=True)