统一音视频时长展示元数据

删除 EditorAsset、CanvasLayer 与画布布局中的通用 durationSeconds 链路。
将上传及生成请求时长统一写入 generationInputs.fields,并由服务端去重补齐。
音频播放只使用 loadedmetadata,素材详情、导出和精选成本读取展示字段。
补充前后端回归测试并同步架构文档与共享决策。
This commit is contained in:
2026-08-05 15:22:24 +08:00
parent a3d3a1b8ba
commit dcf77ad7a8
34 changed files with 295 additions and 123 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import type { EditorProjectResourceSnapshot } from '../../services/image-editor/editorProjectClient';
import { calculateEditorVideoPrice } from '../image-editor/ImageCanvasGenerationModel';
import { buildCreationShowcaseItems } from './creationShowcaseModel';
function createResource(
@@ -544,6 +545,34 @@ describe('creationShowcaseModel', () => {
expect(items[0]?.cost).toBe('5泥点');
});
it('infers video cost from the duration field instead of legacy top-level metadata', () => {
const items = buildCreationShowcaseItems({
activeTab: 'all',
projectResources: [
createResource({
resourceId: 'video-1',
label: '角色宣传片',
assetKind: 'video',
imageSrc: '/generated-editor-videos/video.mp4',
model: 'seedance2.0-fast',
generationInputs: {
fields: [
{ title: '清晰度', value: '720p' },
{ title: '时长', value: '6秒' },
],
references: [],
durationSeconds: 4,
},
}),
],
});
expect(items).toHaveLength(1);
expect(items[0]?.cost).toBe(
`${calculateEditorVideoPrice('seedance2.0-fast', '720p', 6)}泥点`,
);
});
it('keeps character animation videos out of the marketing tab', () => {
const items = buildCreationShowcaseItems({
activeTab: 'marketing',
@@ -819,7 +819,7 @@ function inferAssetCost(asset: EditorAssetSnapshot) {
inferVideoResolution(asset, ['480p', '720p', '1080p'], '480p') as
'480p' | '720p' | '1080p',
normalizeInt(
asset.generationInputs?.durationSeconds,
inferMediaDurationSeconds(asset),
DEFAULT_VIDEO_DURATION_SECONDS,
4,
15,
@@ -832,6 +832,15 @@ function inferAssetCost(asset: EditorAssetSnapshot) {
return null;
}
function inferMediaDurationSeconds(asset: EditorAssetSnapshot) {
const durationField = asset.generationInputs?.fields.find((field) => {
const title = field.title.trim().toLowerCase();
return title === '时长' || title === 'duration';
});
const matchedValue = durationField?.value.match(/\d+(?:\.\d+)?/u)?.[0];
return matchedValue ? Number(matchedValue) : undefined;
}
function inferVideoResolution(
asset: EditorAssetSnapshot,
allowedValues: string[],
@@ -737,7 +737,7 @@ describe('ImageCanvasEditorModel', () => {
});
});
it('hydrates audio media type and sound effect asset kind from saved layout', () => {
it('hydrates audio display duration only from resource generation inputs', () => {
const hydrated = hydrateLayer(
{
layerId: 'layer-audio',
@@ -753,13 +753,16 @@ describe('ImageCanvasEditorModel', () => {
sourceType: 'generated',
mediaType: 'audio',
assetKind: 'sound-effect',
durationSeconds: 3.5,
},
new Map([
[
'resource-audio',
{
imageSrc: '/generated-character-drafts/editor-audios/sfx.mp3',
generationInputs: {
fields: [{ title: '时长', value: '3.5秒' }],
references: [],
},
},
],
]),
@@ -770,8 +773,12 @@ describe('ImageCanvasEditorModel', () => {
src: '/generated-character-drafts/editor-audios/sfx.mp3',
mediaType: 'audio',
assetKind: 'sound-effect',
durationSeconds: 3.5,
generationInputs: {
fields: [{ title: '时长', value: '3.5秒' }],
references: [],
},
});
expect(hydrated).not.toHaveProperty('durationSeconds');
});
it('hydrates character animation sequence fields from the project resource', () => {
@@ -205,7 +205,6 @@ export function createLayerFromAsset(
objectKey: asset.objectKey,
assetObjectId: asset.assetObjectId,
sourceResourceId: asset.sourceResourceId,
durationSeconds: asset.durationSeconds,
sourceAssetId: asset.id,
resourceAssetKind: asset.assetKind ?? assetKind ?? null,
assetKindOverride: null,
@@ -335,7 +334,6 @@ export function serializeLayer(layer: CanvasLayer): EditorProjectLayerSnapshot {
taskId: layer.taskId,
objectKey: layer.objectKey,
assetObjectId: layer.assetObjectId,
durationSeconds: layer.durationSeconds,
sourceResourceId: layer.sourceResourceId,
sourceAssetId: layer.sourceAssetId,
}),
@@ -875,10 +873,6 @@ export function hydrateLayer(
? stringOrNull(resource?.assetObjectId)
: (stringOrNull(snapshot.assetObjectId) ??
stringOrNull(resource?.assetObjectId)),
durationSeconds: isFormalCharacterAnimation
? undefined
: (audioDurationOrNull(snapshot.durationSeconds) ??
audioDurationOrNull(resource?.durationSeconds)),
imageSequenceDurationMs,
sourceResourceId: isFormalCharacterAnimation
? stringOrNull(resource?.sourceResourceId)
@@ -1207,7 +1201,6 @@ export type CanvasLayerResourceMetadata = {
width?: number | null;
height?: number | null;
sourceType?: string | null;
durationSeconds?: number | null;
imageSequenceDurationMs?: number | null;
sourceResourceId?: string | null;
assetKind?: string | null;
@@ -60,7 +60,6 @@ export type EditorAsset = {
showcaseSubmitError?: string | null;
assetKind?: CanvasAssetKind | null;
generationInputs?: CanvasGenerationInputs | null;
durationSeconds?: number;
imageSequenceFrames?: EditorImageSequenceFrameResult[];
imageSequenceDurationMs?: number;
uploadStatus?: 'uploading' | 'failed';
@@ -117,7 +116,6 @@ export type CanvasLayer = {
assetKindOverride?: CanvasAssetKind | null;
assetKind?: CanvasAssetKind | null;
generationInputs?: CanvasGenerationInputs | null;
durationSeconds?: number;
imageSequenceFrames?: EditorImageSequenceFrameResult[];
imageSequenceDurationMs?: number;
previewVideoPath?: string | null;
@@ -1451,7 +1451,7 @@ describe('ImageCanvasEditorView', () => {
fields: [
{ title: '音效提示词', value: '金币跳出音' },
{ title: 'model', value: 'audio1.0' },
{ title: 'duration', value: '8秒' },
{ title: '时长', value: '8秒' },
],
references: [
{
@@ -194,7 +194,7 @@ describe('ImageCanvasExportModel', () => {
},
{ title: '用户输入', value: '用户填写的视觉要求' },
{ title: '游戏分类', value: '游戏音效' },
{ title: 'duration', value: '5秒' },
{ title: '时长', value: '5秒' },
{ title: '处理模型', value: 'birefnet' },
],
references: [],
@@ -207,7 +207,7 @@ describe('ImageCanvasExportModel', () => {
fields: [
{ title: '用户输入', value: '用户填写的视觉要求' },
{ title: '游戏分类', value: '游戏音效' },
{ title: 'duration', value: '5秒' },
{ title: '时长', value: '5秒' },
],
references: [],
});
@@ -12,7 +12,6 @@ import {
isEditorUserVisibleGenerationInputField,
UI_DESIGN_ASSET_EXTRACTION_PROMPT,
} from './ImageCanvasGenerationModel';
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
export function sanitizeExportFilePart(value: string, fallback: string) {
const safeValue = value
@@ -563,20 +562,12 @@ export function buildLayerVisibleExportMetadata(layer: CanvasLayer) {
object: layer.objectKey ?? layer.assetObjectId ?? '-',
};
if (layer.mediaType === 'audio') {
return {
...base,
duration: formatCanvasDurationMetric(layer.durationSeconds).replace(
/^\s*/u,
'',
),
};
}
return {
...base,
resolution: `${layer.originalWidth} x ${layer.originalHeight} px`,
};
return layer.mediaType === 'audio'
? base
: {
...base,
resolution: `${layer.originalWidth} x ${layer.originalHeight} px`,
};
}
export function buildLayerExportMetadata(
@@ -292,7 +292,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
fields: [
{ title: 'prompt', value: '金币收集音' },
{ title: 'model', value: 'audio1.0' },
{ title: 'duration', value: '7秒' },
{ title: '时长', value: '7秒' },
],
references: [],
},
@@ -361,7 +361,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
title: '游戏背景音乐',
prompt: '【系统内置】完整背景音乐提示词',
generationInputs: {
fields: [{ title: 'duration', value: '10秒' }],
fields: [{ title: '时长', value: '10秒' }],
references: [],
},
}),
@@ -529,7 +529,10 @@ function resolveAudioRedrawSoundModel(
}
function resolveAudioRedrawSoundDuration(sourceLayer: CanvasLayer) {
const fieldValue = findGenerationInputFieldValue(sourceLayer, ['duration']);
const fieldValue = findGenerationInputFieldValue(sourceLayer, [
'时长',
'duration',
]);
const matchedValue = fieldValue?.match(/\d+/u)?.[0];
const duration = matchedValue ? Number.parseInt(matchedValue, 10) : null;
if (!duration || !Number.isFinite(duration)) {
@@ -501,7 +501,10 @@ describe('ImageCanvasGenerationLayerModel', () => {
originalHeight: 120,
},
generationInputs: {
fields: [{ title: 'prompt', value: '金币掉落叮当声' }],
fields: [
{ title: 'prompt', value: '金币掉落叮当声' },
{ title: '时长', value: '75秒' },
],
references: [],
},
});
@@ -526,11 +529,18 @@ describe('ImageCanvasGenerationLayerModel', () => {
objectKey: 'generated-character-drafts/editor-audios/sfx.mp3',
assetObjectId: 'assetobj-audio-1',
sourceAssetId: 'asset-audio-bff',
durationSeconds: 75,
generationInputs: {
fields: [
{ title: 'prompt', value: '金币掉落叮当声' },
{ title: '时长', value: '75秒' },
],
references: [],
},
generatedAssetSnapshot: expect.objectContaining({
assetId: 'asset-audio-bff',
}),
});
expect(layer).not.toHaveProperty('durationSeconds');
expect(layer.provider).toBeUndefined();
});
@@ -579,8 +589,8 @@ describe('ImageCanvasGenerationLayerModel', () => {
objectKey: 'generated-character-drafts/editor-videos/video.mp4',
assetObjectId: 'assetobj-video-1',
thumbnailSrc: '/generated-character-drafts/editor-videos/cover.png',
durationSeconds: 5,
});
expect(layer).not.toHaveProperty('durationSeconds');
expect(layer.generatedAssetSnapshot?.assetId).toBe('asset-video-bff');
expect(layer.provider).toBeUndefined();
});
@@ -457,7 +457,6 @@ export function createVideoResultLayer({
sourceAssetId: asset?.assetId,
groupId: sourceLayer?.groupId,
generationInputs: resource?.generationInputs ?? generationInputs,
durationSeconds: generated.durationSeconds,
generatedAssetSnapshot: asset ?? generated.asset ?? undefined,
};
}
@@ -513,7 +512,6 @@ export function createAudioResultLayer({
objectKey: resource?.objectKey ?? generated.objectKey,
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
sourceAssetId: asset?.assetId,
durationSeconds: generated.durationSeconds ?? undefined,
generationInputs: resource?.generationInputs ?? generationInputs,
generatedAssetSnapshot: asset ?? undefined,
};
@@ -546,7 +546,7 @@ describe('ImageCanvasGenerationModel', () => {
fields: [
{ title: 'prompt', value: '金币掉落叮当声' },
{ title: 'model', value: 'audio1.0' },
{ title: 'duration', value: '7秒' },
{ title: '时长', value: '7秒' },
],
references: [],
});
@@ -1031,6 +1031,7 @@ export function buildImageGenerationInputs(
export function buildVideoGenerationInputs(
prompt: string,
durationSeconds: number,
references?: CharacterReferenceImage[],
): CanvasGenerationInputs {
const mediaIndexes = {
@@ -1039,7 +1040,10 @@ export function buildVideoGenerationInputs(
audio: 0,
};
return {
fields: createGenerationInputField('视频描述', prompt),
fields: [
...createGenerationInputField('视频描述', prompt),
...createGenerationInputField('时长', `${durationSeconds}`),
],
references: (references ?? []).flatMap((reference) => {
const mediaType =
reference.mediaType === 'video' || reference.mediaType === 'audio'
@@ -1066,7 +1070,7 @@ export function buildSoundEffectGenerationInputs(
fields: [
...createGenerationInputField('prompt', prompt),
...createGenerationInputField('model', model),
...createGenerationInputField('duration', `${duration}`),
...createGenerationInputField('时长', `${duration}`),
],
references: [],
};
@@ -851,6 +851,10 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
},
result: {
generationInputs: {
fields: [
{ title: '视频描述', value: '根据参考生成角色宣传片' },
{ title: '时长', value: '5秒' },
],
references: [
{
title: '参考图 1',
@@ -1031,7 +1035,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
fields: [
{ title: 'prompt', value: '金币掉落叮当声' },
{ title: 'model', value: 'audio1.0' },
{ title: 'duration', value: '7秒' },
{ title: '时长', value: '7秒' },
],
references: [],
},
@@ -1063,7 +1067,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
fields: [
{ title: 'prompt', value: '按钮确认短促音' },
{ title: 'model', value: 'audio1.0' },
{ title: 'duration', value: '5秒' },
{ title: '时长', value: '5秒' },
],
},
},
@@ -502,6 +502,7 @@ export function buildImageGenerationSubmissionPlan({
),
generationInputs: buildVideoGenerationInputs(
normalizedPrompt,
durationSeconds,
dialog.generationReferences,
),
},
@@ -1,33 +1,3 @@
export function normalizeCanvasDurationSeconds(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) && value > 0
? value
: null;
}
export function formatCanvasDurationLabel(value: unknown) {
const durationSeconds = normalizeCanvasDurationSeconds(value);
if (durationSeconds === null) {
return '--:--';
}
const totalSeconds = Math.max(1, Math.round(durationSeconds));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const paddedSeconds = String(seconds).padStart(2, '0');
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${paddedSeconds}`;
}
return `${minutes}:${paddedSeconds}`;
}
export function formatCanvasDurationMetric(value: unknown) {
// 中文注释:音频对象没有视觉分辨率语义,画布角标统一显示时长。
return `时长 ${formatCanvasDurationLabel(value)}`;
}
type MediaPreviewTone = 'audio' | 'video';
type MediaPreviewMarker = {
@@ -224,7 +224,7 @@ describe('ImageCanvasMetadataModalView', () => {
expect(onClose).toHaveBeenCalledTimes(1);
});
it('renders generated audio metadata with duration instead of resolution', () => {
it('renders audio duration from generation inputs instead of layer metadata', () => {
render(
<ImageCanvasMetadataModalView
layer={createLayer({
@@ -236,9 +236,11 @@ describe('ImageCanvasMetadataModalView', () => {
originalHeight: 120,
model: 'suno',
taskId: 'audio-task-75',
durationSeconds: 75,
generationInputs: {
fields: [{ title: '音乐描述', value: '紧张的地下城循环音乐' }],
fields: [
{ title: '音乐描述', value: '紧张的地下城循环音乐' },
{ title: '时长', value: '75秒' },
],
references: [],
},
})}
@@ -253,7 +255,7 @@ describe('ImageCanvasMetadataModalView', () => {
expect(within(dialog).getByText('音乐描述')).toBeTruthy();
expect(within(dialog).getByText('紧张的地下城循环音乐')).toBeTruthy();
expect(within(dialog).getByText('时长')).toBeTruthy();
expect(within(dialog).getByText('1:15')).toBeTruthy();
expect(within(dialog).getByText('75秒')).toBeTruthy();
expect(within(dialog).queryByText('Resolution')).toBeNull();
expect(within(dialog).queryByText('420 x 120 px')).toBeNull();
});
@@ -6,7 +6,6 @@ import {
getEditorLayerModelDisplayName,
isEditorUserVisibleGenerationInputField,
} from './ImageCanvasGenerationModel';
import { formatCanvasDurationMetric } from './ImageCanvasMediaModel';
export type ImageCanvasMetadataModalViewProps = {
layer: CanvasLayer | null;
@@ -17,13 +16,6 @@ function formatReferenceType(refType: 'project-resource' | 'asset') {
return refType === 'project-resource' ? '项目资源' : '素材';
}
function formatLayerDurationForDisplay(layer: CanvasLayer) {
return formatCanvasDurationMetric(layer.durationSeconds).replace(
/^\s*/u,
'',
);
}
export function ImageCanvasMetadataModalView({
layer,
onClose,
@@ -104,19 +96,14 @@ export function ImageCanvasMetadataModalView({
<dd>
{getEditorLayerModelDisplayName(layer.model)}
</dd>
{layer.mediaType === 'audio' ? (
<>
<dt></dt>
<dd>{formatLayerDurationForDisplay(layer)}</dd>
</>
) : (
{layer.mediaType !== 'audio' ? (
<>
<dt>Resolution</dt>
<dd>
{layer.originalWidth} x {layer.originalHeight} px
</dd>
</>
)}
) : null}
<dt>Task</dt>
<dd>{formatTaskIdForDisplay(layer.taskId)}</dd>
</dl>
@@ -14,6 +14,7 @@ import {
applyUploadAssetReadFailure,
applyUploadAssetReadSuccess,
bindUploadLayerToPersistedAsset,
buildUploadedMediaGenerationInputs,
createUploadCanvasLayer,
createUploadedGenerationReference,
createUploadingAssetPlaceholder,
@@ -539,6 +540,7 @@ describe('ImageCanvasUploadModel', () => {
fileName: '画布视频.mp4',
imageSrc: '/generated/video.mp4',
mediaType: 'video',
durationSeconds: 4.25,
imageDimensions: { width: 1280, height: 720 },
canvasPoint: { x: 110, y: 120 },
canvasSize: { width: 900, height: 640 },
@@ -555,7 +557,19 @@ describe('ImageCanvasUploadModel', () => {
height: 720,
originalWidth: 1280,
originalHeight: 720,
generationInputs: {
fields: [{ title: '时长', value: '4.25秒' }],
references: [],
},
});
expect(layer).not.toHaveProperty('durationSeconds');
});
it('omits invalid or image durations from uploaded media metadata', () => {
expect(buildUploadedMediaGenerationInputs('image', 4)).toBeUndefined();
expect(
buildUploadedMediaGenerationInputs('audio', Number.NaN),
).toBeUndefined();
});
it('uses viewport center fallback for invalid upload points', () => {
@@ -1,6 +1,7 @@
import { resolvePreferredAssetUploadFolder } from './ImageCanvasAssetLibraryModel';
import { resolveLayerResolutionSize } from './ImageCanvasEditorModel';
import type {
CanvasGenerationInputs,
CanvasLayer,
CanvasMediaType,
CanvasViewport,
@@ -26,6 +27,7 @@ type PersistedUploadAsset = {
height: number;
objectKey?: string | null;
assetObjectId?: string | null;
generationInputs?: CanvasGenerationInputs | null;
};
type GenerationReferenceUploadTarget =
| 'character-reference'
@@ -436,6 +438,8 @@ export function applyPersistedUploadAsset({
height: persistedAsset.height,
objectKey: persistedAsset.objectKey ?? undefined,
assetObjectId: persistedAsset.assetObjectId ?? undefined,
generationInputs:
persistedAsset.generationInputs ?? asset.generationInputs,
persisted: true,
uploadStatus: undefined,
uploadProgress: undefined,
@@ -529,6 +533,25 @@ export function normalizeUploadScreenPoint({
};
}
export function buildUploadedMediaGenerationInputs(
mediaType: CanvasMediaType,
durationSeconds: number | undefined,
): CanvasGenerationInputs | undefined {
if (
mediaType === 'image' ||
typeof durationSeconds !== 'number' ||
!Number.isFinite(durationSeconds) ||
durationSeconds <= 0
) {
return undefined;
}
const roundedDurationSeconds = Math.round(durationSeconds * 1_000) / 1_000;
return {
fields: [{ title: '时长', value: `${roundedDurationSeconds}` }],
references: [],
};
}
export function createUploadCanvasLayer({
uploadIndex,
fileName,
@@ -591,7 +614,10 @@ export function createUploadCanvasLayer({
sourceAssetId: `upload-${uploadIndex}`,
objectKey: objectKey ?? undefined,
assetObjectId: assetObjectId ?? undefined,
durationSeconds,
generationInputs: buildUploadedMediaGenerationInputs(
mediaType,
durationSeconds,
),
};
}
@@ -900,7 +900,10 @@ describe('ImageCanvasWorldView', () => {
originalWidth: 420,
originalHeight: 120,
prompt: '紧张的地下城循环音乐',
durationSeconds: 75,
generationInputs: {
fields: [{ title: '时长', value: '75秒' }],
references: [],
},
});
const { props, rerender } = renderWorldView({
layers: [layer],
@@ -948,6 +951,20 @@ describe('ImageCanvasWorldView', () => {
const hoveredAudio =
within(hoveredLayerButton).getByLabelText('画布音频:游戏背景音乐');
const progress = within(hoveredLayerButton).getByLabelText(
'调整游戏背景音乐播放进度',
) as HTMLInputElement;
expect(progress.disabled).toBe(true);
expect(progress.max).toBe('0');
Object.defineProperty(hoveredAudio, 'duration', {
configurable: true,
value: 90,
});
fireEvent.loadedMetadata(hoveredAudio);
expect(progress.disabled).toBe(false);
expect(progress.max).toBe('90');
fireEvent.play(hoveredAudio);
expect(
within(hoveredLayerButton)
@@ -973,7 +990,6 @@ describe('ImageCanvasWorldView', () => {
height: 120,
originalWidth: 420,
originalHeight: 120,
durationSeconds: 12,
});
const { props } = renderWorldView({
layers: [layer],
@@ -364,14 +364,6 @@ function getAudioLayerIcon(layer: CanvasLayer) {
);
}
function getAudioDurationSeconds(durationSeconds: CanvasLayer['durationSeconds']) {
return typeof durationSeconds === 'number' &&
Number.isFinite(durationSeconds) &&
durationSeconds > 0
? durationSeconds
: 0;
}
function formatAudioControlTime(seconds: number) {
if (!Number.isFinite(seconds) || seconds <= 0) {
return '0:00';
@@ -402,21 +394,20 @@ function ImageCanvasAudioLayerCard({
refreshKey: layer.taskId ?? layer.resourceId,
});
const prompt = layer.prompt?.trim();
const fallbackDuration = getAudioDurationSeconds(layer.durationSeconds);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(fallbackDuration);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const showPlaybackButton = isPlaying || isHovered;
const controlDuration = duration > 0 ? duration : fallbackDuration;
const controlDuration = duration > 0 ? duration : 0;
const safeCurrentTime =
controlDuration > 0 ? Math.min(currentTime, controlDuration) : 0;
useEffect(() => {
setIsPlaying(false);
setCurrentTime(0);
setDuration(fallbackDuration);
}, [fallbackDuration, layer.src]);
setDuration(0);
}, [layer.src]);
const togglePlayback = async () => {
const audio = audioRef.current;
@@ -519,7 +510,7 @@ function ImageCanvasAudioLayerCard({
setDuration(
Number.isFinite(nextDuration) && nextDuration > 0
? nextDuration
: fallbackDuration,
: 0,
);
}}
onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)}
@@ -322,7 +322,7 @@ function SubmissionWorkflowHarness({
{layers
.map(
(layer) =>
`${layer.id}:${layer.title}:${layer.sourceResourceId ?? '-'}:${layer.assetKind ?? '-'}:${layer.mediaType ?? '-'}:${layer.src}:${layer.thumbnailSrc ?? '-'}:${layer.imageSequenceDurationMs ?? layer.durationSeconds ?? '-'}`,
`${layer.id}:${layer.title}:${layer.sourceResourceId ?? '-'}:${layer.assetKind ?? '-'}:${layer.mediaType ?? '-'}:${layer.src}:${layer.thumbnailSrc ?? '-'}:${layer.imageSequenceDurationMs ?? '-'}`,
)
.join('|')}
</span>
@@ -1486,6 +1486,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
);
const generationInputs = buildVideoGenerationInputs(
normalizedPrompt,
videoDurationSeconds,
quickEditReferences,
);
const generated = await runEditorGenerationWithWalletRefresh(

Some files were not shown because too many files have changed in this diff Show More