diff --git a/.dockerignore b/.dockerignore index 2f39eb088..1952082f1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -30,6 +30,13 @@ coverage server/temp/media/* server/temp/output/* +# Headless scratch / outputs / sample workspace (rebuilt or not needed in image) +headless/output +headless/assets +tmp-workspace +tmp +*.mp4 + # Git .git .gitignore diff --git a/.gitignore b/.gitignore index 0c5e613dc..b97d90135 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,12 @@ crates/*/Cargo.lock tmp +# Headless render harness: generated outputs, test assets, scratch workspaces. +# (The harness source — headless/*.mjs, src/headless/ — is committed.) +headless/output/ +headless/assets/ +tmp-workspace/ + .vercel # FreeCut workspace artifacts, if the repo folder itself is picked as a diff --git a/CHANGELOG.md b/CHANGELOG.md index 591789f6b..aa8193659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,36 @@ All notable changes to FreeCut. Weekly CalVer: `YYYY.MM.DD` = the Monday of the -## [Current] — week of 2026-05-18 +## [Current] — week of 2026-05-25 + +### Added +- In-app render queue — line up several exports and they render one after another, surviving a page refresh +- One-click quality presets in the export dialog +- Exports now save to a per-project folder, with a notice showing where files land +- Automatic caption styling, with per-item progress in the AI panel + +### Fixed +- Waveforms render from true audio peaks and stay sharp when zoomed in +- Waveforms no longer flash a skeleton when moving a clip to another track +- Preview no longer jumps when entering pen/path edit mode +- Preview stays continuous through transitions during playback +- Remaining placeholder strings are now translated across all 9 languages + +### Improved +- Much smoother timeline zooming and scrolling, especially with many clips on screen +- The editor stays responsive while audio loads — decoding now runs in the background + +## [2026.05.18] — week of 2026-05-18 to 2026-05-24 + +### Added +- Hold (stepped) interpolation for keyframes +- Language switcher on the projects page, with more panels translated (text tools, transitions, scene browser) ### Fixed - Rotated videos display the correct orientation in skim preview and exports - Scrub overlay stays aligned and the skim indicator sits flush on clip edges +- Transitions on same-clip (A-A) splits now render correctly +- Splitting a reversed clip keeps both halves continuous ### Improved - Filmstrips and waveforms render smoother while zooming the timeline diff --git a/headless.html b/headless.html new file mode 100644 index 000000000..caa5d9ee5 --- /dev/null +++ b/headless.html @@ -0,0 +1,12 @@ + + + + + + FreeCut Headless Render Harness + + +
FreeCut headless render harness
+ + + diff --git a/headless/Dockerfile b/headless/Dockerfile new file mode 100644 index 000000000..6c34fb3a9 --- /dev/null +++ b/headless/Dockerfile @@ -0,0 +1,53 @@ +# FreeCut headless render service. +# +# Builds the harness (dist/) and runs serve.mjs with one warm headless Chrome. +# Google Chrome (not Chromium) is used so H.264/AAC (proprietary codecs) work; +# Mesa lavapipe provides software Vulkan so WebGPU effects can render without a +# physical GPU. For best performance / guaranteed effects, run on a GPU host +# with passthrough (see README) instead of software Vulkan. +# +# Build (context = repo root): +# docker build -f headless/Dockerfile -t freecut-headless . +# Run (mount your workspace read-only): +# docker run --rm -p 8787:8787 -v /path/to/FreeCutProjects:/workspace:ro freecut-headless +# curl localhost:8787/health # -> {"ok":true,"gpu":true,...} +FROM node:24-bookworm + +# Google Chrome (proprietary codecs) + software Vulkan (lavapipe) for WebGPU + fonts. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates wget gnupg \ + && wget -qO- https://dl.google.com/linux/linux_signing_key.pub \ + | gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg \ + && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \ + > /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + google-chrome-stable \ + mesa-vulkan-drivers libvulkan1 vulkan-tools \ + fonts-liberation fonts-noto-color-emoji \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Use the system Google Chrome; don't download Playwright's bundled Chromium. +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + +# Install deps (ignore lifecycle scripts: the prepare hook sets up git hooks, +# which aren't present/needed in the image). +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +# Build the harness (dist/headless.html + assets). +COPY . . +RUN npm run build + +# Vulkan ICDs are auto-discovered: on a Linux GPU host (run with +# `--gpus all -e NVIDIA_DRIVER_CAPABILITIES=all`) the NVIDIA Vulkan ICD is +# mounted and WebGPU uses the real GPU; otherwise the bundled Mesa lavapipe +# provides software WebGPU. --no-sandbox is required running Chrome as root. +# (To force software even on a GPU host: -e VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json) +ENV FREECUT_CHROME_ARGS="--no-sandbox" +ENV FREECUT_WORKSPACE=/workspace + +EXPOSE 8787 +CMD ["sh", "-c", "node headless/serve.mjs --workspace \"$FREECUT_WORKSPACE\" --port 8787"] diff --git a/headless/README.md b/headless/README.md new file mode 100644 index 000000000..618334cfb --- /dev/null +++ b/headless/README.md @@ -0,0 +1,242 @@ +# FreeCut Headless + +Render **and edit** FreeCut projects from the command line — no editor UI — by +driving the **real** engine and timeline action modules inside headless Chrome +via Playwright. + +Because the engine depends on browser APIs (WebCodecs, WebGPU, OffscreenCanvas, +OfflineAudioContext), a Node port would be a fragile rewrite. Instead, a tiny +Node driver launches headless Chrome, loads a UI-less harness page (`window.freecut`) +that reuses the exact export pipeline and Zustand timeline stores, and captures +the output. Fidelity matches the in-app export — including hardware GPU effects, +transitions, audio, and (for edits) transition repair + linked-clip cascades. + +Two CLIs: +- **`render.mjs`** (`npm run headless`) — render a project (or a slice) to video/audio. +- **`edit.mjs`** — apply structural edits (add/split/trim/move/delete/transition) and write the project back. + +## How it works + +``` +Node CLI (render.mjs) + ├─ reads the workspace folder on disk (project.json + media//) + ├─ serves the built harness (dist/) + media on one COEP-isolated origin, + │ with HTTP Range (server.mjs) [default — no dev server needed] + └─ launches headless Chrome (Playwright, channel: chrome) + └─ loads headless.html → src/headless/main.ts (window.freecut) + ├─ migrateProject + convertTimelineToComposition + ├─ registers media URLs (range-streamed via mediabunny UrlSource) + └─ renderComposition → Blob → download → saved by the driver +``` + +The browser harness lives in `src/headless/` (TypeScript, built by Vite). The +Node driver lives here in `headless/*.mjs` (plain ESM, run directly). + +Media is **range-streamed**, not downloaded: the harness registers each media +file's HTTP URL (no Blob), so mediabunny reads only the byte ranges it needs. +A 5-second slice of a 3 GB source renders without loading the whole file. + +## Prerequisites + +- Google Chrome installed (the driver uses `channel: 'chrome'`). +- `playwright` (already a devDependency). +- A built harness (`dist/`). Build it once: + + ```bash + npm run build # produces dist/headless.html (re-run after harness changes) + ``` + + The CLI serves `dist/` itself — **no dev server required**. (Or pass `--build` + to have the CLI build automatically when `dist/` is missing.) + +## Usage + +```bash +# List projects in a workspace folder +npm run headless -- --workspace "C:\path\to\workspace" --list + +# Render a project to MP4 (H.264 + AAC), using the project's resolution/fps +npm run headless -- --workspace "C:\path\to\workspace" --project \ + --out ./my-render.mp4 + +# Render only a slice (great for very long projects) +npm run headless -- --workspace "" --project --in 10 --duration 5 + +# Override codec / container / resolution / fps +npm run headless -- --workspace "" --project \ + --codec vp9 --container webm --resolution 1920x1080 --fps 30 --quality ultra + +# Audio only +npm run headless -- --workspace "" --project --audio-only --container mp3 +``` + +### Options + +| Flag | Default | Notes | +|------|---------|-------| +| `--workspace ` | (required) | The FreeCut workspace folder (picked in the app). | +| `--project ` | (required) | Project id under the workspace, or a path to a `project.json`. | +| `--out ` | `headless/output/.` | Output file. | +| `--codec ` | `h264` | `h264 \| h265 \| vp9 \| vp8 \| av1`. Falls back automatically if unsupported. | +| `--container ` | derived | `mp4 \| webm \| mov \| mkv` (or `mp3 \| wav \| m4a` with `--audio-only`). | +| `--resolution ` | project metadata | e.g. `1920x1080`. | +| `--fps ` | project metadata | | +| `--quality ` | `high` | `low \| medium \| high \| ultra` (controls bitrate). | +| `--in ` | 0 | Render range start (seconds). | +| `--out-sec ` | end | Render range end (seconds). | +| `--duration ` | — | Render this many seconds from `--in`. | +| `--audio-only` | off | Render audio only. | +| `--build` | off | Build `dist/` first if the harness isn't built. | +| `--head` | off | Run a visible browser for debugging. | +| `--harness-url ` | — | Dev mode: drive a running `npm run dev` server instead of `dist/`. | + +## Notes & limitations + +- **Media must be mirrored to the workspace folder on disk.** The CLI reads + `media//`. If a media source is missing (imported but never read in + the app), open the project in FreeCut once so it's mirrored, then re-run. +- **Codec support is verified at render time** and falls back the same way the + app does (e.g. H.264 → VP9 if unavailable). Headless Chrome here supports + H.264/HEVC/VP9/AV1 video and AAC/Opus audio with hardware WebGPU. +- **Audio codecs:** AAC/MP3/Opus/Vorbis/FLAC/PCM decode natively; **AC-3/E-AC-3 + (Dolby Digital / DD+) decode via `@mediabunny/ac3`** — the CLI passes each + media's `metadata.json` to the harness, which seeds the media-library store so + the codec is recognized and the AC-3 decoder is registered. Truly exotic + codecs (e.g. DTS) still can't be decoded headlessly; the CLI warns and that + audio is silent (video unaffected). Supporting those would need a Node-side + pre-decode (ffmpeg / `@mediabunny/server`) — not wired up since it needs a + heavy native dependency and is rarely needed. +- A harmless `Video load error` may log — that's the optional DOM `