* Build: use craft-media for bitstream, isobmff and matroska
filmcraft-bitstream, filmcraft-isobmff and filmcraft-matroska re-export
craft_media::{bitstream, isobmff, matroska} from craft-libs. The packages
and their public paths stay, so dependent crates build unchanged. The
integration tests in crates/isobmff/tests and crates/matroska/tests run
against the re-exports.
* Build: pin craft-media at 9db732e
craft-media spells the Matroska colour types in US English: Colour is
Color and VideoInfo::colour is VideoInfo::color. The spec names
element_ids::COLOUR, COLOUR_SPACE and VpcConfig::colour_primaries stay.
* matroska: the oracle skips the duration of a track's last frame
Ported from craft-media: the last audio or video frame of a track cut by
-t is shorter than a codec frame. ffmpeg 8 writes a shortened
BlockDuration and reports the codec frame duration; a remux by our writer
stores DefaultDuration and ffprobe reports the shortened one. Subtitle
cue durations are still compared. This fixes oracle::mjpeg_ac3 with
ffmpeg 8. The HDR test is named hevc_hdr_color, as in craft-media.
* matroska: laced_remux laces every audio track and checks timestamps
craft-media laces only tracks with a DefaultDuration. The remux gives
each audio track without one its mean packet spacing, so Vorbis, FLAC
and PCM lace too, and compares every packet's timestamp with the
source's. Ported from craft-media's oracle_matroska.rs.
* codecs: the Matroska audio timing test laces its Xiph case
craft-media laces only tracks with a DefaultDuration. The test's
Matroska helper sets the AAC access unit's duration (1024 samples,
rounded to the nanosecond), and the test checks that the Xiph-laced file
has laced blocks and the unlaced one has none.
* Build: repin craft-media to f97c84d27 (NAL length-size checks)
craft-libs#245 is on storytold/craft-libs main, so the dependency points there.
* Build: repin craft-media to f0ad6c1e1 (QuickTime v2 PCM entry limits)
* Tests: one integration-test binary per crate
Each crate with several tests/*.rs files keeps them as modules of tests/it/,
so cargo links one test executable per crate instead of one per file. Files
that change process-wide state or time a workload stay binaries of their own.
* Profiles: line tables for workspace crates, no debug info for dependencies
* Windows ARM64: nightly and on release pushes instead of every push to main
* xtask ci: clippy and tests in the dev profile
The release profile's thin LTO and single codegen unit made linking the test
binaries take most of the gate. The codec and audio crates stay at opt-level 3
in the dev profile, so their tests run at release speed.
* xtask ci: nextest, doctests, no examples, one wasm check
Tests run with cargo nextest when it is installed (cargo test otherwise) and
build with --lib --bins --tests, so examples aren't linked; doctests run
separately. The wasm check keeps going past the first failing crate. VP8
fixture generation locks across processes, since nextest runs each test in
its own.
* VP9 synthetic fixtures: write aside and rename
A test in another process could read a fixture ffmpeg was still writing.
* Fuzz tests: a third of each sweep by default, all of it with FILMCRAFT_FUZZ_ITERS
Every seed, mutation kind and thread count still runs. Header bytes each get
one overwrite value and two bit flips in rotation, so every value and bit
index still occurs across the region. FILMCRAFT_FUZZ_ITERS=N runs the whole
sweep plus N random rounds per seed, as before.
* Platform fallback tests: one failure point per hybrid path
The failure-anywhere test injected a failure at four points around every
restart point; the hybrid's paths differ only in the first GOP, around the
second and last restart points, at the end and never. The replay test needs
the first two GOPs.
* Export tests: shorter ranges with the same checks
The bitrate test measures 0.5–1.5 s, where each preset reaches its own
target (on 0–3 s the simple demo left two presets at the same rate). The
every-preset test exports three frames per preset.
* Audio tests: effects in parallel, a table-driven FFT for the Opus quality metric
The extreme-settings test keeps both settings and the 40 s tail for every
effect, one thread each. The Opus quality metric's FFT builds its twiddles
once and allocates nothing per transform (same algorithm and summation order).
* CI: dev-profile gate with nextest
xtask ci no longer builds with --release, so the release-profile overrides
go; nextest is installed for the test step.
* CI: apt packages through the cached install-apt-packages action
* Review fixes: fuzz tests with a panic hook in their own binaries, loud fixture lock
The fuzz tests of av1, h264, hevc, mpeg2v, vp8 and vp9 count panics through
a process-wide hook, so a panic in another test of the same process would
fail them; they are binaries of their own again. The VP8 fixture lock fails
the test instead of skipping it silently, and the full-sweep doc comments say
how many truncation points the full run samples.
* Fuzz tests: rotate the default subset per run (FILMCRAFT_FUZZ_SEED replays one)
* platform tests: decode-switch tests in their own binaries, one NVENC lock
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days / days_from_civil copies (app log
timestamp, Media Browser date column, auto-save file-name stamp, MXF header
timestamp, golden-image attribution date) with chrono as a workspace
dependency. The existing chrono entry on apps/filmcraft (clock feature) was
lifted to the workspace. Output formats are unchanged and pinned by tests.
AGENTS.md now says time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed
chrono's From<SystemTime> unwraps internally, so a clock past year 262143
panicked. logging::timestamp and golden::today now read the duration since the
epoch and go through the fallible DateTime::from_timestamp, reading an
out-of-range or pre-epoch clock as the epoch as the old saturating code did.
timestamp, today and media_browser::format_date write the year with {:04}
rather than %Y, which prefixes a '+' past year 9999.
Tests cover the far-future and year-10000 cases for timestamp and format_date.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* time: preserve out-of-range years instead of the epoch (FredApps review)
`DateTime::from_timestamp(..).unwrap_or_default()` changed the existing output:
inputs the old hand-rolled civil-from-days converted (e.g. `10_000_000_000_000`
s → 318857-05-20 17:46:40) collapsed to `1970-01-01 00:00:00`. Restore the old
conversion domain with a cycle-based helper: reduce by whole 400-year Gregorian
cycles (146_097 days), convert the in-range remainder with chrono, and carry the
year back as i128 (saturating to the output type). Only the year leaves chrono's
range; month, day and time of day are always exact.
Sites (all UTC):
- apps/filmcraft logging.rs `timestamp` (SystemTime, u64 secs): pre-1970 still
clamps to the epoch; a far-future clock keeps its year. u64::MAX/2 s →
"292277026596-12-04T15:30:07.000Z", 10e12 s → "318857-05-20T17:46:40.000Z"
(was "1970-01-01..."). SystemTime cases built with a checked add (Windows-safe).
- crates/engine media_browser.rs `format_date` (u64): 10e12 s → "318857-05-20
17:46", u64::MAX → "584554051223-11-09 07:00" (was "1970-01-01 00:00").
- crates/format autosave.rs `civil` (i64 + i32 offset): keeps years, e.g.
i64::MAX → 292277026596-12-04 15:30:07, i64::MIN → -292277022657-01-27. The
separate `unix + offset_secs` overflow guard (checked_add) is kept: only an
overflowing shift (old code wrapped it to garbage) reads as the epoch.
- crates/mxf from_unix (i64): keeps month/day/time, clamps only `year` to the
u16 field as before. 10e12 s → year 65535, May 20 17:46:40; -10e12 s → year 0,
Aug 13 06:13:20.
- crates/testkit golden.rs `today` (u64): same helper; pinned via `utc_parts`.
chrono stays the only calendar logic (NaiveDate::from_num_days_from_ce_opt on the
in-cycle remainder). No new chrono features; still default-features = false, std.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
craft-libs#169 is merged, so the fork pin is no longer needed. Pinned to
storytold/craft-libs 82a24b006eefa036b43c09a67fb9a3fbd17ffe63 (main after the merge) rather than the older 12281d07,
which predates resolve_absolute and the Windows colon refusal.
Media and proxy paths read from a project file, a project template or an
interchange file are opened only when they name a local file or a file on
the network share the document itself is on (craft-docpath); a relative
path is taken from the document's folder. Other paths count as missing:
the item renders the offline slate with the reason, the open says how many
were not opened, and Link Media lists them. This covers the open-time
scan, rendering and proxies, the growing-files look and interchange
import. Files imported or linked in this session are opened as given.
The Scratch Disks folders use the same check, and Captured Audio and Video
(voice-over takes, pasted images, the default proxy folder) and Audio
Previews follow the rule Auto Save and Video Previews already follow.
The shared release workflow installs trunk, but FilmCraft's web build
runs the wasm-bindgen CLI directly (cargo xtask web), so the 0.7.1 Web
job failed with "wasm-bindgen CLI not found". In CI, package.sh now
installs the CLI at xtask's WASM_BINDGEN version, as the per-app
release.yml did before the move to craft-common-actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: camera RAW is at 0% (docs/codec-parity.md, #383). CinemaDNG,
the one cinema RAW format with a public specification, could not be
read at all.
Fix: new L0 crate filmcraft-dng, written from the public DNG 1.4-1.7,
TIFF 6.0 / TIFF-EP, ITU-T T.81 and CinemaDNG specifications only (no
other decoder's source read; editions in the crate README):
- TIFF container with SubIFDs, bounded (64 IFDs, 4 levels, cycles end
the walk) and checked offsets; the main raw IFD is the largest
NewSubFileType 0 CFA / LinearRaw image.
- Raw data: 1-16-bit uncompressed (MSB-first packing) and lossless JPEG
(process 14, predictors 1-7, 1-4 components, point transform, line
restarts), strips and tiles, tiles decoded in parallel; allocations
capped by size and by the data present.
- Processing per the DNG model: ActiveArea, LinearizationTable, per-site
black levels with deltas, white levels, as-shot white balance with
neutral highlight clipping, Malvar-He-Cutler Bayer demosaic (bilinear
for other patterns), DefaultCrop, ColorMatrix / CameraCalibration /
ForwardMatrix with illuminant interpolation, Bradford to linear
Rec. 709, BaselineExposure; output scene-linear RGBA floats.
- CinemaDNG FrameRate, TimeCodes and ReelName tags.
- A test-only writer (feature `writer`) synthesises DNGs. Tests: raw
samples bit-exact for every depth / predictor / layout, processing
against hand-computed values, hostile files (every truncation, 12k
mutations, cyclic IFDs, huge sizes, bad offsets), and LibRaw's
unprocessed_raw as an external oracle reading the same samples
(skipped when not installed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VP8 WebM showed black video: there was no VP8 decoder. New L0 crate
filmcraft-vp8, implemented from RFC 6386 sections 1-19 (tables extracted
from the RFC text, nothing from the attached reference decoder): every
coding tool, versions 0-3, hidden altref frames, size changes. Bit-exact
with libvpx on 62 libvpx VP8 test vectors, real-world FATE samples and
generated fixtures; never panics on corrupted input.
Wired into codecs for Matroska / WebM (V_VP8, including the alpha layer
of transparent WebM) and MP4 (vp08). VP8 colour is BT.601 (RFC 6386 9.2).
A hidden altref frame is a WebM block of its own with the time of the
frame that follows it: the VP8 time lookup now moves on to that shown
frame instead of repeating the previous one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep FLAC beside main's new AAC Audio format (21 formats, both audio-only
presets), use is_mp4_with / is_mp4_video in audio_codec() and the Audio
Format picker so MP4 (incl. AV1) offers AAC or FLAC, and add flacLevel to
main's export.media params.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- release.yml: new `linux-riscv64` job, cross-compiled on ubuntu-24.04 against
ports.ubuntu.com riscv64 libs (ported from GridCraft), tar.gz only, CLI
smoke-tested under QEMU. Installs libasound2-dev:riscv64 for cpal/ALSA; the
VA-API and NVDEC/NVENC paths are loaded with libloading at run time, so they
need no riscv64 libraries. The draft-release job now needs it, so its tarball
is uploaded and covered by SHA256SUMS.txt.
- packaging/linux/package.sh: CROSS_ARCH / CROSS_TARGET / CROSS_COMPILE /
EMULATOR support, as in GridCraft.
- README and docs/releasing.md list the riscv64 tarball.
- cargo xtask version set 0.6.0 (Cargo.toml, Cargo.lock); refresh contributors.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Problem: symphonia rejects every Layer II frame whose bit rate / mode
combination ISO/IEC 11172-3 does not allow (mono above 192 kbit/s, stereo
below 64 kbit/s). ffmpeg writes mono MP2 at 384 kbit/s by default, so such
tracks played as silence in MPEG-TS / PS, Matroska and raw .mp2 files.
Fix: new L0 crate filmcraft-mpa, a clean-room Layer I / II decoder written
from ISO/IEC 11172-3 and 13818-3 (all modes incl. joint stereo, MPEG-1/2/2.5
rates, the disallowed combinations decoded with the allocation table their
bit rate per channel selects). PacketDecoder::mpeg_audio uses it for layers
I and II (packets of several frames too); raw .mp2 files decode with it,
resyncing after damage. Oracle tests vs ffmpeg: 73-84 dB SNR on ffmpeg /
twolame streams and synthetic Layer I / joint stereo frames; fuzzed damaged
frames never panic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No conflicts. main's new Chinese catalog requires every tl! string:
added zh-CN for the FLAC labels (format, preset, Audio Format entry,
Compression Level and its hint).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New L0 crate written from the AVI RIFF file reference and OpenDML 1.02:
avih and per-stream strh / strf (BITMAPINFOHEADER, WAVEFORMATEX(TENSIBLE))
/ strn, and chunk tables from the best index a file has: the OpenDML indx
super index and ix## standard indexes (field indexes too), else idx1
(offsets relative to movi or absolute) plus a scan of RIFF AVIX
extensions it cannot see, else a scan of the movi lists (key frames then
unknown). Frames are read on demand through a ByteSource.
Sizes are clamped to the file, so a cut-off recording opens with what
is there; rec lists nest at most 3 deep and tables are bounded by what
the file could hold. Tests: hand-built files for every index path,
truncation, and every single-byte mutation and truncation (no panic);
ffmpeg-made files against ffprobe; an ignored test on a 1.16 GB OpenDML
file with AVIX extensions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New L0 crate filmcraft-flac, a FLAC encoder written from RFC 9639:
constant / verbatim / fixed / LPC subframes (Levinson-Durbin on Tukey-
windowed autocorrelation, up to order 12), partitioned Rice residuals,
left/side, side/right and mid/side stereo, CRC-8/CRC-16, STREAMINFO with
MD5 (RFC 1321, own implementation), levels 0-8, all lossless.
Export:
- Format::Flac, an audio-only .flac (16/24-bit), streamed one second at
a time like WAV/AIFF; STREAMINFO is rewritten at the end with frame
sizes, total samples and MD5. Preset "FLAC 48 kHz 24-bit".
- AudioCodec::Flac in MP4 and QuickTime (fLaC + dfLa), one frame per
sample with the short last frame timed exactly; the dfLa STREAMINFO
is replaced before the moov is written (Mp4Writer::set_sample_entry).
MXF stays PCM.
- AudioSettings::flac_level (default 5); Export panel: FLAC (lossless)
audio format and Compression Level; file.exportMedia `format: "flac"`,
`audioCodec`, `flacLevel`. Spanish and Japanese strings.
ffmpeg oracle: every stream decodes bit-exactly with the STREAMINFO MD5
equal to ffmpeg's hash; .flac and FLAC-in-MP4/MOV exports decode to the
same samples as a WAV export of the range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Narrations take schema v14 (main's v13 is audio streams): v13_to_v14, docs updated.
- neural-voices is off by default in the desktop app, like whisper; the model download stays
an explicit, confirmed job.
- Removed the debug probe2 test module from tts-text.
- project_subset prunes narrations of items it drops.
- unique_wav_path is bounded (99 999 names) and returns an error instead of looping.
- narration.rs uses web_time::Instant (wasm clock test); tts.* commands get the
"Text to Speech" shortcut category, with es/ja rows for it and Edit Narration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New L2 crate filmcraft-tts-text: normalize(script, lang) -> Vec<Token> for the
Text to Speech text front end (P2a). English only (EnUs/EnGb), per the amended
brief. Handles cardinals to 999 999 999 999, negatives, decimals, ordinals,
money ($, €, £), percent, years (1100-2099), clock times, numeric and
abbreviated dates, phone-like digit groups, Mr./Mrs./Dr./St./Mt./vs./etc./e.g./
i.e./a.m./p.m., spelled acronyms, &/+@#/, and [pause Ns|ms] markers capped at
10 s. Pure Rust, deps serde + thiserror only, wasm-clean.
Never crashes (AGENTS.md §0): >1 MiB is NormalizeError::TooLong, oversized and
leading-zero numbers are read digit by digit, malformed pause markers become
plain text, all slicing is get()/boundary-safe, and a catch_unwind suite covers
empty/whitespace/oversized/10k-digit/$$$$/mixed-script/emoji/RTL/combining/NUL
inputs plus determinism.
Fix round (Claude review): a '.' after a number now ends the sentence (year
rule and money/decimal follow-through), so "in 1999." reads as a year with a
SentenceEnd and "$1,234,567." gets its final SentenceEnd; new table cases for
sentence-final numbers and the year rule under punctuation (., ,, !, ()); the
remaining direct indexes in non-test code converted to get()/first()/last().
Gates: fmt, clippy -p filmcraft-tts-text --all-targets -D warnings, tests
(25 suites, ~140 table cases), cargo xtask layers, cargo xtask wasm all green.
cargo xtask ci not run this round (Claude runs it when merging); it stops on a
pre-existing filmcraft-engine shortcuts test failure unrelated to this crate.
🤖 Written by Merlin (Hermes Agent, profile merlin)
Write what a narrator says, pick a voice, pitch and pace, and add it to
the timeline. Selecting a narration clip loads it back into the panel;
Save regenerates it in place as one undo step, keeping the clip's start
and length (longer speech kept whole in the file, shorter speech padded
with silence).
- filmcraft-tts (L2): Voice trait, voice catalogue, pause markers
([pause 1s]), and an original formant synthesizer (Basic Female/Male;
robotic placeholder until the neural voices land).
- project: Narration + Project::narrations (schema v13, no-op migration).
- engine: tts.voices/preview/create/edit/inspect; narrations never
overwrite clips (free targeted track, free track, else a new track).
- ui: Text to Speech panel beside Properties, preview playback,
click-to-edit, automation ids.
Gates: fmt, clippy -D warnings, layers, assets and wasm (rustup
toolchain) pass. Tests: all pass except three that fail identically on
main (61238bf) on this machine: shortcuts_tests::assign_reassigns_
conflicts_and_undoes, gpu fx_tests::gpu_effects_match_cpu_exactly, and
the frame pool test, which passes when run alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the Windows installer: the Start Menu shortcut and Add/Remove Programs icon were blank, and the shortcut description was wrong. Release builds no longer open a console window behind the app. Export can no longer panic (filmcraft-export never-crash).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New crates/platform (filmcraft-platform, L5): OS media FFI only. The one
crate allowed `unsafe` (own [lints] with unsafe_code = "deny",
#[allow(unsafe_code)] only on the videotoolbox module, SAFETY comment on
every block, safe Result-returning public API). AGENTS.md §0.3 names the
exception; docs/adr/0001-platform-ffi.md records why and the rules.
- VideoToolbox decoder for avcC / hvcC, 8/10-bit 4:2:0 and 4:2:2: session from
the sample entry's parameter sets with hardware required, asynchronous
decode (2 in flight), biplanar CVPixelBuffer copied to planar Yuv8/Yuv16,
reorder buffer of the stream's own depth, RASL skipped after a starting CRA.
Output callback under catch_unwind.
- HybridDecoder: transparent mid-stream fallback to the software decoder
(replays samples since the last restart point, drops already-returned
pictures). New codecs::software_video_decoder (built-in factories only) and
codecs::hw (switch, counters, NalStreamInfo shared with software decoders;
vui_color / sar_par helpers now used by both).
- Settings ▸ Playback ▸ Hardware decoding: Auto / Off (engine applies it to
the codecs switch); desktop app, CLI/MCP and bench call register().
- perf.stats decode.framesDecoded and decode.hardware counters.
- Tests: bit-exact parity with the software decoders (H.264 High, HEVC Main
open GOP, Main 10; resets, reseeks, flushes), forced-failure fallback,
mutation fuzz of samples and parameter sets, cross-platform fallback with a
stand-in decoder, setting Off, HEVC 4:2:2 vs ffmpeg.
- Bench --hw auto|off; results in docs/performance.md (4K CPU/frame
~86-119 ms -> ~4 ms).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AGENTS.md: a new §0 "Never crash" at the top of the rules (why, outranks feature work, the bans,
Result/? patterns, hostile input numbers, bounded recursion, lock poisoning, the last-resort
guard, a regression test per crash fix), linking craftrules/standards/never-crash.md. §3.1 now
points to it.
- CLAUDE.md non-negotiables, CONTRIBUTING.md and docs/contributing.md: short Never crash notes
pointing to AGENTS.md §0.
- clippy.toml: allow unwrap/expect/panic/indexing in tests, so crates can deny the panicking lints
in production code without touching their tests.
- [workspace.lints.rust] unsafe_code = "forbid" (no unsafe or allow(unsafe_code) in the workspace).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>