Menu

Elabora course format — specification 0.2

On this page

Status: draft for implementation, 6 October 2026. Format id: elabora/0.1. Licence of this text: Apache-2.0. The JSON Schemas in packages/format/schemas/0.1.10/ are normative; this document explains them. Where they disagree, the schemas win.

The format describes an e-learning module as plain files that people, agents and other tools can read and write. It separates three layers with different rules:

Layer Files Who changes it Guarantee
Contract (the plan and its versions) plan.json, plan-versions/ The author and their agent write the plan; a person makes its versions and records the client’s validation of one A version never changes (its content hash); release builds need a validated version and list what changed since
Blocks course.json, screens/*.json Authors and agents, freely Every value is checked against a schema (built-in or component)
Components (templates) components/<id>/ Agents and developers; authors edit instances Templates without code, behaviour hooks, fallback to native blocks, versioned with migrations
Free objects (governed code) objects/<id>/ Agents, on explicit request; authors edit declared props only Manifest + tests + sandbox, replayed at every change

1. Project layout

course.json                 course settings, sections and screen order, variables, theme, tracking
plan.json                   the plan, as it is now: objectives, ordered items, validated texts, questions, sources
screens/<id>.json           one file per screen
objects/<id>/               free objects: object.manifest.json, object.js, object.tests.json, object.notes.md, object.gates.json (last gate run)
components/<id>/            course components (block types and screen layouts): component.json, template.html, component.css, migrations.mjs
theme/                      course stylesheet and embedded fonts (course.json theme)
generators/                 asset generators (*.mjs) and provenance.json of the assets they produced
assets/                     media files referenced by blocks (portable ASCII names); the audio of narration lines in assets/narration/<language>/<screen>/ (§4.7)
plan-versions/V<n>.json     the plan's versions (V1, V2…): frozen copies, their sends and the client's validation
plan-versions/notes.json    the author's notes for the client on changes in no version yet
translations/<lang>.xlf     translations of the course (XLIFF 2.0) and <lang>.approval.json, approval of the translated plan
elabora.lock.json             permanent short keys of stateful elements and component versions (generated; keep under version control)
AGENTS.md                   rules for agents working on this course
sources/                    the author's files the course is made from, and their readable versions (<file>.md, <file>.files/): not part of the course

Tools keep their own data beside these files, in .elabora/ by default: the journal of changes, the backup versions, the agents’ activity and the transcripts of Elabora’s chat (storage). It is machine-local, never part of the course: in Elabora’s chat, everything the agent writes during one turn is one batch of the journal, undone at once, and the tools refused to it (a file edit, a shell) are kept with their target, never any content.

sources/ is reserved: what it holds is never validated, built or published, and a course file that points into it is an error (E_SOURCE_REFERENCED). Tools may keep a readable version of each source beside it (<file>.md, whose first line is an <!-- elabora:readable {…} --> marker, and <file>.files/).

All JSON files are UTF-8, and may carry "$schema" pointing to https://elabora.studio/schemas/0.1.10/<name>.schema.json. Tools write them with two-space indentation and a final newline.

2. Identifiers

Kind Pattern Example Scope
Course elements (sections, screens, layers, blocks, events, objects) ^[a-z][a-z0-9-]{0,47}$ evaluation-q1 Unique in the whole course
Plan elements (objectives, items, texts, questions, sources) ^[A-Z][A-Z0-9_-]{0,31}$ OBJ-2, ITEM-5, Q-12 Unique in the plan
Variables ^[a-z][a-zA-Z0-9_]{0,31}$ bonsReflexes Unique in the course
Options (quiz, decision) ^[a-z0-9]{1,8}$ a, b Unique in their block
Dialogue nodes, endings and their variables ^[a-z][a-z0-9-]{0,47}$ (variables: as course variables) premiere-question Unique in their dialogue; a dialogue’s options are unique in their node
Narration and dialogue lines ^[a-z][a-z0-9-]{0,47}$ l2, m1 Unique in their screen, across its narration, its layers and its dialogues

Ids are permanent: learner progress is keyed by id (§9), and tools never give an element another id on their own. Renaming is one explicit command, elabora rename <old> <new> (MCP elabora_rename), for sections, screens, layers, blocks and events: every reference follows (sections, events, translation unit ids, stylesheet selectors on data-section, data-screen, data-layer and data-block), a screen file takes the new name, and elabora.lock.json keeps the element’s key under its new id. Waivers of the plan that name the id follow it. It is refused for an id already in use or one that does not match the pattern. An id is never reused for something else.

3. Contract layer: plan.json and its versions

The plan is the storyboard the client validates. plan.json is always the plan as it is now (the working copy); its versions are frozen copies of it in plan-versions/ (D52). It holds:

  • objectives[]: observable statements (statement), a short name, Bloom level, success criterion, sources;
  • items[]: the ordered storyboard. Each item has a kind (intro, exposition, demonstration, practice, scenario, assessment, summary), a part, the objectives it serves, optional lockedTexts[] (validated texts, verbatim: true for one the client wants « à l’identique »), questions[] (validated assessment items with options, correctness and feedback) and voiceover[] (the item’s voice-over, line by line: speaker, text; translated with the plan, not kept word for word in the module; written by elabora plan voiceover; a screen covering the item takes it as its narration script, elabora narration from-plan);
  • sources[], businessGoal, audience, duration (the learner time asked for, in minutes: min, max), spacing[] (retrieval planned after the module), waivers[] (approved exceptions to pedagogical warnings).
  • before its validation (D51): decided[], what the agent decided by itself because the author left it open (topic, choice, why, items), and question, the one question it asks the author now (text, topic, items). Validating a version removes both, and no version holds them: they are never part of the contract.

Validated questions are choice questions (mode single or multiple, with options) or numeric questions (mode: "numeric", with answer, tolerance, unit, feedback and misconceptions), reused by quiz and numeric blocks respectively.

Writing the plan. The author and their agent write plan.json at any time, before and after a validation: elabora plan write --from <plan.json> | --patch <patch.json> (MCP elabora_write_plan) checks it against its schema first, and the screens of items moved follow them (D50). A validated text is reworded in the plan (elabora plan text), and every block that shows it by textRef follows. An agent’s write that changes a text kept verbatim (its words, its flag, or the text removed) is refused (E_PLAN_VERBATIM_LOCKED): only the author changes it.

Versions. plan-versions/V<n>.json (schema plan-version) holds version (its number, the file’s name), date, an optional note, contentHash = SHA-256 of the canonical JSON (sorted keys, no whitespace) of plan without $schema, decided and question, and plan, the plan as it was: these never change (a version that does not hold together is E_PLAN_VERSION_INVALID, and does not count). The days it was exported for the client are added to sends[] (date, format), and the client’s validation to validation (by, date, reference). A person makes a version (elabora plan version) and records a validation (elabora plan approve --version Vn, the latest by default), never an agent (E_PLAN_PERSON_ONLY). Exporting the plan’s document shows a version (the latest by default) or the working copy, records the send on the version, and never makes one.

State. The plan’s state follows from its versions: without a validated version it is a draft (W_PLAN_DRAFT; release builds need one: E_PLAN_NOT_APPROVED); when the working copy is the latest validated version it is approved; otherwise it changed since (W_PLAN_MODIFIED_SINCE_VALIDATION, a warning: a release build lists the changes in its report and proof dossier). A text kept verbatim in the validated version that changed since is flagged (W_PLAN_VERBATIM_CHANGED). What changed is computed, never written: planDiff matches the elements of two plans by their ids (added, removed, moved, an item’s part changed, each field, texts word by word), from the latest validated version (the app’s marks) or from the latest version (what is in no version yet); elabora plan diff.

Notes for the client. plan-versions/notes.json (schema plan-notes) holds the author’s notes on changes in no version yet, one per change (target: its key in the diff, item:ITEM-3; text). The next version takes those whose change it holds and freezes them in its notes[]. The document shows them in its margin and Word as comments; packages never carry them.

Translated plans. A translated course (§13) has its own plan, generated from the source plan; its translationOf records the source plan’s id, content hash and language. It is approved on its own (elabora translate approve): the translated course holds the approval as its validated version V1 while the source plan and its translation keep their content.

Coverage rules. Every screen covers at least one plan item; every plan item is covered by at least one screen (E_PLAN_ITEM_UNCOVERED). A validated text is used through textRef on a screen covering its item; a validated question through questionRef. Copies are not accepted as uses.

4. Native layer: course.json and screens

4.1 course.json

format, id, title, language (BCP 47), version (semver), sections[] (each lists its screen ids in learner order), variables[], speakers[] (the voices of the narration, §4.7, and the characters of dialogues, §4.9), theme (charter tokens checked for WCAG 2.2 AA contrast, with the player’s player.* and the dialogues’ dialog.*, custom tokens, free stylesheet, embedded fonts (one entry per file: family, src, weight, style, and unicodeRange for a face split into subset files, latin and latin-ext), named styles (each { "label", "description" }, the name authors pick it by and what it is for, or the description alone), backdrop (its variants written the same way); see docs/design.md), presentation (scroll or slides, content widths in px at the reference window, fit sizes for slides: reference 1600x900 and minimum 1280x720 by default; see docs/design.md §2), navigation (linear/free, lockUntilComplete), player (the learner’s player: two bars of items, §4.8), resources[], glossary[] and credits[] (what the player’s Resources, Glossary and « IA et crédits » show, §4.8), sounds (interface sounds, music and ambiences, §4.12), completion (reportAs, masteryScore), tracking, delivery (mediaQuality: high, the default, ships the media masters; medium and low ship renditions encoded from them for LMS size limits; the presets are in docs/architecture.md, Media), ui (label overrides).

4.2 Screens

A screen has id (equal to its file name), title, covers, blocks[] in reading order, optional layout (a built-in screen layout, opening (D63), or a screen-level course component: a template with named slots; blocks choose a slot) with its layoutProps, style (named style of the theme), layers[] (overlays opened by events, like Storyline layers: id, title, presentation dialog or panel, blocks[], and an optional narration, which gives the layer a voice and a timeline of its own, §4.7) and events[]. completeWhen is viewed (default), interactions-done (every quiz, fill-in, sort and decision answered, every reflection saved, every dialogue’s first run ended, every free object completed; blocks with optional: true aside) or action (only by a complete-screen action). continueRevealsSteps: false leaves the steps of the screen to its events (reveal-step): Continue no longer reveals them. steps says how each step opens, keyed by step number: opens continue (default: a Continue button in the page in scroll presentation, like the continue dividers of Rise; Next relabelled Continue in slides) answered (by itself once every activity of the steps above is answered, right or wrong, like content after a knowledge check) or explored (Continue, or Next in slides, is shown but locked until every activity of the steps above is answered and every control of their blocks is visited, optional blocks aside; locked, it says what is left), and label, the text of its Continue button. Screens with required: false do not count for completion. narration gives the screen a voice and a timeline (§4.7).

4.3 Blocks

{ "id", "type", "props", "blocks"?, "hidden"?, "step"?, "width"?, "slot"?, "style"?, "optional"?, "continuity"?, "note"? }. step (blocks of a screen) holds the block back until the learner presses Continue or an event reveals it (reveal-step): in scroll presentation pending steps take no room, in slides they keep their place. type is a built-in type (below) or the id of a course component (§4.6). width is a layout hint in the default column (full, wide, half, bleed); two consecutive half blocks share a row on wide screens; bleed goes edge to edge of the window in scroll presentation and of the slide in slides, its text keeping the reading width (in a layout’s slot it fills the slot), and a template asks for it from its root with data-t-bleed. slot places the block in the screen layout, or in a slot of the component instance holding it in blocks; style picks a named style. optional: true marks extra the learner may skip (a transcript, more detail): its activities and controls count neither for a step that opens once explored nor for completeWhen: "interactions-done". continuity: { "key", "duration"? } makes blocks of different screens one thing to the learner (D65): going from one of those screens to another, a free object keeps running and moves to the arriving block’s props over duration ms (default 700; free-object contract §4e), another block glides from where its counterpart was, nothing else moves, and less motion makes it a cut. Blocks sharing a key have the same type (free objects the same object), once per screen, never in a layer; a chain of free objects saves one state. Built-in catalogue 0.2:

Type Purpose Key props
heading Section heading (levels 2–4; the screen title is level 1) text or textRef, level
text Markdown subset markdown or textRef
callout Key rule, tip, warning variant, title, markdown or textRef
image Image from assets/ asset, alt or decorative, caption, generated
video Video played by the learner (captions when it has sound, transcript), a muted loop like an animated image (loop, with alt), or decorative asset, title, captions (WebVTT), transcript, loop, alt, decorative, poster, generated
audio Audio with transcript, unless decorative asset, title, transcript, decorative, generated
button Triggers click events label, variant
accordion Panels (« Panneaux »): optional or parallel detail the learner opens (not practice) presentation (accordion, tabs, cards, stack), items[] (id, title, markdown, image, alt, focus), prompt, allowMultiple, hint
math Displayed equation (LaTeX → MathML at build time) tex, caption, label
quiz Single/multiple choice, elaborated feedback; options as text, as picture cards (presentation: "images") or as zones on one picture ("image", §4.10) questionRef, or stem + mode + options[]; scored, weight, attempts; presentation, image, alt, options’ image, alt, x, y
numeric Numeric answer within a tolerance, targeted feedback per misconception questionRef, or stem + answer + tolerance + unit + feedback + misconceptions[]; scored, weight, attempts
fill-in Text with blanks, typed, chosen from menus or placed from a bank of words (§4.10) markdown with blanks [[answer|other accepted]], presentation, stem, distractors, ignoreAccents, feedback; scored, weight, attempts, showCorrectAnswer
sort Items placed in pairs, into categories or in order (§4.10) presentation, stem, items[] (id, text, image, alt, target), targets[], feedback; scored, weight, attempts, showCorrectAnswer
reflection The learner’s own words, or a point on a scale; never scored (§4.10) prompt, presentation (text, scale), maxLength, rows, model, download, scale
decision Scenario choice with consequences prompt, options[], scored, allowChange
dialogue Branching conversation held in the block (§4.9) title, learner, mode, scoring, nodes[] (lines[], options[], next, end), endings[], vars[]
free-object Places a free object object, title, props (declared values), textAlternative, a11y (description and keyboard, what screen readers say, worded for this block), strings, aspectRatio, narrowAspectRatio, minHeight, maxHeight, scored, weight
results Score and status allowRetry, messages
statement The one idea of a page to remember (« Énoncé »); with an author, a quotation presentation (statement, ruled, quote, image), markdown or textRef, author, role, portrait, image, alt, decorative, focus, align
divider A pause between the parts of a long page (« Séparateur »), decoration presentation (line, ornament, number, space), size, restart
gallery Images read together (« Galerie ») presentation (grid, mosaic, carousel, compare), images[] (id, asset, alt, caption, focus, decorative), columns, ratio, caption, zoom
columns Two or three columns of blocks (« Colonnes »), stacked on narrow screens count, widths, align; slots first, second, third
speech A character speaking to the learner in bubbles beside its portrait (« Personnage ») speaker (a speaker of course.json), lines[] (id, markdown), side (start, end), kind (speech, thought)
steps A process or a story in order (« Étapes »; a block, not the screen’s steps), §4.11 presentation (carousel, timeline, scroll), items[] (id, title, markdown, image, alt, decorative, date, region), intro, summary, numbered, figure (replace, build, focus), figureImage, figureAlt, side
hotspots Markers on one picture, each opening a text (« Image interactive »), §4.11 presentation (popover, side, below, tour), image, alt, decorative, focus, items[] (id, x, y, title, markdown, image, alt), markerStyle (number, icon), icon, prompt

image also takes a presentation (centered, the default, wide, band, beside, over) with focus, title and markdown (beside, over), side and share (beside), height (band, over), zoom (centered, wide, beside: the image opens in the player’s lightbox, as a gallery’s images do with zoom); text takes an aside, a note in the margin. Focal point: focus: { "x": 0..1, "y": 0..1 } on an image becomes its object-position, so crops keep the subject. Blocks with several forms name them with presentation, its first value the default (D63).

Panels, characters, the gallery’s carousel (D63). An accordion’s items are the same in its four presentations, and each is a control named by its id in every one (a panel opened, a tab shown, a card turned over, a card of the stack seen), so its visited state survives a change of presentation: accordion (native disclosures, allowMultiple: false one open at a time), tabs (the room of the tallest panel kept; in containers narrower than 560 px the same items as an accordion, the one rendered being the one in the accessibility tree), cards (flip cards in a grid whose cards are as tall as the tallest, the front the title and the image, the back the text; two to a row on phones) and stack (the flip cards one at a time with Previous, Next and the position; a stack opens on its first card). An item’s image needs alt, "" when decorative. A speech block’s speaker gives its name, colour and look (a character’s SVG framed on its face and at rest, a portrait picture, else its initial); each line reads « Name: text » to screen readers, the written name and the portrait hidden from them. A gallery carousel shows one image whole in a frame of ratio, its caption, Previous, Next, a thumbnail per image (wide containers) and the position, in the room of the tallest. With zoom, the images of a gallery (but compare) and an image open in the lightbox, with previous and next between a gallery’s. Checks: W_ITEMS_MANY (tabs over 7, cards over 12, a gallery’s carousel over 5), W_TABS_LABEL_LONG (over 24 characters), E_SPEECH_SPEAKER.

Converting a block (« Transformer en… », D63). A block becomes another presentation of its type, or another type that holds the same content, in place: panels, steps, an interactive picture and a gallery (when its items have pictures) into one another, panels into a sort in pairs, steps into a sort in order or the charts process and milestones, statement, callout and text into one another, an image and a gallery of one image, the bulleted or numbered list of a text into panels or steps. The block’s id and its items’ ids are kept, and items are never reordered (a sort’s placements are saved by index), so visited controls keep their names and translation units whose text moves to another prop follow it; what a conversion keeps, loses (learner progress included: a panel’s visited mark has no place in a sort) and leaves to write is said before anything is converted, and what does not fit the target (too many items, a text too long, a picture missing) is refused with its reason (E_CONVERT_REFUSED). The command is convert_block (CLI elabora convert, MCP elabora_convert_block, the engine’s conversions request); for the lock it is a content update like any other.

Screen layouts. A screen’s layout names a built-in layout (layouts/<id>.schema.json holds its props) or a screen-level course component. Built-in 0.2: opening (« Ouverture »), the head of a lesson then its page, presentation title, band, split or cover, props lead, image, alt, decorative, focus, duration (the page’s reading time, computed by the build), slots question (a question before the lesson) and body (default). Course components may not take a built-in layout’s name; they may fork one.

Markdown subset. CommonMark without raw HTML and without images (use image blocks), plus tables and maths: $…$ inline and $$…$$ display, in LaTeX, rendered as MathML at build time (no script at runtime; read by screen readers). Write a literal dollar as \$. Links to external sites open in a new tab. Text marked x-translatable in the schemas is what translation tools extract. Editors lay a block’s settings out from its schema: x-group names the section a setting goes in (question, options, scoring, data, figure, markers; without one, its content), x-advanced marks one authors rarely change.

Generated media. Images, video and audio produced by an AI model carry generated: true; the player marks them, its « IA et crédits » names them, and the proof dossier lists them (AI Act, art. 50).

Glossary links. A Markdown link to glossary:<id> ([passerelle](glossary:gateway)) names an entry of course.json glossary: the player shows its definition in a tooltip when the word is hovered or focused, and opens the Glossary there when it is followed. An id the glossary does not have is E_GLOSSARY_TERM_UNKNOWN.

4.4 Events

{ id, description?, on, if?, do, else? } — when the trigger fires and every condition holds, run do; otherwise else. do may be empty: an event being written, its trigger chosen before its actions, does nothing until it has one (W_EVENT_NO_ACTION while else has none either).

  • Triggers: screen.enter; click (a control of the block: a button block, or a button, tab, disclosure, sequence item, flip card, details or element brought into view of a component; name filter for one control); quiz.submitted (quiz, numeric, fill-in or sort block, result filter: a fill-in or a sort is correct when all its blanks or items are); decision.selected (decision, option filter); object.event (free-object block, name, where payload filter); variable.changed; layer.closed; timeline.reached (at, an anchor (§4.7): playback of the screen’s timeline crosses the start or end of a line, a word of it, seconds from a step’s start on a screen without narration; never on a seek, again when the learner replays the passage; a line of a layer’s narration names that layer’s timeline, and layer with at.seconds counts from the opening of a layer without narration); timeline.ended (playback reaches the end of the timeline; with layer, the end of that layer’s timeline: hide-layer there closes a layer once it has spoken); media.ended (an audio block, or a video that is not a loop, plays to its end); dialogue.started, dialogue.node (node), dialogue.line (line), dialogue.chosen (node, option), dialogue.ended (ending) for a dialogue block (§4.9).
  • Conditions, all required: { variable, op: eq|neq|gt|gte|lt|lte, value }, or { visited, name? }: the learner has used every control of a block of the course (or the control name); or { dialogue, ending | chosen | score | done }: what the scored run of a dialogue block of the course gave. A control is visited once used; a tab or disclosure once its content is shown. The visit is recorded before the click’s events run.
  • Actions: go-to (screen id, next, previous), show-layer, hide-layer (without layer: the layer open on top, a dialog before a panel, the last opened first; for a Close button inside a layer; its layer.closed fires as for any other), show-block, hide-block, reveal-step (the steps of the screen up to step, as Continue would; a step already shown takes the focus again), set-variable, increment-variable, complete-screen, set-object-props (declared props only), exit-course, pause-timeline and play-timeline (the timeline of the screen; a modal layer pauses it and resumes it by itself; with layer, the timeline of that layer while it is open), seek-timeline (to, an anchor: the playhead goes there as a seek, firing nothing it passes; a moment in a later step reaches that step; with layer, or to a line of a layer’s narration, that layer’s timeline), play-media and pause-media (an audio or video block; playing one pauses the narration), start-dialogue, goto-dialogue (node) and restart-dialogue (a dialogue block, §4.9).

Targets of triggers and of layer/block actions belong to the same screen.

Controls are named in the component template: data-t-click="name" (the name may be bound to a prop, one per item of a list), data-t-tab="k", data-t-toggle="k", and the primitives’ items of a sequence (data-t-item="k"), flip cards (data-t-flip="k"), disclosures (<details data-t-details="k">) and elements visited once in view (data-t-reveal="k"); an unnamed data-t-click and the button block have no name. Names are checked on the rendered instance (E_EVENT_TARGET, E_VISITED_TARGET); a button without a click event that hears it is W_DEAD_BUTTON.

4.5 Variables

{ id, type: boolean|number|string|choice, default, maxLength (strings), options (choice), minimum, maximum, persist }. Persisted variables are saved for resume and count in the suspend data budget, so strings must declare maxLength.

4.6 Components

Every type is a component: built-in types are components shipped with Elabora (block types, and the screen layout opening), and a course defines more in components/<id>/ — block types (level: "block") and screen layouts (level: "screen"). A component has a props schema (strings with x-kind: text, markdown, tex, asset, token), named slots, a template rendered at build time (bindings, data-if, data-each, slots, behaviour hooks, headings that follow the page with data-t-heading, and the player’s primitives: one item at a time, flip cards, the lightbox, one part open at a time, details and elements visited once in view, D63; no script) and a stylesheet (cascade layer elabora.components, over the theme’s rules and under its @layer overrides blocks). A component may extend another (a fork remembers its parent and inherits what it does not redefine); a fork of quiz, numeric, decision, dialogue or results keeps its behaviour — scoring, saved state, SCORM records, focus and announcements stay in the player — so learner state is keyed as for the built-in. Components are not scored by themselves: activity blocks can sit in their slots. Every component has a fallback in native blocks (declared, or generated from its props), used by exports, the client view, translation and detach. Courses copy components from libraries with their origin (pinned) and upgrade them explicitly; when a definition changes the shape of its props, migrations.mjs steps migrate every instance, checked before anything is written. Details: docs/components.md.

4.7 Narration and timeline

Every line a voice says is a voice line (defs.schema.json voiceLine): id, speaker, text, delivery, pronunciations, as below. The lines of a screen’s narration add step and pauseBefore; those of a layer’s narration add pauseBefore; those of a dialogue add their own fields (§4.9: a line with a speaker, and the learner’s answers with learnerVoice, named <node>.<option> in the sidecar’s line). Line ids are unique in the screen, every track counted (the screen’s narration, each layer’s, each dialogue’s: E_NARRATION_LINE_DUPLICATE), so a line’s audio, its translation unit and the cues on it are named by the screen and its id alone.

A narrated screen has a script, its narration.lines[] in the order they are spoken: id (unique in the screen), speaker (a speaker of course.json speakers, the first one by default), text (word for word what is said; translated), step (default 1) and pauseBefore (seconds of silence before the line; 0.4 by default, 0 for the first line), and for generated voices delivery (settings by language over those of the speaker’s voice: a speed, a style) and pronunciations (by language, { text, say }: how the voice says a word of this line). Lines are in step order. course.json speakers[] gives each voice its id, the name learners read in captions and the transcript, and voices by language (provider, voice, settings) for the tools that generate the lines; course.json pronunciations is the course’s lexicon by language (each language its own list, never translated). A voice is told the line’s text with the line’s pronunciations, then the course’s, applied to whole words, case-sensitive, the longest text first; captions and the transcript keep the text. Words in square brackets in a line’s text are stage directions for an expressive voice ([chuchote], [rit], [un temps], in the line’s language, at most 80 characters, on one line): how to say what follows. They are part of the text (its hash, its translation, where translators keep or adapt them) but never words (anchors, word times), never shown (captions, transcript) and never touched by pronunciations; a voice that cannot play them is given the line without them (D29). A speaker’s voice may carry its name as a voice service gives it (information only).

  • The timeline is the lines end to end with their pauses. Each step of the screen is a segment of it, from its first line to its last (a step without lines has an empty segment); a step’s first line starts at least 0.3 s after the previous segment. narration.advance: learner (default) stops playback at the end of each step until Continue plays the next one; auto plays the steps one after the other; next-screen also goes to the next screen at the end (W_AUTO_ADVANCE, WCAG 2.2.1); the learner can turn both back into learner (« M’arrêter à chaque étape »), and phones always go step by step. narration.seek: seen (default: back anywhere, forward up to the furthest point reached), free, none (no seek bar on this screen; the course’s default is seen or free, and the player’s controls hide the bar of every screen with seekBar: false, §4.8). course.json narration gives the course’s defaults of both. A narrated screen has its own Continue in the screen, shown at the end of the step heard (seen, none) or at any time (free); at the end of a step Play does the same; Next goes to the next screen.
  • What is seen and what happens. Seeking recomputes what is seen at that time (the blocks of the steps started, the caption) and fires nothing; timeline triggers fire when playback crosses their cue. The course state never goes back: a step reached stays reached, an answer stays answered. completeWhen: "viewed" means the end of the narration reached by playback.
  • Audio, one file per line and language, next to the script by convention: assets/narration/<language>/<screen>/<line>.m4a (AAC in M4A, mono, at most 192 kb/s, the index first, silence at both ends trimmed to 0.1 s) and its sidecar <line>.json (schema narration-audio: screen, line, language, textHash of the line’s text in that language after Unicode NFC with whitespace runs made one space and trimmed, duration, provider (recording for a human voice), voice, generated, optional audioHash, createdAt, text (the line’s text the audio says), words (when each word of text is said, from the start of the file, with a score: what word anchors resolve to; written by forced alignment, D27) and aligner (what timed them), take (which take of a voice that says a line differently each time), credit (what the module’s credits say of the voice; builds list them in the report and the proof dossier), model (information only), mouth (the mouth’s envelope of a dialogue’s characters: one letter per 1/30 s, c closed, s small, o open, w wide, measured on the audio and its words when the tools write it, and by the build when it is missing; D61), and for generated voices spokenHash (the text the voice was told, after the pronunciations) and settingsHash (the canonical JSON of the speaker’s voice settings with the line’s delivery over them)). A line without both files has no audio (W_NARRATION_AUDIO_MISSING); one whose text, audio file or speaker’s voice changed since, or for a generated voice whose pronunciations or delivery settings changed, is out of date (W_NARRATION_AUDIO_STALE); another model is not. Tools write them (elabora narration import, the voice tools), never by hand.
  • Anchors (defs.schema.json anchor) name a moment of the timeline by what the voice says, never in seconds on a narrated screen: { "line": "l2" } (the line starts), { "line": "l2", "edge": "end" }, { "line": "l2", "word": "imprimante" } (one or a few words of the line, as written, case, punctuation and apostrophes aside; occurrence picks a word said more than once; French elisions such as l' and qu' are words of their own), with offset seconds after (or before). On a screen without narration, { "seconds": 1.5 } from the start of a step (step, else the block’s). { "step": 2 } and { "step": 2, "edge": "end" } name a step’s stretch. A word’s time comes from its line’s word times (the sidecar’s words, filled by forced alignment: elabora narration align, or at once when a voice makes the line or a recording is imported and the aligner of its language is installed); a word the alignment scores very low is one the voice may have swallowed (W_NARRATION_WORD_SWALLOWED); without them it is placed in proportion to the text (I_TIMELINE_WORDS_ESTIMATED). A word the line no longer says is an error (E_TIMELINE_ANCHOR_MISSING); editing a line’s text through Elabora takes its anchors along with their words. Translations carry word anchors as markers in the line’s XLIFF unit.
  • Timed blocks (block.timing, D28): in and out, each { at, effect, duration, direction }, say when a block comes and goes. Without in it shows with its step; without out it stays until the end. Effects: none, fade (default), rise, slide, zoom, wipe, draw (the strokes of its inline drawing: an image block’s SVG with inline: true, or a component’s markup; a wipe otherwise), or an effect of the theme (course.json theme.effects, its stylesheet’s @keyframes t-fx-<name> and t-fx-<name>-out); direction (left, right, up, down) for slide and wipe. Effects move the block relative to its place, never the layout; a timed block keeps its place while it is not there, out of reach of the keyboard and of screen readers, and is recomputed when learners seek. Under reduced motion each effect is a short fade at the same time. A step’s stretch lasts until its last line ends, or until an effect anchored in it ends; on a screen without narration until its last moment, or steps[n].duration. Timing applies to the blocks of the screen and of its components’ slots, and to the blocks of a layer on the layer’s own timeline (below). A stack (block type stack, « Pile ») holds contents that follow one another in the same place, the size of its largest: each content shows from its in until the next one’s (transition crossfade, cut or slide). The validator checks the reading time of what appears (W_TEXT_TOO_FAST), that an activity or a control does not leave before playback stops for the learner (E_ACTIVITY_LEAVES_UNANSWERED), that a block does not leave before it comes (E_TIMING_ORDER), and movement longer than five seconds (W_MOTION_LONG, WCAG 2.2.2).
  • Layers’ timelines (D62). A layer with a narration (lines[] of voice lines with pauseBefore, no step; seek seen, free or none, the course’s default otherwise), or with timed blocks or cues, has a timeline of its own: its lines end to end from its opening, in one stretch, played through. Its blocks anchor to its lines ({ "line": "r2", "word": "poste" }), or, in a layer without narration, to { "seconds": n } from its opening; an anchor never names a line of another timeline (the screen’s for a block of a layer, a layer’s for a block of the screen: E_TIMELINE_ANCHOR_TRACK). The player starts it at 0 each time the layer opens and stops it when it closes; its compact bar (play and pause, replay, the seek bar as seek allows, captions) and its captions are in the layer. Two voices never overlap: while a layer speaks the screen’s timeline is held, until the layer closes (a dialog) or until the end of its narration (a panel). The learner’s Pause stops what is in the layer; seeking and reduced motion work as on a screen; the screen’s transcript adds the lines of each layer under its title. Its audio lives where every line’s does (assets/narration/<language>/<screen>/<line>.m4a). The checks of a screen’s timeline apply in the layer (missing or stale audio, reading time, activities that leave while it plays on, blocks that leave before they come).
  • Packages carry one track per narrated screen (narration/<screen>.m4a, the lines laid end to end, and narration/<screen>.vtt, its captions with the speakers’ voices) and one per narrated layer (narration/<screen>.<layer>.m4a and .vtt: a dot is never in an id, so no screen, layer or language folder can take the name), and the cue times in the course data. Until every line has its audio in the package’s language, the screen has no track and runs silent on times estimated from the text, with its captions. The player draws the captions and the transcript from the script; they answer F3 for the narration. The voices’ credits (their sidecars’ credit: the attribution a CC-BY voice asks for) travel in the course data and end the transcript of their screen and « IA et crédits » (§4.8).

4.8 The player

The learner’s player around the screens (decision D58). course.json player has two bars, top and bottom, each { items[], position }:

  • Items, in order from the start of the line, each { id, enabled (default true), options? }, each in one bar at most (E_PLAYER_ITEM_DUPLICATE): logo (options.asset, an image of assets/, scaled to the bar’s height; options.alt, translated; options.plate, the logo on a white plate when it does not read on its bar), title, spacer (« Espace flexible »: one per bar, always on; the items before it start the line, those after it end it, and the narration’s controls stretch before it does), progress (the share of required screens completed; in a course of two sections or more, one segment per section, as wide as its share of the screens and filled as its screens are completed, the section shown marked, each a button to its first screen, refused like Next while that screen cannot be opened; on a phone a tap opens the outline; options.parts false keeps the bar in one piece, not a button, options.percent false hides the percentage beside it, both true by default), outline (the drop-down table of contents; options.label: its button reads « Sommaire », outline, the default, or the title of the section shown, section, or of the screen shown, screen), controls (a narrated screen’s bar, and the transcript of a screen without narration whose dialogue or layer speaks; options: seekBar, captionsByDefault, volume, transcript, speeds none, 0.75-1.5 or 0.75-2, the default), resources, glossary (options.linkTerms, default true: « Relier les termes aux écrans », below), credits (« IA et crédits »), display (« Affichage »: text size in three steps, larger captions, the keyboard shortcuts, in a module with a dialogue « Texte : tout de suite », and in a module with interface sounds « Sons de l’interface », §4.12), language (« Langue »: the languages of a package of several, D59), previous, next and exit (Quit, in SCORM packages only). An item shows when it is enabled and has something to show (a resource, a glossary entry, an AI mention or a credit, the logo’s file, a timed or speaking screen for the controls); a bar with nothing to show is removed and the screen takes its room. A bar left out is its default; an item in neither bar is not shown. In a bar without a spacer, the room left goes to the title or the controls, else before the first of resources, glossary, credits, exit, previous and next that follows another item.
  • Defaults: top logo (disabled), title, progress, spacer, resources, glossary, credits, exit; bottom outline, controls, spacer, previous, next; display and language in neither bar (enabled, they go after the bottom and the top bar’s spacer). position window (default: the bar stays in the window) or page (the top bar scrolls away; the bottom bar ends the page, after its content and its last step, in scroll presentation).
  • Nothing moves while learners use it. Previous and Next unavailable stay in place, greyed; Next is as wide as its longest label (Next, Continue); a refusal (a locked Next, a Continue that waits) is said in the player’s one tooltip and in the live region.
  • Its look comes from the theme’s player.* tokens (player.top.background, player.top.text, player.bottom.background, player.bottom.text, player.accent, player.onAccent, player.progress, player.tooltip.background, player.tooltip.text, player.radius, player.density), with the charter as default; every pair is checked for contrast (E_THEME_CONTRAST).
  • Resources (resources[]: id, title, description, and asset, a file of assets/ shipped in the package, or url, an https:// address, one of the two): a PDF or an image opens in a preview over the module (the browser’s own viewer for a PDF) with « Télécharger »; another file downloads; a link opens in a new tab. Formats an LMS web server may refuse are W_RESOURCE_TYPE; another address than https:// is E_RESOURCE_URL.
  • Glossary (glossary[]: id, term, definition, variants[], translate: false keeps the term and its variants in every language, source: where the term was found, a file of sources/ or a screen, for the author only): a panel with a search field and the entries in the alphabetical order of the course’s language; texts link to entries (§4.3). « Relier les termes aux écrans » writes those links: the first time a term or a variant is in a screen’s own texts, when the glossary option linkTerms is turned on, for a term added while it is on, and on demand; never otherwise, so a link the author removed stays removed.
  • « IA et crédits » says what was made with AI (generated voices and the speakers who have one, generated images, videos and sounds) and the credits: credits[], one line each, then those the voices ask for. Its button shows « IA » (« AI », « KI » in the player’s languages) when something was made with AI, « i » otherwise. The first time a generated voice plays, its tooltip says « Voix générée par IA » for a while (AI Act, art. 50(5)).
  • Checks: W_PLAYER_NO_WAY_BACK (neither the outline nor Previous), W_PLAYER_END_UNREACHABLE (Next hidden and the last screen out of reach of the screens’ own navigation), W_PLAYER_AI_MENTION_HIDDEN (AI content and « IA et crédits » hidden: AI Act art. 50(4) and (5)), W_PLAYER_CONTROLS_HIDDEN and W_PLAYER_TRANSCRIPT_HIDDEN (narration without its controls or its transcript: WCAG 2.2, EN 301 549, RGAA), W_PLAYER_EXIT_HIDDEN (Quit hidden in an LMS package, §8), I_PLAYER_SPEEDS_NONE, W_PLAYER_LOGO_CONTRAST (an SVG logo whose colours do not stand out from its bar, without a plate). Settings that lived in navigation before 19 October 2026 (showMenu, showProgress, footer) and the course’s narration.seek: "none" are E_PLAYER_SETTING_MOVED.

4.9 Dialogues

A dialogue block holds a branching conversation (D61; guide: docs/dialogues.md). Its nodes[] are moments of the conversation: lines[], then a way out, in this order: options[] (a decision of the learner, under prompt), next (a node, or cases { if, go } where the first that holds wins), end (true: the first of endings[] whose if holds; or an ending’s id), else the next node of the list, else the end. A line is a narration line (id, speaker, text, delivery, pronunciations) with expression, cues (an anchor on its own words with a gesture, an expression or presentation actions) and if; without speaker it is a narration. An option (always the learner’s: no speaker) has id, text, label, quality (good, ok, bad), reply (lines), feedback, points, set and add (the dialogue’s vars), do, next, end, retry, once, fallback, if with unavailable; without next or end it takes its node’s way out. Conditions in a dialogue: chosen (node.option), seen (a node), var (a variable of the dialogue), variable (of the course, read only), score (out of 100). Options and cues run presentation actions only (show-block, hide-block, show-layer, hide-layer, play-media, pause-media, set-object-props, reveal-step): a dialogue never writes the course’s variables nor navigates.

  • The walk is one pure implementation (@elabora/player dialogue/): enter a node and play its lines (those whose if holds), offer its options (greyed when retried here, hidden or greyed by unavailable when their if fails or a once option was taken; the fallback when none can be chosen, else the way out; never one taken by itself), choose (the effects before any later condition), leave (retry: the same options, the choice replaced; else the option’s way, else the node’s), end. A condition that cannot be evaluated holds. A path holds at most 60 decisions.
  • State. The progress of a dialogue is the list of its choices; everything else is recomputed by replaying it (resume, going back, a new version: replay stops at the first choice that no longer exists). mode (practice, assessment, exploration) sets revisit (none, retry, after-end, anytime), feedback (now, debrief, both) and scoring (none, quality, points, ending); a final decision cannot be revisited, nor anything before it.
  • Score and tracking. The first run to an end is the only one scored, and in each decision its first choice: quality is the mean of good 1, ok ½, bad 0 over the decisions met; points the sum over the highest sum a path can reach; ending the ending’s score. A dialogue whose scoring is not none is scored with its weight and records one choice interaction per decision of its scored run.
  • Suspend data. One string per dialogue under its key (dialogue:<block>): p|r|e|f, the current path (each option by its own key in elabora.lock.json, choice:<block>#<node>.<option>; ~ after a first choice replaced; >node for a jump), the scored run when it differs, and bit fields of the options explored and the endings found (ending:<block>#<ending>), places never reused.
  • Characters. course.json speakers[] take color (a colour token), look (svg: an SVG that follows the characters’ convention; or portrait and expressions, pictures by expression) and narrator. The convention (docs/dialogues.md): the SVG’s root declares data-expressions, data-default-expression, data-gestures (name:seconds) and data-portrait (the face’s box for the round avatar); everything that moves is in <g data-part="character">, on which the player sets the state (data-speaking, data-talking, data-mouth closed/small/open/wide, data-expression, data-gesture, data-listening, data-paused, data-reduced-motion); a part is shown when its data-mouth, data-expression and data-gesture match the state (a mouth that names the expression before the generic one), data-part="eyes-closed" is transparent until the character’s blink shows it, and every animation stops under data-paused and plays none under data-reduced-motion. Its own CSS only moves it. No id, no text, under 40 KB (W_CHARACTER_CONTENT); the group, the four generic mouths and the declarations are W_CHARACTER_CONVENTION. The course data carries it sanitized (D33, class t-character, its styles scoped per speaker) with what its root declares.
  • In the player (docs/dialogues.md): the thread is a log whose bubbles arrive whole (character, learner, narration; trainer notes with their quality in words); the options come in a room under it that keeps its height, and the focus waits in its group « Votre réponse » once the learner answers, never before. messaging and scene (the backdrop and the cast from 761 px wide, a thread narrower); pace, reveal (ink, appear, instant; instant under reduced motion or « Texte : tout de suite ») and begins set the rhythm; a line’s words, mouth and cues follow its audio’s time and it ends on the audio’s end; « Choisir autrement », « Reprendre d’ici », « Recommencer » and the debrief as the mode allows. In a slide the dialogue’s box keeps its size and nothing moves while it plays. The theme’s dialog.* tokens colour the bubbles, the words said and to be said and the notes, checked for contrast once the course sets one.
  • Voices. A dialogue’s lines with a speaker are voice lines (§4.7: their audio at assets/narration/<language>/<screen>/<line>.m4a, the voice tools, translation with their markers); with learnerVoice, so are the learner’s answers, as the line <node>.<option> said by the learner speaker. Missing audio is said once per dialogue. A package ships each line’s audio at its media quality as narration/<screen>.<block>/<line>.m4a (narration/<language>/… for another language), and the course data gives each line, and each voiced answer, its voice (src, null without audio; duration; words; mouth; estimated) and each cue its time in its line. Without audio, the voice is estimated from the text.

4.10 Activities: fill-in, sort, reflection (D63)

Three activities of the player, scored, saved, recorded and announced like a quiz; their templates hold no field: the player builds the inputs, lists and buttons in their hooks (docs/components.md §3).

  • fill-in. Blanks are written in its Markdown, [[answer|other accepted]]; the build turns each into a <span data-t-blank="n"> of the rendered text and moves its answers into the course data. typed: a field per blank, accepting an answer whatever its case, the spaces around and between its words and the form of its quotes and apostrophes, its accents too with ignoreAccents; menus: a list per blank of every blank’s first answer and the distractors; bank: those words to place, a word chosen then its blank (a mouse may drag). Lists and banks are shuffled the same way at every build. Scored by the share of its blanks right; after the last try the right answers show in place.
  • sort. pairs (each item to its own target, each target holding one), categories (two to four targets of several items), order (no targets: the items written in the right order, shown in a fixed shuffled order). An item is chosen, then where it goes (WCAG 2.5.7), with the keyboard or by tapping; a mouse may drag; order has move up and move down buttons. Scored by the share of its items placed right (Moodle’s per-pair rule); after the last try each item says where it belonged.
  • reflection. text: a field of maxLength characters (500 by default), saved with a confirmation and, with model, an expert’s answer to compare with; scale: a point from scale.min to scale.max. Never scored: answered once saved, not emptied by a retake. download offers « Mes réponses », a text file of every reflection of the course and its answer, made in the browser.
  • quiz on pictures. presentation: "images": a grid of picture cards, each option with its image and alt; "image": the quiz’s image with each option a zone at its x, y (0 to 1 from the top left), a marker of 44 px at least, named « Zone 1 »… for screen readers until the answer is final, then by its words. A plan question has no pictures: write the question inline, or fork the quiz.
  • Checks. E_FILLIN_NO_BLANK (no blank, an empty one, more than 12), E_SORT_TARGET (an item to a target that does not exist, pairs not one to one, categories outside two to four, a sort without items), W_ACTIVITY_NO_FEEDBACK (a fill-in or a sort without feedback for wrong answers), E_QUIZ_PICTURE (a quiz on pictures without its pictures or zones).

4.11 Steps and interactive pictures (D63)

Two blocks the learner goes through, their items controls of the player’s primitives (docs/components.md §3): visited, saved, read by { "visited": … } conditions and by a screen’s step that opens: "explored", whose locked Continue says what is left in their words (« Il reste 2 étapes à voir », « … événements à voir », « … repères à ouvrir »).

  • steps (« Étapes »). Not the screen’s steps, which pace a screen’s blocks. carousel (default): one step at a time on the sequence primitive, Previous and Next, a button per step and where the learner is, the room of its tallest step kept; intro and summary are its first and last items (ids intro, summary, so no step takes them), and the intro has « Commencer ». timeline (« Frise »): the steps one under the other on a line, each date before it (above it in narrow places), each a reveal, its dot filled once seen. scroll (« Défilé »): native scroll, never held; the step crossing the middle of the window is current (data-t-follow), and on places 560 px wide or more, with motion, a figure stays in view beside the text (side: end or start) and follows it: replace (each step’s image over the one before, a step without one keeping it), build (each step’s image a transparent layer over figureImage, step n showing layers 1 to n), focus (figureImage with each step’s region { x, y, w, h }, 0 to 1 of the picture, outlined and the rest dimmed). Narrower, with less motion, without the player and in slides, each step shows its own picture with it (build: the layers up to it; focus: the whole picture, its region outlined); screen readers always read those, the following figure being hidden from them. Numbers (numbered, default true) are decorative.
  • hotspots (« Image interactive »). Markers of 44 px at x, y (0 to 1 of the whole picture, before any crop: the editor places them the same way), numbered or showing one icon (plus, info, question, eye), named « 2. title », never pulsing. popover (default): the text floats by its marker inside the picture, below it in the picture’s top half and above it otherwise, scrolling inside itself rather than leaving the picture; side and below: the texts share one place beside or under the picture, the room of the longest kept, a hint while none is open; tour: the markers in order on the sequence primitive, the markers its buttons. In places narrower than 560 px the texts are a numbered list under the picture (a tour stays a tour), each entry counting once seen; with a focus, the picture is then shown half as large again, cropped at its sides around it, a marker the crop leaves out kept at its edge.
  • Checks. W_ITEMS_MANY (a carousel’s steps over 7, its intro and summary aside; markers over 8), W_HOTSPOTS_CLOSE (markers closer than 44 px on a phone 375 px wide), W_STEPS_SCROLL_SLIDES (a scroll in a course in slides), E_STEPS_FIGURE (a scroll without what its figure follows), E_BLOCK_ITEM_DUPLICATE (two items with one id, or a step named intro or summary beside the carousel’s), and their pictures’ alt (E_IMAGE_ALT_MISSING).
  • As text. LESSON.md, the client view and translation carry their words: the steps under their titles (numbered, or dated), the intro and the summary, their pictures; the picture, then its markers’ texts in order.

4.12 Sounds (D67)

  • Interface sounds (course.json sounds.interface): short sounds the player makes after a learner’s gesture, the same in dialogues and questions: select (a choice), good, ok, bad (a feedback; a question answered is good or bad), back (going back, starting over, replaying), open and close (a history or a debrief), end (a dialogue that ends well). when: auto (default: on when the module has sound — a voice line whose audio is there, an audio or video block, a sound of the story, music or an ambience), on, off; volume (0 to 1, 0.5 by default, under the player’s volume); replace: a role’s sound replaced by a sound of the course (its id), or null for none. Learners switch them off in « Affichage » (« Sons de l’interface »), kept for the course with the display choices. The player’s own sounds ship as elabora/sounds/<role>.m4a, only in packages whose course has them on. A sound the course names that is not in assets/sounds/ is E_SOUND_MISSING.
  • Sound files: assets/sounds/<id>.<ext> (FLAC for what was lossless, the original’s own bytes for what came lossy: Vorbis, Opus, AAC, MP3) and its sidecar <id>.json (schema sound: title, source, url, sourceId, author, license, licenseUrl, credit, modified, trim, duration, loop, caption by language, generated, checked). Packages ship each sound used as M4A (AAC), at every media quality. A sound without a readable sidecar, or under an unknown, non-commercial (NC) or no-derivatives (ND) licence, is E_SOUND_LICENSE; CC0, a public-domain mark, CC BY, CC BY-SA, own and generated ship.
  • Sound lines: in a screen’s or a layer’s narration.lines and in a dialogue, { id, sound, caption?, volume? } (with step and pauseBefore in a narration): the sound at its place among the lines, as long as it lasts. A narration’s sound line is laid into the narration’s track (no file of its own in the package); its caption goes with the captions, in italics, and in the transcript. Its id shares the screen’s line ids.
  • Actions: play-sound (sound; volume, 0 to 2, default 1, an ambience 0.3; loop: an ambience of the screen; fade, seconds of fade in; fadeOut, seconds of fade out at its end, or, for an ambience, when it stops without a fade of its own; start and end, the part of the file played, in seconds; duck, dB; caption) and stop-sound (sound, or every sound of the story; fade), in events, cues and a dialogue’s options (presentation actions). Sounds of the story and ambiences stop at the end of their screen. Since schemas 0.1.7:
    • A volume above 1 (up to 200 %) raises a sound recorded too quietly, no further than brings its loudest sample, over the part played, to −1 dBFS: louder would clip. A sound already that loud plays at its own level.
    • A part (start, end) plays a piece of a longer file, the file left whole; an ambience loops its part. end after start, both inside the file, else E_SOUND_PART. Fades in and out longer together than the part are shortened together.
    • duck (above 0, at most 30 dB; since 0.1.8, the music and the ambiences only) lowers the music and the ambiences while a sound played once plays: down in 0.1 s as it starts, back up in 0.5 s after it; the deepest of two at once wins. 6 lowers a little, 12 clearly, 20 much. A sound never lowers a voice (the narration, a dialogue’s lines, a layer’s, an audio or video block): the voice comes first. An ambience never ducks (W_SOUND_DUCK_LOOP).
  • Music and ambiences (sounds.background[]: sound, from, to (default from), volume 0.25 (up to 2, as play-sound’s), fade 2 s, start and end, the part of the file looped): a sound looped with a crossfade over a range of screens in the course’s order, playing on from one screen of its range to the next; ranges that do not exist, run backwards or overlap are E_SOUND_BACKGROUND. Music and ambiences are lowered by 20 dB while a voice or a media block plays (WCAG 1.4.7); learners switch them off in « Affichage » (« Musique et ambiances »); hidden, it is W_SOUND_MUSIC_SWITCH_HIDDEN.
  • Captions of sounds are advised when a sound carries meaning no text gives, never required: no finding asks for one. They are units of translation.
  • Credits: « IA et crédits » gives the credit line of each CC BY or BY-SA sound (the sidecar’s credit, else one made from its title, author, licence, changes and address; hidden, W_SOUND_CREDITS_HIDDEN) and says when sounds were generated; CREDITS.txt at the root of the package lists every sound shipped with its source and licence.

5. Free objects

A free object is code under contract. Its manifest (object.manifest.json, schema object-manifest) declares:

  • planRefs (objectives and items it serves; contract layer), pedagogicalRole;
  • props: a JSON Schema subset; each prop has a default and a description (and a title); x-* keywords drive the generated editor (x-group, x-advanced, x-control, x-token, x-text-on, x-object-id, x-editable-by, x-translatable). A list is a prop of type array with x-control: "list" whose items are a simple value or an object of simple fields (each with a type and a title); values are checked item by item. Colours are props of format: "color": bound to a theme token (x-token) when they carry the course identity, with a literal default for a material of the scene; a colour drawn as text names its background (x-text-on) and keeps 4.5:1;
  • objects (parts with stable ids, clickable in the editor), state (schema of getState()), persistState (saved for resume, with a maximum length);
  • events with optional tracking (interaction → cmi.interactions linked to an objective; score field; completion);
  • requires.three: "r186", assets, budgets, a11y, sandbox;
  • strings (the object’s words, ICU messages read with ctx.t) and language; blocks reword them with props.strings.

A pedagogicalRole of decoration makes a decorative object for the course backdrop (theme.backdrop.object): no plan references, no events, no saved state, no focusable element.

The code (object.js) is an ES module exporting mount(container, ctx) that returns { update, getState, setState, dispose }. It runs in <iframe sandbox="allow-scripts"> without allow-same-origin and under a CSP without network. It may import only three, two vendored addons and relative files. Full contract: docs/free-object-contract.md.

5.1 Size and look

The free-object block sets the frame shape: aspectRatio (fixed ratios, auto for a height requested by the object with host.requestHeight(px), fill for the height of a layout slot), narrowAspectRatio under 640 px, minHeight, maxHeight. The frame receives the course theme: --t-* variables, embedded fonts and a small UI kit (t-ui-* classes) so objects look like the course. A layout that floats its own content over the object declares the covered part with --t-safe-top|right|bottom|left; the object receives it in px (ctx.safeArea, host.onSafeArea) and keeps its key content out of it. On a page longer than the window, the object hears where its frame is (ctx.view, host.onView: visible, ratio, progress, entry, exit, as a CSS view() timeline), to appear as the learner scrolls to it.

5b. Design layer

The course stylesheet (theme/), component templates and stylesheets (components/) and generated figures (generators/) are free code for agents, bound by three rules checked by the validator: words stay in blocks and props (no text in CSS, templates or figures), packages stay offline (no external URL), colours come from tokens. Templates hold structure, decoration and bindings only; interactions come from the player’s behaviours. Generated assets record their provenance and are reported when stale or edited by hand. Details and the stable DOM hooks: docs/design.md.

The cascade (theme.cascade, D32): components-over-theme orders the player, the course stylesheet (its defaults), the component stylesheets, then the course stylesheet’s top-level @layer overrides blocks (what it overrides in components on purpose); without it, as written before 13 October 2026, the course stylesheet comes after the components (theme-over-components), and elabora theme upgrade changes the order without changing the look.

theme.backdrop declares a decorative background behind every screen: CSS layers the stylesheet paints, which follow the learner through data-screen, data-section, data-step, data-variant, data-screen-index, data-section-index, data-section-count and data-direction, and an optional decorative object that receives the same context and the room the player’s bars cover; screens pick a variant or none, the others take default, and #elabora-app[data-backdrop] is the variant shown. With a backdrop or without, #elabora-app says where the screen stands (data-screen-index, data-section-index, data-section-count, and --t-screen-index, --t-section-index, --t-section-count, from 0), so a theme can vary by part. In slides, nothing moves when the learner answers: activities reserve the room of their tallest outcome, blocks shown by events keep their place, and layers can open as a panel over part of the screen instead of a modal dialog.

6. Delivery: SCORM 1.2

Version 0.2 compiles to SCORM 1.2, one SCO, no sequencing, the lowest common denominator of the documented LMSs (see docs/delivery-scorm12.md for the choice). The package contains imsmanifest.xml at the root, index.html, the player (elabora/), the compiled course (elabora/course.js, JavaScript rather than JSON because some servers do not serve .json), assets and compiled free objects. Packages are deterministic: the same sources give the same bytes.

7. Tracking model

  • cmi.core.lesson_status follows completion.reportAs (completed-incomplete, passed-incomplete, passed-failed, completed-failed); the status never regresses (completed and passed are final; failed may only improve).
  • The score (cmi.core.score.raw, 0–100) is sent once every scored item is decided: quizzes (correct = full weight), fill-ins and sorts (the share of their blanks or items right, times their weight), scored decisions (option with correct), scored dialogues (their first run to an end, §4.9), scored free objects (their score event, 0–1).
  • cmi.core.lesson_location = the current screen’s key (elabora.lock.json; packages built before 2026-10-07 wrote its id, which the player still reads); cmi.core.exit = suspend (until completion, then according to tracking.exitAfterCompletion); cmi.core.session_time at every commit.
  • cmi.interactions.n for quiz submissions (choice), numeric answers (numeric, with the expected value as correct pattern), fill-ins (one per blank, <block>.<n>: fill-in with the words typed, choice by the word’s letter for menus and words to place), sorts (matching as 1.a,2.c for pairs and categories, sequencing as b,a,c for order), reflections (fill-in, the first 255 characters on one line, or likert; neutral), decisions, each decision of a dialogue’s scored run (choice: its first choice, correct, neutral or wrong from its quality) and tracked free-object events, with the plan objective id.

8. Session end

Packages never rely on unload (Chrome and Edge stopped firing it on 22 September 2026). The player commits after every significant change, saves again on visibilitychange: hidden, commits and finishes on pagehide (when the page is not kept in the back-forward cache), shows a Quit button (the player’s exit item, §4.8; W_PLAYER_EXIT_HIDDEN when it is disabled) that commits and calls LMSFinish, and never calls LMSInitialize again after LMSFinish.

9. Learner state and updates

The suspend data is a compact JSON (v, visited and completed screen keys, quiz answers, numeric answers, decisions, variables, free-object states, completed objects with their score, visibility overrides, the step reached on screens left part-way, the visited controls, each dialogue’s progress (§4.9, under t), fill-in answers, sort placements and saved reflections (§4.10, their worst case at their longest: a reflection at its maxLength, as a persisted string variable), and in a package of several languages the language the learner chose, D59). Every stateful element has a short permanent key from elabora.lock.json (base 36 counter); activities are keyed by behaviour, so a fork of the quiz block keeps a quiz key and changing a block to a fork (or detaching it) keeps its learner state. Because keys never change for an id, and elabora rename carries an element’s key to its new id, a new version of the course keeps the progress of every element that still exists (the screen a learner left is kept by its key too, so a learner resumes on a renamed screen). The compiler computes the worst-case length with the same encoder and fails (E_SUSPEND_DATA_BUDGET) when it exceeds the strictest target LMS.

10. Conformance

A reader accepts every file valid against the schemas. A writer produces files valid against the schemas and preserves unknown free-object files. A validator reports the diagnostics of docs/diagnostics.md with their codes. A compiler produces a SCORM 1.2 package that behaves as §7–9 describe. The reference implementation is @elabora/core.

11. Versioning and migrations

The format follows semantic versioning on elabora/MAJOR.MINOR, given by course.json format. Readers reject a newer version than they know (E_FORMAT_NEWER: nothing past the format is read, nothing is written) and migrate older ones. Every format change ships with its migration, a pure step from one version to the next on the raw files of a project, and a fixture course in the older version.

  • A course in an older format validates and builds: Elabora reads it migrated in memory and reports I_FORMAT_OUTDATED with what the migration changes.
  • elabora migrate [--dry-run] writes the migration as one change after a backup version (elabora undo reverts it). Edits through Elabora, the desktop app when it opens a course and library imports migrate first, the same way.
  • A migration changes the values it must and keeps the rest of each file as it was written (indentation, one block per line); what a new version no longer has is listed as dropped, with why nothing is lost.

0.1 (13 October 2026): the first version, everything this document describes. Its migrations chain is empty: the first format change will bring the first step.

Planned: SCORM 2004 (per-LMS editions), cmi5, more behaviours (matching, ordering, hotspots, fill-in).

12. Interoperability

Simple blocks map to and from LESSON.md 1.9 (Slate): text, heading, callout ↔ note, image, accordion (↔ accordion, tabs), quiz ↔ knowledge-check. Course components are exported through their fallback. Everything else is converted with a loss report (elabora lesson-md export|import). Elabora stays the superset: plan links, per-option feedback, events, layers, free objects and tracking have no LESSON.md equivalent.

Modules made with Storyline 360 (its .story source or a publication) and Rise 360 (its exports) are imported as Elabora modules (elabora import <file> [dir], beta): a slide’s objects are read in order and become blocks, layers, events and variables; what has no block yet falls back to a text, a picture or a note for the author, and import-report.json lists every loss and the choices left to the author. See docs/decisions.md D47.

13. Translation

One SCORM package per language, or one package of every language that learners switch in place (elabora build --languages all, D59: the course’s own language and each translation that is ready and approved; elabora/course.<lang>.js and narration/<lang>/ beside the course’s own; the language chosen kept in the learner’s state). The source course keeps its translations: translations/<lang>.xlf (XLIFF 2.0, one unit per string, with the source text it was translated from) and translations/<lang>.approval.json (approval of the translated plan). Units cover every string a learner or a plan approver reads: course and section titles, label overrides (course.ui), speaker names, narration lines, the plan (objectives, items, locked texts, questions), screen and layer titles, block props marked x-translatable (component props of kind text and markdown), free-object props marked x-translatable, object strings and a11y texts; and the words authors read in the editor about the course’s own design, never shown to learners (editor units): the names of its styles and backdrop variants and what they are for (theme.style.<kind>.<name>, theme.backdrop.variant.<name>, each with its .label), its components’ names, descriptions, slot labels, styles, settings, their sections (x-group) and the words of their choices (x-enum-labels) (component.<id>.…, written in components/<id>/component.json), and the settings, groups and events of the free objects its blocks place (object.<id>.…, in their manifest), so that a course made in another language (a template’s, D68) reads in it in the editor too; a component’s instructions, for agents, stay as written. Editor units hold no language back: translate status counts them apart (editor), readiness, a translated build and a package in several languages do not wait for them, and the translated course keeps the source’s words where none is given. Unit ids are stable and readable: plan.text.TXT-1, block.<id>.options.<id>.feedback, block.<id>.strings.<key>.

  • elabora translate export --to <lang> creates or updates the XLIFF; a unit whose source changed keeps its previous translation as a starting point and is marked to translate again.
  • elabora translate status counts translated, missing and stale units and reports translations that break the text (maths, links, placeholders, schema limits) and word anchors to place.
  • Word anchors (§4.4) travel as inline markers in narration units: <mrk id="w-imprimante" type="elabora:anchor">imprimante</mrk> in the source; the translation puts each marker around its own words (<mrk id="w-imprimante">printer</mrk>), and the translated course anchors there, timed by its own voice. A build refuses a marker left out; --allow-missing anchors it to the start of its line.
  • elabora translate build --to <lang> --out <dir> generates the translated course: a copy of the source (design, objects, assets, regenerated figures) with the translated strings, language set, translationOf in course.json and plan.json. It is validated, fitted and built like any course; edit the source, not the copy.
  • The player has labels in fr, en, de, es, it, nl and pt (ICU plurals); in another language course.ui must give them (W_UI_LABELS_LANGUAGE).

14. Project archives

A course is exchanged as one file, <course-id>.elabora: a ZIP holding the project’s files at its root (unzipped, it is the course folder), a manifest elabora-archive.json with the size and SHA-256 of every file, and optionally the project’s history under history/ (journal, backup versions and their contents). Its format, elabora-archive/1, and what an importer must check are described in docs/architecture/storage.md. elabora export [dir] [--history] [--sources] writes one (with --sources, the author’s sources under sources/); elabora import <archive> reads one into the app’s project library.