binaryMao commited on
Commit
bd6b31b
·
verified ·
1 Parent(s): 0e0456f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +36 -66
app.py CHANGED
@@ -1,7 +1,6 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- ROBOTSMALI — Sous-titrage Bambara (VERSION INTÉGRALE V6.0)
4
- Incrustation de sous-titres avec tous les modèles RobotsMali.
5
  """
6
  import os
7
  import shlex
@@ -11,6 +10,7 @@ import traceback
11
  import random
12
  import textwrap
13
  import time
 
14
  from pathlib import Path
15
 
16
  import numpy as np
@@ -27,7 +27,6 @@ random.seed(1234)
27
  np.random.seed(1234)
28
  torch.manual_seed(1234)
29
 
30
- # TOUS VOS MODÈLES SONT ICI
31
  MODELS = {
32
  "Soloni V1 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v1", "rnnt"),
33
  "Soloni V0 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v0", "rnnt"),
@@ -37,7 +36,6 @@ MODELS = {
37
  "QuartzNet V0 (CTC-char)": ("RobotsMali/stt-bm-quartznet15x5-v0", "ctc_char"),
38
  }
39
 
40
- # EXEMPLE CONFIGURÉ
41
  VIDEO_EXAMPLES = [
42
  ["examples/MARALINKE.mp4", "Soloba V1 (CTC)"]
43
  ]
@@ -47,27 +45,17 @@ _cache = {}
47
  # ---------------------------- # FONCTIONS TECHNIQUES # ----------------------------
48
 
49
  def run_cmd(cmd):
50
- """Exécute une commande système."""
51
  res = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
52
  if res.returncode != 0:
53
  raise RuntimeError(f"Erreur FFmpeg: {res.stdout}")
54
  return res.stdout
55
 
56
- def ffprobe_duration(path):
57
- cmd = f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {shlex.quote(path)}'
58
- out = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
59
- try: return float(out.stdout.strip())
60
- except: return None
61
-
62
  def load_model(name):
63
- """Charge le modèle sélectionné et nettoie le cache si nécessaire."""
64
  if name in _cache: return _cache[name]
65
-
66
- # Nettoyage pour économiser la RAM
67
  if len(_cache) > 0:
68
  _cache.clear()
69
  if torch.cuda.is_available(): torch.cuda.empty_cache()
70
-
71
  repo, mode = MODELS[name]
72
  folder = snapshot_download(repo, local_dir_use_symlinks=False)
73
  nemo_file = next((os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(".nemo")), None)
@@ -79,59 +67,55 @@ def load_model(name):
79
  else:
80
  try: model = nemo_asr.models.EncDecCTCModelBPE.restore_from(nemo_file)
81
  except: model = nemo_asr.models.EncDecCTCModel.restore_from(nemo_file)
82
-
83
  model.to(DEVICE).eval()
84
  _cache[name] = model
85
  return model
86
 
87
- def extract_audio(video_path, out_wav):
88
- """Stabilisation du codec (pour la webcam) et extraction audio."""
89
- tmp_fd, stabilized_mp4 = tempfile.mkstemp(suffix="_stabilized.mp4")
90
- os.close(tmp_fd)
91
- # On force le H.264 pour éviter les erreurs de lecture
92
- run_cmd(f'ffmpeg -hide_banner -loglevel error -y -i {shlex.quote(video_path)} -c:v libx264 -preset ultrafast -crf 23 -c:a aac {shlex.quote(stabilized_mp4)}')
93
- run_cmd(f'ffmpeg -hide_banner -loglevel error -y -i {shlex.quote(stabilized_mp4)} -vn -ac 1 -ar 16000 -f wav {shlex.quote(out_wav)}')
94
- if os.path.exists(stabilized_mp4): os.remove(stabilized_mp4)
95
-
96
  def burn_subtitles(video_path, words, duration):
97
- """Génère le fichier SRT et l'incruste dans la vidéo finale."""
98
- out_path = f"output_{int(time.time())}.mp4"
 
99
  chunk_size = 7
100
  with tempfile.NamedTemporaryFile(suffix=".srt", mode="w", encoding="utf-8", delete=False) as tf:
101
  for i, idx in enumerate(range(0, len(words), chunk_size)):
102
  chunk = words[idx : idx + chunk_size]
103
  start = (idx / len(words)) * duration
104
  end = (min(idx + chunk_size, len(words)) / len(words)) * duration
105
-
106
  def t_srt(sec):
107
  h=int(sec//3600); m=int((sec%3600)//60); s=int(sec%60); ms=int((sec-int(sec))*1000)
108
  return f"{h:02}:{m:02}:{s:02},{ms:03}"
109
-
110
  txt = "\n".join(textwrap.wrap(" ".join(chunk), 40))
111
  tf.write(f"{i+1}\n{t_srt(start)} --> {t_srt(end)}\n{txt}\n\n")
112
  srt_name = tf.name
113
 
114
- # Encodage ultra-rapide pour éviter le timeout
115
  vf = f"subtitles={shlex.quote(srt_name)}:force_style='Fontsize=22,PrimaryColour=&HFFFFFF&,OutlineColour=&H000000&'"
116
- run_cmd(f'ffmpeg -hide_banner -loglevel error -y -i {shlex.quote(video_path)} -vf {shlex.quote(vf)} -c:v libx264 -preset ultrafast -crf 28 -c:a copy {shlex.quote(out_path)}')
 
 
 
 
 
117
  os.remove(srt_name)
118
  return out_path
119
 
120
- # ---------------------------- # PIPELINE # ----------------------------
121
-
122
  def pipeline(video_input, model_name):
123
  try:
124
  if not video_input: return "❌ Veuillez charger une vidéo", None
125
- video_path = video_input
126
 
127
- yield "⏳ Phase 1/3 : Stabilisation & Audio...", None
128
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
129
  wav_path = tf.name
130
 
131
- extract_audio(video_path, wav_path)
132
- duration = ffprobe_duration(video_path) or 10.0 # fallback
 
 
 
 
 
133
 
134
- yield f"⏳ Phase 2/3 : Analyse IA ({model_name})...", None
135
  model = load_model(model_name)
136
  res = model.transcribe([wav_path])[0]
137
  text = res.text if hasattr(res, 'text') else str(res)
@@ -139,17 +123,19 @@ def pipeline(video_input, model_name):
139
 
140
  if not words: return "⚠️ Pas de parole détectée.", None
141
 
142
- yield "⏳ Phase 3/3 : Génération des sous-titres...", None
143
- final_v = burn_subtitles(video_path, words, duration)
144
 
145
  if os.path.exists(wav_path): os.remove(wav_path)
146
- yield "✅ Sous-titrage terminé !", final_v
 
 
147
 
148
  except Exception as e:
149
  traceback.print_exc()
150
- yield f"❌ Erreur critique : {str(e)}", None
151
 
152
- # ---------------------------- # INTERFACE ARTISTIQUE # ----------------------------
153
 
154
  custom_css = """
155
  body { background-color: #0b0e14; }
@@ -160,34 +146,18 @@ body { background-color: #0b0e14; }
160
 
161
  with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
162
  with gr.Column(elem_id="title-header"):
163
- gr.HTML("""
164
- <h1 style='color:#facc15; font-size: 2.5rem; margin:0;'>🤖 ROBOTSMALI</h1>
165
- <p style='color:#94a3b8; font-style:italic;'>Intelligence Artificielle pour le Bambara</p>
166
- <div style="height: 3px; width: 60px; background: #facc15; margin: 15px auto;"></div>
167
- """)
168
 
169
  with gr.Row():
170
  with gr.Column():
171
- gr.Markdown("### 📥 Source Vidéo")
172
- v_in = gr.Video(label=None, mirror_webcam=False)
173
- m_sel = gr.Dropdown(list(MODELS.keys()), value="Soloba V1 (CTC)", label="Modèle IA")
174
- btn = gr.Button("🚀 GÉNÉRER LES SOUS-TITRES", variant="primary")
175
-
176
  with gr.Column():
177
- gr.Markdown("### 📤 Résultat")
178
- status = gr.Markdown("*Prêt pour le traitement...*")
179
- v_out = gr.Video(label=None)
180
-
181
- # EXEMPLES : cache_examples=False est crucial pour que le clic fonctionne
182
- gr.Examples(
183
- examples=VIDEO_EXAMPLES,
184
- inputs=[v_in, m_sel],
185
- label="📺 Vidéo d'exemple",
186
- cache_examples=False
187
- )
188
 
189
- gr.HTML("<div style='text-align: center; color: #475569; padding-top: 20px;'>© 2025 RobotsMali - Bamako</div>")
190
-
191
  btn.click(pipeline, [v_in, m_sel], [status, v_out])
192
 
193
  if __name__ == "__main__":
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ ROBOTSMALI — Sous-titrage Bambara (VERSION INTÉGRALE V6.1 - FIX FINAL OUTPUT)
 
4
  """
5
  import os
6
  import shlex
 
10
  import random
11
  import textwrap
12
  import time
13
+ import shutil
14
  from pathlib import Path
15
 
16
  import numpy as np
 
27
  np.random.seed(1234)
28
  torch.manual_seed(1234)
29
 
 
30
  MODELS = {
31
  "Soloni V1 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v1", "rnnt"),
32
  "Soloni V0 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v0", "rnnt"),
 
36
  "QuartzNet V0 (CTC-char)": ("RobotsMali/stt-bm-quartznet15x5-v0", "ctc_char"),
37
  }
38
 
 
39
  VIDEO_EXAMPLES = [
40
  ["examples/MARALINKE.mp4", "Soloba V1 (CTC)"]
41
  ]
 
45
  # ---------------------------- # FONCTIONS TECHNIQUES # ----------------------------
46
 
47
  def run_cmd(cmd):
 
48
  res = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
49
  if res.returncode != 0:
50
  raise RuntimeError(f"Erreur FFmpeg: {res.stdout}")
51
  return res.stdout
52
 
 
 
 
 
 
 
53
  def load_model(name):
 
54
  if name in _cache: return _cache[name]
 
 
55
  if len(_cache) > 0:
56
  _cache.clear()
57
  if torch.cuda.is_available(): torch.cuda.empty_cache()
58
+
59
  repo, mode = MODELS[name]
60
  folder = snapshot_download(repo, local_dir_use_symlinks=False)
61
  nemo_file = next((os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(".nemo")), None)
 
67
  else:
68
  try: model = nemo_asr.models.EncDecCTCModelBPE.restore_from(nemo_file)
69
  except: model = nemo_asr.models.EncDecCTCModel.restore_from(nemo_file)
 
70
  model.to(DEVICE).eval()
71
  _cache[name] = model
72
  return model
73
 
 
 
 
 
 
 
 
 
 
74
  def burn_subtitles(video_path, words, duration):
75
+ # Création d'un fichier de sortie dans un dossier temporaire Gradio
76
+ out_path = os.path.join(tempfile.gettempdir(), f"final_output_{int(time.time())}.mp4")
77
+
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
+ # Commande d'encodage optimisée pour le Web (H.264 Baseline + Faststart)
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'-movflags +faststart -c:a copy {shlex.quote(out_path)}'
97
+ )
98
+ run_cmd(cmd)
99
  os.remove(srt_name)
100
  return out_path
101
 
 
 
102
  def pipeline(video_input, model_name):
103
  try:
104
  if not video_input: return "❌ Veuillez charger une vidéo", None
 
105
 
106
+ yield "⏳ Phase 1/3 : Analyse Audio...", None
107
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
108
  wav_path = tf.name
109
 
110
+ # Extraction stable
111
+ run_cmd(f'ffmpeg -hide_banner -loglevel error -y -i {shlex.quote(video_input)} -vn -ac 1 -ar 16000 -f wav {shlex.quote(wav_path)}')
112
+
113
+ # Récupération durée
114
+ dur_out = subprocess.run(f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {shlex.quote(video_input)}',
115
+ shell=True, stdout=subprocess.PIPE, text=True).stdout
116
+ duration = float(dur_out.strip()) if dur_out.strip() else 10.0
117
 
118
+ yield f"⏳ Phase 2/3 : Transcription IA ({model_name})...", None
119
  model = load_model(model_name)
120
  res = model.transcribe([wav_path])[0]
121
  text = res.text if hasattr(res, 'text') else str(res)
 
123
 
124
  if not words: return "⚠️ Pas de parole détectée.", None
125
 
126
+ yield "⏳ Phase 3/3 : Encodage vidéo final...", None
127
+ final_v = burn_subtitles(video_input, words, duration)
128
 
129
  if os.path.exists(wav_path): os.remove(wav_path)
130
+
131
+ # On force Gradio à renvoyer le chemin absolu
132
+ yield "✅ Succès !", gr.update(value=final_v)
133
 
134
  except Exception as e:
135
  traceback.print_exc()
136
+ yield f"❌ Erreur: {str(e)}", None
137
 
138
+ # ---------------------------- # INTERFACE # ----------------------------
139
 
140
  custom_css = """
141
  body { background-color: #0b0e14; }
 
146
 
147
  with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
148
  with gr.Column(elem_id="title-header"):
149
+ gr.HTML("<h1 style='color:#facc15;'>🤖 ROBOTSMALI</h1><p style='color:#94a3b8;'>Sous-titrage Bambara Professionnel</p>")
 
 
 
 
150
 
151
  with gr.Row():
152
  with gr.Column():
153
+ v_in = gr.Video(label="Entrée", mirror_webcam=False)
154
+ m_sel = gr.Dropdown(list(MODELS.keys()), value="Soloba V1 (CTC)", label="Modèle")
155
+ btn = gr.Button("🚀 GÉNÉRER", variant="primary")
 
 
156
  with gr.Column():
157
+ status = gr.Markdown("*Prêt*")
158
+ v_out = gr.Video(label="Résultat Final")
 
 
 
 
 
 
 
 
 
159
 
160
+ gr.Examples(examples=VIDEO_EXAMPLES, inputs=[v_in, m_sel], cache_examples=False)
 
161
  btn.click(pipeline, [v_in, m_sel], [status, v_out])
162
 
163
  if __name__ == "__main__":