Instructions to use kesha-humonen/runic-reader-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use kesha-humonen/runic-reader-2 with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Download code/build_corpus.py from kesha-humonen/runic-reader-2: direct link, hf CLI and curl.
- Browser
- Download file 7.64 kB
-
https://huggingface.co/kesha-humonen/runic-reader-2/resolve/main/code/build_corpus.py
- Command line
-
hf download hf://kesha-humonen/runic-reader-2/code/build_corpus.py
-
curl -L -o build_corpus.py https://huggingface.co/kesha-humonen/runic-reader-2/resolve/main/code/build_corpus.py
7.64 kB
| """Build the inscription-level corpus and leakage-free splits. | |
| Sources (all scraped by the thesis authors): | |
| SRD 2014 export (signum, transliteration, English) -> target en | |
| RunesDB / RuneS (transliteration, translation, image URLs) -> target en (langid-filtered) | |
| christerhamp.se Gamla runor (Anton's clean scrape, Swedish) -> target sv | |
| Rows are grouped into inscription clusters by (a) identical normalised transliteration, | |
| (b) identical signum, (c) near-duplicate normalised transliteration (Indel ratio >= 90 for | |
| strings of >= 20 chars) and (d) word-trigram containment >= 0.8 of the shorter string. | |
| Splits are assigned per cluster, so no inscription appears in two splits. | |
| """ | |
| import json | |
| import random | |
| import re | |
| import sys | |
| from collections import Counter, defaultdict | |
| import langid | |
| import numpy as np | |
| import pandas as pd | |
| from rapidfuzz import fuzz, process | |
| sys.path.insert(0, "/Users/humonen/runic_access/code") | |
| from runic_norm import normalize # noqa: E402 | |
| P = "/Users/humonen/runic_access/sources/repos/runic_ai/parsers/" | |
| CH = "/Users/humonen/runic_access/sources/gdrive/Christerhamp/christerhamp_images/christerhamp_gamla_runor_with_filenames.csv" | |
| OUT = "/Users/humonen/runic_access/data/" | |
| SEED = 13 | |
| def base_signum(s): | |
| if not isinstance(s, str): | |
| return None | |
| s = re.split(r"[$§]", s)[0] | |
| s = re.sub(r"\s+", " ", s).strip() | |
| return s or None | |
| def clean_translation(t): | |
| if not isinstance(t, str): | |
| return "" | |
| t = re.sub(r"\s+", " ", t).strip() | |
| return t | |
| rows = [] | |
| srd = pd.read_csv(P + "srd_parallel.csv") | |
| for r in srd.itertuples(): | |
| rows.append(dict(src="srd", signum=base_signum(r.signum), translit_raw=r.transliteration, | |
| translation=clean_translation(r.english), lang="en", image_urls="", page="")) | |
| rdb = pd.read_csv(P + "runes_parsed_parallel_1_10k.csv") | |
| for r in rdb.itertuples(): | |
| rows.append(dict(src="rdb", signum=None, translit_raw=r.transliteration, | |
| translation=clean_translation(r.translation), lang="en", | |
| image_urls=r.images_urls if isinstance(r.images_urls, str) else "", page=r.url)) | |
| ch = pd.read_csv(CH) | |
| for r in ch.itertuples(): | |
| sig = r.stone_id if isinstance(r.stone_id, str) and re.match(r"^[A-ZÅÄÖÖÜa-zåäö]{1,4}\s", r.stone_id) else None | |
| rows.append(dict(src="ch", signum=base_signum(sig), translit_raw=r.transliteration, | |
| translation=clean_translation(r.translation), lang="sv", | |
| image_urls=r.image_url if isinstance(r.image_url, str) else "", page=r.page_url)) | |
| df = pd.DataFrame(rows) | |
| df["norm"] = df.translit_raw.map(normalize) | |
| n0 = len(df) | |
| # translation must carry content | |
| alpha = df.translation.map(lambda t: len(re.findall(r"[A-Za-zÀ-ÿþðÞÐǫ]", t))) | |
| df = df[(df.norm.str.len() >= 2) & (alpha >= 3)].copy() | |
| # RunesDB: keep English (or short name-like glosses, which langid cannot judge) | |
| def rdb_keep(t): | |
| words = t.split() | |
| if len(words) <= 3: | |
| return True | |
| return langid.classify(t)[0] == "en" | |
| mask_rdb = df.src.eq("rdb") | |
| keep = df[mask_rdb].translation.map(rdb_keep) | |
| df = pd.concat([df[~mask_rdb], df[mask_rdb][keep]]) | |
| df = df.reset_index(drop=True) | |
| print(f"rows: raw {n0} -> kept {len(df)}", df.groupby(["src", "lang"]).size().to_dict()) | |
| # ---------- clustering ---------- | |
| parent = list(range(len(df))) | |
| def find(i): | |
| while parent[i] != i: | |
| parent[i] = parent[parent[i]] | |
| i = parent[i] | |
| return i | |
| def union(i, j): | |
| a, b = find(i), find(j) | |
| if a != b: | |
| parent[b] = a | |
| def union_groups(keys): | |
| first = {} | |
| for i, k in enumerate(keys): | |
| if k is None or k == "": | |
| continue | |
| if k in first: | |
| union(first[k], i) | |
| else: | |
| first[k] = i | |
| df["nospace"] = df.norm.str.replace(" ", "", regex=False) | |
| union_groups(df.nospace.tolist()) | |
| union_groups(df.signum.tolist()) | |
| uniq_ns = sorted(set(df.nospace)) | |
| idx_ns = defaultdict(list) | |
| for i, s in enumerate(df.nospace): | |
| idx_ns[s].append(i) | |
| long_ = [s for s in uniq_ns if len(s) >= 8] | |
| M = process.cdist(long_, long_, scorer=fuzz.ratio, score_cutoff=90, workers=-1, dtype=np.uint8) | |
| ii, jj = np.nonzero(M) | |
| near_pairs = [(long_[a], long_[b], int(M[a, b])) for a, b in zip(ii, jj) if a < b] | |
| for a, b, _ in near_pairs: | |
| union(idx_ns[a][0], idx_ns[b][0]) | |
| uniq = sorted(set(df.norm)) | |
| idx_of = defaultdict(list) | |
| for i, s in enumerate(df.norm): | |
| idx_of[s].append(i) | |
| print("near-dup pairs (ratio>=90 on space-free strings >= 8 chars):", len(near_pairs)) | |
| def trigrams(s): | |
| w = s.split() | |
| return {" ".join(w[k:k + 3]) for k in range(len(w) - 2)} | |
| tg = {s: trigrams(s) for s in uniq} | |
| inv = defaultdict(set) | |
| for s, g in tg.items(): | |
| for t in g: | |
| inv[t].add(s) | |
| contain_pairs = 0 | |
| for s, g in tg.items(): | |
| if len(g) < 3: | |
| continue | |
| cand = Counter() | |
| for t in g: | |
| if len(inv[t]) > 200: # formula trigrams shared by hundreds of stones carry no identity | |
| continue | |
| for o in inv[t]: | |
| if o != s: | |
| cand[o] += 1 | |
| for o, c in cand.items(): | |
| if len(tg[o]) >= len(g) and c / len(g) >= 0.8: | |
| union(idx_of[s][0], idx_of[o][0]) | |
| contain_pairs += 1 | |
| print("containment pairs:", contain_pairs) | |
| df["cluster"] = [find(i) for i in range(len(df))] | |
| sizes = df.cluster.value_counts() | |
| print("clusters:", len(sizes), "largest:", sizes.head(5).tolist(), "singletons:", int((sizes == 1).sum())) | |
| # save boundary examples for manual inspection | |
| random.seed(SEED) | |
| border = [p for p in near_pairs if p[2] < 93] | |
| with open(OUT + "inspect_near_dup_pairs.txt", "w") as f: | |
| for a, b, sc in random.sample(border, min(40, len(border))): | |
| f.write(f"{sc}\n {a}\n {b}\n") | |
| big = sizes.index[0] | |
| with open(OUT + "inspect_largest_cluster.txt", "w") as f: | |
| for s in df[df.cluster == big].norm.head(60): | |
| f.write(s + "\n") | |
| # ---------- split ---------- | |
| has_img = df.image_urls.str.len() > 0 | |
| cl_img = df[has_img].groupby("cluster").src.apply(set) | |
| clusters = sorted(sizes.index) | |
| rng = random.Random(SEED) | |
| rng.shuffle(clusters) | |
| # cap the test/val clusters at size <= 8 so a single formulaic mega-cluster cannot dominate | |
| eligible = [c for c in clusters if sizes[c] <= 8] | |
| img_ch = [c for c in eligible if c in cl_img.index and "ch" in cl_img[c]] | |
| img_rdb = [c for c in eligible if c in cl_img.index and "rdb" in cl_img[c] and "ch" not in cl_img[c]] | |
| test = set(img_ch[:220]) | set(img_rdb[:220]) | |
| rest = [c for c in eligible if c not in test] | |
| target_test_rows = 1300 | |
| for c in rest: | |
| if df.cluster.isin(test).sum() >= target_test_rows: | |
| break | |
| test.add(c) | |
| val = set() | |
| for c in [c for c in eligible if c not in test]: | |
| if df.cluster.isin(val).sum() >= 600: | |
| break | |
| val.add(c) | |
| df["split"] = np.where(df.cluster.isin(test), "test", np.where(df.cluster.isin(val), "val", "train")) | |
| print(df.groupby(["split", "lang"]).size().unstack().to_string()) | |
| print("test rows with images:", df[(df.split == "test") & has_img].groupby("src").size().to_dict()) | |
| # leakage audit: max similarity of every test norm to any train norm | |
| tr = sorted(set(df[df.split == "train"].nospace)) | |
| te = df[df.split == "test"].nospace.tolist() | |
| best = process.cdist(te, tr, scorer=fuzz.ratio, workers=-1, dtype=np.uint8).max(axis=1) | |
| print("test->train max ratio: >=90:", int((best >= 90).sum()), ">=80:", int((best >= 80).sum()), "of", len(te)) | |
| df.loc[df.split == "test", "max_ratio_to_train"] = best | |
| df["id"] = [f"{s}{i:05d}" for i, s in zip(range(len(df)), df.src)] | |
| df.to_json(OUT + "corpus.jsonl", orient="records", lines=True, force_ascii=False) | |
| print("wrote", OUT + "corpus.jsonl") | |