@aelionsdk/sdk 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/audio-controller.d.ts +25 -0
- package/dist/audio-controller.d.ts.map +1 -0
- package/dist/audio-controller.js +235 -0
- package/dist/audio-mastering.d.ts +27 -0
- package/dist/audio-mastering.d.ts.map +1 -0
- package/dist/audio-mastering.js +160 -0
- package/dist/composition.d.ts +75 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +146 -0
- package/dist/default-schemas.d.ts +7 -0
- package/dist/default-schemas.d.ts.map +1 -0
- package/dist/default-schemas.js +18 -0
- package/dist/export-job.d.ts +22 -0
- package/dist/export-job.d.ts.map +1 -0
- package/dist/export-job.js +126 -0
- package/dist/extension-host.d.ts +90 -0
- package/dist/extension-host.d.ts.map +1 -0
- package/dist/extension-host.js +367 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/media-provider.d.ts +49 -0
- package/dist/media-provider.d.ts.map +1 -0
- package/dist/media-provider.js +388 -0
- package/dist/migration-materials.d.ts +10 -0
- package/dist/migration-materials.d.ts.map +1 -0
- package/dist/migration-materials.js +148 -0
- package/dist/migration.d.ts +109 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +1232 -0
- package/dist/persistence.d.ts +73 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +245 -0
- package/dist/player.d.ts +22 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +522 -0
- package/dist/preview-controller.d.ts +62 -0
- package/dist/preview-controller.d.ts.map +1 -0
- package/dist/preview-controller.js +377 -0
- package/dist/preview-quality.d.ts +7 -0
- package/dist/preview-quality.d.ts.map +1 -0
- package/dist/preview-quality.js +8 -0
- package/dist/production-media-provider.d.ts +112 -0
- package/dist/production-media-provider.d.ts.map +1 -0
- package/dist/production-media-provider.js +749 -0
- package/dist/project-builder.d.ts +249 -0
- package/dist/project-builder.d.ts.map +1 -0
- package/dist/project-builder.js +953 -0
- package/dist/runtime-material-registry.d.ts +10 -0
- package/dist/runtime-material-registry.d.ts.map +1 -0
- package/dist/runtime-material-registry.js +23 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1114 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1232 @@
|
|
|
1
|
+
import { normalizeRational } from '@aelionsdk/core';
|
|
2
|
+
import { ProjectBuilder, } from './project-builder.js';
|
|
3
|
+
const MIGRATION_INTEGRITY = `sha256:${'9d'.repeat(32)}`;
|
|
4
|
+
export const migrationMaterialPackage = Object.freeze({
|
|
5
|
+
packageId: 'aelion.migration.builtin',
|
|
6
|
+
packageVersion: '1.0.0',
|
|
7
|
+
packageIntegrity: MIGRATION_INTEGRITY,
|
|
8
|
+
});
|
|
9
|
+
export class ProjectMigrationError extends Error {
|
|
10
|
+
diagnostics;
|
|
11
|
+
name = 'ProjectMigrationError';
|
|
12
|
+
constructor(diagnostics) {
|
|
13
|
+
super(diagnostics.map(value => `${value.path}: ${value.message}`).join('\n'));
|
|
14
|
+
this.diagnostics = diagnostics;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function record(value, path) {
|
|
18
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
19
|
+
throw new TypeError(`${path} must be an object`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function records(value, path) {
|
|
24
|
+
if (!Array.isArray(value))
|
|
25
|
+
throw new TypeError(`${path} must be an array`);
|
|
26
|
+
return value.map((entry, index) => record(entry, `${path}[${index.toString()}]`));
|
|
27
|
+
}
|
|
28
|
+
function finite(value, fallback, path) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return fallback;
|
|
31
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
32
|
+
throw new TypeError(`${path} must be finite`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function finiteNumeric(value, fallback, path) {
|
|
37
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
if (Number.isFinite(parsed))
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
return finite(value, fallback, path);
|
|
43
|
+
}
|
|
44
|
+
function requiredString(value, path) {
|
|
45
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
46
|
+
throw new TypeError(`${path} must be a non-empty string`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function optionalString(value) {
|
|
51
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
52
|
+
}
|
|
53
|
+
function jsonClone(value, path) {
|
|
54
|
+
let serialized;
|
|
55
|
+
try {
|
|
56
|
+
serialized = JSON.stringify(value);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new TypeError(`${path} must be JSON-serializable`);
|
|
60
|
+
}
|
|
61
|
+
if (typeof serialized !== 'string') {
|
|
62
|
+
throw new TypeError(`${path} must be JSON-serializable`);
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(serialized);
|
|
65
|
+
}
|
|
66
|
+
function usFromSeconds(value, path) {
|
|
67
|
+
const seconds = finite(value, 0, path);
|
|
68
|
+
const result = Math.round(seconds * 1_000_000);
|
|
69
|
+
if (!Number.isSafeInteger(result))
|
|
70
|
+
throw new RangeError(`${path} exceeds the safe time range`);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function safeUs(value, path) {
|
|
74
|
+
const result = finite(value, 0, path);
|
|
75
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
76
|
+
throw new RangeError(`${path} must be a non-negative safe integer`);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function rationalRate(value, path) {
|
|
81
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
82
|
+
throw new RangeError(`${path} must be a positive finite number`);
|
|
83
|
+
}
|
|
84
|
+
const denominator = 1_000_000;
|
|
85
|
+
const numerator = Math.round(value * denominator);
|
|
86
|
+
if (!Number.isSafeInteger(numerator) || numerator <= 0) {
|
|
87
|
+
throw new RangeError(`${path} cannot be represented as a safe rational rate`);
|
|
88
|
+
}
|
|
89
|
+
return normalizeRational({ numerator, denominator });
|
|
90
|
+
}
|
|
91
|
+
function sourceKey(kind, id) {
|
|
92
|
+
return `${kind}:${id}`;
|
|
93
|
+
}
|
|
94
|
+
function entityId(prefix, index) {
|
|
95
|
+
return `${prefix}_${index.toString()}`;
|
|
96
|
+
}
|
|
97
|
+
function pushDiagnostic(diagnostics, value) {
|
|
98
|
+
if (diagnostics.some(existing => existing.code === value.code &&
|
|
99
|
+
existing.path === value.path &&
|
|
100
|
+
existing.message === value.message)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
diagnostics.push(Object.freeze(value));
|
|
104
|
+
}
|
|
105
|
+
function finish(builder, diagnostics, entityMap, strict) {
|
|
106
|
+
if (strict && diagnostics.some(value => value.severity === 'error')) {
|
|
107
|
+
throw new ProjectMigrationError(Object.freeze([...diagnostics]));
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
project: builder.build(),
|
|
111
|
+
diagnostics: Object.freeze([...diagnostics]),
|
|
112
|
+
entityMap: Object.freeze({ ...entityMap }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function webAvVisual(sprite, width, height) {
|
|
116
|
+
const rect = sprite.rect;
|
|
117
|
+
if (rect === undefined)
|
|
118
|
+
return undefined;
|
|
119
|
+
const flipX = sprite.flip === 'horizontal' ? -1 : 1;
|
|
120
|
+
const flipY = sprite.flip === 'vertical' ? -1 : 1;
|
|
121
|
+
return {
|
|
122
|
+
fit: 'fill',
|
|
123
|
+
positionPx: { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 },
|
|
124
|
+
anchor: { x: 0.5, y: 0.5 },
|
|
125
|
+
scale: {
|
|
126
|
+
x: (rect.w / width) * flipX,
|
|
127
|
+
y: (rect.h / height) * flipY,
|
|
128
|
+
},
|
|
129
|
+
rotationDeg: ((rect.angle ?? 0) * 180) / Math.PI,
|
|
130
|
+
opacity: sprite.opacity ?? 1,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function applyWebAvAnimation(builder, itemId, sprite, asset, width, height) {
|
|
134
|
+
if (sprite.animation === undefined || sprite.animation.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
const baseRect = sprite.rect ?? {
|
|
137
|
+
x: 0,
|
|
138
|
+
y: 0,
|
|
139
|
+
w: asset.width ?? width,
|
|
140
|
+
h: asset.height ?? height,
|
|
141
|
+
angle: 0,
|
|
142
|
+
};
|
|
143
|
+
const frames = [...sprite.animation].sort((left, right) => left.timeUs - right.timeUs);
|
|
144
|
+
const position = [];
|
|
145
|
+
const scale = [];
|
|
146
|
+
const rotation = [];
|
|
147
|
+
const opacity = [];
|
|
148
|
+
for (const frame of frames) {
|
|
149
|
+
const frameWidth = frame.width ?? baseRect.w;
|
|
150
|
+
const frameHeight = frame.height ?? baseRect.h;
|
|
151
|
+
const frameX = frame.x ?? baseRect.x;
|
|
152
|
+
const frameY = frame.y ?? baseRect.y;
|
|
153
|
+
position.push({
|
|
154
|
+
timeUs: safeUs(frame.timeUs, `${sprite.id}.animation.timeUs`),
|
|
155
|
+
value: { x: frameX + frameWidth / 2, y: frameY + frameHeight / 2 },
|
|
156
|
+
});
|
|
157
|
+
scale.push({
|
|
158
|
+
timeUs: frame.timeUs,
|
|
159
|
+
value: {
|
|
160
|
+
x: (frameWidth / width) * (sprite.flip === 'horizontal' ? -1 : 1),
|
|
161
|
+
y: (frameHeight / height) * (sprite.flip === 'vertical' ? -1 : 1),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
rotation.push({
|
|
165
|
+
timeUs: frame.timeUs,
|
|
166
|
+
value: ((frame.angle ?? baseRect.angle ?? 0) * 180) / Math.PI,
|
|
167
|
+
});
|
|
168
|
+
opacity.push({ timeUs: frame.timeUs, value: frame.opacity ?? sprite.opacity ?? 1 });
|
|
169
|
+
}
|
|
170
|
+
builder.setKeyframes(itemId, 'position', position);
|
|
171
|
+
builder.setKeyframes(itemId, 'scale', scale);
|
|
172
|
+
builder.setKeyframes(itemId, 'rotation', rotation);
|
|
173
|
+
builder.setKeyframes(itemId, 'opacity', opacity);
|
|
174
|
+
}
|
|
175
|
+
export function migrateWebAvProject(snapshot, options = {}) {
|
|
176
|
+
const strict = options.strict ?? true;
|
|
177
|
+
const builder = new ProjectBuilder({
|
|
178
|
+
...(options.projectId === undefined ? {} : { projectId: options.projectId }),
|
|
179
|
+
...(options.sequenceId === undefined ? {} : { sequenceId: options.sequenceId }),
|
|
180
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
181
|
+
...(options.frameRate === undefined ? {} : { frameRate: options.frameRate }),
|
|
182
|
+
width: snapshot.width,
|
|
183
|
+
height: snapshot.height,
|
|
184
|
+
...(snapshot.backgroundColor === undefined
|
|
185
|
+
? {}
|
|
186
|
+
: { backgroundColor: snapshot.backgroundColor }),
|
|
187
|
+
});
|
|
188
|
+
const diagnostics = [];
|
|
189
|
+
const entityMap = {};
|
|
190
|
+
const assets = new Map(snapshot.assets.map(value => [value.id, value]));
|
|
191
|
+
for (const asset of snapshot.assets)
|
|
192
|
+
builder.addAsset(asset);
|
|
193
|
+
const visualTracks = new Map();
|
|
194
|
+
const audioTrack = builder.addTrack({ id: 'webav_audio', kind: 'audio', name: 'WebAV audio' });
|
|
195
|
+
const visualTrack = (zIndex) => {
|
|
196
|
+
const existing = visualTracks.get(zIndex);
|
|
197
|
+
if (existing !== undefined)
|
|
198
|
+
return existing;
|
|
199
|
+
const id = builder.addTrack({
|
|
200
|
+
id: entityId('webav_visual', visualTracks.size),
|
|
201
|
+
kind: 'visual',
|
|
202
|
+
name: `WebAV z-index ${zIndex.toString()}`,
|
|
203
|
+
});
|
|
204
|
+
visualTracks.set(zIndex, id);
|
|
205
|
+
return id;
|
|
206
|
+
};
|
|
207
|
+
const sorted = [...snapshot.sprites].sort((left, right) => (left.zIndex ?? 0) - (right.zIndex ?? 0));
|
|
208
|
+
sorted.forEach((sprite, index) => {
|
|
209
|
+
const path = `sprites[${index.toString()}]`;
|
|
210
|
+
const asset = assets.get(sprite.assetId);
|
|
211
|
+
if (asset === undefined)
|
|
212
|
+
throw new ReferenceError(`${path}.assetId is not bound`);
|
|
213
|
+
if (asset.kind !== sprite.kind) {
|
|
214
|
+
pushDiagnostic(diagnostics, {
|
|
215
|
+
code: 'WEBAV_ASSET_KIND_MISMATCH',
|
|
216
|
+
severity: 'error',
|
|
217
|
+
path: `${path}.assetId`,
|
|
218
|
+
message: `${sprite.kind} Sprite cannot bind a ${asset.kind} Asset`,
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const atUs = safeUs(sprite.time.offset, `${path}.time.offset`);
|
|
223
|
+
const durationUs = safeUs(sprite.time.duration, `${path}.time.duration`);
|
|
224
|
+
if (durationUs === 0)
|
|
225
|
+
throw new RangeError(`${path}.time.duration must be positive`);
|
|
226
|
+
const playbackRate = finite(sprite.time.playbackRate, 1, `${path}.time.playbackRate`);
|
|
227
|
+
const rate = rationalRate(playbackRate, `${path}.time.playbackRate`);
|
|
228
|
+
if (Math.abs(rate.numerator / rate.denominator - playbackRate) > 1e-9) {
|
|
229
|
+
pushDiagnostic(diagnostics, {
|
|
230
|
+
code: 'WEBAV_PLAYBACK_RATE_APPROXIMATED',
|
|
231
|
+
severity: 'warning',
|
|
232
|
+
path: `${path}.time.playbackRate`,
|
|
233
|
+
message: `playbackRate was approximated as ${rate.numerator.toString()}/${rate.denominator.toString()}`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (sprite.visible === false) {
|
|
237
|
+
pushDiagnostic(diagnostics, {
|
|
238
|
+
code: 'WEBAV_HIDDEN_SPRITE_SKIPPED',
|
|
239
|
+
severity: 'info',
|
|
240
|
+
path: `${path}.visible`,
|
|
241
|
+
message: 'hidden Sprite was intentionally omitted',
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (sprite.includeAudio === true && sprite.kind !== 'video') {
|
|
246
|
+
pushDiagnostic(diagnostics, {
|
|
247
|
+
code: 'WEBAV_AUDIO_STREAM_UNAVAILABLE',
|
|
248
|
+
severity: 'error',
|
|
249
|
+
path: `${path}.includeAudio`,
|
|
250
|
+
message: 'only a video Sprite can expose a linked audio stream',
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const sourceStartUs = safeUs(sprite.sourceStartUs ?? 0, `${path}.sourceStartUs`);
|
|
255
|
+
const itemId = entityId('webav_item', index);
|
|
256
|
+
if (sprite.kind === 'audio') {
|
|
257
|
+
builder.addMediaClip({
|
|
258
|
+
id: itemId,
|
|
259
|
+
kind: 'audio',
|
|
260
|
+
assetId: sprite.assetId,
|
|
261
|
+
trackId: audioTrack,
|
|
262
|
+
atUs,
|
|
263
|
+
durationUs,
|
|
264
|
+
sourceStartUs,
|
|
265
|
+
sourceDurationUs: Math.max(1, Math.round(durationUs * playbackRate)),
|
|
266
|
+
rate,
|
|
267
|
+
name: sprite.id,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const trackId = visualTrack(sprite.zIndex ?? 0);
|
|
272
|
+
if (sprite.kind === 'image') {
|
|
273
|
+
builder.addImageClip({
|
|
274
|
+
id: itemId,
|
|
275
|
+
assetId: sprite.assetId,
|
|
276
|
+
trackId,
|
|
277
|
+
atUs,
|
|
278
|
+
durationUs,
|
|
279
|
+
name: sprite.id,
|
|
280
|
+
fit: 'fill',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
builder.addMediaClip({
|
|
285
|
+
id: itemId,
|
|
286
|
+
kind: 'video',
|
|
287
|
+
assetId: sprite.assetId,
|
|
288
|
+
trackId,
|
|
289
|
+
atUs,
|
|
290
|
+
durationUs,
|
|
291
|
+
sourceStartUs,
|
|
292
|
+
sourceDurationUs: Math.max(1, Math.round(durationUs * playbackRate)),
|
|
293
|
+
rate,
|
|
294
|
+
name: sprite.id,
|
|
295
|
+
fit: 'fill',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const visual = webAvVisual(sprite, snapshot.width, snapshot.height);
|
|
299
|
+
if (visual !== undefined)
|
|
300
|
+
builder.setVisual(itemId, visual);
|
|
301
|
+
applyWebAvAnimation(builder, itemId, sprite, asset, snapshot.width, snapshot.height);
|
|
302
|
+
if (sprite.includeAudio === true) {
|
|
303
|
+
const audioId = `${itemId}_audio`;
|
|
304
|
+
builder.addMediaClip({
|
|
305
|
+
id: audioId,
|
|
306
|
+
kind: 'audio',
|
|
307
|
+
assetId: sprite.assetId,
|
|
308
|
+
trackId: audioTrack,
|
|
309
|
+
atUs,
|
|
310
|
+
durationUs,
|
|
311
|
+
sourceStartUs,
|
|
312
|
+
sourceDurationUs: Math.max(1, Math.round(durationUs * playbackRate)),
|
|
313
|
+
rate,
|
|
314
|
+
name: `${sprite.id} audio`,
|
|
315
|
+
});
|
|
316
|
+
entityMap[sourceKey('webav-audio', sprite.id)] = audioId;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
entityMap[sourceKey('webav', sprite.id)] = itemId;
|
|
320
|
+
});
|
|
321
|
+
return finish(builder, diagnostics, entityMap, strict);
|
|
322
|
+
}
|
|
323
|
+
function diffusionSettings(checkpoint) {
|
|
324
|
+
const value = checkpoint.settings;
|
|
325
|
+
return value === undefined ? {} : record(value, 'checkpoint.settings');
|
|
326
|
+
}
|
|
327
|
+
function diffusionVisual(clip, width, height, sourceWidth, sourceHeight, path, diagnostics) {
|
|
328
|
+
const visualWidth = finite(clip.width, sourceWidth, 'clip.width');
|
|
329
|
+
const visualHeight = finite(clip.height, sourceHeight, 'clip.height');
|
|
330
|
+
const x = finite(clip.x, 0, 'clip.x') + finite(clip.translateX, 0, 'clip.translateX');
|
|
331
|
+
const y = finite(clip.y, 0, 'clip.y') + finite(clip.translateY, 0, 'clip.translateY');
|
|
332
|
+
const opacityPercent = finite(clip.opacity, 100, 'clip.opacity');
|
|
333
|
+
const blendMode = diffusionBlendMode(clip, path, diagnostics);
|
|
334
|
+
return {
|
|
335
|
+
fit: 'fill',
|
|
336
|
+
positionPx: { x, y },
|
|
337
|
+
anchor: {
|
|
338
|
+
x: finite(clip.anchorX, 0, 'clip.anchorX'),
|
|
339
|
+
y: finite(clip.anchorY, 0, 'clip.anchorY'),
|
|
340
|
+
},
|
|
341
|
+
scale: {
|
|
342
|
+
x: (visualWidth / width) * finite(clip.scaleX, 1, 'clip.scaleX'),
|
|
343
|
+
y: (visualHeight / height) * finite(clip.scaleY, 1, 'clip.scaleY'),
|
|
344
|
+
},
|
|
345
|
+
rotationDeg: finite(clip.rotation, 0, 'clip.rotation'),
|
|
346
|
+
opacity: Math.max(0, Math.min(1, opacityPercent / 100)),
|
|
347
|
+
blendMode,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const DIFFUSION_AELION_BLEND_MODES = new Set([
|
|
351
|
+
'multiply',
|
|
352
|
+
'screen',
|
|
353
|
+
'overlay',
|
|
354
|
+
'darken',
|
|
355
|
+
'lighten',
|
|
356
|
+
'color-dodge',
|
|
357
|
+
'color-burn',
|
|
358
|
+
'hard-light',
|
|
359
|
+
'soft-light',
|
|
360
|
+
'difference',
|
|
361
|
+
'exclusion',
|
|
362
|
+
]);
|
|
363
|
+
function diffusionBlendMode(clip, path, diagnostics) {
|
|
364
|
+
const blend = optionalString(clip.blendMode);
|
|
365
|
+
if (blend === undefined || blend === 'source-over')
|
|
366
|
+
return 'normal';
|
|
367
|
+
if (DIFFUSION_AELION_BLEND_MODES.has(blend)) {
|
|
368
|
+
return blend;
|
|
369
|
+
}
|
|
370
|
+
pushDiagnostic(diagnostics, {
|
|
371
|
+
code: 'DIFFUSION_BLEND_MODE_UNSUPPORTED',
|
|
372
|
+
severity: 'error',
|
|
373
|
+
path: `${path}.blendMode`,
|
|
374
|
+
message: `Canvas blend mode ${blend} has no equivalent Aelion blend mode`,
|
|
375
|
+
});
|
|
376
|
+
return 'normal';
|
|
377
|
+
}
|
|
378
|
+
function diffusionCanvasVisual(clip, width, height, path, diagnostics) {
|
|
379
|
+
const x = finite(clip.x, 0, 'clip.x') + finite(clip.translateX, 0, 'clip.translateX');
|
|
380
|
+
const y = finite(clip.y, 0, 'clip.y') + finite(clip.translateY, 0, 'clip.translateY');
|
|
381
|
+
return {
|
|
382
|
+
fit: 'fill',
|
|
383
|
+
positionPx: { x, y },
|
|
384
|
+
anchor: { x: x / width, y: y / height },
|
|
385
|
+
scale: {
|
|
386
|
+
x: finite(clip.scaleX, 1, 'clip.scaleX'),
|
|
387
|
+
y: finite(clip.scaleY, 1, 'clip.scaleY'),
|
|
388
|
+
},
|
|
389
|
+
rotationDeg: finite(clip.rotation, 0, 'clip.rotation'),
|
|
390
|
+
opacity: Math.max(0, Math.min(1, finite(clip.opacity, 100, 'clip.opacity') / 100)),
|
|
391
|
+
blendMode: diffusionBlendMode(clip, path, diagnostics),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function shapeBox(clip, fallbackWidth, fallbackHeight) {
|
|
395
|
+
const width = finite(clip.width, fallbackWidth, 'clip.width');
|
|
396
|
+
const height = finite(clip.height, fallbackHeight, 'clip.height');
|
|
397
|
+
const anchorX = finite(clip.anchorX, 0, 'clip.anchorX');
|
|
398
|
+
const anchorY = finite(clip.anchorY, 0, 'clip.anchorY');
|
|
399
|
+
const originX = finite(clip.x, 0, 'clip.x') + finite(clip.translateX, 0, 'clip.translateX');
|
|
400
|
+
const originY = finite(clip.y, 0, 'clip.y') + finite(clip.translateY, 0, 'clip.translateY');
|
|
401
|
+
return {
|
|
402
|
+
x: originX - anchorX * width,
|
|
403
|
+
y: originY - anchorY * height,
|
|
404
|
+
width,
|
|
405
|
+
height,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function colorWithOpacity(color, opacityPercent, path) {
|
|
409
|
+
if (opacityPercent >= 100)
|
|
410
|
+
return color;
|
|
411
|
+
const match = /^#([\da-f]{6})([\da-f]{2})?$/iu.exec(color);
|
|
412
|
+
if (match === null) {
|
|
413
|
+
throw new TypeError(`${path} color opacity requires a six- or eight-digit hex color`);
|
|
414
|
+
}
|
|
415
|
+
const existing = match[2] === undefined ? 255 : Number.parseInt(match[2], 16);
|
|
416
|
+
const alpha = Math.round((existing * Math.max(0, Math.min(100, opacityPercent))) / 100);
|
|
417
|
+
return `#${match[1] ?? '000000'}${alpha.toString(16).padStart(2, '0')}`;
|
|
418
|
+
}
|
|
419
|
+
function diffusionStroke(value, path, diagnostics) {
|
|
420
|
+
if (value === undefined)
|
|
421
|
+
return undefined;
|
|
422
|
+
if (!Array.isArray(value))
|
|
423
|
+
throw new TypeError(`${path} must be an array`);
|
|
424
|
+
if (value.length === 0)
|
|
425
|
+
return undefined;
|
|
426
|
+
if (value.length > 1) {
|
|
427
|
+
pushDiagnostic(diagnostics, {
|
|
428
|
+
code: 'DIFFUSION_MULTIPLE_STROKES_UNSUPPORTED',
|
|
429
|
+
severity: 'error',
|
|
430
|
+
path,
|
|
431
|
+
message: 'Aelion v1 can preserve only one shape/text stroke',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const stroke = record(value[0], `${path}[0]`);
|
|
435
|
+
const lineCap = optionalString(stroke.lineCap);
|
|
436
|
+
const lineJoin = optionalString(stroke.lineJoin);
|
|
437
|
+
const miterLimit = finite(stroke.miterLimit, 10, `${path}[0].miterLimit`);
|
|
438
|
+
if ((lineCap !== undefined && lineCap !== 'butt') ||
|
|
439
|
+
(lineJoin !== undefined && lineJoin !== 'miter') ||
|
|
440
|
+
miterLimit !== 10) {
|
|
441
|
+
pushDiagnostic(diagnostics, {
|
|
442
|
+
code: 'DIFFUSION_STROKE_GEOMETRY_UNSUPPORTED',
|
|
443
|
+
severity: 'error',
|
|
444
|
+
path: `${path}[0]`,
|
|
445
|
+
message: 'stroke cap/join/miter geometry differs from Aelion v1',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const color = optionalString(stroke.color) ?? '#000000';
|
|
449
|
+
return {
|
|
450
|
+
color: colorWithOpacity(color, finite(stroke.opacity, 100, `${path}[0].opacity`), `${path}[0]`),
|
|
451
|
+
width: finite(stroke.width, 1, `${path}[0].width`),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function diagnoseDiffusionTextRendering(clip, path, diagnostics) {
|
|
455
|
+
if (clip.background !== undefined) {
|
|
456
|
+
pushDiagnostic(diagnostics, {
|
|
457
|
+
code: 'DIFFUSION_TEXT_BACKGROUND_UNSUPPORTED',
|
|
458
|
+
severity: 'error',
|
|
459
|
+
path: `${path}.background`,
|
|
460
|
+
message: 'text background padding/radius requires a linked Aelion background Shape',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (Array.isArray(clip.shadows) && clip.shadows.length > 0) {
|
|
464
|
+
pushDiagnostic(diagnostics, {
|
|
465
|
+
code: 'DIFFUSION_TEXT_SHADOWS_UNSUPPORTED',
|
|
466
|
+
severity: 'error',
|
|
467
|
+
path: `${path}.shadows`,
|
|
468
|
+
message: 'text shadows require an Aelion text-shadow Material',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (clip.glow !== undefined) {
|
|
472
|
+
pushDiagnostic(diagnostics, {
|
|
473
|
+
code: 'DIFFUSION_TEXT_GLOW_UNSUPPORTED',
|
|
474
|
+
severity: 'error',
|
|
475
|
+
path: `${path}.glow`,
|
|
476
|
+
message: 'text glow requires an Aelion glow Material',
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function diffusionStyle(clip, path, diagnostics) {
|
|
481
|
+
const font = clip.font === undefined ? {} : record(clip.font, 'clip.font');
|
|
482
|
+
const stroke = diffusionStroke(clip.strokes, `${path}.strokes`, diagnostics);
|
|
483
|
+
const fontSize = finiteNumeric(clip.fontSize ?? font.size, 48, `${path}.font.size`);
|
|
484
|
+
const align = optionalString(clip.align);
|
|
485
|
+
return {
|
|
486
|
+
fontFamilies: [optionalString(font.family) ?? 'sans-serif'],
|
|
487
|
+
fontSizePx: fontSize,
|
|
488
|
+
fontWeight: finiteNumeric(font.weight, 400, `${path}.font.weight`),
|
|
489
|
+
fontStyle: optionalString(font.style) ?? 'normal',
|
|
490
|
+
fill: optionalString(clip.color) ?? '#ffffff',
|
|
491
|
+
...(stroke === undefined
|
|
492
|
+
? {}
|
|
493
|
+
: {
|
|
494
|
+
stroke: stroke.color,
|
|
495
|
+
strokeWidthPx: stroke.width,
|
|
496
|
+
}),
|
|
497
|
+
align: align === 'center' ? 'center' : align === 'right' ? 'end' : 'start',
|
|
498
|
+
lineHeightPx: fontSize * finite(clip.leading, 1, `${path}.leading`),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function textStyleWithOpacity(style, value, path) {
|
|
502
|
+
if (value === undefined)
|
|
503
|
+
return style;
|
|
504
|
+
const opacity = finite(value, 100, path);
|
|
505
|
+
return {
|
|
506
|
+
...style,
|
|
507
|
+
...(typeof style.fill === 'string'
|
|
508
|
+
? { fill: colorWithOpacity(style.fill, opacity, path) }
|
|
509
|
+
: {}),
|
|
510
|
+
...(typeof style.stroke === 'string'
|
|
511
|
+
? { stroke: colorWithOpacity(style.stroke, opacity, path) }
|
|
512
|
+
: {}),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function transformedText(value, casing) {
|
|
516
|
+
return casing === 'upper'
|
|
517
|
+
? value.toLocaleUpperCase()
|
|
518
|
+
: casing === 'lower'
|
|
519
|
+
? value.toLocaleLowerCase()
|
|
520
|
+
: value;
|
|
521
|
+
}
|
|
522
|
+
function migrateDiffusionTextContent(clip, sourceText, path, diagnostics) {
|
|
523
|
+
diagnoseDiffusionTextRendering(clip, path, diagnostics);
|
|
524
|
+
const style = diffusionStyle(clip, path, diagnostics);
|
|
525
|
+
if (!Array.isArray(clip.styles) || clip.styles.length === 0) {
|
|
526
|
+
return { text: transformedText(sourceText, clip.casing), style };
|
|
527
|
+
}
|
|
528
|
+
const overrides = clip.styles.flatMap((raw, index) => {
|
|
529
|
+
const override = record(raw, `${path}.styles[${index.toString()}]`);
|
|
530
|
+
const start = finite(override.start, 0, `${path}.styles[${index.toString()}].start`);
|
|
531
|
+
const end = finite(override.end, sourceText.length, `${path}.styles[${index.toString()}].end`);
|
|
532
|
+
if (!Number.isSafeInteger(start) ||
|
|
533
|
+
!Number.isSafeInteger(end) ||
|
|
534
|
+
start < 0 ||
|
|
535
|
+
end <= start ||
|
|
536
|
+
end > sourceText.length) {
|
|
537
|
+
pushDiagnostic(diagnostics, {
|
|
538
|
+
code: 'DIFFUSION_TEXT_STYLE_RANGE_INVALID',
|
|
539
|
+
severity: 'error',
|
|
540
|
+
path: `${path}.styles[${index.toString()}]`,
|
|
541
|
+
message: 'style override range is outside the source text',
|
|
542
|
+
});
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
const overrideStyle = record(override.style, `${path}.styles[${index.toString()}].style`);
|
|
546
|
+
diagnoseDiffusionTextRendering(overrideStyle, `${path}.styles[${index.toString()}].style`, diagnostics);
|
|
547
|
+
return [{ start, end, style: overrideStyle, index }];
|
|
548
|
+
});
|
|
549
|
+
const boundaries = [
|
|
550
|
+
...new Set([0, sourceText.length, ...overrides.flatMap(value => [value.start, value.end])]),
|
|
551
|
+
].sort((left, right) => left - right);
|
|
552
|
+
const runs = boundaries.slice(0, -1).flatMap((start, boundaryIndex) => {
|
|
553
|
+
const end = boundaries[boundaryIndex + 1];
|
|
554
|
+
if (end === undefined || end <= start)
|
|
555
|
+
return [];
|
|
556
|
+
const applicable = overrides.filter(value => value.start <= start && value.end >= end);
|
|
557
|
+
let combined = { ...clip };
|
|
558
|
+
for (const override of applicable) {
|
|
559
|
+
const overrideFont = override.style.font === undefined
|
|
560
|
+
? {}
|
|
561
|
+
: record(override.style.font, `${path}.styles[${override.index.toString()}].style.font`);
|
|
562
|
+
const baseFont = combined.font === undefined ? {} : record(combined.font, `${path}.font`);
|
|
563
|
+
combined = {
|
|
564
|
+
...combined,
|
|
565
|
+
...override.style,
|
|
566
|
+
font: { ...baseFont, ...overrideFont },
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const overrideOpacity = applicable.reduce((value, override) => (override.style.opacity === undefined ? value : override.style.opacity), undefined);
|
|
570
|
+
return [
|
|
571
|
+
{
|
|
572
|
+
text: transformedText(sourceText.slice(start, end), combined.casing),
|
|
573
|
+
style: textStyleWithOpacity(diffusionStyle(combined, `${path}.styles`, diagnostics), overrideOpacity, `${path}.styles.opacity`),
|
|
574
|
+
},
|
|
575
|
+
];
|
|
576
|
+
});
|
|
577
|
+
return {
|
|
578
|
+
text: runs.map(run => run.text).join(''),
|
|
579
|
+
style,
|
|
580
|
+
runs,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function addMigrationMaterial(builder, itemId, materialId, parameters, id) {
|
|
584
|
+
const instanceId = builder.addMaterialInstance({
|
|
585
|
+
id,
|
|
586
|
+
...migrationMaterialPackage,
|
|
587
|
+
materialId,
|
|
588
|
+
parameters,
|
|
589
|
+
previewPolicy: 'required',
|
|
590
|
+
});
|
|
591
|
+
builder.attachEffect(itemId, instanceId);
|
|
592
|
+
}
|
|
593
|
+
const DIFFUSION_EFFECTS = new Set([
|
|
594
|
+
'blur',
|
|
595
|
+
'brightness',
|
|
596
|
+
'contrast',
|
|
597
|
+
'grayscale',
|
|
598
|
+
'hue-rotate',
|
|
599
|
+
'invert',
|
|
600
|
+
'opacity',
|
|
601
|
+
'saturate',
|
|
602
|
+
'sepia',
|
|
603
|
+
]);
|
|
604
|
+
function migrateDiffusionEffects(builder, clip, itemId, path, diagnostics, width, height) {
|
|
605
|
+
if (!Array.isArray(clip.effects))
|
|
606
|
+
return;
|
|
607
|
+
clip.effects.forEach((raw, index) => {
|
|
608
|
+
const effect = record(raw, `${path}.effects[${index.toString()}]`);
|
|
609
|
+
const type = requiredString(effect.type, `${path}.effects[${index.toString()}].type`);
|
|
610
|
+
if (!DIFFUSION_EFFECTS.has(type)) {
|
|
611
|
+
pushDiagnostic(diagnostics, {
|
|
612
|
+
code: 'DIFFUSION_EFFECT_UNSUPPORTED',
|
|
613
|
+
severity: 'error',
|
|
614
|
+
path: `${path}.effects[${index.toString()}]`,
|
|
615
|
+
message: `effect ${type} has no safe Aelion migration Material`,
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const value = finite(effect.value, 0, `${path}.effects[${index.toString()}].value`);
|
|
620
|
+
addMigrationMaterial(builder, itemId, `diffusion-${type}`, type === 'blur' ? { value, width, height } : { value }, `${itemId}_effect_${index.toString()}`);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function diffusionAnimations(clip, path, diagnostics) {
|
|
624
|
+
if (!Array.isArray(clip.animations))
|
|
625
|
+
return [];
|
|
626
|
+
return clip.animations.flatMap((raw, index) => {
|
|
627
|
+
const animation = record(raw, `${path}.animations[${index.toString()}]`);
|
|
628
|
+
const key = requiredString(animation.key, `${path}.animations[${index.toString()}].key`);
|
|
629
|
+
const frames = records(animation.frames, `${path}.animations[${index.toString()}].frames`).map((frame, frameIndex) => {
|
|
630
|
+
if (frame.easing !== undefined || animation.easing !== undefined) {
|
|
631
|
+
pushDiagnostic(diagnostics, {
|
|
632
|
+
code: 'DIFFUSION_EASING_APPROXIMATED',
|
|
633
|
+
severity: 'warning',
|
|
634
|
+
path: `${path}.animations[${index.toString()}].frames[${frameIndex.toString()}]`,
|
|
635
|
+
message: 'Diffusion easing was converted to linear interpolation',
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
const value = frame.value;
|
|
639
|
+
if (value === undefined ||
|
|
640
|
+
(typeof value !== 'number' &&
|
|
641
|
+
typeof value !== 'string' &&
|
|
642
|
+
typeof value !== 'boolean' &&
|
|
643
|
+
value !== null)) {
|
|
644
|
+
throw new TypeError(`${path}.animations[${index.toString()}].frames.value is invalid`);
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
timeUs: usFromSeconds(frame.time, `${path}.animations[${index.toString()}].frames[${frameIndex.toString()}].time`),
|
|
648
|
+
value: value,
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
return [{ key, frames }];
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
function interpolateNumberFrames(frames, timeUs, fallback) {
|
|
655
|
+
const numeric = frames.filter((entry) => typeof entry.value === 'number');
|
|
656
|
+
if (numeric.length === 0)
|
|
657
|
+
return fallback;
|
|
658
|
+
const nextIndex = numeric.findIndex(entry => entry.timeUs >= timeUs);
|
|
659
|
+
if (nextIndex <= 0)
|
|
660
|
+
return numeric[Math.max(0, nextIndex)]?.value ?? fallback;
|
|
661
|
+
const right = numeric[nextIndex];
|
|
662
|
+
const left = numeric[nextIndex - 1];
|
|
663
|
+
if (right === undefined || left === undefined)
|
|
664
|
+
return numeric.at(-1)?.value ?? fallback;
|
|
665
|
+
const progress = (timeUs - left.timeUs) / Math.max(1, right.timeUs - left.timeUs);
|
|
666
|
+
return left.value + (right.value - left.value) * progress;
|
|
667
|
+
}
|
|
668
|
+
function applyDiffusionAnimations(builder, clip, itemId, path, diagnostics) {
|
|
669
|
+
const animations = diffusionAnimations(clip, path, diagnostics);
|
|
670
|
+
const byKey = new Map(animations.map(value => [value.key, value]));
|
|
671
|
+
const applyScalar = (sourceKeyName, property, transform = value => value) => {
|
|
672
|
+
const animation = byKey.get(sourceKeyName);
|
|
673
|
+
if (animation === undefined)
|
|
674
|
+
return;
|
|
675
|
+
builder.setKeyframes(itemId, property, animation.frames.map(frame => ({
|
|
676
|
+
timeUs: frame.timeUs,
|
|
677
|
+
value: transform(finite(frame.value, 0, `${path}.animations.${sourceKeyName}`)),
|
|
678
|
+
})));
|
|
679
|
+
};
|
|
680
|
+
applyScalar('rotation', 'rotation');
|
|
681
|
+
applyScalar('opacity', 'opacity', value => value / 100);
|
|
682
|
+
const positionKeys = ['x', 'y', 'translateX', 'translateY'];
|
|
683
|
+
const positionAnimations = positionKeys.flatMap(key => {
|
|
684
|
+
const value = byKey.get(key);
|
|
685
|
+
return value === undefined ? [] : [value];
|
|
686
|
+
});
|
|
687
|
+
if (positionAnimations.length > 0) {
|
|
688
|
+
const times = [
|
|
689
|
+
...new Set(positionAnimations.flatMap(value => value.frames.map(frame => frame.timeUs))),
|
|
690
|
+
].sort((left, right) => left - right);
|
|
691
|
+
builder.setKeyframes(itemId, 'position', times.map(timeUs => ({
|
|
692
|
+
timeUs,
|
|
693
|
+
value: {
|
|
694
|
+
x: interpolateNumberFrames(byKey.get('x')?.frames ?? [], timeUs, finite(clip.x, 0, `${path}.x`)) +
|
|
695
|
+
interpolateNumberFrames(byKey.get('translateX')?.frames ?? [], timeUs, finite(clip.translateX, 0, `${path}.translateX`)),
|
|
696
|
+
y: interpolateNumberFrames(byKey.get('y')?.frames ?? [], timeUs, finite(clip.y, 0, `${path}.y`)) +
|
|
697
|
+
interpolateNumberFrames(byKey.get('translateY')?.frames ?? [], timeUs, finite(clip.translateY, 0, `${path}.translateY`)),
|
|
698
|
+
},
|
|
699
|
+
})));
|
|
700
|
+
}
|
|
701
|
+
const scaleKeys = ['scale', 'scaleX', 'scaleY'];
|
|
702
|
+
const scaleAnimations = scaleKeys.flatMap(key => {
|
|
703
|
+
const value = byKey.get(key);
|
|
704
|
+
return value === undefined ? [] : [value];
|
|
705
|
+
});
|
|
706
|
+
if (scaleAnimations.length > 0) {
|
|
707
|
+
const times = [
|
|
708
|
+
...new Set(scaleAnimations.flatMap(value => value.frames.map(frame => frame.timeUs))),
|
|
709
|
+
].sort((left, right) => left - right);
|
|
710
|
+
builder.setKeyframes(itemId, 'scale', times.map(timeUs => {
|
|
711
|
+
const common = interpolateNumberFrames(byKey.get('scale')?.frames ?? [], timeUs, 1);
|
|
712
|
+
return {
|
|
713
|
+
timeUs,
|
|
714
|
+
value: {
|
|
715
|
+
x: common *
|
|
716
|
+
interpolateNumberFrames(byKey.get('scaleX')?.frames ?? [], timeUs, finite(clip.scaleX, 1, `${path}.scaleX`)),
|
|
717
|
+
y: common *
|
|
718
|
+
interpolateNumberFrames(byKey.get('scaleY')?.frames ?? [], timeUs, finite(clip.scaleY, 1, `${path}.scaleY`)),
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
}));
|
|
722
|
+
}
|
|
723
|
+
const supported = new Set([
|
|
724
|
+
'x',
|
|
725
|
+
'y',
|
|
726
|
+
'translateX',
|
|
727
|
+
'translateY',
|
|
728
|
+
'scale',
|
|
729
|
+
'scaleX',
|
|
730
|
+
'scaleY',
|
|
731
|
+
'rotation',
|
|
732
|
+
'opacity',
|
|
733
|
+
]);
|
|
734
|
+
animations
|
|
735
|
+
.filter(value => !supported.has(value.key))
|
|
736
|
+
.forEach(value => pushDiagnostic(diagnostics, {
|
|
737
|
+
code: 'DIFFUSION_ANIMATION_UNSUPPORTED',
|
|
738
|
+
severity: 'error',
|
|
739
|
+
path: `${path}.animations.${value.key}`,
|
|
740
|
+
message: `animated property ${value.key} cannot be migrated without semantic loss`,
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
function migrateDiffusionMask(builder, clip, itemId, trackId, range, path, diagnostics) {
|
|
744
|
+
if (clip.mask === undefined)
|
|
745
|
+
return;
|
|
746
|
+
const mask = record(clip.mask, `${path}.mask`);
|
|
747
|
+
const type = requiredString(mask.type, `${path}.mask.type`);
|
|
748
|
+
if (type !== 'RECT' && type !== 'ELLIPSE') {
|
|
749
|
+
pushDiagnostic(diagnostics, {
|
|
750
|
+
code: 'DIFFUSION_MASK_UNSUPPORTED',
|
|
751
|
+
severity: 'error',
|
|
752
|
+
path: `${path}.mask`,
|
|
753
|
+
message: `mask type ${type} cannot be migrated`,
|
|
754
|
+
});
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const maskItemId = `${itemId}_mask`;
|
|
758
|
+
builder.addShapeClip({
|
|
759
|
+
id: maskItemId,
|
|
760
|
+
trackId,
|
|
761
|
+
kind: type === 'ELLIPSE' ? 'ellipse' : 'rectangle',
|
|
762
|
+
atUs: range.atUs,
|
|
763
|
+
durationUs: range.durationUs,
|
|
764
|
+
box: {
|
|
765
|
+
x: finite(mask.x, 0, `${path}.mask.x`),
|
|
766
|
+
y: finite(mask.y, 0, `${path}.mask.y`),
|
|
767
|
+
width: finite(mask.width, 1, `${path}.mask.width`),
|
|
768
|
+
height: finite(mask.height, 1, `${path}.mask.height`),
|
|
769
|
+
},
|
|
770
|
+
fill: '#ffffff',
|
|
771
|
+
...(type === 'RECT' ? { cornerRadiusPx: finite(mask.radius, 0, `${path}.mask.radius`) } : {}),
|
|
772
|
+
});
|
|
773
|
+
builder.setMask(itemId, {
|
|
774
|
+
sourceItemId: maskItemId,
|
|
775
|
+
channel: 'alpha',
|
|
776
|
+
invert: false,
|
|
777
|
+
featherPx: 0,
|
|
778
|
+
space: 'canvas',
|
|
779
|
+
consumeSource: true,
|
|
780
|
+
});
|
|
781
|
+
if (Array.isArray(mask.animations) && mask.animations.length > 0) {
|
|
782
|
+
pushDiagnostic(diagnostics, {
|
|
783
|
+
code: 'DIFFUSION_MASK_ANIMATION_UNSUPPORTED',
|
|
784
|
+
severity: 'error',
|
|
785
|
+
path: `${path}.mask.animations`,
|
|
786
|
+
message: 'animated geometric masks require conversion to Aelion shape keyframes',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function diffusionAudioFades(clip, path, trimmedHeadUs, durationUs, diagnostics) {
|
|
791
|
+
const rawFadeInUs = usFromSeconds(clip.fadeInDurationSeconds, `${path}.fadeInDurationSeconds`);
|
|
792
|
+
const rawFadeOutUs = usFromSeconds(clip.fadeOutDurationSeconds, `${path}.fadeOutDurationSeconds`);
|
|
793
|
+
if (rawFadeInUs < 0 || rawFadeOutUs < 0) {
|
|
794
|
+
throw new RangeError(`${path} audio fade durations must be non-negative`);
|
|
795
|
+
}
|
|
796
|
+
const fadeInUs = Math.max(0, rawFadeInUs - trimmedHeadUs);
|
|
797
|
+
const maximumFadeOutUs = Math.max(0, durationUs - fadeInUs);
|
|
798
|
+
const fadeOutUs = Math.min(rawFadeOutUs, maximumFadeOutUs);
|
|
799
|
+
if (fadeOutUs !== rawFadeOutUs) {
|
|
800
|
+
pushDiagnostic(diagnostics, {
|
|
801
|
+
code: 'DIFFUSION_AUDIO_FADES_OVERLAP',
|
|
802
|
+
severity: 'error',
|
|
803
|
+
path,
|
|
804
|
+
message: 'audio fade durations overlap after trimming at composition time zero',
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return { fadeInUs, fadeOutUs };
|
|
808
|
+
}
|
|
809
|
+
function compatibleAssetKind(type, kind) {
|
|
810
|
+
if (type === 'VIDEO')
|
|
811
|
+
return kind === 'video';
|
|
812
|
+
if (type === 'IMAGE')
|
|
813
|
+
return kind === 'image';
|
|
814
|
+
if (type === 'AUDIO')
|
|
815
|
+
return kind === 'audio' || kind === 'video';
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
export function migrateDiffusionCheckpoint(value, options) {
|
|
819
|
+
const checkpoint = record(value, 'checkpoint');
|
|
820
|
+
const settings = diffusionSettings(checkpoint);
|
|
821
|
+
const width = finite(settings.width, 1920, 'checkpoint.settings.width');
|
|
822
|
+
const height = finite(settings.height, 1080, 'checkpoint.settings.height');
|
|
823
|
+
const strict = options.strict ?? true;
|
|
824
|
+
const checkpointTitle = optionalString(checkpoint.displayName);
|
|
825
|
+
const builder = new ProjectBuilder({
|
|
826
|
+
...(options.projectId === undefined
|
|
827
|
+
? { projectId: 'diffusion_project' }
|
|
828
|
+
: { projectId: options.projectId }),
|
|
829
|
+
...(options.sequenceId === undefined ? {} : { sequenceId: options.sequenceId }),
|
|
830
|
+
...(options.title === undefined
|
|
831
|
+
? checkpointTitle === undefined
|
|
832
|
+
? {}
|
|
833
|
+
: { title: checkpointTitle }
|
|
834
|
+
: { title: options.title }),
|
|
835
|
+
...(options.frameRate === undefined ? {} : { frameRate: options.frameRate }),
|
|
836
|
+
width,
|
|
837
|
+
height,
|
|
838
|
+
...(optionalString(settings.background) === undefined
|
|
839
|
+
? {}
|
|
840
|
+
: {
|
|
841
|
+
backgroundColor: optionalString(settings.background) === 'transparent'
|
|
842
|
+
? '#00000000'
|
|
843
|
+
: (optionalString(settings.background) ?? '#000000'),
|
|
844
|
+
}),
|
|
845
|
+
});
|
|
846
|
+
builder.setProjectExtension('aelion.migration.diffusion', {
|
|
847
|
+
source: '@diffusion-studio/core',
|
|
848
|
+
...(checkpoint.id === undefined ? {} : { sourceId: jsonClone(checkpoint.id, 'checkpoint.id') }),
|
|
849
|
+
...(checkpoint.data === undefined
|
|
850
|
+
? {}
|
|
851
|
+
: { data: jsonClone(checkpoint.data, 'checkpoint.data') }),
|
|
852
|
+
});
|
|
853
|
+
const diagnostics = [];
|
|
854
|
+
const entityMap = {};
|
|
855
|
+
const assets = new Map(options.assets.map(asset => [asset.sourceId, asset]));
|
|
856
|
+
for (const asset of options.assets) {
|
|
857
|
+
builder.addAsset({
|
|
858
|
+
id: asset.assetId,
|
|
859
|
+
kind: asset.kind,
|
|
860
|
+
...(asset.locator === undefined ? {} : { locator: asset.locator }),
|
|
861
|
+
...(asset.name === undefined ? {} : { name: asset.name }),
|
|
862
|
+
...(asset.mimeType === undefined ? {} : { mimeType: asset.mimeType }),
|
|
863
|
+
...(asset.contentHash === undefined ? {} : { contentHash: asset.contentHash }),
|
|
864
|
+
...(asset.byteLength === undefined ? {} : { byteLength: asset.byteLength }),
|
|
865
|
+
...(asset.probeHint === undefined ? {} : { probeHint: asset.probeHint }),
|
|
866
|
+
...(asset.representations === undefined ? {} : { representations: asset.representations }),
|
|
867
|
+
...(asset.metadata === undefined ? {} : { metadata: asset.metadata }),
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
const layers = records(checkpoint.layers, 'checkpoint.layers');
|
|
871
|
+
const transitionWork = [];
|
|
872
|
+
// Diffusion layer 0 is top-most; Aelion paints later Tracks on top.
|
|
873
|
+
[...layers].reverse().forEach((layer, reverseIndex) => {
|
|
874
|
+
const sourceIndex = layers.length - reverseIndex - 1;
|
|
875
|
+
const clips = records(layer.clips, `checkpoint.layers[${sourceIndex.toString()}].clips`);
|
|
876
|
+
const firstType = optionalString(clips[0]?.type) ?? 'BASE';
|
|
877
|
+
const kind = firstType === 'AUDIO' ? 'audio' : firstType === 'CAPTION' ? 'caption' : 'visual';
|
|
878
|
+
const trackId = entityId('diffusion_layer', sourceIndex);
|
|
879
|
+
builder.addTrack({
|
|
880
|
+
id: trackId,
|
|
881
|
+
kind,
|
|
882
|
+
name: optionalString(layer.displayName) ??
|
|
883
|
+
optionalString(layer.name) ??
|
|
884
|
+
`Diffusion layer ${sourceIndex.toString()}`,
|
|
885
|
+
enabled: layer.disabled !== true,
|
|
886
|
+
});
|
|
887
|
+
builder.setTrackExtension(trackId, 'aelion.migration.diffusion', {
|
|
888
|
+
...(layer.mode === undefined ? {} : { mode: jsonClone(layer.mode, 'layer.mode') }),
|
|
889
|
+
...(layer.data === undefined ? {} : { data: jsonClone(layer.data, 'layer.data') }),
|
|
890
|
+
});
|
|
891
|
+
entityMap[sourceKey('diffusion-layer', optionalString(layer.id) ?? sourceIndex.toString())] =
|
|
892
|
+
trackId;
|
|
893
|
+
const itemIds = Array.from({ length: clips.length });
|
|
894
|
+
clips.forEach((clip, clipIndex) => {
|
|
895
|
+
const path = `checkpoint.layers[${sourceIndex.toString()}].clips[${clipIndex.toString()}]`;
|
|
896
|
+
const type = requiredString(clip.type, `${path}.type`);
|
|
897
|
+
const sourceId = optionalString(clip.source);
|
|
898
|
+
const asset = sourceId === undefined ? undefined : assets.get(sourceId);
|
|
899
|
+
const originalDelayUs = usFromSeconds(clip.delay, `${path}.delay`);
|
|
900
|
+
const atUs = Math.max(0, originalDelayUs);
|
|
901
|
+
const rawDurationUs = usFromSeconds(clip.duration, `${path}.duration`);
|
|
902
|
+
if (rawDurationUs <= 0)
|
|
903
|
+
throw new RangeError(`${path}.duration must be positive`);
|
|
904
|
+
const trimmedHeadUs = Math.max(0, -originalDelayUs);
|
|
905
|
+
const durationUs = rawDurationUs - trimmedHeadUs;
|
|
906
|
+
if (durationUs <= 0) {
|
|
907
|
+
pushDiagnostic(diagnostics, {
|
|
908
|
+
code: 'DIFFUSION_CLIP_BEFORE_TIMELINE_SKIPPED',
|
|
909
|
+
severity: 'info',
|
|
910
|
+
path,
|
|
911
|
+
message: 'Clip ends at or before composition time zero',
|
|
912
|
+
});
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const itemId = entityId('diffusion_item', clipIndex + sourceIndex * 1000);
|
|
916
|
+
const name = optionalString(clip.displayName) ?? optionalString(clip.name) ?? optionalString(clip.id);
|
|
917
|
+
const rangeRaw = Array.isArray(clip.range) ? clip.range : undefined;
|
|
918
|
+
const sourceStartUs = (rangeRaw === undefined ? 0 : usFromSeconds(rangeRaw[0], `${path}.range[0]`)) +
|
|
919
|
+
trimmedHeadUs;
|
|
920
|
+
const sourceEndUs = rangeRaw === undefined
|
|
921
|
+
? sourceStartUs + durationUs
|
|
922
|
+
: usFromSeconds(rangeRaw[1], `${path}.range[1]`);
|
|
923
|
+
if (sourceEndUs <= sourceStartUs) {
|
|
924
|
+
pushDiagnostic(diagnostics, {
|
|
925
|
+
code: 'DIFFUSION_SOURCE_RANGE_INVALID',
|
|
926
|
+
severity: 'error',
|
|
927
|
+
path: `${path}.range`,
|
|
928
|
+
message: 'source range must end after its trimmed start',
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
const sourceDurationUs = Math.max(1, sourceEndUs - sourceStartUs);
|
|
932
|
+
const mediaRate = normalizeRational({
|
|
933
|
+
numerator: sourceDurationUs,
|
|
934
|
+
denominator: durationUs,
|
|
935
|
+
});
|
|
936
|
+
if (clip.disabled === true) {
|
|
937
|
+
pushDiagnostic(diagnostics, {
|
|
938
|
+
code: 'DIFFUSION_DISABLED_CLIP_SKIPPED',
|
|
939
|
+
severity: 'info',
|
|
940
|
+
path: `${path}.disabled`,
|
|
941
|
+
message: 'disabled Clip was intentionally omitted',
|
|
942
|
+
});
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (type === 'VIDEO' || type === 'IMAGE' || type === 'AUDIO') {
|
|
946
|
+
if (asset === undefined) {
|
|
947
|
+
pushDiagnostic(diagnostics, {
|
|
948
|
+
code: 'DIFFUSION_SOURCE_UNBOUND',
|
|
949
|
+
severity: 'error',
|
|
950
|
+
path: `${path}.source`,
|
|
951
|
+
message: `source ${sourceId ?? '<missing>'} has no asset binding`,
|
|
952
|
+
});
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (!compatibleAssetKind(type, asset.kind)) {
|
|
956
|
+
pushDiagnostic(diagnostics, {
|
|
957
|
+
code: 'DIFFUSION_SOURCE_KIND_MISMATCH',
|
|
958
|
+
severity: 'error',
|
|
959
|
+
path: `${path}.source`,
|
|
960
|
+
message: `${type} Clip cannot bind an ${asset.kind} Aelion Asset`,
|
|
961
|
+
});
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const fades = diffusionAudioFades(clip, path, trimmedHeadUs, durationUs, diagnostics);
|
|
965
|
+
if (type === 'AUDIO') {
|
|
966
|
+
const volume = clip.muted === true
|
|
967
|
+
? 0
|
|
968
|
+
: finite(clip.baseVolume, 1, `${path}.baseVolume`) *
|
|
969
|
+
finite(clip.volume, 1, `${path}.volume`);
|
|
970
|
+
builder.addMediaClip({
|
|
971
|
+
id: itemId,
|
|
972
|
+
kind: 'audio',
|
|
973
|
+
assetId: asset.assetId,
|
|
974
|
+
trackId,
|
|
975
|
+
atUs,
|
|
976
|
+
durationUs,
|
|
977
|
+
sourceStartUs,
|
|
978
|
+
sourceDurationUs,
|
|
979
|
+
rate: mediaRate,
|
|
980
|
+
...(name === undefined ? {} : { name }),
|
|
981
|
+
gainDb: volume <= 0 ? -120 : 20 * Math.log10(volume),
|
|
982
|
+
...(fades.fadeInUs === 0 ? {} : { fadeInUs: fades.fadeInUs }),
|
|
983
|
+
...(fades.fadeOutUs === 0 ? {} : { fadeOutUs: fades.fadeOutUs }),
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
else if (type === 'IMAGE') {
|
|
987
|
+
builder.addImageClip({
|
|
988
|
+
id: itemId,
|
|
989
|
+
assetId: asset.assetId,
|
|
990
|
+
trackId,
|
|
991
|
+
atUs,
|
|
992
|
+
durationUs,
|
|
993
|
+
...(name === undefined ? {} : { name }),
|
|
994
|
+
fit: 'fill',
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
builder.addMediaClip({
|
|
999
|
+
id: itemId,
|
|
1000
|
+
kind: 'video',
|
|
1001
|
+
assetId: asset.assetId,
|
|
1002
|
+
trackId,
|
|
1003
|
+
atUs,
|
|
1004
|
+
durationUs,
|
|
1005
|
+
sourceStartUs,
|
|
1006
|
+
sourceDurationUs,
|
|
1007
|
+
rate: mediaRate,
|
|
1008
|
+
...(name === undefined ? {} : { name }),
|
|
1009
|
+
fit: 'fill',
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
if (type !== 'AUDIO') {
|
|
1013
|
+
const sourceWidth = asset.width ?? finite(clip.width, width, `${path}.width`);
|
|
1014
|
+
const sourceHeight = asset.height ?? finite(clip.height, height, `${path}.height`);
|
|
1015
|
+
builder.setVisual(itemId, diffusionVisual(clip, width, height, sourceWidth, sourceHeight, path, diagnostics));
|
|
1016
|
+
if (type === 'VIDEO') {
|
|
1017
|
+
const probeHint = asset.probeHint;
|
|
1018
|
+
const hasAudio = asset.hasAudio ??
|
|
1019
|
+
(probeHint !== undefined && Object.hasOwn(probeHint, 'audioCodec')
|
|
1020
|
+
? true
|
|
1021
|
+
: undefined);
|
|
1022
|
+
if (hasAudio === undefined) {
|
|
1023
|
+
pushDiagnostic(diagnostics, {
|
|
1024
|
+
code: 'DIFFUSION_VIDEO_AUDIO_UNKNOWN',
|
|
1025
|
+
severity: 'warning',
|
|
1026
|
+
path: `${path}.source`,
|
|
1027
|
+
message: 'video source audio presence is unknown; set assets[].hasAudio to preserve it explicitly',
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
else if (hasAudio) {
|
|
1031
|
+
const audioTrackId = builder.addTrack({
|
|
1032
|
+
id: `${trackId}_audio_${clipIndex.toString()}`,
|
|
1033
|
+
kind: 'audio',
|
|
1034
|
+
name: `${name ?? itemId} audio`,
|
|
1035
|
+
});
|
|
1036
|
+
const volume = clip.muted === true
|
|
1037
|
+
? 0
|
|
1038
|
+
: finite(clip.baseVolume, 1, `${path}.baseVolume`) *
|
|
1039
|
+
finite(clip.volume, 1, `${path}.volume`);
|
|
1040
|
+
const audioItemId = `${itemId}_audio`;
|
|
1041
|
+
builder.addMediaClip({
|
|
1042
|
+
id: audioItemId,
|
|
1043
|
+
kind: 'audio',
|
|
1044
|
+
assetId: asset.assetId,
|
|
1045
|
+
trackId: audioTrackId,
|
|
1046
|
+
atUs,
|
|
1047
|
+
durationUs,
|
|
1048
|
+
sourceStartUs,
|
|
1049
|
+
sourceDurationUs,
|
|
1050
|
+
rate: mediaRate,
|
|
1051
|
+
gainDb: volume <= 0 ? -120 : 20 * Math.log10(volume),
|
|
1052
|
+
name: `${name ?? itemId} audio`,
|
|
1053
|
+
...(fades.fadeInUs === 0 ? {} : { fadeInUs: fades.fadeInUs }),
|
|
1054
|
+
...(fades.fadeOutUs === 0 ? {} : { fadeOutUs: fades.fadeOutUs }),
|
|
1055
|
+
});
|
|
1056
|
+
entityMap[sourceKey('diffusion-audio', optionalString(clip.id) ?? itemId)] =
|
|
1057
|
+
audioItemId;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
else if (type === 'TEXT') {
|
|
1063
|
+
const box = shapeBox(clip, width * 0.8, height * 0.2);
|
|
1064
|
+
const content = migrateDiffusionTextContent(clip, optionalString(clip.text) ?? '', path, diagnostics);
|
|
1065
|
+
builder.addTextClip({
|
|
1066
|
+
id: itemId,
|
|
1067
|
+
trackId,
|
|
1068
|
+
text: content.text,
|
|
1069
|
+
atUs,
|
|
1070
|
+
durationUs,
|
|
1071
|
+
box,
|
|
1072
|
+
style: content.style,
|
|
1073
|
+
...(content.runs === undefined ? {} : { runs: content.runs }),
|
|
1074
|
+
...(name === undefined ? {} : { name }),
|
|
1075
|
+
});
|
|
1076
|
+
builder.setVisual(itemId, diffusionCanvasVisual(clip, width, height, path, diagnostics));
|
|
1077
|
+
}
|
|
1078
|
+
else if (type === 'CAPTION') {
|
|
1079
|
+
if (asset?.captionText === undefined) {
|
|
1080
|
+
pushDiagnostic(diagnostics, {
|
|
1081
|
+
code: 'DIFFUSION_CAPTION_TEXT_UNBOUND',
|
|
1082
|
+
severity: 'error',
|
|
1083
|
+
path: `${path}.source`,
|
|
1084
|
+
message: 'CaptionSource text must be supplied as assets[].captionText',
|
|
1085
|
+
});
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const content = migrateDiffusionTextContent(clip, asset.captionText, path, diagnostics);
|
|
1089
|
+
if (content.runs !== undefined) {
|
|
1090
|
+
pushDiagnostic(diagnostics, {
|
|
1091
|
+
code: 'DIFFUSION_CAPTION_STYLE_RANGES_UNSUPPORTED',
|
|
1092
|
+
severity: 'error',
|
|
1093
|
+
path: `${path}.styles`,
|
|
1094
|
+
message: 'Aelion v1 Caption Items support one style; use a Text Item for rich runs',
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
builder.addCaptionClip({
|
|
1098
|
+
id: itemId,
|
|
1099
|
+
trackId,
|
|
1100
|
+
text: content.text,
|
|
1101
|
+
atUs,
|
|
1102
|
+
durationUs,
|
|
1103
|
+
box: shapeBox(clip, width * 0.8, height * 0.2),
|
|
1104
|
+
style: content.style,
|
|
1105
|
+
...(name === undefined ? {} : { name }),
|
|
1106
|
+
});
|
|
1107
|
+
builder.setVisual(itemId, diffusionCanvasVisual(clip, width, height, path, diagnostics));
|
|
1108
|
+
}
|
|
1109
|
+
else if (type === 'RECT' || type === 'ELLIPSE' || type === 'POLYGON') {
|
|
1110
|
+
const box = shapeBox(clip, 100, 100);
|
|
1111
|
+
const sides = Math.max(3, Math.round(finite(clip.sides, 6, `${path}.sides`)));
|
|
1112
|
+
const points = type !== 'POLYGON'
|
|
1113
|
+
? undefined
|
|
1114
|
+
: Array.from({ length: sides }, (_, pointIndex) => {
|
|
1115
|
+
const angle = (pointIndex / sides) * Math.PI * 2 - Math.PI / 2;
|
|
1116
|
+
return { x: 0.5 + Math.cos(angle) / 2, y: 0.5 + Math.sin(angle) / 2 };
|
|
1117
|
+
});
|
|
1118
|
+
const stroke = diffusionStroke(clip.strokes, `${path}.strokes`, diagnostics);
|
|
1119
|
+
builder.addShapeClip({
|
|
1120
|
+
id: itemId,
|
|
1121
|
+
trackId,
|
|
1122
|
+
kind: type === 'RECT' ? 'rectangle' : type === 'ELLIPSE' ? 'ellipse' : 'polygon',
|
|
1123
|
+
atUs,
|
|
1124
|
+
durationUs,
|
|
1125
|
+
box,
|
|
1126
|
+
fill: optionalString(clip.fill) ?? '#ffffff',
|
|
1127
|
+
...(stroke === undefined ? {} : { stroke: stroke.color, strokeWidthPx: stroke.width }),
|
|
1128
|
+
...(type === 'RECT' ? { cornerRadiusPx: finite(clip.radius, 0, `${path}.radius`) } : {}),
|
|
1129
|
+
...(points === undefined ? {} : { points }),
|
|
1130
|
+
...(name === undefined ? {} : { name }),
|
|
1131
|
+
opacity: Math.max(0, Math.min(1, finite(clip.opacity, 100, `${path}.opacity`) / 100)),
|
|
1132
|
+
});
|
|
1133
|
+
builder.setVisual(itemId, diffusionCanvasVisual(clip, width, height, path, diagnostics));
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
pushDiagnostic(diagnostics, {
|
|
1137
|
+
code: 'DIFFUSION_CLIP_TYPE_UNSUPPORTED',
|
|
1138
|
+
severity: 'error',
|
|
1139
|
+
path: `${path}.type`,
|
|
1140
|
+
message: `clip type ${type} cannot be migrated`,
|
|
1141
|
+
});
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (clip.data !== undefined) {
|
|
1145
|
+
builder.setItemMetadata(itemId, {
|
|
1146
|
+
diffusion: { data: jsonClone(clip.data, `${path}.data`) },
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
itemIds[clipIndex] = itemId;
|
|
1150
|
+
entityMap[sourceKey('diffusion', optionalString(clip.id) ?? itemId)] = itemId;
|
|
1151
|
+
if (type !== 'AUDIO') {
|
|
1152
|
+
migrateDiffusionEffects(builder, clip, itemId, path, diagnostics, width, height);
|
|
1153
|
+
applyDiffusionAnimations(builder, clip, itemId, path, diagnostics);
|
|
1154
|
+
migrateDiffusionMask(builder, clip, itemId, trackId, { atUs, durationUs }, path, diagnostics);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
clips.forEach((clip, clipIndex) => {
|
|
1158
|
+
const itemId = itemIds[clipIndex];
|
|
1159
|
+
if (itemId === undefined || clip.transition === undefined)
|
|
1160
|
+
return;
|
|
1161
|
+
const nextItemId = itemIds[clipIndex + 1];
|
|
1162
|
+
transitionWork.push({
|
|
1163
|
+
clip,
|
|
1164
|
+
itemId,
|
|
1165
|
+
...(nextItemId === undefined ? {} : { nextItemId }),
|
|
1166
|
+
path: `checkpoint.layers[${sourceIndex.toString()}].clips[${clipIndex.toString()}].transition`,
|
|
1167
|
+
centerUs: Math.max(0, usFromSeconds(clip.delay, 'clip.delay') + usFromSeconds(clip.duration, 'clip.duration')),
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
transitionWork.forEach((work, index) => {
|
|
1172
|
+
const transition = record(work.clip.transition, work.path);
|
|
1173
|
+
if (work.nextItemId === undefined) {
|
|
1174
|
+
pushDiagnostic(diagnostics, {
|
|
1175
|
+
code: 'DIFFUSION_TRANSITION_WITHOUT_SUCCESSOR',
|
|
1176
|
+
severity: 'error',
|
|
1177
|
+
path: work.path,
|
|
1178
|
+
message: 'transition is attached to the final Clip in its Layer',
|
|
1179
|
+
});
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const type = requiredString(transition.type, `${work.path}.type`);
|
|
1183
|
+
const supported = new Set([
|
|
1184
|
+
'dissolve',
|
|
1185
|
+
'slide-from-right',
|
|
1186
|
+
'slide-from-left',
|
|
1187
|
+
'fade-to-black',
|
|
1188
|
+
'fade-to-white',
|
|
1189
|
+
]);
|
|
1190
|
+
if (!supported.has(type)) {
|
|
1191
|
+
pushDiagnostic(diagnostics, {
|
|
1192
|
+
code: 'DIFFUSION_TRANSITION_UNSUPPORTED',
|
|
1193
|
+
severity: 'error',
|
|
1194
|
+
path: `${work.path}.type`,
|
|
1195
|
+
message: `transition ${type} is not supported`,
|
|
1196
|
+
});
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const durationUs = usFromSeconds(transition.duration, `${work.path}.duration`);
|
|
1200
|
+
const atUs = Math.max(0, work.centerUs - Math.floor(durationUs / 2));
|
|
1201
|
+
const materialInstanceId = builder.addMaterialInstance({
|
|
1202
|
+
id: entityId('diffusion_transition_material', index),
|
|
1203
|
+
...migrationMaterialPackage,
|
|
1204
|
+
materialId: `diffusion-${type}`,
|
|
1205
|
+
parameters: {},
|
|
1206
|
+
});
|
|
1207
|
+
builder.addTransition({
|
|
1208
|
+
id: entityId('diffusion_transition', index),
|
|
1209
|
+
fromItemId: work.itemId,
|
|
1210
|
+
toItemId: work.nextItemId,
|
|
1211
|
+
materialInstanceId,
|
|
1212
|
+
atUs,
|
|
1213
|
+
durationUs,
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
const markers = Array.isArray(checkpoint.markers) ? checkpoint.markers : [];
|
|
1217
|
+
markers.forEach((raw, index) => {
|
|
1218
|
+
const marker = record(raw, `checkpoint.markers[${index.toString()}]`);
|
|
1219
|
+
const label = optionalString(marker.displayName) ?? optionalString(marker.name);
|
|
1220
|
+
const color = optionalString(marker.color);
|
|
1221
|
+
builder.addMarker({
|
|
1222
|
+
id: entityId('diffusion_marker', index),
|
|
1223
|
+
timeUs: usFromSeconds(marker.time, `checkpoint.markers[${index.toString()}].time`),
|
|
1224
|
+
...(label === undefined ? {} : { label }),
|
|
1225
|
+
...(color === undefined ? {} : { color }),
|
|
1226
|
+
...(marker.data === undefined
|
|
1227
|
+
? {}
|
|
1228
|
+
: { payload: jsonClone(marker.data, `checkpoint.markers[${index.toString()}].data`) }),
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
return finish(builder, diagnostics, entityMap, strict);
|
|
1232
|
+
}
|