Skip to content

feat: add modular content creation primitive endpoints#390

Open
recoupableorg wants to merge 51 commits intotestfrom
feature/content-primitive-endpoints
Open

feat: add modular content creation primitive endpoints#390
recoupableorg wants to merge 51 commits intotestfrom
feature/content-primitive-endpoints

Conversation

@recoupableorg
Copy link
Copy Markdown
Contributor

@recoupableorg recoupableorg commented Apr 2, 2026

Summary

Full restructure of content primitive endpoints to be generic, consistent, and well-factored.

Route restructure (verb-qualifier naming)

  • content/create/image → content/generate-image
  • content/create/video → content/generate-video
  • content/create/text → content/generate-caption
  • content/create/audio → content/transcribe-audio
  • content/create/render → content/edit (new operations-based endpoint)
  • content/create/upscale → content/upscale
  • New: content/analyze-video (Twelve Labs video analysis)

Generic primitives

  • Renamed music-specific params: face_guide_url → reference_image_url, song_url → audio_url, songs → audio_urls, song → topic
  • Removed unused required fields from primitive schemas: artist_account_id, template, lipsync
  • Added optional model param to generate-image, generate-video, transcribe-audio

Edit endpoint (replaces render)

  • Accepts either template name (deterministic config) or operations array (trim, crop, resize, overlay_text, mux_audio)
  • All operations execute in a single ffmpeg pass

Auth pattern fix

  • Moved validateAuthContext to handler level in all primitives

Code quality (CodeRabbit review)

  • Split createTextHandler into focused helpers
  • Created createPrimitiveRoute factory for shared OPTIONS/CORS wiring
  • All 1742 tests pass

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 2, 2026

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

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Apr 9, 2026 4:53pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 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 multiple new content-creation API routes and their primitive handlers, new Zod schemas and request validation helpers, a Trigger.dev wrapper, and many JSDoc enhancements across the codebase. Routes include CORS OPTIONS handlers and route-level caching/runtime exports.

Changes

Cohort / File(s) Summary
API Route Modules
app/api/content/create/audio/route.ts, app/api/content/create/image/route.ts, app/api/content/create/render/route.ts, app/api/content/create/text/route.ts, app/api/content/create/upscale/route.ts, app/api/content/create/video/route.ts, app/api/content/analyze/route.ts
New Next.js App Router route modules — each adds an OPTIONS CORS preflight handler, re-exports the corresponding primitive as POST, and sets dynamic="force-dynamic", fetchCache="force-no-store", revalidate=0.
Primitive Handlers
lib/content/primitives/createTextHandler.ts, lib/content/primitives/createAudioHandler.ts, lib/content/primitives/createImageHandler.ts, lib/content/primitives/createVideoHandler.ts, lib/content/primitives/createUpscaleHandler.ts, lib/content/primitives/createRenderHandler.ts, lib/content/primitives/createAnalyzeHandler.ts
New exported request handlers that perform auth validation, body validation, call external services (Recoup, FAL, TwelveLabs) or Trigger, normalize responses, and return JSON with consistent CORS/error handling.
Validation Schemas & Helper
lib/content/primitives/schemas.ts, lib/content/primitives/validatePrimitiveBody.ts
New Zod schemas for image/video/text/audio/render/upscale/analyze payloads and a helper that safely parses request JSON and returns validated data or a 400 NextResponse with CORS headers.
Trigger Wrapper
lib/trigger/triggerPrimitive.ts
Adds thin wrapper triggerPrimitive(taskId, payload) delegating to tasks.trigger from @trigger.dev/sdk.
Bulk JSDoc updates
many files under app/api/..., lib/... (e.g., app/api/accounts/[id]/route.ts, lib/admins/..., lib/catalog/..., lib/transcribe/..., lib/supabase/..., lib/slack/..., etc.)
Widespread JSDoc additions/clarifications (mostly @param tags); no runtime or signature changes. These are documentation-only edits across ~50 files.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Route as Route Module
    participant Validator as validatePrimitiveBody
    participant Auth as validateAuthContext
    participant Handler as Primitive Handler
    participant ExtAPI as External API / FAL / TwelveLabs
    participant Trigger as Trigger.dev
    participant Response as NextResponse

    Client->>Route: POST /api/content/create/{type} + body
    Route->>Validator: validatePrimitiveBody(request, schema)
    Validator->>Validator: parse JSON & Zod validate
    alt validation fails
        Validator->>Response: 400 JSON + CORS
    else
        Validator->>Auth: validateAuthContext(request)
        alt auth fails
            Auth->>Response: auth error (NextResponse)
        else
            Auth->>Handler: call handler with accountId + payload
            alt handler uses external API
                Handler->>ExtAPI: fetch / fal.subscribe / TwelveLabs
                alt external error
                    ExtAPI-->>Handler: non-OK / error
                    Handler->>Response: 502/500 JSON + CORS
                else
                    ExtAPI-->>Handler: result data
                    Handler->>Response: 200 JSON + CORS (normalized)
                end
            else handler triggers background task
                Handler->>Trigger: tasks.trigger("create-render", payload)
                Trigger-->>Handler: handle
                Handler->>Response: 202 JSON { runId, status } + CORS
            end
        end
    end
    Response-->>Client: HTTP response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • sweetmantech

Poem

✨ New routes hum, validators align,
Handlers call services, timing fine,
Trigger fires, CORS headers sing,
Schemas guard each incoming thing,
Docs bloom like lights along the line.

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Code exhibits significant duplication across route files and handlers, violating DRY, SRP, and OCP principles with repeated auth flows, error handling, and validation logic. Implement factory functions for route generation, extract shared utilities (error responses, API validation, result extraction), and create handler wrappers to encapsulate common auth+validate+try-catch patterns.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/content-primitive-endpoints

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: 11

🧹 Nitpick comments (2)
lib/content/primitives/createTextHandler.ts (1)

11-80: Split createTextHandler into smaller helpers.

Line 11–80 is doing validation, prompt composition, upstream transport, response normalization, and HTTP mapping in one function. Breaking this up will improve maintainability and testability.

As per coding guidelines: "Keep functions under 50 lines" and "Single responsibility per function".

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

In `@lib/content/primitives/createTextHandler.ts` around lines 11 - 80, Split the
large createTextHandler into smaller helpers: extract validation into keep using
validatePrimitiveBody with createTextBodySchema, move prompt construction into a
composePrompt(data) helper that returns the prompt string, move the fetch call
to callRecoupGenerate(prompt, recoupApiUrl, recoupApiKey) which performs the
POST and throws or returns the parsed JSON, add a normalizeGeneratedText(json)
helper that implements the json.text normalization logic (handle string vs array
and trimming/quote stripping), and finally have a small mapping function
mapToNextResponse(content) that returns the NextResponse with getCorsHeaders and
the font/color metadata; update createTextHandler to orchestrate these helpers
and keep it under ~50 lines. Ensure helper names (composePrompt,
callRecoupGenerate, normalizeGeneratedText, mapToNextResponse) are used so
reviewers can find and test each responsibility independently.
app/api/content/create/text/route.ts (1)

1-13: Use createPrimitiveRoute here to avoid route wiring drift.

This endpoint manually duplicates the OPTIONS/config pattern already centralized in lib/content/primitives/primitiveRoute.ts. Reusing it keeps all primitive routes aligned.

♻️ Proposed refactor
-import { NextResponse } from "next/server";
-import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
 import { createTextHandler } from "@/lib/content/primitives/createTextHandler";
+import { createPrimitiveRoute, dynamic, fetchCache, revalidate } from "@/lib/content/primitives/primitiveRoute";

-export async function OPTIONS() {
-  return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
-}
-
-export { createTextHandler as POST };
-
-export const dynamic = "force-dynamic";
-export const fetchCache = "force-no-store";
-export const revalidate = 0;
+export const { OPTIONS, POST } = createPrimitiveRoute(createTextHandler);
+export { dynamic, fetchCache, revalidate };

As per coding guidelines: “Extract shared logic into reusable utilities following DRY principle”.

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

In `@app/api/content/create/text/route.ts` around lines 1 - 13, The route
duplicates OPTIONS and config wiring; replace the manual export and OPTIONS
handler by using the shared helper createPrimitiveRoute so the endpoint inherits
standard CORS/options and configs. Locate this file's export of
createTextHandler and the manual OPTIONS/exports and refactor to call
createPrimitiveRoute with createTextHandler (referencing createPrimitiveRoute
and createTextHandler) so the route wiring (OPTIONS,
dynamic/fetchCache/revalidate and CORS headers) is centralized in
lib/content/primitives/primitiveRoute.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/content/create/image/route.ts`:
- Around line 1-7: Add JSDoc comments to this module and its primary exports:
place a module-level JSDoc at the top of the file describing the route purpose
(Create image API), accepted payload (referencing createImageBodySchema), and
supported methods; add a brief JSDoc above the handler creation line that
documents createPrimitiveHandler("create-image", createImageBodySchema) and its
validation behavior, and a short JSDoc above the export line for the generated
route exports (OPTIONS, POST) and the re-exported dynamic, fetchCache,
revalidate flags so callers understand their meaning and usage.
- Around line 1-3: The file fails Prettier formatting; run the project's
Prettier formatter (e.g., via npm/yarn script like format or npx prettier
--write) on this route to fix spacing/line breaks and import ordering for the
top-level imports including createPrimitiveHandler, createPrimitiveRoute,
dynamic, fetchCache, revalidate, and createImageBodySchema; ensure the file
matches the repo's Prettier config so CI format checks pass.
- Around line 5-7: Add endpoint-level integration tests for the create-image
route (exports: handler, POST from createPrimitiveRoute) that exercise real auth
behavior instead of mocking validatePrimitiveBody: write tests that hit the POST
endpoint backed by createPrimitiveHandler + createImageBodySchema and cover (1)
success: valid auth and body returns 202; (2) auth errors: missing/invalid
credentials produce 401 and unauthorized account produce 403 by calling the real
validateAuthContext flow; (3) validation error: invalid request body returns 400
from the createImageBodySchema validation; and (4) server error: simulate the
underlying service throwing to get 500; ensure tests only mock external services
(e.g., storage/service clients) but not validateAuthContext for auth-path
assertions and use the same route exports (handler/POST) to exercise endpoint
semantics.

In `@app/api/content/create/text/route.ts`:
- Around line 5-10: Add JSDoc comments to this API route by documenting the
exported OPTIONS handler and the POST export alias: add a JSDoc block above the
OPTIONS function describing its purpose (CORS preflight response), parameters
(none) and return type, and add a JSDoc block above the export line for
createTextHandler (or the createTextHandler function definition if present)
describing the POST handler’s purpose, expected request body, responses, and any
errors; ensure the JSDoc uses standard tags like `@route`, `@method`, `@returns` and
references the symbols OPTIONS and createTextHandler so the route complies with
the “All API routes should have JSDoc comments” guideline.

In `@app/api/content/create/upscale/route.ts`:
- Around line 1-7: Add concise route-level JSDoc comments explaining the API
route purpose and the handlers: document the module at top and annotate the main
handler creation/export lines (createPrimitiveHandler("create-upscale",
createUpscaleBodySchema), the exported handler object via createPrimitiveRoute,
and the exported members OPTIONS and POST) with a short description of the route
behavior, accepted request shape (referencing createUpscaleBodySchema), and any
important response/side-effect notes; place the JSDoc directly above the
handler/exports so route documentation tools pick it up (also include brief
one-line comments for re-exported symbols dynamic, fetchCache, revalidate if
needed).

In `@app/api/content/create/video/route.ts`:
- Around line 1-7: Add a route-level JSDoc block above the endpoint exports in
this module describing the API route purpose and exported handlers: document
that this file defines the "create-video" primitive handler (handler) and
exports the OPTIONS and POST endpoints via createPrimitiveRoute(handler), and
include tags for parameters, request/response shape (based on
createVideoBodySchema), and any middleware/behavior (dynamic, fetchCache,
revalidate) so the route adheres to the project JSDoc standard.
- Around line 1-7: Add route-level tests for the create-video endpoint: write
tests that exercise the exported POST and OPTIONS handlers (created via
createPrimitiveRoute(handler) where handler comes from
createPrimitiveHandler("create-video", createVideoBodySchema)), asserting a
valid request returns HTTP 202 with a JSON body containing runId, malformed
payloads return HTTP 400, authentication failure path is covered if the route
enforces auth, and OPTIONS responses include appropriate CORS headers; follow
the structure and assertions used in the coding-agent route tests (e.g., request
construction, header checks, and status/body expectations) to implement success,
error, auth, and CORS/OPTIONS test cases.

In `@lib/content/primitives/createTextHandler.ts`:
- Around line 32-40: The fetch to `${recoupApiUrl}/api/chat/generate` in
createTextHandler.ts has no timeout; wrap the request in an AbortController
(pass controller.signal to fetch), start a timer (e.g., 5–15s) that calls
controller.abort() on timeout, and clear the timer when the request completes;
catch the AbortError separately to return/log a clear timeout error and rethrow
or return an appropriate response. Update the fetch invocation that uses
recoupApiKey/ prompt to include the signal and ensure the timeout is cleared to
avoid leaks.

In `@lib/content/primitives/primitiveRoute.ts`:
- Line 10: The OPTIONS handler currently returns global wildcard CORS via the
OPTIONS constant and getCorsHeaders(), which is unsafe for endpoints accepting
Authorization/x-api-key; replace this by checking the incoming request Origin
header against an explicit allowlist and only returning that Origin (or no CORS)
when it matches. Update the OPTIONS constant to read
request.headers.get('origin'), validate it against a configured allowlist, and
return a NextResponse with Access-Control-Allow-Origin set to the matched origin
(and appropriate allow headers) or a 204 with no CORS; alternatively implement a
helper (e.g., getRestrictedCorsHeaders or extend getCorsHeaders to accept an
origin) and use that in OPTIONS and in other responses for authenticated
primitive endpoints. Ensure the symbols to change are OPTIONS and getCorsHeaders
so all authenticated routes stop using the wildcard.

In `@lib/content/primitives/schemas.ts`:
- Around line 17-18: Update the numeric timing/size fields in the schema to
reject negative values by adding a lower-bound (e.g., use z.number().min(0) or
.nonnegative()) to audio_start_seconds and audio_duration_seconds and to the
other numeric timing/size fields mentioned in the review (the fields on the
later numeric lines referenced), so each such field uses z.number().min(0) (and
include a clear custom error message if desired) instead of
z.number().optional(); ensure optionality is preserved if needed (e.g.,
z.number().min(0).optional()) so downstream payloads cannot contain negative
timing/size values.
- Line 5: Remove the artist_account_id property from the request body Zod
schemas in lib/content/primitives/schemas.ts (the fields named artist_account_id
in the relevant schema declarations) and update callers to stop expecting it;
instead, derive the account ID from the authentication context (e.g.,
request.user/account in your auth middleware) and inject that value server-side
into the payloads used by functions that create/update artists. Ensure any
validation or Zod schema references no longer include artist_account_id and that
server-side handlers (where these schemas are used) read the authenticated
account ID and attach it to the model before persisting or authorizing
operations.

---

Nitpick comments:
In `@app/api/content/create/text/route.ts`:
- Around line 1-13: The route duplicates OPTIONS and config wiring; replace the
manual export and OPTIONS handler by using the shared helper
createPrimitiveRoute so the endpoint inherits standard CORS/options and configs.
Locate this file's export of createTextHandler and the manual OPTIONS/exports
and refactor to call createPrimitiveRoute with createTextHandler (referencing
createPrimitiveRoute and createTextHandler) so the route wiring (OPTIONS,
dynamic/fetchCache/revalidate and CORS headers) is centralized in
lib/content/primitives/primitiveRoute.ts.

In `@lib/content/primitives/createTextHandler.ts`:
- Around line 11-80: Split the large createTextHandler into smaller helpers:
extract validation into keep using validatePrimitiveBody with
createTextBodySchema, move prompt construction into a composePrompt(data) helper
that returns the prompt string, move the fetch call to
callRecoupGenerate(prompt, recoupApiUrl, recoupApiKey) which performs the POST
and throws or returns the parsed JSON, add a normalizeGeneratedText(json) helper
that implements the json.text normalization logic (handle string vs array and
trimming/quote stripping), and finally have a small mapping function
mapToNextResponse(content) that returns the NextResponse with getCorsHeaders and
the font/color metadata; update createTextHandler to orchestrate these helpers
and keep it under ~50 lines. Ensure helper names (composePrompt,
callRecoupGenerate, normalizeGeneratedText, mapToNextResponse) are used so
reviewers can find and test each responsibility independently.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d59a6f13-51cf-4b51-a0da-e16caa5e3b1d

📥 Commits

Reviewing files that changed from the base of the PR and between 237567d and e41dd84.

⛔ Files ignored due to path filters (3)
  • lib/content/primitives/__tests__/handlePrimitiveTrigger.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/primitives/__tests__/schemas.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/primitives/__tests__/validatePrimitiveBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (12)
  • app/api/content/create/audio/route.ts
  • app/api/content/create/image/route.ts
  • app/api/content/create/render/route.ts
  • app/api/content/create/text/route.ts
  • app/api/content/create/upscale/route.ts
  • app/api/content/create/video/route.ts
  • lib/content/primitives/createTextHandler.ts
  • lib/content/primitives/handlePrimitiveTrigger.ts
  • lib/content/primitives/primitiveRoute.ts
  • lib/content/primitives/schemas.ts
  • lib/content/primitives/validatePrimitiveBody.ts
  • lib/trigger/triggerPrimitive.ts

Comment on lines +1 to +7
import { createPrimitiveHandler } from "@/lib/content/primitives/handlePrimitiveTrigger";
import { createPrimitiveRoute, dynamic, fetchCache, revalidate } from "@/lib/content/primitives/primitiveRoute";
import { createImageBodySchema } from "@/lib/content/primitives/schemas";

const handler = createPrimitiveHandler("create-image", createImageBodySchema);
export const { OPTIONS, POST } = createPrimitiveRoute(handler);
export { dynamic, fetchCache, revalidate };
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

Add JSDoc comments to this route module.

This new API route does not include JSDoc comments.

Based on learnings: “Applies to app/api/**/route.ts : All API routes should have JSDoc comments”.

🧰 Tools
🪛 GitHub Actions: Format Check

[warning] 1-1: Prettier --check reported code style issues in this file.

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

In `@app/api/content/create/image/route.ts` around lines 1 - 7, Add JSDoc comments
to this module and its primary exports: place a module-level JSDoc at the top of
the file describing the route purpose (Create image API), accepted payload
(referencing createImageBodySchema), and supported methods; add a brief JSDoc
above the handler creation line that documents
createPrimitiveHandler("create-image", createImageBodySchema) and its validation
behavior, and a short JSDoc above the export line for the generated route
exports (OPTIONS, POST) and the re-exported dynamic, fetchCache, revalidate
flags so callers understand their meaning and usage.

Comment on lines +5 to +7
const handler = createPrimitiveHandler("create-image", createImageBodySchema);
export const { OPTIONS, POST } = createPrimitiveRoute(handler);
export { dynamic, fetchCache, revalidate };
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify tests for new primitive create endpoints and presence of success/error assertions.
# Expected: at least one test file covers /api/content/create/image and error cases.

rg -n --iglob '*.{test,spec}.{ts,tsx,js,jsx}' \
  -e '/api/content/create/(image|video|audio|render|upscale|text)' \
  -e 'create-(image|video|audio|render|upscale)' \
  -C2

rg -n --iglob '*.{test,spec}.{ts,tsx,js,jsx}' \
  -e '\b(200|202|400|401|403|422|500)\b' \
  -e 'toThrow|rejects|error' \
  -C1

Repository: recoupable/api

Length of output: 50371


🏁 Script executed:

# Check the test file that was found
wc -l lib/content/primitives/__tests__/handlePrimitiveTrigger.test.ts

Repository: recoupable/api

Length of output: 124


🏁 Script executed:

# Read the complete test file to assess coverage
cat -n lib/content/primitives/__tests__/handlePrimitiveTrigger.test.ts

Repository: recoupable/api

Length of output: 3012


🏁 Script executed:

# Check the createPrimitiveHandler and createPrimitiveRoute implementations
find . -name "*.ts" -path "*/lib/content/primitives/*" ! -path "*/__tests__/*" | head -10

Repository: recoupable/api

Length of output: 283


🏁 Script executed:

# Look for the route handler implementation
cat -n app/api/content/create/image/route.ts

Repository: recoupable/api

Length of output: 571


🏁 Script executed:

# Check the validatePrimitiveBody to see what auth errors it can return
cat -n lib/content/primitives/validatePrimitiveBody.ts

Repository: recoupable/api

Length of output: 1494


🏁 Script executed:

# Check the primitiveRoute implementation
cat -n lib/content/primitives/primitiveRoute.ts

Repository: recoupable/api

Length of output: 883


🏁 Script executed:

# Search for any endpoint-level integration tests for the image creation route
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "api/content/create/image" 2>/dev/null | head -5

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Check what error codes are tested in the primitive handler test
rg "expect.*status.*toBe" lib/content/primitives/__tests__/handlePrimitiveTrigger.test.ts

Repository: recoupable/api

Length of output: 217


Tests exist for the handler abstraction but lack endpoint-level coverage for authentication error paths.

The existing tests in lib/content/primitives/__tests__/handlePrimitiveTrigger.test.ts cover the handler's success (202) and some error paths (400, 500), but mock validatePrimitiveBody, which means the actual authentication error paths (401, 403) that validateAuthContext() produces are never exercised. No endpoint-level integration tests for this route were found.

Per the coding guidelines, all API endpoints should have tests covering success and error paths. Add integration tests covering:

  • Success: 202 response with valid auth and data
  • Auth errors: 401 (missing/invalid credentials), 403 (unauthorized account access)
  • Validation errors: 400 (invalid request body)
  • Server errors: 500 (trigger service failure)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/content/create/image/route.ts` around lines 5 - 7, Add endpoint-level
integration tests for the create-image route (exports: handler, POST from
createPrimitiveRoute) that exercise real auth behavior instead of mocking
validatePrimitiveBody: write tests that hit the POST endpoint backed by
createPrimitiveHandler + createImageBodySchema and cover (1) success: valid auth
and body returns 202; (2) auth errors: missing/invalid credentials produce 401
and unauthorized account produce 403 by calling the real validateAuthContext
flow; (3) validation error: invalid request body returns 400 from the
createImageBodySchema validation; and (4) server error: simulate the underlying
service throwing to get 500; ensure tests only mock external services (e.g.,
storage/service clients) but not validateAuthContext for auth-path assertions
and use the same route exports (handler/POST) to exercise endpoint semantics.

* Provides CORS OPTIONS, the POST handler, and Next.js dynamic config.
*/
export function createPrimitiveRoute(handler: (req: NextRequest) => Promise<NextResponse>) {
const OPTIONS = () => new NextResponse(null, { status: 204, headers: getCorsHeaders() });
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

Restrict wildcard CORS on authenticated primitive endpoints.

Line 10 uses global wildcard CORS headers. Since these routes accept Authorization / x-api-key, this broadens cross-origin read access for authenticated calls more than necessary. Prefer an explicit origin allowlist for these endpoints.

🔧 Suggested direction
- const OPTIONS = () => new NextResponse(null, { status: 204, headers: getCorsHeaders() });
+ const OPTIONS = (request: NextRequest) =>
+   new NextResponse(null, {
+     status: 204,
+     headers: getCorsHeaders(/* origin-aware allowlist */),
+   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/primitiveRoute.ts` at line 10, The OPTIONS handler
currently returns global wildcard CORS via the OPTIONS constant and
getCorsHeaders(), which is unsafe for endpoints accepting
Authorization/x-api-key; replace this by checking the incoming request Origin
header against an explicit allowlist and only returning that Origin (or no CORS)
when it matches. Update the OPTIONS constant to read
request.headers.get('origin'), validate it against a configured allowlist, and
return a NextResponse with Access-Control-Allow-Origin set to the matched origin
(and appropriate allow headers) or a 204 with no CORS; alternatively implement a
helper (e.g., getRestrictedCorsHeaders or extend getCorsHeaders to accept an
origin) and use that in OPTIONS and in other responses for authenticated
primitive endpoints. Ensure the symbols to change are OPTIONS and getCorsHeaders
so all authenticated routes stop using the wildcard.

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: 3

🧹 Nitpick comments (4)
lib/content/primitives/createTextHandler.ts (1)

14-96: Refactor createTextHandler into smaller helpers (currently too large and multi-purpose).

createTextHandler is handling too many responsibilities in one block (validation flow, prompt building, upstream call, parsing/normalization, response shaping). Splitting this into private helpers will improve maintainability and testability.

♻️ Proposed refactor
 export async function createTextHandler(request: NextRequest): Promise<NextResponse> {
   const validated = await validatePrimitiveBody(request, createTextBodySchema);
   if (validated instanceof NextResponse) return validated;

   const { data } = validated;

   try {
     const recoupApiUrl = process.env.RECOUP_API_URL ?? "https://recoup-api.vercel.app";
     const recoupApiKey = process.env.RECOUP_API_KEY;
     if (!recoupApiKey) {
       return NextResponse.json(
         { status: "error", error: "RECOUP_API_KEY is not configured" },
         { status: 500, headers: getCorsHeaders() },
       );
     }

-    const prompt = `Generate ONE short on-screen text for a social media video.
-Song or theme: "${data.song}"
-Length: ${data.length}
-Return ONLY the text, nothing else. No quotes.`;
+    const prompt = buildPrompt(data.song, data.length);

     const controller = new AbortController();
     const timeout = setTimeout(() => controller.abort(), 30_000);
     try {
-      const response = await fetch(`${recoupApiUrl}/api/chat/generate`, {
-        method: "POST",
-        headers: { "Content-Type": "application/json", "x-api-key": recoupApiKey },
-        body: JSON.stringify({
-          prompt,
-          model: "google/gemini-2.5-flash",
-          excludeTools: ["create_task"],
-        }),
-        signal: controller.signal,
-      });
+      const response = await requestGeneratedText(recoupApiUrl, recoupApiKey, prompt, controller.signal);

       if (!response.ok) {
         return NextResponse.json(
           { status: "error", error: `Text generation failed: ${response.status}` },
           { status: 502, headers: getCorsHeaders() },
         );
       }

       const json = (await response.json()) as {
         text?: string | Array<{ type: string; text?: string }>;
       };

-      let content: string;
-      if (typeof json.text === "string") {
-        content = json.text.trim();
-      } else if (Array.isArray(json.text)) {
-        content = json.text
-          .filter(p => p.type === "text" && p.text)
-          .map(p => p.text!)
-          .join("")
-          .trim();
-      } else {
-        content = "";
-      }
-
-      content = content.replace(/^["']|["']$/g, "").trim();
+      const content = normalizeGeneratedText(json.text);

       if (!content) {
         return NextResponse.json(
           { status: "error", error: "Text generation returned empty" },
           { status: 502, headers: getCorsHeaders() },
         );
       }
@@
   }
 }
+
+function buildPrompt(song: string, length: string): string {
+  return `Generate ONE short on-screen text for a social media video.
+Song or theme: "${song}"
+Length: ${length}
+Return ONLY the text, nothing else. No quotes.`;
+}
+
+async function requestGeneratedText(
+  recoupApiUrl: string,
+  recoupApiKey: string,
+  prompt: string,
+  signal: AbortSignal,
+): Promise<Response> {
+  return fetch(`${recoupApiUrl}/api/chat/generate`, {
+    method: "POST",
+    headers: { "Content-Type": "application/json", "x-api-key": recoupApiKey },
+    body: JSON.stringify({
+      prompt,
+      model: "google/gemini-2.5-flash",
+      excludeTools: ["create_task"],
+    }),
+    signal,
+  });
+}
+
+function normalizeGeneratedText(text: string | Array<{ type: string; text?: string }> | undefined): string {
+  const content =
+    typeof text === "string"
+      ? text.trim()
+      : Array.isArray(text)
+        ? text
+            .filter(p => p.type === "text" && p.text)
+            .map(p => p.text!)
+            .join("")
+            .trim()
+        : "";
+
+  return content.replace(/^["']|["']$/g, "").trim();
+}

As per coding guidelines: “For domain functions, ensure: Single responsibility per function … Keep functions under 50 lines … DRY … KISS.”

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

In `@lib/content/primitives/createTextHandler.ts` around lines 14 - 96,
createTextHandler is doing too much; split it into focused helpers: keep
createTextHandler for orchestration (validation via validatePrimitiveBody and
early error responses), move prompt construction into buildTextPrompt(data), the
upstream fetch into callRecoupGenerate(prompt, recoupApiUrl, recoupApiKey,
signal) which returns the parsed JSON, move parsing/normalization into
parseGeneratedText(json) to produce a clean content string, and move the final
response shaping into shapeTextResponse(content) which returns the object sent
in NextResponse.json; update createTextHandler to call these helpers, preserve
the existing error/timeout handling (AbortController/clearTimeout) and all
existing checks (RECOUP_API_KEY, response.ok, empty content) and reuse symbols
validatePrimitiveBody and createTextBodySchema unchanged.
lib/content/primitives/createAudioHandler.ts (3)

39-39: Unnecessary as string cast on a string literal.

"fal-ai/whisper" is already a string literal; the as string cast adds noise without value.

✂️ Remove redundant cast
-    const result = await fal.subscribe("fal-ai/whisper" as string, {
+    const result = await fal.subscribe("fal-ai/whisper", {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/createAudioHandler.ts` at line 39, The call to
fal.subscribe in createAudioHandler.ts unnecessarily casts the string literal
"fal-ai/whisper" with as string; remove the redundant cast so the call reads
fal.subscribe("fal-ai/whisper", {...}) (locate the invocation of fal.subscribe
that assigns to result and drop the unnecessary as string).

54-58: Array index access on chunk.timestamp could be fragile.

Accessing chunk.timestamp[0] and chunk.timestamp[1] assumes the array always has at least two elements. The nullish coalescing (?? 0) provides a fallback, but if the Whisper API ever returns malformed data, this could silently produce incorrect timing values.

A defensive check or destructuring with defaults would be slightly more explicit:

🛡️ More defensive timestamp extraction
     const segments = (whisperData.chunks ?? []).map(chunk => ({
-      start: chunk.timestamp[0] ?? 0,
-      end: chunk.timestamp[1] ?? 0,
+      start: Array.isArray(chunk.timestamp) ? (chunk.timestamp[0] ?? 0) : 0,
+      end: Array.isArray(chunk.timestamp) ? (chunk.timestamp[1] ?? 0) : 0,
       text: chunk.text?.trim() ?? "",
     }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/createAudioHandler.ts` around lines 54 - 58, The
mapping over whisperData.chunks in createAudioHandler.ts currently uses
chunk.timestamp[0] and [1] directly which is fragile; update the segment
extraction to defensively destructure and validate timestamps (e.g., const
[start = 0, end = 0] = Array.isArray(chunk.timestamp) ? chunk.timestamp : [])
and ensure start and end are numbers (or skip the chunk) before using them, so
segments uses safe defaults or excludes malformed entries when building the
segments array.

48-51: Use Zod validation for fal-ai/whisper response shape instead of type casting.

The double cast as unknown as { ... } on lines 48-51 bypasses TypeScript's type checking. Since the codebase already uses Zod extensively for validation, define a schema for the whisper response (e.g., whisperResponseSchema) and validate result.data against it. This ensures type safety at runtime and maintains consistency with the existing validation pattern used throughout the codebase.

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

In `@lib/content/primitives/createAudioHandler.ts` around lines 48 - 51, Replace
the unsafe double-cast for whisperData by defining a Zod schema (e.g.,
whisperResponseSchema) that matches the fal-ai/whisper response shape (fields:
optional text: string and optional chunks: Array<{ timestamp: number[]; text:
string }>), then parse/validate result.data with whisperResponseSchema.parse or
safeParse inside createAudioHandler (referencing whisperData and result.data)
and use the validated type instead of the cast; on parse failure throw or handle
the validation error consistently with existing validation patterns in the
codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Line 26: The handler currently calls fal.config({ credentials: falKey })
inside createAudioHandler, mutating module-global Fal configuration per request
and risking race conditions; instead, initialize and configure the Fal client
once at module scope (e.g., create a single falClient or call fal.config(...)
once during module initialization guarded by an environment check) and have
createAudioHandler (and the other primitives createImageHandler,
createVideoHandler, createUpscaleHandler) reuse that singleton client; remove
per-request fal.config calls and replace any reference to the local fal config
with the shared falClient to ensure thread-safe, consistent configuration.
- Around line 39-46: createAudioHandler may hang because fal.subscribe is called
without an AbortSignal; add the same 30s timeout pattern used in
createTextHandler by creating an AbortController, pass controller.signal as the
abortSignal option to fal.subscribe, and set a setTimeout to call
controller.abort() after 30_000 ms (clearing the timer after successful
completion or error). Update the call site for fal.subscribe in
createAudioHandler to include abortSignal and ensure any timer is cleared in
both success and catch/finally paths to avoid leaks.

In `@lib/content/primitives/createVideoHandler.ts`:
- Around line 32-51: The handler silently falls back to image-to-video when
data.lipsync is true but data.song_url is missing; add explicit validation at
the start of the create video flow (the function containing the current block
where data.lipsync and data.song_url are read) to detect if data.lipsync &&
!data.song_url and return a 400 error (or throw a validation error) with a clear
message like "song_url is required when lipsync is true"; alternatively,
implement the same dependency as a schema refinement, but if fixing here,
short-circuit before calling fal.subscribe so
fal.subscribe("fal-ai/ltx-2-19b/audio-to-video", ...) is only invoked when
song_url is present.

---

Nitpick comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Line 39: The call to fal.subscribe in createAudioHandler.ts unnecessarily
casts the string literal "fal-ai/whisper" with as string; remove the redundant
cast so the call reads fal.subscribe("fal-ai/whisper", {...}) (locate the
invocation of fal.subscribe that assigns to result and drop the unnecessary as
string).
- Around line 54-58: The mapping over whisperData.chunks in
createAudioHandler.ts currently uses chunk.timestamp[0] and [1] directly which
is fragile; update the segment extraction to defensively destructure and
validate timestamps (e.g., const [start = 0, end = 0] =
Array.isArray(chunk.timestamp) ? chunk.timestamp : []) and ensure start and end
are numbers (or skip the chunk) before using them, so segments uses safe
defaults or excludes malformed entries when building the segments array.
- Around line 48-51: Replace the unsafe double-cast for whisperData by defining
a Zod schema (e.g., whisperResponseSchema) that matches the fal-ai/whisper
response shape (fields: optional text: string and optional chunks: Array<{
timestamp: number[]; text: string }>), then parse/validate result.data with
whisperResponseSchema.parse or safeParse inside createAudioHandler (referencing
whisperData and result.data) and use the validated type instead of the cast; on
parse failure throw or handle the validation error consistently with existing
validation patterns in the codebase.

In `@lib/content/primitives/createTextHandler.ts`:
- Around line 14-96: createTextHandler is doing too much; split it into focused
helpers: keep createTextHandler for orchestration (validation via
validatePrimitiveBody and early error responses), move prompt construction into
buildTextPrompt(data), the upstream fetch into callRecoupGenerate(prompt,
recoupApiUrl, recoupApiKey, signal) which returns the parsed JSON, move
parsing/normalization into parseGeneratedText(json) to produce a clean content
string, and move the final response shaping into shapeTextResponse(content)
which returns the object sent in NextResponse.json; update createTextHandler to
call these helpers, preserve the existing error/timeout handling
(AbortController/clearTimeout) and all existing checks (RECOUP_API_KEY,
response.ok, empty content) and reuse symbols validatePrimitiveBody and
createTextBodySchema unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f73090b4-9322-494b-9c75-459fb319758e

📥 Commits

Reviewing files that changed from the base of the PR and between 330bfe0 and ac5e069.

⛔ Files ignored due to path filters (32)
  • lib/admins/emails/__tests__/validateGetAdminEmailsQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/admins/pr/__tests__/getPrMergedStatusHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/agents/content/__tests__/handleContentAgentCallback.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artists/__tests__/createArtistPostHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artists/__tests__/validateCreateArtistBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/auth/__tests__/validateAuthContext.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chat/__tests__/integration/chatEndToEnd.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chats/__tests__/createChatHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/handleGitHubWebhook.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/onMergeTestToMainAction.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/validateCreateContentBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/primitives/__tests__/validatePrimitiveBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/evals/callChatFunctions.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/callChatFunctionsWithResult.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/createToolsCalledScorer.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/extractTextFromResult.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/extractTextResultFromSteps.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/getCatalogSongsCountExpected.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/getSpotifyFollowersExpected.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/scorers/CatalogAvailability.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/scorers/QuestionAnswered.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/scorers/ToolsCalled.ts is excluded by !**/evals/** and included by lib/**
  • lib/flamingo/__tests__/getFlamingoPresetsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/github/__tests__/createOrUpdateFileContent.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/notifications/__tests__/createNotificationHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/notifications/__tests__/validateCreateNotificationBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/enrichTaskWithTriggerInfo.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/getTaskRunHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/validateGetTaskRunQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/validateGetTasksQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • package.json is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
📒 Files selected for processing (71)
  • app/api/accounts/[id]/route.ts
  • app/api/admins/coding/slack/route.ts
  • app/api/admins/privy/route.ts
  • app/api/content/create/audio/route.ts
  • app/api/content/create/image/route.ts
  • app/api/content/create/render/route.ts
  • app/api/content/create/text/route.ts
  • app/api/content/create/upscale/route.ts
  • app/api/content/create/video/route.ts
  • app/api/songs/analyze/presets/route.ts
  • app/api/transcribe/route.ts
  • lib/admins/pr/getPrStatusHandler.ts
  • lib/admins/privy/countNewAccounts.ts
  • lib/admins/privy/fetchPrivyLogins.ts
  • lib/admins/privy/getCutoffMs.ts
  • lib/admins/privy/getLatestVerifiedAt.ts
  • lib/admins/privy/toMs.ts
  • lib/ai/getModel.ts
  • lib/ai/isEmbedModel.ts
  • lib/catalog/formatCatalogSongsAsCSV.ts
  • lib/catalog/getCatalogDataAsCSV.ts
  • lib/catalog/getCatalogSongs.ts
  • lib/catalog/getCatalogs.ts
  • lib/chat/toolChains/getPrepareStepResult.ts
  • lib/chats/processCompactChatRequest.ts
  • lib/coding-agent/encodeGitHubThreadId.ts
  • lib/coding-agent/handleMergeSuccess.ts
  • lib/coding-agent/parseMergeActionId.ts
  • lib/coding-agent/parseMergeTestToMainActionId.ts
  • lib/composio/getCallbackUrl.ts
  • lib/content/contentTemplates.ts
  • lib/content/getArtistContentReadiness.ts
  • lib/content/getArtistFileTree.ts
  • lib/content/getArtistRootPrefix.ts
  • lib/content/getContentValidateHandler.ts
  • lib/content/isCompletedRun.ts
  • lib/content/persistCreateContentRunVideo.ts
  • lib/content/primitives/createAudioHandler.ts
  • lib/content/primitives/createImageHandler.ts
  • lib/content/primitives/createRenderHandler.ts
  • lib/content/primitives/createTextHandler.ts
  • lib/content/primitives/createUpscaleHandler.ts
  • lib/content/primitives/createVideoHandler.ts
  • lib/content/primitives/schemas.ts
  • lib/content/primitives/validatePrimitiveBody.ts
  • lib/content/validateGetContentEstimateQuery.ts
  • lib/content/validateGetContentValidateQuery.ts
  • lib/credits/getCreditUsage.ts
  • lib/credits/handleChatCredits.ts
  • lib/emails/processAndSendEmail.ts
  • lib/flamingo/getFlamingoPresetsHandler.ts
  • lib/github/expandSubmoduleEntries.ts
  • lib/github/getRepoGitModules.ts
  • lib/github/resolveSubmodulePath.ts
  • lib/mcp/resolveAccountId.ts
  • lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts
  • lib/prompts/getSystemPrompt.ts
  • lib/slack/getBotChannels.ts
  • lib/slack/getBotUserId.ts
  • lib/slack/getSlackUserInfo.ts
  • lib/spotify/getSpotifyFollowers.ts
  • lib/supabase/account_artist_ids/getAccountArtistIds.ts
  • lib/supabase/account_workspace_ids/getAccountWorkspaceIds.ts
  • lib/supabase/files/createFileRecord.ts
  • lib/supabase/song_artists/insertSongArtists.ts
  • lib/supabase/storage/uploadFileByKey.ts
  • lib/transcribe/processAudioTranscription.ts
  • lib/transcribe/saveAudioToFiles.ts
  • lib/transcribe/saveTranscriptToFiles.ts
  • lib/transcribe/types.ts
  • lib/trigger/triggerPrimitive.ts
✅ Files skipped from review due to trivial changes (59)
  • lib/ai/getModel.ts
  • lib/slack/getBotChannels.ts
  • lib/coding-agent/parseMergeActionId.ts
  • lib/catalog/getCatalogDataAsCSV.ts
  • lib/composio/getCallbackUrl.ts
  • lib/admins/privy/toMs.ts
  • lib/chats/processCompactChatRequest.ts
  • lib/admins/privy/getLatestVerifiedAt.ts
  • lib/slack/getBotUserId.ts
  • lib/supabase/files/createFileRecord.ts
  • app/api/transcribe/route.ts
  • lib/coding-agent/parseMergeTestToMainActionId.ts
  • lib/content/validateGetContentValidateQuery.ts
  • lib/supabase/song_artists/insertSongArtists.ts
  • lib/emails/processAndSendEmail.ts
  • lib/transcribe/saveAudioToFiles.ts
  • lib/mcp/resolveAccountId.ts
  • app/api/accounts/[id]/route.ts
  • lib/chat/toolChains/getPrepareStepResult.ts
  • lib/coding-agent/handleMergeSuccess.ts
  • lib/supabase/account_workspace_ids/getAccountWorkspaceIds.ts
  • lib/spotify/getSpotifyFollowers.ts
  • lib/catalog/formatCatalogSongsAsCSV.ts
  • lib/admins/pr/getPrStatusHandler.ts
  • lib/content/persistCreateContentRunVideo.ts
  • lib/content/getArtistContentReadiness.ts
  • lib/content/getContentValidateHandler.ts
  • lib/transcribe/saveTranscriptToFiles.ts
  • lib/content/validateGetContentEstimateQuery.ts
  • lib/github/resolveSubmodulePath.ts
  • lib/catalog/getCatalogs.ts
  • app/api/songs/analyze/presets/route.ts
  • lib/content/contentTemplates.ts
  • lib/transcribe/processAudioTranscription.ts
  • lib/supabase/storage/uploadFileByKey.ts
  • lib/github/getRepoGitModules.ts
  • lib/admins/privy/fetchPrivyLogins.ts
  • app/api/admins/coding/slack/route.ts
  • lib/credits/getCreditUsage.ts
  • lib/transcribe/types.ts
  • lib/content/getArtistFileTree.ts
  • lib/prompts/getSystemPrompt.ts
  • lib/coding-agent/encodeGitHubThreadId.ts
  • lib/admins/privy/countNewAccounts.ts
  • lib/credits/handleChatCredits.ts
  • lib/content/isCompletedRun.ts
  • lib/supabase/account_artist_ids/getAccountArtistIds.ts
  • lib/admins/privy/getCutoffMs.ts
  • lib/github/expandSubmoduleEntries.ts
  • lib/slack/getSlackUserInfo.ts
  • app/api/admins/privy/route.ts
  • lib/catalog/getCatalogSongs.ts
  • lib/content/getArtistRootPrefix.ts
  • lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts
  • lib/flamingo/getFlamingoPresetsHandler.ts
  • app/api/content/create/audio/route.ts
  • lib/trigger/triggerPrimitive.ts
  • lib/ai/isEmbedModel.ts
  • lib/content/primitives/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/content/primitives/validatePrimitiveBody.ts
  • app/api/content/create/text/route.ts

Comment on lines +39 to +46
const result = await fal.subscribe("fal-ai/whisper" as string, {
input: {
audio_url: songUrl,
task: "transcribe",
chunk_level: "word",
language: "en",
},
});
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:

#!/bin/bash
# Check how createTextHandler implements timeout for consistency
ast-grep --pattern 'AbortController'

Repository: recoupable/api

Length of output: 318


🏁 Script executed:

#!/bin/bash
# Get the full createAudioHandler.ts to see complete implementation
cat -n lib/content/primitives/createAudioHandler.ts

Repository: recoupable/api

Length of output: 2966


🏁 Script executed:

#!/bin/bash
# Get the full createTextHandler.ts to see how timeout is implemented
cat -n lib/content/primitives/createTextHandler.ts

Repository: recoupable/api

Length of output: 3996


🏁 Script executed:

#!/bin/bash
# Search for fal.subscribe usages to understand its API and timeout handling
rg "fal\.subscribe" -A 10 -B 2

Repository: recoupable/api

Length of output: 5146


🏁 Script executed:

#!/bin/bash
# Check if any fal.subscribe calls use signal parameter in the codebase
rg "fal\.subscribe.*signal"

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check fal package.json to understand the fal-ai/client library version
rg '"@fal-ai/client"' -A 2 -B 2 package.json

Repository: recoupable/api

Length of output: 231


🌐 Web query:

fal.subscribe API documentation signal AbortController timeout

💡 Result:

The fal.subscribe API from the @fal-ai/client JavaScript library supports AbortController via AbortSignal for request cancellation and AbortSignal.timeout for client-side timeouts. This feature was added in version 1.2.0 (November 2024). ## Usage fal.subscribe accepts RunOptions (shared with fal.run), which includes: - abortSignal?: AbortSignal - Pass AbortController.signal to cancel the request. - startTimeout?: number - Server-side timeout in seconds (sent as x-fal-request-timeout header). - timeout (client-side): Use AbortSignal.timeout(ms) for client timeout. Example: import { fal } from "@fal-ai/client"; const controller = new AbortController; const timeoutSignal = AbortSignal.timeout(60000); // 60s client timeout const result = await fal.subscribe("fal-ai/flux/dev", { input: { prompt: "a cat" }, abortSignal: AbortSignal.any([controller.signal, timeoutSignal]), // Abort on either startTimeout: 30, // Server start timeout 30s }); controller.abort; // Cancel anytime Cancellation aborts client polling/waiting. For queued requests, use fal.queue.cancel(requestId). Server-side, implement /predict/cancel endpoints to handle GPU cleanup. Official docs: https://docs.fal.ai/reference/client-libraries/javascript/types.common (RunOptions), https://fal-ai.github.io/fal-js/reference/interfaces/FalClient.html (FalClient.subscribe), GitHub issue confirming support: fal-ai/fal-js#84.

Citations:


Add timeout to prevent indefinite hangs from fal.ai Whisper calls.

createAudioHandler lacks timeout protection while createTextHandler uses a 30-second timeout. If the Whisper transcription hangs, the request blocks indefinitely. The @fal-ai/client (v1.9.5) supports AbortSignal for request cancellation via the abortSignal parameter.

⏱️ Suggested: add timeout consistency with other handlers
+    const timeoutSignal = AbortSignal.timeout(30_000); // 30s timeout to match createTextHandler
+
     const result = await fal.subscribe("fal-ai/whisper" as string, {
       input: {
         audio_url: songUrl,
         task: "transcribe",
         chunk_level: "word",
         language: "en",
       },
+      abortSignal: timeoutSignal,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/createAudioHandler.ts` around lines 39 - 46,
createAudioHandler may hang because fal.subscribe is called without an
AbortSignal; add the same 30s timeout pattern used in createTextHandler by
creating an AbortController, pass controller.signal as the abortSignal option to
fal.subscribe, and set a setTimeout to call controller.abort() after 30_000 ms
(clearing the timer after successful completion or error). Update the call site
for fal.subscribe in createAudioHandler to include abortSignal and ensure any
timer is cleared in both success and catch/finally paths to avoid leaks.

Comment on lines +32 to +51
if (data.lipsync && data.song_url) {
const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, {
input: {
image_url: data.image_url,
audio_url: data.song_url,
prompt: data.motion_prompt ?? "person staring at camera, subtle movement",
},
});
const resultData = result.data as Record<string, unknown>;
videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
} else {
const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, {
input: {
image_url: data.image_url,
prompt: data.motion_prompt ?? "nearly still, only natural breathing",
},
});
const resultData = result.data as Record<string, unknown>;
videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
}
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

Consider explicit validation when lipsync=true but song_url is missing.

The current logic silently falls through to the image-to-video model when lipsync is true but song_url is absent. This could surprise callers who expect lipsync behavior but receive a non-lipsync video without any error indication.

Per the schema (context snippet 2), there's no .refine() enforcing this dependency. Consider either:

  1. Adding a schema refinement to require song_url when lipsync=true
  2. Returning a 400 error in the handler when lipsync && !song_url
🛡️ Option: Add validation in handler
   try {
     const { data } = validated;
     let videoUrl: string | undefined;

+    if (data.lipsync && !data.song_url) {
+      return NextResponse.json(
+        { status: "error", error: "song_url is required when lipsync is enabled" },
+        { status: 400, headers: getCorsHeaders() },
+      );
+    }
+
     if (data.lipsync && data.song_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
if (data.lipsync && data.song_url) {
const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, {
input: {
image_url: data.image_url,
audio_url: data.song_url,
prompt: data.motion_prompt ?? "person staring at camera, subtle movement",
},
});
const resultData = result.data as Record<string, unknown>;
videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
} else {
const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, {
input: {
image_url: data.image_url,
prompt: data.motion_prompt ?? "nearly still, only natural breathing",
},
});
const resultData = result.data as Record<string, unknown>;
videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
}
if (data.lipsync && !data.song_url) {
return NextResponse.json(
{ status: "error", error: "song_url is required when lipsync is enabled" },
{ status: 400, headers: getCorsHeaders() },
);
}
if (data.lipsync && data.song_url) {
const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, {
input: {
image_url: data.image_url,
audio_url: data.song_url,
prompt: data.motion_prompt ?? "person staring at camera, subtle movement",
},
});
const resultData = result.data as Record<string, unknown>;
videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
} else {
const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, {
input: {
image_url: data.image_url,
prompt: data.motion_prompt ?? "nearly still, only natural breathing",
},
});
const resultData = result.data as Record<string, unknown>;
videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/createVideoHandler.ts` around lines 32 - 51, The
handler silently falls back to image-to-video when data.lipsync is true but
data.song_url is missing; add explicit validation at the start of the create
video flow (the function containing the current block where data.lipsync and
data.song_url are read) to detect if data.lipsync && !data.song_url and return a
400 error (or throw a validation error) with a clear message like "song_url is
required when lipsync is true"; alternatively, implement the same dependency as
a schema refinement, but if fixing here, short-circuit before calling
fal.subscribe so fal.subscribe("fal-ai/ltx-2-19b/audio-to-video", ...) is only
invoked when song_url is present.

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: 3

♻️ Duplicate comments (1)
lib/content/primitives/createVideoHandler.ts (1)

34-45: ⚠️ Potential issue | 🟠 Major

Enforce song_url when lipsync is enabled.

When lipsync is true and song_url is absent, the handler silently falls back to image-to-video. That’s a behavior mismatch and should return 400 explicitly.

Proposed fix
   try {
     let videoUrl: string | undefined;

+    if (validated.lipsync && !validated.song_url) {
+      return NextResponse.json(
+        { status: "error", error: "song_url is required when lipsync is enabled" },
+        { status: 400, headers: getCorsHeaders() },
+      );
+    }
+
     if (validated.lipsync && validated.song_url) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/createVideoHandler.ts` around lines 34 - 45, The
handler currently falls back to image-to-video when lipsync is true but song_url
is missing; update the logic in createVideoHandler so that when
validated.lipsync is true and validated.song_url is falsy you immediately return
a 400 response (e.g., res.status(400).json(...)) with a clear error message
instead of proceeding to the image-to-video branch; locate the conditional
around validated.lipsync and validated.song_url (the if block that calls
fal.subscribe("fal-ai/ltx-2-19b/audio-to-video"...) and add the explicit
validation+early return before calling fal.subscribe so audio-to-video is only
attempted when song_url is provided.
🧹 Nitpick comments (2)
lib/content/primitives/createVideoHandler.ts (1)

31-53: Refactor duplicate fal.subscribe branches into one flow.

The two branches duplicate request/response handling and push this function past the 50-line guideline. Consolidating model/input selection will improve readability and keep this handler lean.

Refactor sketch
   try {
-    let videoUrl: string | undefined;
-
-    if (validated.lipsync && validated.song_url) {
-      const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, {
-        input: {
-          image_url: validated.image_url,
-          audio_url: validated.song_url,
-          prompt: validated.motion_prompt ?? "person staring at camera, subtle movement",
-        },
-      });
-      const resultData = result.data as Record<string, unknown>;
-      videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
-    } else {
-      const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, {
-        input: {
-          image_url: validated.image_url,
-          prompt: validated.motion_prompt ?? "nearly still, only natural breathing",
-        },
-      });
-      const resultData = result.data as Record<string, unknown>;
-      videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;
-    }
+    const isLipsync = Boolean(validated.lipsync && validated.song_url);
+    const model = isLipsync
+      ? "fal-ai/ltx-2-19b/audio-to-video"
+      : "fal-ai/veo3.1/fast/image-to-video";
+    const input = isLipsync
+      ? {
+          image_url: validated.image_url,
+          audio_url: validated.song_url,
+          prompt: validated.motion_prompt ?? "person staring at camera, subtle movement",
+        }
+      : {
+          image_url: validated.image_url,
+          prompt: validated.motion_prompt ?? "nearly still, only natural breathing",
+        };
+
+    const result = await fal.subscribe(model, { input });
+    const resultData = result.data as Record<string, unknown>;
+    const videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;

As per coding guidelines: "lib/**/*.ts: Keep functions under 50 lines" and "**/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle."

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

In `@lib/content/primitives/createVideoHandler.ts` around lines 31 - 53, The two
fal.subscribe branches duplicate request/response handling; refactor
createVideoHandler by extracting model and input selection into local variables
(use validated.lipsync, validated.song_url to choose model string and include
audio_url only when present), then call fal.subscribe once and parse resultData
to set videoUrl from (result.data?.video)?.url; update references to
validated.motion_prompt and validated.image_url as before and keep variable
names like videoUrl, resultData, and fal.subscribe to locate the changes.
lib/content/primitives/createAudioHandler.ts (1)

15-78: Refactor createAudioHandler into smaller helpers for maintainability.

The handler is doing auth, validation, provider setup, remote call, transformation, and response shaping in one function. Extracting helpers (e.g., pickSongUrl, mapWhisperSegments, transcribeAudio) will reduce complexity and improve testability.

As per coding guidelines: "lib/**/*.ts: ... Keep functions under 50 lines".

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

In `@lib/content/primitives/createAudioHandler.ts` around lines 15 - 78,
createAudioHandler is too large—split it into small helpers: implement
pickSongUrl(validated) to find the first http song URL (used where songUrl is
determined), transcribeAudio(songUrl, falKey) to encapsulate fal.config and the
fal.subscribe call and return the raw result, and
mapWhisperSegments(whisperData) to transform result.data.chunks into the
{start,end,text} segments and extract fullLyrics; then refactor
createAudioHandler to orchestrate: call
validateAuthContext/validatePrimitiveBody as before, call pickSongUrl, call
transcribeAudio and pass its result into mapWhisperSegments, and finally return
the shaped NextResponse. Ensure each new helper is small and testable and keep
createAudioHandler under 50 lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Around line 32-33: The current songUrl extraction (variable songUrl in
createAudioHandler) uses startsWith("http") which is too permissive; update the
validation to trim the candidate string, ensure it begins with either "http://"
or "https://", and verify it is a well-formed URL (e.g., by attempting to
construct a URL object) before using it upstream; if validation fails, set
songUrl to undefined (or return a 400 response) so malformed values never reach
the downstream processing in createAudioHandler.

In `@lib/content/primitives/createImageHandler.ts`:
- Around line 39-45: The code reads provider payload into
resultData/images/image and derives imageUrl but never verifies it's a string;
update the runtime check around imageUrl (used before returning in the
NextResponse) to ensure typeof imageUrl === 'string' (or otherwise
validate/normalize it) and only return when it's a valid string; if it is
missing or not a string, return the same error/400 response path instead of a
truthy non-string value. Ensure you modify the checks that reference resultData,
images, image, and imageUrl so the API contract always returns a string URL.
- Around line 16-21: The code calls validateAuthContext but discards its result
and then accepts artist_account_id from the request body, allowing cross-account
requests; retrieve the authenticated account info from authResult (from
validateAuthContext) and enforce authorization by either removing
artist_account_id from the incoming contract or overriding/validating
validated.artist_account_id against the authenticated account id (e.g., compare
authResult.accountId or authResult.account.id to validated.artist_account_id and
return a 403/NextResponse if they differ); update createImageHandler to bind the
artist to the authenticated account (or reject when they don't match) so
artist_account_id cannot be spoofed.

---

Duplicate comments:
In `@lib/content/primitives/createVideoHandler.ts`:
- Around line 34-45: The handler currently falls back to image-to-video when
lipsync is true but song_url is missing; update the logic in createVideoHandler
so that when validated.lipsync is true and validated.song_url is falsy you
immediately return a 400 response (e.g., res.status(400).json(...)) with a clear
error message instead of proceeding to the image-to-video branch; locate the
conditional around validated.lipsync and validated.song_url (the if block that
calls fal.subscribe("fal-ai/ltx-2-19b/audio-to-video"...) and add the explicit
validation+early return before calling fal.subscribe so audio-to-video is only
attempted when song_url is provided.

---

Nitpick comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Around line 15-78: createAudioHandler is too large—split it into small
helpers: implement pickSongUrl(validated) to find the first http song URL (used
where songUrl is determined), transcribeAudio(songUrl, falKey) to encapsulate
fal.config and the fal.subscribe call and return the raw result, and
mapWhisperSegments(whisperData) to transform result.data.chunks into the
{start,end,text} segments and extract fullLyrics; then refactor
createAudioHandler to orchestrate: call
validateAuthContext/validatePrimitiveBody as before, call pickSongUrl, call
transcribeAudio and pass its result into mapWhisperSegments, and finally return
the shaped NextResponse. Ensure each new helper is small and testable and keep
createAudioHandler under 50 lines.

In `@lib/content/primitives/createVideoHandler.ts`:
- Around line 31-53: The two fal.subscribe branches duplicate request/response
handling; refactor createVideoHandler by extracting model and input selection
into local variables (use validated.lipsync, validated.song_url to choose model
string and include audio_url only when present), then call fal.subscribe once
and parse resultData to set videoUrl from (result.data?.video)?.url; update
references to validated.motion_prompt and validated.image_url as before and keep
variable names like videoUrl, resultData, and fal.subscribe to locate the
changes.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3c31caac-fb16-40f6-b80c-c4cccc389a8a

📥 Commits

Reviewing files that changed from the base of the PR and between ac5e069 and 9b7ee89.

⛔ Files ignored due to path filters (1)
  • lib/content/primitives/__tests__/validatePrimitiveBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (7)
  • lib/content/primitives/createAudioHandler.ts
  • lib/content/primitives/createImageHandler.ts
  • lib/content/primitives/createRenderHandler.ts
  • lib/content/primitives/createTextHandler.ts
  • lib/content/primitives/createUpscaleHandler.ts
  • lib/content/primitives/createVideoHandler.ts
  • lib/content/primitives/validatePrimitiveBody.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • lib/content/primitives/validatePrimitiveBody.ts
  • lib/content/primitives/createRenderHandler.ts
  • lib/content/primitives/createUpscaleHandler.ts
  • lib/content/primitives/createTextHandler.ts

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.

🧹 Nitpick comments (2)
lib/content/primitives/createAnalyzeHandler.ts (2)

32-45: Consider adding a timeout for the external API call.

The fetch call to Twelve Labs has no timeout configured. Video analysis can be a long-running operation, and without a timeout, this request could hang indefinitely, potentially exhausting server resources.

♻️ Proposed fix using AbortSignal.timeout
+  const ANALYZE_TIMEOUT_MS = 120_000; // 2 minutes for video analysis
+
   try {
     const response = await fetch(TWELVELABS_ANALYZE_URL, {
       method: "POST",
       headers: {
         "x-api-key": apiKey,
         "Content-Type": "application/json",
       },
       body: JSON.stringify({
         video: { type: "url", url: validated.video_url },
         prompt: validated.prompt,
         temperature: validated.temperature,
         stream: false,
         ...(validated.max_tokens && { max_tokens: validated.max_tokens }),
       }),
+      signal: AbortSignal.timeout(ANALYZE_TIMEOUT_MS),
     });

Then handle the timeout error in the catch block:

   } catch (error) {
+    if (error instanceof DOMException && error.name === "TimeoutError") {
+      return NextResponse.json(
+        { status: "error", error: "Video analysis timed out" },
+        { status: 504, headers: getCorsHeaders() },
+      );
+    }
     console.error("Video analysis error:", error);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/createAnalyzeHandler.ts` around lines 32 - 45, The
fetch to TWELVELABS_ANALYZE_URL (inside createAnalyzeHandler where response is
awaited) lacks a timeout and can hang; wrap the request with an AbortController
(or AbortSignal.timeout) and pass its signal into fetch, choose a sensible
timeout (e.g., configurable constant) and ensure the controller is cleaned up;
update the catch that handles the fetch to detect and handle AbortError/timeout
separately (log/return a clear timeout error) so long-running video analysis
cannot indefinitely block the server.

38-44: The stream field from validated input is ignored.

The schema (createAnalyzeBodySchema) defines a stream field that defaults to false, but the handler hardcodes stream: false on line 42 instead of using validated.stream. If streaming is intentionally unsupported, consider removing the field from the schema to avoid misleading API consumers.

♻️ Option A: Use the validated stream value
       body: JSON.stringify({
         video: { type: "url", url: validated.video_url },
         prompt: validated.prompt,
         temperature: validated.temperature,
-        stream: false,
+        stream: validated.stream,
         ...(validated.max_tokens && { max_tokens: validated.max_tokens }),
       }),
♻️ Option B: Remove stream from schema if unsupported

In lib/content/primitives/schemas.ts:

 export const createAnalyzeBodySchema = z.object({
   video_url: z.string().url(),
   prompt: z.string().min(1).max(2000),
   temperature: z.number().min(0).max(1).optional().default(0.2),
-  stream: z.boolean().optional().default(false),
   max_tokens: z.number().int().min(1).max(4096).optional(),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/primitives/createAnalyzeHandler.ts` around lines 38 - 44, The
handler is hardcoding stream: false instead of using the parsed value from
createAnalyzeBodySchema; update the request body construction in
createAnalyzeHandler (the place that builds the JSON.stringify payload with
video/prompt/temperature) to use validated.stream rather than a constant false,
or if streaming truly isn't supported remove the stream field from
createAnalyzeBodySchema and any code that expects it (ensure consistency between
createAnalyzeHandler and createAnalyzeBodySchema so the validated.stream value
is either used or no longer present).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/content/primitives/createAnalyzeHandler.ts`:
- Around line 32-45: The fetch to TWELVELABS_ANALYZE_URL (inside
createAnalyzeHandler where response is awaited) lacks a timeout and can hang;
wrap the request with an AbortController (or AbortSignal.timeout) and pass its
signal into fetch, choose a sensible timeout (e.g., configurable constant) and
ensure the controller is cleaned up; update the catch that handles the fetch to
detect and handle AbortError/timeout separately (log/return a clear timeout
error) so long-running video analysis cannot indefinitely block the server.
- Around line 38-44: The handler is hardcoding stream: false instead of using
the parsed value from createAnalyzeBodySchema; update the request body
construction in createAnalyzeHandler (the place that builds the JSON.stringify
payload with video/prompt/temperature) to use validated.stream rather than a
constant false, or if streaming truly isn't supported remove the stream field
from createAnalyzeBodySchema and any code that expects it (ensure consistency
between createAnalyzeHandler and createAnalyzeBodySchema so the validated.stream
value is either used or no longer present).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ce9f8795-1286-4eb2-95d3-bd4755000619

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7ee89 and 619a04f.

⛔ Files ignored due to path filters (2)
  • lib/content/primitives/__tests__/createAnalyzeHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/primitives/__tests__/schemas.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • app/api/content/analyze/route.ts
  • lib/content/primitives/createAnalyzeHandler.ts
  • lib/content/primitives/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/content/primitives/schemas.ts

sidneyswift and others added 28 commits April 9, 2026 11:51
generate-image → image, generate-video → video,
generate-caption → caption, transcribe-audio → transcribe,
analyze-video → analyze. Edit merged into video as PATCH.

Made-with: Cursor
…call

Removes the fetch to /api/chat/generate (API calling itself over HTTP).
Now uses generateText from lib/ai/generateText with LIGHTWEIGHT_MODEL.

Eliminates: network round trip, RECOUP_API_KEY dependency for captions,
30s timeout, and the env var debugging headache.

Made-with: Cursor
DRY: FAL_KEY check + fal.config() was duplicated in 4 handlers.
Extracted to configureFal() — single shared helper.

SRP: Video handler was doing mode inference, field mapping, and
fal call in one function. Extracted buildFalInput() to handle
mode-specific field name mapping (reference→image_urls,
first-last→first_frame_url/last_frame_url, etc.).

Made-with: Cursor
- Removed stream field from createAnalyzeBodySchema (was always
  hardcoded to false in handler — misleading)
- Extracted primitiveOptionsHandler from createPrimitiveRoute for
  reuse in video route (which needs both POST and PATCH)

Made-with: Cursor
Templates are static JSON configs that each primitive applies
server-side when template param is passed:

- generate-image: uses template prompt, picks random reference image,
  appends style rules
- generate-caption: injects template caption guide + examples into
  LLM system prompt
- generate-video: picks random mood + movement from template for
  motion prompt
- edit (PATCH video): loads template edit operations as defaults

4 templates shipped: artist-caption-bedroom, artist-caption-outside,
artist-caption-stage, album-record-store. Reference images uploaded
to Supabase storage with signed URLs.

GET /api/content/templates now returns id + description (like skills).
Override priority: caller params > template defaults.

Made-with: Cursor
…ools

- Move PATCH edit handler from /api/content/video to /api/content
- Add GET /api/content/templates/[id] detail endpoint
- Add template field to video body schema
- Make pipeline template optional (remove default)
- Create 9 content MCP tools via fetch-proxy DRY pattern
  (generate_image, generate_video, generate_caption, transcribe_audio,
   edit_content, upscale_content, analyze_video, list_templates, create_content)
- All 1749 tests pass

Made-with: Cursor
pnpm 9 requires the packages field in workspace config. Adding empty
array since this is not a monorepo. Also fixes onlyBuiltDependencies
format to use proper YAML array syntax.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Not needed for this non-monorepo project. The file was causing CI
failures (packages field missing or empty).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts 76 files that only had JSDoc @param tag additions or import
reordering — no functional changes. Keeps the PR focused on the
content primitive endpoints feature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove default template from validateCreateContentBody (malleable mode)
- Only validate template when provided
- Cast template edit operations to satisfy discriminated union type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the createPrimitiveRoute abstraction with explicit OPTIONS/POST
exports matching the convention used by every other endpoint in the repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace re-export with explicit GET function definition to match the
convention used by every other endpoint in the repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move fal client configuration out of content primitives into its own
domain directory, consistent with lib/ organization conventions.

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

Move each handler into a descriptive subdirectory matching its API route:
- primitives/createImageHandler.ts → image/createImageHandler.ts
- primitives/createVideoHandler.ts → video/createVideoHandler.ts
- primitives/createTextHandler.ts → caption/createTextHandler.ts
- primitives/createAudioHandler.ts → transcribe/createAudioHandler.ts
- primitives/createUpscaleHandler.ts → upscale/createUpscaleHandler.ts
- primitives/createAnalyzeHandler.ts → analyze/createAnalyzeHandler.ts
- primitives/editHandler.ts → edit/editHandler.ts
- primitives/schemas.ts → content/schemas.ts (shared)
- primitives/validatePrimitiveBody.ts → content/validatePrimitiveBody.ts (shared)

All 80 content tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. lib/fal/server.ts: export configured fal client (like supabase serverClient)
2. caption/composeCaptionPrompt.ts: extract to own file (SRP)
3. lib/twelvelabs/analyzeVideo.ts: extract fetch + API key handling (SRP)
4. image/buildImageInput.ts: extract URL generation logic (SRP)
5. templates/index.ts: use satisfies instead of unknown cast (KISS)

All 80 content tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address 5 additional PR review comments:
1. transcribe/transcribeAudio.ts: extract fal transcription logic
2. upscale/upscaleMedia.ts: extract fal upscale logic
3. video/inferMode.ts: extract mode inference to own file
4. video/buildFalInput.ts: extract fal input builder to own file
5. video/generateVideo.ts: extract fal generation logic

Each handler now only does auth, validation, and response formatting.
All 80 content tests pass.

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

Address PR review comments:
1. Move all schemas from schemas.ts into domain-specific validate files
   (validateCreateImageBody, validateCreateVideoBody, etc.)
2. Include validateAuthContext inside each validate function
3. analyzeVideo now accepts raw validated object (KISS)
4. Rename tpl → template (no abbreviations)
5. Delete schemas.ts and validatePrimitiveBody.ts
6. Fix formatting

All 78 content tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Convert all 4 template JSON files to .ts files that export natively
typed Template objects. Removes all casts and satisfies workarounds
from index.ts — templates are now imported with full type safety.

All 78 content tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- types.ts: Template and TemplateEditOperation interfaces
- loadTemplate.ts: load a template by ID
- listTemplates.ts: list all template summaries
- index.ts: re-exports only (no logic)
- Fix circular import: template files now import from types.ts

All 78 content tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inline auth resolution and fetch directly into each MCP tool. Removes
the opaque proxy layer that obfuscated auth logic. Each tool now
explicitly resolves accountId, validates the API key, and makes the
fetch call.

All 1792 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove entire lib/mcp/tools/content/ directory and its registration.
These MCP tools are not defined in the API docs and should not be
included in this PR which focuses on REST endpoints only.

All 1792 tests pass.

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

1. Extract TEMPLATES to templates.ts — single source of truth for both
   loadTemplate and listTemplates
2. Export TEMPLATE_IDS const array, use z.enum(TEMPLATE_IDS) in all
   validate functions for fast-fail on invalid template
3. editHandler passes raw validated to triggerPrimitive (KISS)

All 78 content tests pass.

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align with the working model from the tasks codebase. The previous
fal-ai/veo3.1 model returned "Unprocessable Entity".

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

3 participants