130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
import { addVisualAssetToProject } from './asset-copy'
|
|
import { toModelsDirRelativeRef } from './lora-library'
|
|
import { logger } from './logger'
|
|
import type { RecoveryGenType, RecoveryImporter } from './generation-recovery'
|
|
import { videoGenerationModeFromInputs } from './multi-keyframe'
|
|
|
|
// Mirrors GenSpace's own live-generation completion effect (see the `videoPath` effect in
|
|
// GenSpace.tsx) but operates on the recovery marker's captured context instead of live component
|
|
// state, so it can run in the background while that project's GenSpace isn't mounted. ic-lora/
|
|
// retake/extend write a marker with no full `settings` (see GenerationRecoveryContext) and
|
|
// recover as a standalone video asset here too; retake/extend additionally set `ctx.model` so
|
|
// the pipeline label isn't forced to 'fast'.
|
|
const importVideo: RecoveryImporter = async (ctx, result, { addAsset, modelsDir }) => {
|
|
const videoPath = typeof result === 'string' ? result : result[0]
|
|
if (!videoPath) return
|
|
|
|
const copied = await addVisualAssetToProject(videoPath, ctx.projectId, 'video')
|
|
if (!copied) throw new Error('Could not persist generated video to project storage')
|
|
|
|
const s = ctx.settings
|
|
const genMode = videoGenerationModeFromInputs({
|
|
keyframes: ctx.keyframes,
|
|
audioUrl: ctx.inputAudioUrl,
|
|
imageUrl: ctx.inputImageUrl,
|
|
})
|
|
|
|
addAsset(ctx.projectId, {
|
|
type: 'video',
|
|
path: copied.path,
|
|
bigThumbnailPath: copied.bigThumbnailPath,
|
|
smallThumbnailPath: copied.smallThumbnailPath,
|
|
width: copied.width,
|
|
height: copied.height,
|
|
prompt: ctx.prompt,
|
|
resolution: s?.videoResolution ?? '',
|
|
duration: s?.duration ?? undefined,
|
|
generationParams: {
|
|
mode: genMode,
|
|
prompt: ctx.prompt,
|
|
model: ctx.model ?? s?.model ?? 'fast',
|
|
modelLabel: ctx.modelLabel,
|
|
duration: s?.duration ?? null,
|
|
resolution: s?.videoResolution ?? '',
|
|
fps: s?.fps ?? 24,
|
|
audio: s?.audio ?? false,
|
|
cameraMotion: 'none',
|
|
imageAspectRatio: s?.aspectRatio,
|
|
imageSteps: 4,
|
|
inputImageUrl: ctx.inputImageUrl,
|
|
inputLastImageUrl: ctx.inputLastImageUrl,
|
|
inputAudioUrl: ctx.inputAudioUrl,
|
|
keyframes: ctx.keyframes && ctx.keyframes.length > 0 ? ctx.keyframes : undefined,
|
|
loras: s?.loras && s.loras.length > 0
|
|
? s.loras.map(l => ({ ref: toModelsDirRelativeRef(l.ref, modelsDir), name: l.name, scale: l.scale }))
|
|
: undefined,
|
|
},
|
|
takes: [{
|
|
path: copied.path,
|
|
bigThumbnailPath: copied.bigThumbnailPath,
|
|
smallThumbnailPath: copied.smallThumbnailPath,
|
|
width: copied.width,
|
|
height: copied.height,
|
|
createdAt: Date.now(),
|
|
}],
|
|
activeTakeIndex: 0,
|
|
})
|
|
}
|
|
|
|
// Mirrors GenSpace's `imagePaths` completion effect. A failed copy for one image in a multi-image
|
|
// batch is logged and skipped rather than thrown, same as the live effect — one bad file must not
|
|
// drop the rest of the batch. But if *every* copy in the batch fails, this must throw rather than
|
|
// return normally: checkAndConsumeRecovery treats a clean return as "imported, delete the
|
|
// marker" — silently succeeding on a total failure would drop the result for good.
|
|
const importImage: RecoveryImporter = async (ctx, result, { addAsset }) => {
|
|
const paths = Array.isArray(result) ? result : [result]
|
|
const s = ctx.settings
|
|
const genMode = ctx.inputImageUrl ? 'image-edit' : 'text-to-image'
|
|
let importedAny = false
|
|
|
|
for (const imgPath of paths) {
|
|
const copied = await addVisualAssetToProject(imgPath, ctx.projectId, 'image')
|
|
if (!copied) {
|
|
logger.error(`Could not persist generated image to project storage: ${imgPath}`)
|
|
continue
|
|
}
|
|
importedAny = true
|
|
addAsset(ctx.projectId, {
|
|
type: 'image',
|
|
path: copied.path,
|
|
bigThumbnailPath: copied.bigThumbnailPath,
|
|
smallThumbnailPath: copied.smallThumbnailPath,
|
|
width: copied.width,
|
|
height: copied.height,
|
|
prompt: ctx.prompt,
|
|
resolution: s?.imageResolution ?? '',
|
|
generationParams: {
|
|
mode: genMode,
|
|
prompt: ctx.prompt,
|
|
model: 'fast',
|
|
duration: 5,
|
|
resolution: s?.imageResolution ?? '',
|
|
fps: 24,
|
|
audio: false,
|
|
cameraMotion: 'none',
|
|
imageAspectRatio: s?.aspectRatio,
|
|
imageSteps: s?.imageSteps ?? 4,
|
|
...(ctx.inputImageUrl ? { inputImageUrl: ctx.inputImageUrl, imageEditStrength: s?.imageEditStrength } : {}),
|
|
},
|
|
takes: [{
|
|
path: copied.path,
|
|
bigThumbnailPath: copied.bigThumbnailPath,
|
|
smallThumbnailPath: copied.smallThumbnailPath,
|
|
width: copied.width,
|
|
height: copied.height,
|
|
createdAt: Date.now(),
|
|
}],
|
|
activeTakeIndex: 0,
|
|
})
|
|
}
|
|
|
|
if (!importedAny && paths.length > 0) {
|
|
throw new Error('Could not persist any generated image to project storage')
|
|
}
|
|
}
|
|
|
|
export const builtinRecoveryImporters: Partial<Record<RecoveryGenType, RecoveryImporter>> = {
|
|
video: importVideo,
|
|
image: importImage,
|
|
}
|