Conflicts, settled with the features' behaviour and the catalogue's lookups:
- app.rs: the default title is "AAC — reference app" (the app's name, set by
the host; not UI).
- edit_bar.rs: the title stays a message; the hint is the features' (it
follows a scene's marking).
- grid_cell.rs: the cell's name holds the words it shows (WCAG 2.5.3) and
what it says when that holds them; "Edit: …", the notes in brackets, "Say
the next line", "Undo: bring the message back" and the action names stay
messages (grid-cell-*, output-strip-undo-clear-name).
- lock.rs: "Lock again after" is a group of radio choices; its legend and
choices are the lock-relock-* messages.
- navigation_bar.rs: the bar's name and the partner button's name stay
messages; the recording dot and the new button size are the features'.
- access_settings.rs: the new "start each pass at the place to press" toggle
is a message (access-settings-scan-from-way).
- catalogue_sets.rs: a size's description, or what it holds when started with
its first words, through the page-set catalogue's messages.
- cell_editor.rs: hotspot headings and the hotspot's hide label are new
messages; Undo's descriptions stay messages, with the features' words shown
and same-word hides.
- import_export.rs: the record of choices in a backup, and in what a backup
brings back, are new messages.
- listen_settings.rs, settings.rs: the features' new English, in the existing
messages (settings-scan-bar-note keeps "Delete last word", the button's
name).
- profiles.rs: the Active tag and the new Delete button are messages.
- symbol_settings.rs, print_view.rs: the optional sets' licences ("ARASAAC's
licence: …; our adapted versions are shared under …") and their changes
are messages; licence names come from aac-core (display_name). The changes'
English is the symbol catalogue's own (a test checks they agree).
- voice_banking.rs, voice_offer.rs: the English fixes were already in the
messages; unchanged.
- editing.rs: the undo problem names the words shown.
- visual_scene.rs: deleted (scene_board.rs replaces it); its message
visual-scene-name goes, from English and from es, fr, de and pt.
Merge fixes: the Find a word panel's title is a message (panels-find-word-
title); print_view.rs's file error is file_failed().
The feature tracks' other English is put through the catalogue in the
commits that follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
459 lines
23 KiB
Python
459 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""Build the open page sets: Growing Core and Fixed Paths.
|
||
|
||
python3 tools/pagesets/build.py # update the design documents' tables
|
||
python3 tools/pagesets/build.py --check # fail if they are out of date
|
||
python3 tools/pagesets/build.py --out DIR # write the packages into DIR
|
||
|
||
The packages are not committed: they are built where they are needed (the
|
||
site's build, through tools/pagesets/catalog.py, and aac-core's tests), from
|
||
these sources, byte for byte the same every time. What is committed is the
|
||
catalogue (data/pagesets/index.json, written by catalog.py), which names each
|
||
package with its size and SHA-256, so a build that does not reproduce them
|
||
fails.
|
||
|
||
Reads tools/pagesets/data (the design and its committed layouts), sources/
|
||
(the cited word lists) and the app's own data (glosses, symbols, word
|
||
classes); checks every rule of DESIGN-core.md and DESIGN-motor.md; with --out:
|
||
|
||
growing-core/*.obz one package per size, and reveal examples
|
||
fixed-paths/*.obz one package per size, and filter examples
|
||
families.json both families, for the catalogue (catalog.py)
|
||
cells.json every cell of every board (for aac-core's tests)
|
||
metrics.json the metrics of both designs
|
||
languages.json what each language gets, and the native work it needs
|
||
words.json every word, with its evidence
|
||
|
||
and always the metric tables inside DESIGN-core.md and DESIGN-motor.md.
|
||
|
||
Reproducible: fixed ids (UUID v5), a fixed date, sorted keys. Needs Python
|
||
3.11+ and PyYAML; nothing else, no network.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
|
||
from pslib import costs, langs, metrics, obf # noqa: E402
|
||
from pslib.boards import core_packages, motor_filter, motor_packages # noqa: E402
|
||
from pslib.data import HERE, WORD_CLASS, Data # noqa: E402
|
||
from pslib.layout import load_core, load_motor # noqa: E402
|
||
|
||
|
||
def class_exceptions(data: Data, words: set[str]) -> dict:
|
||
out = {}
|
||
for w in sorted(words):
|
||
e = data.lexicon[w]
|
||
ours = e.word_class
|
||
app = data.app_vocabulary.get(w.lower())
|
||
if app and ours and app[0] != ours:
|
||
out[w] = {"ours": ours, "app": app[0], "why": e.sense or f"used as a {e.pos}"}
|
||
return out
|
||
|
||
|
||
def index_json(pkgs, exceptions) -> dict:
|
||
out = {"version": 1, "packages": [], "class_exceptions": exceptions}
|
||
for p in pkgs:
|
||
boards = []
|
||
for b in p.boards:
|
||
cells = []
|
||
for btn in sorted(b.buttons, key=lambda x: (x.row, x.col)):
|
||
extra = btn.target if btn.action == "navigate" else btn.modifier if btn.action == "modifier" else None
|
||
cells.append([btn.key, btn.row, btn.col, btn.action, extra, btn.hidden, btn.control])
|
||
boards.append({"id": b.id, "name": b.name, "rows": b.rows, "cols": b.cols, "board": b.pageset.get("board"), "cells": cells})
|
||
out["packages"].append({"file": p.file, "set": p.set, "size": p.size, "variant": p.variant, "root": p.root, "boards": boards})
|
||
return out
|
||
|
||
|
||
def words_json(data: Data, pkgs) -> dict:
|
||
out = {}
|
||
for set_name in ("growing-core", "fixed-paths"):
|
||
words = sorted({btn.key for p in pkgs if p.set == set_name and p.variant == "full" for b in p.boards for btn in b.buttons if btn.action == "speak"}, key=str.lower)
|
||
rows = {}
|
||
for w in words:
|
||
e = data.lexicon[w]
|
||
sym = obf.lookup_for_cell(data, w, "speak")
|
||
rows[w] = {
|
||
"pos": e.pos,
|
||
"word_class": WORD_CLASS[e.pos],
|
||
"app_class": (data.app_vocabulary.get(w.lower()) or [None])[0],
|
||
"role": e.role,
|
||
"sense": e.sense,
|
||
"lists": data.in_aac_lists(w),
|
||
"spoken_rank": data.spoken_rank.get(w.lower()),
|
||
"symbol": f"{sym[0]}:{sym[1]}" if sym else None,
|
||
"glosses": len(data.glosses(w)),
|
||
}
|
||
out[set_name] = rows
|
||
return out
|
||
|
||
|
||
|
||
|
||
# ── tables for the design documents ──────────────────────────────────────
|
||
|
||
|
||
def md_table(head: list[str], rows: list[list]) -> str:
|
||
out = ["| " + " | ".join(head) + " |", "|" + "|".join("---" for _ in head) + "|"]
|
||
for r in rows:
|
||
out.append("| " + " | ".join(str(x) for x in r) + " |")
|
||
return "\n".join(out)
|
||
|
||
|
||
def mm(t: dict, key: str) -> str:
|
||
x = t[key]
|
||
return f"{x['width_mm']:.0f}×{x['height_mm']:.0f}"
|
||
|
||
|
||
def core_tables(m: dict, lay, data: Data) -> dict[str, str]:
|
||
t = {}
|
||
rows = []
|
||
for size, s in m["sizes"].items():
|
||
c = s["coverage"]
|
||
rows.append([
|
||
size, s["cells"], s["core_words"], s["pages"], s["words_reachable"],
|
||
f"{c['top25']['direct']}/25", f"{c['top50']['direct']}/50", f"{c['top100']['direct']}/100",
|
||
f"{c['top200']['direct']}/200", f"{c['token_share_direct']:.0%}",
|
||
f"{s['lists']['universal_core']['one_hit']}/{s['lists']['universal_core']['of']}",
|
||
f"{s['lists']['banajee_2003']['one_hit']}/{s['lists']['banajee_2003']['of']}",
|
||
])
|
||
t["core-reach"] = md_table(
|
||
["Size", "Cells", "Core words (1 hit)", "Pages", "Words in reach", "Top 25 spoken", "Top 50", "Top 100", "Top 200",
|
||
"Spoken tokens covered", "Universal Core on the core", "Banajee on the core"], rows)
|
||
rows = []
|
||
for size, s in m["sizes"].items():
|
||
w = s["weighted"]
|
||
rows.append([size, w.get("hits", "–"), w.get("scan", "–"), w.get("touch", "–"), w.get("gaze", "–")])
|
||
t["core-costs"] = md_table(["Size", "Hits per word", "Row-column scan steps", "Touch (Fitts ID, bits)", "Gaze (eccentricity)"], rows)
|
||
rows = []
|
||
for size, s in m["sizes"].items():
|
||
tg = s["targets"]
|
||
rows.append([size, mm(tg, "tablet-landscape"), mm(tg, "tablet-portrait"), mm(tg, "phone-portrait"), mm(tg, "phone-landscape"),
|
||
", ".join(tg["tablet-landscape"]["meets"]) or "none", ", ".join(tg["phone-portrait"]["meets"]) or "none"])
|
||
t["core-targets"] = md_table(["Size", "10.2\" tablet, landscape (mm)", "tablet, portrait", "6.1\" phone, portrait", "phone, landscape",
|
||
"Meets on the tablet (landscape)", "Meets on the phone (portrait)"], rows)
|
||
st = m["stability_core"]
|
||
w = st["worst"]
|
||
t["core-stability"] = "\n".join([
|
||
f"- Pairs of sizes compared (an item present at both): **{st['pairs']}**.",
|
||
f"- Mean shift of a button's centre: **{st['mean_shift']:.3f}** of the board's width or height.",
|
||
f"- Largest shift: **{w['cells']:.2f} cells of the smaller board** (`{w['item']}`, {w['from']} → {w['to']}: "
|
||
f"{w['du']:.3f} across, {w['dv']:.3f} down). Threshold: less than one cell of the smaller board — "
|
||
f"**{'met' if st['within_one_cell_of_smaller'] else 'NOT met'}**.",
|
||
f"- Largest shift between neighbouring sizes of the ladder: **{st['worst_shift_one_step']:.3f}** of the board.",
|
||
f"- Every item's buttons, at every size, share one point of the screen: **{'yes' if st['every_item_keeps_a_common_point'] else 'NO'}**.",
|
||
"- Pages: " + "; ".join(f"{n} {'ok' if s['within_one_cell_of_smaller'] and s['every_item_keeps_a_common_point'] else 'FAILS'}"
|
||
for n, s in m["stability_pages"].items()) + ".",
|
||
])
|
||
rows = []
|
||
names = [s.name for s in lay.sizes]
|
||
for k, size in enumerate(names):
|
||
words = []
|
||
for p in lay.core:
|
||
if p.stage != k:
|
||
continue
|
||
if p.kind == "page":
|
||
words.append(f"**{p.key}** ▸")
|
||
continue
|
||
ev = []
|
||
lists = data.in_aac_lists(p.key)
|
||
if "universal_core" in lists:
|
||
ev.append("U")
|
||
if "banajee_2003" in lists:
|
||
ev.append("B")
|
||
rank = data.spoken_rank.get(p.key.lower())
|
||
ev.append(f"#{rank}" if rank else "#–")
|
||
words.append(f"{p.key} ({', '.join(ev)})")
|
||
rows.append([size, len(words), "; ".join(words)])
|
||
t["core-stages"] = md_table(["Stage", "New", "Words (U: Universal Core, B: Banajee et al. 2003, #: spoken rank)"], rows)
|
||
rows = []
|
||
for name, page in lay.pages.items():
|
||
words = [p.key for p in page.items if p.kind in ("word", "modifier")]
|
||
rows.append([name, page.sizes[0].name, len(words), ", ".join(words[:12]) + (", …" if len(words) > 12 else "")])
|
||
t["core-pages"] = md_table(["Page", "Opens from", "Words", "First words (they appear at the page's first size)"], rows)
|
||
grids = []
|
||
for s in lay.sizes:
|
||
from pslib.layout import core_grid
|
||
grids.append(f"**{s.name}**\n\n```\n{core_grid(lay, s)}\n```")
|
||
t["core-grids"] = "\n\n".join(grids)
|
||
return t
|
||
|
||
|
||
def motor_tables(m: dict, lay, data: Data) -> dict[str, str]:
|
||
t = {}
|
||
rows = []
|
||
for size, s in m["sizes"].items():
|
||
h = s["hits"]
|
||
c = s["coverage"]
|
||
rows.append([size, s["grid"], s["home_words"], s["pages"], s["groups"], s["words"], h.get("1", 0), h.get("2", 0), h.get("3", 0),
|
||
s["mean_hits_unweighted"], s["weighted_spoken"]["hits"], s["weighted_design"]["hits"],
|
||
f"{c['top25']['one_hit']}/25", f"{c['top100']['one_hit']}/100", f"{c['top100']['direct']}/100",
|
||
f"{c['top200']['direct']}/200", f"{c['token_share_direct']:.0%}"])
|
||
t["motor-reach"] = md_table(
|
||
["Locations", "Grid", "Words on Home", "Pages", "Groups", "Words", "1 hit", "2 hits", "3 hits", "Mean hits",
|
||
"Hits per word, spoken weight", "Hits, design weight", "Top 25 in 1 hit", "Top 100 in 1 hit", "Top 100 in reach",
|
||
"Top 200 in reach", "Spoken tokens covered"], rows)
|
||
rows = []
|
||
for size, s in m["sizes"].items():
|
||
w = s["weighted_spoken"]
|
||
rows.append([size, w["hits"], w["scan"], w["touch"], w["gaze"]])
|
||
t["motor-costs"] = md_table(["Locations", "Hits per word", "Row-column scan steps", "Touch (Fitts ID, bits)", "Gaze (eccentricity)"], rows)
|
||
rows = []
|
||
for size, s in m["sizes"].items():
|
||
for name, v in s["tradeoffs"].items():
|
||
rows.append([size, name.replace("_", " "), v["hits"], v["scan"], v["touch"], v["gaze"]])
|
||
t["motor-tradeoffs"] = md_table(["Locations", "Layout", "Hits", "Scan steps", "Touch (bits)", "Gaze"], rows)
|
||
rows = []
|
||
for size, s in m["sizes"].items():
|
||
tg = s["targets"]
|
||
rows.append([size, s["grid"], mm(tg, "tablet-landscape"), mm(tg, "tablet-portrait"), mm(tg, "phone-portrait"), mm(tg, "phone-landscape"),
|
||
", ".join(tg["tablet-landscape"]["meets"]) or "none", ", ".join(tg["phone-portrait"]["meets"]) or "none"])
|
||
t["motor-targets"] = md_table(["Locations", "Grid", "10.2\" tablet, landscape (mm)", "tablet, portrait", "6.1\" phone, portrait",
|
||
"phone, landscape", "Meets on the tablet (landscape)", "Meets on the phone (portrait)"], rows)
|
||
st = m["stability_home"]
|
||
w = st["worst"]
|
||
t["motor-stability"] = (
|
||
f"Home items present at two sizes: {st['pairs']} pairs; mean shift {st['mean_shift']:.3f} of the board; "
|
||
f"largest {w['cells']:.2f} cells of the smaller board (`{w['item']}`, {w['from']} → {w['to']}); "
|
||
f"every item's buttons share a point: {'yes' if st['every_item_keeps_a_common_point'] else 'NO'}."
|
||
)
|
||
rows = []
|
||
d = data.fixed_paths
|
||
for page, spec in d["pages"].items():
|
||
groups = spec.get("groups", {})
|
||
used = {ms.label: sorted(g for g in ms.pages[page]["groups"]) for ms in lay.sizes}
|
||
n = len(spec.get("words", [])) + sum(len(v) for v in groups.values()) + len(spec.get("modifiers", []))
|
||
rows.append([page, n, ", ".join(groups) or "–", "; ".join(f"{k}: {', '.join(v) or 'none'}" for k, v in used.items())])
|
||
t["motor-pages"] = md_table(["Page", "Words", "Groups (by meaning)", "Groups in use, by size"], rows)
|
||
grids = []
|
||
for ms in lay.sizes:
|
||
from pslib.layout import _grid_text
|
||
cells = {cell: key + ("▸" if key in ms.pages else "") for key, cell in ms.home.items()}
|
||
grids.append(f"**{ms.label} ({ms.size.name})**\n\n```\n{_grid_text(ms.size, cells, 11)}\n```")
|
||
t["motor-grids"] = "\n\n".join(grids)
|
||
return t
|
||
|
||
|
||
def lang_table(rep: dict) -> str:
|
||
rows = []
|
||
for l in rep["languages"]:
|
||
if l["code"] == "en":
|
||
rows.append(["en", "English", "ValidatedNativeCorpus", l["prediction_pack"], "source", "source", "–"])
|
||
continue
|
||
gc, fp = l["sets"]["growing-core"], l["sets"]["fixed-paths"]
|
||
rows.append([l["code"], l["name"], l["core_status"], l["prediction_pack"] or "none",
|
||
f"{gc['glossed_share']:.0%} ({gc['glossed_share_by_spoken_frequency']:.0%} by use)",
|
||
f"{fp['glossed_share']:.0%} ({fp['glossed_share_by_spoken_frequency']:.0%} by use)",
|
||
"yes" if any("published core" in n for n in l["needs"]) else ""])
|
||
return md_table(["Code", "Language", "Core status", "Prediction pack", "Growing Core words glossed", "Fixed Paths words glossed",
|
||
"Published native core list exists"], rows)
|
||
|
||
|
||
def fill(doc: Path, tables: dict[str, str]) -> str:
|
||
text = doc.read_text(encoding="utf-8")
|
||
for key, body in tables.items():
|
||
pat = re.compile(rf"(<!-- table:{re.escape(key)} -->\n).*?(<!-- /table -->)", re.S)
|
||
if not pat.search(text):
|
||
raise SystemExit(f"{doc.name}: no <!-- table:{key} --> block")
|
||
text = pat.sub(lambda mt: mt.group(1) + body + "\n" + mt.group(2), text)
|
||
return text
|
||
|
||
|
||
FAMILY_TEXT = {
|
||
"growing-core": {
|
||
"name": "Growing Core",
|
||
"summary": "Core words that grow from 1 to 80 on a board, and no word ever moves.",
|
||
"description": (
|
||
"Our own core vocabulary of the words people say most, in eleven sizes from one button to 80. "
|
||
"A word learned on a small board is in the same place on every bigger one, so reaching for it "
|
||
"carries over. Pages of more words open from the right edge, each with Back in its top-left corner. "
|
||
"Words are coloured by the Fitzgerald Key. You can also start on a bigger board with only the "
|
||
"first words shown, and show the rest later, each in its own place."
|
||
),
|
||
"chooser": "growing-core",
|
||
},
|
||
"fixed-paths": {
|
||
"name": "Fixed Paths",
|
||
"summary": "Every word in one place, reached the same way every time, in 1 to 3 touches.",
|
||
"description": (
|
||
"A motor-plan vocabulary: every word is one, two or three touches from Home, always the same ones, "
|
||
"so the movements become automatic. Made to go back to Home after each word. 30 places (the "
|
||
"largest targets, for eye gaze and head pointers), 60 or 84. You can start with 45 first words "
|
||
"shown and the rest hidden in their places."
|
||
),
|
||
"chooser": "fixed-paths",
|
||
},
|
||
}
|
||
|
||
SOURCES_SHORT = [
|
||
{"name": "Universal Core vocabulary (UNC CLDS / Project Core)", "licence": "CC-BY-SA-4.0"},
|
||
{"name": "Banajee, DiCarlo & Buras Stricklin (2003), core vocabulary for toddlers", "licence": "cited"},
|
||
{"name": "FrequencyWords 2018 (OpenSubtitles), Hermit Dave", "licence": "CC-BY-SA-4.0"},
|
||
{"name": "Mulberry Symbols, OpenMoji, Blissymbolics", "licence": "CC-BY-SA-4.0"},
|
||
]
|
||
|
||
|
||
def families_json(data: Data, pkgs, rep: dict) -> dict:
|
||
"""This generator's families, for tools/pagesets/catalog.py."""
|
||
langs_by_set: dict[str, dict] = {}
|
||
for set_name in ("growing-core", "fixed-paths"):
|
||
out = {}
|
||
for lang in rep["languages"]:
|
||
if lang["code"] == "en":
|
||
out["en"] = {"status": "ValidatedNativeCorpus"}
|
||
else:
|
||
st = lang["sets"][set_name]
|
||
out[lang["code"]] = {"status": lang["core_status"], "glossed": st["glossed_share"]}
|
||
langs_by_set[set_name] = out
|
||
fams = []
|
||
for set_name in ("growing-core", "fixed-paths"):
|
||
d = data.growing_core if set_name == "growing-core" else data.fixed_paths
|
||
packages = []
|
||
for p in (q for q in pkgs if q.set == set_name):
|
||
root = next(b for b in p.boards if b.id == p.root)
|
||
home_words = sum(1 for b in root.buttons if b.action == "speak" and not b.hidden)
|
||
pages = sum(1 for b in root.buttons if b.action == "navigate" and not b.control)
|
||
words = len({btn.key for b in p.boards for btn in b.buttons if btn.action == "speak" and not btn.hidden})
|
||
entry = {
|
||
"key": p.file.rsplit("/", 1)[-1].removesuffix(".obz"),
|
||
"file": p.file,
|
||
"lang": "*",
|
||
"variant": p.variant,
|
||
"size": p.size,
|
||
"rows": root.rows,
|
||
"cols": root.cols,
|
||
"boards": len(p.boards),
|
||
"home": p.root,
|
||
"words": words,
|
||
"home_words": home_words,
|
||
"pages": pages,
|
||
}
|
||
if set_name == "growing-core":
|
||
entry["label"] = p.size.replace("x", " \u00d7 ")
|
||
entry["description"] = f"{home_words} word{'' if home_words == 1 else 's'}" + (
|
||
f" and {pages} pages of more" if pages else ""
|
||
)
|
||
if p.variant != "full":
|
||
entry["reveal_stage"] = p.manifest["reveal_stage"]
|
||
else:
|
||
entry["label"] = f"{p.size} places"
|
||
if p.manifest.get("filter"):
|
||
# Started with fewer words: what each number counts (the
|
||
# words shown first, those of them on Home, all the set's
|
||
# words as it grows), never "40 on Home … 45 in all".
|
||
every = len({btn.key for b in p.boards for btn in b.buttons if btn.action == "speak"})
|
||
entry["description"] = (
|
||
f"{words} words to start with ({home_words} on Home), {every} as it grows, {pages} pages"
|
||
)
|
||
entry["filter"] = p.manifest["filter"]
|
||
else:
|
||
entry["description"] = f"{home_words} words on Home, {pages} pages, {words} words in all"
|
||
packages.append(entry)
|
||
fam = {
|
||
"key": set_name,
|
||
**FAMILY_TEXT[set_name],
|
||
"version": d["version"],
|
||
"licence": "CC-BY-SA-4.0",
|
||
"attribution": data.catalog["attribution"].strip(),
|
||
"sources": SOURCES_SHORT,
|
||
"languages": langs_by_set[set_name],
|
||
"language_note": (
|
||
"The words are English, shown in your language through the app's translations, which are not "
|
||
"your language's own core words. Words without a translation in your language are hidden in "
|
||
"their places; a partner can add their own words there in Edit mode."
|
||
),
|
||
"packages": packages,
|
||
}
|
||
if set_name == "growing-core":
|
||
fam["ladder"] = d["ladder"]
|
||
else:
|
||
fam["recommends"] = {"return_home_after_word": True, "make_home": True}
|
||
# Its first words (the starter filter): a word kept by an earlier
|
||
# version, which does not say the step it is shown from, is placed
|
||
# by them (aac-core samewords::shown_from).
|
||
if "starter" in d.get("filters", {}):
|
||
fam["first_words"] = sorted(motor_filter(data, "starter"))
|
||
fams.append(fam)
|
||
return {"families": fams}
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
ap.add_argument("--check", action="store_true", help="fail if the design documents' tables are out of date")
|
||
ap.add_argument("--out", type=Path, help="write the packages, families.json (for catalog.py), cells.json (for "
|
||
"aac-core's tests) and the reports into this directory")
|
||
args = ap.parse_args()
|
||
data = Data()
|
||
core = load_core(data)
|
||
motor = load_motor(data)
|
||
pkgs = core_packages(data, core) + motor_packages(data, motor)
|
||
|
||
all_words = {btn.key for p in pkgs for b in p.boards for btn in b.buttons if btn.action == "speak"}
|
||
exceptions = class_exceptions(data, all_words)
|
||
cm = metrics.core_metrics(data, core, [p for p in pkgs if p.set == "growing-core"])
|
||
mm_ = metrics.motor_metrics(data, motor, [p for p in pkgs if p.set == "fixed-paths"])
|
||
mx = {
|
||
"about": "Generated by tools/pagesets/build.py. Models and reference lists: DESIGN-core.md and DESIGN-motor.md.",
|
||
"screens": {k: vars(v) for k, v in costs.SCREENS.items()},
|
||
"growing_core": cm,
|
||
"fixed_paths": mm_,
|
||
"endings_the_app_gets_wrong": metrics.endings_report(data, all_words),
|
||
"word_class_differences": exceptions,
|
||
}
|
||
rep = langs.report(data, pkgs)
|
||
|
||
def js(v) -> bytes:
|
||
return (json.dumps(v, ensure_ascii=False, sort_keys=True, indent=1) + "\n").encode()
|
||
|
||
# The design documents' tables: committed, and checked.
|
||
docs: dict[Path, bytes] = {}
|
||
ct = core_tables(cm, core, data)
|
||
ct["languages"] = lang_table(rep)
|
||
ct["endings"] = md_table(["Word", "Ending", "The app says", "Correct"],
|
||
[[e["word"], e["ending"], e["app_says"], e["correct"]] for e in mx["endings_the_app_gets_wrong"]])
|
||
ct["class-differences"] = md_table(["Word", "These sets", "The app's vocabulary.rs", "Why"],
|
||
[[w, v["ours"], v["app"], v["why"]] for w, v in exceptions.items()]) if exceptions else "None."
|
||
no_sym = sorted({btn.key for p in pkgs if p.variant == "full" for b in p.boards for btn in b.buttons
|
||
if not btn.control and not btn.symbol and not obf.lookup_for_cell(data, btn.key, btn.action)}, key=str.lower)
|
||
ct["no-symbol"] = ", ".join(f"`{w}`" for w in no_sym) + f" ({len(no_sym)})"
|
||
for doc, tables in ((HERE / "DESIGN-core.md", ct), (HERE / "DESIGN-motor.md", motor_tables(mm_, motor, data))):
|
||
if doc.exists():
|
||
docs[doc] = fill(doc, tables).encode()
|
||
|
||
if args.out:
|
||
out: dict[Path, bytes] = {}
|
||
for p in pkgs:
|
||
out[args.out / p.file] = obf.obz(data, p.boards, p.root, p.manifest)
|
||
out[args.out / "families.json"] = js(families_json(data, pkgs, rep))
|
||
out[args.out / "cells.json"] = js(index_json(pkgs, exceptions))
|
||
out[args.out / "metrics.json"] = js(mx)
|
||
out[args.out / "languages.json"] = js(rep)
|
||
out[args.out / "words.json"] = js(words_json(data, pkgs))
|
||
for path, content in out.items():
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_bytes(content)
|
||
total = sum(len(c) for p, c in out.items() if p.suffix == ".obz")
|
||
print(f"wrote {len(out)} files to {args.out} ({len(pkgs)} packages, {total / 1e6:.1f} MB)")
|
||
|
||
stale = []
|
||
for path, content in docs.items():
|
||
if args.check:
|
||
if path.read_bytes() != content:
|
||
stale.append(str(path.relative_to(HERE.parent.parent)))
|
||
else:
|
||
path.write_bytes(content)
|
||
if args.check:
|
||
if stale:
|
||
print("out of date (run tools/pagesets/build.py):\n " + "\n ".join(stale))
|
||
sys.exit(1)
|
||
print("the design documents' tables are up to date")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|