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]>
16 KiB
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
- Concepts are extracted from the code, not listed by hand: every cell
label of every layout in
crates/aac-web/src/defaults.rsand every word of the core vocabulary incrates/aac-core/src/vocabulary.rs, normalised withnormalize_concept(lowercase, trim, collapse whitespace, strip trailing?!.,;). - 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.mdas 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.
- Mulberry: the symbol name (
overrides.tomldecides everything ambiguous, one entry per concept per set with a reason:symbol = "<native id>"ornone = trueto 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).- 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; plainbackstays the body part of "my back hurts").cell_concept()inbuild.pyandlookup_for_cellin Rust implement the same rule; add a label toNAV_SENSESwhen 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-skinover a non-realistic neutral,#ffcc22(UTS #51 §2.4's own example; OpenMoji's lemon#fcea2bwould 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 thefillattribute 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#ffeec8needs a decision inpeople.toml, checked on a contact sheet with that colour shown as magenta:tone = truewhen all of it is skin,tone = falsewith 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.
- Mulberry draws skin in one colour,
- 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.tomlmaps 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.tomlgives 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 withswiftcinto 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-checksays 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.