Update app.py
Browse files
app.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
-
ROBOTSMALI — Sous-titrage Bambara (V5.
|
| 4 |
-
-
|
| 5 |
-
-
|
| 6 |
-
-
|
| 7 |
"""
|
| 8 |
import os
|
| 9 |
import shlex
|
|
@@ -28,8 +28,6 @@ random.seed(1234)
|
|
| 28 |
np.random.seed(1234)
|
| 29 |
torch.manual_seed(1234)
|
| 30 |
|
| 31 |
-
SEGMENT_DURATION = 10.0
|
| 32 |
-
|
| 33 |
MODELS = {
|
| 34 |
"Soloni V1 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v1", "rnnt"),
|
| 35 |
"Soloni V0 (RNNT)": ("RobotsMali/soloni-114m-tdt-ctc-v0", "rnnt"),
|
|
@@ -39,8 +37,9 @@ MODELS = {
|
|
| 39 |
"QuartzNet V0 (CTC-char)": ("RobotsMali/stt-bm-quartznet15x5-v0", "ctc_char"),
|
| 40 |
}
|
| 41 |
|
|
|
|
| 42 |
VIDEO_EXAMPLES = [
|
| 43 |
-
["examples/MARALINKE
|
| 44 |
]
|
| 45 |
|
| 46 |
_cache = {}
|
|
@@ -78,7 +77,7 @@ def load_model(name):
|
|
| 78 |
def extract_audio(video_path, out_wav):
|
| 79 |
tmp_fd, stabilized_mp4 = tempfile.mkstemp(suffix="_stabilized.mp4")
|
| 80 |
os.close(tmp_fd)
|
| 81 |
-
# Réencodage H.264 pour supporter le
|
| 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)
|
|
@@ -93,21 +92,14 @@ def clean_audio(wav_path):
|
|
| 93 |
sf.write(clean_path, audio, 16000)
|
| 94 |
return clean_path, audio, 16000
|
| 95 |
|
| 96 |
-
# ---------------------------- #
|
| 97 |
-
|
| 98 |
-
def transcribe(model, wav_path):
|
| 99 |
-
out = model.transcribe([wav_path])
|
| 100 |
-
if isinstance(out, list) and len(out) > 0:
|
| 101 |
-
res = out[0]
|
| 102 |
-
return res.text.strip() if hasattr(res, "text") else str(res).strip()
|
| 103 |
-
return str(out).strip()
|
| 104 |
|
| 105 |
def pipeline(video_input, model_name):
|
| 106 |
try:
|
| 107 |
-
|
| 108 |
-
|
| 109 |
|
| 110 |
-
yield "⏳ Phase 1 :
|
| 111 |
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
|
| 112 |
wav_path = tf.name
|
| 113 |
|
|
@@ -115,25 +107,25 @@ def pipeline(video_input, model_name):
|
|
| 115 |
clean_wav, audio, sr = clean_audio(wav_path)
|
| 116 |
duration = ffprobe_duration(video_path) or (len(audio)/sr)
|
| 117 |
|
| 118 |
-
yield f"⏳ Phase 2 :
|
| 119 |
model = load_model(model_name)
|
| 120 |
-
text = transcribe(
|
| 121 |
-
|
|
|
|
| 122 |
|
| 123 |
-
if not words: return "⚠️ Pas de
|
| 124 |
|
| 125 |
-
yield "⏳ Phase 3 :
|
| 126 |
-
# Heuristique simple pour les sous-titres
|
| 127 |
subs = []
|
| 128 |
-
chunk_size =
|
| 129 |
for i in range(0, len(words), chunk_size):
|
| 130 |
chunk = words[i:i+chunk_size]
|
| 131 |
s = (i / len(words)) * duration
|
| 132 |
e = (min(i + chunk_size, len(words)) / len(words)) * duration
|
| 133 |
subs.append((s, e, "\n".join(textwrap.wrap(" ".join(chunk), 40))))
|
| 134 |
|
| 135 |
-
|
| 136 |
-
yield "✅
|
| 137 |
except Exception as e:
|
| 138 |
traceback.print_exc()
|
| 139 |
yield f"❌ Erreur: {str(e)}", None
|
|
@@ -152,40 +144,49 @@ def burn(video_path, subs):
|
|
| 152 |
os.remove(srt_name)
|
| 153 |
return out_path
|
| 154 |
|
| 155 |
-
# ---------------------------- # INTERFACE
|
| 156 |
|
| 157 |
custom_css = """
|
| 158 |
body { background-color: #0b0e14; }
|
| 159 |
.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); }
|
| 160 |
-
#title-
|
| 161 |
.gr-button-primary { background: linear-gradient(135deg, #059669, #10b981) !important; border: none !important; }
|
| 162 |
"""
|
| 163 |
|
| 164 |
with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
|
| 165 |
-
#
|
| 166 |
-
with gr.Column(elem_id="title-
|
| 167 |
gr.HTML("""
|
| 168 |
<h1 style='color:#facc15; font-size: 2.5rem; margin:0;'>🤖 ROBOTSMALI</h1>
|
| 169 |
-
<p style='color:#94a3b8;
|
| 170 |
<div style="height: 3px; width: 60px; background: #facc15; margin: 15px auto;"></div>
|
| 171 |
""")
|
| 172 |
|
| 173 |
with gr.Row():
|
| 174 |
with gr.Column():
|
| 175 |
-
gr.Markdown("### 📥
|
| 176 |
v_in = gr.Video(label=None, mirror_webcam=False)
|
| 177 |
-
m_sel = gr.Dropdown(list(MODELS.keys()), value="Soloba V1 (CTC)", label="Modèle
|
| 178 |
btn = gr.Button("🚀 GÉNÉRER", variant="primary")
|
| 179 |
|
| 180 |
with gr.Column():
|
| 181 |
-
gr.Markdown("### 📤
|
| 182 |
-
status = gr.Markdown("*
|
| 183 |
v_out = gr.Video(label=None)
|
| 184 |
|
| 185 |
-
|
| 186 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
btn.click(pipeline, [v_in, m_sel], [status, v_out])
|
| 189 |
|
| 190 |
if __name__ == "__main__":
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# -*- coding: utf-8 -*-
|
| 2 |
"""
|
| 3 |
+
ROBOTSMALI — Sous-titrage Bambara (V5.3 - Production)
|
| 4 |
+
- Vidéo d'exemple : examples/MARALINKE.mp4
|
| 5 |
+
- Correction AttributeError: Gradio Div -> Column/HTML
|
| 6 |
+
- Correction Codec Webcam : VP8 -> H.264
|
| 7 |
"""
|
| 8 |
import os
|
| 9 |
import shlex
|
|
|
|
| 28 |
np.random.seed(1234)
|
| 29 |
torch.manual_seed(1234)
|
| 30 |
|
|
|
|
|
|
|
| 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 |
+
# Mise à jour avec le nom simplifié
|
| 41 |
VIDEO_EXAMPLES = [
|
| 42 |
+
["examples/MARALINKE.mp4", "Soloba V1 (CTC)"]
|
| 43 |
]
|
| 44 |
|
| 45 |
_cache = {}
|
|
|
|
| 77 |
def extract_audio(video_path, out_wav):
|
| 78 |
tmp_fd, stabilized_mp4 = tempfile.mkstemp(suffix="_stabilized.mp4")
|
| 79 |
os.close(tmp_fd)
|
| 80 |
+
# Réencodage H.264 pour supporter le flux webcam
|
| 81 |
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)}')
|
| 82 |
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)}')
|
| 83 |
if os.path.exists(stabilized_mp4): os.remove(stabilized_mp4)
|
|
|
|
| 92 |
sf.write(clean_path, audio, 16000)
|
| 93 |
return clean_path, audio, 16000
|
| 94 |
|
| 95 |
+
# ---------------------------- # PIPELINE # ----------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
def pipeline(video_input, model_name):
|
| 98 |
try:
|
| 99 |
+
if not video_input: return "❌ Vidéo introuvable", None
|
| 100 |
+
video_path = video_input
|
| 101 |
|
| 102 |
+
yield "⏳ Phase 1 : Extraction audio...", None
|
| 103 |
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
|
| 104 |
wav_path = tf.name
|
| 105 |
|
|
|
|
| 107 |
clean_wav, audio, sr = clean_audio(wav_path)
|
| 108 |
duration = ffprobe_duration(video_path) or (len(audio)/sr)
|
| 109 |
|
| 110 |
+
yield f"⏳ Phase 2 : Transcription IA ({model_name})...", None
|
| 111 |
model = load_model(model_name)
|
| 112 |
+
text = model.transcribe([clean_wav])[0]
|
| 113 |
+
text_str = text.text if hasattr(text, 'text') else str(text)
|
| 114 |
+
words = [w for w in text_str.split() if len(w) > 1]
|
| 115 |
|
| 116 |
+
if not words: return "⚠️ Pas de parole détectée", None
|
| 117 |
|
| 118 |
+
yield "⏳ Phase 3 : Incrustation des sous-titres...", None
|
|
|
|
| 119 |
subs = []
|
| 120 |
+
chunk_size = 7
|
| 121 |
for i in range(0, len(words), chunk_size):
|
| 122 |
chunk = words[i:i+chunk_size]
|
| 123 |
s = (i / len(words)) * duration
|
| 124 |
e = (min(i + chunk_size, len(words)) / len(words)) * duration
|
| 125 |
subs.append((s, e, "\n".join(textwrap.wrap(" ".join(chunk), 40))))
|
| 126 |
|
| 127 |
+
res_v = burn(video_path, subs)
|
| 128 |
+
yield "✅ Succès !", res_v
|
| 129 |
except Exception as e:
|
| 130 |
traceback.print_exc()
|
| 131 |
yield f"❌ Erreur: {str(e)}", None
|
|
|
|
| 144 |
os.remove(srt_name)
|
| 145 |
return out_path
|
| 146 |
|
| 147 |
+
# ---------------------------- # INTERFACE ARTISTIQUE # ----------------------------
|
| 148 |
|
| 149 |
custom_css = """
|
| 150 |
body { background-color: #0b0e14; }
|
| 151 |
.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); }
|
| 152 |
+
#title-container { text-align: center; padding: 20px; }
|
| 153 |
.gr-button-primary { background: linear-gradient(135deg, #059669, #10b981) !important; border: none !important; }
|
| 154 |
"""
|
| 155 |
|
| 156 |
with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as demo:
|
| 157 |
+
# Remplacement de gr.Div par gr.Column (Fix AttributeError)
|
| 158 |
+
with gr.Column(elem_id="title-container"):
|
| 159 |
gr.HTML("""
|
| 160 |
<h1 style='color:#facc15; font-size: 2.5rem; margin:0;'>🤖 ROBOTSMALI</h1>
|
| 161 |
+
<p style='color:#94a3b8;'>Intelligence Artificielle pour le Bambara</p>
|
| 162 |
<div style="height: 3px; width: 60px; background: #facc15; margin: 15px auto;"></div>
|
| 163 |
""")
|
| 164 |
|
| 165 |
with gr.Row():
|
| 166 |
with gr.Column():
|
| 167 |
+
gr.Markdown("### 📥 Entrée")
|
| 168 |
v_in = gr.Video(label=None, mirror_webcam=False)
|
| 169 |
+
m_sel = gr.Dropdown(list(MODELS.keys()), value="Soloba V1 (CTC)", label="Modèle")
|
| 170 |
btn = gr.Button("🚀 GÉNÉRER", variant="primary")
|
| 171 |
|
| 172 |
with gr.Column():
|
| 173 |
+
gr.Markdown("### 📤 Résultat")
|
| 174 |
+
status = gr.Markdown("*Prêt...*")
|
| 175 |
v_out = gr.Video(label=None)
|
| 176 |
|
| 177 |
+
# Section des exemples (Utilise maintenant MARALINKE.mp4)
|
| 178 |
+
gr.Examples(
|
| 179 |
+
examples=VIDEO_EXAMPLES,
|
| 180 |
+
inputs=[v_in, m_sel],
|
| 181 |
+
label="📺 Exemples de Clips"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
gr.HTML("<div style='text-align: center; color: #475569; margin-top: 30px;'>© 2025 RobotsMali - Bamako</div>")
|
| 185 |
|
| 186 |
btn.click(pipeline, [v_in, m_sel], [status, v_out])
|
| 187 |
|
| 188 |
if __name__ == "__main__":
|
| 189 |
+
# Petit check de debug pour le dossier examples
|
| 190 |
+
if not os.path.exists("examples/MARALINKE.MP4"):
|
| 191 |
+
print("⚠️ ATTENTION : examples/MARALINKE.mp4 est introuvable sur le serveur.")
|
| 192 |
+
demo.launch(share=True, debug=True)
|