Download code/src/preprocessing/build_tablet_structure.py from savastakan/hitit-cuneiform-ocr: direct link, hf CLI and curl.
- Browser
- Download file 5.24 kB
-
https://huggingface.co/savastakan/hitit-cuneiform-ocr/resolve/main/code/src/preprocessing/build_tablet_structure.py
- Command line
-
hf download hf://savastakan/hitit-cuneiform-ocr/code/src/preprocessing/build_tablet_structure.py
-
curl -L -o build_tablet_structure.py https://huggingface.co/savastakan/hitit-cuneiform-ocr/resolve/main/code/src/preprocessing/build_tablet_structure.py
5.24 kB
| #!/usr/bin/env python3 | |
| """ | |
| Build tablet-level structured manifest from mark.txt annotations. | |
| Each tablet → list of lines → list of signs with: | |
| - bbox (x, y, w, h) normalized | |
| - sign label (ABZ/comment) | |
| - language (Hittite/Sumerian/Akkadian/...) | |
| - damage (inferred: 'intact' | 'partial' | 'broken') | |
| - reading order (row_no, col_no, x-sort within line) | |
| Output: datasets/sources/hitit_local/tablet_structure.jsonl | |
| {tablet_id, image_path, width, height, lines: [{row, col, signs:[...]}]} | |
| """ | |
| import argparse | |
| import json | |
| from collections import defaultdict | |
| from pathlib import Path | |
| from PIL import Image | |
| SOURCES = Path("/arf/scratch/stakan/hitit-proje/datasets/sources/hitit_local") | |
| LANG_MAP = { | |
| "Hititçe": "hit", "Hittite": "hit", "Hit": "hit", | |
| "Sümerce": "sum", "Sumerian": "sum", "Sum": "sum", | |
| "Akadca": "akk", "Akkadian": "akk", "Akk": "akk", | |
| "Luvice": "luw", "Luwian": "luw", | |
| "Hurrice": "hur", "Hurrian": "hur", | |
| "Hattice": "hat", | |
| "Palaca": "pal", "Palaic": "pal", | |
| "": "unk", | |
| } | |
| # Damage inference from comment | |
| DAMAGE_MARKS = { | |
| 'x': 'broken', # 'x' standard Assyriology marker for lost sign | |
| 'X': 'broken', | |
| '?': 'uncertain', | |
| '▒': 'broken', | |
| '…': 'broken', | |
| '[': 'partial', # half-bracket usually | |
| ']': 'partial', | |
| '⸢': 'partial', | |
| '⸣': 'partial', | |
| } | |
| def classify_damage(comment, quality=None): | |
| """Return 'intact' | 'partial' | 'broken' | 'uncertain'.""" | |
| if comment is None: | |
| return 'broken' | |
| c = comment.strip().rstrip(",.") | |
| if not c or c in {"x", "X", "_SLASH__SLASH_", "//", "▒", "…"}: | |
| return 'broken' | |
| if any(m in c for m in ('[', ']', '⸢', '⸣', '〈', '〉')): | |
| return 'partial' | |
| if '?' in c: | |
| return 'uncertain' | |
| return 'intact' | |
| def normalize_bbox(mark, W, H): | |
| x = mark.get("x", 0); y = mark.get("y", 0) | |
| w = mark.get("width", 0); h = mark.get("height", 0) | |
| if w < 0: x += w; w = abs(w) | |
| if h < 0: y += h; h = abs(h) | |
| return x / W, y / H, w / W, h / H | |
| def process_tablet(tid): | |
| tdir = SOURCES / tid | |
| mark = tdir / "mark.txt" | |
| if not mark.exists() or mark.stat().st_size == 0: | |
| return None | |
| # Find image | |
| img_path = None | |
| for ext in (".JPG", ".jpg", ".jpeg", ".png", ".PNG"): | |
| for f in tdir.glob(f"*{ext}"): | |
| img_path = f | |
| break | |
| if img_path: break | |
| if not img_path: return None | |
| try: | |
| with Image.open(img_path) as im: | |
| W, H = im.size | |
| except Exception: | |
| return None | |
| try: | |
| data = json.loads(mark.read_text()) | |
| except Exception: | |
| return None | |
| anns = data.get("annotations", []) | |
| if not anns: | |
| return None | |
| # Build sign list with reading-order metadata; dedup by (pk_id, bbox) | |
| signs = [] | |
| seen_pk = set() | |
| for idx, a in enumerate(anns): | |
| pk = a.get("pk_id") | |
| if pk is not None and pk in seen_pk: | |
| continue | |
| m = a.get("mark", {}) | |
| bx, by, bw, bh = normalize_bbox(m, W, H) | |
| comment = (a.get("comment") or "").strip().rstrip(",.") | |
| lang_raw = a.get("lang") or "" | |
| signs.append({ | |
| "idx": idx, | |
| "sign": comment if comment else "x", | |
| "lang": LANG_MAP.get(lang_raw, "unk"), | |
| "lang_raw": lang_raw, | |
| "damage": classify_damage(comment), | |
| "bbox": [round(bx, 4), round(by, 4), round(bw, 4), round(bh, 4)], | |
| "row": a.get("row_no", 0), | |
| "col": a.get("col_no", 0), | |
| "pk": pk, | |
| }) | |
| if pk is not None: | |
| seen_pk.add(pk) | |
| # Group by (row, col), sort within row by x (reading order) | |
| by_line = defaultdict(list) | |
| for s in signs: | |
| by_line[(s["row"], s["col"])].append(s) | |
| lines = [] | |
| for (row, col), items in sorted(by_line.items()): | |
| items.sort(key=lambda s: s["bbox"][0]) # left → right | |
| lines.append({"row": row, "col": col, "signs": items}) | |
| return { | |
| "tablet_id": tid, | |
| "image_path": str(img_path), | |
| "width": W, | |
| "height": H, | |
| "n_signs": len(signs), | |
| "n_lines": len(lines), | |
| "lang_dist": dict((k, sum(1 for s in signs if s["lang"] == k)) | |
| for k in set(s["lang"] for s in signs)), | |
| "damage_dist": dict((k, sum(1 for s in signs if s["damage"] == k)) | |
| for k in set(s["damage"] for s in signs)), | |
| "lines": lines, | |
| } | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--output", default=str(SOURCES / "tablet_structure.jsonl")) | |
| ap.add_argument("--limit", type=int, default=0) | |
| args = ap.parse_args() | |
| tids = sorted(d.name for d in SOURCES.iterdir() | |
| if d.is_dir() and (d / "mark.txt").exists()) | |
| print(f"Scanning {len(tids)} tablets") | |
| n = 0 | |
| with open(args.output, "w") as out: | |
| for tid in tids: | |
| t = process_tablet(tid) | |
| if t is None: | |
| continue | |
| out.write(json.dumps(t, ensure_ascii=False) + "\n") | |
| n += 1 | |
| if args.limit and n >= args.limit: | |
| break | |
| print(f"DONE: {n} tablets → {args.output}") | |
| if __name__ == "__main__": | |
| main() | |