Fix prop confirmation bug, add Wan 2.7 model, refine multiple UI details, improve prop generation quality and aspect ratio, remove text overlays from Asset Center created images, and optimize prop filtering logic
This commit is contained in:
@@ -32,10 +32,15 @@ vi.mock('@/lib/llm-client', () => llmMock)
|
||||
vi.mock('@/lib/llm-observe/internal-stream-context', () => ({
|
||||
withInternalLLMStreamCallbacks: vi.fn(async (_callbacks: unknown, fn: () => Promise<unknown>) => await fn()),
|
||||
}))
|
||||
vi.mock('@/lib/constants', () => ({
|
||||
getArtStylePrompt: vi.fn(() => 'cinematic style'),
|
||||
removeLocationPromptSuffix: vi.fn((text: string) => text.replace(' [SUFFIX]', '')),
|
||||
}))
|
||||
vi.mock('@/lib/constants', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/constants')>()
|
||||
return {
|
||||
...actual,
|
||||
getArtStylePrompt: vi.fn(() => 'cinematic style'),
|
||||
removeLocationPromptSuffix: vi.fn((text: string) => text.replace(' [SUFFIX]', '')),
|
||||
removePropPromptSuffix: vi.fn((text: string) => text),
|
||||
}
|
||||
})
|
||||
vi.mock('@/lib/workers/shared', () => ({ reportTaskProgress: workerMock.reportTaskProgress }))
|
||||
vi.mock('@/lib/workers/utils', () => ({ assertTaskActive: workerMock.assertTaskActive }))
|
||||
vi.mock('@/lib/workers/handlers/llm-stream', () => ({
|
||||
@@ -125,7 +130,8 @@ describe('worker analyze-novel behavior', () => {
|
||||
props: [
|
||||
{
|
||||
name: '金箍棒',
|
||||
summary: '一根两头包裹金片的黑铁长棍',
|
||||
summary: '孙悟空随身铁棍法器',
|
||||
description: '一根黑铁长棍,两端包裹金色金属箍,表面磨损发亮,杆身笔直厚重',
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -194,7 +200,7 @@ describe('worker analyze-novel behavior', () => {
|
||||
{
|
||||
locationId: 'prop-new-1',
|
||||
imageIndex: 0,
|
||||
description: '一根两头包裹金片的黑铁长棍',
|
||||
description: '一根黑铁长棍,两端包裹金色金属箍,表面磨损发亮,杆身笔直厚重',
|
||||
availableSlots: '[]',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Job } from 'bullmq'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { CHARACTER_PROMPT_SUFFIX } from '@/lib/constants'
|
||||
import { CHARACTER_ASSET_IMAGE_RATIO, CHARACTER_PROMPT_SUFFIX, PROP_IMAGE_RATIO, PROP_PROMPT_SUFFIX } from '@/lib/constants'
|
||||
import { TASK_TYPE, type TaskJobData } from '@/lib/task/types'
|
||||
|
||||
const workersUtilsMock = vi.hoisted(() => ({
|
||||
@@ -27,7 +27,7 @@ const prismaMock = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
const sharedMock = vi.hoisted(() => ({
|
||||
generateLabeledImageToCos: vi.fn(async () => 'cos/generated-character.png'),
|
||||
generateCleanImageToStorage: vi.fn(async () => 'cos/generated-character.png'),
|
||||
parseJsonStringArray: vi.fn(() => []),
|
||||
}))
|
||||
|
||||
@@ -39,7 +39,7 @@ vi.mock('@/lib/workers/handlers/image-task-handler-shared', async () => {
|
||||
)
|
||||
return {
|
||||
...actual,
|
||||
generateLabeledImageToCos: sharedMock.generateLabeledImageToCos,
|
||||
generateCleanImageToStorage: sharedMock.generateCleanImageToStorage,
|
||||
parseJsonStringArray: sharedMock.parseJsonStringArray,
|
||||
}
|
||||
})
|
||||
@@ -93,13 +93,19 @@ describe('asset hub character image prompt suffix regression', () => {
|
||||
|
||||
await handleAssetHubImageTask(job)
|
||||
|
||||
const generationCall = sharedMock.generateLabeledImageToCos.mock.calls[0] as unknown as [{ prompt?: string }] | undefined
|
||||
const generationCall = sharedMock.generateCleanImageToStorage.mock.calls[0] as unknown as [{
|
||||
prompt?: string
|
||||
options?: { aspectRatio?: string }
|
||||
label?: string
|
||||
}] | undefined
|
||||
const callArg = generationCall?.[0]
|
||||
const prompt = callArg?.prompt || ''
|
||||
|
||||
expect(prompt).toContain('主角,黑发,冷静')
|
||||
expect(prompt).toContain(CHARACTER_PROMPT_SUFFIX)
|
||||
expect(countOccurrences(prompt, CHARACTER_PROMPT_SUFFIX)).toBe(1)
|
||||
expect(callArg?.options).toEqual(expect.objectContaining({ aspectRatio: CHARACTER_ASSET_IMAGE_RATIO }))
|
||||
expect(callArg?.label).toBeUndefined()
|
||||
})
|
||||
|
||||
it('honors requested count for global location generation', async () => {
|
||||
@@ -124,11 +130,42 @@ describe('asset hub character image prompt suffix regression', () => {
|
||||
locationId: 'global-location-1',
|
||||
imageCount: 1,
|
||||
})
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledTimes(1)
|
||||
expect(sharedMock.generateCleanImageToStorage).toHaveBeenCalledTimes(1)
|
||||
expect(prismaMock.globalLocationImage.update).toHaveBeenCalledTimes(1)
|
||||
expect(prismaMock.globalLocationImage.update).toHaveBeenCalledWith({
|
||||
where: { id: 'global-location-image-1' },
|
||||
data: { imageUrl: 'cos/generated-character.png' },
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the prop prompt suffix in global prop generation prompts', async () => {
|
||||
prismaMock.globalLocation.findFirst.mockResolvedValueOnce({
|
||||
id: 'global-prop-1',
|
||||
name: 'Silver Cutlery',
|
||||
images: [
|
||||
{
|
||||
id: 'global-prop-image-1',
|
||||
description: '银质餐具套装,包含刀叉与汤匙,线条简洁,金属冷白光泽',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await handleAssetHubImageTask(buildJob({
|
||||
type: 'prop',
|
||||
id: 'global-prop-1',
|
||||
}))
|
||||
|
||||
const generationCall = sharedMock.generateCleanImageToStorage.mock.calls[0] as unknown as [{
|
||||
prompt?: string
|
||||
options?: { aspectRatio?: string }
|
||||
label?: string
|
||||
}] | undefined
|
||||
const callArg = generationCall?.[0]
|
||||
const prompt = callArg?.prompt || ''
|
||||
|
||||
expect(prompt).toContain(PROP_PROMPT_SUFFIX)
|
||||
expect(countOccurrences(prompt, PROP_PROMPT_SUFFIX)).toBe(1)
|
||||
expect(callArg?.options).toEqual(expect.objectContaining({ aspectRatio: PROP_IMAGE_RATIO }))
|
||||
expect(callArg?.label).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,8 +25,9 @@ const prismaMock = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
const sharedMock = vi.hoisted(() => ({
|
||||
generateLabeledImageToCos: vi.fn<(input: {
|
||||
generateProjectLabeledImageToStorage: vi.fn<(input: {
|
||||
prompt: string
|
||||
label: string
|
||||
options?: { referenceImages?: string[]; aspectRatio?: string }
|
||||
}) => Promise<string>>(async () => 'cos/character-generated-0.png'),
|
||||
}))
|
||||
@@ -41,7 +42,7 @@ vi.mock('@/lib/workers/handlers/image-task-handler-shared', async () => {
|
||||
)
|
||||
return {
|
||||
...actual,
|
||||
generateLabeledImageToCos: sharedMock.generateLabeledImageToCos,
|
||||
generateProjectLabeledImageToStorage: sharedMock.generateProjectLabeledImageToStorage,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -101,8 +102,9 @@ describe('worker character-image-task-handler behavior', () => {
|
||||
imageUrl: 'cos/character-generated-0.png',
|
||||
})
|
||||
|
||||
const generationInput = sharedMock.generateLabeledImageToCos.mock.calls[0]?.[0] as {
|
||||
const generationInput = sharedMock.generateProjectLabeledImageToStorage.mock.calls[0]?.[0] as {
|
||||
prompt: string
|
||||
label: string
|
||||
options?: { referenceImages?: string[]; aspectRatio?: string }
|
||||
}
|
||||
const realisticStylePrompt = getArtStylePrompt('realistic', 'zh')
|
||||
@@ -111,6 +113,7 @@ describe('worker character-image-task-handler behavior', () => {
|
||||
expect(generationInput.prompt).toContain(realisticStylePrompt)
|
||||
expect(generationInput.prompt.split(CHARACTER_PROMPT_SUFFIX).length - 1).toBe(1)
|
||||
expect(generationInput.prompt.split(realisticStylePrompt).length - 1).toBe(1)
|
||||
expect(generationInput.label).toBe('Hero - 战斗形态')
|
||||
expect(generationInput.options).toEqual(expect.objectContaining({
|
||||
referenceImages: ['normalized-primary-ref'],
|
||||
aspectRatio: '3:2',
|
||||
@@ -129,7 +132,7 @@ describe('worker character-image-task-handler behavior', () => {
|
||||
const job = buildJob({ imageIndex: 0, artStyle: 'japanese-anime' })
|
||||
await handleCharacterImageTask(job)
|
||||
|
||||
const generationInput = sharedMock.generateLabeledImageToCos.mock.calls[0]?.[0] as {
|
||||
const generationInput = sharedMock.generateProjectLabeledImageToStorage.mock.calls[0]?.[0] as {
|
||||
prompt: string
|
||||
}
|
||||
expect(generationInput.prompt).toContain(getArtStylePrompt('japanese-anime', 'zh'))
|
||||
@@ -143,7 +146,7 @@ describe('worker character-image-task-handler behavior', () => {
|
||||
})
|
||||
|
||||
it('uses requested count for grouped generation and expands imageUrls to requested size', async () => {
|
||||
sharedMock.generateLabeledImageToCos
|
||||
sharedMock.generateProjectLabeledImageToStorage
|
||||
.mockResolvedValueOnce('cos/character-generated-0.png')
|
||||
.mockResolvedValueOnce('cos/character-generated-1.png')
|
||||
.mockResolvedValueOnce('cos/character-generated-2.png')
|
||||
@@ -152,7 +155,7 @@ describe('worker character-image-task-handler behavior', () => {
|
||||
|
||||
const result = await handleCharacterImageTask(buildJob({ count: 5 }))
|
||||
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledTimes(5)
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledTimes(5)
|
||||
expect(result).toEqual({
|
||||
appearanceId: 'appearance-2',
|
||||
imageCount: 5,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Job } from 'bullmq'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { LOCATION_IMAGE_RATIO, PROP_IMAGE_RATIO } from '@/lib/constants'
|
||||
import { TASK_TYPE, type TaskJobData } from '@/lib/task/types'
|
||||
|
||||
const utilsMock = vi.hoisted(() => ({
|
||||
@@ -118,7 +119,7 @@ describe('worker image-task-handlers-core', () => {
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
aspectRatio: '1:1',
|
||||
aspectRatio: LOCATION_IMAGE_RATIO,
|
||||
resolution: '1536x1024',
|
||||
referenceImages: ['required-reference-image', 'normalized-reference-image'],
|
||||
}),
|
||||
@@ -132,6 +133,34 @@ describe('worker image-task-handlers-core', () => {
|
||||
expect(updateData.imageUrl).toBe('cos/new-image.png')
|
||||
})
|
||||
|
||||
it('uses the character-matching aspect ratio when modifying project prop images', async () => {
|
||||
prismaMock.locationImage.findUnique.mockResolvedValueOnce({
|
||||
id: 'prop-image-1',
|
||||
locationId: 'prop-1',
|
||||
imageUrl: 'cos/prop-old.png',
|
||||
description: 'silver prop',
|
||||
previousDescription: null,
|
||||
location: { name: 'Silver Prop' },
|
||||
})
|
||||
|
||||
await handleModifyAssetImageTask(buildJob({
|
||||
type: 'prop',
|
||||
locationImageId: 'prop-image-1',
|
||||
modifyPrompt: 'make it brushed silver',
|
||||
generationOptions: { resolution: '1536x1024' },
|
||||
}))
|
||||
|
||||
expect(utilsMock.resolveImageSourceFromGeneration).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
aspectRatio: PROP_IMAGE_RATIO,
|
||||
resolution: '1536x1024',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('updates storyboard panel image and keeps candidateImages reset', async () => {
|
||||
prismaMock.novelPromotionPanel.findUnique.mockResolvedValue({
|
||||
id: 'panel-1',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Job } from 'bullmq'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getArtStylePrompt } from '@/lib/constants'
|
||||
import { LOCATION_IMAGE_RATIO, PROP_IMAGE_RATIO, getArtStylePrompt } from '@/lib/constants'
|
||||
import { TASK_TYPE, type TaskJobData } from '@/lib/task/types'
|
||||
|
||||
const utilsMock = vi.hoisted(() => ({
|
||||
@@ -20,7 +20,7 @@ const prismaMock = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
const sharedMock = vi.hoisted(() => ({
|
||||
generateLabeledImageToCos: vi.fn(async () => 'cos/location-generated-1.png'),
|
||||
generateProjectLabeledImageToStorage: vi.fn(async () => 'cos/location-generated-1.png'),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workers/utils', () => utilsMock)
|
||||
@@ -32,7 +32,7 @@ vi.mock('@/lib/workers/handlers/image-task-handler-shared', async () => {
|
||||
)
|
||||
return {
|
||||
...actual,
|
||||
generateLabeledImageToCos: sharedMock.generateLabeledImageToCos,
|
||||
generateProjectLabeledImageToStorage: sharedMock.generateProjectLabeledImageToStorage,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -100,32 +100,32 @@ describe('worker location-image-task-handler behavior', () => {
|
||||
locationIds: ['location-1'],
|
||||
})
|
||||
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('雨夜街道'),
|
||||
label: 'Old Town',
|
||||
targetId: 'location-image-1',
|
||||
options: expect.objectContaining({ aspectRatio: '1:1' }),
|
||||
options: expect.objectContaining({ aspectRatio: LOCATION_IMAGE_RATIO }),
|
||||
}),
|
||||
)
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('可站位置:'),
|
||||
}),
|
||||
)
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('街道左侧靠墙的留白位置'),
|
||||
}),
|
||||
)
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining('必须使用宽广完整的场景全景构图'),
|
||||
}),
|
||||
)
|
||||
const generationCall = sharedMock.generateLabeledImageToCos.mock.calls[0] as unknown as [{ prompt: string }] | undefined
|
||||
const generationCall = sharedMock.generateProjectLabeledImageToStorage.mock.calls[0] as unknown as [{ prompt: string }] | undefined
|
||||
expect(generationCall).toBeTruthy()
|
||||
if (!generationCall) throw new Error('expected generateLabeledImageToCos call')
|
||||
if (!generationCall) throw new Error('expected generateProjectLabeledImageToStorage call')
|
||||
const generationInput = generationCall[0]
|
||||
expect(generationInput.prompt.split(animeStylePrompt).length - 1).toBe(1)
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('worker location-image-task-handler behavior', () => {
|
||||
it('payload artStyle overrides project artStyle in prompt', async () => {
|
||||
await handleLocationImageTask(buildJob({ imageIndex: 0, artStyle: 'realistic' }))
|
||||
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledWith(
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: expect.stringContaining(getArtStylePrompt('realistic', 'zh')),
|
||||
}),
|
||||
@@ -169,11 +169,21 @@ describe('worker location-image-task-handler behavior', () => {
|
||||
updated: 1,
|
||||
locationIds: ['location-1'],
|
||||
})
|
||||
expect(sharedMock.generateLabeledImageToCos).toHaveBeenCalledTimes(1)
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledTimes(1)
|
||||
expect(prismaMock.locationImage.update).toHaveBeenCalledTimes(1)
|
||||
expect(prismaMock.locationImage.update).toHaveBeenCalledWith({
|
||||
where: { id: 'location-image-1' },
|
||||
data: { imageUrl: 'cos/location-generated-1.png' },
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the same aspect ratio as character generation for prop images', async () => {
|
||||
await handleLocationImageTask(buildJob({ type: 'prop', imageIndex: 0 }))
|
||||
|
||||
expect(sharedMock.generateProjectLabeledImageToStorage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({ aspectRatio: PROP_IMAGE_RATIO }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Job } from 'bullmq'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PROP_IMAGE_RATIO } from '@/lib/constants'
|
||||
import { TASK_TYPE, type TaskJobData, type TaskType } from '@/lib/task/types'
|
||||
|
||||
const utilsMock = vi.hoisted(() => ({
|
||||
@@ -27,6 +28,7 @@ const promptMock = vi.hoisted(() => ({
|
||||
PROMPT_IDS: {
|
||||
NP_CHARACTER_DESCRIPTION_UPDATE: 'np_character_description_update',
|
||||
NP_LOCATION_DESCRIPTION_UPDATE: 'np_location_description_update',
|
||||
NP_PROP_DESCRIPTION_UPDATE: 'np_prop_description_update',
|
||||
},
|
||||
buildPrompt: vi.fn(({ promptId }: { promptId: string }) => `${promptId}-prompt`),
|
||||
}))
|
||||
@@ -200,6 +202,16 @@ describe('modify image syncs descriptions after edit', () => {
|
||||
await handleAssetHubModifyTask(job)
|
||||
|
||||
expect(aiRuntimeMock.executeAiVisionStep).toHaveBeenCalledTimes(1)
|
||||
expect(utilsMock.stripLabelBar).not.toHaveBeenCalled()
|
||||
expect(utilsMock.withLabelBar).not.toHaveBeenCalled()
|
||||
expect(utilsMock.resolveImageSourceFromGeneration).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
referenceImages: ['https://signed/current-image.png', 'https://ref.example/b.png'],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
const globalCharacterUpdateCall = prismaMock.globalCharacterAppearance.update.mock.calls.at(-1) as [unknown] | undefined
|
||||
const updateArg = globalCharacterUpdateCall?.[0]
|
||||
@@ -246,6 +258,17 @@ describe('modify image syncs descriptions after edit', () => {
|
||||
|
||||
await handleAssetHubModifyTask(job)
|
||||
|
||||
expect(utilsMock.stripLabelBar).not.toHaveBeenCalled()
|
||||
expect(utilsMock.withLabelBar).not.toHaveBeenCalled()
|
||||
expect(utilsMock.resolveImageSourceFromGeneration).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
referenceImages: ['https://signed/current-image.png', 'https://ref.example/location.png'],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
const globalLocationUpdateCall = prismaMock.globalLocationImage.update.mock.calls.at(-1) as [unknown] | undefined
|
||||
const updateArg = globalLocationUpdateCall?.[0]
|
||||
const updateData = getUpdateData(updateArg)
|
||||
@@ -253,4 +276,60 @@ describe('modify image syncs descriptions after edit', () => {
|
||||
expect(updateData.description).toBe('VISION_UPDATED_LOCATION')
|
||||
expect(updateData.imageUrl).toBe('cos/new-global-location-image.png')
|
||||
})
|
||||
|
||||
it('syncs project prop descriptions for pure text edits', async () => {
|
||||
aiRuntimeMock.executeAiTextStep.mockResolvedValueOnce({ text: '{"prompt":"TEXT_UPDATED_PROP"}' })
|
||||
|
||||
const job = buildJob(TASK_TYPE.MODIFY_ASSET_IMAGE, {
|
||||
type: 'prop',
|
||||
locationId: 'location-1',
|
||||
imageIndex: 0,
|
||||
modifyPrompt: '把表面改成拉丝银色,并增加刻纹',
|
||||
})
|
||||
|
||||
await handleModifyAssetImageTask(job)
|
||||
|
||||
const locationUpdateCall = prismaMock.locationImage.update.mock.calls.at(-1) as [unknown] | undefined
|
||||
const updateData = getUpdateData(locationUpdateCall?.[0])
|
||||
expect(updateData.previousDescription).toBe('old location description')
|
||||
expect(updateData.description).toBe('TEXT_UPDATED_PROP')
|
||||
expect(updateData.imageUrl).toBe('cos/new-image.png')
|
||||
expect(utilsMock.resolveImageSourceFromGeneration).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
aspectRatio: PROP_IMAGE_RATIO,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('syncs asset-hub prop descriptions for reference-image edits', async () => {
|
||||
utilsMock.uploadImageSourceToCos.mockResolvedValueOnce('cos/new-global-prop-image.png')
|
||||
aiRuntimeMock.executeAiVisionStep.mockResolvedValueOnce({ text: '{"prompt":"VISION_UPDATED_PROP"}' })
|
||||
|
||||
const job = buildJob(TASK_TYPE.ASSET_HUB_MODIFY, {
|
||||
type: 'prop',
|
||||
id: 'global-location-1',
|
||||
imageIndex: 0,
|
||||
modifyPrompt: '改成磨砂银色餐具,去掉多余反光',
|
||||
extraImageUrls: ['https://ref.example/prop.png'],
|
||||
})
|
||||
|
||||
await handleAssetHubModifyTask(job)
|
||||
|
||||
const globalLocationUpdateCall = prismaMock.globalLocationImage.update.mock.calls.at(-1) as [unknown] | undefined
|
||||
const updateData = getUpdateData(globalLocationUpdateCall?.[0])
|
||||
expect(updateData.previousDescription).toBe('global location description')
|
||||
expect(updateData.description).toBe('VISION_UPDATED_PROP')
|
||||
expect(updateData.imageUrl).toBe('cos/new-global-prop-image.png')
|
||||
expect(utilsMock.resolveImageSourceFromGeneration).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
aspectRatio: PROP_IMAGE_RATIO,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -177,6 +177,8 @@ describe('worker reference-to-character', () => {
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ success: true }))
|
||||
expect(generatorApiMock.generateImage).toHaveBeenCalledTimes(3)
|
||||
expect(fontsMock.initializeFonts).not.toHaveBeenCalled()
|
||||
expect(fontsMock.createLabelSVG).not.toHaveBeenCalled()
|
||||
|
||||
const { prompt, options } = readGenerateCall(0)
|
||||
expect(prompt).toContain('冷静黑发角色')
|
||||
@@ -201,6 +203,8 @@ describe('worker reference-to-character', () => {
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ success: true }))
|
||||
expect(generatorApiMock.generateImage).toHaveBeenCalledTimes(3)
|
||||
expect(fontsMock.initializeFonts).not.toHaveBeenCalled()
|
||||
expect(fontsMock.createLabelSVG).not.toHaveBeenCalled()
|
||||
|
||||
const { prompt, options } = readGenerateCall(0)
|
||||
expect(prompt).toContain('BASE_REFERENCE_PROMPT')
|
||||
@@ -237,4 +241,20 @@ describe('worker reference-to-character', () => {
|
||||
expect(cosKeys).toHaveLength(5)
|
||||
expect(cosKeys?.every((item) => item.startsWith('cos/reference-key-'))).toBe(true)
|
||||
})
|
||||
|
||||
it('adds project label bars only for project reference generation', async () => {
|
||||
const job = buildJob(
|
||||
{
|
||||
referenceImageUrls: ['https://example.com/ref-a.png'],
|
||||
characterName: 'Hero',
|
||||
count: 1,
|
||||
},
|
||||
TASK_TYPE.REFERENCE_TO_CHARACTER,
|
||||
)
|
||||
|
||||
await handleReferenceToCharacterTask(job)
|
||||
|
||||
expect(fontsMock.initializeFonts).toHaveBeenCalledTimes(1)
|
||||
expect(fontsMock.createLabelSVG).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user