_extract_video_frames built `-vf thumbnail=N -frames:v N` when no explicit
frame_indices were given. That reads as even sampling and is not: thumbnail=N
selects the most representative frame out of each consecutive N-frame BATCH, so
paired with -frames:v N it stops once N frames have been emitted and never
looks at the rest of the video.
Reproduced against a four-second fixture, solid red for 2s then solid blue
for 2s:
ffmpeg -i clip.mp4 -frames:v 4 -vf "thumbnail=4" out_%04d.png
-> frame_0001 (254,0,0) frame_0002 (254,0,0)
frame_0003 (254,0,0) frame_0004 (254,0,0)
Every sample lands in the first half. The blue half of the clip is invisible,
and because the failure produces four perfectly valid frames the caption pass
has no way to know: it described the clip as one in which nothing changes.
Sampling is now timestamp-driven. Duration comes from ffprobe and one frame is
taken at duration*(i+0.5)/n — inside each slice rather than on its edge, so a
cut landing exactly on a boundary does not sample the frame before or after it
depending on rounding. -ss goes ahead of -i to seek by keyframe, which is fast
and accurate enough for sampling that was never frame-exact.
Same fixture, after:
frame 0 (254,0,0) frame 1 (254,0,0) frame 2 (0,0,255) frame 3 (0,0,255)
Three behaviours preserved deliberately:
* The explicit frame_indices branch is untouched. It was already correct.
* No duration — a stream, or no ffprobe on PATH — falls back to an even pass
over the file rather than returning an empty list.
* A zero or negative duration is treated as unknown, since it would otherwise
divide the sampler by zero.
Also drops probe_cmd, which was assigned and never used.
tests/tools/test_video_understand_sampling.py adds 9 cases with subprocess.run
faked, so they assert the commands built rather than one ffmpeg build's output,
and fail if `thumbnail=` returns. tests/tools/ is 490 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A parametrized pair over `all_sources()`, so the contract is asserted for
every registered adapter rather than the eight that happened to be wrong,
and a new source cannot quietly reintroduce the bug.
- a failing transport propagates out of `search()`
- a genuine 200 carrying no results still returns `[]`
Both halves matter. Fixing only the first would let an adapter "pass" by
raising on everything, which breaks the empty-result case the corpus
builder relies on.
Eight of the propagation cases fail against the unfixed adapters. Three
-- archive_org, wikimedia and pond5_pd -- still swallow errors through a
different shape (a query cascade that continues past a failed strategy,
and an API failure that falls through to the web fallback) and are marked
`xfail(strict=True)` pending the follow-up. That records them as known
defects instead of encoding the bug as expected behavior, and fails the
suite the moment one is fixed and left in the map.
`requests` and `bs4` are stubbed via `sys.modules`, following the
fake-requests pattern in test_atlas_video.py. bs4 is stubbed because it is
an optional dependency that the scraping adapters import at the top of
`search()` -- left alone, those five would fail on the import rather than
on the request wherever bs4 is not installed, and pass for the wrong
reason.
Refs #511
Eight adapters -- dareful, esa, jaxa, loc, mixkit, nara, noaa, videvo --
wrapped their search request in `except Exception`, logged a warning and
returned `[]`. That is indistinguishable from a source that genuinely
matched nothing, so `direct_clip_search` reports `success: True,
clips_downloaded: 0, errors: []` for a run in which every request failed.
`base.StockSource.search` already documents the rule they violate:
"Network errors should be raised -- the corpus builder catches and logs
per-source so one flaky API doesn't poison the whole run." Both callers
already do exactly that: `direct_clip_search` and `corpus_builder` catch
per source and record `{phase, source, query, error}` before continuing,
so the plumbing to receive the truth is already in place.
Each adapter now re-raises after logging. The warning is kept -- it names
the source in the adapter's own words, which the caller's generic record
does not.
Behavior change: a run where every source failed previously reported
success with zero clips and will now surface the errors.
Refs #511
The Star History chart in the README is currently broken and fails to render, so visitors can no longer see the project's growth over time. Fix it by pointing the chart link and image at a working replacement that serves the same chart.
TalkingHead passes videoSrc straight to OffthreadVideo, so relative asset
paths (e.g. "clips/source.mp4" under public/) fail to load at render time.
Every other composition that accepts a video source (TitledVideo, Explainer,
CinematicRenderer) already routes it through resolveAsset; this brings
TalkingHead in line with them.
Verified by rendering the TalkingHead composition with a public/-relative
videoSrc before (load failure) and after (renders) this change.
CaptionOverlay assumes space-delimited text: it hardcodes a " " between
words and lets lines wrap anywhere inside a word. For CJK captions this
produces spurious inter-word spaces (Japanese does not use them) and
mid-word line breaks, including punctuation stranded at line starts.
Changes:
- CaptionOverlay: new wordSeparator prop (default " " keeps current
behavior; CJK callers pass ""). Each word renders as an unbreakable
inline block so lines wrap only at word boundaries - visually unchanged
for space-delimited text, fixes mid-word breaks for CJK.
- WordCaption: optional pageBreakAfter flag; buildPages flushes the page
early when set, so pages can align with sentence/scene boundaries
instead of splitting a clause across pages.
- TalkingHead: exposes captionColor / captionBackgroundColor (previously
hardcoded inline), captionFontFamily, and captionWordSeparator, all
defaulting to the current values.
Tested by rendering TalkingHead stills with Japanese captions before and
after (screenshots in PR), and with default props to confirm the
space-delimited rendering is unchanged. Used in production for Japanese
vertical ad videos.