Spaces:
Running on Zero
Running on Zero
| """Ingest ALREADY-cropped (line image, transcription) data into the Diffu manifest + writer-disjoint | |
| split. The pre-cropped counterpart to ``prepare.py`` (which crops from PAGE/ALTO XML); both produce the | |
| same ``manifest.jsonl`` + ``train/val/test.jsonl`` via the shared ``dataset.write_splits``. | |
| Two input shapes, both deriving the writer/source id automatically — no manual labelling: | |
| # A folder tree, one folder per page/volume, with sidecar text files (line_002.png + line_002.txt): | |
| diffu-ingest --dir crops/ --out-dir data_out # writer id = the folder name | |
| # OR a CSV / JSONL you already have (image, text columns); writer from a column or the path: | |
| diffu-ingest --input lines.jsonl --out-dir data_out --writer-col scribe | |
| diffu-ingest --input lines.csv --out-dir data_out --writer-depth 1 # writer = image's parent dir | |
| Pure stdlib + the dataset helpers — no torch. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import json | |
| import os | |
| from collections.abc import Iterator | |
| from .dataset import char_coverage, normalize_text, write_splits | |
| _IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".tif", ".tiff", ".webp") | |
| def writer_from_path(image_path: str, depth: int = 1) -> str: | |
| """Writer/source proxy = the ``depth``-th parent directory of the image (1 = immediate folder).""" | |
| p = os.path.dirname(image_path) | |
| for _ in range(depth - 1): | |
| p = os.path.dirname(p) | |
| return os.path.basename(p) or "unknown" | |
| def scan_folder(root: str, *, text_suffix: str = ".txt", writer_depth: int = 1) -> list[dict[str, str]]: | |
| """Walk ``root`` for line images, pair each with its sidecar text file, derive the writer from the | |
| folder. Images without a sidecar (or with empty text) are skipped. | |
| """ | |
| records: list[dict[str, str]] = [] | |
| for dirpath, _dirs, files in os.walk(root): | |
| for name in files: | |
| stem, ext = os.path.splitext(name) | |
| if ext.lower() not in _IMAGE_EXTS: | |
| continue | |
| image = os.path.join(dirpath, name) | |
| sidecar = os.path.join(dirpath, stem + text_suffix) | |
| if not os.path.exists(sidecar): | |
| continue | |
| with open(sidecar, encoding="utf-8") as f: | |
| text = normalize_text(f.read()) | |
| if text: | |
| records.append( | |
| {"image": image, "text": text, "writer_id": writer_from_path(image, writer_depth)} | |
| ) | |
| return records | |
| def _read_rows(input_path: str) -> Iterator[dict[str, str]]: | |
| """Yield raw rows from a ``.jsonl`` or ``.csv`` file (auto-detected by extension).""" | |
| if input_path.endswith((".jsonl", ".ndjson")): | |
| with open(input_path, encoding="utf-8") as f: | |
| for line in f: | |
| if line.strip(): | |
| yield json.loads(line) | |
| elif input_path.endswith(".csv"): | |
| with open(input_path, encoding="utf-8", newline="") as f: | |
| yield from csv.DictReader(f) | |
| else: | |
| raise ValueError(f"unsupported input {input_path!r}: expected a .jsonl or .csv file") | |
| def read_records( | |
| input_path: str, | |
| *, | |
| image_col: str = "image", | |
| text_col: str = "text", | |
| writer_col: str | None = "writer_id", | |
| writer_depth: int | None = None, | |
| image_root: str | None = None, | |
| ) -> list[dict[str, str]]: | |
| """Read CSV/JSONL records into the manifest schema ``{image, text, writer_id}``. | |
| The writer id comes from ``writer_col`` if given, else (when ``writer_depth`` is set) from the | |
| image's parent directory. Text is NFC-normalized; empty-text rows are dropped. | |
| Raises: | |
| KeyError: if the first row is missing a requested column (image/text/writer). | |
| """ | |
| records: list[dict[str, str]] = [] | |
| for i, row in enumerate(_read_rows(input_path)): | |
| if i == 0: | |
| needed = {image_col, text_col} | ({writer_col} if writer_col and writer_depth is None else set()) | |
| missing = needed - set(row) | |
| if missing: | |
| raise KeyError( | |
| f"input is missing column(s) {sorted(missing)}; found {sorted(row)}. " | |
| f"Pass --image-col / --text-col / --writer-col, or use --writer-depth." | |
| ) | |
| text = normalize_text(str(row[text_col])) | |
| if not text: | |
| continue | |
| image = str(row[image_col]) | |
| if image_root: | |
| image = os.path.join(image_root, image) | |
| if writer_depth is not None: | |
| writer = writer_from_path(image, writer_depth) | |
| elif writer_col is not None: | |
| writer = str(row[writer_col]) | |
| else: # library callers can pass both None; fail fast (an assert would vanish under python -O) | |
| raise ValueError("read_records needs writer_col or writer_depth set; both are None") | |
| records.append({"image": image, "text": text, "writer_id": writer}) | |
| return records | |
| def write_manifest(records: list[dict[str, str]], out_dir: str) -> str: | |
| """Write records to ``out_dir/manifest.jsonl``; returns the path.""" | |
| os.makedirs(out_dir, exist_ok=True) | |
| manifest = os.path.join(out_dir, "manifest.jsonl") | |
| with open(manifest, "w", encoding="utf-8") as f: | |
| for r in records: | |
| f.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| return manifest | |
| def main() -> None: | |
| ap = argparse.ArgumentParser( | |
| description="Ingest pre-cropped (image, text) lines -> manifest + writer-disjoint split." | |
| ) | |
| src = ap.add_mutually_exclusive_group(required=True) | |
| src.add_argument( | |
| "--dir", help="root folder of line crops + sidecar text files (one folder per page/volume)" | |
| ) | |
| src.add_argument("--input", help="a .jsonl or .csv of line records") | |
| ap.add_argument("--out-dir", required=True, help="output dir for manifest.jsonl + train/val/test.jsonl") | |
| ap.add_argument("--text-suffix", default=".txt", help="sidecar transcription extension (folder mode)") | |
| ap.add_argument( | |
| "--writer-depth", | |
| type=int, | |
| default=None, | |
| help="derive writer id from the N-th parent dir of the image (1 = its folder). " | |
| "Folder mode defaults to 1; in --input mode this overrides --writer-col.", | |
| ) | |
| ap.add_argument("--image-col", default="image") | |
| ap.add_argument("--text-col", default="text") | |
| ap.add_argument("--writer-col", default="writer_id") | |
| ap.add_argument("--image-root", default=None, help="prefix for relative image paths (--input mode)") | |
| ap.add_argument("--val-frac", type=float, default=0.1) | |
| ap.add_argument("--test-frac", type=float, default=0.1) | |
| ap.add_argument("--seed", type=int, default=42) | |
| args = ap.parse_args() | |
| if args.dir: | |
| records = scan_folder(args.dir, text_suffix=args.text_suffix, writer_depth=args.writer_depth or 1) | |
| else: | |
| records = read_records( | |
| args.input, | |
| image_col=args.image_col, | |
| text_col=args.text_col, | |
| writer_col=args.writer_col, | |
| writer_depth=args.writer_depth, | |
| image_root=args.image_root, | |
| ) | |
| if not records: | |
| raise SystemExit("no (image, text) records found — check --dir / --text-suffix or the CSV columns.") | |
| manifest = write_manifest(records, args.out_dir) | |
| n_writers = len({r["writer_id"] for r in records}) | |
| print(f"wrote {len(records)} lines from {n_writers} writers/sources -> {manifest}") | |
| cov = char_coverage(manifest) | |
| print("Swedish diacritic coverage:", {ch: cov.get(ch, 0) for ch in "åäöÅÄÖ"}) | |
| for name, counts in write_splits( | |
| manifest, args.out_dir, val_frac=args.val_frac, test_frac=args.test_frac, seed=args.seed | |
| ).items(): | |
| print(f" {name}: {counts['lines']} lines / {counts['groups']} writers") | |
| if __name__ == "__main__": | |
| main() | |