Spaces:
Sleeping
Sleeping
jefffffff9 Claude Sonnet 4.6 commited on
Commit ·
a5737ac
1
Parent(s): 520ca12
Fix Wikipedia and HF harvest buttons
Browse filesWikipedia fix:
- Add User-Agent header to all requests (Wikipedia API returns 403 without it)
- Extract shared _upload_jsonl() helper to reduce code duplication
HF harvest fix:
- Remove audio re-upload approach (500 HTTP requests -> Gradio timeout)
- Instead write dataset references to dataset_sources.jsonl on the Hub
- One small file upload (~instant) instead of gigabytes of audio
- Kaggle notebook Cell 4 now reads dataset_sources.jsonl dynamically:
any dataset registered in Space Self-Teaching tab is automatically
picked up at training time with no notebook edits needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.py +60 -89
- notebooks/kaggle_master_trainer.ipynb +3 -1
- src/data/web_harvester.py +49 -105
app.py
CHANGED
|
@@ -748,128 +748,99 @@ def _bulk_upload(lang_label: str, zip_file, csv_text: str) -> str:
|
|
| 748 |
|
| 749 |
# ── Internet self-teaching handlers ───────────────────────────────────────────
|
| 750 |
|
| 751 |
-
def
|
| 752 |
-
"""
|
| 753 |
-
if _hf_api is None:
|
| 754 |
-
return "⚠️ HF_TOKEN not set."
|
| 755 |
-
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 756 |
-
if lang not in ("bam", "ful"):
|
| 757 |
-
return "⚠️ Wikipedia harvest only supported for Bambara and Fula."
|
| 758 |
-
|
| 759 |
-
from src.data.web_harvester import harvest_wikipedia_text
|
| 760 |
-
entries = harvest_wikipedia_text(lang, max_articles=max_articles)
|
| 761 |
-
if not entries:
|
| 762 |
-
return "⚠️ No text harvested — check network or try again."
|
| 763 |
-
|
| 764 |
-
# Append to vocabulary.jsonl
|
| 765 |
from huggingface_hub import hf_hub_download
|
| 766 |
for attempt in range(2):
|
| 767 |
try:
|
| 768 |
local = hf_hub_download(
|
| 769 |
-
repo_id=FEEDBACK_REPO_ID, filename=
|
| 770 |
repo_type="dataset", token=HF_TOKEN,
|
| 771 |
)
|
| 772 |
with open(local, encoding="utf-8") as f:
|
| 773 |
existing = f.read()
|
| 774 |
except Exception:
|
| 775 |
existing = ""
|
| 776 |
-
|
| 777 |
-
updated = existing + new_lines
|
| 778 |
try:
|
| 779 |
_hf_api.upload_file(
|
| 780 |
path_or_fileobj=io.BytesIO(updated.encode("utf-8")),
|
| 781 |
-
path_in_repo=
|
| 782 |
repo_id=FEEDBACK_REPO_ID,
|
| 783 |
repo_type="dataset",
|
| 784 |
)
|
| 785 |
-
|
| 786 |
except Exception as e:
|
| 787 |
if attempt == 1:
|
| 788 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
return (
|
| 791 |
f"✅ Wikipedia harvest complete!\n"
|
| 792 |
-
f" Language
|
| 793 |
-
f" Sentences added
|
| 794 |
-
f"
|
| 795 |
)
|
| 796 |
|
| 797 |
|
| 798 |
def _harvest_hf_dataset(lang_label: str, max_samples: int = 500) -> str:
|
| 799 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
if _hf_api is None:
|
| 801 |
return "⚠️ HF_TOKEN not set."
|
| 802 |
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 803 |
if lang not in ("bam", "ful"):
|
| 804 |
-
return "⚠️
|
| 805 |
|
| 806 |
-
from src.data.web_harvester import
|
| 807 |
-
|
| 808 |
-
if not
|
| 809 |
return f"⚠️ No HF dataset configured for {lang}."
|
| 810 |
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
for
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
"audio_file": repo_path,
|
| 826 |
-
"transcription": text, "corrected_text": text,
|
| 827 |
-
"source": f"hf_harvest:{sources[0]['repo']}",
|
| 828 |
-
"is_correction": False, "model": WHISPER_MODEL_ID,
|
| 829 |
-
})
|
| 830 |
-
if len(records) >= max_samples:
|
| 831 |
-
break
|
| 832 |
-
except Exception:
|
| 833 |
-
errors += 1
|
| 834 |
-
if errors > 20:
|
| 835 |
-
break
|
| 836 |
-
|
| 837 |
-
if not records:
|
| 838 |
-
return "⚠️ No samples harvested. Dataset may require accepting terms on HuggingFace first."
|
| 839 |
-
|
| 840 |
-
# Append to corrections.jsonl
|
| 841 |
-
from huggingface_hub import hf_hub_download
|
| 842 |
-
for attempt in range(2):
|
| 843 |
-
try:
|
| 844 |
-
local = hf_hub_download(
|
| 845 |
-
repo_id=FEEDBACK_REPO_ID, filename="corrections.jsonl",
|
| 846 |
-
repo_type="dataset", token=HF_TOKEN,
|
| 847 |
-
)
|
| 848 |
-
with open(local, encoding="utf-8") as f:
|
| 849 |
-
existing = f.read()
|
| 850 |
-
except Exception:
|
| 851 |
-
existing = ""
|
| 852 |
-
new_lines = "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in records)
|
| 853 |
-
updated = existing + new_lines
|
| 854 |
-
try:
|
| 855 |
-
_hf_api.upload_file(
|
| 856 |
-
path_or_fileobj=io.BytesIO(updated.encode("utf-8")),
|
| 857 |
-
path_in_repo="corrections.jsonl",
|
| 858 |
-
repo_id=FEEDBACK_REPO_ID,
|
| 859 |
-
repo_type="dataset",
|
| 860 |
-
)
|
| 861 |
-
break
|
| 862 |
-
except Exception as e:
|
| 863 |
-
if attempt == 1:
|
| 864 |
-
return f"❌ corrections.jsonl update failed: {e}"
|
| 865 |
-
|
| 866 |
-
total = updated.count("\n")
|
| 867 |
-
_maybe_auto_trigger()
|
| 868 |
return (
|
| 869 |
-
f"✅
|
| 870 |
-
f" Source
|
| 871 |
-
f"
|
| 872 |
-
f"
|
|
|
|
| 873 |
)
|
| 874 |
|
| 875 |
|
|
|
|
| 748 |
|
| 749 |
# ── Internet self-teaching handlers ───────────────────────────────────────────
|
| 750 |
|
| 751 |
+
def _upload_jsonl(repo_path: str, entries: list[dict]) -> tuple[int, str | None]:
|
| 752 |
+
"""Append entries to a jsonl file on the Hub. Returns (total_lines, error_or_None)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
from huggingface_hub import hf_hub_download
|
| 754 |
for attempt in range(2):
|
| 755 |
try:
|
| 756 |
local = hf_hub_download(
|
| 757 |
+
repo_id=FEEDBACK_REPO_ID, filename=repo_path,
|
| 758 |
repo_type="dataset", token=HF_TOKEN,
|
| 759 |
)
|
| 760 |
with open(local, encoding="utf-8") as f:
|
| 761 |
existing = f.read()
|
| 762 |
except Exception:
|
| 763 |
existing = ""
|
| 764 |
+
updated = existing + "".join(json.dumps(e, ensure_ascii=False) + "\n" for e in entries)
|
|
|
|
| 765 |
try:
|
| 766 |
_hf_api.upload_file(
|
| 767 |
path_or_fileobj=io.BytesIO(updated.encode("utf-8")),
|
| 768 |
+
path_in_repo=repo_path,
|
| 769 |
repo_id=FEEDBACK_REPO_ID,
|
| 770 |
repo_type="dataset",
|
| 771 |
)
|
| 772 |
+
return updated.count("\n"), None
|
| 773 |
except Exception as e:
|
| 774 |
if attempt == 1:
|
| 775 |
+
return 0, str(e)
|
| 776 |
+
return 0, "unknown error"
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
def _harvest_wikipedia(lang_label: str, max_articles: int = 100) -> str:
|
| 780 |
+
"""Fetch Wikipedia text and append to vocabulary.jsonl."""
|
| 781 |
+
if _hf_api is None:
|
| 782 |
+
return "⚠️ HF_TOKEN not set."
|
| 783 |
+
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 784 |
+
if lang not in ("bam", "ful"):
|
| 785 |
+
return "⚠️ Supported for Bambara and Fula only."
|
| 786 |
|
| 787 |
+
try:
|
| 788 |
+
from src.data.web_harvester import harvest_wikipedia_text
|
| 789 |
+
entries = harvest_wikipedia_text(lang, max_articles=max_articles)
|
| 790 |
+
except Exception as e:
|
| 791 |
+
return f"❌ Harvest error: {e}"
|
| 792 |
+
|
| 793 |
+
if not entries:
|
| 794 |
+
return "⚠️ No sentences extracted. Wikipedia may be temporarily unavailable."
|
| 795 |
+
|
| 796 |
+
total, err = _upload_jsonl("vocabulary.jsonl", entries)
|
| 797 |
+
if err:
|
| 798 |
+
return f"❌ Upload failed: {err}"
|
| 799 |
return (
|
| 800 |
f"✅ Wikipedia harvest complete!\n"
|
| 801 |
+
f" Language : {lang_label}\n"
|
| 802 |
+
f" Sentences added : {len(entries)}\n"
|
| 803 |
+
f" Vocabulary total : {total} entries"
|
| 804 |
)
|
| 805 |
|
| 806 |
|
| 807 |
def _harvest_hf_dataset(lang_label: str, max_samples: int = 500) -> str:
|
| 808 |
+
"""
|
| 809 |
+
Register an HF dataset as a training source by writing its config to
|
| 810 |
+
dataset_sources.jsonl on the Hub. The Kaggle notebook reads this file
|
| 811 |
+
at Cell 4 and loads the dataset directly — no audio re-upload needed.
|
| 812 |
+
"""
|
| 813 |
if _hf_api is None:
|
| 814 |
return "⚠️ HF_TOKEN not set."
|
| 815 |
lang = SUPPORTED_LANGUAGES.get(lang_label, "bam")
|
| 816 |
if lang not in ("bam", "ful"):
|
| 817 |
+
return "⚠️ Supported for Bambara and Fula only."
|
| 818 |
|
| 819 |
+
from src.data.web_harvester import get_hf_dataset_refs
|
| 820 |
+
refs = get_hf_dataset_refs(lang)
|
| 821 |
+
if not refs:
|
| 822 |
return f"⚠️ No HF dataset configured for {lang}."
|
| 823 |
|
| 824 |
+
# Write dataset references with user-requested max_samples cap
|
| 825 |
+
entries = []
|
| 826 |
+
for ref in refs:
|
| 827 |
+
entry = dict(ref)
|
| 828 |
+
entry["max"] = max_samples
|
| 829 |
+
entry["enabled"] = True
|
| 830 |
+
entry["added_at"] = datetime.now(timezone.utc).isoformat()
|
| 831 |
+
entries.append(entry)
|
| 832 |
+
|
| 833 |
+
total, err = _upload_jsonl("dataset_sources.jsonl", entries)
|
| 834 |
+
if err:
|
| 835 |
+
return f"❌ Upload failed: {err}"
|
| 836 |
+
|
| 837 |
+
repos = ", ".join(r["repo"] for r in refs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
return (
|
| 839 |
+
f"✅ Dataset registered for training!\n"
|
| 840 |
+
f" Source(s) : {repos}\n"
|
| 841 |
+
f" Max samples : {max_samples}\n"
|
| 842 |
+
f" The Kaggle notebook will load this dataset directly at training time.\n"
|
| 843 |
+
f" Total registered sources: {total}"
|
| 844 |
)
|
| 845 |
|
| 846 |
|
notebooks/kaggle_master_trainer.ipynb
CHANGED
|
@@ -71,7 +71,9 @@
|
|
| 71 |
"id": "cell-ext-config",
|
| 72 |
"metadata": {},
|
| 73 |
"outputs": [],
|
| 74 |
-
"source":
|
|
|
|
|
|
|
| 75 |
},
|
| 76 |
{
|
| 77 |
"cell_type": "code",
|
|
|
|
| 71 |
"id": "cell-ext-config",
|
| 72 |
"metadata": {},
|
| 73 |
"outputs": [],
|
| 74 |
+
"source": [
|
| 75 |
+
"# -- Cell 4: External dataset configuration -----------------------------------\n# EXTERNAL_DATASETS is loaded dynamically from dataset_sources.jsonl in the\n# feedback repo. The Space's Self-Teaching tab writes dataset references there\n# when the user clicks \"Import from HuggingFace\". This cell reads that file\n# so any dataset registered in the Space is automatically used here.\n\nimport json as _json\nfrom huggingface_hub import hf_hub_download as _hf_dl\n\nEXTERNAL_DATASETS = []\n\n# -- Load dataset_sources.jsonl from Hub (written by Space Self-Teaching tab) --\ntry:\n _src_path = _hf_dl(\n repo_id=FEEDBACK_REPO_ID, filename='dataset_sources.jsonl',\n repo_type='dataset', token=HF_TOKEN,\n )\n with open(_src_path, encoding='utf-8') as _f:\n for _line in _f:\n _line = _line.strip()\n if not _line:\n continue\n _entry = _json.loads(_line)\n if not _entry.get('enabled'):\n continue\n # Normalise keys to what Cell 9 expects\n EXTERNAL_DATASETS.append({\n 'enabled' : True,\n 'repo_id' : _entry.get('repo', _entry.get('repo_id', '')),\n 'config' : _entry.get('config'),\n 'split' : _entry.get('split', 'train'),\n 'text_col' : _entry.get('text_col', 'transcription'),\n 'lang' : _entry.get('lang', _entry.get('language', TRAIN_LANG)),\n 'max_samples': _entry.get('max', _entry.get('max_samples', 2_000)),\n })\n print(f'dataset_sources.jsonl: loaded {len(EXTERNAL_DATASETS)} source(s)')\nexcept Exception as _e:\n print(f'dataset_sources.jsonl not found or empty ({_e}) -- using hardcoded list only')\n\nactive = [d for d in EXTERNAL_DATASETS if d.get('lang') == TRAIN_LANG]\nprint(f'External sources active for {TRAIN_LANG}: {len(active)}')\nfor _d in active:\n print(f\" - {_d['repo_id']} / {_d['config']} (max {_d['max_samples']} samples)\")\nif not active:\n if TRAIN_LANG == 'bam':\n print('Bambara: no external source yet.')\n print(' In the Space -> Self-Teaching tab -> Import from HuggingFace (Bambara).')\n elif TRAIN_LANG == 'ful':\n print('Fula: WaxalNLP ful_asr loaded in Cell 8 -- no extra source needed.')\n"
|
| 76 |
+
]
|
| 77 |
},
|
| 78 |
{
|
| 79 |
"cell_type": "code",
|
src/data/web_harvester.py
CHANGED
|
@@ -5,32 +5,49 @@ into the sahel-agri-feedback HF dataset repo.
|
|
| 5 |
Sources:
|
| 6 |
- RobotsMali/jeli-asr (HF, 33k Bambara audio+text samples)
|
| 7 |
- google/fleurs ff_sn (HF, Fula audio+text)
|
| 8 |
-
- bm.wikipedia.org / ff.wikipedia.org (Wikipedia API, text only
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
"""
|
| 13 |
from __future__ import annotations
|
| 14 |
|
| 15 |
import io
|
| 16 |
import json
|
| 17 |
import time
|
| 18 |
-
from datetime import datetime, timezone
|
| 19 |
-
from typing import Generator
|
| 20 |
|
| 21 |
WIKI_APIS = {
|
| 22 |
"bam": "https://bm.wikipedia.org/w/api.php",
|
| 23 |
"ful": "https://ff.wikipedia.org/w/api.php",
|
| 24 |
}
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
"bam": [
|
| 28 |
-
{
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
],
|
| 31 |
"ful": [
|
| 32 |
-
{
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
],
|
| 35 |
}
|
| 36 |
|
|
@@ -39,8 +56,8 @@ HF_ASR_SOURCES = {
|
|
| 39 |
|
| 40 |
def harvest_wikipedia_text(lang: str, max_articles: int = 100) -> list[dict]:
|
| 41 |
"""
|
| 42 |
-
Fetch up to max_articles article extracts from the language
|
| 43 |
-
Returns list of {word, translation, language} dicts
|
| 44 |
"""
|
| 45 |
import urllib.request, urllib.parse
|
| 46 |
|
|
@@ -48,17 +65,15 @@ def harvest_wikipedia_text(lang: str, max_articles: int = 100) -> list[dict]:
|
|
| 48 |
if not api_url:
|
| 49 |
return []
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
"
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
"apfilterredir": "nonredirects",
|
| 57 |
-
"format": "json",
|
| 58 |
-
})
|
| 59 |
-
with urllib.request.urlopen(f"{api_url}?{params}", timeout=15) as r:
|
| 60 |
-
data = json.loads(r.read())
|
| 61 |
|
|
|
|
|
|
|
|
|
|
| 62 |
titles = [p["title"] for p in data.get("query", {}).get("allpages", [])]
|
| 63 |
if not titles:
|
| 64 |
return []
|
|
@@ -67,105 +82,34 @@ def harvest_wikipedia_text(lang: str, max_articles: int = 100) -> list[dict]:
|
|
| 67 |
entries = []
|
| 68 |
for i in range(0, len(titles), 20):
|
| 69 |
batch = titles[i:i + 20]
|
| 70 |
-
params2 = urllib.parse.urlencode({
|
| 71 |
-
"action": "query",
|
| 72 |
-
"titles": "|".join(batch),
|
| 73 |
-
"prop": "extracts",
|
| 74 |
-
"exsentences": 3,
|
| 75 |
-
"exlimit": len(batch),
|
| 76 |
-
"explaintext": True,
|
| 77 |
-
"format": "json",
|
| 78 |
-
})
|
| 79 |
try:
|
| 80 |
-
|
| 81 |
-
|
|
|
|
| 82 |
for page in data2.get("query", {}).get("pages", {}).values():
|
| 83 |
extract = (page.get("extract") or "").strip()
|
| 84 |
title = page.get("title", "").strip()
|
| 85 |
if not extract or not title:
|
| 86 |
continue
|
| 87 |
-
# Split into sentences, keep those 3–20 words
|
| 88 |
for sentence in extract.replace("\n", " ").split("."):
|
| 89 |
sentence = sentence.strip()
|
| 90 |
words = sentence.split()
|
| 91 |
if 3 <= len(words) <= 20:
|
| 92 |
entries.append({
|
| 93 |
-
"word":
|
| 94 |
-
"translation": title,
|
| 95 |
-
"language":
|
| 96 |
-
"source":
|
| 97 |
})
|
| 98 |
except Exception:
|
| 99 |
pass
|
| 100 |
-
time.sleep(0.3)
|
| 101 |
|
| 102 |
return entries
|
| 103 |
|
| 104 |
|
| 105 |
-
# ── HF dataset
|
| 106 |
-
|
| 107 |
-
def harvest_hf_audio(
|
| 108 |
-
lang: str,
|
| 109 |
-
hf_token: str | None,
|
| 110 |
-
progress_cb=None,
|
| 111 |
-
) -> Generator[tuple[bytes, str, str], None, None]:
|
| 112 |
-
"""
|
| 113 |
-
Yield (wav_bytes, transcription, audio_repo_path) for each sample
|
| 114 |
-
in the configured HF ASR sources for this language.
|
| 115 |
-
|
| 116 |
-
Caller is responsible for writing to corrections.jsonl + uploading audio.
|
| 117 |
-
progress_cb(current, total, message) called periodically if provided.
|
| 118 |
-
"""
|
| 119 |
-
import numpy as np
|
| 120 |
-
|
| 121 |
-
sources = HF_ASR_SOURCES.get(lang, [])
|
| 122 |
-
for src in sources:
|
| 123 |
-
try:
|
| 124 |
-
from datasets import load_dataset, Audio as HFAudio
|
| 125 |
-
except ImportError:
|
| 126 |
-
continue
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
split=src["split"],
|
| 132 |
-
streaming=True,
|
| 133 |
-
token=hf_token,
|
| 134 |
-
trust_remote_code=False,
|
| 135 |
-
)
|
| 136 |
-
ds = ds.cast_column(src["audio_col"], HFAudio(sampling_rate=16_000))
|
| 137 |
-
total = src["max"]
|
| 138 |
-
for i, sample in enumerate(ds.take(total)):
|
| 139 |
-
if progress_cb:
|
| 140 |
-
progress_cb(i + 1, total, f"{src['repo']} ({lang})")
|
| 141 |
-
try:
|
| 142 |
-
audio_arr = np.array(sample[src["audio_col"]]["array"], dtype=np.float32)
|
| 143 |
-
text = (sample.get(src["text_col"]) or "").strip()
|
| 144 |
-
if not text or len(audio_arr) < 3_200: # skip < 0.2s
|
| 145 |
-
continue
|
| 146 |
-
# Convert to WAV bytes
|
| 147 |
-
wav_bytes = _numpy_to_wav_bytes(audio_arr, 16_000)
|
| 148 |
-
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
|
| 149 |
-
repo_path = f"audio/{lang}_{ts}.wav"
|
| 150 |
-
yield wav_bytes, text, repo_path
|
| 151 |
-
except Exception:
|
| 152 |
-
continue
|
| 153 |
-
except Exception:
|
| 154 |
-
continue
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
def _numpy_to_wav_bytes(audio: "np.ndarray", sr: int) -> bytes:
|
| 158 |
-
import struct, io as _io
|
| 159 |
-
audio_pcm = (audio * 32767).clip(-32768, 32767).astype("<i2")
|
| 160 |
-
data_bytes = audio_pcm.tobytes()
|
| 161 |
-
buf = _io.BytesIO()
|
| 162 |
-
# WAV header
|
| 163 |
-
buf.write(b"RIFF")
|
| 164 |
-
buf.write(struct.pack("<I", 36 + len(data_bytes)))
|
| 165 |
-
buf.write(b"WAVE")
|
| 166 |
-
buf.write(b"fmt ")
|
| 167 |
-
buf.write(struct.pack("<IHHIIHH", 16, 1, 1, sr, sr * 2, 2, 16))
|
| 168 |
-
buf.write(b"data")
|
| 169 |
-
buf.write(struct.pack("<I", len(data_bytes)))
|
| 170 |
-
buf.write(data_bytes)
|
| 171 |
-
return buf.getvalue()
|
|
|
|
| 5 |
Sources:
|
| 6 |
- RobotsMali/jeli-asr (HF, 33k Bambara audio+text samples)
|
| 7 |
- google/fleurs ff_sn (HF, Fula audio+text)
|
| 8 |
+
- bm.wikipedia.org / ff.wikipedia.org (Wikipedia API, text only -> vocabulary.jsonl)
|
| 9 |
|
| 10 |
+
HF audio datasets are registered by reference in dataset_sources.jsonl — the
|
| 11 |
+
Kaggle notebook loads them directly at training time. This avoids re-uploading
|
| 12 |
+
gigabytes of audio through the Space (which would timeout every time).
|
| 13 |
"""
|
| 14 |
from __future__ import annotations
|
| 15 |
|
| 16 |
import io
|
| 17 |
import json
|
| 18 |
import time
|
|
|
|
|
|
|
| 19 |
|
| 20 |
WIKI_APIS = {
|
| 21 |
"bam": "https://bm.wikipedia.org/w/api.php",
|
| 22 |
"ful": "https://ff.wikipedia.org/w/api.php",
|
| 23 |
}
|
| 24 |
|
| 25 |
+
# Wikipedia requires a descriptive User-Agent or returns 403
|
| 26 |
+
_UA = "SahelVoiceAI/1.0 (Bambara/Fula language research; huggingface.co/spaces/ous-sow/sahel-agri-voice)"
|
| 27 |
+
|
| 28 |
+
# Datasets registered by reference — Kaggle notebook loads them directly
|
| 29 |
+
HF_DATASET_REGISTRY = {
|
| 30 |
"bam": [
|
| 31 |
+
{
|
| 32 |
+
"repo": "RobotsMali/jeli-asr",
|
| 33 |
+
"config": "jeli-asr",
|
| 34 |
+
"split": "train",
|
| 35 |
+
"audio_col": "audio",
|
| 36 |
+
"text_col": "bam",
|
| 37 |
+
"max": 5_000,
|
| 38 |
+
"license": "cc-by-4.0",
|
| 39 |
+
},
|
| 40 |
],
|
| 41 |
"ful": [
|
| 42 |
+
{
|
| 43 |
+
"repo": "google/fleurs",
|
| 44 |
+
"config": "ff_sn",
|
| 45 |
+
"split": "train",
|
| 46 |
+
"audio_col": "audio",
|
| 47 |
+
"text_col": "transcription",
|
| 48 |
+
"max": 2_000,
|
| 49 |
+
"license": "cc-by-4.0",
|
| 50 |
+
},
|
| 51 |
],
|
| 52 |
}
|
| 53 |
|
|
|
|
| 56 |
|
| 57 |
def harvest_wikipedia_text(lang: str, max_articles: int = 100) -> list[dict]:
|
| 58 |
"""
|
| 59 |
+
Fetch up to max_articles article extracts from the language Wikipedia.
|
| 60 |
+
Returns list of {word, translation, language, source} dicts for vocabulary.jsonl.
|
| 61 |
"""
|
| 62 |
import urllib.request, urllib.parse
|
| 63 |
|
|
|
|
| 65 |
if not api_url:
|
| 66 |
return []
|
| 67 |
|
| 68 |
+
def _get(params: dict) -> dict:
|
| 69 |
+
url = f"{api_url}?{urllib.parse.urlencode(params)}"
|
| 70 |
+
req = urllib.request.Request(url, headers={"User-Agent": _UA})
|
| 71 |
+
with urllib.request.urlopen(req, timeout=20) as r:
|
| 72 |
+
return json.loads(r.read())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
# Step 1: get article titles
|
| 75 |
+
data = _get({"action": "query", "list": "allpages", "aplimit": max_articles,
|
| 76 |
+
"apfilterredir": "nonredirects", "format": "json"})
|
| 77 |
titles = [p["title"] for p in data.get("query", {}).get("allpages", [])]
|
| 78 |
if not titles:
|
| 79 |
return []
|
|
|
|
| 82 |
entries = []
|
| 83 |
for i in range(0, len(titles), 20):
|
| 84 |
batch = titles[i:i + 20]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
try:
|
| 86 |
+
data2 = _get({"action": "query", "titles": "|".join(batch),
|
| 87 |
+
"prop": "extracts", "exsentences": 3,
|
| 88 |
+
"exlimit": len(batch), "explaintext": True, "format": "json"})
|
| 89 |
for page in data2.get("query", {}).get("pages", {}).values():
|
| 90 |
extract = (page.get("extract") or "").strip()
|
| 91 |
title = page.get("title", "").strip()
|
| 92 |
if not extract or not title:
|
| 93 |
continue
|
|
|
|
| 94 |
for sentence in extract.replace("\n", " ").split("."):
|
| 95 |
sentence = sentence.strip()
|
| 96 |
words = sentence.split()
|
| 97 |
if 3 <= len(words) <= 20:
|
| 98 |
entries.append({
|
| 99 |
+
"word": sentence,
|
| 100 |
+
"translation": title,
|
| 101 |
+
"language": lang,
|
| 102 |
+
"source": "wikipedia",
|
| 103 |
})
|
| 104 |
except Exception:
|
| 105 |
pass
|
| 106 |
+
time.sleep(0.3)
|
| 107 |
|
| 108 |
return entries
|
| 109 |
|
| 110 |
|
| 111 |
+
# ── HF dataset registration (reference-based, no re-upload) ──────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
+
def get_hf_dataset_refs(lang: str) -> list[dict]:
|
| 114 |
+
"""Return the dataset reference dicts for this language."""
|
| 115 |
+
return HF_DATASET_REGISTRY.get(lang, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|