binaryMao commited on
Commit
bde1ae6
·
verified ·
1 Parent(s): 33e6f73

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -91
app.py CHANGED
@@ -1,26 +1,13 @@
1
  # -*- coding: utf-8 -*-
2
- """
3
- ROBOTSMALI — Sous-titrage Bambara (VERSION 7.7 - INTÉGRALE)
4
- -=
5
- """
6
- import os
7
- import shlex
8
- import subprocess
9
- import tempfile
10
- import traceback
11
- import textwrap
12
- import time
13
- from pathlib import Path
14
-
15
  import torch
16
  from huggingface_hub import snapshot_download
17
  from nemo.collections import asr as nemo_asr
18
  import gradio as gr
19
 
20
- # ---------------------------- # CONFIGURATION DES MODÈLES # ----------------------------
21
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
22
 
23
- # La liste complète et correcte des modèles RobotsMali
24
  MODELS = {
25
  "Soloba V1 (CTC)": ("RobotsMali/soloba-ctc-0.6b-v1", "ctc"),
26
  "Soloni V1 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v1", "rnnt"),
@@ -30,7 +17,7 @@ MODELS = {
30
  "QuartzNet V0 (CTC-char)": ("RobotsMali/stt-bm-quartznet15x5-v0", "ctc_char"),
31
  }
32
 
33
- # Détection du chemin absolu pour la vidéo d'exemple
34
  def get_absolute_example():
35
  paths = [
36
  os.path.abspath("MARALINKE.mp4"),
@@ -45,8 +32,7 @@ def get_absolute_example():
45
  EXAMPLE_PATH = get_absolute_example()
46
  _cache = {}
47
 
48
- # ---------------------------- # MOTEUR IA & VIDÉO # ----------------------------
49
-
50
  def load_model(name):
51
  if name in _cache: return _cache[name]
52
  _cache.clear()
@@ -61,109 +47,109 @@ def load_model(name):
61
  elif mode == "ctc_char":
62
  model = nemo_asr.models.EncDecCTCModel.restore_from(nemo_file)
63
  else:
64
- try:
65
- model = nemo_asr.models.EncDecCTCModelBPE.restore_from(nemo_file)
66
- except:
67
- model = nemo_asr.models.EncDecCTCModel.restore_from(nemo_file)
68
 
69
  model.to(DEVICE).eval()
70
  _cache[name] = model
71
  return model
72
 
73
- def burn_subtitles(video_path, words, duration):
74
- out_name = f"robotsmali_output_{int(time.time())}.mp4"
75
- out_path = os.path.abspath(out_name)
76
-
77
- # Génération du SRT
78
- chunk_size = 7
79
- with tempfile.NamedTemporaryFile(suffix=".srt", mode="w", encoding="utf-8", delete=False) as tf:
80
- for i, idx in enumerate(range(0, len(words), chunk_size)):
81
- chunk = words[idx : idx + chunk_size]
82
- start = (idx / len(words)) * duration
83
- end = (min(idx + chunk_size, len(words)) / len(words)) * duration
84
- def t_srt(sec):
85
- h=int(sec//3600); m=int((sec%3600)//60); s=int(sec%60); ms=int((sec-int(sec))*1000)
86
- return f"{h:02}:{m:02}:{s:02},{ms:03}"
87
- txt = "\n".join(textwrap.wrap(" ".join(chunk), 40))
88
- tf.write(f"{i+1}\n{t_srt(start)} --> {t_srt(end)}\n{txt}\n\n")
89
- srt_name = tf.name
90
-
91
- # Rendu FFmpeg avec optimisation FastStart pour corriger la durée web
92
- vf = f"subtitles={shlex.quote(srt_name)}:force_style='Fontsize=22,PrimaryColour=&HFFFFFF&,OutlineColour=&H000000&'"
93
- cmd = (
94
- f'ffmpeg -hide_banner -loglevel error -y -i {shlex.quote(video_path)} '
95
- f'-vf {shlex.quote(vf)} -c:v libx264 -pix_fmt yuv420p -preset ultrafast -crf 28 '
96
- f'-c:a aac -b:a 128k -movflags +faststart {shlex.quote(out_path)}'
97
- )
98
- subprocess.run(cmd, shell=True, check=True)
99
- if os.path.exists(srt_name): os.remove(srt_name)
100
- return out_path
101
 
102
- # ---------------------------- # PIPELINE # ----------------------------
 
 
 
 
103
 
104
- def pipeline(video_input, model_name):
 
105
  try:
106
- if not video_input:
107
- yield "### ❌ Erreur : Vidéo manquante.", None
108
- return
109
 
110
- yield "### ⏳ Étape 1/3 : Extraction Audio...", None
111
- wav_path = os.path.abspath("temp_audio.wav")
112
- subprocess.run(f"ffmpeg -y -i {shlex.quote(video_input)} -vn -ac 1 -ar 16000 {wav_path}", shell=True, check=True)
 
 
113
 
114
- # Détection précise de la durée
115
- dur_out = subprocess.run(f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {shlex.quote(video_input)}',
116
- shell=True, stdout=subprocess.PIPE, text=True).stdout
117
- duration = float(dur_out.strip()) if dur_out.strip() else 10.0
118
-
119
- yield f"### ⏳ Étape 2/3 : Transcription avec {model_name}...", None
120
  model = load_model(model_name)
121
- res = model.transcribe([wav_path])[0]
122
- words = (res.text if hasattr(res, 'text') else str(res)).split()
123
-
124
- if not words:
125
- yield "### ⚠️ Aucune parole détectée.", None
126
- return
127
 
128
- yield "### Étape 3/3 : Finalisation Vidéo...", None
129
- final_video = burn_subtitles(video_input, words, duration)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- if os.path.exists(wav_path): os.remove(wav_path)
132
- yield "### ✅ Succès !", final_video
133
 
134
  except Exception as e:
135
  traceback.print_exc()
136
- yield f"### ❌ Erreur : {str(e)}", None
137
-
138
- def force_load_demo():
139
- return EXAMPLE_PATH
140
-
141
- # ---------------------------- # INTERFACE # ----------------------------
142
-
143
- with gr.Blocks(theme=gr.themes.Soft(), css="body { background-color: #0b0e14; }") as demo:
144
- gr.HTML("<h1 style='text-align:center; color:#facc15;'>🤖 ROBOTSMALI V7.7</h1>")
145
 
 
 
 
 
146
  with gr.Row():
147
  with gr.Column():
148
- gr.Markdown("### 📥 CHARGEMENT")
149
- v_in = gr.Video(label="Vidéo source", interactive=True)
 
150
 
151
  if EXAMPLE_PATH:
152
- btn_demo = gr.Button("📂 CHARGER LA DÉMO (MARALINKE)", variant="secondary")
153
 
154
  m_sel = gr.Dropdown(list(MODELS.keys()), value="Soloba V1 (CTC)", label="Modèle IA")
155
  btn_run = gr.Button("🚀 GÉNÉRER", variant="primary")
156
-
157
  with gr.Column():
158
  gr.Markdown("### 📤 RÉSULTAT")
159
  status = gr.Markdown("### État\nPrêt")
160
- v_out = gr.Video(label="Vidéo finale")
161
 
162
  # Actions
163
  if EXAMPLE_PATH:
164
- btn_demo.click(fn=force_load_demo, outputs=v_in)
165
- gr.Examples(examples=[[EXAMPLE_PATH, "Soloba V1 (CTC)"]], inputs=[v_in, m_sel], cache_examples=False)
166
-
167
  btn_run.click(pipeline, [v_in, m_sel], [status, v_out])
168
 
169
  if __name__ == "__main__":
 
1
  # -*- coding: utf-8 -*-
2
+ import os, shlex, subprocess, tempfile, traceback, textwrap, time
 
 
 
 
 
 
 
 
 
 
 
 
3
  import torch
4
  from huggingface_hub import snapshot_download
5
  from nemo.collections import asr as nemo_asr
6
  import gradio as gr
7
 
8
+ # 1. CONFIGURATION DU MATÉRIEL ET DES MODÈLES
9
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
10
 
 
11
  MODELS = {
12
  "Soloba V1 (CTC)": ("RobotsMali/soloba-ctc-0.6b-v1", "ctc"),
13
  "Soloni V1 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v1", "rnnt"),
 
17
  "QuartzNet V0 (CTC-char)": ("RobotsMali/stt-bm-quartznet15x5-v0", "ctc_char"),
18
  }
19
 
20
+ # 2. GESTION DES CHEMINS (Correction du bug de chargement exemple)
21
  def get_absolute_example():
22
  paths = [
23
  os.path.abspath("MARALINKE.mp4"),
 
32
  EXAMPLE_PATH = get_absolute_example()
33
  _cache = {}
34
 
35
+ # 3. MOTEUR IA NEMO
 
36
  def load_model(name):
37
  if name in _cache: return _cache[name]
38
  _cache.clear()
 
47
  elif mode == "ctc_char":
48
  model = nemo_asr.models.EncDecCTCModel.restore_from(nemo_file)
49
  else:
50
+ model = nemo_asr.models.EncDecCTCModelBPE.restore_from(nemo_file)
 
 
 
51
 
52
  model.to(DEVICE).eval()
53
  _cache[name] = model
54
  return model
55
 
56
+ # 4. UTILITAIRES DE SYNCHRONISATION
57
+ def format_ts(seconds):
58
+ td = time.gmtime(seconds)
59
+ ms = int((seconds - int(seconds)) * 1000)
60
+ return f"{time.strftime('%H:%M:%S', td)},{ms:03}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ def get_real_duration(file_path):
63
+ cmd = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {shlex.quote(file_path)}"
64
+ res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
65
+ try: return float(res.stdout.strip())
66
+ except: return 0.0
67
 
68
+ # 5. PIPELINE DE TRAITEMENT
69
+ def pipeline(video_in, model_name):
70
  try:
71
+ if not video_in: return "❌ Erreur : Aucune vidéo détectée.", None
 
 
72
 
73
+ # Étape A : Extraction Audio
74
+ yield "⏳ Extraction de l'audio...", None
75
+ wav_path = os.path.abspath("temp.wav")
76
+ subprocess.run(f"ffmpeg -y -i {shlex.quote(video_in)} -vn -ac 1 -ar 16000 {wav_path}", shell=True, check=True)
77
+ duration = get_real_duration(video_in)
78
 
79
+ # Étape B : Transcription avec Offsets (Alignement Natif)
80
+ yield f"⏳ Transcription IA ({model_name}) avec alignement...", None
 
 
 
 
81
  model = load_model(model_name)
 
 
 
 
 
 
82
 
83
+ # Utilisation de return_hypotheses pour récupérer les timestamps CTC
84
+ hypotheses = model.transcribe([wav_path], return_hypotheses=True)[0]
85
+ words_with_ts = []
86
+
87
+ if hasattr(hypotheses, 'word_offsets') and hypotheses.word_offsets:
88
+ offsets = hypotheses.word_offsets
89
+ words = hypotheses.text.split()
90
+ # Facteur 0.02 (Stride de NeMo) pour convertir frames en secondes
91
+ for i, word in enumerate(words):
92
+ t_start = offsets[i] * 0.02
93
+ words_with_ts.append({"word": word, "start": t_start, "end": t_start + 0.4})
94
+ else:
95
+ # Fallback temporel linéaire si les offsets ne sont pas disponibles (RNNT)
96
+ words = (hypotheses.text if hasattr(hypotheses, 'text') else str(hypotheses)).split()
97
+ for i, w in enumerate(words):
98
+ words_with_ts.append({"word": w, "start": (i/len(words))*duration, "end": ((i+1)/len(words))*duration})
99
+
100
+ # Étape C : Création du SRT segmenté
101
+ yield "⏳ Génération des segments synchronisés...", None
102
+ srt_path = os.path.abspath("output.srt")
103
+ words_per_line = 6
104
+ with open(srt_path, "w", encoding="utf-8") as f:
105
+ for i in range(0, len(words_with_ts), words_per_line):
106
+ chunk = words_with_ts[i:i+words_per_line]
107
+ start_time = chunk[0]['start']
108
+ end_time = chunk[-1]['end'] + 0.5
109
+ f.write(f"{(i//words_per_line)+1}\n{format_ts(start_time)} --> {format_ts(end_time)}\n")
110
+ f.write(" ".join([w['word'] for w in chunk]) + "\n\n")
111
+
112
+ # Étape D : Encodage et "Burn-in"
113
+ yield "⏳ Incrustation des sous-titres (FastStart)...", None
114
+ out_path = os.path.abspath(f"resultat_{int(time.time())}.mp4")
115
+ cmd_ffmpeg = (
116
+ f"ffmpeg -y -i {shlex.quote(video_in)} "
117
+ f"-vf \"subtitles={shlex.quote(srt_path)}:force_style='Alignment=2,FontSize=20,PrimaryColour=&H00FFFF&'\" "
118
+ f"-c:v libx264 -pix_fmt yuv420p -movflags +faststart -c:a aac {out_path}"
119
+ )
120
+ subprocess.run(cmd_ffmpeg, shell=True, check=True)
121
 
122
+ yield "✅ Terminé avec succès !", out_path
 
123
 
124
  except Exception as e:
125
  traceback.print_exc()
126
+ yield f"❌ Erreur : {str(e)}", None
 
 
 
 
 
 
 
 
127
 
128
+ # 6. INTERFACE GRADIO (Webcam + Example Fix)
129
+ with gr.Blocks(theme=gr.themes.Soft(), css="body {background-color: #0b1120;}") as demo:
130
+ gr.HTML("<h1 style='text-align:center; color:#facc15;'>🤖 ROBOTSMALI V10.5</h1>")
131
+
132
  with gr.Row():
133
  with gr.Column():
134
+ gr.Markdown("### 📥 SOURCE")
135
+ # Supporte l'upload ET la webcam
136
+ v_in = gr.Video(label="Webcam ou Fichier", sources=["upload", "webcam"], interactive=True)
137
 
138
  if EXAMPLE_PATH:
139
+ btn_demo = gr.Button("📂 CHARGER LA VIDÉO D'EXEMPLE", variant="secondary")
140
 
141
  m_sel = gr.Dropdown(list(MODELS.keys()), value="Soloba V1 (CTC)", label="Modèle IA")
142
  btn_run = gr.Button("🚀 GÉNÉRER", variant="primary")
143
+
144
  with gr.Column():
145
  gr.Markdown("### 📤 RÉSULTAT")
146
  status = gr.Markdown("### État\nPrêt")
147
+ v_out = gr.Video(label="Vidéo finale synchronisée")
148
 
149
  # Actions
150
  if EXAMPLE_PATH:
151
+ btn_demo.click(fn=lambda: EXAMPLE_PATH, outputs=v_in)
152
+
 
153
  btn_run.click(pipeline, [v_in, m_sel], [status, v_out])
154
 
155
  if __name__ == "__main__":