diff --git a/CHANGELOG.md b/CHANGELOG.md index d4aca17..5f5d725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] +### Fixed +- `step(text, fn, options)` now honors flat caption display options (e.g. + `{ position }`, by analogy with `caption()`) instead of silently dropping + anything outside `options.captionOptions`. +- `lintDemoStoryboard()` no longer emits a spurious "missing mp4" warning when + the demo config sets `mp4`/`crop`/`zoom` — the public caller need not pass + `mp4Requested`. +- A thumbnail-only demo (no mp4/crop/zoom/trim) no longer re-muxes and + overwrites the source `.webm`; the thumbnail is taken from the original clip. + ## [1.3.0] - 2026-06-18 ### Added diff --git a/src/demo.js b/src/demo.js index e052a7f..ff78eaf 100644 --- a/src/demo.js +++ b/src/demo.js @@ -155,7 +155,11 @@ function analyzeDemoStoryboard(demoConfig, { viewport, mp4Requested } = {}) { )); } - if (!mp4Requested) { + // Honor an explicit mp4Requested (only the caller knows about the CLI --mp4 + // flag) but also infer it from the demo config, so public callers like + // lintDemoStoryboard() don't emit a spurious warning when demo.mp4 is set. + const wantsMp4 = mp4Requested || !!(demoConfig.mp4 || demoConfig.crop || demoConfig.zoom); + if (!wantsMp4) { warnings.push(storyboardWarning( 'missing-mp4', 'X/SNS demo clips should emit mp4', @@ -511,9 +515,13 @@ function createDemoController({ page, captions = [], captionOptions = {} }) { caption: (text, options = {}) => render(text, options), async step(text, action, options = {}) { - await render(text, options.captionOptions || {}); + // Caption display options may be passed explicitly (captionOptions) or flat + // (e.g. step(text, fn, { position }) by analogy with caption()); honor both + // so they are not silently dropped. holdMs is the only step-control key. + const { holdMs, captionOptions, ...displayOptions } = options; + await render(text, { ...displayOptions, ...(captionOptions || {}) }); const result = typeof action === 'function' ? await action() : undefined; - const hold = normalizeDelayMs(options.holdMs == null ? DEFAULT_STEP_HOLD_MS : options.holdMs, 'step holdMs'); + const hold = normalizeDelayMs(holdMs == null ? DEFAULT_STEP_HOLD_MS : holdMs, 'step holdMs'); if (hold > 0) await page.waitForTimeout(hold); return result; }, diff --git a/src/video.js b/src/video.js index 1fa3cf6..543dc89 100644 --- a/src/video.js +++ b/src/video.js @@ -147,7 +147,7 @@ function postProcessDemo({ webmPath, mp4, trim, crop, zoom, thumbnail, log, env if (crop) notes.push('cropped'); if (zoom) notes.push('zoomed'); log(`✓ ${path.basename(mp4Path)} (${notes.join(', ')})`); - } else { + } else if (trim) { // Trim-only: stream-copy to a sibling temp file, then swap in place. const tmp = `${webmPath}.trim.webm`; execFileSync(bin, buildFfmpegArgs({ input: webmPath, output: tmp, trim, copy: true }), { @@ -156,6 +156,8 @@ function postProcessDemo({ webmPath, mp4, trim, crop, zoom, thumbnail, log, env fs.renameSync(tmp, webmPath); log(`✓ ${path.basename(webmPath)} trimmed in place`); } + // else: thumbnail-only (no mp4/crop/zoom/trim) — nothing to re-encode; the + // thumbnail below is taken from the original webm without rewriting it. if (thumbnail) { const at = typeof thumbnail === 'object' && thumbnail.at != null ? thumbnail.at : 1; const thumbPath = typeof thumbnail === 'object' && thumbnail.name diff --git a/test/validation-and-readiness.test.js b/test/validation-and-readiness.test.js index 2abced6..b4ad51c 100644 --- a/test/validation-and-readiness.test.js +++ b/test/validation-and-readiness.test.js @@ -7,7 +7,7 @@ */ const { buildVideoFilter, buildFfmpegArgs } = require('../src/video'); -const { normalizeDemoConfigs } = require('../src/demo'); +const { normalizeDemoConfigs, lintDemoStoryboard } = require('../src/demo'); const { buildHandoffRecommendations } = require('../src/integrations'); const { assetRecord } = require('../src/handoff'); @@ -36,6 +36,20 @@ describe('demo config fails fast', () => { }); }); +describe('storyboard lint mp4 warning reflects the demo config', () => { + const captions = [{ at: 1, text: 'before' }, { at: 4, text: 'restore original' }]; + + it('no spurious missing-mp4 when demo.mp4 is set (public caller passes no mp4Requested)', () => { + const lines = lintDemoStoryboard({ name: 'd', mp4: { crf: 18 }, trim: { duration: 25 }, captions }); + expect(lines.some((l) => /mp4/i.test(l))).toBe(false); + }); + + it('still warns when neither the config nor a flag requests mp4', () => { + const lines = lintDemoStoryboard({ name: 'd', trim: { duration: 25 }, captions }); + expect(lines.some((l) => /mp4/i.test(l))).toBe(true); + }); +}); + describe('handoff readiness reflects whether a clip exists', () => { const cwd = '/tmp'; const outDir = '/tmp/store-assets';