English | 日本語
A web page that opens a working folder directly and does what the dev server does: preview, edit the source and the script on the slide, and leave comments. It also opens an Egatari file (.ega.html) (one HTML file that carries its source, following the Egatari format) without any folder, edits it in memory and saves it again. There is nothing to install and no terminal to use. The first screen puts Egatari files first (“Open a file (.ega.html)…”, “New…”; working folders through “Work in a folder…”).
| Form | What it is | Here |
|---|---|---|
| Working folder | A folder with deck.html, narration.md, audio/ …; the commands, AI coding tools (Claude Code, Codex, Gemini CLI …) and other editors read and write it |
Edit it with “Work in a folder…” (Chrome / Edge) |
Egatari file (.ega.html) |
One HTML file that carries its source and audio (--portable); open it in a browser to play |
Edit it with “Open…” (any browser) |
Play-only (--play-only) |
An Egatari file without the source | Cannot be edited (open the original Egatari file or the working folder) |
Built HTML (out/deck.html) |
The presentation output of the command line (node kit/build.mjs; the editor no longer exports it) |
Cannot be edited (open the working folder) |
- Browsers: Google Chrome or Microsoft Edge on a computer (Windows, macOS, Linux, ChromeOS). Folders use the File System Access API (
showDirectoryPicker), which Safari and Firefox do not have; on other browsers the first screen says so, together with the way to make a new lesson there: New… → Have a chat AI write it. An Egatari file can be opened and edited in any current browser; without the file pickers, saving downloads the file, and each save makes a new file in the downloads (the first screen and the save notice say so: open the newest one next time). - Network: none, apart from loading the editor itself. The working folder or file is read and written in the browser and is never uploaded. Only if you use Make AI voice on this computer (in the Audio panel), a separate TTS window of the editor's site downloads the model; the editor page itself still connects nowhere.
| Form | How | Notes |
|---|---|---|
| Hosted | https://studio.egatari.org/ | Published from main by .github/workflows/pages.yml. Loads only the font files a lesson needs. The old address …/editor/ redirects there |
Single-file editor (editor.html) |
npm run build:app, then double-click dist/editor.html |
About 10 MB: all font files are inside. Works from file:// in Chrome and Edge; you can copy the file to anyone |
| Your own server | npm run build:app, then serve dist/ as static files |
Any static host (https, or http://localhost), at the root or under a path |
npm run build:app (node tools/build-app.mjs [--out dist] [--no-single]) needs npm ci first (it uses esbuild, a pinned dev dependency).
-
Open a lesson. An Egatari file opens with “Open a file (.ega.html)…” on the first screen or File ▾ → Open… (below). For a working folder (the folder with
deck.html; if there is nodeck.html, the first.htmlfile at the top of the folder), drop it onto the page or press “Work in a folder…” (first screen, or File ▾). The browser asks once whether the page may edit that folder.- The first screen, top to bottom: New… (the main button: a chat AI writes the lesson, see below), the drop area (“Drop a .ega.html here”) with Open a file (.ega.html)…, Recent files, Samples, a small link All parts (opens the parts page
/parts/in a new tab: on the site relative to the site root,../parts/from a frozen copy/vX.Y.Z/; in the offline editor opened from disk, the hosted https://studio.egatari.org/parts/, as the help's parts link does;partsIndexHrefinapp/parts-gallery.js), a small link Make it with Claude Code or Cowork (advanced) (its target is one constant,WITH_CLAUDE_URLinapp/main.js, until the page is decided) and Work in a folder…. The table of the four kinds of files is in the help (?). A new working folder is made withnpm run new. - Continue: when the most recent item is a working folder, the top of the first screen shows Continue: ‹folder name›. Pressing it makes the browser ask for permission and, if you allow it, opens that folder (if you refuse, the first screen stays). When you reload the page (Cmd+R, F5, or coming back after a while) and the permission for that folder is still there (such as Chrome’s “Allow on every visit”), it reopens without a press. Without the permission, nothing asks until you press the button. An Egatari file closes on reload (unsaved changes stay in the browser and come back from “Unsaved changes” on the first screen)
- Recent files: working folders and Egatari files are listed on the first screen (the browser keeps the handles in IndexedDB; the contents are not stored). When you reopen one, the browser asks for permission again (for an Egatari file only to read; Save asks to write). An Egatari file is listed only when the browser gave a file handle (Open a file (.ega.html)… or drag and drop in Chrome / Edge), not in Safari or Firefox.
- Samples (hosted editor only): the first screen shows the samples of this repository (
examples/, without audio) as Egatari files. The top row “Start here” has Introducing Egatari (the screen's language first, the other one with a language label) and the parts showcase (play-only, and a link to All parts); below it are the cards for each kind of sample, Education, Business and Everyday life (with a subtitle and a count). Pressing one lists its samples as tiles (Back returns). A kind with 7 or more samples is split under group headings (Education: classes, university lectures and conference talks, school in-service training; classes in subject order). A tile has a thumbnail (a step that shows what the lesson does), the title, the setting (a class with a subject reads “School lesson · Science”), labels (Book, Voice, Live, Vertical, and the lesson's language when it differs from the screen's), tags (the parts it uses, up to three, then “+n more”) and Play. Pressing a tag lists only the samples that use that part (the heading says “Samples that use Flow diagram (9)”, with Clear filter beside it);?samples-part=<tag>opens the same filter (from the parts list). The order, kinds, groups and subjects are inexamples/index.json, the same as egatari.org'ssrc/gallery/lessons.json(tools/examples-sync.mjs --check --lessonscompares them). Play fetches the file from the same site (examples/<name>.ega.html), checks it against the list's sha256 (taken at build time) and plays the file itself (its own player) large, in a sandbox frame with the preview's rules; since only the site's own samples play there, it does not ask about their code. Below it, Request to the AI (what was asked to make the sample) and the license. Three buttons, and under them one line: “Keeps its structure, theme and parts; the content is new, from your topic.” (it also describes the main button). Make one based on this sample (the main one, backlog #130) opens New… with the sample's setting and subcategory (step 1), theme (step 3) and parts (ticked in step 4). Only a bundled theme that New… offers is carried over; any other theme name becomesdefault(sampleThemeinkit/core/new-deck-pack.mjs), because a sample made for introductions may use a theme of its own with the Egatari logo, which must never pass to your lesson. Steps 1 and 3 show their answered line with a From the sample badge (gone once you change the answer), and the sample's parts in step 4 carry the same badge (a new guess never unticks them, and fitting the limit never suggests unticking them). The sample's parts are described in full even in the light instructions, including standard parts outside the core four; over the limit, fitting first shortens the core parts the sample does not use, then suggests unticking specialized parts it does not use (in the light instructions, then also standard parts it does not use, in the fixed order of a single part set; never the core four), and only when nothing else is left shortens the sample's own parts (fitSampleinkit/core/llm-pack-assemble.mjs, throughsamplePackFitinkit/core/new-deck-pack.mjs; the line under the meter names what was shortened). Only step 2 is open and empty, with the caret in its title. A line at the top says “Starting from the sample “…”. … Fill in step 2 for your own lesson.” Step 4 has Add the structure of the sample “…” (the heading and point of each of its N slides) to the instructions (ticked; unticking makes the instructions smaller): after “What I want”, the copied instructions list each slide's heading and point and ask to follow that structure and presentation (sampleStructureRequestinkit/core/sample-info.mjs; never the sample's full text; it is not an outline, so the AI fits the number of slides to the length in step 2, short if none; whether to ask in two rounds still depends only on that length). Creating storesfromSample(the sample's name) instudio.newDeckofegatari.json. On the command line,node kit/llm-pack.mjs --from-sample <name> [--request <text|file>] [--plan free|paid] [--lang ja|en]prints, byte for byte, what step 4 copies after writing step 2 (the first line of--requestis the title, the rest the content), fitted as after Untick these; the assembly is shared with the editor (kit/core/new-deck-pack.mjs), andtools/test-app-from-sample.mjscompares them for the four samples × plans × languages. Open a copy to edit (small) opens it exactly like a file you chose: the same import checks, the code question fortmlr2026-calibration(it has its own parts) and the editing prompt before the first edit. Close. The link “Make one based on this sample in Studio” on egatari.org's samples page opens the same New… with?from-sample=<name>(a name fromexamples.json, e.g.kokugo-kanbun; it combines with?lang=, and the parameter leaves the address once read). The sample list is built into each version of the app and nothing is fetched, so it opens with that version's samples at the site root, inside a frozen copy (/vX.Y.Z/) and in the offline editor (editor.html) alike. An unknown name opens New… as usual with one line at the top: “Sample “…” not found. New… is open.” Each sample'sexamples/<name>/sample.json(the request, setting and subcategory, theme (only a bundled one New… offers is carried over), parts, structure, whether it has a book, language and rights) is checked by the build and put in the list (tools/examples-portable.mjs; an invalid one stops the build). The Offline editor (editor.htmlfromfile://) does not show the list.
- The first screen, top to bottom: New… (the main button: a chat AI writes the lesson, see below), the drop area (“Drop a .ega.html here”) with Open a file (.ega.html)…, Recent files, Samples, a small link All parts (opens the parts page
-
Edit. The keys are the same as the dev server (and the toolbar has buttons for them; until the right-panel tabs arrive they are a temporary group Edit (e), Script (n), Comments (c), Audio):
Key Action eedit mode; double-click an outlined element to open its source (HTML, with math as \( … \)), ⌘/Ctrl+Enter to savelshow later steps faintly (on/off, an option like captions): every step is shown, steps not yet reached are dimmed ( --preview-opacity, 0.45 by default); the current step carries no mark; answers markeddata-preview="none"stay hidden. The same as?mode=talk&preview=1nscript drawer for the step on screen (speaker, add / delete / reorder turns; Pronunciation… for how a word is read, below) Alt+click (Mac: ⌥ Option+click) leave a comment about that element ( comments.json); the point you clicked is kept as a pinm(or the “Marker (m)” button in the edit-mode bar, or the pen button of the slide controls, which reads “Marker”, with the keym, while editing;dalso opens it)marker, in edit mode only: draw a circle or an arrow on the slide; when you let go the comment box opens for that mark in the side panel. While it is open you can keep drawing (an arrow plus a circle, several marks, up to 20): each stroke joins the same comment. Leave keeps all the marks with the comment ( strokes); Cancel discards them allclist of open comments, numbered; while it is open, the pins of the step on screen are marked on the slide. Click one to go to its slide and step (its pin flashes), “Resolve” to mark it done The edit-mode bar, the source box, the script drawer, the comment box and the comment list sit in one side panel on the right (the preview shrinks beside it), under an orange bar 編集中 — アプリ / Editing — app. They are part of the editor page, not of the lesson; anything inside the slide area can be drawn by the lesson, so trust only controls in the editor's own panels. While the source or comment box is open, the preview does not take clicks. The preview only points at what you double-clicked or Alt+clicked, and the editor reads the source, shows it and writes it when you press Save (or ⌘/Ctrl+Enter). The keys work with the focus on the preview or on the editor.
Pronunciation… (backlog #101; design in the repository's handoff notes) sets how a word or symbol in the script drawer is read aloud. Select the word (or just put the caret in it: the word at the caret is used, as Word's Phonetic Guide does) and press Pronunciation… below the turns. A small field opens under the turn: Pronunciation of “θ”, the pronunciation (filled in from the word's own reading, else the same word elsewhere in this script, else your dictionary, with a line saying which), Listen with read-aloud and Listen with AI voice (only the reading, with this device's voice or with AI voice on this computer; AI voice is offered only where it runs, and nothing is saved), Apply to all in this script (n more), Add to dictionary, and Apply / Cancel (Enter / Esc), plus Remove pronunciation for a word that already has one.
- What is stored is always the reading syntax
{θ|シータ}innarration.md(format spec §8.3), so the audio depends only on the script, wherever it is made. The drawer does not show{}: a word with a reading is shown with the reading small above it (ruby), and clicking it opens the field to change or remove it. The caret steps over such a word as one unit, and Backspace / Delete remove it whole. Undo and redo (⌘/Ctrl+Z, ⌘/Ctrl+Shift+Z, Ctrl+Y) are kept by the field itself: typing is grouped, and each pronunciation applied or removed is one step. Copy and cut put the source text ({θ|シータ}) on the clipboard; pasting inserts plain text (line breaks joined, as the turn is one line) and shows readings as ruby. A reading typed by hand as{x|y}stays as typed until the drawer is opened again. Enter does nothing in a turn (a turn is one line; use “+ Add a turn”). - Only one word or symbol can be selected (R-reading): a selection with a space, punctuation or an expression (
1-η,θとη), one that covers a reading or a mark such as{cue}, one longer than 24 characters or one across two turns is refused with a line saying what to select.Q*,x′,θ̂,N₀,L殻andt-SNEcount as one word. - Apply to all in this script puts the reading on every occurrence of the word that has none: in this step at once (undoable), in the other steps when you save the step (the status says “saving also adds the pronunciation in 4 places in other steps”; a backup of
narration.mdgoes to.cache/edits/). Words are matched whole:SQLis not changed insideMySQL, and a word ending in a kanji is not changed inside a longer kanji word (astts/yomi.pydoes). Only turn text is changed, never headings,@x-lines or notes. - Add to dictionary remembers the pair in this browser (
localStorage, per script language, up to 1000 words), like an input method's user dictionary. When you select the same word later, in any lesson, its pronunciation is filled in; you still apply it, so the script always holds the reading. A word in the dictionary opens with the box checked; unchecking it and applying removes it. Where the browser keeps no data (private browsing, blocked site data, or storage that fails), the box is disabled with one line saying so (“The dictionary cannot be used in this browser … You can still apply the pronunciation”); everything else in the field works. It is not the reading dictionary of AI voice (tts/yomi.yaml): that one is applied just before synthesis, only to Japanese, and holds only words measured to be misread, while a per-browser list applied at synthesis would make the audio differ between machines without any change in the script. - The dev server's drawer has the same field, without the two Listen buttons.
Edit mode and the presenting tools do not mix. Entering edit mode turns off the laser pointer, pen and fading pen (ink already drawn stays visible but cannot be drawn on), stops autoplay and its audio, and closes the slide overview, black screen, the key list (?), fullscreen and a half-typed slide number. Lecture and captions stay as they are. While editing, the tool buttons (autoplay, laser pointer, fading pen, colors, clear, overview, fullscreen) are hidden from the toolbar and their keys (
xvShift+dBackspaceoabf) do nothing; the pen button stays, relabelled マーカー / Marker with the same icon, and it and the pen keydtoggle the marker instead of the pen (see the table above); a tool chosen in the presenter view is turned off again. Leaving edit mode brings none of them back. Keys typed into a text field (the source box, or an input inside the lesson) are always text, never tool or slide keys. The current mode is shown at the right end of the status bar (Editing, Presenter tool: Pen, or Viewing) and announced to screen readers; it never covers the slide.As with the dev server, a save is refused if the file changed since the preview was built (the source hash or the script revision does not match); reload and try again.
Esc closes the open cards one at a time, topmost first: the source or comment box being written, then whichever of the Audio panel, the recording mode, the script drawer, the comment list and the side panel (quality check, file info, help) was opened last. Shift+Esc closes them all. A script drawer with unsaved input asks first. Dialogs and menus close with Esc as before.
Where it is saved. The left end of the status bar always says where the lesson lives (its tooltip explains it). A working folder shows “Saved in working folder “…””: File ▾ → Save is shown but does nothing there (it says why), because the Save of each card and recording, importing or TTS write to the folder right away; to make an Egatari file, use File ▾ → Save as… (editable,
.ega.html). An Egatari file shows “Saved to “…”” (Save overwrites it), ““…” is open (Save downloads a copy)” when the browser cannot overwrite it (each Save downloads a new file), Unsaved changes when something is only in memory (closing the tab loses it, recordings included) or Not saved to a file yet for a lesson that exists only in memory (pasted from an AI chat, or a slides file opened alone). - What is stored is always the reading syntax
-
Work with an AI coding tool. Keep the editor open while an AI coding tool (or any editor) changes the folder. The editor looks at the files about every 1.5 s; when one changes it rebuilds and reloads the preview on the same slide and step. What you are typing in an editor card or the script drawer stays while the preview reloads (it is in the editor page); if the same element or step changed meanwhile, the save is refused. If the build fails, the error is shown over the preview until the file is fixed.
-
The toolbar. It has four items, File ▾, Slide ▾, Quality check and Preview ▾ (plus the temporary panel group and ? for help), and fits one row from 1280 px wide (below that it wraps). The menus follow the ARIA menu-button pattern: Enter, Space or ↓ on the button opens the menu at its first item, ↑ at its last; in the menu ↑ ↓ move (wrapping), Home / End jump, a letter moves to the next item starting with it, → or Enter opens Export ▸, ← or Esc closes it, Esc closes the menu and returns focus to its button, and Tab or a click outside closes it. While a menu or a dialog is open, keys are not sent to the preview (and
e,n,cdo not toggle the panels).Menu Items File ▾ New… · Open… · Save (Egatari file; in a working folder it explains that the folder is saved as you edit) · Save as… (Egatari file: another file, optionally as a new lesson; working folder: an Egatari file, editable) · Export ▸ (For participants (play-only)…, Make a video (MP4)…, For an LMS (SCORM)…, Print slides · Print book (Print script without a book), and for an Egatari file Export working folder…) · File info… · Work in a folder… Slide ▾ Insert slide… (from a template, or have a chat AI write it) · Duplicate · Delete (the slide selected in the slide list, or the slide on screen when the list is closed) · Revise… · Change theme… Quality check runs the browser quality checker; it runs by itself about 1 s after each build and preview load (not while a build is pending), and its badge shows the errors and warnings it found (in the error colour when there are errors); build and save warnings are listed at the top of the panel Preview ▾ the display modes, one at a time: Check as a slide show (f) (also full screen), Check as a video (they reload the preview in that mode) · Show later steps faintly (l) (on/off for the slide show) · Check in presenter view (p) · the note “To present, open the saved .ega.html in a browser” A dot (●) after File ▾ means an Egatari file has unsaved changes.
- Help (?) also offers Download the AI skill, the same language and version as New…, with the same hint.
- Quality check: the browser quality checker. It runs the checks of
npm run checkin the preview and on the source, with the same codes and messages: layout (overflow and the vertical-layout limits, text cut off by a box or going off the slide, font size in figures, title and conclusion length; the samekit/core/layout-scan.mjs), unrendered math such as leftover$…$, です・ます and punctuation, missing script, script that reads a formula, links to slides, emphasis targets, morphs, image and figure names, slide ids and keys, source outside the 1.0 vocabulary, and palette contrast. Click a result to go to the slide. Only the command has the checks that take screenshots (part determinism, body balance) or read files on disk, and the info lists (Reference: In the editor). The numbers can differ slightly from the command quality checker because the window differs. - The built HTML (
out/deck.html) is no longer exported from the editor; build it withnode kit/build.mjson the command line. - File ▾ → Save as… (working folder): writes the lesson with its source and audio as one Egatari file (editable,
.ega.html) to give to others; the working folder stays as it is. - File ▾ → Export ▸ → For participants (play-only)…: an Egatari file without the source (format §3.7, the same as
--play-only): it plays in any browser and cannot be edited. For an Egatari file it is made from the saved file (Save first if there are unsaved changes); for a working folder from the folder. The dialog has a checkbox Add Live (questions and polls); the file name is the lesson title with.play.ega.html. - File ▾ → Export ▸ → Make a video (MP4)…: videos are not made in the editor but in Google Colab (a video notebook that runs on the CPU). The dialog asks you to upload this lesson's file (.ega.html) to Colab, and Open Colab opens it in a new tab. With unsaved changes the button is Save and open Colab; in a working folder not exported yet it is Save as Egatari file and open Colab (both save or export first, then open Colab). If steps need their audio updated, one line gives the count (the video gets the audio as it is). Handed out as an Egatari file, the lesson plays without making a video. How to use the notebook: Video
- File ▾ → Export ▸ → For an LMS (SCORM)…: a SCORM 1.2 package (
.scorm.zip, the same as--scorm): the play-only copy asindex.htmland animsmanifest.xml, to add to Moodle or another LMS as a SCORM package. The dialog says what the LMS records and when the lesson counts as completed (metadata.completionin plain words); Download saves the zip. Unsaved changes in an Egatari file: Save first, as for the play-only copy. See In an LMS with progress (SCORM). - Add Live (questions and polls) in that dialog: ticking it shows the Live settings in the same dialog, and Export… writes the Egatari file (play-only) with Live (questions and polls in class, format extension
live, draft): make the Live ID and join code in the dialog and copy one string that the host pastes once on the host page (the earlier way, pasting the settings copied on the host page, stays under a fold), choose whether participants can send questions and add polls per step (each shown by a label such as “Slide 4 'XML の壁', step 1”, with the slide id and step in the tooltip), copy the poll settings (with the labels) for the host page, and export. To fix before exporting lists what is missing in the editor's language; Export… keeps the dialog open and names the first problem until it is fixed. The file name gets a mark on the title side with the day the Live starts, for exampleTitle (live 10-30).play.ega.html(題名(ライブ 10-30).play.ega.htmlin Japanese); the extension stays. Importing the results after the lesson is in File ▾ → File info… (attachments). The editor itself connects nowhere; the lesson keeps the settings without the join code. See Live. - File ▾ → Export ▸ → Print slides: the browser's print dialog for the slides (one page per slide with the final state of each, as with
tools/pdf.mjs; choose “Save as PDF”; slide backgrounds, including the section dividers, print in their screen colors even with “Background graphics” off). Print book / Print script: see Book. - Preview ▾ → Check in presenter view (or
pin the lesson): a window with the current slide (later steps shown faintly, for the presenter only), the next slide (all its steps shown), the script as speaker notes, a timer and the drawing tools. It controls the preview in the editor window, which also plays the audio: autoplay started in the presenter view (a or its button) plays the script audio once, in the editor window.pin the preview again closes it. In the presenter view, Esc (when no drawing tool is on),por Close presenter view (p) close the window and return focus to the editor; if the presenter window is fullscreen, they only leave fullscreen. While the window does not have focus, a large “Click here to control” overlay takes the first click. Recommended order on a projector: open the presenter view first, then make the audience window (the editor's Full screen) fullscreen on the projector. - Stuck in the presenter view? If it covers the screen and its keys do not respond, on a Mac press cmd+Tab to get back to the editor window and press
p(on Windows, Alt+Tab; not yet verified on Windows). The key list (?) shows the same tip. - Black screen in the presenter view: while the main window is black, the presenter view shows "Black screen", and
bthere also toggles the black screen on the main window. While it is black, the arrows, Space or PageUp/PageDown in the presenter view end the black screen without moving the slide. - Preview ▾ → Check as a slide show (or
f): the preview fills the screen, to check it as projected. Check as a video shows the preview in the other display mode (Video). Show later steps faintly (orl) turns faint later steps on or off in the slide show, an option like captions. This is a preview: to present, open the saved.ega.htmlin a browser. - Keys: after you click the toolbar, header or status bar, the arrows, PageUp/PageDown, Home/End,
b(black screen: the whole preview turns black, with a faint “Black screen — press b or Esc to return” hint in the corner for 2 s, and again when the mouse moves;bagain, Esc, a click, the arrows, Space, PageUp/PageDown or Home/End bring it back without moving the slide),aandfstill reach the slides (not in edit mode, whereb,aandfdo nothing). While an unsaved script holds the preview, a click or key on it shows why, with Save and Revert next to it.
In the presenter view's current pane, enter the counts in m-predict, or use the timer's +1 min / −1 min buttons. The stage and the current pane show the same counts and timer extensions; changes on the stage appear in the presenter view too. The next pane shows the author's values. Input fields and controls keep their own keys, so typing a count does not advance the presentation. After a mouse click on a timer button, Space advances as usual.
Counts and timer extensions survive moving forward or back through steps on the same slide. Entering from another slide or jumping to a slide (including the current slide) restores the author's values. Viewer input is ignored in video mode and does not change the script, authored durations, timeline, video or print output.
The Slides tab at the left edge of the preview opens a list there (✕ closes it again): every slide in order, with a thumbnail of its final step and its heading. The thumbnails are drawn by the lesson itself in a sandboxed frame behind the list (the same frame rules as the preview; it takes no clicks and is hidden from screen readers); the list, its buttons and its dialogs are part of the editor page. The list remembers whether it was open (in this browser).
- Select: click a slide, or use ↑ ↓ Home End in the list; the preview goes to that slide. When the preview moves, the selection follows.
- Insert slide ▾ inserts after the selected slide either from a template or by a chat AI (Have a chat AI write it…, see “Insert slide…” below). From a template: Title and body, Two columns, Figure (a dashed placeholder and a caption), Section divider (
sec) or Quote (aboxwith aciteline). The editor gives it a new id (s-xxxxxx, never one already used in the document) and adds a script section## Heading {#id}with a placeholder line for each step, after the previous slide's section. The placeholder lines start with the mark✎and the template text is fixed (both are listed inkit/core/template-text.mjs), so what is left unchanged is found: the quality check (here andnpm run check) warns about it (narrPlaceholder,templateText), and before TTS (the Audio panel, Colab'stts-job.json,tts/synth.py) and before exporting an Egatari file (editable, play-only, Live;--portable,--play-only) the editor or the command lists it first. You can go on (Make it anyway / Export anyway); otherwise TTS reads the placeholder aloud and it goes into the captions and the readable layer. - Duplicate puts a copy of the selected slide right after it, with a new id. Its script is copied; its audio is not, so its steps show N steps need audio in the list until you record or make them.
- Delete (or the Delete key in the list) asks first, naming the slide, and lists what goes with it: the script section, the audio records in
audio/manifest.jsonand their files (files another step still uses stay), and recorded takes not used yet. Like the stale-audio rule (format §5.4), nothing is regenerated or deleted silently. - Reorder: drag a slide in the list, press ↑ / ↓, or Alt+↑ / Alt+↓ in the list. The ids do not change, the script sections are put in the same order, and the audio stays attached by its key.
- Undo (above the list) takes back the last slide action: the lesson, the script, the audio manifest and the deleted audio files are written back from the editor's memory, and unused takes return to the Audio panel. It is offered until the next slide action, until an Egatari file is saved, or until another lesson is opened; if one of the three files changed in the meantime (an edit, an outside tool), it is refused. The files are also backed up in
.cache/edits/before each action.
After each action the editor writes deck.html, narration.md and audio/manifest.json (only what changed), rebuilds, shows the affected slide and runs the quality check. A screen reader hears what happened (a live region). An Egatari file keeps the change in memory; the next Save rebuilds the file (tier B), since saving without rebuilding cannot change the slide structure (format §7.2; the editor's tier-A save refuses with source-changed and asks to rebuild). An Egatari file opened for script only (another runtime or registry version) or read-only cannot use these actions. A lesson whose slide ids collide must be fixed first (the action says why). Slides without an id keep the name made from their heading; the new and copied slides always get one.
Audio in the toolbar opens a card in the same trusted side panel. It works the same for a folder and for an opened Egatari file. From the top it has three parts (the design is handoff/audio-flow-2026-10-25.md of the project notes; backlog #82, #83, #85, #89):
- A. The lesson's audio (always on top): one status line and at most one button. The line says how many steps have audio (“No audio yet (8 steps)”, “5 of 8 steps have audio”, “All 8 steps have audio”), how many need their audio updated because the script changed, and how many AI-made steps are still in the old voice after you changed the voice (
inputHashdoes not cover the voice, so the panel compares each AI record'srecipe.voicewith the current voice: the choice in the Egatari file, elsevoice.json, else the default; records withoutrecipe.voice, and dry-run audio, are not counted). In an Egatari file it adds “Not in the file until you save”. The button is Make the rest (n steps) when some steps have no audio, Update audio (n steps) when only stale or old-voice steps are left, Save in an Egatari file with nothing left to make, and absent when there is no audio at all (B is open instead). The button continues with the method of the newest audio: recording (recording mode opens at the first of them), the cloud (it exports the script of those steps, step 1) or this computer (it makes them). The newest audio is the record of the step written last on this page, or else (the manifest has no times) the last record in script order whose method is known. The method comes from the record:kind: "recorded"is recording,kind: "tts"with+colabinrecipe.processingis the cloud, other AI voice is this computer; imported audio does not count. Nothing is remembered (the formerstudio.audioMethodinegatari.jsonis neither read nor written). When the method is unknown or cannot be used on this device, the button opens B. - B. How do you want to add audio? (open at first, folded once every step has audio): the answers in this order: Record your own voice (Open recording mode (r), at the first step without audio), Make AI voice in the cloud (Google Colab), and Make AI voice on this computer (shown only when this browser can make it, i.e. WebGPU in the hosted or served editor; it says the model is downloaded the first time and offers Make on this computer (n steps); on a phone, a tablet or a low-memory device it is shown but cannot be chosen, with the line “AI voice cannot be made on this device. Make it in the cloud (Google Colab)”). The panel opens on the method of the newest audio (a recording → recording, AI voice made in Colab → the cloud, AI voice made on this computer → this computer, or the cloud when this device cannot make it), and on recording when there is no audio. An answer chosen on the page is kept until another workspace is opened. Choosing an AI method shows Voice and ▶ Sample as the first line inside it (one picker, or Voice of the explainer and Voice of the questioner for a dialogue, as in New… ②): above step 1 for the cloud, above the button for this computer. They are not shown while recording is chosen.
- C. This step (the step on screen; it follows the slides, and while it makes or writes audio it stays on its step and says so if the slides moved): the heading “Slide 3 “…” · step 2”, the current audio (“AI voice · 5.2 s”, “Recording · 4.8 s · Needs updating”, “No audio yet”), the buttons ▶ Play, Listen with read-aloud, ● Record (r), Import from a file…, ↶ Revert and (only when this computer can make AI voice) Remake on this computer, and the step's script (read only; it is edited in the Script panel) with “n points where the animation follows the voice” when it has
{cue}/{mark}.
The details:
- Current audio. Plays the step's audio from
audio/manifest.json(read from the folder or file in the editor page, never from the preview), with ▶ Play, its kind (Recording, AI voice, Imported), length and source (for TTS: engine, model, voices, seed). A Needs updating badge means the script changed after the audio was made, or it is unknown which script it was made from (format spec §5.4: itsinputHashisnullor differs from the current script; for a manifest written by an oldertts/synth.pywithoutinputHash, the TTS cache recordaudio/seg_<digest>.jsonis compared instead). The quality check (its build warnings) andnpm run checklist stale steps too. The step's script is shown for reading aloud: the field grows with the script (up to about half the panel) and can be resized from its bottom-right corner, and in a dialogue lesson every turn is labelled Expert or Novice (the script syntax; the voice pickers say explainer and questioner). ↶ Revert puts back the audio the step had before the last new take (see below). - Record. ● Record (r) (and Open recording mode (r) in B, at the first step without audio) opens recording mode at the panel's step: there is one way to record. For a single step, press Space, speak, press Space again and Esc: the panel is back, showing the new take for that step (it stays open behind recording mode). Recording mode shows the script in large type with the level meter beside it, keeps the take used for the step right away, and ↶ Revert to previous here walks back through the earlier audio of the step (no audio file is deleted).
- Import a file… takes a WAV, MP3, M4A, Ogg, WebM or FLAC file (up to 64 MiB; the size is checked before reading). Like a recording, it is used right away.
- Voice. One picker per speaking role (one for a single-speaker lesson, Expert and Novice for a dialogue):
bobandalicefor a Japanese lesson, Kokoro voices for an English lesson (eight American and British voices, the rest under More voices). ▶ Sample plays a short pre-made sample (lab/voices/samples/; no model is loaded). In a working folder the choice is written tovoice.json; in an Egatari file it is kept while the page is open (changingvoice.jsonwould force a rebuild on Save), and the audio record keeps the voice that was used. A name invoice.jsonthat AI voice on this computer does not have is handled the same way for both languages: the panel says so and TTS uses the role's default voice (bob/alice, oram_michael/af_heart); the TTS window refuses unknown names instead of silently picking another voice. - Make AI voice on this computer (B, for every step that needs audio) and Remake on this computer (C, for the step) make AI voice in the browser: Irodori-TTS 500M-v3 for a Japanese lesson (with the reading dictionary
tts/yomi.yaml, applied with the same rules astts/yomi.py; the voices ofvoice.json,bobandaliceby default; seed 0 and 40 steps; the local mlx server also uses seed 0, but the browser draws its noise with another generator, so the waveform is not the same as a local take), Kokoro-82M v1.0 otherwise (bob→am_michael,alice→af_heart, or a Kokoro voice name invoice.json). Each part between{cue}/{mark}boundaries is synthesized and post-processed on its own and the parts are joined liketts/synth.pydoes (350 ms between turns), so the take has cue times (sync: "auto"). The first time, the editor says what it downloads (about 1.26 GB for Irodori fp16, 2.44 GB on GPUs without fp16 shaders, about 330 MB for Kokoro; later runs read the browser cache) and opens a small window of the editor's site (lab/tts-helper.html) that loads the model and does the synthesis on this computer's GPU. That window receives only the text to read (after the reading dictionary) and the voice name, and returns audio. WebGPU is required (a recent Chrome or Edge on a computer), and it is not available in the Offline editor (file://): without them the panel does not offer “this computer” at all (the panel does not frame it around WebGPU). The device can make it when all three hold: WebGPU is available, there is at least 8 GB of memory (navigator.deviceMemory, where the browser reports it), and it is not a small screen with only a coarse pointer (shorter side at most 1024 px: a phone or tablet;deviceLimitinapp/tts-client.js). A device with WebGPU that fails the rest gets the “this computer” answer and Remake on this computer disabled, with the reason in one line (the button's title). Other reference voices needtts/synth.py. The engine follows the language of the script, not of the slides:"lang"invoice.jsonfirst, then the script text (mostly English turns in a Japanese lesson read as English), then<html lang>; the voices offered and the reading dictionary follow the same choice. - Post-processing is the same as
tts/core.py, in the browser (kit/core/audio-core.mjs): mono, 44.1 kHz, leading and trailing silence trimmed (−45 dB over a 20 ms window, 50 ms kept), loudness set to −20 LUFS (EBU R128 integrated loudness, one linear gain; where ffmpeg's loudnorm would switch to dynamic processing because the true peak would pass −1.5 dBTP, the gain stops at −1.5 dBTP instead). Because of that difference the profile has its own name,trim-45-loud-20-cap-v1.tools/test-audio-core.mjscompares lengths and loudness withtts/core.pyon the same input. - Text pre-processing (
kit/core/tts-normalize.mjs, rulejoin-num-counter@1) is applied to each part just before it is sent to the engine, in the browser and in the Colab notebook (colab/egatari_tts.py, same rule, cross-checked with shared examples): the ASCII or full-width spaces between an Arabic numeral (0-9, 0-9, decimals like 1.5 included) and a following Japanese character (hiragana, katakana, kanji) are removed, so “1 歩” is read as “1歩” and not “ichi po”; “3 分”, “10 本” and “2 つ” are joined, while “x 2 y”, “Chapter 3 is” and the space before a numeral (“第 3 章”) are left alone. There is no table of counter readings. This is how the audio is made, not what is said:ttsPartsandinputHashare not changed, and the take's recipe records"textNormalize": "join-num-counter@1"(format spec §5.4). Audio without the field (everything made earlier) is not stale and keeps playing, and the panel does not list it: remake a step (Remake on this computer, or the cloud) if you hear a wrong reading. - Writing a take (right after recording, importing or TTS) encodes it as MP3 (LAME, 64 kbps CBR, mono, 44.1 kHz) and writes
audio/<rec|tts|imp>_<hash>.mp3and the step's record inaudio/manifest.json. The MP3 encoder is not part of the editor's code: it is the unmodifiedlame-wasmfile invendor/lame-wasm/of the editor's site (LAME is LGPL; see NOTICE), loaded only when a take is encoded. The the Offline editor has no MP3 encoder and saves takes as WAV (16 bit, larger files); so does the hosted editor if the encoder cannot be loaded. An MP3 starts 1105 samples (576 encoder + 529 decoder delay, 0.025057 s) late; that is written asstartOffset, and the runtime starts the audio at that time so thatdurand the cue times stay aligned (the previous manifest goes to.cache/edits/; earlier audio files stay, liketts/synth.pyleaves old wav files). The record says where the take came from:kind(recorded,ttsorimported),inputHash(the script the take was made from, at the time it was made), and for TTS arecipe(engine,model,voice,seed,dictionary= SHA-256 of the reading dictionary), and for every kindprocessing(trim-45-loud-20-cap-v1) and, for MP3,startOffset. No device name, original file name or other capture detail is recorded. Recorded and imported takes have no cue times (sync: "none", cues empty): cue-driven motion and emphasis then advance by time. The step's length follows the new audio (durin the manifest; the runtime computes the timeline from it); aduroverride intimeline.jsonstill wins, and the panel warns about it. If the take cannot be written (a read-only file, or you declined the editing prompt), it stays in memory under Audio not used yet with Use this take, until you use it or open another lesson. The previous records for Revert to previous are also kept in memory only (up to 20 per step, until another lesson is opened).tts/synth.pykeeps recorded and imported takes (--replace-recordedreplaces them). - Listen with read-aloud (no audio file is made). Reads the current step's script with this device's read-aloud voice (the preview of format spec §5.6).
{x|y}readings read y, and{cue},{mark:…}and@x-lines are not read; Japanese gets the same reading dictionary (tts/yomi.yaml) and text pre-processing (join-num-counter@1) as the AI voice (kit/core/read-aloud.mjs, shared with the player). Listen to the selected word reads only the word or text selected in the script field (with the step's{x|y}readings; the same entry pointreadAloud(text, lang)inapp/read-aloud.jsis what Listen with read-aloud of Pronunciation… uses). While it reads, the panel shows “Reading aloud”; ■ Stop, moving to another step or closing the panel stops it. Only voices that run on the device (localService) are used, never network voices. Without one for the language the panel says so and points to AI voice (the cloud with Google Colab, or this computer). Nothing is written: not the working folder, the Egatari file or the audio records; it is never saved as audio. - Egatari files: a new take changes the in-memory folder; Save then writes it without rebuilding (tier A: only the step's audio record, its runtime audio entry and the archived audio change).
- AI-generated voice. When any step's audio was made with TTS (
kind: "tts"), the player shows a small “AI voice” badge in the toolbar (its tooltip: “The audio is AI-generated”), the same sentence in the key list (?), and one line in the presenter view for those steps; nothing is shown on the slide or with the captions. For imported audio (kind: "imported") the key list and the presenter view say “How the audio was made is not recorded”; recordings are not marked. Every output, including Egatari files and play-only copies, carries the kind indeck-audio(format spec §5.2, §7.3).
Make AI voice in the cloud (Google Colab) in B works on PCs without WebGPU (most school and office PCs) and on phones. Free Colab (your own Google account) makes the same voices, and the editor imports the result with cue times. The panel shows three numbered steps in the look of the New dialog's steps (a rule on the left, a number badge “Step 1” and a heading, one button). Every step can be used at any time, and a step that has happened on this page gets a ✓ (exporting the script → step 1; importing a ZIP file → steps 2 and 3; opening Colab alone does not).
-
Step 1 Export script (Export script; “Puts the script of the N steps that need audio into one file”). Exports the steps that need audio (no audio, stale, or made in the old voice). The first time in this browser the editor asks once (“The script goes to Google Colab”: the script text is processed in Google’s environment, so include only text you may share; slides and other files are not sent; Export / Cancel; remembered in this browser's
localStorage). The model files come from Hugging Face. The downloadedtts-job.jsonholds only the text to read (after the reading dictionary, split at{cue}/{mark}), the voice names, each step'sinputHash, thedocIdand the target recipe. -
Step 2 Run it in Colab (Open Colab) opens the official notebook
colab/egatari-tts.ipynbin a new tab (the link is pinned to a release tag). Under it the panel lists the Colab instructions as their own list, a–d, so they do not read as steps of the whole flow (the text of the top of the notebook,COLAB_STEPSinkit/core/colab-core.mjs, with c pointing at the panel's step 1): a. press “Connect” at the top right (the notebook is set to a T4 GPU); if it says it cannot connect, Runtime → Change runtime type → “CPU”, then Connect again (CPU works too, only slower); b. Runtime → Run all; c. when asked to choose a file, choose the file from step 1; d. when it finishes, a ZIP file of the audio is downloaded. Only the T4 timing is given (“about 20 s for 10 steps on a T4”); a CPU figure will be added once it has been measured. Japanese uses Irodori-TTS 500M-v3 (ONNX, pinned revision), other languages Kokoro-82M. Cell 1 picks one of two pinned version sets from the CUDA libraries it finds:cuda13(onnxruntime-node 1.30.0, @huggingface/transformers 4.3.1, onnxruntime-gpu 1.31.0) whenlibcublasLt.so.13is there,cuda12(onnxruntime-node 1.24.3, @huggingface/transformers 4.2.0, onnxruntime-gpu 1.26.0) when onlylibcublasLt.so.12is, andcuda13without a GPU; it prints the choice asenv: CUDA libraries … → pin set …. The two sets give audio that is close but not bit-identical, so a step may sound slightly different when it is made again on another Colab image. If the GPU still cannot be used, the notebook prints one line saying it uses the CPU instead, with the reason, and carries on more slowly. Theenv:lines at the top of cell 1 (Python, Node, GPU and CUDA, the CUDA libraries found, the pin set and its versions) and theproviders in useline of cell 3 show which path ran; include them when you report a problem (the versions are inCOLAB_PINSoftools/make-colab-notebook.mjs). The notebook starts one long-lived Node worker (colab/irodori_node.mjs) that loads the six ONNX sessions and the tokenizer once, then synthesizes one segment at a time (same seed and steps per segment, so each segment is deterministic); the model is downloaded withlocal_dirso the files are real (not Hugging Face cache symlinks, which onnxruntime-node refuses), and a failing install, clone or Node call stops with the last 30 lines of its error output. It post-processes each like the editor does (trim-45-loud-20-cap-v1), puts 0.35 s between turns and measures the cue times from the real audio. Without a GPU (or after the fallback to the CPU) Japanese uses a CPU profile: fewer diffusion steps (IRODORI_CPU_STEPSincolab/egatari_tts.py, 16 against 40 on the GPU) and the fp32 weights (onnx/, faster than fp16 on the CPU); cell 1 says so in itsenv:line (… · CPU(Irodori の steps=16)). The worker encodes each reference voice once and reuses it (same audio, less time per segment). Cell 4 prints its own times, from a monotonic clock around the real synthesis: each progress line shows the step's time and the running total (3/10 s-intro:2 (+41.2 s, total 2:15)), and the last line the total; trust these rather than Colab's cell timer, which can be wrong for long runs. TickPROFILEin cell 3 to print the time of each stage (text, reference voice, diffusion steps, decoding, post-processing) for every segment. Its core iscolab/egatari_tts.py, which can be dry-run locally with no model (uv run --no-project --with numpy --with scipy python colab/egatari_tts.py tts-job.json out.zip --dry-run); the notebook is generated bytools/make-colab-notebook.mjs(npm run gen). Your own reference voice (optional, Japanese/Irodori only). In the notebook's optional cell, tickUPLOADand pick a WAV or MP3 of 3-30 s (one speaker), or give a Drive path inDRIVE_PATH. The notebook checks that it decodes, is 3-30 s long and is not silent, converts it to 48 kHz mono (the same as the bundled reference voices) and uses it as the Irodori reference for every speaker of the job; a bad file stops with a message saying why. Leave the cell alone and the built-in voices are used exactly as before. The output format andinputHashdo not change, and Kokoro ignores it. Use only a voice you have the right to use (your own, or the speaker's permission). The file stays in your Colab runtime and is not uploaded anywhere else (it is gone when the runtime is recycled). -
Step 3 Import the audio (Import the audio…; “Choose the ZIP file downloaded in step 2”) asks for the downloaded ZIP file with a plain file input (no folder picker, so a browser without the File System Access API, such as Safari, can use it) and reads it (
records.jsonandaudio/*.wav). The editor recomputes each step'sinputHashfrom the current script and compares it (it does not trust the notebook). A step that matches is written like any other take (MP3,kind: "tts",sync: "auto"with the measuredcues,recipe,inputHash,startOffset; the take-over rule of spec §5.3): at once for a folder, in memory for an Egatari file (then Save, by tier A). A step whose script changed since the export is not used: the panel says “Added AI voice to 6 steps. 2 steps were not used because their script changed after you exported it” with Update audio (2 steps), which continues with the cloud for those steps (there is no separate list of such steps any more). A step that is not in the current script, a record that does not hold up (bad path, a cue count that does not match) and missing audio are skipped with a message. A ZIP file made for another lesson (docIddiffers) is refused (“This ZIP file has no audio for this lesson…”, with the other lesson's title when the job carried one). A different reading dictionary or post-processing version only warns. Each record'srecipealso carriesruntime, the pin set and the execution provider that actually ran after any CPU fallback (cuda12/cuda,cuda13/cpu;none/…when the set is unknown,cuda?when the provider could not be determined), because the two sets and GPU versus CPU give slightly different audio; the import keeps it in the audio manifest'srecipe, drops a malformed value, and it is not part ofinputHash, so it never makes a step stale. The same holds fordiffusionSteps(Irodori's diffusion steps) andprecision(fp16orfp32), which the notebook writes for Japanese.
The ZIP file is a download, so it is untrusted. It is read as bytes with limits applied before anything is allocated (ZIP file 512 MiB, 64 MiB per audio file, records.json 4 MiB, at most 400 steps; zip64, encryption and unknown compression are refused; CRC-32 is checked). Only records.json and audio/<name>.wav are read (names are checked), the audio must decode as a real WAV of the recorded length, and nothing in the ZIP file is interpreted as HTML or code (spec §11).
tts-job.json and records.json (egatari: "tts-job" / "tts-records", v: 1) are this product's interchange between the editor and the notebook, not part of the format spec. The optional title (the lesson title, up to 120 characters) in tts-job.json is copied into records.json and is used only to name the source lesson when an import is refused. Each step's record in records.json has the shape of an audio record of spec §5.2 (file, dur, cues, kind, sync, inputHash, recipe). The tests are tools/test-colab.mjs (zip reader, job, import plan, hostile archives, the notebook, and the Python core's dry-run when uv is available) and tools/test-colab-app.mjs (the editor round trip with a ZIP file made by tools/colab-fixture.mjs; folder and Egatari file).
Lessons with components that declare sounds or implement events(k) show Mute sound effects in the slide controls. Use Tab to focus it and Enter or Space to toggle it; its pressed state means all component sounds are muted. The browser remembers this choice when local storage is available. Sounds play on forward step playback in talk (by hand or with Play) after a user gesture; the visible content remains available. Recording mode temporarily silences component sounds throughout the session and restores the saved choice when closed.
r (in the editor page or in the lesson), or ● Record (r) in the Audio panel, opens a wide screen for recording the whole script with the keyboard alone. It shows the script of the step in large type (a dialogue gets Expert / Novice tags; a long text scrolls inside), and the preview narrows to a stage thumbnail beside it and follows the recording cursor, so you can look at the figure while you read. It lives in the trusted editor page like the Audio panel: the microphone, recording, post-processing and writing are never done in the preview, the preview cannot start a recording (a hint from the lesson can only open this screen), and the preview is inert while the screen is open. It opens at the step on screen (from the Audio panel: at the panel's step) and records the steps that have a script, in order. It is the only way to record: the Audio panel's ● Record (r) opens it, and the panel stays open behind it, so that Esc brings you back to the panel with the new take.
Keep recording is on by default and remembered on this device. Step changes play no takes or sound effects.
| Key | What it does |
|---|---|
| Space | Start / stop recording. On stop the take is post-processed (the same as the Audio panel: leading and trailing silence trimmed, −20 LUFS, mono 44.1 kHz) and used for the step right away (kind recorded, with “Revert to previous” as before). You stay on the step. Right after the screen opens (or moves to a step) the Space button reads Getting ready… until the step is loaded; a Space pressed then is kept and starts the take as soon as it is ready (once, even if pressed twice) |
| Enter | While recording: stop, keep and go to the next step. After a take (or on a step that already has audio): go to the next step. With Keep recording on, recording starts on the next step after the previous take is processed and saved, only if the next step has no audio or its audio needs updating. A step with current audio waits. With the setting off, every next step waits. On the last step, Enter keeps the take and stops. A step with no audio says so and does not advance |
| Backspace | Retake. While recording it throws the take away (nothing is written). After a take it reverts that take with “Revert to previous”, back to the previous audio, ready for a new Space. Only audio recorded in this mode is reverted |
| ← / → | Previous / next step (a take being recorded is stopped and kept first) |
| C | While recording, press at each {cue} / {mark} timing of the script (the timings are shown as chips ①②…; the next one is outlined, a pressed one is checked). The press times become the cues (sync: "manual"; the time is the raw press time minus the silence trimmed from the head). If the number of presses differs from the number of timings, no cues are written (sync: "none", a warning is shown, and figures advance by time): a wrong sync is never made |
| Esc | Close (while recording it throws the take away; Shift+Esc closes every open card). Focus returns to where it was |
One step is Space → speak → Enter. A new take never reuses the cues of the earlier take (spec §5.3): it starts with sync: "none" and no cues, and C fills them in. A step whose audio is stale (the script changed) shows a stale notice; a step with timings but no placed cues shows “figures advance by time”. A dialogue is one take per step (one person voices both roles, or two take turns). The meter shows the input level and the elapsed time (a take stops at 600 s and what was recorded up to then is used), the screen says the length of the take and the gain applied, and warns when the input was so loud that it may have clipped. Capture runs for another 0.15 s to receive delayed samples. Before trimming silence, the stop key’s press is cut from 30 ms before keydown; the start is cut through 30 ms after keyup (at least 120 ms after keydown). Enter uses the same cuts for the end of one take and the start of the next. The microphone stays open (one permission, no delay at the start of a take) until the screen is closed, then it is released. The editor never writes if the write is refused (a read-only file, a declined editing prompt): the take stays under “Audio not used yet” in the Audio panel.
For screen readers the screen is a modal dialog; a live region announces the step (n of N, title, whether it has audio, how many cues), “recording”, “Recording continuously: step 3 of 7” while continuing, each cue pressed, and the result of a take, and an alert region reports problems. Every key also has a button (with its key shortcut), and prefers-reduced-motion makes the meter update only a few times a second. While the screen is open, e, n and r do nothing, and the toolbar is inert.
Draft, experimental (spec §3.8 draft): the allowed formats per role and the content checks are those of the reference.
File ▾ → File info… shows the title, license, creators, copyright and attribution from metadata in egatari.json (read only here; edit egatari.json in a working folder), the format (“Format: Egatari 1.0 (Working Draft …)”: the file's version, with the snapshot of the spec copy in vendor/egatari-spec/SOURCE.json when it has one) and the Studio that made the file (“Made with Studio 0.4.6”, from generator.version; for a working folder “This Studio: …”), with the note “Playback uses the player inside the file. Edit with the latest Studio.”, and under them the attachments (headed “Attachments (draft, experimental)”). It lists the files under attachments/ of a working folder or of an opened Egatari file (format §3.8: original source, PDF, code, data, supplements for readers; see reference). Add files… copies files in (the role is guessed from the format: .pdf → pdf, .tex/.bib/.docx/.md → source, .py/.ipynb/environment files → code, .csv/.tsv/.json/.xlsx → data, images and .txt → supplement) and asks for a title for each file (required, one line of at most 120 characters; prefilled from the file name, editable). A file the format does not allow is refused with the reason and the rule (§3.8): by its name before the title prompt (executables, the extensions operating systems run as scripts such as .js, .bat or .ps1, HTML, SVG, ZIP, macro-enabled Office files and legacy .doc, .xls and .ppt files, audio and video, any format no role takes), by its content after reading and before writing (a PDF with JavaScript, automatic actions other than going to a place in the document, or actions other than GoTo and URI, an encrypted PDF that needs a password, an Office file with macros or external references, a renamed executable, a file of two types, audio or video). Values that a spreadsheet may read as formulas are noted but added. Each row has the role (or none: packed but not listed for readers; a role that does not take the file's format is refused), the title (required: a file without one is packed but not listed; it cannot be cleared), the description (optional, one or two sentences, at most 300 characters, shown under the title in the download list), Rename… (a new name is checked like an added file) and Remove. Roles, titles and descriptions are written to egatari.json (attachments; other members kept, a backup in .cache/edits/). Names outside attachments/, with .., reserved or colliding names are refused, and so are files over the limits (10 MiB each, 20 MiB together, 100 files) before they are read. Removing a file from a folder keeps a backup in .cache/edits/. The editor never opens an attachment and never passes it to the preview. In an Egatari file the changes are in memory until Save, which is a tier A save: only the attachment entries of the index, their chunks and the source hash change. Before the first change of an Egatari file the editing prompt applies, as for any edit. Import live results… adds the results downloaded from a Live as a data attachment with a summary; see Live (after the lesson).
Draft (spec §9C, article-v1). A lesson with a book (article.md or article/<name>.md, see Writing a book) opens like any other lesson; books are written with Claude Code or the command line, and the editor app is for reading, fixing and printing them. The book tools (the Egatari Text parser, about 350 KB compressed) and citeproc-js (only for a book that cites references) are loaded the first time you open a lesson with a book; other lessons never load them.
- Preview ▾ → View: Slides / Slides and book / Book arranges the preview's player as viewers will see it (the player's book pane: sync, Watch on slides, Read in the book, as in the viewer); choosing a layout inside the player updates the menu.
- Book (in the toolbar) opens the Book panel. Slides and book may not match lists each slide and section whose text changed after they were linked (or that were never checked); read them side by side and choose Mark as checked (or Mark all as checked) when they still agree. This records the current state in
articleLinksofegatari.json(spec §9C.7); neither text is ever changed for you. - The Book panel also shows the book's errors and warnings (the same as
npm run check), and a plain text field for each book file (choose the file above it). Save (or ⌘/Ctrl+Enter) writes the file as UTF-8 with LF line endings in Unicode NFC (spec §9C.2): in a working folder it writes the file (the previous text is backed up in.cache/edits/); in an Egatari file it changes the lesson in memory, and File ▾ → Save then rebuilds only the book (the slides stay byte for byte, spec §9C.1). A book with errors is not saved; the message lists what to fix. - File ▾ → Export ▸ → Print book prints the book on A4 portrait; a link to a slide prints in the lesson's language as “→ slide 5, step 3” (“→ slide 5” for step 0; 「→ スライド 5・ステップ 3」 in Japanese). In a lesson without a book the item is Print script: each slide small, in its final state, with its script beside it. Print slides prints the slides one per page, as before.
- When citeproc formatted the citations, the line “Citations formatted by citeproc-js — (c) Frank Bennett — citeproc-js implements the Citation Style Language — https://citationstyles.org/” is shown under the book and in the Book panel (CPAL-1.0 Exhibit B, see NOTICE).
- Not yet: pasting or importing a book (Markdown, TeX, Word), changing links between slides and sections by hand, choosing the citation style, and B5 or 2-up printing. The book view uses the fonts of your device.
An Egatari file is what npm run build -- <deck.html> --portable (or File ▾ → Save as… in the editor) writes (into the top of the working folder, named from the lesson title, and it prints the path). It plays like any built lesson, and it carries the slides, script and audio, so the editor can work on it without the original folder. To hand it to others (Google Drive, email, an LMS), see distributing.md.
-
Open. Press Open a file (.ega.html)… on the first screen or File ▾ → Open…, or drop the file onto the first screen. The editor reads the file as bytes and extracts it in a Web Worker with an HTML parser that runs no scripts; the file itself is never loaded as a page, so nothing in it runs during import. Every hash, size limit and path rule of the format is checked first, exactly as
npm run portable -- unpackdoes. The HTML is parsed with budgets counted while parsing (elements, attributes, nesting depth), and the source files it carries may hold at most 500 000 tags, so a file of millions of tiny elements is refused before a full tree is built; a read that takes longer than 30 s stops the worker and refuses the file. The build and the code check of an opened lesson (folder or Egatari file) also run in a worker, within the build limits (see reference); a build that takes longer than 60 s stops that worker and fails (build-timeout), and the page stays responsive meanwhile. The build reads from the folder only the files it references (at most 8 at a time, each up to its listed size; a file that grew since the listing is refused); a reply from a stopped worker is dropped. Every file is read with its size checked fromFile.sizebefore reading and from the bytes read (app/bounded-read.js);deck.html,narration.mdandcomments.jsonhave fixed limits (32 MiB, 16 MiB, 4 MiB) and a larger one is refused without being read. The source then lives in memory (an in-memory folder); the preview, the editing tools and the quality check work on it as on a folder. Outside changes are not watched (there is no folder). -
Save. Save writes the same file again when the browser gave a writable handle (Open a file (.ega.html)… or drag and drop in Chrome / Edge; the browser may ask for permission the first time). Otherwise it asks where to save, or downloads the file when there is no save dialog (Safari, Firefox). The document id (
docId) stays, and Save never sets or changesderivedFrom. File ▾ → Save as… asks whether to Save as a new file:- ticked (“Save as a new file (it becomes separate from the original)”): asks where to save and writes the lesson under a new
docId, with the original recorded asderivedFrom(itsdocIdand source hash). Copyright, attribution and creators are kept. The open document becomes the new lesson (later saves keep the new id). - not ticked: asks where to save and writes the same lesson (same
docId) to another file.
A dot (●) before the file name (and after File ▾) means there are unsaved changes; leaving the page or opening another file asks first. In every save the license, copyright, attribution and creators, members of the index the editor does not know (at every level: metadata, audio records, archived blobs, parts and themes, extensions) and extension data are kept, and untouched audio is copied byte for byte. The editor may add the settings file (the lesson id and metadata at the root of the source) to a file that lacks it; that changes the source hash, which the save updates anyway.
- ticked (“Save as a new file (it becomes separate from the original)”): asks where to save and writes the lesson under a new
-
Saving without rebuilding. When only script (the text of turns, emphasis and timing), audio or the timing overrides in
timeline.jsonchanged, Save does not rebuild the slides. It rewrites only the parts of the file that the format lists for such an edit (§7.2, tier A): the changed entries of the runtime script, emphasis, timing and audio data; the changed steps of the text layer that is readable without JavaScript; the source hash and the archive index with its hash; the archived files (new content added, content that nothing refers to any more removed); and, only when the new text uses characters the embedded fonts lack, a new font supplement (a style element with just the font files that cover those characters, from the editor's own font files; the fonts already in the file are not touched). Every other byte of the file stays exactly as it was, including runtime code written by another tool or another version. The status bar says “saved without rebuilding”. Removing all of a step's script also removes that step's audio; the save notice says there are warnings, and the Quality check panel names the step. Timing overrides are copied entry by entry; the runtime computes each step's length from them with the same formula as before. -
Saving with a rebuild. When you edited a slide (or anything else outside the list above changed, or the file's compiled lesson and its source disagree), Save rebuilds the whole file from the edited source with the editor's bundled kit, parts and themes, so it plays exactly like the preview. Before saving it warns: “The slides are rebuilt with the current version of this editor. Slides you did not edit may look different.”, says why it cannot save without rebuilding, and saves only after Rebuild and save (Cancel keeps the changes unsaved). A lesson that has never been saved (made from the AI's output, or a
deck.htmlopened on its own) is rebuilt without asking: this editor built it, so nothing else changes. When only the theme changed (Change theme…), the dialog says only that: “You changed the theme, so the slides are rebuilt with the new theme and saved. …”. When the file carried another player (its index names a player other than Egatari Player), a second dialog, “The player will change”, names the file's player and the one the saved file will carry (“This file plays with Other Player 2.0.0. After saving, it will play with Egatari Player 1.0.0. …”); Save saves, Cancel keeps the changes unsaved and the file as it was (format spec §4.1). A newer version of the same player is not asked about. Known gap: the editor does not yet compare the slides before and after a rebuild; check the preview after saving. -
Audio that needs updating. Editing a step's script does not touch its audio: the bytes and the kind stay, the step is listed as stale in the warnings, and in the saved file its audio record still carries the hash of the script it was made from, so every reader sees it as stale. Re-record it or regenerate it in the Audio panel.
-
File ▾ → Export ▸ → Export working folder… writes the source, the audio and
audio/manifest.jsonto an empty folder you choose (the same files asnpm run portable -- unpack); a folder that is not empty is refused. The folder then opens, builds and packs like any working folder. -
File ▾ → Save as… (for an open working folder) writes the folder as an Egatari file (editable,
.ega.html). The suggested file name is the lesson title (metadata.titlein the settings file, otherwise the lesson's<title>), made safe for every OS (no\ / : * ? " < > |or control characters, no leading dot or trailing dot or space, no reserved name such asCON, at most 80 characters and 255 bytes); without a title it is the slides file name. The first time, a lesson id is written to the folder's settings file so that later exports keep it. -
Files exported from a working folder (
exportedFrom, format §3.1). Save as… of a working folder (and--portableon the command line) marks the file as exported from that folder, with the folder's source hash at export (no time, path or name). When such a file is opened, an info line (not a warning) says: “This file was exported from a working folder (the folder that holds the slide and script files). Editing it here does not change the files in that folder. If you don't have the folder, you can edit and save this file as usual.”, with an Export working folder… button (the same as File ▾ → Export ▸ → Export working folder…). Editing is not refused. Save and Save as… keep the marker unchanged, so once the file's source hash differs from it the notice adds “This file has been edited since it was exported.” Save as a new file drops it: a lesson with a newdocIdis not an exported file, and reopening it shows no notice. A lesson that starts in the editor without a folder (pasted, or opened fromdeck.htmlalone) never gets the marker. The unpacked folder carries no marker and becomes the master; its next export writes a fresh one. With both the file and the folder at hand,npm run portable -- compare <file> <dir>says which was edited after the export (reference). -
Not saved into the file: comments and the backups in
.cache/edits/stay in memory only (comments are review notes; the format leaves them out).
While you edit an Egatari file, the editor keeps the unsaved changes in this browser (IndexedDB), so closing the tab or a crash does not lose them. It keeps them about 2 s after a change (at least every 10 s while you keep editing, and at once when the tab is hidden): the file as it was opened, plus only the files that changed since then (the slides, the script, egatari.json, timeline.json, attachments, the book and the audio files with audio/manifest.json; the same files Save writes). Recordings are kept once they are written to the lesson. Comments, half-typed text in an open card and a take still being recorded are not kept.
- Restoring. Opening the same file again (same name and content) asks “You have unsaved changes” — ““…” has unsaved changes from …, kept in this browser. Restore them?” — with Discard changes and Restore. Esc decides later. The first screen lists them under Unsaved changes (each with Restore and Discard changes); Restore there opens the kept copy of the file, so you need not choose it again. Restore writes the changes back onto the file and rebuilds; press Save to save them in the file. If the original file changed since the changes were kept, Save asks where to save instead of overwriting it.
- Two tabs. Each tab keeps its own changes and holds them while it is open (Web Locks): changes open in another tab are not offered or listed, and two tabs editing the same file never overwrite each other's kept changes.
- When they are deleted. Saving the file deletes them; so does Discard changes, either here or when another file is opened. Changes not touched for 30 days, and the oldest beyond 10, are deleted. ? → Unsaved changes kept in this browser shows how many are kept and their size, with Discard all. The browser itself may also delete them (when the disk is nearly full, or Safari after about 7 days without a visit); the editor asks the browser to keep them (persistent storage) the first time.
- Limits. A lesson whose kept changes would exceed 256 MiB is not kept (the editor says so once). When browser storage is full, the editor says “Could not keep your changes in this browser (not enough storage).” once and tries again at the next change; editing goes on. The status bar's tooltip says the changes can be restored only while they are actually kept.
- Privacy. The changes stay on this device, in this browser; they are not uploaded.
- Working folders are written as you edit, so nothing is kept for them.
Each time a file is opened for editing, before the first change (the first save from edit mode or the script drawer, or pressing Edit / Script), the editor shows the lesson's title, creators, copyright, license and attribution as written in the file (they are claims; nothing verifies them). If the license does not permit derivatives (a Creative Commons -ND license, or “All rights reserved”), or there is a copyright notice but no license, it warns that modifying or sharing a modified copy may infringe the rights holder's copyright, and editing starts only after you tick the confirmation. Cancel leaves the file unchanged. Saving never removes the copyright, attribution or creators.
- A slides file (
deck.htmlfrom a working folder, for example downloaded from GitHub: it has<section class="slide">and no built runtime or format blocks) is not refused. The editor explains that its script, audio and own parts live next to it in a folder and offers Open the folder… (the folder picker; the folder then opens as usual), Open this file only and Cancel. This file only makes a new in-memory lesson with justdeck.html: no script or audio; registry parts and themes work, and files it references that are not there (lesson-local parts,{{JSON:…}}data, includes, figures) become build warnings, each once, and are left empty (this applies to every in-memory lesson; a folder still fails the build on a missing file). It is treated like any opened file: the code question comes first if the lesson has code, it has a newdocId, there is no editing prompt (no rights are stated), and a notice says onlydeck.htmlis open and nothing is saved yet. Save asks where: Save as an Egatari file (.ega.html)… or Export to a folder… (an empty folder). - A built presentation (such as
out/deck.html: the built runtime anddeck-meta, but no source) is refused with its own message: it is a presentation build; open the working folder (offered as Open the folder…) or an Egatari file instead. - A play-only Egatari file (
.play.ega.html) is refused with a message that names it: it plays in a browser as is; to edit, open the original Egatari file or the working folder. Any other HTML is “not an Egatari file”.
- Refused, with the reason: a file that is not an Egatari file or is play-only (see above), a broken hash or a missing part, a path that is not allowed (e.g.
../), a duplicate member in the index, a document id that disagrees with the settings file, and a newer source contract. - Newer major version, or a required feature the editor does not implement: not opened for editing and never re-exported. The dialog offers Export working folder… to extract the source when the index and the archived data check out with the rules of the version the editor knows; otherwise extraction is refused too. (Such a file may still play in a browser.)
- Script, audio and timing only: registry parts or themes whose hash differs from the editor's (or that the editor does not have), and an existing file with an unavailable exact part requirement. The editor cannot rebuild such a file faithfully, so a notice says only script, audio and timing can be edited: Edit (slide text) is refused, the script drawer works, and Save rewrites only those parts without rebuilding. Slide editing, theme switching and replacement are disabled. For an unavailable requirement, the notice names the part and what it requires, and asks you to update the parts to rebuild; the preview keeps the file's own player and plays the remaining parts.
- Read-only: a file the editor cannot build for other reasons, and a file of the previous kind whose compiled lesson and source also disagree. The lesson is shown, Save is refused, and Export working folder… works.
- Its own code: whether a lesson has code of its own is decided from the built document — the source after every include and preprocessing step, covering every file the build reads as text whatever its extension (an image file included with
/*INCLUDE:…*/counts), and the same bytes the preview then shows (a<script>withoutsrcwhose content is empty or only whitespace is not code; one with only a comment is); the folder is not read a second time to check. Before anything from such a lesson (or a file with the “extended” profile) runs, the editor says so and asks; the preview starts only after Start the preview (Cancel leaves the file closed). The preview is emptied before the question, so neither the previous lesson nor anything of the new one is shown or runs behind it. While it is open, a notice stays at the top: the code runs only inside the preview, without network access, and cannot change the file. The same question comes for a folder with parts (components/…, any.jsfile) or scripts indeck.html, once per opened folder (also when such code appears later).
A first draft can come from a chat AI (claude.ai, Gemini, ChatGPT, …) without a coding tool. The full instructions for the AI (the detailed instructions) are authoring-with-llms.md (日本語: the standards, the output format and the standard parts) plus one part set pack per kind of material in llm-parts/: stats, data, math, maps, kokugo, english, music, business, tools and science. Each part's tier (standard, specialized, extra) and set come from the parts library 2.0 manifest: standard parts are always in the pack, specialized parts only with their set, and extra parts never (backlog #125). With the light instructions chosen in step 4, the editor copies a lighter pack (step 4 below): the standard parts other than the core four get only their one-line catalogue entry. node tools/make-llm-guide.mjs generates them from the contract, the part READMEs and tools/llm-guide/; npm test fails when they are out of date or over a size cap (PACK_CAP_KB in kit/core/llm-pack-assemble.mjs: light, 45 KB for every standard part; detailed, 150 KB for the standard parts with any two sets). npm run gen prints the size of the light instructions with each set, fitted as New… fits it (fitPack in kit/core/llm-pack-assemble.mjs: over 45 KB the core four are shortened too, then specialized parts are left out), and of the largest pair in the detailed instructions; the detailed instructions with every set at once fits the 150 KB cap too (it is only a warning if it does not). Over a cap, it names the pack (light or detailed, language, part sets), the bytes over and its three largest sections. node kit/llm-pack.mjs --sets stats,math --lang en prints the same text the editor copies for the light instructions with those sets, fitted the same way (stderr says when the core four were shortened or which specialized parts were left out; --no-fit prints the pack as assembled). --plan paid describes every part; --sets all --plan paid is the full pack; --list lists the sets with their fitted and unfitted light sizes in KB (UTF-8 bytes / 1024); --help prints the usage.
Beside Copy the instructions in New…, Download the AI skill offers the make ZIP for the current UI language (ja or en). “Add it to a chat AI such as Claude to make lessons without pasting the instructions. Add it again when Studio is updated (the skill’s instructions and quality checks change with each release)”. Add this ZIP to the chat AI, then paste its output into Studio.
-
A chat AI is one way inside three ordinary commands. The command decides what the output does (a new lesson, inserted slides, or a replacement in the current lesson); there is no choice for it. The words are “output”, “code block” (first introduced as a grey box) and “instructions”, and the buttons are named for what they do (Create, Insert, Replace).
- New… (first screen, or File ▾ → New…) is the chat-AI way only; it opens the page below directly. Slide templates are in Insert slide…; a new working folder is made with
npm run new - Insert slide… (Slide ▾ → Insert slide…, or Insert slide ▾ in the slide list) offers From a template (the slide templates of the slide list) and Have a chat AI write it. New slides go after the selected slide (with the slide list closed and the command chosen from the toolbar, after the slide shown in the preview)
- Revise… (Slide ▾ → Revise…, and at the top of the quality check panel) has a chat AI fix the current lesson.
- Change theme… (Slide ▾ → Change theme…) changes the theme of the open lesson. The dialog lists the same themes as step ③ of New (display names with color swatches and, in small text, what each bundled theme suits; the current one marked, your own themes under My themes; a lesson in
business, which is no longer offered, shows it as the current theme with “No longer offered for new lessons”) and Load theme…, which opens the Load theme dialog (below); a theme loaded there joins the list. Onlydata-themeon the source's<html>changes (every other attribute stays; the slides are not touched).themesinegatari.jsonpins the new theme and the themes it builds on (extends) at their current versions, even when one was pinned to an earlier version (you get the chosen theme's current look), and drops pins that nothing uses any more (throughupdateConfig, so other members keep their bytes). The theme and the themes it builds on are looked up in the same order as the build (the source folder → itsthemes/→ your own themes (the theme folder and the themes saved in this browser; the newer one when both have the name) → bundled). A bundled theme is pinned at its current version inthemes.lock.json, any other at theversionin itstheme.json, or"local". The preview is rebuilt; like the slide-list operations it is an unsaved change (●), and Undo in the slide list restores the source andegatari.jsonbyte for byte. A theme of your own (from the theme folder or a ZIP file) goes into the next save asthemes/<name>/, as before; a theme that cannot be found warnstheme not foundand builds withdefault, as before. In a file where only script, audio and timing can be edited, or one open read-only, the item is disabled and says why.
- New… (first screen, or File ▾ → New…) is the chat-AI way only; it opens the page below directly. Slide templates are in Insert slide…; a new working folder is made with
-
New. A full-screen page (like the new-file screens of Google Slides or Canva); ← Back, Cancel and Esc leave it. It starts with “Answer four questions to make instructions for a chat AI (ChatGPT, Gemini, Claude, …). Paste the AI’s output here and your slides are made.” Every step is a question that moves your thinking one step on, in thinking order (where it is used → content → look → send → receive), one heading each. Only the current question is open (a one-open flow, like a checkout). The five headings are always shown in order; each is a button (
aria-expanded; the current one hasaria-current="step"), and questions not reached yet show only their heading, greyed, with no body. Next collapses an answered question to one line (“Setting: Teaching / School lesson Change”) and opens the next one, scrolled into view with the focus on its first control. Answering step 3 opens step 4. Copy the instructions folds step 4 to “Instructions: Light · Japanese language arts · copied Change” and opens step 5, so you come back from the chat tab to the paste fields; once step 4 is open, step 5 can also be opened from its heading (when you already have the output). Change reopens that question alone: earlier answers keep their summaries and later steps keep their state. Insert slides… and Revise… (three steps each) work the same way (Next in step 1, the copy in step 2; every heading can be opened; the next batch of an outline starts at step 2). The answers stay while the page is open. The copy result and errors are announced (live regions).- Where will you use it? (must be answered: nothing is pre-selected and Next stays disabled until there is an answer; backlog #99). “Tells the AI the structure that suits it (for a lesson: introduction, development, summary). If none fits, choose Other.” Settings: Teaching, Research, Business, Guides and Other. Except for Other, More precisely asks for the subcategory (also required, ending with None of these; nothing is pre-ticked): Teaching has School lesson, University lecture, Training and how-to; Research has Conference talk, Seminar or progress report, Invited or public talk; Business has Report, Proposal and decision, Pitch, Introduction and publicity; Guides has Manual (goal → the steps, one at a time → common pitfalls), Rules and regulations (what the rules are about → who they apply to and when → what you must do → the original text and its date) and Information (waste sorting, library, evacuation, course registration…) (the situation → a list to look up by item or situation → where to ask). The chosen subcategory shows its structure and presentation below and adds them to the request (
- Kind: …,- Structure: …,- Presentation: …). Other writes nothing and None of these writes only the kind line (no structure lines); both are stored assubcategory: nullinstudio.newDeck. - What is it about, and who is it for? (“What you write goes at the end of the instructions.”) Title (shown on the cover) is required; Next waits for it. Audience (Leave it to the AI (it works from your answer to step 1) / Write it), Length (Leave it to the AI (about 5–8 slides) / Set it: a number and slides or minutes) and Content (Leave it to the AI (from the title alone) / Write or paste it here (your notes or source text as it is; a large field opens) / Already given to the chat AI (you attached a PDF or other material, or discussed it; below it, “Paste the instructions into the chat where you gave the material.”)). Each defaults to leaving it to the AI, and its field opens only when you choose to write. The fold More (its summary shows the answers, “More: English, one speaker”, plus “, other wishes” when written) holds Language (日本語 / English / Other, which opens a field; the UI language by default), Speakers (One person explains (default) / Two (one explains, one asks)) and Other wishes (optional) with “At most two speakers” in small text. The placeholders follow step 1 (for Teaching, “How levers work”, “Year 6 pupils”, …). There is no separate field for examples or figures (they go in the content).
- The request (
kit/core/llm-request.mjs, field names from the blank form at the end of the instructions) leaves out what is left to the AI. A has Title (used as is on the cover), Audience, Length (“6 slides”, “20 minutes”), Language, Speakers (“one narrator” or “two (an Expert and Novice dialogue)”) and Other wishes; written content goes after it under “## Content”. “Already given to the chat AI” uses B (B. From the material and discussion in this chat: “Make the lesson from the material given in this chat and what we have discussed so far.”); the More fold is shown here too, and B has the same lines as A: title, audience, length, language, speakers and other wishes. - The length also decides whether to ask in two rounds (minutes count at 1 min ≈ 1 slide;
slideCountOfinkit/core/llm-outline.mjs).
- The request (
- How should it look? Theme (“The colours and fonts of the slides. You can change it later with “Change theme…”.”). The list shows display names (the seven bundled themes by colour name: Indigo, Ink, Kokugo, Evergreen, Edo purple, Dark brown, Teal, with what the selected one suits in small text under the list, e.g. “For research and classes”; and, under My themes, the themes of your theme folder and those loaded from a ZIP file, by the
titleornamein theirtheme.json, otherwise the folder name), the same list as Change theme…; Load theme… at its end opens the Load theme dialog (below). The theme goes into the request as one line (- Theme: <name> (<html data-theme="<name>">)), and an output without a theme gets it. - Send the instructions to your chat AI. First, Choose the instructions for the AI: Light (any AI) (with an “If unsure” badge; the default) and Detailed (for paid AI plans), each with a line under it. Light: “Short instructions of up to about 30,000 characters. You can paste them as they are into the free ChatGPT or Gemini too. Depending on the subjects you choose, some part descriptions are shortened or a few parts are left out.” Detailed: “Long instructions that describe every part in full. They suit AIs that can read long text, such as the paid versions of ChatGPT, Gemini and Claude. Gemini may cut a copy-and-paste at about 30,000 characters, so save the instructions as a file and attach it. Choosing only the subjects you use makes the instructions shorter, so the AI uses fewer tokens and chooses parts more easily.” Under them: “Your choice sets the suggested size limit for the instructions (light 45 KB, detailed 150 KB). A chat AI may not read instructions that are too long to the end.” Up to v0.6.6 the question asked for the chat AI's plan (free or paid); it now asks which instructions to give (owner's decision, 2026-11-07). The internal names stay
plan:free(light) andpaid(detailed), and the instructions themselves are unchanged. The choice changes that limit (capKbinkit/core/new-deck.mjs, fromPACK_CAP_KB) and how much the instructions say (in the light instructions, the standard parts other than the four core ones, Bar chart, Flow diagram, Contents and Predict and check, get only their one-line entry in the component catalogue; backlog #125), not which parts you may choose or how many. Then Which parts may the AI use? (backlog #124,handoff/parts-tiers-2026-10-27.md§2): every part of the parts library that the instructions can describe, as checkboxes, grouped by what to convey (partsList): Compare (amounts, features), Change, Part of a whole, Key number, Steps, Timeline, Cause and effect, Cycle, Hierarchy, Position, Overlap, Two quantities, Spread, Contents, Varying a parameter, Predict, then check and Time-boxing, each part in one group only (its first purpose in the manifest; Ranking is folded into Compare), and last For specific subjects (maps, Japanese language arts, English, music: Map of Japan, World map, Kanbun, Kobun, Modern text reading, Stroke order, Sentence structure, Keyboard, Score). For a school lesson (step 1: Teaching / School lesson) that group comes first. Each group is a labelled group (role="group"); each part is a checkbox inside its label with a thumbnail, the name and a one-line description (aria-describedby; names from thedisplayof the parts library 2.0 manifest, copied intokit/core/llm-pack-data.mjsbynpm run gen, or for a part without one fromkit/core/part-labels.mjs, which also holds the descriptions;node tools/part-labels-check.mjsinnpm run gen:checkcompares them). The thumbnail is the first slide of the part's demo at its last step, made bynpm run thumbs(tools/part-thumbs.mjs→app/part-thumbs.js, 192×120 WebP, about 185 KB for all parts) in a light and a dark version (<picture>withprefers-color-scheme); its alt text names the part (“Example: Bar chart”; on the Japanese screen also the demo's title, 「棒グラフのサンプル:年ごとの売上(架空)」). A part without a demo shows its line drawing instead. The tier and set of each part come from the parts library 2.0 manifest (vendor/components-2/manifest-<version>.json, the default catalogue incomponents2.lock.json), copied intokit/core/llm-pack-data.mjs(TIERS) bynpm run gen. Parts in the extra tier (Huffman tree, Grouping answers) are not listed. What is ticked at first: every standard part, and the parts of a specialized set when the subcategory of step 1 or a word in step 2 (title, content, other wishes) calls for it (recommend,initialParts): University lecture, Conference talk and Seminar or progress report → statistics charts; Training and how-to → the timer; Report, Proposal and decision and Pitch → business; Information → maps; and words such as 分布・統計・データ (statistics), 筆算・分数・関数 (math), 地図・都道府県・人口 (maps), 漢文・漢詩・古文 (Japanese language arts), 英文・文型・関係詞 (English), 音階・和音・楽譜 (music), 市場・KPI・売上 (business), タイマー・ワークショップ・グループワーク (the timer); the English screen uses an English word list (WORDS). The parts ticked this way carry a Suggested badge, and one line per set says why (“Also chose Histogram, Box plot, … because the setting in step 1 is “Seminar or progress report”.”, “… because step 2 mentions “kanbun”.”). A part you tick or untick yourself is never changed by a new guess. New… always starts from these ticks (the open lesson'sstudio.newDeckis not read). The hint says “Grouped by what to convey. All standard parts are chosen. The AI won’t use a part you untick; if it needs one, it draws something close in HTML. Tables, images, equations and shapes are always available.”, and under the list “Parts chosen: N of M”. The copied instructions hold exactly the ticked parts (partsPackOptsinkit/core/llm-pack.mjs: the standard parts and the sets of the ticked specialized parts, every other part excluded so that nothing names it; extra parts are never included). The light instructions with every standard part ticked fit the limit. The light instructions' limit is 45 KB and also 31,000 for the paste length (UTF-16 code units plus line feeds, a line feed counting 2 since a paste may arrive with CRLF; Gemini cuts a paste at about 32,000), whichever is reached first (fitSizeandFREE_PASTE_UNITSinkit/core/llm-pack-assemble.mjs; the same fit asnode kit/llm-pack.mjs). Below the list, Instructions: N KB (suggested limit 45 KB) with a bar (role="meter") against the limit of the chosen instructions; for the light instructions the bar and the over-limit line follow whichever of the two limits is closer. In the light instructions, over the limit the core four (Bar chart, Flow diagram, Contents, Predict and check) first get only their catalogue line too, and a line under the meter says “To fit the light instructions, the descriptions of Bar chart, Flow diagram, Contents, Predict and check are shortened.” (fitPlan; nothing is unticked). If it is still over, the bar is striped and a line says “N KB over the suggested limit, so your chat AI may not read it to the end. Parts you could untick: …” with Untick these: with one part set ticked, the candidates are the standard parts outside the core four in a fixed order (Fishbone diagram, Matrix, Cycle, Venn diagram, Tree, Timeline, Comparison table, Pie chart, Scatter plot, Line chart; charts last because the subject sets lean on them;SINGLE_SET_DROP_ORDERinkit/core/llm-pack-assemble.mjs), stopping as soon as it fits, and the set's own parts and the core four are never suggested; with two or more sets, the candidates are only ticked specialized parts (standard parts are never suggested), the ones that shrink the instructions most first, until the excess is covered (trimCandidates). For a long lesson, “20 slides, so you ask in two rounds…” appears above the size. Copy the instructions copies the instructions with, at the end (instead of the blank form), the answers of steps 1–3. With the light instructions the message is: “Copied (N KB). Paste it into your chat AI’s tab and send. It is long, so it may turn into an attached file; just send it. When the AI has finished, go to step 5.” With the detailed instructions, which are more often too long to paste (Gemini cuts a paste at about 30,000 characters; owner's test, 2026-11-05), Save the instructions as a file is the primary button and Copy the instructions the secondary one; under them, “Attach long instructions to the chat as a file. Gemini may cut a copy-and-paste at about 30,000 characters. ChatGPT turns a long paste into an attachment by itself.” Save the instructions as a file downloadsegatari-instructions-ja.txtoregatari-instructions-en.txt(by the pack's language), the exact bytes Copy the instructions would put on the clipboard (UTF-8, no BOM), and says “Saved (N KB). Attach the downloaded file to your chat AI and send it. When the AI has finished, go to step 5.”; the copy message of the detailed instructions drops the “may turn into an attached file” sentence (the line under the buttons already covers it). If step 1 or 2 is not answered, that question opens and says so. The answered line reads “Instructions: Light · 16 parts · copied”. - Paste the AI’s output. “The AI’s output has two grey boxes (code blocks)…”. Two fields: Slides (deck.html) and Script (narration.md), each with its state below (“Not pasted yet”, “✓ 6 slides”, “✓ Script, 6 sections”, or why it could not be read). The whole output pasted into either field is split into both (“That was the whole output; it was split into slides and script.”), and a block pasted into the wrong field is moved to the right one, with a note. When a code block is pasted with text around it or with its fence lines, only the extracted block stays in the field, with a note (“Took the slides code block out of the output.”); after a split or a move the fields also hold the extracted blocks. There is no hidden waiting state; clearing a field starts it over. Create is enabled once the slides are there; with an empty script field, “It will be made without a script (you can write one later).” appears right beside it. Create, Insert and Replace stay in the same place; while disabled, the first missing thing is shown right beside them (“Answer step 1” → “Write the title in step 2” → “Paste the slides in step 5”; while the outline is awaited, “Paste the outline in step 5”; when a field cannot be read, “See the notice under the fields in step 5”; Insert says “Paste the slides in step 3”, Revise “Paste the slides or the script in step 3”).
- Where will you use it? (must be answered: nothing is pre-selected and Next stays disabled until there is an answer; backlog #99). “Tells the AI the structure that suits it (for a lesson: introduction, development, summary). If none fits, choose Other.” Settings: Teaching, Research, Business, Guides and Other. Except for Other, More precisely asks for the subcategory (also required, ending with None of these; nothing is pre-ticked): Teaching has School lesson, University lecture, Training and how-to; Research has Conference talk, Seminar or progress report, Invited or public talk; Business has Report, Proposal and decision, Pitch, Introduction and publicity; Guides has Manual (goal → the steps, one at a time → common pitfalls), Rules and regulations (what the rules are about → who they apply to and when → what you must do → the original text and its date) and Information (waste sorting, library, evacuation, course registration…) (the situation → a list to look up by item or situation → where to ask). The chosen subcategory shows its structure and presentation below and adds them to the request (
-
When you already have the output (Paste the AI’s output). Under the opening line of the New page is Paste the AI’s output, with “For output you already have, from a skill or from before.” under it (the make skill added to a chat AI tells you to paste its output into New… → “Paste the AI’s output”). It hides steps 1–4 and shows Paste the AI’s output with only the two fields of step 5 (the fields are shared with step 5, so what you pasted stays when you go back and forth). Create works without answering steps 1–4, and while it is disabled the note has no step number (“Paste the slides”). The import is exactly that of step 5 (extracting the code blocks, ids, building, the quality check; with errors, Copy a fix request for your AI in the quality check panel). The setting, title and theme are the pasted lesson's own: the name comes from
<title>, or else the first<h1>(ai-draftwhen there is neither), and the theme stays as<html data-theme>(the theme of step 3 is not added). As no question was answered, nostudio.newDeckand no outline are written. Answer the questions instead goes back to the normal flow as it was. -
Based on a sample (Make one based on this sample). Next to Paste the AI’s output under the opening line of the New page is Make one based on this sample, with “Keeps its structure, theme and parts; the content is new, from your topic.” under it. It hides steps 1–5 and Create, and shows Choose the sample to base it on with the same list as the Samples on the first screen (those with a
sample.json: title, number of slides, the book badge), each with Choose (accessible name “Choose: title”). Choosing one reopens New… from that sample, exactly as Make one based on this sample in the sample window (and?from-sample=) does (see Samples above). Answer the questions instead goes back. The list is built into the app, so it is there in the offline editor too. It is not offered in a New… already started from a sample, nor in a build without samples (--no-examples). -
Read from a file. Read from a file under the two fields of step 5 (also in the paste-only form and in step 3 of Insert and Revise; “Choose the output file (.txt, .md), or drag it onto a field.”) reads output that ChatGPT or Claude handed back as a file. A chosen file goes into the slides field, a dropped file into the field it is dropped on, and it is split and moved exactly like pasted text (and is just as untrusted). It is read only through the bounded reader (
app/bounded-read.js) with the same limit as a pasted deck (32 MB,READ_LIMITS.source). A larger file is refused without being read (“This file is too large (up to 32 MB). Choose the AI’s output file again.”), and a file that is not .txt or .md with “Choose a text file (.txt or .md).”; the fields stay as they were. -
While it is being made. From Create until the lesson is shown (building it from the output, opening it and the first build; usually 1–2 seconds), the same logo as when opening a file is shown with “Creating the lesson…”, and the start screen does not come back in between (also when starting from a sample; Replace on an Egatari file says “Replacing…”). If it cannot be built, the dialog comes back with the reason, and the pasted text is kept.
-
Long lessons in two rounds. From 9 slides (minutes count at 1 min ≈ 1 slide) the copied request ends with “First, send only the outline” (R-outline), and the AI replies with one
```outlinecode block (a table of every slide's heading and one-line point). Step 5 becomes 5. Paste the AI’s outline (an outline field and Next; Create is disabled meanwhile); once read, it collapses to “Outline: 24 slides Change” and shows “The slides are made 6 at a time”, the list, Copy the instructions for slides 1–6 (the wholedeck.htmlof that range, with the outline) and the two fields. Creating keeps the outline inegatari.jsonasstudio.outline({v: 1, next, rows}, throughupdateConfig, so other members stay byte for byte). The notice then shows Outline: N slides left (M in all) and Ask for the next 6, which opens the insert page for the next range (“slides 7–12 of the outline”, with the outline and the previous slide) and movesnexton after they are inserted. A broken outline (no code block, not closed, no table rows, a row without a heading, fewer than 2 or more than 60 slides) is refused with the reason.The editor takes out the two code blocks
deck.htmlandnarration.md(labels such as```html,```markdown,```html title="deck.html"or a file name on the line before are understood; Windows line ends, text before, between and after, and extra code blocks are fine; of two blocks for the same file the last is used). A block copied with its copy button has nolines, so its content decides (`<section class="slide"` → slides; `## heading` with `[0]` lines → script). Text with both but nolines cannot be separated and is refused with how to copy it; text that is neither is refused too. An unclosed code block means the output was cut off: the field says so and Copy instructions to resend appears (for only the missing script, or for a shorter lesson). If only the script was cut off, the slides are kept and the resent script goes into the script field. A whole output copied from the screen without code block marks is taken as slides from<!DOCTYPE html>to</html>and the script after it, with a note. -
Ids. The AI is told not to write slide ids. The editor gives every slide without an id a new one and rewrites the script headings to
## heading {#id}with the same code asnpm run ids(kit/core/slide-ids.mjs); when two slides share a heading, the script sections of that heading are matched to them in order. -
Opened like a file. The source is made into an Egatari file in the build worker and opened exactly like a file you opened: the code check reads the content, so an output with a
<script>(or anything else that runs) asks Start the preview first, and Cancel opens nothing. The new lesson has a newdocId, is not saved yet (●), and Save asks where to save it. When it opens, the editor records inegatari.jsonasstudio.newDeck({v: 1, plan, category, subcategory, parts},partsbeing the ticked parts; thesubjects,toolsandpurposesOffof an earlier record are read but not written,planisfree(light) orpaid(detailed) and an earlier record without it reads asfree, throughupdateConfig) the choices as they were when the instructions were last copied. No seed is written (format spec §8.4: a part's presentation variant is chosen by name only,variant="<name>", or else the part's default;variantattributes in the source are kept as they are, and an earlier source'sdata-egatari-seedis an attribute nothing reads and is left in place). There is no editing prompt (it is your own draft). After it opens, the quality check runs. -
Insert slide… → Have a chat AI write it (backlog #93). Three questions: 1. What slides do you want to add? (where they go, how many (1–10) and what to add) → 2. Send the instructions to your chat AI (short instructions of their own,
kit/core/llm-insert.mjs: “the next n slides, after slide X”, the list of slide headings, and only the slide before the new ones with its script; Send to a different chat from the one that made this lesson (adds the full instructions) puts the instructions first — once ticked, the same primary Save the instructions as a file / secondary Copy the instructions swap and attachment-hint line as New's step 4 appear, since the combined text can be as long as the detailed instructions) → 3. Paste the AI’s output (the same two fields as step 5 of New; with an empty script field, “They will be inserted without a script”) → Insert. The slides of the output go in at that place in order and get new ids the way the slide list does (newIdFor); their script sections (## heading {#id}) go in the same order (a slide without one gets a placeholder line); the change is written the same way as the slide list (a copy in.cache/edits/, Undo, rebuild) and the quality check runs. -
Revise… 1. What should be changed? (optional) → 2. Send the fix request to your chat AI (the quality check findings (the check runs again), what you wrote, the open comments (
comments.json; resolved ones are left out; images of pins or markers are backlog #90) and the current slides and script; if there is nothing to fix, it says so) → 3. Paste the AI’s output (the same two fields; “If only the fixed slides are in the output, only those slides are replaced.”) → Replace. One field alone is enough (with “The script stays as it is.” or “The slides stay as they are.” beside the button). -
Copy a fix request for your AI. In the quality check panel. It writes a fix request to the clipboard (and shows it): the findings (errors and warnings), the standards they break with their text taken from the same instructions (standards have names like
R-title), the notes of the parts the lesson uses (the same text as the instructions), the amounts of overflow and small text (“cut the body by 97 px (about 3 lines of 46 px)”, “raise the text from 23.2px to at least 24px”), build notes about the script, and the currentdeck.htmlandnarration.md. For a long lesson (over 40 000 characters together) it carries only the slides with findings and their script and asks for those slides back. Copy the fix request beside the text copies it again (for example, after you copied something else). Paste it into the same chat; Paste the AI’s output… opens step 3 of Revise…. -
Replacing (Revise…, step 3). The output goes into the current lesson (not for a play-only Egatari file) and the document is kept: for an Egatari file the
docId, settings, audio and parts stay and only the source is replaced (unsaved until you save); for a folderdeck.htmlandnarration.mdare written with a copy in.cache/edits/, and the code check asks again if code appears. An output with only some id'd<section>s replaces just those slides and their script sections. With only the script field, only the script is replaced (just the sections, when every section has an id). -
More in the quality check: script for a step a slide does not have (an error), the script of static slides such as the cover, and readings
{x|y}that hold an expression or several symbols (a warning: one reading per symbol or term). The command quality checker has the same three checks.
Exactly the same as the dev server (dev-server.md#files-it-rewrites), all inside the chosen folder:
deck.html: only the contents of the element you editednarration.md: only the lines of the step you edited (comments and other lines stay)comments.json: comments,[{id, created, key, range, pin, quote, comment, status}](pinis the clicked point{x, y}in slide coordinates, 1280×720; a marker comment addsstrokes, the drawn marks (older comments may have a singlestroke), see contract §13; see the dev server. To look at what the comments point at, runnpm run comment-shots -- <working folder>on the folder: it renders each open comment's step with its pin into.cache/comment-shots/)audio/<rec|tts|imp>_<hash>.mp3andaudio/manifest.json: with Use this take in the Audio panel (only the step's record changes; the previous manifest is backed up)- with the slide list:
deck.html(whole slides inserted, copied, removed or moved),narration.md(whole sections),audio/manifest.jsonand, when a slide is deleted, its audio files inaudio/(backed up first; Undo writes them back) - with the Book panel: the book file you saved (
article.mdorarticle/<name>.md, the whole file) and, with Mark as checked,articleLinksinegatari.json(other members stay byte for byte) .cache/edits/: the previousdeck.html/narration.md/audio/manifest.json(and book file andegatari.json) before each change (the newest 30; cleanup deletes only files named like these backups,<time>-<name>, and leaves anything else in the folder)- with Save as… of a working folder, For participants (play-only)… and For an LMS (SCORM)…: the file you chose (and, the first time, the lesson id in the settings file of the folder; the SCORM zip is only downloaded)
- with an opened Egatari file: nothing until you save (edits are in memory); then only the file you chose, or the files in the empty folder of “Export working folder…” (saving without rebuilding rewrites only the parts listed above)
- Chrome / Edge only. Mobile browsers cannot open folders either.
- Models for AI voice on this computer. The model is stored in the browser's cache for the editor's site (Irodori-TTS about 1.26 GB, Kokoro about 330 MB). Help shows how much storage the site uses (
navigator.storage.estimate) and which models are stored, and Delete models removes them (audio you made stays; the next use downloads again). When the TTS window loads a model it deletes the caches of older model versions and asks the browser to keep the storage (navigator.storage.persist); otherwise the browser may delete the models when the disk is nearly full, and Safari after about a week without a visit. - Command line only: TTS with your own reference voices or the server engines (
tts/synth.py), video (tts/,render/), the command quality checker (npm run check),tools/pdf.mjs. The preview plays the audio inaudio/manifest.json(folder or Egatari file) when you autoplay (a): the editor reads each file and hands its bytes to the preview, which plays them fromblob:URLs of its own document (the sandboxed preview cannot read the folder or the editor'sblob:URLs). - Audio panel in the editor app only. The dev server has no Audio panel (open the folder in the editor app, or use
tts/synth.py). Making AI voice on this computer needs WebGPU and the hosted or served editor. - Fonts are subset as on the command line. Saving or exporting an Egatari file cuts each embedded font down to the characters the lesson uses, with the same harfbuzz (WebAssembly) as the command line, so the fonts are byte-identical to a command-line build (
tools/test-app.mjsandtools/test-font-subset.mjscheck this). The subsetter (font-subset.js, about 2 MB) is loaded the first time you export; if it cannot be loaded, whole font chunks are embedded (larger, same look). The preview does not subset. A later edit that adds characters (a tier A save of the script or the book) appends a small supplement for them in every embedded font; an editor without the font files (the offlineeditor.htmlhas no Mincho) saves anyway and names the characters that will show in the device's font. - No Mincho in the Offline editor. The hosted editor includes BIZ UDPMincho (loaded only for lessons that use it, such as theme
kokugo); the Offline editordist/editor.htmldoes not, to stay small. There a Mincho lesson builds with a warning and shows the Mincho font of the device that opens it. - Bundled parts. Library parts (every version in
vendor/components/, withcomponents.lock.json), themes (themes/, with the earlier versions invendor/themes/andthemes.lock.json) and the runtime (kit/) of the repository are the ones bundled when the editor was built. A lesson is rebuilt with the part version whose hash it recorded, when the editor has it, and likewise with the theme version it recorded. When that theme version is not the current one, the preview keeps the recorded colors and the notice above it says so, lists each color that changes in the current version (old → new, with swatches) and offers Update theme; only that button rebuilds with the current version (it setsthemesinegatari.jsonand the lesson's record; Save then writes the new colors). Nothing is restyled silently (reference, “Theme versions”). Parts and themes inside the working folder (components/<name>/index.js,themes/<name>/) are read from the folder as usual. A theme that is in neither (an institution's theme in your own theme folder, which the command line finds throughEGATARI_THEMESor~/.egatari/themes) builds withdefaultand the warningtheme not found: "<name>", and the notice above the preview offers Load theme… (also in the warnings panel) - Load theme. Load theme… (in the theme notice, at the end of the theme list in New… (step 3), and in My themes… on the first screen) opens a small dialog, Load theme: “Load a theme from a ZIP file or a theme folder.”, with Choose ZIP file…, Choose folder… (only in browsers that can choose folders: Chrome and Edge) and Cancel. The file or folder picker opens from that button, so it works on the iPad too.
- Your theme folder. Choose folder… lets you pick your personal theme folder once, for example
~/.egatari/themes(in the macOS picker, ⌘⇧. shows hidden folders). The editor then looks for themes in the same order and under the same containment rules as the command line: the working folder, itsthemes/, this folder, the bundled themes; a theme's files, logos andextendsparent must stay inside its own root (or the bundled themes). The folder is opened read only and the editor never writes there; its contents are treated like lesson files (theme CSS goes through the same sanitising, logos become data URIs), and only the files of the themes a build uses are read. The browser keeps the folder handle in IndexedDB (not the contents) and asks for permission to read it again the next time it is needed: the notice then offers Use theme folder “”. My themes… shows the current folder and its themes, and Forget this folder forgets it. Exporting or saving an Egatari file stores the user theme it used as the lesson'sthemes/<name>/, so the file rebuilds with the same theme elsewhere (an Egatari file made withnpm run build -- --portablealready carries it there) - Themes from a ZIP file (My themes). In every browser, including Safari, Firefox and the iPad, which cannot choose folders, Choose ZIP file… loads themes from a
.zipfile. Every folder in the ZIP file, up to 5 levels deep, that containstokens.cssis a theme named after that folder (atokens.cssat the top of the ZIP file takes the file's name), and all of them are loaded, soextendsbetween themes in the same ZIP file resolves. Onlytokens.css,theme.json, the logos itslogoslists inside the theme folder and top-levelLICENSE*files are read. The ZIP file is treated as untrusted: at most 32 MB, 3000 entries and 16 MB of theme files (tokens.css 4 MB, theme.json 64 KB, each logo 4 MB); an entry with.., an absolute path or a symbolic link, or a theme name that is not a single lowercase identifier (^[a-z][a-z0-9-]{0,63}$), refuses the whole file with a message that names the entry and says what to do. The themes are saved in this browser (IndexedDB, contents included, one record per theme name) and appear under My themes in the theme list. Loading a theme whose name is already there replaces it: “A theme named “” was already there; it was replaced with the one you just loaded.” My themes… on the first screen lists each saved theme by its display name with its own Delete, which asks first (“Delete “” from this browser. The ZIP file you loaded is kept.”). A lesson whose theme was deleted builds withdefault, and the notice says the theme is not among the themes saved in this browser and to load it again with Load theme…. The build reads saved themes at the same place as the theme folder, in the same order and under the same rules; when the theme folder and the saved themes have a theme with the same name, the one chosen or loaded last is used (choosing such a folder says “A theme named “” is also saved in this browser; the one in the folder is used.”). To make a theme ZIP file, compress the theme folder (on a Mac, Compress in the Finder's context menu; on Windows, Send to → Compressed (zipped) folder), or use Code → Download ZIP on a GitHub repository of themes - Not read: names starting with
., and theout/,audio/,video/,bundle/andnode_modules/folders (up to 3000 files, 10 levels deep).audio/is read only to save an Egatari file. - Rebuilds are not compared yet. A save that rebuilds (after a slide edit) builds the whole file again with the bundled kit, so a file made by an older or newer build changes outside the edited parts, and fonts are embedded whole. The editor does not yet show which slides changed besides the edited ones.
- Font supplements err on the side of more. For a file whose subset fonts declare no
unicode-range(made on the command line by an earlier version), the editor knows only that the characters of the original script are covered, so it may add font files for characters the subset already had (the file grows a little; it never lacks a glyph). - Network in the lesson is blocked. External images and scripts (
https://…) do not load in the editor's preview; the build warns about them anyway.
- One build for both.
kit/core/build-core.mjsis the build (annotation, includes, KaTeX, parts, script, emphasis, timing, themes, assets, fonts) with no Node dependency; it reads files only through an interface it is given.kit/build.mjspasses Node's file system (plus the font subsetter and ffmpeg); the editor passes the chosen folder and the bundled kit, parts, themes, KaTeX, fonts and the same font subsetter. With font subsetting off, both produce byte-identical HTML, and the subset fonts of an export are byte-identical too (tools/test-app.mjschecks this forexamples/minimalandexamples/tmlr2026-calibration). The script parser and the script rewriting (kit/core/narration-core.mjs) and the New… template (kit/templates/new-deck.mjs) are shared the same way. - One editing UI for both, outside the lesson.
server/editor-ui.js(the source card, the script drawer, the comment box and list) runs in the editor page here and in the dev server's frame page. It sends the same requests (/__dev/source,/__dev/edit,/__dev/narration,/__dev/comment, …): the dev server answers them over HTTP, the editor answers them in the page against the folder (app/devapi.js). Inside the lesson there is onlyserver/hints.js, which sends hints (see below). - The lesson runs in a sandbox and cannot write. The preview is an
<iframe sandbox="allow-scripts allow-modals">withoutallow-same-origin, so the lesson has an opaque origin: it cannot reach the editor page, its storage or the folder handles.allow-modalsis there for printing. On top of the page's Content-Security-Policy (which allows loading only the editor itself and no frame navigation,frame-src 'none'), the preview document gets its own policy in force before any byte of the lesson is parsed: the editor builds the document as a trusted prefix (doctype,<html><head>,<meta charset>, the policy<meta>and a small trusted shim) followed by the built lesson unchanged, and never searches the lesson's markup for a place to put the policy. The policy isconnect-src 'none', images, fonts, styles and media only fromdata:/blob:(the build embeds them), no workers, frames, forms or<base>(the preview policy keeps'unsafe-eval'only for yanked part versions, below). The presenter view is a window the editor opens, with the lesson again in a sandboxed iframe under the same policy; the editor relays its messages. - The editor page forbids eval; yanked part versions are the only reason a policy ever allowed it. The page's own Content-Security-Policy has no
'unsafe-eval'(only'wasm-unsafe-eval', which the MP3 encoder's WebAssembly needs and which allows noevalornew Function; the hosted editor and the single-fileeditor.html; a test listens for CSP violations on the page). Components v1.0.0 and v1.0.1 letm-sweepandm-curvesrun formulas withnew Function; the registry has withdrawn (yanked) those versions with v1.0.2 as the replacement (SECURITY.md). When a lesson records a yanked version, the editor shows a notice and a button Rebuild with 1.0.2 and never replaces the version by itself (spec §6; rebuilding can change the parts' identity hashes and what is drawn: the note after the rebuild lists which hashes changed). Until then the lesson still plays, only inside the preview frame, which is a sandbox withoutallow-same-origin(an opaque origin that cannot reach the editor page, its storage or the folder handles). The frame inherits the editor page's policy, so inside the editor a yankedm-sweeporm-curvescannot evaluate its formula and shows its inline placeholder (DECK.componentErrors) while the rest of the lesson plays; the preview policy (PREVIEW_CSP) and the dev server's lesson document (a top-level sandboxed document, no editor page around it) keep'unsafe-eval', so yanked versions do run there. - Hints, not requests. The preview has no way to ask for a write. It sends only hints: “edit the element with this
data-srcrange and hash” (double-click in edit mode), “a comment at this element” (Alt+click; the slide id and step, a short quote of the element, the text near the point and the point in slide coordinates), the keyse/n/c/r/ Esc (ronly opens the recording screen), and its position (deck-pos). A hint carries no text to write. The editor checks the source window and the shape of each hint, ignores an edit hint outside edit mode, reads the current source itself, checks that the range is an editable element of the current build and that the hash matches, and shows its own card; it writes only when you press Save or ⌘/Ctrl+Enter in that card or drawer. The editing prompt and read-only mode apply to these writes as before. The editor sends the preview only what it needs to show: edit mode on/off, the element to highlight, “go to this step”, print and quality check. The point of a comment is checked as in the dev server (numbers only, clamped to the 1280×720 slide, other fields dropped;app/devapi.jschecks it again withkit/core/comment-core.mjs); a forged point only moves the marker, and the markers over the preview are drawn by the editor page from the preview's own size. - The editor touches only the folder or file you chose, and only after the browser's permission prompt.
- A save stays with the lesson it started on. After every wait (rebuild, a question, the permission prompt, the save picker, creating and writing the file) the editor checks that the same lesson is still open, and carries that check into the write itself: if another lesson was opened meanwhile, the file is not replaced (the write is aborted) and nothing else is written. A new lesson id is written to the lesson's settings, and the lesson marked saved, only after the file has been written. An edit made while a save is in progress keeps the unsaved mark, because the saved file does not contain it.
- While you edit in a trusted field (a source or comment box is open, or you are typing in the script drawer or it has unsaved changes), the preview is
inert; hints from the lesson then neither open, close nor switch panels, and the script step you are editing does not change. - Folders and links. In-memory and OPFS folders cannot contain symbolic links. In a folder you pick, Chromium does not show links to the editor at all (they are not listed, cannot be opened by name and cannot be written;
tools/test-security.mjschecks this), so the build cannot read a file outside the folder through one. The File System Access API gives the editor no way to tell links apart itself, so this relies on the browser. - Microphone and TTS only in the editor page. Recording (
getUserMedia,MediaRecorder), importing, post-processing and MP3 encoding run in the editor page; the preview cannot start them, and no audio is taken from the preview. Recording happens only in recording mode; the preview isinertwhile it is open, and while the panel makes or writes audio hints from the lesson do not switch its step. The page's policy still allows no network: AI voice on this computer is made in a separate window of the same site (lab/tts-helper.html, which, like the lab page, loads pinned libraries from jsDelivr and the model from Hugging Face) that is sent only the text to read and the voice name; the editor accepts audio only from that window, checks its sample rate and length, and processes it like a recording. Audio read from the folder is size-limited (manifest 16 MiB, each audio file 64 MiB). - Opening an Egatari file runs nothing from it. The file is read as bytes and parsed in a Web Worker (bundled into the editor; the page's policy allows workers only from
blob:) with the same parsing rules as a browser with scripting enabled (nothing is executed); the editor never navigates to it or inserts it into a page. Only the lesson rebuilt from its source runs, in the same sandboxed preview as a folder.
.github/workflows/pages.yml builds dist/ with node tools/build-app.mjs --out _site --parts and deploys it to GitHub Pages: the editor at the root (/), the Offline editor at /editor.html, the lab page at /lab/irodori.html, and at /editor/ a page that redirects to / keeping the query and # (the editor used to live there). All paths are relative, so the same files work at the custom domain https://studio.egatari.org/ (set in Settings → Pages → Custom domain, covering the whole site) and at https://egatari.github.io/egatari-studio/ (which redirects to the custom domain). Only release tags deploy: pushing a tag vX.Y.Z (three numbers; v0.5.0-rc.1 and other rc tags do not), or running the workflow on such a tag to deploy that release again. Pushes to main and pull requests only build (so a broken build shows up before a release), and running the workflow on a branch is a dry build. The release steps are in RELEASING.md. It deploys nothing until Pages is set up:
- Settings → Pages → Build and deployment → Source: GitHub Actions.
- Settings → Secrets and variables → Actions → Variables: add
EDITOR_PAGES=true(without it, builds still run but nothing deploys). - Settings → Environments →
github-pages→ Deployment branches and tags: add the tag rulev*.*.*(the environment allows onlymainby default, and a deploy from a tag is refused without it). - Push a release tag, or run the workflow on one (Actions → pages → Run workflow, choose the tag).
The build also writes the samples as Egatari files to examples/<name>.ega.html (tools/examples-portable.mjs, about 37 MB together; --no-examples leaves them out), each with its thumbnail (<name>.cover.webp), and the editor's first screen shows them. The samples with a path in examples/index.json (heikin-chuouchi and rashomon, which have a book, and the 17 of egatari.org's gallery) are unmodified copies from egatari/examples: node tools/examples-sync.mjs <clone> --commit <sha> <name>… copies them and writes SOURCE.json with the commit and each file's sha256, and --check (in npm run gen:check) refuses an edited copy, so change them there and copy them again. Thumbnails are in examples/_covers/: the gallery's own pictures for samples on egatari.org (node tools/example-covers.mjs --site <site>/src/gallery), and for Studio-only samples a picked picture (--picked <folder>) or one made from the first slide (npm run covers). They are built from a temporary copy of each sample folder (nothing is written into examples/); a sample without egatari.json (such as minimal, which may be copied as a template) gets a docId derived from its name, so every build is the same document.
The build writes egatari-make-<version>-ja.zip and egatari-make-<version>-en.zip to dist/ from the make generator. Downloads use these site files; the single file editor.html embeds both ZIPs for offline use. Frozen /vX.Y.Z/ copies and the editor site archive carry the ZIPs with the rest of dist/.
With --parts the build also makes the page of samples of all parts (tools/parts-gallery.mjs, about 40 seconds and 10 MB): every part of the parts library version in components2.lock.json (default), each with an introduction and its working sample slides, grouped by what to convey as in step 4 of “New…”, and marked new, changed or sample changed against the previous version. It goes to /parts/v<parts library version>/ (for example https://studio.egatari.org/parts/v2.1.1/), with /parts/index.html listing the versions (the current one first) and /parts/versions.json. The editor's help and step 4 of “New…” link to it with an absolute URL, so the link works from the Offline editor, file:// and Colab too. The page is built from the vendored release alone, with no git and no network: the files of that version from vendor/components/sha256/ (the copies its own release shipped, recorded per part in components2.lock.json catalogues.<version>.content) laid out as its tarball (checked file by file against vendor/components-2/manifest-<version>.json), assembled by the parts library's own gallery.mjs --pack and built with kit/build.mjs. That tool is vendored from a pinned commit of egatari/components in vendor/components-gallery/ (SOURCE.json records the repository, the commit and each file's sha256; node tools/parts-gallery.mjs --verify checks it, --sync <checkout> refreshes it), so the release asset built from a tag makes the same page. Like the editor, a published version is frozen: the assembly moves the parts/ of each earlier release's asset to /parts/ (not under /vX.Y.Z/), and a parts library version keeps the page of the first release that published it; the new build fills only versions not published before.
npm run release:assets (tools/release-assets.mjs) builds the same sample files into out/release/ with an examples.json (name, title, language, slides, size, sha256, and book: whether it has a book). It also writes egatari-make-<version>-ja.zip and egatari-make-<version>-en.zip from the make generator. It uploads nothing; attach the files to the GitHub release by hand, for example gh release create v<version> out/release/*.
The site keeps every release from the hosting floor on (v0.6.0; see Hosting floor below): the root serves the latest release, and /vX.Y.Z/ (for example https://studio.egatari.org/v0.4.2/) a frozen copy of that release with the same layout (/vX.Y.Z/editor.html, /vX.Y.Z/lab/…, /vX.Y.Z/examples/…). Since Pages deploys one artifact, the release deploy assembles it (node tools/site-versions.mjs assemble): the new build at the root and at /v<its version>/, and for each earlier published release at or above the floor the asset editor-site-vX.Y.Z.tar.gz, downloaded, checked against the line editor-site-vX.Y.Z.tar.gz sha256: <hex> in the release notes (and GitHub's own digest of the asset) and unpacked under /vX.Y.Z/. A release without the asset (v0.4.0 and earlier) is skipped; a missing or wrong sha256 fails the build. The asset is the built dist/ packed reproducibly (node tools/site-versions.mjs asset vX.Y.Z builds it from a git worktree at the tag, after npm ci); it is attached at release time (RELEASING.md). versions.json at the root lists the versions; pushes to main and pull requests run the same assembly as a dry build. At about 50 MB a version, the 1 GB limit of GitHub Pages holds about 18 versions; the assembly warns above 800 MB and fails above 1 GB, and both messages name the oldest hosted versions as candidates for the next floor raise.
-
One previous release. The site hosts the current version (at the root and at
/vX.Y.Z/) and only the one release before it (KEEP_PREVIOUS = 1intools/site-versions.mjs; owner decision of 2026-11-11). Each new release moves the window: when v0.6.9 is deployed, v0.6.8 is hosted and v0.6.7 and older get the “no longer hosted” page described below. A release candidate keeps the latest release before it. Sample pages of parts library versions that only older releases published go with them. Every release's whole site stays downloadable from its GitHub release. -
Hosting floor.
HOSTED_FROMintools/site-versions.mjs(now0.6.0; owner decision of 2026-11-06, after v0.6.5's site reached 1030 MB and GitHub Pages refused it) is the oldest version the site hosts. For an earlier release that has the asset under either name,editor-site-vX.Y.Z.tar.gzoreditor-site-vX.Y.Z.archived.tar.gz, the assembly does not download or unpack it; it writes only/vX.Y.Z/index.html, a short page in Japanese and English that says the version is no longer hosted, links to its GitHub release (where the whole site can still be downloaded) and to the latest version, and logs one line per version. These versions are not inversions.json, so the version lists do not show them; other paths under them (/vX.Y.Z/editor.html) return 404. To raise the floor, setHOSTED_FROMto the oldest version to keep (the warning suggests one) and deploy again; the assets on the releases stay as they are. Renaming an asset back toeditor-site-vX.Y.Z.tar.gzdoes not host a version below the floor again; only loweringHOSTED_FROMdoes (and the site must still fit in 1 GB). The assets of v0.4.1–v0.4.8 were renamed toeditor-site-vX.Y.Z.archived.tar.gzbefore the floor existed; they are kept, andnode tools/site-versions.mjs verifychecks only assets namededitor-site-vX.Y.Z.tar.gz. -
Notice and list. A copy shows a line at the top: “You are using Egatari Studio v0.4.2, a frozen copy of that release”, a link to the latest version (the root, keeping
?lang), and a list of the other versions; ✕ hides it. The root lists the versions below its first screen. Copies built before this notice existed (v0.4.1, v0.4.2) get it from the root'sversion-notice.js, which the assembly adds to theirindex.htmlandeditor.html(one<script>after the policy<meta>; nothing else in a copy is changed). -
Shared browser data. All versions are one site (one origin), so they share the browser's data: the recent files list (IndexedDB
motion-slides-editor; it holds only file handles and names, and every version reads it the same way, so a file opened in one version is listed in all), the UI language (localStorage), the user theme folder, and the models for AI voice on this computer (Cache Storage, named by model revision, so versions with the same model share the 1.26 GB download). Only the root removes caches of other model revisions; a copy never does (inFrozenCopyinlab/model-cache.mjs), but the copies v0.4.1 and v0.4.2 predate this rule and would remove a newer model if a later release changes it. A future change to the recent-files store must use a new database name, since older copies keep opening it. There is no service worker; the web app manifest's scope is each copy's own folder.
node tools/test-app.mjs [work folder] [--shots folder] (part of npm test) builds the editor into the work folder, serves it on 127.0.0.1 (and checks the site layout at the root and under a subpath: the editor loads without console errors, /editor/?lang=en#x redirects to /?lang=en#x, /editor.html and /lab/irodori.html load), creates working folders in the browser's origin-private file system (OPFS) and opens them through window.__APP_TEST_OPEN(handle). It checks the byte-identical builds, editing (including a 409 when the file changed meanwhile), the script drawer (adding a turn, CRLF kept, 409), comments, outside changes, build errors, quality check, export, presenter view, the English UI and the Offline editor from file://, and that there are no console errors. A folder with a lesson that tries to write (tools/hostile-deck.mjs: a part and an inline script that send the old write messages and hints with forged text, look for a token, reach for the editor page and fetch / XHR / beacon the write endpoints) must ask before it runs, change nothing in the folder, and be written only when Save is pressed in the editor. For Egatari files it builds one from examples/minimal (with dry-run audio) and the document fixtures of tools/portable-fixtures.mjs, and checks opening, the editing prompt (and the confirmation for a no-derivatives license), editing text and script, saving to a file handle in OPFS, reopening (same docId, unchanged audio hashes, the unpacked folder equals the in-memory one), Export folder and Export Egatari file (byte-identical), the notice for files with their own code, that the canary script never runs in the editor page or the reader worker, the refusals (path traversal, future major version, duplicate member, unknown required feature), read-only files, the download fallback, saving without rebuilding (only the format's regions change, checked by a region-aware diff; a font supplement for a new kanji), the rebuild warning and Cancel, Save as a new file (docId, derivedFrom), unknown index members kept by both kinds of save, a file whose registry theme differs (script only), extraction of a file with an unknown required feature, the empty preview behind the code question, and the toolbar in one row at 1280 and 1440 px in both languages. tools/test-app-slides.mjs (run by tools/test-app.mjs) tests the slide list in a working folder and an Egatari file: the listbox and its keys, adding from each template (a new well-formed unique id, a script section, the quality check), duplicating (script copied, audio not), deleting with its dialog and Undo (slide, script, audio records and files restored byte for byte), reordering by Alt+↓, the buttons and dragging (ids, script order and audio keys), and that the folder reopens and the saved Egatari file (tier B) reads back and reopens cleanly; node tools/test-slide-ops.mjs (in npm test) tests the text operations of kit/core/slide-ops.mjs. node tools/test-pronounce.mjs tests Pronunciation…: the word rules and whole-script changes (kit/core/readings.mjs, narration-core.mjs), and in the editor (Japanese and English UI, stand-in device and AI voices) a word selected and given a reading is {x|y} in the source and ruby in the field, the caret, undo, redo, copy and Backspace, refused selections, the word at the caret, Apply to all (other steps on save, with a backup), the dictionary, both Listen buttons, and, with a localStorage that throws, the disabled dictionary box with its line while the pronunciation is still applied and saved. node tools/test-audio.mjs [work folder] [--shots folder] (in npm test) tests the Audio panel with Chromium's fake microphone (a generated voice-like signal as the capture file) and a stand-in TTS engine (no model is downloaded): recording with the level meter and the inert preview, the takes list, Use this take (MP3, manifest record, backup), the stale badge and warning after a script edit, the quality check, regeneration (reading dictionary applied, cue times, recipe), an Egatari file saved by tier A with only that step's audio changed (region-aware diff), the TTS window protocol with lab/tts-helper.html?stub=1, that without WebGPU only the cloud and recording are offered, and the new panel (the status line and its one action, the voice-changed count, the order of the answers of B, the answer it opens on with no audio and for the newest audio of each kind (recording, Colab, this computer), a stubbed device check (low memory, phone or tablet: “this computer” shown but disabled, with the reason, and the cloud instead), that nothing is written to egatari.json, the buttons of C, the two-speaker voice labels, and that the removed items are gone); node tools/test-colab-app.mjs walks steps 1–3 (the number badges and headings, the privacy question once, the a–d instructions and timing under step 2, the ✓ marks after the export and after the import, the ZIP file through a plain file input with no folder picker; COLAB_SHOTS=<folder> saves screenshots of the panel at 1280 and 390 px wide). node tools/test-audio-core.mjs (in npm test) compares the post-processing with tts/core.py (with uv and ffmpeg), checks the MP3 encoding and its decoded length, the reading dictionary against tts/yomi.py, the records and staleness, and a tier-A save with replaced audio. node tools/test-tier-a.mjs (also in npm test) tests saving without rebuilding on its own, with examples/minimal and examples/tmlr2026-calibration: the region-aware diff, playback in headless Chrome, unpack and pack again, a file from another producer, unknown members, replaced audio, a new lesson and the refusals. node tools/test-workspace.mjs tests the in-memory folder and the rights rules without a browser. tools/test-app.mjs also opens a lesson source deck.html (the question; Cancel; Open the folder…; this file only: an in-memory lesson with only deck.html that passes the quality check and saves as an Egatari file after Save asks where; examples/tmlr2026-calibration/deck.html alone with its missing files as warnings), refuses a built out/deck.html, a play-only file and other HTML each with its own message, opens every sample from the first screen (the code question only for the one with its own code), and checks that dist/examples/ is served at the root and under the subpath and that the Offline editor does not list the samples. It also assembles a versioned site (the build at the root and at /v<version>/, and a stand-in old copy at /v0.0.1/ without its own notice) and checks the version list at the root, choosing an older version, the frozen notice and its link to the latest in both languages, that /v<version>/ loads its own app.js and opens a sample, and that autoplay started in the presenter view plays the preview's audio unmuted while the presenter view plays nothing. node tools/test-site-versions.mjs (in npm test) checks the reproducible packing, the refusal of unsafe archive entries, the sha256 checks and the assembly without a browser.
node tools/test-recording.mjs [work folder] [--shots folder] (part of npm test) checks recording mode with a fake microphone and the keyboard alone (Space / Enter / Backspace / C / arrows / Esc, the cues and their sync, retakes, the live-region announcements, the inert preview, the security of the hint path, and the Audio panel's ● Record (r): it opens at the panel's step, and after one take and Esc the panel shows it); node tools/test-audio.mjs checks the Audio panel and, through recording mode, the gain, the clipping warning, the 600 s limit (shortened in the test), unused takes and a refused microphone; node tools/test-rec-core.mjs checks how the C presses become cues.
The editor follows your browser language (Japanese or English). Switch with the English / 日本語 button at the top right, or open …/?lang=en or ?lang=ja. The choice is remembered; switching while a lesson is open reopens the same folder (an opened Egatari file is closed; unsaved changes are asked about first). The editing tools (edit mode, the source card, the script drawer, comments) follow the editor's language. (In the dev server they follow the lesson's language, <html lang>.)