Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f8fac66
feat: add modular content creation primitive endpoints
sidneyswift Apr 2, 2026
ce177d7
fix: inline route segment config (Next.js 16 requires static analysis)
sidneyswift Apr 2, 2026
daad91c
fix: address CodeRabbit review comments
sidneyswift Apr 2, 2026
3e3b2c5
fix: resolve all lint errors in new primitive files
sidneyswift Apr 2, 2026
626a4ea
refactor: make primitives run inline instead of triggering tasks
sidneyswift Apr 2, 2026
7267c07
fix: enforce validateAuthContext at handler level in content primitives
sidneyswift Apr 2, 2026
58d5c56
feat: add POST /api/content/create/analyze (Twelve Labs video analysis)
sidneyswift Apr 2, 2026
8e5a44e
fix: rename content/create/analyze to content/analyze
sidneyswift Apr 2, 2026
004f179
refactor: rename content primitive routes to verb-qualifier pattern
sidneyswift Apr 2, 2026
88cf700
refactor: make content primitives generic + replace render with edit
sidneyswift Apr 2, 2026
324e28f
fix: address CodeRabbit review — split text handler, DRY route factory
sidneyswift Apr 2, 2026
735b4ab
fix: make image_url optional in generate-video, add prompt field
sidneyswift Apr 2, 2026
490efb8
chore: redeploy with FAL_KEY
sidneyswift Apr 2, 2026
f9182a0
chore: redeploy with updated FAL_KEY
sidneyswift Apr 2, 2026
321fac8
fix: upgrade to nano-banana-2, auto-select t2i vs edit model
sidneyswift Apr 2, 2026
6d79bbc
feat: add num_images, aspect_ratio, resolution to generate-image
sidneyswift Apr 2, 2026
2f13578
feat: set optimal internal defaults for image generation
sidneyswift Apr 2, 2026
2d8b155
feat: auto-select Veo 3.1 model variant based on inputs
sidneyswift Apr 2, 2026
1781d81
feat: full generate-video upgrade — extend mode, duration, resolution…
sidneyswift Apr 2, 2026
8a44e1d
feat: expose missing params for transcribe + upscale, fix generate_au…
sidneyswift Apr 2, 2026
eeceb61
feat: add mode param to generate-video with 6 modes
sidneyswift Apr 2, 2026
ba3e41f
fix: correct text-to-video model ID (fal-ai/veo3.1, not /text-to-video)
sidneyswift Apr 2, 2026
3452794
fix: correct fal field mappings for reference and first-last modes
sidneyswift Apr 2, 2026
ac6c9ce
refactor: simplify endpoint paths per code review
sidneyswift Apr 2, 2026
a5fe5ed
chore: redeploy with updated RECOUP_API_KEY
sidneyswift Apr 2, 2026
f78ab61
chore: redeploy with new RECOUP_API_KEY for caption
sidneyswift Apr 2, 2026
7599d47
fix: always send prompt field to fal (LTX lipsync requires it even wh…
sidneyswift Apr 2, 2026
6d7e40a
refactor: caption handler calls AI SDK directly instead of HTTP self-…
sidneyswift Apr 2, 2026
6be1866
refactor: extract configureFal and buildFalInput (DRY + SRP)
sidneyswift Apr 2, 2026
f2eb5db
fix: remove unused stream from analyze schema, DRY video OPTIONS handler
sidneyswift Apr 3, 2026
2e89b9f
feat: add template support to all content primitives
sidneyswift Apr 3, 2026
d30b890
feat: content V2 — edit route, template detail, malleable mode, MCP t…
sidneyswift Apr 3, 2026
49172c5
fix: add packages field to pnpm-workspace.yaml
sweetmantech Apr 9, 2026
ab51129
fix: remove pnpm-workspace.yaml
sweetmantech Apr 9, 2026
bcbab1f
revert: remove lint-only changes to focus PR on content primitives
sweetmantech Apr 9, 2026
6bb0397
fix: make template optional in content create, fix edit type error
sweetmantech Apr 9, 2026
85e4151
refactor: remove createPrimitiveRoute, use standard route pattern
sweetmantech Apr 9, 2026
ea46eec
refactor: use standard route pattern for template detail endpoint
sweetmantech Apr 9, 2026
0d4c09e
refactor: move configureFal to lib/fal/server.ts
sweetmantech Apr 9, 2026
b8b003c
refactor: replace primitives/ with domain subdirectories under lib/co…
sweetmantech Apr 9, 2026
1de5ce2
refactor: address PR review comments (SRP, KISS)
sweetmantech Apr 9, 2026
e6897aa
refactor: extract business logic from handlers (SRP)
sweetmantech Apr 9, 2026
254e01b
refactor: move schemas into validate functions, fix naming and abbrev…
sweetmantech Apr 9, 2026
f86b4e4
refactor: convert template JSON to typed TypeScript exports (KISS)
sweetmantech Apr 9, 2026
65a75c0
refactor: split templates/index.ts into SRP files
sweetmantech Apr 9, 2026
b078652
refactor: delete callContentEndpoint abstraction (KISS)
sweetmantech Apr 9, 2026
1739c77
refactor: remove all new MCP content tools
sweetmantech Apr 9, 2026
f2b53cc
refactor: single TEMPLATES definition, enum validation, raw validated…
sweetmantech Apr 9, 2026
f232364
refactor: delete triggerPrimitive wrapper, use tasks.trigger directly…
sweetmantech Apr 9, 2026
00e9aa2
refactor: derive TEMPLATE_IDS from TEMPLATES keys (single source of t…
sweetmantech Apr 9, 2026
3269359
fix: use fal-ai/veo3.1/fast/image-to-video model for video generation
sweetmantech Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/api/content/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createAnalyzeHandler } from "@/lib/content/analyze/createAnalyzeHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* POST /api/content/analyze
*
* Analyze a video with AI — describe scenes, check quality, evaluate content.
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return createAnalyzeHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
23 changes: 23 additions & 0 deletions app/api/content/caption/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createTextHandler } from "@/lib/content/caption/createTextHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* POST /api/content/caption
*
* Generate on-screen caption text for a social video.
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return createTextHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
23 changes: 23 additions & 0 deletions app/api/content/image/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createImageHandler } from "@/lib/content/image/createImageHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* POST /api/content/image
*
* Generate an image from a prompt and optional reference image.
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return createImageHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
23 changes: 23 additions & 0 deletions app/api/content/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { editHandler } from "@/lib/content/edit/editHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* PATCH /api/content
*
* Edit media with operations or a template preset.
*/
export async function PATCH(request: NextRequest): Promise<NextResponse> {
return editHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
26 changes: 26 additions & 0 deletions app/api/content/templates/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getContentTemplateDetailHandler } from "@/lib/content/getContentTemplateDetailHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* GET /api/content/templates/[id]
*
* Returns the full template configuration for a given template id.
*/
export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> },
): Promise<NextResponse> {
return getContentTemplateDetailHandler(request, context);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
23 changes: 23 additions & 0 deletions app/api/content/transcribe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createAudioHandler } from "@/lib/content/transcribe/createAudioHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* POST /api/content/transcribe
*
* Transcribe audio into text with word-level timestamps.
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return createAudioHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
23 changes: 23 additions & 0 deletions app/api/content/upscale/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createUpscaleHandler } from "@/lib/content/upscale/createUpscaleHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* POST /api/content/upscale
*
* Upscale an image or video to higher resolution.
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return createUpscaleHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
23 changes: 23 additions & 0 deletions app/api/content/video/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { createVideoHandler } from "@/lib/content/video/createVideoHandler";

/**
* OPTIONS handler for CORS preflight requests.
*/
export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}

/**
* POST /api/content/video
*
* Generate a video from a prompt, image, or existing video.
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return createVideoHandler(request);
}

export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
94 changes: 94 additions & 0 deletions lib/content/__tests__/getContentTemplateDetailHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest, NextResponse } from "next/server";
import { getContentTemplateDetailHandler } from "@/lib/content/getContentTemplateDetailHandler";
import { validateAuthContext } from "@/lib/auth/validateAuthContext";
import { loadTemplate } from "@/lib/content/templates";

vi.mock("@/lib/networking/getCorsHeaders", () => ({
getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })),
}));

vi.mock("@/lib/auth/validateAuthContext", () => ({
validateAuthContext: vi.fn(),
}));

vi.mock("@/lib/content/templates", () => ({
loadTemplate: vi.fn(),
}));

describe("getContentTemplateDetailHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("returns 401 when not authenticated", async () => {
vi.mocked(validateAuthContext).mockResolvedValue(
NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }),
);
const request = new NextRequest("http://localhost/api/content/templates/bedroom", {
method: "GET",
});

const result = await getContentTemplateDetailHandler(request, {
params: Promise.resolve({ id: "bedroom" }),
});

expect(result.status).toBe(401);
});

it("returns 404 for unknown template", async () => {
vi.mocked(validateAuthContext).mockResolvedValue({
accountId: "acc_123",
orgId: null,
authToken: "test-key",
});
vi.mocked(loadTemplate).mockReturnValue(null);

const request = new NextRequest("http://localhost/api/content/templates/nonexistent", {
method: "GET",
});

const result = await getContentTemplateDetailHandler(request, {
params: Promise.resolve({ id: "nonexistent" }),
});
const body = await result.json();

expect(result.status).toBe(404);
expect(body.error).toBe("Template not found");
});

it("returns full template for valid id", async () => {
vi.mocked(validateAuthContext).mockResolvedValue({
accountId: "acc_123",
orgId: null,
authToken: "test-key",
});
const mockTemplate = {
id: "artist-caption-bedroom",
description: "Moody purple bedroom setting",
image: { prompt: "test", reference_images: [], style_rules: {} },
video: { moods: ["calm"], movements: ["slow pan"] },
caption: { guide: { tone: "dreamy", rules: [], formats: [] }, examples: [] },
edit: { operations: [] },
};
vi.mocked(loadTemplate).mockReturnValue(mockTemplate);

const request = new NextRequest(
"http://localhost/api/content/templates/artist-caption-bedroom",
{ method: "GET" },
);

const result = await getContentTemplateDetailHandler(request, {
params: Promise.resolve({ id: "artist-caption-bedroom" }),
});
const body = await result.json();

expect(result.status).toBe(200);
expect(body.id).toBe("artist-caption-bedroom");
expect(body.description).toBe("Moody purple bedroom setting");
expect(body.image).toBeDefined();
expect(body.video).toBeDefined();
expect(body.caption).toBeDefined();
expect(body.edit).toBeDefined();
});
});
Loading
Loading