From daaf8d38f4b3ae80bca55ed569b5b5f3e12cd435 Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Tue, 14 Apr 2026 20:27:50 +0000 Subject: [PATCH] feat: add dsp enum parameter for DSP logo overlay (REC-63) Add `dsp` parameter ("none" | "spotify" | "apple") to the content creation pipeline. The parameter flows from API validation through the Slack bot AI parser to the Trigger.dev task payload. - Add DSP_VALUES constant and DspValue type - Add dsp field to createContentBodySchema with "none" default - Add dsp to content prompt agent schema for Slack bot parsing - Pass dsp through createContentHandler and triggerCreateContent - Add 4 new tests for dsp validation and prompt extraction Co-Authored-By: Paperclip --- .../__tests__/parseContentPrompt.test.ts | 19 ++++++++ .../content/createContentPromptAgent.ts | 9 +++- .../content/handlers/registerOnNewMention.ts | 6 +-- .../validateCreateContentBody.test.ts | 43 +++++++++++++++++++ lib/content/createContentHandler.ts | 1 + lib/content/dspValues.ts | 2 + lib/content/validateCreateContentBody.ts | 4 ++ lib/trigger/triggerCreateContent.ts | 2 + 8 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 lib/content/dspValues.ts diff --git a/lib/agents/content/__tests__/parseContentPrompt.test.ts b/lib/agents/content/__tests__/parseContentPrompt.test.ts index b2bb643ee..9a5c2a1a1 100644 --- a/lib/agents/content/__tests__/parseContentPrompt.test.ts +++ b/lib/agents/content/__tests__/parseContentPrompt.test.ts @@ -12,6 +12,7 @@ vi.mock("../createContentPromptAgent", () => ({ lipsync: false, batch: 1, captionLength: "none", + dsp: "none", upscale: false, template: "artist-caption-bedroom", }, @@ -139,6 +140,7 @@ describe("parseContentPrompt", () => { lipsync: false, batch: 1, captionLength: "none", + dsp: "none", upscale: false, template: "artist-caption-bedroom", }); @@ -153,11 +155,28 @@ describe("parseContentPrompt", () => { lipsync: false, batch: 1, captionLength: "none", + dsp: "none", upscale: false, template: "artist-caption-bedroom", }); }); + it("extracts dsp from prompt", async () => { + const flags: ContentPromptFlags = { + lipsync: false, + batch: 1, + captionLength: "short", + dsp: "spotify", + upscale: false, + template: "artist-release-editorial", + }; + mockGenerate.mockResolvedValue({ output: flags }); + + const result = await parseContentPrompt("make a Spotify editorial video"); + + expect(result.dsp).toBe("spotify"); + }); + it("extracts songs from prompt when specified", async () => { const flags: ContentPromptFlags = { lipsync: true, diff --git a/lib/agents/content/createContentPromptAgent.ts b/lib/agents/content/createContentPromptAgent.ts index 93dc5d10e..4719efbfb 100644 --- a/lib/agents/content/createContentPromptAgent.ts +++ b/lib/agents/content/createContentPromptAgent.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { LIGHTWEIGHT_MODEL } from "@/lib/const"; import { CONTENT_TEMPLATES, DEFAULT_CONTENT_TEMPLATE } from "@/lib/content/contentTemplates"; import { CAPTION_LENGTHS } from "@/lib/content/captionLengths"; +import { DSP_VALUES } from "@/lib/content/dspValues"; import { songsSchema } from "@/lib/content/songsSchema"; const templateNames = CONTENT_TEMPLATES.map(t => t.name) as [string, ...string[]]; @@ -31,6 +32,11 @@ export const contentPromptFlagsSchema = z.object({ .describe( "Whether to upscale for higher quality. True when the prompt mentions high quality, HD, upscale, 4K, or premium.", ), + dsp: z + .enum(DSP_VALUES) + .describe( + "Which DSP (digital streaming platform) logo to overlay on the video. 'none' (default — no DSP logo), 'spotify', or 'apple'. Extract from phrases like 'for Spotify', 'Apple Music editorial', 'Spotify playlist'. If no DSP is mentioned, use 'none'.", + ), template: z.enum(templateNames).describe("Which visual template/scene to use for the video."), songs: songsSchema.describe( "Song names or slugs mentioned in the prompt. Extract from phrases like 'the hiccups song', 'use track X', 'for song Y'. Omit if no specific songs are mentioned.", @@ -43,6 +49,7 @@ export const DEFAULT_CONTENT_PROMPT_FLAGS: ContentPromptFlags = { lipsync: false, batch: 1, captionLength: "none", + dsp: "none", upscale: false, template: DEFAULT_CONTENT_TEMPLATE, }; @@ -59,7 +66,7 @@ ${templateDescriptions} If no template is specified, default to "${DEFAULT_CONTENT_PROMPT_FLAGS.template}". If a parameter is not mentioned, use the default value. -Defaults: lipsync=${DEFAULT_CONTENT_PROMPT_FLAGS.lipsync}, batch=${DEFAULT_CONTENT_PROMPT_FLAGS.batch}, captionLength="${DEFAULT_CONTENT_PROMPT_FLAGS.captionLength}", upscale=${DEFAULT_CONTENT_PROMPT_FLAGS.upscale}, template="${DEFAULT_CONTENT_PROMPT_FLAGS.template}"`; +Defaults: lipsync=${DEFAULT_CONTENT_PROMPT_FLAGS.lipsync}, batch=${DEFAULT_CONTENT_PROMPT_FLAGS.batch}, captionLength="${DEFAULT_CONTENT_PROMPT_FLAGS.captionLength}", dsp="${DEFAULT_CONTENT_PROMPT_FLAGS.dsp}", upscale=${DEFAULT_CONTENT_PROMPT_FLAGS.upscale}, template="${DEFAULT_CONTENT_PROMPT_FLAGS.template}"`; /** * Creates a ToolLoopAgent configured for parsing content creation prompts. diff --git a/lib/agents/content/handlers/registerOnNewMention.ts b/lib/agents/content/handlers/registerOnNewMention.ts index e02bb6ea4..3e88589a3 100644 --- a/lib/agents/content/handlers/registerOnNewMention.ts +++ b/lib/agents/content/handlers/registerOnNewMention.ts @@ -23,9 +23,8 @@ export function registerOnNewMention(bot: ContentAgentBot) { const artistAccountId = "1873859c-dd37-4e9a-9bac-80d3558527a9"; // Parse the user's natural-language prompt into structured flags - const { lipsync, batch, captionLength, upscale, template, songs } = await parseContentPrompt( - message.text, - ); + const { lipsync, batch, captionLength, dsp, upscale, template, songs } = + await parseContentPrompt(message.text); // Extract audio/image attachments from the Slack message const { songUrl, imageUrls } = await extractMessageAttachments(message); @@ -70,6 +69,7 @@ export function registerOnNewMention(bot: ContentAgentBot) { template, lipsync, captionLength, + dsp, upscale, githubRepo, ...(allSongs.length > 0 && { songs: allSongs }), diff --git a/lib/content/__tests__/validateCreateContentBody.test.ts b/lib/content/__tests__/validateCreateContentBody.test.ts index 5ca42a0ed..7b33dc24f 100644 --- a/lib/content/__tests__/validateCreateContentBody.test.ts +++ b/lib/content/__tests__/validateCreateContentBody.test.ts @@ -60,6 +60,7 @@ describe("validateCreateContentBody", () => { template: "artist-caption-bedroom", lipsync: true, captionLength: "none", + dsp: "none", upscale: false, batch: 1, }); @@ -123,6 +124,48 @@ describe("validateCreateContentBody", () => { } }); + it("accepts dsp parameter and includes it in validated output", async () => { + const request = createRequest({ + artist_account_id: "550e8400-e29b-41d4-a716-446655440000", + template: "artist-release-editorial", + dsp: "spotify", + }); + + const result = await validateCreateContentBody(request); + + expect(result).not.toBeInstanceOf(NextResponse); + if (!(result instanceof NextResponse)) { + expect(result.dsp).toBe("spotify"); + } + }); + + it("defaults dsp to none when omitted", async () => { + const request = createRequest({ + artist_account_id: "550e8400-e29b-41d4-a716-446655440000", + }); + + const result = await validateCreateContentBody(request); + + expect(result).not.toBeInstanceOf(NextResponse); + if (!(result instanceof NextResponse)) { + expect(result.dsp).toBe("none"); + } + }); + + it("returns 400 when dsp is invalid", async () => { + const request = createRequest({ + artist_account_id: "550e8400-e29b-41d4-a716-446655440000", + dsp: "tidal", + }); + + const result = await validateCreateContentBody(request); + + expect(result).toBeInstanceOf(NextResponse); + if (result instanceof NextResponse) { + expect(result.status).toBe(400); + } + }); + it("returns auth error response when auth fails", async () => { mockValidateAuthContext.mockResolvedValue( NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }), diff --git a/lib/content/createContentHandler.ts b/lib/content/createContentHandler.ts index 577e81327..bd72d5845 100644 --- a/lib/content/createContentHandler.ts +++ b/lib/content/createContentHandler.ts @@ -49,6 +49,7 @@ export async function createContentHandler(request: NextRequest): Promise