Elabora course format — specification 0.2
On this page
- 1. Project layout
- 2. Identifiers
- 3. Contract layer: plan.json and its versions
- 4. Native layer: course.json and screens
- 4.1 course.json
- 4.2 Screens
- 4.3 Blocks
- 4.4 Events
- 4.5 Variables
- 4.6 Components
- 4.7 Narration and timeline
- 4.8 The player
- 4.9 Dialogues
- 4.10 Activities: fill-in, sort, reflection (D63)
- 4.11 Steps and interactive pictures (D63)
- 4.12 Sounds (D67)
- 5. Free objects
- 5.1 Size and look
- 5b. Design layer
- 6. Delivery: SCORM 1.2
- 7. Tracking model
- 8. Session end
- 9. Learner state and updates
- 10. Conformance
- 11. Versioning and migrations
- 12. Interoperability
- 13. Translation
- 14. Project archives
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), ashortname, Bloom level, success criterion, sources;items[]: the ordered storyboard. Each item has akind(intro,exposition,demonstration,practice,scenario,assessment,summary), apart, the objectives it serves, optionallockedTexts[](validated texts,verbatim: truefor one the client wants « à l’identique »),questions[](validated assessment items with options, correctness and feedback) andvoiceover[](the item’s voice-over, line by line:speaker,text; translated with the plan, not kept word for word in the module; written byelabora 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), andquestion, 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;namefilter for one control);quiz.submitted(quiz, numeric, fill-in or sort block,resultfilter: a fill-in or a sort is correct when all its blanks or items are);decision.selected(decision,optionfilter);object.event(free-object block,name,wherepayload 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, andlayerwithat.secondscounts from the opening of a layer without narration);timeline.ended(playback reaches the end of the timeline; withlayer, the end of that layer’s timeline:hide-layerthere 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 controlname); 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(withoutlayer: the layer open on top, a dialog before a panel, the last opened first; for a Close button inside a layer; itslayer.closedfires as for any other),show-block,hide-block,reveal-step(the steps of the screen up tostep, 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-timelineandplay-timeline(the timeline of the screen; a modal layer pauses it and resumes it by itself; withlayer, 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; withlayer, or to a line of a layer’s narration, that layer’s timeline),play-mediaandpause-media(an audio or video block; playing one pauses the narration),start-dialogue,goto-dialogue(node) andrestart-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;autoplays the steps one after the other;next-screenalso goes to the next screen at the end (W_AUTO_ADVANCE, WCAG 2.2.1); the learner can turn both back intolearner(« 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 isseenorfree, and the player’s controls hide the bar of every screen withseekBar: false, §4.8).course.jsonnarrationgives 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(schemanarration-audio:screen,line,language,textHashof the line’s text in that language after Unicode NFC with whitespace runs made one space and trimmed,duration,provider(recordingfor a human voice),voice,generated, optionalaudioHash,createdAt,text(the line’s text the audio says),words(when each word oftextis said, from the start of the file, with ascore: what word anchors resolve to; written by forced alignment, D27) andaligner(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,cclosed,ssmall,oopen,wwide, measured on the audio and its words when the tools write it, and by the build when it is missing; D61), and for generated voicesspokenHash(the text the voice was told, after the pronunciations) andsettingsHash(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.jsonanchor) 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;occurrencepicks a word said more than once; French elisions such asl'andqu'are words of their own), withoffsetseconds 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’swords, 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):inandout, each{ at, effect, duration, direction }, say when a block comes and goes. Withoutinit shows with its step; withoutoutit stays until the end. Effects:none,fade(default),rise,slide,zoom,wipe,draw(the strokes of its inline drawing: an image block’s SVG withinline: true, or a component’s markup; a wipe otherwise), or an effect of the theme (course.jsontheme.effects, its stylesheet’s@keyframes t-fx-<name>andt-fx-<name>-out);direction(left,right,up,down) forslideandwipe. 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, orsteps[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 typestack, « Pile ») holds contents that follow one another in the same place, the size of its largest: each content shows from itsinuntil the next one’s (transitioncrossfade,cutorslide). 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 withpauseBefore, no step;seekseen,freeornone, 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 asseekallows, 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, andnarration/<screen>.vtt, its captions with the speakers’ voices) and one per narrated layer (narration/<screen>.<layer>.m4aand.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 ofassets/, 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.partsfalse keeps the bar in one piece, not a button,options.percentfalse 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,speedsnone,0.75-1.5or0.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,nextandexit(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 ofresources,glossary,credits,exit,previousandnextthat follows another item. - Defaults: top
logo(disabled),title,progress,spacer,resources,glossary,credits,exit; bottomoutline,controls,spacer,previous,next;displayandlanguagein neither bar (enabled, they go after the bottom and the top bar’s spacer).positionwindow(default: the bar stays in the window) orpage(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, andasset, a file ofassets/shipped in the package, orurl, anhttps://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 areW_RESOURCE_TYPE; another address thanhttps://isE_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 ofsources/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 optionlinkTermsis 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_HIDDENandW_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 innavigationbefore 19 October 2026 (showMenu,showProgress,footer) and the course’snarration.seek: "none"areE_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/playerdialogue/): enter a node and play its lines (those whoseifholds), offer its options (greyed when retried here, hidden or greyed byunavailablewhen theiriffails or aonceoption was taken; thefallbackwhen 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) setsrevisit(none,retry,after-end,anytime),feedback(now,debrief,both) andscoring(none,quality,points,ending); afinaldecision 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:
qualityis the mean of good 1, ok ½, bad 0 over the decisions met;pointsthe sum over the highest sum a path can reach;endingthe ending’sscore. A dialogue whosescoringis notnoneis scored with itsweightand records onechoiceinteraction 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 inelabora.lock.json,choice:<block>#<node>.<option>;~after a first choice replaced;>nodefor 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.jsonspeakers[]takecolor(a colour token),look(svg: an SVG that follows the characters’ convention; orportraitandexpressions, pictures by expression) andnarrator. The convention (docs/dialogues.md): the SVG’s root declaresdata-expressions,data-default-expression,data-gestures(name:seconds) anddata-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-mouthclosed/small/open/wide,data-expression,data-gesture,data-listening,data-paused,data-reduced-motion); a part is shown when itsdata-mouth,data-expressionanddata-gesturematch 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 underdata-pausedand plays none underdata-reduced-motion. Its own CSS only moves it. Noid, no text, under 40 KB (W_CHARACTER_CONTENT); the group, the four generic mouths and the declarations areW_CHARACTER_CONVENTION. The course data carries it sanitized (D33, classt-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.messagingandscene(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 ») andbeginsset 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’sdialog.*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); withlearnerVoice, so are the learner’s answers, as the line<node>.<option>said by thelearnerspeaker. Missing audio is said once per dialogue. A package ships each line’s audio at its media quality asnarration/<screen>.<block>/<line>.m4a(narration/<language>/…for another language), and the course data gives each line, and each voiced answer, itsvoice(src, null without audio;duration;words;mouth;estimated) and each cue itstimein 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 withignoreAccents;menus: a list per blank of every blank’s first answer and thedistractors;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;orderhas 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 ofmaxLengthcharacters (500 by default), saved with a confirmation and, withmodel, an expert’s answer to compare with;scale: a point fromscale.mintoscale.max. Never scored: answered once saved, not emptied by a retake.downloadoffers « Mes réponses », a text file of every reflection of the course and its answer, made in the browser.quizon pictures.presentation: "images": a grid of picture cards, each option with itsimageandalt;"image": the quiz’simagewith each option a zone at itsx,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’ssteps, 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;introandsummaryare its first and last items (idsintro,summary, so no step takes them), and the intro has « Commencer ».timeline(« Frise »): the steps one under the other on a line, eachdatebefore 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:endorstart) and follows it:replace(each step’simageover the one before, a step without one keeping it),build(each step’simagea transparent layer overfigureImage, step n showing layers 1 to n),focus(figureImagewith each step’sregion{ 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 atx,y(0 to 1 of the whole picture, before any crop: the editor places them the same way), numbered or showing oneicon(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;sideandbelow: 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 afocus, 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 namedintroorsummarybeside 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.jsonsounds.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 isgoodorbad),back(going back, starting over, replaying),openandclose(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), ornullfor 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 aselabora/sounds/<role>.m4a, only in packages whose course has them on. A sound the course names that is not inassets/sounds/isE_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(schemasound:title,source,url,sourceId,author,license,licenseUrl,credit,modified,trim,duration,loop,captionby 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, isE_SOUND_LICENSE; CC0, a public-domain mark, CC BY, CC BY-SA,ownandgeneratedship. - Sound lines: in a screen’s or a layer’s
narration.linesand in a dialogue,{ id, sound, caption?, volume? }(withstepandpauseBeforein 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;startandend, the part of the file played, in seconds;duck, dB;caption) andstop-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.endafterstart, both inside the file, elseE_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(defaultfrom),volume0.25 (up to 2, asplay-sound’s),fade2 s,startandend, 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 areE_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 isW_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.txtat 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 adefaultand adescription(and atitle);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 typearraywithx-control: "list"whoseitemsare a simple value or an object of simple fields (each with atypeand atitle); values are checked item by item. Colours are props offormat: "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 ofgetState()),persistState(saved for resume, with a maximum length);eventswith optionaltracking(interaction→cmi.interactionslinked to an objective;scorefield;completion);requires.three: "r186",assets,budgets,a11y,sandbox;strings(the object’s words, ICU messages read withctx.t) andlanguage; blocks reword them withprops.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_statusfollowscompletion.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 withcorrect), 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 totracking.exitAfterCompletion);cmi.core.session_timeat every commit.cmi.interactions.nfor quiz submissions (choice), numeric answers (numeric, with the expected value as correct pattern), fill-ins (one per blank,<block>.<n>:fill-inwith the words typed,choiceby the word’s letter for menus and words to place), sorts (matchingas1.a,2.cfor pairs and categories,sequencingasb,a,cfor order), reflections (fill-in, the first 255 characters on one line, orlikert;neutral), decisions, each decision of a dialogue’s scored run (choice: its first choice,correct,neutralorwrongfrom 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_OUTDATEDwith what the migration changes. elabora migrate [--dry-run]writes the migration as one change after a backup version (elabora undoreverts 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 statuscounts 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-missinganchors 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,languageset,translationOfincourse.jsonandplan.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.uimust 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.