jefffffff9 Claude Sonnet 4.6 commited on
Commit
98eece8
·
1 Parent(s): 9f07d46

Align Space app with Kaggle notebook training pipeline

Browse files

1. Replace PEFT AdapterManager with full-checkpoint loader
- _fine_tuned_models dict replaces _adapter_manager/PEFT
- _reload_adapters_from_hub loads WhisperForConditionalGeneration
from config.json checkpoints (what the notebook actually saves)
- _run_pipeline uses _fine_tuned_models.get(lang, base_model)

2. Tab 3 audio uploads now write to corrections.jsonl + audio/
- Previously saved to training_audio/ which the notebook never read
- Now saved under audio/{lang}_{ts}.wav with corrected_text field
exactly matching what Cell 7 of the notebook expects

3. Phrase pairs now also append to vocabulary.jsonl
- _append_phrases_to_vocabulary_jsonl writes {word, translation, language}
entries so Cell 7 vocab_entries picks them up as synthetic fallback labels

4. Tab 4 updated: correct notebook name, clear data flow docs,
button renamed to "Reload Fine-tuned Models from Hub"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +142 -101
app.py CHANGED
@@ -54,12 +54,11 @@ except ImportError:
54
  return fn
55
 
56
  # ── Module-level model state (CPU-resident between requests) ─────────────────
57
- _whisper_model = None # WhisperForConditionalGeneration (base)
58
- _whisper_processor = None
59
- _adapter_manager = None # AdapterManager (wraps base model with PEFT if adapters loaded)
60
- _model_lock = threading.Lock()
61
- _model_status = "not loaded"
62
- _adapters_loaded = set() # set of language codes with loaded adapters, e.g. {"bam", "ful"}
63
 
64
  from src.tts.mms_tts import MMSTTSEngine
65
  from src.iot.intent_parser import IntentParser
@@ -82,9 +81,8 @@ if HF_TOKEN:
82
  # ── Model loading ─────────────────────────────────────────────────────────────
83
 
84
  def _do_load_whisper():
85
- global _whisper_model, _whisper_processor, _adapter_manager, _model_status
86
  import torch
87
- from src.engine.adapter_manager import AdapterManager
88
 
89
  # Import concrete Whisper classes directly — bypasses transformers __init__.py
90
  # Auto-class exports differ between transformers 4.x and 5.x; direct paths are stable.
@@ -111,48 +109,11 @@ def _do_load_whisper():
111
  token=HF_TOKEN,
112
  )
113
  _whisper_model.eval()
114
-
115
- # Create the AdapterManager wrapping the base model
116
- _adapter_manager = AdapterManager(base_model=_whisper_model, config={})
117
-
118
- # Try to load adapters from the local adapter repo snapshot (if already downloaded)
119
- _try_load_local_adapters()
120
-
121
  _model_status = f"ready ({WHISPER_MODEL_ID})"
122
  except Exception as e:
123
  _model_status = f"error: {e}"
124
 
125
 
126
- def _try_load_local_adapters() -> None:
127
- """Load any adapter snapshots that are already on disk (downloaded previously)."""
128
- global _adapters_loaded
129
- if _adapter_manager is None:
130
- return
131
- if not ADAPTER_REPO_ID:
132
- return
133
- try:
134
- from huggingface_hub import try_to_load_from_cache
135
- lang_dirs = {"bam": "adapters/bambara", "ful": "adapters/fula"}
136
- for lang, subdir in lang_dirs.items():
137
- cached = try_to_load_from_cache(
138
- repo_id=ADAPTER_REPO_ID,
139
- filename=f"{subdir}/adapter_config.json",
140
- repo_type="model",
141
- token=HF_TOKEN,
142
- )
143
- if cached:
144
- import os
145
- adapter_path = str(os.path.dirname(cached))
146
- _adapter_manager.register(lang, adapter_path)
147
- try:
148
- _adapter_manager.load_adapter(lang)
149
- _adapters_loaded.add(lang)
150
- except Exception:
151
- pass
152
- except Exception:
153
- pass # Adapters not cached yet — will load after first Hub download
154
-
155
-
156
  def _ensure_whisper_loaded():
157
  """Load Whisper to CPU in a background thread on first call. Non-blocking."""
158
  global _model_status
@@ -198,13 +159,9 @@ def _run_pipeline(audio_path: str, language_code: str):
198
 
199
  audio_np, _ = librosa.load(audio_path, sr=16000, mono=True)
200
 
201
- # Use adapter-wrapped model if an adapter for this language is loaded;
202
  # otherwise fall back to base Whisper.
203
- if _adapter_manager is not None and language_code in _adapters_loaded:
204
- _adapter_manager.activate(language_code)
205
- active_model = _adapter_manager.get_model()
206
- else:
207
- active_model = _whisper_model
208
 
209
  active_model.to(device)
210
  with _model_lock:
@@ -376,49 +333,56 @@ def _save_feedback_to_hub(
376
  # ── Adapter reload ────────────────────────────────────────────────────────────
377
 
378
  def _reload_adapters_from_hub() -> str:
379
- global _adapters_loaded
 
380
  if _hf_api is None:
381
- return "⚠️ HF_TOKEN not set — cannot download adapters."
382
- if _adapter_manager is None:
383
  return "⏳ Base model not loaded yet — wait for model to finish loading and try again."
384
  try:
 
385
  from huggingface_hub import snapshot_download
 
 
 
 
 
386
  local_dir = snapshot_download(
387
  repo_id=ADAPTER_REPO_ID, repo_type="model", token=HF_TOKEN
388
  )
389
  results = []
390
  for lang, subdir in (("bam", "adapters/bambara"), ("ful", "adapters/fula")):
391
- adapter_path = Path(local_dir) / subdir
392
- if not adapter_path.exists():
393
- results.append(f"⚠️ {lang}: `{subdir}` not found in repo")
394
  continue
395
- # Check that this looks like a valid PEFT adapter
396
- if not (adapter_path / "adapter_config.json").exists():
397
- results.append(f"⚠️ {lang}: `{subdir}` missing adapter_config.json — run training first")
398
  continue
399
  try:
400
- _adapter_manager.register(lang, str(adapter_path))
401
- _adapter_manager.load_adapter(lang)
402
- _adapters_loaded.add(lang)
403
- results.append(f"✅ {lang}: adapter loaded from `{subdir}`")
 
 
404
  except Exception as e:
405
  results.append(f"❌ {lang}: load failed — {e}")
406
 
407
  summary = "\n".join(results)
408
- active = ", ".join(_adapters_loaded) if _adapters_loaded else "none"
409
- return f"{summary}\n\n**Active adapters:** {active}\n**Repo:** `{ADAPTER_REPO_ID}`"
410
  except Exception as e:
411
- return f"❌ Adapter reload failed: {e}"
412
 
413
 
414
  def _get_adapter_status() -> str:
415
  lines = []
416
 
417
- # Show which adapters are currently active in memory
418
- if _adapters_loaded:
419
- lines.append(f"**Active adapters (in memory):** {', '.join(sorted(_adapters_loaded))}")
420
  else:
421
- lines.append("**Active adapters:** none — using base Whisper")
422
 
423
  if _hf_api is None:
424
  lines.append("_HF_TOKEN not set — Hub check skipped._")
@@ -427,15 +391,15 @@ def _get_adapter_status() -> str:
427
  try:
428
  from huggingface_hub import list_repo_files
429
  files = list(list_repo_files(ADAPTER_REPO_ID, repo_type="model", token=HF_TOKEN))
430
- bam_ok = any("bambara" in f and "adapter_config" in f for f in files)
431
- ful_ok = any("fula" in f and "adapter_config" in f for f in files)
432
  lines += [
433
  f"\n**Hub repo:** `{ADAPTER_REPO_ID}`",
434
- f"- Bambara (bam): {'✅ trained adapter present' if bam_ok else '⚠️ not yet trained — run bootstrap notebook'}",
435
- f"- Fula (ful): {'✅ trained adapter present' if ful_ok else '⚠️ not yet trained — run bootstrap notebook'}",
436
  ]
437
  if bam_ok or ful_ok:
438
- lines.append("\n_Click **Reload Adapters** to activate them._")
439
  except Exception as e:
440
  lines.append(f"_Could not read Hub repo: {e}_")
441
 
@@ -445,7 +409,7 @@ def _get_adapter_status() -> str:
445
  # ── Knowledge Base handlers ───────────────────────────────────────────────────
446
 
447
  def _import_phrase_pairs(lang_label: str, pairs_text: str) -> str:
448
- """Import pasted phrase pairs into the phrase library."""
449
  if not pairs_text.strip():
450
  return "⚠️ Nothing entered. Use the format: native phrase | english translation"
451
  lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
@@ -453,10 +417,56 @@ def _import_phrase_pairs(lang_label: str, pairs_text: str) -> str:
453
  if count == 0:
454
  return "⚠️ No valid phrases found. Each line must contain a | separator.\nExample: I ni ce | Hello, good day"
455
  _upload_phrase_additions_to_hub(lang)
 
 
456
  total = _phrase_matcher.phrase_count(lang)
457
  return f"✅ Added {count} phrase(s) for {lang_label}. Library now has {total} phrases. Available immediately."
458
 
459
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  def _upload_phrase_additions_to_hub(lang: str) -> None:
461
  """Persist user phrase additions to HF Hub so they survive Space restarts."""
462
  if _hf_api is None or not FEEDBACK_REPO_ID:
@@ -500,7 +510,7 @@ threading.Thread(target=_load_phrase_additions_from_hub, daemon=True).start()
500
 
501
 
502
  def _save_audio_for_training(lang_label: str, audio_path: str | None, transcript: str, source_note: str) -> str:
503
- """Save an uploaded audio file + transcription as a training sample to HF Hub."""
504
  transcript = transcript.strip()
505
  if audio_path is None:
506
  return "⚠️ Please upload an audio file first."
@@ -508,38 +518,65 @@ def _save_audio_for_training(lang_label: str, audio_path: str | None, transcript
508
  return "⚠️ Please type the transcription — what is said in this audio."
509
 
510
  lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
511
- timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
512
- audio_repo_path = f"training_audio/{lang}/{timestamp}.wav"
513
- meta_repo_path = f"training_audio/{lang}/{timestamp}.txt"
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
  if _hf_api is None or not FEEDBACK_REPO_ID:
516
- return "⚠️ HF_TOKEN not set — file saved locally only, not uploaded to Hub."
517
 
518
  try:
519
- import io
520
  _hf_api.upload_file(
521
  path_or_fileobj=audio_path,
522
  path_in_repo=audio_repo_path,
523
  repo_id=FEEDBACK_REPO_ID,
524
  repo_type="dataset",
525
  )
526
- meta = (
527
- f"language: {lang}\n"
528
- f"transcription: {transcript}\n"
529
- f"source: {source_note.strip() or 'uploaded'}\n"
530
- f"timestamp: {timestamp}\n"
531
- )
532
- _hf_api.upload_file(
533
- path_or_fileobj=io.BytesIO(meta.encode()),
534
- path_in_repo=meta_repo_path,
535
- repo_id=FEEDBACK_REPO_ID,
536
- repo_type="dataset",
537
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  return (
539
- f"✅ Saved to training dataset!\n"
540
  f"Audio: {audio_repo_path}\n"
541
  f"Transcription: {transcript[:80]}{'…' if len(transcript) > 80 else ''}\n"
542
- f"Run the training notebook on Kaggle to include this in the next model update."
543
  )
544
  except Exception as exc:
545
  return f"❌ Upload failed: {exc}"
@@ -815,17 +852,21 @@ def build_ui() -> gr.Blocks:
815
  "run the training notebook to fine-tune the speech model."
816
  )
817
  adapter_status_md = gr.Markdown(value=_get_adapter_status())
818
- reload_btn = gr.Button("🔄 Reload Adapters from Hub")
819
  reload_out = gr.Markdown()
820
 
821
  gr.Markdown("---")
822
  gr.Markdown(
823
  "**Training notebook**: "
824
- "`notebooks/train_colab.ipynb` — open in Kaggle or Colab, run all cells.\n\n"
 
 
 
 
825
  "**Feedback dataset**: "
826
  f"`{FEEDBACK_REPO_ID}` (auto-updated on each save)\n\n"
827
- "**Adapter repo**: "
828
- f"`{ADAPTER_REPO_ID}` (updated after training)"
829
  )
830
 
831
  reload_btn.click(fn=_reload_adapters_from_hub, outputs=[reload_out])
 
54
  return fn
55
 
56
  # ── Module-level model state (CPU-resident between requests) ─────────────────
57
+ _whisper_model = None # WhisperForConditionalGeneration (base)
58
+ _whisper_processor = None
59
+ _fine_tuned_models = {} # lang_code -> WhisperForConditionalGeneration (full checkpoint)
60
+ _model_lock = threading.Lock()
61
+ _model_status = "not loaded"
 
62
 
63
  from src.tts.mms_tts import MMSTTSEngine
64
  from src.iot.intent_parser import IntentParser
 
81
  # ── Model loading ─────────────────────────────────────────────────────────────
82
 
83
  def _do_load_whisper():
84
+ global _whisper_model, _whisper_processor, _model_status
85
  import torch
 
86
 
87
  # Import concrete Whisper classes directly — bypasses transformers __init__.py
88
  # Auto-class exports differ between transformers 4.x and 5.x; direct paths are stable.
 
109
  token=HF_TOKEN,
110
  )
111
  _whisper_model.eval()
 
 
 
 
 
 
 
112
  _model_status = f"ready ({WHISPER_MODEL_ID})"
113
  except Exception as e:
114
  _model_status = f"error: {e}"
115
 
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  def _ensure_whisper_loaded():
118
  """Load Whisper to CPU in a background thread on first call. Non-blocking."""
119
  global _model_status
 
159
 
160
  audio_np, _ = librosa.load(audio_path, sr=16000, mono=True)
161
 
162
+ # Use fine-tuned checkpoint for this language if one has been loaded;
163
  # otherwise fall back to base Whisper.
164
+ active_model = _fine_tuned_models.get(language_code, _whisper_model)
 
 
 
 
165
 
166
  active_model.to(device)
167
  with _model_lock:
 
333
  # ── Adapter reload ────────────────────────────────────────────────────────────
334
 
335
  def _reload_adapters_from_hub() -> str:
336
+ """Download full fine-tuned checkpoints from Hub and hot-swap them into memory."""
337
+ global _fine_tuned_models
338
  if _hf_api is None:
339
+ return "⚠️ HF_TOKEN not set — cannot download checkpoints."
340
+ if _whisper_model is None:
341
  return "⏳ Base model not loaded yet — wait for model to finish loading and try again."
342
  try:
343
+ import torch
344
  from huggingface_hub import snapshot_download
345
+ try:
346
+ from transformers.models.whisper.modeling_whisper import WhisperForConditionalGeneration
347
+ except ImportError:
348
+ from transformers import WhisperForConditionalGeneration
349
+
350
  local_dir = snapshot_download(
351
  repo_id=ADAPTER_REPO_ID, repo_type="model", token=HF_TOKEN
352
  )
353
  results = []
354
  for lang, subdir in (("bam", "adapters/bambara"), ("ful", "adapters/fula")):
355
+ ckpt_path = Path(local_dir) / subdir
356
+ if not ckpt_path.exists():
357
+ results.append(f"⚠️ {lang}: `{subdir}` not found run training notebook first")
358
  continue
359
+ if not (ckpt_path / "config.json").exists():
360
+ results.append(f"⚠️ {lang}: `{subdir}/config.json` missing — incomplete checkpoint")
 
361
  continue
362
  try:
363
+ m = WhisperForConditionalGeneration.from_pretrained(
364
+ str(ckpt_path), torch_dtype=torch.float32
365
+ )
366
+ m.eval()
367
+ _fine_tuned_models[lang] = m
368
+ results.append(f"✅ {lang}: fine-tuned checkpoint loaded from `{subdir}`")
369
  except Exception as e:
370
  results.append(f"❌ {lang}: load failed — {e}")
371
 
372
  summary = "\n".join(results)
373
+ active = ", ".join(_fine_tuned_models) if _fine_tuned_models else "none"
374
+ return f"{summary}\n\n**Active fine-tuned models:** {active}\n**Repo:** `{ADAPTER_REPO_ID}`"
375
  except Exception as e:
376
+ return f"❌ Checkpoint reload failed: {e}"
377
 
378
 
379
  def _get_adapter_status() -> str:
380
  lines = []
381
 
382
+ if _fine_tuned_models:
383
+ lines.append(f"**Fine-tuned models loaded:** {', '.join(sorted(_fine_tuned_models))}")
 
384
  else:
385
+ lines.append("**Fine-tuned models:** none — using base Whisper for all languages")
386
 
387
  if _hf_api is None:
388
  lines.append("_HF_TOKEN not set — Hub check skipped._")
 
391
  try:
392
  from huggingface_hub import list_repo_files
393
  files = list(list_repo_files(ADAPTER_REPO_ID, repo_type="model", token=HF_TOKEN))
394
+ bam_ok = any("bambara" in f and "config.json" in f for f in files)
395
+ ful_ok = any("fula" in f and "config.json" in f for f in files)
396
  lines += [
397
  f"\n**Hub repo:** `{ADAPTER_REPO_ID}`",
398
+ f"- Bambara (bam): {'✅ trained checkpoint present' if bam_ok else '⚠️ not yet trained — run Kaggle notebook'}",
399
+ f"- Fula (ful): {'✅ trained checkpoint present' if ful_ok else '⚠️ not yet trained — run Kaggle notebook'}",
400
  ]
401
  if bam_ok or ful_ok:
402
+ lines.append("\n_Click **Reload Models** to activate them._")
403
  except Exception as e:
404
  lines.append(f"_Could not read Hub repo: {e}_")
405
 
 
409
  # ── Knowledge Base handlers ───────────────────────────────────────────────────
410
 
411
  def _import_phrase_pairs(lang_label: str, pairs_text: str) -> str:
412
+ """Import pasted phrase pairs into the phrase library and append to vocabulary.jsonl."""
413
  if not pairs_text.strip():
414
  return "⚠️ Nothing entered. Use the format: native phrase | english translation"
415
  lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
 
417
  if count == 0:
418
  return "⚠️ No valid phrases found. Each line must contain a | separator.\nExample: I ni ce | Hello, good day"
419
  _upload_phrase_additions_to_hub(lang)
420
+ # Also append to vocabulary.jsonl so the Kaggle training notebook picks them up
421
+ _append_phrases_to_vocabulary_jsonl(lang, pairs_text)
422
  total = _phrase_matcher.phrase_count(lang)
423
  return f"✅ Added {count} phrase(s) for {lang_label}. Library now has {total} phrases. Available immediately."
424
 
425
 
426
+ def _append_phrases_to_vocabulary_jsonl(lang: str, pairs_text: str) -> None:
427
+ """Append phrase pairs to vocabulary.jsonl in the feedback repo (training input)."""
428
+ if _hf_api is None or not FEEDBACK_REPO_ID:
429
+ return
430
+ entries = []
431
+ for line in pairs_text.splitlines():
432
+ if "|" not in line:
433
+ continue
434
+ parts = line.split("|", 1)
435
+ word = parts[0].strip()
436
+ translation = parts[1].strip() if len(parts) > 1 else ""
437
+ if word:
438
+ entries.append({"word": word, "translation": translation, "language": lang})
439
+ if not entries:
440
+ return
441
+ try:
442
+ from huggingface_hub import hf_hub_download
443
+ for attempt in range(2):
444
+ try:
445
+ local = hf_hub_download(
446
+ repo_id=FEEDBACK_REPO_ID, filename="vocabulary.jsonl",
447
+ repo_type="dataset", token=HF_TOKEN,
448
+ )
449
+ with open(local, encoding="utf-8") as f:
450
+ existing = f.read()
451
+ except Exception:
452
+ existing = ""
453
+ new_lines = "".join(json.dumps(e, ensure_ascii=False) + "\n" for e in entries)
454
+ updated = existing + new_lines
455
+ try:
456
+ _hf_api.upload_file(
457
+ path_or_fileobj=io.BytesIO(updated.encode("utf-8")),
458
+ path_in_repo="vocabulary.jsonl",
459
+ repo_id=FEEDBACK_REPO_ID,
460
+ repo_type="dataset",
461
+ )
462
+ break
463
+ except Exception:
464
+ if attempt == 1:
465
+ pass # Silent — phrase library still updated locally
466
+ except Exception:
467
+ pass # Non-critical — phrase library already saved via _upload_phrase_additions_to_hub
468
+
469
+
470
  def _upload_phrase_additions_to_hub(lang: str) -> None:
471
  """Persist user phrase additions to HF Hub so they survive Space restarts."""
472
  if _hf_api is None or not FEEDBACK_REPO_ID:
 
510
 
511
 
512
  def _save_audio_for_training(lang_label: str, audio_path: str | None, transcript: str, source_note: str) -> str:
513
+ """Save uploaded audio + transcription to corrections.jsonl so the Kaggle notebook picks it up."""
514
  transcript = transcript.strip()
515
  if audio_path is None:
516
  return "⚠️ Please upload an audio file first."
 
518
  return "⚠️ Please type the transcription — what is said in this audio."
519
 
520
  lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
521
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
522
+ # Store under audio/ — same path structure that corrections.jsonl expects
523
+ audio_repo_path = f"audio/{lang}_{timestamp}.wav"
524
+
525
+ record = {
526
+ "id": timestamp,
527
+ "timestamp": datetime.now(timezone.utc).isoformat(),
528
+ "language": lang,
529
+ "audio_file": audio_repo_path,
530
+ "transcription": transcript, # notebook reads this field
531
+ "corrected_text": transcript, # also populate corrected_text for compatibility
532
+ "source": source_note.strip() or "uploaded",
533
+ "is_correction": False,
534
+ "model": WHISPER_MODEL_ID,
535
+ }
536
 
537
  if _hf_api is None or not FEEDBACK_REPO_ID:
538
+ return "⚠️ HF_TOKEN not set — cannot upload to Hub."
539
 
540
  try:
541
+ # Upload audio to audio/ (same bucket corrections use)
542
  _hf_api.upload_file(
543
  path_or_fileobj=audio_path,
544
  path_in_repo=audio_repo_path,
545
  repo_id=FEEDBACK_REPO_ID,
546
  repo_type="dataset",
547
  )
548
+
549
+ # Append to corrections.jsonl (same file the notebook reads)
550
+ from huggingface_hub import hf_hub_download
551
+ for attempt in range(2):
552
+ try:
553
+ local_jsonl = hf_hub_download(
554
+ repo_id=FEEDBACK_REPO_ID, filename="corrections.jsonl",
555
+ repo_type="dataset", token=HF_TOKEN,
556
+ )
557
+ with open(local_jsonl, encoding="utf-8") as f:
558
+ existing = f.read()
559
+ except Exception:
560
+ existing = ""
561
+ updated = existing + json.dumps(record, ensure_ascii=False) + "\n"
562
+ try:
563
+ _hf_api.upload_file(
564
+ path_or_fileobj=io.BytesIO(updated.encode("utf-8")),
565
+ path_in_repo="corrections.jsonl",
566
+ repo_id=FEEDBACK_REPO_ID,
567
+ repo_type="dataset",
568
+ )
569
+ break
570
+ except Exception as e:
571
+ if attempt == 1:
572
+ return f"⚠️ Audio uploaded but corrections.jsonl update failed: {e}"
573
+
574
+ total = updated.count("\n")
575
  return (
576
+ f"✅ Saved to training dataset (#{total} total corrections)!\n"
577
  f"Audio: {audio_repo_path}\n"
578
  f"Transcription: {transcript[:80]}{'…' if len(transcript) > 80 else ''}\n"
579
+ f"Run the Kaggle notebook to include this in the next model update."
580
  )
581
  except Exception as exc:
582
  return f"❌ Upload failed: {exc}"
 
852
  "run the training notebook to fine-tune the speech model."
853
  )
854
  adapter_status_md = gr.Markdown(value=_get_adapter_status())
855
+ reload_btn = gr.Button("🔄 Reload Fine-tuned Models from Hub")
856
  reload_out = gr.Markdown()
857
 
858
  gr.Markdown("---")
859
  gr.Markdown(
860
  "**Training notebook**: "
861
+ "`notebooks/kaggle_master_trainer.ipynb` — import to Kaggle, run all cells.\n\n"
862
+ "**What feeds training:**\n"
863
+ "- Tab 2 corrections → `corrections.jsonl` in the feedback dataset\n"
864
+ "- Tab 3 audio uploads → `corrections.jsonl` (same file)\n"
865
+ "- Tab 3 phrase pairs → `vocabulary.jsonl` (used as synthetic fallback labels)\n\n"
866
  "**Feedback dataset**: "
867
  f"`{FEEDBACK_REPO_ID}` (auto-updated on each save)\n\n"
868
+ "**Model checkpoint repo**: "
869
+ f"`{ADAPTER_REPO_ID}` (updated after training, reload above to activate)"
870
  )
871
 
872
  reload_btn.click(fn=_reload_adapters_from_hub, outputs=[reload_out])