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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -41
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- ROBOTSMALI — Sous-titrage Bambara (V5.6 - Production Final)
4
- Logiciel de transcription et d'incrustation vidéo pour le Bambara.
5
  """
6
  import os
7
  import shlex
@@ -10,6 +10,7 @@ import tempfile
10
  import traceback
11
  import random
12
  import textwrap
 
13
  from pathlib import Path
14
 
15
  import numpy as np
@@ -20,12 +21,13 @@ from huggingface_hub import snapshot_download
20
  from nemo.collections import asr as nemo_asr
21
  import gradio as gr
22
 
23
- # ---------------------------- # CONFIGURATION IA # ----------------------------
24
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
25
  random.seed(1234)
26
  np.random.seed(1234)
27
  torch.manual_seed(1234)
28
 
 
29
  MODELS = {
30
  "Soloni V1 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v1", "rnnt"),
31
  "Soloni V0 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v0", "rnnt"),
@@ -35,7 +37,7 @@ MODELS = {
35
  "QuartzNet V0 (CTC-char)": ("RobotsMali/stt-bm-quartznet15x5-v0", "ctc_char"),
36
  }
37
 
38
- # Chemin vers la vidéo d'exemple
39
  VIDEO_EXAMPLES = [
40
  ["examples/MARALINKE.mp4", "Soloba V1 (CTC)"]
41
  ]
@@ -45,9 +47,10 @@ _cache = {}
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 système: {res.stdout}")
51
  return res.stdout
52
 
53
  def ffprobe_duration(path):
@@ -57,7 +60,14 @@ def ffprobe_duration(path):
57
  except: return None
58
 
59
  def load_model(name):
 
60
  if name in _cache: return _cache[name]
 
 
 
 
 
 
61
  repo, mode = MODELS[name]
62
  folder = snapshot_download(repo, local_dir_use_symlinks=False)
63
  nemo_file = next((os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(".nemo")), None)
@@ -75,30 +85,17 @@ def load_model(name):
75
  return model
76
 
77
  def extract_audio(video_path, out_wav):
78
- """Prépare la vidéo (H.264) et extrait l'audio 16kHz."""
79
  tmp_fd, stabilized_mp4 = tempfile.mkstemp(suffix="_stabilized.mp4")
80
  os.close(tmp_fd)
81
- # Réencodage pour supporter tous les formats (Webcam/WebM compris)
82
  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)}')
83
  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)}')
84
  if os.path.exists(stabilized_mp4): os.remove(stabilized_mp4)
85
 
86
- def clean_audio(wav_path):
87
- audio, sr = sf.read(wav_path)
88
- if audio.ndim == 2: audio = audio.mean(axis=1)
89
- if sr != 16000:
90
- audio = librosa.resample(audio.astype(float), orig_sr=sr, target_sr=16000)
91
- max_val = np.max(np.abs(audio)) if audio.size > 0 else 0.0
92
- if max_val > 1e-6: audio = audio / max_val * 0.9
93
- clean_path = wav_path.replace(".wav", "_clean.wav")
94
- sf.write(clean_path, audio, 16000)
95
- return clean_path, audio, 16000
96
-
97
- # ---------------------------- # TRANSCRIPTION & BURNING # ----------------------------
98
-
99
  def burn_subtitles(video_path, words, duration):
100
- """Crée un fichier SRT et l'incruste dans la vidéo."""
101
- out_path = "RobotsMali_Subtitled.mp4"
102
  chunk_size = 7
103
  with tempfile.NamedTemporaryFile(suffix=".srt", mode="w", encoding="utf-8", delete=False) as tf:
104
  for i, idx in enumerate(range(0, len(words), chunk_size)):
@@ -114,51 +111,55 @@ def burn_subtitles(video_path, words, duration):
114
  tf.write(f"{i+1}\n{t_srt(start)} --> {t_srt(end)}\n{txt}\n\n")
115
  srt_name = tf.name
116
 
 
117
  vf = f"subtitles={shlex.quote(srt_name)}:force_style='Fontsize=22,PrimaryColour=&HFFFFFF&,OutlineColour=&H000000&'"
118
- run_cmd(f'ffmpeg -hide_banner -loglevel error -y -i {shlex.quote(video_path)} -vf {shlex.quote(vf)} -c:v libx264 -crf 23 -c:a aac {shlex.quote(out_path)}')
119
  os.remove(srt_name)
120
  return out_path
121
 
 
 
122
  def pipeline(video_input, model_name):
123
  try:
124
- if not video_input: return "❌ Aucune vidéo", None
125
  video_path = video_input
126
 
127
- yield "⏳ Phase 1/3 : Analyse du fichier...", 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
- clean_wav, audio, sr = clean_audio(wav_path)
133
- duration = ffprobe_duration(video_path) or (len(audio)/sr)
134
 
135
- yield f"⏳ Phase 2/3 : Transcription IA ({model_name})...", None
136
  model = load_model(model_name)
137
- res = model.transcribe([clean_wav])[0]
138
  text = res.text if hasattr(res, 'text') else str(res)
139
  words = [w for w in text.split() if len(w) > 1]
140
 
141
- if not words: return "⚠️ Pas de parole détectée", None
142
 
143
- yield "⏳ Phase 3/3 : Incrustation des sous-titres...", None
144
- final_video = burn_subtitles(video_path, words, duration)
145
 
146
- yield "✅ Succès !", final_video
 
 
147
  except Exception as e:
148
  traceback.print_exc()
149
- yield f"❌ Erreur: {str(e)}", None
150
 
151
- # ---------------------------- # INTERFACE GRADIO # ----------------------------
152
 
153
  custom_css = """
154
  body { background-color: #0b0e14; }
155
  .gradio-container { background: rgba(17, 25, 40, 0.8) !important; backdrop-filter: blur(12px); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.1); }
156
- #title-container { text-align: center; padding: 20px; }
157
  .gr-button-primary { background: linear-gradient(135deg, #059669, #10b981) !important; border: none !important; }
158
  """
159
 
160
  with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
161
- with gr.Column(elem_id="title-container"):
162
  gr.HTML("""
163
  <h1 style='color:#facc15; font-size: 2.5rem; margin:0;'>🤖 ROBOTSMALI</h1>
164
  <p style='color:#94a3b8; font-style:italic;'>Intelligence Artificielle pour le Bambara</p>
@@ -167,21 +168,21 @@ with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
167
 
168
  with gr.Row():
169
  with gr.Column():
170
- gr.Markdown("### 📥 Source")
171
  v_in = gr.Video(label=None, mirror_webcam=False)
172
  m_sel = gr.Dropdown(list(MODELS.keys()), value="Soloba V1 (CTC)", label="Modèle IA")
173
  btn = gr.Button("🚀 GÉNÉRER LES SOUS-TITRES", variant="primary")
174
 
175
  with gr.Column():
176
  gr.Markdown("### 📤 Résultat")
177
- status = gr.Markdown("*En attente de traitement...*")
178
  v_out = gr.Video(label=None)
179
 
180
- # Section des exemples avec cache_examples=False pour débloquer le clic
181
  gr.Examples(
182
  examples=VIDEO_EXAMPLES,
183
  inputs=[v_in, m_sel],
184
- label="📺 Exemples Disponibles",
185
  cache_examples=False
186
  )
187
 
 
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
 
10
  import traceback
11
  import random
12
  import textwrap
13
+ import time
14
  from pathlib import Path
15
 
16
  import numpy as np
 
21
  from nemo.collections import asr as nemo_asr
22
  import gradio as gr
23
 
24
+ # ---------------------------- # CONFIGURATION # ----------------------------
25
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
26
  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
  "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
  # ---------------------------- # 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):
 
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)
 
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)):
 
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)
138
  words = [w for w in text.split() if len(w) > 1]
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; }
156
  .gradio-container { background: rgba(17, 25, 40, 0.8) !important; backdrop-filter: blur(12px); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.1); }
157
+ #title-header { text-align: center; padding: 20px; }
158
  .gr-button-primary { background: linear-gradient(135deg, #059669, #10b981) !important; border: none !important; }
159
  """
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>
 
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