open-standards/aac/aac-rs/tools/symbols
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 4f54a8cae0
All checks were successful
standards workspaces build + test / jouleclaw-rs (push) Successful in 2m58s
standards workspaces build + test / aac-rs (push) Successful in 1m3s
standards workspaces build + test / joule-code-rs (push) Successful in 1m9s
standards workspaces build + test / joulecontract-rs (push) Successful in 31s
standards workspaces build + test / map-rs (push) Successful in 33s
standards workspaces build + test / mesh-rs (push) Successful in 32s
standards workspaces build + test / openpay (push) Successful in 1m56s
no-vanity-metrics / Scan committed Markdown for vanity metrics (push) Successful in 3s
standards workspaces build + test / eoc-rs (push) Successful in 1m49s
standards workspaces build + test / jcp-rs (push) Successful in 1m18s
standards workspaces build + test / proof-rs (push) Successful in 1m13s
standards workspaces build + test / sandbox-rs (push) Successful in 2m8s
standards workspaces build + test / smart-byte-rs (push) Successful in 2m35s
standards workspaces build + test / arl-rs (push) Successful in 2m25s
standards workspaces build + test / joule-ui-rs (push) Successful in 2m45s
standards workspaces build + test / jouledesk-rs (push) Successful in 2m16s
standards workspaces build + test / joulehook-rs (push) Successful in 2m53s
standards workspaces build + test / sense-rs (push) Successful in 3m9s
standards workspaces build + test / macOS energy backend (jcp-energy) (push) Successful in 2m46s
feat(symbols): people drawn in no one's skin, gender or age by default
Pictograms must be international in how they show gender, ethnicity and
age. Following Unicode's emoji model (UTS #51):

- Skin. Every person in Mulberry and OpenMoji is drawn in a generic,
  non-realistic colour -- UTS #51's own example, #FFCC22 (OpenMoji's
  lemon would melt into Mulberry's lemon hair, clothes and objects) --
  or in the skin tone the person chooses: one of the five modifiers
  U+1F3FB-U+1F3FF, in OpenMoji's colours (aac_core:🔣:SkinTone).
  The build writes skin as the CSS custom property --aac-skin (OpenMoji's
  skin shadow as --aac-skin-shade) over a neutral fill, so a renderer
  without the style still draws a person neutral, never black.
  - Mulberry: its authors' aac-skin-fill markers where present; for the
    rest, a reviewed decision per symbol in people.toml (a cake's icing
    and a computer case share the skin colour). The build stops on a
    missing or stale decision.
  - OpenMoji: its skin layers, checked element by element against its
    own toned variants.
  A visual audit of all 397 mapped symbols with the skin colour shown as
  magenta, and an adversarial second look, confirmed every call;
  people-review.md lists every symbol that draws a person and how.
- Gender and age. Where Mulberry drew a man's face or a child for a word
  that names neither (the feelings, Speak, eat, drink, doctor, teacher,
  police, hi...), OpenMoji's emoji face (no gender, age or skin) or
  Unicode's gender-neutral person is used instead; police wears a cap
  worn worldwide, not a British helmet.
- Hair keeps its colour whatever the tone.
- App: Settings > Symbols > People in pictures, and the same choice in
  setup; kept with the symbol set and applied on the app root.
- SPEC 4.3 (proposed): people in symbols.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 08:35:11 -04:00
..
bbox feat(aac-web): a symbol in every cell; OBF with licences and languages; 45-language spoken forms 2026-09-30 01:47:08 -04:00
.gitignore feat(aac-core): a licensed symbol library, keyed by concept 2026-09-29 10:50:19 -04:00
build.py feat(symbols): people drawn in no one's skin, gender or age by default 2026-09-30 08:35:11 -04:00
mapping-review.md feat(symbols): people drawn in no one's skin, gender or age by default 2026-09-30 08:35:11 -04:00
overrides.toml feat(symbols): people drawn in no one's skin, gender or age by default 2026-09-30 08:35:11 -04:00
package-lock.json feat(aac-core): a licensed symbol library, keyed by concept 2026-09-29 10:50:19 -04:00
package.json feat(aac-core): a licensed symbol library, keyed by concept 2026-09-29 10:50:19 -04:00
people-review.md feat(symbols): people drawn in no one's skin, gender or age by default 2026-09-30 08:35:11 -04:00
people.toml feat(symbols): people drawn in no one's skin, gender or age by default 2026-09-30 08:35:11 -04:00
README.md feat(symbols): people drawn in no one's skin, gender or age by default 2026-09-30 08:35:11 -04:00
svgo.config.mjs feat(aac-core): a licensed symbol library, keyed by concept 2026-09-29 10:50:19 -04:00

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. Sets under NC or ND terms (ARASAAC, Sclera) and proprietary sets (PCS, SymbolStix, Widgit) are never used.

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.