Spaces:
Sleeping
Sleeping
jefffffff9 Claude Sonnet 4.6 commited on
Commit ·
24b1617
1
Parent(s): 670a1d1
Fix Self-Teaching tab: float sliders, deduplication, Kaggle API fallback
Browse files- Cast slider values to int (Gradio returns float; broke Wikipedia aplimit
and Kaggle ds.take() in notebook Cell 9)
- Deduplicate dataset_sources.jsonl on each HF import (avoid loading same
dataset multiple times per training run)
- Kaggle trigger: try Python API (KaggleApiExtended) first to avoid binary
PATH issues; subprocess + shutil.which as fallback
- Fix Fula dataset registry: fleurs uses legacy script loader incompatible
with datasets>=3.0; switch to google/WaxalNLP ful_asr
- Fix UI description: HF import writes to dataset_sources.jsonl (streamed
by Kaggle), not to corrections.jsonl
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.py +101 -32
- src/data/web_harvester.py +2 -2
app.py
CHANGED
|
@@ -609,27 +609,62 @@ def _count_corrections() -> int:
|
|
| 609 |
|
| 610 |
def _trigger_kaggle_training(lang: str = "bam") -> str:
|
| 611 |
"""
|
| 612 |
-
Push the master trainer notebook to Kaggle
|
| 613 |
-
|
| 614 |
|
| 615 |
-
|
| 616 |
-
The notebook is read from notebooks/ inside the Space filesystem.
|
| 617 |
"""
|
| 618 |
if not KAGGLE_USERNAME or not KAGGLE_KEY:
|
| 619 |
return "⚠️ KAGGLE_USERNAME / KAGGLE_KEY not set in Space secrets."
|
| 620 |
|
| 621 |
-
import subprocess
|
| 622 |
-
|
| 623 |
notebooks_dir = ROOT / "notebooks"
|
| 624 |
-
|
| 625 |
-
|
| 626 |
|
| 627 |
if not nb_file.exists():
|
| 628 |
return "❌ notebooks/kaggle_master_trainer.ipynb not found in Space."
|
| 629 |
if not meta_file.exists():
|
| 630 |
return "❌ notebooks/kernel-metadata.json not found in Space."
|
| 631 |
|
| 632 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
env = {
|
| 634 |
**os.environ,
|
| 635 |
"KAGGLE_USERNAME": KAGGLE_USERNAME,
|
|
@@ -637,18 +672,18 @@ def _trigger_kaggle_training(lang: str = "bam") -> str:
|
|
| 637 |
"PYTHONUTF8": "1",
|
| 638 |
"PYTHONIOENCODING": "utf-8",
|
| 639 |
}
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
return f"✅ Kaggle training triggered!\n{output or 'Kernel version created — check Kaggle for progress.'}"
|
| 649 |
-
else:
|
| 650 |
err = (result.stderr or result.stdout or "unknown error").strip()
|
| 651 |
return f"❌ Kaggle push failed:\n{err}"
|
|
|
|
|
|
|
| 652 |
|
| 653 |
|
| 654 |
def _maybe_auto_trigger() -> None:
|
|
@@ -807,6 +842,7 @@ def _harvest_wikipedia(lang_label: str, max_articles: int = 100) -> str:
|
|
| 807 |
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 808 |
if lang not in ("bam", "ful"):
|
| 809 |
return "⚠️ Supported for Bambara and Fula only."
|
|
|
|
| 810 |
|
| 811 |
try:
|
| 812 |
from src.data.web_harvester import harvest_wikipedia_text
|
|
@@ -839,31 +875,62 @@ def _harvest_hf_dataset(lang_label: str, max_samples: int = 500) -> str:
|
|
| 839 |
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 840 |
if lang not in ("bam", "ful"):
|
| 841 |
return "⚠️ Supported for Bambara and Fula only."
|
|
|
|
| 842 |
|
| 843 |
from src.data.web_harvester import get_hf_dataset_refs
|
| 844 |
refs = get_hf_dataset_refs(lang)
|
| 845 |
if not refs:
|
| 846 |
return f"⚠️ No HF dataset configured for {lang}."
|
| 847 |
|
| 848 |
-
#
|
| 849 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
for ref in refs:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
entry = dict(ref)
|
| 852 |
-
entry["max"]
|
| 853 |
-
entry["enabled"]
|
| 854 |
-
entry["added_at"]
|
| 855 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
|
| 857 |
-
total, err = _upload_jsonl("dataset_sources.jsonl",
|
| 858 |
if err:
|
| 859 |
return f"❌ Upload failed: {err}"
|
| 860 |
|
| 861 |
repos = ", ".join(r["repo"] for r in refs)
|
| 862 |
return (
|
| 863 |
f"✅ Dataset registered for training!\n"
|
| 864 |
-
f" Source(s)
|
| 865 |
f" Max samples : {max_samples}\n"
|
| 866 |
-
f" The Kaggle notebook will
|
|
|
|
| 867 |
f" Total registered sources: {total}"
|
| 868 |
)
|
| 869 |
|
|
@@ -1225,11 +1292,13 @@ def build_ui() -> gr.Blocks:
|
|
| 1225 |
with gr.Column():
|
| 1226 |
gr.Markdown(
|
| 1227 |
"### 🤗 HuggingFace Dataset Import\n"
|
| 1228 |
-
"
|
| 1229 |
"- **Bambara**: `RobotsMali/jeli-asr` (33,000 samples)\n"
|
| 1230 |
-
"- **Fula**: `google/
|
| 1231 |
-
"
|
| 1232 |
-
|
|
|
|
|
|
|
| 1233 |
)
|
| 1234 |
hf_lang = gr.Dropdown(
|
| 1235 |
choices=["Bambara (bam)", "Fula (ful)"],
|
|
|
|
| 609 |
|
| 610 |
def _trigger_kaggle_training(lang: str = "bam") -> str:
|
| 611 |
"""
|
| 612 |
+
Push the master trainer notebook to Kaggle, creating a new kernel version
|
| 613 |
+
(i.e. triggering a run). Requires KAGGLE_USERNAME + KAGGLE_KEY secrets.
|
| 614 |
|
| 615 |
+
Tries the Python API first (no PATH issues), falls back to subprocess.
|
|
|
|
| 616 |
"""
|
| 617 |
if not KAGGLE_USERNAME or not KAGGLE_KEY:
|
| 618 |
return "⚠️ KAGGLE_USERNAME / KAGGLE_KEY not set in Space secrets."
|
| 619 |
|
|
|
|
|
|
|
| 620 |
notebooks_dir = ROOT / "notebooks"
|
| 621 |
+
nb_file = notebooks_dir / "kaggle_master_trainer.ipynb"
|
| 622 |
+
meta_file = notebooks_dir / "kernel-metadata.json"
|
| 623 |
|
| 624 |
if not nb_file.exists():
|
| 625 |
return "❌ notebooks/kaggle_master_trainer.ipynb not found in Space."
|
| 626 |
if not meta_file.exists():
|
| 627 |
return "❌ notebooks/kernel-metadata.json not found in Space."
|
| 628 |
|
| 629 |
+
# Inject credentials into os.environ before any kaggle import —
|
| 630 |
+
# kaggle.authenticate() reads KAGGLE_USERNAME + KAGGLE_KEY from env.
|
| 631 |
+
os.environ["KAGGLE_USERNAME"] = KAGGLE_USERNAME
|
| 632 |
+
os.environ["KAGGLE_KEY"] = KAGGLE_KEY
|
| 633 |
+
|
| 634 |
+
# ── Method 1: Python API (no binary PATH issues) ─────────────────────────
|
| 635 |
+
api_err = None
|
| 636 |
+
try:
|
| 637 |
+
from kaggle.api.kaggle_api_extended import KaggleApiExtended
|
| 638 |
+
_kapi = KaggleApiExtended()
|
| 639 |
+
_kapi.authenticate()
|
| 640 |
+
_kapi.kernels_push_cli(str(notebooks_dir), quiet=True)
|
| 641 |
+
return (
|
| 642 |
+
"✅ Kaggle training triggered!\n"
|
| 643 |
+
f"Kernel: {KAGGLE_KERNEL_SLUG}\n"
|
| 644 |
+
"Check https://www.kaggle.com for run progress."
|
| 645 |
+
)
|
| 646 |
+
except Exception as e:
|
| 647 |
+
api_err = str(e)
|
| 648 |
+
|
| 649 |
+
# ── Method 2: subprocess fallback ────────────────────────────────────────
|
| 650 |
+
import subprocess, shutil
|
| 651 |
+
kaggle_bin = shutil.which("kaggle")
|
| 652 |
+
if kaggle_bin is None:
|
| 653 |
+
for cand in [
|
| 654 |
+
Path(sys.executable).parent / "kaggle",
|
| 655 |
+
Path("/usr/local/bin/kaggle"),
|
| 656 |
+
Path("/usr/bin/kaggle"),
|
| 657 |
+
]:
|
| 658 |
+
if cand.exists():
|
| 659 |
+
kaggle_bin = str(cand)
|
| 660 |
+
break
|
| 661 |
+
|
| 662 |
+
if kaggle_bin is None:
|
| 663 |
+
return (
|
| 664 |
+
f"❌ Kaggle CLI not found (API error: {api_err}).\n"
|
| 665 |
+
"Ensure kaggle>=1.6.0 is in requirements.txt and the Space rebuilt."
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
env = {
|
| 669 |
**os.environ,
|
| 670 |
"KAGGLE_USERNAME": KAGGLE_USERNAME,
|
|
|
|
| 672 |
"PYTHONUTF8": "1",
|
| 673 |
"PYTHONIOENCODING": "utf-8",
|
| 674 |
}
|
| 675 |
+
try:
|
| 676 |
+
result = subprocess.run(
|
| 677 |
+
[kaggle_bin, "kernels", "push", "-p", str(notebooks_dir)],
|
| 678 |
+
capture_output=True, text=True, timeout=60, env=env,
|
| 679 |
+
)
|
| 680 |
+
if result.returncode == 0:
|
| 681 |
+
out = (result.stdout or "").strip()
|
| 682 |
+
return f"✅ Kaggle training triggered!\n{out or 'Kernel version created.'}"
|
|
|
|
|
|
|
| 683 |
err = (result.stderr or result.stdout or "unknown error").strip()
|
| 684 |
return f"❌ Kaggle push failed:\n{err}"
|
| 685 |
+
except Exception as e:
|
| 686 |
+
return f"❌ Kaggle push failed (API: {api_err}) (CLI: {e})"
|
| 687 |
|
| 688 |
|
| 689 |
def _maybe_auto_trigger() -> None:
|
|
|
|
| 842 |
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 843 |
if lang not in ("bam", "ful"):
|
| 844 |
return "⚠️ Supported for Bambara and Fula only."
|
| 845 |
+
max_articles = int(max_articles) # Gradio slider returns float
|
| 846 |
|
| 847 |
try:
|
| 848 |
from src.data.web_harvester import harvest_wikipedia_text
|
|
|
|
| 875 |
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 876 |
if lang not in ("bam", "ful"):
|
| 877 |
return "⚠️ Supported for Bambara and Fula only."
|
| 878 |
+
max_samples = int(max_samples) # Gradio slider returns float
|
| 879 |
|
| 880 |
from src.data.web_harvester import get_hf_dataset_refs
|
| 881 |
refs = get_hf_dataset_refs(lang)
|
| 882 |
if not refs:
|
| 883 |
return f"⚠️ No HF dataset configured for {lang}."
|
| 884 |
|
| 885 |
+
# Read existing entries to avoid duplicates
|
| 886 |
+
from huggingface_hub import hf_hub_download
|
| 887 |
+
try:
|
| 888 |
+
local = hf_hub_download(
|
| 889 |
+
repo_id=FEEDBACK_REPO_ID, filename="dataset_sources.jsonl",
|
| 890 |
+
repo_type="dataset", token=HF_TOKEN,
|
| 891 |
+
)
|
| 892 |
+
with open(local, encoding="utf-8") as f:
|
| 893 |
+
existing_entries = [json.loads(l) for l in f if l.strip()]
|
| 894 |
+
except Exception:
|
| 895 |
+
existing_entries = []
|
| 896 |
+
|
| 897 |
+
existing_keys = {
|
| 898 |
+
(e.get("repo", e.get("repo_id", "")), e.get("config", ""))
|
| 899 |
+
for e in existing_entries
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
new_entries = []
|
| 903 |
+
already = []
|
| 904 |
for ref in refs:
|
| 905 |
+
key = (ref.get("repo", ref.get("repo_id", "")), ref.get("config", ""))
|
| 906 |
+
if key in existing_keys:
|
| 907 |
+
already.append(ref["repo"])
|
| 908 |
+
continue
|
| 909 |
entry = dict(ref)
|
| 910 |
+
entry["max"] = max_samples
|
| 911 |
+
entry["enabled"] = True
|
| 912 |
+
entry["added_at"] = datetime.now(timezone.utc).isoformat()
|
| 913 |
+
new_entries.append(entry)
|
| 914 |
+
|
| 915 |
+
if not new_entries:
|
| 916 |
+
repos = ", ".join(already)
|
| 917 |
+
return (
|
| 918 |
+
f"✅ Already registered!\n"
|
| 919 |
+
f" `{repos}` is already in your training config.\n"
|
| 920 |
+
f" Click 'Trigger Training Now' to start a run with this data."
|
| 921 |
+
)
|
| 922 |
|
| 923 |
+
total, err = _upload_jsonl("dataset_sources.jsonl", new_entries)
|
| 924 |
if err:
|
| 925 |
return f"❌ Upload failed: {err}"
|
| 926 |
|
| 927 |
repos = ", ".join(r["repo"] for r in refs)
|
| 928 |
return (
|
| 929 |
f"✅ Dataset registered for training!\n"
|
| 930 |
+
f" Source(s) : {repos}\n"
|
| 931 |
f" Max samples : {max_samples}\n"
|
| 932 |
+
f" The Kaggle notebook will stream this dataset directly at training time.\n"
|
| 933 |
+
f" Click 'Trigger Training Now' to start a training run.\n"
|
| 934 |
f" Total registered sources: {total}"
|
| 935 |
)
|
| 936 |
|
|
|
|
| 1292 |
with gr.Column():
|
| 1293 |
gr.Markdown(
|
| 1294 |
"### 🤗 HuggingFace Dataset Import\n"
|
| 1295 |
+
"Registers large public datasets as training sources:\n"
|
| 1296 |
"- **Bambara**: `RobotsMali/jeli-asr` (33,000 samples)\n"
|
| 1297 |
+
"- **Fula**: `google/WaxalNLP ful_asr`\n\n"
|
| 1298 |
+
"This writes a reference to `dataset_sources.jsonl`. "
|
| 1299 |
+
"The Kaggle training notebook streams the dataset directly "
|
| 1300 |
+
"at training time — no re-upload needed.\n\n"
|
| 1301 |
+
"**One click is enough** — duplicates are ignored automatically."
|
| 1302 |
)
|
| 1303 |
hf_lang = gr.Dropdown(
|
| 1304 |
choices=["Bambara (bam)", "Fula (ful)"],
|
src/data/web_harvester.py
CHANGED
|
@@ -40,8 +40,8 @@ HF_DATASET_REGISTRY = {
|
|
| 40 |
],
|
| 41 |
"ful": [
|
| 42 |
{
|
| 43 |
-
"repo": "google/
|
| 44 |
-
"config": "
|
| 45 |
"split": "train",
|
| 46 |
"audio_col": "audio",
|
| 47 |
"text_col": "transcription",
|
|
|
|
| 40 |
],
|
| 41 |
"ful": [
|
| 42 |
{
|
| 43 |
+
"repo": "google/WaxalNLP",
|
| 44 |
+
"config": "ful_asr",
|
| 45 |
"split": "train",
|
| 46 |
"audio_col": "audio",
|
| 47 |
"text_col": "transcription",
|