All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 4m3s
Changelog: none Co-Authored-By: Claude Fable 5.2 <[email protected]>
280 lines
16 KiB
Markdown
280 lines
16 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
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 carries only free-licensed content, so the
|
||
pictograms never enter it.
|
||
`build_nc.py` writes them, with their review files, to a directory outside:
|
||
|
||
```bash
|
||
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.
|
||
|