Files
Brancheneinstufung2/transcription-tool/backend/app.py
Floke 281014de43 feat(transcription): v0.5.0 with global speaker management and trimming
- Backend: Added global speaker rename endpoint
- Backend: Hardened JSON parsing and timestamp offsets
- Frontend: Integrated Speaker Management Bar
- Frontend: Added Trim Start/End (Scissors) and Single Line Delete
- Frontend: Fixed various TypeScript and Syntax issues
- Docs: Full documentation of v0.5.0 features
2026-01-24 21:26:01 +00:00

171 lines
5.1 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
# 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)
).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 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)