"""Compile the Hershey pen-stroke letters (public domain, US National Bureau of Standards, 1967) and the KanjiVG
stroke-order data (CC BY-SA 3.0) into weights.safetensors.
python export_weights.py
Nothing here is learned. Each glyph is the centreline a pen follows: a list of strokes, each a list of (x, y) points,
in font units (cap height 21, y down, baseline at y=9). The pen constants in quill.py are written into the same file
so every number the model uses can be inspected in one place.
The safetensors format is an 8-byte little-endian header length, a JSON header, then the raw tensor bytes.
"""
import gzip
import json
import re
import struct
from pathlib import Path
import numpy as np
HERE = Path(__file__).parent
FONTS = ["rowmans", "scripts", "cursive", "futural", "gothiceng"] # pen centrelines; gothic is multi-stroke
FIRST_CHAR = 32
def parse_jhf(path):
"""-> metrics dict, list of 96 glyphs; a glyph is (left, right, [stroke arrays])."""
lines = Path(path).read_text().splitlines()
metrics = json.loads(lines[0][1:])
glyphs = []
for line in lines[1:]:
if not line.strip():
continue
data = line[8:]
val = lambda c: ord(c) - ord("R")
left, right = val(data[0]), val(data[1])
strokes, cur = [], []
for k in range(2, len(data) - 1, 2):
pair = data[k:k + 2]
if pair == " R":
if cur:
strokes.append(np.array(cur, np.float32))
cur = []
else:
cur.append((val(pair[0]), val(pair[1])))
if cur:
strokes.append(np.array(cur, np.float32))
glyphs.append((left, right, strokes))
return metrics, glyphs
# KanjiVG stroke kinds -> how the brush ends the stroke. Hand-set from the Unicode CJK stroke names.
STROKE_KIND = {"tome": 0, "dot": 1, "left_sweep": 2, "right_sweep": 3, "hook": 4, "rise": 5}
KIND_OF = {"㇔": "dot", "㇒": "left_sweep", "㇓": "left_sweep", "㇏": "right_sweep", "㇝": "right_sweep", "㇀": "rise",
**{c: "hook" for c in "㇚㇆㇟㇂㇉㇈㇖㇙㇌㇋㇢㇡"}}
def svg_path_points(d, per_curve=6):
"""SVG path (M/C/S, absolute or relative) -> polyline; each cubic sampled at per_curve points."""
toks = re.findall(r"[MmCcSsLl]|-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?", d)
pts, cur, start, last_ctrl, cmd, i = [], np.zeros(2), None, None, None, 0
t = np.linspace(0, 1, per_curve + 1)[1:, None]
while i < len(toks):
if toks[i].isalpha():
cmd = toks[i]
i += 1
nums = lambda k: np.array([float(x) for x in toks[i:i + k]])
rel = cmd.islower()
if cmd in "Mm":
cur = nums(2) + (cur if rel and pts else 0)
pts.append(cur.copy())
last_ctrl, i = None, i + 2
cmd = "l" if rel else "L"
elif cmd in "Ll":
cur = nums(2) + (cur if rel else 0)
pts.append(cur.copy())
last_ctrl, i = None, i + 2
else:
if cmd in "Cc":
v = nums(6).reshape(3, 2) + (cur if rel else 0)
c1, c2, p = v
i += 6
else: # S: first control mirrors the last one
v = nums(4).reshape(2, 2) + (cur if rel else 0)
c1 = 2 * cur - last_ctrl if last_ctrl is not None else cur.copy()
c2, p = v
i += 4
seg = (1 - t) ** 3 * cur + 3 * (1 - t) ** 2 * t * c1 + 3 * (1 - t) * t ** 2 * c2 + t ** 3 * p
pts.extend(seg)
cur, last_ctrl = p, c2
return np.array(pts, np.float32)
def parse_kanjivg(path):
"""-> list of (codepoint, [(stroke kind, points)])"""
xml = gzip.open(path, "rt", encoding="utf-8").read()
out = []
for m in re.finditer(r'(.*?)', xml, re.S):
strokes = []
for p in re.finditer(r"]*)/>", m.group(2)):
attrs = dict(re.findall(r'([\w:]+)="([^"]*)"', p.group(1)))
n = int(re.search(r"-s(\d+)$", attrs["id"]).group(1))
kind = KIND_OF.get(attrs.get("kvg:type", "").split("/")[0][:1], "tome")
strokes.append((n, STROKE_KIND[kind], svg_path_points(attrs["d"])))
out.append((int(m.group(1), 16), [(k, s) for _, k, s in sorted(strokes, key=lambda x: x[0])]))
return out
def collect():
from quill import STYLES, INKS, CHAIKIN
tensors, fonts_meta = {}, {}
kanji = parse_kanjivg(HERE / "data" / "kanjivg" / "kanjivg-20260714.xml.gz")
pts, stroke_start, kinds, glyph_start = [], [], [], []
n_pts = 0
for _, strokes in kanji:
glyph_start.append(len(stroke_start))
for k, s in strokes:
stroke_start.append(n_pts)
kinds.append(k)
pts.append(s)
n_pts += len(s)
glyph_start.append(len(stroke_start))
stroke_start.append(n_pts)
tensors["glyphs.kanji.points"] = np.concatenate(pts)
tensors["glyphs.kanji.stroke_start"] = np.array(stroke_start, np.int32)
tensors["glyphs.kanji.stroke_kind"] = np.array(kinds, np.int32)
tensors["glyphs.kanji.glyph_start"] = np.array(glyph_start, np.int32)
tensors["glyphs.kanji.codepoint"] = np.array([c for c, _ in kanji], np.int32)
for f in FONTS:
metrics, glyphs = parse_jhf(HERE / "data" / "hershey" / f"{f}.jhf")
pts, stroke_start, glyph_start = [], [], []
for _, _, strokes in glyphs:
glyph_start.append(len(stroke_start))
for s in strokes:
stroke_start.append(sum(len(p) for p in pts))
pts.append(s)
glyph_start.append(len(stroke_start))
stroke_start.append(sum(len(p) for p in pts))
tensors[f"glyphs.{f}.points"] = np.concatenate(pts).astype(np.float32)
tensors[f"glyphs.{f}.stroke_start"] = np.array(stroke_start, np.int32)
tensors[f"glyphs.{f}.glyph_start"] = np.array(glyph_start, np.int32)
tensors[f"glyphs.{f}.advance"] = np.array([(l, r) for l, r, _ in glyphs], np.float32)
fonts_meta[f] = {k.replace("define_", ""): float(v) for k, v in metrics.items()}
tensors["smooth.chaikin"] = np.array(CHAIKIN, np.float32)
for name, rgb in INKS.items():
tensors[f"ink.{name}"] = np.array(rgb, np.float32) / 255
meta = {"charset": "".join(chr(FIRST_CHAR + i) for i in range(96)), "fonts": json.dumps(fonts_meta),
"styles": json.dumps(STYLES), "stroke_kinds": json.dumps(STROKE_KIND),
"source": "Hershey fonts (public domain), via the MIT-licensed Hershey-Fonts package; "
"kanji/kana strokes from KanjiVG (c) Ulrich Apel, CC BY-SA 3.0, http://kanjivg.tagaini.net",
"units": "Hershey: cap height 21, y down, baseline y=9. KanjiVG: 109 x 109 box, y down"}
return tensors, meta
def save_safetensors(tensors, meta, path):
header, blobs, offset = {"__metadata__": meta}, [], 0
for name, t in tensors.items():
dt, code = (np.dtype(" {out.name} ({out.stat().st_size / 1024:.0f} KB)")