# CreatorStudio Suite — Screens & Features

A visual tour of the product. The suite is three web apps behind one portal: **CreatorStudio** (design
what the fleet says), the **Engine dashboard** (watch it run and prove it), and the **Vehicle player**
(hear it on the bus). Every image below is a live view of the running apps — captured **12 August
2026** (a few panels 31 July) from a cloud deployment of the suite (an Azure VM) riding the **live
Baltimore MTA feed** — on the August day of capture the engine was tracking **7 300+ real vehicles,
3 077 of them on a journey** — alongside an **Arriva rail** trigger configuration and a **Vilnius
(Judu)** LED-sign set, so each screen shows the product operating on real fleet data, not a staged
demo.

## Platform home

![Platform home](/docs/media/platform-home-dark.jpg)

*In the capture: the portal home in its dark theme. Each card states its promise in one line — the
Documentation Center's "Every document in one place", the Player's "plays that vehicle's
announcements the instant the engine speaks them, and reports back what actually played", the LED
Signs card's "rendered with the exact pixels the engine drives the real signs with" — with
capability chips (Playlists, Triggers, TTS & SSML, Lexicon; Any device, No install; Pixel-identical,
Kiosk ready) summarising what waits behind each door.*

The portal landing page — five doors into the suite from one origin: the **Documentation Center**
(every document, one place), **CreatorStudio** to author announcements, the **Engine Monitor** to
watch the live fleet, the **Vehicle Player** to turn any device into a bus speaker, and **LED Signs**
to turn any screen into a vehicle's sign. One sign-in, one design system, everything served together
— and the same doors are always one click away on the suite rail inside each app.

### Documentation Center

`/docs` — the one entry point for everything written about the platform: the **handbook** (this
document and its siblings stitched into one continuous read, assembled *live* from the repo on every
request, so it can never go stale against a build), the stakeholder and showcase **decks**, the
printable leave-behinds, and the **raw sources**. Chapters deep-link, and the Markdown is served raw
for anyone who wants to diff it. Neither app carries a documentation panel of its own — they link
here, so there is exactly one published copy.

![Documentation Center — the handbook](/docs/media/docs-center-handbook.jpg)

*In the capture: the handbook's chapters — Overview & Benefits through Architecture, Hanover
LED Signs, Security, Licensing & Packaging, Test Report and Setup & Reference — each a tile naming
the source file it is assembled from, under the two ways in: "Read in the browser" and the raw live
Markdown. (Captured before the newest chapter joined: the as-built **Product Requirements** —
`PRD.md` — now sits as section 3, between Screens & Features and the Functional Specification.)*

![Documentation Center — decks and leave-behinds](/docs/media/docs-center-decks.jpg)

*In the capture: the presentations grid — the 28-slide Product Deck (arrow keys to present, N for
presenter notes, ?print for PDF), the Stakeholder Master Deck, the Showcase Presentation, System
Overview & Architecture, the printable leaflet and one-pager, the Software Team Briefing, and "Built
with AI — By the Numbers", the measured record of this product's own build.*

![Documentation Center — the sources](/docs/media/docs-center-sources.jpg)

*In the capture: the bottom of the page states the contract — the handbook is generated, so you edit
the sources (OVERVIEW.md, SCREENS.md, ARCHITECTURE.md…) and never the built output — and "Where
things live" maps the repo: docs at the root, screenshots under docs/media/, decks under
docs/showcase/. This page is the only place the docs are published; every app's Documentation link
comes back here.*

### User Manual

`/manual` — the Documentation Center's learn-by-doing companion (added after the capture above, so the
portal now has six doors): an **interactive, use-case-based user manual** embedded in the suite. Each
guide is a goal a real user walks in with — *"make the bus announce the next stop"*, *"fix a
mispronounced stop name"*, *"prove what passengers actually heard"* — and walks the real screens step
by step over the same live screenshot set this document embeds, with a deep link into the running app
at the moment each step needs it. Guides are searchable ("What do you want to do?"), filterable by
topic, and tickable — per-step progress lives in the browser, so a new author can work through the
catalogue over days. The suite rail carries a **Manual** entry at its foot in both Angular apps, so
help is one click away from inside the product, and `/help` lands there too.

## CreatorStudio — design the announcements

### Assistant — describe it, don't build it

![Assistant](/docs/media/creator-assistant.jpg)

*In the capture: the Assistant's entry screen — four worked example prompts ("Play a chime when the
doors open, and say 'stand clear of the doors' as they close", "Warn passengers 200 metres before
their stop, and only after 22:00"…), a free-text box, and the promise printed right under it:
"Nothing changes until you review and apply."*

Describe what passengers should hear — *"announce the next stop inside the bus, and the route and
destination outside"* — and the assistant proposes the **playlists and triggers** that produce it:
the wording, the variables, the output channel, the thresholds, and a custom rule when the request
isn't a journey moment ("only after 22:00").

Nothing changes until you say so. Every proposal is itemised on a review card — what it would create,
what it would switch on, and what it would **replace** — each item can be unticked, and applying the
whole plan is a **single undo**. The model never writes the config: its answer is rebuilt field by
field against the real catalogue first, so an invented trigger type, a fact the vehicle feed doesn't
carry, or a `{variable}` the engine can't resolve is dropped and reported rather than published.

**It runs on your own machines.** The assistant asks the portal first, which talks to any
OpenAI-compatible model — a local Ollama or LM Studio, or a hosted provider if you prefer — so no
announcement text has to leave the site and there is no per-call cost. The screen names the model that
answered. With nothing configured it falls back to a cloud service, and then to a keyword draft marked
**Offline draft**; the review-and-apply path is identical either way.

### Playlist library

![Playlist library](/docs/media/creator-playlists-library.jpg)

*In the capture: the Baltimore project's full repertoire — 18 playlists, 16 of them interior
(Approaching Stop, Arrived at Stop, Doors Open, Connection Info, Final Destination Announcement…),
one exterior (Route to Destination) and one on both channels ("Bus not in service. New trip starting
soon"), each row showing its language, voice, element count and last-edited date at a glance — and,
in the left nav, the **Audio sub-menu** the playlist family lives under: Playlists, Voice, TTS
suppliers, Pronunciation, Volume.*

Every announcement is a **playlist** — an ordered list of spoken text, live variables, pauses and audio
clips. They're grouped by where they play (interior / exterior / both) with language and element count
at a glance. Create, duplicate or edit one inline; a worked-example library ships to copy from. Edits
go live fleet-wide the moment you **Publish** — no redeploy.

### Playlist editor

![Playlist editor](/docs/media/creator-playlist-editor-next-stop.jpg)

*In the capture: the interior "Next Stop" playlist — speaker routing offered as Interior / Exterior /
Both, one voice chosen for the whole playlist, an "Also speak in" picker ready to add a bilingual
repeat, and a single static-text element "The next stop is {nextStop}." at 100 % volume with its own
volume slider and an optional custom voice for just that line.*

Build one announcement element by element: **static text**, **dynamic variables** (`{nextStop}`,
`{destination}`, `{connectingServices}`…), pauses and pre-recorded clips. Set the voice per playlist —
or per line — and the speaker routing. "Also speak in" repeats the whole announcement in a second
language and voice for bilingual stops, and **Preview** auditions it through the real TTS voice.

### Triggers — one unified list

![Trigger list](/docs/media/creator-triggers-matrix.jpg)

*In the capture: the whole rule set on one screen — All (39) = the 33 built-in transit triggers plus
Baltimore's custom rules, filterable to Active (7) / Off (32) — Journey: Activated and Running, Doors
Close, Time to Stop, the three distance thresholds, Approaching Last Stop, Detour, Onward Connections
and Service Alerts among the rows, each with its playlist assignment and its LED SIGNS column, because
audio and signs are parallel outputs of one event. "Assign default playlists" and "Assign default LED
signs" wire the standard setup in one click each.*

Built-in and custom triggers live in **one list** — 33 built-in transit triggers (approaching /
arrived / departing a stop, doors, stop request, detour, disruption, last stop and more) and every
custom rule beneath them, with Active / Off / All filter chips and a **Geofences** tab beside. Switch
each on per service and map it to the playlist it should speak, with conditions (distance / time
thresholds, speed, occupancy) and a priority. The engine fires them automatically from the live trip
feed.

![Trigger row expanded](/docs/media/creator-triggers-expanded.jpg)

*In the capture: "Arrived at Stop" opened in place — the AUDIO group (single playlist or an
interior/exterior split, sequences and repeats), the PREREQUISITES gates (doors, speed), PRIORITY
level 5 with interrupt / queue toggles, and the LED SIGNS group binding one template per face,
previewed as live amber pixels: "20 CITY CENTRE" on front and side, the bare "20" on the rear.*

Expanding a row reveals everything that trigger does: its audio (a single playlist, or split
interior/exterior channels, each optionally a **sequence** with a repeat count), its **prerequisite
gates** (door open/closed, stop button, speed window — the announcement is suppressed unless every
set gate holds), its **priority** and interrupt/queue behaviour, and the **LED layout per vehicle
face** for as long as it fires.

### Custom (fact-based) triggers

![Custom triggers](/docs/media/creator-custom-triggers.jpg)

*In the capture: an Arriva rail rule built from raw PIS data — "2 min before next stop (fallback, no
side/transfer data)": WHEN time to the next stop is above 0 s and at most 120 s AND the journey state
is JOURNEY_RUNNING AND (in a nested NOT group) neither the exit side nor any onward connection is
known — THEN play the basic two-minute announcement and wait 180 s before it may fire again, with the
per-face SIGN bindings folded beneath.*

When no built-in trigger fits, compose your own: a **Scratch-style boolean expression** (ALL / ANY /
NOT) over **40 live journey facts** — doors, speed, occupancy, connections, exit side, the stops
ahead, the destination code the driver keyed in, traction-battery charge, even the engine clock and
calendar. **Try it** feeds sample values and tells you whether it would fire. The very same evaluator
then runs in the engine — and the same facts gate the LED sign cycles, so a rotation and an
announcement are driven by one rule set.

![Custom triggers driving LED faces](/docs/media/creator-custom-triggers-led.jpg)

*In the capture: two Vilnius depot triggers built from raw PIS data — "Trip id matches pattern
`-[abc123]*d[123]*-`" and its inverse — each also driving per-face LED layouts, previewed as live
pixels: "20 GRĮŽTA Į PARKĄ" (returning to the depot) on one, "20 CITY CENTRE" on the other, with the
"Try it" sample-value tester folded beneath.*

#### Use case: a rail operator's announcement scheme, no code

![Arriva trigger set](/docs/media/creator-triggers-arriva.jpg)

*In the capture: the Arriva tenant's live rule set — eight active triggers, all custom: "2 min before
next stop" in a side-and-transfers-aware variant and a data-poor fallback, "2 min before the last
stop" and "reached the last stop" each in exit-side/connections and basic variants, and "2 min before
departure from the first stop" split by fast vs local train on the trip-id pattern — every one mapped
to its own Arriva playlist.*

The custom-trigger builder is expressive enough to carry a complete operator scheme: Arriva's train
announcements are **eight custom rules and zero code** — each pair degrading gracefully from
"exit side and onward connections known" to a fallback wording when the feed is thinner, and the
welcome announcement choosing its phrasing by matching the trip id against fast-train patterns
(`snel|express|IC`).

### Geofence zones

![Geofence zones](/docs/media/creator-geofence-zones.jpg)

*In the capture: the Geofences tab beside the trigger list — two zones over the dark Baltimore map, a
free-form purple polygon covering downtown and an amber circle to the north — named, colour-coded and
listed on the left with edit / duplicate / delete actions, with "New circle" and "New polygon" ready,
each zone usable as an "inside / outside" condition in any custom trigger.*

Draw named zones on the map — **circles** or free-form **polygons** — and use "inside / outside this
zone" as a condition in a custom trigger. Drag a corner to reshape, click an edge to add one. Announce
a park-and-ride reminder inside a depot or a chime crossing a boundary — location-driven, no code.

### LED Signs — layouts, displays & vehicles

Author the fleet's **matrix signs** alongside the spoken playlists. The LED Signs screen (`/led-signs`)
is organised into tabs:

- **Layout templates** — reusable pixel canvases (presets by face — front / side / rear / interior).
  Create, duplicate or delete; each row shows a live thumbnail of the layout. The list is a full
  work surface: search by name, sign type or category, filter by panel size, usage or **category**,
  group by **category** (the default), sign type (catalog panel / face), market or usage, sort by
  name / size / elements / usage, fold groups away individually or all at once, and multi-select rows
  or whole groups for bulk duplicate / delete.
  **Categories are the filing drawers.** A config fills up fast — the standard family is seeded for
  every sign resolution, the via and main-stops seeders add one per size, presets and destination
  copies add more — so every template is filed: in the category its author gave it, or in the family
  its id says generated it (*Standard sign layouts*, *Vilnius (Judu) sign set*, *Via destination*,
  *Main stops*, *Not in service*, *Destinations*), and anything else under *My templates*. Existing
  configs therefore open already sorted, with nothing to migrate. Select any number of rows and
  **Move** them into a drawer — pick one in use or type a new name — or **Unfile** them again; it is
  one undo step. A template authored while a category filter is on is filed there, a copy keeps the
  drawer of the template it came from, and the layout editor carries the same Category field.
- **Displays** — cycle trees that pick a layout by trigger / journey state and rotate over time
  (the MatrixRenderer *Display → Cycles → Layout* contract). Same list controls: search, group by
  sign type or usage, sort, and bulk actions.
- **Vehicles** — the hardware roster: Ultima model picker, resolution, display type, position,
  FF address and colour mode; "Assign default templates" wires a standard setup in one click.
  A fleet carries many vehicle configurations and each one opened out is a page of its own, so the tab
  is a **list**: one row per vehicle — name, *In use*, sign count, the faces it covers, and a warning
  chip counting signs with no display bound (they show nothing) — and the full roster only for the
  rows you open. It opens on the vehicle **in use**; putting another one in use opens it too. Filter
  by name, position or resolution, and expand / collapse all.
- **Vehicle faces** — bind each physical sign to a **Template display** or to **Announcement text**
  (mirror what is spoken on the interior).
- **Symbols** — map a live value (e.g. line code) to a pictogram image on the sign.
- **Route colours** — the GTFS `route_color` / `route_text_color` per line, as a table the vehicle
  carries; full-colour panels honour it, single-colour panels ignore it, and a layout's own colour
  rule still wins so legibility is never overridden by branding.
- **Abbreviations** — text-fitting rules shared with the fit checker: whole-value or per-word
  replacements, optionally applied *only when tight* so the full text keeps appearing wherever it
  fits.

![LED layout templates](/docs/media/creator-led-templates.jpg)

*In the capture: 376 templates staying navigable — grouped by category with the Destinations and
Main stops drawers open, every row carrying a live pixel thumbnail, its panel model (SS3-FF 16×120,
Ultima 32×240…), an element count and where it is used, with search, filters and bulk selection
above.*

![LED vehicle roster](/docs/media/creator-led-vehicles.jpg)

*In the capture: the "Example vehicle (US)" roster, in use with six physical signs — Front, Front
extra, Left, Right, Rear, Driver — each with its catalog panel, resolution, FF address and colour
mode, and a live thumbnail of what that face renders right now: "20 CITY CENTRE" on the destination
faces, the bare route number on the rear and driver panels. The three-step banner above spells out
the model: a vehicle is its signs, each sign shows a display, a display picks templates.*

![LED abbreviations](/docs/media/creator-led-abbreviations.jpg)

*In the capture: the abbreviation table that lets one text fit every panel — all twelve Baltimore
CityLink brands mapped to two-letter codes (CityLink RED → RD, BLUE → BL…), each rule with its
"when tight" switch. The full brand fits a 200-column front sign; its code fits the 40-column driver
panel; the same rules feed the fit checker.*

![LED symbols](/docs/media/creator-led-symbols.jpg)

*In the capture: value → pictogram mappings driven by the Vilnius feed — a destination containing
"Oro uosta" (airport) wears the plane symbol, "Stotis" (station) the train, "apylanka" (detour) the
diversion sign — with an upload slot beside the built-in set for a fleet's own artwork.*

![LED route colours](/docs/media/creator-led-route-colours.jpg)

*In the capture: the per-line colour table — red "ordinary city line" chips for lines 11, 1 and 5,
the green express 3G, the blue-and-yellow night N9, and Baltimore's CityLink BLUE and RED — each with
background / text / outline swatches and a live rendered chip, importable straight from GTFS.*

Layouts and displays publish with the rest of the config; the engine rasterises them with the same
Luminator **FNT** fonts the hardware ships and emits mono or **RGB Mobitec FF** frames — or
**Hanover HCPS/SuperX** frames for signs rostered with the Hanover protocol (vendor-independent).

### Destinations — the pre-programmed destination lists

Reached from **LED → Destinations**: the left nav nests it under LED (and `/led-signs/destinations`
redirects there), because a destination is not a peer of the sign authoring — it is what those signs
show for a code the driver enters. The LED Signs screen links across to it as well.

A **destination** is the code a driver keys in and the text every sign then shows. The PIS-PT feed
selects one by number (`pis/0/destination`'s `number`, optionally scoped to a line by
`externalDisplay.lineCode`) — the standard says the number alone is the normal case, so without a
stored list a code-only selection leaves the signs blank.

The Destinations screen (`/destinations`) holds those lists, and the editor is **WYSIWYG — you type on
the sign**:

- Pick a code on the left; the vehicle-in-use's panel fills the editor, rendered by the same
  rasteriser the engine publishes with: the sign's real resolution, its FNT font, its colour mode, its
  symbols and abbreviation rules. Not a mock-up of the sign — the sign.
- **Click any text on the panel and type.** The editable areas are outlined and labelled (Destination,
  Row 2, Via, Line number…); the caret sits over the pixels and every keystroke re-renders them. Tab
  and Shift+Tab walk the fields, Enter keeps, Esc puts the value back. A newly added destination opens
  straight into typing. Areas the author cannot change — the template's own words, the live
  "Stopping at:" row, a value the template pins — say so instead of pretending to be editable.
- Which field sits behind which area is discovered by probing the template, so it works for plain
  `{token}` text, raw expressions and priority chains alike: on a destination that has a via, the
  via-priority area edits the **via**, because that is what the sign is showing.
- **Arrange it freely.** Drag any area to move it, pull its handles to resize, and use the toolbar to
  set horizontal and vertical alignment, the **font size** (Auto fits the biggest that will go —
  pinning a size is how two signs are made to match), what happens when the text is too long (scale /
  scroll / clip / wrap), the exact X / Y / W / H, the front-to-back order, or to remove it.
- **All text settings** opens the *same inspector the template editor uses*, on the selected area:
  content and `{variables}`, the raw expression, the text rules (letter case, show-only-part, rotate
  the parts of a value, symbol slot), the font ladder and character spacing, the scrolling switches
  and speed, alternations, blink, and the text / outline colours — plus the element type itself, which
  keeps the name and box it occupied. An area driven by a destination field says so, so its *Content*
  (the `{token}` that fetches the value) is never mistaken for the value: that is still typed on the
  sign. Because arranging already forks the template to this destination, none of it can disturb the
  others.
- **Add text** puts another line on the sign: a destination field, or **static text** you type in
  place. **Add shape** draws a **line**, **box** or **circle** — separators, frames and highlights,
  with a thickness and a solid/outline switch. **Add symbol** offers the built-in **public-transport
  pictograms** — accessibility (wheelchair, priority seat, pram), on-board facilities (bicycles,
  Wi-Fi, air conditioning, power, toilet, luggage, CCTV), vehicle modes (bus, **school bus**, **rail
  replacement bus**, tram, train, metro, ferry), wayfinding (arrows, **Park & Ride**, exit, lift, stop
  requested) and notices (no smoking, no entry, info, warning, time, ticket, tick, cross). **Add image**
  places your own PNG. Everything lands where there is room and is then dragged and sized like
  anything else.
- **A pictogram per panel height.** The picker has a size switch — *this sign*, or 8 / 12 / 16 / 24 px —
  and each height is a separately drawn glyph, previewed exactly as the panel will show it. A curve
  rasterised down to 12 pixels is a smudge, so the sizes a fleet actually runs are drawn by hand and
  the shapes only take over on panels no hand-drawn size fits (roughly 20 px and up, where they are
  honest). The engine draws from the same definitions, so the sign and the preview agree.
- Arranging gives **that destination its own copy** of the template, bound to that sign, and says so:
  every other destination keeps the shared one. *Use the shared template* puts it back.
- **Fit is checked as you type**: an area whose text no longer fits turns red with the exact pixels it
  needs versus what it has, and the sign tabs carry a badge — so a name that fits the front but not
  the rear is caught at the keyboard, not on the street.
- **Characters the sign font cannot draw are named, not silently changed.** The Luminator FNT set is
  latin-1 (ANSI, `dfCharSet 0`) and its substitution glyph is `@`, so Lithuanian, Latvian, Polish and
  Czech letters have no artwork: `Zuikių g.` reached the panel as `Zuiki@ g.`, and the files' blank
  placeholder cells punched 25px holes into words. The renderer now falls back to the base letter
  (`ų → u`, `ž → z`, `ė → e`) — measuring and rasterising fold identically, so nothing shifts — and
  the fit check reports the substitution on the element ("ų → u"), in the editor and in **Check fit**.
  Only a character with no latin base at all still shows the font's `@`. Proper diacritics would need
  Baltic / Central-European FNT files from Luminator; nothing else in the pipeline is in the way.
- Switch panels with the sign tabs, or glance at **On the other signs** below to see the same
  destination on every face at once. Zoom is 2×–8×.
- Code, line code and out-of-service sit beside the panel; rows, symbols, notes and an optional
  **template per sign** for one destination live under **More**. Duplicate codes are flagged — only
  the first would ever be reachable.
- **Check fit** runs the whole list through the vehicle's templates in one pass, with the same report
  (and one-click abbreviations) the LED Signs screen uses.
- **Review sheet** (`/destinations/review/:id`) — a printable page of every code, its text and its
  rendered signs, with an approval block: Print → Save as PDF, or send it for sign-off.

![Destination editor](/docs/media/creator-destinations-editor.jpg)

*In the capture: the "Vilnius (Judu) destinations" list in use, code 5 — Fabijoniškės on line 3G —
open in the editor. The panel renders "3G Fabijoniskes" as live amber pixels with the honest warning
that the latin-1 sign font draws š and ė as their base letters; four of the six sign tabs carry red
fit badges, and "On the other signs" shows the same code on every face at once — including the Right
panel clipping to "abijoniskes", exactly the problem the badges flag, and the green route-colour flag
on the full-colour front-extra panel.*

![Destination symbol picker](/docs/media/creator-destination-symbols.jpg)

*In the capture: the built-in pictogram library opened from the editor — accessibility, on-board,
vehicle, wayfinding and notice symbols, each previewed as real LED dots, with the size switch (8 /
12 / 16 / 24 px) choosing between separately hand-drawn glyphs, so what the picker shows at 16 px is
pixel-for-pixel what a 16-px panel will display.*

The list in use publishes with the rest of the config; the engine resolves an incoming code through it.
Each list has a **Priority** setting (control bar on the Destinations screen) that decides how a matched
code applies against the live feed:

- **Signs win, feed for speech** (default) — the stored text takes priority on the SIGNS; spoken
  announcements follow the live feed (using the stored name only when the feed sends none, so a
  code-only journey still speaks a destination).
- **Signs & speech win** — the stored text takes priority on both the signs and the announcements.
- **Feed always wins** — the live feed always wins; the stored list is only a lookup fallback for what
  the feed doesn't send.

A partial entry only overrides the fields it defines; a code with no list match leaves the live feed
untouched, whatever the priority. The **destination code** and the **line code** are also trigger facts,
so a custom trigger can fire on a specific pre-programmed destination. The **Live Fleet Dashboard →
Monitor → Displays** drawer shows the current **Destination code** and whether it resolved to a
pre-programmed entry (which then takes priority) or falls through to the live feed. With `DESTINATION_LIST_PUBLISH=true` the engine also
publishes the list itself on `pis/0/list/destinations` so a driver console can offer the codes — off by
default, since a real PIS system may own that topic.

### Layout editor

![Layout editor](/docs/media/creator-led-layout-editor.jpg)

*In the capture: the 200×24 "Destination + Main stops" template at 6× zoom — the Destination element
selected on the canvas with its inspector open (exact X/Y/W/H, content "{destinationMain}", an
expression override, letter-case and symbol-slot rules) while the live-preview strip below renders
the very frame the sign would show for the sample journey state.*

A visual pixel editor for one layout: drag and resize **Text**, **Image** and **Rectangle** elements on
a canvas that matches the sign resolution. Set overflow (Clip / Scale / Wrap / Scroll / WrapScale),
FNT font ladder, alignment, scroll, alternation, blink and colours. Drop in the same live variables
the audio playlists use (`{nextStop}`, `{destination}`, `{lineCode}`…) or a `textExpression` over
`globalState`. Colour rules restyle the whole face when a field matches a value. Sample journey state
drives an amber / RGB preview so what you see is what the bus shows.

### Display editor

![Display editor](/docs/media/creator-led-displays.jpg)

*In the capture: a display's three cycles — Not in service, Via destination, Main stops — each with a
visual trigger ("Vehicle is not in service", "Destination has a 'via'") and, beneath it, the raw
expression it compiles to; the Main stops cycle rotates two layouts on a 10 s / 5 s beat, and the
live preview renders the winning layout ("20 CITY CENTRE") for the editable sample state.*

Compose the **cycle tree** for one display: ordered cycles, each with a visual trigger condition
(the same Scratch-style builder as custom triggers) that compiles to an `enabledExpression`, and
timed rotations that point at a `layoutId` for a duration. A live preview shows which layout wins
for the current sample state — the same resolver the engine uses at runtime.

### Triggers → LED faces

On both the built-in trigger matrix and custom triggers, bind a **layout per vehicle face** (front /
side / rear / interior) for as long as that trigger is firing. Audio playlist and LED override are
parallel outputs of the same event — speak "Next stop Central" and show a dedicated destination
layout at the same moment. "Assign default LED signs" fills the usual in-service /
not-in-service defaults.

### Route simulator

![Route simulator](/docs/media/creator-simulator-route26.jpg)

*In the capture: Baltimore route 26 (Patapsco Station – Mondawmin), the 04:59 PATAPSCO LR trip with
its 52 stops, mid-replay — the vehicle-in-use's LED faces rendering live per position ("26 PATAPSCO
LR", the front-extra panel scrolling "Stopping at: Mond…"), door / stop-request / journey-state
override controls, the event timeline auditioning each spoken line at Mondawmin Metro Station and
PULASKI ST & BRYANT AVE with interior / exterior badges, and the full trip traced on the map.*

Replay a real **GTFS** trip stop by stop and see exactly what would be spoken **and shown on the LED
signs** at each event — *before* anything reaches a vehicle. The timeline models trip-start,
approaching / arrived / departing, doors and stop-request (custom triggers interleaved); each row
auditions through the real TTS and previews the active matrix layouts. Catch a silent stop, a wrong
variable or a blank face on the bench.

### Pronunciation lexicon

![Lexicon](/docs/media/creator-settings-pronunciation.jpg)

*In the capture: the 69-entry lexicon with the clickable IPA phoneme palette and Import GTFS above,
GTFS-imported expansions (ENT → "Ear Nose and Throat", EXPY → "Expressway", FS → "Far Side", GBMC →
"Greater Baltimore Medical Center") each with a play button, an approval mark and a blue GTFS source
chip — and below, the pre-recorded stop-names section with its bulk MP3 import, where voice-talent
clips replace TTS per stop.*

Fix how the voice says things: plain respellings ("AVE" → "Avenue") or IPA phonemes, optionally per
language. Import stop and route names straight from GTFS and let AI expand the abbreviations — so
"AACC" comes out "Anne Arundel Community College", not letter by letter. Applied everywhere the engine
speaks.

### The Audio sub-menu — voice, suppliers, pronunciation, volume

The audio family lives under **Audio** in the left nav — the voice is the audio *output*, not a
setting, so it sits beside the playlists it speaks rather than behind a settings cog:

| Screen | Nav | What it holds |
| --- | --- | --- |
| **Playlists** | Audio ▸ Playlists | The announcement library (above). |
| **Voice** | Audio ▸ Voice | The voice, language and speed new playlists start from — plus one click to switch every existing playlist to it. |
| **TTS suppliers** | Audio ▸ TTS suppliers | Compare all 13 suppliers on the same line, then pick the one — and the voice — the engine renders with, and a failover. |
| **Pronunciation** | Audio ▸ Pronunciation | The phonetic / IPA lexicon and pre-recorded stop-name clips (above). |
| **Volume** | Audio ▸ Volume | Volume adaptation rules combining time of day, weekdays, route, stop and geofence. |

The old `/settings/…` deep links still redirect to their new homes. **Settings** proper (below) keeps
only what is genuinely project-plumbing: timezone, the GTFS feed, and the configuration file. The
toolbar keeps the actions — undo/redo, import/export and **Publish**.

![Project voice](/docs/media/creator-settings-voice.jpg)

*In the capture: Audio ▸ Voice — English (US), voice Micah22k_NV at 1.00×, the one-click "All
playlists use this voice" already confirmed ("All 18 playlists use Micah22k_NV"), and the "Which
supplier speaks it" card linking across to TTS suppliers — with the reminder that voice changes reach
the engine only on Publish.*

![Volume adaptation](/docs/media/creator-settings-audio.jpg)

*In the capture: Audio ▸ Volume — the default volume at 80% and one rule live: "Night quiet hours",
dropping announcements to 60% between 22:00 and 06:00. A rule combines any of time of day, weekdays,
route, stop and geofence (all its conditions must hold; the most specific matching rule wins) —
routes and stops picked from the imported GTFS feed, geofences from the circle/polygon zones drawn
on the Triggers map. One-click examples ("Weekday rush boost", "Quiet zone") seed common rules.*

#### Voices & TTS suppliers (Audio ▸ TTS suppliers)

![Voices and TTS suppliers](/docs/media/creator-settings-suppliers.jpg)

*In the capture: the supplier bench with the same line ("The next stop is Central Station.") ready to
play through every card — twelve suppliers on screen: Acapela configured and selected as the engine
supplier with voice Dimitris22k_NV, Azure Speech and ElevenLabs beside it, Google Cloud TTS / Amazon
Polly / ReadSpeaker awaiting credentials with their market-scorecard ranks showing, the self-hosted
Kokoro, Piper and eSpeak NG cards, and the endpoint-based Cerence, CereProc and Picovoice Orca — each
with Play, "Use for engine" and "Failover" actions.*

Play one line through every supplier — **13 of them**: Azure Speech, ElevenLabs, Acapela, Google
Cloud TTS, Amazon Polly, ReadSpeaker, the self-hosted Kokoro / Piper / eSpeak NG, the endpoint-based
Cerence / CereProc / Picovoice Orca, and a silent mock — back to back, and pick which the engine
renders with — *and* which voice that supplier speaks with. Both choices go live on Publish. The
picked voice replaces the engine's env default (`AZURE_SPEECH_VOICE` and friends), so it is what
every playlist left on "provider default" says. The A/B comparison uses the engine's real clients, so
what you hear is what the fleet gets.
Each supplier card shows **capability badges** — SSML lexicon, per-element volume, offline — straight from
the engine's own capability descriptor, so what a supplier silently can't do (say, honour a per-line
volume) is visible before it is picked, and the playlist editor repeats the warning next to any volume
the active supplier would ignore. A second supplier can be marked as the **failover**: the engine
renders through it while the primary's circuit breaker is open, so a vendor outage keeps announcements
speaking (in the failover's voice) instead of degrading to the engine's fallback tone — and the
dashboard's Health panel shows each supplier's synth count, mean latency and characters sent.
(`/voices` and the old `/settings/suppliers` still work and land here.)

### Settings — the project plumbing

![General settings](/docs/media/creator-settings-general.jpg)

*In the capture: Settings — the timezone the calendar triggers run on (engine default / host local,
with a live "Now in this zone" readout), the GTFS-feed slot that powers the Simulator and every stop
picker (imported.zip · 65 routes · 13 733 trips · 3 963 stops), and the configuration-file card that
makes the whole project one portable JSON.*

Two file actions read alike and are not, so both say which is which: **Import / Export** (toolbar, and
Settings → Configuration file) loads a file **into the open project, replacing it**; the project
**⋮ → Import as a new project…** leaves the open one untouched and creates a separate project.

### Publish history & rollback

![Publish history](/docs/media/creator-history-rollback.jpg)

*In the capture: ten versions of the project, v10 tagged LIVE (published 12 Aug, 12:02), every
earlier row one click from Restore, with publisher, timestamp and playlist / trigger counts per
version — running on the cloud deployment, where the `config_versions` table stores the trail
durably.*

Every publish is recorded — who published what, and when, with playlist and trigger counts. The current
version is tagged **LIVE**; **restore** any earlier version in one click (it re-publishes as a new
version, so nothing is ever lost). Sortable columns and a free-text filter (publisher, note, version)
keep long audit trails navigable. Governance for a fleet that speaks to the public.

## Engine dashboard — operate & audit

The dashboard is six menu entries, deliberately few: **Monitor** (the cockpit — map, feed and the
displays/source drawer), **Performance & Health**, **Proof of Play**, **History**, **Play on a
device** and **Published config** — a read-only view of what CreatorStudio last published, *not* a
settings screen; the dashboard has none, because every project setting belongs to the Creator and the
operational context (source mode, tenant, vehicle, TFT layout) belongs in the cockpit drawer next to
what it affects. Screens that were once separate pages — Live, Map, LED Signs — are panels
*inside* the cockpit now, because an operator watching a bus needs them at the same time, not in
different tabs; their old links redirect. Documentation is not a dashboard tab either: it lives in
the **Documentation Center** (`/docs`), reached from the suite rail that every app shares.

### Live monitor cockpit

![Live monitor cockpit](/docs/media/engine-monitor-cockpit.jpg)

*In the capture: live vehicle 13027 on Baltimore's route 22 to HOPKINS BAYVIEW, 27% through its
60-stop journey — current stop 40th St & Rotunda Mall Dr, next University Pkwy "arriving now" — with
the Source drawer scoped to the baltimore-md-mta tenant (8 tenants on the broker, **3 077 vehicles on
a journey**, the bridge forwarding 167 msg/s), the raw Inbound PT-PIS feed streaming beneath the
vehicle picker, and a header pill flagging 2 450 vehicles silent for more than five minutes.*

The operator's cockpit for one vehicle, all on one screen: a **heading-up map** (the bus always points
up) showing the journey, stops coloured by what happened at each, and where every announcement fired; a
live **feed** (this vehicle's timeline, or the whole fleet — click a bus to follow it); and a drawer of
tools — the raw PIS-PT signal table, one-click signal injection, and the LED-sign / TFT previews.

![Trigger inputs and signal injection](/docs/media/engine-monitor-trigger-inputs.jpg)

*In the capture: the drawer's Triggers tab for the same vehicle — every fact the trigger evaluator
sees, live, with its source topic (journey state, next stop 40th St & Rotunda Mall Dr, doors closed,
speed 2 km/h), above the one-click injection buttons — stop request, doors open, off-route, crowded,
alert, destination override, alarm — for exercising the engine without touching a bus.*

![Fleet feed](/docs/media/engine-monitor-fleet.jpg)

*In the capture: the right rail switched to the whole fleet — stop requests, arrivals and departures
from vehicles across CityLink RED and BLUE and route 63 scrolling in one feed — while the Source
drawer filters the 11 vehicles currently on a journey and offers to serve them all.*

### LED signs & TFT preview

![LED signs and TFT preview](/docs/media/engine-monitor-display-previews.jpg)

*In the capture: every surface of vehicle 13027 mid-journey on route 22 — the interior LED scrolling
"NEXT STOP IS UNIVERS…", the exterior front / side / left / rear faces showing "22 HOPKINS BAYVIEW",
and the 1920×610 interior TFT beneath on its published layout — all driven by one live journey state,
while fleet vehicle cards (Departing / Arrived at Stop) stream in the right rail.*

See what the passenger sees: the **interior LED** dot-matrix, the **exterior** front / side / rear
destination signs, and the interior **TFT** — all rendered live from the engine's output using the same
font the hardware ships, so the preview matches the bus exactly.

### LED Signs (live matrix)

The cockpit's **Displays** drawer loads the published layouts / displays / vehicle roster and renders
every face from live MQTT fleet state (or a sample journey). The same cycle + trigger-override
pipeline the engine uses decides which layout is active; interior / announcement-mode faces mirror the
spoken text. Reload from the running config, or jump to Creator when nothing is published yet. This
was a page of its own (`/signs`) until it became clear it was a second view of what the cockpit
already shows — the route now redirects to the cockpit.

### Performance & timing

*(Performance and Health share one menu entry, switched by a segmented control — they are two reads
of the same live engine beat.)*

![Performance and timing](/docs/media/engine-performance-timing.jpg)

*In the capture: a fully warmed cache — four live announcements (approaching / departing / arrived at
stop), every one a cache hit: 3 ms average lead time end to end, 0 ms average TTS, 100% cache hit
rate, the timing bars showing resolve / render / publish with no synth segment at all. The cold
phrase is paid for once, then the whole fleet reuses it in single-digit milliseconds.*

Per-announcement latency broken into resolve / render / TTS / publish, with the cache hit-rate. A cached
phrase completes in about **2 ms**; a cold synth ~1 s, then it's cached and reused fleet-wide. Prove the
engine is fast — and see exactly where any slow announcement spent its time.

### Health & telemetry

![Health and telemetry](/docs/media/engine-health-live.jpg)

*In the capture: one engine tracking **7 322 real vehicles** at ~400 msg/s with 21 ms event-loop lag
and 249 MB of memory — 8.4 trigger events/s derived from the stream, the upstream bridge at 158
forwarded messages/s (109 597 in total this session), and the rolling telemetry table underneath.
The same numbers the load ladder measured, now on live traffic.*

Live engine vitals — messages/s, events/s, event-loop lag, memory, cache hit-rate, in-flight / queued /
dropped renders, synth-dedup — plus upstream-bridge throughput. Sparklines and a rolling table flag a
stale or struggling engine at a glance.

### Announcement history

![Announcement history](/docs/media/engine-history.jpg)

*In the capture: 48 records for vehicle 13027's route-22 run — every row showing the trigger
(Approaching / Departing / Arrived at Stop), the stop, the announced text with its LED line, the
language and voice, the interior output channel and the render time in milliseconds.*

Its own menu entry beside Proof of Play: the durable trail of **every announcement the engine has
spoken**, across restarts — vehicle, trigger, stop, transcript and the lexicon-applied spoken text,
voice and language, volume and timing. Filter by vehicle / trigger / free text, auto-refresh, export
CSV. Proof of Play answers *what a passenger was proven to hear*; History answers *what the engine
said* — different questions, so they are no longer stacked behind one segmented control.

### Proof of Play — the audit trail

![Proof of Play audit](/docs/media/engine-proof-journeys.jpg)

*In the capture: a live route-22 run under audit — the journey group (47 records, 11:51–12:03)
expanded straight into its evidence: each record a rendered LED strip ("THE NEXT STOP IS…", "NOW
ARRIVING AT…") with AUDIO / INTERIOR surface badges and its firing trigger per stop, under the
group-by chips (journey / route / destination / date / hour / vehicle) and the CSV / JSON proof-pack
export.*

The durable, tamper-evident record of **what was actually played and shown, where and when** — grouped
here by journey, each record with the LED transcript. This is the accessibility (ADA) compliance
evidence: *dispatched* vs *vehicle-confirmed* played, exportable as a signed CSV / JSON proof pack.

### Proof of Play — every record

![Proof of Play records](/docs/media/engine-proof-records.jpg)

*In the capture: the same audit flattened to one row per record — all 47 of 47 for the HOPKINS
BAYVIEW run, each carrying its time, vehicle, surfaces (AUDIO / INTERIOR), route → destination, the
announced content, the stop, its GPS location and the firing trigger, under the filter bar with its
tenant chip and the CSV / JSON export.*

### Proof of Play — on the map

![Proof of Play map trail](/docs/media/engine-proof-trail.jpg)

*In the capture: all 321 records as dots over the Baltimore map — the route corridors through
downtown, out to Dundalk and Middle River, traced by time-ordered trail lines; every announcement of
the afternoon verifiable by the place it fired, one click from its transcript.*

The same audit plotted geographically: a time-ordered trail of every announcement along the route (audio
vs exterior-sign changes), each point clickable for its transcript and detail. Prove an announcement
fired at the right *place*, not just at the right time.

### Proof of Play — replay

![Proof of Play replay](/docs/media/engine-proof-replay-live.jpg)

*In the capture: record 9 of 47 — vehicle 13027 on route 22 — re-rendered as the passenger
experienced it: the interior LED scrolling "…T STOP IS LIBERTY HEIGH…", the front and side signs
showing "22 HOPKINS BAYVIEW", and the spoken transcript beneath ("Next stop is LIBERTY HEIGHTS
Avenue & DRUID PARK Drive Northbound"), with transport controls to step the journey frame by frame.*

Step through a journey and re-show exactly what a passenger experienced — the interior LED, the exterior
signs and the spoken transcript — reconstructed from the record, frame by frame. (The record is the
proof; the replay re-voices it for review.)

### Play on a device

![Play on a device](/docs/media/engine-play-device-qr.jpg)

*In the capture: the QR code for vehicle 13027 ready to scan, with open-here and copy-link
alternatives — and the "Before you blame the player" card that saves the first support call: tap
Start once (browsers block audio until touched), check the iPhone silent switch, reach the broker's
WebSocket port, turn the volume up.*

Turn any phone, tablet or the onboard unit into a vehicle's speaker: scan the **QR code** and that
device plays the vehicle's announcements the instant the engine speaks them — and reports back what
actually played, closing the proof-of-play loop. No install.

### Published config (triggers → playlists)

![Published config](/docs/media/engine-published-triggers.jpg)

*In the capture: 18 playlists and 33 triggers as the engine runs them — cards for Trip Start,
Approaching / Arrived / Departing Stop, Doors Open (At Stop), Stop Request Button and Journey: Not In
Traffic, each naming the playlist it speaks with every variable a distinct chip — read straight from
the running process, with Import config and "Edit in CreatorStudio" beside the reload.*

What the engine is executing *right now*, read straight from the running process: every trigger and the
playlist(s) it speaks, with the templated text (`{routeNumber}`, `{nextStop}`, `{connectingServices}`…).
The definitive "what will this bus say" view, plus a config import for offline setups.

## On the vehicle

### Passenger information app

![Boarding a vehicle](/docs/media/player-boarding.jpg)

*In the capture: the boarding screen — type or scan a vehicle (here 13023), "Board this vehicle" or
"Show this vehicle's LED signs", and the hand-off QR whose link carries the ids in the path
(`/player/v/baltimore-md-mta/13023`), so it survives every camera app and proxy; the next-stop card
waits for the journey.*

![Passenger information app](/docs/media/player-passenger-info.jpg)

*In the capture: the boarded rider view on live CityLink LIME data — "This service to northwest
hospital", current stop Saratoga St & Saint Paul St, 5 of 52 stops done — with the personal "getting
off at" picker set to Edmondson Ave & Arlington Ave and its "11 stops until" alert banner counting
down, and an "End & leave" control, all in a plain browser page.*

The browser player doubles as a **passenger information system**: the live headsign (route →
destination), current and next stop with an arrival countdown, journey progress, and a personalised
**"getting off at"** stop picker that counts down and alerts you the moment your stop is next. It rides
the vehicle's existing feed, speaks the announcements, keeps working offline, and needs no install.

### LED signs on any screen

![LED player](/docs/media/player-led-signs.jpg)

The player's LED half (`/player/led`) — like the standalone `/sign` and `/vehicle` pages — turns any
browser into the vehicle's signs, rendered with the exact pixels the engine drives the hardware with.

*In the capture: all six faces of live vehicle 24056 on one page — the amber front sign (route RD to
UM MEDICAL CENTER over a scrolling stops line), the full-colour front-extra panel drawing the RD flag
white-on-red, both side signs, and the rear and driver panels showing the bare route code — each at
its true resolution with the real FNT fonts, each openable full-screen on its own display, with a
kiosk view for the whole board.*
