Files
ClaudeMarketing/app/api/assets/[id]/repurpose/route.ts
T
Trey t 2ab8af64d4 feat: add variation spawner and content repurposing
Variation spawner: select an image asset → spawn N variations that keep the
same emotional pattern/visual style but explore different hook angles. Runs
a focused ad-creative-designer agent with the original as a Gemini reference
image. New assets link back via parentAssetId.

Content repurposing: resize an image to all other platform dimensions using
Sharp (cover crop). Captions are re-toned for the target platform via Claude
CLI. No external APIs needed — fully local.

- Add parentAssetId self-relation to Asset model
- lib/repurpose.ts: Sharp resize, platform format mapping, caption re-toning
- lib/variations.ts: asset DNA extraction, variation prompt builder, mini-pipeline
- API routes: /api/assets/[id]/repurpose (GET formats, POST resize)
- API routes: /api/assets/[id]/variations (GET existing, POST spawn)
- Repurpose modal: checkbox list of target formats
- Variation modal: count picker, async launch
- Asset card: Repurpose + Variations buttons on image assets
- Asset lineage: "Derived from" shown on child assets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 22:46:42 -05:00

80 lines
2.5 KiB
TypeScript

import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { repurposeImage, retoneCaption, getAvailableFormats } from "@/lib/repurpose";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session) return new Response("Unauthorized", { status: 401 });
const { id } = await params;
const asset = await prisma.asset.findUnique({ where: { id } });
if (!asset) return Response.json({ error: "Not found" }, { status: 404 });
const formats = getAvailableFormats(asset.dimensions);
return Response.json({ formats, currentDimensions: asset.dimensions });
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session) return new Response("Unauthorized", { status: 401 });
const { id } = await params;
const body = await request.json();
const { formats: targetFormats } = body as { formats: string[] };
const asset = await prisma.asset.findUnique({ where: { id } });
if (!asset) return Response.json({ error: "Not found" }, { status: 404 });
if (asset.type !== "image") {
return Response.json({ error: "Only image assets can be repurposed" }, { status: 400 });
}
const outputDir = `outputs/repurposed_${id.slice(0, 8)}`;
const resized = await repurposeImage(asset.filePath, targetFormats, outputDir);
const results = [];
for (const file of resized) {
const originalMeta = asset.metadata ? JSON.parse(asset.metadata) : {};
let newMeta = { ...originalMeta };
// Re-tone caption if platform changed and caption exists
if (originalMeta.caption && asset.platform && file.platform !== asset.platform) {
try {
const newCaption = await retoneCaption(
originalMeta.caption,
asset.platform,
file.platform
);
newMeta = { ...newMeta, caption: newCaption, originalCaption: originalMeta.caption };
} catch {
// Keep original caption if re-toning fails
}
}
const newAsset = await prisma.asset.create({
data: {
campaignId: asset.campaignId,
type: "image",
platform: file.platform,
format: "png",
filePath: file.filePath,
fileName: file.fileName,
dimensions: file.dimensions,
metadata: JSON.stringify(newMeta),
status: "draft",
parentAssetId: asset.id,
},
});
results.push(newAsset);
}
return Response.json({ created: results.length, assets: results });
}