@ume-group/contracts 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +37 -0
  2. package/dist/adserving.d.ts +150 -0
  3. package/dist/adserving.d.ts.map +1 -0
  4. package/dist/adserving.js +8 -0
  5. package/dist/campaigns.d.ts +37 -0
  6. package/dist/campaigns.d.ts.map +1 -0
  7. package/dist/campaigns.js +8 -0
  8. package/dist/gausst.d.ts +236 -0
  9. package/dist/gausst.d.ts.map +1 -0
  10. package/dist/gausst.js +307 -0
  11. package/dist/gausst.test.d.ts +2 -0
  12. package/dist/gausst.test.d.ts.map +1 -0
  13. package/dist/gausst.test.js +71 -0
  14. package/dist/index.d.ts +1531 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +1112 -0
  17. package/dist/layer2/index.d.ts +9 -0
  18. package/dist/layer2/index.d.ts.map +1 -0
  19. package/dist/layer2/index.js +10 -0
  20. package/dist/layer2/shaders.d.ts +185 -0
  21. package/dist/layer2/shaders.d.ts.map +1 -0
  22. package/dist/layer2/shaders.js +604 -0
  23. package/dist/layer2/webcam-utils.d.ts +113 -0
  24. package/dist/layer2/webcam-utils.d.ts.map +1 -0
  25. package/dist/layer2/webcam-utils.js +147 -0
  26. package/dist/layer2/webcam-utils.test.d.ts +2 -0
  27. package/dist/layer2/webcam-utils.test.d.ts.map +1 -0
  28. package/dist/layer2/webcam-utils.test.js +18 -0
  29. package/dist/layer2.d.ts +558 -0
  30. package/dist/layer2.d.ts.map +1 -0
  31. package/dist/layer2.js +376 -0
  32. package/dist/layer2.test.d.ts +2 -0
  33. package/dist/layer2.test.d.ts.map +1 -0
  34. package/dist/layer2.test.js +65 -0
  35. package/dist/perspective.d.ts +28 -0
  36. package/dist/perspective.d.ts.map +1 -0
  37. package/dist/perspective.js +157 -0
  38. package/dist/segmentation/MediaPipeSegmenter.d.ts +201 -0
  39. package/dist/segmentation/MediaPipeSegmenter.d.ts.map +1 -0
  40. package/dist/segmentation/MediaPipeSegmenter.js +434 -0
  41. package/dist/segmentation/index.d.ts +5 -0
  42. package/dist/segmentation/index.d.ts.map +1 -0
  43. package/dist/segmentation/index.js +4 -0
  44. package/dist/webcam/GarbageMatteDragManager.d.ts +63 -0
  45. package/dist/webcam/GarbageMatteDragManager.d.ts.map +1 -0
  46. package/dist/webcam/GarbageMatteDragManager.js +183 -0
  47. package/dist/webcam/WebcamStreamManager.d.ts +103 -0
  48. package/dist/webcam/WebcamStreamManager.d.ts.map +1 -0
  49. package/dist/webcam/WebcamStreamManager.js +356 -0
  50. package/dist/webcam/index.d.ts +5 -0
  51. package/dist/webcam/index.d.ts.map +1 -0
  52. package/dist/webcam/index.js +2 -0
  53. package/openapi/admetise.yaml +632 -0
  54. package/openapi/includu.yaml +621 -0
  55. package/openapi/integration.yaml +372 -0
  56. package/openapi/shared/schemas.yaml +227 -0
  57. package/package.json +53 -0
package/dist/index.js ADDED
@@ -0,0 +1,1112 @@
1
+ /**
2
+ * Shared types for Includu Platform
3
+ *
4
+ * Used by:
5
+ * - @includu/editor (timeline editor)
6
+ * - @includu/cms (video library)
7
+ */
8
+ // ============================================================================
9
+ // Debug Utilities
10
+ // ============================================================================
11
+ /**
12
+ * Debug flag - true in Vite dev mode, false in production builds.
13
+ * Can also be set manually for testing.
14
+ */
15
+ export let DEBUG = !!import.meta.env?.DEV;
16
+ /** Override the DEBUG flag (useful for testing or manual control) */
17
+ export function setDebug(value) {
18
+ DEBUG = value;
19
+ }
20
+ /** Debug logger - only outputs when DEBUG is true (dev mode by default) */
21
+ export function debug(...args) {
22
+ if (DEBUG)
23
+ console.log(...args);
24
+ }
25
+ /**
26
+ * Creates default corner offsets (no perspective adjustment)
27
+ */
28
+ export function createDefaultCornerOffsets() {
29
+ return {
30
+ tl: { dx: 0, dy: 0 },
31
+ tr: { dx: 0, dy: 0 },
32
+ br: { dx: 0, dy: 0 },
33
+ bl: { dx: 0, dy: 0 }
34
+ };
35
+ }
36
+ /**
37
+ * Computes absolute corner positions from overlay position and corner offsets
38
+ * This is how corners are calculated for rendering
39
+ */
40
+ export function computeCornersFromOffsets(position, offsets) {
41
+ // Base corners from overlay position (normalized 0-1)
42
+ const left = (position.x - position.width / 2) / 100;
43
+ const right = (position.x + position.width / 2) / 100;
44
+ const top = (position.y - position.height / 2) / 100;
45
+ const bottom = (position.y + position.height / 2) / 100;
46
+ // Apply offsets (convert from % of overlay size to normalized coordinates)
47
+ const widthNorm = position.width / 100;
48
+ const heightNorm = position.height / 100;
49
+ return {
50
+ tl: {
51
+ x: left + (offsets.tl.dx / 100) * widthNorm,
52
+ y: top + (offsets.tl.dy / 100) * heightNorm
53
+ },
54
+ tr: {
55
+ x: right + (offsets.tr.dx / 100) * widthNorm,
56
+ y: top + (offsets.tr.dy / 100) * heightNorm
57
+ },
58
+ br: {
59
+ x: right + (offsets.br.dx / 100) * widthNorm,
60
+ y: bottom + (offsets.br.dy / 100) * heightNorm
61
+ },
62
+ bl: {
63
+ x: left + (offsets.bl.dx / 100) * widthNorm,
64
+ y: bottom + (offsets.bl.dy / 100) * heightNorm
65
+ }
66
+ };
67
+ }
68
+ /**
69
+ * @deprecated Use createDefaultCornerOffsets instead
70
+ * Creates default corner points for a rectangular overlay
71
+ * Uses the overlay's position to calculate initial corner positions
72
+ */
73
+ export function createDefaultCornerPoints(position) {
74
+ // Convert percentage position to normalized 0-1 coordinates
75
+ const left = (position.x - position.width / 2) / 100;
76
+ const right = (position.x + position.width / 2) / 100;
77
+ const top = (position.y - position.height / 2) / 100;
78
+ const bottom = (position.y + position.height / 2) / 100;
79
+ return {
80
+ tl: { x: left, y: top },
81
+ tr: { x: right, y: top },
82
+ br: { x: right, y: bottom },
83
+ bl: { x: left, y: bottom }
84
+ };
85
+ }
86
+ /**
87
+ * Migrate old absolute corners to new offset-based format
88
+ * Call this when loading projects with old corner format
89
+ */
90
+ export function migrateToCornerOffsets(position, corners) {
91
+ // Calculate what the natural corners would be
92
+ const naturalLeft = (position.x - position.width / 2) / 100;
93
+ const naturalRight = (position.x + position.width / 2) / 100;
94
+ const naturalTop = (position.y - position.height / 2) / 100;
95
+ const naturalBottom = (position.y + position.height / 2) / 100;
96
+ const widthNorm = position.width / 100;
97
+ const heightNorm = position.height / 100;
98
+ // Calculate offsets as percentage of overlay dimensions
99
+ return {
100
+ tl: {
101
+ dx: widthNorm > 0 ? ((corners.tl.x - naturalLeft) / widthNorm) * 100 : 0,
102
+ dy: heightNorm > 0 ? ((corners.tl.y - naturalTop) / heightNorm) * 100 : 0
103
+ },
104
+ tr: {
105
+ dx: widthNorm > 0 ? ((corners.tr.x - naturalRight) / widthNorm) * 100 : 0,
106
+ dy: heightNorm > 0 ? ((corners.tr.y - naturalTop) / heightNorm) * 100 : 0
107
+ },
108
+ br: {
109
+ dx: widthNorm > 0 ? ((corners.br.x - naturalRight) / widthNorm) * 100 : 0,
110
+ dy: heightNorm > 0 ? ((corners.br.y - naturalBottom) / heightNorm) * 100 : 0
111
+ },
112
+ bl: {
113
+ dx: widthNorm > 0 ? ((corners.bl.x - naturalLeft) / widthNorm) * 100 : 0,
114
+ dy: heightNorm > 0 ? ((corners.bl.y - naturalBottom) / heightNorm) * 100 : 0
115
+ }
116
+ };
117
+ }
118
+ // ============================================================================
119
+ // Recording Session Metadata
120
+ // ============================================================================
121
+ /**
122
+ * Session metadata for a recorded participation.
123
+ *
124
+ * Captures everything needed to re-publish the participant's recording
125
+ * as a new monetizable version with dynamic overlays and ads.
126
+ *
127
+ * Classify an overlay for recording: should it be burned into the video
128
+ * (static visual) or kept as dynamic metadata (interactive/monetizable)?
129
+ *
130
+ * Some overlays (e.g., clickable images) are BOTH — burned visually into
131
+ * the recording but also preserved as dynamic metadata so their click URL
132
+ * can be restored when re-publishing.
133
+ */
134
+ export function classifyOverlayForRecording(overlay) {
135
+ switch (overlay.type) {
136
+ case 'cta':
137
+ return { burnable: false, dynamic: true };
138
+ case 'image': {
139
+ const content = overlay.content;
140
+ const hasClickUrl = !!(content.clickUrl?.trim());
141
+ // Images with click URLs are dynamic (ads/CTAs) — don't burn in
142
+ return { burnable: !hasClickUrl, dynamic: hasClickUrl };
143
+ }
144
+ // sprite-sequence and video: burnable in Phase 2 (not yet)
145
+ case 'sprite-sequence':
146
+ case 'video':
147
+ return { burnable: false, dynamic: false };
148
+ default:
149
+ // text, emoji, shape — always burn in
150
+ return { burnable: true, dynamic: false };
151
+ }
152
+ }
153
+ export const RECORDING_TIER_LIMITS = {
154
+ free: {
155
+ maxDurationMs: 3 * 60 * 1000, // 3 minutes
156
+ maxFileSize: 100 * 1024 * 1024, // 100 MB
157
+ label: 'Free (3 min)'
158
+ },
159
+ standard: {
160
+ maxDurationMs: 15 * 60 * 1000, // 15 minutes
161
+ maxFileSize: 300 * 1024 * 1024, // 300 MB
162
+ label: 'Standard (15 min)'
163
+ },
164
+ professional: {
165
+ maxDurationMs: 60 * 60 * 1000, // 60 minutes
166
+ maxFileSize: 500 * 1024 * 1024, // 500 MB
167
+ label: 'Professional (60 min)'
168
+ }
169
+ };
170
+ // ============================================================================
171
+ // Standard Ad Slot Templates (Default Set)
172
+ // ============================================================================
173
+ /**
174
+ * Standard ad slot templates following IAB guidelines
175
+ * These are the default templates that Admetise provides.
176
+ * Can be extended/modified via Admetise admin.
177
+ */
178
+ export const STANDARD_AD_TEMPLATES = [
179
+ // Video Ad Templates
180
+ {
181
+ id: 'preroll-15',
182
+ name: 'Standard Preroll (15s)',
183
+ description: 'Video ad before content starts, up to 15 seconds',
184
+ type: 'preroll',
185
+ category: 'video',
186
+ duration: 15,
187
+ maxDuration: 15,
188
+ active: true,
189
+ sortOrder: 1,
190
+ icon: 'play-circle'
191
+ },
192
+ {
193
+ id: 'preroll-30',
194
+ name: 'Extended Preroll (30s)',
195
+ description: 'Video ad before content starts, up to 30 seconds',
196
+ type: 'preroll',
197
+ category: 'video',
198
+ duration: 30,
199
+ maxDuration: 30,
200
+ active: true,
201
+ sortOrder: 2,
202
+ icon: 'play-circle'
203
+ },
204
+ {
205
+ id: 'midroll-15',
206
+ name: 'Standard Midroll (15s)',
207
+ description: 'Video ad during content, up to 15 seconds',
208
+ type: 'midroll',
209
+ category: 'video',
210
+ duration: 15,
211
+ maxDuration: 15,
212
+ active: true,
213
+ sortOrder: 10,
214
+ icon: 'pause-circle'
215
+ },
216
+ {
217
+ id: 'midroll-30',
218
+ name: 'Extended Midroll (30s)',
219
+ description: 'Video ad during content, up to 30 seconds',
220
+ type: 'midroll',
221
+ category: 'video',
222
+ duration: 30,
223
+ maxDuration: 30,
224
+ active: true,
225
+ sortOrder: 11,
226
+ icon: 'pause-circle'
227
+ },
228
+ {
229
+ id: 'postroll-15',
230
+ name: 'Standard Postroll (15s)',
231
+ description: 'Video ad after content ends, up to 15 seconds',
232
+ type: 'postroll',
233
+ category: 'video',
234
+ duration: 15,
235
+ maxDuration: 15,
236
+ active: true,
237
+ sortOrder: 20,
238
+ icon: 'stop-circle'
239
+ },
240
+ // Display Overlay Templates (IAB Standard Sizes)
241
+ {
242
+ id: 'overlay-leaderboard',
243
+ name: 'Leaderboard Overlay (728×90)',
244
+ description: 'Bottom banner overlay, IAB standard leaderboard',
245
+ type: 'overlay',
246
+ category: 'display',
247
+ width: 728,
248
+ height: 90,
249
+ position: 'bottom-left',
250
+ iabSize: '728x90',
251
+ active: true,
252
+ sortOrder: 30,
253
+ icon: 'rectangle-horizontal'
254
+ },
255
+ {
256
+ id: 'overlay-medium-rectangle',
257
+ name: 'Medium Rectangle Overlay (300×250)',
258
+ description: 'Corner overlay, IAB standard medium rectangle',
259
+ type: 'overlay',
260
+ category: 'display',
261
+ width: 300,
262
+ height: 250,
263
+ position: 'bottom-right',
264
+ iabSize: '300x250',
265
+ active: true,
266
+ sortOrder: 31,
267
+ icon: 'square'
268
+ },
269
+ {
270
+ id: 'overlay-banner',
271
+ name: 'Banner Overlay (468×60)',
272
+ description: 'Compact bottom banner overlay',
273
+ type: 'overlay',
274
+ category: 'display',
275
+ width: 468,
276
+ height: 60,
277
+ position: 'bottom-left',
278
+ iabSize: '468x60',
279
+ active: true,
280
+ sortOrder: 32,
281
+ icon: 'rectangle-horizontal'
282
+ },
283
+ // Companion Ad Templates
284
+ {
285
+ id: 'companion-medium-rectangle',
286
+ name: 'Companion Medium Rectangle (300×250)',
287
+ description: 'Companion ad displayed alongside video player',
288
+ type: 'companion',
289
+ category: 'companion',
290
+ width: 300,
291
+ height: 250,
292
+ iabSize: '300x250',
293
+ active: true,
294
+ sortOrder: 40,
295
+ icon: 'layout-sidebar-right'
296
+ },
297
+ {
298
+ id: 'companion-wide-skyscraper',
299
+ name: 'Companion Wide Skyscraper (160×600)',
300
+ description: 'Tall companion ad beside video player',
301
+ type: 'companion',
302
+ category: 'companion',
303
+ width: 160,
304
+ height: 600,
305
+ iabSize: '160x600',
306
+ active: true,
307
+ sortOrder: 41,
308
+ icon: 'layout-sidebar-right'
309
+ }
310
+ ];
311
+ // ============================================================================
312
+ // Utility Functions
313
+ // ============================================================================
314
+ /**
315
+ * Find a template by ID
316
+ *
317
+ * @param templateId - The template ID to find
318
+ * @param templates - Array of templates (defaults to STANDARD_AD_TEMPLATES)
319
+ * @returns The template or undefined
320
+ */
321
+ export function findTemplate(templateId, templates = STANDARD_AD_TEMPLATES) {
322
+ return templates.find((t) => t.id === templateId);
323
+ }
324
+ /**
325
+ * Maps an AdSlotPlacement to an AdZone using template data
326
+ *
327
+ * @param placement - The placement from Includu
328
+ * @param template - The template definition from Admetise
329
+ * @param fps - Frames per second of the video
330
+ * @returns AdZone for Admetise
331
+ */
332
+ export function mapPlacementToZone(placement, template, fps) {
333
+ return {
334
+ id: placement.id,
335
+ templateId: template.id,
336
+ type: template.type,
337
+ enabled: true,
338
+ presetPosition: template.position,
339
+ width: template.width,
340
+ height: template.height,
341
+ triggerTime: placement.triggerFrame ? placement.triggerFrame / fps : undefined,
342
+ triggerPercent: placement.triggerPercent,
343
+ maxDuration: template.maxDuration,
344
+ centerPosition: placement.position,
345
+ scale: placement.scale,
346
+ rotation: placement.rotation,
347
+ rotationX: placement.rotationX,
348
+ rotationY: placement.rotationY,
349
+ rotationEnabled: placement.rotationEnabled,
350
+ perspective: placement.perspective
351
+ };
352
+ }
353
+ /**
354
+ * @deprecated Use mapPlacementToZone instead
355
+ * Maps an Includu AdSlot to an Admetise AdZone (legacy support)
356
+ */
357
+ export function mapAdSlotToZone(slot, fps) {
358
+ return {
359
+ id: slot.id,
360
+ templateId: slot.templateId || `legacy-${slot.type}`,
361
+ type: slot.type,
362
+ enabled: true,
363
+ presetPosition: slot.position,
364
+ width: slot.width,
365
+ height: slot.height,
366
+ triggerTime: slot.triggerFrame ? slot.triggerFrame / fps : undefined,
367
+ triggerPercent: slot.triggerPercent,
368
+ maxDuration: slot.maxDuration
369
+ };
370
+ }
371
+ /**
372
+ * Maps all placements from a manifest to AdZones
373
+ *
374
+ * @param manifest - Published manifest with monetization config
375
+ * @param templates - Available templates (defaults to STANDARD_AD_TEMPLATES)
376
+ * @returns Array of AdZones, or empty array if monetization disabled
377
+ */
378
+ export function mapManifestToZones(manifest, templates = STANDARD_AD_TEMPLATES) {
379
+ if (!manifest.monetization?.enabled) {
380
+ return [];
381
+ }
382
+ // Prefer new placements field, fall back to legacy adSlots
383
+ if (manifest.monetization.placements?.length) {
384
+ return manifest.monetization.placements
385
+ .map((placement) => {
386
+ const template = findTemplate(placement.templateId, templates);
387
+ if (!template) {
388
+ console.warn(`Template not found: ${placement.templateId}`);
389
+ return null;
390
+ }
391
+ return mapPlacementToZone(placement, template, manifest.fps);
392
+ })
393
+ .filter((zone) => zone !== null);
394
+ }
395
+ // Legacy support for adSlots
396
+ if (manifest.monetization.adSlots?.length) {
397
+ return manifest.monetization.adSlots.map((slot) => mapAdSlotToZone(slot, manifest.fps));
398
+ }
399
+ return [];
400
+ }
401
+ /**
402
+ * Get templates grouped by category
403
+ *
404
+ * @param templates - Array of templates (defaults to STANDARD_AD_TEMPLATES)
405
+ * @returns Object with templates grouped by category
406
+ */
407
+ export function getTemplatesByCategory(templates = STANDARD_AD_TEMPLATES) {
408
+ const result = {
409
+ video: [],
410
+ display: [],
411
+ companion: []
412
+ };
413
+ for (const template of templates.filter((t) => t.active)) {
414
+ result[template.category].push(template);
415
+ }
416
+ // Sort by sortOrder
417
+ for (const category of Object.keys(result)) {
418
+ result[category].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
419
+ }
420
+ return result;
421
+ }
422
+ /**
423
+ * Get templates filtered by type
424
+ *
425
+ * @param type - The ad slot type to filter by
426
+ * @param templates - Array of templates (defaults to STANDARD_AD_TEMPLATES)
427
+ * @returns Filtered templates
428
+ */
429
+ export function getTemplatesByType(type, templates = STANDARD_AD_TEMPLATES) {
430
+ return templates.filter((t) => t.active && t.type === type);
431
+ }
432
+ // ============================================================================
433
+ // Perspective Transform Utilities
434
+ // ============================================================================
435
+ export { computeMatrix3d, isValidQuad } from './perspective';
436
+ // ============================================================================
437
+ // Layer 2: 3D Composite Types
438
+ // ============================================================================
439
+ export * from './layer2';
440
+ // ============================================================================
441
+ // Admetise Campaign Types (F1)
442
+ // ============================================================================
443
+ export * from './campaigns';
444
+ // ============================================================================
445
+ // Admetise Ad Serving Contracts (Player ↔ Admetise boundary)
446
+ // ============================================================================
447
+ export * from './adserving';
448
+ // ============================================================================
449
+ // Gausst Coordinate System Utilities
450
+ // ============================================================================
451
+ export * from './gausst';
452
+ /**
453
+ * Standard overlay template presets
454
+ * Designed for ease of use across all devices
455
+ */
456
+ export const OVERLAY_TEMPLATE_PRESETS = [
457
+ // ==================== LOWER THIRDS ====================
458
+ {
459
+ id: 'lower-third-name',
460
+ name: 'Name Card',
461
+ description: 'Speaker name and title',
462
+ category: 'lower-thirds',
463
+ icon: '👤',
464
+ type: 'text',
465
+ position: { x: 25, y: 85, width: 40, height: 12 },
466
+ durationSeconds: 5,
467
+ textDefaults: {
468
+ text: 'Speaker Name',
469
+ fontSize: 2.5,
470
+ fontFamily: 'Inter, sans-serif',
471
+ fontWeight: 'bold',
472
+ color: '#ffffff',
473
+ textAlign: 'left',
474
+ background: {
475
+ color: 'rgba(0, 0, 0, 0.7)',
476
+ opacity: 1,
477
+ padding: { x: 12, y: 8 },
478
+ scale: 1,
479
+ borderRadius: 4
480
+ }
481
+ }
482
+ },
483
+ {
484
+ id: 'lower-third-subtitle',
485
+ name: 'Subtitle Bar',
486
+ description: 'Full-width subtitle or caption',
487
+ category: 'lower-thirds',
488
+ icon: '💬',
489
+ type: 'text',
490
+ position: { x: 50, y: 90, width: 80, height: 8 },
491
+ durationSeconds: 4,
492
+ textDefaults: {
493
+ text: 'Your subtitle text here',
494
+ fontSize: 2,
495
+ fontFamily: 'Inter, sans-serif',
496
+ fontWeight: 'normal',
497
+ color: '#ffffff',
498
+ textAlign: 'center',
499
+ background: {
500
+ color: 'rgba(0, 0, 0, 0.6)',
501
+ opacity: 1,
502
+ padding: { x: 8, y: 6 },
503
+ scale: 1,
504
+ borderRadius: 4
505
+ }
506
+ }
507
+ },
508
+ // ==================== TITLES ====================
509
+ {
510
+ id: 'title-centered',
511
+ name: 'Center Title',
512
+ description: 'Large centered title',
513
+ category: 'titles',
514
+ icon: '🎬',
515
+ type: 'text',
516
+ position: { x: 50, y: 50, width: 70, height: 15 },
517
+ durationSeconds: 3,
518
+ textDefaults: {
519
+ text: 'Your Title',
520
+ fontSize: 4,
521
+ fontFamily: 'Inter, sans-serif',
522
+ fontWeight: 'bold',
523
+ color: '#ffffff',
524
+ textAlign: 'center',
525
+ background: {
526
+ color: 'rgba(0, 0, 0, 0.5)',
527
+ opacity: 1,
528
+ padding: { x: 15, y: 10 },
529
+ scale: 1,
530
+ borderRadius: 8
531
+ }
532
+ }
533
+ },
534
+ {
535
+ id: 'title-chapter',
536
+ name: 'Chapter Title',
537
+ description: 'Section or chapter heading',
538
+ category: 'titles',
539
+ icon: '📖',
540
+ type: 'text',
541
+ position: { x: 50, y: 15, width: 50, height: 10 },
542
+ durationSeconds: 3,
543
+ textDefaults: {
544
+ text: 'Chapter 1',
545
+ fontSize: 3,
546
+ fontFamily: 'Inter, sans-serif',
547
+ fontWeight: 'bold',
548
+ color: '#ffffff',
549
+ textAlign: 'center',
550
+ background: {
551
+ color: 'rgba(229, 57, 53, 0.9)',
552
+ opacity: 1,
553
+ padding: { x: 12, y: 6 },
554
+ scale: 1,
555
+ borderRadius: 4
556
+ }
557
+ }
558
+ },
559
+ // ==================== BADGES ====================
560
+ {
561
+ id: 'badge-corner',
562
+ name: 'Corner Badge',
563
+ description: 'Small badge in corner',
564
+ category: 'badges',
565
+ icon: '🏷️',
566
+ type: 'text',
567
+ position: { x: 90, y: 10, width: 15, height: 6 },
568
+ durationSeconds: 0, // 0 = show for entire video
569
+ textDefaults: {
570
+ text: 'LIVE',
571
+ fontSize: 1.5,
572
+ fontFamily: 'Inter, sans-serif',
573
+ fontWeight: 'bold',
574
+ color: '#ffffff',
575
+ textAlign: 'center',
576
+ background: {
577
+ color: 'rgba(229, 57, 53, 1)',
578
+ opacity: 1,
579
+ padding: { x: 10, y: 5 },
580
+ scale: 1,
581
+ borderRadius: 4
582
+ }
583
+ }
584
+ },
585
+ {
586
+ id: 'badge-new',
587
+ name: 'NEW Badge',
588
+ description: 'Highlight new content',
589
+ category: 'badges',
590
+ icon: '✨',
591
+ type: 'text',
592
+ position: { x: 10, y: 10, width: 12, height: 5 },
593
+ durationSeconds: 5,
594
+ textDefaults: {
595
+ text: 'NEW',
596
+ fontSize: 1.2,
597
+ fontFamily: 'Inter, sans-serif',
598
+ fontWeight: 'bold',
599
+ color: '#000000',
600
+ textAlign: 'center',
601
+ background: {
602
+ color: 'rgba(255, 235, 59, 1)',
603
+ opacity: 1,
604
+ padding: { x: 8, y: 4 },
605
+ scale: 1,
606
+ borderRadius: 12
607
+ }
608
+ }
609
+ },
610
+ // ==================== CTA ====================
611
+ {
612
+ id: 'cta-subscribe',
613
+ name: 'Subscribe Button',
614
+ description: 'Call to action button',
615
+ category: 'cta',
616
+ icon: '🔔',
617
+ type: 'cta',
618
+ position: { x: 85, y: 85, width: 20, height: 8 },
619
+ durationSeconds: 10,
620
+ ctaDefaults: {
621
+ text: 'Subscribe',
622
+ url: '#subscribe',
623
+ style: {
624
+ backgroundColor: '#E53935',
625
+ textColor: '#ffffff',
626
+ borderRadius: 8,
627
+ fontSize: 1.2, // em units
628
+ fontWeight: 'bold',
629
+ padding: { x: 20, y: 12 }
630
+ }
631
+ }
632
+ },
633
+ {
634
+ id: 'cta-learn-more',
635
+ name: 'Learn More',
636
+ description: 'Link to more info',
637
+ category: 'cta',
638
+ icon: '🔗',
639
+ type: 'cta',
640
+ position: { x: 50, y: 80, width: 25, height: 8 },
641
+ durationSeconds: 8,
642
+ ctaDefaults: {
643
+ text: 'Learn More',
644
+ url: '#',
645
+ style: {
646
+ backgroundColor: '#1976D2',
647
+ textColor: '#ffffff',
648
+ borderRadius: 24,
649
+ fontSize: 1.0, // em units
650
+ fontWeight: 'bold',
651
+ padding: { x: 24, y: 10 }
652
+ }
653
+ }
654
+ },
655
+ {
656
+ id: 'cta-picture-frame',
657
+ name: 'Clickable Picture Frame',
658
+ description: 'Clickable image overlay for promotions and links',
659
+ category: 'cta',
660
+ icon: '🖼️',
661
+ type: 'image',
662
+ position: { x: 50, y: 50, width: 30, height: 15 },
663
+ durationSeconds: 5,
664
+ imageDefaults: {
665
+ src: '',
666
+ alt: 'Clickable Picture Frame',
667
+ fit: 'contain',
668
+ opacity: 1,
669
+ borderRadius: 0,
670
+ clickUrl: '#'
671
+ }
672
+ },
673
+ // ==================== SOCIAL ====================
674
+ {
675
+ id: 'social-reaction',
676
+ name: 'Reaction',
677
+ description: 'Emoji reaction',
678
+ category: 'social',
679
+ icon: '😀',
680
+ type: 'emoji',
681
+ position: { x: 15, y: 50, width: 10, height: 10 },
682
+ durationSeconds: 2,
683
+ emojiDefaults: {
684
+ emoji: '👍',
685
+ size: 4
686
+ }
687
+ },
688
+ {
689
+ id: 'social-heart',
690
+ name: 'Heart',
691
+ description: 'Like/love reaction',
692
+ category: 'social',
693
+ icon: '❤️',
694
+ type: 'emoji',
695
+ position: { x: 85, y: 50, width: 10, height: 10 },
696
+ durationSeconds: 2,
697
+ emojiDefaults: {
698
+ emoji: '❤️',
699
+ size: 4
700
+ }
701
+ },
702
+ // ==================== GRAPHICS & LOGOS ====================
703
+ {
704
+ id: 'graphics-logo-corner',
705
+ name: 'Logo Bug',
706
+ description: 'Small logo in corner (watermark)',
707
+ category: 'graphics',
708
+ icon: '🏷️',
709
+ type: 'image',
710
+ position: { x: 90, y: 10, width: 12, height: 12 },
711
+ durationSeconds: 0, // 0 = full video duration
712
+ imageDefaults: {
713
+ src: '',
714
+ alt: 'Logo',
715
+ fit: 'contain',
716
+ opacity: 0.8,
717
+ borderRadius: 0
718
+ }
719
+ },
720
+ {
721
+ id: 'graphics-watermark-center',
722
+ name: 'Center Watermark',
723
+ description: 'Semi-transparent centered logo',
724
+ category: 'graphics',
725
+ icon: '💧',
726
+ type: 'image',
727
+ position: { x: 50, y: 50, width: 30, height: 30 },
728
+ durationSeconds: 0,
729
+ imageDefaults: {
730
+ src: '',
731
+ alt: 'Watermark',
732
+ fit: 'contain',
733
+ opacity: 0.3,
734
+ borderRadius: 0
735
+ }
736
+ },
737
+ {
738
+ id: 'graphics-banner',
739
+ name: 'Image Banner',
740
+ description: 'Full-width image banner',
741
+ category: 'graphics',
742
+ icon: '🖼️',
743
+ type: 'image',
744
+ position: { x: 50, y: 90, width: 80, height: 15 },
745
+ durationSeconds: 5,
746
+ imageDefaults: {
747
+ src: '',
748
+ alt: 'Banner',
749
+ fit: 'contain',
750
+ opacity: 1,
751
+ borderRadius: 8
752
+ }
753
+ },
754
+ {
755
+ id: 'graphics-sponsor',
756
+ name: 'Sponsor Logo',
757
+ description: 'Sponsor or partner logo placement',
758
+ category: 'graphics',
759
+ icon: '🤝',
760
+ type: 'image',
761
+ position: { x: 10, y: 10, width: 15, height: 10 },
762
+ durationSeconds: 10,
763
+ imageDefaults: {
764
+ src: '',
765
+ alt: 'Sponsor',
766
+ fit: 'contain',
767
+ opacity: 1,
768
+ borderRadius: 4
769
+ }
770
+ },
771
+ {
772
+ id: 'social-picture-frame',
773
+ name: 'Picture Frame',
774
+ description: 'Image overlay for photos and graphics',
775
+ category: 'social',
776
+ icon: '🖼️',
777
+ type: 'image',
778
+ position: { x: 50, y: 50, width: 30, height: 15 },
779
+ durationSeconds: 5,
780
+ imageDefaults: {
781
+ src: '',
782
+ alt: 'Picture Frame',
783
+ fit: 'contain',
784
+ opacity: 1,
785
+ borderRadius: 0
786
+ }
787
+ },
788
+ // ==================== MOTION GRAPHICS ====================
789
+ {
790
+ id: 'motiongfx-sprite',
791
+ name: 'Sprite Animation',
792
+ description: 'Animated sprite sequence (pre-rendered effects, mascots, particles)',
793
+ category: 'motion-gfx',
794
+ icon: '🎬',
795
+ type: 'sprite-sequence',
796
+ position: { x: 50, y: 50, width: 20, height: 20 },
797
+ durationSeconds: 5,
798
+ spriteDefaults: {
799
+ spritesheetUrl: '',
800
+ frameWidth: 256,
801
+ frameHeight: 256,
802
+ frameCount: 1,
803
+ columns: 1,
804
+ framesPerSecond: 12,
805
+ loop: true,
806
+ opacity: 1
807
+ }
808
+ },
809
+ {
810
+ id: 'motiongfx-video',
811
+ name: 'Video Overlay',
812
+ description: 'Video with alpha channel (WebM VP9, animated effects, transitions)',
813
+ category: 'motion-gfx',
814
+ icon: '🎥',
815
+ type: 'video',
816
+ position: { x: 50, y: 50, width: 30, height: 30 },
817
+ durationSeconds: 5,
818
+ videoDefaults: {
819
+ videoUrl: '',
820
+ loop: true,
821
+ muted: true,
822
+ opacity: 1,
823
+ fit: 'contain'
824
+ }
825
+ }
826
+ ];
827
+ /**
828
+ * Get overlay templates by category
829
+ */
830
+ export function getOverlayTemplatesByCategory(category) {
831
+ return OVERLAY_TEMPLATE_PRESETS.filter((t) => t.category === category);
832
+ }
833
+ /**
834
+ * Find an overlay template by ID
835
+ */
836
+ export function findOverlayTemplate(id) {
837
+ return OVERLAY_TEMPLATE_PRESETS.find((t) => t.id === id);
838
+ }
839
+ /** Category labels for display */
840
+ const CATEGORY_LABELS = {
841
+ 'lower-thirds': 'Lower Thirds',
842
+ titles: 'Titles',
843
+ badges: 'Badges',
844
+ cta: 'Call to Action',
845
+ social: 'Social & Reactions',
846
+ graphics: 'Graphics & Logos',
847
+ 'motion-gfx': 'Motion Graphics'
848
+ };
849
+ /**
850
+ * Get all overlay template categories with their templates
851
+ */
852
+ export function getOverlayTemplateCategoriesWithTemplates() {
853
+ const categories = ['lower-thirds', 'titles', 'badges', 'cta', 'social', 'graphics'];
854
+ return categories.map((category) => ({
855
+ category,
856
+ label: CATEGORY_LABELS[category],
857
+ templates: getOverlayTemplatesByCategory(category)
858
+ }));
859
+ }
860
+ /**
861
+ * Get overlay template categories for the Overlays track (excludes branding categories)
862
+ */
863
+ export function getOverlayTrackCategories() {
864
+ // Categories that go on the Overlays timeline track
865
+ const overlayCategories = ['lower-thirds', 'titles', 'badges', 'cta', 'social'];
866
+ return overlayCategories.map((category) => ({
867
+ category,
868
+ label: CATEGORY_LABELS[category],
869
+ templates: getOverlayTemplatesByCategory(category)
870
+ }));
871
+ }
872
+ /**
873
+ * Get branding template categories for the Branding track
874
+ */
875
+ export function getBrandingTrackCategories() {
876
+ // Categories that go on the Branding timeline track
877
+ const brandingCategories = ['graphics'];
878
+ return brandingCategories.map((category) => ({
879
+ category,
880
+ label: CATEGORY_LABELS[category],
881
+ templates: getOverlayTemplatesByCategory(category)
882
+ }));
883
+ }
884
+ /**
885
+ * Get Motion Graphics template categories for the Motion GFX track
886
+ */
887
+ export function getMotionGfxTrackCategories() {
888
+ // Categories that go on the Motion GFX timeline track
889
+ const motionGfxCategories = ['motion-gfx'];
890
+ return motionGfxCategories.map((category) => ({
891
+ category,
892
+ label: CATEGORY_LABELS[category],
893
+ templates: getOverlayTemplatesByCategory(category)
894
+ }));
895
+ }
896
+ /**
897
+ * @deprecated Use getMotionGfxTrackCategories instead
898
+ */
899
+ export function get3DTrackCategories() {
900
+ return getMotionGfxTrackCategories();
901
+ }
902
+ // ============================================================================
903
+ // Coordinate System (Three.js / Metric)
904
+ // ============================================================================
905
+ /**
906
+ * Coordinate system constants for Three.js video compositing
907
+ *
908
+ * Based on Gausst legacy code (Patent US8761580B2):
909
+ * - Y-up axis (Three.js default)
910
+ * - Metric units: 1 unit = 1 meter
911
+ * - Reference: 1920 video pixels = 1 meter
912
+ *
913
+ * @see docs/architecture/coordinate-system-design.md
914
+ */
915
+ export const COORDINATE_SYSTEM = {
916
+ /**
917
+ * Reference scale: 1920 pixels = 1 meter in world space
918
+ * This ensures a standard 1920×1080 video is 1m × 0.5625m
919
+ */
920
+ PIXELS_PER_METER: 1920,
921
+ /**
922
+ * Layer Z positions in meters
923
+ * - VIDEO: Background video plane
924
+ * - THREE_D_MIN/MAX: 3D tracked surfaces, webcam
925
+ * - HUD_MIN/MAX: HTML overlays via CSS2DRenderer
926
+ */
927
+ LAYER_Z: {
928
+ VIDEO: 0,
929
+ THREE_D_MIN: 0.01,
930
+ THREE_D_MAX: 10,
931
+ HUD_MIN: 10.1,
932
+ HUD_MAX: 100
933
+ },
934
+ /**
935
+ * Camera defaults for orthographic rendering (video plane)
936
+ */
937
+ CAMERA: {
938
+ NEAR: 0.01,
939
+ FAR: 1000,
940
+ POSITION_Z: 50 // Camera 50m from video plane
941
+ },
942
+ /**
943
+ * Orthographic camera settings for HUD overlays (Layer 3)
944
+ * Provides predictable 2D positioning for sprites, text, CTAs
945
+ *
946
+ * Bounds are set dynamically based on video dimensions.
947
+ * Objects are positioned in meters relative to center (0,0).
948
+ */
949
+ HUD_CAMERA: {
950
+ /** Camera Z position (arbitrary for orthographic) */
951
+ POSITION_Z: 10,
952
+ /** Near clipping plane */
953
+ NEAR: 0.1,
954
+ /** Far clipping plane */
955
+ FAR: 100,
956
+ /** Default object Z position */
957
+ OBJECT_Z: 0
958
+ },
959
+ /**
960
+ * Perspective camera settings for webcam composite layer (Layer 2)
961
+ * Camera positioned at eye height for 3D scene placement
962
+ * Reserved for future webcam/chroma key compositing
963
+ */
964
+ SCENE_CAMERA: {
965
+ /** Camera X position (centered) */
966
+ POSITION_X: 0,
967
+ /** Camera Y position (eye height) */
968
+ POSITION_Y: 1.2,
969
+ /** Camera Z position (distance from scene) */
970
+ POSITION_Z: 4,
971
+ /** Vertical field of view in degrees (~43mm equivalent) */
972
+ FOV: 47,
973
+ /** Near clipping plane */
974
+ NEAR: 0.1,
975
+ /** Far clipping plane */
976
+ FAR: 100,
977
+ /** Look-at target Y position */
978
+ LOOK_AT_Y: 1.2
979
+ },
980
+ /**
981
+ * Film gate constants (for future camera matching)
982
+ * Based on 35mm full-frame sensor
983
+ */
984
+ FILM_GATE: {
985
+ HORIZONTAL_APERTURE: 1.417, // inches (36mm)
986
+ VERTICAL_APERTURE: 0.945 // inches (24mm)
987
+ }
988
+ };
989
+ /**
990
+ * Convert video pixel dimensions to world-space meters
991
+ *
992
+ * @param pixelWidth - Video width in pixels
993
+ * @param pixelHeight - Video height in pixels
994
+ * @returns World-space dimensions in meters
995
+ */
996
+ export function pixelsToMeters(pixelWidth, pixelHeight) {
997
+ return {
998
+ width: pixelWidth / COORDINATE_SYSTEM.PIXELS_PER_METER,
999
+ height: pixelHeight / COORDINATE_SYSTEM.PIXELS_PER_METER
1000
+ };
1001
+ }
1002
+ /**
1003
+ * Convert world-space meters to video pixels
1004
+ *
1005
+ * @param meterWidth - Width in meters
1006
+ * @param meterHeight - Height in meters
1007
+ * @returns Pixel dimensions
1008
+ */
1009
+ export function metersToPixels(meterWidth, meterHeight) {
1010
+ return {
1011
+ width: meterWidth * COORDINATE_SYSTEM.PIXELS_PER_METER,
1012
+ height: meterHeight * COORDINATE_SYSTEM.PIXELS_PER_METER
1013
+ };
1014
+ }
1015
+ /**
1016
+ * Convert overlay percentage position (0-100) to world-space meters
1017
+ *
1018
+ * @param percentX - X position as percentage (0-100, left to right)
1019
+ * @param percentY - Y position as percentage (0-100, top to bottom)
1020
+ * @param videoWidthMeters - Video width in meters
1021
+ * @param videoHeightMeters - Video height in meters
1022
+ * @returns World-space position {x, y} in meters, centered at origin
1023
+ */
1024
+ export function percentToWorldSpace(percentX, percentY, videoWidthMeters, videoHeightMeters) {
1025
+ // Convert percentage (0-100) to normalized (-0.5 to 0.5)
1026
+ const normalizedX = (percentX - 50) / 100;
1027
+ const normalizedY = (50 - percentY) / 100; // Y inverted for screen coords
1028
+ return {
1029
+ x: normalizedX * videoWidthMeters,
1030
+ y: normalizedY * videoHeightMeters
1031
+ };
1032
+ }
1033
+ /**
1034
+ * Convert world-space meters to overlay percentage position (0-100)
1035
+ *
1036
+ * @param worldX - X position in meters (origin = center)
1037
+ * @param worldY - Y position in meters (origin = center)
1038
+ * @param videoWidthMeters - Video width in meters
1039
+ * @param videoHeightMeters - Video height in meters
1040
+ * @returns Percentage position {x, y} (0-100)
1041
+ */
1042
+ export function worldSpaceToPercent(worldX, worldY, videoWidthMeters, videoHeightMeters) {
1043
+ const normalizedX = worldX / videoWidthMeters;
1044
+ const normalizedY = worldY / videoHeightMeters;
1045
+ return {
1046
+ x: normalizedX * 100 + 50,
1047
+ y: 50 - normalizedY * 100 // Y inverted for screen coords
1048
+ };
1049
+ }
1050
+ /**
1051
+ * Convert Gausst tracking packet to world-space transform
1052
+ *
1053
+ * @param packet - Raw tracking data from hardware
1054
+ * @returns Position in meters, rotation in radians, FOV in degrees
1055
+ */
1056
+ export function trackingPacketToWorldSpace(packet) {
1057
+ const DEG_TO_RAD = Math.PI / 180;
1058
+ return {
1059
+ position: {
1060
+ x: packet.x / 100000, // mm×100 → meters
1061
+ y: packet.y / 100000,
1062
+ z: packet.z / 100000
1063
+ },
1064
+ rotation: {
1065
+ x: (packet.tilt / 1000) * DEG_TO_RAD, // Tilt (pitch)
1066
+ y: (-packet.pan / 1000) * DEG_TO_RAD, // Pan (yaw), negated per Gausst
1067
+ z: (packet.roll / 1000) * DEG_TO_RAD // Roll
1068
+ },
1069
+ fov: packet.fov / 1000
1070
+ };
1071
+ }
1072
+ /**
1073
+ * Convert FOV to focal length (for matching real camera optics)
1074
+ *
1075
+ * Formula from Gausst: focalLength = (aperture / 2) / tan(fov / 2) * 25.4
1076
+ *
1077
+ * @param fovDegrees - Field of view in degrees
1078
+ * @param filmApertureInches - Horizontal film aperture (default: 1.417" = 36mm)
1079
+ * @returns Focal length in millimeters
1080
+ */
1081
+ export function fovToFocalLength(fovDegrees, filmApertureInches = COORDINATE_SYSTEM.FILM_GATE.HORIZONTAL_APERTURE) {
1082
+ const fovRadians = fovDegrees * (Math.PI / 180);
1083
+ const apertureMM = filmApertureInches * 25.4;
1084
+ return (apertureMM / 2) / Math.tan(fovRadians / 2);
1085
+ }
1086
+ /**
1087
+ * Convert focal length to FOV
1088
+ *
1089
+ * @param focalLengthMM - Focal length in millimeters
1090
+ * @param filmApertureInches - Horizontal film aperture (default: 1.417" = 36mm)
1091
+ * @returns Field of view in degrees
1092
+ */
1093
+ export function focalLengthToFov(focalLengthMM, filmApertureInches = COORDINATE_SYSTEM.FILM_GATE.HORIZONTAL_APERTURE) {
1094
+ const apertureMM = filmApertureInches * 25.4;
1095
+ const fovRadians = 2 * Math.atan((apertureMM / 2) / focalLengthMM);
1096
+ return fovRadians * (180 / Math.PI);
1097
+ }
1098
+ // ============================================================================
1099
+ // Layer 2 (WebGL/3D) Module Re-exports
1100
+ // ============================================================================
1101
+ // Shader exports
1102
+ export { webcamVertexShader, webcamFragmentShader, getDefaultPlayerUniforms, getDefaultEditorUniforms } from './layer2/shaders';
1103
+ // Webcam utility exports
1104
+ export { WEBCAM_BASE_HEIGHT_METERS, SILHOUETTE_HEIGHT_RATIO, getWebcamBaseDimensions, getWebcamMeshYOffset, getObjectEuler, getCameraEuler, GAM_PIXELS_TO_METERS, AD_TEMPLATE_DIMENSIONS, getAdDimensions, CHROMA_KEY_COLORS, getChromaKeyColorHex, featherToUV, featherEdgesToUV } from './layer2/webcam-utils';
1105
+ // ============================================================================
1106
+ // Segmentation Module
1107
+ // ============================================================================
1108
+ // NOTE: MediaPipeSegmenter is NOT exported from main entry point because
1109
+ // @mediapipe/tasks-vision requires browser APIs and breaks SSR.
1110
+ // Import directly from '@ume/contracts/segmentation' in browser contexts:
1111
+ // import { MediaPipeSegmenter } from '@ume/contracts/segmentation';
1112
+ // ============================================================================