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>
Since the release workflow moved to storytold/craft-common-actions, it has
passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads
values from the release environment, but only for names the caller lists, so
the AZURE_* names that Trusted Signing uses came through empty and Windows
builds would ship unsigned. Same fix as storytold/craft-launcher#174.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
sequence.applyVideoTransition {edge: "out"} with a clip selected put
transitions on both free edges (speed_duration_moves_fade_out_to_the_new_clip_end
failed); with edge given only that side is used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The only part of #37 not already on main: FxStage::new runs inside a
validation error scope, so a device where Rgba32Float is not renderable
(software Vulkan/GL) logs and renders effects on the CPU instead of panicking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The probe only logged and always returned None, opened a VA display on every
H.264 Auto export, and made VA-API decode fail to load where libva lacks the
new symbol. A real VA-API encoder can come in its own PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
de, ru and uk hold only menu labels (their tests check every entry is one);
Indonesian mirrors the Spanish catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Other formats' start and priming (AAC edit lists, …) are handled and
oracle-tested elsewhere; enable_gapless for them could move their audio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The filter costs radius² per pixel; with only the image size as a cap, a
project file's radius could make one 4K frame take hours.
Regression test: median_radius_stays_in_the_parameter_range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tiny header naming ~1000 channels on a 64M-pixel line sized each chunk
buffer at hundreds of GB and Vec::with_capacity aborted the process.
Chunks over 256 MB are refused, and the RLE / zlib buffers reserve with
try_reserve_exact. Regression test: huge_scan_lines_are_refused_before_allocating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#608 drops any timeline drag on Escape before the release runs, but the
live box from #1028 had already changed the selection, and only the
release put it back (timeline_marquee_ui escape_cancels_the_box...).
Escape now restores the Marquee's starting selection when it drops it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #1109 a press on the ruler moves the playhead at once, so the press
already opens the scrub stream. The drag start then calls stop(), which
stopped the audio device but left scrub.open set, so later grains never
restarted it (meters_ui the_meters_move_while_scrubbing: no output).
stop() now marks the scrub stream closed with the device.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pen_overlay read resp.drag_started_by() inside ui.data_mut(), which holds
the context's write lock that drag_started_by needs. A Shift-drag that
placed a point hung for egui's 10 s lock timeout, then panicked
(shift_constrains_pen_mask_segments_to_45_degrees got 3 points, not 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test ran egui frames without a renderer and dropped the output's
textures delta, which epaint 0.36 turns into a panic. The global panic hook
then failed crash::tests::hook_records_and_logs_worker_panics too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#954 let fraction operands run to any cardinal length, so the "7/2026"
left after an impossible date's "13/" read "seven over two thousand
twenty-six" instead of a slash and the year (normalize::tests::dates).
Three digits covers every case #954 added (7/100, 100/7).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
About FilmCraft, System Compatibility Report, Sequence Settings and the
other modal dialogs were placed with `Window::anchor(CENTER_CENTER)`,
and egui makes anchored windows immovable, so dragging the title bar did
nothing. They now open at the centre of the app window through a
centre pivot and `default_pos` (`widgets::dialog_center`, or the
`CenteredDialog::centered` shorthand on one-line chains) and can be
moved like any other window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rate_stretch_items skips clips on locked tracks and refuses the edit when
every clip is locked, which is what the command loop on main did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split from #728 on current main. Full validation is recorded in the draft PR; the trim and mask-modifier failures reproduce on unchanged main.
Co-Authored-By: Codex <noreply@openai.com>
#1048 renamed two option labels after the Indonesian catalog (#878) was
written, so indonesian_covers_the_menus_and_has_no_dead_entries failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Quick Export path field was refilled from the stored path every frame,
so typing into it was overwritten. It is now a file name field that keeps
what is typed while it has focus and applies each non-empty value; a typed
or pasted full path replaces the folder too.
Under it the folder is shown as an accent-coloured, underlined link with a
folder icon. Clicking it opens the save dialog in the current folder with
the current file name; a chosen path is applied and shown, cancelling
changes nothing, and Export writes there. A preset change now swaps only
the extension, so a chosen name is kept.
Export mode's File Name and Location fields had the same refill problem
when emptied and use the same draft handling.
The desktop save dialog hook accepts a full path and starts in its folder.
A number field took only a plain number; anything else was thrown away.
Now every number field takes a calculation and applies its result:
the blue drag values (widgets::hot_number, in Effect Controls,
Properties, Lumetri and the mixers) and the dialogs' and Settings'
number fields (egui DragValue, through the new widgets::number_drag).
- New calc module: + - * / (and the multiplication and division signs),
brackets, decimals and signs; a unit (letters, %, degrees) and spaces
are ignored, but letters between digits (1e3, 10x20) are refused
rather than dropped. A calculation starting with * or / applies to
the field's current value; + and - at the start are signs. Bad input
gives nothing, never a panic: the text is capped at 256 characters,
nesting at 64, and only finite results come back.
- hot_number opens with its value selected, so a typed *2 applies to
it; End then /3 adds to it.
- The transition duration field reads timecode first (1.00 is a
second) and takes a calculation in frames when the text is neither a
frame count nor a timecode.
Tests: calc (calculations, units, a leading * or /, bad input);
mixer_ui (the clip mixer's dB value and the Audio Gain dialog take
calculations).
Since UI.6 (#881) control gestures set the modifiers of every frame they
run, so the Shift that the test pushed by hand before plain ui.click calls
was replaced and the #862 constraint test failed on main. The clicks now
pass modifiers {shift: true}, like the drag already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebasing on main's undo coalescing (#887): its run() reads the press memory once before the loop, so every parameter written in the first frame of a press began its own step, and a Program monitor handle that changes position and anchor point together took two. Only the first write of a frame begins the step now. The keyframe menu tests collapse the built-in effects by clicking their headers, which selects the effect since the handles change; they click the twirl arrow.
Follow-ups to the transform handles from trying them by hand:
- Only the left button drags on the Program monitor. A middle or right drag
moved the transform box; the same held for the mask body, vertices,
Bezier, feather and expansion handles and the pen, the Essential Graphics
layer boxes and the monitor guides and rulers, which now take the left
button only too.
- Anywhere just outside the box rotates, beside an edge as beside a corner,
as in Premiere. The rotate pointer is a curved double arrow that faces
away from the box wherever the pointer is around it. A click a little
further out (beyond the rotate zone) still lets go of the box.
- The anchor point is Premiere's circled cross (radius 7, the cross inside
the circle, thin lines crossing at the exact point) and takes hold within
11 points. Over it the pointer is the plain arrow with a small anchor
point beside it, so it reads apart from the move pointer over the picture.
- Effect Controls: a hovered or selected effect header (and mask row) is
highlighted from one point in, so it no longer cuts through the focused
panel's outline (effect_controls::row_fill).
Tests: transform_handles_ui gains only_the_left_button_drags_the_handles,
guides_masks_and_graphics_take_the_left_button_only and
rotating_from_beside_an_edge.
Selecting Motion in Effect Controls (its header or one of its properties) draws a box
around the clip's picture in the Program monitor, with eight handles and the anchor
point. Double-clicking a clip's picture selects its Motion (and the clip). The Transform
effect gets the same box for its own properties while it is the selected effect.
- Drag inside the box: Position. Drag a handle: Scale about the anchor point; with
Uniform Scale off a side handle changes only the width and a top/bottom handle only
the height (Motion: scale / scale_width; Transform: scale_height / scale_width).
Drag just outside a corner: Rotation. Drag the anchor point: Anchor Point, with
Position changing by the same amount so the picture stays put.
- Values go through effects.setParam at the playhead on every frame of the drag, with
Effect Controls' merge/begin handling (effect_controls::run, #201): a stopwatch-on
property gets a keyframe there, and one drag is one undo step even when it changes
two parameters.
- The handles are drawn a little smaller than the Essential Graphics ones (5/4 pt
squares, finer anchor marker) but grab within 8 pt; there is no system rotate
cursor, so a code-drawn one replaces it over the rotate zone.
- Clicking an effect header in Effect Controls now selects the effect instead of
twirling it; only the arrow twirls (new id effectControls.effect.<fx>.twirl). The
selected header is highlighted like a selected mask row.
Engine: EditorState.selected_effect and the effects.select command (journal off, like
masks.select). The selection is the effect id plus which instance of it, not its index,
because applying an effect inserts it first. Selecting an effect clears the mask
selection and selecting or creating a mask clears the effect selection; undo, redo and
effects.remove drop or follow it. effects.setParam's merge also takes a key, so a
gesture that changes several parameters is one undo step.
Automation ids: program.transform.box, program.transform.handle.<0-7>,
program.transform.rotate.<0-3>, program.transform.anchor.
Tests: transform_handles_ui (select, move, corner and edge scale, rotate, anchor,
keyframe at the playhead, one undo step per drag, header selects / arrow twirls,
Transform effect, double-click selection); engine tests for effects.select and the
merge key. Tests that clicked headers to fold effects now click the arrow.
timeline.roll follows linked partners since #870, but Trim mode already
selects the linked audio edit point next to the video one and rolls every
selected point, so a linked cut moved twice (trim_mode_roll_ripple_and_toggle:
+10 frames instead of +5). timeline.roll takes an optional followLinks
(default true); Trim mode passes false.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply Video / Audio Transition worked only at an edit point under the
playhead. As in Premiere, with clips selected they now add the transition
at the selected clips' free edges (video or audio side only), and with an
edit point selected at that edge only, wherever the playhead is. Edges
that already have a transition keep it; one undo step per press; the
call's effect / frames / params / reverse apply to every edge. With
nothing of that kind selected, the playhead behaviour is unchanged.
Shares the edge collection of Apply Default Transitions to Selection (⇧D).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two more built-in presets follow Default in Settings > Labels > Label
Color Presets. Bright is 16 saturated hues spaced around the colour
wheel plus a gray, named Red to Gray. Roles names the labels after what
clips are (B-Roll, Footage, Dialogue, Graphics, Interview, Sequence,
Visual Effects, Bin, Titles, Music, Sound Effects, Voice-Over, Captions,
Ambience, Color Grade, Stock), ordered so the label defaults for new
video, movies, audio, stills, sequences, Dynamic Link items, bins and
captions land on the matching role. Like Default they can be chosen
and reverted to but not deleted, and an imported preset with one of
their names is renamed "<name> (imported)".
A Label Color Presets row under Label Defaults picks the 16 label names
and colours from a preset. The dropdown lists the built-in "Default"
(the labels FilmCraft ships with) and the presets in the Label Presets
folder beside the preferences, and reads "<name> (Modified)" once the
labels differ from the chosen preset. Icon buttons: Revert (back to the
preset's colours and names), Import (copies a preset file into the
folder and selects it), Export (saves the dialog's labels to a file
named as the preset), Show Folder, and Delete (user presets only).
Choosing a preset or reverting applies on OK like the other fields;
Cancel discards it.
Presets are small JSON files: a name and 16 labels, each a name and a
labels, names trimmed and at most 64 characters, valid colours); a bad
file shows its error in the dialog and changes nothing. The same is
reachable as labels.presets, labels.importPreset, labels.exportPreset
and labels.deletePreset, with the selection stored as labels.preset.
About FilmCraft and Join the ArtCraft Discord… in the macOS app menu take
the translated labels of the same Help menu commands and are updated on
a language switch. Hide, Hide Others and Quit get their text from the
catalogs instead of muda's English defaults and keep their native
actions. menus::NATIVE_APP_MENU lists these native-only labels together
with the Settings submenu, so the app and the tests use the same keys.
German rows for the three new keys; "Quit FilmCraft" is already
translated in es, ja, zh-cn and fr (the app.quit shortcut), the other
languages show English until they have translations. The German menu
test accepts and requires the native app menu labels.
Closes#1145
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timeline.place kept a sub-frame start and rounded the duration to whole
frames, so the clip ran past time + duration and the sequence ended
between frames. The start and the end now each snap to the nearest frame,
as a timeline drag already does. sequence.inspect's durationFrames now
comes from the same frame_span export uses, so the two always agree.
Fixes#1101
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interface had no face for Arabic or Hebrew, so caption text, track and media names showed
missing-glyph boxes in the Text panel and on the timeline while the Program monitor drew them.
crates/ui-egui/src/rtl.rs adds the craft-fonts Arab / Hebr faces, else one installed face per
script (Noto Sans Arabic, SF Arabic, Segoe UI...), as the last fallbacks of every UI font family,
as cjk.rs does for Chinese.
egui already shapes each font run with harfrust, so the letters join, but it lays runs out left to
right with no bidi pass and multi-word Arabic read backwards. rtl::Lines lays captions out in
reading order: lines are broken on the logical text as egui wraps it, each line is reordered with
unicode-bidi (UAX #9, as crates/text does for titles) and every right-to-left word is its own
layout section for egui's shaper. Caption blocks on the timeline and the Text panel caption list
use it; a caption being edited keeps egui's logical order, since TextEdit places the caret by
character index. Left-to-right text is laid out exactly as before.
The macOS menu bar built every item enabled and its updater never applied
enablement, so Edit > Undo and Redo stayed selectable with empty history.
Items are now created with their computed state, and the frame loop sends
the menu tree to the native menu whenever it changes; the updater applies
only the labels, shortcuts and enablement that differ.
Fixes#902
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exports and rendered frames now record which offline items (missing,
made offline, unreadable) they drew the placeholder slate for. The engine
reports them as `offlineMedia` in file.exportMedia (wait), jobs.list
export entries and export.queue items; the bridge and MCP long exports
pass it through. filmcraft-cli export and render name each item on
stderr and exit 1, unless --allow-offline accepts the placeholders.
Fixes#1054
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TTML/DFXP reader ignored begin/end/dur on <body>, so a body begin
offset was lost and captions landed early. Body and div are now treated
as par time containers: their begins offset descendants, their ends
clip them, paragraphs without an end last until the container ends,
and end + dur take the earlier. seq containers warn instead of being
silently mistimed.
Fixes#895
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key put inside `settings` that ExportSettings doesn't have did nothing
and the export ran as if it were absent: `filmcraft-cli export --settings
'{"loudnessLufs":-14}'` (a flat parameter in the wrong place) exported at
-18.3 LUFS with no warning, and a misspelt nested key was dropped the same
way.
settings_from_params now compares the patch with the resolved settings and
refuses the first key they don't have, matching keys as `merge` does (the
camelCase spelling of a snake_case field, #531, counts):
`settings: unknown key `loudnessLufs` (see export.presets.get for the settings' keys)`.
A null value is accepted anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
effects.setParam clamped an explicit `time` into the clip, so a keyframe
past the end replaced one sitting on the last frame and a ramp across a
cut was compressed to finish at the clip end. The same happened before
the start. An explicit time now keeps its keyframe where it was asked,
outside the clip if need be, as effects.moveKeyframe already allows; only
the playhead fallback stays pinned to the clip.
Fixes#1055
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Timeline followed the playhead only while playing, so a jump
while stopped (Home, End, edit points, markers) left the playhead off screen.
Fix: when the playhead of the shown sequence changes while stopped and falls
outside the view, the view scrolls to it with the page-scroll margin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Shift-click in the Project panel toggled one item like Ctrl/Cmd-click; the items between were never selected and there was no anchor.
Fix: remember the last item clicked without Shift as the anchor (ProjectPanelUi::anchor); Shift-click selects the range anchor..clicked in the order the view shows (List, Icon, Freeform, search filter respected), Ctrl/Cmd+Shift adds it to the selection. Ctrl/Cmd-click still toggles. Pure helper click_selection with unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci: refactor workflows to use reusable artcraft-common-actions
* ci: point reusable workflows to storytold/craft-common-actions
* ci: pin storytold/craft-common-actions to commit 27a978f and pass secrets explicitly
* ci: use @main (unpinned) for craft-common-actions with explanation comment
Premiere writes a stereo clip to FCP7 XML as two mono clip items on two
tracks (sourcetrack trackindex 1 and 2). The importer already picks
channel 0 and channel 1 for them, but left both centred, and a centred
one-channel clip plays on both sides. So both channels summed on L and
on R: +3 dB per channel for an uncorrelated stereo effect (and its L-R
doubled), +6 dB for dual-mono dialogue.
Import: a clip that plays one channel of a split two-channel file is
panned to that channel's side (channel 0 left, channel 1 right). An
Audio Pan filter on the clip item still overrides it.
Export: a one-channel clip always writes its Audio Pan, also when it is
centred, so a centred Breakout to Mono round-trips centred.
Measured with filmcraft-cli (import, export WAV): a split stereo file
and a split dual-mono file now come out at 0.00 dB and sample-identical
to the source (before: +3.01 dB and +6.02 dB per channel); a mono file
on a mono track was and stays at 0 dB.
Tests (crates/interchange/tests/fcp7.rs):
split_stereo_halves_are_panned_to_their_side and
split_stereo_half_keeps_its_audio_pan fail without the import change;
centred_mono_breakout_round_trips_centred fails without the export
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HrAqZmrrDLRpxBJatxvaJ
German (de) landed in #630: the localization parity table and the
ROADMAP language table now list it as menus only, 371 of 3,498 strings
(10.6%), native-reviewed. The localization dimension counts 2
menus-only and 5 none, and 5 of Premiere's 10 languages well and 2
partly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: .dng files were not importable, and a DNG (a TIFF) would have
gone to the TIFF decoder, which reads at best the embedded preview. A
CinemaDNG shot (a folder of numbered DNG frames) could not be edited.
Fix: "dng" joins the importable still extensions. filmcraft-media
develops DNGs with filmcraft-dng into scene-linear Rec. 709 float
frames (transfer Linear, like OpenEXR), so highlights above 1.0 reach
the compositor. A single DNG is a still; a numbered folder imports
through the existing image-sequence path as one clip, whose frame rate
and start time code come from the first frame's CinemaDNG FrameRate and
TimeCodes tags (else Settings > Media > Indeterminate Media Timebase).
Lossy and JPEG XL DNGs are refused as unsupported by name.
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>
Problem: the interpolation filters took their taps as an array of T
slices built per block row with `array::from_fn`. The array went through
the stack and was read back with wider loads than it was written with
(store-forwarding stalls): about 10 % of the 8-tap luma interpolation
time, plus the per-tap Option handling.
Fix: the filters take the taps as a function (row shifted by k, or k-th
row from x), inlined into the multiply-add loop, so the tap addresses stay
in registers. Same arithmetic, same samples.
Tests: the interpolation unit tests (16-bit lanes against the 32-bit
filter, in-place against copied windows), conformance suite, identical
YUV on the five bench streams. User instructions -1 to -4 % on all five;
cycles -2 to -5 % except g250_500 (within the run-to-run noise of the
shared machine).
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>
AMD and Intel GPUs expose VA-API EncSlice, not NVENC. The export path
still opened NVENC first and then waited forever on wgpu map_async
(radv). Probe EncSlice before NVENC, fall through to the software
encoder, and bound GPU readback waits to eight seconds so a wedged
device fails the GPU path instead of blocking the export.
Problem: x265 (by default) and other encoders set weighted_pred_flag in the
PPS and then signal the default weights (2^denom, offset 0) for most
slices. Every block of those slices took the scalar 32-bit explicit
weighting path (8-265 / 8-266) instead of the 16-bit-lane default one, and
lost the full-sample copy shortcut.
Fix: when every list a block predicts from has the default weight and a
zero offset for a component, the block uses the default weighted
prediction (8-262 / 8-263) and, for uni-prediction at full-sample
positions, the reference copy. With log2WD = denom + shift1 the weighted
sums are exactly the default ones multiplied by 2^denom, so the samples
are identical.
Tests: a unit test checks the identity for every block width, bit depth
and denominator, and explicit weighting against 8-265 / 8-266 with extreme
weights and offsets; the weighted-prediction conformance fixtures; identical
YUV on the five bench streams. Cycles: -5 % on 1080p x265 medium, -8 % on
the weighted-prediction stream, -3 % on 10-bit, neutral elsewhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: after each picture the decoder classified every 4x4 block for
DecodeStats with an if / else-if chain: about 130 000 unpredictable
branches per 1080p picture, 1.5-2 % of single-thread decoding time.
Fix: `block_counts` sums flag comparisons in one branch-free pass
(inter = coded - intra - skip), with the same results.
Tests: a unit test compares it with the if / else-if chain over every
flag combination; identical decoded YUV on the five bench streams.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: every prediction block copied its whole reference window (block
plus filter margin, w + 7 by h + 7 luma samples) into a 16-bit scratch
buffer before interpolating, one line at a time with a row lookup per line.
After the move to 8-bit storage this copy was the largest single cost of
motion compensation (about 10 % of decoding time on 1080p streams with
much motion), and windows crossing the left or right picture edge were
copied sample by sample with a clamp each.
Fix: when the window lies inside the picture horizontally (almost always),
`Frame::window_rows` hands out the reference lines themselves (lines above
or below the picture replicated by clamping the line index, 8-228) and
the filters read the 8- or 16-bit samples directly: horizontal taps are the
line shifted by k, vertical taps the k-th line. Only windows crossing the
left or right edge are still copied, now as edge fill + run copy + edge
fill. The arithmetic is unchanged, so the prediction is identical.
Tests: in-place interpolation equals interpolation of the copied window
for random fractional vectors, block sizes, both chroma planes and both
sample types (2000 blocks each, inside and across every edge); edge
replicated lines equal the per-sample clamp for every offset; conformance
suite and identical YUV on the five bench streams at 1 and 3 threads.
User instructions on 1080p: -9 % (g250_500), -2 to -6 % elsewhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: every picture was stored with 16-bit samples whatever its bit depth.
For 8-bit streams (almost all Main content) that doubled the memory traffic
of the hottest paths: motion-compensation reference windows and full-sample
copies, deblocking and SAO, the per-picture planes and published rows, and
the output copy, which also had to narrow every sample to u8.
Fix: a `Sample` trait (u8, u16) and `Frame` rows stored in the picture's
type (`Rows::U8` when luma and chroma are 8-bit, `Rows::U16` otherwise).
`PicState`, `SliceDecoder`, the interpolation / weighting stores, intra
prediction and the in-loop filters are generic over the sample type and
monomorphised once per type; the decoder picks the type per picture from
its SPS. All arithmetic stays in the same integer types as before, so the
output is unchanged sample for sample. 8-bit output rows are now copied as
they are. Reading a frame with the other sample type (never done: the DPB
is flushed when the bit depth changes) yields nothing instead of a panic,
and the non-threaded build no longer has an `expect` on unpublished rows.
Tests: new unit tests for the storage type, exact crops of both types and
harmless wrong-type access; the full-sample copy test now runs on u8 and
u16 frames; the ffmpeg conformance suite (with and without the threads
feature); identical decoded YUV on five 1080p/720p streams (8-bit, 10-bit,
weighted prediction) at 1 and 3 threads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the software HEVC decoder was 2.7-4x slower per core than ffmpeg
on 1080p Main. Motion compensation was about half of the profile: the
weighted-prediction stores clipped in emulated 32-bit SSE2 lanes, every
block line of a reference window was a libc memmove call, and full-sample
uni-prediction went through window copy, shift and store. Deblocking
divided per edge to find the slice of each block.
Fix (output unchanged, sample for sample):
- put_uni / put_bi in 16-bit lanes (prediction samples are 14-bit; bi uses
the exact halved-sum identity), dispatched on constant block widths.
- 8-bit first interpolation passes accumulate in 16-bit lanes (the
filtered value stays within -24*255..88*255).
- Full-sample uni-prediction without weighting copies the reference
samples straight into the picture (Frame::copy_block), and block lines
are copied as fixed-size arrays instead of memmove calls.
- Deblocking takes the slices from edge_params instead of dividing per
edge, returns bS 0 for identical motion in one slice, and filters luma
edges on a gathered 4x8 block.
- 8-bit output planes are copied to u8 directly.
Tests: equivalence against the 32-bit formulas and the previous filter
(new unit tests), the ffmpeg conformance suite, and identical decoded YUV
on five 1080p/720p streams (8-bit, 10-bit, weighted prediction).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pressing Escape during an active drag in the Timeline cancels the operation without committing changes on mouse release. For ruler scrub, restores the playhead to its starting time.
Clear used to strip every track item that referenced a deleted Project file. Those clips now stay on the sequence as Media Offline, and media.relink still finds the item. Unused media, bins, and generators are still removed.
Source marks only read `time` (ticks) and otherwise used the Source playhead,
so agents passing `frame:24` marked frame 0. Parse the same time formats as
Program, using the Source item rate.
The vertical layout path never emitted underline rectangles, so layer and
run underlines were a no-op. Place a tall strip to the right of each
underlined cell (vertical-rl) and compare horizontal vs vertical "A" at 60px.
Plain layout() underlines sat on the unshifted baseline while layout_rich()
runs already subtracted baseline_shift. Match the two so underline y tracks
the glyphs for a full-paragraph style.
"Create a bin of subclips from each detected cut point" and "Create a
clip marker at each detected cut point". Drops the old strings from the
catalogs.
The command and its dialog already existed in the Clip menu, but the
clip context menu, where people look for it, didn't offer it. It now sits
after Speed/Duration… and opens the same dialog.
Handle zero-softness Extract as a hard cutoff in both CPU and GPU paths.
The former 0.0001-wide substitute fade introduced grey edge pixels and
amplified transfer-function rounding beyond the existing parity tolerance.
Retain the positive-softness calculation and add cutoff, inversion and
premultiplied-alpha regressions without relaxing the comparison tolerance.
Constrain caption font automation targets to the visible part of each
row. A partly clipped row could previously expose a click point outside
the popup, leaving the selected font unchanged. Preserve the existing
font-loading behavior, assets and automation IDs.
A six-channel clip on a stereo track played only source channels 0 and 1,
so the centre (dialogue in film mixes), LFE and surrounds were dropped.
Fold it down with ITU-R BS.775, as the Mix and the Source Monitor already
do. A six-channel Modify > Audio Channels pick folds the same way; mono
and stereo picks keep playing exactly the picked channels.
The same rule as for drag-and-drop: Escape while the mouse button is held
takes the drag back and leaves everything as it was before the press.
- Value drags in panels (hot numbers, sliders and color pickers in Effect
Controls, Properties, Lumetri and elsewhere): the engine gets
Session::undo_mark / revert_to, and the app marks the history when a press
begins and reverts to the mark on Escape. revert_to drops the steps without
adding them to redo.
- Lumetri made one undo step per frame of a drag; it now goes through
effect_controls::run and merges them into one, as Effect Controls does
(Properties does since #522).
- Drags that commit on release (keyframes, influence handles, graph keys,
trim monitor, caption size, audio FX editor, monitor guides) skip the commit
when Escape ends them. UI-only sliders (Project and Media Browser thumbnail
size, Export settings) go back to their value at the press; a sequence tab
dragged out of the dock goes back home.
- Dialogs: Escape with a button held no longer closes the dialog
(widgets::escape_closes, also for the modal's own close). Dialogs with a
draft keep it at the press and put it back on Escape
(widgets::revert_drag_on_escape), color pickers included.
Tests: reverting_to_a_mark_takes_back_a_gesture_without_redo,
reverting_to_a_lost_mark_does_nothing; escape_drag_ui gains a
Properties value drag, the thumbnail-size slider and the Properties color
picker; clip_dialogs_ui and settings_ui check that a dialog field and a
Settings color go back while the dialog stays open.
egui ends a drag when Escape is pressed, and FilmCraft treated that stop as a
release: the drag was committed wherever the pointer was. Escape now abandons
it: nothing moves, nothing is dropped, nothing goes on the undo stack.
- Timeline: clip moves, trims, rolls, slips, slides and stretches, caption
blocks and automation keyframes. The clips stay put and stay selected.
- Items carried between panels (a clip or sequence from the Project panel,
Source monitor drags, effects, presets, graphics templates): the payload
is cleared in drag_ghost. panels::cancel_drag / drag_cancelled keep a
cancelled drag from restarting until the button comes up.
- Freeform card moves go back, and the next drag works.
- Media Browser drags, which import on drag start, take the import back even
when released over a panel.
Tests: escape_drag_ui (5 tests: Timeline clip, caption, Project panel item,
Freeform card, effect; each with the same drag without Escape as a control)
and escape_takes_back_a_media_browser_drag. Automation keyframes have no
test fixture. The ui-egui suite passes except the two Windows CRLF failures
fixed in #488.
On a Rec. 709 sequence every PQ, HLG, BT.2020 or P3 clip left the GPU
path: each frame was converted to a float image on the CPU (curve, HLG
OOTF, gamut matrix, BT.2390 tone mapping, desaturation) and resampled
there. A 2160x3840 10-bit HLG clip took 90 ms per frame (48 ms scaled to
fit a 1080x1920 sequence) against 33 ms at 30 fps; the same clip in SDR
took 18 ms.
The compositor's shader now runs that input transform itself:
InputTransform::gpu_params gives its stages as numbers (log curves and
camera-gamut compression stay on the CPU), PlanLayer::input carries the
transform for Y'CbCr layers on a plain pipeline, and execute_cpu applies
the same transform as the CPU reference. HDR clips now plan in 15-19 ms
per frame, and the GPU result is within one output code of the CPU's for
HLG, PQ (with and without a mastering peak) and BT.2020 SDR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ticks on the monitors' time ruler were spaced evenly across the
bar whatever the zoom, so zooming or scrolling left them looking
static. They now sit on whole frames, placed through the same mapping
as the playhead, markers and In/Out, with the step chosen from the
on-screen width of a frame as on the Timeline's ruler: minor ticks at
least 12 pt apart and major ticks at least 110 pt apart, the minor step
always dividing the major one. Zooming in spreads them out and fills in
finer steps; scrolling moves them with the time they mark.
Pressing on a grouped tool and dragging the pointer a few points in any
direction opens its flyout straight away, without waiting for the long
press. Letting go over a flyout row picks that tool and closes the
flyout; letting go anywhere else leaves it open, as after a long press.
Right-click and the 0.35 s long press work as before.
While the Program or Source monitor plays and its time ruler is zoomed
in, the zoom range follows the playhead the same way the Timeline does,
honouring Settings > Timeline > Timeline Playback Auto-Scrolling: page
scrolling moves the range so the playhead is near its left edge when it
runs off the right edge or jumps before the left one; smooth scrolling
keeps the playhead in the middle; no scrolling leaves the range alone.
The range keeps its width. Scrubbing, clicking and moving the playhead
while stopped never move it.
The Audio Meters panel now leaves out what doesn't fit instead of
drawing text over other text. As it narrows, the loudness readout drops
its LUFS/dBTP units, then the readout goes, then the dB scale goes so
the bars keep their room. As it gets shorter, the scale numbers thin out
to every 6, 12 or 24 dB and then go, and the channel letters and "dB"
under the bars go. At the default size it looks as before. The scale
numbers are listed under the automation id audioMeters.scale.
While the playhead is scrubbed with audio scrubbing on, the meters (and
the Audio Track Mixer's) show the grains being heard, as they do in
playback.
The clips a box touches are selected as it is dragged, and a clip that
leaves the box as it shrinks is deselected again, instead of the
selection only changing on release. Shift or Ctrl, held when the drag
begins, add to the selection the drag started with, as in the Project
panel. Escape cancels the box and puts that selection back. The
release still runs a single timeline.select, so the drag is one
selection change.
The Program and Source monitors get a zoom scroll bar under their time
ruler, drawn like the Timeline's: drag a handle at either end of the
thumb to zoom in or out (down to five frames), drag the middle to
scroll, or press beside the thumb to move it there. The ruler then
shows only that part: playhead, markers, In/Out handles and ticks are
mapped to it, clicks and scrubs set a time within it, and anything
outside it is not drawn. Each monitor keeps its own range in its view
state (time_zoom).
The transport buttons are now 28 pt square with larger icons, spaced
6 pt apart and centred; on a narrow monitor the spacing tightens before
buttons move to the overflow menu.
The top edge of the highest video track now resizes the video tracks,
like the lines between headers do (same cursor, drag up to grow). The
bottom edge of the last audio track can now also be grabbed from just
below the line, in the empty space under the tracks, so both outer
edges work the same way. Each edge has an automation id
(timeline.track.<V3>.resizeTop, timeline.track.<A1>.resize).
Scrolling over a panel group's tab strip makes the next tab active
(wheel down) or the previous one (wheel up), one tab per wheel notch.
Trackpad scrolling adds up until it reaches a notch, so a small swipe
moves one tab rather than several. It stops at the first and last tab
and does nothing over the panel's content. Works for docked and
floating groups, and steps through the Timeline's sequence tabs too.
Problem: the post stage of a VP9 frame (band assembly, loop filter,
publication) ran on one thread. Later frames wait per band for their
references, so the serial loop filter (~30 % of the profile) set the
pace of every inter frame; 16 threads were only ~1.8x one thread.
Fix: rows of a frame's loop filter run as a wavefront, row r two
superblocks behind row r - 1 (what the raster order of 8.8.1 allows),
taken by the post thread and helper jobs on the decoder's pool. No lock
is held while a superblock is filtered: each row filters its own band
and rows exchange copies of the 8 rows the filter reaches across a band
boundary. Output stays bit-identical; one thread / wasm run the rows in
order as before. A row is only taken by a running thread, so waits
always make progress, and helpers run under catch_unwind (a failure
publishes placeholder bands as before). New conformance fixture
narrow_tall (2 superblock columns) and a repeated multi-thread 1080p
decode test. 1.2-1.5x faster with 16 threads on a shared machine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mouse wheel over the Program and Source monitors now zooms about
10% per notch instead of 40%, exponential in the wheel travel, so one
notch in followed by one notch out returns to the same magnification.
Pinch zoom, the 10%-1600% range and zooming about the pointer are
unchanged.
file.open restores the saved sequence tabs and views when
timeline.restoreOpenSequences is on, but prefs.schema still marked the
preference wired:false ("stored, no effect yet").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vui_color took the VUI matrix, transfer and range but not the primaries,
so every decoded H.264 / HEVC frame reported BT.709 primaries. Stream info
read them, so a BT.2020 file without a colr box said BT.2020 in its info
and BT.709 on its frames (Rec709 instead of Rec2020 for the renderer).
Pass colour_primaries through, for the software and hardware decoders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the 20 strings the Japanese catalog was missing (caption track
deletion, panel docking, eyedropper, edit point trim types, transport
overflow, effect shutter angle), update the hardware decoding note to the
current English source (NVDEC), and fix house-style slips (half-width
parentheses, a space before パネル, 長さ for Duration, Replicate no longer
shares 複製 with Duplicate).
Add a test that requires a Japanese entry for every menu label, tl!
literal and dynamic name, as the Chinese catalog already has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without DefaultDuration the rate came from the median step between
neighbouring blocks in file order. Blocks are in decode order, so with
B-frames neighbours are two or more frames apart in presentation time and
a 25 fps clip reported 12.5. Step between the sorted presentation times
instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MP4 reader took the AudioSpecificConfig's channel configuration as a
count, but it is an index into ISO/IEC 14496-3 Table 1.19: configuration 7
(7.1) reported seven channels and dropped the last decoded one, and 12
(7.1 back, what ffmpeg 9 writes for 7.1) reported twelve. Count from the
table (with Amd 4: 11 → 7, 12 and 14 → 8, 13 → 24), and decode
configurations 11 and 12, reordered to the WAV/SMPTE order used for 5.1.
Checked against ffmpeg's decode channel by channel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Track Select Forward tool (A) a press selected every clip from the
pointer on, but a drag never started, so the clips could not be moved with
the tool as in Premiere Pro. A drag now starts a move of the selection, with
Option copying as with the Selection tool.
A press on a transition over a cut did nothing with these tools; it now
counts as its track, as the clips under it do.
Regression test: track_select_drag_ui (the drag test fails before the fix).
Review on #827: a start marker left pending by any early exit (force-quit,
hang, unrelated crash, two launches racing) moved the user down a rung for
good, with no way back but deleting a file.
- Each start writes its own attempt file (graphics-start/<pid>-<n>.json)
that is removed after two painted frames, on a normal exit, or 15 s after
launch whatever happened. A later launch only counts attempts that are
older than that window, so a hang, a later crash or force-quit, and an
instance that is still starting are never failures.
- graphics-start.json now holds {mode, failures}: two failed starts in a row
in the same mode move on to the next one; a start that paints resets the
count. A wgpu start-up error still moves on at once.
- All files are written to a per-process temp name and renamed into place,
so concurrent launches never see or leave half a file.
- A fallback mode shows a notice (graphics.notice) with Use Default Graphics
(graphics.notice.reset) and Close; es, fr, ja and zh-cn strings.
- --graphics default|dx12|software picks a mode by hand and keeps it;
`--graphics default` undoes a fallback. Modes this platform lacks are
refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ffmpeg's CLI handles Matroska priming differently by version: 9.x trims
the 481-sample CodecDelay, 2023-24 static builds keep the priming
samples, so comparing from sample 2 put the priming ramp against our
tone (max error = peak 0.125). The .mkv case now searches lags within
two samples of 0 (trimmed) and of the stream's own CodecDelay (kept),
over the interior of both decodes; the plain .mp2 file stays at lag 0.
Review nits: the ID3v2 skip offset uses saturating_add (test with the
largest tag size); every Layer II table's sblimit is checked <= 32 at
compile time and clamped to SUBBANDS before indexing the per-subband
arrays (row() is a match, so it cannot index out of range).
Verified with ffmpeg 9.0.2 and the static N-71064 (2024-06) build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Placement already limited overlap to half of each clip, but Cross Dissolve
still used the requested overlapFrames, so a 30-frame dissolve on two
20-frame clips started before the first clip. Apply the clamped per-cut
overlap as the transition length.
ffmpeg's CLI handles Matroska priming differently by version: 9.x trims
CodecDelay, 2023-24 static builds keep the priming samples (AAC 1024,
AC-3 / E-AC-3 256, MP3 1105), so a +-40-sample best-lag search against
its decode landed on a spurious minimum (4 or 24 samples).
matroska_audio_oracle now has ffmpeg encode a reference signal we
generate (low-passed xorshift noise) and asserts our decode lines up
with that signal to the sample, the stream's own CodecDelay trimmed:
version-independent, and it fails on the old ms-rounded trim (-16).
ffmpeg's decode is compared after a best-lag search bounded by the
CodecDelay; its lag is reported, the waveform must match (< 1e-3).
E-AC-3 joins the cases. eac3_oracle's .mkv case accepts lag 0 or the
256-sample priming; MP4 / MOV / TS stay strictly at lag 0.
Verified with ffmpeg 9.0.2 and the static N-71064 (2024-06) build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OK used to close the dialog even after file.newSequence failed, so
reopening reset the name and settings. Stay open on validation
errors so one field can be fixed.
Conflict in apps/filmcraft/src/main.rs: kept the graphics start loop and
app_creator, and carried over --control-input-only (#881) into them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace With Clip ▸ Match Frame used a forward-only offset, so a reversed
clip lined the Source monitor up with the wrong media time (source 50 at
sequence 20 became 69). Align source_in from reverse-aware source mapping.
Non-ripple clip.speedDuration shortened the clip but left one-sided
out-edge transitions at the old timeline range. Reposition them with
the same edge-follow used for a regular trim.
Selecting two clip edges and trimming by frames applied each edge as its own
timeline.trim, so Undo restored only the last clip. Collapse those edits (and
extend-to-playhead / apply-default-transition) into one history snapshot so
Undo and Redo cover every selected point.
A neighbour on only one track used to clamp that partner alone, so picture
and sound stayed linked at different rates. The tightest room now limits
the whole group.
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>
A selection that holds a bin beside items (one id space) made the whole drop
fail with "no such item"; only items are placed now.
Session::edit_group raised the history limit while a group ran and did not
restore it if the group panicked. It now restores it and rolls the group back
before the panic goes on.
The Browse panel's Install… button calls the module's route directly and drops its
result, while only menus.rs surfaced a refused file — so installing another
application's template package (an EffectCraft .ectemplate ZIP renamed to .fcgt,
the case in the report) looked like nothing happened. The engine has refused ZIP
archives with a clear message since M10.7; the route now puts that message on the
status bar itself, whichever caller arrives.
Headless: clicking Install with the picker pointed at a ZIP-named file requires
the status bar to name the ZIP archive; without the surfacing the status stays
empty — the reporter's exact symptom.
Timeline clipitem markers already round-tripped; source-media annotations on
master clips were omitted with an empty export report. Write those markers on
<clip> so re-import keeps them.
Fixesstorytold/filmcraft#900.
Export was overwriting Stack markers with the using clip's markers, so a nest with no instance markers lost its sequence markers, and instance markers were imported onto the nested sequence. Keep sequence markers on the Stack and store clip instance markers in metadata.filmcraft.clip.
FCPXML writes point markers as one frame at the sequence rate. Import
compared duration to 1 tick, so a 24fps one-frame marker became a
range. Treat duration ≤ one sequence frame as a FilmCraft point.
Use ds64.dataSize for an RF64 data-size sentinel instead of the rest of
the file, so chunks after the audio are no longer decoded as samples.
Fixes#925
Prevent concurrent fixture producers from replacing files held open by
external decoders. Add a four-consumer Windows sharing regression.
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Trim, roll, slip, slide, rate stretch and Speed/Duration now fail with
"track is locked" instead of changing clips on a locked track, matching
same-track move. Linked partners on locked tracks are left alone.
file.openDemoProject left Session.path pointing at the previous
.fcproj, so File ▸ Save overwrote that project with the demo.
Match New Project: drop the path (and the associated preview dir)
so Save requires Save As and the original file stays intact.
Startup scan used to delete an unlocked recovery session whenever
session metadata could not be parsed, even if snapshot.fcproj was
intact. Offer that snapshot for recovery instead of removing it.
APNG and animated GIF files opened as stills showing only their first
frame. They now open as movie clips (crates/media/src/animated.rs):
- every frame is decoded and composited onto the full canvas by the
image crate (APNG blend/dispose ops, GIF disposal), keeping alpha
- each frame lasts its own delay; GIF delays <= 10 ms play as 100 ms
and APNG delays < 10 ms as 10 ms, as browsers do
- consecutive identical frames are merged; the clip is one pass of the
animation and its timebase is the most common frame length
- hostile input: at most 10,000 frames / 1 GiB of decoded pictures; a
damaged frame ends the clip at the frames before it; files with one
frame (or none that decode) still open as stills, as before
Tests: media unit tests (timing, transparency, merging, single-frame
files stay stills, truncated/bit-flipped files never panic) and an
engine test that imports an animated GIF and reopens it from a saved
project.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkQ2PDrNFjs9Vm8T44tdjA
Carry #754's "new" track handling into place_one, and add it to items: the
first item adds the track and the rest of the list follows it onto that track.
A drag of Effect Controls / Properties numbers already used merge+begin, but
the press-start memory was only written on frames that emitted setParam.
egui drops unused temp data after a quiet frame, so a Scale scrub that
paused between percents began a new undo step for every 1%. Refresh the
drag session on idle frames while the button is down; still begin a new
step on the next press. Same keep-alive for graphics property drags.
file.reveal is enabled when a selected clip has a local path, greyed out
with no selection, and the OS opener reveals the file (xdg-open of the
parent folder on Linux).
Transport and program streams listed a single audio stream, so file.import
and New Sequence From Clip kept one clip. Probe every audio PID (capped),
decode each independently, and place linked clips per stream. MP4/MKV
already did this; MXF same-rate PCM is still interleaved as one stream.
A focused text field now owns every unmodified key, so an input method can
compose there. Vietnamese Telex builds its diacritics from plain letters
(`a` + `s` → `á`, `a` + `r` → `ả`, `a` + `j` → `ạ`), and the editor bound
`A` `B` `C` `H` `J` `K` `L` `N` `P` `R` `T` `U` `V` `Y` `Z` as single-key tool
shortcuts, which consumed those presses before any field saw them. No
diacritic could be typed (#243).
`egui_wants_keyboard_input` was not enough to guard this: it is read before the
pass lays the UI out, so it still reports the previous pass, and it is true for
any focused widget, so using it would leave the editor without its single-key
tool shortcuts while a slider has focus. `text_edit_focused` is the precise
check.
Chords with a modifier keep firing while a field is focused: an IME does not
consume Ctrl+S and friends. Esc (trim cancel) is handled before the guard and
is unaffected.
Tests: the four cases in `text_focus_shortcut_tests` fail without the guard
(`an_unmodified_key_reaches_a_focused_text_field`,
`the_telex_trigger_keys_are_left_to_the_field`) and pass with it.
Problem: the AV1 in-loop filters ran on one thread per frame (postfilter::apply
had an unused pool). On a single-tile stream they are a quarter to a third of a
frame, and since references are waited for as whole frames, that serial tail
sat on the critical path of every frame that references it.
Fix: the three filters run in bands of 64 luma rows on the decoder's existing
rayon pool, one join per filter per frame. CDEF and loop restoration write only
their own rows of a separate output. The loop filter's edges never touch the
same sample, and only a band's top horizontal edges reach into the band above:
each band refilters the vertical edges of a copy of the 8 rows above it, runs
those edges against the copy, and the samples they changed are written back.
With one thread the code path is the old one. Output stays bit-exact (1 vs 16
threads vs libdav1d; new 4:2:2 / 4:4:4 libaom fixtures). perf.rs gains a
low-delay 1080p and a single-tile 2160p fixture and the all-core stage
breakdown. 16 threads: 1080p GOP 1.16-1.30x, low delay 1.28x, 2160p 1.14x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A damaged file whose frames fail to decode or read again and again
rendered black with no explanation. In the monitors only, after 3
consecutive Decode/Io failures of the same item its failing frames show
the existing MEDIA UNREADABLE slate; a decoded frame clears it. Pending
(still-loading) reads never count, and exports never see it.
Problem: OpenEXR sequences (#806) were written uncompressed only, because the
workspace had no deflate encoder; the files are 2-4x larger than necessary and
Premiere offers an EXR compression choice.
Fix: a small clean-room DEFLATE (RFC 1951) encoder in a zlib (RFC 1950)
wrapper in filmcraft-export (greedy LZ77 with bounded hash chains; stored,
fixed or dynamic Huffman per block, whichever is smallest; length-limited
codes). The EXR writer gains RLE, ZIPS (1 line) and ZIP (16 lines) chunks with
the format's byte interleave + delta predictor; chunks that do not shrink are
stored raw. New ExportSettings::exr_compression (default None, as before),
`exrCompression` param on file.exportMedia, Compression combo in Export mode
(es/fr/ja/zh-cn strings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Indonesian, one of the 12 key languages in
docs/localization-parity.md, had no catalog (0%), so Indonesian users got
the interface in English and System Language never picked Indonesian.
Fix: add crates/ui-egui/src/i18n/id.tsv, clean-room translations of every
entry in the Spanish catalog plus the native macOS Settings menu (3,541),
with a glossary in its header. Language::Id ("Bahasa Indonesia") is
persisted as `id`; it appears in Edit > Language (also the native macOS
menu), Settings > General, and System Language picks it for `id-*` locales
and the legacy `in-*` code older Java/Android runtimes report. Tests cover
the core menus, reject entries without a Spanish counterpart, check glyph
coverage with the bundled fonts, switching/persistence and locale
matching. Attribution sidecar, ATTRIBUTION.md row, docs (gaps.md G14,
localization-parity, ROADMAP, ui-egui README) updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin GOP-cache eviction on the requested pts so a burst past the playhead
does not drop the target and walk the GOP again. Preroll only schedules
the frames that gate clock start. bench_playback no longer meters preroll
jobs as on-clock latency.
This is the seek stall from #848, not the ~3× per-core gap vs ffmpeg.
Problem: #802 gave Edit > Label a colour swatch per label, but the Label
submenus of the Project panel item menu and the Timeline clip menu still
listed bare built-in names: no colour, no custom names from Preferences >
Labels, and no automation ids.
Fix: a shared menus::label_choice draws each choice as Edit > Label does
(the user's name, a swatch of the user's colour in the left gutter) and
registers it (project.itemMenu.label.<Label>,
timeline.clipMenu.edit.label.<Label>); both context menus use it, and their
Label submenu buttons get automation ids too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the GIF exporter gave every frame the same delay of
1000·den/num ms, which the encoder truncates to whole centiseconds.
At 24 fps each 41.67 ms frame became 4 cs, so a 4.0 s sequence played
in 3.84 s (4 % fast); other non-integer rates drifted the same way.
Fix: each frame's delay now runs from its rounded start time to the
next frame's (gif_delay_cs), so delays alternate (4, 4, 5, 4, 4, 4 cs
at 24 fps) and add up to the exact duration. Checked i128 math also
replaces the unchecked `(1000 * den) as u32` cast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Portuguese (Brazil) catalog, one of Premiere's interface
languages and one of the 12 key languages in docs/gaps.md G14, covered
only the menus (348 of ~3,540 strings); panels, dialogs, Settings pages,
effect names, the Keyboard Shortcuts dialog and status messages showed
in English.
Fix: pt-br.tsv now has every entry Spanish has plus the native macOS
Settings item, written from the plain meaning of the English text with
ordinary Brazilian video-editing vocabulary (infinitive commands,
polite imperative instructions, sentence case); the header records the
conventions and a term glossary. Like Japanese and French, tests
require only the menus (now every menu label, not just the core set),
so later strings show in English until translated; a Portuguese entry
without a Spanish one fails, and a new test checks the bundled fonts
cover every Portuguese letter. The core-menu set that Ukrainian,
German, Russian and French must cover moves from pt-br.tsv (no longer
menus-only) to the menus-only Ukrainian catalog, unchanged in content
for those tests. Attribution sidecar and ATTRIBUTION.md entry added.
Docs: G14, localization-parity, ROADMAP language row, ui-egui README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timeline.roll follows linked partners at the same through-edit when linked
selection is on, so a +12 frame roll on V1 also moves A1. The command
reports linked:false when the cut is not followed as a pair. Reversed
clips keep the unmoved-edge media mapping (#526/#558).
Problem: program-monitor markers.add (and markers.addRange, addRangeInOut,
addChapter, addFlashCue) mapped `color` with Label::from_name and fell back
to Green on any unknown name, so `{color:"Red"}` succeeded and stored Green;
markers.edit silently ignored an unknown colour.
Fix: one marker_color_p helper turns a non-label `color` into a bad-params
error that lists the accepted label names; every marker command (and the
Source monitor path, which already rejected it) uses it. Absent or null
still means the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clips and markers before sequence start were accepted, then unreachable
in the GUI and cut off on export. Time, frame and seconds before 0 now
return a parameter error; zero remains valid.
Problem: the pen mask tool ignored Shift, so straight horizontal,
vertical or diagonal mask edges could not be drawn precisely (Premiere
26.0 constrains mask lines with Shift). Separately, path_from_json
accepted the `{"p","in","out"}` vertex form through serde first, which
silently dropped the `in`/`out` tangents, so a pen mask's dragged-out
curves were committed as corners.
Fix: with Shift held, a new pen point snaps onto the nearest 45° ray
from the previous point (in screen space), the rubber-band preview
follows, and a Shift-drag constrains the Bézier handle it pulls out.
path_from_json now parses the vertex forms before falling back to the
serialized MaskPath.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the desktop app picked the project among its launch files with a
case-sensitive ends_with(".fcproj"), so CUT.FCPROJ opened through a Windows
file association or the Linux MIME glob was imported as media. Relative
launch paths were stored as given and went missing from another folder.
std::env::args panicked on a non-Unicode argument. filmcraft-cli import
used canonicalize, which stores \\?\ verbatim paths on Windows.
Fix: args::project_and_media finds the project by a case-insensitive
extension and makes every launch path absolute with std::path::absolute
(no verbatim prefix, symlinks kept). Both apps read args_os; the desktop
app keeps --data-dir exactly. filmcraft-cli import uses std::path::absolute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: masks.roundCorners (#813) rounded rectangle/polygon mask corners only from the
command line, control channel or MCP; the Program monitor had no Premiere 26-style
corner-roundness handle.
Fix: a selected mask with sharp corners between straight edges shows a roundness handle
just inside its top-left corner (automation id program.mask.roundness). Dragging it into
the shape moves it along the corner bisector, previews the rounded outline live with the
radius, and on release dispatches one masks.roundCorners (one undo step per drag). The
radius is capped where no corner can grow further. Ellipses, pen masks and rounded corners
show no handle. Tooltip translated (es/fr/ja/zh-cn). New headless UI test
mask_roundness_ui; docs/gaps.md G18 updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the cpal error callback marked the stream failed on its first
error of any kind, so the UI stopped audio and played the rest of the
playback without sound. cpal's ALSA backend reports transient errors
(POLLERR after an xrun, spurious poll returns, short writes) that way
and keeps the stream alive; after POLLERR it no longer calls back.
Reports: a second or two of audio, then silence while meters move (#23).
Fix: AudioOut::recover, called every playback frame, lets CpalOut
restart the stream: at once when the device is gone, or when an error
was reported and no samples were consumed for 300 ms. The restarted
stream reuses the shared play-ahead mixer and played-frame clock and
must keep the sample rate and channel count; at most 3 restarts in
10 s, then playback falls back to the wall clock as before. Errors
while samples keep flowing no longer silence playback. The device
callback runs the mixer under catch_unwind: a panic plays silence.
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: no test did a real editing job end to end. Codec and container
oracles exist, but nothing imported phone- or camera-like media, edited
it through the command layer and checked the exported file (G6).
Fix: apps/filmcraft-cli/tests/workflows.rs drives the `filmcraft-cli mcp`
binary over stdio on media generated with ffmpeg into target/fixtures:
- a VFR H.264 + AAC MP4: sequence from clip, extract, -6 dB audio gain,
title, H.264 MP4 export;
- a long-GOP H.264 MKV with AAC + AC-3 tracks: ripple trims, colour matte,
centred cross dissolve, save; a new CLI process exports ProRes.
ffprobe / ffmpeg check streams, frame count, duration, which source frame
lands where, A/V sync across the cut, title pixels, the dissolve mix and
audio levels. The tests skip without ffmpeg (testkit oracles).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: DPX import rejected the 10/12-bit packed layout (packing 0)
and the Y'CbCr descriptors (100 CbYCrY, 101 CbYACrYA, 102 CbYCr,
103 CbYCrA) with an "unsupported" error.
Fix: unpack packed components back to back, low bits first, each scan
line on a 32-bit boundary (as ffmpeg reads 12-bit packed files), and
convert Y'CbCr to RGB with the matrix the colorimetric field names
(BT.709; BT.601 for the 601 and composite codes; by picture size when
unspecified), in video range unless the reference codes span the full
code range; 4:2:2 chroma is shared by each pixel pair. Unit tests for
every new layout and hostile headers; an ffmpeg oracle test builds
packed 12-bit RGB and 8-bit 4:2:2 / 4:4:4 / 4:4:4:4 files from
ffmpeg's test picture and matches its decode within one 8-bit step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: FilmCraft exports OpenEXR sequences (#806) but could not open
.exr files at all, so EXR plates and renders from other apps could not
be imported (docs/gaps.md G10).
Fix: a clean-room OpenEXR reader in filmcraft-media (still/exr.rs) for
single-part scan-line files with no, RLE, ZIPS or ZIP compression and
half / float / uint channels, with our own zlib/DEFLATE decoder (no new
dependency). R, G, B, A (or luminance Y) become premultiplied linear
RGBA float frames, the compositor's working format, so values above 1.0
survive; the data window is placed in the display window. Stills open
through open_bytes, sequences through ImageSequenceSource; tiled, deep,
multi-part, subsampled and PIZ / PXR24 / B44 / DWA files are refused
with a clear error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy and Cut snapshot files or an image already on the system clipboard.
Paste ignores that same media so Cmd+V and the Edit menu paste the clips
instead of re-importing stale files the platform left beside the clip names.
A GopCache decodes with its lock held, and that decoding starts rayon work
(rotating iPhone frames, cropping, slice-threaded decoders). On the global pool
a worker that took part of it could, waiting inside it, pick up another render
job asking the same cache for a frame and block on the lock, while the lock
holder waited for the work on that worker's stack: exports of rotated iPhone
HEVC clips stopped making frames in about half the runs.
The locked decoding now runs on a separate decode pool, whose threads never wait
for a cache's lock; a caller that is a rayon worker waits on a plain thread so it
does no other rayon work while holding the lock. The cancel/catch-up/draft/
background hints travel with it (filmcraft_media::cancel::Context). Intra-only
streams crop and rotate before taking the lock again.
Second version of #662 (reverted): the regression test's stand-in decoder summed
with overflow, which panics in debug builds; it now wraps explicitly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `cargo xtask parity` (#781, #782) measured only the menu bar; the
Effects panel's effects and transitions had no measured parity figure
(docs/gaps.md G7).
Fix: `--area effects|transitions` compares the catalogue the engine's
effect definitions list in the Effects panel (new `effect_catalog`
example in filmcraft-project, the panel's own top-folder filter) with a
reference of folder paths or bare names (default the local, never
committed plan/premiere/<area>.json). Same report, caps and `--min` as
menus; placement by folder; Presets / Lumetri Presets and the other
area's items are out of scope, so one panel snapshot serves both areas.
`--area menus` (default) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging files from the explorer onto the Timeline imported them into the
Project bin but never placed them when no sequence was open, because only
the clip-area interact path consumed pending_timeline_drop. The empty
Timeline now creates a sequence from the first imported item and places
any further items after it, matching the existing Project-panel drop.
Audio effects applied to the video side of a pair now land on the linked audio, and Effect Controls lists that clip's parameters and Custom Setup while the picture is selected.
File ▸ Close aliases Close Sequence. The shortcut editor targeted
sequence.close while Cmd+W lived on file.close (and Timeline), so Clear
left the chord bound. Treat the pair as one shortcut family.
Fixesstorytold/filmcraft#773
With NVENC registered on Linux, `filmcraft-cli --test cli` aborts in 2 of 45 runs on an RTX 3090
(`malloc(): mismatching next->prev_size`, `malloc(): unaligned fastbin chunk detected`) right
after an export finishes; 0 of 30 runs fail with this reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a graphics driver that crashes inside wgpu instance or device
creation (Intel Vulkan igvk64.dll #512#413#793, AMD atio6axx.dll #477)
killed every launch the same way until the user set WGPU_BACKEND by hand;
a start-up error wgpu did report ended in an error dialog.
Fix: each start leaves a marker in the data folder (graphics-start.json),
cleared once the window has painted two frames. A marker still pending at
the next launch moves on along the platform's ladder (Windows: Vulkan+DX12,
DX12 only, software/WARP; Linux: default, software/lavapipe or llvmpipe)
and keeps the mode that works. A wgpu start-up error moves on within the
same process (eframe reuses its event loop). Fallback modes don't register
the GPU export renderer, whose own instance would load the failed driver.
An explicit WGPU_BACKEND leaves the marker alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere imports Targa and DPX as stills and numbered sequences.
FilmCraft exports both (#779) but could not read either: the extensions
were not importable and the image crate has no DPX reader, so even our
own Targa / DPX renders could not come back in.
Fix: clean-room decoders in filmcraft-media. Targa from the TGA 2.0 spec:
image types 1/2/3 and RLE 9/10/11, colour maps, 15/16/24/32-bit, 8/16-bit
grayscale, any origin. DPX from SMPTE ST 268: first element, luma / RGB /
RGBA / ABGR, 8/16-bit, filled 10/12-bit (methods A and B), either byte
order, padded lines, orientations 0-3. Both bound allocations by the
data present and reject hostile headers with errors. Stills and image
sequences share one decode_raster entry point; "tga" and "dpx" join the
importable still extensions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere's "AAC Audio" format has no FilmCraft equivalent. AAC
was only written inside H.264 / H.265 / AV1 movies; the only audio-only
exports were Waveform Audio and AIFF.
Fix: a new `aac` export format ("AAC Audio") that encodes the sequence
mix with the existing AAC-LC encoder into an MPEG-4 audio file (.m4a,
encoder delay in an edit list), or a raw ADTS stream when the path ends
in .aac. Bitrate, sample rate, channels and loudness normalisation follow
the audio settings. Adds an "AAC Audio 48 kHz 320 kbps" Audio Only
preset, CLI extension guessing (.m4a / .aac), es/fr/ja/zh-cn strings, and
updates G13 (also noting ProRes 4444 / XQ is already in the export UI).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zooming a monitor took the zoom dropdown, which zooms about the middle
of the picture, and then scrolling or the Hand tool to bring the part
you wanted into view; at Fit the wheel did nothing.
Now, on the Program and Source monitors:
- The mouse wheel (or a pinch) zooms about the pointer, from Fit too,
between the dropdown's 10% and 1600%: the point under the pointer
stays under it. It zooms as fast as the Timeline's wheel zoom, through
the same wheel_input.
- A middle-button drag that starts over the picture pans it, whatever
the tool; the Hand tool still pans with the left button.
- Fit resets zoom and pan, and each monitor keeps its own, as before.
Comparison View, which always fits both frames, ignores the wheel.
Tests: view_ui (wheel zoom about the pointer, middle-button pan, the 10%
floor, Fit, Source untouched).
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>
Problem: Premiere 26's redesigned mask tools give rectangle masks
rounded corners; FilmCraft masks only had sharp corners.
Fix: MaskPath::with_rounded_corners replaces each sharp corner between
straight edges by a circular fillet (two vertices joined by a cubic arc,
handles 4/3·tan(φ/4)·r, shrunk to fit half of each edge; hostile radii
and indices are clamped or ignored). masks.add takes `cornerRadius` for
a rounded rectangle, and the new masks.roundCorners command rounds an
existing mask's corners (or the listed vertices) in one undo step, on
every Mask Path keyframe so animated paths keep interpolating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New Bin only selected the folder and started rename, so Clear still targeted clips that were selected before. The trash button and Delete then removed those items and left the bin. Clicking a bin already deselects items; creating one now does the same, and Clear drops the rename field so it does not keep editing a bin that is about to go.
Problem: Premiere offers OpenEXR as an image-sequence export format;
FilmCraft could not write EXR at all (docs/gaps.md G13).
Fix: a new export format (id exr, "OpenEXR") writing numbered .exr stills,
with a preset, the Include Alpha Channel option, the file.exportMedia format
list and es/ja/zh-cn strings. The writer in crates/export/src/raster.rs
follows the public OpenEXR file layout: version 2 single-part scan-line
file, half-float B/G/R(/A) channels, no compression, one scan line per
chunk, Rec.709 chromaticities; values are scene-linear (the pipeline's
transfer undone) with premultiplied alpha, as EXR defines. No new
dependency. ZIP / PIZ / DWA compression stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Morph Cut rendered as a plain Cross Dissolve, so a jump cut in a
talking-head shot showed the subject twice at half opacity during the
transition, and the GPU plan mixed it as a dissolve too.
Fix: Morph Cut warps both frames along the dense optical flow between them
(the Lucas-Kanade flow of #780, crates/render/src/flow.rs) and blends, so the
subject moves from its place in the outgoing shot to its place in the incoming
one; where the frames do not correspond it stays the cross dissolve. Frames
inside a Morph Cut are planned whole on the CPU (no longer a GPU-mixable
"simple" transition) and cost as a complex transition in the preview estimate.
Morph Cut goldens re-blessed. No face tracking yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: MP4 and QuickTime name MPEG audio without its layer (ffmpeg
stores MP2 as `mp4a` + esds object type 0x6B), and we always made the
layer III decoder for it. symphonia's decoder reads only the layer it
was made for, so every MP2 frame was rejected and the track played as
silence; MOV's `.mp2` sample entry was not recognised at all.
Fix: the MPEG audio decoder for MP4 / MOV is made on the first packet,
for the layer its frame header names (ISO/IEC 11172-3 2.4.2.3);
`.mp2` maps to the MPEG audio codec like `.mp3`; the track is labelled
"MPEG Audio" for layers I/II, as Matroska's is. Decoded tracks now match
ffmpeg sample-exactly (edit list trims the 481-sample encoder delay).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: French, one of Premiere's interface languages and one of the
12 key languages in docs/gaps.md G14, had no catalog; #498 asked for it.
Fix: a new fr.tsv catalog with every entry Spanish has (menus, panels,
dialogs, Settings pages, the Effects / Effect Controls registry, the
Keyboard Shortcuts dialog, status messages) plus the native macOS
Settings item, written from the plain meaning of the English text with
ordinary French editing vocabulary (infinitive commands, "vous", French
sentence case and spacing). Language::Fr is persisted as `fr`;
app.language.french joins Edit > Language (checkmark, native macOS menu),
Settings > General offers Français, and System Language picks French for
any fr-* locale. Like Japanese, tests require only the core menus, so
later strings show in English until translated; a French entry without
a Spanish one fails. A new test checks the bundled fonts cover every
French letter (the glyph check is shared with Ukrainian). Docs: G14,
localization-parity, ROADMAP language row, ui-egui README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with a transition selected, Effect Controls showed only its
settings. Premiere puts the transition's own timeline beside them (clip
A, clip B and the transition over their cut), where it can be slid
over the cut or resized by dragging; FilmCraft could do that only in the
Timeline.
Fix: right of the settings (the clip view's divider and width), a time
ruler over the cut, clip A above, the transition between, clip B below,
the cut and the playhead. Dragging the transition's middle slides it,
dragging an end changes its duration from that end (the Timeline's
rules, transitions::drag_span), measured against the view the drag began
in; each drag is one undoable sequence.setTransition. The ruler moves the
playhead. Every part has an automation id; no new UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: ffmpeg stores the encoder priming as CodecDelay in ns (1024 AAC
samples at 48 kHz = 21.333 ms, 256 AC-3 samples at 44.1 kHz = 5.805 ms)
and block timestamps in ms. The demuxer subtracts the delay rounded to
1 ms, and fixed-length packets were anchored on that rounded stamp, so
the whole track played up to 16 samples late against ffmpeg's decode.
Only Opus undid the rounding.
Fix: one stamp-to-samples conversion for every Matroska audio codec
that undoes the demuxer's rounding and removes the exact CodecDelay
(rounded to the nearest sample; the element holds whole ns), with
checked math against hostile timebases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Premiere offers JPEG, Targa and DPX as image-sequence export
formats; FilmCraft only wrote PNG, TIFF and BMP sequences (docs/gaps.md G13).
Fix: three new export formats (ids jpg, tga, dpx), numbered like the other
sequences, with presets, the export UI (JPEG quality slider, Targa alpha)
and es/ja/zh-cn strings. JPEG uses the image crate at the export quality;
Targa (TGA 2.0, uncompressed 24/32-bit) and DPX (SMPTE ST 268, 10-bit RGB
method A, BT.709) are written by a small hostile-input-safe writer in
crates/export/src/raster.rs. Format::is_image_sequence replaces the
hand-kept format lists. OpenEXR stays open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a speed-changed clip set to Time Interpolation > Optical Flow
rendered exactly like Frame Blending, a cross-fade of the two neighbouring
source frames, so moving objects showed double images.
Fix: new filmcraft_render::flow. It computes dense coarse-to-fine
Lucas-Kanade flow both ways on a luma pyramid at most 640 px wide, drops
motion wherever standing still explains the window as well (flicker on
static content), forward-splats both flows to the in-between time
(Baker et al. 2011), and warps both frames there. Each fetch is weighted
by how well its own flow agrees, so uncovered background comes from the
frame that shows it. Pixels that look the same in both frames, or where
neither fetch agrees, fade to the plain cross-fade. media_image uses it
for OpticalFlow clips. docs/gaps.md G18 and target-app-parity updated
(also drops the #415 / #458 items, already fixed on main).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: files with E-AC-3 audio (MP4 ec-3, MOV, Matroska A_EAC3, MPEG-TS)
imported silent: filmcraft-ac3 refused bsid 11-16.
Fix: clean-room E-AC-3 decoding from ATSC A/52:2012 Annex E in filmcraft-ac3:
bsi and audfrm syntax, 1/2/3/6-block syncframes, reduced sample rates,
frame-based exponent strategies (exponents reused across short syncframes),
SNR offset strategies, default band structures, spectral extension
(translation, notch filter, noise blending). Independent substream 0 is
decoded; dependent substreams and other programs are skipped (7.1 plays as
its 5.1 core). AHT and enhanced coupling are refused as unsupported. The
audio blocks share the AC-3 path; AC-3 output is unchanged. filmcraft-codecs
routes E-AC-3 in MP4/MOV, Matroska and TS to the decoder; the TS splitter
joins dependent substream frames to their access unit.
Tests: ffmpeg oracle for raw E-AC-3 (64.8-91.0 dB SNR) and in MP4, MOV,
Matroska and TS (79.5-91.3 dB, random access identical), mutation fuzz,
synthetic frames for headers, substream skipping, refusals and exponent
reuse across 1-block syncframes, spectral extension unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Render Effects In to Out and Render In to Out rendered every
render-bar segment that In/Out touched, in full. Segments run between
edit points, so In/Out inside one long clip rendered the whole clip
(often the whole timeline).
Fix: render only the marked frames of each segment as a partial preview,
`<hash>-<off>-<len>.mov`, frames counted from the segment start (so a
ripple keeps it, like whole `<hash>.mov` previews). The store indexes
parts per hash; the render bar is green over rendered frames only,
playback reads the part holding a frame, later renders fill only the
gaps, and Delete Render Files In to Out removes only files holding
marked frames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NVENC HEVC encoder (Main 8-bit SDR and Main 10 HDR PQ / HLG) was gated to
Windows. It now also runs on 64-bit Linux, on the CUDA device the H.264 encoder
already uses: the H.265 format and HDR probes are registered there and the
HEVC tests run on both systems.
NVENC registration on Linux now happens after the VA-API probe. Started beside
it, the background HEVC probe's first cuInit could deadlock with
libva-nvidia-driver, which calls cuInit from its library constructor under the
loader lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the menu parity figure in docs/target-app-parity.md came from an
ad hoc label-matching script over string literals; nothing repeatable
measured the real menu bar (docs/gaps.md G7).
Fix: `cargo xtask parity` dumps the shipped menu bar (new
`menus::default_menu_items`, deterministic: English, default prefs,
built-in workspaces; printed by `examples/menu_tree.rs`) and compares it
with a reference list of menu paths, by default the local, never-committed
`plan/premiere/menus.json` (any JSON of paths via --reference; parsing is
depth- and size-capped). Presence and placement are reported separately
(same path, elsewhere, missing); app/Help/recent/account/macOS items are
out of scope. Writes a markdown checklist (target/parity/ or --out) and
--min fails below a presence threshold. Without a reference it prints our
inventory only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holding Option (Alt) while dragging timeline clips now leaves the
originals in place and drops copies where the pointer lets go, as in
Premiere. The copies are selected; copies of linked clips are linked to
each other, and transitions that would travel with a move are copied.
- edit: copy_items (overwrite / insert, new ids, links and transitions)
- engine: timeline.move takes `copy: true`, returns `copied`
- ui: the drag remembers Option, shows the copy under the pointer while
the originals stay, copy cursor
- tests: engine copy/link/locked-track test, headless Option-drag UI test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In an open GOP (iPhone HEVC; x265 `open-gop`) each CRA after the first is
followed in the file by RASL pictures shown before it, predicted from the
GOP before. GopCache started a decoding run at `sync_before(i)`, the sync
sample at or before the wanted sample in decoding order: for a RASL
picture that is the CRA after it, and the decoders (software and
VideoToolbox) rightly leave a run's leading RASL pictures out. The frame
was never produced, so the request failed ("frame not produced") and the
clip's layer was left out: a black frame (or the track below) wherever a
seek landed on one of the 3-in-24 RASL frames of iPhone video, such as a
clip trimmed to start on one, the parked playhead, and parallel export.
- `decode_start`: a sample shown before its sync sample starts the run at
the sync sample before (the MPEG and MXF readers already did this in
their own `sync_before`; this covers MP4/MOV and Matroska too).
- A running decoder that started after that point is not continued for
it: it left the picture out.
Tests: a synthetic open-GOP stream in gop.rs, and an x265 open-GOP oracle
test (MP4 and Matroska, every frame from a fresh source and each CRA
before its leading pictures) against ffmpeg. Both fail without the fix.
Problem: the Timeline only reacted to double-clicks on transitions and
nested sequences, so double-clicking an ordinary clip did nothing instead
of Premiere's opening it in the Source monitor.
Fix: `source.open` takes an optional `clip` (a clip of the active
sequence): its media opens in the Source monitor, parked on the frame
under the playhead when the playhead is over the clip, otherwise on the
clip's first frame. The Timeline's Selection-tool double-click on a clip
body (not its edge, not a nested sequence or graphic) dispatches it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Source monitor only knew the one clip it showed. Opening
another clip forgot the previous one, there was no list to switch back
from, and no way to close a Source clip (Premiere keeps a list of the
recently opened clips with Close / Close All).
Fix: EditorState keeps `source_history` (most recent first, at most 20),
updated after every command; items removed from the project drop out.
New commands `source.close` (the next most recent clip opens in its
place) and `source.closeAll`. The Source panel menu lists the recent
clips (the shown one ticked) to switch between, then Close / Close All.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- contributors/people.toml: keep both new entries (maxm11, ossini)
- timeline.rs: take upstream's Alt-click through timeline.select (it
clears the gap selection), and its empty-space arm (#683); only an
empty stretch of a track selects a gap
A rectangle's rounded corners could only be set by typing a Corner
Radius in Properties. As in Premiere, a selected rectangle now shows
four small circles just inside its corners: dragging one towards the
middle rounds all four corners (Corner Radius is one value), dragging it
back out squares them, and the radius stops at half the shorter side,
where the renderer caps it too.
The circles sit on each corner's diagonal at the radius, at least 12
points in, and show only on rectangles at least four times that across
on screen. While dragging, a white outline of the rounded rectangle
previews the result; the radius is set on release with graphics.set, one
undo step per drag. Automation ids: program.layer.<clip>.<layer>.corner.<n>.
Tests: graphics_handles_ui (the circles round all four corners, clamp
and undo; ellipses and tiny rectangles have none).
Problem: a file that opens but whose picture FilmCraft cannot decode
(H.264 High 10 / 4:2:2 / 4:4:4, interlaced, a codec without a decoder)
rendered black with no sign of why. The H.264 and HEVC decoders'
"unsupported" errors reached the media source as generic decode errors,
and the renderer drops frames that fail.
Fix: the H.264 / HEVC wrappers report unsupported features as
CodecError::Unsupported. The media pool wraps every file source it opens
so a frame failing as unsupported renders the existing MEDIA UNREADABLE
slate and the item is marked Unreadable with the decoder's reason (as
for files that fail to open). Decodable frames, damaged frames and the
sound are untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
filmcraft-gpu asks wgpu for no backend (`default-features = false`); a build
that includes the app gets them through eframe. A CLI built on its own
(`cargo run --release -p filmcraft-cli`, as the README and
docs/contributing.md do) has none, and `wgpu::Instance::default()` panics
there: an export with GPU rendering on ended as "internal error (see the
crash log); the job was stopped".
ExportRenderer::new returns None in such a build, like a host without an
adapter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With GPU rendering on, a frame the planner hands back whole (an adjustment
layer, a complex transition, an HDR / wide-gamut sequence) was still rendered
by render_sequence, but inside RendererPool::with: as many frames at a time as
there are pooled renderers (2 to 4) instead of one per export thread. A
timeline graded on an adjustment layer exported slower with Auto than with Off.
Pipeline::frame asks the planner first (plan::is_cpu_frame, the test
plan_frame itself makes) and renders such a frame on the CPU directly. It
counts as a fallback now, not as a GPU frame. The picture does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the panel menu's Maximize Frame replaced the whole dock tree with a
one-panel group, so there was no way back short of resetting the workspace,
and closing that panel left an empty dock. DockNode::open_near only inserted
beside the requested neighbour, Project or Program, so with those gone (or
no panel left) every Window > panel command was a silent no-op.
Fix: the panel menu now toggles the same Maximize/Restore Frame state as
` / Shift+` (labelled Restore Frame while maximized) and keeps the tree;
a maximized panel that was closed counts as restored, and switching
workspace clears it. open_near falls back to the first group, even an
empty one, as its doc comment promised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Quit, Cmd+Q or the window's close button exited at once with
unsaved changes, leaving them to the recovery journal, so every launch
after an unsaved session offered a recovery (docs/project-files.md: no
"Save changes?" prompt on quit yet).
Fix: a close request with unsaved changes is cancelled and the existing
Save Project prompt asks Cancel / Don't Save / Save (automation ids
quit.*), as in Premiere. Don't Save discards the changes and their
recovery snapshot; Save on a never-saved project (here and in Close
Project) opens the Save dialog and returns to the prompt when cancelled.
The control channel's app.quit still quits without asking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: file.exportFrame rendered and wrote the still without looking at
filmcraft_media::pending. On the web, a frame WebCodecs is still decoding
(or whose Blob bytes are still loading) is left out of the render, so the
command succeeded with a transparent/blank picture passed off as the
requested frame.
Fix: clear the pending flag before rendering and check it afterwards; when
a read was deferred, fail with "the frame is still loading; export it
again in a moment" and write nothing. A later call exports the real frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: dragging a clip's edge with the Selection tool (a regular trim)
moved the edge but left a fade in from nothing / fade out to nothing where
it was; only Ripple trims moved transitions along with the cut.
Fix: after a regular trim, the trimmed clip's one-sided transitions are put
back on its edges, and shortened when the clip gets shorter than them.
Transitions between two clips are untouched (a regular trim cannot move
their cut).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: moving a clip dropped the transitions at its edges (#227, point 8
of #374). `edit::move_items` deletes the moved clips before placing them
again, and that deletion removes every transition that refers to them, so
a fade on a clip's edge, or the cut between two clips moved together,
vanished.
Fix: before the move, collect the transitions whose clips all move onto one
track by one offset, and put them back on that track shifted by the offset
once the clips have landed (overwrite and insert moves). A crossfade whose
partner stays behind is still dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: dropping a project item in the empty space above the top video
track or below the last audio track did nothing: the Timeline only
accepted drops on an existing track row, and timeline.place could only
target existing tracks.
Fix: timeline.place accepts "new" as `track` / `audioTrack`. It adds a
track after the last one of its kind for each stream the item carries,
then places the clip there, as one undo step (the new tracks are removed
again if the placement fails). The Timeline shows the drop ghost in the
empty space and places on a new video track (above) or audio track
(below); the other stream goes on the track of the same number, or on a
new one when that track does not exist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: in the Effects panel, the Lumetri Presets rows and the wide-panel
thumbnail grid only sensed clicks. They applied through double-click or the
Apply menu, never through a drag, unlike every other effect and preset.
Fix: rows and grid cells sense drags and start an effect payload
`lumetri:<name>`. The Timeline's effect drop applies it to the clip under the
pointer with `lumetri.applyPreset` (one undo step). The drag ghost shows the
preset's name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a bin stayed wherever it was created. `project.moveToBin` only
moved project items, and bin rows and cards in the Project panel could not
be dragged, so the only way to nest bins was to create them inside each
other.
Fix: `project.moveToBin` also takes bin ids in `items` (as `project.delete`
does) and moves the bin with everything in it, skipping the root and any
move that would put a bin inside itself. In the Project panel, bin rows
(List view) and bin cards (Icon view) can be dragged: dropping one on
another bin nests it there, and dropping it on empty space moves it into
the bin the view shows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a project had nowhere to keep free text (to-dos, instructions
for whoever opens it next, frame numbers).
Fix: plain-text `Project::notes` (schema v15, no-op migration so older
builds refuse instead of dropping it), `project.notes` /
`project.setNotes {text, merge?}` engine commands (undoable, capped at
1 MiB; calls with one `merge` key share an undo step), and a
Window > Project Notes panel whose editor (`notes.text`) commits every
change, one typing session per undo step. Translations for es, ja,
zh-cn (and the panel title for pt-br, uk).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The split between the effect list and the keyframe area was fixed. Now a
divider between them drags left or right, as in Premiere: it shows a
resize cursor and turns blue under the pointer, and the width it sets
stays while FilmCraft runs (UiState::effect_controls_split, automation id
effectControls.divider). The effect list keeps the 260 points its rows
are laid out for (values from 150 points in, the reset button at the
end), the keyframe area at least 60.
Scrolling was tied to the effect list. The scroll area now spans the list
and the keyframe area, so the mouse wheel scrolls anywhere over the panel
and the scrollbar sits at its right edge. The ruler's click and drag are
handled after the rows, which now reach under it, so they still move the
playhead.
At narrow widths labels no longer run into what follows them: property,
effect and mask names are cut off with "…" (effect_controls::row_label),
and a dropdown narrows to the space left in its row.
Smaller readability fixes in the same panel:
- Keyframes in the lane are drawn at about Premiere's size (a diamond ~9
points wide instead of ~7; Hold squares and Bezier circles to match),
with a grab area to match.
- The keyframe navigator's arrows are taller (11 points, clickable over 20
of the row's 22) and its diamond larger; Properties uses the same
navigator, so it gets them too.
- Checkboxes (Bypass, Uniform Scale, a mask's Inverted) sit centred between
their row lines (effect_controls::fit_to_row, which also fits dropdowns).
Tests: keyframes_ui effect_controls_divider_and_scrolling_anywhere (the
divider drags, keeps its width and both minimum widths; the wheel scrolls
the rows over the keyframe area).
Effect Controls now draws a thin line under every row, as Premiere does,
across the names, the values and the keyframe lane, so a row can be
followed from its name to its value and keyframes. Each line runs through
the middle of the spacing between rows, so a row sits centred between its
two lines. It covers every kind of row: effect headers, properties, Custom
Setup, the value and velocity graphs, the mask rows, and a selected
transition's settings. The colour is the theme's separator, so it follows
the dark and light themes.
- effect_controls::row_line draws the line; effect_controls::
fit_dropdown_to_row keeps a dropdown (Blend Mode, a mask's mode) inside
its row, clear of the lines.
- The keyframe lane's ruler and clip bar are now painted after the rows, on
their own background, so rows scrolled up under them no longer draw over
them (their lines, and keyframe diamonds, which could already do so).
Problem: every New Bin entry (Cmd+B, File > New > Bin, the Project panel
menu, the footer button and the context menus) ran `file.newBin` with the
fixed name "New Bin", so each bin had to be renamed by hand afterwards.
Fix: `file.newBin` without a `name` from the UI makes the bin in the shown
bin (or the given parent), selects it and opens its inline rename field;
agents that pass `name` still name it directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the Lumetri vignette scaled its horizontal coordinate by
`1 + vround * 0.0` and applied aspect correction only for negative
Roundness, so 0 … +100 rendered identically (CPU path, the GPU op and the
GPU shader parameters alike). Negative values moved towards a circle,
the direction positive values should go.
Fix: one helper, `gpufx::lumetri_vignette_aspect`, scales the horizontal
coordinate by aspect^roundness: 0 keeps the ellipse that follows the
frame, +100 is a circle, negative values widen the oval. Non-finite or
out-of-range values fall back safely. The CPU path, the CPU FxOp and the
GPU parameter packing all use it, so they stay in parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: MkvSource used the raw Segment timestamps as media time and the
absolute Info/Duration endpoint as the clip length. A remux that keeps its
source's first PTS (ffmpeg -copyts, 1.48 s in the report) repeated its
opening picture until then and made a 3.48 s clip out of 2 s of media.
Fix: after opening, every track moves back by the earliest start among the
playable tracks (an audio track's start before CodecDelay, never below
zero), so a delay between picture and sound survives. The duration is the
Segment end minus that origin, never less than the main track's samples.
Zero-origin files are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Markers > Clear Selected Marker removes only a marker exactly at
the playhead, but a click on a marker on the Timeline ruler left the
playhead on the frame under the pointer (with snapping off), and
right-clicking a marker opened the clip menu, so a marker placed on the
Timeline could not be removed from there.
Fix: a press on a marker's pentagon puts the playhead exactly on it, and
right-clicking a marker opens a marker menu (Clear Selected Marker, Clear
Markers; automation ids timeline.markerMenu.<command>). The pentagon's
rect is shared by drawing and hit-testing (timeline_hit::marker_rect).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Appearance > Highlight Color replaced only the accent
(selections, primary buttons). The playhead, the timecodes, the focused
panel's outline, drag values and the track-targeting buttons stayed blue,
so a pink highlight left an interface that was mostly blue.
A highlight of one's own now colours those too: each keeps the
highlight's hue and is as much more or less saturated and bright as the
theme's own blue is than the theme's highlight (the playhead and
timecodes a little lighter, the targeting buttons a little deeper). With
the default highlight nothing is derived, so every theme looks exactly as
before. The track-targeting fill, written into the Timeline code, is a
theme token now (track_target).
Tests: theme (the default highlight leaves the five colours as designed
in every theme; a pink one gives them its hue, lighter and darker as the
default blues are).
Problem: timeline.slideLeft/Right(5) slid every expanded selection member by
the raw requested delta, so when a neighbour limited only one linked partner
(e.g. a short sound-only follower), picture and sound ended up at different
starts. timeline.slide already clamped to a common delta.
Fix: add edit::slide_items, which clamps the delta by the most restrictive
item and slides all items by it, and use it from both the keyboard Slide
commands and timeline.slide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a handle of a rectangle, ellipse or polygon in the Program
monitor changed the layer's Scale, and a corner scaled width and height
alike, so a shape could not be made wider or taller from a corner.
As in Premiere, the handles now change the shape's Size (Premiere's
Path) and leave Scale alone: the dragged side or corner follows the
pointer, the opposite one stays, and the anchor point is renumbered so
that it and Position stay on the same spot of the picture. A corner
moves freely; Shift keeps the proportions. A path has no Size, so it
still stretches (and its corners follow the same Shift rule).
The paragraph-text box and the shape box share the side-dragging code
(drag_sides).
Tests: graphics_handles_ui (a shape's handles change its Size and Shift
keeps the ratio; a polygon's handles keep the opposite corner).
Problem: `filmcraft-cli --demo --version` (or `--compact -V`, `--data-dir d
--version`) failed with "missing subcommand" and exit 2. The version check only
looked at the very first argument, so a preceding global option left
`--version` to the option parser and no subcommand behind.
Fix: parse the arguments first and treat `--version` anywhere, or `-V` in the
subcommand position, as a version query (`Args::wants_version`), still before
the hardware decoders are registered. `--help` keeps priority.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: our AAC decoder outputs channels in AAC element order (C, L, R,
Ls, Rs, LFE for channel configuration 6) and PacketDecoder passed that
through unchanged, while the mixer, panner, loudness and export all use
WAV/SMPTE order (L, R, C, LFE, Ls, Rs). A 5.1 AAC import therefore put
centre in L, L in R, R in C, the surrounds one slot early and LFE in Rs.
The export path already reorders for the encoder; the decode path did not.
Fix: PacketDecoder reorders AAC output for channel configurations 3-6
into WAV/SMPTE order, like the AC-3 decoder and Opus mapping family 1
already do. The Opus reorder now shares the same bounds-checked helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a video track with no edit list (x264 MP4 written without
elst, fragmented MP4) kept its raw first PTS, the B-frame composition
delay, as its origin. Media time zero repeated the first picture for
that delay, and since the clip lasts the media duration, the last
pictures never appeared in a sequence made from it.
Fix: when the video track has no edit list, move every track back by
the earliest start among the video and audio tracks, as ffmpeg does
with an input's start time. Video-only files now start at their first
picture; a file whose audio starts at zero keeps its A/V sync, and
explicit edit lists (empty edits, delays) are untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `--gpu-rendering` was missing from the CLI's list of
value-taking options, so `export --gpu-rendering off out.wav` stored the
option without a value and turned `off` into the output path (or dropped
the setting silently when the option came after the filename).
Fix: list `--gpu-rendering` among the valued options, as documented in
the CLI help.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
captions.deleteTrack existed but nothing in the UI reached it, so extra
caption tracks could not be removed and piled up in the timeline.
- Caption track header right-click menu: Delete Caption Track, and
Delete Empty Caption Tracks (disabled when none is empty). Automation
ids `timeline.captionTrack.C<n>.menu.delete` / `.menu.deleteEmpty`.
- sequence.deleteTracks takes `"captions": "empty" | "C2" | id`. Unlike
video and audio, every caption track may go (a sequence can have
none). Unknown names/ids and other value types are errors. The
selection of captions on deleted tracks is cleared; one undo step.
- Sequence > Delete Tracks... gets a Caption Tracks section (shown when
the sequence has caption tracks), ids `deleteTracks.captions*`.
- Spanish strings for the new labels.
Tests: engine `delete_tracks_removes_caption_tracks`; headless UI tests
for the header menu and the dialog (delete_caption_tracks_ui.rs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `#rrggbb` field next to a colour swatch (Appearance ▸ Highlight
Color) was refilled from the saved colour on every frame and kept an
edit only when the whole text was already a valid colour. One typed or
deleted character never is, so the field snapped back and nothing could
be typed.
While the field has focus it now keeps what is typed, and each valid
colour is saved as it comes; leaving the field shows the saved colour.
Tests: settings_ui::hex_field_takes_typing_a_character_at_a_time.
Format::Av1 in export settings and jobs, SampleEntry::av1 (av01 + av1C) in
isobmff, and the Export panel's Format list, profile and encoder rows. No
encoder ships here: like H.265, AV1 is listed greyed out until a platform
encoder registers a probe, and a job without one fails with "AV1 encoder not
available yet". Split out of #409; the VA-API AV1 encoder follows on main's
vaapi::va design.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The caption style's `font` was stored and settable through
captions.setStyle, but the burn-in renderer always drew Inter SemiBold.
Lay out, wrap and measure captions with the track's family (Inter when
the name is empty or not installed), so the Program monitor and burned-in
exports use the chosen font.
Add a font dropdown to the caption style strip in the Text panel's
Captions tab (automation id `text.captions.style.font`, options
`text.captions.style.font.option.<family>`, registered only while
visible). macOS-private faces (names starting with '.') are not offered:
some lack a space glyph and run words together.
Tests: renderer regression test (a non-Inter family changes the pixels;
failed before), fallback test for empty/unknown names, and a headless UI
test that picks Noto Serif from the dropdown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The line under each track was drawn in the Timeline background colour,
the same as the tracks, so the tracks ran into each other. It now uses
the separator colour the track headers already use, and the top video
track also gets a line above it, in the headers and the track area.
- The playhead's head ran the full height of the ruler and covered the
markers and the timecodes. It is now the short head Effect Controls
uses, on the tick row.
Problem: mediaBrowser.import flattened a folder into its first-level
files (not recursive) and created no bins, so sub-folders were skipped
and the folder hierarchy was lost in the project.
Fix: a folder becomes a bin named after it (in `bin`, else the root)
holding its files, and each sub-folder with media a bin inside that, as
Premiere's Media Browser does. Folders without media get no bin. The
walk is bounded (depth 32, 4096 folders) against symlink loops. The
result also lists the new `bins`. Import as Image Sequence and plain
file imports behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ui.set table row is one long line; #412 (PR #656) also edits it, so
the two PRs conflicted with each other in docs/control-protocol.md
whichever merged second. The thumbnails parameter is now described in
its own paragraph below the table, leaving that row untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
i18n::tests::spanish_translates_dynamic_names lists every built-in
export preset's name and description; the three new ProRes 4444 presets
had no Spanish entry, so ui-egui's lib tests failed. Adds them to es, ja
and zh-cn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timeline.place takes items: [...] and file.newSequence takes fromItems: [...]; both run as
one undo step through the new Session::edit_group, which rolls back if any item fails.
Files dropped from the OS use the same command instead of one timeline.place per file,
so they are one undo step too.
i18n::tests::spanish_translates_dynamic_names lists every settings
label and choice; "UI Scale" and "Auto (system)" had no Spanish entry,
so ui-egui's lib tests failed. Adds them to es, ja and zh-cn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with the Source monitor focused, L only ever played at normal
speed, and J and the slow shuttle commands returned "Source playback
currently supports normal forward speed": the Source clock always ran at 1x.
Fix: the Source clock runs at a signed speed. L/J play forward/backward
and pressing the same key again doubles the speed up to 8x; Shuttle Slow
Right/Left play at 1/4 speed. As in the Program monitor, speeds other than
normal forward play without sound, and backward playback stops on the
first frame. docs/monitors.md updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking empty time between two clips selects the gap, drawn as a light
block across it. Delete or Backspace closes it; right-clicking it opens
a menu with Ripple Delete. A cut's edit point menu right-clicked before
no longer stands in for the gap's menu.
timeline.selectGap selects the empty time on one track between two
clips (or before the first one). Delete, Backspace (edit.clear) and
Ripple Delete then close it as one "Ripple Delete" undo step, as in
Premiere Pro. The selection lives in the editor state (gapSelection in
sequence.inspect), is replaced by a clip, transition or edit point
selection, and is dropped by an edit that changes the gap.
sequence.closeGap shares the same code.
Closing a gap on a locked track skipped that track but still moved the
sync-locked ones, pulling them out of sync with it. It is now refused
with the usual "locked" error.
Problem: a video clip only ever showed its first frame. Show Video
Thumbnails was a plain on/off switch, so a wide clip at high zoom stayed a
flat colour after its head.
Fix: the wrench menu keeps Show Video Thumbnails and adds Head (the
default, as before), Head and Tail and Continuous under it. Continuous
tiles aspect-correct frames across the clip on a grid anchored at its head,
only where the panel shows them (at most 256 per clip), each frame the
source time under the tile's left edge (speed, reverse, frame holds,
multicam angles), requested at thumbnail priority through the existing
frame cache. A clip too narrow for the tiles falls back to its head. The
mode is UI state (`thumbnail_mode`, serde default Head, so saved settings
keep working) and `ui.set {timeline: {thumbnails}}` sets it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deselecting by clicking somewhere empty only worked in some spots.
- Timeline: a click on no track at all (below the tracks, between the
video and audio tracks) did nothing; it now deselects clips, transitions
and edit points, as a click on an empty stretch of a track does, and a
drag from there draws a box selection.
- Text editing: a text layer being edited took the keyboard back on every
frame and read every key, so after clicking another panel, typing there
(the Effects search, say) went into the text (also #227). Moving to
another panel now ends the edit; Properties and Essential Graphics, which
style the text, keep it. While another field has the keyboard the keys
are its own, and the text takes the keyboard back only when nothing has
it. Only a move counts, so an edit an agent starts with another panel
focused goes on.
- Program monitor: a click around the picture, or on the picture's empty
space, ends the text edit and deselects the graphic layers and the mask
(graphics::deselect_on_monitor); before, only the picture's empty space
ended an edit or deselected layers, and a mask stayed selected.
The transform box (#639) follows in its own change once that lands.
Tests: deselect_ui (3: empty Timeline space deselects and starts a box;
clicking another panel ends a text edit while Properties keeps it; empty
monitor space deselects a layer and a mask).
Problem: every FileReader owned its std::fs::File for its whole
lifetime, so each open media source pinned one descriptor. A project
with hundreds of sources hit the soft limit (256 on macOS, 1024 on
Linux) and the remaining media failed to open or showed as offline.
Fix: FileReader keeps its path and borrows the file from a shared LRU
pool of at most MAX_OPEN_FILES (128) open files. Past the cap the least
recently read file is closed and its reader reopens it on the next
read; a file that is now shorter than when it was opened is refused
instead of being read as the old one. Dropping a reader closes its file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the macOS bundle had no NSMicrophoneUsageDescription and empty
hardened-runtime entitlements, so macOS never asked for microphone access
and voice-over recording got no input.
Fix: declare NSMicrophoneUsageDescription in Info.plist.in and grant
com.apple.security.device.audio-input in entitlements.plist; an xtask test
guards both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: WAV had no streaming reader, so open_reader fell back to reading
the whole file into memory, and WavSource kept those bytes for its
lifetime. Importing a 90-minute WAV cost about its size in memory and
blocked until the whole file was read.
Fix: WavSource reads through a SharedReader. Opening walks the chunk
headers (fmt, data, bext, iXML) without touching the samples, and
audio() reads only the frame range a request needs, in bounded blocks.
open_reader takes WAV this way after the registered reader openers;
WavSource::parse keeps working on in-memory bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the focused panel's shortcuts were all tried before the
application-wide ones. egui ignores extra Shift and Alt when matching a
key press, so with the Timeline focused its `Delete` (Clear, #243)
swallowed the app-wide `Shift+Delete` (Ripple Delete) and the clip was
removed without closing the gap.
Fix: try the most specific chords first across both scopes; the sort is
stable, so a panel shortcut still overrides an application shortcut on
the same chord.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FrameStreamInfo::parameters_changed compared the in-band AV1 sequence header with
av1C's byte for byte. SVT-AV1 writes a provisional av1C header whose coding tool
flags (CDEF, restoration, warped motion...) differ from the in-band one, so every
such MP4 dropped to the software decoder from its first frame, on every hardware
backend. Headers now compare by what the picture format depends on: profile,
maximum size and colour configuration. A header that does not parse still counts
as a change. Regression test with an SVT-AV1 720p 10-bit av1C / in-band pair.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Effect Controls only offered the graph twirl-down for scalar
(Float) parameters. Position is a point (Vec2), so animating it showed
keyframes but never the value/velocity graphs that Scale has.
Fix: point parameters are graphable too. The value graph draws an X and
a Y curve with a draggable keyframe on each (a drag changes only that
coordinate); the influence handles sit on the X curve, since both
coordinates share them; the velocity graph shows the speed along the
path. NaN "auto" coordinates are skipped instead of drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the ProRes encoder already codes 4444 / 4444 XQ (optionally
with alpha), but export only knew the four 422 flavours: any other
proresProfile fell back to HQ and the encoder was always fed a 4:2:2
frame, so graphics could not round-trip with their alpha.
Fix: proresProfile accepts 4444 and 4444xq. The ProRes export encoder
builds a 4:4:4 frame for them and, with Include Alpha Channel, codes
the straight alpha of the unflattened picture (ExportSettings::
supports_alpha / keeps_alpha). Export Settings offers both profiles and
the alpha checkbox for them; new presets Apple ProRes 4444, 4444 with
Alpha and 4444 XQ; summary text and the MXF coding label name the
profile (and the MXF descriptor keeps its 12-bit 4:4:4 depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Transform effect listed a shutter angle but had no override, so motion blur never used it. Checking Use Effect Shutter Angle now samples the animated transform across that angle.
Problem: FilmCraft had no UI scale preference. The interface always
followed the scale the desktop reported (150% on some X11 setups), and
only Ctrl+-/Ctrl++ could change it, for the session only.
Fix: add appearance.uiScale (Auto, 75%-300%) to Settings > Appearance.
A fixed value sets egui's zoom so a point is that many physical pixels
whatever the system reports; it is applied when the setting or the
system scale changes, so Ctrl+-/Ctrl++ keep working for the session,
and Auto gives the system's scale back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Reset Effect replaced the effect with a fresh instance, so every
keyframe (and every mask) on it was deleted, and Effect Controls had no
way to reset a single property.
Fix: effects.reset now resets the parameters in place, as Premiere does:
a static parameter gets its default, an animated one keeps its keyframes
and gets the default as a keyframe at the playhead; masks stay. New
effects.resetParam does the same for one parameter, and every Effect
Controls property row has a reset button for it (automation id
effectControls.<effect>.<param>.reset).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Camera files such as CLIP.MOV were hidden or skipped on Linux because sibling matching and tests assumed lowercase suffixes. Import, the media browser, and image-sequence numbering now accept mixed-case extensions.
Headless import merged into the current project, so a demo or leftover timeline mixed into export. replace:true starts from file.newProject with the same name, then imports, which is the documented empty-project-start workflow.
Relaxed and Aggressive previously only scaled two internal multipliers, so the matte stayed at Default. Switching Setting now writes Premiere-like matte values and the renderer uses that set, including Custom for hand-tuned keys.
Problem: presets.apply replaced an intrinsic effect (Opacity) wholesale, so
applying Fade Out after Fade In discarded the Fade In keyframes (and vice
versa).
Fix: when a preset lands on an intrinsic effect, keep the clip's existing
keyframes that lie outside the span the preset animates for that parameter.
Re-applying the same preset still replaces its own ramp; static preset values
behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The project Look-Ahead Time parameter was stored but never applied, so 0 ms and 30 ms mixed the same clip. The limiter now sizes its look-ahead from that parameter and pre-rolls for the chosen window.
Problem: after merging an FCP7 XML / OTIO / EDL / AAF document, the import
linked each media file by reading it whole into memory (services.read_file +
open_bytes). Multi-GB 4K files took minutes and stayed resident, while a
direct import opens them in well under a second.
Fix: link through MediaPool::open_file, as import_streamed does: the host's
random-access reader reads only the container index now and samples on
demand (hosts without a reader still read the file whole, as before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: dragging a clip edge in the Timeline previewed the raw pointer
delta, so the edge kept growing past the end of the clip's media (or into
the neighbouring clip) and only jumped back when the button came up and
`timeline.trim` clamped it (#653, point 2 of #374).
Fix: `commands::trim_delta` computes, without editing, the delta
`timeline.trim` would apply (the same clamp across linked partners, now
shared by both), and the Timeline's trim drag shows that delta.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A GopCache decodes with its lock held, and that decoding starts rayon work
(rotating iPhone frames, cropping, slice-threaded decoders). On the global pool
a worker that took part of it could, waiting inside it, pick up another render
job asking the same cache for a frame and block on the lock, while the lock
holder waited for the work on that worker's stack: exports of rotated iPhone
HEVC clips stopped making frames in about half the runs.
The locked decoding now runs on a separate decode pool, whose threads never wait
for a cache's lock; a caller that is a rayon worker waits on a plain thread so it
does no other rayon work while holding the lock. The cancel/catch-up/draft/
background hints travel with it (filmcraft_media::cancel::Context). Intra-only
streams crop and rotate before taking the lock again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: every link (Help menu, About dialog, header Discord button, Import
screen, Settings and Keyboard help) went through egui's `ctx.open_url`. The
workspace builds eframe without its `links` feature, so egui-winit only logs
"Cannot open url - feature links not enabled" and nothing opens on Linux,
Windows or macOS.
Fix: add a `HostHooks::open_url` hook, as PhotoCraft does. `links::open` now
takes the app and calls the hook, falling back to `ctx.open_url` when it is
unset or fails (the web build keeps opening a new tab). The desktop app sets
it to `open::that_detached` (open 5, MIT), which does not block the UI
thread. Test: every Help link reaches the hook, and a failing hook falls
back to `ctx.open_url`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the keyframe lane highlighted every keyframe whose time equals the
playhead. A click on a keyframe moves the playhead there, so every
parameter's keyframe at that frame lit up (point 4 of #412).
Fix: a serde keyframe selection in the UI state (clip, effect index, param,
mask, media time). A click (or drag start) selects exactly that keyframe and
still moves the playhead; highlighting reads the selection. A moved keyframe
stays selected; references to deleted keyframes or clips no longer shown are
dropped. Readable via ui.inspect, settable via ui.set keyframeSelection.
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>
Conflict in crates/engine/src/export_tools.rs: both sides added an
import (BitrateMode for CRF, GpuRendering from main); both kept.
main's new Chinese catalog requires every tl! string: added zh-CN for
"Quality (CRF)" and "CRF (constant quality)".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with the Project panel focused, Cmd+A ran project.selectAll,
which selected every item in the project: items in closed bins and in
bins of other tabs too.
Fix: project.selectAll takes an optional `bin` (its direct items) and
`expanded` (sub-bins twirled open in List view, whose items are shown
and count). Unknown or malformed ids are an error and leave the
selection alone; without `bin` it still selects the whole project. The
UI passes the shown tab's bin and, in List view, the open bins; while a
search filters the panel it selects exactly the rows shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AviSource maps AVI streams onto the decoders, opened from bytes or a
streaming reader (Media Browser probe):
- Video: Motion JPEG, H.264 and HEVC (Annex B, our decoders) and
uncompressed BGR24 / BGRX32 / YUY2 / UYVY / I420 / YV12 / NV12 / grey
(new RawVideoDecoder). Empty "drop frame" chunks hold the picture
before. AVI has no timestamps: H.264 with B-frames is stored in decode
order, so the presentation order comes from the pictures' order counts
(mxf::avc_presentation_order), keeping the slots the pictures occupy.
A writer that kept a fine time base (ffmpeg copying an MP4's 1/600)
fills the slots between pictures with empty chunks; the reported frame
rate is the one the pictures play at. DivX / Xvid and DV are named and
report that they have no decoder; their sound plays.
- Audio: PCM (integer and float) from the chunks; MPEG audio and AC-3
frames, which AVI chunks need not follow, found again across chunk
boundaries with the new filmcraft_mpegts::frame_bytes.
- Motion JPEG frames without Huffman tables (the AVI1 convention) decode
with the standard ITU-T T.81 Annex K tables (also for MOV).
Tests against ffmpeg's decode of the same files: H.264 with B-frames
bit-exact under random access, also copied from a 1/600 MP4;
uncompressed video and PCM bit-exact; JPEG and MP3 / MP2 / AC-3 within
decoder tolerance; drop frames; DivX; JPEG without tables equal to the
same frame with them. Engine: an AVI imports, makes a sequence, plays
picture and sound, and probes without reading it whole.
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>
Drop-frame counting never emits the first base/15 frame labels of a minute
except each tenth (frames 00-01 at 29.97, 00-03 at 59.94). Accepting them let
a damaged 00:01:00;00 normalize to a different start time, so fields_to_frames
now rejects those labels and the start timecode is left unset.
Address review of #460:
- Read only the fixed-size rtmd header (18 bytes) instead of read_sample,
which allocated the whole untrusted stsz size (a corrupt multi-GiB size
could exhaust memory during import).
- Validate hours/minutes/seconds/frames against the frame base and drop the
start timecode when they are out of range, instead of normalizing damaged
metadata into a plausible but wrong offset.
Sony XAVC cameras such as the FX3 and a6400 keep the clip's start timecode
in an rtmd (real-time metadata) data track rather than a QuickTime tmcd
track, so probe reported start_timecode: null and timecode sync could not
use the footage. Read the hours/minutes/seconds/drop-frame/frames block from
the first rtmd sample and expose it as MediaInfo::start_timecode, falling
back only when no tmcd track is present (#460).
The bin/item id split checked p.root.find_bin for every queued id, and
find_bin walks the whole tree for an item id, making a flat N-item export
O(N^2). Build the bin id set and each bin's direct children once, then
test membership and expand from the index, so closure() is O(N log N)
and bin expansion never re-walks the tree.
Problem: a command that takes `clips` / `clip` answered "no clips
selected" when the caller named clips that are not in the active
sequence. Over MCP or the control channel that reads as if the
explicit ids were ignored: a clip id of another sequence (for example
from `sequence.inspect {"item": …}`), an id that is no clip at all (a
track or project item id from the same inspect output) and a string
id like "29" all gave the same message.
Fix: when the selection check fails and the parameters named clips
under a key the command documents, but none is a clip of the active
sequence, the reason names the first one and what is wrong with it:
the sequence it is in (with the `sequence.open` call to make it
active), that no clip has that id, or that the value is not a clip id.
Enablement itself is unchanged: such a call is still refused, and a
call that names nothing keeps the selection's reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Icon view each row of cards was laid out as a hover-only strip, so
only the strip under the last row sensed clicks. With an empty project
the whole panel is that strip and double-click opened Import; as soon
as the project had cards (for example after placing audio in the
timeline, which adds the clip and its sequence), most of the visible
blank space sat beside or between the cards and ignored double-clicks.
A click-sensing background now covers the visible Icon view behind the
cards (registered first, so cards keep their own clicks) with the same
handlers as the empty strip: click deselects, double-click imports,
right-click opens the panel menu.
Tests double-click with real pointer input at 60 fps: between and beside
the demo project's bins, and beside the cards after a tone is placed on
A1; both failed before (0 imports) and pass now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A media file the refresh couldn't open (still being written, or not media
FilmCraft reads) kept its old stamp, so every look saw it as changed and
opened it again on the UI thread. It now keeps its new stamp and is tried
again once it changes again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When wgpu falls back to a software rasterizer (llvmpipe / SwiftShader /
softpipe - containers and VMs without GPU device access, missing drivers),
set_wgpu accepted it as the GPU compositor (llvmpipe satisfies the Rgba16Float
checks), so the preview quietly composited through WGSL on a CPU rasterizer:
GPU usage 0 %, CPU saturated, laggy playback, no visible explanation.
software_rasterizer() now detects CPU rasterizers (device_type == Cpu, or
llvmpipe / SwiftShader / softpipe in the name) and gpu_compositor_unsupported
rejects them first, so the monitors use the native CPU compositor, which
renders the same frames faster than wgpu-on-llvmpipe. The status bar shows the
reason ("GPU acceleration is unavailable; preview renders on the CPU
(software rasterizer (...))") in addition to the log line. New tl! string with
Spanish, Japanese and Simplified Chinese translations.
Test: gpu_fallback_tests::software_rasterizers_are_detected.
Co-Authored-By: Xiaomi MiMo <noreply@xiaomi.com>
Language::De is persisted as `de` and translated through a new de.tsv
catalog that covers every menu label and the native macOS Settings menu,
written from the plain meaning of the English labels with informal "du"
and infinitive commands. The new app.language.german command checks,
switches and restores the language like the others, the native macOS
menu maps it back, Settings > General offers Deutsch, and System Language
picks German for any de-* locale (de-DE, de-AT, de-CH ...).
A new test, built like the Ukrainian one, fails when German misses a core
menu label (the Brazilian Portuguese entries, the top-level menus, the
macOS Settings menu) or keeps an entry that is no longer a menu label;
menu labels added later use English until they are translated. The
System Language test no longer uses `de` as an unsupported example.
Closes#490
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+V with a screenshot or an image copied in another app, or with files
copied in Explorer / Finder, now pastes them. In the Timeline they are
imported and placed at the playhead, end to end, without overwriting
anything: each clip goes on the first free track from the targeted one
outward (up for video, down for audio), and a track is added when every
track is busy there. Ctrl+Shift+V inserts them on the targeted tracks, as
Paste Insert does. In the Project panel Ctrl+V imports into the shown bin.
One undo step takes the paste back.
Copying clips already puts their names on the system clipboard as text, so
media on the clipboard was copied after them; without media, Paste pastes
the copied clips as before.
- engine: edit.pasteMedia {paths, insert, place, bin}; paste_media::
save_pasted_image writes "Pasted Image <n>.png" where voice-over takes and
narrations go (voiceover::media_dir: Scratch Disks > Captured Audio and
Video, else next to the project, else the data directory), trying a
bounded number of names as voiceover::unique_wav_path does.
timeline.place's default length moves to commands::full_duration so both
share it.
- ui-egui: HostHooks::clipboard_media reads the clipboard (ui-egui stays
OS-independent); menus::invoke turns edit.paste / edit.pasteInsert into a
media paste when the Timeline or Project panel has focus. egui-winit drops
the Ctrl+V press when the clipboard holds no text but passes the release,
so a V release without its press puts the press back. That also makes
Ctrl+V paste copied clips when the system clipboard is empty.
- filmcraft (desktop): the hook, with arboard (already in the tree through
egui): copied files first, else an image.
Not covered: the web build, whose browser clipboard works differently.
Tests: paste_media_tests (5: image above the footage with nothing
overwritten and one undo step, tracks added when every track is busy,
Paste Insert, import only, saved image names) and clipboard_paste_ui (4:
an image pasted from a key release with no text, copied files and Paste
Insert, the Project panel imports, copied clips still paste and a plain V
pastes nothing).
While an edge was dragged, the Timeline drew the clip at its new length
from the untrimmed item, so draw_waveform spread the clip's whole source
range over the shorter (or longer) box: the waveform squeezed and the
sound under the pointer moved, which made it hard to trim to where the
music starts (#374, item 13). Trim and roll previews now draw from the
item the edit will leave, so every timeline time still covered shows the
same media and the waveform is cut off at the moving edge. Video
thumbnails show the new first frame the same way. Rate Stretch, which
really fits the same media into a new length, still squeezes.
The item update is factored out of trim and ripple_trim_group into
filmcraft_edit::move_edge, which the preview uses too, so preview and
edit agree for speed-changed, reversed and frame-held clips.
Rendered mid-drag against the released result, the music clip's
waveform matched in 15% of pixel columns before and 100% after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Portuguese and Ukrainian catalogs only hold menu labels, so they keep the
English name like the other menu-less commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > Media > Automatically refresh growing files and Refresh
growing files every were listed but not wired. The desktop app now
stamps each file media item's file (size + modification time) on a
worker thread every N seconds and as soon as its window comes back to
the front, and reads changed files again the way import opens them: the
item gets the new MediaInfo (length, frame size) and identity, and the
media pool swaps in a new source (the old one indexes the file as it
was). Not an undo step and a clean project stays clean (bump_view). This
also covers a render written over the file a clip uses (an EffectCraft
or After Effects round trip). A file that can't be opened yet is tried
again. media.refreshChanged runs one look (CLI, MCP).
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>
The ISO-BMFF path created the FLAC decoder with no extra data, so every
FLAC track in an MP4 or MOV failed with "flac: missing extra data". The
decoder now gets the STREAMINFO body that `dfLa` carries after its
4-byte block header, as the Matroska path already did.
Regression test `mp4_flac_audio` (an ffmpeg-made MP4 with FLAC audio)
failed before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GpuRendering defaults to Off (opt-in per export) until measured on Windows and macOS;
docs and ROADMAP say so.
- The shared export device installs an uncaptured-error handler and a device-lost callback
that log once and retire GPU export for the session (frames go to the CPU) instead of
wgpu's default panic.
- RendererPool drops a renderer that panicked and retires the pool, so the rest of the
export renders on the CPU (regression test in gpu_hook_scenarios); the duplicate
hostile-size test that raced on the global registry is folded into it.
- Rebased onto main's render_plan/transitions: render_export_prepared reads the linear
accumulator after render_plan; transition (Composite) plans fall back to the CPU.
- es/ja rows for the new Export strings; web_time::Instant in the export pipeline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
source_monitor markers and format's audio-stream clamp now detach with Arc::make_mut;
restore the final braces in clip_ops.rs and project/src/lib.rs (cargo fmt);
scripted.rs is back to main (the test covered key dispatch, which this PR doesn't change).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ja.tsv rows for the new appearance strings (no duplicate Dark/Light);
uk.tsv gets Next Appearance Mode, which the pt-br menu catalog now has.
- appearance.colorTheme is legacy input but no longer goes stale: it follows the
mode and theme choices (the theme a fixed mode shows, the dark theme in Auto).
- docs/project-files.md documents appearanceMode / darkTheme / lightTheme.
- #489's menu/settings label test compares with the theme cards' choices.
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>
- One code everywhere: Language::ZhCn, persisted as `zh-cn`, catalog zh-cn.tsv.
- general.interfaceLanguage accepts and lists zh-cn (Settings > General).
- macOS native menu follows app.language.chinese.
- System Language picks Simplified Chinese only for zh, zh-CN, zh-SG and zh-Hans
(not zh-TW / zh-HK / zh-MO / zh-Hant).
- Drop the second CJK font loader: cjk.rs already installs the Chinese fallback,
so switching needs only cjk::available(); without a face the switch is refused
and a saved zh-cn preference falls back to English at startup, like Japanese.
- Remove the fork's build-zh.yml workflow.
- Tests: Chinese covers every menu label and tl! literal; switching persists or is
refused without a font; locale mapping. New strings get es/ja/zh rows.
Plumbing from #389 by @demon820308.
Co-authored-by: demon820308 <zwbigi@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session kept the parser's user data in a Box and later wrote through that Box,
which invalidates the raw pointer the callbacks use (Stacked/Tree Borrows).
Rust code now reaches the state only through the same into_raw pointer, never
across the parse call, and Drop frees it after the parser is destroyed.
The setting test accepts either Linux backend's decoder when NVDEC registered,
and the README notes perf.stats names NVDEC for streams VA-API decodes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WAV and AIFF sizes were 32-bit with unchecked arithmetic: past 4 GiB
(24-bit 5.1 at 96 kHz after about 41 minutes) a debug build panicked
and a release build wrote a wrapped, corrupt header. The whole file was
also built in memory first.
- WAV past 4 GiB is written as RF64 (EBU Tech 3306) with a ds64 chunk;
smaller files stay plain RIFF.
- AIFF has no 64-bit form: a longer one is refused before anything is
written, naming WAV instead.
- The header is written from the known length, then the samples one
second at a time, so memory stays flat; progress advances per second.
- Odd-sized data chunks get their pad byte; hostile lengths, channel
counts and sample rates are errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Nothing is written unless FILMCRAFT_DEV_DESKTOP_ENTRY=1 (debug builds no longer write into
~/.local/share on every cargo run).
- The 256/512 PNGs are copied from this checkout's assets/app-icon at runtime instead of
include_bytes! into every Linux binary; a missing icon is logged and skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- When the default output cannot be opened, probe pipewire/pulse first and raw hw:/plughw:
last (those take the card exclusively).
- The fallback list is probed only after the default fails, and the pick is cached per host,
so devices()/configure() no longer open every PCM.
- Status-bar notes go through i18n::t/fmt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
data_mut() and drag_delta() both take egui's non-reentrant context
write-lock; calling the second inside the first froze the app on the
first frame of a drag in the Trim Monitor. The delta is now read
before the lock is taken, and a regression test drags through the
real Trim Monitor.
Constant quality has no bitrate cap, but the level was chosen from the
picture size and rate alone: CRF 0 exports of 1080p camera footage
averaged 105 Mb/s (147 Mb/s peak) while signalling High@L4 (25 Mb/s).
The level now allows a worst-case estimate of 3 bits per pixel at
CRF 0, halving every 6 steps, so CRF 0 at 1080p30 signals 5.1 and the
default CRF 23 stays at 4.0.
The user_data_unregistered SEI on the first access unit now lists the
settings x264-style (rc=crf crf=18.0 profile=high level=4.0 ...).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds BitrateMode::Crf with ExportSettings::crf (0-51, default 23) and
maps it to the built-in encoder's RateControl::Crf, which export never
used. The Export panel offers "CRF (constant quality)" with a Quality
(CRF) slider in place of the bitrate fields; file.exportMedia takes
`crf` and `bitrateMode: "crf"`. H.265 (hardware only) rejects it, and
VideoToolbox / NVENC decline it so the built-in encoder runs. Settings
saved before this read as CRF 23.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The i18n CRLF normalisation already landed with #488, so main's version is kept.
abi_tests now asserts the HEVC VUI field offsets (inside the VUI and within
NV_ENC_CONFIG_HEVC) to rule out a binding mismatch behind the reversed timing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main's cli()/out! refactor: `--help` is checked as a parsed flag, so
`filmcraft-cli export --help` prints the reference too (test extended).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native file dialogs are requested with a continuation (FilmcraftApp::pick for
commands, pick_ui for panel buttons). On Linux the desktop host shows the rfd
dialog on a worker thread (HostHooks::run_dialog) and the result is applied
when it arrives, so the frame loop, playback and the control channel keep
running and GNOME no longer offers to force-quit the window. One dialog at a
time; a path for a dialog closed meanwhile goes nowhere. macOS / Windows keep
their synchronous pickers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A split-stereo left clip exported trackindex 1 and re-imported as full stereo,
losing the left/right split. Treat trackindex 1 as channel 0 when another
audio clip item of the same file in the sequence uses a higher index.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A WebM VP9 track with AlphaMode 1 stores its alpha as a second, independently coded VP9
stream in each block's BlockAdditional (BlockAddID 1). The Matroska reader detected
AlphaMode (has_alpha was true) but never read the additions, so transparent areas
rendered black.
- matroska: BlockGroups' BlockAdditions are parsed; the BlockAdditional with ID 1 of an
unlaced block is kept as Sample::addition, read with MkvFile::read_sample_addition.
- codecs: a WebM VP9 track with AlphaMode and additions gets an AlphaDecoder
(webm_alpha.rs): it decodes each sample's alpha frame with a second software VP9
decoder and attaches its luma plane (full range, converted to the picture's depth)
as the frame's alpha. After a seek the alpha decoder resynchronises from the nearest
alpha key frame. Colour pictures are held until their alpha picture is out, since the
two decoders may return pictures after different calls. A missing or undecodable alpha
frame leaves that picture opaque; it never fails the colour decode.
Tests: vp9_webm_alpha_layer_matches_libvpx (frames out of order and forward, colour and
alpha exact against ffmpeg's libvpx decode; fails before: no alpha plane; passed 200 of
200 runs with four in parallel), set_alpha unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qq8BK1LmohzE1ga4NgAdWM
- marker_export test: build the Session with struct-update syntax (clippy field_reassign_with_default, #454)
- es/ja catalogs: Export Markers as CSV… (#454) and the gradient fill labels (#514); drop the duplicate
"Gradient" row #514 added to es.tsv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging with the left button from empty space in the Project panel draws a
selection rectangle, and every clip whose row or card it touches is selected,
live. In Icon view the gaps between cards count as empty space too, so a click
there deselects. A plain drag replaces the selection, Shift or Ctrl adds to
it, and Escape cancels and restores the selection the drag began with. The
selection goes through project.select; the rectangle registers
project.marquee.
Synthetic input (control channel, MCP, UI tests) now sends ModifiersChanged
around a pointer press and release that carry modifiers, so a drag that egui
recognises frames after the press still sees Shift / Ctrl, as with real keys.
This also lets automated Shift-drags of the Timeline marquee add to the
selection.
Limits: the rectangle does not select bins (the panel tracks one selected
bin), and in List view, where rows span the full width, it starts in the
empty space below the rows.
Tests: five project_panel_ui tests. The ui-egui suite passes except the two
Windows CRLF failures fixed in #488.
tests/setting.rs: keep the check against the backend register() reported
(main's fix expects VA-API on Linux, which NVDEC now sits in front of).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Translate all 347 Spanish/Portuguese menu keys plus Clip, Audio and Settings.
Add the Ukrainian language command, serialized UI state, native macOS
menu updates, and coverage and Cyrillic glyph tests.
Co-Authored-By: Codex <noreply@openai.com>
The parser's picture parameters were passed to cuvidDecodePicture unread.
Their shared head is now checked first: a null data pointer, zero length, no
slices, or a slice offset past the end of the data is an error, so the
session fails over to the software decoder and the driver never sees the
picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DIFAT guard came from the header's sector count and the chain guard from
the FAT length, both of which a file can inflate. A chain cannot have more
sectors than the file holds: bound both walks by that.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An index that declares a key size of 0 made the importer loop `count` times
(a u32 from the file) pushing an empty key each time. Reject a non-empty
collection with key size 0, and require that count * size fits in the index
stream before the loop starts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Timeline's timecode and the Program and Source monitors' current-time
timecodes were display-only. They now share one widget,
widgets::timecode_field, which replaces the unused timecode_label:
- Dragging scrubs that panel's playhead, 1 frame per point (Shift x10), and
stops at the start (for the Source monitor, the clip's start). The pointer
is hidden while dragging and put back where it was pressed.
- A click opens a text field with the whole value selected. Enter or clicking
away goes to the typed time through parse_timecode, so `1000` and `+15`
work. Escape cancels. An invalid time is reported in the status bar.
Left/Right on the selected value start from its last digit.
The Timeline and Program monitor use playhead.set, the Source monitor
source.setPlayhead.
Tests: timecode_ui (5 tests). The ui-egui suite passes except the two
Windows CRLF failures fixed in #488.
Selecting a nonempty bin reported success with items:[bin] while writing
an empty project: closure() kept the bin id (which names no item) and
project_subset() then pruned everything. Expand a named non-root bin to
the items it holds, as project.delete does, so only real items are kept
and reported. An empty bin now returns an actionable error instead of a
misleading empty destination.
Route audio controls, meters, effect graphs, timeline controls, caption lanes, and effect badges through theme tokens so Light mode no longer retains dark-only surfaces. Add palette regression coverage and a three-theme mixer harness test while preserving Dark and Medium colors.
Co-Authored-By: Codex <noreply@openai.com>
`captions.list` resolved `track` with track_param and treated an
unresolved selector the same as an omitted one, so a missing numeric id
(or a label past the last track, such as `C3` with two tracks) returned
every caption track with success. It now fails with "no such caption
track", as captions.export, setTrack, setStyle, deleteTrack and add do.
Omitting `track` still lists every track, and valid ids and C-labels
still return just their track.
Test: two caption tracks; each id and each label returns only its own
track; a missing id, `C3`, `C0` and a non-label string are errors.
Fixes#545
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a second Linux decoder backend, crates/platform/src/nvdec, that talks to
NVIDIA's driver directly (libnvcuvid + the CUDA driver, both loaded at run
time). It is registered in front of VA-API: what NVDEC declines still reaches
VA-API, then the software decoders. H.264 8-bit and HEVC 8/10-bit, 4:2:0
progressive.
NVIDIA's parser takes Annex B access units, so there is no host-side DPB.
Pictures are read back as NV12 / P016 through the existing biplanar path and
are bit-exact with the software decoders. The CUDA context is the one
nvenc::device already retains; it gains push/pop and copy-to-host. A seek
recreates the parser and keeps the decoder.
Also fixes tests/setting.rs, which expected the Media Foundation backend name
on Linux, and updates the Hardware decoding tooltip and its Spanish row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graphics.newShape, newRectangle, newEllipse and newPolygon already take
`seconds`, `track` and `time` through place_video_clip, as graphics.newText
does. Their parameter docs (what command_list shows) and the table in
docs/graphics.md listed only position, size and clip; they now list all of
them and say that position is the shape's centre.
place_video_clip takes the id of the command that called it for its errors.
It named graphics.newText in every placement error (V3 is not free here),
whether a shape, a file, a template or an upgraded caption had asked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A choice parameter takes its option name as well as its index, matched
without case, as transition parameters and graphics.set already do.
"keyframe": true writes a keyframe at `time` and animates the parameter (the
stopwatch and the value in one step, like clipMixer.set). Without it nothing
changes: a parameter that is not animated gets a static value whatever the
time. A parameter that cannot be animated refuses the keyframe.
The result is {"keyframes": n} instead of null, so a caller can tell that a
value sent with a time became a static value (0) and not a keyframe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While files are dragged over the window from the OS, the Timeline shows a
placeholder at the snapped drop point on the row under the cursor, labelled
with the file count. The cursor is polled each frame because the OS sends no
motion events during such a drag.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
winit on Windows sends no cursor events while files are dragged in, so the
pointer was stale and the drop was never placed on the timeline. The desktop
app now passes a host hook that reads the cursor from the OS (GetCursorPos),
which the UI maps into window points.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Dropping OS files on the Timeline panel now imports them and places them
(overwrite) at the pointer, snapped, chained end to end. Controlled by
Settings > Timeline > "Place files dropped onto the Timeline directly on
the Timeline" (timeline.dropImportToTimeline, default on). Drops elsewhere
stay a plain bin import.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Colour parameters in Effect Controls (the Ultra Key key colour, Color Key, mattes...) get a pipette
button. Armed, a click in the Program monitor fills the parameter with the colour under the pointer
(5x5 average, so a noisy green screen gives its true green); Esc disarms.
The colour is read from a render of the sequence at the playhead with the armed effect switched off,
so an Ultra Key that already removed the green can still pick it. The value is stored the way the
colour picker beside it stores it, through effects.setParam (one undo step).
When linking a selection containing multiple video clips, create one link
per video clip with its overlapping audio clips rather than a single link
for all clips, matching Premiere Pro behavior.
Stops were mixed in encoded sRGB, and any run with its own fill turned the whole layer into a solid colour. The ramp now blends in linear light, turns with the layer, and leaves a per-character fill solid.
With one enabled and one disabled clip selected, Clip > Enable (Shift+E) set every
selected clip to the same state: the first press enabled both, the next disabled both.
Premiere flips each clip separately (confirmed by the reporter in #484), so a mixed
selection swaps. Each clip in the selection (with its linked clips) now flips on its
own; a clip named twice in the parameters still flips once.
Test: enable_flips_each_selected_clip (fails before the fix: the mixed press left
both clips enabled).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qq8BK1LmohzE1ga4NgAdWM
Appearance › Fill Type paints a ramp across the layer bounds, and the ramp turns with the layer. A per-character fill colour still paints those characters as a solid.
Adobe's Change transition duration page shows the Trim-In / Trim-Out icon on a transition's end
in the Timeline: the drag trims that end, and the other stays. A centred transition dragged by one
end therefore becomes Custom Start, rather than growing on both sides. The alignment rule ("how
the transition changes length depends on the alignment") stays for typed durations: Effect
Controls' Duration and Set Transition Duration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timeline: clicking a transition selects it (outlined; Shift adds / removes) and opens it in Effect
Controls; Delete removes it. Its ends are edge zones: dragging one changes the duration (a Center
at Cut transition grows on both sides and stays centred, any other keeps its other end), dragging
its middle slides it over the cut (Custom Start); each drag is previewed and committed as one
sequence.setTransition. Double-clicking it opens Set Transition Duration.
Effect Controls for a selected transition: Duration (timecode), Alignment (Center at Cut, Start
at Cut, End at Cut; Custom Start shown when dragged off-centre), Reverse, its own settings
(numbers, angles, choices, colours, points, checkboxes), Reset. Dragging a value is one undo step.
Tests: transitions_ui.rs drives it all over the control channel (select, Effect Controls, the Delete
key, undo, edge and middle drags, the Alignment menu, the dialog, Push's settings), with
FILMCRAFT_UI_SNAPSHOT_DIR snapshots. Spanish for the two new strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`path_to_file_url` treated `\\?\C:\x` (the form `canonicalize` returns on
Windows) as a UNC share named `?`, so FCP7 XML exports on Windows wrote
`file://%3F/C:/...` and Premiere opened the media offline. Drop the
`\\?\` prefix (and map `\\?\UNC\server\share` to `//server/share`) before
building the URL. Verified: the exported XML now imports into Premiere
Pro 2026 on Windows with all media online.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- filmcraft_edit::transitions: where an applied transition may sit (over its cut, within the
clips it joins; a fade to or from nothing keeps its clip edge), its alignment as Effect
Controls shows it (Center at Cut / Start at Cut / End at Cut / Custom Start), set_span, remove,
and drag_span for the Timeline (an end changes the duration, the middle slides it).
- EditorState::transition_selection; timeline.select {"transitions": [...]} replaces the clip,
edit point and caption selections, and a clip selection clears it.
- edit.clear (Delete) removes the selected transitions when no clips are selected;
sequence.removeTransition removes given or selected ones; Ripple Delete stays clip-only.
- sequence.setTransition also takes duration / frames (following the alignment: Center at Cut and
Custom Start move both ends, Start at Cut the end, End at Cut the beginning), align, start, and
merge / begin so a drag is one undo step. Out-of-range times are refused before any frame
arithmetic, atomically.
- sequence inspection reports each transition's alignment and the transition selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MP4 source reported the sample entry's size, only swapped for the display rotation, so two
crops were lost: the codec's own (H.264 SPS frame cropping, HEVC conformance window), which the
decoder applies but the reported size and sequences made from the clip didn't, and the clean
aperture (`clap`), parsed but never used. An iPhone clip (1920x1440 coded, 1744x1308 clean
aperture, -90 degree display matrix) showed 1440x1920 instead of 1308x1744.
H.264 / HEVC sizes now come from the stream's parameter sets; the clean aperture is a rectangle
inside that picture (ignored when it makes no sense), reported as the size and cut from every
frame by the GOP cache before the display rotation, through the new VideoFrame::cropped.
Tests: bit-exact against ffmpeg (which applies both crops) for an H.264 SPS crop, an HEVC
conformance window, centred and off-centre clean apertures and a clean aperture with a rotation;
clean apertures that make no sense are ignored without a panic; VideoFrame::cropped on every pixel
format with odd sizes and misaligned corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8-bit NVENC sessions (H.265 Main, hardware H.264) now take the export's RGBA
frames as ABGR input buffers and NVENC converts them to BT.709 limited 4:2:0
on the GPU, with the same codes as rgba_to_yuv420_8. The CPU conversion ran
on the encode thread, where it competed with the render of the next frames
for the thread pool (about 13 ms per 1080p frame instead of 1.5 ms) and held
NVENC back. A driver that refuses RGB input falls back to the CPU conversion
into NV12. HEVC Main 10 is unchanged.
60 s of a 1080p30 project as H.265 in the app: 18.7 s -> 15.0 s (96 -> 120
fps); headless 30.3 s -> 16.8 s.
Co-Authored-By: Claude <noreply@anthropic.com>
Review feedback on #246:
- Gate the install to dev runs: a debug build, or a release run with
FILMCRAFT_DEV_DESKTOP_ENTRY set (the documented cargo run --release flow);
never when APPIMAGE or FLATPAK_ID is set — an AppImage mounts at a fresh
/tmp/.mount_* each launch, so its Exec path changes every time, and packaged
installs ship their own entry and must not write into the profile.
- Escape Exec for both desktop-entry layers exactly like
packaging/linux/install.sh: shell layer first (` " $ \ get a backslash),
then the file-value layer (every backslash doubled), always double-quoted.
Binary paths with =, % or control characters are rejected instead of written
(install.sh rejects them for the same reason).
- Compare the Exec path exactly: parse the first Exec argument through both
layers instead of a substring contains, so /a/filmcraft-old no longer
counts as ours and a stale entry is rewritten.
- eprintln -> log::warn for the skip message (the logging module landed meanwhile).
🤖 Generated with [Kimi Code](https://github.com/storyteller/kimi-code)
Co-Authored-By: Kimi <noreply@storyteller.dev>
Add ReReddo's own entry to contributors/people.toml, display name only, as
AGENTS.md asks of contributors. The name is shown once the maintainers'
contributors.py run verifies it.
spanish_translates_every_tl_literal cuts each source file at
"#[cfg(test)]\nmod tests". On a Windows checkout with core.autocrlf=true the
files have CRLF, so the cut never matched. The scan then read the test module,
including its own assertion text, and failed with "use a plain string literal
in tl! and tlf!".
Normalise CRLF to LF when reading the sources. Checkouts with LF endings
(Linux, macOS, CI) are unaffected.
Tests: cargo test -p filmcraft-ui-egui --lib passes (71 tests). The test failed
before this change on a Windows checkout.
View > Appearance called the middle color theme "Medium" while Settings >
Appearance > Color Theme called the same choice "Dark". Use the Settings
name, which is also the stored value and the name in docs/project-files.md.
Only the menu label changes. Command ids (view.theme.dark/medium/light) and
stored values (darkest/dark/light) are as before. In pt-br.tsv the "Medium"
entry becomes "Dark" (Escuro), because that catalog may only hold menu labels.
Adds a settings_ui test that each View > Appearance label equals the matching
Settings dropdown label.
Tests: settings_ui and the i18n tests pass, except
i18n::tests::spanish_translates_every_tl_literal. It fails on Windows
checkouts with core.autocrlf=true (the source scan looks for "\n") and is
unrelated to this change.
The engine already keeps one playhead per sequence while a project is open, and the
project file's view already stores each sequence's zoom, scroll and track heights, but
not its playhead: after saving and reopening, every sequence started at zero.
ProjectView gains `playheads` (sequence id -> Tick), written beside the tabs and views
and restored with them when "Restore open sequences when opening projects" is on. Files
written before it have none and open as they did. Values from a file are not trusted:
ids that are not sequences are dropped, playheads are clamped to 0..ProjectView::MAX_PLAYHEAD
and snapped to a frame of their sequence.
Tests: each_sequence_reopens_with_its_playhead_where_it_was_left (fails without the
fix), hostile playheads in a_damaged_view_in_a_project_file_is_cleaned_up_or_ignored,
a deleted sequence's playhead is not written, and the format round trip carries it.
Also adds my contributor credit entry (people.toml).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qq8BK1LmohzE1ga4NgAdWM
Every VA-API session opened its own display and terminated it when dropped. With NVIDIA's driver
that is a CUDA context each: 0.2-0.3 s to open, 60-70 ms to terminate and about 100 MB of video
memory, on the thread creating or dropping the decoder (a frame worker at every clip change, the
UI thread when media is removed: a ~0.6 s freeze deleting five played clips, and dropped frames at
clip changes).
Sessions now take the process's shared display (the startup probe's, kept for the life of the
process like the libraries) and make only their own configuration, context and surfaces. Display
is Sync on the strength of libva's Multithreading Guide (va.h): its functions and drivers are
thread-safe, and every session uses only its own VA objects. A session that cannot be made on the
shared display is tried once on a new display, which is shared from then on if it works (a GPU
reset).
On NVIDIA (RTX 3060): deleting five played clips 650-690 ms -> 132-198 ms, dropped frames in 20 s
of playback 2-3 -> 0, peak video memory 1281 -> 1010 MiB. Intel and software unchanged.
Test: tests/vaapi_displays.rs (decoders one after another and six at once on six threads open no
display; pictures bit-exact). The VA-API hardware suite passes on NVIDIA and Intel, and the
concurrency tests 20 times each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like Premiere, the waveform in an audio clip now grows and shrinks with
its Volume level, keyframes included, so dragging the volume line down
visibly lowers the waveform. A bypassed or disabled Volume leaves it as
it was.
The dB readout next to the pointer resized itself on every value change
and sometimes wrapped into a column of single characters. It is now one
line in a monospace font with a fixed number format, so the box stays
put.
With the Selection or Pen tool the line on audio clips is now a grab
target of its own (Grab::Volume): dragging it up or down changes the
clip's Volume level live, as one undo step. With keyframes it moves the
two keyframes around the pointer, like Premiere. The Pen tool or a
Cmd/Ctrl-click adds a keyframe on the line; keyframes drag in time and
level and right-click deletes them.
A plain click on the line still selects the clip and a right-click
still opens the clip menu, since presses go through the timeline's own
hit-testing.
The white line on audio clips was drawn at the level of the first frame
only, squeezed above the waveform, and not at all with waveforms off.
It now spans the clip under the name on the track fader scale, bends
with Volume keyframes and shows them as diamonds. Tracks showing a
track keyframe lane hide it.
effects.setKeyframe and effects.moveKeyframe take merge/begin like
effects.setParam does, and moveKeyframe can set the value on the way
(same as mixer.moveKeyframe). The timeline volume line needs this to
update keyframes live while dragging without flooding the undo history.
Same as Effect Controls: with a video clip and its linked audio selected
the Audio section (Level, Pan) now edits the audio clip instead of being
hidden behind Transform and Crop.
Selecting a video clip with linked selection on also selects its audio,
but the panel only listed the video effects. Now the linked audio clip's
Volume, Channel Volume and Panner follow under an Audio heading, like in
Premiere.
After a hardware decoder failed, the replay of the run through the software decoder stopped at the
first sample the software decoder rejected (`?` in the replay loop): the pictures it had already
output were dropped, the rest of the run was never fed, the error was returned for the failed
sample even when the software decoder accepted it, and the replay log was not restored. That
happens when the GPU tolerated a damaged sample our decoder rejects and then fails later in the
same run; one picture went missing on a damaged HEVC file through VA-API (Intel).
The replay now behaves like software decoding from the restart point: a rejected sample is skipped
(logged when the hardware had already decoded it); if the failed sample itself is rejected its
error is this call's, and the pictures replayed before it are kept in `carry` for the next output.
Regression test (every platform, no GPU): a stand-in hardware decoder that decodes one damaged
sample the software decoder rejects and fails at that sample or later; every software picture
comes out once and in order, errors are reported where the software decoder reports them, and the
pictures from the next restart point are the software decoder's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Transcript / Captions / Graphics tabs were laid out at 7 px per
character. That suits Latin text, but Japanese glyphs are about twice as
wide, so with the interface in Japanese the labels ran into each other.
A tab is now as wide as its label is drawn when that is wider than the
old estimate, measured in the semibold the active tab uses so selecting a
tab does not move its neighbours. Latin labels keep their old widths.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ja.tsv now has the same 3,466 entries as es.tsv (menus, panels, dialogs,
the Settings pages, the Effects registry, Keyboard Shortcuts, tooltips and
status messages) instead of 89 menu-only entries.
The 46 old rows that came from PhotoCraft/PDFCraft and match no FilmCraft
string are dropped. Five rows that were wrong for a video editor are
fixed: Horizontal/Vertical are 水平/垂直 (every use is geometry, not
writing direction), Orientation is 向き, Type is 種類 and Effect is
エフェクト. The other 38 rows that were already in use are unchanged.
ROADMAP.md and crates/ui-egui/README.md now list Japanese next to Spanish
as covering the whole interface.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A 1440x1080 clip with 4:3 pixels showed squeezed: the demuxers read its
pixel aspect ratio, but nothing after import used it. New Sequence From
Clip made a 1440x1080 sequence with square pixels, the Motion transform
sized clips from storage pixels (pillarboxed at 1440 wide in a 1920x1080
sequence), and the monitors drew the frame at its storage aspect.
- project: checked_par / sane_par / par_ratio / conformed_size treat
file and project ratios as hostile (a zero term, or more than 8:1
either way, is square). MediaClip::pixel_aspect (Interpret Footage
wins over the file), Project::source_par (subclips follow the parent,
nested sequences have their own, graphics and adjustment layers have
none) and Project::conformed_source_size.
- render: motion_matrix takes the source's ratio and works in square
display units (anchor in source pixels, position in sequence pixels,
scale and rotation at the display aspect); Scale to Frame Size fits
the display size. CPU, GPU plan, adjustment regions and the mask /
graphics overlays share it.
- engine: New Sequence From Clip, New Sequence from an item and
multicam sequences copy the clip's ratio; default media scaling and
Fit / Fill frame use the display size; generators made for a
non-square sequence get its pixels; transcodes render pixel for
pixel. clip.interpretFootage takes pixelAspect ([num, den] or "file").
- ui: the Source and Program monitors draw the picture at its display
aspect; overlays map both axes; Sequence Settings shows the display
aspect (16:9 for 1440x1080 at 4:3).
Tests: render par_tests (fill, GPU plan parity, Scale to Frame Size,
non-square sequences, rotation, Interpret Footage, hostile ratios),
engine par_tests (New Sequence From Clip, overwrite into 1920x1080,
default scaling, Fit / Fill, Interpret Footage with undo and refused
values, hostile file ratios, generators, and ffmpeg-made MP4 and MKV
files), project unit tests, and the headless par_ui monitor test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The interface became translatable since this branch started. The edit point menu now draws its
labels (and automation labels) in the interface language like the clip menu: the trim types
through i18n::t, Apply Default Transitions and Join Through Edits with tl! (both already in
the Spanish catalog). Spanish entries for the five trim types use the catalog's existing
vocabulary (recorte de rizo, edición de rodillo, recortar), and the trim types are listed with
the clip menu's labels in the test that requires Spanish for every such name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in the timeline's right-click handling (crates/ui-egui/src/panels/timeline.rs):
#220 now remembers the right-clicked clip so Unlink keeps only it selected, and this branch
opens the edit point menu on a clip's edge. Both are kept: the clip is remembered on every
right-click, and an edge also stores the edit point menu's target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delegate HEVC VUI timing to NVENC's configured frame rate instead of the
explicit timing pair reversed by the affected driver. Preserve existing
stream quality/timing assertions and cover integer and fractional rates.
Normalize line endings in the translation test's source scanner so it
excludes test modules in Windows checkouts as well as LF checkouts.
Assisted-by: Codex
ffmpeg writes MP4 without a `colr` box unless asked (`+write_colr`), and
color_from only read `colr` (or vpcC / apvC), so every such file fell back
to BT.709 limited: an HEVC PQ file was detected as rec709 / hdr false, a
full-range H.264 file as limited range, and BT.601 SD with BT.709
primaries. Decoded frames were right (the decoders read the VUI); the
media info, HDR detection and Interpret Footage defaults were not.
Matroska without a `Colour` element had the same gap.
New `stream_color` reads the colour a codec configuration carries in its
bitstream with the decoders' own parsers: the VUI of the SPS in avcC /
hvcC and the color_config of the AV1 sequence header in av1C. Precedence:
`colr` (nclx / nclc) or Matroska `Colour`, then vpcC / apvC / the
bitstream, field by field (what the container leaves unspecified falls
through), then the existing size defaults (BT.601 matrix for SD, BT.709
otherwise). nclc has no range flag, so the bitstream's range now stands;
an ICC `colr` no longer hides the bitstream's code points. A truncated or
corrupt parameter set gives no colour and the defaults.
Tests build avcC / hvcC / av1C bit by bit: HEVC PQ → rec2100-pq, full-range
H.264 → Full, BT.601 SD (NTSC and PAL) primaries, unspecified VUI keeps the
defaults, colr wins, Matroska without Colour, and every truncation and
single-bit flip of each configuration under catch_unwind. An ffmpeg-oracle
test checks real x264 / x265 files against their `+write_colr` remux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Play audio while scrubbing in Source and Program Monitors" (audio.scrubAudio,
Shift+S) was saved and toggled but never read. Now, while the playhead moves
with a mouse button down and playback is stopped, an 80 ms grain of the
sequence's sound (4 ms fades at its ends) plays from the new position.
It is detected once per frame in the app loop, so the Timeline ruler, the
monitors' scrub bars and dragged timecodes all scrub audibly. One output
stream stays open for the whole scrub and each move only re-aims it; it closes
shortly after the button is released, when the setting is off, or when
playback starts and takes the device.
Rebased on main, where Source monitor playback (source_playback) arrived:
it owns the audio device while it plays too, so a scrub stream still
lingering when Source playback starts no longer stops its sound on release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two share code points; putting the Chinese faces first drew Japanese UI text's kanji
with Chinese shapes. Also clears the test's texture deltas (epaint 0.36 panics on
dropping them unapplied).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rejecting the whole proxy lost proxy playback for every multi-track recording
(and video-only proxies of media with audio); only the audio needs the original.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File ▸ New ▸ Sequence… (Cmd+N) and the Project panel's New Item ▸ Sequence…
made a sequence with the default settings at once. They now open the
Sequence Settings dialog (#355) as New Sequence (`open_new`): the General
tab from the default settings, named like file.newSequence would name it,
and a Tracks tab with the number of video and audio tracks. OK runs
file.newSequence with them; drop-frame timecode and Maximum Render Quality,
which the command has no parameter for, follow with sequence.settings when
they aren't the defaults. Making a sequence from a clip, or by dropping one
on an empty timeline, still passes params and stays direct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #355, as suggested in #225: the dialog's General tab gets a
Sequence Name field, filled with the sequence's name when it opens. OK
sends `name` to `sequence.settings` (which already accepts it) only when it
changed, so renaming is part of the dialog's single undo step; a blank name
keeps the old one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained NVENC H.265 Main / Main 10 (#328, #360), export stage timings
(#370) and VA-API decoding (#358). Resolution: main's NVENC session, FFI and
export factory, with this PR's changes ported onto them:
- ffi: the portable Guid (now also for the HEVC GUIDs), NV_ENC_DEVICE_TYPE_CUDA;
- session: device.rs (Direct3D 11 / retained CUDA primary context), the
Linux libnvidia-encode.so.1 loader, Device::KIND;
- export: factory_with and its tests, the even-size check, the saturating
max bitrate; H.265 through NVENC stays Windows-only (the factory declines
it on Linux, where nothing probed or tested it);
- Cargo.toml: main's Linux libloading dependency covers both users;
- docs: README / ROADMAP / architecture now say NVENC H.264 runs on Windows
and Linux; ADR and platform README keep both addenda / test rows;
- abi_tests: note why the MSVC-generated expectations hold on LP64 Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#238's i18n landed on main; its catalog-coverage tests need Spanish for
the new Loop marked range checkbox and the Source play/stop command names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NVENC export: keep main's Main 10 / Main dispatch and time both colour
conversions (rgbf_to_yuv420_10, rgba_to_yuv420_8) as Stage::Convert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A clip with audio_stream >= MAX_AUDIO_STREAMS is reset to stream 0 and media
with more than MAX_AUDIO_STREAMS streams keeps the first ones, each with a
warning: one bad index must never lose a whole project.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps #359's contiguous_starts audio timing, now per stream in Mp4Audio /
MkvAudio (starts and preroll in source sample frames). MP4 audio streams and
their decoders are built from the same (track, info) pairs so they stay
index-aligned. Adapts new main code to audio_streams / TrackItem::audio_stream.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Root's MetaDictionary was written with no properties. pyaaf2 loads
its ClassDefinitions and TypeDefinitions sets to merge them with its
built-in model and refused every file: "cannot find index stream:
ClassDefinitions-3 index".
The MetaDictionary now has both sets (pids 0x0003, 0x0004, keyed by the
definition AUID 0x0005), empty: they only carry extensions to the
baseline model and everything FilmCraft writes is baseline. An empty
strong-reference set also writes its real key size (16 for AUIDs, 32
for MobIDs) instead of 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
encode / encode_10 compute the plane sizes they check with saturating_mul;
fill_p010, rgbf_to_yuv420_10 and the HDR export test use as_chunks instead of
chunks_exact with a constant size (clippy::chunks_exact_to_as_chunks).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Unix the benchmark measures the test thread's CPU time, so suite load
does not move it and it keeps running. Without a per-thread CPU clock
(Windows) it falls back to wall time, which is load-dependent and flaky on
shared CI runners; ignore it only there and document how to run it.
The GPU effect-source hunks were dropped in the merge in favour of #376's
fs_fx_source, now on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copyTo that never settles kept copying > 0 and bypassed the stale timeout,
wedging the session; now busy requires recent output. plane() computes each
row's start/end with checked arithmetic and bounds it by the validated end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Translate the new Sequence Settings dialog (labels, sections, tabs,
values, hints), the shared keyframe navigator's tooltips, the
non-macOS tool and mask hints, Include Alpha Channel and the export
status. Update the hardware decoding description to main's wording.
Automation ids and labels stay English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pool::trim() from Pipeline::drop flushed the whole process-wide pool after every
render-preview segment and proxy, and threw away playback's U8/U16 planes too.
Now Pipeline::drop calls pool::trim_f32() only when !settings.part_of_batch.
PoolStats gains f32_idle_bytes so the tests can check the float shelf alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Gentoo paragraph sat after the build-from-source and control-protocol
notes; it belongs with the other Linux install options. Mark it clearly as
community-maintained.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forcing DX12 compiles shaders through FXC, which rejects fx.wgsl (silent CPU
fallback), and leaves machines without DX12 with no backend. Leaving GL out is
what avoids the AMD atio6axx.dll crash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainers chose #337's handling of Ctrl/Cmd shortcut collisions off
macOS (drop_control_collisions / audit_for / builtin_for), which audits the
collisions and keeps documented winners rather than keeping the first listed
binding. This reverts crates/engine/src/shortcuts.rs and its tests to main
so the two approaches don't overlap; the MPEG-oracle ffprobe-JSON change and
the frame-pool test rewrite from this PR are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release bundle manifest installs filmcraft and filmcraft-cli; the
from-source manifest built and installed only the desktop app, so a
Flatpak built from source had no headless CLI or stdio MCP server.
It now builds -p filmcraft -p filmcraft-cli and installs
/app/bin/filmcraft-cli. The default command stays filmcraft; the CLI
runs with `flatpak run --command=filmcraft-cli ai.storyteller.filmcraft`
(the manifest header and docs/releasing.md show it). Runtime, SDK and
finish-args are unchanged.
packaging-lint now also checks that both manifests install the same
programs, {filmcraft, filmcraft-cli}; it fails on the old manifest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts: keep main's macOS/other hint text and mask vertex
tooltip variants (each wrapped in tl!), take main's shared keyframe_nav
helper and translate its tooltips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merlin's effects-heavy bench scene (GPU3c) settles it: with layers and
effects the GPU compositor exports 2.4x faster at 1080p (13.0 -> 5.0-5.6 s,
737 -> 238 CPU ms/frame) and 3.2x at 4K (38.7-39.3 -> 12.0 s, 2955 -> 775
CPU ms/frame); a single plain clip takes the same time either way
(decoding / encoding bound). Every Auto row rendered all its frames on the
GPU, without fallbacks. The CPU reference renderer stays the fallback for
anything the GPU stage does not run.
Trade-off: the GPU matches the CPU within the compositor's parity
tolerance, not bit for bit, so an export's bytes can depend on the
machine's GPU; Off keeps them byte-reproducible (the determinism tests
register no GPU renderer and are unaffected).
- settings: GpuRendering::Auto is the #[default]; ExportSettings::default.
- test: the default, settings saved without the field, and "off".
- docs/performance.md: GPU3 results (1080p and 4K).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add CPU parity test verifying FxOp::Lumetri::apply matches original fn lumetri
exactly (0.00e0 sample difference) across all parity cases. Restore doc comments
on fn on and fn grade_space.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)
Port the common slice of Lumetri Color (Basic Correction, Creative without
sharpen, and Vignette) to the GPU effect stage (FxOp::Lumetri, OP_LUMETRI = 31).
Lumetri instances with unsupported sections (input LUT, Look / look LUT,
sharpen, curves, color wheels, HSL secondary, or HDR grading) fall back to CPU.
Parity tests pass within max relative difference 4.28e-7 and 0.000% flips.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)
The previous commit raised the GPU's un-premultiply threshold for Invert
(alpha channel) to 1e-4, which the CPU reference does not use (it is
1e-6): alphas between the two inverted differently. The 6 % flipped
pixels came from the bilinear leak the same commit fixed in load4; with
it, the CPU's threshold passes gpu_effects_match_cpu_exactly (and the
gpu and golden suites, serially).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix subpixel bilinear interpolation leakage during 1:1 source texture
sampling in composite.wgsl load4, and guard against divide-by-zero /
subpixel noise amplification in OP_INVERT_ALPHA in fx.wgsl.
All fx_tests::gpu_effects_match_cpu_exactly test cases now pass on AMD
(Radeon RX 7900, Mesa 26.2.2, Vulkan) within EXACT (1e-4) and 0.000% flips.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)
Move Video Limiter from CPU (crates/render/src/vfx/color.rs) to the GPU
compositor (filmcraft_render::gpufx::FxOp, crates/gpu/src/fx.rs, crates/gpu/src/fx.wgsl).
Parity matches CPU reference within 9.28e-6 across all axis modes, clip levels,
compressions, and gamut warning color.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)
Add Vignette effect to the GPU effect stage (render::gpufx, gpu::fx,
fx.wgsl). Evaluated parameters are shared between the CPU reference and
GPU compute passes. Replaces old CPU vignette body in vfx::color with
gpufx::FxOp::eval and gpufx::FxOp::apply.
Gates: fmt, clippy, test -p filmcraft-render, test -p filmcraft-gpu,
layers, wasm all verified.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)
GPU export rendering was ~20 % slower in wall-clock time than CPU
rendering: one renderer behind a mutex made the export workers queue.
- Export workers share a pool of up to four renderers on one wgpu device
(one per worker was no faster: the read-back then contends for memory
bandwidth).
- render_export_prepared composites into the linear accumulator and
copies it to a reused staging buffer in one submission (no sRGB resolve
pass); read_accumulator shares the read-back, which now returns None
instead of stale pooled values when a row is missing.
- f16_to_f32_fast: same result as f16_to_f32 for all 65536 inputs (test).
- Timing counters (export::export_lock_wait, gpu::export_renderer::timing)
printed by the export bench.
RX 7900, same build, Off / Auto / Off: H.264 + AAC 4.67 / 4.71 / 4.76 s,
ProRes 422 4.20 / 4.21 / 4.21 s; CPU per frame 287-292 -> 277 (H.264),
316-317 -> 300 (ProRes). Was 4.86 -> 5.78 s. Parity, not faster: on this
single-clip scene decoding and encoding dominate (rendering is ~15 of
~290 CPU ms per frame), so GPU rendering stays Off by default.
Gates: fmt, clippy (gpu, export, ui-egui), layers, assets; gpu
export_pipeline, golden and export tests pass. Pre-existing on the
previous commit: fx_tests::gpu_effects_match_cpu_exactly fails on this
AMD GPU, and the gpu unit tests segfault in the driver when run in
parallel (pass serially apart from that test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set GpuRendering default to Off in ExportSettings so exports use the CPU
reference renderer by default until GPU export achieves lower wall-clock time.
🤖 Written by agy (Antigravity Agent, Gemini 3.8 Flash)
Add off-screen GPU export renderer (filmcraft-gpu::ExportRenderer) reading back
the linear float accumulator as premultiplied RGBA f32, plug it into the
export pipeline via FrameRenderer hook and GpuRendering setting, register
in desktop app and CLI, and record export.gpu_frames statistics.
🤖 Completed by agy (Antigravity Agent, Gemini 3.8 Flash) based on draft by Merlin
Caption blocks chose move / trim in / trim out where egui recognised the drag, about 9 px in
from a press 2 px inside the edge and past the 5 px edge zone, so they moved instead of
trimming. They now decide at the press origin and measure the drag from it. UI test that
failed before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hovering a clip edge draws the edit-point bracket at half strength on the edge a press would
grab: red for a ripple; yellow for a trim, roll, rate stretch or remix; on both sides of the
cut for a roll. It follows the tool and the modifiers, from the same grab_at as the press.
The bracket drawing is shared with the selected edit points (trim_monitor::paint_bracket).
An ignored test renders the four cases at a demo cut to PNGs for review:
cargo test -p filmcraft-ui-egui --test timeline_trim_ui -- --ignored hover_bracket_screenshots
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reversed clip's source time is the instant just before its mirrored
position (source_out - rel - 1 tick). The MP4 reader rounds a request to
the nearest unit of the track timescale before picking the frame at or
before it, so one tick below a frame boundary landed on the next frame:
a clip trimmed to source frames 10-29 played 30 down to 11.
video_source_time snaps a reversed clip's video request down to the
start of the media frame that contains it (clamped to the source In and
the unsnapped time). Both places that ask a source for a clip's picture
use it: media_frame and the plan path. source_time_at is unchanged for
audio and nested sequences, and forward clips and frame holds keep
their request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pressing a clip edge and dragging inward selected and moved the clip: the press was hit-tested
only once egui recognised a drag, 6 pt later, by when the pointer had left the 7 px edge zone.
And at a cut the left clip's Out edge claimed both sides, so the right clip's In edge could not
be grabbed.
What a press grabs is now worked out on the frame the button goes down (timeline_hit::grab_at,
shared by the cursor, the press and ui.timeline.hit) and acted on by both the click and the
drag. Inside a clip beats outside it, so the side of a cut picks the clip; zero-width clips
have no edges. Trims, rolls, rate stretches and remixes move by how far the pointer moved since
the press, and a moved clip stays under the point it was grabbed by. ui.timeline.hit takes
`modifiers` and reports `kind`, the trim a press would start.
Unit tests for the zones and tools; headless UI tests that drag a pixel or a few per frame at
60 fps (before: the clip moved, the wrong clip was trimmed, a wobbly click selected the clip, a
moved clip lagged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings > General > Interface Language gains System Language
(`system`), now the default: a new installation shows the first of the
user's preferred languages that the interface has (es-419, pt_BR.UTF-8,
ja-JP...; any Portuguese gets pt-br), otherwise English. A language
already chosen, or saved in preferences.json, is kept.
The host supplies the preferred languages through
HostHooks::system_languages: sys-locale on the desktop (LANGUAGE,
LC_ALL, LC_MESSAGES, LANG on Linux; the OS lists on macOS/Windows) and
navigator.languages on the web. Without the hook (tests, headless),
System Language is English, so tests do not depend on the machine's
locale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
write_clip rounded start, end, in and out to frames independently, so
a clip placed by seconds at 23.976 (start 0, in 20 s, 3 s long) was
written as start 0 / end 72 / in 480 / out 551: 71 source frames for
72 record frames.
At normal speed and on a freeze frame the source span is the record
span, so out is now in + (end - start). Speed-changed clips keep their
independently rounded out. The master duration is raised to out when
rounding pushes out past it; pproTicksIn/Out still carry exact ticks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A menu entry runs its command with no parameters, so every File >
Export entry whose command needs a `path` failed with "need `path`",
from the egui menu bar and the native macOS menu alike.
- Media… opens the Export mode, like Cmd+M (Premiere's File > Export >
Media); with a `path` it still exports directly.
- EDL, Final Cut Pro XML, FCPXML, OpenTimelineIO, Avid Log Exchange and
Selection as FilmCraft Project ask the host's save panel for the
destination, suggest the sequence's (or project's) name with the
right extension, then export. "Selection" suggests "<project>
selection.fcproj" so it does not offer to overwrite the open project.
Cancelling writes nothing and reports no error.
Disabled entries (no sequence, no selection) still fail before any
dialog opens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rfd hands filter extensions to the XDG portal, GTK and Zenity as
case-sensitive `*.ext` globs, so `CLIP.MP4` and `clip.Mp4` were hidden.
`file_filters::extensions` keeps the given extensions first (the save
dialogs' default suffix) and, outside Windows and macOS, adds one
bracket pattern per extension (`mp4` -> `[mM][pP]4`), the form the
portal's FileChooser docs recommend.
Every filtered dialog uses it: import, relink, Open file, Open
Project, Save Project and Save As.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tarball now ships install.sh. It installs filmcraft, filmcraft-cli,
the desktop entry, icons, MIME type, metainfo and docs under ~/.local
without sudo; --prefix takes any absolute path (sudo ./install.sh
--prefix /usr/local for all users). Running it again updates.
The desktop entry's Exec/TryExec point at the installed binary's
absolute path, escaped per the Desktop Entry spec, so the menu entry
works without ~/.local/bin on PATH. The script checks the bundle is
complete before writing, refuses /, prefixes inside the extracted
bundle and characters launchers cannot express (=, %, control
characters), and treats the cache refresh tools as optional.
packaging/linux/test_install.py covers install and update, launching
through gio, bad arguments, an incomplete bundle, --help and a real
package.sh tarball build; Packaging lint runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Right after importing many 4K clips the Project panel and the timeline ask
every clip for a thumbnail. Up to six frame workers decoded them at once,
each with a frame-threaded software H.264 decoder (16 threads and 18
pictures in flight here, about 0.5 GB at 4K), and the decoders stayed: 42
4K clips with hardware decoding Off took the process past 6 GB during import.
Thumbnail jobs (item frames at the Media Browser / thumbnail priorities) now
run under filmcraft_media::cancel::with_background: H.264 decoders made for
them use 2 threads (thread_limited), parallelism comes from the workers
decoding different files, and the decoders of caches only thumbnails used
are released after each thumbnail (the clip served last keeps its own). A
monitor or playback request on such a cache replaces the small decoder with
a full one. Sequence frames are never background (the scopes ask for the
program frame at 40 while playing).
42 4K clips (Windows), peak private bytes: hardware Off 6.1-6.8 GB (killed)
-> 3.1-3.2 GB; hardware Auto 2.7-2.8 GB (#381) -> 2.3-2.4 GB. Thumbnails of
12 / 42 clips ready in 0.6 / 2.2 s instead of 2.2 s / not finishing.
Example crates/ui-egui/examples/thumb_burst.rs reproduces the burst.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cache asked for a frame within IDLE (3 s) counted as in use, and the caps
of #41 (4 live decoders, 1 GiB of frames) only bound caches idle for longer,
so moving the playhead across 42 4K clips kept 42 hardware decoders (6.5 GB
of VRAM) and up to 3.9 GB of frames. Now only caches touched within RECENT
(500 ms) are protected: caches idle 0.5-3 s give up decoders beyond
HARD_DECODERS (8) and frames beyond the shared budget, least recently used
first; the 3 s rule down to 4 decoders is unchanged. store() refreshes
last_used.
Measured with a 42-clip 4K reproduction (Windows, Media Foundation): peak
private bytes 6.6 -> 4.0 GB, frame cache 3.9 -> 1.1 GB, live decoders
42 -> 8-14, GPU memory 6.5 -> 4.3-4.6 GB, 0 dropped frames before and after.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Draw effect sources from working pixel positions and integer decimation.
Cover identity, alpha inversion and box minification without changing tolerances.
Co-authored-by: Codex <codex@openai.com>
The DMG opened as a plain Finder window. It now shows the app icon's figure on
the FilmCraft colour, with FilmCraft.app and the Applications link side by side.
Same approach as PhotoCraft's and VectorCraft's (storytold/photocraft#1201,
storytold/vectorcraft#493).
- packaging/macos/dmg/: background.svg (source; text outlined, no paper grain,
the figure an <image> of assets/app-icon), background.tiff (1x + 2x HiDPI,
16-colour palette, Deflate, sRGB; ~106 KB)
and dmg-layout.DS_Store (Finder view settings: window, icon size 128,
positions, background).
- generate.py writes the TIFF and the layout on any OS, byte-for-byte
reproducible, with nothing from the machine that ran it (no paths, users,
disk names or volume UUIDs).
- package.sh copies them into the staging folder; the image is still built with
hdiutil makehybrid. Nothing extra runs in the signing job.
- The volume name drops the version: the layout finds the background through
an alias that includes the volume name. The DMG file name keeps the version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A step now renders batch k+1 on the rayon pool while the calling thread
encodes, muxes and mixes the audio of batch k. The file is byte-identical
with or without overlap. A prefetch that hits pending media is dropped and
rendered again, cancel frees it, a render panic becomes an error, and wasm
never overlaps. perf.stats export.stages gains waitMs.
Memory: up to 2 x batch x frame bytes of finished frames are in flight, so
the batch is capped to a 512 MiB budget (1080p keeps 16, 4K SDR 8, 4K HDR 2
frames). Measured peak RSS vs before: 1080p +67..+158 MB; 4K SDR -535..-927
MB; 4K HDR about -2.6 GB (5.2 -> 2.6 GB). Wall time 1.06-1.16x faster.
Known pre-existing web issue, untouched: AudioOut::pull advances before the
pending check, so a retried audio group is lost.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
filmcraft_export::Stage, stage_times(), note_stage() and timed(): the wall
time of setup, loudness, render, encode (and its RGB->YUV convert part),
audio, mux and finish, summed per process like export.hardware. Converters
in the H.264 / ProRes / NVENC / VideoToolbox encoders are timed as Convert.
Two clock reads per stage per batch; the exported file does not change
(determinism tests). Test: an export grows every stage counter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve inherited physical key collisions when loading built-in presets,
while preserving established explicit winners and deliberate custom conflicts.
Read every ffprobe display-frame timestamp and register the requested credit.
Verify exact frame-plane reuse without depending on concurrent global metrics.
Co-authored-by: Codex <codex@openai.com>
remove_partial treated any `<stem><digits>.<ext>` file as a still of an image sequence, so
cancelling an export to `clip.mp4` also deleted a `clip1.mp4` written in the same second (or
during the export). Only PNG/TIFF/BMP sequence exports write numbered stills, and their frame
numbers have at least three digits (`image_sequence_path`); match only those.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Runtime-load the NVIDIA encode and CUDA driver libraries, retaining the primary context through session destruction. Preserve the Windows Direct3D path and default-off selection, and test driver rejection and deterministic software fallback.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Cancel the export draft when clicking outside the dialog, including top menus and mode tabs, while allowing the background control to receive the click. Keep the dialog's own format and depth dropdowns interactive.
Cover dismissal for both monitors, mode switching, dropdown selection, and background monitor clicks in a UI regression. No frame is exported when clicking away.
Assisted-by: Codex
When the format changes the extension (APV into `out.mp4` writes
`out.mp4.mov`), `filmcraft-cli export` said "exported out.mp4" on stderr
while stdout JSON and the filesystem had `out.mp4.mov`. The message now
takes the path(s) from the export result: `path` of a direct export, each
item's `path` with --queue; the requested path only when the result names
none. Unit test for the helper; end-to-end test (direct and queued APV
export) that failed before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HDR sequences export as real 10-bit HEVC Main 10 through NVENC where the GPU
reports 10-bit HEVC: float BT.2020 R'G'B' -> rgbf_to_yuv420_10 -> P010 input;
VUI BT.2020 + PQ/HLG; HDR10 mastering-display / content-light as SEI on every
IDR (PQ); colr/mdcv/clli in the sample entry. New register_hdr_probe /
hdr_available in the export crate; macOS and GPUs without 10-bit keep
tone-mapping HDR sequences to 8-bit SDR HEVC. ABI asserts extended and
re-verified against nvEncodeAPI.h 12.1 (0 drift). 8-bit SDR HEVC unchanged;
the two #328 tests that asserted HDR is declined now expect Main 10.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Update MediaInfo to store all discovered audio streams in audio_streams
- Retain independent stream metadata and decoder state for MP4/MOV and Matroska/WebM
- Update audio_placement_specs to allocate successive tracks via ensure_audio_tracks
- Isolate clip rendering to read assigned audio stream rather than replaying stream 0
- Bump project schema to v13 with backwards compatibility for legacy v12 projects
Resolves#312Fixes#200Fixes#315
Matroska timestamps are whole milliseconds and a 1024-sample AAC frame at 48 kHz lasts
21.33 ms, so frames are stamped at 0, 1008, 2064, 3072… samples; OBS's remux to MP4 carries
that rounding into the sample durations (1008, 1008, 1056…). The MKV and MP4 readers put each
decoded packet at its own timestamp, overwriting the end of one packet and leaving a hole of
silence after another: a click every few packets, crackling throughout playback and export.
Packets of codecs with a fixed frame length (AAC, MPEG audio, AC-3) now run on from one
another and resynchronise to the timestamp only across gaps of more than half a packet, the
rule the MPEG-TS/PS reader already uses. Laced Matroska frames without a duration (which
repeat the block's timestamp) follow on too, and MKV Opus uses the same helper. MP4 audio
packet starts are kept in samples instead of track timescale units.
Regression tests mux our own AAC into MKV (millisecond stamps, plain and laced, a real gap)
and into an MP4 with OBS-remux durations: block-by-block playback and seeks must equal the
packets decoded back to back (all fail before the fix). An ffmpeg oracle test plays
ffmpeg-made MKVs with AAC, MP3, MP2 and AC-3 and requires them as smooth as ffmpeg's decode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Display the captured frame with the same frame rate and drop-frame timecode format as Source or Program, instead of decimal seconds. Keep the captured timecode fixed while the export dialog is open.
Add a UI regression comparing both monitor labels at standard and NTSC rates, with Program drop-frame formatting and playhead movement after capture.
Assisted-by: Codex
HEVC through VA-API the same way as H.264: the software HEVC decoder's
parsers and DPB (now generic over the picture type in filmcraft-hevc:
POC, reference picture sets, reference lists, output order, RASL
handling) drive the stateless hardware. Main, Main 10 and Main Still
Picture, 4:2:0 8/10-bit (P010 read-back); scaling lists, tiles, WPP,
dependent slices, weighted prediction. Missing references and range
extensions go to the software decoder.
Tests: hardware parity on HEVC Main / Main 10 incl. open GOP, scaling
lists, slices+WPP, weighted prediction, tskip+AMP, 1080p/2160p (streamed
comparison to stay within memory); HEVC front end against a recording
stand-in on every OS; ABI checks extended to va_dec_hevc.h. Benchmark on
Iris Xe: 4K HEVC 203 -> 10 ms CPU per frame, 4K Main 10 212 -> 18 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu item ran sequence.settings with no parameters and changed nothing. It now opens the
dialog: Timebase, Frame Size with "Scale motion effects proportionally" (on by default), the
timecode Display Format (drop-frame / non-drop-frame for 29.97, 59.94, 119.88), Channel Format,
Sample Rate, Maximum Render Quality, and the Color Management tab, applied in one undo step; OK
with nothing changed adds no undo step. Settings FilmCraft has no equivalent for yet (Editing
Mode, Pixel Aspect Ratio, Fields, Frames / Feet + Frames, Number of Channels, audio Display
Format, Video Previews, Maximum Bit Depth, VR Properties) are shown greyed out with their value
and a "Not supported in FilmCraft yet" hint; Composite in Linear Color is shown on (the
compositor is always linear). Every control has an automation id.
sequence_ui.rs: two tests driving the dialog over the control channel, and
FILMCRAFT_UI_SNAPSHOT_DIR support like the other UI tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- filmcraft_edit::frame_size::scale_motion: Sequence Settings' "Scale motion effects
proportionally when changing frame size". Every video clip's Motion Position maps through
p' = c1 + k(p - c0) and Scale / Scale Width multiply by k = min(W1/W0, H1/H0), static values
and keyframes: a plain proportional scale for the same aspect, and on an aspect change the old
frame fits inside the new one, centred. Automatic positions stay automatic, Scale to Frame
Size clips keep their Scale (the renderer refits them), Anchor Point and Rotation stay.
- sequence.settings: new dropFrame, maxRenderQuality, workingSpace, wideGamut, autoToneMap and
scaleMotion (default false, so existing callers are unchanged) params, in the same undo step;
returns {"scaledClips": n}. Drop-frame on a rate without it is refused atomically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wire both camera buttons to captured-monitor frame export with original media at full resolution. Add a movable settings dialog with format/depth selection, a visible destination folder, Browse, overwrite confirmation, and optional project import.
Reuse existing 8-bit still encoding; add JPEG and direct float-to-16-bit PNG/TIFF encoding. Cover monitor routing, captured frame time, proxy bypass, hostile parameters, depth, import, cancellation, overwrite, and fractional header movement with synthetic regressions.
Assisted-by: Codex
Stateless VA-API decoding with FilmCraft's own H.264 host side: the
software decoder's SPS/PPS/slice-header parsers and its DPB (now generic
over the picture type in filmcraft-h264) drive the GPU, so reference
marking, reference lists and output order are shared and the pictures are
bit-exact with the software decoder. libva is loaded at run time; no libva
or driver means software decoding as before. frame_num gaps (open-GOP
seeks) get the software decoder's stand-in frames. HybridDecoder keeps the
transparent software fallback.
Tests: hardware parity on 7 H.264 streams incl. 1080p/2160p, seeks,
flushes, forced failures, fuzzing, concurrency; front end against a
recording stand-in on every OS; ABI checks against va.h. Docs: platform
README, ADR 0001 addendum, architecture, performance (H.264 2160p CPU per
frame 417 -> 11 ms on Iris Xe), roadmap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Left to choose, NVENC answered a peak bitrate above its level's Main tier
limit with the High tier: 1080p30 at 12/18 Mbit/s was written as level 4 High
tier, 2160p30/60 at 40/60 Mbit/s as 5.0/5.1 High tier, which many hardware
decoders refuse. The level is now the lowest whose Main tier limits hold the
picture size, sample rate and peak bitrate (H.265 Table A.8), e.g. 1080p30 at
18 Mbit/s is level 4.1 Main tier. Unit test of the table; hardware test that
the hvcC says Main tier at the expected level.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status bar was a fixed #1c1c1c, dark on the Light and Medium themes
and on a theme an embedding app sets through FilmcraftApp::tokens. It now
takes the header bar's colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An app that embeds FilmCraft can hide the header bar and the status bar
and give the panels the whole window. Saved UI state without the fields
keeps both shown. With the status bar hidden, a finished preview render
still plays: the job watching moved out of job_status into watch_jobs,
which runs every frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
post and repaint wake the egui context that start stores in a private
thread-local. An app that embeds FilmCraft and builds WebApp in its own
eframe runner had no way to set it, so posted actions and window.filmcraft
requests waited for the next input event. start now calls the same
set_context.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Play source video and audio, drag marked ranges as video, audio, or a linked pair, and route Source commands to clip marks and markers. Add shared draggable In/Out handles and range movement, preserving scrubber priority and explicit-only Program marks.
Include original code-drawn controls, documentation, attribution, and regression coverage for playback, placement, routing, undo, cancellation, and hostile parameters.
Assisted-by: Codex
eframe's default wgpu instance enables PRIMARY | GL on Windows. Creating
the OpenGL backend can crash inside AMD's atio6axx.dll before the window
appears, so the app flashes in Task Manager and exits. When WGPU_BACKEND
is not set, the Windows instance now enables only DirectX 12; an explicit
WGPU_BACKEND is still honoured. macOS, Linux and web are unchanged.
Same fix as storytold/designcraft#168.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyVbT6Vv4BwWZ4hY8973qD
Normalize explicit relink prefixes, compare native paths in portable tests, close fixture sessions before directory moves, and preserve exact GPU effect-source pixel centres. Keep the hardware-dependent mixer benchmark opt-in with its 4x target unchanged.
Shortcut portability changes reused from storytold/filmcraft#37 (bd944ee and f2b36fe). The existing export fix from #282 remains in its original parent commit.
Assisted-by: Codex
tick_of() used floor division (div_euclid) which truncated the frame
duration for non-standard rates like 37.516 fps (9379/250) where the
frame duration is 6770871094.999 ticks, not an integer. This caused
frame_at(tick_of(f)) to return f-1 instead of f, making step forward
stall at frame 0 and step back jump backward by 2+ frames.
Changed tick_of() to use ceiling division so the round-trip is exact
for all integer frame indices. Ceiling is correct because the frame
duration is always > 1 tick, so floor(ceil(f*D) / D) == f for every
integer f. frame_duration() remains floor division (unchanged) since
many callers do end - frame_duration() to get the previous frame.
All 11 standard broadcast rates are unaffected since their frame
durations are already exact integers (ceil == floor there).
Added regression tests:
- exotic_frame_rate_roundtrip: round-trip over -20k..20k for 5
non-standard rates, snap invariant, step-back via frame_duration,
forward/backward stepping simulation
- exotic_frame_rate_durations: frame_duration is floor for exotic,
exact for standard
- exotic_frame_tick_roundtrip proptest: arbitrary FrameRate::new(num,
den) round-trip
Thanks to @justjohn12345 for catching that round (not ceil) still
fails for ~50% of frames starting at f=938.
`filmcraft-cli commands | head -c 10` panicked with "failed printing to stdout: Broken pipe
(os error 32)". Output now goes through `outln!` / `out!`. When stdout fails, later output is
dropped but the program does not stop: the rest of `exec`, `run` and `import` (later script
lines, `--save`, `--save-as`) still runs and the exit status still reports a failing command.
Exiting at the first failed print would report success while skipping the save, e.g.
`filmcraft-cli run edits.jsonl --project p.fcproj --save | head -1`. A closed pipe is not an
error; any other stdout write error is reported and makes the exit status 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- register() asks the HEVC probe on a thread of its own (the first probe is
about half a second: D3D11 device + NVENC session), so the first draw of
Export's format list does not wait for it; tests/nvenc_hevc_warm.rs.
- A machine with no HEVC encoder is not a declined hardware attempt: the
factory returns None without counting `declined`.
- HEVC tests skip only when the probe says there is no HEVC encoder; on a
machine that has one, a configuration NVENC refuses fails the test.
- Wording: NVENC is FilmCraft's only HEVC encoder on Windows (Windows itself
ships others).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One NVENC session, ring and NV12 path now serve H.264 and HEVC (Codec enum,
Profile::HevcMain). HEVC Main 8-bit 4:2:0 SDR BT.709, MP4/MOV hvc1, parameter
sets only in an hvcC built from the SPS the encoder wrote. The H.265 format is
available through a cached NVENC probe; choosing it is the opt-in, declines are
counted errors. ABI asserts regenerated from nvEncodeAPI.h 12.1 (no drift) and
extended for HEVC; tests, docs and roadmap updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Loudness Auto-Match metered every clip as the stereo pair clip_signal
plays, so a mono source (or a mono pick from Modify > Audio Channels /
Breakout to Mono), copied onto both sides, read 3.01 LU louder than the
file and got 3 dB too little gain. BS.1770 and ffmpeg's ebur128 measure
a mono programme as one channel; now so does clip_loudness, through a new
render::audio::clip_is_mono. Stereo and wider sources are unchanged, as
are the export loudness pass and the Meters panel (they measure the real
output mix).
Regression test: the same speech as a 1-channel WAV and as dual-mono
stereo measure 3.01 LU apart, the mono clip agrees with a one-channel
meter over the file within 0.1 LU, and a mono pick counts as mono.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Properties panel: the diamond on a row was the stopwatch (effects.toggleAnimation),
so clicking it on a keyframe dropped every keyframe and kept the value at the
playhead. It is now Add/Remove Keyframe (effects.addKeyframe), as in Premiere:
it turns animation on with a keyframe at the playhead, adds one there, or removes
the one there, and the value falls back to what the remaining keyframes give in
Properties, Effect Controls and the Program monitor. An animated row shows the
navigator (previous / diamond / next); the diamond is filled only while the
playhead is on a keyframe. Crop's diamonds work too (the effect is applied first).
Effect Controls: the keyframe lane has a time ruler (ticks and sequence timecode
over the clip's stretch of the sequence) with the playhead's handle on it; click
or drag the ruler to move the playhead. The playhead is drawn only while it is on
the clip. Both panels share one navigator widget, with automation ids
<panel>.<effect>.<param>.addKeyframe / .prevKeyframe / .nextKeyframe, plus
effectControls.ruler and effectControls.playhead.
Tests: engine add/remove at the playhead with undo, redo and bad parameters;
headless UI tests of the navigator, Crop and the ruler; ruler labels and limits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a value was dragged in Effect Controls or Properties, the Program monitor
kept the picture from before the drag and only caught up when the mouse rested.
Two causes:
- Every step of a drag is a new project revision, so the frame the monitor asks
for is never ready on the refresh that asks for it, and the stand-in lookup
only knew frames of the same revision. The monitor now remembers the frames it
asked for on its latest paused refreshes and, when neither the frame due nor a
cached frame just before it is ready, shows the newest of those that is. A
backward or fast scrub had the same gap and gets the same stand-in. Playback
is unchanged.
- A frame still rendering was cancelled when the same frame was asked for at a
newer revision. With frames slower than a refresh (CPU effects) each one was
thrown away for the next. Such a job now finishes; a request for a different
frame of the view (scrubbing) still cancels it.
perf.stats gains `monitor`: the frame and revision of the picture the Program
monitor shows, against the playhead's frame and the project's revision.
Tests (headless, perf_ui): a parameter changed on every UI pass and a backward
scrub leave the picture at most one step behind; a frame being rendered survives
a request for the same frame at a newer revision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Format::Hevc` is available only where a hardware HEVC session can be created, and
`hardware_encode::hevc_available` finds out by creating a small one on the first question. That
question comes from the first draw of Export mode's format list, on the UI thread, and the session
takes about 0.1 s (88-100 ms in seven of eight fresh processes on an Apple M1, 222 ms in the first):
the app stalls for that long the first time Export is opened.
`register()` now starts the probe on a thread of its own (`warm_hevc_probe`, once however many times
`register` is called). Whoever asks while it runs waits for the same `OnceLock` answer, no longer than
asking first; a thread that cannot start, or a probe that panics (caught), leaves the question to the
first caller as before. `hevc_probed()` says whether the answer exists, for diagnostics and tests.
Test: `tests/hevc_probe.rs` registers, never calls `hevc_available`, and waits for `hevc_probed()` (it
fails after 60 s without the call in `register`).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The export pipeline hands the float image of every frame it converts back to
`filmcraft_frame::pool` (#184), which keeps up to 320 MiB of them, and the decoders' plane shelves
hold up to 192 MiB of each type. An export's images are the size of its frames, which nothing else
asks for, so a finished export left them idle until the next one.
`pool::trim()` frees every idle buffer. `Pipeline` calls it when it is dropped, one place for every
way an export ends (done, failed, cancelled, dropped half way by a host that steps it, as the web
build does) and for every caller of `export` / `Exporter` (previews, proxies and AAF/OMF mixdowns
share the pipeline). A job still rendering at that moment allocates a few images again: the pool is
only a cache.
Measured with a synthetic 3 s 1080p24 matte exported as PNG frames three times in one process
(Apple M1, `footprint`, 10 s after the last export): 575 MB without the trim, the pool holding
332 MB idle, and 258 MB with it. macOS returns the freed pages over a few seconds, not at once.
Tests: `crates/frame/tests/pool_trim.rs` (every shelf emptied, the pool works afterwards) and
`crates/export/tests/pool_trim.rs` (an image sequence, a finished and a cancelled video, an export
that could not start and a stepped export dropped half way each leave nothing idle; without the
trim they fail).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- expand_home uses std_home_dir (USERPROFILE on Windows) instead of
HOME only; the Export panel reuses the engine function.
- std_home_dir prefers USERPROFILE on Windows, where Git Bash sets
HOME to a Unix-style path.
- On Windows, / in an export path becomes \: after a verbatim
prefix (what canonicalize returns) / is not a separator, giving
os error 123.
- One is_folder check for output_path and export.queue.add, so a
trailing \ no longer numbers several queued sequences.
- filmcraft-tts (feature kokoro): Kokoro-82M on candle, implemented from
the StyleTTS 2 / iSTFTNet / ALBERT papers, config.json and the
checkpoint's tensor shapes; official .pth read with candle's restricted
pickle reader; voice packs from their zip. Neural Voice: tts-text
phonemize -> Kokoro per chunk -> pauses -> pitch via audio-dsp.
- catalog: 9 US English voices (model card grade C+ or better) + CMUdict,
every file pinned to a revision and SHA-256 (336 MB).
- speech: generic download_files (same verified downloader).
- engine: tts.render (background synthesis into Session::tts_cache),
tts.downloadVoices (background job, cancellable), voices list with
package state; feature neural-voices (default on in the desktop app).
- ui: natural/basic voice groups, consent box (size, sources, licences),
download progress + Cancel, synthesis never blocks the window;
ui.set {tts:{...}} for agents.
Measured: Whisper-base WER on 50 original sentences 2.1-3.6 % for all 9
voices (built-in robotic voice 96.6 %); 2.2-2.3x real time on CPU.
Gates: fmt, clippy, layers, assets, wasm pass; tests pass except
shortcuts_tests::assign_reassigns_conflicts_and_undoes (fails on main)
and the frame pool test (flaky under load, passes alone).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix round from Claude's P2b review. The normalizer's terminator run now picks
the strongest terminator (? over ! over .) and carries it through a new
backwards-compatible Token::SentenceEndWith(char) (SentenceEnd still means
"."); phonemize attaches ? and ! to the sentence's last word so Kokoro can
shape intonation: "Ready?" -> "ɹˈɛdɪ?", "Go!" -> "ɡˈO!", "Really?!" -> ?.
Also converts the remaining direct indexes in non-test code to
get()/get_mut()/iterators (letter_to_sound slices, the tion/sion and
first-vowel stress writes, the scanner's char access).
New cases: nine terminator tests in normalize (incl. "Is it 1999?",
"Call 555-0134!", "Wait... what?", "Are you there?!") and nine phonemize
assertions (Ready?, Go!, Done., No!!, Are you there?!, ...). All
SentenceEnd-matching tests updated.
Gates: fmt, clippy -p filmcraft-tts-text --all-targets -D warnings, tests
(37 suites), xtask layers, xtask wasm - all green.
🤖 Written by Merlin (Hermes Agent, profile merlin)
New module phonemize in filmcraft-tts-text: normalize(script) -> words ->
Kokoro phoneme symbols, the interface Claude's Kokoro code (P3) calls. Public
API per the brief's contract: Lexicon (CMUdict parser, 16 MiB / 500 000-entry
caps, malformed lines skipped and counted), Overrides (case-insensitive,
raw /phonemes/ or respellings, capped at 1 000 entries of 200 chars),
Chunk::Phonemes/Pause with MAX_CHUNK_CHARS = 400, phonemize(), and SYMBOLS —
the exact character set emitted (ARPAbet→Kokoro table from the brief, stress
marks ˈ/ˌ before the vowel, function words unstressed, punctuation attached).
Lookup order: overrides → dictionary (first pronunciation) → morphology
(-s/-'s with voicing, -ed, -ing, -er, -ly, un-, re-) → letter-to-sound rules
written from scratch (tion/sion, ture/sure, ough, igh, digraphs, silent e,
soft c/g, doubled consonants, final le). Hyphenated words split per part.
Tests: CMUdict parse table (alternates, comments, malformed counting, caps),
46 exact phonemize cases, 36 letter-to-sound cases, chunk splitting at
commas ≤ 400 chars, a symbol-set test over all normalizer inputs plus
hostile ones, and catch_unwind hostiles (huge, empty, emoji, RTL, NUL,
10 000-letter word, 1 001 overrides). Optional measured-accuracy test
(FILMCRAFT_CMUDICT): held-out CMUdict words → 9.1% word accuracy, 37.9%
phoneme error rate (229 words, 2026-10-08). CMUdict is downloaded with the
voices at run time, never committed.
Gates: fmt, clippy -p filmcraft-tts-text --all-targets -D warnings, tests
(37 suites), xtask layers, xtask wasm (44 crates) — all green.
🤖 Written by Merlin (Hermes Agent, profile merlin)
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>
The Selection and Zoom status-bar hints and the mask-vertex tooltip
named Mac keys (Opt, Cmd) on every platform. Show Alt/Ctrl on
Windows and Linux, matching the keys the code actually reads
(egui `alt` / `command`). macOS text is unchanged.
- release.yml: new `flatpak` jobs (x86_64 on ubuntu-24.04, aarch64 on ubuntu-24.04-arm) that
repackage the Linux tarball as filmcraft-<v>-linux-<arch>.flatpak, and a `freebsd` job that
builds filmcraft-<v>-freebsd-x86_64.tar.gz in a FreeBSD 14.3 VM; both feed the draft release.
- AppImages embed gh-releases-zsync update information and ship a .zsync beside them; the
Linux job checks both.
- Jobs that sign nothing (version, Linux, Flatpak, FreeBSD, web) no longer use the `release`
environment, so a dispatch on a branch dry-runs them; the signing jobs and the draft-release
job stay in the environment, so nothing is published from a branch.
- packaging/linux/flatpak-bundle.sh + bundle manifest (same runtime and finish-args as the
Flathub manifest; packaging-lint checks they agree), packaging/freebsd/package.sh (with a
--dry-run that packaging-lint runs), and a nightly FreeBSD build workflow.
- README: Downloads section listing every build; docs/releasing.md updated.
Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Windows and Linux keyboards' Delete key is the forward-delete key. In
FilmCraft Default it was bound only to Project ▸ Clear (application-
wide), so with the Timeline focused it ran project.delete, which does
nothing without a Project-panel selection: the selected clips stayed.
Backspace (the Mac "delete" key) already cleared them.
Add a Timeline-panel binding of Delete to edit.clear, as Premiere has
it; in the Project panel and application-wide Delete still runs
Project ▸ Clear.
Tests: shortcuts_tests::delete_clears_timeline_clips_and_project_items
(Delete resolves to edit.clear with the Timeline focused and to
project.delete otherwise, macOS and Windows) and
keyboard_ui::delete_key_clears_the_selected_timeline_clip (headless app:
D selects a clip, Delete removes it, undo restores it). Both failed
before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bindings are written Cmd+… and parse to `command`; egui's matching then
requires `command`. A physical Ctrl press off macOS carries it
(egui-winit sets command = ctrl there), but ui.key built its event from
parse_shortcut, which mapped "Ctrl" to `ctrl` alone, so an injected
Ctrl+Z matched nothing while Cmd+Z worked. ui.click's `ctrl` modifier
had the same gap.
Off macOS "Ctrl" and "Cmd" are one key (the engine's Chord::effective
already says so), so parse_shortcut maps both to `command` there, and
the control channel synthesises presses the way the OS reports them:
a press of either carries both flags. macOS is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On a machine whose ALSA default PCM is misconfigured (missing 99-pipewire-default.conf symlink, #23/#106) the host default could not be opened: playback ran without sound and without any user-facing message. The device picker now falls back to the first output device that can open, logs why, and a new defaulted AudioOut::note() surfaces the note in the status bar (first frame and when playback starts); a machine with no device at all reports it too.
🤖 Generated with [Kimi Code](https://github.com/storyteller/kimi-code)
Co-Authored-By: Kimi <noreply@storyteller.dev>
A caller that names its targets does not need a selection: enabled_for
re-runs the enablement check with the named ids standing in for it. But
named_items only recognised project items, and project.delete's `items`
take bin ids too (the Project panel's selection holds both), so
{"items":[<bin ids>]} with nothing selected was still "not available
right now: select an item in the Project panel".
Count a named bin as a target, except the project's own top bin, which
no command acts on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Move conditional no-op match arms into guards
- Preserve renderer predicate order with De Morgan's law
- Strict clippy, fmt, layers, assets and WASM pass on Rust 1.95
- Clean engine runs match main: 379 pass, 10 fail, 3 ignored
- Full CI is not green; upstream baseline failures remain
The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: GPU compositor fallbacks, audio output
problems, a panicked frame job or audio mixer, container warnings. A
launch from a desktop menu has no terminal either, so there was nothing
to attach to a bug report.
apps/filmcraft/src/logging.rs (no new dependency):
- standard error plus <data dir>/Logs/filmcraft.log, next to the crash
logs (--data-dir / FILMCRAFT_DATA_DIR respected, Help > Reveal Log Files
opens the folder); each launch rotates the previous log to
filmcraft.1.log and .2.log, so the log of a run that crashed survives
the next start; the file is capped at 16 MiB
- records logged before the data directory is known are buffered
(bounded) and written once the file is attached
- default levels: info for filmcraft*, warn for everything else;
RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
error as the only sink; a poisoned lock is tolerated
- the realtime audio callbacks do not log; a record holds the lock for
one write of an already formatted line
The logger is installed first in main (before the panic hook) and the
file is opened after argument parsing (--help, --version and usage
errors leave no file). The start-up eprintln!s in main (auto-save
unavailable, open/recovery errors, no window) now go through the logger.
Documented in docs/contributing.md (environment variables, Logs).
Right-clicking a clip's edge on the timeline now selects that edit point and
opens Premiere's edit point menu instead of the clip menu: Ripple Trim In,
Ripple Trim Out, Roll Edit, Trim In and Trim Out (each re-selects the cut with
that trim type, greyed out when the needed side has no clip), Apply Default
Transitions, and Join Through Edits. The body of a clip still opens the clip
menu.
Join Through Edits from this menu joins only the right-clicked cut (and its
linked partner): `sequence.joinThroughEdits` takes a new `cut: [left, right]`
parameter, since `clips: [id]` also matched any other through edit touching
the same piece.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unlinking from the timeline clip menu cleared the link, but the picture and
its sound stayed selected together, so dragging the picture still moved the
sound and Unlink looked like it did nothing. The clip that was right-clicked
is now the only one selected after Unlink, and can be dragged away straight
away. Link leaves the selection as it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dev run (cargo run) has no installed .desktop entry or hicolor icon for
the app id the window sets (ai.storyteller.filmcraft), so KDE (Wayland and
X11) falls back to the generic Wayland icon in the taskpanel and the
window topbar. Packaging installs both; development did not.
At startup on Linux, when no ai.storyteller.filmcraft.desktop exists in
any XDG data dir, install one into $XDG_DATA_HOME/~/.local/share
(applications/) plus the 256/512 hicolor PNGs, with Exec pointing at the
running binary. A packaged entry is never overridden; failures print a
warning and the app carries on (AGENTS.md §0).
🤖 Generated with [Kimi Code](https://github.com/storyteller/kimi-code)
Co-Authored-By: Kimi <noreply@storyteller.dev>
The export pipeline flattened every frame over black
(`over_black_rgba8`), so a PNG sequence of a sequence with transparency
came out on a black background, while Export Frame already wrote straight
RGBA. `ExportSettings.alpha` (serde default false, Premiere's default)
makes the pipeline emit straight sRGB RGBA for PNG and TIFF sequences and
`encode_still` keep the channel in TIFF; BMP and video formats ignore it.
Exposed as the `alpha` flat parameter of file.exportMedia and an
"Include Alpha Channel" checkbox in Export mode's Video section, shown
for PNG and TIFF only.
Fixes#160
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolve the shown Project bin for file-dialog and dropped imports. Capture the browser destination before asynchronous media reads so navigation cannot redirect an in-flight batch.
Allocate an unused virtual path for every browser import so equal-size media cannot replace existing sources. Add an OPFS recovery regression for issue #194.
The web build runs every job synchronously, and four job bodies (proxies,
mask tracking, video and audio previews, Project Manager) still called
std::time::Instant::now() for their "Done in N s" report — which panics
with "time not implemented on this platform" on wasm32 and took the whole
app down. They now use web_time::Instant like the rest of the engine
(std::time::Instant on native targets). autosave imports its clock from
web_time too, and a regression test scans the crate's non-test sources
for std::time::Instant so the next copy-pasted job fails cargo test
instead of the web app.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brazilian Portuguese (#176) moves to the catalog system as i18n/pt-br.tsv (the
menu entries unchanged) and joins the general.interfaceLanguage preference. The
new UI text from main is wrapped and translated into Spanish: the credits
window, Text Properties, H.265 export rows, Clip Speed / Duration, Color Matte
dialogs, the voice-over discard button, the time-left suffix and the audio
output status messages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OPFS recovery snapshot (apps/filmcraft-web) had two problems:
- #197: opfs::write never reported failures, and Autosave recorded the
revision as snapshotted before the write ran. A failed write was only
retried after the next edit.
- #193: the first tick of a clean session wrote {"dirty": false} to the
meta. Starting with ?norecover or ?fresh therefore discarded a
snapshot that was never reopened.
The decision of when to snapshot or mark clean moves to a pure module,
recovery_policy, tested natively in tests/recovery_policy.rs. Failed
writes (snapshot or clean meta) are retried after the 5 s interval
without another edit, and the meta is only marked clean to retire a
snapshot this session wrote. opfs::write_then reports whether a queued
write landed (coalesced writes report the newest one's result).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HrAqZmrrDLRpxBJatxvaJ
The Program monitor's Go to In and Go to Out buttons (Shift+I / Shift+O) did
nothing when the sequence had no In / Out mark. Like Premiere (and like the
Source monitor already did), they now go to the start / end of the sequence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Custom macOS larger runners in a runner group do not pick up jobs; the
built-in M2 Pro label does. Only release.yml uses it, and the
actions_macos_xl budget caps the monthly spend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The standard macOS runners are shared with all CI and queue for an hour or
more. The macos-release runner (GitHub-hosted M2 Pro) is in a runner group
usable only by release.yml on the release branch, with a monthly spend cap
on the actions_macos_xl SKU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A drag value changes only once the mouse moves, after the press frame, so sending `begin`
only on the press frame merged two drags of the same parameter into one undo step.
Remember the press that began a step by its start time instead; unit test drives two drags
through `run` with a real egui context (fails on the press-frame version).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The clip menu's Speed/Duration… applied 50% at once, and Cmd+R / Clip ▸
Speed/Duration… ran it at 100% with no dialog. It now opens Clip Speed /
Duration (speed, the resulting duration, Reverse Speed, Ripple Edit, Time
Interpolation), seeded from the selected clip.
- effects.setParam took one undo step per frame of a drag, so Undo after
dragging Time Remapping's speed (or any Effect Controls value) crept back in
small steps. It takes `merge` / `begin` like the mixer and masks, and Effect
Controls sends them while the mouse button is down: a drag is one step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Help ▸ About FilmCraft gets tabs About · Contributors · Models, following the craftrules standard
(standards/contributors.md): contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by crates/ui-egui/build.rs as static
tables (nothing read at run time; the web build has them too). Each contributor line: @username,
opt-in display/real name, PRs, commits, lines +/−/Δ, binary assets +/−, first/last commit.
Sortable by every column; alphabetical ignores case and the leading @. Tabs and credits controls
register automation ids (about.tab.*, about.credits.*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors docs: craftrules is two levels up (../../craftrules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- mxf: read_pcm bounds a request by the sample frames the file actually stores (plus 16M
samples of zero padding) instead of a fixed 16,777,216-sample cap, so whole-clip reads of
long stereo MXF (transcription) work again; PCM/AES3 chunk buffers are clamped to the source
length instead of a 256 MiB cap; decode_pcm's output is bounded by its input, so its
fixed cap goes (the AES3 >8-channel refusal stays; a request is also bounded by the source
length divided by the channel count). read_sample keeps only the source-length bound.
- mxf: index_entries no longer fails open() past 1,048,576 entries (the sparse BTreeMap
already bounds memory by real entries); long recordings open.
- project/format: new Sequence/Track/CaptionTrack::check_bounds() hold only the hostile-value
bounds (settings, marks, overflowing time ranges); project load and export ranges use it, so
projects that break only structural rules (overlap, zero duration) still open and export.
check() = check_bounds() + the structural rules, as before.
- project: frame-size cap raised to 268,435,456 pixels, 32768 per side (16K, 16384x8192).
- engine: checked_u32_p accepts integer-valued floats (1920.0); renderable_sequence returns
an EngineError (new Session::try_render_program_at, used by automation render) instead of
eprintln.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web: Link Media Locate… links the chosen file to the selected missing clip (#111)
Link Media ▸ Locate…, `media.attachProxies` and `media.reconnectFullRes` all
need the chosen file's path back synchronously so they can call the engine
with `item` + `path`. The existing `pick_files` hook imports every chosen
file as a new project item, so on the web build (where the browser picker is
asynchronous and the hook returned `Vec::new()`) the command ran without a
path and the file silently landed as a new project item instead of being
relinked. On native the same hook worked because rfd returns paths
synchronously.
Add a separate `pick_file_for_relink` hook that returns the chosen file's
virtual path synchronously. Native hosts open an rfd dialog; the web host
registers the file in `/files/<name>` via a pending slot and runs
`media.relink` itself when the picker resolves. The three call sites prefer
the new hook and fall back to `pick_files` when the host has not installed
it (so the import flow used by every other dialog stays unchanged).
Fixes#111
Co-Authored-By: ninso112 <ninso112@pm.me>
* Co-ship: Luna161 ack'd and reviewed fix/111-link-media-locate
Reviewed the pick_file_for_relink hook routing, the take_files relink path,
and the regression test. Looks correct end to end; ready for upstream review.
Co-Authored-By: ninso112 <ninso112@pm.me>
* #111: relink hint names its command; no pending state after a cancelled picker
- RelinkHint carries the command (media.relink / attachProxies / reconnectFullRes), so the
web host no longer runs media.relink for Attach Proxies and Reconnect Full Resolution.
- The web picker hands the hint to its own callback instead of a thread-local slot, so a
cancelled picker can't turn the next ordinary import into a relink.
- Replace the hook-only unit tests with a UI test that drives Link Media ▸ Locate….
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: ninso112 <ninso112@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With clips selected under the playhead, Ripple Trim Previous/Next Edit to
Playhead (and the plain trims) now trim only their tracks and linked partners,
like Add Edit; with none selected they use the targeted tracks as before. All
tracks are targeted by default, so W on a V1 clip also cut the music on A2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A refused ripple delete / ripple trim now names the sync-locked track whose
clip is in the way and how to proceed, instead of a generic "would break sync"
(the edit is refused the same way Premiere refuses it).
- Dragging the playhead no longer snaps onto its own position (it stuck and moved
in SNAP_PX jumps), snaps on the initial press too, and "Snap playhead in
Timeline when Snap is enabled" is on by default, as in Premiere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows and Linux the macOS Control key folds into the primary modifier, so
the default set already reports conflicts there (documented in shortcuts.rs).
The test now checks that assigning adds exactly one conflict and that undo
restores the original set, on every platform.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recent ffprobe versions report PTS=N/A for a program-stream picture whose
timestamp is inferred, so the oracle dropped that frame and compared 29 of 30.
best_effort_timestamp keeps the inferred time and all 30 frames are compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Translations move from Rust tables to one catalog per language
(crates/ui-egui/src/i18n/<code>.tsv), looked up through HashMaps. UI code
wraps its strings in tl!("…") and tlf!("…{name}", name); names from
registries (effects and parameters, settings, commands, panels, workspaces,
export and effect presets) are translated where they are drawn, so command
ids, automation ids and preference values stay English.
Spanish now covers menus, panels, dialogs, the Settings pages, the Effects and
Effect Controls registry, the Keyboard Shortcuts dialog and status messages
(about 3,350 entries). Tests fail when a tl!/tlf! literal, a menu label or a
registry name has no Spanish entry, and check catalog syntax, duplicates,
placeholders and ellipses. Searches match the Spanish label as well as the
English source. Window titles keep their English egui ids.
The language is stored in the general.interfaceLanguage preference (also in
Settings > General) and restored on startup; a failed preferences write still
switches the interface and says it could not be saved. Template values are
inserted literally, so a file name containing braces is not re-substituted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
egui-winit reports the clipboard shortcuts as Event::Copy / Cut / Paste
instead of key presses, so the shortcut bindings never matched them.
Outside text fields, add the key press each clipboard event stands for
before matching bindings, and put copied clip names on the system
clipboard so Ctrl+V is delivered right after a copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQNunPf8nMeAQi6M92RT68
Merge origin/main into the NVENC branch. #183 already provides the
HardwareEncoding enum, ExportSettings.hardware_encoding, the
hardwareEncoding export param and the Export > Video > Hardware Encoding
checkbox, so the duplicates from this branch are dropped and NVENC uses
them. filmcraft-export is now an all-platform dependency of
filmcraft-platform (VideoToolbox and NVENC both register encoders).
Never-crash fixes in the NVENC backend:
- submit() rejects a driver input pitch narrower than a row (and unlocks
the buffer); fill_nv12 uses get_mut instead of slicing rows.
- composition offsets use saturating math and i32::try_from; frame-rate,
size and timestamp casts are checked; sizes above u16::MAX decline.
- Annex B scanning and NAL type checks use get()/first().
- A failed nvEncOpenEncodeSessionEx that returns a handle destroys it.
ffi.rs carries NVIDIA's MIT notice for nvEncodeAPI.h, listed in
ATTRIBUTION.md under a new "Third-party source code" section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refresh() cleared the Missing badge, but the media pool kept the
MEDIA NOT FOUND slate cached under the item, so the monitors kept
showing it. The regression test now checks the rendered frame too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Type-tool click now goes through new_text_action; Vertical Type swaps
in graphics.newVerticalText and never draws a paragraph box (vertical text
is point text). Drops the dated ROADMAP log line (the maintainers keep the log).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graphics.newFromFile resolved the Motion anchor with the sequence frame size
instead of the imported picture's size, so a picture smaller than the frame
got its anchor at the frame centre (e.g. 960,540 for a 4x2 image). Setting
Motion ▸ Position then moved the wrong point and the picture landed far from
where it was asked to go. place_video_clip now resolves auto points with the
source's own size (Project::source_size), falling back to the frame size.
The new-layer-from-file test checks the anchor is the picture's centre; it
fails before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause: the only Select All was `edit.selectAll`, which fills the
timeline clip selection (`state.selection`). Nothing filled the Project
panel's selection (`state.project_selection`), so with 161 imported
items the user had to click each one.
Fix:
- `project_panel::commands()` gets `project.selectAll` (selects every
item in the project) and `project.deselectAll` (clears the Project
panel selection). They are regular journalled commands with no menu
entry and no shortcut of their own, so they add no shortcut conflicts.
- `handle_shortcuts` (ui-egui) remaps a fired `edit.selectAll` /
`edit.deselectAll` to the `project.*` command when the Project panel
has focus. Cmd+A / Cmd+Shift+A keep acting on timeline clips
everywhere else, and `edit.selectAll` behaves exactly as before.
Out of scope: no menu entry. The Edit ▸ Select All menu item still
calls `edit.selectAll` directly, because menu clicks don't go through
the focus remap. The regression test checks the engine commands but not
the UI keyboard remap.
Regression test: project_panel_tests::select_all_selects_every_project_item
(fails without the fix: unknown command).
Co-Authored-By: ninso112 <ninso112@pm.me>
Co-Authored-By: Luna161 <Rosenkranzrene@gmail.com>
Adds `Format::Hevc` (Export > Format > H.265 (HEVC); `"format": "hevc"` in
`file.exportMedia`): HEVC Main, 8-bit 4:2:0, in MP4 (`hvc1`) or QuickTime, with AAC audio,
encoded by the macOS hardware encoder. There is no built-in H.265 encoder (one in pure Rust
is a project of its own, and x265 is GPL), so the format exists only where the OS has one.
- `videotoolbox_encode`: the session wrapper creates HEVC sessions too (`VtProfile::HevcMain`;
the profile also says which codec). Keyframes are the HEVC random access points (NAL types
16 to 21); the parameter sets are VPS / SPS / PPS; the `hvcC` record in the MP4 sample
entry is the one VideoToolbox writes (sample description extension atoms), so profile,
level and flags are the encoder's own. `VtEncoder::uses_hardware()` asks VideoToolbox
whether the session runs on a hardware encoder (a test checks H.264 and HEVC), so a
requirement dropped by accident could never become a silent software encode.
- `hardware_encode`: one adapter for both codecs. Choosing the format is the opt-in (the
`hardware_encoding` setting is for H.264). `hevc_available()` (one small hardware session,
created once) is registered with the new `filmcraft_export::register_format_probe`, so
`available(Format::Hevc)`, the format list of Export mode and `exportFormats` follow the
machine. `Format::has_builtin_encoder` says which formats need such a probe.
- `export`: `Format::is_h26x` where H.264 and H.265 behave alike (MP4 or QuickTime, AAC, size
estimate, summary line). Two-pass VBR is refused up front ("H.265 export has no two-pass
mode") and HDR sequences export as SDR, because there is no encoder to fall back to.
- Export mode: H.265 shows H.264's multiplexer, audio, metadata and bitrate controls, without
profile, level, two-pass or the hardware checkbox, and says "Encoder: Hardware".
Apple M1 (8 cores), 1080p25, a 9:29 timeline (camera clip, ProRes 4444 overlays, AAC,
loudness): hardware H.265 at 10 Mb/s 195 s, hardware H.264 at 20 Mb/s 204 s (CPU 1029 s and
1042 s; the same H.264 export took 193 s and 200 s on other runs). It is not faster: the codec
that compresses further is the more expensive one (raw encoders: 166 and 189 frames/s), and
the export is bound by the CPU compositor, not by the media engine.
Quality per bit, 30 s of the same timeline against a ProRes 422 HQ export (average PSNR over
the three planes, SSIM): 20 Mb/s 46.88 dB / 0.9902 (H.264) and 46.91 dB / 0.9903 (H.265);
10 Mb/s 44.96 / 0.9863 and 45.09 / 0.9863; 6 Mb/s 43.72 / 0.9833 and 44.31 / 0.9843; 4 Mb/s
42.91 / 0.9807 and 43.64 / 0.9825; 3 Mb/s 42.37 / 0.9786 and 43.05 / 0.9807. The same picture
at 10 Mb/s and above; at 3 to 6 Mb/s H.265 needs 22 to 31 % less bitrate for the same PSNR.
Both formats therefore keep the same default bitrate. Details in docs/performance.md (HW3).
Tests: tests/hardware_encode.rs gets the HEVC counterparts of the H.264 ones (what is taken
and declined, a round trip through our HEVC decoder with luma PSNR above 30 dB, keyframes, MP4
and QuickTime exports with AAC, ffmpeg / ffprobe reading `hevc` / `Main` / `hvc1` /
`yuv420p` / BT.709, exact sizes, hostile configurations, encoders dropped at any point, the
format list agreeing with the probe, sessions on the hardware encoder); settings_tests.rs
checks names, ids, defaults, the summary, two-pass refusal and the probe; export_ui.rs the
controls H.265 shows and hides.
Stacked on the hardware H.264 encoder (the first commit of this branch).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Export mode, the status bar and the Progress panel showed "Encoding 37%" and nothing about how
long the rest would take. They now say "37% · 2:05 left".
- `Progress::eta()` (the new `pace` module) is the time left at the job's speed over the last
15 seconds. Each call is also a reading, so the UI polling on every frame it draws the job
feeds it, and none of the places that advance `done` had to change. A trailing window follows
the real speed changes of an export (an intro, a banner, plain camera footage) where an
average since the start lags; a stall makes the estimate grow instead of freezing it; a long
idle start (the loudness pass) is not counted as the job's speed; nothing is shown until a
second of readings has progress, and nothing once the job is done or finished.
- `format_eta`: `45 s`, `2:05`, `1:02:05`, rounded up.
- Shown in the status bar ("Exporting... 37% · 2:05 left", and the `status.job.progress`
automation label), in the Export queue (on its own line: the column is narrow) and in the
Progress panel. The engine adds `etaSeconds` (null while unknown) to every job of `jobs.list`
and to the encoding items of `export.queue.list`; docs/agents.md says so.
Measured on a whole 9:29 export (193 s, Apple M1), asking every 3 s: the first estimate comes
after 6 s; from 30 s on, the predicted end of the export is on average 1.1 s early and never
more than 14.5 s off (7.5 % of the run). The status bar read "9% · 3:00 left" at 21 s and
"97% · 5 s left" at 184 s.
Tests: `pace` (steady job, speed change, stall, idle start, count going back, bounded history,
extreme values, a whole `Progress`), `format_eta`, the engine (`etaSeconds` of jobs and queue
items), and export_ui (status bar label and queue item).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The CPU compositor, which is what export runs, works on premultiplied linear f32 images,
33 MB each at 1080p, and every layer was one of them. A profile of a 9:29 export with a
hardware H.264 encoder (so the encoder is no longer the bottleneck) put 61-64 % of the busy
CPU in compositing, 12 % in zeroing and copying memory, 8-11 % each in the float -> 8-bit
and RGBA -> Y'CbCr conversions and 4-7 % in decoding the ProRes overlays: a banner that is
transparent except for its bottom 100 rows still cost a whole conversion and a whole mix,
and the bottom layer paid for a zero-filled canvas and a pass that mixes it over that canvas.
Three changes, each of which gives the bits the general path gives:
- The canvas is allocated when a layer needs it, and an opaque first layer (a Y'CbCr frame
without an alpha plane, opacity 100 %, Normal blend, the size of the output) becomes the
canvas instead of being mixed over zeros.
- A clip of plain media whose frame has an alpha plane (a ProRes 4444 banner) is converted
and mixed only inside the rectangle where its alpha is not zero: `VideoFrame::alpha_region`,
`to_linear_f32_region` and `blend::composite_at` (`composite` is `composite_at` at the
origin; Dissolve's pattern follows the pixel's place in the output). A clip with effects,
opacity masks, frame blending, colour management or a picture that is not placed one to
one on the output takes the general path, as before.
- `filmcraft_frame::pool` keeps idle f32 images (320 MiB at most). `take_f32_overwritten`
hands one back with its old contents, which the conversions overwrite whole (so no
zero-fill), and the export pipeline gives each frame's image back after the 8-bit
conversion.
Apple M1 (8 cores), a machine that was not idle, so the two builds alternate and the
median is reported:
- Synthetic 1080p frame (`cargo run --release -p filmcraft-render --example frame_bench`,
5 rounds): camera only 8.9 -> 6.5 ms, camera + banner 16.2 -> 7.8 ms, same picture checksum.
- A real project (1080p25 camera clip, ProRes 4444 banner and subscribe bar), 30 s with the
banner exported with the built-in encoder, which is deterministic: 46.4 -> 36.7 s, and
both exports are the same file (same MD5), so every frame of the real pipeline matches.
- The same project with a hardware encoder, 3 rounds: 30 s with the banner 17.4 -> 11.2 s
(CPU 99 -> 56 s), 30 s of the camera alone 11.4 -> 8.8 s, 20 s with the subscribe bar
11.1 -> 7.0 s.
Tests: `render/src/region_tests.rs` compares each shortcut with the general path bit for bit
(an opaque bottom layer, banners over and under a camera, an 8-bit sticker, every blend mode,
several opacities, moved, scaled and effect clips, a bigger source, reduced-resolution
rendering), each render twice so the second starts from recycled buffers, and checks which
layers take a shortcut; the frame tests check that a region is the crop of the whole
conversion, that `alpha_region` is the box around what is not transparent, and that a buffer
full of NaN never leaks into a conversion, for every pixel format. Breaking a shortcut on
purpose (a region one pixel off, an opaque layer taken as the canvas at half opacity, a
conversion that skips the alpha channel) makes them fail.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Export encodes H.264 on the CPU with the built-in encoder, which is most of the time
of an export (about two thirds of the busy CPU in a profile of a 20 s export). On
macOS the media engine can do it: a VideoToolbox compression session behind
`filmcraft_export::VideoEncoder`, registered by `filmcraft_platform::register()`.
- `ExportSettings::hardware_encoding` (`HardwareEncoding::Off` | `Auto`; Export > Video >
Hardware Encoding, `"hardwareEncoding": "off|auto"` in `file.exportMedia`, a boolean is
understood too), off by default: the built-in encoder's output is byte-identical on
every machine, a hardware encoder's is not.
- `videotoolbox_encode` (FFI, same containment as the decoder: SAFETY comments, no
panic across the callback, safe API) and `hardware_encode` (safe adapter and factory).
- The factory declines, so the built-in encoder is used, for other formats, MXF, two-pass
VBR, HDR, odd sizes, non-square pixels and any configuration VideoToolbox cannot create a
hardware session for. An encoder that fails mid-export stops it with an error: unlike a
decoder, it cannot hand a half-written stream to another one.
- No B-frames: on real 1080p footage the quality is the same with and without them
(+-0.3 dB at equal bitrate) and the bitrate lands closer to the target, and without
them every frame is presented at its decode time (no composition offsets, no edit list).
- `export::register_encoder` ignores a factory registered twice; `encoder_registered`.
Apple M1, 8 cores, single runs: a 9:29 timeline (camera clip, ProRes 4444 overlays with
alpha, AAC, loudness) exports in 311 s instead of 793 s (2.5x faster); its densest 134 s
in 84 s instead of 395 s (4.7x). The outputs compare at SSIM 0.990 / PSNR 46.5 dB. The encoder
alone runs 200 fps at 1080p (docs/performance.md).
Tests (tests/hardware_encode.rs): what is taken and declined, a round trip through our
software decoder, an export that decodes in our decoder and in ffmpeg, the fallback for
everything hardware declines, exact output sizes, an empty range, hostile configurations
and encoders dropped at any point; the engine's `hardwareEncoding` parameter.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
NVENC through nvEncodeAPI64.dll (API 12.1, FFI layout checked against the C header by generated
tests), registered as an Export encoder in front of the software one; Export > Hardware encoding
(off by default). Declines two-pass VBR, HDR, MXF, unsupported sizes; B-frames, IDR/GOP, VUI, avcC.
Tests: decode with our decoder (PSNR), pipeline export vs software, decline paths.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMorBHwct72udS2LiC3ZHp
Language::PtBr is persisted as pt-br and translated through a new
PORTUGUESE table with the same 347 menu labels as SPANISH, so every
translated key still resolves to a real menu item. The new
app.language.portuguese command checks, switches and restores the
language like the other three, and the native menu maps it back.
Cargo.lock is untouched.
file.import created new items but never rebuilt Session::offline.missing.
The UI reads that list to drive the Program slate and the Project
panel 'Media missing' badge, so any re-imported item the project
already listed as missing stayed listed as missing until on_open ran
again (project reopen) or the caller manually relinked.
Add a single crate::relink::refresh(s) call at the end of file.import,
matching the call on_open already makes after a project opens. Closes
the slate/badge symptom without requiring reopen or per-item relink.
The deeper SharedSource cache eviction issue the reporter observed
with media.autoRelink is out of scope here; a follow-up should call
MediaPool::remove for items whose path now resolves before re-insert.
Co-Authored-By: ninso112 <ninso112@pm.me>
Co-Authored-By: Luna161 <Rosenkranzrene@gmail.com>
Add Edit always cut every targeted track, so with one clip selected it cut
all tracks. As in Premiere, selected clips under the playhead are now cut
and nothing else; with none there, the targeted tracks are, as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
egui 0.36 reports pressing Shift, Ctrl, Alt or the Windows / Cmd key as a
key event of its own, before the key they modify. The Keyboard Shortcuts
recorder took that first press and refused it ("AltLeft can't be used as a
shortcut"), so no chord with a modifier could be recorded. Modifier keys on
their own are now skipped; the chord is recorded with the key they modify.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every layer of the selected graphic was outlined on the Program monitor,
so a box stayed after its layer was deselected, and a layer whose
visibility was off kept its box and could still be clicked and dragged.
As in Premiere Pro 26: a layer has a box only while it is selected (or
typed into), and a layer whose visibility is off is not on the monitor
at all: no box, no handles, no click target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Looking at a folder probes each visible file for its properties. A format
no streaming reader takes (AVI, which isn't supported at all, and WAV,
MP3 and the other byte-opened formats) fell through open_reader's
whole-file path: a 290 MB AVI took 518 MB of memory and a full read just
to report that it isn't supported, a 247 MB WAV 500 MB.
- filmcraft-media: open_reader_within(.., max_whole) refuses, after the
sniffing head, a file no reader opener takes when it is larger than
max_whole. open_reader is open_reader_within with no limit.
- engine: MediaPool::probe_file uses it with PROBE_WHOLE_FILE_MAX (64 MiB);
media_browser::probe calls probe_file. Import and playback still use
open_file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extends the Windows backend (#141) to VP9 (profiles 0 and 2) and AV1 (main, 8/10-bit), 4:2:0.
- codecs::hw: FrameStreamInfo (vpcC / av1C): profile, bit depth, chroma, picture size and colour as
the software decoders report them (vp9_color / av1_color are now shared with them), random-access
samples, in-band format changes; StreamInfo lets HybridDecoder handle NAL and frame-coded streams.
- platform: media_foundation::stream (what each codec needs: DXVA profile, MFT request, input
format), MfDecoder generalised; VP9 frames / AV1 temporal units go in as they are; the MFT's
declared picture size (display aperture) is checked against the bitstream's.
- Declined up front: VP9 profiles 1/3, AV1 profiles 1/2, 4:2:2/4:4:4, mono, 12-bit, no codec
extension, no DXVA profile. A key frame of another size or a different sequence header falls back
to the software decoder.
- Tests: bit-exact parity (360p / 1080p / 2160p, seeks, resets, flush, hidden frames), forced
failure, damaged samples and av1C, format change fallback, WebM / Matroska through the media stack.
- Bench: AV1 fixtures with libaom when ffmpeg lacks SVT-AV1.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMorBHwct72udS2LiC3ZHp
readFile (missing path), importUrl and openProject (HTTP errors) and openProject (neither a File
nor a URL) rejected with plain strings, so e.message was undefined and e instanceof Error false,
unlike execute / request. Their promises now go through as_error, which wraps a string in an Error
and passes an Error through unchanged.
Checked in Chrome against the web build: all four cases reject with an Error carrying the message
("/files/nope.mp4: not found", "…: HTTP 404", "need a File or URL").
Fixes#90
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manifest granted only xdg-pictures, on the premise that the portals make filesystem access
unnecessary. But a project refers to its media by absolute path and the document portal exposes
only the project file, so a project in ~/Videos or ~/Music opened with all its media missing.
Grant the home directory and the usual external-drive mount points (/media, /run/media, /mnt), as
other video editors on Flathub do; nothing system-wide.
Fixes#107
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/releasing.md (#99): release.yml points readers to it twice and it didn't exist. Written from
the workflow and packaging scripts as they are (macOS, Windows x64/x86/arm64, Linux x86_64/aarch64
with the glibc 2.35 floor, web; draft release, secrets).
- filmcraft-cli help (#86): export lists --scale, --quality, --no-audio and --queue, and apv among
the formats.
- README (#87): more than 650 engine commands (671 today), not about 135.
- README (#88): the web line says the browser app ships with every release and links docs/web.md.
- CLAUDE.md (#85): the Illustrator-class sibling is ../vectorcraft (formerly DrawCraft).
Fixes#85Fixes#86Fixes#87Fixes#88Fixes#99
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#146 landed the same transcript.generate / downloadModel / import changes, so this branch keeps
main's versions of those files and adds what #146 missed: Transcribe Sequence (sequence.transcribe)
is disabled like transcript.generate in a build without speech-to-text, and docs/transcripts.md
describes the disabled state. A test asserts both directions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The playhead follows the audio clock (frames the output device consumed).
An output stream can open without error and then never call back (ALSA
with a busy or misconfigured device): the clock stayed at 0 and Play did
nothing, with no message.
advance_playback now notes when the audio clock last moved; if it stands
still for AUDIO_STALL_S (1 s) while playing, playback continues from where
the audio got to on the wall clock, the stream is stopped, a warning is
logged and the status bar says the audio output is not responding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
file.exportMedia with wait: true was forwarded to the app unchanged. The app then encoded on its UI
thread: it showed no progress and answered no other control request until the encode ended, and the
control server's 60 s reply limit reported "timeout" for any longer export while it went on and
succeeded.
BridgeClient::execute now starts the export without wait and polls jobs.list until the job
finishes, returning the same {job, path, result} (or the export's error). filmcraft-cli --bridge
exec and MCP bridge mode both use it, and MCP command_run in bridge mode now gets the same progress
notifications and cancellation as headless mode.
Tested against a fake app (wait: true never reaches it, one export started, result and error
returned) and end to end against the desktop app: a blocking demo export returned its 642-frame
result with exit 0 while ui.inspect was answered in ~0.05 s throughout.
Fixes#91Fixes#92
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- transcript.generate is enabled only with a recogniser: one installed in
Session::transcriber, or the build's own (feature `whisper`). Otherwise
describe, command_list enabled_only and the menu report it disabled with
"speech-to-text is not available in this build" as the reason (#97).
Transcribe Sequence, which runs it, follows.
- transcript.downloadModel is enabled only with `speech-download` (#98).
- file.import with Automatically transcribe clips on no longer skips
transcription silently when it can't run: the disabled command's reason
lands in the import's errors as "transcription: ..." (#89).
- docs/transcripts.md describes the disabled state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The binaries are built on Ubuntu 22.04 (glibc 2.35). The deb says libc6 (>= 2.35), but the rpm
named no glibc floor, so dnf installed it on the RHEL 9 family (glibc 2.34), where both binaries
then failed with "GLIBC_2.35 not found". Now dnf refuses the install there.
Fixes#93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- transcript.generate is enabled only with an installed transcriber or the whisper feature; it
reported enabled and then always failed with "speech-to-text is not available in this build".
- transcript.downloadModel is enabled only with the speech-download feature.
- An import with Automatically transcribe clips (all imported) on reports a
"transcription: speech-to-text is not available in this build …" error instead of importing
silently untranscribed.
Fixes#97Fixes#98Fixes#89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- File ▸ New ▸ Color Matte… (and Project ▸ New Item) opens a New Color Matte
dialog: color picker, name, width, height and duration. Called with params
(CLI, control, MCP) it still creates the matte directly.
- project.matteColor {item?, color}: change a Color Matte's color (the
selected one by default); every clip of it follows and it undoes.
Double-clicking a Color Matte in the Project panel opens its color picker,
as in Premiere.
- media pool: a generator's cache key now holds its parameters, so a matte
whose color changes (or comes back with undo) is generated again instead of
the pool serving the first one forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both tools are annotated readOnlyHint, but a call with `seconds` moved the active sequence's
playhead (and, with Selection Follows Playhead on, the selection) to that time, in headless and
bridge mode alike.
- headless: Session::render_program_at renders the frame at `seconds` (frame-snapped, as the
playhead would be) without touching the session.
- bridge: the Program monitor screenshot needs the live playhead there, so the tool records the
playhead and selection, takes the screenshot and puts both back (the selection only if it changed,
since selecting also leaves trim mode).
Regression test: rendering at 5 s and 8 s with Selection Follows Playhead on leaves playhead and
selection unchanged and still renders the requested frame (it moved the playhead to 8 s before).
Fixes#114
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text layer is now one of two types, each with Premiere Pro 26's handles:
- Point text (Type tool click): every handle scales the layer about its
anchor point, both axes alike; the font size is untouched.
- Paragraph text (Type tool drag): the dragged rectangle is a box the text
wraps in. Handles move that side or corner of the box, the opposite side
stays, the text re-wraps at the same size. Lines below the box are not
shown and the bottom-right handle turns into a red plus.
- The anchor point can be dragged with the Selection tool; the layer stays.
It is picked before anything under it, so a shape dragged by its exact
centre moves its anchor.
- Properties > Text > wrench opens Text Properties, where Text Layer Type
switches a layer between the two (graphics.setTextType); the text stays
where it is.
Shapes keep stretching away from their opposite side.
New: text layer parameter box_height, graphics.newText `box`, and
textType / box / overflow / anchor / scale in graphics.list. A layer saved
before a parameter existed can now be given it with graphics.set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
make_sequence had ffmpeg write numbered stills straight into the shared seq_<ext> directory and
treated it as ready once frames 1 and 12 existed. Two tests generating it at once (png_sequence_exact
and missing_frames_hold_the_previous_frame) read each other's half-written PNGs; one that fails to
decode is held as missing, so frame 6 equalled frame 3. Six parallel processes on a fresh fixture
dir failed 18 of 120 runs before, 0 of 120 after.
ffmpeg now writes into a testkit temp_path directory that is renamed into place (with a .complete
marker) only when all twelve frames are there; the gap copy is private to its test.
Fixes#125
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- codecs: a resampled MP4 audio read scales the edit-list offset to the source rate in i128 and
saturates the window arithmetic; a hostile media_time overflowed i64.
- engine: media_duration follows a subclip's parent chain in a loop bounded at 16 hops instead of
recursing; a cyclic or very deep chain in a damaged project overflowed the stack.
Each has a regression test that failed before (overflow panic / stack overflow).
Fixes#66
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 1-pass ABR rate factor counts one second of virtual inter frames so the
first I frame gets a fair share of the budget. That history was priced once,
at the first frame, with the model's initial coefficient guess, which is
several times too high on ordinary content, and then only faded out. Every
VBR/CBR export therefore coded its first second 6-9 dB below the rest.
A virtual frame now costs what the P and B frames coded so far cost on
average (the old guess only until the first of them), re-estimated per frame.
On the synthetic 640x360p60 test at 2 Mb/s, 0.1-0.5 s goes from 25.0 dB to
32.1 dB against 33 dB after the first second, at the same bitrate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New backend in crates/platform (media_foundation/): a Direct3D-aware decoder MFT is driven at the
level of single access units (Annex B), given the process's D3D11 video device through an
IMFDXGIDeviceManager so it decodes with DXVA, and its NV12 / P010 textures are read back through a
staging texture into planar Yuv8 / Yuv16. H.264 Baseline/Main/High and HEVC Main / Main 10, 4:2:0.
- Hardware is verified: non-D3D-aware or async MFTs, streams the GPU's DXVA decoder does not list,
and system-memory output (Microsoft's MFT software fallback) are declined or fail the stream so
HybridDecoder continues with our decoder.
- mfplat.dll is loaded at run time so Windows N editions still start.
- register() / registered() / hardware_decoder_for() cover Windows; perf.stats reports the backend.
- Tests (Windows): bit-exact parity with the software decoders at 640x360, 1080p and 2160p
(resets, seeks, flush), forced mid-stream failure, damaged input, concurrency, repeated init.
- Bench: runs on Windows (ffmpeg on PATH, process/thread CPU clocks), HEVC Main 10 2160p row,
hardware counters per row.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMorBHwct72udS2LiC3ZHp
A negative clip duration no longer panics the clamp when a nest follows its contents; a nested
sequence's claimed sample rate is clamped before it sizes the mix buffer; audio_needs lists each
nested sequence once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: both branches change start_audio, so the nightly updater stops on the conflict.
Solution: keep play-ahead's structure; the rate converter lives in playback_mix (now FnMut).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with the device at another rate than the sequence (44.1 kHz
device, 48 kHz sequence), each buffer truncated its source start (phase
lost every block: a buzz), took the nearest sample (aliasing), and asked
the mixer for overlapping spans, so the mixer's graph cache missed every
buffer and rebuilt effect state.
Solution: audio-dsp StreamResampler interpolates each output sample from
its absolute position (exact rational), bit-identical however the stream
is cut, and pulls the source as one contiguous stream. start_audio uses
it when rates differ and restarts it when the rate or layout changes.
Context: tests cover block-size invariance at four rate pairs, a 1 kHz
tone within 3e-3, contiguous pulls, seeks and hostile input. Same lines
as #131; whichever lands second needs a small rebase.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: playback decoded and mixed every ~10 ms device buffer inside the
cpal callback (AAC decode, effects, opening a clip's file at a cut, rayon,
locks). On a loaded Mac any stall missed the device deadline: static and
dropouts, different on every play.
Solution: play_ahead mixes on its own thread into a lock-free ring kept
200 ms ahead; the callback only copies. An underrun plays silence, is
counted, and the mixer skips the same span so sound stays locked to the
picture. Any device width plays the channels it shares with the mix.
perf.stats reports playback.audio (callbacks, underruns, missingFrames,
minLeadMs). The program mix moved to playback_mix so bench_audio can play a
real project through a simulated 48 kHz device and count dropouts.
Context: on a real 22-sequence project, load average 38-58, 15 s per cut:
mixing in the callback overran 8-170 times, mixing ahead 0 (ring never
below 149 ms). The web build keeps mixing in its callback (no threads).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- aac: the decoder notes SBR data in fill elements (EXT_SBR_DATA /
EXT_SBR_DATA_CRC), so implicitly signalled HE-AAC is recognised;
Decoder::sbr() covers explicit signalling too.
- codecs: aac_output_rate() gives an AAC stream's output rate (the
extension rate, or twice a <= 24 kHz core when SBR data is present); the
MP4 reader reports it instead of the core rate, so sequences made from an
HE-AAC clip get 44.1 kHz instead of 22.05 kHz. Our decoder now also takes
explicitly signalled HE-AAC (it decodes the AAC-LC core) and, when the
stream rate is twice the core rate, interpolates the core to it with a
streaming half-band upsampler. The SBR high band is not reconstructed yet.
- export: the AAC bitrate is capped at 6 bits per sample and channel
(ISO/IEC 14496-3 frame limit) instead of failing with "bitrate out of
range" (320 kbps stereo at 22.05 kHz).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Linux, macOS, web and Windows packaging still carried PhotoCraft's
image-editor metadata.
- nfpm.yaml (#94): deb/rpm summary and description describe the video
editor; filmcraft-cli is a command runner, MCP server and exporter;
Debian section video instead of graphics.
- AppStream (#100): AudioVideo/Video/AudioVideoEditing categories, matching
the desktop entry; video keywords; the screenshot is the README hero image
(the old filmcraft-demo.jpg URL was a 404).
- macOS Info.plist.in (#100): export the .fcproj project type (JSON) instead
of PhotoCraft's .pcraft; app category video; drop the PSB and QOI imported
types, which FilmCraft does not read.
- filmcraft.wxs header comment: the MSI declares no file associations.
- web hosting guide: iframe title "FilmCraft video editor".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add Español next to English and 日本語 (Edit ▸ Language ▸ Español, also in the
native menu bar). SPANISH covers every menu, submenu and menu item label
(347 entries, Latin American Spanish); names, formats and percentages stay
as they are. A new test checks that every entry is a current menu label, so
a renamed command cannot leave a dead entry behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured from Premiere screenshots at 1x: panel tabs and tooltips are a
humanist sans at 12 px; the big timecode is regular weight, 76 x 10 px
for 00:00:16:05, blue #4096f3; menus are the system's, 13 pt, #dedede.
- The interface face is Source Sans 3 (OFL), the closest open face to
Premiere's, read from craft-fonts (CRAFT_FONTS_DIR) like the Japanese
faces. Inter stays right behind it for symbols it lacks, and is the
face when the app is built without craft-fonts. The web build embeds
it too.
- The timecode over the timeline, under the Source and Program monitors
and in the Reference Monitor is regular weight at 16 (Source Sans 3's
width for Premiere's), in the timecode blue #4096f3 (also the scrubby
value blue).
- The menu bar's menus and submenus are 13 pt in #dedede.
Tests: with craft-fonts, every weight of the interface text is Source
Sans 3 with Inter behind it; without, the interface keeps Inter; Japanese
still renders from craft-fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel menu mark (≡) on the active tab was three 1.5 pt lines placed
at fractional positions, so at 1x they straddled pixels and anti-aliasing
made the gaps look uneven (3 px then 4 px). Every line icon had the same
half-pixel blur.
The icon pen now draws strokes a whole number of device pixels wide and
snaps every horizontal or vertical segment (and any that runs less than
a pixel sideways) onto the pixel grid, rounding from the icon's centre
the same way on both sides, so equal steps on the 16 grid stay equal on
screen at 1x and 2x. The tab's menu mark is now the Hamburger icon drawn
by the pen instead of hand-drawn lines.
Tests: every icon at 1x and 2x at four sizes off the pixel grid has only
whole-pixel straight lines; the menu mark's two gaps are equal at 1x,
1.5x and 2x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A line that is not a JSON object with a string `method` (an HTTP request
line, other non-JSON text, a JSON array, invalid UTF-8, a line over 4 MiB)
now gets one error reply and the connection is closed, so a browser's
cross-origin fetch can't smuggle a command in its body. Connections are
capped at 16. Same transport as LightCraft (storytold/lightcraft#129).
Fixes#119.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A linked clip that spans the cut while its partner reaches past it too is
no L cut, and still refuses, like the same clip unlinked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ripple trim that shortens a clip's head was refused when the previous
clip's linked sound ran on past the cut on a sync-locked track (an L cut).
That clip never moves, so its sound stays put and the L cut still ends
where it did. Unlinked material across the cut (a music bed) still refuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Window > Workspaces only had the nine built-in layouts; a layout
you arranged could not be kept, renamed or brought back.
Solution: one alphabetical list of built-in and user workspaces, plus
Save Changes to this Workspace, Save as New Workspace..., Reset to Saved
Layout and Edit Workspaces... (rename, delete; built-ins restore instead
of delete). Saved in workspaces.json beside preferences.json, so an older
build rewriting preferences never erases them. The workspace in use
reopens at launch; ui.set and window.workspace.<name> switch to user
workspaces like built-ins.
Context: tests in crates/ui-egui/tests/workspaces_ui.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three problems with scrolling the Timeline:
1. Cmd + wheel zoomed, like Option + wheel. egui turns Cmd + wheel into a
zoom before the panel sees it, and the panel only knew "zoom" and "scroll
sideways".
2. The plain wheel scrolled sideways; the tracks could only be scrolled up
and down with Shift held.
3. The view could be scrolled without limit past the end of the sequence (39
minutes past a 26 second one). There the zoom scroll bar's thumb sat at
the right end and stayed there when dragged: the bar's range grew with the
scroll position, so the view moved but the thumb did not.
Now, as Premiere Pro 26.5.2 does on macOS (checked in the app):
- wheel: the tracks under the pointer, up and down (video tracks above the
divider, audio tracks below);
- Cmd + wheel: the Timeline, sideways;
- Option + wheel: zoom about the pointer.
The existing setting Timeline Mouse Scrolling ("Vertical" by default) is
wired up: "Horizontal" swaps the first two. A sideways gesture (trackpad
swipe, tilt wheel, Shift + wheel) always scrolls sideways and a pinch zooms.
The wheel is read from its events, with their own modifiers.
The Timeline ends ten minutes past the end of the sequence, where Premiere's
view stops too, and the view is kept inside it however it is moved (wheel,
scroll bar, zoom, a script; not-a-number positions are reset).
The zoom scroll bar spans that extent. Its thumb is as wide as the share in
view, never narrower than 34 points, and its travel covers the whole scroll
range: it reaches both ends of the bar and stays under the pointer while
dragged (the drag follows the pointer's position, not a sum of steps). A
click or press beside the thumb brings the thumb there. The handles at its
ends zoom as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The placement and track type lists had a fixed width, narrower than the
dialog, which left an empty band down its right side. Each row is now a label
in a column as wide as the widest label and a list that takes the rest of the
row, up to the group's right edge, as in Premiere's dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The probe guessed the pixel format from the codec profile alone: HEVC
reported nothing, H.264 High 4:2:2 had no bit depth, and ProRes 4444
claimed alpha because of its fourcc.
- HEVC: chroma format and bit depth from hvcC.
- H.264: from the avcC High-profile extension (ISO/IEC 14496-15 §5.3.3),
else from the first SPS; unknown (empty) if neither can be read.
- ProRes: the first frame header gives the chroma format and alpha
(alpha_channel_type), as the first DNx frame header already does.
Checked against ffprobe on ffmpeg-made HEVC 4:2:0/4:2:2 8/10-bit,
H.264 4:2:0/4:2:2 8/10-bit and ProRes 422 HQ / 4444 with and without
alpha. Unit tests cover hvcC, avcC (extension and SPS fallback) and
ProRes 4444 with and without alpha.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sequence ▸ Add Tracks… added one video track at once: no dialog, and no way
in the interface to add an audio track.
It now opens the dialog Premiere Pro 26.5.2 shows, checked in the app:
- "Add video tracks": Amount, Placement.
- "Add audio tracks": Amount, Placement, Track type (Standard, 5.1, Adaptive,
Mono).
- "Add audio submix tracks": Amount, Placement, Track type (Stereo, 5.1,
Adaptive, Mono); its placement is off while the sequence has no submix
track.
Placement is "Before First Track" or after one of the tracks, after the last
by default. The defaults are Premiere's: one video and one audio track, no
submix track. OK is one undo step.
`sequence.addTracks` takes the same choices as params (`videoAfter`,
`audioAfter`, `submixAfter`, `audioType`, `submix`, `submixType`) and returns
the new tracks' ids. Called without a place it still appends, and without
amounts it still adds one video track, so scripts keep working; only the menu
item opens the dialog. Tracks are numbered by their place, as in Premiere: a
track added after Video 1 is the new Video 2 and default names above it move
up, while a name the user gave is kept.
Amounts were not checked before (any number of tracks was created). At most
99 tracks are added at a time and a sequence has at most 999 of a kind; a
wrong amount, place or type is an error that changes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/architecture.md still listed every nested sequence among the things
rendered on the CPU and handed to the GPU as one image. Since NEST11 that
holds only for nests that are not plain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/captions.md still said nested sequences do not burn in their own
captions. Since NEST10 the captions of a nested sequence are drawn wherever
the nest is shown, in the Program monitor and in exports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nested sequence was left out of both formats as a gap, with a report entry
telling the user to "render and replace" it first (a command FilmCraft does
not have). An AAF with nested compositions was read with gaps in their place.
What Premiere Pro 26.5.2 does, read off files it exported for a sequence with
a nest:
- AAF: the nested sequence is a composition mob of its own, and its clips are
source clips that point at it.
- OMF: the nested sequence's sound is rendered into the document as one clip
named after the sequence.
Export
- AAF: a nested sequence is written as a composition mob (no usage code; the
exported sequence stays the top-level one). A clip of it is a source clip
at that mob's first slot of the clip's kind, its start in that slot's edit
units. Nests in nests are followed; a nest used twice is written once. With
embedded or consolidated audio, the media of the clips inside a nest is
prepared with the rest (`NestNeeds::Inside`).
- OMF: `audio_needs` lists the range of the nested sequence each nest clip
plays (`NestNeeds::Render`); the engine mixes it through the export audio
pipeline with what is on the clip, and it is written as media. Without that
essence the nest is a gap and the report names the sequence.
Import (AAF)
- A source clip that points at a composition becomes a clip of a nested
sequence; the composition is read once.
- Premiere Pro's layout is read: all video tracks in one slot (a nested scope
with a segment per track; before, only the last segment was read, so a
Premiere AAF came in with one video track), and "top-level" tagged on nested
compositions too (top-level are those no other composition uses).
- A composition without a frame size takes that of the first picture it
shows, not of the first picture in the file.
A sequence that claims to be inside itself is a gap with a report entry in
both directions; the walks are bounded (16 levels).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The export report already said a nested sequence is written "as a single
source (reel AX)", but the reel came from the reel mode: with the default
(file name) a nest called "Nested Sequence 02" got reel NESTED_S. A nested
sequence has no tape and no file, so its reel is now AX whatever the mode.
Premiere Pro 26.5.2 writes the same for a nest (seen in an EDL it exported):
one event from reel AX, the nested sequence's name in "FROM CLIP NAME", and
the nested sequence's own time as source timecode. A test pins that event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while looking at the tab menu in the running app: once open, a panel
menu stayed open until one of its entries was chosen. It was meant to close on
a click elsewhere, but the test for that needed the press and the release of
the click in the same frame, which a real mouse never delivers. A right-click
on a tab now opens the menu, so the stuck menu was easy to run into.
The menu now remembers the frame it opened in (the click that opens it is not
a click elsewhere) and closes on any later click outside it, or on Escape. A
right-click on another tab moves it to that tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Premiere Pro 26.5.2 reopens a project with the same sequence tabs, in the
same order, the same one shown, each at the zoom it was left at. FilmCraft
opened every project on its first sequence; the "Restore open sequences when
opening projects" preference existed and nothing read it.
A saved project file now has a `view` beside `project`: the open sequences in
tab order, the active one, and each sequence's view from NEST12. It is not
part of the project: no undo step, no "edited" mark, and no schema bump (older
builds ignore it). `file.open` restores it when the preference is on.
Nothing in the view is trusted. Ids that are not sequences of the project and
repeats are dropped, numbers are clamped, and a view that cannot be read is
ignored instead of failing the open. A view with no open sequence leaves the
project on its first sequence, so a project saved by a script or the CLI does
not open on an empty Timeline.
Opening or recovering a project now tells the frontend which sequence is
shown, so the Timeline takes that sequence's view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked in Premiere Pro 26.5.2: a right-click on a sequence tab that is not
shown shows it and opens the panel menu, which has "Close Panel", "Close Other
Timeline Panels" and "Reveal Sequence in Project"; a dragged tab follows the
pointer and its neighbours make room.
- A right-click on any tab shows it and opens its panel menu.
- The Timeline's menu gains "Close Other Timeline Panels" (new command
`sequence.closeOthers`); its entries have automation ids
(`panel.menu.Timeline.close`, `.closeOthers`, `.revealSequence`).
- A sequence tab dragged sideways changes places with a neighbour when the
pointer passes that neighbour's middle (new command `sequence.moveTab`).
- Tabs that do not fit: the shown tab is always in the strip (tabs before it
are left out until it fits), and the chevrons at the right end open a list
of every tab instead of stepping to the next one. Ids `timeline.tabs.more`
and `timeline.tabs.list.<sequence>`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Timeline had one view for all sequence tabs, and showing another sequence
fitted it to the panel every time. In Premiere Pro 26.5.2 each sequence tab
has its own zoom and its own track heights (expanding the tracks of one
sequence leaves the others as they were).
Each sequence now has a `SequenceView` (zoom, horizontal scroll, the vertical
scroll of the video and audio halves, video and audio track height) in
`EditorState::timeline_views`. The panel's view is the active sequence's: it
is put away when another sequence is shown and taken back when the sequence is
shown again, also when a tab is closed. A sequence shown for the first time is
fitted, at the default track heights. A view changed in the session (a
command, the control channel) is taken over by the panel. The view of a
deleted sequence is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frame plan. A nest was always handed to the compositor as one image rendered
on the CPU. A plain nest (same frame size and colour settings as the sequence
it is in, no effects, masks, opacity, blend mode or motion on the clip, every
layer inside it blended normally, not a multicam angle) now contributes its own
layers to the plan, with its captions, so the GPU composites them directly.
Nests in nests are followed up to the nesting depth limit. Anything that is
not plain is planned as one image as before.
Waveforms. Premiere draws a waveform on the sound clip of a nest. The Timeline
now mixes the nested sequence in the background (under `catch_unwind`), keeps
one peak per 256 samples, and mixes again when the sequence's sound changes,
showing the old waveform until the new one is ready.
Tests: a plain nest is planned as its own layers and looks the same as the
reference render, also nested twice and over another layer; five kinds of nest
that are not plain are still drawn right; a nest's sound clip gets a waveform
that follows its sequence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sound. A nest's audio went around the path that clips of media take, so speed,
reverse and the clip's audio effects did nothing to it, and a nested sequence
at another sample rate was mixed at the outer rate without converting. The
nested mix is now read through the same path as a media source (`effected`
takes a reader instead of a `MediaSource`): it is mixed at the nested
sequence's own rate, resampled to the outer one, and then goes through speed,
reverse and the clip's effects like any clip. The nesting depth limit still
applies.
Captions. Premiere Pro 26.5.2 draws the captions of a nested sequence in the
sequence it is nested in, as part of the nest's picture. A nested sequence is
now rendered with its captions whether or not the outer sequence shows its
own, and the preview cache key of a nest covers its caption tracks.
Tests: a nest sounds as its sequence does, keeps pitch and timing at another
sample rate, changes with speed and reverse like a clip, and starts later when
trimmed in; speed, reverse and frame hold pick the right frame of the nested
sequence; opacity and a Crop effect apply to the nest's whole picture; nested
captions are drawn and outer ones only when asked for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nested sequence whose frame size differs from the sequence it is in comes in
centred at its own size with Scale 100, as in Premiere Pro 26.5.2 (a 1280x720
nest in a 1920x1080 sequence). The clip context menu gains Premiere's "Fit to
frame" and "Fill frame" next to "Scale to Frame Size".
Tests pin the behaviour: a smaller nest sits in the middle, a larger one is
cropped by the frame, a nest at another frame rate shows its sequence at the
same moment, Fit and Fill frame set Scale to 150 for that pair of sizes, and
Scale to Frame Size fills the frame and leaves Scale alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enable (ticked when on), Flatten, and the cameras of the selected nested
sequence clip to switch to; greyed out when no selected clip is a nested
sequence. The commands already existed in the Clip menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clip.revealInProject (clip context menu) selects the clip's source in the
Project panel and shows it there: the panel comes forward with the search
cleared, the list opens the bins on the way, and the icon and freeform views go
into the bin that holds the item. sequence.revealInProject (Timeline tab menu)
does the same for the sequence being edited.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mark In / Mark Out on a sequence loaded in the Source Monitor did nothing: the
marks were only stored for media. They are now the sequence's own In and Out, so
Insert and Overwrite edit in the marked part (as a nest, or as its clips with
the nest toggle off).
Tests also pin Match Frame on a nest: it loads the nested sequence at the frame
the nest shows, through a trim and a speed change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Timeline's "Insert and overwrite sequences as nests or individual clips"
button was drawn always on and did nothing. It now toggles
(sequence.nestSequences). Off, a sequence is edited in as its clips, as observed
in Premiere Pro 26.5.2: the clips come with their transitions and links, the
source's tracks that hold clips go to consecutive tracks from the destination
track up, and missing tracks are added. A sequence can then go into itself as
plain clips. Multi-camera sources stay one clip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Observed in the app on 2026-10-06 (about 35 test nests):
- The nest is a linked picture + sound pair only when the selected sound is on
one track and that track is free for the nest's length. With sound on several
tracks, or another clip in the way on its track, the nest is picture only: the
selected sound stays in the sequence (unlinked) and a copy with its own ids
goes into the nest. How many video tracks the selection uses does not matter.
- Where the picture goes. With L the lowest selected video track and `a` the
number of the highest audio track holding selected sound, it takes the lowest
track T from L up such that T is free for the nest's whole length, and so is
every video track numbered from T up to a - 1 that the selection had clips on.
If none will do, a new track is added. Before, the nest could land on top of
an unselected clip.
- In the new sequence, picture tracks keep their positions up to the highest one
used. Sound tracks are A1 followed by the other tracks used, in order
(A1 + A3 become A1, A2; A3 alone becomes an empty A1 and A2).
- After Nest nothing is selected in the timeline and the new sequence is
selected in the Project panel; clip.nest returns the nest's clips.
- Make Subsequence loads its result in the Source Monitor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nest keeps its length when its sequence changes. Tests now pin what already
held: it trims out to reveal added material and no further, shows and plays
nothing past shortened contents, and the outer sequence shows edits made inside.
New: the Timeline hatches the part of a nest past the end of its contents, as
Premiere does (Project::nest_overhang, automation id timeline.clip.<id>.empty).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nest… copied only the clips: transitions between them were lost and the new
sequence's tracks had default names and stereo layouts. Transitions whose clips
are all nested now go into the nest with them, and the tracks are laid out like
the parent's (shared with Make Subsequence). Clips on locked tracks are left out
instead of being duplicated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sequence placed inside itself (directly or through another nest) crashed the
app: nested audio mixing and the frame plan's multi-camera shortcut recursed
until the stack overflowed.
- render: the audio mixer and the multi-camera shortcut follow nests to the same
depth as the picture (MAX_NEST_DEPTH), then give silence / fall back.
- project: Project::nest_cycle finds a sequence that contains itself.
- engine: every edit that would create one is refused, whichever command asks
(place, Source insert/overwrite, paste, ...). A project opened with a cycle
already in it can still be edited and repaired.
Regression tests: both crashes (stack overflow before), the refusals, repair.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync-locked tracks rippled from the grabbed member's start, so grabbing a
J cut's early sound checked and shifted from before the picture's cut and
was refused. For a head trim the cut is the latest member's start. Adds the
test only the new stretch catches: a clip starting at the cut on a
sync-locked track, which the old stretch let fall out of sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ripple trim that shortens a clip's head was refused as a sync-lock
conflict whenever a sync-locked track held material just BEFORE the cut,
because the check looked at the stretch before it. With a J cut that is
the outgoing clip's sound on the other audio track, so the J-cut take
could never lose its head. The stretch that closes is the one just after
the cut; the clip's own linked partners stay the caller's to trim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GPU compositor uploaded only the Y, Cb and Cr planes of a Yuv8/Yuv16 frame and
the shader returned alpha 1.0 for them, so a ProRes 4444 overlay (banner, lower third,
outro) was drawn opaque in the Program monitor and covered the footage under it.
Exports were correct because they use the CPU compositor.
Frames with an alpha plane now upload it as a fourth texture (R8Unorm, or R16Float
converted like the other planes), bound at binding 5 of the layer and blend layouts.
A uniform slot (p3.y) carries the alpha scale (0 = no alpha plane); the shader reads
the alpha at full resolution and premultiplies the linear colour by it.
Adds gpu_draws_yuv_alpha_plane_like_the_cpu: an alpha ramp over an opaque layer at
8, 10 and 12 bits, GPU against the CPU compositor. It fails without the change
(p99 diff 190/255) and passes with it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Checked in Premiere Pro 26.5.2: the tab's menu item is "Close Panel" and it
closes that sequence's tab. Premiere also leaves nothing selected after Nest,
so the tabs test no longer relies on the selection to find the nest clip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clip duration came from the media duration (`mdhd`), which runs on
the decode timeline. ffmpeg / x264 MP4s with B-frames skip the reorder
delay with an edit, so every one imported a frame too long (a 120-frame
file became 121 frames, the last a held repeat). A video track delayed
by a leading empty edit was a frame short, and its last frame could not
be reached.
Use the sum of the track's edits (movie timescale, what `tkhd` records)
when there is an edit list. Fragmented files, and edit lists that add up
to nothing or overflow a Tick, keep the media duration.
Tests: a unit test with crafted edit lists (skip, empty edit, hostile
sizes) and an ffmpeg oracle test (libx264 with B-frames: 60 frames,
was 61).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An overwrite move shortens, splits or removes whatever already sits where a
clip lands, as Premiere's drag does, but the reply only named the clips it
moved: a script moving clips one at a time never learned that an earlier move
had cut one it had not reached yet. The reply now lists each covered clip
under `overwritten` with its length before and after and the ids what is
left of it lives on as. The edit itself is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ffmpeg prints "corrupt decoded frame" as a warning, so the oracle's
`-v error -ec 0` decode let a stream with a whole slice missing pass
silently. Switching that decode to `-v warning` is not enough: with
`-ec 0`, ffmpeg 7.1/8.1 flags every picture that has more than one
slice, libx264's multi-slice streams included, while the pixels are
unchanged. That is the 119/120 in #74; the encoder's bitstream is not
at fault (VideoToolbox decodes the same streams bit-identically, and
ffmpeg with strict error detection reports nothing).
The oracle now decodes with ffmpeg's default concealment, `-v warning`
and `-err_detect +crccheck+bitstream+buffer+explode`, fails on any
output or a non-zero exit, and still compares every pixel with the
encoder's reconstruction. A negative control drops and truncates
slices (CAVLC and CABAC, 3 slices) and checks the oracle reports each
one; it fails with the old flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem (#73): a 30 fps WebM played and exported with every third frame
missing and the frame before it shown twice (20 unique frames a second).
The same happens at 29.97, 60, 59.94, 24 and 23.976 fps, and with MP4
files whose video track has a 1 ms timescale. 25 and 50 fps were fine.
Cause: Matroska stores block timestamps in TimestampScale units (usually
1 ms), rounded to the nearest unit, so at 30 fps frame 2 (66.67 ms) is
stored as 67. MkvSource::video_frame converted the requested time to track
units with a floor (66), and the lookup (last sample with pts <= target)
returned frame 1. Every frame whose timestamp was rounded up was skipped.
Mp4Source::video_at, and the web build's WebCodecs MP4 path, floored the
same way against the track timescale; normal MP4 timescales hold frame
times exactly, so only coarse (1000) timescales were hit.
Fix: new Tick::to_rational_round (nearest unit, halves up, total for zero
and negative timebases), used for the video lookup target and the
catch-up threshold in mkv.rs, mp4.rs and webcodecs.rs. A requested time
now matches a frame whose stored timestamp is within half a unit of it,
the same precision the timestamps have. Truncating muxers (stamps rounded
down) still work. Times on exact timebases (90 kHz, 15360, 30000/1001…)
convert exactly as before.
Test: crates/codecs/src/rounded_pts_tests.rs builds synthetic Motion-JPEG
streams in the test (each picture shows its index as black/white blocks),
muxed as WebM with 1 ms timestamps and as MOV with a 1000 timescale, at
30, 29.97, 60, 59.94, 24, 23.976 and 25 fps with nearest and truncated
stamps. Requesting every frame's start over 3 s must show every frame
exactly once, and every frame's midpoint must show that frame. Before the
fix both tests failed ("30 fps, Nearest ms stamps: 30 of 90 frames never
shown [2, 5, 8, 11]…, 30 shown twice [1, 4, 7, 10]…"). The time crate
tests cover the rounding, ties and hostile timebases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merged Japanese interface (#38) looked only for a system font. When
FilmCraft is built with craft-fonts, theme::install already appends the
Japanese faces to every font family, so switching to Japanese uses them
(no system scan) and works on the web too; otherwise it falls back to an
installed system font as before. Docs use an absolute CRAFT_FONTS_DIR
(build.rs runs in crates/text, so a relative path resolves from there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A corner handle scales both axes alike with the opposite corner held in place;
the edge handles (top, right, bottom, left) are now grabbable and stretch only
their own axis with the opposite edge held in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a corner handle of a graphic layer in the Program monitor scaled about
the layer's anchor, so a text layer (anchor at its bottom-left) grew up and to
the right whichever corner was dragged. The opposite corner now stays in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A short export can finish inside one 100 ms poll, which left a single progress
notification and no time to answer the ping. The test now tries longer exports
until one outlasts a few polls, and cancels a longer one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Timeline panel shows one tab per open sequence (x closes the active one);
closing a sequence keeps the panel, and opening one restores a closed panel.
- Double-clicking a nested sequence clip opens it in its own tab.
- The clip context menu follows Premiere's grouping and adds Paste/Remove
Attributes, Ungroup, Synchronize, Merge Clips, Make Subsequence, Reveal Nested
Sequence and the frame hold / field options; right-click selects the clip.
- Nest... asks for the name (Nested Sequence Name) and numbers the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/text/build.rs embeds the fonts of a storytold/craft-fonts checkout
named by CRAFT_FONTS_DIR as filmcraft_text::fonts::CRAFT_FONTS (native:
all of them; wasm32: only BIZ UDPGothic Regular). Unset, CRAFT_FONTS is
empty and nothing changes. A bad CRAFT_FONTS_DIR is a warning, or an
error with CRAFT_FONTS_REQUIRED=1. Never a Cargo dependency, no network.
- UI: the Japanese faces are the last fallback of every egui font family
(BIZ UDPGothic first, Bold first in the semibold/medium families).
- Text engine: registered after the bundled faces; fallback_for picks
them after the bundled faces and before system faces (Mincho for serif
text, Gothic otherwise, nearest weight).
- Tests: Japanese glyphs (no tofu) in the UI and title paths, skipped
without craft-fonts; the theme works with CRAFT_FONTS empty.
- Release: every build job checks out craft-fonts at a pinned commit and
builds with CRAFT_FONTS_DIR + CRAFT_FONTS_REQUIRED; packages carry
OFL-<family>.txt. NOTICE, ATTRIBUTION.md (optional build input), AGENTS.md,
CLAUDE.md, README and docs/contributing.md describe it.
FilmCraft main bundles no Japanese font, so no font files move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release builds add an arm64 MSI and portable zip, cross-compiled on the x64
runner (signing and WiX as for x64). package.ps1 checks each binary's PE
header before packaging: Machine must match -Arch, the app must be a GUI
program and the CLI a console program. It skips the CLI smoke run when an
ARM64 build can't run on the host. A new Windows ARM64 workflow packages the
ARM64 MSI, installs it on a Windows 11 ARM64 runner, checks both installed
exes are ARM64, runs the CLI, and uninstalls. Ported from printcraft#66.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
trim_decoders dropped the decoders it took from idle caches while the requesting cache's lock was held. Dropping a decoder can block (a VideoToolbox session waits for its frames in flight), so the render path paid that latency under the lock. trim_decoders now returns the released decoders and frame_late keeps them in a local declared before its lock guard, so they drop after the guard on every return path; the cap is enforced exactly as before.
New tests/pool_bitexact.rs: primes the frame pool with 0xAA-filled plane buffers, then decodes an H.264 fixture into them; every frame must match ffmpeg bit for bit, and the pool must report the reuse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bin inside another named bin goes with it and is not counted again;
an item named and also inside a named bin is counted once. The reply's
counts now match what went. The params line stays {"items":[id]?} so
a caller naming targets keeps the same shape as before.
Problem: no command could delete a bin. `project.delete` takes item
ids, and a bin's id passed to it answered ok and changed nothing, so a
bin emptied after an XML import (every clip moved to new bins) stayed
in the project for good.
Solution: `project.delete` also accepts bin ids in `items`. A bin
named there is taken out of the tree with its sub-bins, and every item
inside goes the way a named item goes (out of the project, off every
timeline). The project's own top bin is never removed. The reply now
says how many items and bins went. One undo step, as before.
Test: clear_deletes_a_bin_with_everything_in_it (fails before: the
bins stay).
Windows release builds have no console (windows_subsystem = "windows", added on main after this branch), so --help printed nothing and an unknown option or a bad FILMCRAFT_CONTROL_PORT quit with exit code 2 and no visible message. The text is still printed, and on Windows release builds also shown in a message box.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_speed_group accepted NaN (speed <= 0.0 is false for NaN), and a tiny speed (1e-7) saturated the new duration to i64::MAX, after which the ripple shift overflowed (panic in debug, wraparound in release). Non-finite speeds are refused, the new duration is capped at what the timeline can hold, and the ripple shift uses checked arithmetic; a refused edit changes nothing (it works on a copy). Regression test with hostile speeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Language switches English/Japanese and persists in UI state
(`app.language.english` / `app.language.japanese`, reachable through the
control channel). Core native and in-window menu labels are translated;
untranslated labels stay English. The in-window header fits the document
title beside the localized menus.
No font is bundled. Japanese text uses a Japanese font already installed on
the system (Hiragino, Yu Gothic, Meiryo, Noto Sans CJK JP, Source Han Sans,
IPA, … or any installed face covering the menus' characters), added to every
theme font family as the last fallback. Without one, switching to Japanese is
refused with a message and a saved Japanese setting falls back to English
(the web build cannot read system fonts). Titles already fall back to
installed fonts through the text engine.
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>
The five 'mux created' / 'audio' expects in the stepped exporter now return
an ExportError::Encode instead of panicking if the writer or audio pipeline
is missing, and the non-stepped dispatch returns Unsupported instead of
unreachable!(). The crate root gets the never-crash deny attribute, so every
FilmCraft production crate now carries it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MSI Icon id had no .ico/.exe extension, which Windows Installer needs for
advertised shortcuts and ARPPRODUCTICON, so both showed a blank icon. Release
builds of the desktop app are now GUI-subsystem (no console window; the CLI stays
a console program), the shortcut/summary description names what FilmCraft does, and
packaging-lint rejects an Icon id without an extension. Found in printcraft#57.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `edit.rippleDelete` on a clip whose linked sound has a
different edge than its picture (a J or an L cut) treated the two
ranges as two gaps and closed both on every affected track. With two or
more clips after it the later ones were moved up twice and the edit was
refused ("items overlap"). With one clip after it the delete went
through, and sequence markers after it were moved by both gaps (one
that landed inside the second gap was removed).
Solution: one set of stretches closes on every track: the time the
deleted clips covered, less whatever stays on the tracks that lose a
clip. Those tracks move up by the same amount, into time that is free
on all of them, so later clips stay in sync and nothing is overwritten.
For a J or an L cut that is the picture's range. Straight cuts close as
before, also with the sounds on alternating tracks. A track that is
locked loses nothing and does not count. `ripple_delete_items` returns
the stretches it closed and the command ripples the markers by those,
so the two cannot disagree.
Things to know. A gap can stay on one track when a clip that stays on
another of the tracks covers part of the deleted time: a sound shorter
than its picture with other sound right up against it, or unlinked
clips on different tracks deleted in one call. When nothing can close
at all the edit is refused and nothing changes; some such calls went
through before, moving clips by a gap another track did not have. A
selection wholly on locked tracks is refused as locked; before, it
deleted nothing and still shifted the sync-locked tracks.
Tests: an edit-crate test (a split on both sides, two neighbours,
clips apart in time, sounds on alternating tracks, a short sound with
and without neighbours, a locked track, the refusal); engine tests
through `edit.rippleDelete` for a J cut with a marker, undo and redo,
an L cut, two neighbours with a split between them, two neighbours with
sounds on alternating tracks, and a straight cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `timeline.trim` with `mode: "ripple"` on the clip before a
split edit moved the later picture and left its linked sound behind.
With a J cut (the next clip's sound starts before its picture, on a
sync-locked track where the trimmed clip has no partner) a sound that
led its picture by 12 frames led by 20 after lengthening by 8, with no
error. Shortening was refused as a sync-lock conflict, because the
early sound sits in the stretch that closes.
Solution: in `ripple_trim_group`, a clip that starts before the cut,
reaches past it and is linked to a clip that shifts now shifts with it,
on the tracks that ripple (the members' tracks and sync-locked tracks).
It no longer counts as material in the way of a shortening. When it has
no room to follow (another clip right before it, or the sequence start)
the trim is refused with the sync-lock error and nothing changes (an
overlap on any track that ripples now reports that error instead of
the generic "items overlap"). A
linked clip that ends before the cut is left alone, as before, and so
are the trimmed clips' own partners. A transition into a follower from
a clip that stays behind would be left over a gap; it is removed.
Tests: an edit-crate test for longer, shorter, both refusals, the
clip that ends before the cut, the In edge, the trimmed clip's own
partners and the transition; engine tests through `timeline.trim` for
longer and shorter with undo and redo, and for both refusals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `timeline.place {item, track: "A3"}` (an audio track given as
`track`). For an item with picture and sound it put a video item on the
audio track A3 and a second, sound clip on the default audio track. For
a sound-only item it ignored `track` and placed on the default audio
track.
Solution: `track` is where the picture goes and `audioTrack` where the
sound goes. An audio track given as `track` now places the sound alone,
on that track. What cannot be meant is a parameter error that says why,
and changes nothing: an audio track as `track` together with
`audioTrack`; a video track as `audioTrack`; an audio track for an item
with no sound; a video track alone for an item with no picture. Calls
that name a video track as `track` and an audio track as `audioTrack`
(the timeline panel's drop does) are unchanged. The parameter hint and
`docs/agents.md` say so.
Tests: track_params_tests.rs places a clip with picture and sound and a
sound-only item on audio tracks by name and by id, checks each refusal
for its error kind, its text, an untouched project and undo history,
and checks the usual forms still work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `clip.speedDuration` with `ripple: true` on a clip that has
linked sound set the speed on the picture and then on its sound, and
each one rippled the later material on its own. Slower (50%) moved the
later clips by twice the change and left a double gap. Faster (200%)
was refused ("items overlap") when more than one clip followed: the
second ripple pulled the farther clips onto the nearer ones.
Solution: `set_speed_group` changes a clip and its linked partners on
other tracks as one edit and ripples once, on the members' tracks and
on sync-locked tracks. The command groups the selected clips by link
(one clip per track in a group); clips that are not linked, or linked
clips of one track, still change one after the other. When the members'
lengths change by different amounts (a split edit), later material
moves by the largest change, so no member is overlapped and the tracks
stay in sync; the shorter member is followed by a gap. The group edit
works on a copy, so a failure changes nothing. `set_speed` keeps its
signature and behaviour for one clip.
Tests: an edit-crate test (slower, faster, a split edit both ways, two
clips of one track and a missing clip refused with nothing changed);
engine tests through `clip.speedDuration` for slower with undo and
redo, faster, two clips in one call, no ripple, picture and sound both
named with Linked Selection off, linked clips of one track, a split
edit, and a clip that is not in the sequence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a clip set to play backward (`clip.speedDuration` with
`reverse: true`) read back from `sequence.inspect` exactly like a
forward clip: same `speed`, same source range, nothing that said it was
reversed. An agent could set it and not confirm it.
Solution: clips gain `reverse` (bool) in `sequence.inspect`, named as
in the project file and in the command. Additive; existing keys are
unchanged. `docs/agents.md` lists it and says what it means for the
source range.
Tests: sequence_inspect_tests.rs reverses a demo clip through the
command and checks that only that clip reports `reverse: true`, and
none after undo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a Final Cut 7 XML whose Basic Motion filter sets only `scale`
makes the importer add the Motion effect with its defaults, where
`position` and `anchor` are the NaN "auto" sentinel. Placing a clip
resolves those with `resolve_auto_points`; the interchange import did
not (any format: the clips are built with the effect defaults).
serde_json writes NaN as `null`, and the reader then refused the file:
"project file is damaged: invalid type: null, expected f64". The project
could be saved but never opened again.
Solution:
- project: a point parameter's value (`ParamValue::Vec2`) reads a `null`
coordinate back as NaN, so "auto" survives a save. Only there: `Vec2`
itself is unchanged, and a `null` in a mask vertex or any other point
is still a damaged file.
- project: `Project::resolve_placed_auto_points` resolves the points of
the clips in chosen sequences. Points measured in the frame use the
sequence size. `anchor` is measured in the clip's source
(`Project::source_size`, a subclip through its parent, as the renderer
resolves it) and is resolved only for sources the caller says have
their final size.
- engine: the import resolves in the edits it already makes, so the undo
steps are what they were. In "Import ...": everything but the anchors
of file media, whose size is only what the document says so far (an
EDL says nothing and the importer assumes the sequence size). In each
"Link Media": the anchors of that file's clips, from the probed size.
A clip whose file is not found keeps an "auto" anchor, which the
renderer centres in whatever size the media has, as before this
change.
- engine: a later Link Media (relink.rs `apply`, behind `media.relink`
and `media.autoRelink`) resolves those waiting anchors in its own undo
step, when it has read the file. Nothing else resolves them: a file
that merely comes back at its old path, or a forced relink that does
not read the file, leaves the anchor "auto" (still rendered centred).
Why no schema step and no repair on load: the file's shape is not
changed. Files with `null` points were written by every build before
this one, at the current schema version, so a migration (keyed on the
version) cannot select them, and a bump would only make every new
project unreadable to those builds. They open now and their points are
"auto", exactly as in the session that wrote them; resolving them on
load would mean guessing the size of media that was never linked.
docs/project-files.md describes `null` points.
Tests: project
`point_params_keep_auto_through_json_and_no_other_point_reads_null` and
`placed_auto_points_resolve_once_the_source_size_is_final`; format
`auto_points_written_as_null_open_again`; engine
(interchange_auto_points_tests.rs): import with the media missing, save,
reopen, undo, redo; a linked file whose real size differs from the
declared and the sequence size (undo of Link Media and of the import,
redo); an EDL clip smaller than its sequence; import with the media
missing, save, reopen, relink (anchor becomes a number, undo, redo, a
second relink leaves it). All fail without the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Behaviour change to the default: with Linked Selection on (the default),
`timeline.move` now also moves the linked partners of the listed clips.
A partner lands on its own track in the same mode as the listed clip, so
a caller that lists only a picture clip can now overwrite audio at the
destination time. `"linked": false` gives the old behaviour (only the
listed clips move); the timeline panel passes it, so dragging is
unchanged.
Problem: `timeline.move {"moves": [{clip, track, time}]}` moved only the
listed clip. Its linked sound stayed where it was, out of sync, although
Linked Selection was on and `timeline.trim`, slide and select all act on
linked partners. Separately, a `time` near i64::MAX overflowed the tick
addition in the edit (a panic in debug builds, a wrapped position in
release).
Solution: a linked partner that is not itself in `moves` moves by the
same time offset and stays on its own track. A partner that is listed is
moved once, to where the list says. Two kinds of partner are left
exactly as they are: one on a locked track (the listed clip still moves,
as it did before), and one whose position would not change, which is the
case when the listed clip only changes track. Re-placing such a partner
would drop the transitions at its edges and report a move that did not
happen. A new optional `linked` parameter overrides the toggle for one
call. The command returns `{"moved": [ids]}`, the clips that moved.
No clip is moved before the sequence start. When one would be (a split
edit whose sound starts earlier, or several listed clips asked too far
left), all clips of the call land later by the same amount, so the
spacing asked for is kept. This replaces the per-clip clamp for a call
that lists several clips with a time below zero; a single clip still
lands at zero. The offsets are summed in i128 and checked on the way
back: a start past Tick::MAX is a parameter error and nothing moves.
Tests (timeline_move_tests.rs): partners follow, undo / redo, both
listed, `linked: false`, toggle off, `linked: true`; a locked partner; a
picture moved to another track at the same time leaves its sound and the
sound's crossfade untouched; the common stop at the sequence start with
a split edit and with two listed clips; hostile times (i64::MAX,
i64::MIN, junk). All fail without the change, the hostile one with the
overflow panic. docs/control-protocol.md and docs/agents.md describe the
default, the `linked` parameter and the result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a command gated on the selection refused to run although the
caller named what to act on. `clip.replaceFromBin {clips, item}` answered
"select a clip in the Project panel"; `edit.clear`, `edit.rippleDelete`,
`edit.pasteAttributes`, `clip.enable`, `clip.link`, `clip.speedDuration`
and `clip.audioGain` with `clips` answered "no clips selected". A script
or agent had to change the user's selection first. The cause is the order
in Session::execute: `enabled()` runs before the parameters are read, and
it only knows the selection.
Fix: Session::execute asks enablement per call. When `enabled()` passes,
nothing changes. When it fails and the parameters name targets under a
key the command documents as ids (`clips` / `clip`: clips of the active
sequence, `items` / `item`: project items), the same `enabled()` runs
once more with those targets standing in for the selection, and the
selections are put back before the command runs. So only the selection
condition is answered by the named targets: no sequence, an empty
clipboard or the wrong kind of clip still disable the command, ids of
nothing do not count, and a command that does not document the key
(`edit.cut`) still needs a selection. This works for every command
because the predicates already resolve their targets the way `run` does
with empty parameters. `is_enabled` (menus, `engine.commands`) is
unchanged.
`clip.audioGain` already read `clips`; its parameter doc now says so.
Tests (explicit_targets_tests.rs): the commands above with and without
parameters, undo/redo, the selections left untouched, menu enablement
unchanged, and the cases that must still be refused. Four of the five
fail without the change in execute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a clip item whose `<file id="...">` reference has no definition
anywhere in the document was dropped without a word, so a sequence could
come back one clip short with an empty import report. FilmCraft's own
export writes titles and adjustment layers this way.
Solution: the importer adds a warning that names the clip item: `clip
"Title" refers to a file the document does not define; skipped`. Nothing
else about the import changes.
Test: `fcp7_import_reports_a_clip_item_whose_file_is_not_defined`
(FilmCraft's own export, a hand-written document, and a document whose
files are all defined).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: exporting a sequence with a graphic (title) clip to Final Cut
Pro 7 XML returned a report that listed other losses and said nothing
about the title. The XML holds a clip item for it that points at a file
the document never defines, and importing the XML gives the sequence
back without the title. OTIO (a clip with a missing media reference,
read back as offline media) and EDL (an ordinary event) were silent in
the same way; FCPXML said "clip without media skipped" and AAF "graphics,
titles and adjustment layers are not exported" without saying which clip.
Adjustment layers take the same paths.
Solution: every exporter names each graphic clip and adjustment layer
it cannot carry, one warning per clip, with what was written instead:
`graphic clip "Title" at frame 48 has no Final Cut Pro 7 XML equivalent:
it is written as a clip item without media and is not read back on
import`. One helper (`common::uncarried_clip`) builds the name for all
formats. What is written is not changed, byte for byte.
Why only a warning: FCP7 XML has a Text generator, but this crate's
importer does not read one (unknown generators become Black Video), and
a title here is a stack of text and shape layers with per-character
styles, pins and roll settings that the generator's few parameters
cannot hold. Writing one would need a new import path and would still
lose most of a title, so the honest result for now is to say what
happens. crates/interchange/README.md has the table per format.
Tests: crates/interchange/tests/uncarried_clips.rs (each format names
the title and the layer once; two titles are two lines; nothing is said
without such clips; a re-import gives back what the report says), and
the engine's `interchange_export_reports_the_title_it_cannot_carry`
through `file.exportInterchange`. The two naming tests fail without the
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: a clip with AAC audio in MP4 whose sample rate differs from the
sequence's (48 kHz clip in a 44.1 kHz sequence) exported with loud
low-frequency thumps, several times full scale, recurring through the clip.
Every exported container was affected (AAC clips them into thumps, PCM
clips at full scale); a WAV export had a single one at the clip's start.
Cause: Mp4Source::audio decodes a window starting at s0, the requested
presentation sample minus the edit-list offset, then resamples it. The
resampler derived its sub-sample phase from s0, so the edit-list offset
(-1024 samples of AAC priming) was applied a second time: the read
position started about 1024 samples before the window, was clamped to
index 0 with a large negative fraction, and linear interpolation
extrapolated a ramp over the first ~940 samples of every request. Export
asks for audio one batch of video frames at a time, so the ramp repeated
at each request. Sources at the sequence rate take the copy path and
were unaffected, as were tracks without an edit list.
Fix: take the phase from the presentation sample the window starts at
(before the edit-list offset is subtracted).
Test: aac_mp4_resampled_reads_follow_the_edit_list
(crates/codecs/tests/mp4_oracle.rs, with `require_ffmpeg!` and the
shared fixture helper as docs/testing.md asks of new oracle tests) reads
an ffmpeg AAC fixture at 44.1 kHz in frame-sized pieces and compares it
with the 48 kHz decode interpolated at the same times (peak was 7.35
against a source level of 0.15 before the fix). docs/testing.md lists it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: when the replacement is shorter than the clip's slot, the clip
silently runs past the end of its media and the renderer holds the last
frame. Nothing in the command's result says so, so a script or agent
that swaps a take cannot tell.
Solution: the result of the three Replace With Clip commands
(`clip.replaceFromSource`, `clip.replaceFromSourceMatchFrame`,
`clip.replaceFromBin`) gains `short`: one `{clip, shortByFrames}` per
replaced clip that now plays past the end of its media, in sequence
frames and never more than the clip's own length. It is empty when none
does; stills and other sources without an end, and held frames, are
never short. The edit itself is not changed, and the parameter docs are
untouched (results are described in docs/control-protocol.md).
Test: `replace_with_clip_reports_clips_that_run_past_the_new_media`
(From Bin with a 12-frame subclip, undo / redo, From Source Monitor,
Match Frame, the cases that are not short, and an In past the end of the
new media capped at the clip's length).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `clip.replaceFromBin` always restarts the clip at the new
item's In point (or its start). That matches Premiere's Replace With
Clip > From Bin, but a script or agent swapping one take for another has
no way to keep the clip's place in the media.
Solution: the default is unchanged, and so is its result. An optional
`keepSourceIn` (default false) keeps each clip's current source In. A
subclip that restricts trims has no media outside its range, so there a
kept In outside the range is moved to the subclip's first or last frame;
with `keepSourceIn` the result gains `sourceInClamped`, the clips this
was done for. The parameter doc lists the new parameter only (a result
description in it would take the command out of the Keyboard Shortcuts
list, which reads the doc to see whether a command needs parameters).
Tests: `replace_from_bin_keeps_the_source_in_on_request` (default and
its result, keepSourceIn, undo / redo, the disabled case, subclips before,
after and around the In, an unrestricted subclip, junk values), and
`list_searches_labels_and_keys` now asserts the three Replace With Clip
commands are bindable. docs/control-protocol.md describes the commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `filmcraft --help` opened the app. Every argument the parser
did not know was taken as a file to open, so `--help`, `-h` or a
mistyped flag started a window (and tried to import "--help" as media).
`--control` with a missing or invalid port, and an invalid
`FILMCRAFT_CONTROL_PORT`, started the app without the control server and
said nothing, which leaves a script waiting for a server that never
comes.
Solution: argument parsing moves into its own module (`args.rs`,
`args::parse`), which runs before anything else starts. `--help` / `-h`
print the usage (the options the app parses: --control, --demo, --empty,
--recover, --no-recover, --data-dir, --version) and exit 0. An unknown
option, or `--control` / `--data-dir` without a usable value, prints the
error and the usage on stderr and exits 2. The whole line is checked, so
`--version --bogus` is an error too. A `FILMCRAFT_CONTROL_PORT` that is
not a port is the same error as a bad `--control` (exit 2), for the same
reason; an empty value counts as unset, and the value is not looked at
when `--control` is given or for `--help` / `--version`. `--` ends the
options, so a file whose name starts with a dash can still be opened; a
lone `-` and the `-psn_...` argument macOS adds to app launches are not
treated as options.
Tests (in args.rs): `help_and_version_do_not_start_the_app`,
`unknown_options_are_errors`,
`a_bad_port_in_the_environment_is_an_error_like_a_bad_control_flag`,
`known_options_and_files_parse_as_before`. docs/contributing.md
mentions `--help` and the errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `file.exportInterchange {"format": "<anything unknown>"}` wrote
FCP7 XML and returned ok, so a typo or an unsupported format produced a
file of the wrong kind under the name that was asked for.
Solution: only a missing `format` means FCP7 XML. A given format must be
one of the six documented names (edl, xml, fcpxml, otio, aaf, omf: each
format's extension, in either case). Anything else, including a value
that is not a string, is a parameter error that lists exactly those
names, and nothing is written. No other spelling is accepted (".xml",
"fcpxmld", "omfi"): none was accepted before either, they silently
became XML. The parameter doc now lists aaf and omf, which the command
already took, and the default; docs/architecture.md says the same.
Test: `export_interchange_refuses_an_unknown_format`, next to the
interchange round-trip test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `sequence.inspect` is how an agent verifies an edit, but it
left out where a clip ends in the sequence and in its media, a clip's
audio gain, and the comment of a marker, so those edits could not be
checked from the readback.
Solution: each clip also reports `end`, `endFrame`, `sourceOut` (which
depends on speed, so it cannot be derived from `sourceIn` + `duration`)
and `gainDb`; each sequence marker also reports `comment`. Every
existing key stays. docs/agents.md lists what the readback holds.
Test: `sequence_inspect_reports_clip_ends_gain_and_marker_comments`
(sequence_inspect_tests.rs), on a clip at 200 %, a clip with gain and a
marker with a comment made through the commands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `timeline.place {"audioTrack": "A2"}` on a sequence that has
only A1 placed the clip on A1, overwrote what was there and returned ok.
`track_p` returned `None` both for "no track asked for" and for "that
track does not exist", so callers fell back to the source-patched track.
It also took any first letter other than V as an audio track, accepted
track ids that are not in the sequence, and `split_at(1)` panicked on an
empty or non-ASCII name.
Solution: `track_p` returns `Result<Option<TrackId>>` and takes the
command id, like the other fallible parameter helpers. An absent (or
null) parameter is `Ok(None)` and keeps every default as it was. A name
or id the active sequence does not have is a parameter error
(`EngineError::BadParams`): "invalid parameters for `timeline.place`: no
track A2 in this sequence (`audioTrack`)". All callers propagate it:
timeline.place (`track`, `audioTrack`), timeline.move, timeline.razor,
timeline.setTrack, timeline.setTargeting, sequence.closeGap,
sequence.deleteTrack, sequence.deleteTracks, the go-to-gap commands,
sequence.transcribe and the multicam sync reference track.
Not changed: a `timeline.move` entry without a `clip`, `track` or `time`
is still skipped, as before; only a track that was named and is missing
is an error.
Test: `naming_a_missing_track_is_an_error_not_a_default` in
track_params_tests.rs (missing and hostile names, the error kind, nothing
edited, no undo step, existing tracks by name and id still resolve).
docs/agents.md notes the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MEM1-MEM3 twelve clips on a timeline still took 5.1 GB, only 2.5 GB of
it live: each decoded picture allocated three planes, each eviction freed
them, and the system allocator kept the freed pages.
filmcraft_frame::pool keeps the planes of frames the GOP caches evict (up to
192 MiB per sample type; a frame something else still holds is left alone).
The H.264 decoder, through a plane allocator hook that keeps the crate free
of dependencies, and the VideoToolbox path take their output planes from it.
perf.stats reports decode.planePoolMB and decode.planesReused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remove_partial removed every '<stem>[digits].<any ext>' file in the output folder modified since the export started, so cancelling an export to trailer.mp4 also deleted trailer.aep or trailer2.psd if they were saved during the export. It now removes only the output file, numbered stills with the output's extension, and the .srt/.vtt caption sidecar. Regression test with decoy files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app called filmcraft_platform::register() as an argument of
log::info!. A log macro does not evaluate its arguments while no logger takes
its level, and the app installs none, so VideoToolbox was never registered:
perf.stats showed hardware decoding enabled with 0 sessions, 0 declined and
every frame decoded in software. The CLI and the bench register directly and
were not affected.
Registration is now a statement of its own, with a start-up test that runs
without a logger.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each GOP cache keeps up to 384 MB of decoded frames (at least 64), and
nothing bounded the total: twelve clips on a timeline held 3.8 GB of frames
nobody was looking at, and no perf.stats counter showed it.
The pool now counts the frames of every cache against a 1 GiB budget. Over
it, caches idle for 3 s give up frames, least recently used cache first.
Caches in use keep their own budget, since a frame evicted before it is
shown costs a re-decode from the keyframe.
perf.stats reports decode.cacheMB and decode.cacheBudgetMB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every source kept its decoder for good once it had decoded a frame, with its
reference pictures and the pictures its frame threads hold in flight: about
290 MB per H.264 clip, 3.5 GB for twelve clips on a timeline.
The GOP caches now share a pool. Beyond four live decoders, the least
recently used caches that have been idle for 3 s drop theirs and make a new
one when asked again (a restart at the sync sample, as after a seek). Caches
in use keep their decoder whatever the count, so a composite of many layers
never restarts decoders per frame. Other caches are only try-locked, so two
caches trimming each other cannot deadlock.
perf.stats reports decode.liveDecoders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop host had no Services::reader, so MediaPool::open_file read every
clip whole and kept its bytes for as long as the source stayed open: 2.9 GB
for twelve clips on a timeline (malloc_history, 15 allocations in open_file).
FsServices::reader now returns a FileReader that keeps the file open and
serves positional reads, so MP4/MOV, Matroska and the other reader openers
load the index and then single samples, as the web host already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real-file test oracles live in storytold/photocraft-corpus, explained in
craftrules standards/test-corpora.md. Never commit large binary fixtures
here; fetch them pinned and sha256-verified.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ROADMAP honest assessment, Where we are lacking and Running now; README status and codec note. 35 crates (platform).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A media clip whose enabled standard effects all have a GPU implementation
plans as a GPU layer carrying them (render::plan::LayerFx): parameters are
evaluated on the CPU at the frame's time (render::gpufx::FxOp::eval, shared
with the CPU path so both clamp hostile values identically), the source is
drawn into an Rgba32Float working image at the size the CPU decodes it, the
effects run as WGSL compute passes (gpu::fx, fx.wgsl) with the CPU
reference's math, and the result is placed with Motion, opacity and blend
mode as before. Layers without effects keep the single draw.
GPU effects: Brightness & Contrast, ProcAmp, Tint, Black & White, Color
Balance, Leave Color, Change to Color, Color Pass, Color Replace, Channel
Mix, ASC CDL, Gamma Correction, Levels, Extract, Invert, Posterize, Alpha
Adjust, Gaussian / Camera / Directional Blur (+ legacy aliases), Sharpen,
Unsharp Mask, Crop, Edge Feather, Transform, H/V Flip, Mirror, Offset.
Masked effects, opacity masks, non-finite parameters and minifying
Transform/Crop zooms stay on the CPU. Devices without compute shaders run
the effects on the CPU inside the compositor.
Also fixes a debug-build overflow in Image::transformed for non-finite maps.
Parity tests (exact f32 checks and composited plans), a golden test that
plans effect chains as GPU layers, and the stack3-fx playback benchmark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ROADMAP now reports two numbers: the feature checklist (~87%, what exists)
and ready for real work (~50-60%, an honest estimate). Adds a by-dimension
assessment with evidence, a prioritised "Where we are lacking" list (hardware
acceleration, a measured parity tool, real-world media corpus, workflow
acceptance tests, Windows/Linux runtime CI, delivery codecs, plugins, AI, web),
and where we're going. Fixes stale rows (M5 effect count, Running now) and logs
2026-10-05.
README: the stale "45%" status replaced with both numbers and the biggest
gaps; the codec note now lists what imports today and what's missing.
docs/agents.md: agents read the honest assessment first, prefer its gaps,
and report progress as measured vs estimated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PlaneView's slices were tied to the pixel buffer's lifetime, not the lock's, so nothing stopped a later edit from reading plane memory after CVPixelBufferUnlockBaseAddress. They now borrow the Locked guard.
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>
All 27 blend modes now composite on the GPU instead of sending the
whole frame to the CPU.
- render::plan: PlanLayer carries its Blend; plan_frame no longer falls
back to a CPU frame for non-Normal modes. Media, graphic clips,
multicam angles and CPU-rendered layer images (standard effects) keep
their clip's mode; inside a transition the mode is Normal, as in the
CPU reference. execute_cpu composites with each layer's mode.
- gpu: Normal keeps the single fixed-function pass. Dissolve also uses
fixed-function "over": the CPU's 64-bit pixel hash is reproduced in
WGSL on u32 pairs (pattern bit-exact). The other 25 modes copy the
accumulator region under the layer's quad into a backdrop texture and
composite in fs_blend with the CPU math (sRGB-encoded straight colour,
W3C formulas, back to linear, same premultiplied mix and clamping).
- Tests: per-mode GPU-vs-CPU parity (gpu crate: overlapping transformed
layers, partial opacity/alpha, empty/partial backdrop; golden: every
mode through plan_frame incl. an effect layer, blended clips in a
cross dissolve), exact-value edge cases (black/white, Divide by 0,
Dodge/Burn at 0 and 1, zero alpha) within 4.9e-4, Dissolve pattern
exact, off-output layers, quad bounds.
- Bench: new stack3-blend playback scenario (V2 Screen, V3 Overlay);
CPU ms/frame 151-214 -> 86-89, Full 19/173 and 138/54 -> 191/1, 192/0.
- Docs: architecture §5, gpu crate docs, testing, performance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
command_run file.exportMedia with wait: true (headless) runs as the
background export job and uses the MCP progress and cancellation utilities:
- with _meta.progressToken, notifications/progress reports frames done /
total with the job status, at most every 100 ms (none without a token);
- notifications/cancelled stops the job at the next batch (jobs.cancel),
deletes the partial output and sends no response;
- the session lock is only held for short polls, so ping, doc_inspect,
jobs.list etc. are answered while it encodes.
Without wait, and in bridge mode, nothing changes. Test
export_progress_and_cancel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- resources/list and resources/read serve the project (tree + active
sequence, as doc_inspect) and the command catalog (as command_list) as
JSON, for clients that attach context from resources.
- Results are built with rmcp's types, so resultType follows the negotiated
revision; for 2026-07-28 clients (current Claude Code) they also carry
ttlMs and cacheScope, like the generated tools/list. The resource list is
fixed (10 min), document reads are never cached (0).
- get_info now declares the resources capability; the instructions text
moves to a const, unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every tool has a title and readOnly / destructive / idempotent / openWorld
hints, set in its #[tool] attribute, so clients can run the read-only
tools without asking and ask before edits.
- New tools: command_batch (several commands, stop at the first error by
default), doc_inspect (project tree + active sequence in one call) and
render_preview (= render_frame).
- An unknown tool argument is a -32602 error naming it and the accepted
ones, instead of being ignored.
- A line that is not JSON gets a -32700 reply (id null) instead of being
dropped without an answer: serve_io filters lines in front of rmcp.
- A panicking command no longer poisons the session lock for the rest of the
session ("session lock poisoned"); it is reported as "internal error: …"
(AGENTS.md §0.6).
- docs/agents.md § Conventions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exporting the same project twice could give different files, because two
export parameters followed the number of cores the process sees:
- The muxer got one chunk per render batch, and a batch is one frame per
rayon thread. The chunk layout (stsc/stco, PCM packet sizes) changed with
the core count; the samples themselves did not.
- The H.264 encoder used one slice per core (capped at one per four MB rows),
so the bitstream and every decoded picture changed with the core count.
On a host whose CPU quota changes (containers, power limits), two exports in a
row differed. Now packets are muxed in fixed groups of 16 output frames
(INTERLEAVE), each followed by its audio, whatever the batch size, and H.264
exports use one slice per four macroblock rows. Hosts with 17 or more threads
already used these values, so their 1080p output does not change.
Tests: determinism_tests (fail on main): same bytes for ProRes, H.264 and
Motion-JPEG at 2/3/5/16 cores and 1/7 frames per step; 64 slices for a
4096-line H.264 export.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:32:49 -07:00
777 changed files with 162567 additions and 9147 deletions
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.filmcraft.yml')); assert m['id'] == 'ai.storyteller.filmcraft' and m['command'] == 'filmcraft'"
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
echo "Draft release **$title** at \`${GITHUB_SHA::9}\`:"
echo
sed 's/^/ /' SHA256SUMS.txt
} >>"$GITHUB_STEP_SUMMARY"
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
# Windows on ARM: package the ARM64 release the way release.yml does (cross-compiled on an x64
# runner, unsigned here), then install that MSI on a Windows 11 ARM64 runner, check the installed
# binaries are ARM64, run the CLI natively, and uninstall. (A native ARM64 `cargo test` job is left
# out for now: PdfCraft saw an intermittent access violation in egui UI tests there.)
name:Windows ARM64
on:
push:
branches:[main]
pull_request:
paths:
- Cargo.toml
- Cargo.lock
- "**/Cargo.toml"
- "**/build.rs"
- .github/workflows/windows-arm64.yml
- packaging/windows/**
schedule:
- cron:"41 4 * * 1"# weekly, so drift in the runner images shows up
workflow_dispatch:
concurrency:
group:windows-arm64-${{ github.ref }}
cancel-in-progress:true
permissions:
contents:read
jobs:
windows-arm64:
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
- [ROADMAP.md](ROADMAP.md), [docs/target-app-parity.md](docs/target-app-parity.md), [docs/gaps.md](docs/gaps.md): stage, parity with Premiere Pro and the ranked work list (craftrules `standards/progress-docs.md`)
## Contributor credits (About window)
- About ▸ Contributors/Models are compiled into the binary from `contributors/contributors.json`
(commit stats; generated, never hand-edit) and `contributors/people.toml` (names people chose for
themselves). See `docs/contributors.md`.
- **Agents working for a contributor:** when you prepare a PR, check whether your human's GitHub
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
whether they want to be credited by more than their username: a real name, a display name, and/or
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
@@ -9,6 +9,11 @@ iconography, images or other Adobe assets; open licences only; every asset attri
| Asset | Author | Source | Licence |
|---|---|---|---|
| `crates/ui-egui/src/i18n/id.tsv` | FilmCraft contributors | Original work: clean-room Indonesian interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/fr.tsv` | FilmCraft contributors | Original work: clean-room French interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/pt-br.tsv` | FilmCraft contributors | Original work: clean-room Brazilian Portuguese interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/ru.tsv` | FilmCraft contributors | Original work: clean-room Russian interface translations | MIT OR Apache-2.0 |
| `crates/ui-egui/src/i18n/uk.tsv` | FilmCraft contributors | Original work: clean-room Ukrainian interface translations | MIT OR Apache-2.0 |
| `assets/fonts/Inter-Regular.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-Medium.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-SemiBold.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
@@ -26,6 +31,8 @@ iconography, images or other Adobe assets; open licences only; every asset attri
| `docs/images/filmcraft-effects.png` | FilmCraft contributors | Original work: FilmCraft screenshot crop; NASA Earth Views (US Government work, public domain) | MIT OR Apache-2.0; footage public domain |
| `docs/images/filmcraft-timeline.png` | FilmCraft contributors | Original work: FilmCraft UI crop; thumbnails from Night of the Living Dead (1968) and Carnival of Souls (1962), CC0 Chopin waveform | MIT OR Apache-2.0; film frames public domain |
| `assets/app-icon/filmcraft.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; canonical vector | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `packaging/macos/dmg/background.svg` | @XusBadia | Original work: FilmCraft macOS DMG window background, derived from the FilmCraft app icon (`assets/app-icon/filmcraft.svg`); rendered to `background.tiff` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) |
| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) |
| `assets/app-icon/filmcraft-small.svg` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; lighter vector | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft-1024.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; 1024 px render | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/filmcraft-macos-512.png` | Brandon Thomas (FilmCraft owner) | Original work: FilmCraft app icon (owl with ear tufts), drawn by the owner in ArtCraft and vectorised; macOS runtime Dock icon (512 px) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
@@ -99,6 +106,7 @@ is traced or derived from Adobe artwork.
| Asset | Where |
|---|---|
| UI icons (tools, transport, panels, header) | `crates/ui-egui/src/icons.rs`: vector paths on a 16×16 grid |
| Source and Program range braces and trim hover cues | `crates/ui-egui/src/panels/monitor.rs` and `source_range.rs`: original generic vector shapes by FilmCraft contributors, MIT OR Apache-2.0; no third-party artwork |
| Align / distribute / paragraph-alignment glyphs in the graphics panel | `crates/ui-egui/src/panels/graphics.rs` (`align_glyph`): bars and lines drawn with the egui painter |
| Built-in graphics templates (Lower Third – Slab / Rule, Ticker – Crawl, Title – Centered / Boxed, End Credits – Roll, Callout – Pointer / Tag) and their placeholder text | `crates/project/src/gtemplate.rs` (`builtin_templates`): original designs defined in code with the bundled Inter font; thumbnails are rendered at runtime, no template, image or font files |
@@ -106,6 +114,21 @@ is traced or derived from Adobe artwork.
| Lumetri Presets (Effects panel: Cinematic, Film Emulation, Monochrome, Technical) and their preview picture | `crates/render/src/lumetri_presets.rs`: Lumetri parameter values chosen by contributors, descriptive names, and a procedural preview picture (dusk sky, ridges, lake, grey ramp, colour chips) |
| Colour palette and layout metrics | `crates/ui-egui/src/theme.rs`: colour values and sizes only; no artwork |
## Optional build input: craft-fonts
These fonts are **not files in this repository**. Builds made with the optional build input
`CRAFT_FONTS_DIR` (all official releases; see [docs/contributing.md](docs/contributing.md#building-with-craft-fonts))
embed them from [storytold/craft-fonts](https://github.com/storytold/craft-fonts), where each file,
its author, pinned upstream source and licence text are listed in its
| BIZ UDPGothic Regular, Bold (Morisawa) | Japanese UI text; Japanese fallback for sans titles (the web build embeds only Regular) | OFL-1.1 |
| Shippori Mincho Regular (FONTDASU) | Japanese fallback for serif titles (native builds) | OFL-1.1 |
| BIZ UDMincho Regular (Morisawa) | Japanese fallback for serif titles (native builds) | OFL-1.1 |
## First-party brand marks
The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not open source and
@@ -124,6 +147,15 @@ remove them (AGENTS.md §1.8).
| `docs/brand/artcraft-mark.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, not open source; `docs/brand/LICENSE-brand.txt` (not MIT/Apache) |
## Third-party source code
Source files that are transcribed from third-party code rather than written from scratch. Each keeps
the original copyright and permission notice in its header.
| File | Author | Source | Licence |
|---|---|---|---|
| `crates/platform/src/nvenc/ffi.rs` | NVIDIA Corporation (`nvEncodeAPI.h`); FilmCraft contributors (Rust transcription) | Structures (including `NV_ENC_CONFIG_HEVC` and its bit-field masks, `NV_ENC_PIC_PARAMS_HEVC`, `NV_ENC_SEI_PAYLOAD`, `NV_ENC_TIME_CODE`), function table, codec / profile GUIDs (H.264, HEVC Main, HEVC Main 10) and constants (`NV_ENC_LEVEL_HEVC_*`, tier, CU size, `NV_ENC_BUFFER_FORMAT_YUV420_10BIT`, `NV_ENC_CAPS_SUPPORT_10BIT_ENCODE`, BT.2020 / PQ / HLG VUI codes) transcribed from `nvEncodeAPI.h`, NVIDIA Video Codec SDK, API 12.1 (https://developer.nvidia.com/video-codec-sdk) | MIT (Copyright (c) 2010-2023 NVIDIA Corporation; notice kept in the file header) |
## Downloaded at runtime
These files are never in the repository or the app bundle. FilmCraft downloads them into the
FilmCraft is a clean-room, open-source, pure-Rust non-linear video editor targeting Adobe Premiere Pro parity (and beyond). Native on macOS, Windows, Linux; web via WASM. Sibling of `../photocraft` (Photoshop), `../printcraft` (Acrobat) and `../drawcraft` (Illustrator), with the same conventions.
FilmCraft is a clean-room, open-source, pure-Rust non-linear video editor targeting Adobe Premiere Pro parity (and beyond). Native on macOS, Windows, Linux; web via WASM. Sibling of `../photocraft` (Photoshop), `../pdfcraft` (Acrobat) and `../vectorcraft` (Illustrator; formerly DrawCraft), with the same conventions.
## Start every session here
`plan/` is maintainer-local (gitignored, not in the repo). Each step names the public equivalent to use if you don't have it.
1. Read `plan/STATUS.md` (current milestone, next unchecked task, blockers). Public: [`ROADMAP.md`](ROADMAP.md), then [`docs/gaps.md`](docs/gaps.md) (ranked shortfalls) and [`docs/roadmap.md`](docs/roadmap.md) (current focus).
2. Read the task in `plan/execution-plan.md` §3, the relevant section of `plan/architecture.md`, and the README/docs of the crate you touch. Public: [`docs/architecture.md`](docs/architecture.md), [`docs/contributing.md`](docs/contributing.md), [`docs/testing.md`](docs/testing.md). Visual/behaviour reference: `plan/premiere/` (local only, never committed).
3. Follow the autonomous operation protocol (`plan/execution-plan.md` §7; public summary: [`docs/agents.md`](docs/agents.md) §5–7). Don't stop to ask unless it lists the decision as the user's.
## Non-negotiables
**Read [`AGENTS.md`](AGENTS.md) first; its rules override everything here.**
- **Never crash** ([`AGENTS.md`](AGENTS.md) §0, standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md)). People depend on FilmCraft; a crash loses their work, so this outranks feature work. No `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!`/`unimplemented!` or `unsafe` outside tests: return `Result<T, E>` and propagate with `?` (`ok_or`, `let … else`, `if let`). Treat every number from media, project files, presets or CLI/MCP/control params as hostile: `get()` instead of `[i]`, checked/saturating math, no division by possibly-zero values, capped allocations, bounded recursion. `lock().unwrap_or_else(PoisonError::into_inner)`; background jobs run under `catch_unwind`. The panic hook + UI `catch_unwind` (`crates/ui-egui/src/crash.rs`) are the last-resort guard, not a licence. Every crash fix gets a regression test that panicked before. In particular (§1): never use Adobe iconography, images or any other Adobe asset. Every asset must be openly licensed (OSS / public domain / Creative Commons, or original work by a contributor) and must have a `<file>.attribution` sidecar plus an entry in `ATTRIBUTION.md`. `cargo xtask assets` enforces this.
- **Fonts** live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this repo (no new font files here; [`AGENTS.md`](AGENTS.md) §1.9, standard: [`craftrules/standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md)). Build with them: `git clone https://github.com/storytold/craft-fonts ../craft-fonts && CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run -p filmcraft` (`crates/text/build.rs` embeds them as `filmcraft_text::fonts::CRAFT_FONTS`, used for Japanese in the UI and titles). Unset, `CRAFT_FONTS` is empty and everything must still build, test and run; never add craft-fonts to a `Cargo.toml`. Tests on its glyphs skip when it is empty, so run the gates with and without `CRAFT_FONTS_DIR` when you touch fonts.
- **Clean-room.** Never read/disassemble anything inside Adobe app bundles (names/listings only). Never copy Adobe icons, presets, LUTs, fonts. Never copy GPL/LGPL/AGPL code (FFmpeg, x264, x265, MLT, Kdenlive, Shotcut, Olive, LAME…). ffmpeg/ffprobe run only as external test oracles / fixture generators — never linked or shipped.
- **Pure Rust** in the product. OS media APIs only via Rust bindings in `crates/platform` (planned; not created yet), optional, behind traits.
- **Pure Rust** in the product. OS media APIs only via Rust bindings in `crates/platform` (hardware decoding; the one crate allowed `unsafe`, see `AGENTS.md` §0.3 and `docs/adr/0001-platform-ffi.md`), optional, behind traits, with our decoders as the fallback.
- **Layering** ([`docs/architecture.md`](docs/architecture.md) §1, enforced by `cargo xtask layers`): nothing below L5 depends on egui/eframe/winit/rfd/cpal. L0 codec/container crates depend only on `filmcraft-bitstream`.
- **Exact time:** all time is `filmcraft_time::Tick` (254 016 000 000/s). Never use f64 seconds for edit math.
- **Everything is a command** (`crates/engine`): id, label, menu path, shortcut, params, enabled(), run(). UI, CLI, control channel and MCP all dispatch by id.
@@ -82,7 +83,7 @@ FilmCraft is a non-linear editor for people who know Premiere: the same panels,
- **Three-point editing.** Mark In and Out in the Source monitor, then insert (`,`) or overwrite (`.`) onto the patched tracks. Lift (`;`) and extract (`'`) take ranges back out.
- **Every trim.** Ripple, roll, slip, slide, rate stretch and razor tools. Trim mode selects edit points as ripple, roll or trim, nudges them a frame at a time (`⌥←``⌥→`, ×5 with `⇧`), toggles the trim type with `⌃T` and extends them to the playhead with `E`. `Q` and `W` ripple-trim to the playhead. The **Trim Monitor** shows both sides of the edit, and **dynamic trimming** trims live while it plays: `L` forward, `J` back, `K` to stop and commit as one undo step.
- **Exact time.** Every edit is computed on integer ticks: 254,016,000,000 per second, which divides evenly by every common frame rate and sample rate. 23.976, 29.97 drop-frame and 59.94 are exact, not approximate.
- **The details pros rely on.** Markers with colours, names and durations; add edit (`⌘K`) on one or all tracks; nesting; copy, paste and paste insert; ripple delete and close gap; snapping; unlimited undo with a History panel.
- **The details pros rely on.** Markers with colours, names and durations; export sequence review notes with **Markers ▸ Export Markers as CSV…**; add edit (`⌘K`) on one or all tracks; nesting; copy, paste and paste insert; ripple delete and close gap; snapping; unlimited undo with a History panel.
- **Your keys.** A Keyboard Shortcuts editor (`⌥⌘K`) with a drawn keyboard, panel-specific shortcuts, conflict warnings and presets for FilmCraft, Premiere Pro, Final Cut Pro and Avid key layouts.
- **Never lose work.** Saves are atomic, auto-save keeps a rolling set of versions, and a crash-recovery journal written about a second after each edit brings back unsaved changes after a crash or power cut.
@@ -136,7 +137,7 @@ FilmCraft is a non-linear editor for people who know Premiere: the same panels,
- **Premiere 26's full Video Effects bin (93 effects in 16 folders), the Legacy bin and the obsolete effects old projects use, plus around 30 transitions.** Blurs (Bokeh, Focus, Compound), keys (Ultra Key, Track Matte), distortions (Corner Pin, Turbulent Displace, Warp Stabilizer), Lights & Glows, Immersive Video (VR) effects on equirectangular footage, Posterize Time, Echo and more. Cross dissolve, dip to black or white, film dissolve, wipes, irises, pushes, slides, zooms, page peel, cube spin and more.
- **Motion and opacity on every clip:** position, scale, rotation, anchor point and anti-flicker, plus 26 blend modes.
- **Keyframes like Premiere's:** linear, Bezier, auto and continuous Bezier, hold, ease in and ease out. Effect Controls shows a keyframe lane for every parameter, and each animated parameter opens into **value and velocity graphs** with draggable influence handles.
- **Keyframes like Premiere's:** linear, Bezier, auto and continuous Bezier, hold, ease in and ease out. Effect Controls shows a keyframe lane for every parameter under a time ruler with the playhead's handle, and each animated parameter opens into **value and velocity graphs** with draggable influence handles. Effect Controls and the Properties panel share the keyframe navigator (◀ ◆ ▶): add or remove the keyframe at the playhead, step to the previous or next one.
- **A GPU compositor** built on wgpu (Metal, Vulkan, DirectX 12, WebGPU). It samples YUV straight from the decoder with footprint supersampling and blends in linear light. A CPU path renders the same frames, and the two are tested against each other.
<p align="center">
@@ -174,6 +175,7 @@ No FFmpeg inside. The video codecs, AAC, Opus and the containers are our own Rus
| **HEVC / H.265** | ✓ | | Main and Main 10, bit-exact on 41 streams (tiles, WPP, PCM, long-term references), about 225 fps at 1080p |
| **VP8** | ✓ | | Every coding tool, bitstream versions 0–3, hidden altref frames, alpha layer of transparent WebM; bit-exact with libvpx on its published VP8 test vectors; in WebM/Matroska and MP4 |
| **VP9** | ✓ | | Profiles 0–3, 8/10/12-bit, 4:2:0 to 4:4:4, tiles and superframes; bit-exact on 50+ streams; in WebM/Matroska and MP4 |
| **Opus** | ✓ | | SILK, CELT, hybrid and multistream surround; range-exact on every RFC 8251 conformance vector; in WebM/Matroska and MP4 |
| **AV1** | ✓ | | Main profile, 8/10-bit 4:2:0 and monochrome, every coding tool incl. film grain, superres and spatial layers; bit-exact with libdav1d on the libaom vectors tested; in MP4 and WebM/Matroska |
@@ -182,9 +184,10 @@ No FFmpeg inside. The video codecs, AAC, Opus and the containers are our own Rus
| **MP3, FLAC, ALAC, Vorbis** | ✓ | | Via the [symphonia](https://github.com/pdeljanov/Symphonia) crate (MPL-2.0) for now, to be replaced by our own |
| **AVI** | ✓ | | AVI 1.0 and OpenDML (files past 1 GB); Motion JPEG, H.264, HEVC and uncompressed RGB / YUV with PCM, MP3, MP2 or AC-3. DivX / Xvid and DV not yet |
| **Stills** | ✓ | ✓ | Import PNG, JPEG, GIF, WebP, TIFF and BMP (animated GIF and APNG as clips, with transparency); export PNG sequences and animated GIF |
MXF and hardware decode are next ([roadmap](ROADMAP.md)).
Also imported: MPEG-2 / MPEG-1 video, AC-3, MP2, MXF (OP1a / OP-Atom), MPEG transport and program streams (AVCHD, broadcast, DVD), Ogg and image sequences. Not yet: AV1 export, camera RAW, E-AC-3. Hardware decoding works on macOS (VideoToolbox), Windows (Media Foundation) and, for H.264 and HEVC, Linux (VA-API); H.264 export can use NVIDIA's encoder on Windows and Linux (opt-in), and H.265 (HEVC Main, 8-bit) export works through it (NVENC on Windows, VideoToolbox on macOS) ([#30](https://github.com/storytold/filmcraft/issues/30)).
<br>
@@ -196,7 +199,7 @@ MXF and hardware decode are next ([roadmap](ROADMAP.md)).
<p align="center"><sub>Export mode, set to write the trailer as H.264 MP4.</sub></p>
- **H.264 MP4 with AAC, using our own encoders.** A 6-second 960×540 render takes 1.3 seconds and decodes cleanly in ffmpeg with error concealment switched off.
- **H.264 MP4 with AAC, using our own encoders.** A 6-second 960×540 render takes 1.3 seconds and decodes in ffmpeg without a single warning, with strict error detection on.
- **Also:** Apple ProRes 422 HQ and Motion JPEG in QuickTime, MXF OP1a and Avid-style OP-Atom (DNxHR, ProRes or H.264 with PCM and start timecode), PNG sequences, animated GIF and WAV.
- **Background jobs** with progress and cancel, so you keep editing while it renders.
- **Render previews:** the render bar marks segments green, yellow or red; rendered previews are cached by content, so an edit only invalidates what it touches and undo brings the green back.
@@ -220,7 +223,7 @@ Import merges the document's bins, media and sequences into your project as one
## Built for agents
Every menu item, button, slider and drag in FilmCraft is a **command** with an id, typed parameters and an enabled state. There are about 135 engine commands so far, with the rest of Premiere's catalogue on the way. The UI, the CLI, a JSON control channel and an **MCP server** all dispatch the same commands, so Claude or any agent can cut, trim, grade, mix and export exactly the way a person does. The UI can also be driven at the level of mouse and keyboard: every widget has an automation id, and agents can click, drag, type and take screenshots.
Every menu item, button, slider and drag in FilmCraft is a **command** with an id, typed parameters and an enabled state. There are more than 650 engine commands (`filmcraft-cli commands` lists them), with the rest of Premiere's catalogue on the way. The UI, the CLI, a JSON control channel and an **MCP server** all dispatch the same commands, so Claude or any agent can cut, trim, grade, mix and export exactly the way a person does. The UI can also be driven at the level of mouse and keyboard: every widget has an automation id, and agents can click, drag, type and take screenshots.
```jsonc
// over the control channel (JSON lines on TCP) or as MCP tool calls
@@ -238,7 +241,7 @@ The trailer and the grades in these screenshots were built exactly this way, by
## Everywhere
- **Native** on macOS, Windows and Linux, with a native macOS menu bar.
- **The web:** every crate up to the engine compiles to `wasm32`; the browser front end is next.
- **The web:** the same engine and UI run in the browser via WebAssembly (`apps/filmcraft-web`, see [docs/web.md](docs/web.md)); every release ships it as `filmcraft-web-<version>.zip`.
- **Swappable UI.** The interface is one crate (`ui-egui`) over the engine, so a different front end can replace it without touching editing logic.
<br>
@@ -252,6 +255,13 @@ cargo run --release -p filmcraft-cli -- commands # list every engine c
cargo run --release -p filmcraft-cli -- mcp # MCP server (headless)
```
Japanese text in the interface and in titles comes from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (release builds always include it; without it FilmCraft uses its own and the system's fonts):
CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p filmcraft
```
The control protocol is documented in [docs/control-protocol.md](docs/control-protocol.md). Stuck, or want to show what you made? Ask in [Discord](https://discord.gg/artcraft).
## Documentation
@@ -266,11 +276,29 @@ The control protocol is documented in [docs/control-protocol.md](docs/control-pr
| [docs/control-protocol.md](docs/control-protocol.md) | Control-channel and MCP reference |
| [docs/project-files.md](docs/project-files.md) | `.fcproj` format, schema migrations, auto-save and crash recovery |
| [docs/graphics.md](docs/graphics.md) · [docs/captions.md](docs/captions.md) | Text engine, graphic clips and tools; caption tracks and formats |
| [ROADMAP.md](ROADMAP.md) | Milestones and estimates |
| [ROADMAP.md](ROADMAP.md) | Stage, parity numbers, languages, what's next and the progress log |
| [docs/target-app-parity.md](docs/target-app-parity.md) · [docs/gaps.md](docs/gaps.md) · [docs/roadmap.md](docs/roadmap.md) | Parity assessment against Premiere Pro, the ranked list of shortfalls, milestones and current focus |
FilmCraft is young and moving fast: roughly 45% of the way to Premiere Pro parity, with editing, trimming, colour, keyframes, titles, captions, mixing, codecs and export working today. [ROADMAP.md](ROADMAP.md) tracks every milestone with estimates.
FilmCraft is young and moving fast. Editing, trimming, multicam, colour, keyframes, effects, titles, captions, mixing, codecs and export work today.
FilmCraft is in **alpha**. We track two numbers ([docs/target-app-parity.md](docs/target-app-parity.md), re-measured 2026-10-10 against Premiere Pro 26.5.2):
- **Feature breadth: ~86%.** Premiere Pro's menu items, effects, transitions, panels and formats that exist in FilmCraft (menus 92% and effects 100% measured).
- **Ready for real work: ~56%.** Our honest estimate of how close FilmCraft is to replacing Premiere on real projects.
The biggest gaps today:
- **Speed on big footage.** Hardware decoding works on macOS and Windows, and for H.264 and HEVC on Linux (VA-API, or NVDEC with NVIDIA's driver; VP9 and AV1 decode in software there). Blend modes and the most common effects run on the GPU, but Lumetri, keys and export rendering still run on the CPU; H.264 encoding can use the hardware encoder on macOS and on Windows and Linux with an NVIDIA GPU (opt-in, Export ▸ Hardware encoding), and H.265 export is hardware-only on both (HDR sequences export Main 10 PQ / HLG with NVENC; VideoToolbox writes 8-bit SDR) ([#30](https://github.com/storytold/filmcraft/issues/30)).
- **Premiere projects.** `.prproj` files can't be opened yet; bring sequences over as FCP7 XML, AAF or OTIO.
- **No plugins.** No VST3 / Audio Units or OpenFX hosting.
- **Delivery codecs.** H.264 is our only software delivery-codec export; H.265 exports only through a hardware encoder (NVIDIA on Windows: Main 8-bit SDR and Main 10 HDR PQ / HLG; VideoToolbox on macOS: 8-bit Main, SDR); no AV1 export yet.
- **Real-world media and platforms.** Our decoders are bit-exact on conformance streams, but camera and phone files in the wild are less tested. Windows and Linux get far less testing than macOS.
- **AI features.** Few so far; speech to text is optional and off by default.
Bug reports with real footage are the most useful thing you can send us: [open an issue](https://github.com/storytold/filmcraft/issues) or tell us in [Discord](https://discord.gg/artcraft).
| Rendering and output | `render`, `gpu`, `export` |
@@ -288,6 +316,84 @@ A layered Cargo workspace:
Nothing below the front ends depends on a UI toolkit or OS API. `cargo xtask ci` checks formatting, lints, tests, the layering rules, asset attribution and the wasm build.
## Downloads
**New to FilmCraft?** Download it from the [FilmCraft page on getartcraft.com](https://getartcraft.com/apps/filmcraft). That's the easiest way to install it.
**Want a specific build or format?** On GitHub, the [latest release](https://github.com/storytold/filmcraft/releases/latest) has every build listed below, and [all releases](https://github.com/storytold/filmcraft/releases) has earlier versions and their notes. `<ver>` in the file names is the version number, and `SHA256SUMS.txt` lists a checksum for every file.
@@ -336,7 +442,9 @@ FilmCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APAC
Copyright (c) 2026 ArtCraft Team and the FilmCraft contributors. Required notices are in [NOTICE](NOTICE).
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md). Builds made with
[craft-fonts](https://github.com/storytold/craft-fonts) (all official releases) also embed its fonts
(OFL-1.1), listed in its [ATTRIBUTION.md](https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md).
**Footage and music in the screenshots:** NASA's Apollo 11 film and television footage from [images.nasa.gov](https://images.nasa.gov) (launch, Launch Control Center, lunar surface and recovery) and the Apollo 11 air-to-ground voice transcript, all US Government works in the public domain (NASA does not endorse this project); *Night of the Living Dead* (1968), *Carnival of Souls* (1962) and *Charade* (1963), all in the US public domain; *Earth Views from the ISS* by NASA; Chopin's Nocturne Op. 48 No. 1 and Ballade No. 1, performed for Musopen and released under CC0. The media itself is not in this repository. Sources and details for every asset are in [ATTRIBUTION.md](ATTRIBUTION.md).
@@ -355,3 +463,7 @@ Forks and modified versions must remove them.
<sub>Made by the <a href="https://getartcraft.com/">ArtCraft</a> team and community.</sub>
</p>
## Star history
[](https://www.star-history.com/?repos=storytold%2Ffilmcraft&type=date&legend=top-left)
Progress toward feature parity with Adobe Premiere Pro, with estimates. Updated as milestones land.
- UI.6: Compact Source and Program monitors separate timecodes from view settings and retain Source drag choices. Transport controls use the shared toolbar sizing helper, with overflow actions in an operable More menu and a scrolling fallback at tiny widths.
- UI.3: Panel tabs, edge splits, tab reordering and floating groups use the shared docking model and drop resolver. Floating geometry is saved with named workspaces while legacy layout JSON remains readable. Dock dividers use bounded shared geometry and keyboard-accessible resize handles. Tools and dock tabs expose actual widget focus, paint focus outlines, and support keyboard traversal/activation. Control input reaches focused widgets in background windows; optional input isolation supports unattended validation. Existing workspace trees and saved layout formats are retained.
**Stage: alpha** · next: beta, ~19 points of ready-for-real-work and ~400–700 h away, plus opening Premiere's `.prproj` ([why](docs/target-app-parity.md#stage); passes the [core-workflow gate](docs/roadmap.md#alpha-gate))
## Parity scorecard
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (language table: German ships the menus, #630) · **Target:** Adobe Premiere Pro 2026 (26.5.2)
Measured against Premiere Pro 26.5 on this machine. Menu items: the native menu bar dump, minus
| Panels and UI fidelity | 12% | all main panels incl. Metadata, Timecode, Events, Progress, Reference Monitor, full Lumetri Scopes, audio effect editors; Project panel List/Icon/Freeform with view presets and hover scrub; Media Browser with Favorites/navigation/ingest | ~85% |
| Robust on real-world material (codec edge cases, 4K/8K performance, pro workflows) | +120–200 | **+30–50 h** |
| **Total to "100% and better"** | **~210–325** | **~50–82 h (≈2–3.5 days, 24/7)** |
| **Feature breadth** | **~86%** | Does each Premiere feature exist? Menus 92% (measured: label diff against Premiere 26.5.2's menu dump), effects / transitions / audio effects 100% (measured by tests), other areas estimated |
| **Ready for realwork** | **~56%** (51–61%) | How close FilmCraft is to replacing Premiere for an editor on real projects: depth, correctness, file compatibility, speed, stability, ecosystem. Estimated depth per area, combined by a [written formula](docs/target-app-parity.md#three-readiness-numbers) |
| **Mainstream practitioner** | **~55%** | A typical professional editor's weekly work only, discounted ×0.90 interaction, ×0.92 stability, ×0.88 file exchange with Premiere users ([method](docs/target-app-parity.md#mainstream-practitioner)) |
Two numbers, because they answer different questions. The gap between them is the work that
matters most now: breadth is nearly done, depth is not.
Limits on speed: CPU and disk on one machine (more than ~5 agents slows everyone down: each worktree
build is 6–7 GB and a full `cargo xtask ci` takes 20–60 min under load), and a single integrator
merging, resolving conflicts (e.g. two agents bumping the project schema) and re-running CI. With one
agent and no parallelism, multiply wall-clock by ~3–4.
### Readiness by audience
Not reachable clean-room and locally: **Generative Extend** (needs a large video-generation model).
**Enhance Speech** and **Auto Reframe** are feasible only with openly licensed models we can ship.
| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates |
|---|---|---|---|
| Full target (ready for real work) | ~56% | ~1,450–2,600 (~70% parallelizes) | Plugin hosting, AI models, localization incl. RTL / Devanagari, camera RAW and delivery codecs, `.prproj`, hardware acceleration, UI fidelity |
| Mainstream practitioner | ~55% | ~500–850 (~65% parallelizes) | `.prproj` import (60–120), UI fidelity (120–200), stability and PR CI (40–80), 10-bit / 4:2:2 camera decode (30–50), playback / hardware (60–100), import and export gaps (60–100) |
| Essentials user | ~60% | ~180–350 (~60% parallelizes) | Launch crashes on Windows / Intel / older macOS (40–80), discoverability and timeline friction (60–110), playback lag (30–60), common import gaps (20–40), audio devices (15–30) |
## Milestones
Method and calibration: [docs/target-app-parity.md](docs/target-app-parity.md#three-readiness-numbers).
Status: ✅ done · 🟡 in progress · ⬜ not started. Estimates are remaining agent-hours.
## By dimension
| # | Milestone | Status | Done | Remaining | Est. |
Also shipped: Ukrainian (menus only, 10%). Detail: [docs/localization-parity.md](docs/localization-parity.md).
## Upcoming
Ranked; detail and "done when" in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gaps.md).
1.**No launch crashes, and CI on pull requests** (macOS 12, Intel UHD, Windows 11 reports; no workflow runs the tests today). 40–80 h.
2.**Open Premiere's `.prproj`**, the beta blocker. 60–120 h.
3.**10-bit / 4:2:2 camera media without hardware** (H.264 High 10 / 4:2:2, HEVC 4:2:2; #626). 30–50 h.
4.**Measure fidelity against Premiere** and add `cargo xtask parity`. 50–90 h.
5.**Real-world media corpus and workflow acceptance tests.** 40–70 h.
6.**Hardware acceleration, continued** (#30): zero-copy upload, more effects on the GPU, Quick Sync / AMD encoders, VideoToolbox Main 10. 120–200 h.
7.**UI fidelity:** Program monitor transform handles, Effect Controls, docking. 120–200 h.
8.**Plugin hosting** (VST3 / AU, then OpenFX). Owner decision needed. 200–400 h.
## Progress log
- **2026-10-10 (changed media):** Settings ▸ Media ▸ Automatically refresh growing files works: media files that change on disk while the project is open (a recording still being written, a render written over the file a clip uses) are read again every *Refresh growing files every* seconds and as soon as the window comes back to the front: the new length, picture and identity, without an undo step (`media.refreshChanged`, `media_watch`).
- **2026-10-10 (re-measure):** full parity re-measure against Premiere Pro 26.5.2: menu label diff 338 / 366 (92%), installed bundle listings (10 UI languages, exporter and codec frameworks), 188 open issues. Breadth ~86% (file formats now counted), ready ~55%, stage **alpha**; progress docs reorganized per the craftrules standard ([target-app-parity](docs/target-app-parity.md), [gaps](docs/gaps.md), [roadmap](docs/roadmap.md), [codec](docs/codec-parity.md), [file-format](docs/file-format-parity.md), [hardware](docs/hardware-parity.md), [UI](docs/ui-parity.md), [localization](docs/localization-parity.md)).
- **2026-10-10:** ~75 community PRs merged: GPU export rendering (Auto) with Vignette, Video Limiter and Lumetri basic / creative / vignette on the GPU (#421); Linux NVDEC H.264 / HEVC decode (#549); text-to-speech narrations with Kokoro-82M (#278); Japanese for the whole interface (#449) and Ukrainian menus (#407); ALSA default-device fallback (#254, fixes #23); drop import into the timeline (#517); system appearance (#505); timecode scrub editing (#576); NVENC RGBA input (#486); marker CSV export (#454); many fixes (#526–#575).
- **2026-10-09:** release 0.5.0. NVENC H.265 export on Windows, Main and Main 10 HDR (#328, #360); NVENC H.264 export (#386); VA-API H.264 / HEVC decode (#358); Source monitor playback (#423); multi-stream container audio, schema v13 (#361); New Sequence and Sequence Settings dialogs (#269, #355); Spanish for the panels (#238); background decoders and memory stress tests (#385, #381).
- **2026-10-08 (Source range dragging):** implicit full-clip In/Out, video-only/audio-only/linked both controls, picture/waveform drag gestures and one-command undoable timeline placement. Shared Source/Program draggable frame-bounded handles and range translation, cancellation, Source marker/navigation routing and marked-range playback/looping.
- **2026-10-08 (Source playback):** normal forward playback for video/audio media and subclips in the Source monitor, an independent Source clock, Play/Pause and focused Space, frame stepping, device-clock audio with wall-clock fallback, seeking, and end-of-clip stopping. Windows desktop review verified a real video file and standalone WAV, including In/Out marking. Reverse/shuttle Source playback and source sequences remain unsupported; no parity percentage changed.
- **2026-10-08 (night):** HDR H.265 (HEVC Main 10, PQ and HLG, BT.2020, limited range) export on Windows through NVENC (#30): an HDR sequence now exports as real 10-bit HDR (`rgbf_to_yuv420_10` into P010 input buffers; VUI BT.2020 + PQ / HLG; the HDR10 mastering-display and content-light messages as SEI on every IDR for PQ, through the generic SEI payload array because NVENC 12.1 has no mastering-display field; `colr` / `mdcv` / `clli` in the sample entry), where the GPU reports 10-bit HEVC (new `register_hdr_probe` / `hdr_available` in the export crate; elsewhere, and on macOS, HDR sequences still tone-map to 8-bit SDR HEVC, and `settings.sdr` forces that). ffprobe reads `hevc` / `Main 10` / `hvc1` / `yuv420p10le` / `bt2020` / `smpte2084` or `arib-std-b67` / `bt2020nc` / `tv` with the mastering-display and content-light side data for PQ; ffmpeg's decode equals ours code for code; the in-process round trip is at 71.1 dB luma PSNR on the 10-bit scale with 803 distinct levels on a ramp and the peak code 940 intact (RTX 5060). 8-bit SDR H.265 is unchanged.
- **2026-10-08:** H.265 (HEVC Main, 8-bit 4:2:0, SDR) export on Windows through NVENC (#30): the NVENC session, ring and NV12 path of the H.264 encoder now serve both codecs (the H.264 export is byte-for-byte unchanged); the format is available where the GPU has an HEVC encoder, choosing it is the opt-in, and `hvcC` is built from the SPS the encoder wrote. ffprobe reads `hevc` / `Main` / `hvc1` / `yuv420p` / BT.709; our own HEVC decoder decodes it at 48.9 dB luma PSNR (RTX 5060). Main 10 and HDR remain.
- **2026-10-08 (#285):** `filmcraft-cli` no longer panics with "Broken pipe" when its stdout is closed early (`filmcraft-cli commands | head`): later output is dropped, the rest of the work (script lines, `--save`, `--save-as`) still runs, and the exit status still reports failures. Other stdout write errors are reported and make the exit status 1. No change to the checklist or estimates.
- **2026-10-06:** 21 community PRs landed (#19, #28, #38, #39, #41–#60). Highlights: hardware decoding is now actually on in the desktop app (#41:`register()` sat inside a `log::info!` that never ran, so #33's VideoToolbox path only worked in the CLI and benchmarks); decode memory on long timelines cut sharply (frame pool, shared GOP-cache budget, positional file reads); exports are byte-identical across machines (#19); crash fix for odd track names over MCP (#44); ripple trim, ripple delete and speed changes move split-edit clips once (#56–#58); Japanese interface using an installed system font (#38; no fonts bundled); MCP annotations, resources, export progress and cancellation (#28).
- **2026-10-05:** honest assessment added (checklist ~87% vs ready for real work ~50–60%). Hardware acceleration started (#30): blend modes on the GPU (#32); VideoToolbox decode + `platform` crate, the one crate allowed `unsafe` (#33). Community fixes landed: AIFF import, export start/end ranges, transitions following ripple trims, Slide moving linked audio (#5–#8).
- **2026-10-04 (later):** interchange + MXF export (M11.4/M6.6): own compound-file container (`crates/cfb`), AAF Edit Protocol import/export (embedded/linked/consolidated audio, video mixdown, handles, breakout to mono), OMF 2.0 export (Bento), MXF OP1a/OP-Atom writer (DNxHR/ProRes/H.264/PCM) as export formats. 1605 tests.
@@ -116,3 +159,15 @@ Status: ✅ done · 🟡 in progress · ⬜ not started. Estimates are remaining
- **2026-09-30 (midday):** HEVC decoder, Matroska/WebM demuxer and audio DSP merged; HEVC import wired; asset rules (AGENTS.md, ATTRIBUTION.md, `cargo xtask assets`); README with hero screenshot and the Craft family.
| 2026-10-11 | minor | Language table: German ships the menus (#630, 10.6%) |
| 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience |
| 2026-10-10 | minor | Headline adds mainstream practitioner (~55%) and essentials user (~60%); ready for real work ~55% -> ~56% (formula written out, same inputs); beta distance ~19 points |
| 2026-10-10 | minor | Stage banner links the core-workflow gate (all six workflows pass on macOS; stays alpha) |
| 2026-10-10 | major | Restructured to the craftrules progress-docs shape: stage banner, headline numbers, dimension / feature / language tables, upcoming; re-measured against Premiere 26.5.2 (breadth ~87 → ~86%, ready ~50–60% → ~55%); honest assessment and scorecard moved to docs/target-app-parity.md, "Where we are lacking" to docs/gaps.md, milestones to docs/roadmap.md, interface localization to docs/localization-parity.md |
| 2026-10-08 | minor | Log entries for HEVC export, Source playback and range dragging |
| 2026-10-05 | major | Honest assessment added: checklist ~87% vs ready for real work ~50–60% |
log::info!("graphics mode {} chosen with --graphics",mode.name());
Saved{mode,failures: 0}
}
None=>{
ifdecision.moved_on{
letfailed=saved.map_or(first,|s|s.mode).name();
log::warn!(
"the last {FAILURES_TO_MOVE_ON} starts with graphics mode {failed} died before their window painted; starting with {} (`--graphics default` or the notice in the window goes back)",
decision.saved.mode.name()
);
}elseifdecision.saved.failures>0{
log::warn!("the last start with graphics mode {} died before its window painted",decision.saved.mode.name());
description="List every command (id, label, menu path, shortcut, parameter doc, enabled now). Menus, shortcuts, panels and timeline gestures all map to these ids."
title="List commands",
description="List every command (id, label, menu path, shortcut, parameter doc, enabled now). Menus, shortcuts, panels and timeline gestures all map to these ids.",
description="Run a command by id with JSON params, e.g. {\"id\":\"timeline.razor\",\"params\":{\"seconds\":3.5}} or {\"id\":\"effects.apply\",\"params\":{\"effect\":\"Gaussian Blur\"}}. Undoable edits land in History."
title="Run a command",
description="Run a command by id with JSON params, e.g. {\"id\":\"timeline.razor\",\"params\":{\"seconds\":3.5}} or {\"id\":\"effects.apply\",\"params\":{\"effect\":\"Gaussian Blur\"}}. Undoable edits land in History.",
#[tool(description = "Project tree (bins and items with ids, types, durations), active sequence, source clip.")]
#[tool(
title="Run several commands",
description="Run several commands in order: {steps: [{id, params?}], stop_on_error?: true}. Returns {completed, failed, results: [{ok, result | error}]}; each edit is its own undo step.",
description="The active sequence's frame at `seconds` (default: the playhead) as PNG, same as render_frame (bridge mode: Program monitor screenshot).",
description="Live app: press-drag-release from one point/element to another (move clips, trim edges, scrub, resize panels, drop project items onto tracks)."
title="Live app: drag",
description="Live app: press-drag-release from one point/element to another (move clips, trim edges, scrub, resize panels, drop project items onto tracks).",
instructions="FilmCraft video editor (Premiere Pro-class). Every edit is an engine command: `command_list` to discover ids/params, `command_run` to execute (undoable). `project_inspect`/`sequence_inspect` return ids you can pass to commands; `render_frame` shows the result. In bridge mode the `ui_*` tools drive the live app: `ui_elements` lists clickable ids, `ui_click`/`ui_drag`/`ui_key` operate it, `ui_screenshot` shows it. Time is in ticks: 254016000000 per second (commands also accept `seconds`, `frame` or `timecode`)."
)]
implServerHandlerforFilmcraftMcp{}
implFilmcraftMcp{
/// The first argument `tool` doesn't take, as a message naming the ones it does.
Some(format!("unknown argument \"{bad}\" for {tool}; expected: {accepted}"))
}
}
constINSTRUCTIONS: &str="FilmCraft video editor (Premiere Pro-class). Every edit is an engine command: `command_list` to discover ids/params, `command_run` to execute (undoable; `command_batch` runs several). `doc_inspect` (or `project_inspect`/`sequence_inspect`) returns ids you can pass to commands; `render_preview` shows the result. In bridge mode the `ui_*` tools drive the live app: `ui_elements` lists clickable ids, `ui_click`/`ui_drag`/`ui_key` operate it, `ui_screenshot` shows it. Time is in ticks: 254016000000 per second (commands also accept `seconds`, `frame` or `timecode`).";
/// Resources: the project (as `doc_inspect`) and the command catalog (as `command_list`).
constDOCUMENT_URI: &str="filmcraft://document";
constCOMMANDS_URI: &str="filmcraft://commands";
/// MCP 2026-07-28 clients (current Claude Code) require `ttlMs` and `cacheScope` on list and read
/// results; rmcp fills `resultType` and leaves the cache hints to the server, as the generated
/// `tools/list` does. The resource list never changes; the document does, so reads aren't cached.
| Index | OpenDML `indx` super index → `ix##` standard indexes (field indexes too), else `idx1` (offsets relative to `movi` or absolute), else a scan of the `movi` lists |
| Large files | `RIFF AVIX` extensions: through the OpenDML index, or scanned after `idx1`, which only covers the first RIFF |
| Key frames | from the index; a scan marks every chunk and says so (`keyframes_known`) |
| Damage | sizes clamped to the file (a cut-off recording opens with what is there), `rec ` lists nested at most 3 deep, tables bounded by what the file could hold |
`filmcraft-codecs` maps the streams onto decoders (`AviSource`): Motion JPEG, H.264, HEVC and
@@ -16,7 +16,7 @@ and writes caption files, converts them to and from caption tracks, and draws ca
| Scenarist SCC `.scc` | yes | yes | CEA-608 at 29.97 fps, drop-frame or not. Reads pop-on, roll-up and paint-on (channel 1); writes pop-on. |
| MacCaption MCC `.mcc` | yes | yes | One SMPTE ST 334-2 caption distribution packet (CDP) per frame in MacCaption's hex text with letter abbreviations. Writes 29.97 fps (`30DF` / `30`) with CEA-608 field 1 (the SCC schedule) **and** CEA-708 service 1 (two-window pop-on emulation). Reads CEA-608 field 1 when present, else CEA-708 service 1; time code rates 30DF, 30, 60DF, 60, 25, 24, 50. |
| EBU STL `.stl` | yes | yes | EBU Tech 3264-E: GSI block + 128-byte TTI blocks, `STL25.01` (25 fps) or `STL30.01` (read/written as 29.97 fps, non-drop labels), ISO/IEC 6937 Latin text (accents as diacritic + letter), italics / underline codes as `<i>` / `<u>`, extension blocks for long subtitles, start-of-programme (TCP) subtracted. |
| TTML `.ttml` (IMSC1) | yes | yes | Writes the IMSC1 Text profile (TTML2 namespaces): `<p>` per cue, `<br/>`, styled `<span>`s for `<i>` / `<b>` / `<u>`, `ttm:agent` speakers, exact frame times (`123f`) at the sequence rate. Reads any TTML (TTML1, TTML2, the 2006 `ttaf1` drafts): clock / frame / offset / tick times, `dur`, nested time containers, timed spans, referenced styles. |
| TTML `.ttml` (IMSC1) | yes | yes | Writes the IMSC1 Text profile (TTML2 namespaces): `<p>` per cue, `<br/>`, styled `<span>`s for `<i>` / `<b>` / `<u>`, `ttm:agent` speakers, exact frame times (`123f`) at the sequence rate. Reads any TTML (TTML1, TTML2, the 2006 `ttaf1` drafts): clock / frame / offset / tick times, `dur`, nested time containers (`body` / `div` begins offset and ends clip their contents; `seq` containers are read as `par` with a warning), timed spans, referenced styles. |
| DFXP `.dfxp` | yes | yes | TTML1 with the DFXP presentation profile; clock times in milliseconds (frame-exact after `snap_to_frames`). |
All cue times are exact `Tick`s. Milliseconds convert exactly (1 ms = 254 016 000 ticks); SCC
Loaded 100 of 777 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.