#!/usr/bin/env python """Ingest and clean the MTSamples CSV into structured JSONL.""" from __future__ import annotations import argparse import csv import json import os import re from collections import Counter from datetime import datetime, timezone from typing import Dict, Iterable, List, Tuple SECTION_RE = re.compile(r"([A-Z][A-Z0-9 /&\-]{1,}):") def normalize_whitespace(text: str) -> str: text = text.replace("\u0000", "").replace("\ufffd", "") text = text.replace("\r\n", "\n").replace("\r", "\n") text = re.sub(r"\s+", " ", text) return text.strip() def clean_section_content(text: str) -> str: text = normalize_whitespace(text) text = re.sub(r"([.!?])\s*,", r"\1", text) text = re.sub(r"[ ,;:]+$", "", text) return text def split_sections(text: str) -> List[Tuple[str, str]]: matches = list(SECTION_RE.finditer(text)) if not matches: cleaned = clean_section_content(text) return [("FULL_TEXT", cleaned)] if cleaned else [] sections: List[Tuple[str, str]] = [] for idx, match in enumerate(matches): start = match.end() end = matches[idx + 1].start() if idx + 1 < len(matches) else len(text) heading = match.group(1).strip() content = text[start:end] content = content.lstrip(" ,;:-\n\t") content = clean_section_content(content) if content: sections.append((heading, content)) return sections def build_doc_id(row: Dict[str, str], fallback_index: int) -> str: raw_id = (row.get("") or "").strip() if not raw_id: raw_id = str(fallback_index) return f"mtsamples_{raw_id}" def iter_mtsamples(path: str) -> Iterable[Dict[str, str]]: with open(path, newline="", encoding="utf-8") as handle: reader = csv.DictReader(handle) for row in reader: yield row def ingest(input_path: str, output_dir: str) -> Dict[str, object]: os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, "cleaned_mtsamples.jsonl") section_counts = Counter() doc_count = 0 section_count = 0 empty_section_count = 0 with open(output_path, "w", encoding="utf-8") as out_handle: for idx, row in enumerate(iter_mtsamples(input_path)): doc_id = build_doc_id(row, idx) transcription = (row.get("transcription") or "").strip() sections = split_sections(transcription) if not sections and transcription: empty_section_count += 1 for section_idx, (section, content) in enumerate(sections): record = { "doc_id": doc_id, "page": None, "section": section, "section_index": section_idx, "content": content, "source": { "filename": os.path.basename(input_path), "row_id": row.get("") or str(idx), "sample_name": (row.get("sample_name") or "").strip(), "medical_specialty": (row.get("medical_specialty") or "").strip(), "description": (row.get("description") or "").strip(), "keywords": (row.get("keywords") or "").strip(), }, } out_handle.write(json.dumps(record, ensure_ascii=True) + "\n") section_counts[section] += 1 section_count += 1 doc_count += 1 stats = { "input_file": os.path.abspath(input_path), "output_file": os.path.abspath(output_path), "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), "document_count": doc_count, "section_count": section_count, "empty_section_count": empty_section_count, "top_sections": section_counts.most_common(10), "notes": [ "Page numbers are not present in the MTSamples CSV; page is emitted as null.", ], } stats_path = os.path.join(output_dir, "ingestion_stats.json") with open(stats_path, "w", encoding="utf-8") as stats_handle: json.dump(stats, stats_handle, indent=2, ensure_ascii=True) return stats def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Ingest and clean MTSamples CSV.") parser.add_argument( "--input", default=os.path.join("00_Data", "mtsamples.csv"), help="Path to mtsamples.csv", ) parser.add_argument( "--output-dir", default=os.path.join("01_data_ingestion", "output"), help="Output directory for JSONL and stats.", ) return parser.parse_args() def main() -> None: args = parse_args() ingest(args.input, args.output_dir) if __name__ == "__main__": main()