diff --git a/spx-gui/src/components/asset/gen/animation/AnimationSettingsInput.vue b/spx-gui/src/components/asset/gen/animation/AnimationSettingsInput.vue index d5717d731..37fab6913 100644 --- a/spx-gui/src/components/asset/gen/animation/AnimationSettingsInput.vue +++ b/spx-gui/src/components/asset/gen/animation/AnimationSettingsInput.vue @@ -4,7 +4,7 @@ import { useMessageHandle } from '@/utils/exception' import type { AnimationGen } from '@/models/spx/gen/animation-gen' import { UIButton } from '@/components/ui' import SettingsInput from '../common/SettingsInput.vue' -import ReferenceCostumeInput from '../common/ReferenceCostumeInput.vue' +import ReferenceImageInput from '../common/ReferenceImageInput.vue' import ArtStyleInput from '../common/ArtStyleInput.vue' import PerspectiveInput from '../common/PerspectiveInput.vue' import AnimationLoopModeInput from './AnimationLoopModeInput.vue' @@ -44,11 +44,13 @@ const submitText = computed(() => { @enrich="handleEnrich" > - diff --git a/spx-gui/src/components/asset/gen/common/ReferenceCostumeInput.vue b/spx-gui/src/components/asset/gen/common/ReferenceCostumeInput.vue deleted file mode 100644 index d73705f57..000000000 --- a/spx-gui/src/components/asset/gen/common/ReferenceCostumeInput.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/spx-gui/src/components/asset/gen/common/ReferenceImageInput.vue b/spx-gui/src/components/asset/gen/common/ReferenceImageInput.vue new file mode 100644 index 000000000..b4bbfc3ef --- /dev/null +++ b/spx-gui/src/components/asset/gen/common/ReferenceImageInput.vue @@ -0,0 +1,317 @@ + + + + + + + + + + {{ $t(name) }} + + + + + {{ $t(tooltipText) }} + + + + {{ $t(dropdownHintText) }} + + + + + + {{ option.name }} + + + + + + + + + + + + + + + + + + + + + diff --git a/spx-gui/src/components/asset/gen/costume/CostumeSettingsInput.vue b/spx-gui/src/components/asset/gen/costume/CostumeSettingsInput.vue index 8c97d50cc..1f3e4782e 100644 --- a/spx-gui/src/components/asset/gen/costume/CostumeSettingsInput.vue +++ b/spx-gui/src/components/asset/gen/costume/CostumeSettingsInput.vue @@ -4,7 +4,7 @@ import { useMessageHandle } from '@/utils/exception' import type { CostumeGen } from '@/models/spx/gen/costume-gen' import { UIButton } from '@/components/ui' import SettingsInput from '../common/SettingsInput.vue' -import ReferenceCostumeInput from '../common/ReferenceCostumeInput.vue' +import ReferenceImageInput from '../common/ReferenceImageInput.vue' import ArtStyleInput from '../common/ArtStyleInput.vue' import PerspectiveInput from '../common/PerspectiveInput.vue' import FacingInput from './FacingInput.vue' @@ -42,10 +42,12 @@ const submitText = computed(() => { @enrich="handleEnrich" > - diff --git a/spx-gui/src/components/asset/gen/sprite/SpriteSettingsInput.vue b/spx-gui/src/components/asset/gen/sprite/SpriteSettingsInput.vue index 1db68f5ef..03c8dfd27 100644 --- a/spx-gui/src/components/asset/gen/sprite/SpriteSettingsInput.vue +++ b/spx-gui/src/components/asset/gen/sprite/SpriteSettingsInput.vue @@ -6,6 +6,7 @@ import SettingsInput from '../common/SettingsInput.vue' import SpriteCategoryInput from './SpriteCategoryInput.vue' import ArtStyleInput from '../common/ArtStyleInput.vue' import PerspectiveInput from '../common/PerspectiveInput.vue' +import ReferenceImageInput from '../common/ReferenceImageInput.vue' import EnrichableSubmitButton from '../common/EnrichableSubmitButton.vue' const props = withDefaults( @@ -63,6 +64,10 @@ const submitText = computed(() => { + { :loading="imageGenerating" @enrich="handleEnrich" @submit="handleSubmit" - >{{ $t(submitText) }} + {{ $t(submitText) }} + diff --git a/spx-gui/src/models/spx/gen/animation-gen.test.ts b/spx-gui/src/models/spx/gen/animation-gen.test.ts index 7abe11725..f012a2aeb 100644 --- a/spx-gui/src/models/spx/gen/animation-gen.test.ts +++ b/spx-gui/src/models/spx/gen/animation-gen.test.ts @@ -141,7 +141,7 @@ describe('AnimationGen', () => { await gen.enrich() // Try to generate video without reference costume - await expect(gen.generateVideo()).rejects.toThrow('reference costume expected') + await expect(gen.generateVideo()).rejects.toThrow('reference image expected') }) it('should throw error when extracting frames without video', async () => { diff --git a/spx-gui/src/models/spx/gen/animation-gen.ts b/spx-gui/src/models/spx/gen/animation-gen.ts index 6d80ffcf2..8ae9ac3f5 100644 --- a/spx-gui/src/models/spx/gen/animation-gen.ts +++ b/spx-gui/src/models/spx/gen/animation-gen.ts @@ -36,6 +36,7 @@ export type AnimationGenInits = { id?: string settings?: Partial> referenceCostumeId?: string | null + referenceImage?: File | null video?: File framesConfig?: FramesConfig enrichPhase?: Phase @@ -48,9 +49,16 @@ export type AnimationGenInits = { export type RawAnimationGenConfig = Prettify< Omit< AnimationGenInits, - 'enrichPhase' | 'generateVideoTask' | 'generateVideoPhase' | 'extractFramesTask' | 'finishPhase' | 'video' + | 'enrichPhase' + | 'generateVideoTask' + | 'generateVideoPhase' + | 'extractFramesTask' + | 'finishPhase' + | 'video' + | 'referenceImage' > & { videoPath?: string + referenceImagePath?: string enrichPhaseSerialized?: PhaseSerialized generateVideoTaskSerialized?: TaskSerialized generateVideoPhaseSerialized?: PhaseSerialized @@ -96,6 +104,7 @@ export class AnimationGen extends Disposable { ...inits.settings } this.referenceCostumeId = inits.referenceCostumeId ?? null + this.referenceImage = inits.referenceImage ?? null this.enrichPhase = inits.enrichPhase ?? new Phase({ en: 'enrich animation settings', zh: '丰富动画设置' }) this.generateVideoTask = inits.generateVideoTask ?? null this.generateVideoPhase = @@ -161,6 +170,13 @@ export class AnimationGen extends Disposable { } setReferenceCostume(costumeId: string | null) { this.referenceCostumeId = costumeId + if (costumeId != null) this.referenceImage = null + } + + referenceImage: File | null = null + setReferenceImage(file: File | null) { + this.referenceImage = file + if (file != null) this.referenceCostumeId = null } get generateVideoState() { @@ -170,9 +186,9 @@ export class AnimationGen extends Disposable { this.setVideo(null) this.setFramesConfig(null) const video = await this.generateVideoPhase.run(async (reporter) => { - const costume = this.referenceCostume - if (costume == null) throw new Error('reference costume expected') - const referenceFrameUrl = await saveFile(costume.img) + const refImg = this.referenceImage ?? this.referenceCostume?.img ?? null + if (refImg == null) throw new Error('reference image expected') + const referenceFrameUrl = await saveFile(refImg) const settings = { ...this.settings, referenceFrameUrl } this.generateVideoTask?.tryCancel() this.generateVideoTask = new Task(TaskType.GenerateAnimationVideo) @@ -289,6 +305,12 @@ export class AnimationGen extends Disposable { config.videoPath = videoPath } + if (this.referenceImage != null) { + const refPath = `${assetsPath}/referenceImage${extname(this.referenceImage.name)}` + files[refPath] = this.referenceImage + config.referenceImagePath = refPath + } + return [config, files] } @@ -303,6 +325,7 @@ export class AnimationGen extends Disposable { id, settings, referenceCostumeId, + referenceImagePath, framesConfig, videoPath, enrichPhaseSerialized, @@ -320,6 +343,10 @@ export class AnimationGen extends Disposable { const inits: AnimationGenInits = { id: genId } inits.settings = settings if (referenceCostumeId != null) inits.referenceCostumeId = referenceCostumeId + if (referenceImagePath != null) { + const refFile = files[referenceImagePath] + if (refFile != null) inits.referenceImage = refFile + } if (framesConfig != null) inits.framesConfig = framesConfig if (enrichPhaseSerialized != null) inits.enrichPhase = Phase.load(enrichPhaseSerialized) if (generateVideoTaskSerialized != null) inits.generateVideoTask = Task.load(generateVideoTaskSerialized) diff --git a/spx-gui/src/models/spx/gen/costume-gen.ts b/spx-gui/src/models/spx/gen/costume-gen.ts index 381d4322e..4e195ba47 100644 --- a/spx-gui/src/models/spx/gen/costume-gen.ts +++ b/spx-gui/src/models/spx/gen/costume-gen.ts @@ -33,6 +33,7 @@ export type CostumeGenInits = { id?: string settings?: Partial> referenceCostumeId?: string + referenceImage?: File | null image?: File enrichPhase?: Phase generateTask?: Task @@ -41,8 +42,12 @@ export type CostumeGenInits = { } export type RawCostumeGenConfig = Prettify< - Omit & { + Omit< + CostumeGenInits, + 'result' | 'enrichPhase' | 'generateTask' | 'generatePhase' | 'finishPhase' | 'image' | 'referenceImage' + > & { imagePath?: string + referenceImagePath?: string enrichPhaseSerialized?: PhaseSerialized generateTaskSerialized?: TaskSerialized generatePhaseSerialized?: PhaseSerialized @@ -87,6 +92,7 @@ export class CostumeGen extends Disposable { ...inits.settings } this.referenceCostumeId = inits.referenceCostumeId ?? null + this.referenceImage = inits.referenceImage ?? null this.image = inits.image ?? null return reactive(this) as this } @@ -143,6 +149,13 @@ export class CostumeGen extends Disposable { } setReferenceCostume(costumeId: string | null) { this.referenceCostumeId = costumeId + if (costumeId != null) this.referenceImage = null + } + + referenceImage: File | null = null + setReferenceImage(file: File | null) { + this.referenceImage = file + if (file != null) this.referenceCostumeId = null } image: File | null = null @@ -156,8 +169,8 @@ export class CostumeGen extends Disposable { async generate() { this.setImage(null) const image = await this.generatePhase.run(async (reporter) => { - const referenceCostume = this.referenceCostume - const referenceImageUrl = referenceCostume != null ? await saveFile(referenceCostume.img) : null + const refImg = this.referenceImage ?? this.referenceCostume?.img ?? null + const referenceImageUrl = refImg != null ? await saveFile(refImg) : null const settings = { ...this.settings, referenceImageUrl } this.generateTask?.tryCancel() this.generateTask = new Task(TaskType.GenerateCostume) @@ -236,6 +249,11 @@ export class CostumeGen extends Disposable { finishPhaseSerialized } if (imagePath != null) config.imagePath = imagePath + if (this.referenceImage != null) { + const refPath = `${assetsPath}/referenceImage${extname(this.referenceImage.name)}` + files[refPath] = this.referenceImage + config.referenceImagePath = refPath + } return [config, files] } @@ -250,6 +268,7 @@ export class CostumeGen extends Disposable { id, settings, imagePath, + referenceImagePath, enrichPhaseSerialized, generateTaskSerialized, generatePhaseSerialized, @@ -262,6 +281,10 @@ export class CostumeGen extends Disposable { const inits: CostumeGenInits = { id: genId } if (settings != null) inits.settings = settings if (referenceCostumeId != null) inits.referenceCostumeId = referenceCostumeId + if (referenceImagePath != null) { + const refFile = files[referenceImagePath] + if (refFile != null) inits.referenceImage = refFile + } if (enrichPhaseSerialized != null) inits.enrichPhase = Phase.load(enrichPhaseSerialized) if (generateTaskSerialized != null) inits.generateTask = Task.load(generateTaskSerialized) if (generatePhaseSerialized != null) { diff --git a/spx-gui/src/models/spx/gen/sprite-gen.ts b/spx-gui/src/models/spx/gen/sprite-gen.ts index fc4d768f6..3e4c66caf 100644 --- a/spx-gui/src/models/spx/gen/sprite-gen.ts +++ b/spx-gui/src/models/spx/gen/sprite-gen.ts @@ -23,7 +23,7 @@ import type { Animation } from '../animation' import { getProjectSettings, mapPhaseResult, Phase, Task, type PhaseSerialized, type TaskSerialized } from './common' import { CostumeGen, type RawCostumeGenConfig } from './costume-gen' import { AnimationGen, type RawAnimationGenConfig } from './animation-gen' -import { createFileWithUniversalUrl } from '../../common/cloud' +import { createFileWithUniversalUrl, saveFile } from '../../common/cloud' import type { File, Files } from '../../common/file' import { fromConfig, toConfig, listDirs } from '../../common/file' import { @@ -53,6 +53,7 @@ export type SpriteGenInits = { settings?: Partial imageIndex?: number | null selectedItem?: SpriteGenSelected | null + referenceImage?: File | null animationGenIdBindings?: Partial> enrichPhase?: Phase genImagesTask?: Task @@ -65,8 +66,15 @@ export type SpriteGenInits = { export type RawSpriteGenConfig = Prettify< Omit< SpriteGenInits, - 'enrichPhase' | 'genImagesTask' | 'genImagesPhase' | 'prepareContentPhase' | 'costumes' | 'animations' + | 'enrichPhase' + | 'genImagesTask' + | 'genImagesPhase' + | 'prepareContentPhase' + | 'costumes' + | 'animations' + | 'referenceImage' > & { + referenceImagePath?: string enrichPhaseSerialized?: PhaseSerialized genImagesTaskSerialized?: TaskSerialized genImagesPhaseSerialized?: PhaseSerialized @@ -111,6 +119,7 @@ export class SpriteGen extends Disposable { } this.imageIndex = inits.imageIndex ?? null this.selectedItem = inits.selectedItem ?? null + this.referenceImage = inits.referenceImage ?? null this.previewProject = new SpxProject() this.previewSprite = this.createSprite() this.previewProject.addSprite(this.previewSprite) @@ -200,6 +209,9 @@ export class SpriteGen extends Disposable { this.setImageIndex(null) return this.genImagesPhase.run(async (reporter) => { const settings = this.getDefaultCostumeSettings() + if (this.referenceImage != null) { + settings.referenceImageUrl = await saveFile(this.referenceImage) + } this.genImagesTask?.tryCancel() this.genImagesTask = new Task(TaskType.GenerateCostume) await this.genImagesTask.start({ settings, n: 4 }) @@ -416,6 +428,11 @@ export class SpriteGen extends Disposable { this.selectedItem = item } + referenceImage: File | null = null + setReferenceImage(file: File | null) { + this.referenceImage = file + } + finish() { const previewSprite = this.previewSprite const sprite = this.createSprite() @@ -493,6 +510,7 @@ export class SpriteGen extends Disposable { settings, imageIndex, selectedItem, + referenceImagePath, animationGenIdBindings, enrichPhaseSerialized, genImagesTaskSerialized, @@ -510,6 +528,10 @@ export class SpriteGen extends Disposable { inits.settings = settings if (imageIndex != null) inits.imageIndex = imageIndex if (selectedItem != null) inits.selectedItem = selectedItem + if (referenceImagePath != null) { + const refFile = files[referenceImagePath] + if (refFile != null) inits.referenceImage = refFile + } if (animationGenIdBindings != null) inits.animationGenIdBindings = animationGenIdBindings if (enrichPhaseSerialized != null) inits.enrichPhase = Phase.load(enrichPhaseSerialized) if (genImagesTaskSerialized != null) inits.genImagesTask = Task.load(genImagesTaskSerialized) @@ -573,6 +595,11 @@ export class SpriteGen extends Disposable { costumeConfigs, animationConfigs } + if (this.referenceImage != null) { + const refPath = `${basePath}/referenceImage${extname(this.referenceImage.name)}` + files[refPath] = this.referenceImage + config.referenceImagePath = refPath + } if (this.imageIndex != null) config.imageIndex = this.imageIndex if (this.selectedItem != null) config.selectedItem = this.selectedItem files[`${spriteGenAssetPath}/${this.name}/${spriteGenConfigFileName}`] = fromConfig(spriteGenConfigFileName, config)