@clipwright/core 0.36.0 → 0.37.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 (88) hide show
  1. package/dist/actors.d.ts +8 -0
  2. package/dist/actors.js +2 -0
  3. package/dist/actors.js.map +1 -1
  4. package/dist/caption-geometry.d.ts +43 -0
  5. package/dist/caption-geometry.js +41 -0
  6. package/dist/caption-geometry.js.map +1 -0
  7. package/dist/contract-dispositions.d.ts +997 -35
  8. package/dist/contract-dispositions.js +25 -5
  9. package/dist/contract-dispositions.js.map +1 -1
  10. package/dist/contracts/v1/workflow-brief.d.ts +2024 -38
  11. package/dist/contracts/v1/workflow-brief.js +2 -2
  12. package/dist/contracts/v1/workflow-brief.js.map +1 -1
  13. package/dist/contracts/v1/workflow-catalog.d.ts +2154 -72
  14. package/dist/contracts/v1/workflow-client.d.ts +1207 -70
  15. package/dist/contracts/v1/workflow-plan.d.ts +3234 -111
  16. package/dist/edit-billing.d.ts +1 -0
  17. package/dist/edit-billing.js +2 -0
  18. package/dist/edit-billing.js.map +1 -1
  19. package/dist/edit-document.d.ts +22 -0
  20. package/dist/edit-document.js +3 -0
  21. package/dist/edit-document.js.map +1 -1
  22. package/dist/edit-long-form.js +1 -1
  23. package/dist/edit-long-form.js.map +1 -1
  24. package/dist/edit-revision.d.ts +9 -0
  25. package/dist/edit-rules.d.ts +1 -0
  26. package/dist/edit-rules.js +3 -0
  27. package/dist/edit-rules.js.map +1 -1
  28. package/dist/edit-shots.d.ts +2 -0
  29. package/dist/edit-validate.d.ts +4 -2
  30. package/dist/edit-validate.js +18 -2
  31. package/dist/edit-validate.js.map +1 -1
  32. package/dist/errors.d.ts +1 -0
  33. package/dist/faceless-media-record.d.ts +27 -0
  34. package/dist/faceless-media-record.js +15 -1
  35. package/dist/faceless-media-record.js.map +1 -1
  36. package/dist/faceless-public.d.ts +107 -0
  37. package/dist/faceless-public.js +34 -1
  38. package/dist/faceless-public.js.map +1 -1
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +4 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/long-form-revision.d.ts +8 -0
  43. package/dist/openapi.js +2 -2
  44. package/dist/openapi.js.map +1 -1
  45. package/dist/presenter-beats.d.ts +1 -1
  46. package/dist/reference-analysis.d.ts +436 -75
  47. package/dist/reference-analysis.js +149 -28
  48. package/dist/reference-analysis.js.map +1 -1
  49. package/dist/reference-display.d.ts +36 -0
  50. package/dist/reference-display.js +78 -0
  51. package/dist/reference-display.js.map +1 -0
  52. package/dist/reference-profile.d.ts +2352 -36
  53. package/dist/reference-profile.js +138 -10
  54. package/dist/reference-profile.js.map +1 -1
  55. package/dist/reference-staging.d.ts +59 -57
  56. package/dist/reference-staging.js +6 -5
  57. package/dist/reference-staging.js.map +1 -1
  58. package/dist/resolved-timeline.d.ts +199 -2
  59. package/dist/resolved-timeline.js +17 -5
  60. package/dist/resolved-timeline.js.map +1 -1
  61. package/dist/scene-place.d.ts +225 -0
  62. package/dist/scene-place.js +120 -0
  63. package/dist/scene-place.js.map +1 -0
  64. package/dist/skills.d.ts +4680 -22
  65. package/dist/skills.js +150 -17
  66. package/dist/skills.js.map +1 -1
  67. package/dist/templates/index.d.ts +14 -4
  68. package/dist/templates/index.js +49 -5
  69. package/dist/templates/index.js.map +1 -1
  70. package/dist/ugc-interview.d.ts +122 -0
  71. package/dist/ugc-interview.js +260 -0
  72. package/dist/ugc-interview.js.map +1 -0
  73. package/dist/ugc-layers.d.ts +88 -6
  74. package/dist/ugc-layers.js +193 -5
  75. package/dist/ugc-layers.js.map +1 -1
  76. package/dist/voice-admission.d.ts +947 -2
  77. package/dist/workflows/methods/faceless-explainer.d.ts +599 -13
  78. package/dist/workflows/methods/kinetic-launch.d.ts +599 -13
  79. package/dist/workflows/methods/product-image-story.d.ts +599 -13
  80. package/dist/workflows/methods/reference-remake.d.ts +599 -13
  81. package/dist/workflows/methods/shared.d.ts +599 -13
  82. package/dist/workflows/methods/ugc-faq-answer.d.ts +599 -13
  83. package/dist/workflows/methods/ugc-product-intro.d.ts +599 -13
  84. package/dist/workflows/methods/website-proof-demo.d.ts +599 -13
  85. package/dist/workflows/reference-remake.d.ts +131 -35
  86. package/dist/workflows/reference-remake.js +179 -18
  87. package/dist/workflows/reference-remake.js.map +1 -1
  88. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { type ReferenceProfile } from '../reference-profile.js';
2
+ import { type ReferenceProfileInput as ReferenceProfile } from '../reference-profile.js';
3
3
  import type { CapabilityProfile } from '../contracts/v1/workflow-catalog.js';
4
4
  import { type WorkflowPlanDocument } from '../contracts/v1/workflow-plan.js';
5
5
  import { type WorkflowIssue } from '../contracts/v1/workflow-primitives.js';
@@ -159,6 +159,43 @@ export declare const planReferenceRemake: z.ZodObject<{
159
159
  reacting: "reacting";
160
160
  speaking: "speaking";
161
161
  }>;
162
+ key_frame_s: z.ZodOptional<z.ZodNumber>;
163
+ frames_s: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
164
+ speaker: z.ZodOptional<z.ZodNullable<z.ZodString>>;
165
+ speakers: z.ZodOptional<z.ZodArray<z.ZodObject<{
166
+ id: z.ZodString;
167
+ seconds: z.ZodNumber;
168
+ }, z.core.$strict>>>;
169
+ gaze: z.ZodOptional<z.ZodObject<{
170
+ direction: z.ZodEnum<{
171
+ camera: "camera";
172
+ closed: "closed";
173
+ down: "down";
174
+ mixed: "mixed";
175
+ not_visible: "not_visible";
176
+ off_left: "off_left";
177
+ off_right: "off_right";
178
+ up: "up";
179
+ }>;
180
+ frames_s: z.ZodArray<z.ZodNumber>;
181
+ }, z.core.$strict>>;
182
+ gestures: z.ZodOptional<z.ZodArray<z.ZodObject<{
183
+ kind: z.ZodEnum<{
184
+ both_hands_to_viewer: "both_hands_to_viewer";
185
+ count_on_fingers: "count_on_fingers";
186
+ hold_object: "hold_object";
187
+ open_palms: "open_palms";
188
+ other: "other";
189
+ point_at_element: "point_at_element";
190
+ point_at_viewer: "point_at_viewer";
191
+ shrug: "shrug";
192
+ thumbs_up: "thumbs_up";
193
+ wave: "wave";
194
+ }>;
195
+ at_s: z.ZodNumber;
196
+ label: z.ZodString;
197
+ frames_s: z.ZodArray<z.ZodNumber>;
198
+ }, z.core.$strict>>>;
162
199
  }, z.core.$strict>>;
163
200
  captions: z.ZodObject<{
164
201
  present: z.ZodBoolean;
@@ -176,6 +213,12 @@ export declare const planReferenceRemake: z.ZodObject<{
176
213
  punch: "punch";
177
214
  }>;
178
215
  hidden_under_graphics: z.ZodBoolean;
216
+ box: z.ZodOptional<z.ZodObject<{
217
+ left: z.ZodNumber;
218
+ top: z.ZodNumber;
219
+ width: z.ZodNumber;
220
+ height: z.ZodNumber;
221
+ }, z.core.$strict>>;
179
222
  }, z.core.$strict>;
180
223
  pacing: z.ZodObject<{
181
224
  layer_change_every_s: z.ZodNumber;
@@ -188,12 +231,6 @@ export declare const planReferenceRemake: z.ZodObject<{
188
231
  beat_index: z.ZodNumber;
189
232
  suggested_substitute: z.ZodString;
190
233
  }, z.core.$strict>>;
191
- analysis: z.ZodObject<{
192
- models: z.ZodArray<z.ZodString>;
193
- cost_usd: z.ZodOptional<z.ZodNumber>;
194
- latency_ms: z.ZodNumber;
195
- warnings: z.ZodOptional<z.ZodArray<z.ZodString>>;
196
- }, z.core.$strict>;
197
234
  voice: z.ZodOptional<z.ZodObject<{
198
235
  version: z.ZodLiteral<"voice-profile-v1">;
199
236
  method: z.ZodObject<{
@@ -390,13 +427,13 @@ export declare const planReferenceRemake: z.ZodObject<{
390
427
  height: z.ZodNumber;
391
428
  }, z.core.$strict>>;
392
429
  rect_method: z.ZodOptional<z.ZodEnum<{
393
- "cv-before-after/v1": "cv-before-after/v1";
394
- "cv-cells/v1": "cv-cells/v1";
395
- "cv-parallax/v1": "cv-parallax/v1";
396
- "cv-template/v1": "cv-template/v1";
397
- "vision-still": "vision-still";
398
- "vision-video": "vision-video";
399
- "yunet-native/v1": "yunet-native/v1";
430
+ "estimated-still": "estimated-still";
431
+ "estimated-video": "estimated-video";
432
+ "frames-box/v1": "frames-box/v1";
433
+ "frames-face/v1": "frames-face/v1";
434
+ "frames-layer/v1": "frames-layer/v1";
435
+ "frames-motion/v1": "frames-motion/v1";
436
+ "frames-split/v1": "frames-split/v1";
400
437
  }>>;
401
438
  rects: z.ZodOptional<z.ZodArray<z.ZodObject<{
402
439
  t_s: z.ZodNumber;
@@ -441,13 +478,13 @@ export declare const planReferenceRemake: z.ZodObject<{
441
478
  displacement: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
442
479
  scale_from: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
443
480
  method: z.ZodOptional<z.ZodEnum<{
444
- "cv-before-after/v1": "cv-before-after/v1";
445
- "cv-cells/v1": "cv-cells/v1";
446
- "cv-parallax/v1": "cv-parallax/v1";
447
- "cv-template/v1": "cv-template/v1";
448
- "vision-still": "vision-still";
449
- "vision-video": "vision-video";
450
- "yunet-native/v1": "yunet-native/v1";
481
+ "estimated-still": "estimated-still";
482
+ "estimated-video": "estimated-video";
483
+ "frames-box/v1": "frames-box/v1";
484
+ "frames-face/v1": "frames-face/v1";
485
+ "frames-layer/v1": "frames-layer/v1";
486
+ "frames-motion/v1": "frames-motion/v1";
487
+ "frames-split/v1": "frames-split/v1";
451
488
  }>>;
452
489
  }, z.core.$strict>>;
453
490
  exit: z.ZodOptional<z.ZodObject<{
@@ -483,13 +520,13 @@ export declare const planReferenceRemake: z.ZodObject<{
483
520
  duration_s: z.ZodOptional<z.ZodNumber>;
484
521
  displacement: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
485
522
  method: z.ZodOptional<z.ZodEnum<{
486
- "cv-before-after/v1": "cv-before-after/v1";
487
- "cv-cells/v1": "cv-cells/v1";
488
- "cv-parallax/v1": "cv-parallax/v1";
489
- "cv-template/v1": "cv-template/v1";
490
- "vision-still": "vision-still";
491
- "vision-video": "vision-video";
492
- "yunet-native/v1": "yunet-native/v1";
523
+ "estimated-still": "estimated-still";
524
+ "estimated-video": "estimated-video";
525
+ "frames-box/v1": "frames-box/v1";
526
+ "frames-face/v1": "frames-face/v1";
527
+ "frames-layer/v1": "frames-layer/v1";
528
+ "frames-motion/v1": "frames-motion/v1";
529
+ "frames-split/v1": "frames-split/v1";
493
530
  }>>;
494
531
  }, z.core.$strict>>;
495
532
  layer: z.ZodOptional<z.ZodObject<{
@@ -501,13 +538,13 @@ export declare const planReferenceRemake: z.ZodObject<{
501
538
  unknown: "unknown";
502
539
  }>;
503
540
  method: z.ZodEnum<{
504
- "cv-before-after/v1": "cv-before-after/v1";
505
- "cv-cells/v1": "cv-cells/v1";
506
- "cv-parallax/v1": "cv-parallax/v1";
507
- "cv-template/v1": "cv-template/v1";
508
- "vision-still": "vision-still";
509
- "vision-video": "vision-video";
510
- "yunet-native/v1": "yunet-native/v1";
541
+ "estimated-still": "estimated-still";
542
+ "estimated-video": "estimated-video";
543
+ "frames-box/v1": "frames-box/v1";
544
+ "frames-face/v1": "frames-face/v1";
545
+ "frames-layer/v1": "frames-layer/v1";
546
+ "frames-motion/v1": "frames-motion/v1";
547
+ "frames-split/v1": "frames-split/v1";
511
548
  }>;
512
549
  covered_by: z.ZodArray<z.ZodEnum<{
513
550
  body: "body";
@@ -610,11 +647,70 @@ export declare const planReferenceRemake: z.ZodObject<{
610
647
  end_s: z.ZodNumber;
611
648
  speaker: z.ZodOptional<z.ZodString>;
612
649
  }, z.core.$strict>>>;
650
+ transcript_language: z.ZodOptional<z.ZodNullable<z.ZodString>>;
613
651
  hook: z.ZodOptional<z.ZodObject<{
614
652
  text: z.ZodString;
615
653
  end_s: z.ZodNumber;
616
654
  caption: z.ZodOptional<z.ZodString>;
617
655
  }, z.core.$strict>>;
656
+ speakers: z.ZodOptional<z.ZodArray<z.ZodObject<{
657
+ id: z.ZodString;
658
+ on_screen: z.ZodBoolean;
659
+ role: z.ZodEnum<{
660
+ guest: "guest";
661
+ interviewer: "interviewer";
662
+ narrator: "narrator";
663
+ other: "other";
664
+ presenter: "presenter";
665
+ }>;
666
+ word_count: z.ZodNumber;
667
+ caption: z.ZodNullable<z.ZodObject<{
668
+ colour: z.ZodNullable<z.ZodString>;
669
+ style: z.ZodString;
670
+ position: z.ZodNullable<z.ZodEnum<{
671
+ bottom: "bottom";
672
+ lower: "lower";
673
+ middle: "middle";
674
+ top: "top";
675
+ }>>;
676
+ box: z.ZodOptional<z.ZodObject<{
677
+ left: z.ZodNumber;
678
+ top: z.ZodNumber;
679
+ width: z.ZodNumber;
680
+ height: z.ZodNumber;
681
+ }, z.core.$strict>>;
682
+ }, z.core.$strict>>;
683
+ frames_s: z.ZodArray<z.ZodNumber>;
684
+ }, z.core.$strict>>>;
685
+ segments: z.ZodOptional<z.ZodArray<z.ZodObject<{
686
+ start_s: z.ZodNumber;
687
+ end_s: z.ZodNumber;
688
+ text: z.ZodString;
689
+ speaker: z.ZodString;
690
+ mouth: z.ZodOptional<z.ZodObject<{
691
+ vision: z.ZodNullable<z.ZodEnum<{
692
+ closed: "closed";
693
+ moving: "moving";
694
+ not_visible: "not_visible";
695
+ }>>;
696
+ measured: z.ZodEnum<{
697
+ moving: "moving";
698
+ not_measured: "not_measured";
699
+ still: "still";
700
+ }>;
701
+ score: z.ZodNullable<z.ZodNumber>;
702
+ }, z.core.$strict>>;
703
+ frames_s: z.ZodArray<z.ZodNumber>;
704
+ }, z.core.$strict>>>;
705
+ spellings: z.ZodOptional<z.ZodArray<z.ZodObject<{
706
+ heard: z.ZodString;
707
+ shown: z.ZodString;
708
+ start_s: z.ZodNumber;
709
+ end_s: z.ZodNumber;
710
+ }, z.core.$strict>>>;
711
+ analysis: z.ZodObject<{
712
+ warnings: z.ZodOptional<z.ZodArray<z.ZodString>>;
713
+ }, z.core.$strip>;
618
714
  }, z.core.$strict>;
619
715
  script: z.ZodString;
620
716
  actor_id: z.ZodString;
@@ -5,13 +5,15 @@ import { assetId } from '../asset-identity.js';
5
5
  import { captionLanguageOf } from '../caption-language.js';
6
6
  import { LONG_FORM_QUALIFICATION_MAX_DURATION_SEC } from '../long-form-profile.js';
7
7
  import { estimateDurationSec } from '../pacing.js';
8
- import { referenceProfile } from '../reference-profile.js';
8
+ import { referenceProfileInput } from '../reference-profile.js';
9
9
  import { narrationPaceFromVoice, PAUSE_TRIM_CEILING_S, pauseTrimFromVoice } from '../reference-voice.js';
10
10
  import { countActorShots, coverWordOf, lexicalTokens, planSemanticInserts, quoteOccurrences } from '../semantic-inserts.js';
11
11
  import { actorId, callStage, isNarratedUgcInput, makeUgcInput, makeUgcInputShape, MAX_NARRATED_ACTOR_SHOTS, ugcCall } from '../skills.js';
12
12
  import { CALL_PHONE_DEFAULTS, CALL_STAGE_SIDES } from '../ugc-call.js';
13
- import { templateFaceWarnings, templateMediaOf, templateTextRefusal } from '../templates/index.js';
14
- import { faceZone, frameCanvasOf, OVERLAY_POSITIONS, overlayRect, rectsOverlap } from '../ugc-layers.js';
13
+ import { TEMPLATE_KINDS, templateFaceWarnings, templateMediaOf, templateTextRefusal } from '../templates/index.js';
14
+ import { CAPTION_POSITION_VERTICAL, captionEdgeAt } from '../caption-geometry.js';
15
+ import { fitPlacedRect, place, plannedFace, presenterPlace, resolvePlace, sharedArea } from '../scene-place.js';
16
+ import { FACE_COVER_SHARE, faceZone, frameCanvasOf, LAYER_ENTERS, MAX_ENTER_OFFSET_S, MAX_OVERLAYS, OVERLAY_POSITIONS, overlayRect, ownPixels, rectsOverlap } from '../ugc-layers.js';
15
17
  import { isEnglishScript, measuredPaceOf, PACE_MATCH_SHARE, VOICE_PACE_RANGE, voicesNearestPace } from '../voice-pace.js';
16
18
  import { BREAK_TAG_SOURCE, FIXED_PACE_MODELS, resolveTtsModel } from '../voices.js';
17
19
  import { workflowPlanDocument } from '../contracts/v1/workflow-plan.js';
@@ -19,7 +21,7 @@ import { workflowText } from '../contracts/v1/workflow-primitives.js';
19
21
  import { queryWorkflowMethod } from './catalog.js';
20
22
  // The reference-remake compiler (#857 S2a): a profile and an accepted script become a complete plan whose native input is a make_ugc input.
21
23
  export const planReferenceRemake = z.strictObject({
22
- reference_profile: referenceProfile.describe('The reference_profile analyze_reference returned, or one you wrote to the same schema.'),
24
+ reference_profile: referenceProfileInput.describe('The reference_profile analyze_reference returned, or one you wrote to the same schema.'),
23
25
  script: workflowText(10_000).describe('The accepted narration, copied into the plan unchanged.'),
24
26
  actor_id: actorId, voice: makeUgcInputShape.voice, voice_id: makeUgcInputShape.voice_id,
25
27
  assets: z.array(z.strictObject({ asset_id: assetId, media_type: z.enum(['image', 'video']) })).max(12).default([])
@@ -37,6 +39,34 @@ const MARKER = 'ref-long-form';
37
39
  const GENERATE = 'pass a ready ast_* asset, or make one separately with generate_image or generate_video (its own quote), then compile again';
38
40
  /** The saved plan keeps the reference's techniques, not its words: the remake copies no line of it, and the analysis keeps them already. */
39
41
  const withoutWords = ({ transcript: _transcript, hook: _hook, ...profile }) => profile;
42
+ /** A layer sits under the captions when its top is at most this share of the height below them and its centre this share of the width off theirs. */
43
+ const UNDER_CAPTIONS = 0.03;
44
+ /** With no caption box measured, a centred layer whose top lies this far below where top-anchored captions start: a line to a short block. */
45
+ const UNMEASURED_CAPTIONS_BELOW = [0.03, 0.15];
46
+ const ENTER_OF = { pop: 'pop', fade: 'fade', slide: 'slide', scale_up: 'scale_up', tilt_in: 'tilt_in' };
47
+ const EXIT_OF = { cut: 'cut', fade: 'fade', slide: 'slide', pop: 'pop_out' };
48
+ const FRAME = { left: 0, top: 0, width: 1, height: 1 };
49
+ const round = (value, places = 4) => Math.round(value * 10 ** places) / 10 ** places;
50
+ const median = (values) => {
51
+ const sorted = [...values].sort((a, b) => a - b), mid = sorted.length >> 1;
52
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
53
+ };
54
+ /** The reference presenter's face while its graphics show (#973 S5): each element's box over its face-unit twin gives the face; freezes stand in. */
55
+ function referenceFaceOf(profile) {
56
+ const staging = profile.staging, { width, height } = profile.source;
57
+ if (!staging)
58
+ return undefined;
59
+ const twins = staging.elements.flatMap(({ rect_hold: rect, rect_hold_face: twin }) => {
60
+ if (!rect || !twin)
61
+ return [];
62
+ const unit = rect.height / twin.height;
63
+ return [{ height: unit, x: rect.left - (twin.left * unit * height) / width, y: rect.top - twin.top * unit }];
64
+ });
65
+ const faces = twins.length ? twins : staging.freezes.map(({ face_box_start: box }) => ({ height: box.height, x: box.left + box.width / 2, y: box.top + box.height / 2 }));
66
+ if (!faces.length)
67
+ return undefined;
68
+ return { face_height: round(median(faces.map(face => face.height))), face_centre: [round(median(faces.map(face => face.x))), round(median(faces.map(face => face.y)))] };
69
+ }
40
70
  export function referenceScriptWords(script) {
41
71
  const tags = [...script.matchAll(new RegExp(BREAK_TAG_SOURCE, 'g'))].map(match => [match.index, match.index + match[0].length]);
42
72
  const words = [];
@@ -215,6 +245,81 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
215
245
  return ranges.filter(range => words.slice(range.start, range.end).some(word => !word.tag));
216
246
  };
217
247
  const wordAt = (span, share) => quote(Math.min(span.end - 1, span.start + Math.max(1, Math.floor(share * (span.end - span.start)))), 2, span.end);
248
+ // Measured staging drawn as the reference drew it (#973 S5): a box under the captions, on the face or on the frame, its entry from our list.
249
+ const canvas = frameCanvasOf(DEFAULT_ASPECT_RATIO), measuredFace = referenceFaceOf(profile);
250
+ const presenterOk = longForm && executor?.field_dispositions.presenter_place === 'implemented';
251
+ const presenter = measuredFace && presenterOk ? presenterPlace.safeParse(measuredFace).data : undefined;
252
+ if (measuredFace && !presenter) {
253
+ note(0, `the reference presenter's face, ${measuredFace.face_height} of the frame's height`, !longForm
254
+ ? 'the actor\'s own framing: framing the presenter needs long-form qualification, not available right now'
255
+ : presenterOk ? 'the actor\'s own framing: the face size is outside what the presenter can be framed to' : 'the actor\'s own framing');
256
+ }
257
+ const face = plannedFace(DEFAULT_ASPECT_RATIO, canvas, framing, presenter);
258
+ /** Elements whose staging the plan draws or names on its own; the rest get the one note below. */
259
+ const drawn = new Set();
260
+ const gaps = { entry: [], exit: [], styles: new Map(), templates: [], inferred: [] };
261
+ // A box never measured on the frames is an estimate, and a note says so (#930 S2).
262
+ const sourceOf = (element) => element.rect_method?.startsWith('estimated') ? 'vision-pass' : 'measured';
263
+ const elementOf = (beat, graphic) => profile.staging?.elements.find(entry => entry.beat_index === beat && entry.graphic_index === graphic && entry.rect_hold);
264
+ const captionsSeen = profile.captions.present && !profile.captions.hidden_under_graphics;
265
+ const underCaptions = (rect) => {
266
+ const centre = rect.left + rect.width / 2, box = profile.captions.box;
267
+ if (!captionsSeen)
268
+ return undefined;
269
+ if (box) {
270
+ const gap = rect.top - box.top - box.height;
271
+ return Math.abs(centre - box.left - box.width / 2) <= UNDER_CAPTIONS && gap >= -0.005 && gap <= UNDER_CAPTIONS ? { gap: round(Math.max(0, gap)) } : undefined;
272
+ }
273
+ // No box: caption lines are centred, and a top-anchored block starts at its position's height while a bottom-anchored one ends there.
274
+ const at = CAPTION_POSITION_VERTICAL[profile.captions.position] / 100, topEdge = captionEdgeAt(at * 100) === 'top';
275
+ const [low, high] = topEdge ? [at + UNMEASURED_CAPTIONS_BELOW[0], at + UNMEASURED_CAPTIONS_BELOW[1]] : [at - 0.005, at + UNDER_CAPTIONS];
276
+ return Math.abs(centre - 0.5) <= UNDER_CAPTIONS && rect.top >= low && rect.top <= high ? {} : undefined;
277
+ };
278
+ /** The element's place on our presenter, or 'face' when it would cover our face; `own` is a template's own rect, which never stands by captions. */
279
+ const stagedPlace = (element, own) => {
280
+ const rect = element.rect_hold, under = own ? undefined : underCaptions(rect);
281
+ const parsed = place.safeParse(under ? { relative_to: 'captions', side: 'below', ...(under.gap === undefined ? {} : { gap: under.gap }), width: round(rect.width) }
282
+ : element.rect_hold_face ? { relative_to: 'face', rect: element.rect_hold_face } : { relative_to: 'frame', rect });
283
+ if (!parsed.success)
284
+ return undefined;
285
+ if (under)
286
+ return { place: parsed.data, inferred: under.gap === undefined };
287
+ const box = resolvePlace(parsed.data, { canvas, face }), shown = own ? fitPlacedRect(box, ownPixels(own, canvas), canvas) : box;
288
+ if (sharedArea(shown, FRAME) < (shown.width * shown.height) / 2)
289
+ return undefined;
290
+ return sharedArea(shown, face) > FACE_COVER_SHARE * face.width * face.height ? 'face' : { place: parsed.data, inferred: false };
291
+ };
292
+ /** The word the reference showed it on, at the same share of our beat's words, and its measured lead or lag on that word. */
293
+ const enterOf = (element, beat, span) => {
294
+ const shown = element.entry?.first_visible_s, source = profile.beats[beat];
295
+ if (shown === undefined)
296
+ return {};
297
+ const said = (profile.transcript ?? []).filter(word => word.start_s >= source.start_s && word.start_s < source.end_s);
298
+ let at = 0;
299
+ said.forEach((word, index) => { if (word.start_s <= shown)
300
+ at = index; });
301
+ const share = said.length ? (at + 0.5) / said.length : Math.min(1, Math.max(0, (shown - source.start_s) / (source.end_s - source.start_s)));
302
+ const found = quote(span.start + Math.min(span.end - span.start - 1, Math.floor(share * (span.end - span.start))), 2, span.end);
303
+ if (!found)
304
+ return {};
305
+ const offset = said.length ? round(Math.min(MAX_ENTER_OFFSET_S, Math.max(-MAX_ENTER_OFFSET_S, shown - said[at].start_s)), 2) : 0;
306
+ return { anchor: found.anchor, cover_words: span.end - found.start, ...(offset ? { enter_offset_s: offset } : {}) };
307
+ };
308
+ const motionOf = (element, beat) => {
309
+ const style = element.entry?.style, leave = element.exit?.style, enter = style && ENTER_OF[style], exit = leave && EXIT_OF[leave];
310
+ const unmapped = (what) => gaps.styles.set(what, [...gaps.styles.get(what) ?? [], beat]);
311
+ if (!style || style === 'unknown')
312
+ gaps.entry.push(beat);
313
+ else if (!enter)
314
+ unmapped(`${style} entries`);
315
+ if (!leave || leave === 'unknown')
316
+ gaps.exit.push(beat);
317
+ else if (!exit)
318
+ unmapped(`${leave} exits`);
319
+ const duration = element.entry?.duration_s;
320
+ return { ...(enter ? { enter, ...(duration ? { enter_duration_s: Math.min(1.5, Math.max(0.05, round(duration, 2))) } : {}) } : {}),
321
+ ...(exit && exit !== 'cut' ? { exit } : {}) };
322
+ };
218
323
  const spans = planSpans({ profile, words });
219
324
  const layers = [];
220
325
  const pool = [...request.assets], images = () => pool.filter(asset => asset.media_type === 'image').length;
@@ -242,11 +347,22 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
242
347
  const kind = graphic.kind;
243
348
  if (kind === 'particles' || kind === 'unmapped')
244
349
  continue;
245
- if (kind in MEDIA_KINDS) {
350
+ // A logo card is a brand picture: it shows as an overlay while an image is left in assets, else as the text card it maps to (#973 S5).
351
+ const room = layers.filter(layer => layer.field === 'overlays').length < MAX_OVERLAYS;
352
+ const wantsLogo = graphic.source_kind === 'logo_card' && longForm && !layered, logo = wantsLogo && images() > 0 && room;
353
+ if (wantsLogo && !logo) {
354
+ note(index, `logo_card (${room ? 'no image left in assets' : `a run shows at most ${MAX_OVERLAYS} overlays`})`, 'a text card with its words');
355
+ }
356
+ if (kind in MEDIA_KINDS || logo) {
357
+ const media = logo ? 'logo_card' : kind;
246
358
  if (layered) {
247
359
  note(index, `${kind} (one media layer per beat)`, 'the first one shows here');
248
360
  continue;
249
361
  }
362
+ if (MEDIA_KINDS[kind] === 'overlays' && !room) {
363
+ note(index, `${kind} (a run shows at most ${MAX_OVERLAYS} overlays)`, 'the presenter stays on screen on these words');
364
+ continue;
365
+ }
250
366
  if (pool.length === 0) {
251
367
  note(index, `${kind} (no image or video left in assets)`, GENERATE);
252
368
  continue;
@@ -255,16 +371,28 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
255
371
  note(index, `${kind} (media layers need long-form qualification, not available right now)`, 'the presenter stays on screen on these words');
256
372
  continue;
257
373
  }
258
- const asset = take(), field = MEDIA_KINDS[kind], base = { ...envelope(kind), asset_id: asset.asset_id, media_type: asset.media_type };
259
- const { kind: _kind, ...item } = base;
374
+ const asset = take(logo), field = logo ? 'overlays' : MEDIA_KINDS[kind];
375
+ const { kind: _kind, ...item } = { ...envelope(media), asset_id: asset.asset_id, media_type: asset.media_type };
376
+ const element = field === 'overlays' ? elementOf(index, beat.graphics.indexOf(graphic)) : undefined, staged = element && stagedPlace(element);
377
+ if (element && staged)
378
+ drawn.add(element.id);
379
+ layered = true;
380
+ if (element && staged && staged !== 'face') {
381
+ if (staged.inferred)
382
+ gaps.inferred.push(index);
383
+ layers.push({ field, beat: index, kind: media, item: { ...item, ...enterOf(element, index, span), ...motionOf(element, index), place: staged.place } });
384
+ continue;
385
+ }
260
386
  const position = graphic.position.replace(/^split_/, ''), wanted = OVERLAY_POSITIONS.includes(position) ? position : 'top_right';
261
387
  // Off the face: the reference place, else a corner on its side (right by default), a band last.
262
388
  const side = wanted.endsWith('left') ? 'left' : 'right', rank = (place) => place.endsWith(side) ? 0 : place.includes('_') ? 1 : 2;
263
389
  const clear = (place) => !rectsOverlap(overlayRect(place, frameCanvasOf(DEFAULT_ASPECT_RATIO)), faceZone(DEFAULT_ASPECT_RATIO, framing));
264
390
  const placed = clear(wanted) ? wanted : [...OVERLAY_POSITIONS].sort((left, right) => rank(left) - rank(right)).find(clear) ?? wanted;
265
- if (field === 'overlays' && placed !== position)
266
- note(index, `${kind} at ${graphic.position}`, `${placed}, to keep the face clear`);
267
- layers.push({ field, beat: index, kind, item: field === 'overlays' ? { ...item, position: placed }
391
+ if (staged === 'face')
392
+ note(index, `${media}: its ${sourceOf(element)} place, over the presenter's face`, `${placed}, to keep the face clear`);
393
+ else if (field === 'overlays' && placed !== position)
394
+ note(index, `${media} at ${graphic.position}`, `${placed}, to keep the face clear`);
395
+ layers.push({ field, beat: index, kind: media, item: field === 'overlays' ? { ...item, position: placed }
268
396
  : field === 'layout_windows' ? { ...item, layout: 'split' } : item });
269
397
  layered = true;
270
398
  fullFrame ||= field === 'inserts';
@@ -289,9 +417,25 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
289
417
  if ((built.kind === 'phone_call' || built.kind === 'notification') && /[^\p{Script=Latin}\p{N}\p{P}\p{S}\s]/u.test(script)) {
290
418
  note(index, `${built.kind} card text stays English ("${built.kind === 'phone_call' ? 'Incoming call' : 'Messages'}")`, 'edit the plan to change it');
291
419
  }
292
- const item = { ...envelope(built.kind), ...(built.placement ? { placement: built.placement } : {}), data: built.data };
420
+ const item = { ...envelope(built.kind),
421
+ ...(built.placement ? { placement: built.placement } : {}), data: built.data };
422
+ // A measured box over the presenter takes the template whole, drawn as its kind draws with no place of its own (#973 S4, S5).
423
+ const element = elementOf(index, beat.graphics.indexOf(graphic)), definition = TEMPLATE_KINDS.find(entry => entry.kind === built.kind);
424
+ const { position: _place, ...unplaced } = built.data, own = definition?.data.safeParse(unplaced);
425
+ const staged = element && own?.success && (built.placement ?? definition.placements[0]) === 'over'
426
+ ? stagedPlace(element, definition.rect(own.data, DEFAULT_ASPECT_RATIO, undefined, framing)) : undefined;
427
+ if (element && staged)
428
+ drawn.add(element.id);
429
+ if (staged)
430
+ item.data = unplaced;
431
+ if (staged === 'face')
432
+ note(index, `${built.kind}: its ${sourceOf(element)} place, over the presenter's face`, `the ${built.kind}'s default place, to keep the face clear`);
433
+ else if (staged) {
434
+ item.place = staged.place;
435
+ gaps.templates.push(index);
436
+ }
293
437
  // A reference place over the face gives way to the kind's default, which every kind keeps off it.
294
- if ('position' in built.data && templateFaceWarnings({ templates: [item], presenter_framing: framing }).length) {
438
+ if (!item.place && 'position' in built.data && templateFaceWarnings({ templates: [item], presenter_framing: framing }).length) {
295
439
  const { position: _position, ...data } = built.data;
296
440
  item.data = data;
297
441
  note(index, `${built.kind} at ${graphic.position}`, `the ${built.kind}'s default place, to keep the face clear`);
@@ -418,20 +562,35 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
418
562
  phone: { ...CALL_PHONE_DEFAULTS, ...request.call.phone }, ...(stage ? { stage } : {}) };
419
563
  for (const layer of layers.filter(layer => layer.beat === listening && layer.kind === 'phone_call'))
420
564
  layers.splice(layers.indexOf(layer), 1);
565
+ // The call's own phone draws the staging, so the dropped card's word on it goes too.
566
+ for (const entry of notes.filter(entry => entry.beat_index === listening && entry.what.startsWith('phone_call: its ')))
567
+ notes.splice(notes.indexOf(entry), 1);
421
568
  }
422
- // Measured staging the composition draws only for the call's phone: any other measured graphic the plan places keeps its default place, and says
423
- // so once per beat and kind (rule 3).
569
+ // Measured staging is drawn for overlays, templates over the presenter and the call's phone: any other measured graphic the plan places keeps its
570
+ // default place, and says so once per beat and kind (rule 3).
424
571
  const placed = new Set(layers.filter(layer => layer.field !== 'framing' && layer.field !== 'caption_emphasis').map(layer => layer.beat));
425
572
  const named = new Set();
426
573
  for (const element of profile.staging?.elements ?? []) {
427
574
  const graphic = element.graphic_index === null ? undefined : profile.beats[element.beat_index]?.graphics[element.graphic_index];
428
575
  const key = `${element.beat_index}:${graphic?.kind}`;
429
- if (!graphic || !placed.has(element.beat_index) || named.has(key) || (call && element.beat_index === listening && element.kind === 'phone_call'))
576
+ if (!graphic || !placed.has(element.beat_index) || named.has(key) || drawn.has(element.id)
577
+ || (call && element.beat_index === listening && element.kind === 'phone_call'))
430
578
  continue;
431
579
  named.add(key);
432
- // A box CV never measured comes from the vision pass, and the note says so (#930 S2).
433
- const source = element.rect_method?.startsWith('vision') ? 'vision-pass' : 'measured';
434
- note(element.beat_index, `${graphic.kind}: its ${source} place and entry (reference staging)`, 'its default place; measured staging is drawn for the call\'s phone only');
580
+ note(element.beat_index, `${graphic.kind}: its ${sourceOf(element)} place and entry (reference staging)`, 'its default place; measured staging is drawn for overlays, templates over the presenter and the call\'s phone');
581
+ }
582
+ const beatsOf = (list) => [...new Set(list)].map(beat => beat + 1).join(', ');
583
+ if (gaps.entry.length)
584
+ note(gaps.entry[0], `how the overlays on beats ${beatsOf(gaps.entry)} enter (reference staging did not measure it)`, 'a cut');
585
+ if (gaps.exit.length)
586
+ note(gaps.exit[0], `how the overlays on beats ${beatsOf(gaps.exit)} leave (reference staging did not measure it)`, 'a cut');
587
+ for (const [what, list] of gaps.styles) {
588
+ note(list[0], `${what} of the overlays on beats ${beatsOf(list)}`, what.endsWith('exits') ? 'a cut' : `a cut: overlays enter by ${LAYER_ENTERS.join(', ')}`);
589
+ }
590
+ if (gaps.templates.length)
591
+ note(gaps.templates[0], `the measured entry of the templates on beats ${beatsOf(gaps.templates)}`, 'each template\'s own entry');
592
+ if (gaps.inferred.length) {
593
+ note(gaps.inferred[0], `the reference's caption box, for the graphics on beats ${beatsOf(gaps.inferred)} (not measured)`, `placed under the captions: each sits centred just below where the reference's ${profile.captions.position} captions are`);
435
594
  }
436
595
  const aggregate = (values, keep, what, substitute) => {
437
596
  for (const value of new Set(values.filter((entry) => entry !== undefined && keep(entry)))) {
@@ -504,6 +663,8 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
504
663
  ...(captionsPlaced ? { caption_position: profile.captions.position } : {}) } : {}),
505
664
  ...field('caption_emphasis'), ...field('templates'), ...field('overlays'), ...field('inserts'), ...field('layout_windows'), ...field('framing'),
506
665
  ...(call ? { call } : {}) };
666
+ if (presenter && isNarratedUgcInput(native))
667
+ native.presenter_place = presenter;
507
668
  // The quote refuses customer media without this policy; the note below discloses it (rule 3).
508
669
  if (['overlays', 'inserts', 'layout_windows'].some(name => native[name]) || templateMediaOf(native.templates).length) {
509
670
  native.broll_policy = 'anyone';