Browse Source

Constrain lighting relight outputs

TEST-x
xuzhijie 2 months ago
parent
commit
541689e1b5
  1. 40
      src/components/__tests__/GenerateImageNode.test.tsx
  2. 3
      src/components/nodes/GenerateImageNode.tsx
  3. 3
      src/store/execution/__tests__/nanoBananaExecutor.test.ts
  4. 9
      src/utils/__tests__/lighting.test.ts
  5. 18
      src/utils/lighting.ts

40
src/components/__tests__/GenerateImageNode.test.tsx

@ -362,6 +362,46 @@ describe("GenerateImageNode", () => {
expect(screen.getByTitle("Next image")).toBeInTheDocument();
});
it("should hide carousel controls when generations path is not configured", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
const state = {
updateNodeData: mockUpdateNodeData,
regenerateNode: mockRegenerateNode,
addNode: mockAddNode,
incrementModalCount: mockIncrementModalCount,
decrementModalCount: mockDecrementModalCount,
providerSettings: defaultProviderSettings,
generationsPath: null,
isRunning: false,
currentNodeIds: [],
groups: {},
nodes: [],
recentModels: [],
trackModelUsage: vi.fn(),
getNodesWithComments: vi.fn(() => []),
markCommentViewed: vi.fn(),
setNavigationTarget: vi.fn(),
};
return selector(state);
});
render(
<TestWrapper>
<GenerateImageNode {...createNodeProps({
outputImage: "data:image/png;base64,abc123",
imageHistory: [
{ id: "img1", timestamp: Date.now(), prompt: "test1", aspectRatio: "1:1", model: "nano-banana" },
{ id: "img2", timestamp: Date.now(), prompt: "test2", aspectRatio: "1:1", model: "nano-banana" },
],
selectedHistoryIndex: 0,
})} />
</TestWrapper>
);
expect(screen.queryByTitle("Previous image")).not.toBeInTheDocument();
expect(screen.queryByTitle("Next image")).not.toBeInTheDocument();
});
it("should show current position in carousel", () => {
render(
<TestWrapper>

3
src/components/nodes/GenerateImageNode.tsx

@ -335,7 +335,6 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
const loadImageById = useCallback(async (imageId: string) => {
if (!generationsPath) {
console.error("Generations path not configured");
return null;
}
@ -445,7 +444,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
const supportsResolution = currentModelId === "nano-banana-pro" || currentModelId === "nano-banana-2";
const aspectRatios = currentModelId === "nano-banana-2" ? EXTENDED_ASPECT_RATIOS : BASE_ASPECT_RATIOS;
const resolutions = currentModelId === "nano-banana-2" ? RESOLUTIONS_NB2 : RESOLUTIONS_PRO;
const hasCarouselImages = (nodeData.imageHistory || []).length > 1;
const hasCarouselImages = Boolean(generationsPath) && (nodeData.imageHistory || []).length > 1;
// Count visible Gemini controls to match ModelParameters grid/max-width rules
const geminiControlCount = 2 // Model + Aspect Ratio (always)

3
src/store/execution/__tests__/nanoBananaExecutor.test.ts

@ -216,7 +216,8 @@ describe("executeNanoBanana", () => {
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body.prompt).toContain("right side key light");
expect(body.prompt).toContain("70% brightness");
expect(body.prompt).toContain("Preserve the original subject identity");
expect(body.prompt).toContain("same exact person");
expect(body.prompt).toContain("Only change illumination");
});
it("uses NewApiWG default for legacy image nodes when Gemini is not configured", async () => {

9
src/utils/__tests__/lighting.test.ts

@ -38,10 +38,13 @@ describe("lighting utilities", () => {
mainLightDirection: "front",
});
expect(prompt).toContain("Image editing task");
expect(prompt).toContain("STRICT image-to-image relighting task");
expect(prompt).toContain("uploaded image");
expect(prompt).toContain("relight the uploaded image");
expect(prompt).toContain("Only change the lighting");
expect(prompt).toContain("Do not add new people or objects");
expect(prompt).toContain("same exact person");
expect(prompt).toContain("Preserve the original composition");
expect(prompt).toContain("Only change illumination");
expect(prompt).toContain("lighting reference only");
expect(prompt).toContain("Do not add or remove people or objects");
});
});

18
src/utils/lighting.ts

@ -27,7 +27,7 @@ export const LIGHTING_PRESETS: LightingPreset[] = [
mainLightDirection: "front",
lightPosition: { x: 0, y: 0, z: 1 },
beamAngle: 45,
smartPrompt: "柯达胶片感",
smartPrompt: "柯达胶片感的曝光和柔和高光,只调整打光",
},
},
{
@ -71,7 +71,7 @@ export const LIGHTING_PRESETS: LightingPreset[] = [
lightPosition: { x: 0, y: 0, z: 1 },
beamAngle: 45,
rimLight: false,
smartPrompt: "让画面拥有《银翼杀手2045》同款打光,极简背景",
smartPrompt: "《银翼杀手2045》同款青紫高反差打光,只调整光影",
},
},
{
@ -113,7 +113,7 @@ export const LIGHTING_PRESETS: LightingPreset[] = [
mainLightDirection: "front",
lightPosition: { x: 0, y: 0, z: 1 },
beamAngle: 45,
smartPrompt: "让画面光影变成“黄金时刻”",
smartPrompt: "黄金时刻暖色光影,只调整光照和阴影",
},
},
{
@ -127,7 +127,7 @@ export const LIGHTING_PRESETS: LightingPreset[] = [
mainLightDirection: "front",
lightPosition: { x: 0, y: 0, z: 1 },
beamAngle: 45,
smartPrompt: "让画面质感变成清晨同款冷灰色调",
smartPrompt: "清晨冷灰色温和柔和阴影,只整光照和色温",
},
},
];
@ -236,11 +236,13 @@ export function buildOptimizedLightingPrompt(
: "User intent: relight the uploaded image according to the lighting plan.";
return [
"Image editing task: use the uploaded image as the source image and generate one finished relit image.",
"STRICT image-to-image relighting task: use the uploaded image as the locked source image.",
userIntent,
lightingPrompt,
"Preserve the original subject identity, pose, clothing, composition, camera angle, and image style as much as possible.",
"Only change the lighting, shadow direction, highlight intensity, and ambient mood required by the lighting plan.",
"Do not add new people or objects, do not crop the subject, do not change the character design, and do not create a different scene.",
"The output must show the same exact person from the source image: identical face identity, facial features, age, expression, hair, body shape, hands, pose, clothing, accessories, and camera/phone.",
"Preserve the original composition, camera angle, crop, background, object layout, readable text overlays, and image style.",
"Only change illumination: light direction, brightness, color temperature, shadows, highlights, reflections, and ambient lighting mood required by the lighting plan.",
"Treat any preset or style wording as a lighting reference only, not permission to redesign the person, outfit, background, pose, scene, or photographic composition.",
"Do not add or remove people or objects, do not beautify or re-age the person, do not replace the face, do not change hairstyle or clothing, do not crop the subject, and do not create a different scene.",
].join("\n");
}

Loading…
Cancel
Save