open-standards/aac/aac-rs/tools/symbols/README.md
Transaction Science edd04264eb check-no-nc.mjs: a file named for a set is caught in every folder
The filename check sat inside the content word scan, which skips the
third-party folders, so vendor/arasaac-loader.js passed. Filenames are now
checked in every folder; only the content scan leaves third-party data out
(its dictionaries hold the Italian word "sclera"). Planted: files named for
a set in vendor/, voices/, models/lang/ (a folder) and pkg/ are caught.
prerender-static.sh's comment and the README now describe both this and the
decompressed magic-byte check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 09:08:06 -04:00

16 KiB
Raw Permalink Blame History

Symbol sets

build.py turns three openly licensed symbol sets into one SVG sprite per set plus a concept → symbol index, so that every cell can show a picture. The app build never downloads symbols: it only reads the committed outputs.

set licence role
Mulberry Symbols 3.6.1 (commit 9cbab9f4) CC BY-SA 4.0, and the symbols themselves must never be charged for default style
OpenMoji 17.0.0 (colour SVG) CC BY-SA 4.0 second style
Blissymbolics BCI-AV 2026-07-25 CC BY-SA 4.0 third style; covers function words the pictographic sets cannot draw

Licences were checked at the primary sources on 2026-09-29; the URLs and full attribution are in crates/aac-web/public/symbols/LICENSES.md. Proprietary sets (PCS, SymbolStix, Widgit) are never used. Two sets under non-commercial terms, ARASAAC and Sclera, are optional: see "Non-commercial sets" below.

Everything this tool writes from the sets is itself CC BY-SA 4.0, not the workspace's Apache-2.0: the sprites, the contact sheets (review/*.png, each titled with its set's attribution) and mapping-review.md. Mulberry's condition also binds the product: the symbols, and symbols derived from them, must stay free of cost and freely available to all, so they can never sit behind a paid tier or a paid hosted library. Only added value (hosting, curation) may be charged for.

Outputs

path what
crates/aac-web/public/symbols/<set>.<sha256-12>.svg sprite: <symbol id="<set>-<concept-slug>" viewBox="…"> per symbol
crates/aac-web/public/symbols/LICENSES.md attribution for every set
crates/aac-core/src/symbols/index.json sets (licence, attribution, file) and concept → {set: slug}
crates/aac-core/src/symbols/normalize_cases.json parity fixture for normalize_concept in Python and Rust
tools/symbols/mapping-review.md every concept, the symbol each set uses, and how it was chosen
tools/symbols/people-review.md every symbol that draws a person, and whether it takes the chosen skin tone
tools/symbols/review/<set>.png contact sheet of every mapped concept, labelled

Board cells are drawn with aac_core::symbols::lookup_for_cell(label, action, preferred) (plain lookup(label, preferred) for a bare word); the sprite file comes from set_info(set).file. Symbol ids carry their set as a prefix (mulberry-home), so sprites can be inlined into one page without clashes; referencing them by file (<use href="symbols/<file>#<id>">) works too.

Rebuilding

cd aac/aac-rs
(cd tools/symbols && npm ci)                            # once: svgo from package-lock.json
python3.14 tools/symbols/build.py                       # everything above
python3.14 tools/symbols/build.py --review-pages /tmp/p # plus 48-per-page sheets for close review
cargo test -p aac-core symbols && cargo test -p aac-web --test symbol_coverage

Needs Python ≥ 3.11 (standard library only), npm (svgo 4.1.0 and its whole dependency tree are pinned by package-lock.json; npm ci installs exactly that into the git-ignored node_modules/, and the build runs it with svgo.config.mjs), rsvg-convert for the contact sheets, and optionally the brotli CLI for size reporting. Sources are downloaded once into tools/symbols/.cache/ (git-ignored) and checked against the SHA-256 pinned in build.py on every run; a mismatch stops the build. The sprites, index, fixture and mapping-review.md are deterministic: rebuilding from the same sources reproduces the same hashes on any machine. The contact sheets depend on the local rasteriser and fonts (committed ones: rsvg-convert 2.63.2 with cairo 1.18.6, pango 1.58.2) and are review aids, not reproducible outputs. A rebuild deletes the previous hashed sprite of each set.

To move to a new release of a set, update its URL and SHA-256 in SOURCES (and the version in SET_INFO), rebuild, and review the contact sheets.

How concepts are mapped

  1. Concepts are extracted from the code, not listed by hand: every cell label of every layout in crates/aac-web/src/defaults.rs and every word of the core vocabulary in crates/aac-core/src/vocabulary.rs, normalised with normalize_concept (lowercase, trim, collapse whitespace, strip trailing ?!.,;).
  2. Automatic candidates come from each set's own index:
    • Mulberry: the symbol name (eat_,_to → "eat"; numbered variants rank after the plain one). Letters, flags, maps and anatomically rated symbols are excluded — Mulberry's "I" is the letter, not the pronoun.
    • OpenMoji: the CLDR annotation. Tags are listed in mapping-review.md as proposals but never accepted automatically (the tag "you" is on the waving hand). Skin-tone variants, flags and brand logos are excluded — the annotation "apple" is the Apple logo.
    • Bliss: the BCI-AV glosses, first gloss before later ones, verb forms (-(to)) for verbs. Gendered forms, (OLD) forms and letters are excluded. Where a vocabulary word is a verb, verb forms are preferred.
  3. overrides.toml decides everything ambiguous, one entry per concept per set with a reason: symbol = "<native id>" or none = true to reject a wrong automatic match. Where one label means different things on different boards, the default-board sense wins ("break" is the rest break of "I need a break", "chicken" is food).
  4. Cell concepts. A cell whose picture must come from what it does gets its own concept instead of its label's: message-bar action cells are keyed by their action (action:speakall, action:clear), and navigation cells whose label is a homograph by their navigation sense (nav:back, an arrow; plain back stays the body part of "my back hurts"). cell_concept() in build.py and lookup_for_cell in Rust implement the same rule; add a label to NAV_SENSES when another navigation label needs its own sense.

A wrong picture is worse than none, so a concept stays unmapped in a set unless the match is confident; the runtime then falls back to the next set.

Sprite safety

Each symbol is optimised by svgo (styles inlined into presentation attributes), then rebuilt from an allow-list of drawing elements and attributes: no <script>, <style>, <foreignObject>, <image>, classes, event attributes or external references survive, and internal ids are prefixed with the symbol's slug so symbols sharing the sprite cannot collide. check_sprite() re-verifies the result and the Rust test sprites_exist_and_contain_every_symbol checks it again.

Locale-sensitive choices (Mulberry's beckoning "come", thumbs for good/bad, the left arrow for Back in right-to-left scripts) are listed at the top of overrides.toml for per-locale overrides later.

Known limits: three symbols carry text drawn in the viewer's fallback font (Mulberry's ambulance lettering and runner number, and Bliss 'count').

People

A board is used all over the world, so its people default to no one's skin, gender or age (SPEC §4.3), following Unicode's emoji model:

  • Skin. Skin is written as the CSS custom property --aac-skin over a non-realistic neutral, #ffcc22 (UTS #51 §2.4's own example; OpenMoji's lemon #fcea2b would melt into Mulberry's lemon hair, clothes and objects), so a client draws every person in the skin tone the person chooses (aac_core::symbols::SkinTone: OpenMoji's colours for the five modifiers U+1F3FB–U+1F3FF). The neutral colour stays as the fill attribute under the property, so a renderer that ignores the style still draws a person neutral, never black. Hair keeps its colour whatever the tone: no hair colour belongs to a skin tone.
    • Mulberry draws skin in one colour, #ffeec8. Where its authors mark the skin (class="aac-skin-fill"), the build tones exactly those elements. Every other mapped symbol that uses #ffeec8 needs a decision in people.toml, checked on a contact sheet with that colour shown as magenta: tone = true when all of it is skin, tone = false with a reason when some is not (a cake's icing, a computer case). The build stops on a symbol without a decision, and on a decision for a symbol no longer drawn.
    • OpenMoji needs no list: its own skin-tone variants draw the same elements in the same order, so the build tones exactly the elements they change to the skin and shadow colours (--aac-skin, --aac-skin-shade), and leaves a badge that shares the skin's yellow alone. A person Unicode gives no skin tones (a family, a silhouette) is listed as drawn.
  • Gender and age. A word that names no gender or age gets a neutral picture: where the default set draws a man's face or a child for such a word, overrides.toml maps it to OpenMoji's emoji face (no gender, age or skin) or to Unicode's gender-neutral person, and the feelings stay one face style. Mulberry's generic figure (bald, shirt and trousers) is kept.

people-review.md lists every symbol that draws a person, and how it is drawn.

Non-commercial sets

ARASAAC and Sclera are licensed for non-commercial use only. A free app with no adverts may offer them, as Cboard and AsTeRICS Grid do; they are never offered in anything paid for, and nothing falls back to them (aac_core::symbols::{SymbolSet::NONCOMMERCIAL, Offering}).

set licence source
ARASAAC (Government of Aragón; pictograms by Sergio Palao) its own: CC BY-NC-SA (per arasaac.org/terms-of-use, no version); our adapted pack: CC BY-NC-SA 4.0 the API's catalogue (api.arasaac.org/v1/pictograms/all/<lang>) and 2500 px renders
Sclera (Sclera vzw) CC BY-NC 2.0 BE the sclera.be downloads picto_{en,nl,fr,de}.zip

The terms, read at the sources on 2026-10-10:

  • ARASAAC (arasaac.org/terms-of-use): resources "are published under Creative Commons License BY-NC-SA, authorizing their use for non-profit purposes providing the source and the author, and are shared under the same license. Therefore, the use of these resources within any product or publication for commercial purposes is excluded." Derived works "must be distributed with the same Creative Commons License BY-NC-SA. The author (Sergio Palao), the owner (Gobierno de Aragón), their origin (ARASAAC) and the license under which is distributed ... must be cited." The ARASAAC logo is required only on signage of public spaces. Its API "is only available for non-commercial applications."
  • Sclera (sclera.be/en/picto/copyright): free use with two conditions, credit "www.sclera.be" as the source, and non-commercial use; commercial use needs a signed agreement. The licence linked there is CC BY-NC 2.0 Belgium, whose section 4 asks for the licence's address with every copy.

Not in this repository. It is mirrored to Codeberg, whose terms of use (§2(1)1) do not accept NC licences, so the pictograms never enter it. build_nc.py writes them, with their review files, to a directory outside:

cd aac/aac-rs
AAC_SYMBOLS_NC_CACHE=/path/to/cache python3.14 tools/symbols/build_nc.py --out ../../../aac-symbol-packs [--refresh]
python3 tools/symbols/check_packs_nc.py ../../../aac-symbol-packs   # what a deployment checks

What is committed here is ours, or a fact about theirs: build_nc.py, overrides-nc.toml (decisions by pictogram number), nc-sources.lock.json (every source file's URL, date and SHA-256; a rebuild stops on any change), crates/aac-core/src/symbols/index-nc.json (the sets, their licences, concept → symbol id, and each pack's name, size and SHA-256), and the NC part of LICENSES.md. Needs Python ≥ 3.11 with PyYAML, Pillow and NumPy, and cwebp (the libwebp version is recorded in the catalogue).

Mapping by meaning. The concepts are build.py's plus the page sets' words (tools/pagesets/data/lexicon.en.yaml) and the symbols the phrase sets ask for (symbol, want_symbol). A candidate's keywords in every language both sides have (23 for ARASAAC; nl, fr and de for Sclera, whose four downloads are joined by identical images) are compared with the app's audited glosses: a match needs an English keyword and at least two other languages agreeing (a quarter of those compared), or, without an English keyword, more than half of at least four. Among equals, ARASAAC's own core-vocabulary pictograms (its aac drawings: a plain figure, no gender or age) are preferred, and a child figure is passed over unless the concept is about children. Sclera's "allowed" (green ground) and "not allowed" (red cross) versions are never used: its file names often omit the qualifier ("eat" is a crossed-out plate).

Checks that stop the build.

  • One picture per board. No two concepts on one board may draw the same pictogram: two cells would look the same. The boards are every default board (defaults.rs, board by board) and every page set's boards (the page-set generators are run into a scratch directory). Where ARASAAC draws two words of a board with one picture, overrides-nc.toml gives the second its own picture in the right sense, or none (an open set draws it).
  • No drawn words. The words drawn in every chosen pictogram are read with Apple Vision (ocr_vision.swift, built with swiftc into the cache; macOS only, elsewhere the build says the check did not run). Words a reader of another language would need (Spanish forms, "School") mean another picture or none. What may stay (sums on a blackboard, "BLA BLA", the euro on ARASAAC's money) is listed with a reason in [arasaac-text] and [sclera-text]; readers on different Macs see different things, so what any of them reads is listed. The Spanish ¡ and ¿, which the reader does not report, are checked by eye on the contact sheets. Where the reader cannot run (not macOS) the build stops, unless --skip-drawn-text-check says the packs are not being rebuilt for a release.

Not in a build without them. The optional sets' catalogue, the pack loader and every word about them are behind the nc-symbols cargo feature (aac-core and aac-web). The standard's own build, and a deployment's build without it, hold nothing that names them; their section of LICENSES.md is LICENSES-nc.md beside the packs, which a deployment that ships them appends. prerender-static.sh stops if its site holds a pack (under models/symbols/, named *.pack, or starting with a pack's magic bytes AACSYMP1 as stored or, for a .br/.gz sidecar, once decompressed, wherever it is), if sw.js precaches one, if a file in any folder is named for a set, or if any file of the app's own names a set in its content (.gz/.br sidecars decompressed; the third-party data in voices/, ai/, asr/, ae/, vendor/ and models/lang/ is left out of that word scan only): node tools/symbols/check-no-nc.mjs <site> [app/], tested with planted files by node --test tools/symbols/check-no-nc.test.mjs.

People drawn without skin. A few ARASAAC people are line drawings with no skin colour, so they never take the chosen tone (the family-tree aunt, uncle and cousin; nobody; the hands of hard and soft). They are kept only where ARASAAC has no picture in the same sense that takes the tone, each with its reason in [arasaac-people], and listed in people-review-nc.md.

People. ARASAAC's API draws a pictogram's skin in a colour it is asked for. The skin=white and skin=black renders differ exactly where skin is drawn; each pixel is unmixed linearly between the two skin colours, so a pack carries the picture with its skin cut out and a grey mask of the skin. The app paints the chosen tone through the mask (--aac-skin, Unicode's neutral by default, as for the other sets); every pictogram is checked by re-drawing ARASAAC's own skin=white render from the two layers. Hair keeps ARASAAC's colour: no hair colour belongs to a skin tone.

Packs. <set>.<sha256-12>.pack: a JSON header (the licence it is shared under, the set's own where it differs, attribution, conditions, source and its version, the changes made, symbols) and WebP images: ARASAAC's at 320 px, near-lossless, with lossless skin masks; Sclera's at its own 283 px, lossless. A deployment serves them under {base}/models/symbols/ and builds the app with AAC_SYMBOL_PACKS=arasaac,sclera. The app offers them only while the device is signed out or on a free plan (aac-web account.rs: any paid plan hides them and draws the open sets). It downloads one only when a person chooses its set, checks its size and SHA-256, keeps it in IndexedDB (never Cache Storage, which iPad Safari evicts), and puts its symbols in the page, so cells, the picker, print and OBF export draw them like any other. Print credits and every exported picture carry the set's attribution, conditions and licence.