storyboard-html-preview — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited storyboard-html-preview (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are turning structured storyboard files into a single shareable HTML document. The output is what an editor, stakeholder, or client opens in a browser without thinking about it.
The constraint is non-negotiable: single file, no build step, no server, works offline. Any time the output requires "run this build command" or "host this somewhere," the skill has failed.
Trigger when the user:
storyboard.md + shots.json + asks for a deliverable for reviewShare, export and print requests about anything else are not for this skill. It does not start on its own after a storyboard-architect run; the architect offers it and the user picks.
After ./install.sh, the validators and helpers are in ~/.claude/shotkit-tools/ and the brand packs are in ~/.claude/shotkit-brand-packs/. A --project install puts both under ./.claude/ instead. From a repo clone they are tools/ and brand-packs/. The commands below give the installed path first and the clone path second. The tools need pip install pyyaml jsonschema.
One file: preview.html. Self-contained. Inline CSS. No JavaScript dependencies (vanilla JS only, embedded). The brand fonts are linked from Google Fonts, with a system fallback stack behind each one, so the page reads the same offline in the fallback fonts. No external images (placeholder slots; if generated images exist, embed as base64 OR reference relative paths).
output/
├── run.json # input, for the run id and date
├── storyboard.md # input
├── shots.json # input
├── text-overlays.json # input
├── brand-lock.snapshot.md # input
├── frames/round-N/ # input, if generation has happened
├── critiques/round-N/ # input, for the verdict badges
└── preview.html # ← what this skill producesIf the user has generated frames, the HTML references them via relative paths so the file works when the whole output/ folder is shared. Resolve a shot's frame in this order:
shot.assets.generated marked accepted: trueshot.assets.generatedframes/round-{highest}/{shot_id}.{png,jpg,jpeg,webp}generated/{shot_id}.{ext}, the pre-3.0.0 flat layoutData first, convention second. The data says which frame was accepted; the path convention only says which file sits in the folder.
If no frames exist yet, the HTML uses styled placeholder cards with the shot spec, still useful for review and handoff.
Required:
shots.jsontext-overlays.jsonbrand-lock.snapshot.mdOptional:
run.json (for the run id and date; without it the page says "not recorded")storyboard.md (for narrative context, surface the brief at the top)frames/round-N/{shot_id}.{png,jpg,jpeg,webp} (if generation has happened)critiques/round-N/{shot_id}.critique.json (for verdict badges)Validate before rendering, and stop if it fails:
python ~/.claude/shotkit-tools/validate_shots.py output/
# from a repo clone:
# python tools/validate_shots.py output/From brand-lock.snapshot.md, extract:
A font value in a brand-lock carries more than a family, for example Archivo Bold 700 wdth 80. Split it before it goes into CSS:
Archivo) goes into font-family, in quotes, ahead of a system fallbackstack: "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif
700, or a name such as Bold) goes into font-weightwdth 80) goes into font-stretch as 80%Writing the whole value into font-family makes the declaration invalid, and the page renders in the browser default. Use the brand-lock's mono font for labels, ids and timings. When it declares none, use the system mono stack.
The HTML preview should feel like the brand without going overboard. Quiet branding, not loud.
When the tools are installed, render with the CLI. It does every step below and writes preview.html into the output folder:
python ~/.claude/shotkit-tools/shots-to-html.py output/
# from a repo clone:
# python tools/shots-to-html.py output/Build the page by hand, following the steps below, only where the tools are not available (for example a Claude.ai upload).
Use templates/preview.html.tpl as the structural template. Read it before generating.
The HTML structure:
<!DOCTYPE html>
<html>
<head>
<meta>
<title>{project title}</title>
<style>
/* All CSS inline. ~200 lines. Brand-aware. */
/* Print stylesheet included. */
</style>
</head>
<body>
<header>
<!-- Project title, duration, aspect, generated timestamp -->
</header>
<section class="brief">
<!-- Brief summary if storyboard.md provides one -->
</section>
<section class="series-lock">
<!-- Character / environment / lighting / color grade -->
</section>
<section class="shots">
<!-- One card per shot -->
<article class="shot" id="shot_01">
<div class="shot-frame">
<!-- generated image OR styled placeholder -->
</div>
<div class="shot-meta">
<!-- timestamp, framing, angle, motion -->
</div>
<div class="shot-subject">
<!-- subject description -->
</div>
<div class="shot-text-overlay">
<!-- if on_screen_text exists, show overlay content with timing -->
</div>
<div class="shot-rationale">
<!-- rationale text -->
</div>
</article>
<!-- ... -->
</section>
<footer>
<!-- audit trail: brand-lock snapshot reference, timestamp -->
</footer>
<script>
/* Vanilla JS only. Optional: keyboard nav, jump-to-shot, expand/collapse. */
</script>
</body>
</html>Resolve each shot's frame by the order in "What you produce" above, then reference it by a path relative to the output root:
<img src="frames/round-2/shot_01.png" alt="shot_01: hook" loading="lazy" />This works when the whole output folder is zipped and shared.
For hard-copy print (a single file with no folder structure), the skill can offer to inline frames as base64. Ask the user which they prefer if frames are present.
If a shot's assets.generated entry carries a sha256 and the file no longer matches it, render the frame but say so on the page. That mismatch means the frame changed after it was recorded, which is exactly the case where a preview quietly showing the new file is worse than one that flags it.
If no frames exist, render styled placeholder cards showing the framing, subject, and shot spec. These are still useful for stakeholder review at the storyboard stage.
Template flag convention. When composing the per-shot context for preview.html.tpl, set exactly one of:
has_image: true and image_path: "frames/round-2/shot_NN.png", when a frame existshas_no_image: true, when none does (renders the placeholder card)The template uses two parallel {{#if}} blocks rather than {{#if}}/{{else}} to keep the rendering portable across template engines.
For text overlays, set has_overlays: true and an overlays array on the shot. Each entry carries id, content, font, weight, color, size, position_class, position_label, enter_at, enter_animation, exit_at, exit_animation. The template iterates that array with {{#each overlays}}.
It is an array because shots.json and text-overlays.json both let a shot carry several overlays, and every one of them has to render.
For verdict badges, set has_verdict, verdict, verdict_round, and verdict_class (the lowercased verdict) from the newest critique for that shot under critiques/. Omit them when the shot has no critique.
Escape everything. Subjects, rationales, VO lines, and overlay copy are model-generated prose that lands in both text and attribute contexts. One angle bracket in a rationale, or one quote in an overlay font name, breaks the page a client is reading. shots-to-html.py escapes every substitution by default and reserves raw output for the inlined CSS alone.
For every shot with an on_screen_text reference, the HTML shows:
This gives the reviewer a sense of what the final composited frame will look like, even before final compositing happens.
Include @media print rules that:
The user should be able to hit Cmd-P / Ctrl-P and get a clean PDF.
The output is one .html file. If you find yourself wanting a separate stylesheet or JS file, inline it. If you find yourself wanting a build step, you're solving the wrong problem.
No CDN scripts. No external CSS frameworks. The file must work with no internet connection.
One exception: the page links the brand's fonts from Google Fonts, one stylesheet per family, so a reader who is online sees the real typefaces. Offline, or for a family Google does not host, each font falls back to the system stack named after it and nothing else on the page changes. When the user wants a page that makes no network request at all, leave the links out (shots-to-html.py --no-web-fonts).
Hit Cmd-P. The result should be a clean PDF. If layout breaks across page boundaries, the print stylesheet is broken.
The palette's background role is the page background, and its ink role is the text. For a dark brand that means a dark page: its ground is its background, not an accent, and a light page would not look like the brand. Every other color is an accent: a rule, a label, a badge, an id. Never flood a section with the accent color. The reviewer's job is to read the storyboard, not admire the design.
Stakeholders open links on phones. The HTML should be readable on mobile without horizontal scroll. Simple responsive CSS.
templates/preview.html.tpl, the structural templatetemplates/styles.css.tpl, the CSS to inlinetemplates/print.css.tpl, the print rulesThe skill reads all three and assembles them into a single preview.html.
Before declaring done, verify:
with --no-web-fonts
shots.json is presentbrand_lock_ref from shots.json is what the footer links to, not a hardcoded name{{ remains anywhere in the outputThe CLI renderer checks the mechanical half of that list against itself:
python ~/.claude/shotkit-tools/shots-to-html.py --selftest
# from a repo clone:
# python tools/shots-to-html.py --selftest"Run" is when the storyboard was produced, read from run.json. "Rendered" is when the page was written. They are separate lines in the footer and they must stay separate.
A single "Generated" date would restamp the run as today every time the page is re-rendered, and the footer would name a brand-lock date that has nothing to do with the frames above it.
If the brand-lock on disk no longer hashes to what run.json recorded, say so on the page. The reader is looking at frames built against a brand state they can no longer see.
Generated preview.html files ship next to the storyboards that produced them:
../storyboard-architect/examples/30s-pain-proof-promise/preview.html../storyboard-architect/examples/60s-founder-explainer/preview.html../visual-asset-critic/examples/worked-run/preview.html, with frames and verdict badgesOpen them in a browser to calibrate quality. All three are re-rendered in CI with pinned timestamps and the build fails if the output moves, so they are also the regression test for this skill's output.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.