footprint-storydeck 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +17 -7
- package/dist/deck/check.js +11 -1
- package/dist/deck/compile.js +2 -1
- package/dist/deck/init.js +1 -0
- package/dist/deck/parse.js +10 -2
- package/dist/deck/review.js +3 -2
- package/dist/narration.js +1 -1
- package/dist/pptx.js +4 -4
- package/dist/video.js +1 -1
- package/dist/voice.js +1 -1
- package/index.d.ts +1 -1
- package/narration.d.ts +2 -2
- package/package.json +2 -2
- package/plugin/skills/storydeck/SKILL.md +9 -9
- package/pptx.d.ts +2 -2
- package/video.d.ts +3 -3
- package/voice.d.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1 — footprint-narration is now footprint-story
|
|
4
|
+
|
|
5
|
+
- **footprint-narration is now footprint-story.** StoryDeck's shared narration code (shown-vs-spoken text, captions,
|
|
6
|
+
chapters, the voice cache) comes from `footprint-story` 0.3.0, the same package under its new name: what StoryDeck
|
|
7
|
+
and footprint-storyreel share, now with a `/motion` door too. Nothing a deck says, shows or voices changes. A project
|
|
8
|
+
that imports `footprint-narration` itself beside StoryDeck should move to `footprint-story` (the two doors export
|
|
9
|
+
exactly what they did).
|
|
10
|
+
|
|
11
|
+
## 0.8.0 — a part can have its own look (data-theme)
|
|
12
|
+
|
|
13
|
+
- **A part can have its own look: `data-theme`.** `<section … data-theme="ledger">` in clicks.html puts
|
|
14
|
+
`data-theme="ledger"` on each slide of that part (plain or on a map), so a deck's theme.css can give one part its own
|
|
15
|
+
tokens and pieces — a talk in several parts, each dressed for its subject. A theme is one lower-case name (letters,
|
|
16
|
+
digits, dashes; 40 at most), refused otherwise with the part's name. A part without one is drawn byte for byte as
|
|
17
|
+
before.
|
|
18
|
+
The title and closing slides are parts, so they can have a look of their own. `check` warns (`theme-unstyled`) when a
|
|
19
|
+
part asks for a theme that theme.css never selects, and `review` ends a click's first line with its theme. The README
|
|
20
|
+
shows how the page around the slide follows the part on screen (`:root:has([data-deck-active][data-theme=…])`).
|
|
21
|
+
|
|
3
22
|
## 0.7.0 — a deck from a folder, an editing loop, co-editing and wire motion
|
|
4
23
|
|
|
5
24
|
- **A packet rides its wire's curve, and a wire shows itself in.** On a `path` click, a `pkt` packet now follows the
|
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ through it the same way.
|
|
|
240
240
|
A talk lives on after the room: someone opens the deck alone, or it becomes a video. The words were in the
|
|
241
241
|
speaker notes all along, so narration is data on each click, and the library turns it into speech, captions and
|
|
242
242
|
timing — the same words, three uses. The rules underneath — what a voice says for a number or a symbol, the clip
|
|
243
|
-
cache, the captions and YouTube's chapter rule — are [footprint-
|
|
243
|
+
cache, the captions and YouTube's chapter rule — are [footprint-story](https://github.com/footprintjs/footprint-story)'s,
|
|
244
244
|
shared with footprint-storyreel; StoryDeck adds what is the deck's own:
|
|
245
245
|
|
|
246
246
|
- **what the voice says** — `narrationText(notes)`: the notes as a voice reads them. An acronym stays as written
|
|
@@ -249,7 +249,7 @@ shared with footprint-storyreel; StoryDeck adds what is the deck's own:
|
|
|
249
249
|
another script its reading), numbers become words, a lone symbol that means something is said (`SAY`: & + = < > ×
|
|
250
250
|
% and Greek letters), any other word without a letter is a pause that keeps its sentence end (a forced aligner
|
|
251
251
|
needs letters in every word), and a line after `[draft]` (a bridge you wrote for the deck) is said, and shown apart;
|
|
252
|
-
- **a clip per click, cached by what it says** — `footprint-
|
|
252
|
+
- **a clip per click, cached by what it says** — `footprint-story/voice` (Node). A clip's key is a hash of the
|
|
253
253
|
voice settings and the exact words, so editing one note re-voices that click and nothing else, and an interrupted
|
|
254
254
|
run keeps every batch it finished. The voice is a port (`{ name, synthesize(scenes, { work }) }`); `chatterboxKit`
|
|
255
255
|
is one adapter — a local voice kit (Chatterbox, words aligned with MMS_FA; nothing is uploaded). `footprint-storydeck/voice`
|
|
@@ -259,7 +259,7 @@ shared with footprint-storyreel; StoryDeck adds what is the deck's own:
|
|
|
259
259
|
the browser's where a click has none — captions lighting sentence by sentence (styles: `footprint-storydeck/narration.css`).
|
|
260
260
|
|
|
261
261
|
```js
|
|
262
|
-
import { voiceClips, chatterboxKit } from 'footprint-
|
|
262
|
+
import { voiceClips, chatterboxKit } from 'footprint-story/voice';
|
|
263
263
|
import { narrationText } from 'footprint-storydeck/narration';
|
|
264
264
|
import { embedClips } from 'footprint-storydeck/voice';
|
|
265
265
|
import { notesData, notesRuntime, listenRuntime } from 'footprint-storydeck/narration-html';
|
|
@@ -290,7 +290,7 @@ would drop one entrance frame in six at 30 fps, and a test with the real ffmpeg
|
|
|
290
290
|
```js
|
|
291
291
|
import { chromium } from 'playwright-core';
|
|
292
292
|
import { renderVideo, deckStageDriver } from 'footprint-storydeck/video';
|
|
293
|
-
import { clipFor } from 'footprint-
|
|
293
|
+
import { clipFor } from 'footprint-story/voice';
|
|
294
294
|
import { narrationText, writtenText } from 'footprint-storydeck/narration';
|
|
295
295
|
|
|
296
296
|
const steps = clicks.map(c => {
|
|
@@ -399,10 +399,20 @@ npx footprint-storydeck build my-talk # my-talk/index.html — one page, eve
|
|
|
399
399
|
ticks the line (`- [x]`). Pointing costs the person a second and the agent no screenshot. Each note is signed with a
|
|
400
400
|
key kept with you (`~/.storydeck/key`) for that deck's folder, so a feedback.md that came with someone else's deck is
|
|
401
401
|
never read as your requests: its lines are counted, never listed.
|
|
402
|
+
- **A part can have its own look.** `<section data-label="3 · Storage" … data-theme="ledger">` puts
|
|
403
|
+
`data-theme="ledger"` on each of that part's slides, so `theme.css` can dress it: redefine the tokens for it
|
|
404
|
+
(`[data-theme="ledger"] { --bg: …; --text: …; }`, and `:root.dark [data-theme="ledger"]` for dark) and give its pieces
|
|
405
|
+
their own style. Tokens inherit, so the chrome, the focus and every piece of that part follow. A theme is one
|
|
406
|
+
lower-case name (letters, digits and dashes); a part without one is drawn as before, byte for byte. The title and the
|
|
407
|
+
closing slide are parts too, so they can have their own look. To have the page around the slide (the letterbox)
|
|
408
|
+
follow the part on screen as well, select the active slide in the same rule:
|
|
409
|
+
`[data-theme="ledger"], :root:has([data-deck-active][data-theme="ledger"]) { … }`. `check` warns about a theme
|
|
410
|
+
that theme.css never selects (a misspelt name changes nothing), and `review` names each click's theme.
|
|
402
411
|
- **Read first, look last.** A reviewer — a person, or a model that pays for every screenshot — reads the deck as text:
|
|
403
412
|
`outline` (a line per click), `review --click 14` (its headline and words, what it lights, greys, brings in and moves,
|
|
404
413
|
its route, its notes), `check` (what to fix, each with its click and its fix: a headline over 12 words, pieces that
|
|
405
|
-
light just by coming in, a click that changes nothing, a piece never shown, clicks without notes
|
|
414
|
+
light just by coming in, a click that changes nothing, a piece never shown, clicks without notes, a theme theme.css
|
|
415
|
+
never styles). `check --layout`
|
|
406
416
|
opens the page in a browser, offline, and finds text that spills out of its box (it needs Playwright installed beside
|
|
407
417
|
StoryDeck: `npm i -D footprint-storydeck playwright-core`; without it, it says so and the other checks run). `mcp` serves the same tools to an AI assistant:
|
|
408
418
|
`claude mcp add storydeck -- npx footprint-storydeck mcp my-talk`.
|
|
@@ -537,9 +547,9 @@ const { theme, toggle, setTheme } = useTheme(); // flips an html class + persi
|
|
|
537
547
|
| `planFocus` · `focusStep` · `readFocus` · `FOCUS` | focus: per click, what it is about and what happens to the rest (also `footprint-storydeck/focus`) |
|
|
538
548
|
| `readPieces` · `readStep` · `drawFocus` · `wrapStage` · `slideClass` · `focusOverlay` · `focusRuntime` | focus, written into slide HTML (also `footprint-storydeck/focus-html`; styles in `footprint-storydeck/focus.css`) |
|
|
539
549
|
| `idleTour` · `tourPlan` · `tourText` | tour: a deck (or a figure) that shows its point while nobody is using it, and its plan in data and in one line (also `footprint-storydeck/tour`) |
|
|
540
|
-
| `narrationText` · `writtenText` | narration: what a click says (also `footprint-storydeck/narration`); the rules beneath, sentences, captions and chapters are `footprint-
|
|
550
|
+
| `narrationText` · `writtenText` | narration: what a click says (also `footprint-storydeck/narration`); the rules beneath, sentences, captions and chapters are `footprint-story`'s |
|
|
541
551
|
| `notesData` · `notesRuntime` · `listenRuntime` | narration in the page: notes, presenter window, listen mode (also `footprint-storydeck/narration-html`; styles in `footprint-storydeck/narration.css`) |
|
|
542
|
-
| `embedClips` | `footprint-storydeck/voice` (Node only): a deck's clips inside one page; the clip cache, the voice port and its local-kit adapter are `footprint-
|
|
552
|
+
| `embedClips` | `footprint-storydeck/voice` (Node only): a deck's clips inside one page; the clip cache, the voice port and its local-kit adapter are `footprint-story/voice`'s |
|
|
543
553
|
| `renderVideo` · `planVideo` · `deckStageDriver` · `ffmpegMajor` | `footprint-storydeck/video` (Node only, ffmpeg 5.0+): the deck as a narrated video, with captions and chapters |
|
|
544
554
|
| `exportPptx` · `pptxParts` · `zip` · `unzip` · `checkPptx` | `footprint-storydeck/pptx` (Node only): the deck as a PowerPoint file — a slide per click, its picture, notes and clip — through a footprintjs flowchart that records why |
|
|
545
555
|
| `loadDeck` · `buildDeck` · `serveDeck` · `outline` · `reviewClick` · `checkDeck` · `layoutCheck` · `initDeck` · `serveMcp` · `runCli` | `footprint-storydeck/deck` (Node only): a deck folder made into one page, served for editing, read back as text — the `storydeck` command (also `npx footprint-storydeck`); its page's base styles are `footprint-storydeck/deck.css` |
|
package/dist/deck/check.js
CHANGED
|
@@ -43,6 +43,16 @@ function missingNotes(compiled) {
|
|
|
43
43
|
if (!without.length || without.length * 2 > compiled.clicks.length) return [];
|
|
44
44
|
return [problem("info", "no-notes", null, `click${without.length === 1 ? "" : "s"} ${without.map((c) => c.n).join(", ")} ha${without.length === 1 ? "s" : "ve"} no notes \u2014 the presenter view shows nothing there and listen mode skips ${without.length === 1 ? "it" : "them"}`)];
|
|
45
45
|
}
|
|
46
|
+
function unstyledThemes(compiled) {
|
|
47
|
+
const out = [], seen = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const c of compiled.clicks) {
|
|
49
|
+
if (!c.theme || seen.has(c.theme)) continue;
|
|
50
|
+
seen.add(c.theme);
|
|
51
|
+
const selects = new RegExp(`\\[\\s*data-theme\\s*[~|^$*]?=\\s*(["']?)${c.theme}\\1\\s*(?:[is]\\s*)?\\]`).test(compiled.themeCss ?? "");
|
|
52
|
+
if (!selects) out.push(problem("warn", "theme-unstyled", c, `part "${c.part}" asks for data-theme="${c.theme}", which theme.css never selects \u2014 fix the name, or give it its tokens there ([data-theme="${c.theme}"] { --bg: \u2026; })`));
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
46
56
|
function checkDeck(compiled) {
|
|
47
57
|
const out = [];
|
|
48
58
|
compiled.clicks.forEach((c, i) => {
|
|
@@ -50,7 +60,7 @@ function checkDeck(compiled) {
|
|
|
50
60
|
if (c.kind === "map") out.push(...litOnEntry(c, compiled.maps[c.map].pieces));
|
|
51
61
|
out.push(...noChange(c, compiled.clicks[i - 1]));
|
|
52
62
|
});
|
|
53
|
-
out.push(...neverShown(compiled), ...missingNotes(compiled));
|
|
63
|
+
out.push(...neverShown(compiled), ...missingNotes(compiled), ...unstyledThemes(compiled));
|
|
54
64
|
return out;
|
|
55
65
|
}
|
|
56
66
|
const failure = (error) => [problem("error", "compile", null, error.message)];
|
package/dist/deck/compile.js
CHANGED
|
@@ -73,7 +73,7 @@ function planClicks(parts, maps, { area, canvas, icons }) {
|
|
|
73
73
|
const head = (kicker, headline) => `<div class="head" data-chrome><div class="kicker">${esc(kicker)}</div>${headline ? `<h2>${headline}</h2>` : ""}</div>`;
|
|
74
74
|
const foot = (footer, n, total) => `<div class="foot" data-chrome><span>${esc(footer)}</span><span class="count">${n} / ${total}</span></div>`;
|
|
75
75
|
function drawClick(c, i, total, { footer, area, canvas }) {
|
|
76
|
-
const open = `<section data-label="${esc(c.label)}"`;
|
|
76
|
+
const open = `<section data-label="${esc(c.label)}"${c.part.theme ? ` data-theme="${esc(c.part.theme)}"` : ""}`;
|
|
77
77
|
if (c.kind === "plain") return `${open} class="slide plain">${head(c.part.kicker, "")}${c.inner}${foot(footer, i + 1, total)}</section>`;
|
|
78
78
|
return `${open} class="slide on-map ${slideClass(c.look)}">${head(c.part.kicker, c.headline)}<div class="map map-${c.map.name}">${wrapStage(drawFocus(c.map.html, c.look), c.look)}</div>${focusOverlay(c.look, { area, canvas: { w: canvas[0], h: canvas[1] } })}${foot(footer, i + 1, total)}</section>`;
|
|
79
79
|
}
|
|
@@ -108,6 +108,7 @@ function compileDeck(source, { dev = false } = {}) {
|
|
|
108
108
|
label: c.label,
|
|
109
109
|
part: c.part.label,
|
|
110
110
|
kicker: c.part.kicker,
|
|
111
|
+
theme: c.part.theme ?? null,
|
|
111
112
|
kind: c.kind,
|
|
112
113
|
map: c.kind === "map" ? c.map.name : null,
|
|
113
114
|
headline: plainText(c.headline),
|
package/dist/deck/init.js
CHANGED
|
@@ -24,6 +24,7 @@ const MAP = `<!--
|
|
|
24
24
|
const CLICKS = `<!--
|
|
25
25
|
The deck, click by click.
|
|
26
26
|
<section data-label="\u2026" data-kicker="\u2026"> one part of the story; without data-map it is one plain slide
|
|
27
|
+
<section \u2026 data-theme="ledger"> optional: its slides carry data-theme, so theme.css can dress that part
|
|
27
28
|
<section \u2026 data-map="map"> a part told on map.html: every <step> inside is ONE click
|
|
28
29
|
<step data-in="\u2026" data-out="\u2026" data-on="\u2026" data-focus="\u2026">
|
|
29
30
|
data-in pieces that come in (they stay) data-out pieces that leave
|
package/dist/deck/parse.js
CHANGED
|
@@ -84,6 +84,13 @@ function takeNotes(html) {
|
|
|
84
84
|
}
|
|
85
85
|
const without = (html, ranges) => ranges.reduceRight((out, r) => out.slice(0, r.start) + out.slice(r.end), html);
|
|
86
86
|
const where = (label) => `"${label}"`;
|
|
87
|
+
const THEME = /^[a-z][a-z0-9-]{0,39}$/;
|
|
88
|
+
function themeOf(label, attrs) {
|
|
89
|
+
const theme = attrOf(attrs, "data-theme");
|
|
90
|
+
if (theme === null) return null;
|
|
91
|
+
if (!THEME.test(theme)) throw new Error(`${where(label)}: data-theme="${theme}" \u2014 a theme is one lower-case name (letters, digits and dashes, at most 40), such as "ledger"`);
|
|
92
|
+
return theme;
|
|
93
|
+
}
|
|
87
94
|
function readMapStep(label, k, attrs, inner) {
|
|
88
95
|
const { html, notes } = takeNotes(inner);
|
|
89
96
|
let heads;
|
|
@@ -113,10 +120,11 @@ function parseClicks(html) {
|
|
|
113
120
|
seen.add(label);
|
|
114
121
|
const kicker = attrOf(attrs, "data-kicker") ?? "";
|
|
115
122
|
const map = attrOf(attrs, "data-map");
|
|
123
|
+
const theme = themeOf(label, attrs);
|
|
116
124
|
if (map === null) {
|
|
117
125
|
if (/<step\b/i.test(body)) throw new Error(`${where(label)} has <step> clicks but no data-map \u2014 name the map they are told on (data-map="map")`);
|
|
118
126
|
const { html: inner, notes } = takeNotes(body);
|
|
119
|
-
parts.push({ label, kicker, map: null, body: inner, notes });
|
|
127
|
+
parts.push({ label, kicker, theme, map: null, body: inner, notes });
|
|
120
128
|
continue;
|
|
121
129
|
}
|
|
122
130
|
if (!map.trim()) throw new Error(`${where(label)}: data-map is empty \u2014 name a map (a file in the deck, e.g. data-map="map")`);
|
|
@@ -129,7 +137,7 @@ function parseClicks(html) {
|
|
|
129
137
|
if (!steps.length) throw new Error(`${where(label)} is told on map "${map}" but has no <step> \u2014 a map part is one click per <step>`);
|
|
130
138
|
const outside = plainText(takeNotes(without(body, steps)).html);
|
|
131
139
|
if (outside) throw new Error(`${where(label)}: "${outside.slice(0, 60)}" is outside every <step> \u2014 put a headline or notes inside the step they belong to`);
|
|
132
|
-
parts.push({ label, kicker, map: map.trim(), steps: steps.map(({ attrs: a, inner }, k) => readMapStep(label, k, a, inner)) });
|
|
140
|
+
parts.push({ label, kicker, theme, map: map.trim(), steps: steps.map(({ attrs: a, inner }, k) => readMapStep(label, k, a, inner)) });
|
|
133
141
|
}
|
|
134
142
|
if (!parts.length) throw new Error('the clicks file has no <section> \u2014 a deck is one <section data-label="\u2026"> per part');
|
|
135
143
|
return parts;
|
package/dist/deck/review.js
CHANGED
|
@@ -2,6 +2,7 @@ import { wordCount } from "./parse.js";
|
|
|
2
2
|
const nameOf = (pieces, i) => pieces[i].keys[0];
|
|
3
3
|
const unique = (names) => [...new Set(names)];
|
|
4
4
|
const list = (names, max = 14) => names.length ? `${names.slice(0, max).join(", ")}${names.length > max ? `, \u2026 (${names.length - max} more)` : ""}` : "\u2014";
|
|
5
|
+
const themed = (c) => c.theme ? ` \xB7 theme ${c.theme}` : "";
|
|
5
6
|
const quote = (s, max = 160) => s.length > max ? `"${s.slice(0, max - 1)}\u2026"` : `"${s}"`;
|
|
6
7
|
function pieceGroups(click, pieces) {
|
|
7
8
|
const by = (pred) => unique(click.look.pieces.map((p, i) => pred(p) ? nameOf(pieces, i) : null).filter(Boolean));
|
|
@@ -56,10 +57,10 @@ function reviewClick(compiled, which, { maxWords = compiled.config?.checks?.maxW
|
|
|
56
57
|
const lines = [];
|
|
57
58
|
const words = (n) => `${n} word${n === 1 ? "" : "s"}`;
|
|
58
59
|
if (c.kind === "plain") {
|
|
59
|
-
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (plain slide)`);
|
|
60
|
+
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (plain slide)${themed(c)}`);
|
|
60
61
|
lines.push(`text (${words(wordCount(c.text))}): ${quote(c.text, 300)}`);
|
|
61
62
|
} else {
|
|
62
|
-
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (map "${c.map}", step ${c.stepIndex + 1} of ${c.steps})`);
|
|
63
|
+
lines.push(`click ${c.n} of ${compiled.clicks.length} \xB7 "${c.label}" (map "${c.map}", step ${c.stepIndex + 1} of ${c.steps})${themed(c)}`);
|
|
63
64
|
const hw = wordCount(c.headline);
|
|
64
65
|
lines.push(`headline: ${c.headline ? `${quote(c.headline)} (${words(hw)}${hw > maxWords ? `, over ${maxWords}` : ""})` : "\u2014"}`);
|
|
65
66
|
const pieces = compiled.maps[c.map].pieces;
|
package/dist/narration.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spokenMap, autoRules } from "footprint-
|
|
1
|
+
import { spokenMap, autoRules } from "footprint-story";
|
|
2
2
|
const AUTO = autoRules();
|
|
3
3
|
const writtenText = (notes) => (Array.isArray(notes) ? notes : [notes ?? ""]).join(" ").replace(/\[draft\]\s*/g, "").replace(/\s+/g, " ").trim();
|
|
4
4
|
function narrationText(notes, { spell, say } = {}) {
|
package/dist/pptx.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { clipFor } from "footprint-
|
|
5
|
+
import { clipFor } from "footprint-story/voice";
|
|
6
6
|
import { narrationText, writtenText } from "./narration.js";
|
|
7
7
|
import { planVideo } from "./video.js";
|
|
8
8
|
import { pptxParts, pngSize, plainText, isSound, AUDIO, LANG_TAG, SOUND } from "./pptxParts.js";
|
|
@@ -49,13 +49,13 @@ function headOf(file) {
|
|
|
49
49
|
}
|
|
50
50
|
function soundProblem(clip, ext) {
|
|
51
51
|
const { size, head } = headOf(clip.audio);
|
|
52
|
-
if (!size) return `its clip ${clip.audio} is empty (0 bytes) \u2014 voice it again (footprint-
|
|
52
|
+
if (!size) return `its clip ${clip.audio} is empty (0 bytes) \u2014 voice it again (footprint-story/voice \xB7 voiceClips)`;
|
|
53
53
|
return isSound(head, ext) ? null : `its clip ${clip.audio} is not a .${ext} file (no ${SOUND[ext].sign}) \u2014 voice it again, or convert it with ffmpeg`;
|
|
54
54
|
}
|
|
55
55
|
function clipProblem(c, { voiced, maxClip }) {
|
|
56
56
|
const clip = c.clip, d = clip?.duration, ext = extOf(clip?.audio);
|
|
57
|
-
if (!clip) return voiced && c.spoken.trim() ? ["unvoiced", "it says words but has no clip \u2014 voice it (footprint-
|
|
58
|
-
if (!clip.audio || !existsSync(clip.audio)) return ["missing", `its clip ${clip.audio} is not there \u2014 voice it again (footprint-
|
|
57
|
+
if (!clip) return voiced && c.spoken.trim() ? ["unvoiced", "it says words but has no clip \u2014 voice it (footprint-story/voice \xB7 voiceClips), or give its step its clip"] : null;
|
|
58
|
+
if (!clip.audio || !existsSync(clip.audio)) return ["missing", `its clip ${clip.audio} is not there \u2014 voice it again (footprint-story/voice \xB7 voiceClips)`];
|
|
59
59
|
if (!AUDIO[ext]) return ["unplayable", `its clip is .${ext} \u2014 PowerPoint plays .wav, .m4a and .mp3 (convert it with ffmpeg)`];
|
|
60
60
|
const bytes = soundProblem(clip, ext);
|
|
61
61
|
if (bytes) return ["unplayable", bytes];
|
package/dist/video.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { captionCues, captionFile, readCaptions, youtubeChapters } from "footprint-
|
|
5
|
+
import { captionCues, captionFile, readCaptions, youtubeChapters } from "footprint-story";
|
|
6
6
|
const NUM = (n) => String(n).padStart(3, "0");
|
|
7
7
|
const entry = (file) => `file '${path.resolve(file).replace(/'/g, "'\\''")}'`;
|
|
8
8
|
const failure = (done) => done?.error ? done.error.message : done?.signal ? `signal ${done.signal}` : done?.status !== 0 ? `exit ${done?.status}` : null;
|
package/dist/voice.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { clipFor } from "footprint-
|
|
4
|
+
import { clipFor } from "footprint-story/voice";
|
|
5
5
|
const failure = (done) => done?.error ? done.error.message : done?.signal ? `signal ${done.signal}` : done?.status !== 0 ? `exit ${done?.status}` : null;
|
|
6
6
|
function embedClips(texts, { cache, profile, run = spawnSync }) {
|
|
7
7
|
cache = path.resolve(cache);
|
package/index.d.ts
CHANGED
|
@@ -192,7 +192,7 @@ export * from './focusHtml';
|
|
|
192
192
|
// Tour — a deck or a figure that shows its point by itself while nobody is using it (tour.d.ts).
|
|
193
193
|
export * from './tour';
|
|
194
194
|
// Narration — what a deck says on each click (narration.d.ts), in the page: notes, presenter, listen mode (narrationHtml.d.ts).
|
|
195
|
-
// The rules a voice reads by, sentences, captions and chapters are footprint-
|
|
195
|
+
// The rules a voice reads by, sentences, captions and chapters are footprint-story's (its own types).
|
|
196
196
|
// footprint-storydeck/voice, /video and /pptx (Node-only) carry their own types.
|
|
197
197
|
export * from './narration';
|
|
198
198
|
export * from './narrationHtml';
|
package/narration.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* storydeck · narration (narration.js) — what a deck says on each click. The rules a voice reads by, the sentences,
|
|
3
|
-
* the captions and the chapters are footprint-
|
|
3
|
+
* the captions and the chapters are footprint-story's: import them from 'footprint-story'.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** The notes as written, for captions: [draft] marks dropped, white space collapsed. */
|
|
7
7
|
export function writtenText(notes: string | readonly string[] | null | undefined): string;
|
|
8
|
-
/** The notes as the voice should read them: footprint-
|
|
8
|
+
/** The notes as the voice should read them: footprint-story's automatic rules over the written text (`spell`, `say` replace its default lists). */
|
|
9
9
|
export function narrationText(notes: string | readonly string[] | null | undefined, options?: { spell?: Readonly<Record<string, string>>; say?: Readonly<Record<string, string>> }): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "footprint-storydeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "One source, many lenses — render structured content as a Read article, a Scroll (scrollytelling) story, or a Watch deck; and make a deck from a folder with a live dev loop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
}
|
|
119
119
|
},
|
|
120
120
|
"dependencies": {
|
|
121
|
-
"footprint-
|
|
121
|
+
"footprint-story": "^0.3.0",
|
|
122
122
|
"markdown-it": "^14.1.0"
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
@@ -8,9 +8,9 @@ description: Build, focus, narrate and film talks and posts with StoryDeck — o
|
|
|
8
8
|
A talk or a post is written once: **sections** of slide steps plus prose. StoryDeck shows it as an article
|
|
9
9
|
(Read), a scrollytelling story (Scroll) or a click-through deck (Watch), decides per click what the audience
|
|
10
10
|
looks at (focus), and turns the speaker notes into a voice, captions and a video (narration). Most of it is
|
|
11
|
-
plain data and string builders; only `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` and `footprint-
|
|
11
|
+
plain data and string builders; only `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` and `footprint-story/voice` touch the
|
|
12
12
|
file system. The narration rules underneath (what a voice says for a number, captions, chapters, the clip cache)
|
|
13
|
-
are footprint-
|
|
13
|
+
are footprint-story's, a package StoryDeck depends on and shares with footprint-storyreel.
|
|
14
14
|
|
|
15
15
|
## Pick the door
|
|
16
16
|
|
|
@@ -22,15 +22,15 @@ are footprint-narration's, a package StoryDeck depends on and shares with footpr
|
|
|
22
22
|
| that focus written into slide HTML | `footprint-storydeck/focus-html` + `footprint-storydeck/focus.css`, `focusRuntime` once in the page | build + page |
|
|
23
23
|
| a deck (or figure) that shows its point while nobody is using it (`idleTour`, its plan `tourPlan`, a notes line `tourText`) | `footprint-storydeck/tour`; declared as `<deck-stage data-tour="3 4 5 9:6s">` + `(${idleTour})()`, or `SlideDeck`'s `tour` / post.json's `tour` | build + page |
|
|
24
24
|
| what each click says (`narrationText`, `writtenText`: `[draft]`-aware) | `footprint-storydeck/narration` | anywhere |
|
|
25
|
-
| the rules beneath (`spokenMap`, `autoRules`, `SPELL`, `SAY`), sentences, captions, YouTube chapters | `footprint-
|
|
25
|
+
| the rules beneath (`spokenMap`, `autoRules`, `SPELL`, `SAY`), sentences, captions, YouTube chapters | `footprint-story` | anywhere |
|
|
26
26
|
| notes (N), presenter (P), listen mode (L) in the page | `footprint-storydeck/narration-html` + `footprint-storydeck/narration.css` | build + page |
|
|
27
|
-
| a clip per click in a voice, cached (`voiceClips`, `clipFor`, `chatterboxKit`, `pickSteps`) | `footprint-
|
|
27
|
+
| a clip per click in a voice, cached (`voiceClips`, `clipFor`, `chatterboxKit`, `pickSteps`) | `footprint-story/voice` | **Node only** |
|
|
28
28
|
| those clips carried inside one page (`embedClips`) | `footprint-storydeck/voice` | **Node only** |
|
|
29
29
|
| the deck as a narrated video | `footprint-storydeck/video` (+ ffmpeg 5.0 or newer on the path, Playwright's `chromium` passed in) | **Node only** |
|
|
30
30
|
| the deck as PowerPoint (.pptx): a slide per click, its picture, notes and clip | `footprint-storydeck/pptx` — `exportPptx` (the same driver as the video; no ffmpeg; `npm i footprintjs` beside it, an optional peer) | **Node only** |
|
|
31
31
|
| a deck from a folder: one page, a dev server that reloads on the same click, the deck read back as text | the `storydeck` command (`npx footprint-storydeck init · dev · outline · review · check · build · mcp`); `footprint-storydeck/deck` for scripts | **Node only** |
|
|
32
32
|
|
|
33
|
-
Never import `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` or `footprint-
|
|
33
|
+
Never import `footprint-storydeck/voice`, `footprint-storydeck/video`, `footprint-storydeck/pptx` or `footprint-story/voice` into browser code: they use `node:fs`
|
|
34
34
|
and child processes.
|
|
35
35
|
The main entry (`footprint-storydeck`) re-exports only what runs anywhere.
|
|
36
36
|
|
|
@@ -99,7 +99,7 @@ whole path .4 s).
|
|
|
99
99
|
**Narrate a deck in a voice.**
|
|
100
100
|
|
|
101
101
|
```js
|
|
102
|
-
import { voiceClips, chatterboxKit } from 'footprint-
|
|
102
|
+
import { voiceClips, chatterboxKit } from 'footprint-story/voice';
|
|
103
103
|
import { narrationText } from 'footprint-storydeck/narration';
|
|
104
104
|
import { embedClips } from 'footprint-storydeck/voice';
|
|
105
105
|
import { notesData, notesRuntime, listenRuntime } from 'footprint-storydeck/narration-html';
|
|
@@ -113,14 +113,14 @@ page += notesData(clicks.map(c => c.notes)) + voice.tags + `<script>(${notesRunt
|
|
|
113
113
|
|
|
114
114
|
The engine is a port — `{ name, synthesize(scenes, { work }) }`, scenes `{ id, text }`, results `{ id, audio,
|
|
115
115
|
duration, words: [{ text, start, end }] }` — so a cloud voice or a test fake plugs in the same way as the local kit.
|
|
116
|
-
A clip keeps its timed words (`clipFor(…).words`; `null` for a clip voiced before footprint-
|
|
116
|
+
A clip keeps its timed words (`clipFor(…).words`; `null` for a clip voiced before footprint-story).
|
|
117
117
|
|
|
118
118
|
**Render the narrated video.** A strip first, then the whole.
|
|
119
119
|
|
|
120
120
|
```js
|
|
121
121
|
import { chromium } from 'playwright-core';
|
|
122
122
|
import { renderVideo, deckStageDriver } from 'footprint-storydeck/video';
|
|
123
|
-
import { clipFor } from 'footprint-
|
|
123
|
+
import { clipFor } from 'footprint-story/voice';
|
|
124
124
|
import { narrationText, writtenText } from 'footprint-storydeck/narration';
|
|
125
125
|
|
|
126
126
|
const steps = clicks.map(c => {
|
|
@@ -149,7 +149,7 @@ It is pictures + notes + sound, not editable PowerPoint shapes.
|
|
|
149
149
|
|
|
150
150
|
- **A clip's key is its recipe**: the voice profile's settings and the exact spoken text. Change the words a
|
|
151
151
|
voice reads — a note, or the `spell` list — and those clicks are voiced again; everything else stays cached.
|
|
152
|
-
`spell` replaces the default list: extend it with `{ ...SPELL, K8s: 'K eight s' }` (`SPELL` from `footprint-
|
|
152
|
+
`spell` replaces the default list: extend it with `{ ...SPELL, K8s: 'K eight s' }` (`SPELL` from `footprint-story`;
|
|
153
153
|
a value is words — a letter in every word, no digits, no comma, semicolon or colon at its end — or it is refused).
|
|
154
154
|
- **Every word needs letters.** A lone symbol that means something is said (`SAY`: & + = < > × % and Greek
|
|
155
155
|
letters; pass `say` to change it); any other (— → ... an emoji) is a pause that keeps its sentence end. The local
|
package/pptx.d.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* its clip. Node. The export is a footprintjs flowchart: every run leaves a record that explains it. footprintjs is an
|
|
4
4
|
* optional peer dependency: `npm i footprintjs` beside StoryDeck to export.
|
|
5
5
|
*/
|
|
6
|
-
import type { VoiceProfile } from 'footprint-
|
|
6
|
+
import type { VoiceProfile } from 'footprint-story/voice';
|
|
7
7
|
import type { VideoClip, VideoDriver, VideoStep } from './video';
|
|
8
8
|
|
|
9
9
|
/** A click to export: as renderVideo takes it, plus the slide's headline (the title, before the label). */
|
|
10
10
|
export interface PptxStep extends VideoStep { headline?: string }
|
|
11
11
|
/** A click of a deck: its label (and headline), and its notes — narration-html's notesData entry ({ now }), a string, or a list of strings. */
|
|
12
12
|
export interface DeckClick { label?: string; headline?: string; notes?: string | readonly string[] | { now: readonly string[] } | null }
|
|
13
|
-
/** Where a deck's clips are: footprint-
|
|
13
|
+
/** Where a deck's clips are: footprint-story/voice's cache and profile, and the reading rules the clips were voiced with. */
|
|
14
14
|
export interface PptxVoice { cache: string; profile: VoiceProfile; spell?: Readonly<Record<string, string>>; say?: Readonly<Record<string, string>> }
|
|
15
15
|
/** A click as exported: its number, title, notes as written, words as spoken, clip, and the seconds it stays (null: the speaker clicks). */
|
|
16
16
|
export interface PlannedSlide {
|
package/video.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* storydeck · video (video.js) — a narrated deck as a video: every click on screen while its clip plays. Node + ffmpeg 5.0+.
|
|
3
3
|
*/
|
|
4
|
-
import type { Clip } from 'footprint-
|
|
5
|
-
import type { Chapters } from 'footprint-
|
|
4
|
+
import type { Clip } from 'footprint-story/voice';
|
|
5
|
+
import type { Chapters } from 'footprint-story';
|
|
6
6
|
|
|
7
|
-
/** A cached clip as the video uses it (`words` is not read: a clip from before footprint-
|
|
7
|
+
/** A cached clip as the video uses it (`words` is not read: a clip from before footprint-story has none). */
|
|
8
8
|
export type VideoClip = Omit<Clip, 'words'> & { words?: Clip['words'] };
|
|
9
9
|
/** A click to render: its label, its words as written and as spoken, and its clip (or null). */
|
|
10
10
|
export interface VideoStep { label?: string; written?: string; spoken?: string; clip?: VideoClip | null }
|
package/voice.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* storydeck · voice (voice.js) — a deck's clips inside one page. Node. The clip cache, the voice port and the kit
|
|
3
|
-
* adapter are footprint-
|
|
3
|
+
* adapter are footprint-story's: import them from 'footprint-story/voice'.
|
|
4
4
|
*/
|
|
5
|
-
import type { VoiceProfile } from 'footprint-
|
|
5
|
+
import type { VoiceProfile } from 'footprint-story/voice';
|
|
6
6
|
|
|
7
7
|
type Run = (cmd: string, args: string[], options?: object) => { status: number | null };
|
|
8
8
|
|