mozed54 commited on
Commit
c5cdc29
·
verified ·
1 Parent(s): e554eb5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -71
app.py CHANGED
@@ -56,27 +56,16 @@ print("Whisper pret !")
56
  # ── Chargement Gemma ──────────────────────────────────────────────
57
  print("Chargement Gemma...")
58
  gemma_tokenizer = AutoTokenizer.from_pretrained(GEMMA_BASE, token=HF_TOKEN)
59
-
60
- # device_map="auto" place le modèle sur meta device → les poids LoRA
61
- # ne peuvent pas être copiés (no-op). Fix : charger sans device_map,
62
- # puis déplacer manuellement après merge.
63
  gemma_base = AutoModelForCausalLM.from_pretrained(
64
- GEMMA_BASE,
65
- torch_dtype=torch_dtype,
66
- token=HF_TOKEN,
67
- low_cpu_mem_usage=True, # évite la meta device contrairement à device_map
68
  )
69
  gemma_adapter = snapshot_download(
70
  repo_id=GEMMA_REPO, repo_type="dataset",
71
  token=HF_TOKEN if HF_TOKEN else None,
72
  )
73
- # is_trainable=False + pas de device_map = chargement correct des poids LoRA
74
- gemma_model = PeftModel.from_pretrained(
75
- gemma_base, gemma_adapter, is_trainable=False,
76
- )
77
  gemma_model = gemma_model.merge_and_unload()
78
- gemma_model = gemma_model.to(device) # déplacement après merge
79
- gemma_model.eval()
80
  print("Gemma pret !")
81
 
82
  # ── État global ───────────────────────────────────────────────────
@@ -108,7 +97,6 @@ def process_audio(audio):
108
 
109
  def append_and_upload(feedback_file, entry, repo_id):
110
  api = HfApi(token=HF_TOKEN)
111
- # Les repos existent déjà — on ne crée pas, ça évite l'erreur 403
112
  local = f"/tmp/{repo_id.replace('/', '')}.jsonl"
113
  try:
114
  existing = hf_hub_download(
@@ -126,35 +114,14 @@ def append_and_upload(feedback_file, entry, repo_id):
126
  )
127
 
128
 
129
- # ── Prompt système pour Gemma ─────────────────────────────────────
130
- # Prompt 100% en arabe/darija — aucun mot français/anglais pour ne pas
131
- # contaminer la langue de sortie. Exemples positifs pour ancrer le style.
132
- SYSTEM_PROMPT = """<start_of_turn>user
133
- أنت مساعد جزائري. دايما تجاوب بالدارجة الجزائرية، ما تكتبش أي كلمة بالفرانساوي أو الإنجليزي.
134
- إذا ما فهمتش الكلام، قول فقط: "ما فهمتش واش قلت، عاود من فضلك"
135
- جاوب بجملة واحدة أو جملتين على الأكثر، مباشرة بدون مقدمات.
136
-
137
- مثال 1:
138
- كلام: كيفاش نعمل كسكسي؟
139
- جواب: تسلقي الكسكسي على البخار وتحضري المرق بالخضرة واللحم، وكي يكون كل شي ناضج تخلطيهم.
140
-
141
- مثال 2:
142
- كلام: شحال الساعة؟
143
- جواب: ما عنديش ساعة، شوف في تيليفونك.
144
-
145
- مثال 3:
146
- كلام: واش تعرف تعاونني؟
147
- جواب: إيه، قولي واش تحتاج.
148
-
149
- كلام: {text}
150
- <end_of_turn>
151
  <start_of_turn>model
152
- جواب: """
153
 
154
  # ── Troncature sécurisée ──────────────────────────────────────────
155
  def truncate_gemma_output(text: str) -> str:
156
- """Coupe proprement sur stop tokens et longueur max."""
157
- # Stop tokens — tout ce qui suit est hors contexte
158
  stop_tokens = [
159
  "<end_of_turn>", "<start_of_turn>",
160
  "\nسؤال:", "\nسوال:", "\nالسؤال:",
@@ -165,17 +132,12 @@ def truncate_gemma_output(text: str) -> str:
165
  if token in text:
166
  text = text.split(token)[0].strip()
167
 
168
- # Supprimer les lignes vides en tête + "جواب: " ajouté par le prompt
169
  text = text.strip()
170
- if text.startswith("جواب:"):
171
- text = text[len("جواب:"):].strip()
172
 
173
- # Garder seulement les 2 premières phrases (. أو ؟ أو !)
174
  sentences = re.split(r'(?<=[.!?؟])\s+', text)
175
  if len(sentences) > 2:
176
  text = " ".join(sentences[:2]).strip()
177
 
178
- # Limite absolue : 300 caractères
179
  if len(text) > 300:
180
  text = text[:300].rsplit(" ", 1)[0].strip() + "..."
181
 
@@ -184,11 +146,10 @@ def truncate_gemma_output(text: str) -> str:
184
 
185
  # ── Génération Gemma avec streaming ───────────────────────────────
186
  def generate_gemma_response(transcription: str, progress=None):
187
- """Génère la réponse Gemma avec streaming."""
188
  if progress:
189
  progress(0.6, desc="🤖 Préparation du prompt...")
190
 
191
- prompt = SYSTEM_PROMPT.format(text=transcription)
192
  inputs = gemma_tokenizer(prompt, return_tensors="pt").to(device)
193
 
194
  streamer = TextIteratorStreamer(
@@ -198,14 +159,13 @@ def generate_gemma_response(transcription: str, progress=None):
198
  decode_kwargs={"skip_special_tokens": True},
199
  )
200
 
201
- # temperature basse = colle à la darija, évite le drift français/anglais
202
  generation_kwargs = dict(
203
  **inputs,
204
- max_new_tokens=100, # Court : 1-2 phrases suffisent
205
  do_sample=True,
206
- temperature=0.3, # Très bas = déterministe, reste en darija
207
  top_p=0.80,
208
- top_k=30, # Petit vocab = moins de drift linguistique
209
  repetition_penalty=1.4,
210
  no_repeat_ngram_size=4,
211
  pad_token_id=gemma_tokenizer.eos_token_id,
@@ -232,12 +192,7 @@ def generate_gemma_response(transcription: str, progress=None):
232
 
233
 
234
  # ── ÉTAPE 1 : Transcription Whisper uniquement ────────────────────
235
- # CORRECTION : whisper et gemma sont maintenant séparés en 2 fonctions.
236
- # transcribe_only() → retourne la transcription + prépare l'état
237
- # respond_gemma() → appelée via .then() après que la transcription est affichée
238
-
239
  def transcribe_only(audio, progress=gr.Progress()):
240
- """Étape 1 — Whisper uniquement. Gemma ne tourne PAS encore."""
241
  path = process_audio(audio)
242
  if path is None:
243
  return (
@@ -252,7 +207,6 @@ def transcribe_only(audio, progress=gr.Progress()):
252
  result = whisper_pipe(path, return_timestamps=False)
253
  text = result["text"].strip()
254
 
255
- # Sauvegarde état
256
  current_audio["path"] = path
257
  current_audio["filename"] = generate_audio_name()
258
  current_exchange["transcription"] = text
@@ -260,7 +214,6 @@ def transcribe_only(audio, progress=gr.Progress()):
260
 
261
  progress(1.0, desc="✅ Transcription terminée")
262
 
263
- # Retourne : transcription affichée, feedbacks cachés, bouton transcrire désactivé
264
  return (
265
  text,
266
  gr.update(visible=False),
@@ -277,9 +230,8 @@ def transcribe_only(audio, progress=gr.Progress()):
277
  )
278
 
279
 
280
- # ── ÉTAPE 2 : Réponse Gemma (déclenchée après transcription) ──────
281
  def respond_gemma(progress=gr.Progress()):
282
- """Étape 2 — Gemma génère sa réponse en streaming."""
283
  transcription = current_exchange.get("transcription")
284
  if not transcription:
285
  yield (
@@ -296,7 +248,6 @@ def respond_gemma(progress=gr.Progress()):
296
  last_partial = ""
297
  for partial in generate_gemma_response(transcription, progress=progress):
298
  last_partial = partial
299
- # Pendant le streaming : feedbacks pas encore visibles
300
  yield (
301
  partial,
302
  gr.update(visible=False),
@@ -304,11 +255,9 @@ def respond_gemma(progress=gr.Progress()):
304
  gr.update(interactive=False),
305
  )
306
 
307
- # Sauvegarde réponse finale
308
  current_exchange["reponse"] = last_partial
309
  progress(1.0, desc="✅ Terminé")
310
 
311
- # Fin : afficher les feedbacks, réactiver le bouton
312
  yield (
313
  last_partial,
314
  gr.update(visible=True),
@@ -327,7 +276,6 @@ def respond_gemma(progress=gr.Progress()):
327
 
328
  # ── Régénération Gemma seule ──────────────────────────────────────
329
  def regenerate_gemma_only(progress=gr.Progress()):
330
- """Régénère uniquement la réponse Gemma sans re-transcrire."""
331
  transcription = current_exchange.get("transcription")
332
  if not transcription:
333
  yield "", gr.update(visible=False)
@@ -425,11 +373,39 @@ button.danger { background: #dc2626 !important; color: #fff !important; border:
425
  button.danger:hover { background: #b91c1c !important; }
426
  button.regenerate { background: #7c3aed !important; color: #fff !important; border: none !important; border-radius: 10px !important; font-weight: 500 !important; padding: 0.5rem 1rem !important; }
427
  button.regenerate:hover { background: #6d28d9 !important; }
428
- #transcription-box textarea { background: #0f0f13 !important; border: 1.5px solid #1e1e2a !important; border-radius: 14px !important; color: #e0e0ec !important; font-size: 1rem !important; line-height: 1.7 !important; padding: 1.2rem !important; direction: rtl !important; text-align: right !important; transition: border-color 0.4s, box-shadow 0.4s !important; min-height: 120px !important; }
429
  #transcription-box.has-text textarea { border-color: #22c97a !important; box-shadow: 0 0 0 3px rgba(34,201,122,0.12) !important; }
430
- #gemma-box textarea { background: #0f0f13 !important; border: 1.5px solid #1e1e2a !important; border-radius: 14px !important; color: #e0e0ec !important; font-size: 1rem !important; line-height: 1.7 !important; padding: 1.2rem !important; direction: rtl !important; text-align: right !important; transition: border-color 0.4s, box-shadow 0.4s !important; min-height: 120px !important; }
431
  #gemma-box.has-text textarea { border-color: #a855f7 !important; box-shadow: 0 0 0 3px rgba(168,85,247,0.12) !important; }
432
  .feedback-panel { background: #16161f; border: 1px solid #1e1e2a; border-radius: 14px; padding: 1.2rem 1.5rem; margin-top: 0.8rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  footer { display: none !important; }
434
  """
435
 
@@ -515,10 +491,6 @@ with gr.Blocks(title="Whisper + Gemma · Darija", css=css, js=js) as demo:
515
  reset_btn = gr.Button("🔄 Réinitialiser tout", variant="danger")
516
 
517
  # ── Callbacks ─────────────────────────────────────────────────
518
- # CORRECTION : chaîne en 2 étapes avec .then()
519
- # Étape 1 → Whisper : affiche la transcription, Gemma reste vide
520
- # Étape 2 → Gemma : se lance APRÈS que Whisper a fini, en streaming
521
-
522
  mic_btn.click(
523
  fn=transcribe_only,
524
  inputs=[mic_input],
@@ -539,7 +511,6 @@ with gr.Blocks(title="Whisper + Gemma · Darija", css=css, js=js) as demo:
539
  outputs=[gemma_out, fb_whisper, fb_gemma, file_btn],
540
  )
541
 
542
- # Régénération Gemma seule
543
  regenerate_btn.click(
544
  fn=regenerate_gemma_only,
545
  inputs=[],
 
56
  # ── Chargement Gemma ──────────────────────────────────────────────
57
  print("Chargement Gemma...")
58
  gemma_tokenizer = AutoTokenizer.from_pretrained(GEMMA_BASE, token=HF_TOKEN)
 
 
 
 
59
  gemma_base = AutoModelForCausalLM.from_pretrained(
60
+ GEMMA_BASE, torch_dtype=torch_dtype,
61
+ device_map="auto", token=HF_TOKEN,
 
 
62
  )
63
  gemma_adapter = snapshot_download(
64
  repo_id=GEMMA_REPO, repo_type="dataset",
65
  token=HF_TOKEN if HF_TOKEN else None,
66
  )
67
+ gemma_model = PeftModel.from_pretrained(gemma_base, gemma_adapter)
 
 
 
68
  gemma_model = gemma_model.merge_and_unload()
 
 
69
  print("Gemma pret !")
70
 
71
  # ── État global ───────────────────────────────────────────────────
 
97
 
98
  def append_and_upload(feedback_file, entry, repo_id):
99
  api = HfApi(token=HF_TOKEN)
 
100
  local = f"/tmp/{repo_id.replace('/', '')}.jsonl"
101
  try:
102
  existing = hf_hub_download(
 
114
  )
115
 
116
 
117
+ # ── Prompt Gemma ──────────────────────────────────────────────────
118
+ PROMPT_TEMPLATE = """<start_of_turn>user
119
+ {text}<end_of_turn>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  <start_of_turn>model
121
+ """
122
 
123
  # ── Troncature sécurisée ──────────────────────────────────────────
124
  def truncate_gemma_output(text: str) -> str:
 
 
125
  stop_tokens = [
126
  "<end_of_turn>", "<start_of_turn>",
127
  "\nسؤال:", "\nسوال:", "\nالسؤال:",
 
132
  if token in text:
133
  text = text.split(token)[0].strip()
134
 
 
135
  text = text.strip()
 
 
136
 
 
137
  sentences = re.split(r'(?<=[.!?؟])\s+', text)
138
  if len(sentences) > 2:
139
  text = " ".join(sentences[:2]).strip()
140
 
 
141
  if len(text) > 300:
142
  text = text[:300].rsplit(" ", 1)[0].strip() + "..."
143
 
 
146
 
147
  # ── Génération Gemma avec streaming ───────────────────────────────
148
  def generate_gemma_response(transcription: str, progress=None):
 
149
  if progress:
150
  progress(0.6, desc="🤖 Préparation du prompt...")
151
 
152
+ prompt = PROMPT_TEMPLATE.format(text=transcription)
153
  inputs = gemma_tokenizer(prompt, return_tensors="pt").to(device)
154
 
155
  streamer = TextIteratorStreamer(
 
159
  decode_kwargs={"skip_special_tokens": True},
160
  )
161
 
 
162
  generation_kwargs = dict(
163
  **inputs,
164
+ max_new_tokens=100,
165
  do_sample=True,
166
+ temperature=0.3,
167
  top_p=0.80,
168
+ top_k=30,
169
  repetition_penalty=1.4,
170
  no_repeat_ngram_size=4,
171
  pad_token_id=gemma_tokenizer.eos_token_id,
 
192
 
193
 
194
  # ── ÉTAPE 1 : Transcription Whisper uniquement ────────────────────
 
 
 
 
195
  def transcribe_only(audio, progress=gr.Progress()):
 
196
  path = process_audio(audio)
197
  if path is None:
198
  return (
 
207
  result = whisper_pipe(path, return_timestamps=False)
208
  text = result["text"].strip()
209
 
 
210
  current_audio["path"] = path
211
  current_audio["filename"] = generate_audio_name()
212
  current_exchange["transcription"] = text
 
214
 
215
  progress(1.0, desc="✅ Transcription terminée")
216
 
 
217
  return (
218
  text,
219
  gr.update(visible=False),
 
230
  )
231
 
232
 
233
+ # ── ÉTAPE 2 : Réponse Gemma ───────────────────────────────────────
234
  def respond_gemma(progress=gr.Progress()):
 
235
  transcription = current_exchange.get("transcription")
236
  if not transcription:
237
  yield (
 
248
  last_partial = ""
249
  for partial in generate_gemma_response(transcription, progress=progress):
250
  last_partial = partial
 
251
  yield (
252
  partial,
253
  gr.update(visible=False),
 
255
  gr.update(interactive=False),
256
  )
257
 
 
258
  current_exchange["reponse"] = last_partial
259
  progress(1.0, desc="✅ Terminé")
260
 
 
261
  yield (
262
  last_partial,
263
  gr.update(visible=True),
 
276
 
277
  # ── Régénération Gemma seule ──────────────────────────────────────
278
  def regenerate_gemma_only(progress=gr.Progress()):
 
279
  transcription = current_exchange.get("transcription")
280
  if not transcription:
281
  yield "", gr.update(visible=False)
 
373
  button.danger:hover { background: #b91c1c !important; }
374
  button.regenerate { background: #7c3aed !important; color: #fff !important; border: none !important; border-radius: 10px !important; font-weight: 500 !important; padding: 0.5rem 1rem !important; }
375
  button.regenerate:hover { background: #6d28d9 !important; }
376
+ #transcription-box textarea { background: #0f0f13 !important; border: 1.5px solid #1e1e2a !important; border-radius: 14px !important; color: #e0e0ec !important; font-size: 1rem !important; line-height: 1.7 !important; padding: 1.2rem !important; direction: rtl !important; text-align: right !important; min-height: 120px !important; }
377
  #transcription-box.has-text textarea { border-color: #22c97a !important; box-shadow: 0 0 0 3px rgba(34,201,122,0.12) !important; }
378
+ #gemma-box textarea { background: #0f0f13 !important; border: 1.5px solid #1e1e2a !important; border-radius: 14px !important; color: #e0e0ec !important; font-size: 1rem !important; line-height: 1.7 !important; padding: 1.2rem !important; direction: rtl !important; text-align: right !important; min-height: 120px !important; }
379
  #gemma-box.has-text textarea { border-color: #a855f7 !important; box-shadow: 0 0 0 3px rgba(168,85,247,0.12) !important; }
380
  .feedback-panel { background: #16161f; border: 1px solid #1e1e2a; border-radius: 14px; padding: 1.2rem 1.5rem; margin-top: 0.8rem; }
381
+
382
+ /* ── Fix clignotement mobile sur audio long ─────────────────────── */
383
+ /* Hauteur fixe + suppression des transitions sur le waveform */
384
+ .gr-audio, [data-testid="audio"], .audio-container {
385
+ height: 80px !important;
386
+ min-height: 80px !important;
387
+ max-height: 80px !important;
388
+ overflow: hidden !important;
389
+ contain: layout style !important;
390
+ }
391
+ .gr-audio *, [data-testid="audio"] *, .audio-container * {
392
+ transition: none !important;
393
+ animation: none !important;
394
+ }
395
+ /* Empêche le canvas waveform de forcer un reflow de page */
396
+ .gr-audio canvas, [data-testid="audio"] canvas {
397
+ display: block !important;
398
+ width: 100% !important;
399
+ height: 48px !important;
400
+ max-height: 48px !important;
401
+ }
402
+ /* Stabilise les colonnes audio pendant l'enregistrement */
403
+ .gr-audio .wrap, .audio-recorder {
404
+ position: relative !important;
405
+ height: 80px !important;
406
+ overflow: hidden !important;
407
+ }
408
+
409
  footer { display: none !important; }
410
  """
411
 
 
491
  reset_btn = gr.Button("🔄 Réinitialiser tout", variant="danger")
492
 
493
  # ── Callbacks ─────────────────────────────────────────────────
 
 
 
 
494
  mic_btn.click(
495
  fn=transcribe_only,
496
  inputs=[mic_input],
 
511
  outputs=[gemma_out, fb_whisper, fb_gemma, file_btn],
512
  )
513
 
 
514
  regenerate_btn.click(
515
  fn=regenerate_gemma_only,
516
  inputs=[],