retake-demos 0.2.0 → 0.2.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/README.md CHANGED
@@ -78,7 +78,7 @@ Exit codes: `0` good · `2` a step failed or the video is a raw fallback · `3`
78
78
  name: first-project
79
79
  title: "Creating your first project"
80
80
  url: http://localhost:3000
81
- preset: post-landscape # 1080p @ 30fps · `retake presets`
81
+ preset: post-landscape # `retake presets` — 1920×1080 page + caption band
82
82
  viewport: { width: 1440, height: 1080 }
83
83
  scale: 1.8 # page drawn at 1.8× so text stays crisp
84
84
  reducedMotion: true
@@ -113,15 +113,17 @@ The full template is [`demos/example.yaml`](demos/example.yaml); `retake init` c
113
113
 
114
114
  Think in the publishing format, not the browser window.
115
115
 
116
- | Preset | Canvas | For |
117
- |---|---:|---|
118
- | `draft` | 960×540 @ 15fps | iterating — same layout as the finals at a quarter of the pixels, fastest |
119
- | `preview-fast` | 1920×1080 @ 24fps | checking timing and framing, hardware-encoded |
120
- | `post-landscape` *(default)* | 1920×1080 @ 30fps | the general-purpose demo, plus a CRF-14 master |
121
- | `post-square` | 1080×1080 @ 30fps | feeds |
122
- | `post-vertical` | 1080×1920 @ 30fps | shorts and reels |
123
- | `docs-gif` | 1440×900 @ 24fps | README and docs GIFs |
124
- | `master` | 1920×1080 @ 30fps | archive, CRF 12 |
116
+ | Preset | Video | Page area | For |
117
+ |---|---:|---:|---|
118
+ | `draft` | 960×616 | 960×540 | iterating — the finals' layout at a quarter of the pixels, fastest |
119
+ | `preview-fast` | 1920×1230 | 1920×1080 | checking timing and framing, hardware-encoded |
120
+ | `post-landscape` *(default)* | 1920×1230 | 1920×1080 | the general-purpose demo, plus a CRF-14 master |
121
+ | `post-square` | 1080×1080 | 1080×950 | feeds — a true square |
122
+ | `post-vertical` | 1080×1920 | 1080×1750 | shorts and reels — a true 9:16 |
123
+ | `docs-gif` | 1440×1000 | 1440×900 | README and docs GIFs |
124
+ | `master` | 1920×1230 | 1920×1080 | archive, CRF 12 |
125
+
126
+ **The video is the page area plus the caption band.** Landscape keeps a full 1920×1080 page and the band goes under it (1920×1230) — the captions never cover the app. Square and vertical fit the band inside the canvas, because a feed that wants 1:1 or 9:16 will letterbox anything else. A manifest's `viewport` replaces the page area and the video follows it, so a 1240×1080 viewport gives a 1240×1230 video. `retake presets` prints this table, and `retake validate` tells you the exact size of the video a manifest will produce before you record it.
125
127
 
126
128
  **Page scale** is what makes it crisp: the page lays out as if the viewport were smaller and every glyph is drawn at 2×, so 1080p text reads on a phone. **Camera** eases toward whatever the demo just touched, at render time, clamped so it can never crop the thing it points at — per scene: `camera: static` or `camera: { focus: ".result", zoom: 1.3 }`. Captions, camera, speed, trim, format and layout are all render-time: change them and `retake render` takes seconds, never touches the browser.
127
129
 
package/dist/cli.js CHANGED
@@ -384,15 +384,26 @@ program
384
384
  .description("list quality presets")
385
385
  .action(async () => {
386
386
  const { PRESETS } = await import("./presets.js");
387
- for (const p of Object.values(PRESETS))
388
- say(`${p.name.padEnd(16)} ${String(p.width + "×" + p.height).padEnd(10)} ${p.fps}fps scale ${p.scale}× ${p.layout.padEnd(5)} — ${p.description}`);
387
+ say(`${"preset".padEnd(16)} ${"video".padEnd(11)} ${"page".padEnd(11)} fps layout`);
388
+ for (const p of Object.values(PRESETS)) {
389
+ const video = p.layout === "card" ? `${p.width}×${p.height}` : `${p.width}×${p.height + (p.layout === "band" ? p.bandHeight : 0)}`;
390
+ say(`${p.name.padEnd(16)} ${video.padEnd(11)} ${String(p.width + "×" + p.height).padEnd(11)} ${String(p.fps).padEnd(4)} ${p.layout.padEnd(5)} — ${p.description}`);
391
+ }
392
+ say(`\nvideo = page + caption band. A manifest's \`viewport\` replaces the page size (the video follows it).`);
389
393
  });
390
394
  program
391
395
  .command("validate")
392
396
  .argument("<manifest>")
393
- .action((file) => {
397
+ .action(async (file) => {
394
398
  const { manifest } = loadManifest(file);
399
+ const { resolve } = await import("./manifest.js");
400
+ const q = resolve(manifest);
401
+ // The video is the page area plus the caption band, so say the real number
402
+ // here rather than letting it surprise someone after a two-minute take.
403
+ const outW = q.layout === "card" ? q.width : q.viewport.width;
404
+ const outH = (q.layout === "card" ? q.height : q.viewport.height) + (q.layout === "band" ? q.bandHeight : 0);
395
405
  say(`ok: ${manifest.name} · ${manifest.steps.length} steps · ${manifest.steps.filter((s) => s.action === "scene").length} scenes`);
406
+ say(` ${q.name} · video ${outW}×${outH}${q.layout === "band" ? ` (page ${q.viewport.width}×${q.viewport.height} + ${q.bandHeight}px caption band)` : ""} @ ${q.fps}fps${q.gif ? ` · gif ${q.gif.width}px` : ""}`);
396
407
  for (const w of warnings(manifest))
397
408
  say(`⚠ ${w}`);
398
409
  });
package/dist/manifest.js CHANGED
@@ -339,7 +339,14 @@ export function resolve(m) {
339
339
  const p = PRESETS[m.preset] ?? PRESETS[DEFAULT_PRESET];
340
340
  const viewport = { width: m.viewport?.width ?? p.width, height: m.viewport?.height ?? p.height };
341
341
  const gifOverride = m.outputs.gif;
342
- const gif = gifOverride === false ? false : gifOverride === true ? p.gif : p.gif === false ? { width: gifOverride.width ?? 720, fps: gifOverride.fps ?? 15 } : { width: gifOverride.width ?? p.gif.width, fps: gifOverride.fps ?? p.gif.fps };
342
+ /* `gif: true` means "yes, a GIF" on any preset. Only docs-gif carries GIF
343
+ settings, so asking for one anywhere else used to resolve to the preset's
344
+ `false` and produce nothing, silently. */
345
+ const GIF_DEFAULT = { width: 900, fps: 15 };
346
+ const gif = gifOverride === false ? false
347
+ : gifOverride === true ? (p.gif === false ? GIF_DEFAULT : p.gif)
348
+ : p.gif === false ? { width: gifOverride.width ?? GIF_DEFAULT.width, fps: gifOverride.fps ?? GIF_DEFAULT.fps }
349
+ : { width: gifOverride.width ?? p.gif.width, fps: gifOverride.fps ?? p.gif.fps };
343
350
  const cursor = m.cursor === false ? false : { style: (m.cursor === true ? "default" : m.cursor.style ?? "default"), size: m.cursor === true ? p.cursorSize : m.cursor.size ?? p.cursorSize, idleHideMs: m.cursor === true ? 2000 : m.cursor.idleHideMs ?? 2000, idleHide: m.cursor === true ? undefined : m.cursor.idleHide };
344
351
  const captions = m.captions === false ? false : { fontSize: m.captions === true ? p.captionFontSize : m.captions.fontSize ?? p.captionFontSize, color: m.captions === true ? undefined : m.captions.color };
345
352
  return {
package/dist/presets.js CHANGED
@@ -38,19 +38,22 @@ export const PRESETS = {
38
38
  },
39
39
  "post-square": {
40
40
  name: "post-square",
41
- width: 1080, height: 1080, scale: 1.6, fps: 30, crf: 17,
41
+ /* The page area is the canvas minus the caption band, so the file really
42
+ is 1080×1080 — a square preset that produced 1080×1210 got letterboxed
43
+ by every feed. Landscape keeps its full-height page on purpose. */
44
+ width: 1080, height: 950, scale: 1.6, fps: 30, crf: 17,
42
45
  gif: false, layout: "band", cameraZoom: 1.25,
43
46
  cursorSize: 60, captionFontSize: 40, bandHeight: 130,
44
47
  encoder: "x264", master: true,
45
- description: "1080×1080, feed-friendly.",
48
+ description: "A true 1080×1080 (page 1080×950 + caption band), feed-friendly.",
46
49
  },
47
50
  "post-vertical": {
48
51
  name: "post-vertical",
49
- width: 1080, height: 1920, scale: 2, fps: 30, crf: 17,
52
+ width: 1080, height: 1750, scale: 2, fps: 30, crf: 17,
50
53
  gif: false, layout: "band", cameraZoom: 1.3,
51
54
  cursorSize: 60, captionFontSize: 44, bandHeight: 170,
52
55
  encoder: "x264", master: true,
53
- description: "9:16 for shorts/reels. Apps with a narrow column suit it best.",
56
+ description: "A true 1080×1920 (page 1080×1750 + caption band) for shorts/reels. Narrow-column apps suit it best.",
54
57
  },
55
58
  "docs-gif": {
56
59
  name: "docs-gif",
@@ -381,7 +381,9 @@ function saveDraftSoon() { clearTimeout(draftTimer); draftTimer = setTimeout(asy
381
381
  }, 500); }
382
382
 
383
383
  // ---------- demo view ----------
384
- const PRESET_LABEL = { 'post-landscape': '1080p', 'post-square': 'square', 'post-vertical': 'vertical', 'docs-gif': 'docs', 'proof-fast': 'small', 'master': 'master', 'preview-fast': 'preview' };
384
+ // Shape, not a resolution: the video is the page plus the caption band, so
385
+ // "1080p" was never true of what post-landscape actually writes (1920×1230).
386
+ const PRESET_LABEL = { 'post-landscape': 'landscape', 'post-square': 'square', 'post-vertical': 'vertical', 'docs-gif': 'docs', 'proof-fast': 'small', 'master': 'master', 'preview-fast': 'preview', 'draft': 'draft' };
385
387
  async function select(name) {
386
388
  if (dirty && !confirm('Discard unsaved manifest edits?')) return;
387
389
  current = name; dirty = false; setMsg('');
@@ -708,7 +710,7 @@ document.addEventListener('keydown', (e) => {
708
710
  // ---------- boot ----------
709
711
  (async () => {
710
712
  setQuality(quality);
711
- try { const ps = await api('/api/presets'); $('sPreset').innerHTML = ps.filter((p) => p.name !== 'preview-fast').map((p) => `<option value="${p.name}">${p.name} — ${p.width}×${p.height}</option>`).join(''); } catch {}
713
+ try { const ps = await api('/api/presets'); $('sPreset').innerHTML = ps.filter((p) => p.name !== 'preview-fast').map((p) => { const h = p.layout === 'card' ? p.height : p.height + (p.layout === 'band' ? p.bandHeight : 0); return `<option value="${p.name}">${p.name} — ${p.width}×${h}</option>`; }).join(''); } catch {}
712
714
  try { settings = await api('/api/settings'); $('modelTag').textContent = settings.active ? 'model: ' + settings.active : 'no model — set one in Settings'; } catch {}
713
715
  const list = await loadDemos();
714
716
  // Arriving from the review page's Edit link: /classic#<demo> opens that demo.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retake-demos",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Demo-as-code for coding agents: a YAML manifest in, a product walkthrough video out. Rerun the demo instead of re-recording it.",
5
5
  "keywords": [
6
6
  "demo",