Skip to content

feat: add ffmpeg-edit task for PATCH /api/content#128

Merged
sweetmantech merged 13 commits intomainfrom
feature/create-render-task
Apr 9, 2026
Merged

feat: add ffmpeg-edit task for PATCH /api/content#128
sweetmantech merged 13 commits intomainfrom
feature/create-render-task

Conversation

@sweetmantech
Copy link
Copy Markdown
Contributor

@sweetmantech sweetmantech commented Apr 9, 2026

Summary

  • Adds the ffmpeg-edit Trigger.dev task that the API's PATCH /api/content endpoint triggers
  • Accepts a video URL (required) and an array of edit operations
  • Processes operations sequentially via ffmpeg, uploads result to fal.ai storage

Context

Operations Supported

Type What it does FFmpeg filter
trim Cut a time range -ss / -t
crop Crop to aspect ratio or dimensions crop=
resize Scale to width/height scale=
overlay_text Add text overlay with color/position drawtext=

Files

File Purpose
src/schemas/ffmpegEditSchema.ts Zod schema (video_url required, 4 operation types)
src/tasks/ffmpegEditTask.ts Task: download → ffmpeg → upload
src/content/buildRenderFfmpegArgs.ts Compose ffmpeg args from operations
src/content/buildCropFilter.ts Aspect ratio and dimension crop logic
src/content/buildOverlayTextFilter.ts Text escaping, positioning, styling
src/content/downloadMediaToFile.ts Shared: fetch → write to disk
src/content/runFfmpeg.ts Shared: execFile with 10MB maxBuffer
src/content/uploadToFalStorage.ts Shared: read → upload to fal.ai
src/content/falServer.ts Pre-configured fal client

DRY with existing pipeline

Shared utilities extracted from renderFinalVideo used by both tasks:

  • downloadMediaToFile, runFfmpeg, uploadToFalStorage
  • escapeDrawtext, stripEmoji

Test plan

  • 316 total tests pass
  • Schema validation tests (video_url required, no mux_audio, color regex)
  • buildRenderFfmpegArgs tests (all 4 operations, crop math, malformed aspect)
  • runFfmpeg maxBuffer test
  • End-to-end tested on Trigger.dev dev environment

🤖 Generated with Claude Code

Adds the Trigger.dev task that the API's PATCH /api/content endpoint
triggers. Accepts video or audio input with an array of edit operations
(trim, crop, resize, overlay_text, mux_audio) and processes them
sequentially via ffmpeg. Uploads result to fal.ai storage.

- createRenderPayloadSchema: Zod schema matching the API's edit body
- createRenderTask: schemaTask with ffmpeg processing pipeline
- 12 tests covering schema validation and task configuration

All 296 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a modular FFmpeg-based video editing pipeline: builds ffmpeg args, runs ffmpeg, downloads/uploads media, initializes Fal client, and validates edit payloads with Zod. Introduces a Trigger.dev task that orchestrates these steps and returns uploaded output metadata.

Changes

Cohort / File(s) Summary
FFmpeg Argument Builder & Tests
src/content/buildRenderFfmpegArgs.ts, src/content/__tests__/buildRenderFfmpegArgs.test.ts
New exported function to construct ffmpeg CLI args from ordered operations (trim, crop, resize, overlay_text); includes emoji stripping, color/stroke escaping, filter chaining and tests validating filters, positions, chaining, trim args, and encoding flags.
FFmpeg Execution Wrapper & Tests
src/content/runFfmpeg.ts, src/content/__tests__/runFfmpeg.test.ts
Adds promisified execFile wrapper runFfmpeg(args) that invokes ffmpeg with a raised maxBuffer; test asserts configured maxBuffer size.
Media Download / Upload Helpers
src/content/downloadMediaToFile.ts, src/content/uploadToFalStorage.ts
Adds downloadMediaToFile(url, filePath) to fetch and write media, and uploadToFalStorage(filePath, filename, mimeType) to read file, upload via Fal client, and return {url, mimeType, sizeBytes} plus UploadResult type.
Fal Client Init
src/content/falServer.ts
Creates and exports a configured Fal client using process.env.FAL_KEY, throwing at module load if unset.
Render Orchestration / Integration
src/content/renderFinalVideo.ts
Refactors to delegate download, ffmpeg execution, and upload to the new helper modules; consumes upload result metadata (url, mimeType, sizeBytes).
Zod Schemas & Task Types
src/schemas/ffmpegEditSchema.ts, src/tasks/__tests__/ffmpegEditTask.test.ts
Adds Zod schemas for edit operations and payload (FfmpegEditPayload), with refinements and defaults; tests validate schema behavior, defaults, and invalid cases.
Trigger.dev Task & Tests
src/tasks/ffmpegEditTask.ts, src/tasks/__tests__/ffmpegEditTask.test.ts
New ffmpeg-edit schemaTask: downloads input, builds args, runs ffmpeg, uploads result, cleans temp files, and returns {status: "completed", url, mimeType, sizeBytes}; tests assert task metadata and schema parsing.

Sequence Diagram

sequenceDiagram
    participant Task as Trigger.dev Task
    participant Input as Remote URL
    participant FS as File System
    participant FFmpeg as FFmpeg
    participant Fal as Fal Storage

    Task->>Input: fetch media URL
    Input-->>Task: media data
    Task->>FS: write temp input file
    Task->>Task: build ffmpeg args (operations)
    Task->>FFmpeg: run ffmpeg with args (input -> output)
    FFmpeg->>FS: read input / write output
    FFmpeg-->>Task: return success
    Task->>Fal: upload output file
    Fal-->>Task: { url, mimeType, sizeBytes }
    Task->>FS: delete temp files
    Task-->>Caller: return { status, url, mimeType, sizeBytes }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through args and temp-file loam,
drew text and cropped each rambunctious frame,
ran ffmpeg, sent the bytes to Fal's home,
now URLs return where base64 once came.
Hooray — carrots, filters, and a tidy name! 🍃

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add ffmpeg-edit task for PATCH /api/content' accurately describes the primary change: adding a new Trigger.dev task that handles ffmpeg-based video editing operations for the PATCH /api/content endpoint.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/create-render-task

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/schemas/createRenderSchema.ts`:
- Around line 9-19: The crop/resize schemas allow invalid or no-op ops: tighten
the zod schemas in createRenderSchema.ts by (1) validating aspect (the aspect
field in the type: "crop" object) against an expected format (e.g. "W:H" numeric
pair) using a string refinement, (2) adding a refinement on the crop variant
(type: "crop") to require either a valid aspect OR both width and height present
and positive, and (3) adding a refinement on the resize variant (type: "resize")
to require at least one of width or height present and positive; update any
error messages to be clear so buildFfmpegArgs will never receive
missing/negative dimensions or produce scale=-1:-1.
- Around line 36-42: The createRenderPayloadSchema currently allows zero or both
of video_url and audio_url; update createRenderPayloadSchema to enforce exactly
one input URL by adding a schema-level refinement (e.g., using .refine or
.superRefine) that checks that exactly one of the two optional fields
(video_url, audio_url) is present and non-empty, and return a clear validation
error message when the check fails; keep the existing field validators for
video_url and audio_url and reference createRenderPayloadSchema, video_url, and
audio_url when making the change.

In `@src/tasks/__tests__/createRenderTask.test.ts`:
- Around line 111-118: The test name "rejects empty operations array" mismatches
the assertion for createRenderPayloadSchema.safeParse
(expect(result.success).toBe(true)); either rename the test to reflect that an
empty operations array is valid (e.g., "accepts empty operations array") or
invert the assertion to expect(false) so it truly asserts rejection; update the
spec title referencing createRenderPayloadSchema and the test block name passed
to it() accordingly to keep behavior and intent aligned.

In `@src/tasks/createRenderTask.ts`:
- Around line 133-145: The overlay_text case in createRenderTask.ts builds the
drawtext filter but ignores the op.font field declared in the schema
(createRenderSchema.ts); update the filter construction in the "overlay_text"
branch to include the font by adding a fontfile=${op.font} entry (or
conditionally include it only when op.font is provided) to the filter array so
the drawtext filter uses the requested font; ensure you reference op.font (and
escape it if needed) and keep the rest of the drawtext entries intact.
- Around line 148-166: In the mux_audio case fix the FFmpeg argument ordering
and mapping: ensure all input flags added to extraInputs (including
op.audio_url) are pushed into args before output-side options like -vf and
mappings (reorder where args.push(...extraInputs) occurs relative to
args.push("-vf", ...)); and when op.replace is false, change the amix usage to
produce a labeled filter output and map that label (e.g., build a filter like
combining [0:a][1:a]amix=inputs=2 into a label and then push a -map for that
label) so audioMapping maps the mixed output rather than the original tracks
(update how audioMapping is constructed in the mux_audio branch accordingly,
referencing mux_audio case, extraInputs, audioMapping, videoFilters and args).
- Around line 44-45: The task builds a video-centric pipeline unconditionally
(uses inputPath/outputPath, maps 0:v:0, forces -c:v libx264/-pix_fmt/+faststart
and returns video/*), which breaks audio-only payloads (payload.audio_url);
update createRenderTask to detect audio-only payloads (presence of
payload.audio_url and absence of video sources) and branch: when audio-only, set
outputPath/extension and MIME to an audio type, remove video mapping and video
codec/pix_fmt/faststart options, map the audio stream(s) only (e.g., 0:a:0), and
set appropriate ffmpeg args for audio encoding; when video is present, keep the
existing video pipeline behavior. Ensure any metadata returned reflects audio
MIME/type when audio-only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be9df658-83bd-4a0b-a72c-972c35e2e363

📥 Commits

Reviewing files that changed from the base of the PR and between 8b3958f and 80f248c.

📒 Files selected for processing (3)
  • src/schemas/createRenderSchema.ts
  • src/tasks/__tests__/createRenderTask.test.ts
  • src/tasks/createRenderTask.ts

Comment on lines +9 to +19
z.object({
type: z.literal("crop"),
aspect: z.string().optional(),
width: z.number().int().positive().optional(),
height: z.number().int().positive().optional(),
}),
z.object({
type: z.literal("resize"),
width: z.number().int().positive().optional(),
height: z.number().int().positive().optional(),
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject incomplete crop/resize operations up front.

aspect accepts any string, crop can arrive with no usable dimensions, and resize allows both dimensions to be omitted. Those payloads either no-op or generate invalid filters like scale=-1:-1 in buildFfmpegArgs. Tighten this with schema refinements: validate aspect format, require aspect or both crop dimensions, and require at least one resize dimension.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/createRenderSchema.ts` around lines 9 - 19, The crop/resize
schemas allow invalid or no-op ops: tighten the zod schemas in
createRenderSchema.ts by (1) validating aspect (the aspect field in the type:
"crop" object) against an expected format (e.g. "W:H" numeric pair) using a
string refinement, (2) adding a refinement on the crop variant (type: "crop") to
require either a valid aspect OR both width and height present and positive, and
(3) adding a refinement on the resize variant (type: "resize") to require at
least one of width or height present and positive; update any error messages to
be clear so buildFfmpegArgs will never receive missing/negative dimensions or
produce scale=-1:-1.

Comment on lines +133 to +145
case "overlay_text": {
const escaped = op.content.replace(/'/g, "'\\''").replace(/:/g, "\\:");
const filter = [
`drawtext=text='${escaped}'`,
`fontsize=${op.max_font_size}`,
`fontcolor=${op.color}`,
`borderw=${Math.max(2, Math.round(op.max_font_size / 14))}`,
`bordercolor=${op.stroke_color}`,
"x=(w-tw)/2",
op.position === "top" ? "y=180" : op.position === "center" ? "y=(h-th)/2" : `y=h-th-120`,
].join(":");
videoFilters.push(filter);
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the createRenderTask.ts file
fd -t f "createRenderTask.ts"

Repository: recoupable/tasks

Length of output: 89


🏁 Script executed:

# Search for createRenderSchema definition
rg "createRenderSchema" --type ts -B 2 -A 20

Repository: recoupable/tasks

Length of output: 2982


🏁 Script executed:

# Look for font-related code in createRenderTask.ts
rg "font" "src/tasks/createRenderTask.ts" -B 2 -A 2

Repository: recoupable/tasks

Length of output: 340


🏁 Script executed:

# Find and read the createRenderSchema file
fd -t f "createRenderSchema*" src/

Repository: recoupable/tasks

Length of output: 93


🏁 Script executed:

# Read the createRenderSchema file
cat -n "src/schemas/createRenderSchema.ts"

Repository: recoupable/tasks

Length of output: 1820


🏁 Script executed:

# Read more context from createRenderTask.ts around the overlay_text case
sed -n '120,160p' "src/tasks/createRenderTask.ts"

Repository: recoupable/tasks

Length of output: 1358


Wire the font field into the drawtext filter or remove it from the schema.

The overlay_text operation exposes a font option in the schema (line 23 of createRenderSchema.ts), but the drawtext filter built in the implementation never includes it. Callers can send a font and get silently default-styled output. Either add fontfile=${op.font} to the filter array or remove the field from the public schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tasks/createRenderTask.ts` around lines 133 - 145, The overlay_text case
in createRenderTask.ts builds the drawtext filter but ignores the op.font field
declared in the schema (createRenderSchema.ts); update the filter construction
in the "overlay_text" branch to include the font by adding a fontfile=${op.font}
entry (or conditionally include it only when op.font is provided) to the filter
array so the drawtext filter uses the requested font; ensure you reference
op.font (and escape it if needed) and keep the rest of the drawtext entries
intact.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/tasks/createRenderTask.ts">

<violation number="1" location="src/tasks/createRenderTask.ts:96">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

The `buildFfmpegArgs` function contains the core logic of this PR (5 operation types, ~60 lines of ffmpeg argument construction including aspect-ratio math, text escaping, and audio mapping) but is not exported and has zero test coverage. All 12 tests only check schema parsing and static task config properties — none exercise the actual behavior this PR adds. Export this function and add unit tests for each operation type, especially the crop aspect-ratio branch, the `overlay_text` escaping, and the `mux_audio` replace vs. mix paths.</violation>

<violation number="2" location="src/tasks/createRenderTask.ts:153">
P1: The `mux_audio` with `replace: false` generates incorrect ffmpeg arguments. The `amix` filter output is never mapped — the subsequent `-map 0:a -map 1:a` references the original unmixed streams, so the audio mixing never actually happens. Also, if any video filters are present, the combination of `-vf` and `-filter_complex` will cause ffmpeg to error.

The correct approach labels the filter inputs/outputs and maps only the mixed result.</violation>
</file>

<file name="src/schemas/createRenderSchema.ts">

<violation number="1" location="src/schemas/createRenderSchema.ts:3">
P0: `z.discriminatedUnion()` does not exist in Zod v4 (this project uses zod@4.3.6). This will throw a `TypeError` at runtime. In Zod v4, use `z.union()` instead — it automatically detects discriminated unions by shared literal fields.</violation>

<violation number="2" location="src/schemas/createRenderSchema.ts:11">
P2: All fields in the `crop` operation (`aspect`, `width`, `height`) are optional, so `{ type: "crop" }` passes validation despite being semantically invalid — ffmpeg's `crop=` filter needs at least a dimension or aspect ratio. Consider adding a `.refine()` to require at least one of these fields. The same issue applies to the `resize` operation below.</violation>
</file>

<file name="src/tasks/__tests__/createRenderTask.test.ts">

<violation number="1" location="src/tasks/__tests__/createRenderTask.test.ts:111">
P2: Test name says "rejects empty operations array" but the assertion expects `result.success` to be `true`. Rename to accurately describe the behavior, e.g. `"accepts empty operations array"`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

sweetmantech and others added 2 commits April 9, 2026 16:28
…nd stripEmoji

- Extract ffmpeg arg builder from task into content/buildRenderFfmpegArgs.ts (SRP)
- Reuse escapeDrawtext for text escaping (was reimplemented inline)
- Reuse stripEmoji for cleaning overlay text (was missing)
- Task file now only handles download → delegate → upload (no ffmpeg logic)
- 12 new tests for buildRenderFfmpegArgs covering all operation types
- Zero changes to existing pipeline code

All 308 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract 3 shared utilities from renderFinalVideo so both the existing
content pipeline and the new create-render task use the same code:

- downloadMediaToFile(url, path) — fetch + write to disk
- runFfmpeg(args) — execFile wrapper
- uploadToFalStorage(path, name, mime) — read + upload to fal.ai

renderFinalVideo now composes these instead of inlining the logic.
createRenderTask uses the same primitives.

Existing pipeline behavior is unchanged — same inputs, same outputs.
All 308 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/content/buildRenderFfmpegArgs.ts">

<violation number="1" location="src/content/buildRenderFfmpegArgs.ts:51">
P2: When both `width` and `height` are omitted (both are optional in the schema), this produces `scale=-1:-1` which is invalid — ffmpeg requires at least one concrete dimension when using `-1`. Add a guard or add a `.refine()` to the schema requiring at least one dimension.</violation>

<violation number="2" location="src/content/buildRenderFfmpegArgs.ts:67">
P2: The `color` and `stroke_color` values are interpolated directly into the colon-delimited drawtext filter string without sanitization. A value containing `:` (e.g., `"white:enable=0"`) would inject additional ffmpeg drawtext parameters. Either validate color format in the schema (e.g., with a regex) or escape colons in these values.</violation>
</file>

<file name="src/tasks/createRenderTask.ts">

<violation number="1" location="src/tasks/createRenderTask.ts:11">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

Unused imports `escapeDrawtext` and `stripEmoji` — these are only needed inside `buildRenderFfmpegArgs`, which already imports them. Leftover dead imports from the extraction refactor.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/content/__tests__/buildRenderFfmpegArgs.test.ts (1)

113-144: Add a regression test for replace: false + filter/input ordering.

Line 113 onward only asserts replace: true. Please add a case that asserts replace: false produces a valid mixed-audio mapping, and a case combining video filters + mux_audio that verifies all -i inputs appear before output options (like -vf), so ffmpeg argument-order regressions are caught early.

✅ Suggested test additions
+  it("builds mux_audio with replace=false using mixed output mapping", () => {
+    const args = buildRenderFfmpegArgs("in.mp4", "out.mp4", [
+      { type: "mux_audio", audio_url: "https://example.com/song.mp3", replace: false },
+    ]);
+    expect(args).toContain("-filter_complex");
+    const fcIndex = args.indexOf("-filter_complex");
+    expect(args[fcIndex + 1]).toContain("amix=inputs=2");
+    const mapIndices = args.reduce((acc: number[], v, i) => (v === "-map" ? [...acc, i] : acc), []);
+    expect(mapIndices.length).toBeGreaterThanOrEqual(2);
+  });
+
+  it("places extra -i inputs before output options when filters are present", () => {
+    const args = buildRenderFfmpegArgs("in.mp4", "out.mp4", [
+      { type: "crop", aspect: "9:16" },
+      { type: "mux_audio", audio_url: "https://example.com/song.mp3", replace: true },
+    ]);
+    const lastInputIndex = args.lastIndexOf("-i");
+    const vfIndex = args.indexOf("-vf");
+    expect(lastInputIndex).toBeGreaterThan(-1);
+    expect(vfIndex).toBeGreaterThan(-1);
+    expect(lastInputIndex).toBeLessThan(vfIndex);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/__tests__/buildRenderFfmpegArgs.test.ts` around lines 113 - 144,
Add two assertions to the buildRenderFfmpegArgs tests: first, add a case calling
buildRenderFfmpegArgs with a mux_audio operation where replace: false and assert
the mixed-audio mapping is produced (i.e., the original audio input remains
mapped and the new audio is appended, checking the positions of "-map" entries
similar to the existing replace=true test but expecting original audio map like
"0:a:0" and new audio like "1:a:0"); second, add a combined test that includes
video filters (e.g., crop and overlay_text) plus a mux_audio and assert that all
input URLs/“-i” entries (the audio input string) appear in args before output
options like "-vf" (use args.indexOf to compare positions for the audio input
and "-vf"), ensuring ffmpeg input ordering is preserved when replace is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/content/buildRenderFfmpegArgs.ts`:
- Around line 77-94: The mux_audio branch uses incorrect amix syntax and places
-vf before all inputs; update the "mux_audio" case in buildRenderFfmpegArgs.ts
so that when op.replace is false you build a labeled filter_complex like
"[0:a][1:a]amix=inputs=2[aout]" and map the mixed audio with "-map", "[aout]"
(instead of mapping "0:a" and "1:a" directly), while keeping the replace=true
behavior mapping video and the single audio input ("-map", "0:v:0", "-map",
"1:a:0"). Also change argument ordering so extraInputs (the "-i", op.audio_url"
entries) are added before applying output options like "-vf" (move the
videoFilters/args.push("-vf", ...) after you push extraInputs and audioMapping)
to ensure FFmpeg input-bound options are correctly ordered.
- Around line 39-44: The current crop logic in the "crop" case uses op.aspect to
build a crop string that can produce dimensions larger than the source (e.g.,
`crop=iw:iw*${h}/${w}`) and fail; update the logic in buildRenderFfmpegArgs so
that after parsing op.aspect into w and h you compute the targetAspect = w/h and
sourceAspect = sourceWidth/sourceHeight (or compare using iw and ih
algebraically) and then choose the crop formula based on whether sourceAspect >
targetAspect or not: if source is wider than target, crop width to match target
using width = ih * targetAspect and height = ih (crop by width); otherwise crop
height using width = iw and height = iw / targetAspect (crop by height); push
the resulting safe crop filter string to videoFilters and ensure you only push
when w and h are valid numbers.

---

Nitpick comments:
In `@src/content/__tests__/buildRenderFfmpegArgs.test.ts`:
- Around line 113-144: Add two assertions to the buildRenderFfmpegArgs tests:
first, add a case calling buildRenderFfmpegArgs with a mux_audio operation where
replace: false and assert the mixed-audio mapping is produced (i.e., the
original audio input remains mapped and the new audio is appended, checking the
positions of "-map" entries similar to the existing replace=true test but
expecting original audio map like "0:a:0" and new audio like "1:a:0"); second,
add a combined test that includes video filters (e.g., crop and overlay_text)
plus a mux_audio and assert that all input URLs/“-i” entries (the audio input
string) appear in args before output options like "-vf" (use args.indexOf to
compare positions for the audio input and "-vf"), ensuring ffmpeg input ordering
is preserved when replace is false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bde0407-01e9-4f7e-bf20-1be2ffe60365

📥 Commits

Reviewing files that changed from the base of the PR and between 80f248c and 693753b.

📒 Files selected for processing (3)
  • src/content/__tests__/buildRenderFfmpegArgs.test.ts
  • src/content/buildRenderFfmpegArgs.ts
  • src/tasks/createRenderTask.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tasks/createRenderTask.ts

downloadMediaToFile now delegates to the existing downloadImageBuffer
for fetch + error handling, instead of reimplementing the same logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

const falKey = process.env.FAL_KEY;
if (!falKey) throw new Error("FAL_KEY environment variable is required");
fal.config({ credentials: falKey });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a server config file for fal similar to what we have in the API codebase.
https://github.com/recoupable/api/pull/390/changes#diff-44a25f14cfa1d5d3f40a563c2a803ed9bb6f4ae1f6cbde820fd4da6500ee3ed9

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/content/runFfmpeg.ts">

<violation number="1" location="src/content/runFfmpeg.ts:13">
P1: ffmpeg stderr output can exceed the default 1 MB `maxBuffer`, killing the process with `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`. Increase the limit to accommodate verbose ffmpeg output on long or complex renders.</violation>
</file>

<file name="src/content/downloadMediaToFile.ts">

<violation number="1" location="src/content/downloadMediaToFile.ts:12">
P2: Downloading large video/audio files entirely into an in-memory `Buffer` before writing to disk risks high memory usage or OOM in the task worker. Consider streaming the response body directly to the file using `node:stream/promises` `pipeline` with `fs.createWriteStream`, which avoids holding the full file in memory.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

* @param filePath - Local path to write the downloaded file.
*/
export async function downloadMediaToFile(url: string, filePath: string): Promise<void> {
const { buffer } = await downloadImageBuffer(url);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Downloading large video/audio files entirely into an in-memory Buffer before writing to disk risks high memory usage or OOM in the task worker. Consider streaming the response body directly to the file using node:stream/promises pipeline with fs.createWriteStream, which avoids holding the full file in memory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/content/downloadMediaToFile.ts, line 12:

<comment>Downloading large video/audio files entirely into an in-memory `Buffer` before writing to disk risks high memory usage or OOM in the task worker. Consider streaming the response body directly to the file using `node:stream/promises` `pipeline` with `fs.createWriteStream`, which avoids holding the full file in memory.</comment>

<file context>
@@ -0,0 +1,14 @@
+ * @param filePath - Local path to write the downloaded file.
+ */
+export async function downloadMediaToFile(url: string, filePath: string): Promise<void> {
+  const { buffer } = await downloadImageBuffer(url);
+  await writeFile(filePath, buffer);
+}
</file context>
Fix with Cubic

1. Replace z.discriminatedUnion with z.union (Zod v4 compatibility)
2. Add .refine() to crop/resize requiring at least one dimension
3. Validate color values with regex (prevent ffmpeg injection)
4. Fix mux_audio replace:false — use labeled amix filter outputs
5. Add falServer.ts config (matches API's lib/fal/server.ts pattern)
6. Remove unused imports from createRenderTask
7. Fix test name "rejects" → "accepts" for empty operations
8. Add tests for crop/resize validation and mux_audio mixing
9. uploadToFalStorage uses falServer instead of raw @fal-ai/client

All 312 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/schemas/createRenderSchema.ts">

<violation number="1" location="src/schemas/createRenderSchema.ts:28">
P2: Color regex accepts invalid 5- and 7-digit hex values (e.g. `#12345`, `#1234567`). Restrict to the four valid CSS hex lengths: 3, 4, 6, or 8 digits.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

sweetmantech and others added 4 commits April 9, 2026 16:50
Template operations are incomplete by design — overlay_text has styling
but no content (filled dynamically), mux_audio has replace flag but no
audio_url (uses top-level audio_url from request).

Changes:
- Make overlay_text.content optional in schema, skip if missing
- Make mux_audio.audio_url optional in schema, fall back to payload audio_url
- buildRenderFfmpegArgs accepts fallbackAudioUrl parameter
- 3 new tests for template operation handling

All 315 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extra -i inputs (mux_audio URL) must come before -vf filters,
otherwise ffmpeg tries to apply the video filter to the audio input.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug: when crop + mux_audio were combined, -vf was placed before the
audio -i input, causing ffmpeg to error with "Option vf cannot be
applied to input url".

Test reproduces the exact failure from Trigger.dev run
run_cmns08hb901730hk6s9vskc4a.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
src/schemas/createRenderSchema.ts (1)

40-46: ⚠️ Potential issue | 🟠 Major

Schema allows both or neither input URLs.

The schema accepts payloads with both video_url and audio_url (silently preferring video_url at runtime) or neither (causing a runtime error). Adding a refinement ensures validation fails early with a clear message.

🛡️ Proposed fix: enforce exactly one input URL
-export const createRenderPayloadSchema = z.object({
+export const createRenderPayloadSchema = z.object({
   accountId: z.string().min(1, "accountId is required"),
   video_url: z.string().url().optional(),
   audio_url: z.string().url().optional(),
   operations: z.array(editOperationSchema),
   output_format: z.enum(["mp4", "webm", "mov"]).default("mp4"),
-});
+}).refine(
+  ({ video_url, audio_url }) => Boolean(video_url) !== Boolean(audio_url),
+  { message: "Provide exactly one of video_url or audio_url", path: ["video_url"] }
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/createRenderSchema.ts` around lines 40 - 46, The
createRenderPayloadSchema currently allows both or neither of video_url and
audio_url; add a zod refinement on createRenderPayloadSchema to require exactly
one of video_url or audio_url is present and non-empty so validation fails early
with a clear message. Locate createRenderPayloadSchema and attach a .refine(...)
that checks (Boolean(video_url) !== Boolean(audio_url)) and returns a custom
error message like "exactly one of video_url or audio_url must be provided" so
payloads with both or neither are rejected.
src/content/buildRenderFfmpegArgs.ts (2)

93-101: ⚠️ Potential issue | 🟠 Major

FFmpeg argument ordering: -vf placed before audio input -i.

Output options like -vf must come after all input declarations. Currently, -vf is pushed at line 94, then extraInputs (containing -i audioUrl) is pushed at line 97. This places -vf before the second -i, which can cause ffmpeg errors or unexpected behavior.

🐛 Proposed fix: reorder to place inputs before output options
-  if (videoFilters.length > 0) {
-    args.push("-vf", videoFilters.join(","));
-  }
-
   args.push(...extraInputs);
 
+  if (videoFilters.length > 0) {
+    args.push("-vf", videoFilters.join(","));
+  }
+
   if (audioMapping.length > 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/buildRenderFfmpegArgs.ts` around lines 93 - 101, The ffmpeg "-vf"
output option is being pushed before additional input declarations, which can
break ffmpeg; in the buildRenderFfmpegArgs flow move the blocks that push
extraInputs and audioMapping (args.push(...extraInputs) and
args.push(...audioMapping)) so they occur before the videoFilters handling, and
then push the "-vf" (videoFilters.join(",")) after those input-related pushes;
update the code around the videoFilters / extraInputs / audioMapping logic
(references: videoFilters, extraInputs, audioMapping, args) so all "-i" inputs
are added to args prior to any output options like "-vf".

40-48: ⚠️ Potential issue | 🔴 Critical

Crop aspect formula can produce dimensions exceeding source bounds.

The formula on line 44 doesn't account for source aspect ratio. For example, a 1920×1080 landscape source with a 9:16 portrait aspect produces crop=1920:3413 (since 1920×16/9≈3413), which exceeds the 1080 source height and causes ffmpeg to fail.

The fix must condition on whether the source is wider or taller than the target aspect:

🐛 Proposed fix using ffmpeg conditional expressions
       case "crop":
         if (op.aspect) {
           const [w, h] = op.aspect.split(":").map(Number);
           if (w && h) {
-            videoFilters.push(w > h ? `crop=ih*${w}/${h}:ih` : `crop=iw:iw*${h}/${w}`);
+            const ratio = `${w}/${h}`;
+            // Use conditional: if source is wider than target, limit by height; else limit by width
+            videoFilters.push(
+              `crop=w='if(gte(iw/ih,${ratio}),ih*${ratio},iw)':h='if(gte(iw/ih,${ratio}),ih,iw/(${ratio}))'`
+            );
           }
         } else if (op.width || op.height) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/buildRenderFfmpegArgs.ts` around lines 40 - 48, The crop aspect
handling in buildRenderFfmpegArgs (inside the "crop" case where op.aspect is
parsed into [w,h] and videoFilters is pushed) can produce crop dimensions that
exceed the source; replace the current w>h ternary expression with a conditional
that compares the source aspect (iw/ih) to the target aspect (w/h) and selects a
crop that is bounded by the source dimensions (e.g., use an ffmpeg conditional
expression or calculate width/height so neither exceeds iw or ih). Ensure you
still parse op.aspect into w and h, but build the crop filter using a
conditional based on iw/ih vs w/h so the resulting crop width and height never
exceed the source frame.
🧹 Nitpick comments (2)
src/schemas/createRenderSchema.ts (1)

10-11: Consider validating aspect format.

The aspect field accepts any string, but buildRenderFfmpegArgs expects the "W:H" format (e.g., "9:16"). Invalid formats like "invalid" or "16-9" would cause silent failures or NaN in the ffmpeg filter.

♻️ Proposed: add regex validation
   z.object({
     type: z.literal("crop"),
-    aspect: z.string().optional(),
+    aspect: z.string().regex(/^\d+:\d+$/, "aspect must be in W:H format (e.g., '9:16')").optional(),
     width: z.number().int().positive().optional(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/createRenderSchema.ts` around lines 10 - 11, The aspect field in
the createRenderSchema (where type: z.literal("crop") and aspect:
z.string().optional()) currently allows any string but buildRenderFfmpegArgs
expects "W:H" (e.g., "9:16"); update the schema to validate aspect with a regex
that enforces two positive integers separated by a colon (e.g., /^\d+:\d+$/) and
keep it optional (use aspect: z.string().regex(...).optional()) so invalid
values like "invalid" or "16-9" are rejected at validation time.
src/content/runFfmpeg.ts (1)

12-14: Consider adding timeout and buffer configuration for robustness.

execFile uses a default maxBuffer of ~1MB, which verbose ffmpeg output can exceed, causing ENOBUFS errors. Long-running renders also risk hanging indefinitely. These are optional improvements for production reliability.

♻️ Optional: Add timeout and increased buffer
 export async function runFfmpeg(args: string[]): Promise<void> {
-  await execFileAsync("ffmpeg", args);
+  await execFileAsync("ffmpeg", args, {
+    maxBuffer: 10 * 1024 * 1024, // 10MB for verbose output
+    timeout: 10 * 60 * 1000,     // 10 min safety timeout
+  });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/runFfmpeg.ts` around lines 12 - 14, The runFfmpeg function can
overflow the default execFile buffer or hang; update runFfmpeg to accept
optional exec options (e.g., timeout and maxBuffer) and pass them into
execFileAsync so callers can increase maxBuffer (e.g., 10MB+) and set a sensible
timeout. Specifically, modify the runFfmpeg signature (runFfmpeg) to accept an
optional options parameter and forward that object to execFileAsync("ffmpeg",
args, options) so you can configure timeout and maxBuffer when invoking FFmpeg.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/content/buildRenderFfmpegArgs.ts`:
- Around line 93-101: The ffmpeg "-vf" output option is being pushed before
additional input declarations, which can break ffmpeg; in the
buildRenderFfmpegArgs flow move the blocks that push extraInputs and
audioMapping (args.push(...extraInputs) and args.push(...audioMapping)) so they
occur before the videoFilters handling, and then push the "-vf"
(videoFilters.join(",")) after those input-related pushes; update the code
around the videoFilters / extraInputs / audioMapping logic (references:
videoFilters, extraInputs, audioMapping, args) so all "-i" inputs are added to
args prior to any output options like "-vf".
- Around line 40-48: The crop aspect handling in buildRenderFfmpegArgs (inside
the "crop" case where op.aspect is parsed into [w,h] and videoFilters is pushed)
can produce crop dimensions that exceed the source; replace the current w>h
ternary expression with a conditional that compares the source aspect (iw/ih) to
the target aspect (w/h) and selects a crop that is bounded by the source
dimensions (e.g., use an ffmpeg conditional expression or calculate width/height
so neither exceeds iw or ih). Ensure you still parse op.aspect into w and h, but
build the crop filter using a conditional based on iw/ih vs w/h so the resulting
crop width and height never exceed the source frame.

In `@src/schemas/createRenderSchema.ts`:
- Around line 40-46: The createRenderPayloadSchema currently allows both or
neither of video_url and audio_url; add a zod refinement on
createRenderPayloadSchema to require exactly one of video_url or audio_url is
present and non-empty so validation fails early with a clear message. Locate
createRenderPayloadSchema and attach a .refine(...) that checks
(Boolean(video_url) !== Boolean(audio_url)) and returns a custom error message
like "exactly one of video_url or audio_url must be provided" so payloads with
both or neither are rejected.

---

Nitpick comments:
In `@src/content/runFfmpeg.ts`:
- Around line 12-14: The runFfmpeg function can overflow the default execFile
buffer or hang; update runFfmpeg to accept optional exec options (e.g., timeout
and maxBuffer) and pass them into execFileAsync so callers can increase
maxBuffer (e.g., 10MB+) and set a sensible timeout. Specifically, modify the
runFfmpeg signature (runFfmpeg) to accept an optional options parameter and
forward that object to execFileAsync("ffmpeg", args, options) so you can
configure timeout and maxBuffer when invoking FFmpeg.

In `@src/schemas/createRenderSchema.ts`:
- Around line 10-11: The aspect field in the createRenderSchema (where type:
z.literal("crop") and aspect: z.string().optional()) currently allows any string
but buildRenderFfmpegArgs expects "W:H" (e.g., "9:16"); update the schema to
validate aspect with a regex that enforces two positive integers separated by a
colon (e.g., /^\d+:\d+$/) and keep it optional (use aspect:
z.string().regex(...).optional()) so invalid values like "invalid" or "16-9" are
rejected at validation time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc7db496-b7be-49b6-ad93-2d1c315a5fb4

📥 Commits

Reviewing files that changed from the base of the PR and between 693753b and 2a57c33.

📒 Files selected for processing (10)
  • src/content/__tests__/buildRenderFfmpegArgs.test.ts
  • src/content/buildRenderFfmpegArgs.ts
  • src/content/downloadMediaToFile.ts
  • src/content/falServer.ts
  • src/content/renderFinalVideo.ts
  • src/content/runFfmpeg.ts
  • src/content/uploadToFalStorage.ts
  • src/schemas/createRenderSchema.ts
  • src/tasks/__tests__/createRenderTask.test.ts
  • src/tasks/createRenderTask.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tasks/createRenderTask.ts
  • src/content/tests/buildRenderFfmpegArgs.test.ts

Bug: when only audio_url is provided (no video_url), the input is an
audio file. Video operations (crop, resize, overlay_text) and video
codec flags (-c:v, -map 0:v:0) fail because there's no video stream.

Reproduces Trigger.dev run failure: "Stream map '0:v:0' matches no
streams" when template operations include crop on an audio-only input.

Changes:
- buildRenderFfmpegArgs accepts audioOnly option
- Skips crop, resize, overlay_text when audioOnly
- mux_audio omits video mapping when audioOnly
- Output encoding skips video codec when audioOnly
- Task detects audioOnly from payload (no video_url, has audio_url)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/content/buildRenderFfmpegArgs.ts">

<violation number="1" location="src/content/buildRenderFfmpegArgs.ts:86">
P2: In audio-only mode, `mux_audio` fallback can remap to a second input and drop earlier audio edits (e.g., `trim`).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

- createRenderTask.ts → ffmpegEditTask.ts
- createRenderSchema.ts → ffmpegEditSchema.ts
- Task ID: "create-render" → "ffmpeg-edit"
- All exports and test references updated

All 318 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
break;
}

case "mux_audio": {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this case required now that we are video only?

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/schemas/ffmpegEditSchema.ts (1)

11-11: ⚠️ Potential issue | 🟡 Minor

Validate aspect string format to prevent invalid crop filters.

The aspect field accepts any string, but downstream code in buildRenderFfmpegArgs.ts expects a "W:H" format and splits on :. Invalid formats like "foo" or "9-16" will silently produce broken crop filters.

🛡️ Proposed fix
-    aspect: z.string().optional(),
+    aspect: z.string().regex(/^\d+:\d+$/, "aspect must be in W:H format (e.g. '9:16')").optional(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/ffmpegEditSchema.ts` at line 11, The aspect field currently
allows any string which can break buildRenderFfmpegArgs.ts that expects "W:H";
change the aspect schema in ffmpegEditSchema (the aspect property) to validate
the "W:H" integer pattern (e.g. use z.string().regex(/^\d+:\d+$/, 'aspect must
be in "W:H" integer format').optional()) so invalid values like "foo" or "9-16"
are rejected with a clear error message.
🧹 Nitpick comments (3)
src/tasks/__tests__/ffmpegEditTask.test.ts (1)

163-177: Dynamic imports may not reset between tests due to module caching.

Each test dynamically imports ffmpegEditTask, but after the first import, subsequent tests receive the cached module. Since vi.clearAllMocks() only clears mock call history (not module cache), the task object is shared across tests. This works here because you're only reading properties, but could cause issues if tests modified state.

Consider importing once at the top of the describe block or using vi.resetModules() in beforeEach if isolation is needed.

♻️ Alternative approach
 describe("ffmpegEditTask", () => {
+  let ffmpegEditTask: Awaited<typeof import("../ffmpegEditTask")>["ffmpegEditTask"];
+
   beforeEach(() => {
     vi.clearAllMocks();
     process.env.FAL_KEY = "test-key";
   });
 
-  it("exports a task with id ffmpeg-edit", async () => {
-    const { ffmpegEditTask } = await import("../ffmpegEditTask");
+  beforeAll(async () => {
+    const mod = await import("../ffmpegEditTask");
+    ffmpegEditTask = mod.ffmpegEditTask;
+  });
+
+  it("exports a task with id ffmpeg-edit", () => {
     expect(ffmpegEditTask.id).toBe("ffmpeg-edit");
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tasks/__tests__/ffmpegEditTask.test.ts` around lines 163 - 177, The tests
repeatedly use dynamic imports of ffmpegEditTask which may return a cached
module across tests; to ensure test isolation either import ffmpegEditTask once
at the top of the describe block and reuse that value for the three tests (so
ffmpegEditTask, ffmpegEditTask.id, ffmpegEditTask.schema,
ffmpegEditTask.machine, ffmpegEditTask.maxDuration are read from the same stable
import), or add vi.resetModules() in a beforeEach to clear the module cache
before each test so each dynamic import yields a fresh module instance.
src/tasks/ffmpegEditTask.ts (1)

7-7: Use logger from @trigger.dev/sdk/v3 instead of custom logStep.

Per coding guidelines, tasks should use the SDK's built-in logger for logging. This ensures proper integration with Trigger.dev's logging infrastructure and dashboard.

♻️ Proposed change
-import { logStep } from "../sandboxes/logStep";
+import { logger } from "@trigger.dev/sdk/v3";
 ...
-    logStep("ffmpeg-edit task started", true, {
+    logger.info("ffmpeg-edit task started", {
       accountId: payload.accountId,
 ...
-    logStep("Downloading input media");
+    logger.info("Downloading input media");
 ...
-    logStep("Running ffmpeg", true, { args: ffmpegArgs.join(" ") });
+    logger.info("Running ffmpeg", { args: ffmpegArgs.join(" ") });
 ...
-    logStep("Uploading rendered output");
+    logger.info("Uploading rendered output");
 ...
-    logStep("Render complete", true, { url: result.url, sizeBytes: result.sizeBytes });
+    logger.info("Render complete", { url: result.url, sizeBytes: result.sizeBytes });

As per coding guidelines: "Use logger from @trigger.dev/sdk/v3 for logging".

Also applies to: 30-34, 47-47, 52-52, 55-55, 58-58

src/schemas/ffmpegEditSchema.ts (1)

3-38: Consider using z.discriminatedUnion() for better performance and error messages.

Since all operation objects have a type literal discriminator, z.discriminatedUnion("type", [...]) would provide more efficient parsing and clearer validation errors when an operation fails to match.

♻️ Suggested refactor
-export const editOperationSchema = z.union([
+export const editOperationSchema = z.discriminatedUnion("type", [
   z.object({
     type: z.literal("trim"),
     start: z.number().nonnegative(),
     duration: z.number().positive(),
   }),
-  z.object({
+  z.object({
     type: z.literal("crop"),
     ...
-  }).refine(...),
+  }),
   // Note: .refine() cannot be used directly inside discriminatedUnion members.
   // You may need to validate crop/resize constraints in a wrapper or superRefine.

Note: If you keep z.union() due to the .refine() calls on crop/resize, consider adding a comment explaining this tradeoff.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/ffmpegEditSchema.ts` around lines 3 - 38, Replace the top-level
z.union with z.discriminatedUnion("type", [...]) for editOperationSchema to
leverage the literal "type" discriminator (keep each variant object
schemas—including the .refine calls on the crop and resize schemas—attached to
their respective object definitions); update the definition line from
z.union([...]) to z.discriminatedUnion("type", [...]) and ensure the crop and
resize objects still call .refine(...) as before so their validation remains
intact; if you intentionally keep z.union, add a short comment by
editOperationSchema explaining the tradeoff for the refine usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tasks/ffmpegEditTask.ts`:
- Around line 36-37: The temp directory stored in tempDir (created via
join(tmpdir(), `render-${randomUUID()}`) in ffmpegEditTask) is never removed;
update the cleanup in the finally block (the section that currently deletes
individual files around lines 66-70) to also remove the entire tempDir after
deleting files by invoking a recursive delete (e.g., fs.rm or fs.promises.rm
with recursive/force) or equivalent rmdir call, and guard it with try/catch so
failures don't mask other errors; reference tempDir and the existing per-file
cleanup code to add this final directory removal.
- Around line 39-40: The temp filenames and upload MIME are hardcoded for video;
when payload.audioOnly is true, change the inputPath to use an audio extension
(e.g., input.wav or input.<inferred audio ext>) instead of "input.mp4", set
outputPath extension accordingly, and ensure the upload uses an audio MIME like
"audio/${payload.output_format}" rather than "video/${payload.output_format}";
update the logic around inputPath, outputPath and the MIME type construction so
it branches on payload.audioOnly (or inspects payload.output_format) to pick the
correct file extension and "audio/" vs "video/" media type while keeping
existing variable names inputPath, outputPath and payload.output_format.

---

Duplicate comments:
In `@src/schemas/ffmpegEditSchema.ts`:
- Line 11: The aspect field currently allows any string which can break
buildRenderFfmpegArgs.ts that expects "W:H"; change the aspect schema in
ffmpegEditSchema (the aspect property) to validate the "W:H" integer pattern
(e.g. use z.string().regex(/^\d+:\d+$/, 'aspect must be in "W:H" integer
format').optional()) so invalid values like "foo" or "9-16" are rejected with a
clear error message.

---

Nitpick comments:
In `@src/schemas/ffmpegEditSchema.ts`:
- Around line 3-38: Replace the top-level z.union with
z.discriminatedUnion("type", [...]) for editOperationSchema to leverage the
literal "type" discriminator (keep each variant object schemas—including the
.refine calls on the crop and resize schemas—attached to their respective object
definitions); update the definition line from z.union([...]) to
z.discriminatedUnion("type", [...]) and ensure the crop and resize objects still
call .refine(...) as before so their validation remains intact; if you
intentionally keep z.union, add a short comment by editOperationSchema
explaining the tradeoff for the refine usage.

In `@src/tasks/__tests__/ffmpegEditTask.test.ts`:
- Around line 163-177: The tests repeatedly use dynamic imports of
ffmpegEditTask which may return a cached module across tests; to ensure test
isolation either import ffmpegEditTask once at the top of the describe block and
reuse that value for the three tests (so ffmpegEditTask, ffmpegEditTask.id,
ffmpegEditTask.schema, ffmpegEditTask.machine, ffmpegEditTask.maxDuration are
read from the same stable import), or add vi.resetModules() in a beforeEach to
clear the module cache before each test so each dynamic import yields a fresh
module instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc2ffb50-09ba-4cad-b212-f90283437d6c

📥 Commits

Reviewing files that changed from the base of the PR and between 2a57c33 and 9c4d8ef.

📒 Files selected for processing (5)
  • src/content/__tests__/buildRenderFfmpegArgs.test.ts
  • src/content/buildRenderFfmpegArgs.ts
  • src/schemas/ffmpegEditSchema.ts
  • src/tasks/__tests__/ffmpegEditTask.test.ts
  • src/tasks/ffmpegEditTask.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/content/buildRenderFfmpegArgs.ts

Comment on lines +36 to +37
const tempDir = join(tmpdir(), `render-${randomUUID()}`);
await mkdir(tempDir, { recursive: true });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Temp directory is created but never deleted.

The finally block removes individual files but leaves the temp directory (render-<uuid>) behind. Over time, this could accumulate orphan directories.

🧹 Proposed fix
+import { unlink, mkdir, rm } from "node:fs/promises";
 ...
     } finally {
       await Promise.all(
         [inputPath, outputPath].map((p) => unlink(p).catch(() => undefined)),
       );
+      await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
     }

Also applies to: 66-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tasks/ffmpegEditTask.ts` around lines 36 - 37, The temp directory stored
in tempDir (created via join(tmpdir(), `render-${randomUUID()}`) in
ffmpegEditTask) is never removed; update the cleanup in the finally block (the
section that currently deletes individual files around lines 66-70) to also
remove the entire tempDir after deleting files by invoking a recursive delete
(e.g., fs.rm or fs.promises.rm with recursive/force) or equivalent rmdir call,
and guard it with try/catch so failures don't mask other errors; reference
tempDir and the existing per-file cleanup code to add this final directory
removal.

Comment on lines +39 to +40
const inputPath = join(tempDir, "input.mp4");
const outputPath = join(tempDir, `output.${payload.output_format}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded video/* MIME type and .mp4 input extension break audio-only workflows.

When audioOnly is true:

  1. Line 39 names the temp input input.mp4 even for audio files
  2. Line 56 always uploads with video/${output_format} MIME type

This could cause downstream issues with audio-only renders being mislabeled.

🛡️ Proposed fix
+    const inputExt = audioOnly ? "mp3" : "mp4";
+    const mimePrefix = audioOnly ? "audio" : "video";
-    const inputPath = join(tempDir, "input.mp4");
+    const inputPath = join(tempDir, `input.${inputExt}`);
     const outputPath = join(tempDir, `output.${payload.output_format}`);
     ...
-    const result = await uploadToFalStorage(outputPath, `rendered.${payload.output_format}`, `video/${payload.output_format}`);
+    const result = await uploadToFalStorage(outputPath, `rendered.${payload.output_format}`, `${mimePrefix}/${payload.output_format}`);

Also applies to: 56-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tasks/ffmpegEditTask.ts` around lines 39 - 40, The temp filenames and
upload MIME are hardcoded for video; when payload.audioOnly is true, change the
inputPath to use an audio extension (e.g., input.wav or input.<inferred audio
ext>) instead of "input.mp4", set outputPath extension accordingly, and ensure
the upload uses an audio MIME like "audio/${payload.output_format}" rather than
"video/${payload.output_format}"; update the logic around inputPath, outputPath
and the MIME type construction so it branches on payload.audioOnly (or inspects
payload.output_format) to pick the correct file extension and "audio/" vs
"video/" media type while keeping existing variable names inputPath, outputPath
and payload.output_format.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/tasks/ffmpegEditTask.ts">

<violation number="1" location="src/tasks/ffmpegEditTask.ts:21">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

This change violates the “Flag AI Slop and Fabricated Changes” clause for PR/docs claims not implemented: the PR claims a `create-render` task, but the code registers `ffmpeg-edit`. Align the task ID with the stated API contract or correct the PR metadata.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

* order using ffmpeg. Uploads the result to fal.ai storage.
*/
export const ffmpegEditTask = schemaTask({
id: "ffmpeg-edit",
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag AI Slop and Fabricated Changes

This change violates the “Flag AI Slop and Fabricated Changes” clause for PR/docs claims not implemented: the PR claims a create-render task, but the code registers ffmpeg-edit. Align the task ID with the stated API contract or correct the PR metadata.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tasks/ffmpegEditTask.ts, line 21:

<comment>This change violates the “Flag AI Slop and Fabricated Changes” clause for PR/docs claims not implemented: the PR claims a `create-render` task, but the code registers `ffmpeg-edit`. Align the task ID with the stated API contract or correct the PR metadata.</comment>

<file context>
@@ -17,8 +17,8 @@ import { buildRenderFfmpegArgs } from "../content/buildRenderFfmpegArgs";
-export const createRenderTask = schemaTask({
-  id: "create-render",
+export const ffmpegEditTask = schemaTask({
+  id: "ffmpeg-edit",
   schema: createRenderPayloadSchema,
   maxDuration: 600,
</file context>
Fix with Cubic

Addresses all 5 PR review comments:

1. Remove mux_audio and audio_url — video-only endpoint
2. Remove audioOnly handling — no longer needed
3. Fix crop aspect math — 9:16 now correctly narrows width (ih*9/16:ih)
   instead of expanding height. Fixes "Invalid too big size" ffmpeg error.
4. runFfmpeg maxBuffer increased to 10MB for verbose ffmpeg stderr
5. Color regex restricted to valid CSS hex lengths (3/4/6/8 digits)

SRP: buildRenderFfmpegArgs split into sub-functions:
- buildCropFilter() — aspect ratio and dimension crop logic
- buildOverlayTextFilter() — text escaping, positioning, styling

Schema renamed: createRenderPayloadSchema → ffmpegEditPayloadSchema
(deprecated alias kept for backwards compat)

All 316 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/schemas/ffmpegEditSchema.ts (1)

13-18: ⚠️ Potential issue | 🟠 Major

crop validation is too permissive and allows invalid ffmpeg operations.

Current rules accept malformed aspect values and one-sided crop dimensions. That can pass validation but later generate invalid crop filters.

Proposed schema tightening
   z.object({
     type: z.literal("crop"),
-    aspect: z.string().optional(),
+    aspect: z
+      .string()
+      .regex(/^[1-9]\d*:[1-9]\d*$/, "aspect must be W:H with positive integers")
+      .optional(),
     width: z.number().int().positive().optional(),
     height: z.number().int().positive().optional(),
-  }).refine(data => data.aspect || data.width || data.height, {
-    message: "crop requires at least one of: aspect, width, height",
+  }).refine((data) => Boolean(data.aspect) || (data.width !== undefined && data.height !== undefined), {
+    message: "crop requires either a valid aspect or both width and height",
   }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/ffmpegEditSchema.ts` around lines 13 - 18, The crop schema
currently allows invalid aspect strings and one-sided dimensions; tighten
validation on the crop object (the crop schema with keys aspect, width, height)
by: 1) validating aspect with a strict pattern (e.g., a regex matching "N:M"
positive integers) instead of plain z.string(); 2) ensure width and height
remain positive ints but enforce that if one of width or height is provided the
other must also be present (use a .refine on the crop object to require both or
neither); and 3) keep the overall refine that requires at least aspect or both
dimensions so invalid combinations (malformed aspect or lone width/height) are
rejected.
src/content/buildRenderFfmpegArgs.ts (1)

69-74: ⚠️ Potential issue | 🔴 Critical

crop aspect math can exceed source bounds and fail ffmpeg.

Line 73 chooses crop math only from target orientation, not source-vs-target aspect comparison. This still produces invalid crops for valid inputs (e.g., ultra-wide sources), causing runtime ffmpeg failures.

Proposed fix
 function buildCropFilter(op: { aspect?: string; width?: number; height?: number }): string {
   if (op.aspect) {
     const [w, h] = op.aspect.split(":").map(Number);
     if (w && h) {
-      return w >= h ? `crop=iw:iw*${h}/${w}` : `crop=ih*${w}/${h}:ih`;
+      const ratio = `${w}/${h}`;
+      return `crop=if(gte(iw/ih\\,${ratio})\\,ih*${ratio}\\,iw):if(gte(iw/ih\\,${ratio})\\,ih\\,iw/${ratio})`;
     }
   }
   return `crop=${op.width ?? -1}:${op.height ?? -1}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/buildRenderFfmpegArgs.ts` around lines 69 - 74, The crop math in
buildCropFilter only looks at target orientation (w >= h) and can produce crop
values larger than the source (causing ffmpeg failures); update buildCropFilter
to compare source aspect to target aspect (use ffmpeg expressions like iw/ih vs
w/h) and emit a conditional crop expression that clamps to source bounds (use
ffmpeg min() or if() expressions so the computed crop width/height never exceed
iw/ih, e.g. choose cropping by width when source is wider than target and by
height otherwise, but expressed with safe ffmpeg arithmetic), referencing
buildCropFilter and the local w,h/op.aspect variables so the new expression
always yields values within input dimensions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tasks/__tests__/ffmpegEditTask.test.ts`:
- Around line 112-122: The test currently may pass vacuously because it only
checks for absence of audio_url when parse succeeded; explicitly assert the
parse succeeded before further assertions by adding an assertion on
createRenderPayloadSchema.safeParse(...) result (e.g.,
expect(result.success).toBe(true)) and then perform the property check on
result.data; reference the test block using the existing
createRenderPayloadSchema and the "does not accept audio_url param" test to
locate where to add the success assertion.

---

Duplicate comments:
In `@src/content/buildRenderFfmpegArgs.ts`:
- Around line 69-74: The crop math in buildCropFilter only looks at target
orientation (w >= h) and can produce crop values larger than the source (causing
ffmpeg failures); update buildCropFilter to compare source aspect to target
aspect (use ffmpeg expressions like iw/ih vs w/h) and emit a conditional crop
expression that clamps to source bounds (use ffmpeg min() or if() expressions so
the computed crop width/height never exceed iw/ih, e.g. choose cropping by width
when source is wider than target and by height otherwise, but expressed with
safe ffmpeg arithmetic), referencing buildCropFilter and the local w,h/op.aspect
variables so the new expression always yields values within input dimensions.

In `@src/schemas/ffmpegEditSchema.ts`:
- Around line 13-18: The crop schema currently allows invalid aspect strings and
one-sided dimensions; tighten validation on the crop object (the crop schema
with keys aspect, width, height) by: 1) validating aspect with a strict pattern
(e.g., a regex matching "N:M" positive integers) instead of plain z.string(); 2)
ensure width and height remain positive ints but enforce that if one of width or
height is provided the other must also be present (use a .refine on the crop
object to require both or neither); and 3) keep the overall refine that requires
at least aspect or both dimensions so invalid combinations (malformed aspect or
lone width/height) are rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3fda437a-2110-4a77-acbb-820b618d495f

📥 Commits

Reviewing files that changed from the base of the PR and between 9c4d8ef and 4010d8d.

📒 Files selected for processing (7)
  • src/content/__tests__/buildRenderFfmpegArgs.test.ts
  • src/content/__tests__/runFfmpeg.test.ts
  • src/content/buildRenderFfmpegArgs.ts
  • src/content/runFfmpeg.ts
  • src/schemas/ffmpegEditSchema.ts
  • src/tasks/__tests__/ffmpegEditTask.test.ts
  • src/tasks/ffmpegEditTask.ts
✅ Files skipped from review due to trivial changes (1)
  • src/content/runFfmpeg.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tasks/ffmpegEditTask.ts
  • src/content/tests/buildRenderFfmpegArgs.test.ts

Comment on lines +112 to +122
it("does not accept audio_url param", () => {
const result = createRenderPayloadSchema.safeParse({
accountId: "acc-123",
video_url: "https://example.com/video.mp4",
audio_url: "https://example.com/audio.mp3",
operations: [{ type: "trim", start: 0, duration: 5 }],
});
if (result.success) {
expect(result.data).not.toHaveProperty("audio_url");
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This test can pass vacuously without validating behavior.

Please assert result.success explicitly before checking property stripping; otherwise a parse failure still passes this test.

Proposed fix
   it("does not accept audio_url param", () => {
     const result = createRenderPayloadSchema.safeParse({
       accountId: "acc-123",
       video_url: "https://example.com/video.mp4",
       audio_url: "https://example.com/audio.mp3",
       operations: [{ type: "trim", start: 0, duration: 5 }],
     });
+    expect(result.success).toBe(true);
     if (result.success) {
       expect(result.data).not.toHaveProperty("audio_url");
     }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("does not accept audio_url param", () => {
const result = createRenderPayloadSchema.safeParse({
accountId: "acc-123",
video_url: "https://example.com/video.mp4",
audio_url: "https://example.com/audio.mp3",
operations: [{ type: "trim", start: 0, duration: 5 }],
});
if (result.success) {
expect(result.data).not.toHaveProperty("audio_url");
}
});
it("does not accept audio_url param", () => {
const result = createRenderPayloadSchema.safeParse({
accountId: "acc-123",
video_url: "https://example.com/video.mp4",
audio_url: "https://example.com/audio.mp3",
operations: [{ type: "trim", start: 0, duration: 5 }],
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).not.toHaveProperty("audio_url");
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tasks/__tests__/ffmpegEditTask.test.ts` around lines 112 - 122, The test
currently may pass vacuously because it only checks for absence of audio_url
when parse succeeded; explicitly assert the parse succeeded before further
assertions by adding an assertion on createRenderPayloadSchema.safeParse(...)
result (e.g., expect(result.success).toBe(true)) and then perform the property
check on result.data; reference the test block using the existing
createRenderPayloadSchema and the "does not accept audio_url param" test to
locate where to add the success assertion.

* - 9:16 (portrait): keep full height, narrow width → crop=ih*9/16:ih
* - 16:9 (landscape): keep full width, narrow height → crop=iw:iw*9/16
*/
function buildCropFilter(op: { aspect?: string; width?: number; height?: number }): string {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SRP - new lib file for buildCropFilter

/**
* Build the ffmpeg drawtext= filter for text overlay.
*/
function buildOverlayTextFilter(op: {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SRP - new lib file for buildOverlayTextFilter

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/schemas/ffmpegEditSchema.ts">

<violation number="1" location="src/schemas/ffmpegEditSchema.ts:3">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

PR description claims mux_audio/audio-only edits are supported, but this schema change removes the mux_audio operation and makes video_url required, so that behavior isn’t implemented. Update the PR description or restore mux_audio support to avoid shipping misleading documentation.</violation>
</file>

<file name="src/content/__tests__/buildRenderFfmpegArgs.test.ts">

<violation number="1" location="src/content/__tests__/buildRenderFfmpegArgs.test.ts:145">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

This test claims to verify mux_audio handling, but it never passes a mux_audio operation. As written, it only tests the trim path and doesn’t validate the claimed behavior.</violation>

<violation number="2" location="src/content/__tests__/buildRenderFfmpegArgs.test.ts:157">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

This test only asserts that `args` is non-empty, which doesn’t verify the claimed signature change at all. It reads like a placeholder rather than a real validation.</violation>
</file>

<file name="src/content/buildRenderFfmpegArgs.ts">

<violation number="1" location="src/content/buildRenderFfmpegArgs.ts:35">
P2: Guard against malformed aspect strings before adding a crop filter. With the current helper, any non-"w:h" aspect that still passes schema validation emits `crop=-1:-1`, which doesn't reflect a valid requested crop. Consider validating the aspect format in the schema or skipping the crop when parsing fails.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

@@ -0,0 +1,49 @@
import { z } from "zod";
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag AI Slop and Fabricated Changes

PR description claims mux_audio/audio-only edits are supported, but this schema change removes the mux_audio operation and makes video_url required, so that behavior isn’t implemented. Update the PR description or restore mux_audio support to avoid shipping misleading documentation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/schemas/ffmpegEditSchema.ts, line 3:

<comment>PR description claims mux_audio/audio-only edits are supported, but this schema change removes the mux_audio operation and makes video_url required, so that behavior isn’t implemented. Update the PR description or restore mux_audio support to avoid shipping misleading documentation.</comment>

<file context>
@@ -1,5 +1,7 @@
 import { z } from "zod";
 
+const cssColorRegex = /^[a-zA-Z]+$|^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
+
 export const editOperationSchema = z.union([
</file context>
Fix with Cubic

… up tests

Address PR review comments:
1. buildCropFilter.ts — own file, guards against malformed aspect strings
2. buildOverlayTextFilter.ts — own file with text escaping and positioning
3. buildRenderFfmpegArgs.ts — now under 65 LOC, composes sub-functions
4. Remove weak/placeholder tests, add malformed aspect test
5. Remove all mux_audio and audioOnly references from tests

All 316 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 9, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

@sweetmantech sweetmantech changed the title feat: add create-render task for PATCH /api/content feat: add ffmpeg-edit task for PATCH /api/content Apr 9, 2026
@sweetmantech sweetmantech merged commit 46f2978 into main Apr 9, 2026
2 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