Skip to content

Commit 472a6fe

Browse files
sweetmantechclaude
andauthored
fix: face-swap instruction and overlay image routing for editorial template (#126)
* 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) <noreply@anthropic.com> * fix: KISS — bake face-swap language into editorial style-guide.json 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 229b7a2 commit 472a6fe

4 files changed

Lines changed: 49 additions & 2 deletions

File tree

src/content/__tests__/loadArtistReleaseEditorial.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ describe("loadTemplate artist-release-editorial", () => {
3434
expect(typeof sg.customInstruction).toBe("string");
3535
});
3636

37+
it("customInstruction includes face-swap language referencing the headshot", async () => {
38+
const template = await loadTemplate("artist-release-editorial");
39+
const sg = template.styleGuide as Record<string, unknown>;
40+
const instruction = sg.customInstruction as string;
41+
const lower = instruction.toLowerCase();
42+
43+
expect(lower).toContain("headshot");
44+
expect(lower).toContain("face");
45+
});
46+
3747
it("customInstruction generates only the artist portrait with no composited elements", async () => {
3848
const template = await loadTemplate("artist-release-editorial");
3949
const sg = template.styleGuide as Record<string, unknown>;

src/content/templates/artist-release-editorial/style-guide.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"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",
44
"usesFaceGuide": true,
55
"usesImageOverlay": true,
6-
"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.",
6+
"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.",
77
"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.",
88

99
"camera": {

src/tasks/__tests__/createContentTask.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ vi.mock("../../content/fetchGithubFile", () => ({
3232
fetchGithubFile: vi.fn().mockResolvedValue(Buffer.from("fake-png")),
3333
}));
3434

35+
vi.mock("../../content/downloadImageBuffer", () => ({
36+
downloadImageBuffer: vi.fn().mockResolvedValue({
37+
buffer: Buffer.from("fake-image"),
38+
contentType: "image/png",
39+
}),
40+
}));
41+
42+
vi.mock("../../content/detectFace", () => ({
43+
detectFace: vi.fn().mockResolvedValue(false),
44+
}));
45+
3546
vi.mock("../../content/generateContentImage", () => ({
3647
generateContentImage: vi.fn().mockResolvedValue("https://fal.ai/image.png"),
3748
}));
@@ -138,6 +149,32 @@ describe("createContentTask", () => {
138149
await expect(mockRun(VALID_PAYLOAD)).rejects.toThrow("face-guide.png not found");
139150
});
140151

152+
it("does not pass additionalImageUrls to generateContentImage when usesImageOverlay is true", async () => {
153+
const { loadTemplate } = await import("../../content/loadTemplate");
154+
vi.mocked(loadTemplate).mockResolvedValueOnce({
155+
name: "artist-release-editorial",
156+
imagePrompt: "editorial scene",
157+
usesFaceGuide: true,
158+
usesImageOverlay: true,
159+
styleGuide: { customInstruction: "Generate editorial photo." },
160+
captionGuide: null,
161+
captionExamples: [],
162+
videoMoods: [],
163+
videoMovements: [],
164+
referenceImagePaths: [],
165+
});
166+
167+
await mockRun({
168+
...VALID_PAYLOAD,
169+
template: "artist-release-editorial",
170+
images: ["https://example.com/cover1.png", "https://example.com/cover2.png"],
171+
});
172+
173+
const { generateContentImage } = await import("../../content/generateContentImage");
174+
const callArgs = vi.mocked(generateContentImage).mock.calls[0][0];
175+
expect(callArgs.additionalImageUrls).toBeUndefined();
176+
});
177+
141178
it("throws when FAL_KEY is missing", async () => {
142179
delete process.env.FAL_KEY;
143180
await expect(mockRun(VALID_PAYLOAD)).rejects.toThrow("FAL_KEY");

src/tasks/createContentTask.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export const createContentTask = schemaTask({
100100
faceGuideUrl: faceGuideUrl ?? undefined,
101101
referenceImagePath,
102102
prompt: fullPrompt,
103-
additionalImageUrls,
103+
additionalImageUrls: template.usesImageOverlay ? undefined : additionalImageUrls,
104104
});
105105

106106
// --- Step 6: Upscale image (optional) ---

0 commit comments

Comments
 (0)