@mevdragon/vidfarm-devcli 0.12.0 → 0.14.0
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/SKILL.director.md +29 -7
- package/SKILL.platform.md +4 -4
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +254 -80
- package/dist/src/account-pages-legacy.js +9 -14
- package/dist/src/app.js +1081 -133
- package/dist/src/cli.js +107 -9
- package/dist/src/composition-runtime.js +88 -9
- package/dist/src/devcli/clips.js +5 -0
- package/dist/src/devcli/composition-edit.js +176 -8
- package/dist/src/devcli/transitions.js +205 -0
- package/dist/src/editor-chat.js +6 -5
- package/dist/src/frontend/homepage-view.js +1 -1
- package/dist/src/frontend/template-editor-chat.js +9 -6
- package/dist/src/homepage.js +9 -48
- package/dist/src/hyperframes/composition.js +183 -2
- package/dist/src/primitive-registry.js +237 -47
- package/dist/src/services/hyperframes.js +60 -15
- package/dist/src/services/providers.js +1 -0
- package/dist/src/services/serverless-records.js +19 -2
- package/dist/src/services/storage.js +12 -0
- package/dist/src/template-editor-shell.js +87 -48
- package/package.json +1 -1
- package/public/assets/homepage-client-app.js +23 -23
- package/public/assets/page-runtime-client-app.js +31 -31
|
@@ -98,10 +98,16 @@ export function normalizeKenBurns(value) {
|
|
|
98
98
|
export const TRANSITION_PRESETS = [
|
|
99
99
|
"crossfade",
|
|
100
100
|
"fade-black",
|
|
101
|
+
"fade-white",
|
|
102
|
+
"flash",
|
|
103
|
+
"smoke",
|
|
104
|
+
"blur",
|
|
101
105
|
"slide-left",
|
|
102
106
|
"slide-right",
|
|
103
107
|
"slide-up",
|
|
104
108
|
"slide-down",
|
|
109
|
+
"whip-left",
|
|
110
|
+
"whip-right",
|
|
105
111
|
"wipe-left",
|
|
106
112
|
"wipe-right",
|
|
107
113
|
"wipe-up",
|
|
@@ -110,14 +116,49 @@ export const TRANSITION_PRESETS = [
|
|
|
110
116
|
"zoom-out",
|
|
111
117
|
"circle-open"
|
|
112
118
|
];
|
|
119
|
+
// Exit transitions: the clip animates AWAY over its last data-transition-out-
|
|
120
|
+
// duration seconds. Same declarative contract as the entrance presets; the
|
|
121
|
+
// keyframes omit the `from` state so the animation departs from the element's
|
|
122
|
+
// own inline styles, and the inline --vf-tr-out-delay var (clip duration minus
|
|
123
|
+
// out duration) positions the animation window at the clip's tail so the
|
|
124
|
+
// time-based scrub in the producer and preview runtimes needs no extra logic.
|
|
125
|
+
export const TRANSITION_OUT_PRESETS = [
|
|
126
|
+
"fade",
|
|
127
|
+
"fade-black",
|
|
128
|
+
"fade-white",
|
|
129
|
+
"flash",
|
|
130
|
+
"smoke",
|
|
131
|
+
"blur",
|
|
132
|
+
"slide-left",
|
|
133
|
+
"slide-right",
|
|
134
|
+
"slide-up",
|
|
135
|
+
"slide-down",
|
|
136
|
+
"wipe-left",
|
|
137
|
+
"wipe-right",
|
|
138
|
+
"wipe-up",
|
|
139
|
+
"wipe-down",
|
|
140
|
+
"zoom-in",
|
|
141
|
+
"zoom-out",
|
|
142
|
+
"circle-close"
|
|
143
|
+
];
|
|
113
144
|
export const TRANSITION_DEFAULT_DURATION = 0.5;
|
|
114
145
|
export const TRANSITION_MIN_DURATION = 0.1;
|
|
115
146
|
export const TRANSITION_MAX_DURATION = 2.5;
|
|
116
147
|
// Two butt-cut clips can drift by a few ms through timeline edits; treat ends
|
|
117
148
|
// within this window as adjacent when granting the outgoing clip's hold.
|
|
118
149
|
export const TRANSITION_ADJACENCY_EPSILON = 0.05;
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
// V2: entrance AND exit animations compose on one element via cascading custom
|
|
151
|
+
// properties (--vf-tr-in-name / --vf-tr-out-name) feeding a two-slot animation
|
|
152
|
+
// list — per-preset attribute selectors can't merge two animation-names, vars
|
|
153
|
+
// can. The [data-kenburns] combo rules below fix a V1 latent clash where Ken
|
|
154
|
+
// Burns and a transition on the same <img> fought over animation-name (equal
|
|
155
|
+
// specificity, last stylesheet won); the (0,2,0) combo selectors prepend the
|
|
156
|
+
// vf-kb animation to the list instead.
|
|
157
|
+
export const TRANSITION_STYLE_MARKER = "__VF_TRANSITIONS_V2__";
|
|
158
|
+
// The exact V1 block previous builds baked into stored compositions. Injectors
|
|
159
|
+
// string-replace this with the current TRANSITION_CSS so old drafts/publishes
|
|
160
|
+
// upgrade in place instead of accumulating duplicate style blocks.
|
|
161
|
+
export const LEGACY_TRANSITION_CSS_V1 = `/*__VF_TRANSITIONS_V1__*/
|
|
121
162
|
@keyframes vf-tr-crossfade { from { opacity: 0; } }
|
|
122
163
|
@keyframes vf-tr-fade-black { 0% { opacity: 0; filter: brightness(0); } 35% { opacity: 1; filter: brightness(0); } }
|
|
123
164
|
@keyframes vf-tr-slide-left { from { translate: 100% 0; } }
|
|
@@ -151,12 +192,119 @@ export const TRANSITION_CSS = `/*${TRANSITION_STYLE_MARKER}*/
|
|
|
151
192
|
[data-transition="zoom-out"] { animation-name: vf-tr-zoom-out; }
|
|
152
193
|
[data-transition="circle-open"] { animation-name: vf-tr-circle-open; }
|
|
153
194
|
`;
|
|
195
|
+
export const TRANSITION_CSS = `/*${TRANSITION_STYLE_MARKER}*/
|
|
196
|
+
@keyframes vf-tr-crossfade { from { opacity: 0; } }
|
|
197
|
+
@keyframes vf-tr-fade-black { 0% { opacity: 0; filter: brightness(0); } 35% { opacity: 1; filter: brightness(0); } }
|
|
198
|
+
@keyframes vf-tr-fade-white { 0% { opacity: 0; filter: saturate(0.55) brightness(3); } 45% { opacity: 1; filter: saturate(0.7) brightness(2.1); } }
|
|
199
|
+
@keyframes vf-tr-flash { 0% { filter: saturate(0.3) brightness(5); } 55% { filter: saturate(0.9) brightness(1.6); } }
|
|
200
|
+
@keyframes vf-tr-smoke { 0% { opacity: 0; filter: blur(26px) saturate(0.65) brightness(1.55); } 55% { opacity: 1; } }
|
|
201
|
+
@keyframes vf-tr-blur { from { opacity: 0; filter: blur(16px); } }
|
|
202
|
+
@keyframes vf-tr-slide-left { from { translate: 100% 0; } }
|
|
203
|
+
@keyframes vf-tr-slide-right { from { translate: -100% 0; } }
|
|
204
|
+
@keyframes vf-tr-slide-up { from { translate: 0 100%; } }
|
|
205
|
+
@keyframes vf-tr-slide-down { from { translate: 0 -100%; } }
|
|
206
|
+
@keyframes vf-tr-whip-left { 0% { translate: 110% 0; filter: blur(14px); } 60% { filter: blur(5px); } }
|
|
207
|
+
@keyframes vf-tr-whip-right { 0% { translate: -110% 0; filter: blur(14px); } 60% { filter: blur(5px); } }
|
|
208
|
+
@keyframes vf-tr-wipe-left { from { clip-path: inset(0 0 0 100%); } to { clip-path: inset(0 0 0 0); } }
|
|
209
|
+
@keyframes vf-tr-wipe-right { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
|
|
210
|
+
@keyframes vf-tr-wipe-up { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); } }
|
|
211
|
+
@keyframes vf-tr-wipe-down { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); } }
|
|
212
|
+
@keyframes vf-tr-zoom-in { from { opacity: 0; scale: 1.18; } }
|
|
213
|
+
@keyframes vf-tr-zoom-out { from { opacity: 0; scale: 0.82; } }
|
|
214
|
+
@keyframes vf-tr-circle-open { from { clip-path: circle(0% at 50% 50%); } to { clip-path: circle(75% at 50% 50%); } }
|
|
215
|
+
@keyframes vf-tr-out-fade { to { opacity: 0; } }
|
|
216
|
+
@keyframes vf-tr-out-fade-black { to { filter: brightness(0); } }
|
|
217
|
+
@keyframes vf-tr-out-fade-white { to { filter: saturate(0.35) brightness(6); } }
|
|
218
|
+
@keyframes vf-tr-out-flash { 65% { filter: brightness(1); } 100% { filter: saturate(0.3) brightness(5); } }
|
|
219
|
+
@keyframes vf-tr-out-smoke { to { opacity: 0; filter: blur(24px) saturate(0.7) brightness(1.5); } }
|
|
220
|
+
@keyframes vf-tr-out-blur { to { opacity: 0; filter: blur(16px); } }
|
|
221
|
+
@keyframes vf-tr-out-slide-left { to { translate: -100% 0; } }
|
|
222
|
+
@keyframes vf-tr-out-slide-right { to { translate: 100% 0; } }
|
|
223
|
+
@keyframes vf-tr-out-slide-up { to { translate: 0 -100%; } }
|
|
224
|
+
@keyframes vf-tr-out-slide-down { to { translate: 0 100%; } }
|
|
225
|
+
@keyframes vf-tr-out-wipe-left { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 100% 0 0); } }
|
|
226
|
+
@keyframes vf-tr-out-wipe-right { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 0 100%); } }
|
|
227
|
+
@keyframes vf-tr-out-wipe-up { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 100% 0); } }
|
|
228
|
+
@keyframes vf-tr-out-wipe-down { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(100% 0 0 0); } }
|
|
229
|
+
@keyframes vf-tr-out-zoom-in { to { opacity: 0; scale: 1.18; } }
|
|
230
|
+
@keyframes vf-tr-out-zoom-out { to { opacity: 0; scale: 0.82; } }
|
|
231
|
+
@keyframes vf-tr-out-circle-close { from { clip-path: circle(75% at 50% 50%); } to { clip-path: circle(0% at 50% 50%); } }
|
|
232
|
+
[data-transition], [data-transition-out] {
|
|
233
|
+
animation-name: var(--vf-tr-in-name, none), var(--vf-tr-out-name, none);
|
|
234
|
+
animation-duration: var(--vf-tr-dur, 0.5s), var(--vf-tr-out-dur, 0.5s);
|
|
235
|
+
animation-delay: 0s, var(--vf-tr-out-delay, 0s);
|
|
236
|
+
animation-timing-function: var(--vf-tr-ease, ease-in-out), var(--vf-tr-out-ease, ease-in-out);
|
|
237
|
+
animation-fill-mode: both, both;
|
|
238
|
+
animation-play-state: paused, paused;
|
|
239
|
+
}
|
|
240
|
+
[data-kenburns][data-transition], [data-kenburns][data-transition-out] {
|
|
241
|
+
animation-name: var(--vf-kb-name, none), var(--vf-tr-in-name, none), var(--vf-tr-out-name, none);
|
|
242
|
+
animation-duration: var(--vf-kb-dur, 5s), var(--vf-tr-dur, 0.5s), var(--vf-tr-out-dur, 0.5s);
|
|
243
|
+
animation-delay: 0s, 0s, var(--vf-tr-out-delay, 0s);
|
|
244
|
+
animation-timing-function: var(--vf-kb-ease, linear), var(--vf-tr-ease, ease-in-out), var(--vf-tr-out-ease, ease-in-out);
|
|
245
|
+
animation-fill-mode: both, both, both;
|
|
246
|
+
animation-play-state: paused, paused, paused;
|
|
247
|
+
}
|
|
248
|
+
[data-kenburns="zoom-in"] { --vf-kb-name: vf-kb-zoom-in; }
|
|
249
|
+
[data-kenburns="zoom-out"] { --vf-kb-name: vf-kb-zoom-out; }
|
|
250
|
+
[data-kenburns="pan-left"] { --vf-kb-name: vf-kb-pan-left; }
|
|
251
|
+
[data-kenburns="pan-right"] { --vf-kb-name: vf-kb-pan-right; }
|
|
252
|
+
[data-kenburns="pan-up"] { --vf-kb-name: vf-kb-pan-up; }
|
|
253
|
+
[data-kenburns="pan-down"] { --vf-kb-name: vf-kb-pan-down; }
|
|
254
|
+
[data-kenburns="zoom-in-left"] { --vf-kb-name: vf-kb-zoom-in-left; }
|
|
255
|
+
[data-kenburns="zoom-in-right"] { --vf-kb-name: vf-kb-zoom-in-right; }
|
|
256
|
+
[data-transition="crossfade"] { --vf-tr-in-name: vf-tr-crossfade; }
|
|
257
|
+
[data-transition="fade-black"] { --vf-tr-in-name: vf-tr-fade-black; }
|
|
258
|
+
[data-transition="fade-white"] { --vf-tr-in-name: vf-tr-fade-white; }
|
|
259
|
+
[data-transition="flash"] { --vf-tr-in-name: vf-tr-flash; --vf-tr-ease: cubic-bezier(0.3, 0, 0.55, 1); }
|
|
260
|
+
[data-transition="smoke"] { --vf-tr-in-name: vf-tr-smoke; --vf-tr-ease: cubic-bezier(0.25, 0.4, 0.3, 1); }
|
|
261
|
+
[data-transition="blur"] { --vf-tr-in-name: vf-tr-blur; }
|
|
262
|
+
[data-transition="slide-left"] { --vf-tr-in-name: vf-tr-slide-left; --vf-tr-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
263
|
+
[data-transition="slide-right"] { --vf-tr-in-name: vf-tr-slide-right; --vf-tr-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
264
|
+
[data-transition="slide-up"] { --vf-tr-in-name: vf-tr-slide-up; --vf-tr-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
265
|
+
[data-transition="slide-down"] { --vf-tr-in-name: vf-tr-slide-down; --vf-tr-ease: cubic-bezier(0.22, 1, 0.36, 1); }
|
|
266
|
+
[data-transition="whip-left"] { --vf-tr-in-name: vf-tr-whip-left; --vf-tr-ease: cubic-bezier(0.16, 1, 0.3, 1); }
|
|
267
|
+
[data-transition="whip-right"] { --vf-tr-in-name: vf-tr-whip-right; --vf-tr-ease: cubic-bezier(0.16, 1, 0.3, 1); }
|
|
268
|
+
[data-transition="wipe-left"] { --vf-tr-in-name: vf-tr-wipe-left; --vf-tr-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
269
|
+
[data-transition="wipe-right"] { --vf-tr-in-name: vf-tr-wipe-right; --vf-tr-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
270
|
+
[data-transition="wipe-up"] { --vf-tr-in-name: vf-tr-wipe-up; --vf-tr-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
271
|
+
[data-transition="wipe-down"] { --vf-tr-in-name: vf-tr-wipe-down; --vf-tr-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
272
|
+
[data-transition="zoom-in"] { --vf-tr-in-name: vf-tr-zoom-in; --vf-tr-ease: cubic-bezier(0.16, 1, 0.3, 1); }
|
|
273
|
+
[data-transition="zoom-out"] { --vf-tr-in-name: vf-tr-zoom-out; --vf-tr-ease: cubic-bezier(0.16, 1, 0.3, 1); }
|
|
274
|
+
[data-transition="circle-open"] { --vf-tr-in-name: vf-tr-circle-open; --vf-tr-ease: cubic-bezier(0.3, 0.6, 0.3, 1); }
|
|
275
|
+
[data-transition-out="fade"] { --vf-tr-out-name: vf-tr-out-fade; }
|
|
276
|
+
[data-transition-out="fade-black"] { --vf-tr-out-name: vf-tr-out-fade-black; }
|
|
277
|
+
[data-transition-out="fade-white"] { --vf-tr-out-name: vf-tr-out-fade-white; }
|
|
278
|
+
[data-transition-out="flash"] { --vf-tr-out-name: vf-tr-out-flash; --vf-tr-out-ease: cubic-bezier(0.45, 0, 0.7, 0.4); }
|
|
279
|
+
[data-transition-out="smoke"] { --vf-tr-out-name: vf-tr-out-smoke; --vf-tr-out-ease: cubic-bezier(0.7, 0, 0.75, 0.6); }
|
|
280
|
+
[data-transition-out="blur"] { --vf-tr-out-name: vf-tr-out-blur; }
|
|
281
|
+
[data-transition-out="slide-left"] { --vf-tr-out-name: vf-tr-out-slide-left; --vf-tr-out-ease: cubic-bezier(0.55, 0.06, 0.68, 0.19); }
|
|
282
|
+
[data-transition-out="slide-right"] { --vf-tr-out-name: vf-tr-out-slide-right; --vf-tr-out-ease: cubic-bezier(0.55, 0.06, 0.68, 0.19); }
|
|
283
|
+
[data-transition-out="slide-up"] { --vf-tr-out-name: vf-tr-out-slide-up; --vf-tr-out-ease: cubic-bezier(0.55, 0.06, 0.68, 0.19); }
|
|
284
|
+
[data-transition-out="slide-down"] { --vf-tr-out-name: vf-tr-out-slide-down; --vf-tr-out-ease: cubic-bezier(0.55, 0.06, 0.68, 0.19); }
|
|
285
|
+
[data-transition-out="wipe-left"] { --vf-tr-out-name: vf-tr-out-wipe-left; --vf-tr-out-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
286
|
+
[data-transition-out="wipe-right"] { --vf-tr-out-name: vf-tr-out-wipe-right; --vf-tr-out-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
287
|
+
[data-transition-out="wipe-up"] { --vf-tr-out-name: vf-tr-out-wipe-up; --vf-tr-out-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
288
|
+
[data-transition-out="wipe-down"] { --vf-tr-out-name: vf-tr-out-wipe-down; --vf-tr-out-ease: cubic-bezier(0.45, 0.05, 0.35, 0.95); }
|
|
289
|
+
[data-transition-out="zoom-in"] { --vf-tr-out-name: vf-tr-out-zoom-in; --vf-tr-out-ease: cubic-bezier(0.5, 0, 0.75, 0.4); }
|
|
290
|
+
[data-transition-out="zoom-out"] { --vf-tr-out-name: vf-tr-out-zoom-out; --vf-tr-out-ease: cubic-bezier(0.5, 0, 0.75, 0.4); }
|
|
291
|
+
[data-transition-out="circle-close"] { --vf-tr-out-name: vf-tr-out-circle-close; --vf-tr-out-ease: cubic-bezier(0.6, 0, 0.7, 0.4); }
|
|
292
|
+
`;
|
|
154
293
|
export function clampTransitionDuration(value) {
|
|
155
294
|
const next = Number(value);
|
|
156
295
|
if (!Number.isFinite(next) || next <= 0)
|
|
157
296
|
return TRANSITION_DEFAULT_DURATION;
|
|
158
297
|
return Math.max(TRANSITION_MIN_DURATION, Math.min(TRANSITION_MAX_DURATION, next));
|
|
159
298
|
}
|
|
299
|
+
// Seconds into the clip at which its exit animation must start so it finishes
|
|
300
|
+
// exactly at the clip's end. Written as the inline --vf-tr-out-delay var.
|
|
301
|
+
export function transitionOutDelaySeconds(layerDurationSeconds, outDurationSeconds) {
|
|
302
|
+
const layerDuration = Number(layerDurationSeconds);
|
|
303
|
+
const outDuration = Number(outDurationSeconds);
|
|
304
|
+
if (!Number.isFinite(layerDuration) || !Number.isFinite(outDuration))
|
|
305
|
+
return 0;
|
|
306
|
+
return Math.max(0, Number((layerDuration - outDuration).toFixed(3)));
|
|
307
|
+
}
|
|
160
308
|
export function normalizeTransition(value) {
|
|
161
309
|
if (!value)
|
|
162
310
|
return null;
|
|
@@ -168,6 +316,28 @@ export function normalizeTransition(value) {
|
|
|
168
316
|
duration: raw.duration !== undefined ? clampTransitionDuration(raw.duration) : undefined
|
|
169
317
|
};
|
|
170
318
|
}
|
|
319
|
+
export function normalizeTransitionOut(value) {
|
|
320
|
+
if (!value)
|
|
321
|
+
return null;
|
|
322
|
+
const raw = typeof value === "string" ? { preset: value } : value;
|
|
323
|
+
if (!TRANSITION_OUT_PRESETS.includes(raw.preset))
|
|
324
|
+
return null;
|
|
325
|
+
return {
|
|
326
|
+
preset: raw.preset,
|
|
327
|
+
duration: raw.duration !== undefined ? clampTransitionDuration(raw.duration) : undefined
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// Upgrades stored composition HTML that still carries the V1 transition
|
|
331
|
+
// stylesheet: exact-replaces the legacy block with the current one. If the V1
|
|
332
|
+
// marker is present but the block has drifted, callers' normal marker-guarded
|
|
333
|
+
// injection appends the V2 block AFTER it, which also wins (equal-specificity
|
|
334
|
+
// rules, later source order) — this replace just avoids duplicate blocks in
|
|
335
|
+
// the common case.
|
|
336
|
+
export function upgradeLegacyTransitionCss(html) {
|
|
337
|
+
if (!html || html.includes(TRANSITION_STYLE_MARKER) || !html.includes("__VF_TRANSITIONS_V1__"))
|
|
338
|
+
return html;
|
|
339
|
+
return html.split(LEGACY_TRANSITION_CSS_V1).join(TRANSITION_CSS);
|
|
340
|
+
}
|
|
171
341
|
// ---------------------------------------------------------------------------
|
|
172
342
|
// Animated captions. Same declarative contract as Ken Burns: the caption layer
|
|
173
343
|
// <div> (the element that owns data-start/data-duration) carries
|
|
@@ -476,6 +646,10 @@ function timedAttrs(layer) {
|
|
|
476
646
|
if (transition) {
|
|
477
647
|
attrs.push(`data-transition="${escapeAttr(transition.preset)}"`, `data-transition-duration="${num(clampTransitionDuration(transition.duration ?? TRANSITION_DEFAULT_DURATION))}"`);
|
|
478
648
|
}
|
|
649
|
+
const transitionOut = normalizeTransitionOut(layer.transitionOut);
|
|
650
|
+
if (transitionOut) {
|
|
651
|
+
attrs.push(`data-transition-out="${escapeAttr(transitionOut.preset)}"`, `data-transition-out-duration="${num(clampTransitionDuration(transitionOut.duration ?? TRANSITION_DEFAULT_DURATION))}"`);
|
|
652
|
+
}
|
|
479
653
|
}
|
|
480
654
|
if (["caption", "text", "shape", "html"].includes(layer.kind)) {
|
|
481
655
|
attrs.push(`data-text-background-style="${escapeAttr(layer.textBackgroundStyle || "plain")}"`, `data-text-background-color="${escapeAttr(layer.background || "#000000")}"`, `data-font-family="${escapeAttr(layer.fontFamily || "TikTok Sans")}"`);
|
|
@@ -504,6 +678,13 @@ function layerStyle(layer) {
|
|
|
504
678
|
if (transition) {
|
|
505
679
|
styles.push(`--vf-tr-dur:${num(clampTransitionDuration(transition.duration ?? TRANSITION_DEFAULT_DURATION))}s`);
|
|
506
680
|
}
|
|
681
|
+
// Exit transitions additionally need the delay var so the animation window
|
|
682
|
+
// sits at the clip's tail (duration − out duration).
|
|
683
|
+
const transitionOut = normalizeTransitionOut(layer.transitionOut);
|
|
684
|
+
if (transitionOut) {
|
|
685
|
+
const outDur = clampTransitionDuration(transitionOut.duration ?? TRANSITION_DEFAULT_DURATION);
|
|
686
|
+
styles.push(`--vf-tr-out-dur:${num(outDur)}s`, `--vf-tr-out-delay:${num(transitionOutDelaySeconds(positiveNumber(layer.duration, 0), outDur))}s`);
|
|
687
|
+
}
|
|
507
688
|
}
|
|
508
689
|
if (["caption", "text", "shape", "html"].includes(layer.kind)) {
|
|
509
690
|
const fontFamily = layer.fontFamily || "TikTok Sans";
|
|
@@ -8,6 +8,8 @@ import { definePrimitive } from "./primitive-sdk.js";
|
|
|
8
8
|
import { PrimitiveMediaLambdaService } from "./services/primitive-media-lambda.js";
|
|
9
9
|
import { estimateGhostcutCostUsd, isGhostcutConfigured, pollStatus as pollGhostcutStatus, submitSubtitleRemoval } from "./services/ghostcut.js";
|
|
10
10
|
import { buildSrtFromSegments, defaultSpeechModelFor, inferSpeechProviderForVoice, speechVoiceMismatch } from "./services/speech.js";
|
|
11
|
+
import { buildCaptionCues, cuesFromText } from "./services/captions.js";
|
|
12
|
+
import { CAPTION_ANIMATIONS, CAPTION_STYLE_PRESETS, captionStylePresetById } from "./hyperframes/composition.js";
|
|
11
13
|
import { buildHtmlImageCompositionHtml, buildMediaDedupeCompositionHtml, DEDUPE_DEFAULT_EFFECTS } from "./primitives/hyperframes-media.js";
|
|
12
14
|
const imageProviderSchema = z.enum(["openai", "gemini", "openrouter"]);
|
|
13
15
|
const videoProviderSchema = z.enum(["openai", "gemini", "openrouter"]);
|
|
@@ -406,9 +408,58 @@ const sttPayloadSchema = z.preprocess((raw) => {
|
|
|
406
408
|
source_url: mediaSourceUrlSchema,
|
|
407
409
|
media_type: z.enum(["auto", "video", "audio"]).default("auto"),
|
|
408
410
|
diarize: z.boolean().default(true),
|
|
411
|
+
// Word-level timings on each segment (animated captions). Real timestamps
|
|
412
|
+
// come from the openai path (whisper-1); other providers keep segment-level
|
|
413
|
+
// output and downstream callers estimate word windows.
|
|
414
|
+
word_timestamps: z.boolean().default(false),
|
|
409
415
|
language: z.string().min(2).max(16).optional(),
|
|
410
416
|
prompt: z.string().min(1).max(2000).optional()
|
|
411
417
|
}));
|
|
418
|
+
// Animated captions from speech: STT with word-level timings, paged into
|
|
419
|
+
// ready-to-apply cue layers. Either transcribe a source_url or page plain text.
|
|
420
|
+
const captionsPayloadSchema = z.preprocess((raw) => {
|
|
421
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
422
|
+
return raw;
|
|
423
|
+
const payload = raw;
|
|
424
|
+
return {
|
|
425
|
+
...payload,
|
|
426
|
+
source_url: payload.source_url ??
|
|
427
|
+
payload.source_video_url ??
|
|
428
|
+
payload.source_audio_url ??
|
|
429
|
+
payload.source_media_url ??
|
|
430
|
+
payload.video_url ??
|
|
431
|
+
payload.audio_url ??
|
|
432
|
+
payload.url
|
|
433
|
+
};
|
|
434
|
+
}, z.object({
|
|
435
|
+
provider: speechProviderSchema.optional(),
|
|
436
|
+
model: z.string().min(1).optional(),
|
|
437
|
+
source_url: mediaSourceUrlSchema.optional(),
|
|
438
|
+
media_type: z.enum(["auto", "video", "audio"]).default("auto"),
|
|
439
|
+
/** Skip STT entirely: page this script across the window instead. */
|
|
440
|
+
text: z.string().min(1).max(20_000).optional(),
|
|
441
|
+
language: z.string().min(2).max(16).optional(),
|
|
442
|
+
prompt: z.string().min(1).max(2000).optional(),
|
|
443
|
+
/** Named look: spotlight | karaoke | word-pop | stack-up | neon | bounce. */
|
|
444
|
+
style: z.string().min(1).max(32).optional(),
|
|
445
|
+
animation: z.enum(CAPTION_ANIMATIONS).optional(),
|
|
446
|
+
active_color: z.string().min(1).max(64).optional(),
|
|
447
|
+
highlight_color: z.string().min(1).max(64).optional(),
|
|
448
|
+
uppercase: z.boolean().optional(),
|
|
449
|
+
color: z.string().min(1).max(64).optional(),
|
|
450
|
+
background: z.string().min(1).max(64).optional(),
|
|
451
|
+
background_style: z.enum(["plain", "outline", "highlight-solid", "highlight-translucent"]).optional(),
|
|
452
|
+
font_family: z.string().min(1).max(64).optional(),
|
|
453
|
+
font_size: z.number().int().min(8).max(200).optional(),
|
|
454
|
+
max_words_per_cue: z.number().int().min(1).max(8).default(4),
|
|
455
|
+
/** Shift every cue by this many seconds (where the narration layer starts). */
|
|
456
|
+
offset_sec: z.number().min(0).max(3600).default(0),
|
|
457
|
+
/** Text mode window: where the paged script starts and how long it spans. */
|
|
458
|
+
window_start: z.number().min(0).max(3600).default(0),
|
|
459
|
+
window_duration: z.number().positive().max(3600).optional()
|
|
460
|
+
}).refine((payload) => Boolean(payload.source_url || payload.text), {
|
|
461
|
+
message: "Provide source_url (audio/video to transcribe) or text (script to page)."
|
|
462
|
+
}));
|
|
412
463
|
const probeVideoPayloadSchema = z.object({
|
|
413
464
|
source_video_url: mediaSourceUrlSchema
|
|
414
465
|
});
|
|
@@ -561,6 +612,7 @@ const PRIMITIVE_BRAINSTORM_PRODUCT_PLACEMENT_ID = "primitive:brainstorm_product_
|
|
|
561
612
|
const PRIMITIVE_MEDIA_DEDUPE_ID = "primitive:media_dedupe";
|
|
562
613
|
const PRIMITIVE_TTS_ID = "primitive:tts";
|
|
563
614
|
const PRIMITIVE_STT_ID = "primitive:stt";
|
|
615
|
+
const PRIMITIVE_CAPTIONS_ID = "primitive:captions";
|
|
564
616
|
// Gemini inline audio caps out around 20MB; at the 64kbps mono mp3 we extract,
|
|
565
617
|
// that is roughly 40 minutes of speech.
|
|
566
618
|
const MAX_TRANSCRIBE_AUDIO_BYTES = 20 * 1024 * 1024;
|
|
@@ -1686,6 +1738,55 @@ const ttsPrimitive = definePrimitive({
|
|
|
1686
1738
|
}
|
|
1687
1739
|
}
|
|
1688
1740
|
});
|
|
1741
|
+
// Shared source→speech-ready-audio loader for the STT-family primitives.
|
|
1742
|
+
// Plain audio URLs pass through; videos (and unknown containers) go through
|
|
1743
|
+
// the ffmpeg seam, which demuxes and normalizes into a small mono mp3 the
|
|
1744
|
+
// speech providers accept.
|
|
1745
|
+
async function loadSpeechAudioForPrimitive(ctx, payload) {
|
|
1746
|
+
const sourceKind = payload.media_type === "auto" ? inferSpeechSourceKind(payload.source_url) : payload.media_type;
|
|
1747
|
+
let audioBytes;
|
|
1748
|
+
let audioContentType;
|
|
1749
|
+
let extractionMetadata = null;
|
|
1750
|
+
if (sourceKind === "audio") {
|
|
1751
|
+
ctx.logger.progress(0.1, "Fetching source audio");
|
|
1752
|
+
const response = await fetch(payload.source_url);
|
|
1753
|
+
if (!response.ok) {
|
|
1754
|
+
throw new Error(`Could not fetch source audio (${response.status}).`);
|
|
1755
|
+
}
|
|
1756
|
+
audioBytes = new Uint8Array(await response.arrayBuffer());
|
|
1757
|
+
audioContentType = response.headers.get("content-type")?.split(";")[0]?.trim() || "audio/mpeg";
|
|
1758
|
+
}
|
|
1759
|
+
else {
|
|
1760
|
+
ctx.logger.progress(0.1, "Extracting audio track from source", {
|
|
1761
|
+
executionMode: config.VIDFARM_PRIMITIVE_MEDIA_LAMBDA_URL ? "lambda" : "local_fallback"
|
|
1762
|
+
});
|
|
1763
|
+
const extracted = await executePrimitiveMediaOperation(ctx, {
|
|
1764
|
+
operation: "video_extract_audio",
|
|
1765
|
+
payload: {
|
|
1766
|
+
source_video_url: payload.source_url,
|
|
1767
|
+
output_format: "mp3",
|
|
1768
|
+
audio_bitrate_kbps: 64
|
|
1769
|
+
},
|
|
1770
|
+
outputKey: "source-audio.mp3"
|
|
1771
|
+
});
|
|
1772
|
+
if (!extracted.publicUrl) {
|
|
1773
|
+
throw new Error("Audio extraction produced no readable audio URL.");
|
|
1774
|
+
}
|
|
1775
|
+
const response = await fetch(extracted.publicUrl);
|
|
1776
|
+
if (!response.ok) {
|
|
1777
|
+
throw new Error(`Could not read extracted audio (${response.status}).`);
|
|
1778
|
+
}
|
|
1779
|
+
audioBytes = new Uint8Array(await response.arrayBuffer());
|
|
1780
|
+
audioContentType = extracted.contentType || "audio/mpeg";
|
|
1781
|
+
extractionMetadata = extracted.metadata ?? null;
|
|
1782
|
+
}
|
|
1783
|
+
if (audioBytes.byteLength > MAX_TRANSCRIBE_AUDIO_BYTES) {
|
|
1784
|
+
const sizeMb = Math.round(audioBytes.byteLength / (1024 * 1024));
|
|
1785
|
+
const maxMb = Math.round(MAX_TRANSCRIBE_AUDIO_BYTES / (1024 * 1024));
|
|
1786
|
+
throw new Error(`Audio is too large to transcribe (${sizeMb}MB > ${maxMb}MB, roughly 40 minutes of speech). Trim the source with /videos/trim or /audio/trim and transcribe in parts.`);
|
|
1787
|
+
}
|
|
1788
|
+
return { audioBytes, audioContentType, extractionMetadata };
|
|
1789
|
+
}
|
|
1689
1790
|
const sttPrimitive = definePrimitive({
|
|
1690
1791
|
id: PRIMITIVE_STT_ID,
|
|
1691
1792
|
kind: "stt",
|
|
@@ -1699,53 +1800,9 @@ const sttPrimitive = definePrimitive({
|
|
|
1699
1800
|
jobs: {
|
|
1700
1801
|
async run(ctx, input) {
|
|
1701
1802
|
const payload = sttPayloadSchema.parse(input);
|
|
1702
|
-
const
|
|
1703
|
-
let audioBytes;
|
|
1704
|
-
let audioContentType;
|
|
1705
|
-
let extractionMetadata = null;
|
|
1706
|
-
if (sourceKind === "audio") {
|
|
1707
|
-
ctx.logger.progress(0.1, "Fetching source audio");
|
|
1708
|
-
const response = await fetch(payload.source_url);
|
|
1709
|
-
if (!response.ok) {
|
|
1710
|
-
throw new Error(`Could not fetch source audio (${response.status}).`);
|
|
1711
|
-
}
|
|
1712
|
-
audioBytes = new Uint8Array(await response.arrayBuffer());
|
|
1713
|
-
audioContentType = response.headers.get("content-type")?.split(";")[0]?.trim() || "audio/mpeg";
|
|
1714
|
-
}
|
|
1715
|
-
else {
|
|
1716
|
-
// Videos (and unknown extensions) go through the ffmpeg seam: it
|
|
1717
|
-
// demuxes video AND normalizes odd audio containers into a small
|
|
1718
|
-
// mono mp3 the speech providers accept.
|
|
1719
|
-
ctx.logger.progress(0.1, "Extracting audio track from source", {
|
|
1720
|
-
executionMode: config.VIDFARM_PRIMITIVE_MEDIA_LAMBDA_URL ? "lambda" : "local_fallback"
|
|
1721
|
-
});
|
|
1722
|
-
const extracted = await executePrimitiveMediaOperation(ctx, {
|
|
1723
|
-
operation: "video_extract_audio",
|
|
1724
|
-
payload: {
|
|
1725
|
-
source_video_url: payload.source_url,
|
|
1726
|
-
output_format: "mp3",
|
|
1727
|
-
audio_bitrate_kbps: 64
|
|
1728
|
-
},
|
|
1729
|
-
outputKey: "source-audio.mp3"
|
|
1730
|
-
});
|
|
1731
|
-
if (!extracted.publicUrl) {
|
|
1732
|
-
throw new Error("Audio extraction produced no readable audio URL.");
|
|
1733
|
-
}
|
|
1734
|
-
const response = await fetch(extracted.publicUrl);
|
|
1735
|
-
if (!response.ok) {
|
|
1736
|
-
throw new Error(`Could not read extracted audio (${response.status}).`);
|
|
1737
|
-
}
|
|
1738
|
-
audioBytes = new Uint8Array(await response.arrayBuffer());
|
|
1739
|
-
audioContentType = extracted.contentType || "audio/mpeg";
|
|
1740
|
-
extractionMetadata = extracted.metadata ?? null;
|
|
1741
|
-
}
|
|
1742
|
-
if (audioBytes.byteLength > MAX_TRANSCRIBE_AUDIO_BYTES) {
|
|
1743
|
-
const sizeMb = Math.round(audioBytes.byteLength / (1024 * 1024));
|
|
1744
|
-
const maxMb = Math.round(MAX_TRANSCRIBE_AUDIO_BYTES / (1024 * 1024));
|
|
1745
|
-
throw new Error(`Audio is too large to transcribe (${sizeMb}MB > ${maxMb}MB, roughly 40 minutes of speech). Trim the source with /videos/trim or /audio/trim and transcribe in parts.`);
|
|
1746
|
-
}
|
|
1803
|
+
const { audioBytes, audioContentType, extractionMetadata } = await loadSpeechAudioForPrimitive(ctx, payload);
|
|
1747
1804
|
ctx.logger.progress(0.45, "Transcribing speech");
|
|
1748
|
-
const provider = payload.provider ?? "gemini";
|
|
1805
|
+
const provider = payload.provider ?? (payload.word_timestamps ? "openai" : "gemini");
|
|
1749
1806
|
const model = payload.model ?? defaultSpeechModelFor("stt", provider) ?? "";
|
|
1750
1807
|
const transcription = await ctx.providers.transcribeSpeech({
|
|
1751
1808
|
provider,
|
|
@@ -1754,7 +1811,8 @@ const sttPrimitive = definePrimitive({
|
|
|
1754
1811
|
contentType: audioContentType,
|
|
1755
1812
|
prompt: payload.prompt,
|
|
1756
1813
|
language: payload.language,
|
|
1757
|
-
diarize: payload.diarize
|
|
1814
|
+
diarize: payload.diarize,
|
|
1815
|
+
wordTimestamps: payload.word_timestamps
|
|
1758
1816
|
});
|
|
1759
1817
|
ctx.logger.progress(0.85, "Storing transcript artifacts");
|
|
1760
1818
|
const speakers = [...new Set(transcription.segments.map((segment) => segment.speaker))];
|
|
@@ -1802,6 +1860,137 @@ const sttPrimitive = definePrimitive({
|
|
|
1802
1860
|
}
|
|
1803
1861
|
}
|
|
1804
1862
|
});
|
|
1863
|
+
// Speech → animated captions convenience primitive. One job: transcribe the
|
|
1864
|
+
// narration WITH word-level timings (openai/whisper-1 real timestamps, other
|
|
1865
|
+
// providers estimated), page it into CapCut-style cues, and return the cue
|
|
1866
|
+
// list shaped EXACTLY for the editor's `editor_action set_captions` — the
|
|
1867
|
+
// output even carries a ready `set_captions_action` object so an agent can
|
|
1868
|
+
// pipe it through without any cue math. Composition-agnostic: it never
|
|
1869
|
+
// mutates a fork; POST /api/v1/compositions/:forkId/captions is the one-step
|
|
1870
|
+
// variant that applies server-side.
|
|
1871
|
+
const captionsPrimitive = definePrimitive({
|
|
1872
|
+
id: PRIMITIVE_CAPTIONS_ID,
|
|
1873
|
+
kind: "captions",
|
|
1874
|
+
operations: {
|
|
1875
|
+
run: {
|
|
1876
|
+
description: "Transcribe narration (or page a script) into animated word-by-word caption cues ready for the editor's set_captions action.",
|
|
1877
|
+
inputSchema: captionsPayloadSchema,
|
|
1878
|
+
workflow: "run"
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1881
|
+
jobs: {
|
|
1882
|
+
async run(ctx, input) {
|
|
1883
|
+
const payload = captionsPayloadSchema.parse(input);
|
|
1884
|
+
const preset = payload.style
|
|
1885
|
+
? captionStylePresetById(payload.style)
|
|
1886
|
+
: payload.animation
|
|
1887
|
+
? null
|
|
1888
|
+
: CAPTION_STYLE_PRESETS[0];
|
|
1889
|
+
if (payload.style && !preset) {
|
|
1890
|
+
throw new Error(`Unknown caption style "${payload.style}". Use one of: ${CAPTION_STYLE_PRESETS.map((entry) => entry.id).join(", ")} — or pass animation directly.`);
|
|
1891
|
+
}
|
|
1892
|
+
const animation = payload.animation ?? preset?.animation ?? "highlight-pop";
|
|
1893
|
+
let cues;
|
|
1894
|
+
let wordTimestampSource = "estimated";
|
|
1895
|
+
let transcriptText = null;
|
|
1896
|
+
let transcriptArtifacts = {};
|
|
1897
|
+
if (payload.text?.trim()) {
|
|
1898
|
+
const tokens = payload.text.trim().split(/\s+/).filter(Boolean);
|
|
1899
|
+
const windowDuration = payload.window_duration ?? Math.max(0.4, tokens.length * 0.32);
|
|
1900
|
+
cues = cuesFromText(payload.text, payload.window_start + payload.offset_sec, windowDuration, {
|
|
1901
|
+
maxWordsPerCue: payload.max_words_per_cue
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
const { audioBytes, audioContentType } = await loadSpeechAudioForPrimitive(ctx, {
|
|
1906
|
+
source_url: payload.source_url,
|
|
1907
|
+
media_type: payload.media_type
|
|
1908
|
+
});
|
|
1909
|
+
ctx.logger.progress(0.45, "Transcribing speech with word timings");
|
|
1910
|
+
// openai first: whisper-1 is the only provider with REAL word-level
|
|
1911
|
+
// timestamps; providerAttempts falls back to the caller's other keys.
|
|
1912
|
+
const provider = payload.provider ?? "openai";
|
|
1913
|
+
const model = payload.model ?? defaultSpeechModelFor("stt", provider) ?? "";
|
|
1914
|
+
const transcription = await ctx.providers.transcribeSpeech({
|
|
1915
|
+
provider,
|
|
1916
|
+
model,
|
|
1917
|
+
audio: audioBytes,
|
|
1918
|
+
contentType: audioContentType,
|
|
1919
|
+
prompt: payload.prompt,
|
|
1920
|
+
language: payload.language,
|
|
1921
|
+
diarize: false,
|
|
1922
|
+
wordTimestamps: true
|
|
1923
|
+
});
|
|
1924
|
+
if (!transcription.segments.length) {
|
|
1925
|
+
throw new Error("Transcription returned no speech segments — the source may be silent.");
|
|
1926
|
+
}
|
|
1927
|
+
transcriptText = transcription.text || null;
|
|
1928
|
+
wordTimestampSource = transcription.segments.some((segment) => segment.words?.length) ? "provider" : "estimated";
|
|
1929
|
+
ctx.logger.progress(0.7, "Paging transcript into caption cues");
|
|
1930
|
+
cues = buildCaptionCues(transcription.segments, {
|
|
1931
|
+
maxWordsPerCue: payload.max_words_per_cue,
|
|
1932
|
+
offsetSec: payload.offset_sec
|
|
1933
|
+
});
|
|
1934
|
+
const srt = buildSrtFromSegments(transcription.segments);
|
|
1935
|
+
const storedSrt = srt ? await ctx.storage.putText("transcript.srt", srt, "application/x-subrip; charset=utf-8") : null;
|
|
1936
|
+
if (storedSrt?.url)
|
|
1937
|
+
transcriptArtifacts.srt_url = storedSrt.url;
|
|
1938
|
+
}
|
|
1939
|
+
if (!cues.length) {
|
|
1940
|
+
throw new Error("No caption cues produced from the source.");
|
|
1941
|
+
}
|
|
1942
|
+
// Style echo in the exact field names editor_action set_captions takes,
|
|
1943
|
+
// so the agent can spread this straight into the action.
|
|
1944
|
+
const styleFields = {
|
|
1945
|
+
...(preset ? { caption_style: preset.id } : {}),
|
|
1946
|
+
caption_animation: animation,
|
|
1947
|
+
...(payload.active_color ?? preset?.activeColor ? { caption_active_color: payload.active_color ?? preset?.activeColor } : {}),
|
|
1948
|
+
...(payload.highlight_color ?? preset?.highlightColor ? { caption_highlight_color: payload.highlight_color ?? preset?.highlightColor } : {}),
|
|
1949
|
+
...((payload.uppercase ?? preset?.uppercase) !== undefined ? { caption_uppercase: payload.uppercase ?? preset?.uppercase } : {}),
|
|
1950
|
+
...(payload.color ?? preset?.color ? { color: payload.color ?? preset?.color } : {}),
|
|
1951
|
+
...(payload.background ?? preset?.background ? { background: payload.background ?? preset?.background } : {}),
|
|
1952
|
+
...(payload.background_style ?? preset?.textBackgroundStyle ? { background_style: payload.background_style ?? preset?.textBackgroundStyle } : {}),
|
|
1953
|
+
...(payload.font_family ? { font_family: payload.font_family } : {}),
|
|
1954
|
+
...(payload.font_size ? { font_size: payload.font_size } : {})
|
|
1955
|
+
};
|
|
1956
|
+
const setCaptionsAction = {
|
|
1957
|
+
action_type: "set_captions",
|
|
1958
|
+
captions: cues,
|
|
1959
|
+
...styleFields,
|
|
1960
|
+
explanation: "Apply transcribed animated captions."
|
|
1961
|
+
};
|
|
1962
|
+
ctx.logger.progress(0.85, "Storing caption artifacts");
|
|
1963
|
+
const storedCaptions = await ctx.storage.putJson("captions.json", {
|
|
1964
|
+
source_url: payload.source_url ?? null,
|
|
1965
|
+
word_timestamps: wordTimestampSource,
|
|
1966
|
+
style: styleFields,
|
|
1967
|
+
cue_count: cues.length,
|
|
1968
|
+
cues,
|
|
1969
|
+
set_captions_action: setCaptionsAction,
|
|
1970
|
+
...(transcriptText ? { transcript_text: transcriptText } : {})
|
|
1971
|
+
});
|
|
1972
|
+
const cuesInline = JSON.stringify(cues).length <= 60_000;
|
|
1973
|
+
ctx.logger.progress(1, "Caption cues ready");
|
|
1974
|
+
return {
|
|
1975
|
+
progress: 1,
|
|
1976
|
+
output: {
|
|
1977
|
+
files: compactUrls([storedCaptions.url, transcriptArtifacts.srt_url]),
|
|
1978
|
+
primary_file_url: storedCaptions.url,
|
|
1979
|
+
cue_count: cues.length,
|
|
1980
|
+
word_timestamps: wordTimestampSource,
|
|
1981
|
+
...styleFields,
|
|
1982
|
+
// The cue list, ready to pass as editor_action set_captions `captions`.
|
|
1983
|
+
captions: cuesInline ? cues : undefined,
|
|
1984
|
+
captions_truncated: !cuesInline,
|
|
1985
|
+
// Fully-formed editor_action arguments — apply verbatim.
|
|
1986
|
+
set_captions_action: cuesInline ? setCaptionsAction : undefined,
|
|
1987
|
+
captions_json_url: storedCaptions.url,
|
|
1988
|
+
transcript: transcriptArtifacts.srt_url ? { srt_url: transcriptArtifacts.srt_url } : undefined
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1805
1994
|
const videoProbePrimitive = definePrimitive({
|
|
1806
1995
|
id: PRIMITIVE_VIDEO_PROBE_ID,
|
|
1807
1996
|
kind: "video_probe",
|
|
@@ -2380,6 +2569,7 @@ class PrimitiveRegistry {
|
|
|
2380
2569
|
[audioNormalizePrimitive.id, audioNormalizePrimitive],
|
|
2381
2570
|
[ttsPrimitive.id, ttsPrimitive],
|
|
2382
2571
|
[sttPrimitive.id, sttPrimitive],
|
|
2572
|
+
[captionsPrimitive.id, captionsPrimitive],
|
|
2383
2573
|
[hyperframesRenderPrimitive.id, hyperframesRenderPrimitive],
|
|
2384
2574
|
[brainstormHooksPrimitive.id, brainstormHooksPrimitive],
|
|
2385
2575
|
[brainstormAwarenessStagesPrimitive.id, brainstormAwarenessStagesPrimitive],
|