@omelhorsite/video-sdk 0.2.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/dist/index.js ADDED
@@ -0,0 +1,1197 @@
1
+ // src/types.ts
2
+ var RESOLUTION_PRESETS = [
3
+ { id: "1080x1920", label: "9:16 (1080x1920)", width: 1080, height: 1920 },
4
+ { id: "1920x1080", label: "16:9 (1920x1080)", width: 1920, height: 1080 },
5
+ { id: "1080x1080", label: "1:1 (1080x1080)", width: 1080, height: 1080 },
6
+ { id: "720x1280", label: "9:16 (720x1280)", width: 720, height: 1280 },
7
+ { id: "1280x720", label: "16:9 (1280x720)", width: 1280, height: 720 }
8
+ ];
9
+ var projectDuration = (project) => {
10
+ let end = 0;
11
+ for (const track of project.tracks)
12
+ for (const clip of track.clips)
13
+ end = Math.max(end, clip.start + clip.duration);
14
+ return end;
15
+ };
16
+ var findAsset = (project, id) => project.assets.find((a) => a.id === id);
17
+ var emptyProject = (width = 1080, height = 1920, fps = 30) => ({
18
+ id: crypto.randomUUID().slice(0, 8),
19
+ name: "Sem titulo",
20
+ width,
21
+ height,
22
+ fps,
23
+ background: "#000000",
24
+ tracks: [
25
+ { id: "t-video", kind: "video", name: "Video 1", clips: [] },
26
+ { id: "t-music", kind: "audio", name: "Musica", clips: [] },
27
+ { id: "t-text", kind: "text", name: "Texto", clips: [] }
28
+ ],
29
+ assets: []
30
+ });
31
+ // src/placement.ts
32
+ var computePlacement = (assetWidth, assetHeight, project, transform) => {
33
+ const fit = Math.min(project.width / assetWidth, project.height / assetHeight);
34
+ const width = Math.max(2, Math.round(assetWidth * fit * transform.scale));
35
+ const height = Math.max(2, Math.round(assetHeight * fit * transform.scale));
36
+ return {
37
+ x: Math.round(transform.x * project.width - width / 2),
38
+ y: Math.round(transform.y * project.height - height / 2),
39
+ width,
40
+ height
41
+ };
42
+ };
43
+ var fadeFactor = (t, clip) => {
44
+ const rel = t - clip.start;
45
+ if (rel < 0 || rel > clip.duration)
46
+ return 0;
47
+ const fin = clip.fadeIn > 0 ? Math.min(1, Math.max(0, rel / clip.fadeIn)) : 1;
48
+ const fout = clip.fadeOut > 0 ? Math.min(1, Math.max(0, (clip.duration - rel) / clip.fadeOut)) : 1;
49
+ return fin * fout;
50
+ };
51
+ // src/fx.ts
52
+ var FLASH_STRIPS = 96;
53
+ var FLASH_WAVE_SPEED = 1.5;
54
+ var PEAK_AT = 0.66;
55
+ var RISE_SHAPE = 0.7;
56
+ var FALL_SHAPE = 0.99;
57
+ var HOLD_GAIN = 1.014;
58
+ var SPREAD = 0.24;
59
+ var REACH = 0.71;
60
+ var WARM_AT = 0.91;
61
+ var HOT_AT = 0.63;
62
+ var FLASH_CUT_AT = 0.476;
63
+ var DEFAULT_FLASH = {
64
+ intensity: 0.85,
65
+ punch: 0.06,
66
+ wave: 0.02,
67
+ freq: 3.2,
68
+ color: "#ffffff",
69
+ warm: "#f8f399",
70
+ focusX: 0.34,
71
+ focusY: 0.72
72
+ };
73
+ var DEFAULT_FLASH_DURATION = 0.25;
74
+ var smoothstep = (x) => {
75
+ const c = x < 0 ? 0 : x > 1 ? 1 : x;
76
+ return c * c * (3 - 2 * c);
77
+ };
78
+ var flashEnvelope = (p) => {
79
+ if (p <= 0 || p >= 1)
80
+ return 0;
81
+ const ramp = p < PEAK_AT ? Math.pow(p / PEAK_AT, RISE_SHAPE) : Math.pow((1 - p) / (1 - PEAK_AT), FALL_SHAPE);
82
+ return Math.min(1, smoothstep(ramp) * HOLD_GAIN);
83
+ };
84
+ var flashStateAt = (t, clip) => {
85
+ if (clip.duration <= 0)
86
+ return null;
87
+ const progress = (t - clip.start) / clip.duration;
88
+ if (progress < 0 || progress > 1)
89
+ return null;
90
+ const envelope = flashEnvelope(progress);
91
+ return {
92
+ progress,
93
+ envelope,
94
+ zoom: 1 + clip.punch * envelope,
95
+ amp: clip.wave * envelope,
96
+ phase: progress * 2 * Math.PI * FLASH_WAVE_SPEED
97
+ };
98
+ };
99
+ var flashGlowAtRadius = (state, clip, r) => {
100
+ const g = Math.min(1, Math.max(0, state.envelope * (1 + SPREAD) - SPREAD * r / REACH));
101
+ return {
102
+ alpha: clip.intensity * smoothstep(g),
103
+ hot: smoothstep((g - WARM_AT) / (1 - WARM_AT)) * smoothstep((state.envelope - HOT_AT) / (1 - HOT_AT))
104
+ };
105
+ };
106
+ var flashGlowAt = (state, clip, xNormalized, yNormalized, aspect) => flashGlowAtRadius(state, clip, Math.hypot((xNormalized - clip.focusX) * aspect, yNormalized - clip.focusY));
107
+ var flashGlowReach = (state) => state.envelope <= 0 ? 0 : state.envelope * (1 + SPREAD) * REACH / SPREAD;
108
+ var SHADOW_OPACITY = 0.5;
109
+ var SHADOW_DROP = 0.45;
110
+ var shadowMargin = (sigmaPixels) => Math.ceil(sigmaPixels * 3 + sigmaPixels * SHADOW_DROP);
111
+ var flashBandOffset = (state, freq, band) => state.amp * Math.sin(band * 2 * Math.PI * freq + state.phase);
112
+ var flashBandOf = (yNormalized) => Math.floor(yNormalized * FLASH_STRIPS) / FLASH_STRIPS;
113
+ var makeFlashClip = (id, cutAt, overrides = {}) => {
114
+ const duration = overrides.duration ?? DEFAULT_FLASH_DURATION;
115
+ return {
116
+ id,
117
+ kind: "fx",
118
+ fx: "flash",
119
+ start: Math.max(0, cutAt - FLASH_CUT_AT * duration),
120
+ duration,
121
+ fadeIn: 0,
122
+ fadeOut: 0,
123
+ ...DEFAULT_FLASH,
124
+ ...overrides
125
+ };
126
+ };
127
+ var flashCutTime = (clip) => clip.start + FLASH_CUT_AT * clip.duration;
128
+ var hexToYuv = (hex) => {
129
+ const h = hex.replace(/^#/, "");
130
+ const r = parseInt(h.slice(0, 2), 16) || 0;
131
+ const g = parseInt(h.slice(2, 4), 16) || 0;
132
+ const b = parseInt(h.slice(4, 6), 16) || 0;
133
+ return [
134
+ 0.299 * r + 0.587 * g + 0.114 * b,
135
+ 128 - 0.168736 * r - 0.331264 * g + 0.5 * b,
136
+ 128 + 0.5 * r - 0.418688 * g - 0.081312 * b
137
+ ];
138
+ };
139
+ var n = (x) => {
140
+ const s = x.toFixed(6);
141
+ return s.replace(/\.?0+$/, "") || "0";
142
+ };
143
+ var flashGeqExpr = (clip, warmValue, coldValue) => {
144
+ const sm = (reg) => `ld(${reg})*ld(${reg})*(3-2*ld(${reg}))`;
145
+ return [
146
+ `st(1, clip((T-${n(clip.start)})/${n(clip.duration)}, 0, 1))`,
147
+ `st(2, clip(if(lt(ld(1),${n(PEAK_AT)}), pow(ld(1)/${n(PEAK_AT)},${n(RISE_SHAPE)}), pow((1-ld(1))/${n(1 - PEAK_AT)},${n(FALL_SHAPE)})), 0, 1))`,
148
+ `st(2, clip(${sm(2)}*${n(HOLD_GAIN)}, 0, 1))`,
149
+ `st(3, 1+${n(clip.punch)}*ld(2))`,
150
+ `st(6, ${n(clip.wave)}*W*ld(2)*sin(floor(Y/H*${FLASH_STRIPS})/${FLASH_STRIPS}` + `*2*PI*${n(clip.freq)} + ld(1)*2*PI*${n(FLASH_WAVE_SPEED)}))`,
151
+ `st(7, hypot((X/W-${n(clip.focusX)})*(W/H), Y/H-${n(clip.focusY)}))`,
152
+ `st(8, clip(ld(2)*${n(1 + SPREAD)} - ${n(SPREAD / REACH)}*ld(7), 0, 1))`,
153
+ `st(5, ${n(clip.intensity)}*(${sm(8)}))`,
154
+ `st(4, clip((ld(2)-${n(HOT_AT)})/${n(1 - HOT_AT)}, 0, 1))`,
155
+ `st(9, clip((ld(8)-${n(WARM_AT)})/${n(1 - WARM_AT)}, 0, 1))`,
156
+ `st(9, (${sm(9)})*(${sm(4)}))`,
157
+ `p((X-W/2)/ld(3)+W/2+ld(6), (Y-H/2)/ld(3)+H/2)*(1-ld(5))` + ` + (${n(warmValue)} + ${n(coldValue - warmValue)}*ld(9))*ld(5)`
158
+ ].join("; ");
159
+ };
160
+
161
+ // src/compile.ts
162
+ var fmt = (n2) => {
163
+ const s = n2.toFixed(4);
164
+ return s.replace(/\.?0+$/, "") || "0";
165
+ };
166
+ var BLUR_PASSES = 2;
167
+ var blurSteps = (sigma, enable) => {
168
+ const r = Math.max(1, Math.round((-1 + Math.sqrt(1 + 12 * sigma * sigma / BLUR_PASSES)) / 2));
169
+ return Array.from({ length: BLUR_PASSES }, () => `avgblur=sizeX=${r}:sizeY=${r}:${enable}`).join(",");
170
+ };
171
+ var hexColor = (color) => `0x${color.replace(/[^0-9a-fA-F]/g, "").padStart(6, "0")}`;
172
+ var compile = (project, options) => {
173
+ const duration = projectDuration(project);
174
+ if (duration <= 0)
175
+ throw new Error("timeline vazia: nada para exportar");
176
+ const inputs = [];
177
+ let inputCount = 0;
178
+ const visuals = [];
179
+ const audios = [];
180
+ const texts = [];
181
+ const effects = [];
182
+ const videoTracks = project.tracks.filter((t) => t.kind === "video" && !t.muted);
183
+ const audioTracks = project.tracks.filter((t) => t.kind === "audio" && !t.muted);
184
+ const textTracks = project.tracks.filter((t) => t.kind === "text" && !t.muted);
185
+ for (const track of videoTracks) {
186
+ for (const clip of [...track.clips].sort((a, b) => a.start - b.start)) {
187
+ if (clip.kind === "fx") {
188
+ effects.push(clip);
189
+ continue;
190
+ }
191
+ if (clip.kind !== "video" && clip.kind !== "image")
192
+ continue;
193
+ const asset = findAsset(project, clip.assetId);
194
+ if (!asset)
195
+ throw new Error(`asset em falta: ${clip.assetId}`);
196
+ if (!asset.width || !asset.height)
197
+ throw new Error(`asset visual sem dimensoes: ${asset.name}`);
198
+ if (clip.kind === "image") {
199
+ inputs.push("-loop", "1", "-t", fmt(clip.duration), "-i", asset.path);
200
+ } else {
201
+ inputs.push("-ss", fmt(clip.in), "-t", fmt(clip.duration), "-i", asset.path);
202
+ }
203
+ const inputIndex = inputCount++;
204
+ visuals.push({ clip, inputIndex, assetWidth: asset.width, assetHeight: asset.height });
205
+ if (clip.kind === "video" && !clip.muted && clip.volume > 0) {
206
+ audios.push({
207
+ start: clip.start,
208
+ duration: clip.duration,
209
+ fadeIn: clip.fadeIn,
210
+ fadeOut: clip.fadeOut,
211
+ volume: clip.volume,
212
+ inputIndex
213
+ });
214
+ }
215
+ }
216
+ }
217
+ for (const track of audioTracks) {
218
+ for (const clip of [...track.clips].sort((a, b) => a.start - b.start)) {
219
+ if (clip.kind !== "audio")
220
+ continue;
221
+ const asset = findAsset(project, clip.assetId);
222
+ if (!asset)
223
+ throw new Error(`asset em falta: ${clip.assetId}`);
224
+ inputs.push("-ss", fmt(clip.in), "-t", fmt(clip.duration), "-i", asset.path);
225
+ audios.push({
226
+ start: clip.start,
227
+ duration: clip.duration,
228
+ fadeIn: clip.fadeIn,
229
+ fadeOut: clip.fadeOut,
230
+ volume: clip.volume,
231
+ inputIndex: inputCount++
232
+ });
233
+ }
234
+ }
235
+ for (const fx of effects) {
236
+ if (!fx.soundAssetId)
237
+ continue;
238
+ const asset = findAsset(project, fx.soundAssetId);
239
+ if (!asset)
240
+ throw new Error(`asset em falta no flash ${fx.id}: ${fx.soundAssetId}`);
241
+ const cut = flashCutTime(fx);
242
+ const soundDuration = Math.min(asset.duration ?? fx.duration, duration - cut);
243
+ if (soundDuration <= 0)
244
+ continue;
245
+ inputs.push("-t", fmt(soundDuration), "-i", asset.path);
246
+ audios.push({
247
+ start: cut,
248
+ duration: soundDuration,
249
+ fadeIn: 0,
250
+ fadeOut: 0,
251
+ volume: fx.soundVolume ?? 1,
252
+ inputIndex: inputCount++
253
+ });
254
+ }
255
+ for (const track of textTracks) {
256
+ for (const clip of [...track.clips].sort((a, b) => a.start - b.start)) {
257
+ if (clip.kind === "text" && clip.text.trim())
258
+ texts.push(clip);
259
+ }
260
+ }
261
+ const graph = [];
262
+ graph.push(`color=c=${hexColor(project.background)}:s=${project.width}x${project.height}` + `:r=${fmt(project.fps)}:d=${fmt(duration)}[bg]`);
263
+ let current = "bg";
264
+ let chainId = 0;
265
+ for (const { clip, inputIndex, assetWidth, assetHeight } of visuals) {
266
+ const place = computePlacement(assetWidth, assetHeight, project, clip.transform);
267
+ const steps = [
268
+ `fps=${fmt(project.fps)}`,
269
+ `scale=${place.width}:${place.height}:flags=${options.scaleFlags ?? "lanczos"}`,
270
+ "setsar=1",
271
+ "format=rgba"
272
+ ];
273
+ if (clip.transform.opacity < 1)
274
+ steps.push(`colorchannelmixer=aa=${fmt(clip.transform.opacity)}`);
275
+ if (clip.fadeIn > 0)
276
+ steps.push(`fade=t=in:st=0:d=${fmt(clip.fadeIn)}:alpha=1`);
277
+ if (clip.fadeOut > 0)
278
+ steps.push(`fade=t=out:st=${fmt(Math.max(0, clip.duration - clip.fadeOut))}:d=${fmt(clip.fadeOut)}:alpha=1`);
279
+ steps.push(`setpts=PTS-STARTPTS+${fmt(clip.start)}/TB`);
280
+ if (clip.blurBehind && clip.blurBehind > 0) {
281
+ const sigma = clip.blurBehind * project.width;
282
+ const blurred = `b${chainId}`;
283
+ const gate = `enable='between(t,${fmt(clip.start)},${fmt(clip.start + clip.duration)})'`;
284
+ graph.push(`[${current}]${blurSteps(sigma, gate)}[${blurred}]`);
285
+ current = blurred;
286
+ }
287
+ const label = `v${chainId}`;
288
+ const out = `c${chainId}`;
289
+ const window = `enable='between(t,${fmt(clip.start)},${fmt(clip.start + clip.duration)})'`;
290
+ graph.push(`[${inputIndex}:v]${steps.join(",")}[${label}]`);
291
+ let over = label;
292
+ if (clip.shadow && clip.shadow > 0) {
293
+ const sigma = clip.shadow * project.width;
294
+ const margin = shadowMargin(sigma);
295
+ const shape = `s${chainId}`;
296
+ const shadow = `sh${chainId}`;
297
+ const lifted = `l${chainId}`;
298
+ graph.push(`[${label}]split[${over = `k${chainId}`}][${shape}]`);
299
+ graph.push(`[${shape}]pad=iw+${margin * 2}:ih+${margin * 2}:${margin}:${margin}:color=#00000000,` + `colorchannelmixer=rr=0:rg=0:rb=0:ra=0:gr=0:gg=0:gb=0:ga=0:` + `br=0:bg=0:bb=0:ba=0:ar=0:ag=0:ab=0:aa=${fmt(SHADOW_OPACITY)},` + `${blurSteps(sigma, "enable=1")}[${shadow}]`);
300
+ graph.push(`[${current}][${shadow}]overlay=x=${place.x - margin}` + `:y=${place.y - margin + Math.round(sigma * SHADOW_DROP)}:${window}[${lifted}]`);
301
+ current = lifted;
302
+ }
303
+ graph.push(`[${current}][${over}]overlay=x=${place.x}:y=${place.y}:${window}[${out}]`);
304
+ current = out;
305
+ chainId++;
306
+ }
307
+ for (const clip of texts) {
308
+ const raster = options.textImages?.[clip.id];
309
+ if (!raster)
310
+ throw new Error(`clip de texto sem raster (textImages['${clip.id}']): "${clip.text}"`);
311
+ inputs.push("-loop", "1", "-t", fmt(clip.duration), "-i", raster.path);
312
+ const inputIndex = inputCount++;
313
+ const x = Math.round(clip.x * project.width - raster.width / 2);
314
+ const y = Math.round(clip.y * project.height - raster.height / 2);
315
+ const steps = [`fps=${fmt(project.fps)}`, "setsar=1", "format=rgba"];
316
+ if (clip.fadeIn > 0)
317
+ steps.push(`fade=t=in:st=0:d=${fmt(clip.fadeIn)}:alpha=1`);
318
+ if (clip.fadeOut > 0)
319
+ steps.push(`fade=t=out:st=${fmt(Math.max(0, clip.duration - clip.fadeOut))}:d=${fmt(clip.fadeOut)}:alpha=1`);
320
+ steps.push(`setpts=PTS-STARTPTS+${fmt(clip.start)}/TB`);
321
+ const label = `v${chainId}`;
322
+ const out = `c${chainId}`;
323
+ graph.push(`[${inputIndex}:v]${steps.join(",")}[${label}]`);
324
+ graph.push(`[${current}][${label}]overlay=x=${x}:y=${y}` + `:enable='between(t,${fmt(clip.start)},${fmt(clip.start + clip.duration)})'[${out}]`);
325
+ current = out;
326
+ chainId++;
327
+ }
328
+ if (options.captionOverlay) {
329
+ const cap = options.captionOverlay;
330
+ inputs.push("-i", cap.path);
331
+ const inputIndex = inputCount++;
332
+ const label = `v${chainId}`;
333
+ const out = `c${chainId}`;
334
+ graph.push(`[${inputIndex}:v]fps=${fmt(project.fps)},setsar=1,format=rgba[${label}]`);
335
+ graph.push(`[${current}][${label}]overlay=x=${cap.x}:y=${cap.y}:eof_action=pass[${out}]`);
336
+ current = out;
337
+ chainId++;
338
+ }
339
+ if (effects.length) {
340
+ graph.push(`[${current}]format=yuv420p[fxin]`);
341
+ current = "fxin";
342
+ for (const fx of effects.sort((a, b) => a.start - b.start)) {
343
+ const cold = hexToYuv(fx.color);
344
+ const warm = hexToYuv(fx.warm);
345
+ const out = `fx${chainId}`;
346
+ graph.push(`[${current}]geq=lum='${flashGeqExpr(fx, warm[0], cold[0])}'` + `:cb='${flashGeqExpr(fx, warm[1], cold[1])}'` + `:cr='${flashGeqExpr(fx, warm[2], cold[2])}'` + `:enable='between(t,${fmt(fx.start)},${fmt(fx.start + fx.duration)})'[${out}]`);
347
+ current = out;
348
+ chainId++;
349
+ }
350
+ }
351
+ graph.push(`[${current}]format=yuv420p,setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709[vout]`);
352
+ const audioLabels = [];
353
+ audios.forEach((entry, i) => {
354
+ const steps = ["asetpts=PTS-STARTPTS"];
355
+ if (entry.volume !== 1)
356
+ steps.push(`volume=${fmt(entry.volume)}`);
357
+ if (entry.fadeIn > 0)
358
+ steps.push(`afade=t=in:st=0:d=${fmt(entry.fadeIn)}`);
359
+ if (entry.fadeOut > 0)
360
+ steps.push(`afade=t=out:st=${fmt(Math.max(0, entry.duration - entry.fadeOut))}:d=${fmt(entry.fadeOut)}`);
361
+ const delayMs = Math.round(entry.start * 1000);
362
+ if (delayMs > 0)
363
+ steps.push(`adelay=${delayMs}:all=1`);
364
+ const label = `a${i}`;
365
+ graph.push(`[${entry.inputIndex}:a]${steps.join(",")}[${label}]`);
366
+ audioLabels.push(label);
367
+ });
368
+ let audioOut = null;
369
+ if (audioLabels.length === 1) {
370
+ audioOut = audioLabels[0];
371
+ } else if (audioLabels.length > 1) {
372
+ graph.push(`${audioLabels.map((l) => `[${l}]`).join("")}amix=inputs=${audioLabels.length}` + `:duration=longest:normalize=0[aout]`);
373
+ audioOut = "aout";
374
+ }
375
+ const args = [
376
+ "-y",
377
+ ...inputs,
378
+ "-filter_complex",
379
+ graph.join(";"),
380
+ "-map",
381
+ "[vout]"
382
+ ];
383
+ if (audioOut)
384
+ args.push("-map", `[${audioOut}]`);
385
+ args.push("-r", fmt(project.fps), "-t", fmt(duration));
386
+ if (options.videoCodec === "videotoolbox") {
387
+ args.push("-c:v", "h264_videotoolbox", "-b:v", options.bitrate ?? "14000k", "-tag:v", "avc1", "-pix_fmt", "yuv420p");
388
+ } else {
389
+ args.push("-c:v", "libx264", "-preset", options.preset ?? "slow", "-crf", String(options.crf ?? 18), "-pix_fmt", "yuv420p");
390
+ }
391
+ args.push("-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709");
392
+ if (audioOut)
393
+ args.push("-c:a", "aac", "-b:a", options.audioBitrate ?? "192k");
394
+ args.push("-movflags", "+faststart", options.output);
395
+ return { args, duration };
396
+ };
397
+ // src/packfile.ts
398
+ import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
399
+ var OMSV_SCHEMA = 1;
400
+ var isZipData = (bytes) => bytes.length > 1 && bytes[0] === 80 && bytes[1] === 75;
401
+ var sanitize = (name) => name.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 80);
402
+ var assetEntryName = (asset) => `assets/${asset.id}-${sanitize(asset.name || "media")}`;
403
+ var packProject = ({ project, assetBytes, appVersion, nowIso }) => {
404
+ const entries = {};
405
+ const relProject = {
406
+ ...project,
407
+ assets: project.assets.map((a) => assetBytes.has(a.id) ? { ...a, path: assetEntryName(a) } : a)
408
+ };
409
+ const manifest = {
410
+ format: "omsv",
411
+ schema: OMSV_SCHEMA,
412
+ savedAt: nowIso,
413
+ appVersion
414
+ };
415
+ entries["manifest.json"] = [strToU8(JSON.stringify(manifest, null, 2)), { level: 6 }];
416
+ entries["timeline.json"] = [strToU8(JSON.stringify(relProject, null, 2)), { level: 6 }];
417
+ for (const asset of relProject.assets) {
418
+ const bytes = assetBytes.get(asset.id);
419
+ if (bytes)
420
+ entries[asset.path] = [bytes, { level: 0 }];
421
+ }
422
+ return zipSync(entries);
423
+ };
424
+ var unpackProject = (bytes) => {
425
+ const files = unzipSync(bytes);
426
+ const timelineRaw = files["timeline.json"];
427
+ if (!timelineRaw)
428
+ throw new Error("ficheiro .omsv sem timeline.json");
429
+ const manifest = files["manifest.json"] ? JSON.parse(strFromU8(files["manifest.json"])) : { format: "omsv", schema: 0 };
430
+ if (manifest.schema > OMSV_SCHEMA)
431
+ throw new Error(`este projecto e de um schema mais recente (${manifest.schema} > ${OMSV_SCHEMA}); actualiza o software`);
432
+ const project = JSON.parse(strFromU8(timelineRaw));
433
+ const assetBytes = new Map;
434
+ for (const [name, data] of Object.entries(files))
435
+ if (name.startsWith("assets/") && !name.endsWith("/"))
436
+ assetBytes.set(name, data);
437
+ return { manifest, project, assetBytes };
438
+ };
439
+ // src/captions.ts
440
+ var DEFAULT_CAPTION_STYLE = {
441
+ fontScale: 0.05,
442
+ strokeScale: 0.13,
443
+ pos: 0.78,
444
+ maxWords: 3,
445
+ maxChars: 20,
446
+ gap: 0.4,
447
+ tail: 0.12,
448
+ color: "#ffffff",
449
+ highlight: "#ffd600",
450
+ stroke: "#000000",
451
+ maxWidth: 0.9,
452
+ fontFamily: "Montserrat, Helvetica, Arial, sans-serif"
453
+ };
454
+ var SENT_END = ".?!:;,";
455
+ var groupWords = (words, style) => {
456
+ const chunks = [];
457
+ let cur = [];
458
+ let chars = 0;
459
+ for (const w of words) {
460
+ const prev = cur[cur.length - 1];
461
+ const g = prev ? w.t0 - prev.t1 : 0;
462
+ const would = chars + w.text.length + (cur.length ? 1 : 0);
463
+ const brk = cur.length > 0 && (cur.length >= style.maxWords || would > style.maxChars || g > style.gap || prev !== undefined && SENT_END.includes(prev.text.slice(-1)));
464
+ if (brk) {
465
+ chunks.push(cur);
466
+ cur = [];
467
+ chars = 0;
468
+ }
469
+ cur.push(w);
470
+ chars += w.text.length + (cur.length > 1 ? 1 : 0);
471
+ }
472
+ if (cur.length)
473
+ chunks.push(cur);
474
+ return chunks;
475
+ };
476
+ var captionStates = (chunks, tail) => {
477
+ const states = [];
478
+ chunks.forEach((words, ci) => {
479
+ const next = ci + 1 < chunks.length ? chunks[ci + 1][0].t0 : undefined;
480
+ words.forEach((w, j) => {
481
+ const t0 = w.t0;
482
+ let t1;
483
+ if (j + 1 < words.length)
484
+ t1 = words[j + 1].t0;
485
+ else {
486
+ t1 = words[words.length - 1].t1 + tail;
487
+ if (next !== undefined)
488
+ t1 = Math.min(t1, next);
489
+ }
490
+ states.push({ words, active: j, t0, t1: Math.max(t1, t0) });
491
+ });
492
+ });
493
+ return states;
494
+ };
495
+ var sourceSpans = (project, assetId) => {
496
+ const spans = [];
497
+ for (const track of project.tracks) {
498
+ if (track.kind !== "video" || track.muted)
499
+ continue;
500
+ for (const clip of track.clips) {
501
+ if (clip.kind !== "video")
502
+ continue;
503
+ const v = clip;
504
+ if (v.assetId !== assetId)
505
+ continue;
506
+ spans.push({ in: v.in, out: v.in + v.duration, start: v.start });
507
+ }
508
+ }
509
+ return spans.sort((a, b) => a.start - b.start);
510
+ };
511
+ var BORDA = 0.12;
512
+ var remapWords = (words, spans, borda = BORDA) => {
513
+ const out = [];
514
+ for (const w of words) {
515
+ let best;
516
+ for (const span2 of spans) {
517
+ const overlap = Math.min(w.t1, span2.out + borda) - Math.max(w.t0, span2.in - borda);
518
+ if (overlap <= 0)
519
+ continue;
520
+ if (!best || overlap > best.overlap)
521
+ best = { span: span2, overlap };
522
+ }
523
+ if (!best)
524
+ continue;
525
+ const { span } = best;
526
+ const t0 = Math.min(Math.max(w.t0, span.in), span.out) - span.in + span.start;
527
+ const t1 = Math.min(Math.max(w.t1, span.in), span.out) - span.in + span.start;
528
+ if (t1 > t0)
529
+ out.push({ t0, t1, text: w.text });
530
+ }
531
+ return out.sort((a, b) => a.t0 - b.t0);
532
+ };
533
+ var timelineWords = (project) => {
534
+ const all = [];
535
+ for (const src of project.captions ?? [])
536
+ all.push(...remapWords(src.words, sourceSpans(project, src.assetId)));
537
+ return all.sort((a, b) => a.t0 - b.t0);
538
+ };
539
+ // src/slice.ts
540
+ var temSource = (c) => c.kind === "video" || c.kind === "image" || c.kind === "audio";
541
+ var sliceClip = (clip, from, to) => {
542
+ const end = clip.start + clip.duration;
543
+ if (end <= from || clip.start >= to)
544
+ return;
545
+ const cutFront = Math.max(0, from - clip.start);
546
+ const cutBack = Math.max(0, end - to);
547
+ const duration = clip.duration - cutFront - cutBack;
548
+ if (duration <= 0)
549
+ return;
550
+ const out = {
551
+ ...clip,
552
+ start: Math.max(0, clip.start - from),
553
+ duration,
554
+ fadeIn: cutFront > 0 ? Math.max(0, clip.fadeIn - cutFront) : clip.fadeIn,
555
+ fadeOut: cutBack > 0 ? Math.max(0, clip.fadeOut - cutBack) : clip.fadeOut
556
+ };
557
+ if (cutFront > 0 && temSource(out) && out.kind !== "image")
558
+ out.in = clip.in + cutFront;
559
+ return out;
560
+ };
561
+ var sliceProject = (project, from, to) => {
562
+ if (!(to > from))
563
+ throw new Error(`janela invalida: ${from}..${to}`);
564
+ return {
565
+ ...project,
566
+ tracks: project.tracks.map((track) => ({
567
+ ...track,
568
+ clips: track.clips.map((c) => sliceClip(c, from, to)).filter((c) => c !== undefined)
569
+ }))
570
+ };
571
+ };
572
+ // src/cloud/cloud.ts
573
+ import {
574
+ CAPTION_CHUNKED_THRESHOLD,
575
+ OAuthTokenProvider,
576
+ Oms,
577
+ OmsApiError,
578
+ OmsAuthError,
579
+ OmsDeviceDeniedError,
580
+ OmsDeviceExpiredError,
581
+ OmsError,
582
+ OmsNetworkError,
583
+ OmsQuotaError,
584
+ OmsTimeoutError,
585
+ captionUploadSize,
586
+ collect,
587
+ decodeIdToken,
588
+ fetchToolArtifact,
589
+ file,
590
+ readInsufficientScope,
591
+ scopesOf
592
+ } from "@omelhorsite/sdk";
593
+
594
+ // src/cloud/srt.ts
595
+ var TIME = /(\d+):(\d\d):(\d\d)[,.](\d{1,3})/;
596
+ var parseTime = (raw) => {
597
+ const m = TIME.exec(raw);
598
+ if (!m)
599
+ return;
600
+ const ms = m[4].padEnd(3, "0");
601
+ return Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]) + Number(ms) / 1000;
602
+ };
603
+ var parseSrt = (text) => {
604
+ const cues = [];
605
+ const blocks = text.replace(/\r/g, "").replace(/^WEBVTT[^\n]*\n/, "").split(/\n\s*\n/);
606
+ for (const block of blocks) {
607
+ const lines = block.split(`
608
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
609
+ const arrowAt = lines.findIndex((l) => l.includes("-->"));
610
+ if (arrowAt < 0)
611
+ continue;
612
+ const [a, b] = lines[arrowAt].split("-->");
613
+ const t0 = parseTime(a ?? "");
614
+ const t1 = parseTime(b ?? "");
615
+ if (t0 === undefined || t1 === undefined)
616
+ continue;
617
+ const body = lines.slice(arrowAt + 1).join(" ").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
618
+ if (!body)
619
+ continue;
620
+ cues.push({ t0, t1: Math.max(t1, t0), text: body });
621
+ }
622
+ return cues;
623
+ };
624
+ var MIN_WORD = 0.06;
625
+ var wordsFromCues = (cues) => {
626
+ const out = [];
627
+ const sorted = [...cues].sort((a, b) => a.t0 - b.t0);
628
+ sorted.forEach((cue, i) => {
629
+ const tokens = cue.text.split(/\s+/).filter((t) => t.length > 0);
630
+ if (!tokens.length)
631
+ return;
632
+ const next = sorted[i + 1]?.t0;
633
+ const end = next !== undefined ? Math.min(cue.t1, next) : cue.t1;
634
+ const span = Math.max(end - cue.t0, MIN_WORD * tokens.length);
635
+ const weights = tokens.map((t) => t.length + 1);
636
+ const total = weights.reduce((s, w) => s + w, 0);
637
+ let cursor = cue.t0;
638
+ tokens.forEach((text, j) => {
639
+ const dur = span * weights[j] / total;
640
+ const t0 = cursor;
641
+ const t1 = j === tokens.length - 1 ? cue.t0 + span : cursor + dur;
642
+ out.push({ t0, t1: Math.max(t1, t0 + MIN_WORD), text });
643
+ cursor = t1;
644
+ });
645
+ });
646
+ return out;
647
+ };
648
+
649
+ // src/cloud/cloud.ts
650
+ var DEFAULT_CLIENT_ID = "oms-video";
651
+ var OMS_VIDEO_SCOPES = [
652
+ "openid",
653
+ "storage:read",
654
+ "storage:write",
655
+ "tools:read",
656
+ "tools:write"
657
+ ];
658
+ var PROJECTS_FOLDER = "oms-video";
659
+ var PROJECT_EXTENSION = ".omsv";
660
+ var withExtension = (name) => {
661
+ const clean = name.replace(/[\/\\]+/g, "_").trim() || "projecto";
662
+ return clean.toLowerCase().endsWith(PROJECT_EXTENSION) ? clean : `${clean}${PROJECT_EXTENSION}`;
663
+ };
664
+ var toFileInput = (input) => file(input.data, input.name, {
665
+ ...input.size === undefined ? {} : { size: input.size },
666
+ ...input.contentType === undefined ? {} : { contentType: input.contentType }
667
+ });
668
+ var sizeOf = (input) => input.size ?? (input.data instanceof Blob ? input.data.size : input.data.byteLength);
669
+ var toProject = (node) => ({
670
+ id: node.id,
671
+ name: node.name,
672
+ size: node.size,
673
+ createdAt: node.created_at,
674
+ updatedAt: node.updated_at
675
+ });
676
+
677
+ class Cloud {
678
+ clientId;
679
+ baseUrl;
680
+ scopes;
681
+ anon;
682
+ tokens;
683
+ oms;
684
+ usesOAuth;
685
+ store;
686
+ folderId;
687
+ constructor(options) {
688
+ this.clientId = options.clientId ?? DEFAULT_CLIENT_ID;
689
+ this.scopes = options.scopes ?? OMS_VIDEO_SCOPES;
690
+ this.store = options.store;
691
+ const shared = {
692
+ ...options.baseUrl === undefined ? {} : { baseUrl: options.baseUrl },
693
+ ...options.fetch === undefined ? {} : { fetch: options.fetch },
694
+ ...options.clientName === undefined ? {} : { clientName: options.clientName }
695
+ };
696
+ this.anon = new Oms(shared);
697
+ this.baseUrl = this.anon.baseUrl;
698
+ if (options.sessionToken) {
699
+ this.tokens = null;
700
+ this.usesOAuth = false;
701
+ this.oms = new Oms({ ...shared, token: options.sessionToken });
702
+ } else {
703
+ this.tokens = new OAuthTokenProvider({
704
+ store: options.store,
705
+ refresh: (refreshToken) => this.anon.auth.refresh(refreshToken, { clientId: this.clientId })
706
+ });
707
+ this.usesOAuth = true;
708
+ this.oms = new Oms({ ...shared, tokens: this.tokens });
709
+ }
710
+ }
711
+ client() {
712
+ return this.oms;
713
+ }
714
+ async login(options) {
715
+ if (!this.tokens)
716
+ throw new OmsError("Esta instancia usa um token de sessao fixo; nao ha' login a fazer.", "invalid_request");
717
+ const grant = await this.anon.auth.device.start({
718
+ clientId: this.clientId,
719
+ scope: this.scopes.join(" ")
720
+ });
721
+ await options.onPrompt({ ...grant, url: grant.verificationUriComplete ?? grant.verificationUri });
722
+ const set = await this.anon.auth.device.wait({
723
+ clientId: this.clientId,
724
+ deviceCode: grant.deviceCode,
725
+ intervalMs: options.pollIntervalMs ?? grant.intervalMs,
726
+ expiresAt: grant.expiresAt,
727
+ ...options.onPoll === undefined ? {} : { onPoll: options.onPoll },
728
+ ...options.signal === undefined ? {} : { signal: options.signal }
729
+ });
730
+ await this.tokens.set(set);
731
+ return sessionFrom(set);
732
+ }
733
+ async logout() {
734
+ if (!this.tokens)
735
+ return;
736
+ const set = await this.store.load();
737
+ if (set?.refreshToken) {
738
+ try {
739
+ await this.anon.auth.revoke(set.refreshToken, { clientId: this.clientId });
740
+ } catch {}
741
+ }
742
+ await this.tokens.clear();
743
+ }
744
+ async session() {
745
+ if (!this.tokens)
746
+ return null;
747
+ const set = await this.store.load();
748
+ if (!set)
749
+ return null;
750
+ try {
751
+ return sessionFrom(set);
752
+ } catch {
753
+ return null;
754
+ }
755
+ }
756
+ async whoami() {
757
+ const stored = await this.session();
758
+ const claims = await this.oms.auth.userinfo();
759
+ const scopes = stored?.scopes ?? [];
760
+ let handle = typeof claims.preferred_username === "string" ? claims.preferred_username : undefined;
761
+ let email = typeof claims.email === "string" ? claims.email : undefined;
762
+ if (!this.usesOAuth || scopes.includes("profile")) {
763
+ try {
764
+ const me = await this.oms.auth.whoami();
765
+ handle = me.handle;
766
+ email = me.email ?? email;
767
+ } catch {}
768
+ }
769
+ return {
770
+ sub: claims.sub,
771
+ scopes,
772
+ ...stored?.expiresAt === undefined ? {} : { expiresAt: stored.expiresAt },
773
+ ...handle === undefined ? {} : { handle },
774
+ ...email === undefined ? {} : { email }
775
+ };
776
+ }
777
+ async projectsFolder() {
778
+ if (this.folderId) {
779
+ try {
780
+ return await this.oms.storage.get(this.folderId);
781
+ } catch {
782
+ this.folderId = undefined;
783
+ }
784
+ }
785
+ const roots = await this.oms.storage.roots();
786
+ if (!roots.home)
787
+ throw new OmsError("A conta nao tem pasta home no storage.", "not_found");
788
+ let node;
789
+ try {
790
+ node = await this.oms.storage.resolvePath(PROJECTS_FOLDER);
791
+ if (node.kind !== "directory")
792
+ throw new OmsError(`Existe um ficheiro chamado "${PROJECTS_FOLDER}" na home; a pasta dos projectos nao pode ser criada.`, "conflict");
793
+ } catch (e) {
794
+ if (!(e instanceof OmsError) || e.code !== "not_found")
795
+ throw e;
796
+ node = await this.oms.storage.createDirectory({ name: PROJECTS_FOLDER, parentId: roots.home });
797
+ }
798
+ this.folderId = node.id;
799
+ return node;
800
+ }
801
+ async listProjects() {
802
+ const folder = await this.projectsFolder();
803
+ const page = await this.oms.storage.list({ parentId: folder.id, order: "updated_at:desc" });
804
+ const nodes = await collect(page, 2000);
805
+ return nodes.filter((n2) => n2.kind === "file" && n2.name.toLowerCase().endsWith(PROJECT_EXTENSION)).map(toProject);
806
+ }
807
+ async findProject(idOrName) {
808
+ try {
809
+ const node = await this.oms.storage.get(idOrName);
810
+ if (node.kind === "file")
811
+ return toProject(node);
812
+ } catch (e) {
813
+ if (!(e instanceof OmsApiError) || e.status !== 404)
814
+ throw e;
815
+ }
816
+ const folder = await this.projectsFolder();
817
+ for (const name of [idOrName, withExtension(idOrName)]) {
818
+ const page = await this.oms.storage.list({ parentId: folder.id, exactSearch: { name }, pageSize: 2 });
819
+ const hit = page.items.find((n2) => n2.kind === "file");
820
+ if (hit)
821
+ return toProject(hit);
822
+ }
823
+ return null;
824
+ }
825
+ async uploadProject(input, options = {}) {
826
+ const name = withExtension(input.name);
827
+ const folder = await this.projectsFolder();
828
+ const existing = (await this.oms.storage.list({ parentId: folder.id, exactSearch: { name }, pageSize: 2 })).items.find((n2) => n2.kind === "file");
829
+ let backup;
830
+ if (existing)
831
+ backup = await this.oms.storage.rename(existing.id, `${name}.a-substituir-${Date.now()}`);
832
+ let node;
833
+ try {
834
+ const results = await this.oms.storage.uploads.upload({
835
+ parentId: folder.id,
836
+ files: [toFileInput({ ...input, name, size: sizeOf(input) })]
837
+ }, {
838
+ ...options.onProgress === undefined ? {} : { onProgress: options.onProgress },
839
+ ...options.signal === undefined ? {} : { signal: options.signal }
840
+ });
841
+ const result = results[0];
842
+ if (!result || result.error)
843
+ throw new OmsError(`O storage recusou "${name}"${result?.error ? ` (${result.error.code}): ${result.error.message}` : "."}`, result?.error?.code === "quota_exceeded" ? "quota_exceeded" : "invalid_request");
844
+ if (result.node)
845
+ node = result.node;
846
+ else if (result.fs_node_id)
847
+ node = await this.oms.storage.get(result.fs_node_id);
848
+ else
849
+ throw new OmsError(`O upload de "${name}" acabou sem no' de volta.`, "api_error");
850
+ } catch (e) {
851
+ if (backup) {
852
+ try {
853
+ await this.oms.storage.rename(backup.id, name);
854
+ } catch {}
855
+ }
856
+ throw e;
857
+ }
858
+ if (backup) {
859
+ try {
860
+ await this.oms.storage.trash([backup.id]);
861
+ } catch {}
862
+ }
863
+ return toProject(node);
864
+ }
865
+ async downloadProject(id, options = {}) {
866
+ const node = await this.oms.storage.get(id);
867
+ if (node.kind !== "file")
868
+ throw new OmsError(`"${node.name}" e' uma pasta, nao um projecto.`, "invalid_request");
869
+ const { stream, size } = await this.oms.storage.downloadStream(id, {
870
+ ...options.signal === undefined ? {} : { signal: options.signal }
871
+ });
872
+ const total = size ?? node.size;
873
+ const chunks = [];
874
+ let loaded = 0;
875
+ options.onProgress?.({ phase: "download", loaded: 0, total });
876
+ const reader = stream.getReader();
877
+ for (;; ) {
878
+ const { done, value } = await reader.read();
879
+ if (done)
880
+ break;
881
+ if (!value)
882
+ continue;
883
+ chunks.push(value);
884
+ loaded += value.byteLength;
885
+ options.onProgress?.({ phase: "download", loaded, total });
886
+ }
887
+ const data = new Uint8Array(loaded);
888
+ let at = 0;
889
+ for (const chunk of chunks) {
890
+ data.set(chunk, at);
891
+ at += chunk.byteLength;
892
+ }
893
+ return { data, name: node.name, project: toProject(node) };
894
+ }
895
+ async downloadUrl(id) {
896
+ const node = await this.oms.storage.get(id);
897
+ if (node.kind !== "file")
898
+ throw new OmsError(`"${node.name}" e' uma pasta, nao um projecto.`, "invalid_request");
899
+ return { url: await this.oms.storage.downloadUrl(id), project: toProject(node) };
900
+ }
901
+ async transcribe(media, options = {}) {
902
+ const run = await this.oms.tools.transcription.run({
903
+ audio: toFileInput(media),
904
+ ...options.language === undefined ? {} : { language: options.language }
905
+ }, waitOptions(options));
906
+ if (run.status === "failed")
907
+ throw new OmsError(`A transcricao falhou: ${run.error ?? "o servidor nao deu razao"}.`, "server_error");
908
+ let cues = [];
909
+ if (run.srt_url) {
910
+ const srt = await fetchToolArtifact(this.oms.http, run.srt_url, {
911
+ ...options.signal === undefined ? {} : { signal: options.signal }
912
+ });
913
+ cues = parseSrt(await srt.text());
914
+ }
915
+ const language = run.detected_language ?? run.language ?? undefined;
916
+ return {
917
+ id: run.id,
918
+ text: run.text ?? cues.map((c) => c.text).join(" "),
919
+ ...language === undefined ? {} : { language },
920
+ cues,
921
+ words: wordsFromCues(cues),
922
+ approximate: true,
923
+ durationSeconds: run.duration_seconds
924
+ };
925
+ }
926
+ async captionsUpload(video, options = {}) {
927
+ const input = toFileInput({ ...video, size: sizeOf(video) });
928
+ const size = captionUploadSize(input);
929
+ if (this.usesOAuth && size !== undefined && size > CAPTION_CHUNKED_THRESHOLD)
930
+ throw new OmsError(`O video tem ${(size / 1048576).toFixed(0)} MiB e o upload por partes das captions nao aceita tokens OAuth ` + `(limitacao do backend). Renderiza mais pequeno (crf mais alto) ou fica abaixo de ${CAPTION_CHUNKED_THRESHOLD / 1048576} MiB.`, "unsupported");
931
+ return this.oms.tools.captions.upload({ video: input }, {
932
+ ...options.onProgress === undefined ? {} : { onProgress: options.onProgress },
933
+ ...options.signal === undefined ? {} : { signal: options.signal }
934
+ });
935
+ }
936
+ async captionsTranscribe(video, options = {}) {
937
+ const uploaded = await this.captionsUpload(video, options);
938
+ return this.captionsTranscribeJob(uploaded, options);
939
+ }
940
+ async captionsTranscribeJob(job, options = {}) {
941
+ const start = options.start ?? 0;
942
+ const end = options.end ?? job.duration;
943
+ const done = await this.oms.tools.captions.transcribe(job.id, { start, end, ...options.language === undefined ? {} : { language: options.language } }, waitOptions(options));
944
+ if (done.status === "failed")
945
+ throw new OmsError(`A transcricao das captions falhou: ${done.error ?? "o servidor nao deu razao"}.`, "server_error");
946
+ return { job: done, words: done.words ?? [] };
947
+ }
948
+ async captionsRender(jobId, options = {}) {
949
+ const rendered = await this.oms.tools.captions.render(jobId, {
950
+ ...options.words === undefined ? {} : { words: options.words },
951
+ ...options.style === undefined ? {} : { style: options.style }
952
+ }, waitOptions(options));
953
+ if (rendered.status === "failed")
954
+ throw new OmsError(`A render das captions falhou: ${rendered.error ?? "o servidor nao deu razao"}.`, "server_error");
955
+ const output = await fetchToolArtifact(this.oms.http, this.oms.tools.captions.outputUrl(rendered), {
956
+ ...options.signal === undefined ? {} : { signal: options.signal }
957
+ });
958
+ return { job: rendered, output };
959
+ }
960
+ async captions(video, options = {}) {
961
+ const uploaded = await this.captionsUpload(video, options);
962
+ let words = options.words;
963
+ let job = uploaded;
964
+ if (!words || words.length === 0) {
965
+ const transcribed = await this.captionsTranscribeJob(uploaded, options);
966
+ job = transcribed.job;
967
+ words = transcribed.words;
968
+ }
969
+ if (words.length === 0)
970
+ throw new OmsError("Nao ha' palavras para legendar: a transcricao veio vazia.", "invalid_request");
971
+ const rendered = await this.captionsRender(job.id, {
972
+ ...options,
973
+ words
974
+ });
975
+ if (!options.keepJob) {
976
+ try {
977
+ await this.oms.tools.captions.delete(job.id);
978
+ } catch {}
979
+ }
980
+ return { job: rendered.job, words, output: rendered.output };
981
+ }
982
+ }
983
+ var waitOptions = (options) => ({
984
+ ...options.onProgress === undefined ? {} : { onProgress: options.onProgress },
985
+ ...options.signal === undefined ? {} : { signal: options.signal },
986
+ ...options.waitTimeoutMs === undefined ? {} : { waitTimeoutMs: options.waitTimeoutMs }
987
+ });
988
+ var bodyError = (body) => typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : undefined;
989
+ var bodyMessage = (body) => typeof body === "object" && body !== null && typeof body.message === "string" ? body.message : undefined;
990
+ var sessionFrom = (set) => {
991
+ if (!set.idToken)
992
+ throw new OmsError("O grant nao trouxe id token; foi pedido sem `openid`?", "invalid_request");
993
+ const claims = decodeIdToken(set.idToken);
994
+ return {
995
+ sub: claims.sub,
996
+ scopes: scopesOf(set),
997
+ ...set.expiresAt === undefined ? {} : { expiresAt: set.expiresAt },
998
+ ...typeof claims.preferred_username === "string" ? { handle: claims.preferred_username } : {},
999
+ ...typeof claims.email === "string" ? { email: claims.email } : {}
1000
+ };
1001
+ };
1002
+ var cloudCaptionStyle = (style, frame) => {
1003
+ const rgb = (hex) => {
1004
+ if (!hex)
1005
+ return;
1006
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
1007
+ if (!m)
1008
+ return;
1009
+ const n2 = parseInt(m[1], 16);
1010
+ return [n2 >> 16 & 255, n2 >> 8 & 255, n2 & 255];
1011
+ };
1012
+ const out = {};
1013
+ if (style.fontScale !== undefined && frame.height > 0)
1014
+ out.fontscale = Math.min(0.09, Math.max(0.03, style.fontScale * frame.width / frame.height));
1015
+ if (style.strokeScale !== undefined)
1016
+ out.stroke_factor = Math.min(0.3, Math.max(0, style.strokeScale));
1017
+ if (style.pos !== undefined)
1018
+ out.pos = Math.min(0.9, Math.max(0.3, style.pos));
1019
+ if (style.maxWords !== undefined)
1020
+ out.max_words = Math.min(6, Math.max(1, Math.round(style.maxWords)));
1021
+ if (style.gap !== undefined)
1022
+ out.gap = Math.min(1, Math.max(0.1, style.gap));
1023
+ const yellow = rgb(style.highlight);
1024
+ const white = rgb(style.color);
1025
+ const stroke = rgb(style.stroke);
1026
+ if (yellow)
1027
+ out.yellow = yellow;
1028
+ if (white)
1029
+ out.white = white;
1030
+ if (stroke)
1031
+ out.stroke = stroke;
1032
+ return out;
1033
+ };
1034
+ var describeCloudError = (e) => {
1035
+ if (e instanceof OmsDeviceExpiredError)
1036
+ return "O codigo expirou antes de ser aprovado. Volta a entrar.";
1037
+ if (e instanceof OmsDeviceDeniedError)
1038
+ return "A autorizacao foi recusada no browser.";
1039
+ const scope = readInsufficientScope(e);
1040
+ if (scope) {
1041
+ return scope.required.length ? `A sessao nao tem os escopos ${scope.required.join(", ")}. Sai e volta a entrar.` : "Esta rota nao aceita tokens OAuth (limitacao do backend).";
1042
+ }
1043
+ if (e instanceof OmsApiError && e.status === 403 && bodyError(e.body) === "insufficient_scope")
1044
+ return `Esta rota precisa de um escopo que a sessao nao tem, ou nao aceita tokens OAuth: ${bodyMessage(e.body) ?? e.message}`;
1045
+ if (e instanceof OmsAuthError) {
1046
+ const said = bodyMessage(e.body) ?? bodyError(e.body) ?? e.message;
1047
+ const where = e.url ? ` em ${e.method ?? ""} ${e.url.replace(/^https?:\/\/[^/]+/, "")}`.trimEnd() : "";
1048
+ return `O servidor recusou a credencial (${e.status}${where})${said ? `: ${said}` : ""}. Se a sessao expirou ou foi revogada, volta a entrar (omsv cloud login).`;
1049
+ }
1050
+ if (e instanceof OmsQuotaError)
1051
+ return e.retryAfterMs ? `Demasiados pedidos; tenta daqui a ${Math.ceil(e.retryAfterMs / 1000)}s.` : "A quota diaria desta ferramenta esgotou-se.";
1052
+ if (e instanceof OmsTimeoutError)
1053
+ return e.code === "aborted" ? "Cancelado." : "O servidor demorou demasiado a responder.";
1054
+ if (e instanceof OmsNetworkError)
1055
+ return `Sem ligacao a ${"url" in e && e.url ? e.url : "API"}: ${e.message}`;
1056
+ if (e instanceof OmsApiError && e.status === 413)
1057
+ return "O ficheiro e' grande de mais para esta rota.";
1058
+ if (e instanceof Error) {
1059
+ if ("error" in e && e.error === "invalid_client")
1060
+ return `O client_id nao esta' registado no servidor de autorizacao. ${e.message}`;
1061
+ return e.message;
1062
+ }
1063
+ return String(e);
1064
+ };
1065
+ // src/cloud/store.ts
1066
+ var CREDENTIALS_VERSION = 1;
1067
+ var parseStoredCredentials = (text) => {
1068
+ if (!text)
1069
+ return null;
1070
+ let raw;
1071
+ try {
1072
+ raw = JSON.parse(text);
1073
+ } catch {
1074
+ return null;
1075
+ }
1076
+ if (typeof raw !== "object" || raw === null)
1077
+ return null;
1078
+ const rec = raw;
1079
+ if (rec.version !== CREDENTIALS_VERSION)
1080
+ return null;
1081
+ if (typeof rec.baseUrl !== "string" || typeof rec.clientId !== "string")
1082
+ return null;
1083
+ const tokens = rec.tokens;
1084
+ if (typeof tokens !== "object" || tokens === null)
1085
+ return null;
1086
+ const t = tokens;
1087
+ if (typeof t.accessToken !== "string" || t.accessToken.length === 0)
1088
+ return null;
1089
+ return {
1090
+ version: 1,
1091
+ baseUrl: rec.baseUrl,
1092
+ clientId: rec.clientId,
1093
+ tokens: {
1094
+ accessToken: t.accessToken,
1095
+ tokenType: typeof t.tokenType === "string" ? t.tokenType : "Bearer",
1096
+ ...typeof t.refreshToken === "string" ? { refreshToken: t.refreshToken } : {},
1097
+ ...typeof t.idToken === "string" ? { idToken: t.idToken } : {},
1098
+ ...typeof t.expiresAt === "number" ? { expiresAt: t.expiresAt } : {},
1099
+ ...typeof t.scope === "string" ? { scope: t.scope } : {}
1100
+ }
1101
+ };
1102
+ };
1103
+ var stringTokenStore = (inner, identity) => {
1104
+ const mine = (stored) => stored !== null && stored.baseUrl === identity.baseUrl && stored.clientId === identity.clientId;
1105
+ return {
1106
+ async load() {
1107
+ const stored = parseStoredCredentials(await inner.load());
1108
+ return mine(stored) ? stored.tokens : null;
1109
+ },
1110
+ async save(tokens) {
1111
+ const stored = { version: 1, ...identity, tokens };
1112
+ await inner.save(JSON.stringify(stored, null, 2));
1113
+ },
1114
+ async clear() {
1115
+ const text = await inner.load();
1116
+ if (text === null)
1117
+ return;
1118
+ const stored = parseStoredCredentials(text);
1119
+ if (stored === null || mine(stored))
1120
+ await inner.clear();
1121
+ }
1122
+ };
1123
+ };
1124
+
1125
+ // src/cloud/index.ts
1126
+ import {
1127
+ DEFAULT_BASE_URL,
1128
+ memoryTokenStore,
1129
+ OmsApiError as OmsApiError2,
1130
+ OmsAuthError as OmsAuthError2,
1131
+ OmsError as OmsError2,
1132
+ OmsNetworkError as OmsNetworkError2,
1133
+ OmsQuotaError as OmsQuotaError2,
1134
+ OmsTimeoutError as OmsTimeoutError2
1135
+ } from "@omelhorsite/sdk";
1136
+ export {
1137
+ wordsFromCues,
1138
+ unpackProject,
1139
+ timelineWords,
1140
+ stringTokenStore,
1141
+ sourceSpans,
1142
+ sliceProject,
1143
+ sliceClip,
1144
+ shadowMargin,
1145
+ sessionFrom,
1146
+ remapWords,
1147
+ projectDuration,
1148
+ parseStoredCredentials,
1149
+ parseSrt,
1150
+ packProject,
1151
+ memoryTokenStore,
1152
+ makeFlashClip,
1153
+ isZipData,
1154
+ hexToYuv,
1155
+ groupWords,
1156
+ flashStateAt,
1157
+ flashGlowReach,
1158
+ flashGlowAtRadius,
1159
+ flashGlowAt,
1160
+ flashGeqExpr,
1161
+ flashEnvelope,
1162
+ flashCutTime,
1163
+ flashBandOffset,
1164
+ flashBandOf,
1165
+ findAsset,
1166
+ fadeFactor,
1167
+ emptyProject,
1168
+ describeCloudError,
1169
+ computePlacement,
1170
+ compile,
1171
+ cloudCaptionStyle,
1172
+ captionStates,
1173
+ assetEntryName,
1174
+ SHADOW_OPACITY,
1175
+ SHADOW_DROP,
1176
+ RESOLUTION_PRESETS,
1177
+ PROJECT_EXTENSION,
1178
+ PROJECTS_FOLDER,
1179
+ OmsTimeoutError2 as OmsTimeoutError,
1180
+ OmsQuotaError2 as OmsQuotaError,
1181
+ OmsNetworkError2 as OmsNetworkError,
1182
+ OmsError2 as OmsError,
1183
+ OmsAuthError2 as OmsAuthError,
1184
+ OmsApiError2 as OmsApiError,
1185
+ OMS_VIDEO_SCOPES,
1186
+ OMSV_SCHEMA,
1187
+ FLASH_WAVE_SPEED,
1188
+ FLASH_STRIPS,
1189
+ FLASH_CUT_AT,
1190
+ DEFAULT_FLASH_DURATION,
1191
+ DEFAULT_FLASH,
1192
+ DEFAULT_CLIENT_ID,
1193
+ DEFAULT_CAPTION_STYLE,
1194
+ DEFAULT_BASE_URL,
1195
+ Cloud,
1196
+ CREDENTIALS_VERSION
1197
+ };