247 lines
8.2 KiB
Python
247 lines
8.2 KiB
Python
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
|
|
|
|
@app.post("/api/meetings/{meeting_id}/retry")
|
|
def retry_meeting(
|
|
meeting_id: int,
|
|
background_tasks: BackgroundTasks,
|
|
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")
|
|
|
|
# Check if chunks directory exists
|
|
chunk_dir = os.path.join(settings.UPLOAD_DIR, "chunks", str(meeting_id))
|
|
if not os.path.exists(chunk_dir) or not os.listdir(chunk_dir):
|
|
raise HTTPException(400, detail="Original audio chunks not found. Please re-upload.")
|
|
|
|
# Reset status
|
|
meeting.status = "QUEUED"
|
|
db.commit()
|
|
|
|
# Trigger Retry Task
|
|
from .services.orchestrator import retry_meeting_task
|
|
background_tasks.add_task(retry_meeting_task, meeting.id, SessionLocal)
|
|
|
|
return {"status": "started", "message": "Retrying transcription..."}
|
|
|
|
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
|
|
|
|
# PRIORITY 1: Mounted Volume (Development / Live Update)
|
|
static_path = "/app/frontend/dist"
|
|
|
|
# PRIORITY 2: Built-in Image Path (Production)
|
|
if not os.path.exists(static_path):
|
|
static_path = "/frontend_static"
|
|
|
|
# PRIORITY 3: Local Development (running python directly)
|
|
if not os.path.exists(static_path):
|
|
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)
|