File size: 7,314 Bytes
e55c8a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env python
"""
Add newly extracted subject files to the MCQ dataset, safely.

Why a script instead of `cat a b > c`: on Windows PowerShell the `>` redirect re-encodes the text and
corrupts Sinhala. This reads and writes UTF-8 explicitly and validates every line.

It figures out each input file's format on its own:
  * raw extraction format  (original_question / answer_1..5 / correct_answer)  -> runs mcq_dataset
    convert on it (same cleaning, junk-stripping and filters as the original pipeline)
  * MCQ schema             (question / choices / answer)                        -> passed through
It then merges everything into one converted file, ready for clean -> dedupe -> split -> build-sft.

    python add_new_data.py --base data/converted.jsonl \
        --new data/art.jsonl data/buddhim.jsonl data/dancing.jsonl data/sinhala.jsonl \
        --out data/all_converted.jsonl

Prints a per-file report (rows kept, subjects, exam types/grades) so you can see what you actually
added before spending GPU time. Put mcq_dataset.py in the same folder.
"""
import argparse
import collections
import importlib.util
import json
import os
import sys
from argparse import Namespace

HERE = os.path.dirname(os.path.abspath(__file__))


def load_mcq_module():
    path = os.path.join(HERE, "mcq_dataset.py")
    if not os.path.exists(path):
        sys.exit(f"mcq_dataset.py not found next to this script ({HERE})")
    spec = importlib.util.spec_from_file_location("mcq_dataset", path)
    m = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(m)
    return m


def read_jsonl(path):
    rows, bad = [], 0
    with open(path, encoding="utf-8-sig") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                rows.append(json.loads(line))
            except json.JSONDecodeError:
                bad += 1
    if bad:
        print(f"  [warn] {bad} unparseable line(s) skipped in {os.path.basename(path)}")
    return rows


def detect_format(rows):
    if not rows:
        return "empty"
    r = rows[0]
    if "choices" in r and "answer" in r and "question" in r:
        return "mcq"
    if "original_question" in r or "answer_1" in r or "correct_answer" in r:
        return "raw"
    return "unknown"


def describe(rows, label):
    subj = collections.Counter(r.get("subject") or "?" for r in rows)
    lvl = collections.Counter()
    for r in rows:
        md = r.get("metadata") or {}
        lvl[str(md.get("exam_type") or r.get("level") or "?") + "/" +
            str(md.get("grade") or r.get("grade") or "?")] += 1
    nch = collections.Counter(len(r.get("choices") or []) for r in rows)
    print(f"  {label}: {len(rows)} rows")
    print(f"    subjects : {dict(subj.most_common(8))}")
    print(f"    level/grade: {dict(lvl.most_common(8))}")
    print(f"    n_choices: {dict(sorted(nch.items()))}")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--base", default="data/converted.jsonl",
                    help="existing converted file to append to ('' = start fresh)")
    ap.add_argument("--new", nargs="+", required=True, help="newly extracted subject files")
    ap.add_argument("--out", required=True)
    ap.add_argument("--tmp_dir", default="", help="where to write per-file converted output "
                                                  "(default: alongside each input)")
    ap.add_argument("--keep_latin", action="store_true")
    ap.add_argument("--latin_allow", nargs="*", default=[])
    ap.add_argument("--min_confidence", choices=["low", "medium", "high"], default="low")
    args = ap.parse_args()
    for stream in (sys.stdout, sys.stderr):
        try:
            stream.reconfigure(encoding="utf-8")
        except Exception:                                                # noqa: BLE001
            pass

    mcq = load_mcq_module()
    merged, seen_ids = [], set()

    if args.base:
        if not os.path.exists(args.base):
            sys.exit(f"--base {args.base} not found (pass --base '' to start fresh)")
        base_rows = read_jsonl(args.base)
        if detect_format(base_rows) != "mcq":
            sys.exit(f"--base {args.base} is not in MCQ schema; pass the CONVERTED file")
        print(f"[base] {args.base}")
        describe(base_rows, "existing")
        merged.extend(base_rows)

    for path in args.new:
        if not os.path.exists(path):
            print(f"[skip] {path} not found")
            continue
        rows = read_jsonl(path)
        fmt = detect_format(rows)
        print(f"\n[new] {os.path.basename(path)}  format={fmt}")
        if fmt == "raw":
            out_dir = args.tmp_dir or os.path.dirname(path) or "."
            os.makedirs(out_dir, exist_ok=True)
            stem = os.path.splitext(os.path.basename(path))[0]
            conv = os.path.join(out_dir, f"{stem}_conv.jsonl")
            rej = os.path.join(out_dir, f"{stem}_latin_rejects.jsonl")
            mcq.cmd_convert(Namespace(inp=path, out=conv, min_confidence=args.min_confidence,
                                      skip_needs_review=False, keep_shared_stem=False,
                                      keep_latin=args.keep_latin, latin_allow=args.latin_allow,
                                      rejects=rej))
            rows = read_jsonl(conv)
        elif fmt == "mcq":
            print("  already in MCQ schema: passed through without conversion")
        else:
            print(f"  [skip] unrecognised format; first keys: {sorted(rows[0])[:8] if rows else '-'}")
            continue
        describe(rows, "added")
        merged.extend(rows)

    # ids must be unique, or later stages (and any per-question analysis) get confused
    out_rows, dup = [], 0
    for i, r in enumerate(merged):
        rid = r.get("id")
        if rid is None or rid in seen_ids:
            dup += 1
            rid = f"{rid or 'row'}__{i}"
        seen_ids.add(rid)
        r["id"] = rid
        out_rows.append(r)
    if dup:
        print(f"\n[ids] {dup} missing/duplicate id(s) were made unique")

    os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
    with open(args.out, "w", encoding="utf-8") as f:
        for r in out_rows:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

    print(f"\n[out] {len(out_rows)} rows -> {args.out}")
    describe(out_rows, "combined")
    print("\nNext (run from the same folder):")
    print(f"  python mcq_dataset.py validate {args.out}")
    print(f"  python mcq_dataset.py clean {args.out} data/all_cleaned.jsonl --rejects data/all_clean_rejects.jsonl")
    print("  python mcq_dataset.py dedupe data/all_cleaned.jsonl data/all_deduped.jsonl "
          "--blocklist test_easy_block.jsonl --conflicts data/all_conflicts.jsonl")
    print("  python mcq_dataset.py stats data/all_deduped.jsonl")
    print("  python mcq_dataset.py split data/all_deduped.jsonl --train data/train2.jsonl "
          "--val data/val2.jsonl --val_frac 0.1")
    print("  python mcq_dataset.py build-sft data/train2.jsonl data/sft_train2.jsonl --shuffle_copies 2")
    print("  python mcq_dataset.py build-sft data/val2.jsonl data/sft_val2.jsonl "
          "--shuffle_copies 0 --strip_zwj_prob 0 --templates si_instruction")


if __name__ == "__main__":
    main()