Files
prompt-optimizer/packages/ui/tests/unit/services/ImageStyleExtractor.reference-migration.spec.ts
linshen 46d7d0552d feat(image): refine reference style migration flow
- Strengthen the style migration contract around natural subject integration and visual hierarchy
- Remove the image2image reference extraction entry from the migrated UI flow
- Normalize migration placeholders and document the updated migration design
2026-04-01 23:24:51 +08:00

387 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockUnderstand, mockProcessTemplate } = vi.hoisted(() => ({
mockUnderstand: vi.fn(),
mockProcessTemplate: vi.fn(),
}))
vi.mock('@prompt-optimizer/core', async () => {
const actual = await vi.importActual<typeof import('@prompt-optimizer/core')>(
'@prompt-optimizer/core',
)
return {
...actual,
createImageUnderstandingService: () => ({
understand: mockUnderstand,
}),
TemplateProcessor: {
processTemplate: mockProcessTemplate,
},
}
})
import { resolveReferencePromptPreview } from '../../../src/services/ImageStyleExtractor'
describe('ImageStyleExtractor reference migration pipeline', () => {
const modelConfig = {
provider: 'gemini',
model: 'gemini-2.5-flash',
apiKey: 'test-key',
} as any
beforeEach(() => {
vi.clearAllMocks()
})
it('按参考图生成时,单次视觉调用直接返回最终提示词和变量', async () => {
const templateManager = {
getTemplate: vi.fn().mockResolvedValue({
id: 'image-prompt-from-reference-image',
name: 'Generate Prompt From Reference Image',
content: 'unused',
metadata: {
version: '1.0.0',
lastModified: Date.now(),
templateType: 'image-prompt-composition',
language: 'zh',
},
}),
}
mockProcessTemplate.mockReturnValue([
{ role: 'system', content: 'generate prompt from image' },
{ role: 'user', content: 'describe the image and return prompt plus defaults' },
])
mockUnderstand.mockResolvedValue({
content: JSON.stringify({
prompt: {
: {
: '一只{{主体颜色}}的猫',
: '胶片感插画',
},
},
defaults: {
: '棕色',
},
}),
})
const phases: string[] = []
const preview = await resolveReferencePromptPreview({
mode: 'replicate',
originalPrompt: '',
imageB64: 'ZmFrZS1pbWFnZQ==',
mimeType: 'image/png',
modelConfig,
templateManager: templateManager as any,
referenceMode: 'text2image',
onStageChange: (stage) => phases.push(stage),
})
expect(templateManager.getTemplate).toHaveBeenCalledWith('image-prompt-from-reference-image')
expect(mockUnderstand).toHaveBeenCalledTimes(1)
expect(mockUnderstand).toHaveBeenCalledWith(
expect.objectContaining({
modelConfig,
systemPrompt: 'generate prompt from image',
userPrompt: 'describe the image and return prompt plus defaults',
responseMimeType: 'application/json',
images: [
{
b64: 'ZmFrZS1pbWFnZQ==',
mimeType: 'image/png',
},
],
}),
)
expect(preview.prompt).toContain('{{主体颜色}}')
expect(preview.variableDefaults).toEqual({
: '棕色',
})
expect(preview.rawText).toContain('"defaults"')
expect(phases).toEqual(['generating-preview'])
})
it('风格迁移时,单次视觉调用保留原始主体语义并吸收参考图风格', async () => {
const templateManager = {
getTemplate: vi.fn().mockResolvedValue({
id: 'image-prompt-migration',
name: 'Migrate Prompt With Reference Image',
content: 'unused',
metadata: {
version: '1.0.0',
lastModified: Date.now(),
templateType: 'image-prompt-migration',
language: 'zh',
},
}),
}
mockProcessTemplate.mockReturnValue([
{ role: 'system', content: 'migrate prompt with image style' },
{ role: 'user', content: 'keep original subject, transfer the image style' },
])
mockUnderstand.mockResolvedValue({
content: JSON.stringify({
prompt: {
: {
: '两只{{主体颜色}}的猫',
: '胶片感、傍晚逆光',
},
},
defaults: {
: '棕色',
: '金毛犬',
},
}),
})
const preview = await resolveReferencePromptPreview({
mode: 'migrate',
originalPrompt: '两只棕色的猫',
imageB64: 'ZmFrZS1pbWFnZQ==',
mimeType: 'image/png',
modelConfig,
templateManager: templateManager as any,
referenceMode: 'text2image',
})
expect(templateManager.getTemplate).toHaveBeenCalledWith('image-prompt-migration')
expect(mockUnderstand).toHaveBeenCalledTimes(1)
expect(preview.prompt).toContain('两只{{主体颜色}}的猫')
expect(preview.prompt).toContain('胶片感、傍晚逆光')
expect(preview.prompt).not.toContain('金毛犬')
expect(preview.variableDefaults).toEqual({
: '棕色',
})
})
it('复刻图片时即使当前已有原始提示词,也只根据参考图生成结果', async () => {
const templateManager = {
getTemplate: vi.fn().mockResolvedValue({
id: 'image-prompt-from-reference-image',
name: 'Generate Prompt From Reference Image',
content: 'unused',
metadata: {
version: '1.0.0',
lastModified: Date.now(),
templateType: 'image-prompt-composition',
language: 'zh',
},
}),
}
mockProcessTemplate.mockImplementation((_template, context) => {
expect(context).toMatchObject({
originalPrompt: '',
})
expect(context.promptRequirement).toContain('当前没有原始提示词')
return [
{ role: 'system', content: 'replicate from image only' },
{ role: 'user', content: 'ignore current prompt and reconstruct from the image' },
]
})
mockUnderstand.mockResolvedValue({
content: JSON.stringify({
prompt: {
: {
: '一只{{主体颜色}}的小猫',
: '居中近景',
},
},
defaults: {
: '橘色',
},
}),
})
const preview = await resolveReferencePromptPreview({
mode: 'replicate',
originalPrompt: '两只棕色的猫,发在朋友圈',
imageB64: 'ZmFrZS1pbWFnZQ==',
mimeType: 'image/png',
modelConfig,
templateManager: templateManager as any,
referenceMode: 'text2image',
})
expect(templateManager.getTemplate).toHaveBeenCalledWith('image-prompt-from-reference-image')
expect(mockUnderstand).toHaveBeenCalledTimes(1)
expect(preview.prompt).toContain('{{主体颜色}}')
expect(preview.prompt).not.toContain('朋友圈')
expect(preview.variableDefaults).toEqual({
: '橘色',
})
})
it('只保留 prompt 中实际出现且合法的前 5 个变量默认值', async () => {
const templateManager = {
getTemplate: vi.fn().mockResolvedValue({
id: 'image-prompt-from-reference-image',
name: 'Generate Prompt From Reference Image',
content: 'unused',
metadata: {
version: '1.0.0',
lastModified: Date.now(),
templateType: 'image-prompt-composition',
language: 'zh',
},
}),
}
mockProcessTemplate.mockReturnValue([
{ role: 'system', content: 'generate prompt from image' },
{ role: 'user', content: 'return prompt plus defaults' },
])
mockUnderstand.mockResolvedValue({
content: JSON.stringify({
prompt: {
: {
: '{{数量}}只{{颜色}}的{{主体}}在{{场景}}里{{动作}}',
线: '黄昏逆光',
},
},
defaults: {
: '猫',
: '两',
: '棕色',
: '奔跑',
: '草地',
线: '黄昏逆光',
'无 效': 'bad',
},
}),
})
const preview = await resolveReferencePromptPreview({
mode: 'replicate',
originalPrompt: '',
imageB64: 'ZmFrZS1pbWFnZQ==',
mimeType: 'image/png',
modelConfig,
templateManager: templateManager as any,
referenceMode: 'text2image',
})
expect(preview.variableDefaults).toEqual({
: '猫',
: '两',
: '棕色',
: '奔跑',
: '草地',
})
expect(preview.variableDefaults).not.toHaveProperty('光线')
expect(preview.variableDefaults).not.toHaveProperty('无 效')
})
it('会将单花括号占位规范化为双花括号,并在缺失 defaults 时保留空变量值', async () => {
const templateManager = {
getTemplate: vi.fn().mockResolvedValue({
id: 'image-prompt-migration',
name: 'Migrate Prompt With Reference Image',
content: 'unused',
metadata: {
version: '1.0.0',
lastModified: Date.now(),
templateType: 'image-prompt-migration',
language: 'zh',
},
}),
}
mockProcessTemplate.mockReturnValue([
{ role: 'system', content: 'migrate prompt with image style' },
{ role: 'user', content: 'keep original subject, transfer the image style' },
])
mockUnderstand.mockResolvedValue({
content: JSON.stringify({
prompt: {
: '一辆 {跑车颜色} 的未来感跑车',
: '{场景主题}',
: '3D动画渲染电影级氛围',
},
defaults: {},
}),
})
const preview = await resolveReferencePromptPreview({
mode: 'migrate',
originalPrompt: '一辆银色的未来感跑车,停在雨夜街头',
imageB64: 'ZmFrZS1pbWFnZQ==',
mimeType: 'image/png',
modelConfig,
templateManager: templateManager as any,
referenceMode: 'text2image',
})
expect(preview.prompt).toContain('{{跑车颜色}}')
expect(preview.prompt).toContain('{{场景主题}}')
expect(preview.variableDefaults).toEqual({
: '',
: '',
})
})
it('会将中文书名号包裹的变量占位规范化为双花括号', async () => {
const templateManager = {
getTemplate: vi.fn().mockResolvedValue({
id: 'image-prompt-migration',
name: 'Migrate Prompt With Reference Image',
content: 'unused',
metadata: {
version: '1.0.0',
lastModified: Date.now(),
templateType: 'image-prompt-migration',
language: 'zh',
},
}),
}
mockProcessTemplate.mockReturnValue([
{ role: 'system', content: 'migrate prompt with image style' },
{ role: 'user', content: 'keep original subject, transfer the image style' },
])
mockUnderstand.mockResolvedValue({
content: JSON.stringify({
prompt: {
: '一个「主体人物」,「人物动作」',
: '保留参考图的「背景元素」和灯光结构',
},
defaults: {
: '年轻男生',
: '戴着耳机,侧脸特写',
: '城市夜景',
},
}),
})
const preview = await resolveReferencePromptPreview({
mode: 'migrate',
originalPrompt: '一个戴着耳机的年轻男生侧脸特写',
imageB64: 'ZmFrZS1pbWFnZQ==',
mimeType: 'image/png',
modelConfig,
templateManager: templateManager as any,
referenceMode: 'text2image',
})
expect(preview.prompt).toContain('{{主体人物}}')
expect(preview.prompt).toContain('{{人物动作}}')
expect(preview.prompt).toContain('{{背景元素}}')
expect(preview.prompt).not.toContain('「主体人物」')
expect(preview.variableDefaults).toEqual({
: '年轻男生',
: '戴着耳机,侧脸特写',
: '城市夜景',
})
})
})