jefffffff9 Claude Sonnet 4.6 commited on
Commit
a5737ac
·
1 Parent(s): 520ca12

Fix Wikipedia and HF harvest buttons

Browse files

Wikipedia 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 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 _harvest_wikipedia(lang_label: str, max_articles: int = 100) -> str:
752
- """Fetch Wikipedia text for this language and append to vocabulary.jsonl."""
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="vocabulary.jsonl",
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
- new_lines = "".join(json.dumps(e, ensure_ascii=False) + "\n" for e in entries)
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="vocabulary.jsonl",
782
  repo_id=FEEDBACK_REPO_ID,
783
  repo_type="dataset",
784
  )
785
- break
786
  except Exception as e:
787
  if attempt == 1:
788
- return f"❌ Upload failed: {e}"
 
 
 
 
 
 
 
 
 
 
789
 
 
 
 
 
 
 
 
 
 
 
 
 
790
  return (
791
  f"✅ Wikipedia harvest complete!\n"
792
- f" Language : {lang_label}\n"
793
- f" Sentences added : {len(entries)}\n"
794
- f" Total vocabulary entries: {updated.count(chr(10))}"
795
  )
796
 
797
 
798
  def _harvest_hf_dataset(lang_label: str, max_samples: int = 500) -> str:
799
- """Pull audio+transcription from public HF datasets into corrections.jsonl."""
 
 
 
 
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 "⚠️ HF dataset harvest only supported for Bambara and Fula."
805
 
806
- from src.data.web_harvester import harvest_hf_audio, HF_ASR_SOURCES
807
- sources = HF_ASR_SOURCES.get(lang, [])
808
- if not sources:
809
  return f"⚠️ No HF dataset configured for {lang}."
810
 
811
- records = []
812
- errors = 0
813
- for wav_bytes, text, repo_path in harvest_hf_audio(lang, HF_TOKEN):
814
- try:
815
- _hf_api.upload_file(
816
- path_or_fileobj=io.BytesIO(wav_bytes),
817
- path_in_repo=repo_path,
818
- repo_id=FEEDBACK_REPO_ID,
819
- repo_type="dataset",
820
- )
821
- ts = repo_path.split("_")[-1].replace(".wav", "")
822
- records.append({
823
- "id": ts, "timestamp": datetime.now(timezone.utc).isoformat(),
824
- "language": lang,
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"✅ HF dataset harvest complete!\n"
870
- f" Source : {sources[0]['repo']}\n"
871
- f" Imported : {len(records)} samples ({errors} errors)\n"
872
- f" Dataset : {total} total corrections\n"
 
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": "# -- Cell 4: External dataset configuration -----------------------------------\n# Data source reality check (as of 2026):\n#\n# Bambara ASR:\n# - google/WaxalNLP -> no 'bam' subset exists\n# - Common Voice 'bm' -> moved to Mozilla Data Collective (not on HF)\n# - PRIMARY SOURCE -> user corrections in sahel-agri-feedback\n#\n# Fula ASR:\n# - google/WaxalNLP 'ful_asr' -> AVAILABLE (handled in Cell 8)\n# - Common Voice 'ff' -> moved to Mozilla Data Collective (not on HF)\n#\n# To add a new dataset later, add an entry with enabled=True.\n# Any HF dataset with an 'audio' column and a text column works.\n\nEXTERNAL_DATASETS = [\n # Example -- uncomment and set enabled=True when a Bambara HF dataset appears:\n # {\n # 'enabled' : False,\n # 'repo_id' : 'MALIBA-AI/bambara-asr', # check HF for availability\n # 'config' : None,\n # 'split' : 'train',\n # 'text_col' : 'transcription',\n # 'lang' : 'bam',\n # 'max_samples': 5_000,\n # },\n]\n\nactive = [d for d in EXTERNAL_DATASETS if d.get('enabled') and d.get('lang') == TRAIN_LANG]\nprint(f'External sources active for {TRAIN_LANG}: {len(active)}')\nif not active:\n if TRAIN_LANG == 'bam':\n print('Bambara: no external HF dataset available.')\n print(' Training will use user corrections from sahel-agri-feedback.')\n print(' Collect corrections via the Space to grow this dataset over time.')\n elif TRAIN_LANG == 'ful':\n print('Fula: WaxalNLP ful_asr loaded in Cell 8 -- no extra external source needed.')"
 
 
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 → vocabulary.jsonl)
9
 
10
- All writes go through the same corrections.jsonl / vocabulary.jsonl files
11
- that the Kaggle training notebook reads — no special handling needed.
 
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
- HF_ASR_SOURCES = {
 
 
 
 
27
  "bam": [
28
- {"repo": "RobotsMali/jeli-asr", "config": "jeli-asr", "split": "train",
29
- "audio_col": "audio", "text_col": "bam", "max": 5_000},
 
 
 
 
 
 
 
30
  ],
31
  "ful": [
32
- {"repo": "google/fleurs", "config": "ff_sn", "split": "train",
33
- "audio_col": "audio", "text_col": "transcription", "max": 2_000},
 
 
 
 
 
 
 
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's Wikipedia.
43
- Returns list of {word, translation, language} dicts suitable for vocabulary.jsonl.
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
- # Step 1: get a list of article titles
52
- params = urllib.parse.urlencode({
53
- "action": "query",
54
- "list": "allpages",
55
- "aplimit": max_articles,
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
- with urllib.request.urlopen(f"{api_url}?{params2}", timeout=15) as r:
81
- data2 = json.loads(r.read())
 
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": sentence,
94
- "translation": title, # use article title as loose context
95
- "language": lang,
96
- "source": "wikipedia",
97
  })
98
  except Exception:
99
  pass
100
- time.sleep(0.3) # be polite to Wikipedia servers
101
 
102
  return entries
103
 
104
 
105
- # ── HF dataset audio harvest ──────────────────────────────────────────────────
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
- try:
129
- ds = load_dataset(
130
- src["repo"], src["config"],
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, [])