@hypersocial/decks-cli 0.9.5 → 0.9.7
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/README.md +2 -2
- package/package.json +1 -1
- package/skills/hyperdecks/SKILL.md +71 -18
- package/skills/hyperdecks/chat.md +5 -4
- package/src/checkRules.mjs +45 -4
- package/src/commands.mjs +19 -12
- package/src/export-video.mjs +290 -43
- package/src/exportHelp.mjs +1 -1
- package/src/files.mjs +1 -0
- package/src/firstDeck.mjs +1 -1
- package/src/guide.mjs +1 -1
- package/src/layouts.mjs +2 -2
- package/src/loadingBlock.mjs +45 -14
- package/src/ops.mjs +6 -3
- package/src/presenter.mjs +2 -2
- package/src/preview-capture.mjs +24 -2
- package/src/preview.mjs +2 -2
- package/src/runtime.mjs +34 -6
- package/templates/layouts/_deck.html +47 -3
- package/templates/layouts/split.html +48 -0
- package/templates/narrated-deck.html +3 -3
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ decks activity
|
|
|
34
34
|
|
|
35
35
|
## Auth
|
|
36
36
|
|
|
37
|
-
`decks login` opens the production broker at `https://
|
|
37
|
+
`decks login` opens the production broker at `https://hyperdecks.ai` and
|
|
38
38
|
stores a refresh credential in:
|
|
39
39
|
|
|
40
40
|
```text
|
|
@@ -261,7 +261,7 @@ Capture defaults to the initial slide, 4 seconds, 600px width and 10 fps, with a
|
|
|
261
261
|
`--json` prints one line on stdout, identical to `<out>/manifest.json`:
|
|
262
262
|
|
|
263
263
|
```json
|
|
264
|
-
{ "status": "exported | uploaded | failed", "shareUrl": "https://
|
|
264
|
+
{ "status": "exported | uploaded | failed", "shareUrl": "https://hyperdecks.ai/v/<token>",
|
|
265
265
|
"files": [{ "type": "gif | png | mp4", "path": "...", "url": "https://...convex.cloud/api/storage/...", "bytes": 74662, "width": 600, "height": 338 }],
|
|
266
266
|
"email": { "sendable": true, "path": "<out>/email.html", "html": "<a href=\"<shareUrl>\"><img src=\"<gif url>\" ...></a>" },
|
|
267
267
|
"warnings": [] }
|
package/package.json
CHANGED
|
@@ -225,7 +225,7 @@ The deck contract (`check` enforces or warns on each line):
|
|
|
225
225
|
so put slots on different sides across slides, as the scaffold's
|
|
226
226
|
`.slot-left` does); the first slide has `data-intro`.
|
|
227
227
|
- The player script stays
|
|
228
|
-
`<script type="module" src="https://
|
|
228
|
+
`<script type="module" src="https://hyperdecks.ai/player/v1/player.js" data-manifest="…">`.
|
|
229
229
|
`narrate` fills `data-manifest`; do not edit it by hand.
|
|
230
230
|
- A deck with no narration (a product tour, an imported design deck, a silent
|
|
231
231
|
walkthrough) says so: `data-silent` on the player script. `check` then warns
|
|
@@ -448,11 +448,8 @@ saves one.
|
|
|
448
448
|
|
|
449
449
|
<!-- font pairings -->
|
|
450
450
|
|
|
451
|
-
**
|
|
452
|
-
|
|
453
|
-
the deck's colours: paste the `<svg>` in place of a features card's icon. Use
|
|
454
|
-
`icons <subject...> --style` (`generate_icon_set`) only for illustrated icons
|
|
455
|
-
in a style of your own.
|
|
451
|
+
**Textures, icons, pictures:** the `look` topic (one texture, one icon set
|
|
452
|
+
from `search_icons` or `generate_icon_set`, a picture on every other slide).
|
|
456
453
|
|
|
457
454
|
**Pick the layout from what the slide says.** Each layout's file in
|
|
458
455
|
`templates/layouts/` opens with its slot side, presenter and the cues that
|
|
@@ -463,6 +460,7 @@ suit it.
|
|
|
463
460
|
| `hero` | the opening, a section opener; the presenter is the visual, no `.art` unless it shows the real subject (check fails while the placeholder `<div class="art">` is in: swap it for an `<img class="art">` or delete it) | stage, bottom-right; `[wave]`, `[look: h1]` |
|
|
464
461
|
| `number` | one number with its source: proof, traction, a delta | stage; `[look: #<id>-figure]` as it is said |
|
|
465
462
|
| `product` | a screenshot in a browser frame (`.frame.card` for a phone) with the point beside it | stage; `[look: #<id>-frame]`, `[reveal: #<id>-callout]` |
|
|
463
|
+
| `split` | the workhorse: a real picture (phone, laptop, 16:9) beside sticker, title, three ticks | stage, bottom-left; `[look: #<id>-frame]`, `[go: #<id>-t1]` … `t3` |
|
|
466
464
|
| `phones` | a phone app's flow: a fanned row of phone cards | stage, bottom-left; `[go: #<id>-p1]` … `p3`, `[go: slot]` |
|
|
467
465
|
| `quote` | a customer or user in their own words, quoted exactly | stage, bottom-left; `[look: #<id>-quote]` |
|
|
468
466
|
| `compare` | before/after, them/us, three parallel lines each | stage; `[look: #<id>-before]`, `[go: #<id>-after]` |
|
|
@@ -476,12 +474,10 @@ suit it.
|
|
|
476
474
|
two to four: `phones` (`--tilt`, `--shadow` on a card). A few slides at most:
|
|
477
475
|
`bg-dots` (never under text), a `.deco` of inked tiles.
|
|
478
476
|
|
|
479
|
-
Presenter style follows the deck (step 2
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
picture. Three layouts (`quote`, `phones`, `closer`) sit left; put a `slot-left` on others
|
|
484
|
-
where there is room so slide changes move the presenter.
|
|
477
|
+
Presenter style follows the deck (step 2): `stage` when the presenter carries
|
|
478
|
+
the story (the layouts keep the slot's corner clear); `corner` where a big
|
|
479
|
+
visual is the point; `glass` on a dark or busy picture. `quote`, `split`,
|
|
480
|
+
`phones` and `closer` sit left; add `slot-left` elsewhere where there is room.
|
|
485
481
|
|
|
486
482
|
**Visuals: real first, generated second.**
|
|
487
483
|
|
|
@@ -495,10 +491,12 @@ where there is room so slide changes move the presenter.
|
|
|
495
491
|
floating shapes, abstract "tech" art, clip-art metaphors) makes a deck look
|
|
496
492
|
cheap. A visual earns its place only if it shows something real from the
|
|
497
493
|
story: the product, the data, a concrete situation or object. When in doubt,
|
|
498
|
-
leave it out: one big statement in type beats a picture of nothing.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
494
|
+
leave it out: one big statement in type beats a picture of nothing. But a
|
|
495
|
+
deck of nothing but type is not the answer: reach for the real things
|
|
496
|
+
first (`look` topic). The presenter is the hero of the opening slide;
|
|
497
|
+
don't put generated art behind it.
|
|
498
|
+
- Icons: from the deck's one set (`look` topic), labelling things or in a
|
|
499
|
+
`.sticker`; not one per sentence. Bullets are `.ticks`.
|
|
502
500
|
- Images: `gen-image` (`generate_image`) only for a concrete subject the
|
|
503
501
|
deck is about (a product in use, a place, a real object), never a metaphor.
|
|
504
502
|
One style per deck, described the same way every time (subject, medium,
|
|
@@ -543,6 +541,61 @@ still.
|
|
|
543
541
|
- No CSS animation on slides beyond `.reveal`; the player animates the slide
|
|
544
542
|
change.
|
|
545
543
|
|
|
544
|
+
<!-- topic: look -->
|
|
545
|
+
#### Look: textures, icons, pictures
|
|
546
|
+
|
|
547
|
+
What the house decks do that a plain agent deck doesn't. Decide these three
|
|
548
|
+
once per deck, before writing slides, and say them in your plan.
|
|
549
|
+
|
|
550
|
+
**1. A picture on at least every other slide**, the most real one you have:
|
|
551
|
+
|
|
552
|
+
1. the product itself: `capture_url` (phone: width 390, height 844), or a
|
|
553
|
+
screen recording or photo the person gives you (`upload_deck_asset`);
|
|
554
|
+
2. their people, logos and app icons (with permission);
|
|
555
|
+
3. real data drawn with `render_chart`;
|
|
556
|
+
4. only then `generate_image`, of a concrete subject (a place, an object,
|
|
557
|
+
the product in use), never a metaphor or abstract "tech" art.
|
|
558
|
+
|
|
559
|
+
No product to show (a plan, a strategy)? Make the slides visual with type
|
|
560
|
+
and the kit below: a `number` slide, stickers, ticks on a `panel`.
|
|
561
|
+
|
|
562
|
+
**2. One texture**, on two or three slides (an opener, a closer, a slide
|
|
563
|
+
with a visual), never under body text. On the section:
|
|
564
|
+
|
|
565
|
+
| Class | Looks like | For |
|
|
566
|
+
|---|---|---|
|
|
567
|
+
| `bg-lines` | diagonal speed lines fading toward `--fade-at` | product slides, beside a picture |
|
|
568
|
+
| `bg-halftone` | a corner of halftone dots | openers, numbers |
|
|
569
|
+
| `bg-rays` | rays from `--rays-at` | an opener or a closer |
|
|
570
|
+
| `bg-dots` | a fading dot grid | a phones row |
|
|
571
|
+
|
|
572
|
+
Grounds carry the rest: alternate `on-accent`, plain, `on-ink` and the soft
|
|
573
|
+
washes `on-tint`, `on-tint-2`, `on-tint-3` so each slide change reads.
|
|
574
|
+
|
|
575
|
+
**3. One icon set.** Pick one source and keep it for every icon in the deck:
|
|
576
|
+
Lucide (`search_icons`, same `strokeWidth` throughout); or one
|
|
577
|
+
`generate_icon_set` call with every subject you need and one `preset`
|
|
578
|
+
(`line`, `duotone`, `sticker`, `clay`; the palette defaults to the brand's);
|
|
579
|
+
or the product's own app icons and logos. Never mix two.
|
|
580
|
+
Put an icon or logo in a `.sticker` (tilted tile, hard shadow) on openers
|
|
581
|
+
and product slides; use `.ticks` (accent squares) for lists, not icons.
|
|
582
|
+
|
|
583
|
+
**Kit (in every layout deck's shell):** `.sticker` (`--tilt`; `.photo` for
|
|
584
|
+
an app icon or a face), `.ticks`, `.panel`, `.pill`, `.frame.card` (a phone),
|
|
585
|
+
`.frame.laptop` (a desktop shot). The `split` layout is image plus copy.
|
|
586
|
+
|
|
587
|
+
**Image prompts.** One recipe per deck, the same every time:
|
|
588
|
+
`<concrete subject>, <setting>, <medium: e.g. editorial photo, soft daylight>,
|
|
589
|
+
palette <ink hex> <accent hex> <paper hex>, no text, no logos`
|
|
590
|
+
(`generate_image { brand: true }` adds the palette and the "no text" for
|
|
591
|
+
you). Generate at the slide's shape (wide for a hero), in one batch.
|
|
592
|
+
|
|
593
|
+
**Slide recipes.** Opener: `on-accent` + `bg-rays` or `bg-halftone`, the logo
|
|
594
|
+
or a few app-icon stickers, one display line. Product or feature: `split`
|
|
595
|
+
with a real screen. Proof: `number` (huge, sourced) or `chart`. Plan:
|
|
596
|
+
`timeline` on a tint. People: `team` with real faces. Closer: `on-ink`, the
|
|
597
|
+
one action, stickers again.
|
|
598
|
+
|
|
546
599
|
<!-- topic: script -->
|
|
547
600
|
### 3. Write slides and script
|
|
548
601
|
|
|
@@ -653,7 +706,7 @@ overflowing slide; `check <slug> --hosted --layout` measures a hosted draft.
|
|
|
653
706
|
`--forbid` lists phrases that must not reach this audience (another customer,
|
|
654
707
|
an internal codename). It matches the page's text, alt text, titles and
|
|
655
708
|
script.md, not URLs in attributes, so forbidding your own brand still passes
|
|
656
|
-
with the player's `
|
|
709
|
+
with the player's `hyperdecks.ai` script tag. Messages and what to do:
|
|
657
710
|
|
|
658
711
|
| Message starts with | Fix |
|
|
659
712
|
|---|---|
|
|
@@ -842,7 +895,7 @@ Capture defaults to the initial slide, 4 seconds, 600px width and 10 fps, with a
|
|
|
842
895
|
`--json` prints one line on stdout, identical to `<out>/manifest.json`:
|
|
843
896
|
|
|
844
897
|
```json
|
|
845
|
-
{ "status": "exported | uploaded | failed", "shareUrl": "https://
|
|
898
|
+
{ "status": "exported | uploaded | failed", "shareUrl": "https://hyperdecks.ai/v/<token>",
|
|
846
899
|
"files": [{ "type": "gif | png | mp4", "path": "...", "url": "https://...convex.cloud/api/storage/...", "bytes": 74662, "width": 600, "height": 338 }],
|
|
847
900
|
"email": { "sendable": true, "path": "<out>/email.html", "html": "<a href=\"<shareUrl>\"><img src=\"<gif url>\" ...></a>" },
|
|
848
901
|
"warnings": [] }
|
|
@@ -4,7 +4,7 @@ You are building a narrated deck through the Hyperdecks connector: no terminal,
|
|
|
4
4
|
no git, no folder. The deck's files (`index.html`, `script.md`, `evidence.md`)
|
|
5
5
|
live on Hyperdecks as a draft; you read and edit them with tools, and nothing
|
|
6
6
|
reaches a recipient until `release_deck`. Topics: call `deck_guide` with
|
|
7
|
-
`format`, `script`, `scripts`, `design`, `layouts`, `presenter`, `transitions` or `check`
|
|
7
|
+
`format`, `script`, `scripts`, `design`, `look`, `layouts`, `presenter`, `transitions` or `check`
|
|
8
8
|
before the step that needs it.
|
|
9
9
|
|
|
10
10
|
## First time here?
|
|
@@ -21,7 +21,8 @@ makes the hello deck; continue from step 5 below.
|
|
|
21
21
|
|
|
22
22
|
## The loop
|
|
23
23
|
|
|
24
|
-
1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides
|
|
24
|
+
1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides
|
|
25
|
+
and `deck_guide { topic: "look" }` to pick the deck's texture, icon set and pictures.
|
|
25
26
|
`get_brand` for the person's saved brand: write in its designMd's voice.
|
|
26
27
|
None yet? Offer to import it from what they have: `import_brand` with
|
|
27
28
|
their website (`url`), a PowerPoint or PDF they attach (`file`) or one of
|
|
@@ -46,8 +47,8 @@ makes the hello deck; continue from step 5 below.
|
|
|
46
47
|
layouts' stand-ins (hero's `<div class="art">`, product's
|
|
47
48
|
`<div class="shot">`, chart's placeholder `<svg>`) fail `check_deck` until
|
|
48
49
|
you swap each for an `<img>` of its real file, or delete the hero's art.
|
|
49
|
-
Icons: `search_icons { query }`
|
|
50
|
-
|
|
50
|
+
Icons: one set per deck (`look` topic): `search_icons { query }` for real
|
|
51
|
+
line icons as inline `<svg>` in `currentColor`, or one `generate_icon_set`.
|
|
51
52
|
For motion, `generate_video` (poll `check_video_job`): `loop: true` for a
|
|
52
53
|
clip that repeats with no jump, `transparent: true` for a moving cut-out
|
|
53
54
|
over the slide. Paste the finished job's `markup` as is (a transparent
|
package/src/checkRules.mjs
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
// the deck's index.html and script.md as text, with the manifest and clip fetches
|
|
6
6
|
// injected.
|
|
7
7
|
|
|
8
|
-
export const PLAYER_V1 = "https://
|
|
8
|
+
export const PLAYER_V1 = "https://hyperdecks.ai/player/v1/player.js";
|
|
9
|
+
// Decks released before the app moved to hyperdecks.ai load the same player
|
|
10
|
+
// from the old host, which serves it forever; both pass.
|
|
11
|
+
export const PLAYER_V1_LEGACY = "https://decks.hypersocial.ai/player/v1/player.js";
|
|
12
|
+
export const isPlayerV1 = (src) => src === PLAYER_V1 || src === PLAYER_V1_LEGACY;
|
|
9
13
|
export const DEFAULT_MAX_DURATION = 180;
|
|
10
14
|
export const MOBILE_TITLE_CHARS = 28;
|
|
11
15
|
export const CAPTION_MIN_CHARS = 4;
|
|
@@ -61,6 +65,9 @@ export const MESSAGES = {
|
|
|
61
65
|
morphPair: (id, prev) => `slide ${id}: morph needs an element with the same data-morph="<name>" on this slide and the one before it (${prev})`,
|
|
62
66
|
transitionStale: (where, script, manifest) => `${where}: script.md says transition "${script}" but the manifest has "${manifest}": run \`hyperdecks narrate\` so the player gets it`,
|
|
63
67
|
flyTwice: (ids) => `fly is for the deck's one big moment, but slides ${ids.join(", ")} all use it`,
|
|
68
|
+
// Warnings from the look topic (deck_guide look): the deck passes, but a person should look.
|
|
69
|
+
fewPictures: (shown, total) => `only ${shown} of ${total} slides show a picture (a screenshot, a recording, a photo, a chart); aim for a real one on every other slide (deck_guide topic look)`,
|
|
70
|
+
mixedIcons: (sets) => `the deck mixes icon sets (${sets.join(" and ")}); keep one set per deck (deck_guide topic look)`,
|
|
64
71
|
// A layout's stand-in left in: `how` is the tool (hosted) or command (CLI) that makes the real file.
|
|
65
72
|
placeholder: (id, what, el, how, img) => `slide ${id}: ${what} is still the layout's placeholder (${el}); ${how}, then put it in as ${img}`,
|
|
66
73
|
// Warnings (check --layout): the deck still passes.
|
|
@@ -522,6 +529,10 @@ const PLACEHOLDERS = [
|
|
|
522
529
|
layout: "l-product", tag: "div", cls: "shot", what: "the product screenshot", img: '<img class="shot" src="…" alt="…">',
|
|
523
530
|
how: { hosted: "capture the real page with capture_url", cli: "capture the real page with `hyperdecks screenshot <url>`" },
|
|
524
531
|
},
|
|
532
|
+
{
|
|
533
|
+
layout: "l-split", tag: "div", cls: "shot", what: "the picture", img: '<img class="shot" src="…" alt="…"> (or <video class="shot" src="…" autoplay muted loop playsinline>)',
|
|
534
|
+
how: { hosted: "capture the app at phone size with capture_url (width 390, height 844), or upload a screen recording or photo with upload_deck_asset", cli: "capture the app at phone size with `hyperdecks screenshot <url> --width 390 --height 844`, or add a screen recording or photo with `hyperdecks asset add`" },
|
|
535
|
+
},
|
|
525
536
|
{
|
|
526
537
|
layout: "l-phones", tag: "div", cls: "shot", what: "each phone screen", img: '<img class="shot" src="…" alt="…"> (or <video class="shot" src="…" autoplay muted loop playsinline>) in each card',
|
|
527
538
|
how: { hosted: "capture the app at phone size with capture_url (width 390, height 844), or upload a screen recording with upload_deck_asset", cli: "capture the app at phone size with `hyperdecks screenshot <url> --width 390 --height 844`, or add a screen recording with `hyperdecks asset add`" },
|
|
@@ -549,7 +560,7 @@ export function htmlErrors(html, { exists = () => true, forbid = [], script = ""
|
|
|
549
560
|
const errs = [...presenterStyleErrors(html, script), ...transitionErrors(html, script), ...cueSelectorErrors(html, script), ...cueGestureErrors(script), ...placeholderErrors(html, { hosted })];
|
|
550
561
|
const player = playerScript(html);
|
|
551
562
|
if (!player) errs.push(MESSAGES.noPlayer());
|
|
552
|
-
else if (player.src
|
|
563
|
+
else if (!isPlayerV1(player.src)) errs.push(MESSAGES.player(player.src));
|
|
553
564
|
// The loading state must be in <head> to hold the page from its first paint,
|
|
554
565
|
// with its fail-open so a deck whose player never loads still shows.
|
|
555
566
|
const loading = /<style\b[^>]*\sdata-hd-loading(?=[\s>=])[^>]*>([\s\S]*?)<\/style>/i.exec(/<head\b[\s\S]*?<\/head>/i.exec(uncommented(html))?.[0] ?? "");
|
|
@@ -574,7 +585,7 @@ export function htmlErrors(html, { exists = () => true, forbid = [], script = ""
|
|
|
574
585
|
const page = textOf(uncommented(html).replace(/<(script|style)\b[\s\S]*?<\/\1>/g, " ")).toLowerCase();
|
|
575
586
|
// The raw HTML too (alt text, <title>, meta content, inline script), less
|
|
576
587
|
// attribute values that are URLs: the player's own src (PLAYER_V1) names
|
|
577
|
-
// decks.hypersocial.ai, so forbidding the brand would otherwise always fail.
|
|
588
|
+
// hyperdecks.ai (or decks.hypersocial.ai), so forbidding the brand would otherwise always fail.
|
|
578
589
|
const raw = withoutUrlAttrs(uncommented(html)).toLowerCase();
|
|
579
590
|
for (const phrase of phraseList(forbid)) {
|
|
580
591
|
const p = phrase.toLowerCase();
|
|
@@ -740,7 +751,7 @@ export function absoluteRefs(html) {
|
|
|
740
751
|
}
|
|
741
752
|
|
|
742
753
|
// Hosts a released deck can rely on: our storage and app, the player, and fonts.
|
|
743
|
-
const HOSTED_ASSET_HOSTS = [/\.convex\.cloud$/, /\.convex\.site$/, /(^|\.)hypersocial\.ai$/, /^fonts\.(googleapis|gstatic)\.com$/];
|
|
754
|
+
const HOSTED_ASSET_HOSTS = [/\.convex\.cloud$/, /\.convex\.site$/, /(^|\.)hypersocial\.ai$/, /(^|\.)hyperdecks\.ai$/, /^fonts\.(googleapis|gstatic)\.com$/];
|
|
744
755
|
const isHostedAsset = (url) => {
|
|
745
756
|
try {
|
|
746
757
|
const host = new URL(url).hostname;
|
|
@@ -750,6 +761,35 @@ const isHostedAsset = (url) => {
|
|
|
750
761
|
}
|
|
751
762
|
};
|
|
752
763
|
|
|
764
|
+
// The look topic as warnings. A slide shows a picture when it holds an <img>
|
|
765
|
+
// (not the brand logo or an icon), <video>, <canvas>, <iframe>, a chart <svg> or an
|
|
766
|
+
// inline-style url() background (not a stylesheet's: that is usually a texture); a deck of three or more slides with fewer than a third that do
|
|
767
|
+
// reads as all type (the topic aims for every other slide; a plan with no
|
|
768
|
+
// product to show may have fewer). Icons count as one set each: inline Lucide <svg> (by stroke width),
|
|
769
|
+
// and <img> icons (class icon, or a generated icon-* asset).
|
|
770
|
+
const iconImg = (tag) => /\bicon\b/.test(attr(tag, "class") ?? "") || /\/icon-[\w-]+\.(png|webp|svg)\b/i.test(attr(tag, "src") ?? "");
|
|
771
|
+
export function lookWarnings(html) {
|
|
772
|
+
const all = slides(html);
|
|
773
|
+
const out = [];
|
|
774
|
+
const picture = (body) =>
|
|
775
|
+
/<(video|canvas|iframe)\b/i.test(body) ||
|
|
776
|
+
/<svg\b[^>]*class=["'][^"']*\bchart\b/i.test(body) ||
|
|
777
|
+
[...body.matchAll(/\bstyle=("[^"]*"|'[^']*')/gi)].some(([, v]) => /\burl\(/i.test(v)) ||
|
|
778
|
+
[...body.matchAll(/<img\b[^>]*>/gi)].some(([tag]) => !/\bbrand-logo\b/.test(attr(tag, "class") ?? "") && !iconImg(tag));
|
|
779
|
+
const shown = all.filter((s) => picture(uncommented(s.body)) || /\burl\(/i.test(attr(s.tag, "style") ?? "")).length;
|
|
780
|
+
if (all.length >= 3 && shown * 3 < all.length) out.push(MESSAGES.fewPictures(shown, all.length));
|
|
781
|
+
const body = uncommented(all.map((s) => s.body).join("\n"));
|
|
782
|
+
const sets = new Set();
|
|
783
|
+
for (const [tag] of body.matchAll(/<svg\b[^>]*>/gi)) {
|
|
784
|
+
if (/\blucide/.test(attr(tag, "class") ?? "")) sets.add(`Lucide at stroke-width ${attr(tag, "stroke-width") ?? "2"}`);
|
|
785
|
+
}
|
|
786
|
+
for (const [tag] of body.matchAll(/<img\b[^>]*>/gi)) {
|
|
787
|
+
if (iconImg(tag)) sets.add("image icons");
|
|
788
|
+
}
|
|
789
|
+
if (sets.size > 1) out.push(MESSAGES.mixedIcons([...sets].sort()));
|
|
790
|
+
return out;
|
|
791
|
+
}
|
|
792
|
+
|
|
753
793
|
// Every rule on the deck's text. `exists(ref)` answers whether a local reference
|
|
754
794
|
// resolves (true, false or "outside"); `hosted: true` means there is no folder,
|
|
755
795
|
// so every local reference is an error naming upload_deck_asset, a hotlinked
|
|
@@ -769,6 +809,7 @@ export async function checkSources({ html, script = "" }, opts = {}, { fetch: fe
|
|
|
769
809
|
const errors = htmlErrors(html, { exists, forbid, script, hosted: !!opts.hosted, strict });
|
|
770
810
|
const warnings = [];
|
|
771
811
|
if (!notesRequired(html, { strict })) for (const id of slidesWithoutNotes(html)) warnings.push(MESSAGES.noteSoft(id));
|
|
812
|
+
warnings.push(...lookWarnings(html));
|
|
772
813
|
// Without a folder (hosted, or a caller that passes no reader) only the inline scripts are read.
|
|
773
814
|
const walked = deckFiles(html, opts.hosted ? {} : { pathExists: opts.pathExists, read: opts.read, isDir: opts.isDir });
|
|
774
815
|
for (const m of walked.missing) errors.push(MESSAGES.scriptRef(m.ref, m.from));
|
package/src/commands.mjs
CHANGED
|
@@ -376,8 +376,9 @@ commands:
|
|
|
376
376
|
engagement <recipient|token> [--deck <slug>] [--json]
|
|
377
377
|
what one recipient watched per deck: narration heard, replays, drop-off, visits
|
|
378
378
|
gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--transparent] [--image <url>] [--size <s>]
|
|
379
|
-
[--quality <q>] [--count <n>] [--seed <n>] [--slug <deck>] [--download <file>]
|
|
380
|
-
default model gpt-image-2.5; --seed applies to nano-banana-2.1
|
|
379
|
+
[--quality <q>] [--count <n>] [--seed <n>] [--brand] [--slug <deck>] [--download <file>]
|
|
380
|
+
default model gpt-image-2.5; --seed applies to nano-banana-2.1;
|
|
381
|
+
--brand adds your brand's colours and "no text, no logos" to the prompt
|
|
381
382
|
image-job <jobId> [--json]
|
|
382
383
|
gen-video "<prompt>" [--model h3-max|kling-v3] [--image <url>] [--seconds 5] [--aspect 16:9|9:16] [--loop] [--transparent] [--slug <deck>] [--name <name>] [--download <file>]
|
|
383
384
|
a 3–10s clip via fal (default MiniMax H3 Max); --image animates that still and keeps its aspect
|
|
@@ -390,8 +391,9 @@ commands:
|
|
|
390
391
|
a branded SVG: { "kind": "bar|line|donut|number", "labels", "series", "values", "value", "palette" }
|
|
391
392
|
icons search <word...> [--limit 8] [--stroke-width 2] [--json]
|
|
392
393
|
real icons from the Lucide library as inline SVG in currentColor (free, offline)
|
|
393
|
-
icons <subject...> --style "<style>" [--palette "#hex,#hex"] [--slug <deck>] [--name <prefix>]
|
|
394
|
-
a style-consistent set of generated (illustrated) icons, one per subject (max 12)
|
|
394
|
+
icons <subject...> --preset line|duotone|sticker|clay | --style "<style>" [--palette "#hex,#hex"] [--slug <deck>] [--name <prefix>]
|
|
395
|
+
a style-consistent set of generated (illustrated) icons, one per subject (max 12);
|
|
396
|
+
palette defaults to your brand's colours
|
|
395
397
|
brand [show] your saved brand (colours, fonts, logo, design.md) that every new deck starts from
|
|
396
398
|
brand set [--accent "#hex"] [--ink|--paper|--accent-2|--card "#hex"] [--fonts <pairing>]
|
|
397
399
|
[--display|--body|--mono "<Google Fonts family>"] [--logo <url|file>] [--no-logo]
|
|
@@ -415,13 +417,16 @@ commands:
|
|
|
415
417
|
a PDF of the released deck: one 1600x900 page per slide (a script page after each
|
|
416
418
|
with --notes), a footer with the deck's tracked link; kept 30 days
|
|
417
419
|
export <slug|deck-dir> --video [--size 1080p|720p|4k] [--shape 16:9|9:16|1:1] [--no-captions]
|
|
418
|
-
[--no-avatar] [--title-card] [--end-card [--link <url>]] [--
|
|
420
|
+
[--no-avatar] [--title-card] [--end-card [--link <url>]] [--slides <a-b>] [--upload]
|
|
421
|
+
[--out <file.mp4>] [--json]
|
|
419
422
|
an MP4 of the narrated deck, drawn frame by frame so the avatar is smooth (slower
|
|
420
423
|
than real time: a 4-minute deck takes about 25 minutes with the live head on a
|
|
421
|
-
laptop
|
|
422
|
-
the
|
|
423
|
-
|
|
424
|
-
|
|
424
|
+
laptop; the dashboard's Export → Video splits it across machines, about 10
|
|
425
|
+
minutes): the avatar, voice and captions, none of the player's chrome. A slug
|
|
426
|
+
records the released copy, a deck folder its working copy. --slides 3-5 records
|
|
427
|
+
only those slides (counting from 1). --title-card adds the title and presenter
|
|
428
|
+
(2s), --end-card the deck's "Video viewers" link (3s); --upload keeps it with the
|
|
429
|
+
deck's exports for 30 days
|
|
425
430
|
export list [<slug>] [--json] your exports and their download links
|
|
426
431
|
export status <exportId> [--json] one export: a hosted video's progress, its link when done
|
|
427
432
|
export cancel <exportId> stop a hosted video export (Export → Video, export_deck mp4)
|
|
@@ -474,7 +479,7 @@ async function genImage(pos, opts) {
|
|
|
474
479
|
const prompt = pos.join(" ").trim() || opts.prompt;
|
|
475
480
|
if (!prompt) {
|
|
476
481
|
throw new Error(
|
|
477
|
-
'usage: gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--image <url>] [--transparent] [--size <s>] [--quality <q>] [--count <n>] [--format png|jpeg|webp] [--seed <n>] [--slug <deck>] [--name <name>] [--download <file>]',
|
|
482
|
+
'usage: gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--image <url>] [--transparent] [--size <s>] [--quality <q>] [--count <n>] [--format png|jpeg|webp] [--seed <n>] [--brand] [--slug <deck>] [--name <name>] [--download <file>]',
|
|
478
483
|
);
|
|
479
484
|
}
|
|
480
485
|
let out = await ops.generateImage(opts, {
|
|
@@ -487,6 +492,7 @@ async function genImage(pos, opts) {
|
|
|
487
492
|
format: opts.format,
|
|
488
493
|
model: opts.model,
|
|
489
494
|
seed: opts.seed,
|
|
495
|
+
brand: opts.brand,
|
|
490
496
|
slug: opts.slug,
|
|
491
497
|
name: opts.name,
|
|
492
498
|
});
|
|
@@ -625,12 +631,13 @@ async function chart(pos, opts) {
|
|
|
625
631
|
}
|
|
626
632
|
|
|
627
633
|
async function icons(pos, opts) {
|
|
628
|
-
if (!pos.length || !opts.style) {
|
|
629
|
-
throw new Error('usage: icons <subject...> --style "<style>" [--palette "#hex,#hex"] [--model nano-banana-2.1|gpt-image-2.5|gpt-image-2] [--slug <deck>] [--name <prefix>]');
|
|
634
|
+
if (!pos.length || (!opts.style && !opts.preset)) {
|
|
635
|
+
throw new Error('usage: icons <subject...> --preset line|duotone|sticker|clay | --style "<style>" [--palette "#hex,#hex"] [--model nano-banana-2.1|gpt-image-2.5|gpt-image-2] [--slug <deck>] [--name <prefix>]');
|
|
630
636
|
}
|
|
631
637
|
const queued = await ops.generateIconSet(opts, {
|
|
632
638
|
subjects: pos,
|
|
633
639
|
style: opts.style,
|
|
640
|
+
preset: opts.preset,
|
|
634
641
|
palette: opts.palette ? String(opts.palette).split(",").map((c) => c.trim()).filter(Boolean) : undefined,
|
|
635
642
|
model: opts.model,
|
|
636
643
|
slug: opts.slug,
|