import type { DesignAsset, DesignSpecificationValues, DesignUserFieldOperation, DesignWorkspace, } from '../../../shared/image-workspace'; import { insertReferenceToken, promptReferencesImage, } from './reference-tokens'; type ReferenceValue = DesignSpecificationValues['references'][number]; function createReferenceId(): string { const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `reference-${random}`.slice(0, 64); } export function creatorPromptForWorkspace(workspace: DesignWorkspace): string { const values = workspace.form.specification.values; return values.content.concept?.trim() || values.content.narrative?.trim() || [ ...values.content.subjects.map((subject) => subject.description.trim()), values.content.action?.trim(), values.content.environment?.trim(), values.content.emotional_tone?.trim(), ].filter((value): value is string => Boolean(value)).join(','); } /** * Build the explicit field changes for using an existing image as a video's * first frame. The asset is only bound to the design; it is never copied or * deleted, and existing reference rows stay in place so @图片 numbers remain * stable. */ export function createVideoFirstFrameOperations( workspace: DesignWorkspace, asset: DesignAsset, promptOverride?: string, ): { operations: DesignUserFieldOperation[]; nextPrompt: string; referenceIndex: number } { if (asset.mediaType !== 'image') { throw new Error('视频开始画面必须使用图片'); } const values = workspace.form.specification.values; const references = values.references; const currentFirstFrameIndex = references.findIndex((reference) => ( reference.role === 'first_frame' || reference.asset_id === values.video.first_frame_asset_id )); const selectedReferenceIndex = references.findIndex((reference) => ( reference.asset_id === asset.assetId )); let nextReferences: ReferenceValue[]; let referenceIndex: number; if (selectedReferenceIndex >= 0) { referenceIndex = selectedReferenceIndex; nextReferences = references.map((reference, index) => { if (index === referenceIndex) return { ...reference, role: 'first_frame' }; if (index === currentFirstFrameIndex && index !== referenceIndex) { return { ...reference, role: 'inspiration' }; } return reference; }); } else if (currentFirstFrameIndex >= 0) { referenceIndex = currentFirstFrameIndex; nextReferences = references.map((reference, index) => ( index === referenceIndex ? { ...reference, asset_id: asset.assetId, asset_revision: null, role: 'first_frame' } : reference )); } else { referenceIndex = references.length; nextReferences = [ ...references, { id: createReferenceId(), asset_id: asset.assetId, asset_revision: null, role: 'first_frame', preserve: [], adapt: [], do_not_copy: [], reviewed_observations: [], }, ]; } const prompt = promptOverride ?? creatorPromptForWorkspace(workspace); const inserted = promptReferencesImage(prompt, referenceIndex) ? { value: prompt } : insertReferenceToken(prompt, referenceIndex, null); const operations: DesignUserFieldOperation[] = [ { kind: 'set', path: 'references', value: nextReferences }, { kind: 'set', path: 'video.first_frame_asset_id', value: asset.assetId }, ]; if (inserted.value.trim() !== prompt.trim()) { operations.push({ kind: 'set', path: 'content.concept', value: inserted.value.trim() }); } return { operations, nextPrompt: inserted.value.trim(), referenceIndex }; }