Spaces:
Running on Zero
Running on Zero
| """ | |
| scripts/convert_probes.py | |
| Convert probe CSV/XLSX to canonical JSONL for the G-MASS pipeline. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import unicodedata | |
| from pathlib import Path | |
| from typing import Dict, List | |
| try: | |
| import pandas as pd | |
| except Exception: | |
| pd = None | |
| CANONICAL_FIELDS = [ | |
| "probe_id", | |
| "disease_domain", | |
| "failure_category", | |
| "english_prompt", | |
| "twi_prompt", | |
| "prompt_twi_validated", | |
| "ghanaian_en_prompt", | |
| "translation_status", | |
| ] | |
| COMMON_COLUMN_MAP = { | |
| "id": "probe_id", | |
| "probe": "probe_id", | |
| "prompt": "english_prompt", | |
| "english_prompt": "english_prompt", | |
| "english prompt": "english_prompt", | |
| "twi_prompt": "twi_prompt", | |
| "twi prompt": "twi_prompt", | |
| "prompt_twi_validated": "prompt_twi_validated", | |
| "final_approved_twi": "twi_prompt", | |
| "ghanaian_en_prompt": "ghanaian_en_prompt", | |
| "final_approved_ghanaian_english": "ghanaian_en_prompt", | |
| "failure_category": "failure_category", | |
| "disease_domain": "disease_domain", | |
| } | |
| def normalize_text(s: object) -> str: | |
| if s is None: | |
| return "" | |
| if not isinstance(s, str): | |
| s = str(s) | |
| s = s.replace("\x00", "") | |
| s = s.strip() | |
| s = unicodedata.normalize("NFKC", s) | |
| return s | |
| def map_columns(columns: List[str]) -> Dict[str, str]: | |
| col_map: Dict[str, str] = {} | |
| for col in columns: | |
| low = str(col).lower() | |
| if low in COMMON_COLUMN_MAP: | |
| col_map[col] = COMMON_COLUMN_MAP[low] | |
| continue | |
| if "probe" in low and "id" in low: | |
| col_map[col] = "probe_id" | |
| elif "english" in low and "prompt" in low: | |
| col_map[col] = "english_prompt" | |
| elif "twi" in low and "prompt" in low: | |
| col_map[col] = "twi_prompt" | |
| elif "ghana" in low or "ghanaian" in low: | |
| col_map[col] = "ghanaian_en_prompt" | |
| elif "domain" in low: | |
| col_map[col] = "disease_domain" | |
| elif "failure" in low or "category" in low: | |
| col_map[col] = "failure_category" | |
| return col_map | |
| def canonicalize_row(row: Dict[str, object], col_map: Dict[str, str]) -> Dict[str, object]: | |
| out: Dict[str, object] = {} | |
| for src_col, value in row.items(): | |
| if src_col in col_map: | |
| out[col_map[src_col]] = normalize_text(value) | |
| if not out.get("twi_prompt") and out.get("prompt_twi_validated"): | |
| out["twi_prompt"] = out.get("prompt_twi_validated") | |
| return out | |
| def dataframe_to_jsonl(df, out_path: Path, strict: bool = False) -> int: | |
| required = ["probe_id", "english_prompt"] | |
| col_map = map_columns(list(df.columns)) | |
| written = 0 | |
| with out_path.open("w", encoding="utf-8") as out: | |
| for i, row in df.iterrows(): | |
| row_dict = {col: row[col] for col in df.columns} | |
| rec = canonicalize_row(row_dict, col_map) | |
| missing = [r for r in required if not rec.get(r)] | |
| if missing: | |
| msg = f"Row {i+1} missing required fields: {missing}" | |
| if strict: | |
| raise ValueError(msg) | |
| else: | |
| print("WARNING:", msg) | |
| continue | |
| out_rec = {k: rec.get(k) for k in CANONICAL_FIELDS if rec.get(k) is not None} | |
| out.write(json.dumps(out_rec, ensure_ascii=False) + "\n") | |
| written += 1 | |
| return written | |
| def load_input(path: Path, sheet: str | None = None): | |
| suffix = path.suffix.lower() | |
| if suffix in (".csv", ".tsv"): | |
| if pd is None: | |
| raise EnvironmentError("pandas is required to read CSV/TSV files") | |
| sep = "," if suffix == ".csv" else "\t" | |
| return pd.read_csv(path, sep=sep, dtype=str, keep_default_na=False) | |
| if suffix in (".xlsx", ".xls"): | |
| if pd is None: | |
| raise EnvironmentError("pandas is required to read Excel files") | |
| return pd.read_excel(path, sheet_name=sheet or 0, dtype=str) | |
| if suffix in (".jsonl", ".ndjson"): | |
| records = [] | |
| with path.open("r", encoding="utf-8") as f: | |
| for line in f: | |
| if not line.strip(): | |
| continue | |
| records.append(json.loads(line)) | |
| import pandas as _pd | |
| return _pd.DataFrame(records) | |
| raise ValueError(f"Unsupported input file type: {path.suffix}") | |
| def main(argv=None) -> int: | |
| p = argparse.ArgumentParser(description="Convert probe CSV/XLSX to canonical JSONL") | |
| p.add_argument("input", help="Input file (csv, xlsx, jsonl)") | |
| p.add_argument("--out", required=True, help="Output JSONL path") | |
| p.add_argument("--sheet", default=None, help="Excel sheet name or index") | |
| p.add_argument("--strict", action="store_true", help="Fail on first missing required field") | |
| args = p.parse_args(argv) | |
| in_path = Path(args.input) | |
| out_path = Path(args.out) | |
| if not in_path.exists(): | |
| print(f"Input file not found: {in_path}") | |
| return 2 | |
| if pd is None: | |
| print("ERROR: pandas is required for convert_probes.py. Install with: pip install pandas openpyxl") | |
| return 3 | |
| df = load_input(in_path, args.sheet) | |
| df.columns = [str(c).strip() for c in df.columns] | |
| written = dataframe_to_jsonl(df, out_path, strict=args.strict) | |
| print(f"Wrote {written} records to {out_path}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |