Skip to content

Sync develop → staging#263

Merged
walterlow merged 42 commits into
stagingfrom
develop
May 30, 2026
Merged

Sync develop → staging#263
walterlow merged 42 commits into
stagingfrom
develop

Conversation

@walterlow
Copy link
Copy Markdown
Owner

Summary

Promotes 41 commits from develop to staging. 229 files changed, +12,977 / −3,059. Headlined by a new headless render+edit CLI, an in-app render queue, timeline zoom/scroll perf work, and a core UI/data-flow refactor.

Highlights

Headless render + edit CLI (new)

  • Playwright-driven render + edit CLI and UI-less harness; warm-browser batch rendering
  • Render service (serve.mjs) + shared render core; Dockerfile with platform-aware Chrome args and GPU health checks
  • Richer edit ops (addClip, addTrack, keyframes, effects, transform); real AAC audio via WASM encoder
  • Fails loudly when WebGPU is missing for GPU effects; decodes AC-3/E-AC-3 by seeding media metadata

Export

  • In-app render queue with per-project persistence (paused-by-default, saves on status change)
  • Per-project exports folder by id with location notice; still surfaces loose top-level files
  • Dropped legacy top-level exports merge

Timeline performance

  • Window video filmstrip tiles to the visible range
  • Stage zoom-out clip mounts under a global per-frame budget
  • Dropped per-frame forced reflow in the zoom scroll path; tracked scrollLeft passed to viewport-sync RAF
  • Waveform rendered from true peak envelope

Media library & import

  • Refactored media preparation into a readiness gate; optimized import prep for large filmstrips/waveforms
  • Atomicized project media unlink + timeline save
  • Surface re-imported media and fixed duplicate banner

Preview / audio / text fixes

  • Keep continuous overlay across transition windows during playback
  • Skim preview to in/out boundary while dragging markers
  • Keep video audio off the mixer; split embedded audio reliably
  • Unify text layout across render paths to stop skim shift

Core refactor & misc

  • Refactor of core UI and data flow; lazy-load editor panels and optimize project list
  • Route editor lazy imports through feature contracts
  • i18n partials for media preparation / keyframes; changelog roll-up

Test plan

  • Smoke-test editor: import media, scrub, zoom in/out, playback across transitions
  • Export via in-app render queue (per-project folder)
  • Headless render + edit CLI on a sample project

🤖 Generated with Claude Code

walterlow added 30 commits May 29, 2026 00:44
…udget

Zoom-out pulls clips into range and mounted the whole newly-visible cluster
(TimelineItem trees) in one synchronous commit, spiking the frame to ~33ms and
locking the gesture at ~30fps. Zoom-in unmounts, so it stayed smooth.

Stage the mid-gesture expansion so clips mount a few per frame via a shared rAF
coordinator. The budget is global across all track hooks (not per-track) since
the mount cost is on one main thread — per-track would multiply by track count
and reintroduce the spike. The settle path still mounts any stragglers at once.
At extreme zoom (timeline >1M px wide), a fast zoom fling ran at a sustained
20-30fps. LoAF attribution showed ~16ms of forced synchronous layout per frame,
not React (dev Profiler put React commit at ~2.5ms median). The scroll-clamp
and pending-scroll layout effects read container.clientWidth / scrollLeft right
after writing the new width every frame, forcing a reflow of the giant tree.

Use the cached viewport width (invariant under horizontal zoom) and the tracked
scrollLeft instead of live reads, and pass the just-written scrollLeft into
syncViewportFromContainer so it skips the read-back. Cuts dropped frames on a
full fling-to-max from ~51 to ~7-15 with a 17ms (60fps) median.
The scroll handler already records scrollLeft into scrollLeftRef before
scheduling a viewport sync, but the RAF then re-read container.scrollLeft —
forcing a synchronous reflow whenever a zoom width write landed the same frame.
Hand the tracked value to syncViewportFromContainer instead. Removes the last
per-frame forced layout in the zoom path (LoAF forcedStyleAndLayout ~16ms/frame
to ~0); moved scrollLeftRef up so it's in scope for the scheduler.
The clip waveform height came from a hand-tuned heuristic
(mean*0.38 + max2*0.34 + needle*2.35, then ^1.05) that produced two
zoom-dependent artifacts: the needle*2.35 term exploded isolated
transients into disproportionate spikes, and the stable mean+max2 terms
masked real per-column peak dips, flattening loud/compressed content into
a uniform saturated band.

Replace it with a faithful peak envelope (peak*0.85 + mean*0.15) and a
gentle perceptual gamma (^0.72) that lifts quiet passages without
distorting loud transients. Extracted into a shared, tested
computeWaveformAmplitude helper so both the regular and compound-clip
renderers stay in sync.
ClipFilmstrip rendered one <img> tile per slot across the clip's entire
width, ignoring the visibleStartRatio/visibleEndRatio props it already
received. A clip spanning a large fraction of a max-zoom timeline mounted
thousands of tiles that React reconciled and the compositor painted on
every scroll frame, making navigator scrubbing drop to ~20fps with a
~460ms stall on first paint.

Window the slot grid to the visible region (+ pad) via the existing
computeFilmstripRenderWindow helper, mirroring ImageFilmstrip and the
waveform's TiledCanvas. Off-window areas keep showing the repeating
cover-frame background, so windowing is seamless. Cuts the giant-clip
tile count from ~6300 to ~210 and restores ~60fps scrubbing.
…g playback

A transition only stayed on the per-frame fast-scrub overlay during playback when some item incidentally forced the continuous overlay (GPU effect, blend, corner-pin, or composition). A plain transition therefore relied on an unrelated item happening to keep it on; when that item ended at the cut, the overlay dropped into the buffered path, which could leave second-half frames un-rendered and collapse the wipe to a single clip until the window ended.

This was most visible with reversed clips but was not reverse-specific, and only affected live preview (export decodes every frame synchronously).

Force the continuous overlay on active transition-window frames during playback as well as scrubbing, so every transition renders every frame via the path that already worked for the transitions that looked correct.
Diagnosing 'live preview looks wrong but export is fine' transition bugs meant hand-pasting console probes and reading raw per-frame dumps. Add a reusable DEV-only facility instead.

Adds src/shared/logging/preview-trace.ts: a trace sink plus a pure analyzePreviewTrace() that summarizes, per transition half, which overlay path the pump chose and which participants composited, flags stalls/gaps and mid-window overlay path switches, and emits a plain-language verdict (unit-tested).

Wires permanent import.meta.env.DEV-guarded hooks (tree-shaken from prod, no-op unless a trace is running) into the render pump's overlay decision and the renderer's transition-participant draws, and exposes window.__DEBUG__.captureTransition(frame?) (one-call seek+play+record+analyze) plus __DEBUG__.previewTrace.{start,stop,clear,events}().
Split the pure store-hydration half out of loadTimeline into
hydrateTimelineStoresFromProject(project), and the pure
stores-to-ProjectTimeline serialization out of saveTimeline into
buildTimelineFromStores(). Both are behavior-preserving; loadTimeline
and saveTimeline now call them.

This lets callers that already hold a Project and persist project.json
themselves (the headless render/edit harness) hydrate the stores and
serialize them back without the storage read, migrated-project persist,
orphaned-media dialog, or thumbnail-generation side effects.
Add blobUrlManager.registerUrl(mediaId, url): track an externally-hosted
URL (e.g. an HTTP URL) for a media id WITHOUT loading the bytes into a
Blob. Because no Blob is registered in the object-url registry, consumers
that build a mediabunny input fall through to UrlSource — i.e. the media
is range-streamed over HTTP instead of held fully in memory. The entry's
blob field is now optional and a guarded revoke skips external URLs;
existing acquire() behavior is unchanged.

Used by the headless render harness to stream source media from disk
without downloading whole files.
Render and edit FreeCut projects from the CLI with no editor UI, by
driving the real render engine and timeline action modules inside
headless Chrome. The engine relies on browser APIs (WebCodecs, WebGPU,
OffscreenCanvas, OfflineAudioContext), so a Node driver launches Chrome
and loads a UI-less Vite entry (headless.html -> src/headless/main.ts)
exposing window.freecut, instead of porting the engine to Node.

- src/headless/main.ts: renderProject/renderTimeline reuse the export
  pipeline (migrate -> convert -> renderComposition); media is registered
  as HTTP URLs and range-streamed via mediabunny UrlSource.
- src/headless/edit.ts: editProject hydrates the real stores, applies ops
  through the real action modules (add/update/move/remove/split/trim/
  transition), and serializes back.
- headless/render.mjs, headless/edit.mjs: the two CLIs (npm run headless).
- headless/server.mjs: standalone server (built dist/ + media, one
  COEP-isolated origin, HTTP Range) so no dev server is required.
- headless/media-server.mjs, lib/workspace.mjs, probe/smoke scripts, README.
- vite.config.ts: multi-entry (index.html + headless.html).
- package.json: playwright devDependency + "headless" script.

Validated on real projects: text, video+audio, nested compound clips,
GPU adjustment effects, crossfade transitions, range slices, a 5s slice
of a 3GB MKV (range streaming, no OOM), and an edit->render round-trip.
The export audio path keys AC-3/E-AC-3 detection on getMediaAudioCodecById,
which reads the media-library store — empty in the headless harness, so
Dolby Digital / DD+ tracks were dropped (silent). The render CLI now reads
each referenced media's metadata.json and passes it through; the harness
seeds useMediaLibraryStore so the codec is recognized, the @mediabunny/ac3
decoder is registered, and the audio decodes (verified on a real E-AC-3
clip: -29 dB mean, not silence).

Also warn in the CLI when a media's audioCodecSupported is false (e.g. DTS),
which the browser can't decode headlessly — that audio stays silent.
Probe navigator.gpu at render start. If the composition uses enabled GPU
effects (which have no Canvas2D fallback) and WebGPU is unavailable, throw
a clear error instead of silently rendering without them. Transitions keep
their Canvas2D fallback, so a missing GPU there is only a warning. Important
for headless/CI/Docker environments where the GPU may be absent or software.
- Servers now resolve media dynamically from the workspace (resolver instead
  of a fixed id->path map), so one warm browser + server handles any project.
- render.mjs: extract prepareJob/renderJob; add --batch <jobs.json> to render
  many jobs (each with the same keys as the CLI flags) reusing one browser.
- test.mjs (npm run headless:test): self-contained regression — builds the
  harness, asserts a text render (duration/bytes/mime) and an edit op, no
  workspace/media/ffprobe needed, non-zero exit on failure.
- add npm run headless:edit; media-server accepts a Map or a resolver.
…ts, transform

Extend the edit harness with ops that drive the real action modules:
- addClip <mediaId>: builds a video (+ linked audio companion) / audio / image
  item, computing the source range from the media's metadata (seeded into the
  media-library store); edit.mjs passes metadata.json for addClip media.
- addTrack: create a classic video/audio track (createClassicTrack + setTracks).
- addKeyframe / removeKeyframes: keyframe-actions.
- addEffect (gpuEffectType + params, or a full effect) / removeEffect.
- setTransform: updateItemTransform.

Verified via edit->render: addClip places real content with audio; addEffect
gpu-grayscale and setTransform render through WebGPU headlessly.
Extract the shared render core (settings, range, media resolution, harness/
media servers, per-page renderJob) into headless/lib/render-core.mjs so the
CLI and the service share one implementation; render.mjs is now a thin CLI
over it.

Add headless/serve.mjs (npm run headless:serve): a long-lived service that
keeps one warm Chrome + harness over a workspace and exposes GET /health,
GET /projects, POST /render (streams the file), POST /edit. Requests are
serialized to avoid GPU/CPU contention. Verified health/projects/render/edit
end to end.
- chromeLaunchArgs(): pick the ANGLE backend per platform (d3d11/metal/vulkan)
  instead of hardcoding Windows d3d11, and allow FREECUT_CHROME_ARGS to append
  flags (e.g. --no-sandbox in Docker). render/serve/test use it.
- serve.mjs GET /health now probes WebGPU and reports { ok, gpu, harnessUrl }.
- headless/Dockerfile: Google Chrome (H.264/AAC) + Mesa lavapipe (software
  WebGPU), builds dist/, runs serve.mjs; mount the workspace read-only. The
  WebGPU-presence check fails loudly if effects can't render in-container.
- .dockerignore: exclude headless scratch + sample workspace from the context.

Note: the image build/run wasn't validated here (no Docker daemon in this
session); the non-Docker paths (render/edit/serve/test) are all verified.
Linux Chrome has no WebCodecs AAC encoder, so mp4/AAC output silently dropped
audio. The harness now checks AudioEncoder.isConfigSupported and returns the
issue in summary.warnings; the CLI prints it and the service sets an
X-Freecut-Warnings response header (ASCII-sanitized so a warning can never turn
a successful render into a 500). Opus (webm) and mp3 still carry audio.

Docker validated end to end: builds, /health reports gpu:true (software WebGPU
via lavapipe), and video / text / transition renders work; mp4 returns a valid
video with the AAC warning, vp9/webm carries Opus audio. Heavy GPU effects can
hit a Dawn device-loss under software WebGPU and surface as a clear error — use
a GPU host for those. README documents all of this.
Issue 1 (AAC dropped on Linux) — FIXED: register the @mediabunny/aac-encoder
WASM polyfill when no native AAC encoder exists, so mp4/AAC carries audio
everywhere (verified in-container: h264 + aac at -13dB). Uses mediabunny's
canEncodeAudio so the post-register check is accurate; warns only if a codec
still can't encode. Added the dep + vite optimizeDeps exclude.

Issue 2 (effects need a real GPU) — software WebGPU (lavapipe AND SwiftShader)
both hit a Dawn device-loss on the effects pipeline; not fixable via flags.
Resolution is to use a real GPU:
- Dockerfile no longer force-pins lavapipe, so on a Linux host with the NVIDIA
  Container Toolkit (`--gpus all -e NVIDIA_DRIVER_CAPABILITIES=all`) the GPU's
  Vulkan ICD is auto-discovered and effects use the real GPU; otherwise lavapipe
  provides software WebGPU (verified still gpu:true + renders).
- Docker Desktop on Windows/WSL2 exposes CUDA/NVENC but no Vulkan ICD, so
  containerized WebGPU is software-only there — run the service natively for GPU
  effects. chromeLaunchArgs() gained a FREECUT_CHROME_ARGS_REPLACE override.
README documents the native-vs-Linux-GPU-host guidance.
…are GPU

Docker is for deploying on a Linux host with an NVIDIA GPU (real Vulkan via the
Container Toolkit). Docker Desktop on Windows/WSL2 has no Vulkan ICD, so WebGPU
is software-only there — run natively for GPU effects. README repositioned
accordingly (Windows = native; Docker = Linux GPU server).

serve.mjs now probes the WebGPU adapter: logs it at startup, warns loudly when
it's software (no real GPU), and GET /health returns
{ gpu: { available, vendor, architecture }, software } so an operator can
confirm the real GPU is in use (e.g. nvidia/lovelace vs google/swiftshader).
Add a serial render queue to the in-app export pipeline so users can
line up multiple segments/jobs and a dedicated Exports dialog to manage
them and browse saved outputs.

- Queue store + serial runner (one job at a time — the WebGPU device is
  a tab-wide singleton, so concurrent renders would contend)
- Extract shared render-pipeline core (settings/codec resolution +
  worker/main-thread orchestration) reused by single export and queue
- Snapshot the timeline at enqueue so later edits don't change a queued job
- Segment generators: current range, one-per-marker, fixed-duration chunks
  (split within the active I/O range; one codec probe per batch)
- Outputs saved to <workspace>/exports/; Exports dialog tabs: queue +
  saved-files browser (download/delete)
- Persist the queue per project to projects/<id>/render-queue.json:
  restore on load, requeue in-flight jobs, debounced status-keyed saves,
  deduped snapshots, project-switch safe
- Toolbar queue button with active-count badge; i18n for all 9 languages
…diately

- On load, keep a restored queue with pending work PAUSED until the user
  resumes, so a refresh never silently kicks off renders (no pause when
  there's nothing queued, so jobs added after a refresh still run)
- Replace the 600ms debounced save with an immediate microtask-coalesced
  save so a job's terminal status is on disk before a refresh can lose it
- Save renders to the project's own projects/<id>/exports/ folder (was a
  shared top-level exports/); list/read/delete are now project-scoped
- Saved exports tab shows where files live (workspace folder + this
  project's exports folder) and notes the browser can't open the OS folder
  directly — use Download to get a copy (File System Access API has no
  reveal-in-folder capability)
- Thread projectId through the Exports dialog; i18n for all 9 languages
…level files

Save renders to projects/<id>/exports/ (grouped with the project, removed
with it). To avoid hiding files made before this change, listExportFiles
merges the project's folder with any loose files still in the top-level
exports/ folder; read/delete are addressed by full path so both work.
The feature is brand new, so there's nothing to migrate — list only the
project's own projects/<id>/exports/ folder instead of also surfacing loose
files from the top-level exports/ folder.
Drop the "browsers can't open it directly" caveat; just tell users the
files can be found in this project's local folder. Updated all 9 languages.
… exports

Extract parseArgs + chromeLaunchArgs into headless/lib/cli.mjs (the
hardcoded GPU_ARGS copies in probe/smoke/media-smoke were Windows-only;
they now get the platform-correct ANGLE backend + env override). edit.mjs
reuses render-core's startHarness (generalized for the media-less edit
path) instead of its own copy. Share collectAddClipMedia (workspace.mjs)
and seedMediaLibrary (src/headless/seed-media.ts). Remove unused
deleteRenderQueue + exportsDir exports; simplify abortJob to void.
Text rendered through three independent layout implementations — DOM/CSS at
rest, Canvas 2D for skim+export, and the GPU glyph atlas — plus a separate
auto-fit height calc. All three manual paths measured letter-spacing as
(n-1)·LS while CSS uses n·LS (trailing spacing after the last glyph), so
centered/letter-spaced titles shifted ~2px horizontally when skimming vs at
rest. The remaining vertical/stroke difference came from skim painting text on
a 2D canvas while at-rest used the DOM/CSS renderer.

- Add src/shared/typography/{text-style,text-measurer,text-block-layout} as the
  single source of style defaults + span cascade, CSS-equivalent measurement
  (native ctx.letterSpacing + fontKerning), and block geometry.
- Route the Canvas 2D renderer, GPU glyph atlas, DOM text-content, and the
  auto-fit height calc through it; canvas now draws one fillText/strokeText per
  line (no per-character path).
- Keep styled text (shadow/stroke) on the DOM Player during skim, like captions,
  so it uses the same renderer as at rest — no vertical/stroke/AA pop.

Export now matches the preview for letter-spaced text.
…ably

Audio and video are meant to live on separate tracks, but a video clip
could reach the timeline with un-split embedded audio (preview canvas
drop, or legacy/pre-existing projects), which then surfaced the video
track in the mixer with a fader/EQ strip.

- Mixer: isAudioMixerTrack() now excludes video tracks on principle,
  regardless of any un-split embedded audio.
- Canvas drop: a dropped video carrying audio splits onto a linked
  audio track via the new addItemWithLinkedAudio action, matching the
  timeline-drop behavior instead of bypassing the split.
- Load-time backfill: surgically split audible videos that lack a
  companion (root + compositions), preserving existing track names and
  order; runs alongside the legacy A/V layout repair.
- Add a reusable embedded-audio-split core (reuses makeGeneratedAudioItem
  / cloneVolumeKeyframes) plus tests.
Dragging the in/out handles now scrubs the preview to the boundary
frame so you can see exactly where you're trimming, then clears the
preview override on release.

- Source monitor: in/out handle drags and range slide set the preview
  source frame (out is exclusive, so it skims to out - 1).
- Timeline in/out markers + range drag: same skim-to-boundary behavior
  via the playback preview frame.
walterlow added 11 commits May 30, 2026 00:58
Re-importing a file removed from the project (but still in the workspace)
hit the dedup duplicate path, which only stripped the optimistic
placeholder — leaving the file invisible and showing a spurious
"already exists in library" banner.

- ensureImportedMediaVisible now guarantees the resolved record appears
  exactly once (covering fresh imports, dedup re-adds, and a concurrent
  loadMediaItems wiping the placeholder mid-import)
- the "already exists" banner now fires only when the file is already
  visibly in the library, not for by-design cross-workspace re-association
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 30, 2026

Too many files changed for review. (229 files found, 100 file limit)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
freecut Ready Ready Preview, Comment May 30, 2026 2:24pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Important

Review skipped

Too many files!

This PR contains 228 files, which is 78 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0912452a-758c-473c-9635-02fe77f51b5d

📥 Commits

Reviewing files that changed from the base of the PR and between 2f98baf and 3d344a7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (228)
  • .dockerignore
  • .gitignore
  • CHANGELOG.md
  • headless.html
  • headless/Dockerfile
  • headless/README.md
  • headless/edit.mjs
  • headless/lib/cli.mjs
  • headless/lib/render-core.mjs
  • headless/lib/workspace.mjs
  • headless/media-server.mjs
  • headless/media-smoke.mjs
  • headless/probe.mjs
  • headless/render.mjs
  • headless/serve.mjs
  • headless/server.mjs
  • headless/smoke.mjs
  • headless/test.mjs
  • package.json
  • src/app.tsx
  • src/app/debug/project-debug.ts
  • src/data/changelog.json
  • src/features/editor/components/audio-meter-panel.tsx
  • src/features/editor/components/audio-meter-utils.test.ts
  • src/features/editor/components/audio-meter-utils.ts
  • src/features/editor/components/editor.test.tsx
  • src/features/editor/components/editor.tsx
  • src/features/editor/components/media-sidebar-effects-tab.tsx
  • src/features/editor/components/media-sidebar.tsx
  • src/features/editor/components/preview-area.test.tsx
  • src/features/editor/components/preview-area.tsx
  • src/features/editor/components/properties-sidebar/clip-panel/index.tsx
  • src/features/editor/components/properties-sidebar/index.test.tsx
  • src/features/editor/components/properties-sidebar/index.tsx
  • src/features/editor/components/settings-dialog.tsx
  • src/features/editor/components/toolbar.tsx
  • src/features/editor/deps/export-contract.ts
  • src/features/editor/deps/media-library-contract.ts
  • src/features/editor/deps/preview-contract.ts
  • src/features/editor/deps/scene-browser-contract.ts
  • src/features/editor/deps/timeline-ui.ts
  • src/features/export/components/export-dialog.tsx
  • src/features/export/components/exports-dialog.tsx
  • src/features/export/components/render-queue-panel.tsx
  • src/features/export/components/render-queue-persistence.tsx
  • src/features/export/components/render-queue-runner.tsx
  • src/features/export/hooks/use-client-render.ts
  • src/features/export/hooks/use-render-queue-persistence.ts
  • src/features/export/hooks/use-render-queue-runner.ts
  • src/features/export/index.ts
  • src/features/export/stores/render-queue-store.ts
  • src/features/export/utils/build-render-job.ts
  • src/features/export/utils/canvas-item-renderer/text.ts
  • src/features/export/utils/canvas-item-renderer/video.ts
  • src/features/export/utils/canvas-pool.ts
  • src/features/export/utils/render-pipeline.ts
  • src/features/export/utils/render-queue-control.ts
  • src/features/keyframes/utils/effect-animatable-properties.ts
  • src/features/media-library/components/media-card.test.tsx
  • src/features/media-library/components/media-card.tsx
  • src/features/media-library/components/media-grid.tsx
  • src/features/media-library/components/media-library.tsx
  • src/features/media-library/components/media-picker-dialog.tsx
  • src/features/media-library/components/missing-media-dialog.tsx
  • src/features/media-library/contracts/timeline.ts
  • src/features/media-library/deps/analysis-contract.ts
  • src/features/media-library/deps/scene-browser-contract.ts
  • src/features/media-library/deps/storage.ts
  • src/features/media-library/deps/timeline-services.ts
  • src/features/media-library/index.ts
  • src/features/media-library/services/compound-clip-thumbnail-service.ts
  • src/features/media-library/services/media-analysis-service-loader.ts
  • src/features/media-library/services/media-analysis-service.ts
  • src/features/media-library/services/media-captioning-service.ts
  • src/features/media-library/services/media-library-service-loader.ts
  • src/features/media-library/services/media-library-service.generated-image.test.ts
  • src/features/media-library/services/media-library-service.test.ts
  • src/features/media-library/services/media-library-service.ts
  • src/features/media-library/services/media-processor-service.ts
  • src/features/media-library/services/media-transcription-service.ts
  • src/features/media-library/services/proxy-service.test.ts
  • src/features/media-library/services/proxy-service.ts
  • src/features/media-library/stores/media-delete-actions.ts
  • src/features/media-library/stores/media-import-actions.test.ts
  • src/features/media-library/stores/media-import-actions.ts
  • src/features/media-library/stores/media-library-store.ts
  • src/features/media-library/stores/media-preparation-store.ts
  • src/features/media-library/stores/media-relinking-actions.ts
  • src/features/media-library/utils/caption-items.ts
  • src/features/media-library/utils/media-resolver.ts
  • src/features/media-library/workers/media-processor.worker.ts
  • src/features/preview/components/inline-composition-preview.test.tsx
  • src/features/preview/components/inline-composition-preview.tsx
  • src/features/preview/components/playback-controls.test.tsx
  • src/features/preview/components/playback-controls.tsx
  • src/features/preview/components/source-monitor.tsx
  • src/features/preview/components/video-preview.sync.test.tsx
  • src/features/preview/deps/export-contract.ts
  • src/features/preview/deps/media-library-contract.ts
  • src/features/preview/hooks/use-canvas-media-drop.test.tsx
  • src/features/preview/hooks/use-canvas-media-drop.ts
  • src/features/preview/hooks/use-gpu-effects-overlay.ts
  • src/features/preview/hooks/use-preview-render-pump-controller.ts
  • src/features/preview/hooks/use-preview-renderer-controller.ts
  • src/features/preview/utils/media-resolver.test.ts
  • src/features/preview/utils/text-layout.test.ts
  • src/features/preview/utils/text-render-guard.test.ts
  • src/features/preview/utils/text-render-guard.ts
  • src/features/project-bundle/deps/media-library-contract.ts
  • src/features/project-bundle/services/bundle-export-service.ts
  • src/features/project-bundle/services/json-export-service.ts
  • src/features/projects/components/project-list.tsx
  • src/features/projects/deps/media-library-contract.ts
  • src/features/projects/stores/project-store.test.ts
  • src/features/projects/stores/project-store.ts
  • src/features/scene-browser/components/scene-browser-panel.tsx
  • src/features/scene-browser/deps/analysis-contract.ts
  • src/features/scene-browser/deps/media-library-contract.ts
  • src/features/scene-browser/utils/embeddings-cache.ts
  • src/features/scene-browser/utils/lazy-thumb.ts
  • src/features/settings/stores/settings-store.test.ts
  • src/features/settings/stores/settings-store.ts
  • src/features/timeline/components/clip-filmstrip/index.test.tsx
  • src/features/timeline/components/clip-filmstrip/index.tsx
  • src/features/timeline/components/clip-waveform/amplitude.test.ts
  • src/features/timeline/components/clip-waveform/amplitude.ts
  • src/features/timeline/components/clip-waveform/compound-clip-waveform.tsx
  • src/features/timeline/components/clip-waveform/index.tsx
  • src/features/timeline/components/keyframe-graph-panel.tsx
  • src/features/timeline/components/timeline-content.tsx
  • src/features/timeline/components/timeline-in-out-markers.tsx
  • src/features/timeline/components/timeline-item/clip-content.test.tsx
  • src/features/timeline/components/timeline-item/clip-content.tsx
  • src/features/timeline/components/timeline-item/use-caption-dialog-state.ts
  • src/features/timeline/components/timeline-item/use-timeline-item-actions.ts
  • src/features/timeline/components/timeline-markers.tsx
  • src/features/timeline/components/timeline-media-drop-zone.tsx
  • src/features/timeline/components/timeline-track.tsx
  • src/features/timeline/contracts/editor.ts
  • src/features/timeline/contracts/media-library.ts
  • src/features/timeline/deps/analysis.ts
  • src/features/timeline/deps/export-contract.ts
  • src/features/timeline/deps/keyframe-editors.ts
  • src/features/timeline/deps/keyframes-contract.ts
  • src/features/timeline/deps/media-library-resolver.ts
  • src/features/timeline/deps/media-library-service.ts
  • src/features/timeline/deps/media-transcription-service.ts
  • src/features/timeline/hooks/use-filmstrip.test.tsx
  • src/features/timeline/hooks/use-filmstrip.ts
  • src/features/timeline/hooks/use-visible-items.test.ts
  • src/features/timeline/hooks/use-visible-items.ts
  • src/features/timeline/hooks/use-waveform-prefetch.ts
  • src/features/timeline/hooks/use-waveform.ts
  • src/features/timeline/services/filmstrip-cache-config.ts
  • src/features/timeline/services/filmstrip-cache.test.ts
  • src/features/timeline/services/filmstrip-cache.ts
  • src/features/timeline/services/reverse-conform-service.test.ts
  • src/features/timeline/services/reverse-conform-service.ts
  • src/features/timeline/services/waveform-cache.test.ts
  • src/features/timeline/services/waveform-cache.ts
  • src/features/timeline/services/waveform-opfs-storage.ts
  • src/features/timeline/services/waveform-worker.ts
  • src/features/timeline/stores/actions/edit/freeze-frame-actions.ts
  • src/features/timeline/stores/actions/item-actions.ts
  • src/features/timeline/stores/actions/legacy-av-actions.ts
  • src/features/timeline/stores/actions/source-edit-actions.ts
  • src/features/timeline/stores/timeline-persistence.ts
  • src/features/timeline/stores/timeline-store-facade.in-out-points.test.ts
  • src/features/timeline/stores/timeline-store-facade.test.ts
  • src/features/timeline/stores/timeline-store-facade.ts
  • src/features/timeline/stores/timeline-viewport-store.ts
  • src/features/timeline/types.ts
  • src/features/timeline/utils/embedded-audio-split.test.ts
  • src/features/timeline/utils/embedded-audio-split.ts
  • src/features/timeline/utils/external-file-project-match.test.ts
  • src/features/timeline/utils/external-file-project-match.ts
  • src/features/timeline/utils/legacy-av-track-repair.ts
  • src/features/timeline/utils/media-validation.test.ts
  • src/features/timeline/utils/media-validation.ts
  • src/features/timeline/workers/filmstrip-extraction-worker.ts
  • src/features/workspace-gate/workspace-gate.tsx
  • src/headless/edit.ts
  • src/headless/main.ts
  • src/headless/seed-media.ts
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/partials/keyframes.json
  • src/i18n/locales/partials/media.json
  • src/i18n/locales/partials/render-queue.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh.json
  • src/infrastructure/browser/blob-url-manager.ts
  • src/infrastructure/gpu-text/glyph-atlas-text-pipeline.ts
  • src/infrastructure/storage/cache-version.ts
  • src/infrastructure/storage/index.ts
  • src/infrastructure/storage/workspace-fs/ai-outputs/types.ts
  • src/infrastructure/storage/workspace-fs/captions.ts
  • src/infrastructure/storage/workspace-fs/exports.ts
  • src/infrastructure/storage/workspace-fs/media.test.ts
  • src/infrastructure/storage/workspace-fs/media.ts
  • src/infrastructure/storage/workspace-fs/paths.ts
  • src/infrastructure/storage/workspace-fs/project-media.test.ts
  • src/infrastructure/storage/workspace-fs/project-media.ts
  • src/infrastructure/storage/workspace-fs/render-queue.ts
  • src/infrastructure/storage/workspace-fs/scenes.ts
  • src/infrastructure/thumbnails/sample-strategy.ts
  • src/main.tsx
  • src/routeTree.gen.ts
  • src/routes/editor/$projectId.tsx
  • src/routes/projects/index.lazy.tsx
  • src/routes/projects/index.tsx
  • src/routes/projects/new.lazy.tsx
  • src/routes/projects/new.tsx
  • src/runtime/composition-runtime/components/text-content.tsx
  • src/runtime/composition-runtime/utils/text-layout.ts
  • src/shared/logging/preview-trace.test.ts
  • src/shared/logging/preview-trace.ts
  • src/shared/typography/text-block-layout.consistency.test.ts
  • src/shared/typography/text-block-layout.ts
  • src/shared/typography/text-measurer.ts
  • src/shared/typography/text-style.ts
  • src/types/keyframe.ts
  • vite.config.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The "short cooldown after the overlap ends" sync test pinned its frame-61
assertion to a single captured mock renderer instance. Transition entry now
cycles through several short-lived renderer instances (the structure key churns
as the session settles), so the cooldown frame is drawn by a renderer created
after the captured handle — the frame still renders and displays correctly
(displayed=61, overlay visible), but the call landed on a different instance.

Assert the cooldown frame was rendered by some instance instead, keeping the
displayed-frame and visibility checks that capture the test's real contract.
Fixes the flaky/failing Preview Sync Stress quality check.
@walterlow walterlow merged commit 165e7ae into staging May 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant