Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
},
Expand Down
4 changes: 3 additions & 1 deletion src/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }), {
Expand All @@ -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
Expand Down
16 changes: 15 additions & 1 deletion test/validation-and-readiness.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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';
Expand Down