@zync/zync-screnplay-player 0.1.251 → 0.1.252
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/dist/bundle.js +1 -1
- package/dist/screenplay/RemotionRenderer/RemotionRenderer.js +43 -42
- package/dist/screenplay/RemotionRenderer/assets/wordObjects/LICENSE +21 -0
- package/dist/screenplay/RemotionRenderer/assets/wordObjects/images.js +24 -0
- package/dist/screenplay/RemotionRenderer/assets/wordObjects/manifest.json +67 -0
- package/dist/screenplay/RemotionRenderer/components/effects/CaptionWordObjectOverlay.js +243 -0
- package/dist/screenplay/RemotionRenderer/components/effects/Sentence.js +10 -3
- package/dist/screenplay/RemotionRenderer/components/layouts/Keyword.js +7 -9
- package/dist/screenplay/RemotionRenderer/components/layouts/KeywordStudioBackdrop.js +5 -7
- package/dist/screenplay/RemotionRenderer/components/layouts/SimpleFrameSentence.js +3 -1
- package/dist/screenplay/RemotionRenderer/components/utils/FaceCenteredVideo.js +9 -0
- package/dist/screenplay/RemotionRenderer/helpers/captionWordObjectGeometry.js +139 -0
- package/dist/screenplay/RemotionRenderer/helpers/captionWordObjects.js +112 -0
- package/dist/screenplay/RemotionRenderer/helpers/headingColors.js +12 -0
- package/dist/screenplay/RemotionRenderer/helpers/normalizeDisabledTranscripts.js +3 -629
- package/dist/screenplay/RemotionRenderer/main/lib/layouts/LayoutFactory.js +0 -15
- package/dist/screenplay/RemotionRenderer/main/lib/layouts/SimpleFrameLayout.js +8 -3
- package/dist/screenplay/RemotionRenderer/theme/themes/default/CaptionPunctuated.js +29 -6
- package/dist/screenplay/RemotionRenderer/theme/themes/default/CaptionWordBoom.js +42 -47
- package/dist/screenplay/RemotionRenderer/theme/themes/default/CaptionWordBubble.js +30 -7
- package/dist/screenplay/RemotionRenderer/theme/themes/default/Sentence/AnimatedSentence.js +1 -0
- package/dist/screenplay/RemotionRenderer/theme/themes/default/Sentence.js +8 -4
- package/dist/screenplay/RemotionRenderer/theme/themes/fullcover/TextFullCover.js +10 -5
- package/dist/screenplay/RemotionRenderer/theme/themes/glassmorphism/Sentence.js +14 -4
- package/dist/screenplay/RemotionRenderer/tracks/LayoutVideoTrack.js +1 -1
- package/dist/screenplay/RemotionRenderer/tracks/Soundtrack.js +20 -3
- package/dist/timing.cjs +1050 -0
- package/package.json +8 -4
package/dist/timing.cjs
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
const EPSILON = 1e-6;
|
|
2
|
+
const DEFAULT_FPS = 30;
|
|
3
|
+
const MIN_CHUNK_DURATION_SECONDS = 0.26;
|
|
4
|
+
|
|
5
|
+
const cloneValue = (value) => {
|
|
6
|
+
if (value === undefined) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return JSON.parse(JSON.stringify(value));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const toFiniteNumber = (value, fallback = null) => {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed)) {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return parsed;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isDisabled = (value) =>
|
|
23
|
+
value === true || value === "true" || value === 1 || value === "1";
|
|
24
|
+
|
|
25
|
+
const buildInterval = (start, end) => {
|
|
26
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (end <= start + EPSILON) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { start, end };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isMeaningfulDuration = (duration) =>
|
|
38
|
+
Number.isFinite(duration) &&
|
|
39
|
+
duration > Math.max(EPSILON, MIN_CHUNK_DURATION_SECONDS - EPSILON);
|
|
40
|
+
|
|
41
|
+
const mergeIntervals = (intervals = []) => {
|
|
42
|
+
if (!Array.isArray(intervals) || intervals.length === 0) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalized = intervals
|
|
47
|
+
.map((interval) => buildInterval(interval?.start, interval?.end))
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.sort((a, b) => a.start - b.start);
|
|
50
|
+
|
|
51
|
+
if (normalized.length === 0) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return normalized.reduce((merged, current) => {
|
|
56
|
+
const previous = merged[merged.length - 1];
|
|
57
|
+
|
|
58
|
+
if (!previous) {
|
|
59
|
+
merged.push({ ...current });
|
|
60
|
+
return merged;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (current.start <= previous.end + EPSILON) {
|
|
64
|
+
previous.end = Math.max(previous.end, current.end);
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
merged.push({ ...current });
|
|
69
|
+
return merged;
|
|
70
|
+
}, []);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const subtractIntervals = (range, cuts = []) => {
|
|
74
|
+
const start = toFiniteNumber(range?.start, null);
|
|
75
|
+
const end = toFiniteNumber(range?.end, null);
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
!Number.isFinite(start) ||
|
|
79
|
+
!Number.isFinite(end) ||
|
|
80
|
+
end <= start + EPSILON
|
|
81
|
+
) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(cuts) || cuts.length === 0) {
|
|
86
|
+
return [{ start, end }];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const mergedCuts = mergeIntervals(cuts);
|
|
90
|
+
const kept = [];
|
|
91
|
+
let cursor = start;
|
|
92
|
+
|
|
93
|
+
for (const cut of mergedCuts) {
|
|
94
|
+
if (cut.end <= cursor + EPSILON) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (cut.start >= end - EPSILON) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (cut.start > cursor + EPSILON) {
|
|
103
|
+
kept.push({
|
|
104
|
+
start: cursor,
|
|
105
|
+
end: Math.min(cut.start, end),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cursor = Math.max(cursor, cut.end);
|
|
110
|
+
|
|
111
|
+
if (cursor >= end - EPSILON) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (cursor < end - EPSILON) {
|
|
117
|
+
kept.push({ start: cursor, end });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return kept.filter((interval) => interval.end > interval.start + EPSILON);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const clipIntervalToRange = (interval, rangeStart, rangeEnd) => {
|
|
124
|
+
const start = Math.max(interval.start, rangeStart);
|
|
125
|
+
const end = Math.min(interval.end, rangeEnd);
|
|
126
|
+
return buildInterval(start, end);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const mapTimeAfterCuts = (time, cuts = []) => {
|
|
130
|
+
const normalizedTime = toFiniteNumber(time, null);
|
|
131
|
+
if (!Number.isFinite(normalizedTime)) {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!Array.isArray(cuts) || cuts.length === 0) {
|
|
136
|
+
return normalizedTime;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let removedDuration = 0;
|
|
140
|
+
|
|
141
|
+
for (const cut of cuts) {
|
|
142
|
+
if (cut.start >= normalizedTime) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
removedDuration += Math.max(
|
|
147
|
+
0,
|
|
148
|
+
Math.min(normalizedTime, cut.end) - cut.start
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Math.max(0, normalizedTime - removedDuration);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const isCaptionEffect = (effect) =>
|
|
156
|
+
typeof effect?.type === "string" && effect.type.includes("caption");
|
|
157
|
+
|
|
158
|
+
const getSegmentTiming = (segment) => {
|
|
159
|
+
const duration = Math.max(
|
|
160
|
+
0,
|
|
161
|
+
toFiniteNumber(segment?.layout?.data?.duration, 0)
|
|
162
|
+
);
|
|
163
|
+
const trimLeft = Math.max(
|
|
164
|
+
0,
|
|
165
|
+
toFiniteNumber(segment?.layout?.data?.trimLeft, 0)
|
|
166
|
+
);
|
|
167
|
+
const trimRight = Math.max(
|
|
168
|
+
0,
|
|
169
|
+
toFiniteNumber(segment?.layout?.data?.trimRight, 0)
|
|
170
|
+
);
|
|
171
|
+
const playableStart = trimLeft;
|
|
172
|
+
const playableEnd = Math.max(playableStart, duration - trimRight);
|
|
173
|
+
const playableDuration = Math.max(0, playableEnd - playableStart);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
duration,
|
|
177
|
+
trimLeft,
|
|
178
|
+
trimRight,
|
|
179
|
+
playableStart,
|
|
180
|
+
playableEnd,
|
|
181
|
+
playableDuration,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const getCaptionPunctuationRange = (entry = {}) => {
|
|
186
|
+
const punctuations = Array.isArray(entry?.punctuations)
|
|
187
|
+
? entry.punctuations
|
|
188
|
+
: [];
|
|
189
|
+
|
|
190
|
+
if (!punctuations.length) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let minStart = Infinity;
|
|
195
|
+
let maxEnd = 0;
|
|
196
|
+
|
|
197
|
+
punctuations.forEach((punctuation) => {
|
|
198
|
+
const start = toFiniteNumber(punctuation?.start, Infinity);
|
|
199
|
+
const end = toFiniteNumber(punctuation?.end, -Infinity);
|
|
200
|
+
|
|
201
|
+
if (start < minStart) {
|
|
202
|
+
minStart = start;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (end > maxEnd) {
|
|
206
|
+
maxEnd = end;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!Number.isFinite(minStart) || maxEnd <= minStart + EPSILON) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
start: minStart,
|
|
216
|
+
end: maxEnd,
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const getCaptionEntryOffset = (entry = {}) => {
|
|
221
|
+
const directOffset = toFiniteNumber(entry?.offset, null);
|
|
222
|
+
|
|
223
|
+
if (Number.isFinite(directOffset)) {
|
|
224
|
+
return directOffset;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const originalOffset = toFiniteNumber(entry?.originalOffset, null);
|
|
228
|
+
|
|
229
|
+
if (Number.isFinite(originalOffset)) {
|
|
230
|
+
return originalOffset;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const punctuationRange = getCaptionPunctuationRange(entry);
|
|
234
|
+
|
|
235
|
+
return punctuationRange?.start ?? null;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const getCaptionEntryTiming = (entry = {}) => {
|
|
239
|
+
const offset = getCaptionEntryOffset(entry);
|
|
240
|
+
const directDuration = toFiniteNumber(entry?.duration, null);
|
|
241
|
+
|
|
242
|
+
if (Number.isFinite(offset) && Number.isFinite(directDuration)) {
|
|
243
|
+
if (directDuration > EPSILON) {
|
|
244
|
+
return { offset, duration: directDuration };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const punctuationRange = getCaptionPunctuationRange(entry);
|
|
251
|
+
|
|
252
|
+
if (!punctuationRange) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const resolvedOffset = Number.isFinite(offset)
|
|
257
|
+
? offset
|
|
258
|
+
: punctuationRange.start;
|
|
259
|
+
const resolvedDuration = punctuationRange.end - resolvedOffset;
|
|
260
|
+
|
|
261
|
+
if (resolvedDuration <= EPSILON) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
offset: resolvedOffset,
|
|
267
|
+
duration: resolvedDuration,
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const normalizeCaptionEntriesWithTiming = (entries = []) => {
|
|
272
|
+
if (!Array.isArray(entries)) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return entries.map((entry) => {
|
|
277
|
+
const normalizedEntry = { ...entry };
|
|
278
|
+
const timing = getCaptionEntryTiming(entry);
|
|
279
|
+
|
|
280
|
+
if (timing) {
|
|
281
|
+
normalizedEntry.offset = timing.offset;
|
|
282
|
+
normalizedEntry.duration = timing.duration;
|
|
283
|
+
|
|
284
|
+
if (normalizedEntry.originalOffset == null) {
|
|
285
|
+
normalizedEntry.originalOffset = timing.offset;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (Array.isArray(entry?.punctuations)) {
|
|
290
|
+
normalizedEntry.punctuations = entry.punctuations.map((punctuation) => ({
|
|
291
|
+
...punctuation,
|
|
292
|
+
...(punctuation?.start != null && punctuation?.originalStart == null
|
|
293
|
+
? { originalStart: punctuation.start }
|
|
294
|
+
: {}),
|
|
295
|
+
...(punctuation?.end != null && punctuation?.originalEnd == null
|
|
296
|
+
? { originalEnd: punctuation.end }
|
|
297
|
+
: {}),
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return normalizedEntry;
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const collectDisabledTranscriptIntervals = (transcripts = []) => {
|
|
306
|
+
if (!Array.isArray(transcripts)) {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const intervals = [];
|
|
311
|
+
|
|
312
|
+
transcripts.forEach((transcript) => {
|
|
313
|
+
if (!isDisabled(transcript?.disabled)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const offset = toFiniteNumber(transcript?.offset, null);
|
|
318
|
+
const duration = toFiniteNumber(transcript?.duration, null);
|
|
319
|
+
if (
|
|
320
|
+
!Number.isFinite(offset) ||
|
|
321
|
+
!Number.isFinite(duration) ||
|
|
322
|
+
duration <= EPSILON
|
|
323
|
+
) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
intervals.push({
|
|
328
|
+
start: offset,
|
|
329
|
+
end: offset + duration,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return mergeIntervals(intervals);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const collectSegmentDisabledIntervals = (segment, timing) => {
|
|
337
|
+
const effects = Array.isArray(segment?.effects) ? segment.effects : [];
|
|
338
|
+
const cuts = [];
|
|
339
|
+
|
|
340
|
+
effects.forEach((effect) => {
|
|
341
|
+
if (!isCaptionEffect(effect)) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const intervals = collectDisabledTranscriptIntervals(
|
|
346
|
+
effect?.data?.transcript_text || []
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
intervals.forEach((interval) => {
|
|
350
|
+
const clipped = clipIntervalToRange(
|
|
351
|
+
interval,
|
|
352
|
+
timing.playableStart,
|
|
353
|
+
timing.playableEnd
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (clipped) {
|
|
357
|
+
cuts.push(clipped);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return mergeIntervals(cuts);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const collectGlobalDisabledIntervals = (screenplay) => {
|
|
366
|
+
const allIntervals = [];
|
|
367
|
+
const segmentTrack = screenplay?.tracks?.find(
|
|
368
|
+
(track) => track.type === "segment"
|
|
369
|
+
);
|
|
370
|
+
const captionTrack = screenplay?.tracks?.find(
|
|
371
|
+
(track) => track.type === "caption"
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
let segmentTimelineCursor = 0;
|
|
375
|
+
|
|
376
|
+
if (Array.isArray(segmentTrack?.segments)) {
|
|
377
|
+
segmentTrack.segments.forEach((segment) => {
|
|
378
|
+
const timing = getSegmentTiming(segment);
|
|
379
|
+
if (isDisabled(segment?.layout?.removed)) {
|
|
380
|
+
const removed = buildInterval(
|
|
381
|
+
segmentTimelineCursor,
|
|
382
|
+
segmentTimelineCursor + timing.playableDuration
|
|
383
|
+
);
|
|
384
|
+
if (removed) allIntervals.push(removed);
|
|
385
|
+
}
|
|
386
|
+
const localCuts = collectSegmentDisabledIntervals(segment, timing);
|
|
387
|
+
|
|
388
|
+
localCuts.forEach((cut) => {
|
|
389
|
+
const start = segmentTimelineCursor + (cut.start - timing.trimLeft);
|
|
390
|
+
const end = segmentTimelineCursor + (cut.end - timing.trimLeft);
|
|
391
|
+
const absolute = buildInterval(start, end);
|
|
392
|
+
|
|
393
|
+
if (absolute) {
|
|
394
|
+
allIntervals.push(absolute);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
segmentTimelineCursor += timing.playableDuration;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (Array.isArray(captionTrack?.segments)) {
|
|
403
|
+
captionTrack.segments.forEach((segment) => {
|
|
404
|
+
const segmentOffset = toFiniteNumber(segment?.data?.offset, 0);
|
|
405
|
+
const intervals = collectDisabledTranscriptIntervals(
|
|
406
|
+
segment?.data?.transcript_text || []
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
intervals.forEach((interval) => {
|
|
410
|
+
const absolute = buildInterval(
|
|
411
|
+
segmentOffset + interval.start,
|
|
412
|
+
segmentOffset + interval.end
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
if (absolute) {
|
|
416
|
+
allIntervals.push(absolute);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const initialCuts = mergeIntervals(allIntervals);
|
|
423
|
+
// Video drops tiny retained chunks around cuts. Include those discarded
|
|
424
|
+
// source ranges before normalizing every track so they share one timeline.
|
|
425
|
+
segmentTimelineCursor = 0;
|
|
426
|
+
for (const segment of segmentTrack?.segments || []) {
|
|
427
|
+
const timing = getSegmentTiming(segment);
|
|
428
|
+
const start = segmentTimelineCursor;
|
|
429
|
+
const end = start + timing.playableDuration;
|
|
430
|
+
segmentTimelineCursor = end;
|
|
431
|
+
if (
|
|
432
|
+
isDisabled(segment?.layout?.removed) ||
|
|
433
|
+
!initialCuts.some(
|
|
434
|
+
(cut) => cut.start < end - EPSILON && cut.end > start + EPSILON
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
continue;
|
|
438
|
+
for (const range of subtractIntervals({ start, end }, initialCuts)) {
|
|
439
|
+
if (!isMeaningfulDuration(range.end - range.start))
|
|
440
|
+
allIntervals.push(range);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return mergeIntervals(allIntervals);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const normalizeTextAndPunctuations = (text, punctuations = []) => {
|
|
447
|
+
if (!Array.isArray(punctuations) || punctuations.length === 0) {
|
|
448
|
+
return {
|
|
449
|
+
text,
|
|
450
|
+
punctuations: [],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (typeof text !== "string") {
|
|
455
|
+
return {
|
|
456
|
+
text,
|
|
457
|
+
punctuations,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const words = text.split(" ");
|
|
462
|
+
if (words.length === 0) {
|
|
463
|
+
return {
|
|
464
|
+
text,
|
|
465
|
+
punctuations,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const wordIndexOrder = [];
|
|
470
|
+
const seen = new Set();
|
|
471
|
+
|
|
472
|
+
punctuations.forEach((punctuation) => {
|
|
473
|
+
const index = toFiniteNumber(punctuation?.index, null);
|
|
474
|
+
if (!Number.isInteger(index)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (index < 0 || index >= words.length) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!seen.has(index)) {
|
|
483
|
+
seen.add(index);
|
|
484
|
+
wordIndexOrder.push(index);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (wordIndexOrder.length === 0) {
|
|
489
|
+
return {
|
|
490
|
+
text,
|
|
491
|
+
punctuations: [],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const filteredWords = wordIndexOrder
|
|
496
|
+
.map((wordIndex) => words[wordIndex])
|
|
497
|
+
.filter((word) => word !== undefined);
|
|
498
|
+
|
|
499
|
+
if (filteredWords.length === 0) {
|
|
500
|
+
return {
|
|
501
|
+
text,
|
|
502
|
+
punctuations: [],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const indexMap = new Map(
|
|
507
|
+
wordIndexOrder.map((wordIndex, mappedIndex) => [wordIndex, mappedIndex])
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const normalizedPunctuations = punctuations
|
|
511
|
+
.filter((punctuation) =>
|
|
512
|
+
indexMap.has(toFiniteNumber(punctuation?.index, null))
|
|
513
|
+
)
|
|
514
|
+
.map((punctuation) => ({
|
|
515
|
+
...punctuation,
|
|
516
|
+
index: indexMap.get(toFiniteNumber(punctuation?.index, null)),
|
|
517
|
+
}));
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
text: filteredWords.join(" "),
|
|
521
|
+
punctuations: normalizedPunctuations,
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const clipTranscriptToRange = ({
|
|
526
|
+
transcript,
|
|
527
|
+
rangeStart,
|
|
528
|
+
rangeEnd,
|
|
529
|
+
offsetMode,
|
|
530
|
+
offsetBase = 0,
|
|
531
|
+
stripDisabled = false,
|
|
532
|
+
}) => {
|
|
533
|
+
if (!transcript) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (stripDisabled && isDisabled(transcript?.disabled)) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const transcriptStart = toFiniteNumber(transcript?.offset, null);
|
|
542
|
+
const transcriptDuration = toFiniteNumber(transcript?.duration, null);
|
|
543
|
+
if (
|
|
544
|
+
!Number.isFinite(transcriptStart) ||
|
|
545
|
+
!Number.isFinite(transcriptDuration) ||
|
|
546
|
+
transcriptDuration <= EPSILON
|
|
547
|
+
) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const transcriptEnd = transcriptStart + transcriptDuration;
|
|
552
|
+
const overlapStart = Math.max(transcriptStart, rangeStart);
|
|
553
|
+
const overlapEnd = Math.min(transcriptEnd, rangeEnd);
|
|
554
|
+
if (overlapEnd <= overlapStart + EPSILON) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const clonedTranscript = cloneValue(transcript);
|
|
559
|
+
const mapOffset = (value) =>
|
|
560
|
+
offsetMode === "relative" ? value - offsetBase : value;
|
|
561
|
+
|
|
562
|
+
clonedTranscript.offset = mapOffset(overlapStart);
|
|
563
|
+
clonedTranscript.duration = overlapEnd - overlapStart;
|
|
564
|
+
|
|
565
|
+
const punctuations = Array.isArray(transcript?.punctuations)
|
|
566
|
+
? transcript.punctuations
|
|
567
|
+
: [];
|
|
568
|
+
|
|
569
|
+
const clippedPunctuations = punctuations
|
|
570
|
+
.map((punctuation) => {
|
|
571
|
+
const punctuationStart = toFiniteNumber(punctuation?.start, null);
|
|
572
|
+
const punctuationEnd = toFiniteNumber(punctuation?.end, null);
|
|
573
|
+
|
|
574
|
+
if (
|
|
575
|
+
!Number.isFinite(punctuationStart) ||
|
|
576
|
+
!Number.isFinite(punctuationEnd) ||
|
|
577
|
+
punctuationEnd <= punctuationStart + EPSILON
|
|
578
|
+
) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const clippedStart = Math.max(punctuationStart, overlapStart);
|
|
583
|
+
const clippedEnd = Math.min(punctuationEnd, overlapEnd);
|
|
584
|
+
if (clippedEnd <= clippedStart + EPSILON) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
...punctuation,
|
|
590
|
+
...(punctuation.wordObject && punctuationStart < rangeStart - EPSILON
|
|
591
|
+
? { __wordObjectOnsetClipped: true }
|
|
592
|
+
: {}),
|
|
593
|
+
start: mapOffset(clippedStart),
|
|
594
|
+
end: mapOffset(clippedEnd),
|
|
595
|
+
};
|
|
596
|
+
})
|
|
597
|
+
.filter(Boolean);
|
|
598
|
+
|
|
599
|
+
const wasClipped =
|
|
600
|
+
overlapStart > transcriptStart + EPSILON ||
|
|
601
|
+
overlapEnd < transcriptEnd - EPSILON;
|
|
602
|
+
|
|
603
|
+
if (wasClipped && clippedPunctuations.length > 0) {
|
|
604
|
+
const { text, punctuations: normalizedPunctuations } =
|
|
605
|
+
normalizeTextAndPunctuations(transcript?.text, clippedPunctuations);
|
|
606
|
+
|
|
607
|
+
clonedTranscript.text = text;
|
|
608
|
+
clonedTranscript.punctuations = normalizedPunctuations;
|
|
609
|
+
} else {
|
|
610
|
+
clonedTranscript.text = transcript?.text;
|
|
611
|
+
clonedTranscript.punctuations = clippedPunctuations;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return clonedTranscript;
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const normalizeTranscriptsForRange = ({
|
|
618
|
+
transcripts,
|
|
619
|
+
rangeStart,
|
|
620
|
+
rangeEnd,
|
|
621
|
+
offsetMode,
|
|
622
|
+
offsetBase,
|
|
623
|
+
stripDisabled,
|
|
624
|
+
}) => {
|
|
625
|
+
if (!Array.isArray(transcripts)) {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return transcripts
|
|
630
|
+
.map((transcript) =>
|
|
631
|
+
clipTranscriptToRange({
|
|
632
|
+
transcript,
|
|
633
|
+
rangeStart,
|
|
634
|
+
rangeEnd,
|
|
635
|
+
offsetMode,
|
|
636
|
+
offsetBase,
|
|
637
|
+
stripDisabled,
|
|
638
|
+
})
|
|
639
|
+
)
|
|
640
|
+
.filter(Boolean);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const normalizeSegmentTrack = (segmentTrack, globalCuts) => {
|
|
644
|
+
if (!segmentTrack || !Array.isArray(segmentTrack?.segments)) {
|
|
645
|
+
return segmentTrack;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const normalizedSegments = [];
|
|
649
|
+
let timelineCursor = 0;
|
|
650
|
+
|
|
651
|
+
segmentTrack.segments.forEach((segment, segmentIndex) => {
|
|
652
|
+
const timing = getSegmentTiming(segment);
|
|
653
|
+
const { duration, playableStart, playableEnd, playableDuration, trimLeft } =
|
|
654
|
+
timing;
|
|
655
|
+
|
|
656
|
+
const isRemoved = isDisabled(segment?.layout?.removed);
|
|
657
|
+
if (isRemoved) {
|
|
658
|
+
// Timed tracks consume the same global cut below. Omit this layout now
|
|
659
|
+
// so preparing the returned screenplay again cannot apply it twice.
|
|
660
|
+
timelineCursor += playableDuration;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (playableDuration <= EPSILON) {
|
|
664
|
+
const untouched = cloneValue(segment);
|
|
665
|
+
untouched.__transitionAfter = true;
|
|
666
|
+
untouched.__originSegmentIndex = segmentIndex;
|
|
667
|
+
normalizedSegments.push(untouched);
|
|
668
|
+
timelineCursor += playableDuration;
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const localCuts = [];
|
|
673
|
+
const segmentAbsoluteStart = timelineCursor;
|
|
674
|
+
const segmentAbsoluteEnd = segmentAbsoluteStart + playableDuration;
|
|
675
|
+
|
|
676
|
+
globalCuts.forEach((globalCut) => {
|
|
677
|
+
if (globalCut.end <= segmentAbsoluteStart + EPSILON) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (globalCut.start >= segmentAbsoluteEnd - EPSILON) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const overlapStart = Math.max(globalCut.start, segmentAbsoluteStart);
|
|
686
|
+
const overlapEnd = Math.min(globalCut.end, segmentAbsoluteEnd);
|
|
687
|
+
if (overlapEnd <= overlapStart + EPSILON) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const localStart = trimLeft + (overlapStart - segmentAbsoluteStart);
|
|
692
|
+
const localEnd = trimLeft + (overlapEnd - segmentAbsoluteStart);
|
|
693
|
+
const localInterval = buildInterval(localStart, localEnd);
|
|
694
|
+
|
|
695
|
+
if (localInterval) {
|
|
696
|
+
localCuts.push(localInterval);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const mergedLocalCuts = mergeIntervals(localCuts);
|
|
701
|
+
const keptRanges = subtractIntervals(
|
|
702
|
+
{ start: playableStart, end: playableEnd },
|
|
703
|
+
mergedLocalCuts
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
if (keptRanges.length === 0) {
|
|
707
|
+
timelineCursor += playableDuration;
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
keptRanges.forEach((keptRange, chunkIndex) => {
|
|
712
|
+
const chunkDuration = keptRange.end - keptRange.start;
|
|
713
|
+
if (
|
|
714
|
+
chunkDuration <= EPSILON ||
|
|
715
|
+
(mergedLocalCuts.length > 0 && !isMeaningfulDuration(chunkDuration))
|
|
716
|
+
) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const clonedChunk = cloneValue(segment);
|
|
721
|
+
|
|
722
|
+
clonedChunk.layout.data.trimLeft = keptRange.start;
|
|
723
|
+
clonedChunk.layout.data.trimRight = Math.max(0, duration - keptRange.end);
|
|
724
|
+
clonedChunk.__transitionAfter = chunkIndex === keptRanges.length - 1;
|
|
725
|
+
clonedChunk.__originSegmentIndex = segmentIndex;
|
|
726
|
+
|
|
727
|
+
if (Array.isArray(clonedChunk.effects)) {
|
|
728
|
+
clonedChunk.effects = clonedChunk.effects.map((effect) => {
|
|
729
|
+
if (!isCaptionEffect(effect)) {
|
|
730
|
+
return effect;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const normalizedEffect = cloneValue(effect);
|
|
734
|
+
normalizedEffect.data = normalizedEffect.data || {};
|
|
735
|
+
normalizedEffect.data.transcript_text = normalizeTranscriptsForRange({
|
|
736
|
+
transcripts: effect?.data?.transcript_text || [],
|
|
737
|
+
rangeStart: keptRange.start,
|
|
738
|
+
rangeEnd: keptRange.end,
|
|
739
|
+
offsetMode: "absolute",
|
|
740
|
+
offsetBase: keptRange.start,
|
|
741
|
+
stripDisabled: true,
|
|
742
|
+
});
|
|
743
|
+
return normalizedEffect;
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
normalizedSegments.push(clonedChunk);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
timelineCursor += playableDuration;
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
...segmentTrack,
|
|
755
|
+
segments: normalizedSegments,
|
|
756
|
+
};
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const resolveSegmentDuration = (segment, trackType) => {
|
|
760
|
+
const explicitDuration = toFiniteNumber(segment?.data?.duration, null);
|
|
761
|
+
if (Number.isFinite(explicitDuration) && explicitDuration > EPSILON) {
|
|
762
|
+
return explicitDuration;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (trackType !== "caption") {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const transcripts = Array.isArray(segment?.data?.transcript_text)
|
|
770
|
+
? segment.data.transcript_text
|
|
771
|
+
: [];
|
|
772
|
+
|
|
773
|
+
const maxEnd = transcripts.reduce((currentMax, transcript) => {
|
|
774
|
+
const offset = toFiniteNumber(transcript?.offset, null);
|
|
775
|
+
const duration = toFiniteNumber(transcript?.duration, null);
|
|
776
|
+
|
|
777
|
+
if (
|
|
778
|
+
!Number.isFinite(offset) ||
|
|
779
|
+
!Number.isFinite(duration) ||
|
|
780
|
+
duration <= EPSILON
|
|
781
|
+
) {
|
|
782
|
+
return currentMax;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return Math.max(currentMax, offset + duration);
|
|
786
|
+
}, 0);
|
|
787
|
+
|
|
788
|
+
return maxEnd > EPSILON ? maxEnd : null;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const shiftSourceMediaStart = (segment, deltaInSeconds) => {
|
|
792
|
+
if (
|
|
793
|
+
!segment?.data ||
|
|
794
|
+
!Number.isFinite(deltaInSeconds) ||
|
|
795
|
+
deltaInSeconds === 0
|
|
796
|
+
) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const sourceVideoStart = toFiniteNumber(
|
|
801
|
+
segment?.data?.sourceVideo?.start,
|
|
802
|
+
segment?.data?.sourceVideo ? 0 : null
|
|
803
|
+
);
|
|
804
|
+
if (Number.isFinite(sourceVideoStart)) {
|
|
805
|
+
segment.data.sourceVideo.start = Math.max(
|
|
806
|
+
0,
|
|
807
|
+
sourceVideoStart + deltaInSeconds
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const sourceAudioStart = toFiniteNumber(
|
|
812
|
+
segment?.data?.sourceAudio?.start,
|
|
813
|
+
segment?.data?.sourceAudio ? 0 : null
|
|
814
|
+
);
|
|
815
|
+
if (Number.isFinite(sourceAudioStart)) {
|
|
816
|
+
segment.data.sourceAudio.start = Math.max(
|
|
817
|
+
0,
|
|
818
|
+
sourceAudioStart + deltaInSeconds
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const normalizeTimedTrack = (
|
|
824
|
+
track,
|
|
825
|
+
globalCuts,
|
|
826
|
+
trackType,
|
|
827
|
+
timelineEnd = null
|
|
828
|
+
) => {
|
|
829
|
+
if (!track || !Array.isArray(track?.segments)) {
|
|
830
|
+
return track;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const normalizedSegments = [];
|
|
834
|
+
|
|
835
|
+
track.segments.forEach((segment) => {
|
|
836
|
+
const segmentStart = toFiniteNumber(segment?.data?.offset, 0);
|
|
837
|
+
let segmentDuration = resolveSegmentDuration(segment, trackType);
|
|
838
|
+
// An open-ended soundtrack plays to the pre-cut segment timeline end.
|
|
839
|
+
// Bound it in the same coordinates as globalCuts so source audio can be
|
|
840
|
+
// advanced or split exactly like an explicitly timed audio segment.
|
|
841
|
+
if (
|
|
842
|
+
trackType === "audio" &&
|
|
843
|
+
!Number.isFinite(segmentDuration) &&
|
|
844
|
+
timelineEnd > EPSILON
|
|
845
|
+
) {
|
|
846
|
+
segmentDuration = timelineEnd - segmentStart;
|
|
847
|
+
if (segmentDuration <= EPSILON) return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (!Number.isFinite(segmentDuration) || segmentDuration <= EPSILON) {
|
|
851
|
+
const shiftedSegment = cloneValue(segment);
|
|
852
|
+
shiftedSegment.data = shiftedSegment.data || {};
|
|
853
|
+
shiftedSegment.data.offset = mapTimeAfterCuts(segmentStart, globalCuts);
|
|
854
|
+
|
|
855
|
+
if (trackType === "caption") {
|
|
856
|
+
shiftedSegment.data.transcript_text = normalizeTranscriptsForRange({
|
|
857
|
+
transcripts: segment?.data?.transcript_text || [],
|
|
858
|
+
rangeStart: 0,
|
|
859
|
+
rangeEnd: Number.POSITIVE_INFINITY,
|
|
860
|
+
offsetMode: "relative",
|
|
861
|
+
offsetBase: 0,
|
|
862
|
+
stripDisabled: true,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
if (shiftedSegment.data.transcript_text.length === 0) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
normalizedSegments.push(shiftedSegment);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const segmentEnd = segmentStart + segmentDuration;
|
|
875
|
+
const wasCut = globalCuts.some(
|
|
876
|
+
(cut) =>
|
|
877
|
+
cut.start < segmentEnd - EPSILON && cut.end > segmentStart + EPSILON
|
|
878
|
+
);
|
|
879
|
+
const keptRanges = subtractIntervals(
|
|
880
|
+
{ start: segmentStart, end: segmentEnd },
|
|
881
|
+
globalCuts
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
keptRanges.forEach((keptRange) => {
|
|
885
|
+
const chunkDuration = keptRange.end - keptRange.start;
|
|
886
|
+
if (
|
|
887
|
+
chunkDuration <= EPSILON ||
|
|
888
|
+
(wasCut && !isMeaningfulDuration(chunkDuration))
|
|
889
|
+
) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const clonedChunk = cloneValue(segment);
|
|
894
|
+
clonedChunk.data = clonedChunk.data || {};
|
|
895
|
+
clonedChunk.data.offset = mapTimeAfterCuts(keptRange.start, globalCuts);
|
|
896
|
+
clonedChunk.data.duration = chunkDuration;
|
|
897
|
+
|
|
898
|
+
const sourceTimeDelta = keptRange.start - segmentStart;
|
|
899
|
+
shiftSourceMediaStart(clonedChunk, sourceTimeDelta);
|
|
900
|
+
|
|
901
|
+
if (trackType === "caption") {
|
|
902
|
+
const localStart = keptRange.start - segmentStart;
|
|
903
|
+
const localEnd = keptRange.end - segmentStart;
|
|
904
|
+
|
|
905
|
+
clonedChunk.data.transcript_text = normalizeTranscriptsForRange({
|
|
906
|
+
transcripts: segment?.data?.transcript_text || [],
|
|
907
|
+
rangeStart: localStart,
|
|
908
|
+
rangeEnd: localEnd,
|
|
909
|
+
offsetMode: "relative",
|
|
910
|
+
offsetBase: localStart,
|
|
911
|
+
stripDisabled: true,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
if (clonedChunk.data.transcript_text.length === 0) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
normalizedSegments.push(clonedChunk);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
...track,
|
|
925
|
+
segments: normalizedSegments,
|
|
926
|
+
};
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const normalizeDisabledTranscripts = (screenplayInput) => {
|
|
930
|
+
const screenplay = cloneValue(screenplayInput) || {};
|
|
931
|
+
if (!Array.isArray(screenplay?.tracks)) {
|
|
932
|
+
return screenplay;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Repair legacy caption timing before deriving cuts, using the same helper
|
|
936
|
+
// that prepares editor previews and saved screenplays.
|
|
937
|
+
for (const track of screenplay.tracks) {
|
|
938
|
+
for (const segment of track?.segments || []) {
|
|
939
|
+
const carriers = [
|
|
940
|
+
segment,
|
|
941
|
+
...(Array.isArray(segment?.effects) ? segment.effects : []),
|
|
942
|
+
];
|
|
943
|
+
for (const carrier of carriers) {
|
|
944
|
+
if (Array.isArray(carrier?.data?.transcript_text))
|
|
945
|
+
carrier.data.transcript_text = normalizeCaptionEntriesWithTiming(
|
|
946
|
+
carrier.data.transcript_text
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const globalDisabledIntervals = collectGlobalDisabledIntervals(screenplay);
|
|
953
|
+
if (globalDisabledIntervals.length === 0) {
|
|
954
|
+
return screenplay;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const segmentTimelineEnd = (
|
|
958
|
+
screenplay.tracks.find((track) => track.type === "segment")?.segments || []
|
|
959
|
+
).reduce(
|
|
960
|
+
(end, segment) => end + getSegmentTiming(segment).playableDuration,
|
|
961
|
+
0
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
screenplay.tracks = screenplay.tracks.map((track) => {
|
|
965
|
+
if (!track || !Array.isArray(track?.segments)) {
|
|
966
|
+
return track;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
switch (track.type) {
|
|
970
|
+
case "segment":
|
|
971
|
+
return normalizeSegmentTrack(track, globalDisabledIntervals);
|
|
972
|
+
case "effect":
|
|
973
|
+
return normalizeTimedTrack(track, globalDisabledIntervals, "effect");
|
|
974
|
+
case "audio":
|
|
975
|
+
return normalizeTimedTrack(
|
|
976
|
+
track,
|
|
977
|
+
globalDisabledIntervals,
|
|
978
|
+
"audio",
|
|
979
|
+
segmentTimelineEnd
|
|
980
|
+
);
|
|
981
|
+
case "caption":
|
|
982
|
+
return normalizeTimedTrack(track, globalDisabledIntervals, "caption");
|
|
983
|
+
default:
|
|
984
|
+
return track;
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
return screenplay;
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// Canonical, dependency-free timing artifact. Zync vendors this file verbatim in
|
|
992
|
+
// common/helper/screenplayTiming.cjs; verify parity before releasing either repo.
|
|
993
|
+
const prepareScreenplay = (input) => {
|
|
994
|
+
const screenplay = normalizeDisabledTranscripts(input);
|
|
995
|
+
for (const track of screenplay.tracks || []) {
|
|
996
|
+
if (track.type === "segment")
|
|
997
|
+
track.segments = (track.segments || [])
|
|
998
|
+
.map((segment, index) => {
|
|
999
|
+
segment.__originSegmentIndex ??= index;
|
|
1000
|
+
return segment;
|
|
1001
|
+
})
|
|
1002
|
+
.filter((s) => !isDisabled(s.layout?.removed));
|
|
1003
|
+
}
|
|
1004
|
+
return screenplay;
|
|
1005
|
+
};
|
|
1006
|
+
const getScreenplayTiming = (input, { prepared = false } = {}) => {
|
|
1007
|
+
const screenplay = prepared ? input : prepareScreenplay(input);
|
|
1008
|
+
const segments = (
|
|
1009
|
+
screenplay.tracks?.find((t) => t.type === "segment")?.segments || []
|
|
1010
|
+
).filter((s) => !isDisabled(s.layout?.removed));
|
|
1011
|
+
const segmentFrames = segments.map((segment) =>
|
|
1012
|
+
Math.max(
|
|
1013
|
+
1,
|
|
1014
|
+
Math.round(getSegmentTiming(segment).playableDuration * DEFAULT_FPS)
|
|
1015
|
+
)
|
|
1016
|
+
);
|
|
1017
|
+
let cursor = 0;
|
|
1018
|
+
const sequences = segments.map((segment, index) => {
|
|
1019
|
+
const frames = segmentFrames[index];
|
|
1020
|
+
const overlapFrames =
|
|
1021
|
+
screenplay.input?.theme === "none" &&
|
|
1022
|
+
index < segments.length - 1 &&
|
|
1023
|
+
frames >= 15 &&
|
|
1024
|
+
segmentFrames[index + 1] >= 15 &&
|
|
1025
|
+
(segment.transitionAfter ?? segment.__transitionAfter) !== false
|
|
1026
|
+
? 15
|
|
1027
|
+
: 0;
|
|
1028
|
+
const startFrame = cursor;
|
|
1029
|
+
cursor += frames - overlapFrames;
|
|
1030
|
+
return {
|
|
1031
|
+
originSegmentIndex: segment.__originSegmentIndex ?? index,
|
|
1032
|
+
startFrame,
|
|
1033
|
+
endFrame: startFrame + frames,
|
|
1034
|
+
durationInFrames: frames,
|
|
1035
|
+
overlapFrames,
|
|
1036
|
+
};
|
|
1037
|
+
});
|
|
1038
|
+
return {
|
|
1039
|
+
fps: DEFAULT_FPS,
|
|
1040
|
+
durationInFrames: cursor,
|
|
1041
|
+
durationSeconds: cursor / DEFAULT_FPS,
|
|
1042
|
+
sequences,
|
|
1043
|
+
};
|
|
1044
|
+
};
|
|
1045
|
+
module.exports = {
|
|
1046
|
+
normalizeCaptionEntriesWithTiming,
|
|
1047
|
+
normalizeDisabledTranscripts,
|
|
1048
|
+
prepareScreenplay,
|
|
1049
|
+
getScreenplayTiming,
|
|
1050
|
+
};
|