# 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/..svg` | sprite: `` 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/.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 (``) 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 = ""` 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 `