Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions lib/agents/content/__tests__/parseContentPrompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ vi.mock("../createContentPromptAgent", () => ({
lipsync: false,
batch: 1,
captionLength: "none",
dsp: "none",
upscale: false,
template: "artist-caption-bedroom",
},
Expand Down Expand Up @@ -139,6 +140,7 @@ describe("parseContentPrompt", () => {
lipsync: false,
batch: 1,
captionLength: "none",
dsp: "none",
upscale: false,
template: "artist-caption-bedroom",
});
Expand All @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion lib/agents/content/createContentPromptAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]];
Expand Down Expand Up @@ -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.",
Expand All @@ -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,
};
Expand All @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions lib/agents/content/handlers/registerOnNewMention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -70,6 +69,7 @@ export function registerOnNewMention(bot: ContentAgentBot) {
template,
lipsync,
captionLength,
dsp,
upscale,
githubRepo,
...(allSongs.length > 0 && { songs: allSongs }),
Expand Down
43 changes: 43 additions & 0 deletions lib/content/__tests__/validateCreateContentBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe("validateCreateContentBody", () => {
template: "artist-caption-bedroom",
lipsync: true,
captionLength: "none",
dsp: "none",
upscale: false,
batch: 1,
});
Expand Down Expand Up @@ -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 }),
Expand Down
1 change: 1 addition & 0 deletions lib/content/createContentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function createContentHandler(request: NextRequest): Promise<NextRe
template: validated.template,
lipsync: validated.lipsync,
captionLength: validated.captionLength,
dsp: validated.dsp,
upscale: validated.upscale,
githubRepo,
songs: validated.songs,
Expand Down
2 changes: 2 additions & 0 deletions lib/content/dspValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DSP_VALUES = ["none", "spotify", "apple"] as const;
export type DspValue = (typeof DSP_VALUES)[number];
4 changes: 4 additions & 0 deletions lib/content/validateCreateContentBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { resolveArtistSlug } from "@/lib/content/resolveArtistSlug";
import { songsSchema } from "@/lib/content/songsSchema";

import { CAPTION_LENGTHS, type CaptionLength } from "@/lib/content/captionLengths";
import { DSP_VALUES, type DspValue } from "@/lib/content/dspValues";

export const createContentBodySchema = z.object({
artist_account_id: z
Expand All @@ -17,6 +18,7 @@ export const createContentBodySchema = z.object({
template: z.string().min(1, "template cannot be empty").optional(),
lipsync: z.boolean().optional().default(false),
caption_length: z.enum(CAPTION_LENGTHS).optional().default("none"),
dsp: z.enum(DSP_VALUES).optional().default("none"),
upscale: z.boolean().optional().default(false),
batch: z.number().int().min(1).max(30).optional().default(1),
songs: songsSchema,
Expand All @@ -30,6 +32,7 @@ export type ValidatedCreateContentBody = {
template?: string;
lipsync: boolean;
captionLength: CaptionLength;
dsp: DspValue;
upscale: boolean;
batch: number;
songs?: string[];
Expand Down Expand Up @@ -92,6 +95,7 @@ export async function validateCreateContentBody(
template,
lipsync: result.data.lipsync ?? false,
captionLength: result.data.caption_length,
dsp: result.data.dsp,
upscale: result.data.upscale ?? false,
batch: result.data.batch ?? 1,
songs: result.data.songs,
Expand Down
2 changes: 2 additions & 0 deletions lib/trigger/triggerCreateContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface TriggerCreateContentPayload {
lipsync: boolean;
/** Controls caption length: "none" skips captions, "short", "medium", or "long". */
captionLength: "none" | "short" | "medium" | "long";
/** Which DSP logo to overlay on the video: "none", "spotify", or "apple". */
dsp: "none" | "spotify" | "apple";
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Use the DspValue type from lib/content/dspValues.ts instead of duplicating the union inline. This PR already introduces DspValue as the single source of truth for valid DSP values—using the inline union here creates a second definition that can drift.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/trigger/triggerCreateContent.ts, line 12:

<comment>Use the `DspValue` type from `lib/content/dspValues.ts` instead of duplicating the union inline. This PR already introduces `DspValue` as the single source of truth for valid DSP values—using the inline union here creates a second definition that can drift.</comment>

<file context>
@@ -8,6 +8,8 @@ export interface TriggerCreateContentPayload {
   /** Controls caption length: "none" skips captions, "short", "medium", or "long". */
   captionLength: "none" | "short" | "medium" | "long";
+  /** Which DSP logo to overlay on the video: "none", "spotify", or "apple". */
+  dsp: "none" | "spotify" | "apple";
   /** Whether to upscale image and video for higher quality. */
   upscale: boolean;
</file context>
Fix with Cubic

/** Whether to upscale image and video for higher quality. */
upscale: boolean;
/** GitHub repo URL so the task can fetch artist files. */
Expand Down
Loading