105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
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 };
|
||
}
|