/ transcription_api.py
transcription_api.py
1 from fastapi import APIRouter, File, UploadFile, BackgroundTasks, HTTPException, Depends, Form, Request, Query 2 from fastapi.responses import JSONResponse, FileResponse 3 from typing import Dict, List, Optional, Union, Any 4 import os 5 import uuid 6 import time 7 import json 8 import aiofiles 9 import logging 10 from tempfile import NamedTemporaryFile 11 from pathlib import Path 12 13 # Import des fonctions de transcription 14 from transcription_utils import process_monologue, process_multiple_speakers 15 16 # Import des dépendances d'authentification 17 from auth import validate_api_key, authorize_advanced_models 18 from database import record_api_usage 19 from auth_models import UsageRecord 20 21 # Configuration pour le stockage des vidéos et transcriptions 22 UPLOAD_DIR = Path("uploads/videos") 23 RESULTS_DIR = Path("results/transcriptions") 24 UPLOAD_DIR.mkdir(parents=True, exist_ok=True) 25 RESULTS_DIR.mkdir(parents=True, exist_ok=True) 26 27 # Logging 28 logger = logging.getLogger("transcription_api") 29 30 # Création du router 31 transcription_router = APIRouter( 32 prefix="/api/transcription", 33 tags=["video transcription"], 34 responses={401: {"description": "Non autorisé"}}, 35 ) 36 37 # Stockage en mémoire des tâches en cours 38 transcription_tasks = {} 39 40 # Classe pour suivre la progression 41 class ProgressTracker: 42 def __init__(self, task_id: str): 43 self.task_id = task_id 44 self.progress = 0 45 self.message = "Initializing..." 46 47 def __call__(self, progress: float, desc: str = None): 48 self.progress = float(progress) * 100 49 if desc: 50 self.message = desc 51 52 # Mettre à jour l'état de la tâche 53 if self.task_id in transcription_tasks: 54 transcription_tasks[self.task_id]["progress"] = self.progress 55 transcription_tasks[self.task_id]["message"] = self.message 56 57 # Fonction pour vérifier si l'extension est autorisée 58 def is_allowed_video_file(filename: str) -> bool: 59 allowed_extensions = [".mp4", ".avi", ".mov", ".mkv", ".webm"] 60 return any(filename.lower().endswith(ext) for ext in allowed_extensions) 61 62 # Fonction pour sauvegarder une vidéo uploadée 63 async def save_uploaded_video(file: UploadFile) -> str: 64 if not is_allowed_video_file(file.filename): 65 raise HTTPException( 66 status_code=400, 67 detail="Format de fichier non autorisé. Formats acceptés: .mp4, .avi, .mov, .mkv, .webm" 68 ) 69 70 # Générer un nom de fichier unique 71 file_extension = os.path.splitext(file.filename)[1].lower() 72 filename = f"{uuid.uuid4().hex}{file_extension}" 73 file_path = UPLOAD_DIR / filename 74 75 # Sauvegarder le fichier 76 async with aiofiles.open(file_path, "wb") as out_file: 77 content = await file.read() 78 await out_file.write(content) 79 80 return str(file_path) 81 82 # Fonction pour traiter une transcription monologue 83 async def process_transcription_monologue( 84 task_id: str, 85 video_path: str, 86 model_size: str, 87 keep_video: bool, 88 api_key_info: Any 89 ): 90 progress_tracker = ProgressTracker(task_id) 91 92 try: 93 # Mettre à jour l'état de la tâche 94 transcription_tasks[task_id]["status"] = "running" 95 transcription_tasks[task_id]["started_at"] = time.time() 96 97 # Chemin pour le fichier de sortie 98 output_txt = RESULTS_DIR / f"{task_id}_monologue.txt" 99 100 # Exécuter la transcription 101 start_time = time.time() 102 result = process_monologue( 103 video_path, 104 str(output_txt), 105 model_size, 106 progress=progress_tracker 107 ) 108 109 # Créer un fichier JSON avec les résultats détaillés 110 result_file = RESULTS_DIR / f"{task_id}_monologue.json" 111 results = { 112 "transcription": result["transcription"], 113 "segments": result["segments"], 114 "processing_time": time.time() - start_time 115 } 116 117 with open(result_file, "w", encoding="utf-8") as f: 118 json.dump(results, f, ensure_ascii=False, indent=2) 119 120 # Mettre à jour l'état de la tâche 121 transcription_tasks[task_id].update({ 122 "status": "completed", 123 "results": results, 124 "completed_at": time.time(), 125 "message": "Transcription complete", 126 "progress": 100, 127 "result_file": str(result_file), 128 "text_file": str(output_txt) 129 }) 130 131 logger.info(f"Tâche de transcription monologue {task_id} terminée avec succès") 132 133 except Exception as e: 134 logger.error(f"Erreur lors de la transcription monologue {task_id}: {str(e)}") 135 transcription_tasks[task_id].update({ 136 "status": "failed", 137 "error": str(e), 138 "completed_at": time.time(), 139 "message": f"Error: {str(e)}", 140 "progress": 100 141 }) 142 143 finally: 144 # Supprimer le fichier vidéo si demandé 145 if not keep_video and os.path.exists(video_path): 146 try: 147 os.unlink(video_path) 148 except Exception as e: 149 logger.warning(f"Impossible de supprimer le fichier vidéo {video_path}: {str(e)}") 150 151 # Fonction pour traiter une transcription avec identification des locuteurs 152 async def process_transcription_multispeaker( 153 task_id: str, 154 video_path: str, 155 model_size: str, 156 huggingface_token: str, 157 keep_video: bool, 158 api_key_info: Any 159 ): 160 progress_tracker = ProgressTracker(task_id) 161 162 try: 163 # Mettre à jour l'état de la tâche 164 transcription_tasks[task_id]["status"] = "running" 165 transcription_tasks[task_id]["started_at"] = time.time() 166 167 # Chemin pour le fichier de sortie 168 output_txt = RESULTS_DIR / f"{task_id}_multispeaker.txt" 169 170 # Exécuter la transcription avec identification des locuteurs 171 start_time = time.time() 172 result = process_multiple_speakers( 173 video_path, 174 str(output_txt), 175 model_size, 176 huggingface_token, 177 progress=progress_tracker 178 ) 179 180 # Créer un fichier JSON avec les résultats détaillés 181 result_file = RESULTS_DIR / f"{task_id}_multispeaker.json" 182 results = { 183 "segments": result["segments"], 184 "processing_time": time.time() - start_time 185 } 186 187 with open(result_file, "w", encoding="utf-8") as f: 188 json.dump(results, f, ensure_ascii=False, indent=2) 189 190 # Mettre à jour l'état de la tâche 191 transcription_tasks[task_id].update({ 192 "status": "completed", 193 "results": results, 194 "completed_at": time.time(), 195 "message": "Transcription with speaker identification complete", 196 "progress": 100, 197 "result_file": str(result_file), 198 "text_file": str(output_txt) 199 }) 200 201 logger.info(f"Tâche de transcription multispeaker {task_id} terminée avec succès") 202 203 except Exception as e: 204 logger.error(f"Erreur lors de la transcription multispeaker {task_id}: {str(e)}") 205 transcription_tasks[task_id].update({ 206 "status": "failed", 207 "error": str(e), 208 "completed_at": time.time(), 209 "message": f"Error: {str(e)}", 210 "progress": 100 211 }) 212 213 finally: 214 # Supprimer le fichier vidéo si demandé 215 if not keep_video and os.path.exists(video_path): 216 try: 217 os.unlink(video_path) 218 except Exception as e: 219 logger.warning(f"Impossible de supprimer le fichier vidéo {video_path}: {str(e)}") 220 221 @transcription_router.post("/monologue", tags=["video transcription"]) 222 async def start_monologue_transcription( 223 background_tasks: BackgroundTasks, 224 file: UploadFile = File(...), 225 model_size: str = Form("medium"), 226 keep_video: bool = Form(False), 227 api_key_info = Depends(validate_api_key) 228 ): 229 """ 230 Démarre une transcription monologue (sans identification des locuteurs). 231 232 Args: 233 file: Fichier vidéo à transcrire 234 model_size: Taille du modèle Whisper (tiny, base, small, medium, large) 235 keep_video: Si True, conserve la vidéo après la transcription 236 237 Returns: 238 Un objet de réponse avec l'ID de la tâche pour vérifier l'état plus tard 239 """ 240 # Vérifier l'autorisation pour les modèles avancés 241 if model_size in ["large"]: 242 authorize_advanced_models(api_key_info) 243 244 # Sauvegarder la vidéo uploadée 245 video_path = await save_uploaded_video(file) 246 247 # Générer un ID de tâche 248 task_id = str(uuid.uuid4()) 249 250 # Enregistrer la nouvelle tâche 251 transcription_tasks[task_id] = { 252 "type": "monologue", 253 "status": "pending", 254 "video_path": video_path, 255 "filename": file.filename, 256 "created_at": time.time(), 257 "keep_video": keep_video, 258 "model_size": model_size, 259 "message": "Task queued", 260 "progress": 0, 261 "user_id": api_key_info.user_id, 262 "api_key_id": api_key_info.id 263 } 264 265 # Lancer la tâche en arrière-plan 266 background_tasks.add_task( 267 process_transcription_monologue, 268 task_id=task_id, 269 video_path=video_path, 270 model_size=model_size, 271 keep_video=keep_video, 272 api_key_info=api_key_info 273 ) 274 275 # Enregistrer l'utilisation 276 usage_record = UsageRecord( 277 user_id=api_key_info.user_id, 278 api_key_id=api_key_info.key, 279 request_path="/api/transcription/monologue", 280 request_method="POST", 281 tokens_input=0, # À compléter plus tard 282 tokens_output=0, # À compléter plus tard 283 processing_time=0.0, # À mettre à jour une fois terminé 284 status_code=200 285 ) 286 record_api_usage(usage_record) 287 288 logger.info(f"Tâche de transcription monologue créée avec ID: {task_id}") 289 290 return { 291 "task_id": task_id, 292 "status": "pending", 293 "message": "Monologue transcription task started" 294 } 295 296 @transcription_router.post("/multispeaker", tags=["video transcription"]) 297 async def start_multispeaker_transcription( 298 background_tasks: BackgroundTasks, 299 file: UploadFile = File(...), 300 model_size: str = Form("medium"), 301 huggingface_token: str = Form(None), 302 keep_video: bool = Form(False), 303 api_key_info = Depends(validate_api_key) 304 ): 305 """ 306 Démarre une transcription avec identification des locuteurs. 307 308 Args: 309 file: Fichier vidéo à transcrire 310 model_size: Taille du modèle Whisper (tiny, base, small, medium, large) 311 huggingface_token: Token Hugging Face pour l'accès au modèle (facultatif) 312 keep_video: Si True, conserve la vidéo après la transcription 313 314 Returns: 315 Un objet de réponse avec l'ID de la tâche pour vérifier l'état plus tard 316 """ 317 # Vérifier l'autorisation pour les modèles avancés (cette analyse est toujours considérée comme avancée) 318 authorize_advanced_models(api_key_info) 319 320 # Vérifier la présence d'un token Hugging Face 321 if not huggingface_token and not os.environ.get("HUGGINGFACE_TOKEN"): 322 raise HTTPException( 323 status_code=400, 324 detail="Un token Hugging Face est requis pour l'identification des locuteurs. Fournissez-le dans la requête ou définissez la variable d'environnement HUGGINGFACE_TOKEN." 325 ) 326 327 # Sauvegarder la vidéo uploadée 328 video_path = await save_uploaded_video(file) 329 330 # Générer un ID de tâche 331 task_id = str(uuid.uuid4()) 332 333 # Enregistrer la nouvelle tâche 334 transcription_tasks[task_id] = { 335 "type": "multispeaker", 336 "status": "pending", 337 "video_path": video_path, 338 "filename": file.filename, 339 "created_at": time.time(), 340 "keep_video": keep_video, 341 "model_size": model_size, 342 "huggingface_token": "[REDACTED]" if huggingface_token else None, 343 "message": "Task queued", 344 "progress": 0, 345 "user_id": api_key_info.user_id, 346 "api_key_id": api_key_info.id 347 } 348 349 # Lancer la tâche en arrière-plan 350 background_tasks.add_task( 351 process_transcription_multispeaker, 352 task_id=task_id, 353 video_path=video_path, 354 model_size=model_size, 355 huggingface_token=huggingface_token, 356 keep_video=keep_video, 357 api_key_info=api_key_info 358 ) 359 360 # Enregistrer l'utilisation 361 usage_record = UsageRecord( 362 user_id=api_key_info.user_id, 363 api_key_id=api_key_info.key, 364 request_path="/api/transcription/multispeaker", 365 request_method="POST", 366 tokens_input=0, # À compléter plus tard 367 tokens_output=0, # À compléter plus tard 368 processing_time=0.0, # À mettre à jour une fois terminé 369 status_code=200 370 ) 371 record_api_usage(usage_record) 372 373 logger.info(f"Tâche de transcription multispeaker créée avec ID: {task_id}") 374 375 return { 376 "task_id": task_id, 377 "status": "pending", 378 "message": "Multi-speaker transcription task started" 379 } 380 381 @transcription_router.get("/tasks/{task_id}", tags=["video transcription"]) 382 async def get_transcription_task_status( 383 task_id: str, 384 api_key_info = Depends(validate_api_key) 385 ): 386 """ 387 Vérifie l'état d'une tâche de transcription. 388 389 Args: 390 task_id: L'identifiant de la tâche 391 392 Returns: 393 L'état actuel de la tâche de transcription 394 """ 395 if task_id not in transcription_tasks: 396 raise HTTPException(status_code=404, detail=f"Tâche {task_id} non trouvée") 397 398 task_info = transcription_tasks[task_id] 399 400 # Vérifier si la tâche appartient à l'utilisateur associé à la clé API 401 if task_info.get("user_id") != api_key_info.user_id: 402 raise HTTPException( 403 status_code=403, 404 detail="Vous n'avez pas accès à cette tâche" 405 ) 406 407 # Préparer la réponse 408 response = { 409 "task_id": task_id, 410 "type": task_info.get("type"), 411 "status": task_info.get("status"), 412 "message": task_info.get("message"), 413 "progress": task_info.get("progress", 0), 414 "filename": task_info.get("filename"), 415 "model_size": task_info.get("model_size"), 416 "created_at": task_info.get("created_at"), 417 "started_at": task_info.get("started_at"), 418 "completed_at": task_info.get("completed_at") 419 } 420 421 # Ajouter les résultats si la tâche est terminée 422 if task_info.get("status") == "completed": 423 response["results"] = task_info.get("results") 424 response["result_file"] = task_info.get("result_file") 425 response["text_file"] = task_info.get("text_file") 426 elif task_info.get("status") == "failed": 427 response["error"] = task_info.get("error") 428 response["error_type"] = task_info.get("error_type") 429 response["message"] = task_info.get("message") 430 response["progress"] = 100 431 response["completed_at"] = task_info.get("completed_at") 432 else: 433 response["message"] = task_info.get("message") 434 response["progress"] = task_info.get("progress", 0) 435 response["created_at"] = task_info.get("created_at") 436 response["started_at"] = task_info.get("started_at") 437 response["completed_at"] = task_info.get("completed_at") 438 response["result_file"] = task_info.get("result_file") 439 response["text_file"] = task_info.get("text_file") 440 return JSONResponse(response) 441 442 @transcription_router.get("/tasks/{task_id}", tags=["video transcription"]) 443 async def get_transcription_task_status( 444 task_id: str, 445 api_key_info = Depends(validate_api_key) 446 ): 447 """ 448 Vérifie l'état d'une tâche de transcription. 449 450 Args: 451 task_id: L'identifiant de la tâche 452 453 Returns: 454 L'état actuel de la tâche de transcription 455 """ 456 if task_id not in transcription_tasks: 457 raise HTTPException(status_code=404, detail=f"Tâche {task_id} non trouvée") 458 459 task_info = transcription_tasks[task_id] 460 461 # Vérifier si la tâche appartient à l'utilisateur associé à la clé API 462 if task_info.get("user_id") != api_key_info.user_id: 463 raise HTTPException( 464 status_code=403, 465 detail="Vous n'avez pas accès à cette tâche" 466 ) 467 468 # Préparer la réponse de base 469 response = { 470 "task_id": task_id, 471 "type": task_info.get("type"), 472 "status": task_info.get("status"), 473 "message": task_info.get("message"), 474 "progress": task_info.get("progress", 0), 475 "filename": task_info.get("filename"), 476 "model_size": task_info.get("model_size"), 477 "created_at": task_info.get("created_at"), 478 "started_at": task_info.get("started_at"), 479 "completed_at": task_info.get("completed_at") 480 } 481 482 # Ajouter les résultats si la tâche est terminée ou échouée 483 if task_info.get("status") == "completed": 484 response["results"] = task_info.get("results") 485 response["result_file"] = task_info.get("result_file") 486 response["text_file"] = task_info.get("text_file") 487 elif task_info.get("status") == "failed": 488 response["error"] = task_info.get("error") 489 490 return JSONResponse(response) 491 492 493 @transcription_router.get("/download/{task_id}/{file_type}", tags=["video transcription"]) 494 async def download_file( 495 task_id: str, 496 file_type: str, 497 api_key_info = Depends(validate_api_key) 498 ): 499 """ 500 Permet de télécharger le fichier de transcription associé à une tâche. 501 502 Args: 503 task_id: Identifiant de la tâche. 504 file_type: Type de fichier à télécharger ("text" ou "json"). 505 506 Returns: 507 Un FileResponse contenant le fichier demandé. 508 """ 509 if task_id not in transcription_tasks: 510 raise HTTPException(status_code=404, detail="Tâche non trouvée") 511 512 task_info = transcription_tasks[task_id] 513 514 # Vérifier si la tâche appartient à l'utilisateur 515 if task_info.get("user_id") != api_key_info.user_id: 516 raise HTTPException(status_code=403, detail="Accès non autorisé") 517 518 if file_type == "text": 519 file_path = task_info.get("text_file") 520 elif file_type == "json": 521 file_path = task_info.get("result_file") 522 else: 523 raise HTTPException(status_code=400, detail="Type de fichier non valide. Utilisez 'text' ou 'json'.") 524 525 if not file_path or not os.path.exists(file_path): 526 raise HTTPException(status_code=404, detail="Fichier non trouvé") 527 528 return FileResponse(file_path, filename=os.path.basename(file_path)) 529 530 531 @transcription_router.get("/tasks", tags=["video transcription"]) 532 async def list_user_tasks(api_key_info = Depends(validate_api_key)): 533 """ 534 Liste toutes les tâches de transcription pour l'utilisateur connecté. 535 536 Returns: 537 Une liste de tâches avec leurs états. 538 """ 539 user_tasks = [ 540 { 541 "task_id": task_id, 542 "type": info.get("type"), 543 "status": info.get("status"), 544 "message": info.get("message"), 545 "progress": info.get("progress", 0), 546 "filename": info.get("filename"), 547 "model_size": info.get("model_size"), 548 "created_at": info.get("created_at"), 549 "started_at": info.get("started_at"), 550 "completed_at": info.get("completed_at") 551 } 552 for task_id, info in transcription_tasks.items() if info.get("user_id") == api_key_info.user_id 553 ] 554 return JSONResponse({"tasks": user_tasks})