@clipwright/core 0.41.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 (54) hide show
  1. package/dist/actors.d.ts +5 -0
  2. package/dist/actors.js +22 -2
  3. package/dist/actors.js.map +1 -1
  4. package/dist/contract-dispositions.d.ts +5 -0
  5. package/dist/contracts/v1/workflow-brief.d.ts +10 -0
  6. package/dist/contracts/v1/workflow-catalog.d.ts +10 -0
  7. package/dist/contracts/v1/workflow-client.d.ts +10 -0
  8. package/dist/contracts/v1/workflow-plan.d.ts +15 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/live-shot-kinds.d.ts +12 -0
  13. package/dist/live-shot-kinds.js +11 -0
  14. package/dist/live-shot-kinds.js.map +1 -1
  15. package/dist/moves.js +2 -1
  16. package/dist/moves.js.map +1 -1
  17. package/dist/presenter-takes.d.ts +10 -0
  18. package/dist/presenter-takes.js +59 -0
  19. package/dist/presenter-takes.js.map +1 -0
  20. package/dist/reference-profile.d.ts +2 -1
  21. package/dist/reference-profile.js +27 -5
  22. package/dist/reference-profile.js.map +1 -1
  23. package/dist/run-static-warnings.js +1 -1
  24. package/dist/run-static-warnings.js.map +1 -1
  25. package/dist/scene-graph.d.ts +8 -0
  26. package/dist/scene-graph.js +35 -0
  27. package/dist/scene-graph.js.map +1 -1
  28. package/dist/scene-place.d.ts +3 -0
  29. package/dist/scene-place.js +3 -0
  30. package/dist/scene-place.js.map +1 -1
  31. package/dist/skills.d.ts +34 -1
  32. package/dist/skills.js +34 -5
  33. package/dist/skills.js.map +1 -1
  34. package/dist/templates/index.d.ts +8 -0
  35. package/dist/templates/index.js +10 -0
  36. package/dist/templates/index.js.map +1 -1
  37. package/dist/ugc-scenes.js +2 -1
  38. package/dist/ugc-scenes.js.map +1 -1
  39. package/dist/voice-admission.d.ts +5 -0
  40. package/dist/workflows/methods/faceless-explainer.d.ts +5 -0
  41. package/dist/workflows/methods/kinetic-launch.d.ts +5 -0
  42. package/dist/workflows/methods/product-image-story.d.ts +5 -0
  43. package/dist/workflows/methods/reference-remake.d.ts +5 -0
  44. package/dist/workflows/methods/shared.d.ts +5 -0
  45. package/dist/workflows/methods/ugc-faq-answer.d.ts +5 -0
  46. package/dist/workflows/methods/ugc-product-intro.d.ts +5 -0
  47. package/dist/workflows/methods/website-proof-demo.d.ts +5 -0
  48. package/dist/workflows/reference-remake.d.ts +12 -0
  49. package/dist/workflows/reference-remake.js +98 -7
  50. package/dist/workflows/reference-remake.js.map +1 -1
  51. package/dist/workflows/remake-motion.d.ts +2 -0
  52. package/dist/workflows/remake-motion.js +2 -1
  53. package/dist/workflows/remake-motion.js.map +1 -1
  54. package/package.json +1 -1
@@ -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,14 +4,15 @@ 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
18
  import { MAX_SCENES, SCENE_TEXT_MAX_CHARS } from '../ugc-scenes.js';
@@ -62,6 +63,10 @@ const remakeFields = {
62
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 ' +
63
64
  'presenter-second price in list_prices, shown on the plan\'s quote.'),
64
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.'),
65
70
  };
66
71
  const revisionPair = (value, context) => {
67
72
  if ((value.plan_id === undefined) !== (value.base_version === undefined)) {
@@ -124,6 +129,8 @@ function shotFramingOf(size, take) {
124
129
  }
125
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. */
126
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;
127
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. */
128
135
  const UNMEASURED_CAPTIONS_BELOW = [0.03, 0.15];
129
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. */
@@ -554,6 +561,15 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
554
561
  gaps.fitted.push(beat);
555
562
  return { fit: 'auto', ...(box ? { box } : {}) };
556
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
+ };
557
573
  const spans = planSpans({ profile, words });
558
574
  const beatAt = (word) => Math.max(0, spans.findIndex(span => span !== null && word >= span.start && word < span.end));
559
575
  /** The first script words a quote of the request names, on the beat holding them. */
@@ -776,7 +792,7 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
776
792
  push(field === 'overlays' ? { ...item, position: placed }
777
793
  : screen ? { ...item, ...screenSplitOf(measured, graphic.position, index) }
778
794
  : field === 'layout_windows' ? { ...item, layout: 'split', ...(measured ? splitOf(measured, index, media) : {}) }
779
- : { ...item, ...insertFrameOf(measured, asset.media_type, index, media) });
795
+ : { ...item, ...insertFrameOf(measured, asset.media_type, index, media), ...(field === 'inserts' ? flashed(measured, span) : {}) });
780
796
  layered = true;
781
797
  fullFrame ||= field === 'inserts';
782
798
  windowed ||= field === 'layout_windows';
@@ -806,6 +822,8 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
806
822
  ...(built.placement ? { placement: built.placement } : {}), data: built.data };
807
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).
808
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));
809
827
  const { position: _place, ...unplaced } = built.data, own = definition?.data.safeParse(unplaced);
810
828
  const staged = element && own?.success && (built.placement ?? definition.placements[0]) === 'over'
811
829
  ? stagedPlace(element, definition.rect(own.data, DEFAULT_ASPECT_RATIO, undefined, framing)) : undefined;
@@ -1289,10 +1307,11 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1289
1307
  const action = [...new Set([...beat.gestures ?? []].sort((a, b) => a.at_s - b.at_s).flatMap(entry => GESTURE_ACTIONS[entry.kind] ?? []))].join(', then ');
1290
1308
  const acted = beat.presenter_behaviour === 'reacting' || !!action;
1291
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.
1292
1311
  const shot = { id: `ref-${index + 1}-shot`, anchor: { startWord: span.start, endWord: span.end },
1293
1312
  ...beat.presenter_behaviour === 'reacting' ? { kind: 'action', action: action || REACTION }
1294
1313
  : action ? { kind: 'talking_action', action } : { kind: 'talking' },
1295
- ...acted && wider ? { framing: wider } : {} };
1314
+ ...acted && wider ? { framing: wider } : {}, ...acted && request.cast ? { cast: request.cast } : {} };
1296
1315
  return [{ beat: index, words: span.end - span.start, shot }];
1297
1316
  });
1298
1317
  const kept = new Set([...wanted].sort((a, b) => b.words - a.words || a.beat - b.beat).slice(0, MAX_LIVE_SHOTS));
@@ -1386,6 +1405,23 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1386
1405
  note(0, `captions at the reference's ${referenceAt} height (there they would cover the presenter's face)`, captionPosition !== referenceAt
1387
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');
1388
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
+ }
1389
1425
  let native = {}, marked = false;
1390
1426
  // Every pass that does not break drops one layer or adds the one marker, so the loop ends.
1391
1427
  for (;;) {
@@ -1432,13 +1468,26 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1432
1468
  continue;
1433
1469
  }
1434
1470
  // A move the run refuses goes before the layer does: the refused layer's own, else one on a layer the refusal names.
1435
- 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);
1436
1481
  if (moved) {
1437
- const { keys } = moves.get(moved), item = { ...moved.item };
1482
+ const entry = moves.get(moved), keys = blamed(moved), item = { ...moved.item };
1438
1483
  for (const key of keys)
1439
1484
  delete item[key];
1440
1485
  moved.item = item;
1441
- 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);
1442
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 '));
1443
1492
  continue;
1444
1493
  }
@@ -1462,6 +1511,12 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1462
1511
  })) : undefined;
1463
1512
  const shots = plan ? countActorShots(plan) : 1;
1464
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
+ }
1465
1520
  const layerOf = (entry) => inserts.find(layer => layer.item.id === entry.insertId);
1466
1521
  const candidates = plan.inserts.filter(entry => !requested.has(layerOf(entry)));
1467
1522
  if (!candidates.length) {
@@ -1474,6 +1529,38 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1474
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');
1475
1530
  continue;
1476
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
+ }
1477
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.
1478
1565
  const lead = longForm && !marked && !isNarratedUgcInput(native) ? quote(0, 6, words.length) : undefined;
1479
1566
  if (lead) {
@@ -1483,6 +1570,9 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1483
1570
  }
1484
1571
  break;
1485
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
+ }
1486
1576
  const sounded = [...new Set(motion.sounded)].map(beat => beat + 1).join(', ');
1487
1577
  if ((motion.sounded.length || request.edit_sounds === true) && !native.edit_sounds) {
1488
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')
@@ -1490,6 +1580,7 @@ export function compileReferenceRemake(input, capabilities, planSpans = proporti
1490
1580
  : 'none unless you ask: compile again with edit_sounds: true for a sound on every cut');
1491
1581
  }
1492
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),
1493
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}` : ''}`] : [],
1494
1585
  ...native.music ? [`music under the voice, as you asked${music >= 0 ? '; the reference has a music bed' : ''}`] : [],
1495
1586
  ...native.shots ? [`live shots of the presenter on beats ${beatsOf(layers.filter(layer => layer.field === 'shots').map(layer => layer.beat))}, as you asked; ` +