Browse Source

Fix reference image handling for lighting

- Reference image is always sent as second image to model (not limited
  to smart free mode), original image stays as first image
- Prompt explicitly tells model: first image is source, second is
  lighting reference to analyze and apply
- Preview always shows original image, reference image no longer
  replaces it in the globe preview
- Show uploaded reference image thumbnail with clear button in UI
- Add rim light prompt with specific position description and color
TEST-x
xuzhijie 2 months ago
parent
commit
12256e16ea
  1. 33
      src/components/lighting/LightingEffectModal.tsx
  2. 5
      src/store/execution/nanoBananaExecutor.ts
  3. 4
      src/utils/__tests__/lighting.test.ts
  4. 14
      src/utils/lighting.ts

33
src/components/lighting/LightingEffectModal.tsx

@ -1007,7 +1007,8 @@ export function LightingEffectModal({
}, [isOpen, value]); }, [isOpen, value]);
const modalWidthClass = settings.smartMode ? "max-w-[760px]" : "max-w-[458px]"; const modalWidthClass = settings.smartMode ? "max-w-[760px]" : "max-w-[458px]";
const appliedPreviewImage = settings.referenceImage || sourceImage; // 预览始终显示原始图片(sourceImage)作为打光主体,参考图不替代原图
const appliedPreviewImage = sourceImage;
const selectedPreset = useMemo( const selectedPreset = useMemo(
() => LIGHTING_PRESETS.find((preset) => preset.id === settings.selectedPresetId), () => LIGHTING_PRESETS.find((preset) => preset.id === settings.selectedPresetId),
@ -1221,9 +1222,35 @@ export function LightingEffectModal({
<button <button
type="button" type="button"
onClick={() => referenceInputRef.current?.click()} onClick={() => referenceInputRef.current?.click()}
className="rounded-lg border border-dashed border-neutral-700 bg-neutral-900 px-2 text-xs text-neutral-400 transition-colors hover:border-neutral-500 hover:text-neutral-100" className="relative h-20 w-20 shrink-0 overflow-hidden rounded-lg border border-dashed border-neutral-700 bg-neutral-900 text-xs text-neutral-400 transition-colors hover:border-neutral-500 hover:text-neutral-100"
> >
+<br /> {settings.referenceImage ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={settings.referenceImage} alt="打光参考图" className="h-full w-full object-cover" />
<div
role="button"
tabIndex={0}
className="absolute right-0.5 top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-black/60 text-[10px] leading-none text-white hover:bg-red-600"
onClick={(e) => {
e.stopPropagation();
patchSettings({ referenceImage: null });
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.stopPropagation();
patchSettings({ referenceImage: null });
}
}}
>
×
</div>
</>
) : (
<span className="flex h-full items-center justify-center text-center leading-tight">
+<br />
</span>
)}
</button> </button>
</div> </div>
<input <input

5
src/store/execution/nanoBananaExecutor.ts

@ -106,8 +106,9 @@ export async function executeNanoBanana(
const activeLighting = nodeData.lighting?.enabled ? nodeData.lighting : upstreamLighting; const activeLighting = nodeData.lighting?.enabled ? nodeData.lighting : upstreamLighting;
const lightingPrompt = images.length > 0 ? buildLightingPrompt(activeLighting) : ""; const lightingPrompt = images.length > 0 ? buildLightingPrompt(activeLighting) : "";
// 智能自由模式:将打光参考图追加到图片数组,作为第二张图传给模型 // 打光参考图:将参考图追加到图片数组,作为第二张图传给模型
if (isSmartFreeMode(activeLighting) && activeLighting.referenceImage) { // 原始图片始终为第一张,参考图为第二张,模型根据参考图的打光风格对原图重打光
if (activeLighting?.enabled && activeLighting.referenceImage) {
images = [...images, activeLighting.referenceImage]; images = [...images, activeLighting.referenceImage];
} }

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

@ -113,7 +113,7 @@ describe("lighting utilities", () => {
referenceImage: "data:image/png;base64,abc", referenceImage: "data:image/png;base64,abc",
}); });
expect(prompt).toContain("second image is provided"); expect(prompt).toContain("SECOND image is the lighting reference");
expect(prompt).toContain("lighting reference"); expect(prompt).toContain("FIRST image");
}); });
}); });

14
src/utils/lighting.ts

@ -282,8 +282,11 @@ export function buildLightingPrompt(settings?: LightingSettings | null): string
beamAngleInstruction(lighting.beamAngle), beamAngleInstruction(lighting.beamAngle),
]; ];
// 轮廓光:在主光源之外追加第二个光源,位于主体后方
// 颜色与主光源一致,位置由用户在球体背面拖拽决定
if (lighting.rimLight) { if (lighting.rimLight) {
const rimPos = lighting.rimLightPosition ?? DEFAULT_LIGHTING_SETTINGS.rimLightPosition; const rimPos = lighting.rimLightPosition ?? DEFAULT_LIGHTING_SETTINGS.rimLightPosition;
// 根据 3D 坐标判断轮廓光的水平/垂直/深度方位,生成可读描述
const rimHoriz = rimPos.x <= -0.25 ? "camera-left" : rimPos.x >= 0.25 ? "camera-right" : "centered"; const rimHoriz = rimPos.x <= -0.25 ? "camera-left" : rimPos.x >= 0.25 ? "camera-right" : "centered";
const rimVert = rimPos.y <= -0.25 ? "above" : rimPos.y >= 0.25 ? "below" : "at subject level"; const rimVert = rimPos.y <= -0.25 ? "above" : rimPos.y >= 0.25 ? "below" : "at subject level";
const rimDepth = rimPos.z <= -0.25 ? "directly behind" : rimPos.z >= 0.25 ? "in front of" : "beside"; const rimDepth = rimPos.z <= -0.25 ? "directly behind" : rimPos.z >= 0.25 ? "in front of" : "beside";
@ -319,7 +322,8 @@ export function buildOptimizedLightingPrompt(
): string { ): string {
const lightingPrompt = buildLightingPrompt(settings); const lightingPrompt = buildLightingPrompt(settings);
const userPrompt = (basePrompt ?? "").trim(); const userPrompt = (basePrompt ?? "").trim();
const smartRef = isSmartFreeMode(settings); const lighting = normalizeLightingSettings(settings);
const hasRefImage = !!lighting.referenceImage;
// 打光未启用 → 原样返回用户提示词 // 打光未启用 → 原样返回用户提示词
if (!lightingPrompt) { if (!lightingPrompt) {
@ -332,7 +336,7 @@ export function buildOptimizedLightingPrompt(
// 严格约束:锁定人物身份、构图、场景,只允许改变光照 // 严格约束:锁定人物身份、构图、场景,只允许改变光照
const constraints = [ const constraints = [
"STRICT image-to-image relighting task: use the uploaded image as the locked source image.", "STRICT image-to-image relighting task: the FIRST image is the source image to be relit — do not modify its content.",
userIntent, userIntent,
lightingPrompt, lightingPrompt,
"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.", "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.",
@ -342,10 +346,10 @@ export function buildOptimizedLightingPrompt(
"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.", "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.",
]; ];
// 智能参考图模式:告知模型第二张图片是打光参考 // 打光参考图:第二张图片是打光参考,分析其光照风格应用到原图
if (smartRef) { if (hasRefImage) {
constraints.push( constraints.push(
"If a second image is provided, use it as the lighting reference — match its lighting style, direction, color temperature, and mood." "The SECOND image is the lighting reference — analyze its lighting style, direction, color temperature, shadows, and mood, then apply the same lighting to the FIRST image."
); );
} }

Loading…
Cancel
Save