File size: 7,258 Bytes
e4c97d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Build ASR fine-tuning manifests from licensed, public-safe Nigerian speech already on dynacc-h100.
-> /scratch/asr/data/{train.jsonl, dev.jsonl, realcs_test.jsonl, stats.json}
Each row: id, src, lang, spk, text (ASR target, normalized), dur, pcm (24 kHz int16 file), off24, n24, licence.
Excluded (never touched): African Voices / WaZoBia, NaijaVoices, AfriSpeech (NC), Flash-AV. Held out: WAXAL test+validation,
FLEURS dev/test/val, NaijaS2ST dev, IgboSynCorp speakers that produced natural code-switched clips (-> realcs test).
Target normalization (matches duplex asr_score.norm): NFC, lowercase, punctuation -> space (apostrophes kept);
Igbo: tone marks (acute/grave/macron) stripped, dot-below and n-dot kept (WAXAL/standard Igbo orthography);
Yoruba/Hausa: kept as written by the source."""
import json, os, re, unicodedata, random, collections
H = "/home/dev"; OUT = "/scratch/asr/data"; os.makedirs(OUT, exist_ok=True)
random.seed(11)
LIC = {"waxal": "CC-BY-SA-4.0 (WAXAL Media Trust subsets)", "fleurs": "CC-BY-4.0 (Google FLEURS)",
       "slr70": "CC-BY-SA-4.0 (OpenSLR 70, Nigerian English)", "slr86": "CC-BY-SA-4.0 (OpenSLR 86, Yoruba)",
       "bible_yor": "CC-BY-SA-4.0 (Open.Bible / Biblica)", "bible_hau": "CC-BY-SA-4.0 (Open.Bible / Biblica)",
       "open_bible_igbo": "CC-BY-SA-4.0 (Open.Bible / Biblica)", "igboapi": "CC-BY-4.0 (IgboAPI)",
       "naijas2st_igbo": "CC-BY-4.0 (NaijaS2ST)", "igbosyncorp": "CC0-1.0 (IgboSynCorp)"}
# per-source sampling multiplier per epoch (Bible + dictionary reads down-weighted, Pidgin up-weighted)
W = {"waxal:ibo": 1.0, "waxal:yor": 1.0, "waxal:hau": 1.0, "waxal:pcm": 3.0, "fleurs:eng": 1.0, "fleurs:hau": 1.0,
     "fleurs:ibo": 1.0, "fleurs:yor": 1.0, "slr70:eng": 0.6, "slr86:yor": 1.0, "bible_yor:yor": 0.35, "bible_hau:hau": 0.35,
     "open_bible_igbo:ibo": 0.15, "igboapi:ibo": 0.25, "naijas2st_igbo:ibo": 0.5, "igbosyncorp:ibo": 1.5}
TONE = {"\u0300", "\u0301", "\u0304", "\u0302", "\u030c"}

def norm(s, lang):
    s = re.sub(r"<[^>]+>", " ", s); s = re.sub(r"\[\d+\]", " ", s)
    s = unicodedata.normalize("NFC", s.lower())
    if lang == "ibo":
        s = unicodedata.normalize("NFC", "".join(c for c in unicodedata.normalize("NFD", s) if c not in TONE))
    s = s.replace("\u2019", "'").replace("`", "'")
    s = re.sub(r"[^\w\s']", " ", s); s = re.sub(r"(^|\s)'+|'+(\s|$)", " ", s)
    return re.sub(r"\s+", " ", s).strip()

def load(d):
    return [json.loads(l) for l in open(f"{d}/meta.jsonl")]

rows = []
for ds in ["waxal", "fleurs", "slr70", "slr86", "bible_yor", "bible_hau"]:
    d = f"{H}/data3/{ds}"
    for r in load(d):
        rows.append(dict(id=r["id"], src=ds, lang=r["lang"], spk=r.get("spk"), split=r.get("split"), raw=r["text"], dur=r["dur"],
                         pcm=f"{d}/pcm24k.i16", off24=r["off24"], n24=r["n24"]))
for ds in ["open_bible_igbo", "igboapi", "naijas2st_igbo", "igbosyncorp"]:
    d = f"{H}/data_p3/p3set/{ds}"
    for r in load(d):
        rows.append(dict(id=r["id"], src=ds, lang=r["lang"], spk=r.get("speaker"), split=r.get("split"), raw=r.get("text_plain") or r.get("text_human"),
                         dur=r.get("dur24") or r["dur"], pcm=f"{d}/pcm24k.i16", off24=r["off24"], n24=r["n24"]))
for r in rows:
    r["text"] = norm(r["raw"] or "", r["lang"]); r["licence"] = LIC[r["src"]]
print("all rows", len(rows), collections.Counter((r["src"], r["split"]) for r in rows))

# English lexicon (for detecting real English insertions in held-out Igbo/Yoruba/Hausa speech)
ec, nc = collections.Counter(), collections.Counter()
for r in rows:
    if r["split"] != "train": continue
    (ec if r["lang"] == "eng" else nc if r["lang"] in ("ibo", "yor", "hau") else collections.Counter()).update(r["text"].split())
ENG = {w for w, c in ec.items() if c >= 2 and len(w) >= 3 and w.isascii() and w.isalpha() and nc[w] <= max(1, c // 50)}
def eng_words(t):
    return [w for w in t.split() if w in ENG]

# real code-switch test: held-out ibo/yor/hau clips with >=1 English word (insertional CS, real speech) + IgboSynCorp natural CS
realcs, hold_spk = [], set()
for r in rows:
    if r["lang"] not in ("ibo", "yor", "hau") or not r["text"]: continue
    ew = eng_words(r["text"]); nw = len(r["text"].split())
    if r["src"] == "igbosyncorp" and len(ew) >= 2 and nw >= 3:
        hold_spk.add(r["spk"])
    if r["split"] in ("test", "dev") and r["src"] in ("waxal", "naijas2st_igbo") and ew and nw >= 4 and len(ew) < nw:
        realcs.append(dict(r, cs_eng=ew, cs_kind="insertional"))
for r in rows:
    if r["src"] == "igbosyncorp" and r["spk"] in hold_spk and eng_words(r["text"]) and len(r["text"].split()) >= 3:
        realcs.append(dict(r, cs_eng=eng_words(r["text"]), cs_kind="natural_conversational"))
print("realcs", len(realcs), collections.Counter((r["src"], r["lang"]) for r in realcs), "held-out igbosyncorp speakers", len(hold_spk))

# dev: WAXAL validation 40/lang + FLEURS eng dev/test 40 (FLEURS val = the duplex eval English set, untouched here)
dev = []
for L in ["ibo", "yor", "hau", "pcm"]:
    c = [r for r in rows if r["src"] == "waxal" and r["lang"] == L and r["split"] in ("validation", "val") and 1.5 < r["dur"] < 15]
    random.shuffle(c); dev += c[:40]
c = [r for r in rows if r["src"] == "slr70" and r["split"] != "train" and 1.5 < r["dur"] < 15]
if not c:
    sp = sorted({r["spk"] for r in rows if r["src"] == "slr70"}); hold70 = set(sp[:4])
    c = [r for r in rows if r["src"] == "slr70" and r["spk"] in hold70]
else:
    hold70 = set()
random.shuffle(c); dev += c[:40]

train, drop = [], collections.Counter()
for r in rows:
    k = f"{r['src']}:{r['lang']}"
    if r["split"] != "train": drop["not_train"] += 1; continue
    if r["src"] == "slr70" and r["spk"] in hold70: drop["slr70_dev_spk"] += 1; continue
    if r["src"] == "igbosyncorp" and r["spk"] in hold_spk: drop["realcs_spk"] += 1; continue
    if k not in W: drop["no_weight:" + k] += 1; continue
    if not (0.8 <= r["dur"] <= 20) or len(r["text"]) < 2: drop["dur_or_empty"] += 1; continue
    if len(r["text"]) > r["dur"] * 25: drop["too_many_chars"] += 1; continue
    r["w"] = W[k]; train.append(r)
print("dropped", drop)
def dump(name, R):
    with open(f"{OUT}/{name}.jsonl", "w") as f:
        for r in R:
            f.write(json.dumps({k: v for k, v in r.items() if k != "raw"} | {"ref_raw": r["raw"]}, ensure_ascii=False) + "\n")
dump("train", train); dump("dev", dev); dump("realcs_test", realcs)
st = collections.defaultdict(lambda: [0, 0.0, 0.0])
for r in train:
    s = st[f"{r['src']}:{r['lang']}"]; s[0] += 1; s[1] += r["dur"] / 3600; s[2] += r["dur"] * r["w"] / 3600
lang_h = collections.Counter()
for k, v in st.items(): lang_h[k.split(":")[1]] += v[2]
out = dict(sources={k: dict(n=v[0], hours=round(v[1], 2), hours_per_epoch=round(v[2], 2), licence=LIC[k.split(':')[0]]) for k, v in sorted(st.items())},
           lang_hours_per_epoch={k: round(v, 1) for k, v in lang_h.items()}, total_hours=round(sum(v[1] for v in st.values()), 1),
           epoch_hours=round(sum(v[2] for v in st.values()), 1), dev=len(dev), realcs=len(realcs), eng_lexicon=len(ENG))
json.dump(out, open(f"{OUT}/stats.json", "w"), indent=1); print(json.dumps(out, indent=1))
json.dump(sorted(ENG), open(f"{OUT}/eng_lexicon.json", "w"))
print("PREP_DONE")