diff --git a/frontend/contexts/ProjectContext.tsx b/frontend/contexts/ProjectContext.tsx index af01554de..47b286a73 100644 --- a/frontend/contexts/ProjectContext.tsx +++ b/frontend/contexts/ProjectContext.tsx @@ -179,11 +179,14 @@ export function ProjectProvider({ children }: { children: React.ReactNode }) { createdAt: Date.now(), } - mutateProject(projectId, project => ({ + const persistedProject = mutateProject(projectId, project => ({ ...project, assets: [newAsset, ...project.assets], updatedAt: Date.now(), })) + if (!persistedProject) { + throw new Error(`Cannot add asset: project ${projectId} was not found`) + } return newAsset }, [mutateProject]) diff --git a/frontend/views/GenSpace.tsx b/frontend/views/GenSpace.tsx index 2f8de5750..8a5116a1d 100644 --- a/frontend/views/GenSpace.tsx +++ b/frontend/views/GenSpace.tsx @@ -1446,6 +1446,7 @@ export function GenSpace() { prompt: string input: { videoPath: string; direction: ExtendDirection; duration: number; videoDuration: number } } | null>(null) + const persistingExtendResultRef = useRef(null) const [retakeInitial, setRetakeInitial] = useState<{ videoPath: string | null duration?: number @@ -1850,60 +1851,69 @@ export function GenSpace() { })() }, [retakeResult, isRetaking, currentProjectId, activeProject?.assets, activeRetakeSource, addAsset, addTakeToAsset, setPendingRetakeUpdate, resetRetake]) - // When extend completes, save the longer video as a new asset. + // When extend completes, save the longer video as a new asset. Keep the recovery marker until + // the project write succeeds: copying/thumbnails can finish before a later storage error, and + // dropping the marker earlier would leave a valid output stranded with no retry path. useEffect(() => { if (!extendResult || !currentProjectId || isExtending) return const submission = extendSubmissionRef.current if (!submission) return - extendSubmissionRef.current = null - localStorage.removeItem(GENERATION_RECOVERY_KEY) + const generationKey = extendResult.videoPath + if (persistingExtendResultRef.current === generationKey) return + persistingExtendResultRef.current = generationKey ;(async () => { - const usedPrompt = submission.prompt - const usedInput = submission.input - const copied = await addVisualAssetToProject(extendResult.videoPath, currentProjectId, 'video') - if (!copied) { - logger.error('Could not persist extend result to project storage') - setLocalError(createLocalGenerationError('Failed to save extend output to project storage.')) - resetExtend() - return - } + try { + const usedPrompt = submission.prompt + const usedInput = submission.input + const copied = await addVisualAssetToProject(extendResult.videoPath, currentProjectId, 'video') + if (!copied) throw new Error('Could not persist extend result to project storage') - addAsset(currentProjectId, { - type: 'video', - path: copied.path, - bigThumbnailPath: copied.bigThumbnailPath, - smallThumbnailPath: copied.smallThumbnailPath, - width: copied.width, - height: copied.height, - prompt: usedPrompt, - resolution: '', - duration: usedInput.videoDuration + usedInput.duration, - generationParams: { - mode: 'extend', - prompt: usedPrompt, - model: 'pro', - duration: usedInput.videoDuration + usedInput.duration, - resolution: '', - fps: 24, - audio: true, - cameraMotion: 'none', - extendVideoPath: copied.path, - extendDuration: usedInput.duration, - extendDirection: usedInput.direction, - }, - takes: [{ + addAsset(currentProjectId, { + type: 'video', path: copied.path, bigThumbnailPath: copied.bigThumbnailPath, smallThumbnailPath: copied.smallThumbnailPath, width: copied.width, height: copied.height, - createdAt: Date.now(), - }], - activeTakeIndex: 0, - }) - setMode('video') - resetExtend() + prompt: usedPrompt, + resolution: '', + duration: usedInput.videoDuration + usedInput.duration, + generationParams: { + mode: 'extend', + prompt: usedPrompt, + model: 'pro', + duration: usedInput.videoDuration + usedInput.duration, + resolution: '', + fps: 24, + audio: true, + cameraMotion: 'none', + extendVideoPath: copied.path, + extendDuration: usedInput.duration, + extendDirection: usedInput.direction, + }, + takes: [{ + path: copied.path, + bigThumbnailPath: copied.bigThumbnailPath, + smallThumbnailPath: copied.smallThumbnailPath, + width: copied.width, + height: copied.height, + createdAt: Date.now(), + }], + activeTakeIndex: 0, + }) + + extendSubmissionRef.current = null + persistingExtendResultRef.current = null + localStorage.removeItem(GENERATION_RECOVERY_KEY) + setMode('video') + resetExtend() + } catch (error) { + persistingExtendResultRef.current = null + logger.error(`Failed to persist extend result: ${error}`) + setLocalError(createLocalGenerationError('Failed to save extend output to project storage. The app will retry automatically.')) + resetExtend() + } })() }, [extendResult, isExtending, currentProjectId, addAsset, resetExtend])