|
|
| """
|
| Shared helpers for reading MSMARCO-XI. One place, so every script agrees.
|
|
|
| THE ACTUAL PASSAGES LAYOUT (confirmed from the real file)
|
| --------------------------------------------------------
|
| passages: Struct({
|
| 'English_passages' : List(String),
|
| 'Translated_passages': List(String),
|
| 'is_selected' : List(Int64),
|
| })
|
|
|
| This is a STRUCT OF PARALLEL LISTS, not a list of structs. One row holds one
|
| dict whose three values are equal-length lists, aligned by position:
|
|
|
| row["English_passages"][i] <-> row["Translated_passages"][i]
|
| <-> row["is_selected"][i]
|
|
|
| That index `i` is exactly the `passage_index` half of the canonical ID, so the
|
| same passage keeps the same identity in every language file. There is NO url
|
| field, so pseudo-document grouping falls back to query_id.
|
|
|
| `iter_passages` normalises both layouts so callers never branch on it.
|
| """
|
| from __future__ import annotations
|
|
|
| import os
|
| from pathlib import Path
|
| from typing import Iterator
|
|
|
|
|
| ISO3_TO_ISO2 = {
|
| "asm": "as", "ben": "bn", "guj": "gu", "hin": "hi", "kan": "kn",
|
| "mal": "ml", "mar": "mr",
|
| "nep": "ne", "npi": "ne",
|
| "ory": "or", "ori": "or",
|
| "pan": "pa", "san": "sa", "tam": "ta", "tel": "te", "urd": "ur",
|
| "eng": "en",
|
| }
|
|
|
| LANG_NAMES = {
|
| "as": "Assamese", "bn": "Bengali", "gu": "Gujarati", "hi": "Hindi",
|
| "kn": "Kannada", "ml": "Malayalam", "mr": "Marathi", "ne": "Nepali",
|
| "or": "Odia", "pa": "Punjabi", "sa": "Sanskrit", "ta": "Tamil",
|
| "te": "Telugu", "ur": "Urdu", "en": "English",
|
| }
|
|
|
|
|
| def norm_lang(raw) -> str:
|
| """'asm_Beng' -> 'as'. Unknown 2-letter codes pass through."""
|
| if not raw:
|
| return "en"
|
| code = str(raw).split("_")[0].lower()
|
| return ISO3_TO_ISO2.get(code, code if len(code) == 2 else code)
|
|
|
|
|
| def iter_passages(row, t_key: str, en_key: str | None,
|
| sel_key: str | None, url_key: str | None
|
| ) -> Iterator[tuple[int, str | None, str | None, int, str | None]]:
|
| """Yield (index, text, text_en, is_selected, url) for either layout.
|
|
|
| Layout A - struct of parallel lists (MSMARCO-XI):
|
| {"Translated_passages": [...], "English_passages": [...], "is_selected": [...]}
|
| Layout B - list of structs (the common alternative):
|
| [{"passage_text": ..., "is_selected": ...}, ...]
|
| """
|
| def at(seq, i):
|
| return seq[i] if isinstance(seq, (list, tuple)) and i < len(seq) else None
|
|
|
| if isinstance(row, dict):
|
| texts = row.get(t_key)
|
| if isinstance(texts, (list, tuple)):
|
| ens = row.get(en_key) if en_key else None
|
| sels = row.get(sel_key) if sel_key else None
|
| urls = row.get(url_key) if url_key else None
|
| for i, txt in enumerate(texts):
|
| yield (i, txt, at(ens, i), int(at(sels, i) or 0), at(urls, i))
|
| else:
|
| yield (0, texts,
|
| row.get(en_key) if en_key else None,
|
| int(row.get(sel_key) or 0) if sel_key else 0,
|
| row.get(url_key) if url_key else None)
|
|
|
| elif isinstance(row, (list, tuple)):
|
| for i, item in enumerate(row):
|
| if isinstance(item, dict):
|
| yield (i, item.get(t_key),
|
| item.get(en_key) if en_key else None,
|
| int(item.get(sel_key) or 0) if sel_key else 0,
|
| item.get(url_key) if url_key else None)
|
|
|
|
|
| def detect_layout(row) -> str:
|
| if isinstance(row, dict):
|
| if any(isinstance(v, (list, tuple)) for v in row.values()):
|
| return "struct_of_lists"
|
| return "single_struct"
|
| if isinstance(row, (list, tuple)) and row and isinstance(row[0], dict):
|
| return "list_of_structs"
|
| return "unknown"
|
|
|
|
|
| def default_root() -> Path:
|
| """Data root, resolved without relying on the environment.
|
|
|
| Order: $VOICERAG_ROOT -> <repo>/../voicerag_data
|
|
|
| The second form is derived from this file's location, so it is correct on
|
| jupyter-pod (/workspace/carbine/anurag) and kls-headnode
|
| (/home/dgx-i-carbine/carbine/anurag) alike, and in a fresh shell where
|
| nothing has been exported.
|
| """
|
| env = os.environ.get("VOICERAG_ROOT")
|
| if env:
|
| return Path(env).expanduser().resolve()
|
| return (Path(__file__).resolve().parents[2] / "voicerag_data").resolve()
|
|
|
|
|
| def resolve_files(report: dict, root: Path) -> tuple[list[Path], list[str]]:
|
| """Map file paths recorded in schema_report.json onto the CURRENT data root.
|
|
|
| The same NFS export is mounted at different points on the two hosts:
|
| jupyter-pod /workspace/carbine/anurag/voicerag_data
|
| kls-headnode /home/dgx-i-carbine/carbine/anurag/voicerag_data
|
|
|
| A report written on one host therefore holds paths that do not exist on the
|
| other. Rather than force a re-run of schema inspection, we rebase: try the
|
| recorded path, then root-relative, then splice the path from its `hf_cache`
|
| segment onto the current root.
|
|
|
| Returns (existing_paths, unresolved_strings).
|
| """
|
| found: list[Path] = []
|
| missing: list[str] = []
|
| for f in report.get("files", []):
|
| p = Path(f)
|
| if p.exists():
|
| found.append(p)
|
| continue
|
| cand = root / f
|
| if cand.exists():
|
| found.append(cand)
|
| continue
|
| parts = p.parts
|
| if "hf_cache" in parts:
|
| cand = root.joinpath(*parts[parts.index("hf_cache"):])
|
| if cand.exists():
|
| found.append(cand)
|
| continue
|
| missing.append(f)
|
| return found, missing
|
|
|
|
|
| def load_report(root: Path) -> dict:
|
| """Read schema_report.json and rebase its file list onto `root`."""
|
| import json
|
| rp = root / "data" / "schema_report.json"
|
| if not rp.exists():
|
| raise SystemExit(
|
| f"\nMissing prerequisite: {rp}\n\n"
|
| "Run first: python scripts/02_inspect_schema.py --root " + str(root) + "\n"
|
| )
|
| rep = json.loads(rp.read_text())
|
| files, missing = resolve_files(rep, root)
|
| if not files:
|
| raise SystemExit(
|
| f"\nNone of the {len(rep.get('files', []))} parquet paths in {rp.name} exist "
|
| f"under {root}.\n"
|
| f" first recorded path: {rep.get('files', ['<none>'])[0]}\n\n"
|
| "The report was probably written on the other host. Re-run:\n"
|
| f" python scripts/02_inspect_schema.py --root {root}\n"
|
| )
|
| if missing:
|
| print(f" note: {len(missing)} of {len(rep['files'])} recorded paths "
|
| f"could not be resolved (using {len(files)})")
|
| rep["files"] = [str(f) for f in files]
|
| return rep
|
|
|
| def pick_device(explicit: str | None = None) -> str:
|
| """Which torch device to use. $VOICERAG_DEVICE wins.
|
|
|
| It has to win, because on a ZeroGPU Space autodetection is actively wrong.
|
| Free-tier Gradio Spaces are ZeroGPU-only, and OUTSIDE a @spaces.GPU function
|
| ZeroGPU enables a CUDA *emulation* so models can be placed on 'cuda' during
|
| startup. torch.cuda.is_available() therefore returns True while no real GPU
|
| is attached, and autodetect would put bge-m3 on a device that cannot run a
|
| forward pass.
|
|
|
| app.py declares one @spaces.GPU function -- ZeroGPU refuses to start a Space
|
| without one -- and never calls it. Quota is billed per call, so the Space
|
| consumes none of the free tier's 5-minute daily allowance and can serve
|
| unlimited requests, instead of going dark when a judge exhausts it.
|
| """
|
| import os
|
|
|
| if explicit:
|
| return explicit
|
| env = os.environ.get("VOICERAG_DEVICE", "").strip().lower()
|
| if env:
|
| return env
|
| import torch
|
|
|
| return "cuda" if torch.cuda.is_available() else "cpu"
|
|
|