Menu

D24. The slide timeline: narration and timed steps (8 Oct 2026)

This entry is phase 1 of the timeline design, built in the core. Authors coming from Storyline expect a slide narrated by a voice, with things that appear as the voice names them, Play, Pause and a seek bar. Storyline’s timeline fires its triggers again when the learner drags the bar back and undoes no state, the source of most bugs its community reports.

  • What is seen, what happens. What is seen at a time t is a function of t and of the steps reached: which blocks show, the caption, the line spoken. What happens is what events do (variables, layers, navigation, completion). Seeking recomputes what is seen and fires nothing; timeline triggers fire only when playback crosses their cue (previous < cue ≤ now, the “time marches on” rule of HTML media cues), so replaying a passage crosses its cues again. The course state never goes back: an answer stays answered, a step reached stays reached, a block of a later step hides when the learner seeks back before it and keeps its answer. Snapshots that would undo state on a seek back were rejected (they make cheating easy and confuse learners).
  • Steps are segments. A narrated screen’s timeline is its narration lines in order, each line in a step (step, default 1), each step’s segment running from its first line to its last. advance: "learner" (default) stops playback at the end of each segment not yet followed by a reached step, and Continue reveals the next step and plays its segment; "auto" chains the segments; "next-screen" chains them and then goes to the next screen by itself (W_AUTO_ADVANCE, WCAG 2.2.1: only this one imposes a time limit); course.narration gives the course’s defaults of advance and seek; the learner’s preference « M’arrêter à chaque étape », and phones (step by step, scrolling to the first new block), turn auto and next-screen into learner. A narrated screen has its own Continue, in the screen: in slides its place is kept at the end of the screen from the start, and it shows at the end of the step heard (seek seen or none) or at any time (free); on phones it comes in the page where the next step opens. At the end of a step, Play does what Continue does; Next goes to the next screen. Steps keep everything they had: Continue, opens: "answered", reveal-step, the reserved room in slides, the focus on the first new block, and resume: the suspend data’s p (the step reached) reopens a narrated screen at the start of that step’s segment, so it costs no new byte. A step may hold lines and no block.
  • Script first, anchors to lines. screen.narration.lines[] holds the script (id, unique in its screen; speaker, a speaker of course.speakers; text, translated with the course; step; pauseBefore, seconds of silence before the line, 0.4 by default, 0 for the first line). Cues are written against lines (timeline.reached { at: { line, edge } }), never in seconds: a voice generated again, recorded or translated has other durations, and a cue on a line survives them. Seconds are only for screens without narration, and word anchors, timing.in/out on blocks and seconds anchors are phase 2: the anchor object has room for word, offset and seconds. completeWhen: "viewed" on a narrated screen means the end of its timeline reached by playback; with navigation.lockUntilComplete, Continue also waits until the segment shown has been heard.
  • Audio lives next to the script, by language, one file per line. assets/narration/<language>/<screen>/<line>.m4a (AAC in M4A, mono, a conformant master: at most 192 kb/s, 96 kb/s advised; silence at both ends trimmed to 0.1 s at most, the pauses come from pauseBefore) and its sidecar assets/narration/<language>/<screen>/<line>.json (schema narration-audio): screen, line, language, textHash (sha256: and the hex SHA-256 of the line’s text in that language after Unicode NFC, whitespace runs made one space and trimmed: narrationTextHash in @elabora/core), duration (seconds), provider (recording for a human voice), voice, generated (true for a synthetic voice, AI Act art. 50), optional audioHash (of the M4A when the sidecar was written), createdAt, words ({ text, start, end } in seconds from the start of the file, for phase 2), take, credit and model, and for a generated voice spokenHash (the text it was told after the pronunciations of the line, then of the course: line.pronunciations, course.pronunciations, by language) and settingsHash (its speaker’s voice settings with the line’s delivery over them). A line is out of date when its text hash, its audio hash or, for a generated voice, its speaker’s voice for that language (course.speakers[].voices[<language>]: provider, voice, free settings), its spoken hash or its settings hash no longer match; another model does not make it out of date; it is missing without both files. Takes other than the one in the course stay in the voice tool’s own data. Files are found by convention, not named in the screen, so a project check reads them (asset changes re-run project checks). Renaming a screen moves its folders; the build never ships these files.
  • Built into one track per screen. At build, the lines of a screen in the course’s language are decoded to PCM, laid end to end with their pauses (a step’s first line at least 0.3 s after the previous segment, so stopping at a segment’s end never lets a sound through) and encoded once (AAC-LC, mono, 48 kHz, 64 kb/s: about 0.5 MB a minute) into narration/<screen>.m4a, cached in the project’s data by the hash of its inputs, deterministic. Cue times come from the decoded samples and go into the course data with the caption cues; a WebVTT file with speaker voices (<v Name>) ships next to the track, and the transcript (the script with the speakers’ names) is drawn by the player. Until every line of a screen has its audio in the language built, the screen has no track: its timeline runs on estimated durations (15 characters a second), silent, with captions (W_NARRATION_AUDIO_MISSING). One package per language carries only its voice.
  • The player’s clock. One timeline per narrated screen, driven by its <audio>: the player reads currentTime at every animation frame, so the timeline waits when the audio waits for the network and speed (playbackRate 0.75–2, pitch kept) follows; without audio it counts performance.now(); in tests a virtual clock advances only when told (window.ELABORA_TEST.virtualTime, D6’s rule for objects). A modal layer pauses the timeline and its closing resumes it; a panel does not. A sound or video block that plays pauses the narration, and the narration pauses them: two voices never overlap. When the browser refuses sound before a gesture (play() rejected), a large Play button waits for the learner.
  • The bar shows only on narrated screens, in the footer before Previous, in place of the counter, so that the footer keeps its height (the app’s mockups, validated on 8 October): Play/Pause, Replay the step, the seek bar (WAI-ARIA APG seek slider: role="slider", aria-valuetext with the time and the step, arrows ±5 s, Page Up/Down a step, Home/End; the part not heard yet dotted), the time, Captions, the playback settings under « 1× » (speeds 0.75–2× and « M’arrêter à chaque étape », offered when the module has screens that chain), Mute and Transcript (by step; a line already heard takes the playhead there); below 1280 px Mute and Transcript go into « … », on phones the settings too. Seeking is seen by default (back anywhere, forward up to the furthest point reached), free or none (no bar: Play, Pause, Replay, the time and the step in words). The learner’s Pause stops the voice, the animations and the free objects. Shortcuts take modifiers (Ctrl+Alt+P, R and C, as Storyline’s; aria-keyshortcuts), never one character (WCAG 2.1.4); Space acts on the bar’s buttons only. Captions show in a band laid over the bottom of the screen that moves nothing, naming the speaker when it changes. Markup and stylesheet stay isolated (.t-timeline*, #elabora-app[data-timeline]); the mockups’ final look comes with the app’s stylesheet.
  • No new dependency, no new format version. These are compatible additions to the format, as do: [] and hide-layer without a layer were: a course without narration is unchanged and keeps its bytes. Revisit if: authors need cues inside a line before phase 2 (then word anchors from the voices’ word timings), learners need several voices per screen at once (then tracks per speaker), or LMS players refuse M4A (then MP3 tracks).