@clipwright/core 0.40.0 → 0.42.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.
Files changed (90) hide show
  1. package/dist/account-actors.d.ts +1 -0
  2. package/dist/account-actors.js +33 -31
  3. package/dist/account-actors.js.map +1 -1
  4. package/dist/actor-bounds.d.ts +3 -3
  5. package/dist/actor-bounds.js +3 -3
  6. package/dist/actor-bounds.js.map +1 -1
  7. package/dist/actors.d.ts +5 -0
  8. package/dist/actors.js +22 -2
  9. package/dist/actors.js.map +1 -1
  10. package/dist/contract-dispositions.d.ts +5 -0
  11. package/dist/contracts/v1/workflow-brief.d.ts +12 -0
  12. package/dist/contracts/v1/workflow-brief.js +5 -2
  13. package/dist/contracts/v1/workflow-brief.js.map +1 -1
  14. package/dist/contracts/v1/workflow-catalog.d.ts +10 -0
  15. package/dist/contracts/v1/workflow-client.d.ts +10 -0
  16. package/dist/contracts/v1/workflow-plan.d.ts +15 -0
  17. package/dist/faceless-price.d.ts +2 -2
  18. package/dist/faceless-price.js +2 -2
  19. package/dist/faceless-price.js.map +1 -1
  20. package/dist/faceless-public.js +5 -5
  21. package/dist/faceless-public.js.map +1 -1
  22. package/dist/generations.d.ts +1 -1
  23. package/dist/generations.js +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/live-shot-kinds.d.ts +12 -0
  28. package/dist/live-shot-kinds.js +11 -0
  29. package/dist/live-shot-kinds.js.map +1 -1
  30. package/dist/long-form-profile.d.ts +1 -1
  31. package/dist/long-form-profile.js +1 -1
  32. package/dist/motion-styles.d.ts +1 -1
  33. package/dist/motion-styles.js +1 -1
  34. package/dist/motion-styles.js.map +1 -1
  35. package/dist/moves.d.ts +1 -1
  36. package/dist/moves.js +4 -3
  37. package/dist/moves.js.map +1 -1
  38. package/dist/music.d.ts +1 -1
  39. package/dist/music.js +1 -1
  40. package/dist/presenter-takes.d.ts +10 -0
  41. package/dist/presenter-takes.js +59 -0
  42. package/dist/presenter-takes.js.map +1 -0
  43. package/dist/reference-profile.d.ts +4 -1
  44. package/dist/reference-profile.js +33 -9
  45. package/dist/reference-profile.js.map +1 -1
  46. package/dist/resolved-timeline.js +1 -1
  47. package/dist/run-static-warnings.js +1 -1
  48. package/dist/run-static-warnings.js.map +1 -1
  49. package/dist/scene-graph.d.ts +8 -0
  50. package/dist/scene-graph.js +35 -0
  51. package/dist/scene-graph.js.map +1 -1
  52. package/dist/scene-place.d.ts +3 -0
  53. package/dist/scene-place.js +3 -0
  54. package/dist/scene-place.js.map +1 -1
  55. package/dist/skills.d.ts +43 -8
  56. package/dist/skills.js +45 -14
  57. package/dist/skills.js.map +1 -1
  58. package/dist/templates/index.d.ts +9 -1
  59. package/dist/templates/index.js +15 -5
  60. package/dist/templates/index.js.map +1 -1
  61. package/dist/ugc-call.d.ts +4 -4
  62. package/dist/ugc-call.js +4 -4
  63. package/dist/ugc-call.js.map +1 -1
  64. package/dist/ugc-framing.d.ts +1 -1
  65. package/dist/ugc-framing.js +1 -1
  66. package/dist/ugc-interview.d.ts +3 -3
  67. package/dist/ugc-interview.js +3 -3
  68. package/dist/ugc-interview.js.map +1 -1
  69. package/dist/ugc-layers.d.ts +7 -3
  70. package/dist/ugc-layers.js +17 -6
  71. package/dist/ugc-layers.js.map +1 -1
  72. package/dist/ugc-scenes.d.ts +3 -3
  73. package/dist/ugc-scenes.js +5 -4
  74. package/dist/ugc-scenes.js.map +1 -1
  75. package/dist/voice-admission.d.ts +5 -0
  76. package/dist/workflows/methods/faceless-explainer.d.ts +5 -0
  77. package/dist/workflows/methods/kinetic-launch.d.ts +5 -0
  78. package/dist/workflows/methods/product-image-story.d.ts +5 -0
  79. package/dist/workflows/methods/reference-remake.d.ts +5 -0
  80. package/dist/workflows/methods/shared.d.ts +5 -0
  81. package/dist/workflows/methods/ugc-faq-answer.d.ts +5 -0
  82. package/dist/workflows/methods/ugc-product-intro.d.ts +5 -0
  83. package/dist/workflows/methods/website-proof-demo.d.ts +5 -0
  84. package/dist/workflows/reference-remake.d.ts +12 -0
  85. package/dist/workflows/reference-remake.js +108 -16
  86. package/dist/workflows/reference-remake.js.map +1 -1
  87. package/dist/workflows/remake-motion.d.ts +2 -0
  88. package/dist/workflows/remake-motion.js +2 -1
  89. package/dist/workflows/remake-motion.js.map +1 -1
  90. package/package.json +1 -1
@@ -1706,6 +1706,11 @@ export declare const kineticLaunch: {
1706
1706
  product?: boolean | {
1707
1707
  placement: "beside" | "hand";
1708
1708
  } | undefined;
1709
+ cast?: {
1710
+ asset_id: string;
1711
+ role: string;
1712
+ action?: string | undefined;
1713
+ }[] | undefined;
1709
1714
  transition?: "cut" | "flash" | "light_leak" | "whoosh" | undefined;
1710
1715
  }[] | undefined;
1711
1716
  caption_emphasis?: (string | {
@@ -1706,6 +1706,11 @@ export declare const productImageStory: {
1706
1706
  product?: boolean | {
1707
1707
  placement: "beside" | "hand";
1708
1708
  } | undefined;
1709
+ cast?: {
1710
+ asset_id: string;
1711
+ role: string;
1712
+ action?: string | undefined;
1713
+ }[] | undefined;
1709
1714
  transition?: "cut" | "flash" | "light_leak" | "whoosh" | undefined;
1710
1715
  }[] | undefined;
1711
1716
  caption_emphasis?: (string | {
@@ -1706,6 +1706,11 @@ export declare const referenceRemake: {
1706
1706
  product?: boolean | {
1707
1707
  placement: "beside" | "hand";
1708
1708
  } | undefined;
1709
+ cast?: {
1710
+ asset_id: string;
1711
+ role: string;
1712
+ action?: string | undefined;
1713
+ }[] | undefined;
1709
1714
  transition?: "cut" | "flash" | "light_leak" | "whoosh" | undefined;
1710
1715
  }[] | undefined;
1711
1716
  caption_emphasis?: (string | {
@@ -1721,6 +1721,11 @@ export declare function defineWorkflowMethod(value: MethodDescriptorArgs): {
1721
1721
  product?: boolean | {
1722
1722
  placement: "beside" | "hand";
1723
1723
  } | undefined;
1724
+ cast?: {
1725
+ asset_id: string;
1726
+ role: string;
1727
+ action?: string | undefined;
1728
+ }[] | undefined;
1724
1729
  transition?: "cut" | "flash" | "light_leak" | "whoosh" | undefined;
1725
1730
  }[] | undefined;
1726
1731
  caption_emphasis?: (string | {
@@ -1706,6 +1706,11 @@ export declare const ugcFaqAnswer: {
1706
1706
  product?: boolean | {
1707
1707
  placement: "beside" | "hand";
1708
1708
  } | undefined;
1709
+ cast?: {
1710
+ asset_id: string;
1711
+ role: string;
1712
+ action?: string | undefined;
1713
+ }[] | undefined;
1709
1714
  transition?: "cut" | "flash" | "light_leak" | "whoosh" | undefined;
1710
1715
  }[] | undefined;
1711
1716
  caption_emphasis?: (string | {
@@ -1706,6 +1706,11 @@ export declare const ugcProductIntro: {
1706
1706
  product?: boolean | {
1707
1707
  placement: "beside" | "hand";
1708
1708
  } | undefined;
1709
+ cast?: {
1710
+ asset_id: string;
1711
+ role: string;
1712
+ action?: string | undefined;
1713
+ }[] | undefined;
1709
1714
  transition?: "cut" | "flash" | "light_leak" | "whoosh" | undefined;
1710
1715
  }[] | undefined;
1711
1716
  caption_emphasis?: (string | {
@@ -1706,6 +1706,11 @@ export declare const websiteProofDemo: {
1706
1706
  product?: boolean | {
1707
1707
  placement: "beside" | "hand";
1708
1708
  } | undefined;
1709
+ cast?: {
1710
+ asset_id: string;
1711
+ role: string;
1712
+ action?: string | undefined;
1713
+ }[] | undefined;
1709
1714
  transition?: "cut" | "flash" | "light_leak" | "whoosh" | undefined;
1710
1715
  }[] | undefined;
1711
1716
  caption_emphasis?: (string | {
@@ -950,6 +950,11 @@ export declare const planReferenceRemake: z.ZodObject<{
950
950
  cover_words: z.ZodOptional<z.ZodNumber>;
951
951
  }, z.core.$strict>>>;
952
952
  }, z.core.$strict>>;
953
+ cast: z.ZodOptional<z.ZodArray<z.ZodObject<{
954
+ asset_id: z.ZodString;
955
+ role: z.ZodString;
956
+ action: z.ZodOptional<z.ZodString>;
957
+ }, z.core.$strict>>>;
953
958
  }, z.core.$strict>;
954
959
  /** What a client sends (#1172): the profile, or the analysis_id of an analysis the server reads it from. */
955
960
  export declare const planReferenceRemakeRequest: z.ZodObject<{
@@ -1084,6 +1089,11 @@ export declare const planReferenceRemakeRequest: z.ZodObject<{
1084
1089
  cover_words: z.ZodOptional<z.ZodNumber>;
1085
1090
  }, z.core.$strict>>>;
1086
1091
  }, z.core.$strict>>;
1092
+ cast: z.ZodOptional<z.ZodArray<z.ZodObject<{
1093
+ asset_id: z.ZodString;
1094
+ role: z.ZodString;
1095
+ action: z.ZodOptional<z.ZodString>;
1096
+ }, z.core.$strict>>>;
1087
1097
  reference_profile: z.ZodOptional<z.ZodObject<{
1088
1098
  version: z.ZodLiteral<"reference-profile-v1">;
1089
1099
  source: z.ZodObject<{
@@ -1928,9 +1938,11 @@ export type ReferenceSpanPlanner = (input: {
1928
1938
  }) => ReferenceSpan[];
1929
1939
  type ReferenceGraphic = ReferenceProfile['beats'][number]['graphics'][number];
1930
1940
  /** What the asset library says of a request's asset (#1170): the server reads it, a direct compile may pass none. */
1941
+ /** `seconds`: a video asset's measured length, which caps the words its clip covers. */
1931
1942
  export type ReferenceAssetFacts = ReadonlyMap<string, {
1932
1943
  name: string;
1933
1944
  role?: string;
1945
+ seconds?: number;
1934
1946
  }>;
1935
1947
  /** The analysis's name for a graphic in its gap list: its device, else its own kind. */
1936
1948
  export declare const sceneGapWhat: (graphic: ReferenceGraphic) => string;
@@ -4,23 +4,25 @@ import { assetId } from '../asset-identity.js';
4
4
  import { captionLanguageOf } from '../caption-language.js';
5
5
  import { LONG_FORM_QUALIFICATION_MAX_DURATION_SEC } from '../long-form-profile.js';
6
6
  import { CANVAS_FPS } from '../media-timebase.js';
7
- import { estimateDurationSec, WORDS_PER_SECOND_MAX } from '../pacing.js';
7
+ import { estimateDurationSec, WORDS_PER_SECOND_MAX, WORDS_PER_SECOND_MIN } from '../pacing.js';
8
8
  import { referenceAnalysisId } from '../reference-analysis.js';
9
9
  import { actorIdScope } from '../account-actors.js';
10
10
  import { referenceProfileInput } from '../reference-profile.js';
11
11
  import { narrationPaceFromVoice, PAUSE_TRIM_CEILING_S, pauseTrimFromVoice } from '../reference-voice.js';
12
12
  import { AD_PAUSE_TRIM } from '../ad-pause-trim.js';
13
13
  import { countActorShots, coverWordOf, lexicalTokens, planSemanticInserts, quoteOccurrences } from '../semantic-inserts.js';
14
- import { actorId, callStage, insertBox, isNarratedUgcInput, makeUgcInput, makeUgcInputShape, MAX_INSERTS, MAX_LIVE_SHOTS, MAX_NARRATED_ACTOR_SHOTS, motionStyle, ugcCall, ugcTemplate } from '../skills.js';
14
+ import { actorId, callStage, insertBox, isNarratedUgcInput, makeUgcInput, makeUgcInputShape, MAX_INSERTS, MAX_LIVE_SHOTS, MAX_NARRATED_ACTOR_SHOTS, MAX_PRESENTER_TAKES, MAX_SHOT_CAST, motionStyle, ugcCall, ugcLiveShotCastMember, ugcTemplate } from '../skills.js';
15
+ import { ugcMarkedComponentsRefusal } from '../presenter-takes.js';
15
16
  import { CALL_PHONE_DEFAULTS, CALL_STAGE_SIDES } from '../ugc-call.js';
16
17
  import { MAX_TEMPLATES, TEMPLATE_ENTER_FRAMES, TEMPLATE_KIND_IDS, TEMPLATE_KINDS, TEMPLATE_PLACEMENTS, templateFaceWarnings, templateMediaOf, templateTextRefusal } from '../templates/index.js';
17
- import { MAX_SCENES, SCENE_BRIEF_MAX_CHARS, SCENE_TEXT_MAX_CHARS } from '../ugc-scenes.js';
18
+ import { MAX_SCENES, SCENE_TEXT_MAX_CHARS } from '../ugc-scenes.js';
18
19
  import { MAX_FRAMING } from '../ugc-framing.js';
19
20
  import { CAPTION_POSITION_VERTICAL, captionBandBox, captionEdgeAt } from '../caption-geometry.js';
20
21
  import { fitPlacedRect, place, plannedFace, presenterPlace, resolvePlace, sharedArea } from '../scene-place.js';
21
22
  import { FACE_COVER_SHARE, faceZone, frameCanvasOf, LAYER_ENTERS, layerWordRange, MAX_ENTER_OFFSET_S, MAX_OVERLAYS, OVERLAY_POSITIONS, overlayRect, ownPixels, MAX_LAYOUT_WINDOWS, rectsOverlap, SPLIT_MEDIA_SHARE } from '../ugc-layers.js';
22
23
  import { isEnglishScript, measuredPaceOf, PACE_MATCH_SHARE, VOICE_PACE_RANGE, voicesNearestPace } from '../voice-pace.js';
23
24
  import { BREAK_TAG_SOURCE, FIXED_PACE_MODELS, resolveTtsModel } from '../voices.js';
25
+ import { WORKFLOW_ADDED_MAX } from '../contracts/v1/workflow-brief.js';
24
26
  import { saveWorkflowPlan, workflowPlanDocument } from '../contracts/v1/workflow-plan.js';
25
27
  import { workflowId, workflowText } from '../contracts/v1/workflow-primitives.js';
26
28
  import { queryWorkflowMethod } from './catalog.js';
@@ -32,20 +34,20 @@ import { beatCue, FLASH_SNAP_S, planMotion } from './remake-motion.js';
32
34
  // The reference-remake compiler (#857 S2a): a profile and an accepted script become a complete plan whose native input is a make_ugc input.
33
35
  const remakeFields = {
34
36
  plan_id: workflowId.optional(), base_version: saveWorkflowPlan.shape.base_version.optional(),
35
- review_notes: z.array(workflowText(2000)).max(10).optional(),
37
+ review_notes: z.array(workflowText(2000)).max(50).optional(),
36
38
  reference_profile: referenceProfileInput.describe('The reference_profile analyze_reference returned, or one you wrote to the same schema.'),
37
39
  script: workflowText(10_000).describe('The accepted narration, copied into the plan unchanged.'),
38
40
  actor_id: actorId, voice: makeUgcInputShape.voice, voice_id: makeUgcInputShape.voice_id,
39
41
  assets: z.array(z.strictObject({ asset_id: assetId, media_type: z.enum(['image', 'video']),
40
- anchor: z.string().trim().min(1).max(300).optional().describe('Words of the script, as written, it shows on: always placed there.') }))
41
- .max(12).default([])
42
+ anchor: z.string().trim().min(1).max(2000).optional().describe('Words of the script, as written, it shows on: always placed there.') }))
43
+ .max(60).default([])
42
44
  .describe('Your ready ast_* media. Without anchor each goes to the line its name matches, else in order (named in warnings). Nothing is generated.'),
43
- line_edits: z.array(z.strictObject({ quote: z.string().trim().min(1).max(300).describe('Words of the script, as written.'),
45
+ line_edits: z.array(z.strictObject({ quote: z.string().trim().min(1).max(2000).describe('Words of the script, as written.'),
44
46
  template: z.strictObject({ kind: z.enum(TEMPLATE_KIND_IDS), data: z.record(z.string(), z.unknown()), placement: z.enum(TEMPLATE_PLACEMENTS).optional() })
45
47
  .optional().describe('The graphic on these words, as a make_ugc template: kind, data, placement.') }))
46
- .max(MAX_TEMPLATES).optional()
48
+ .max(60).refine(edits => edits.filter(edit => edit.template).length <= MAX_TEMPLATES, { message: `at most ${MAX_TEMPLATES} line edits carry a template, the most a run draws` }).optional()
47
49
  .describe('The user\'s shot notes: on these words the reference\'s graphics go, and the template shows, if given. Pass all of them on every compile.'),
48
- site: z.string().min(1).max(32).optional().describe('Your site, for an end card on the closing beat.'),
50
+ site: z.string().min(1).max(64).optional().describe('Your site, for an end card on the closing beat.'),
49
51
  audience: workflowText(2000).optional().describe('Who watches and whom they trust where, as shown to the user: for example a homeowner ' +
50
52
  'who trusts an electrician at the fuse box. Kept in the plan brief.'),
51
53
  message: workflowText(4000).optional(),
@@ -61,6 +63,10 @@ const remakeFields = {
61
63
  'on screen; true: every beat where they speak or react on screen; false: none. Costs more than the usual take: each shot kind has its own ' +
62
64
  'presenter-second price in list_prices, shown on the plan\'s quote.'),
63
65
  motion_style: motionStyle.optional().describe('As make_ugc motion_style; spans sit on the script\'s words. Free.'),
66
+ cast: z.array(ugcLiveShotCastMember).min(1).max(MAX_SHOT_CAST)
67
+ .refine(cast => new Set(cast.map(person => person.asset_id)).size === cast.length, { message: 'each cast picture appears once' }).optional()
68
+ .describe(`Up to ${MAX_SHOT_CAST} more people beside the actor in every acting live shot, as make_ugc shots cast: a picture, a role and ` +
69
+ 'what they do. Without an acting live shot the plan names it undrawn.'),
64
70
  };
65
71
  const revisionPair = (value, context) => {
66
72
  if ((value.plan_id === undefined) !== (value.base_version === undefined)) {
@@ -123,6 +129,8 @@ function shotFramingOf(size, take) {
123
129
  }
124
130
  /** 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. */
125
131
  const UNDER_CAPTIONS = 0.03;
132
+ /** A full-frame picture or card the reference showed this long or less was flashed, not held (#1196). */
133
+ const FLASH_HOLD_S = 1.2;
126
134
  /** 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. */
127
135
  const UNMEASURED_CAPTIONS_BELOW = [0.03, 0.15];
128
136
  /** The cuts a hook's live shot may take of ours, flash first: both are pictures only, so none adds a sound the customer did not ask for. */
@@ -195,7 +203,7 @@ export function sceneBriefOf(graphic, asked, site, words, role) {
195
203
  (role && role !== 'other' ? `; for the ${role.replace(/_/g, ' ')} beat` : '');
196
204
  const text = site && /\b(site|url|link|domain|website|web address)\b|\.(com|io|ai|app|co)\b/iu.test(graphic.label_hint) ? site
197
205
  : WORD_DEVICES.has(device) ? words.slice(0, SCENE_TEXT_MAX_CHARS) : undefined;
198
- return { brief: brief.replace(/[\p{Cc}]/gu, ' ').slice(0, SCENE_BRIEF_MAX_CHARS), ...(text ? { text } : {}) };
206
+ return { brief: brief.replace(/[\p{Cc}]/gu, ' '), ...(text ? { text } : {}) };
199
207
  }
200
208
  /** 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. */
201
209
  function referenceFaceOf(profile) {
@@ -553,6 +561,15 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
553
561
  gaps.fitted.push(beat);
554
562
  return { fit: 'auto', ...(box ? { box } : {}) };
555
563
  };
564
+ /** A full-frame picture or card the reference only flashed covers the words its measured time takes at our pace, from the beat's first;
565
+ * the presenter or the next shot says the rest (#1196). */
566
+ const flashed = (element, span) => {
567
+ const shown = element?.entry?.first_visible_s, gone = element?.exit?.last_visible_s;
568
+ if (shown === undefined || gone === undefined || gone - shown > FLASH_HOLD_S)
569
+ return {};
570
+ const found = quote(span.start, Math.max(1, Math.ceil((gone - shown) / (duration / words.length))), span.end);
571
+ return found && found.end < span.end ? { anchor: found.anchor, cover_words: found.end - found.start } : {};
572
+ };
556
573
  const spans = planSpans({ profile, words });
557
574
  const beatAt = (word) => Math.max(0, spans.findIndex(span => span !== null && word >= span.start && word < span.end));
558
575
  /** The first script words a quote of the request names, on the beat holding them. */
@@ -775,7 +792,7 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
775
792
  push(field === 'overlays' ? { ...item, position: placed }
776
793
  : screen ? { ...item, ...screenSplitOf(measured, graphic.position, index) }
777
794
  : field === 'layout_windows' ? { ...item, layout: 'split', ...(measured ? splitOf(measured, index, media) : {}) }
778
- : { ...item, ...insertFrameOf(measured, asset.media_type, index, media) });
795
+ : { ...item, ...insertFrameOf(measured, asset.media_type, index, media), ...(field === 'inserts' ? flashed(measured, span) : {}) });
779
796
  layered = true;
780
797
  fullFrame ||= field === 'inserts';
781
798
  windowed ||= field === 'layout_windows';
@@ -805,6 +822,8 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
805
822
  ...(built.placement ? { placement: built.placement } : {}), data: built.data };
806
823
  // A measured box over the presenter takes the template whole, drawn as its kind draws with no place of its own (#973 S4, S5).
807
824
  const element = elementOf(index, beat.graphics.indexOf(graphic)), definition = TEMPLATE_KINDS.find(entry => entry.kind === built.kind);
825
+ if ((built.placement ?? definition?.placements[0]) === 'full' && !definition?.quotes && !definition?.spreads)
826
+ Object.assign(item, flashed(element, span));
808
827
  const { position: _place, ...unplaced } = built.data, own = definition?.data.safeParse(unplaced);
809
828
  const staged = element && own?.success && (built.placement ?? definition.placements[0]) === 'over'
810
829
  ? stagedPlace(element, definition.rect(own.data, DEFAULT_ASPECT_RATIO, undefined, framing)) : undefined;
@@ -1288,10 +1307,11 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1288
1307
  const action = [...new Set([...beat.gestures ?? []].sort((a, b) => a.at_s - b.at_s).flatMap(entry => GESTURE_ACTIONS[entry.kind] ?? []))].join(', then ');
1289
1308
  const acted = beat.presenter_behaviour === 'reacting' || !!action;
1290
1309
  const wider = shotFramingOf(beat.shot_size, framing);
1310
+ // The request's cast stands beside the actor in every acting shot (#1192): those shots share one setup, so one cast.
1291
1311
  const shot = { id: `ref-${index + 1}-shot`, anchor: { startWord: span.start, endWord: span.end },
1292
1312
  ...beat.presenter_behaviour === 'reacting' ? { kind: 'action', action: action || REACTION }
1293
1313
  : action ? { kind: 'talking_action', action } : { kind: 'talking' },
1294
- ...acted && wider ? { framing: wider } : {} };
1314
+ ...acted && wider ? { framing: wider } : {}, ...acted && request.cast ? { cast: request.cast } : {} };
1295
1315
  return [{ beat: index, words: span.end - span.start, shot }];
1296
1316
  });
1297
1317
  const kept = new Set([...wanted].sort((a, b) => b.words - a.words || a.beat - b.beat).slice(0, MAX_LIVE_SHOTS));
@@ -1385,6 +1405,23 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1385
1405
  note(0, `captions at the reference's ${referenceAt} height (there they would cover the presenter's face)`, captionPosition !== referenceAt
1386
1406
  ? `${captionPosition} captions, clear of the face` : byCaptions ? 'kept there: graphics stand by the captions' : 'kept there: no caption height is clear of the face');
1387
1407
  }
1408
+ // A clip shorter than its words would freeze on its last frame: it covers only the words it outlasts at the slowest pace, and the presenter
1409
+ // says the rest (#1198). A fit that leaves the run more presenter shots than it renders is undone first, below.
1410
+ const fitted = new Map();
1411
+ for (const layer of layers.filter(entry => entry.field === 'inserts')) {
1412
+ const item = layer.item;
1413
+ const seconds = item.media_type === 'video' && item.asset_id ? facts?.get(item.asset_id)?.seconds : undefined;
1414
+ const range = seconds === undefined ? undefined : layerWordRange(tokens, item);
1415
+ if (seconds === undefined || range === undefined || typeof range === 'string')
1416
+ continue;
1417
+ const most = Math.floor(seconds * WORDS_PER_SECOND_MIN + 1e-9), short = range.coverWord - range.startWord > most
1418
+ ? quote(range.startWord, most, range.startWord + most) : undefined;
1419
+ if (!short)
1420
+ continue;
1421
+ fitted.set(layer, { item, line: `the clip on "${'quote' in item.anchor ? item.anchor.quote : short.anchor.quote}" covers its first ` +
1422
+ `${short.end - short.start} words, as long as it runs, so it never freezes; the presenter says the rest` });
1423
+ layer.item = { ...item, anchor: short.anchor, cover_words: short.end - short.start };
1424
+ }
1388
1425
  let native = {}, marked = false;
1389
1426
  // Every pass that does not break drops one layer or adds the one marker, so the loop ends.
1390
1427
  for (;;) {
@@ -1431,13 +1468,26 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1431
1468
  continue;
1432
1469
  }
1433
1470
  // A move the run refuses goes before the layer does: the refused layer's own, else one on a layer the refusal names.
1434
- const moved = [dropped, ...moves.keys()].find(layer => moves.has(layer) && (layer === dropped || issue.message.includes(`"${layer.item.id}"`)));
1471
+ // A refusal of a transition takes only the transition it names: the refused layer's own, or the cited layer's "whose …" (#1198 item 7).
1472
+ const passing = /would show (a hidden presenter frame|the presenter between)/.test(issue.message);
1473
+ const blamed = (layer) => {
1474
+ const { keys } = moves.get(layer), id = layer.item.id;
1475
+ if (!passing)
1476
+ return layer === dropped || issue.message.includes(`"${id}"`) ? keys : [];
1477
+ return keys.includes('transition') && (layer === dropped ? issue.message.includes(' has a ') : issue.message.includes(`"${id}", whose `))
1478
+ ? ['transition'] : [];
1479
+ };
1480
+ const moved = [dropped, ...moves.keys()].find(layer => moves.has(layer) && blamed(layer).length > 0);
1435
1481
  if (moved) {
1436
- const { keys } = moves.get(moved), item = { ...moved.item };
1482
+ const entry = moves.get(moved), keys = blamed(moved), item = { ...moved.item };
1437
1483
  for (const key of keys)
1438
1484
  delete item[key];
1439
1485
  moved.item = item;
1440
- moves.delete(moved);
1486
+ const left = entry.keys.filter(key => !keys.includes(key));
1487
+ if (left.length)
1488
+ moves.set(moved, { keys: left, added: entry.added.filter(line => line.key === undefined || left.includes(line.key)) });
1489
+ else
1490
+ moves.delete(moved);
1441
1491
  note(moved.beat, `${moved.kind}: its ${keys.map(key => key.replace('_', ' ')).join(' and ')} (${issue.message})`, keys.map(key => key === 'transition' ? 'a cut' : 'a still picture').join(' and '));
1442
1492
  continue;
1443
1493
  }
@@ -1461,6 +1511,12 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1461
1511
  })) : undefined;
1462
1512
  const shots = plan ? countActorShots(plan) : 1;
1463
1513
  if (plan && (shots === 0 || shots > MAX_NARRATED_ACTOR_SHOTS)) {
1514
+ const undo = [...fitted.keys()].filter(layer => layers.includes(layer)).at(-1);
1515
+ if (undo) {
1516
+ undo.item = fitted.get(undo).item;
1517
+ fitted.delete(undo);
1518
+ continue;
1519
+ }
1464
1520
  const layerOf = (entry) => inserts.find(layer => layer.item.id === entry.insertId);
1465
1521
  const candidates = plan.inserts.filter(entry => !requested.has(layerOf(entry)));
1466
1522
  if (!candidates.length) {
@@ -1473,6 +1529,38 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1473
1529
  note(dropped.beat, `${dropped.kind} (inserts leave ${shots} actor appearances; a run renders 1 to ${MAX_NARRATED_ACTOR_SHOTS})`, 'left out; the presenter speaks these words');
1474
1530
  continue;
1475
1531
  }
1532
+ // The quote's take budget (#1197), so the plan is never refused at quote: past it a live shot goes, the one on the fewest words first;
1533
+ // with no shot left, a picture the plan placed itself; with only the request's own pictures left, the compile is refused in the quote's words.
1534
+ const overBudget = ugcMarkedComponentsRefusal(checked.data, executor?.field_dispositions.visible_ai_label === 'implemented');
1535
+ if (overBudget) {
1536
+ const wordsOf = (layer) => { const range = rangeOf(layer); return range ? range.coverWord - range.startWord : 0; };
1537
+ const fewest = (candidates) => candidates.reduce((best, layer) => wordsOf(layer) < wordsOf(best) ? layer : best);
1538
+ const shotLayers = layers.filter(layer => layer.field === 'shots');
1539
+ if (shotLayers.length) {
1540
+ const shot = fewest(shotLayers);
1541
+ layers.splice(layers.indexOf(shot), 1);
1542
+ moves.delete(shot);
1543
+ note(shot.beat, `a live shot on this beat (with it the video makes more presenter takes than the ${MAX_PRESENTER_TAKES} one video ` +
1544
+ 'renders, or more than it holds beside your pictures; the shortest shots go first)', 'the presenter\'s usual take');
1545
+ continue;
1546
+ }
1547
+ const pictures = layers.filter(layer => ['inserts', 'overlays', 'layout_windows'].includes(layer.field)
1548
+ || (layer.field === 'templates' && templateMediaOf([layer.item]).length > 0));
1549
+ const placed = pictures.filter(layer => !requested.has(layer));
1550
+ if (!placed.length) {
1551
+ return { kind: 'invalid', issues: [{ kind: 'invalid_request', path: pictures.map(layer => requested.get(layer)).find(Boolean) ?? 'assets',
1552
+ message: overBudget }] };
1553
+ }
1554
+ const picture = fewest(placed);
1555
+ for (const gone of ours.get(picture) ?? [picture])
1556
+ if (layers.includes(gone)) {
1557
+ layers.splice(layers.indexOf(gone), 1);
1558
+ moves.delete(gone);
1559
+ ours.delete(gone);
1560
+ }
1561
+ note(picture.beat, `${picture.kind} (${overBudget})`, 'left out; the presenter speaks these words');
1562
+ continue;
1563
+ }
1476
1564
  // A remake bills on the long-form tariff: with nothing else narrated, a medium framing (scale 1) over the whole script makes it so.
1477
1565
  const lead = longForm && !marked && !isNarratedUgcInput(native) ? quote(0, 6, words.length) : undefined;
1478
1566
  if (lead) {
@@ -1482,6 +1570,9 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1482
1570
  }
1483
1571
  break;
1484
1572
  }
1573
+ if (request.cast && !layers.some(layer => layer.field === 'shots' && layer.item.cast)) {
1574
+ note(0, `your cast (${request.cast.map(person => person.role).join(', ')})`, 'not drawn: the plan has no acting live shot, where the reference presenter gestures or reacts');
1575
+ }
1485
1576
  const sounded = [...new Set(motion.sounded)].map(beat => beat + 1).join(', ');
1486
1577
  if ((motion.sounded.length || request.edit_sounds === true) && !native.edit_sounds) {
1487
1578
  note(motion.sounded[0] ?? 0, motion.sounded.length ? `the whoosh and click sounds on beats ${sounded}` : 'edit_sounds, a sound on every cut', !soundsOn ? (request.edit_sounds !== true ? 'none' : longForm ? 'none: edit_sounds is not available on this server' : 'none: edit_sounds needs a long-form run')
@@ -1489,6 +1580,7 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1489
1580
  : 'none unless you ask: compile again with edit_sounds: true for a sound on every cut');
1490
1581
  }
1491
1582
  const added = [...layers.flatMap(layer => moves.get(layer)?.added ?? []).sort((a, b) => a.beat_index - b.beat_index).map(entry => entry.what),
1583
+ ...[...fitted].filter(([layer]) => layers.includes(layer)).map(([, fit]) => fit.line),
1492
1584
  ...native.edit_sounds ? [`a sound on every cut, as you asked${motion.sounded.length ? `; the reference has a whoosh or a click on beats ${sounded}` : ''}`] : [],
1493
1585
  ...native.music ? [`music under the voice, as you asked${music >= 0 ? '; the reference has a music bed' : ''}`] : [],
1494
1586
  ...native.shots ? [`live shots of the presenter on beats ${beatsOf(layers.filter(layer => layer.field === 'shots').map(layer => layer.beat))}, as you asked; ` +
@@ -1580,7 +1672,7 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1580
1672
  reference_profile: { ...withoutWords(profile), music: '', unmapped,
1581
1673
  beats: profile.beats.map((beat, index) => ({ ...beat, spoken_summary: '', graphics: beat.graphics.map((graphic, at) => ({ ...graphic, label_hint: '',
1582
1674
  ...(converted.has(graphic) ? { kind: converted.get(graphic) } : scened.has(`${index}:${at}`) ? { kind: 'scene' } : {}) })) })) },
1583
- ...(added.length ? { added: added.slice(0, 40) } : {}), ...(request.review_notes?.length ? { review_notes: request.review_notes } : {}) },
1675
+ ...(added.length ? { added: added.slice(0, WORKFLOW_ADDED_MAX) } : {}), ...(request.review_notes?.length ? { review_notes: request.review_notes } : {}) },
1584
1676
  beats, claims: [], assets: [] });
1585
1677
  return document.success ? { kind: 'compiled', document: document.data }
1586
1678
  : { kind: 'invalid', issues: document.error.issues.map(issue => ({ kind: 'invalid_plan', path: issue.path.join('.'), message: issue.message })) };