/ 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})