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
20 changes: 20 additions & 0 deletions src/content/__tests__/resolveDspLogoUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it, expect } from "vitest";
import { resolveDspLogoUrl } from "../resolveDspLogoUrl";

describe("resolveDspLogoUrl", () => {
it("returns null for 'none'", () => {
expect(resolveDspLogoUrl("none")).toBeNull();
});

it("returns a URL for 'spotify'", () => {
const url = resolveDspLogoUrl("spotify");
expect(url).toContain("spotify");
expect(url).toMatch(/^https?:\/\//);
});

it("returns a URL for 'apple'", () => {
const url = resolveDspLogoUrl("apple");
expect(url).toContain("apple");
expect(url).toMatch(/^https?:\/\//);
});
});
16 changes: 16 additions & 0 deletions src/content/resolveDspLogoUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { DspValue } from "../schemas/contentCreationSchema";

const DSP_LOGO_URLS: Record<Exclude<DspValue, "none">, string> = {
spotify: "https://recoup-api.vercel.app/api/assets/dsp-logo-spotify.png",
apple: "https://recoup-api.vercel.app/api/assets/dsp-logo-apple.png",
};

/**
* Returns the DSP logo URL for the given DSP value, or null if "none".
*
* @param dsp - The DSP value from the content creation payload.
*/
export function resolveDspLogoUrl(dsp: DspValue): string | null {
if (dsp === "none") return null;
return DSP_LOGO_URLS[dsp];
}
5 changes: 5 additions & 0 deletions src/schemas/contentCreationSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ import { z } from "zod";
export const CAPTION_LENGTHS = ["none", "short", "medium", "long"] as const;
export type CaptionLength = (typeof CAPTION_LENGTHS)[number];

export const DSP_VALUES = ["none", "spotify", "apple"] as const;
export type DspValue = (typeof DSP_VALUES)[number];

export const createContentPayloadSchema = z.object({
accountId: z.string().min(1, "accountId is required"),
artistSlug: z.string().min(1, "artistSlug is required"),
template: z.string().min(1, "template is required"),
lipsync: z.boolean().default(false),
/** Controls caption text length: "none" skips captions, "short" (1-2 lines), "medium" (3-5 lines), "long" (paragraph). */
captionLength: z.enum(CAPTION_LENGTHS).default("none"),
/** Which DSP logo to overlay on the video: "none" (no logo), "spotify", or "apple". */
dsp: z.enum(DSP_VALUES).default("none"),
/** Whether to upscale the image and video for higher quality. Adds ~2 min to pipeline. */
upscale: z.boolean().default(false),
/** GitHub repo URL so the task can fetch artist files (face-guide, songs). */
Expand Down
19 changes: 19 additions & 0 deletions src/tasks/__tests__/createContentTask.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,25 @@ describe("createContentTask", () => {
expect(result.captionText).toBe("");
});

it("includes DSP logo URL in overlay images when dsp is set", async () => {
await mockRun({ ...VALID_PAYLOAD, dsp: "spotify" });

const { renderFinalVideo } = await import("../../content/renderFinalVideo");
const callArgs = vi.mocked(renderFinalVideo).mock.calls[0][0] as Record<string, unknown>;
const overlayUrls = callArgs.overlayImageUrls as string[];
expect(overlayUrls).toBeDefined();
expect(overlayUrls.some((url: string) => url.includes("spotify"))).toBe(true);
});

it("does not include DSP logo when dsp is none", async () => {
await mockRun({ ...VALID_PAYLOAD, dsp: "none" });

const { renderFinalVideo } = await import("../../content/renderFinalVideo");
const callArgs = vi.mocked(renderFinalVideo).mock.calls[0][0] as Record<string, unknown>;
// Template doesn't have usesImageOverlay, so overlayImageUrls should be undefined
expect(callArgs.overlayImageUrls).toBeUndefined();
});

it("throws when FAL_KEY is missing", async () => {
delete process.env.FAL_KEY;
await expect(mockRun(VALID_PAYLOAD)).rejects.toThrow("FAL_KEY");
Expand Down
7 changes: 6 additions & 1 deletion src/tasks/createContentTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
generateVideo,
} from "../recoup/contentApi";
import { resolveCaptionText } from "../content/resolveCaptionText";
import { resolveDspLogoUrl } from "../content/resolveDspLogoUrl";

/**
* Content-creation task — full pipeline that generates a social-ready video.
Expand Down Expand Up @@ -133,14 +134,18 @@ export const createContentTask = schemaTask({

// --- Step 10: Final render (ffmpeg) ---
logStep("Rendering final video (ffmpeg)");
const dspLogoUrl = resolveDspLogoUrl(payload.dsp);
const overlayUrls = template.usesImageOverlay
? [...additionalImageUrls, ...(dspLogoUrl ? [dspLogoUrl] : [])]
: dspLogoUrl ? [dspLogoUrl] : undefined;
const finalVideo = await renderFinalVideo({
videoUrl,
songBuffer: audioClip.songBuffer,
audioStartSeconds: audioClip.startSeconds,
audioDurationSeconds: audioClip.durationSeconds,
captionText,
hasAudio: payload.lipsync,
overlayImageUrls: template.usesImageOverlay ? additionalImageUrls : undefined,
overlayImageUrls: overlayUrls,
});

// --- Return result ---
Expand Down
Loading