feat: add modular content creation primitive endpoints#390
feat: add modular content creation primitive endpoints#390recoupableorg wants to merge 51 commits intotestfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (2)
lib/content/primitives/createTextHandler.ts (1)
11-80: SplitcreateTextHandlerinto 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: UsecreatePrimitiveRoutehere 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
⛔ Files ignored due to path filters (3)
lib/content/primitives/__tests__/handlePrimitiveTrigger.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/schemas.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/validatePrimitiveBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (12)
app/api/content/create/audio/route.tsapp/api/content/create/image/route.tsapp/api/content/create/render/route.tsapp/api/content/create/text/route.tsapp/api/content/create/upscale/route.tsapp/api/content/create/video/route.tslib/content/primitives/createTextHandler.tslib/content/primitives/handlePrimitiveTrigger.tslib/content/primitives/primitiveRoute.tslib/content/primitives/schemas.tslib/content/primitives/validatePrimitiveBody.tslib/trigger/triggerPrimitive.ts
| 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 }; |
There was a problem hiding this comment.
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.
| const handler = createPrimitiveHandler("create-image", createImageBodySchema); | ||
| export const { OPTIONS, POST } = createPrimitiveRoute(handler); | ||
| export { dynamic, fetchCache, revalidate }; |
There was a problem hiding this comment.
🧩 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' \
-C1Repository: recoupable/api
Length of output: 50371
🏁 Script executed:
# Check the test file that was found
wc -l lib/content/primitives/__tests__/handlePrimitiveTrigger.test.tsRepository: recoupable/api
Length of output: 124
🏁 Script executed:
# Read the complete test file to assess coverage
cat -n lib/content/primitives/__tests__/handlePrimitiveTrigger.test.tsRepository: recoupable/api
Length of output: 3012
🏁 Script executed:
# Check the createPrimitiveHandler and createPrimitiveRoute implementations
find . -name "*.ts" -path "*/lib/content/primitives/*" ! -path "*/__tests__/*" | head -10Repository: recoupable/api
Length of output: 283
🏁 Script executed:
# Look for the route handler implementation
cat -n app/api/content/create/image/route.tsRepository: 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.tsRepository: recoupable/api
Length of output: 1494
🏁 Script executed:
# Check the primitiveRoute implementation
cat -n lib/content/primitives/primitiveRoute.tsRepository: 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 -5Repository: 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.tsRepository: 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() }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
lib/content/primitives/createTextHandler.ts (1)
14-96: RefactorcreateTextHandlerinto smaller helpers (currently too large and multi-purpose).
createTextHandleris 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: Unnecessaryas stringcast on a string literal.
"fal-ai/whisper"is already a string literal; theas stringcast 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 onchunk.timestampcould be fragile.Accessing
chunk.timestamp[0]andchunk.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 validateresult.dataagainst 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
⛔ Files ignored due to path filters (32)
lib/admins/emails/__tests__/validateGetAdminEmailsQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/admins/pr/__tests__/getPrMergedStatusHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/content/__tests__/handleContentAgentCallback.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artists/__tests__/createArtistPostHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artists/__tests__/validateCreateArtistBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/auth/__tests__/validateAuthContext.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/chat/__tests__/integration/chatEndToEnd.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/chats/__tests__/createChatHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/handleGitHubWebhook.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/onMergeTestToMainAction.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/__tests__/validateCreateContentBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/validatePrimitiveBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/evals/callChatFunctions.tsis excluded by!**/evals/**and included bylib/**lib/evals/callChatFunctionsWithResult.tsis excluded by!**/evals/**and included bylib/**lib/evals/createToolsCalledScorer.tsis excluded by!**/evals/**and included bylib/**lib/evals/extractTextFromResult.tsis excluded by!**/evals/**and included bylib/**lib/evals/extractTextResultFromSteps.tsis excluded by!**/evals/**and included bylib/**lib/evals/getCatalogSongsCountExpected.tsis excluded by!**/evals/**and included bylib/**lib/evals/getSpotifyFollowersExpected.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/CatalogAvailability.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/QuestionAnswered.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/ToolsCalled.tsis excluded by!**/evals/**and included bylib/**lib/flamingo/__tests__/getFlamingoPresetsHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/github/__tests__/createOrUpdateFileContent.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/createNotificationHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/validateCreateNotificationBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/enrichTaskWithTriggerInfo.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/getTaskRunHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/validateGetTaskRunQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/validateGetTasksQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**package.jsonis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by none
📒 Files selected for processing (71)
app/api/accounts/[id]/route.tsapp/api/admins/coding/slack/route.tsapp/api/admins/privy/route.tsapp/api/content/create/audio/route.tsapp/api/content/create/image/route.tsapp/api/content/create/render/route.tsapp/api/content/create/text/route.tsapp/api/content/create/upscale/route.tsapp/api/content/create/video/route.tsapp/api/songs/analyze/presets/route.tsapp/api/transcribe/route.tslib/admins/pr/getPrStatusHandler.tslib/admins/privy/countNewAccounts.tslib/admins/privy/fetchPrivyLogins.tslib/admins/privy/getCutoffMs.tslib/admins/privy/getLatestVerifiedAt.tslib/admins/privy/toMs.tslib/ai/getModel.tslib/ai/isEmbedModel.tslib/catalog/formatCatalogSongsAsCSV.tslib/catalog/getCatalogDataAsCSV.tslib/catalog/getCatalogSongs.tslib/catalog/getCatalogs.tslib/chat/toolChains/getPrepareStepResult.tslib/chats/processCompactChatRequest.tslib/coding-agent/encodeGitHubThreadId.tslib/coding-agent/handleMergeSuccess.tslib/coding-agent/parseMergeActionId.tslib/coding-agent/parseMergeTestToMainActionId.tslib/composio/getCallbackUrl.tslib/content/contentTemplates.tslib/content/getArtistContentReadiness.tslib/content/getArtistFileTree.tslib/content/getArtistRootPrefix.tslib/content/getContentValidateHandler.tslib/content/isCompletedRun.tslib/content/persistCreateContentRunVideo.tslib/content/primitives/createAudioHandler.tslib/content/primitives/createImageHandler.tslib/content/primitives/createRenderHandler.tslib/content/primitives/createTextHandler.tslib/content/primitives/createUpscaleHandler.tslib/content/primitives/createVideoHandler.tslib/content/primitives/schemas.tslib/content/primitives/validatePrimitiveBody.tslib/content/validateGetContentEstimateQuery.tslib/content/validateGetContentValidateQuery.tslib/credits/getCreditUsage.tslib/credits/handleChatCredits.tslib/emails/processAndSendEmail.tslib/flamingo/getFlamingoPresetsHandler.tslib/github/expandSubmoduleEntries.tslib/github/getRepoGitModules.tslib/github/resolveSubmodulePath.tslib/mcp/resolveAccountId.tslib/mcp/tools/transcribe/registerTranscribeAudioTool.tslib/prompts/getSystemPrompt.tslib/slack/getBotChannels.tslib/slack/getBotUserId.tslib/slack/getSlackUserInfo.tslib/spotify/getSpotifyFollowers.tslib/supabase/account_artist_ids/getAccountArtistIds.tslib/supabase/account_workspace_ids/getAccountWorkspaceIds.tslib/supabase/files/createFileRecord.tslib/supabase/song_artists/insertSongArtists.tslib/supabase/storage/uploadFileByKey.tslib/transcribe/processAudioTranscription.tslib/transcribe/saveAudioToFiles.tslib/transcribe/saveTranscriptToFiles.tslib/transcribe/types.tslib/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
| const result = await fal.subscribe("fal-ai/whisper" as string, { | ||
| input: { | ||
| audio_url: songUrl, | ||
| task: "transcribe", | ||
| chunk_level: "word", | ||
| language: "en", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.tsRepository: 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 2Repository: 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.jsonRepository: 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:
- 1: feature request: public way to stop fal.run, fal.subscribe fal-ai/fal-js#84
- 2: https://fal-ai.github.io/fal-js/reference/interfaces/FalClient.html
- 3: https://registry.npmjs.org/%40fal-ai%2Fclient
- 4: https://docs.fal.ai/reference/client-libraries/javascript/types.common
- 5: https://docs.fal.ai/api-reference/client-libraries/javascript/queue
- 6: https://docs.fal.ai/documentation/development/handle-cancellations
- 7: https://docs.fal.ai/documentation/development/calling-your-endpoints
- 8: https://docs.fal.ai/clients/javascript
- 9: https://docs.fal.ai/reference/client-libraries/javascript
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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:
- Adding a schema refinement to require
song_urlwhenlipsync=true - 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.
| 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.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
lib/content/primitives/createVideoHandler.ts (1)
34-45:⚠️ Potential issue | 🟠 MajorEnforce
song_urlwhenlipsyncis enabled.When
lipsyncistrueandsong_urlis absent, the handler silently falls back to image-to-video. That’s a behavior mismatch and should return400explicitly.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 duplicatefal.subscribebranches 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: RefactorcreateAudioHandlerinto 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
⛔ Files ignored due to path filters (1)
lib/content/primitives/__tests__/validatePrimitiveBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (7)
lib/content/primitives/createAudioHandler.tslib/content/primitives/createImageHandler.tslib/content/primitives/createRenderHandler.tslib/content/primitives/createTextHandler.tslib/content/primitives/createUpscaleHandler.tslib/content/primitives/createVideoHandler.tslib/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
There was a problem hiding this comment.
🧹 Nitpick comments (2)
lib/content/primitives/createAnalyzeHandler.ts (2)
32-45: Consider adding a timeout for the external API call.The
fetchcall 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: Thestreamfield from validated input is ignored.The schema (
createAnalyzeBodySchema) defines astreamfield that defaults tofalse, but the handler hardcodesstream: falseon line 42 instead of usingvalidated.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
⛔ Files ignored due to path filters (2)
lib/content/primitives/__tests__/createAnalyzeHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/schemas.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (3)
app/api/content/analyze/route.tslib/content/primitives/createAnalyzeHandler.tslib/content/primitives/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/content/primitives/schemas.ts
generate-image → image, generate-video → video, generate-caption → caption, transcribe-audio → transcribe, analyze-video → analyze. Edit merged into video as PATCH. Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
…en empty) 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>
f2593fd to
3269359
Compare
Summary
Full restructure of content primitive endpoints to be generic, consistent, and well-factored.
Route restructure (verb-qualifier naming)
Generic primitives
Edit endpoint (replaces render)
Auth pattern fix
Code quality (CodeRabbit review)