From be43777bf7e20160437a43625aa638fd39143536 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 8 Apr 2026 10:34:14 -0500 Subject: [PATCH 1/2] fix: include face-swap instruction in editorial template and route overlays correctly - resolveImageInstruction: prepend FACE_SWAP_INSTRUCTION to customInstruction when usesFaceGuide is true, so the model uses the face guide for face-swapping - createContentTask: skip passing additionalImageUrls to generateContentImage when template.usesImageOverlay is true (those images are for video overlays only) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__tests__/resolveImageInstruction.test.ts | 6 ++- src/content/resolveImageInstruction.ts | 7 +++- src/tasks/__tests__/createContentTask.test.ts | 37 +++++++++++++++++++ src/tasks/createContentTask.ts | 2 +- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/content/__tests__/resolveImageInstruction.test.ts b/src/content/__tests__/resolveImageInstruction.test.ts index 7766ba5..a66ac35 100644 --- a/src/content/__tests__/resolveImageInstruction.test.ts +++ b/src/content/__tests__/resolveImageInstruction.test.ts @@ -26,11 +26,13 @@ describe("resolveImageInstruction", () => { expect(result).toBe("Place the album art onto a vinyl sleeve."); }); - it("uses customInstruction even when usesFaceGuide is true", () => { + it("prepends face-swap instruction to customInstruction when usesFaceGuide is true", () => { const result = resolveImageInstruction( makeTemplate({ usesFaceGuide: true, styleGuide: { customInstruction: "Custom instruction here." } }), ); - expect(result).toBe("Custom instruction here."); + expect(result).toContain(FACE_SWAP_INSTRUCTION); + expect(result).toContain("Custom instruction here."); + expect(result.indexOf(FACE_SWAP_INSTRUCTION)).toBeLessThan(result.indexOf("Custom instruction here.")); }); it("falls back to FACE_SWAP_INSTRUCTION when usesFaceGuide is true and no customInstruction", () => { diff --git a/src/content/resolveImageInstruction.ts b/src/content/resolveImageInstruction.ts index c5e62d2..2c8d9a8 100644 --- a/src/content/resolveImageInstruction.ts +++ b/src/content/resolveImageInstruction.ts @@ -8,6 +8,11 @@ import type { TemplateData } from "./loadTemplate"; */ export function resolveImageInstruction(template: TemplateData): string { const custom = template.styleGuide?.customInstruction; - if (typeof custom === "string" && custom.trim().length > 0) return custom.trim(); + if (typeof custom === "string" && custom.trim().length > 0) { + if (template.usesFaceGuide) { + return `${FACE_SWAP_INSTRUCTION} ${custom.trim()}`; + } + return custom.trim(); + } return template.usesFaceGuide ? FACE_SWAP_INSTRUCTION : NO_FACE_INSTRUCTION; } diff --git a/src/tasks/__tests__/createContentTask.test.ts b/src/tasks/__tests__/createContentTask.test.ts index da511ab..d0a63d3 100644 --- a/src/tasks/__tests__/createContentTask.test.ts +++ b/src/tasks/__tests__/createContentTask.test.ts @@ -32,6 +32,17 @@ vi.mock("../../content/fetchGithubFile", () => ({ fetchGithubFile: vi.fn().mockResolvedValue(Buffer.from("fake-png")), })); +vi.mock("../../content/downloadImageBuffer", () => ({ + downloadImageBuffer: vi.fn().mockResolvedValue({ + buffer: Buffer.from("fake-image"), + contentType: "image/png", + }), +})); + +vi.mock("../../content/detectFace", () => ({ + detectFace: vi.fn().mockResolvedValue(false), +})); + vi.mock("../../content/generateContentImage", () => ({ generateContentImage: vi.fn().mockResolvedValue("https://fal.ai/image.png"), })); @@ -138,6 +149,32 @@ describe("createContentTask", () => { await expect(mockRun(VALID_PAYLOAD)).rejects.toThrow("face-guide.png not found"); }); + it("does not pass additionalImageUrls to generateContentImage when usesImageOverlay is true", async () => { + const { loadTemplate } = await import("../../content/loadTemplate"); + vi.mocked(loadTemplate).mockResolvedValueOnce({ + name: "artist-release-editorial", + imagePrompt: "editorial scene", + usesFaceGuide: true, + usesImageOverlay: true, + styleGuide: { customInstruction: "Generate editorial photo." }, + captionGuide: null, + captionExamples: [], + videoMoods: [], + videoMovements: [], + referenceImagePaths: [], + }); + + await mockRun({ + ...VALID_PAYLOAD, + template: "artist-release-editorial", + images: ["https://example.com/cover1.png", "https://example.com/cover2.png"], + }); + + const { generateContentImage } = await import("../../content/generateContentImage"); + const callArgs = vi.mocked(generateContentImage).mock.calls[0][0]; + expect(callArgs.additionalImageUrls).toBeUndefined(); + }); + it("throws when FAL_KEY is missing", async () => { delete process.env.FAL_KEY; await expect(mockRun(VALID_PAYLOAD)).rejects.toThrow("FAL_KEY"); diff --git a/src/tasks/createContentTask.ts b/src/tasks/createContentTask.ts index b23c837..0ac1285 100644 --- a/src/tasks/createContentTask.ts +++ b/src/tasks/createContentTask.ts @@ -100,7 +100,7 @@ export const createContentTask = schemaTask({ faceGuideUrl: faceGuideUrl ?? undefined, referenceImagePath, prompt: fullPrompt, - additionalImageUrls, + additionalImageUrls: template.usesImageOverlay ? undefined : additionalImageUrls, }); // --- Step 6: Upscale image (optional) --- From c92f6e1899af208f775c3f72f41e320e150d49e1 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 8 Apr 2026 10:42:47 -0500 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20KISS=20=E2=80=94=20bake=20face-swap?= =?UTF-8?q?=20language=20into=20editorial=20style-guide.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the resolveImageInstruction logic change. Instead, adds face-swap instructions directly to the editorial template's customInstruction field. Simpler: no code change needed, just template content. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../__tests__/loadArtistReleaseEditorial.test.ts | 10 ++++++++++ src/content/__tests__/resolveImageInstruction.test.ts | 6 ++---- src/content/resolveImageInstruction.ts | 7 +------ .../artist-release-editorial/style-guide.json | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/content/__tests__/loadArtistReleaseEditorial.test.ts b/src/content/__tests__/loadArtistReleaseEditorial.test.ts index e1f2748..906acb2 100644 --- a/src/content/__tests__/loadArtistReleaseEditorial.test.ts +++ b/src/content/__tests__/loadArtistReleaseEditorial.test.ts @@ -34,6 +34,16 @@ describe("loadTemplate artist-release-editorial", () => { expect(typeof sg.customInstruction).toBe("string"); }); + it("customInstruction includes face-swap language referencing the headshot", async () => { + const template = await loadTemplate("artist-release-editorial"); + const sg = template.styleGuide as Record; + const instruction = sg.customInstruction as string; + const lower = instruction.toLowerCase(); + + expect(lower).toContain("headshot"); + expect(lower).toContain("face"); + }); + it("customInstruction generates only the artist portrait with no composited elements", async () => { const template = await loadTemplate("artist-release-editorial"); const sg = template.styleGuide as Record; diff --git a/src/content/__tests__/resolveImageInstruction.test.ts b/src/content/__tests__/resolveImageInstruction.test.ts index a66ac35..7766ba5 100644 --- a/src/content/__tests__/resolveImageInstruction.test.ts +++ b/src/content/__tests__/resolveImageInstruction.test.ts @@ -26,13 +26,11 @@ describe("resolveImageInstruction", () => { expect(result).toBe("Place the album art onto a vinyl sleeve."); }); - it("prepends face-swap instruction to customInstruction when usesFaceGuide is true", () => { + it("uses customInstruction even when usesFaceGuide is true", () => { const result = resolveImageInstruction( makeTemplate({ usesFaceGuide: true, styleGuide: { customInstruction: "Custom instruction here." } }), ); - expect(result).toContain(FACE_SWAP_INSTRUCTION); - expect(result).toContain("Custom instruction here."); - expect(result.indexOf(FACE_SWAP_INSTRUCTION)).toBeLessThan(result.indexOf("Custom instruction here.")); + expect(result).toBe("Custom instruction here."); }); it("falls back to FACE_SWAP_INSTRUCTION when usesFaceGuide is true and no customInstruction", () => { diff --git a/src/content/resolveImageInstruction.ts b/src/content/resolveImageInstruction.ts index 2c8d9a8..c5e62d2 100644 --- a/src/content/resolveImageInstruction.ts +++ b/src/content/resolveImageInstruction.ts @@ -8,11 +8,6 @@ import type { TemplateData } from "./loadTemplate"; */ export function resolveImageInstruction(template: TemplateData): string { const custom = template.styleGuide?.customInstruction; - if (typeof custom === "string" && custom.trim().length > 0) { - if (template.usesFaceGuide) { - return `${FACE_SWAP_INSTRUCTION} ${custom.trim()}`; - } - return custom.trim(); - } + if (typeof custom === "string" && custom.trim().length > 0) return custom.trim(); return template.usesFaceGuide ? FACE_SWAP_INSTRUCTION : NO_FACE_INSTRUCTION; } diff --git a/src/content/templates/artist-release-editorial/style-guide.json b/src/content/templates/artist-release-editorial/style-guide.json index 5ef7954..c9f2714 100644 --- a/src/content/templates/artist-release-editorial/style-guide.json +++ b/src/content/templates/artist-release-editorial/style-guide.json @@ -3,7 +3,7 @@ "description": "Editorial promo featuring artist press photo with playlist covers and DSP branding — the kind of polished-but-organic visual an artist's team drops alongside a new release", "usesFaceGuide": true, "usesImageOverlay": true, - "customInstruction": "Generate a clean editorial-style press photo of the artist. The image should contain ONLY the artist — no text, no overlays, no graphics, no album art, no branding. Just a professional press photo that looks like it was taken for a magazine feature or editorial spread. Polished lighting but still feeling authentic and personal.", + "customInstruction": "Replace the person in the scene with the person from the white background headshot. Use the face, hair, and features exactly as they appear in the headshot. Generate a clean editorial-style press photo of the artist. The image should contain ONLY the artist — no text, no overlays, no graphics, no album art, no branding. Just a professional press photo that looks like it was taken for a magazine feature or editorial spread. Polished lighting but still feeling authentic and personal. Remove any text, captions, watermarks, or overlays. The image should be clean with no text on it.", "imagePrompt": "A professional editorial press photo of an artist. Clean, intentional lighting — soft key light with subtle rim light separation. The artist is posed naturally, looking directly at camera or slightly off-axis. The background is a solid or subtly textured surface (concrete wall, draped fabric, muted gradient) that does not distract from the subject. The mood is confident and polished but not sterile. Shot on a DSLR or medium format camera, shallow depth of field, cinematic color grade leaning warm or desaturated depending on the artist's aesthetic. The image contains ONLY the artist — no text, no graphics, no overlays.", "camera": {