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/synth.py from kesha-humonen/runic-reader-2: direct link, hf CLI and curl.
- Browser
- Download file 8.24 kB
-
https://huggingface.co/kesha-humonen/runic-reader-2/resolve/main/code/synth.py
- Command line
-
hf download hf://kesha-humonen/runic-reader-2/code/synth.py
-
curl -L -o synth.py https://huggingface.co/kesha-humonen/runic-reader-2/resolve/main/code/synth.py
8.24 kB
| """Procedural synthetic photographs of carved runic inscriptions. | |
| Lexicon: words of the *train* split only (frequency >= 2), sampled with temperature 0.7 into | |
| 1-5 word phrases. Each transliteration is mapped to runes with a fixed table (Younger Futhark | |
| long-branch core, dotted medieval runes and Elder Futhark signs for the remaining letters), | |
| rendered with one of three runic fonts, and composited onto a noise-based stone texture as | |
| an incised groove (height map + directional light), optionally painted, laid out straight, | |
| vertical, in a framed band or along a curve, then degraded (lichen, blur, perspective, | |
| exposure, JPEG). The label is the transliteration string itself. | |
| """ | |
| import argparse | |
| import io | |
| import json | |
| import math | |
| import os | |
| import random | |
| from collections import Counter | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFilter, ImageFont | |
| RUNE = { | |
| "f": "ᚠ", "u": "ᚢ", "þ": "ᚦ", "o": "ᚬ", "ą": "ᚭ", "r": "ᚱ", "k": "ᚴ", "h": "ᚼ", "n": "ᚾ", | |
| "i": "ᛁ", "a": "ᛅ", "s": "ᛋ", "t": "ᛏ", "b": "ᛒ", "m": "ᛘ", "l": "ᛚ", "R": "ᛦ", | |
| "e": "ᛂ", "g": "ᚵ", "d": "ᛑ", "p": "ᛔ", "y": "ᚤ", "c": "ᛍ", "v": "ᚡ", "z": "ᛎ", | |
| "x": "ᛪ", "q": "ᛩ", "w": "ᚹ", "j": "ᛃ", "ð": "ᚧ", "ï": "ᛇ", "ŋ": "ᛜ", "æ": "ᛆ", | |
| "ø": "ᚯ", "ö": "ᚯ", "å": "ᚮ", "ä": "ᛅ", | |
| } | |
| SEPARATORS = ["᛫", "᛬", "×", ":", "᛭", " "] | |
| FONT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fonts") | |
| FONTS = ["NotoSansRunic-Regular.ttf", "Junicode-Regular.ttf", "Junicode-Bold.ttf"] | |
| def lexicon(corpus_path): | |
| words = Counter() | |
| for l in open(corpus_path): | |
| r = json.loads(l) | |
| if r["split"] == "train": | |
| words.update(w for w in r["norm"].split() if 2 <= len(w) <= 20) | |
| return {w: c for w, c in words.items() if c >= 2} | |
| def sample_phrase(rng, words, weights): | |
| n = rng.choices([1, 2, 3, 4, 5], weights=[2, 3, 3, 2, 1])[0] | |
| return " ".join(rng.choices(words, weights=weights, k=n)) | |
| def smooth_noise(rng, h, w, octaves=5): | |
| nrng = np.random.default_rng(rng.randint(0, 2**31)) | |
| out = np.zeros((h, w), np.float32) | |
| amp = 1.0 | |
| for o in range(octaves): | |
| sh, sw = max(2, h // (2 ** (octaves - o))), max(2, w // (2 ** (octaves - o))) | |
| base = nrng.standard_normal((sh, sw)).astype(np.float32) | |
| up = np.array(Image.fromarray(base).resize((w, h), Image.BICUBIC)) | |
| out += amp * up | |
| amp *= 0.55 | |
| out -= out.min() | |
| return out / (out.max() + 1e-6) | |
| def stone(rng, h, w): | |
| base = np.array([rng.uniform(90, 175)] * 3, np.float32) | |
| tint = np.array([rng.uniform(-18, 22), rng.uniform(-12, 10), rng.uniform(-25, 8)], np.float32) | |
| n1 = smooth_noise(rng, h, w) | |
| nrng = np.random.default_rng(rng.randint(0, 2**31)) | |
| speck = (nrng.random((h, w)) < rng.uniform(0.01, 0.08)).astype(np.float32) | |
| speck = np.array(Image.fromarray((speck * 255).astype(np.uint8)).filter(ImageFilter.GaussianBlur(0.8)), np.float32) / 255.0 | |
| img = base + tint + (n1[..., None] - 0.5) * rng.uniform(40, 110) | |
| img += (speck[..., None] * rng.uniform(-60, 60)) | |
| return img | |
| def text_mask(rng, runes, W, H): | |
| font = ImageFont.truetype(os.path.join(FONT_DIR, rng.choice(FONTS)), 10) | |
| size = int(H * rng.uniform(0.12, 0.3)) | |
| for _ in range(40): | |
| font = font.font_variant(size=size) | |
| bbox = font.getbbox(runes) | |
| if bbox[2] - bbox[0] <= W * 0.92: | |
| break | |
| size = int(size * 0.9) | |
| tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] | |
| m = Image.new("L", (W, H), 0) | |
| d = ImageDraw.Draw(m) | |
| x = (W - tw) // 2 - bbox[0] + rng.randint(-W // 20, W // 20) | |
| y = (H - th) // 2 - bbox[1] + rng.randint(-H // 12, H // 12) | |
| stroke = rng.choice([0, 0, 1, 2]) | |
| d.text((x, y), runes, fill=255, font=font, stroke_width=stroke, stroke_fill=255) | |
| band = rng.random() < 0.45 | |
| if band: | |
| pad = int(th * rng.uniform(0.25, 0.5)) | |
| lw = max(2, size // 12) | |
| d.line([(x + bbox[0] - pad, y + bbox[1] - pad), (x + bbox[2] + pad, y + bbox[1] - pad)], fill=255, width=lw) | |
| d.line([(x + bbox[0] - pad, y + bbox[3] + pad), (x + bbox[2] + pad, y + bbox[3] + pad)], fill=255, width=lw) | |
| return np.array(m, np.float32) / 255.0 | |
| def bend(mask, rng): | |
| h, w = mask.shape | |
| amp = rng.uniform(0.05, 0.18) * h | |
| freq = rng.uniform(0.5, 1.5) | |
| ys, xs = np.mgrid[0:h, 0:w] | |
| shift = amp * np.sin(2 * math.pi * freq * xs / w + rng.uniform(0, 6.28)) | |
| src_y = np.clip((ys - shift).astype(np.int32), 0, h - 1) | |
| return mask[src_y, xs] | |
| def render(rng, translit, out_long=768): | |
| runes = "".join(RUNE.get(ch, "") if ch != " " else "\x00" for ch in translit) | |
| sep = rng.choice(SEPARATORS) | |
| runes = runes.replace("\x00", f" {sep} " if sep != " " else " ").replace(" ", " ") | |
| layout = rng.choices(["h", "v", "curve"], weights=[5, 3, 2])[0] | |
| W, H = 1100, rng.choice([320, 420, 520]) | |
| mask = text_mask(rng, runes, W, H) | |
| if layout == "curve": | |
| mask = bend(mask, rng) | |
| depth = np.array(Image.fromarray((mask * 255).astype(np.uint8)).filter( | |
| ImageFilter.GaussianBlur(rng.uniform(1.0, 3.0))), np.float32) / 255.0 | |
| gy, gx = np.gradient(-depth) | |
| ang = rng.uniform(0, 2 * math.pi) | |
| light = (gx * math.cos(ang) + gy * math.sin(ang)) * rng.uniform(250, 600) | |
| tex = stone(rng, H, W) | |
| groove = depth[..., None] * rng.uniform(20, 70) | |
| img = tex - groove + light[..., None] | |
| if rng.random() < 0.35: # painted runes, as on many restored runestones | |
| paint = np.array(rng.choice([(150, 30, 30), (170, 40, 35), (30, 30, 30), (200, 200, 190)]), np.float32) | |
| a = np.clip(mask * rng.uniform(0.6, 0.95), 0, 1)[..., None] | |
| img = img * (1 - a) + paint * a | |
| if rng.random() < 0.5: # lichen / moss blotches | |
| blot = smooth_noise(rng, H, W, 4) > rng.uniform(0.72, 0.85) | |
| col = np.array(rng.choice([(170, 170, 120), (90, 110, 60), (200, 190, 150)]), np.float32) | |
| img = np.where(blot[..., None], img * 0.4 + col * 0.6, img) | |
| img = np.clip(img, 0, 255).astype(np.uint8) | |
| im = Image.fromarray(img) | |
| if layout == "v": | |
| im = im.rotate(rng.choice([90, -90]), expand=True) | |
| fill = tuple(int(v) for v in img.mean((0, 1))) | |
| im = im.rotate(rng.uniform(-6, 6), expand=True, fillcolor=fill) | |
| w, h = im.size | |
| dx, dy = rng.uniform(-0.06, 0.06) * w, rng.uniform(-0.06, 0.06) * h | |
| coeffs = (1 + rng.uniform(-0.05, 0.05), rng.uniform(-0.05, 0.05), dx, rng.uniform(-0.05, 0.05), | |
| 1 + rng.uniform(-0.05, 0.05), dy, rng.uniform(-1e-4, 1e-4), rng.uniform(-1e-4, 1e-4)) | |
| im = im.transform(im.size, Image.PERSPECTIVE, coeffs, Image.BICUBIC, fillcolor=fill) | |
| if rng.random() < 0.6: | |
| im = im.filter(ImageFilter.GaussianBlur(rng.uniform(0.3, 1.6))) | |
| arr = np.array(im, np.float32) | |
| arr = arr * rng.uniform(0.6, 1.3) + rng.uniform(-30, 30) | |
| arr += np.random.default_rng(rng.randint(0, 2**31)).normal(0, rng.uniform(2, 10), arr.shape) | |
| im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) | |
| im.thumbnail((out_long, out_long)) | |
| buf = io.BytesIO() | |
| im.save(buf, "JPEG", quality=rng.randint(55, 92)) | |
| return Image.open(buf).convert("RGB") | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--corpus", required=True) | |
| ap.add_argument("--out_dir", required=True) | |
| ap.add_argument("--n", type=int, default=6000) | |
| ap.add_argument("--seed", type=int, default=13) | |
| ap.add_argument("--start", type=int, default=0) | |
| args = ap.parse_args() | |
| rng = random.Random(args.seed + args.start) | |
| lex = lexicon(args.corpus) | |
| words = sorted(lex) | |
| weights = [lex[w] ** 0.7 for w in words] | |
| os.makedirs(os.path.join(args.out_dir, "img"), exist_ok=True) | |
| with open(os.path.join(args.out_dir, f"labels_{args.start}.jsonl"), "w") as f: | |
| for i in range(args.start, args.start + args.n): | |
| t = sample_phrase(rng, words, weights) | |
| name = f"img/syn{i:06d}.jpg" | |
| render(rng, t).save(os.path.join(args.out_dir, name), quality=90) | |
| f.write(json.dumps({"id": f"syn{i:06d}", "image": name, "target": t}, ensure_ascii=False) + "\n") | |
| print("lexicon", len(words), "wrote", args.n) | |
| if __name__ == "__main__": | |
| main() | |