@nodaro/prompts 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +5453 -4271
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1221 -63
- package/dist/index.d.ts +1221 -63
- package/dist/index.js +5366 -4273
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
- package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
- package/src/__tests__/catalog-packs.test.ts +130 -0
- package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
- package/src/__tests__/catalog-terms.test.ts +162 -0
- package/src/__tests__/character-default-role.test.ts +3 -2
- package/src/__tests__/content-free-contract.test.ts +45 -0
- package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
- package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
- package/src/__tests__/parameter-hint-mode.test.ts +385 -0
- package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
- package/src/__tests__/person-packs.test.ts +166 -0
- package/src/__tests__/project-all-catalogs.test.ts +26 -0
- package/src/__tests__/prompt-builder.test.ts +53 -0
- package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
- package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
- package/src/__tests__/term.test.ts +104 -0
- package/src/__tests__/transitions.test.ts +8 -5
- package/src/__tests__/upstream-immutability.test.ts +23 -0
- package/src/action-fx.ts +67 -17
- package/src/aesthetic.ts +54 -6
- package/src/atmosphere.ts +64 -23
- package/src/backdrop.ts +44 -30
- package/src/camera-format.ts +33 -11
- package/src/camera-motions.ts +89 -0
- package/src/catalog-packs.ts +125 -0
- package/src/catalog-sidecar-coverage.ts +36 -0
- package/src/character-fx.ts +112 -39
- package/src/color-look.ts +44 -27
- package/src/composition-effects.ts +21 -7
- package/src/era.ts +24 -0
- package/src/exposure-settings.ts +76 -18
- package/src/framing.ts +100 -0
- package/src/held-prop.ts +125 -63
- package/src/identity-lock.ts +12 -5
- package/src/image-reference-doctrine.ts +55 -0
- package/src/index.ts +6 -0
- package/src/instrumentation.ts +148 -57
- package/src/lens.ts +31 -15
- package/src/lighting.ts +120 -59
- package/src/loop-subject.ts +27 -1
- package/src/materials.ts +123 -69
- package/src/mood.ts +123 -51
- package/src/music-genre.ts +171 -67
- package/src/music-mood.ts +85 -17
- package/src/parameter-prompt-hint.ts +168 -56
- package/src/person-packs.ts +182 -0
- package/src/person.ts +506 -408
- package/src/photo-genre.ts +37 -22
- package/src/photographer.ts +138 -1
- package/src/picker-catalogs.ts +86 -40
- package/src/picker-wiring.ts +1 -1
- package/src/pose.ts +105 -40
- package/src/post-process-effects.ts +51 -8
- package/src/prompt-builder.ts +16 -1
- package/src/provider-prompt-doctrine.ts +1 -2
- package/src/render-quality.ts +25 -7
- package/src/setting.ts +30 -14
- package/src/style-presets.ts +1 -1
- package/src/style.ts +33 -15
- package/src/styling.ts +155 -94
- package/src/surround-fill.ts +67 -0
- package/src/temporal.ts +80 -18
- package/src/term.ts +155 -0
- package/src/transitions.ts +73 -26
- package/src/voice-character.ts +190 -90
- package/src/voice-delivery.ts +83 -12
package/src/camera-motions.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* motion enabled.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { resolveTerm, type PickerHintMode } from "./term.js"
|
|
11
|
+
|
|
10
12
|
export type CameraMotionCategory =
|
|
11
13
|
| "default"
|
|
12
14
|
| "pan"
|
|
@@ -27,6 +29,12 @@ export interface CameraMotion {
|
|
|
27
29
|
readonly category: CameraMotionCategory
|
|
28
30
|
readonly description: string
|
|
29
31
|
readonly promptHint: string
|
|
32
|
+
/**
|
|
33
|
+
* Compact professional term for this move ("whip pan left", "push-pull
|
|
34
|
+
* swing") — authored only where the lowercased label is not what a
|
|
35
|
+
* cinematographer would write in a prompt. See `term.ts`.
|
|
36
|
+
*/
|
|
37
|
+
readonly term?: string
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
@@ -44,6 +52,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
44
52
|
category: "default",
|
|
45
53
|
description: "Fixed camera, no movement",
|
|
46
54
|
promptHint: "locked off static camera, no camera movement",
|
|
55
|
+
term: "locked-off static camera",
|
|
47
56
|
},
|
|
48
57
|
{
|
|
49
58
|
id: "handheld",
|
|
@@ -51,6 +60,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
51
60
|
category: "default",
|
|
52
61
|
description: "Natural handheld shake",
|
|
53
62
|
promptHint: "handheld camera with subtle natural shake and micro movements",
|
|
63
|
+
term: "handheld camera",
|
|
54
64
|
},
|
|
55
65
|
{
|
|
56
66
|
id: "steadicam",
|
|
@@ -58,6 +68,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
58
68
|
category: "default",
|
|
59
69
|
description: "Smooth stabilized walking shot",
|
|
60
70
|
promptHint: "smooth steadicam shot, gliding stabilized movement through the scene",
|
|
71
|
+
term: "smooth steadicam shot",
|
|
61
72
|
},
|
|
62
73
|
|
|
63
74
|
// Pan — rotation on vertical axis
|
|
@@ -164,6 +175,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
164
175
|
category: "dolly",
|
|
165
176
|
description: "Slow subtle push toward subject",
|
|
166
177
|
promptHint: "slow push in, gentle forward movement toward the subject",
|
|
178
|
+
term: "slow push-in",
|
|
167
179
|
},
|
|
168
180
|
{
|
|
169
181
|
id: "pull-out",
|
|
@@ -171,6 +183,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
171
183
|
category: "dolly",
|
|
172
184
|
description: "Slow subtle pull back from subject",
|
|
173
185
|
promptHint: "slow pull out, gentle backward movement away from the subject",
|
|
186
|
+
term: "slow pull-out",
|
|
174
187
|
},
|
|
175
188
|
{
|
|
176
189
|
id: "breathing",
|
|
@@ -185,6 +198,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
185
198
|
category: "dolly",
|
|
186
199
|
description: "Camera swings toward subject then away",
|
|
187
200
|
promptHint: "push-pull swing, the camera moves toward the subject then back away in a single move, a swinging approach-and-retreat that creates emphasis or hesitation",
|
|
201
|
+
term: "push-pull swing",
|
|
188
202
|
},
|
|
189
203
|
{
|
|
190
204
|
id: "creep-in",
|
|
@@ -192,6 +206,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
192
206
|
category: "dolly",
|
|
193
207
|
description: "Imperceptibly slow push-in over time",
|
|
194
208
|
promptHint: "creep-in, imperceptibly slow push-in over many seconds, building dread or tension",
|
|
209
|
+
term: "slow creeping push-in",
|
|
195
210
|
},
|
|
196
211
|
{
|
|
197
212
|
id: "creep-out",
|
|
@@ -199,6 +214,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
199
214
|
category: "dolly",
|
|
200
215
|
description: "Imperceptibly slow pull-out over time",
|
|
201
216
|
promptHint: "creep-out, imperceptibly slow pull-out over many seconds, isolating the subject in space",
|
|
217
|
+
term: "slow creeping pull-out",
|
|
202
218
|
},
|
|
203
219
|
|
|
204
220
|
// Truck — lateral slide
|
|
@@ -261,6 +277,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
261
277
|
category: "roll",
|
|
262
278
|
description: "Camera rotates a full 360° on its axis",
|
|
263
279
|
promptHint: "full 360 degree spin, the camera rotates a complete revolution on its own lens axis",
|
|
280
|
+
term: "full 360 degree camera roll",
|
|
264
281
|
},
|
|
265
282
|
|
|
266
283
|
// Orbit / Arc
|
|
@@ -298,6 +315,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
298
315
|
category: "orbit",
|
|
299
316
|
description: "Full circular arc around subject",
|
|
300
317
|
promptHint: "full 360 degree orbit, the camera arcs a complete revolution around the subject",
|
|
318
|
+
term: "full 360 degree orbit",
|
|
301
319
|
},
|
|
302
320
|
|
|
303
321
|
// Crane / Jib
|
|
@@ -344,6 +362,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
344
362
|
category: "tracking",
|
|
345
363
|
description: "Follow subject from behind",
|
|
346
364
|
promptHint: "follow shot, camera trails the subject from behind at a constant distance",
|
|
365
|
+
term: "follow shot from behind",
|
|
347
366
|
},
|
|
348
367
|
{
|
|
349
368
|
id: "lead",
|
|
@@ -351,6 +370,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
351
370
|
category: "tracking",
|
|
352
371
|
description: "Move ahead of advancing subject",
|
|
353
372
|
promptHint: "lead shot, camera moves backward ahead of the advancing subject",
|
|
373
|
+
term: "leading tracking shot",
|
|
354
374
|
},
|
|
355
375
|
{
|
|
356
376
|
id: "drone-follow",
|
|
@@ -358,6 +378,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
358
378
|
category: "tracking",
|
|
359
379
|
description: "Elevated drone tracking subject",
|
|
360
380
|
promptHint: "aerial drone follow shot, elevated camera smoothly tracking the subject from above and behind",
|
|
381
|
+
term: "aerial drone follow shot",
|
|
361
382
|
},
|
|
362
383
|
{
|
|
363
384
|
id: "dolly-track",
|
|
@@ -365,6 +386,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
365
386
|
category: "tracking",
|
|
366
387
|
description: "Dolly on parallel track alongside subject",
|
|
367
388
|
promptHint: "dolly on a parallel track alongside the subject, smooth lateral tracking with strong foreground parallax",
|
|
389
|
+
term: "parallel dolly tracking shot",
|
|
368
390
|
},
|
|
369
391
|
{
|
|
370
392
|
id: "gimbal-walk",
|
|
@@ -372,6 +394,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
372
394
|
category: "tracking",
|
|
373
395
|
description: "Smooth walking shot on a 3-axis gimbal",
|
|
374
396
|
promptHint: "gimbal walk, smooth walking shot on a 3-axis gimbal with floating steady forward motion",
|
|
397
|
+
term: "smooth gimbal walking shot",
|
|
375
398
|
},
|
|
376
399
|
{
|
|
377
400
|
id: "ronin-glide",
|
|
@@ -379,6 +402,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
379
402
|
category: "tracking",
|
|
380
403
|
description: "Slow gliding move on a Ronin/Movi gimbal",
|
|
381
404
|
promptHint: "ronin glide, slow gliding move on a Ronin or Movi gimbal, cinematic float without any shake",
|
|
405
|
+
term: "slow gliding gimbal move",
|
|
382
406
|
},
|
|
383
407
|
{
|
|
384
408
|
id: "serpentine",
|
|
@@ -386,6 +410,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
386
410
|
category: "tracking",
|
|
387
411
|
description: "Camera weaves through obstacles in S-curves",
|
|
388
412
|
promptHint: "serpentine track, the camera weaves through obstacles in S-curves, snaking forward along a winding path",
|
|
413
|
+
term: "serpentine tracking shot",
|
|
389
414
|
},
|
|
390
415
|
|
|
391
416
|
// Special angles / rigs
|
|
@@ -395,6 +420,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
395
420
|
category: "special",
|
|
396
421
|
description: "First person point of view",
|
|
397
422
|
promptHint: "POV shot, first person perspective as seen through the subject's eyes",
|
|
423
|
+
term: "first person pov shot",
|
|
398
424
|
},
|
|
399
425
|
{
|
|
400
426
|
id: "over-the-shoulder",
|
|
@@ -402,6 +428,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
402
428
|
category: "special",
|
|
403
429
|
description: "Frame past a character's shoulder",
|
|
404
430
|
promptHint: "over the shoulder shot, framing past one character's shoulder onto another",
|
|
431
|
+
term: "over-the-shoulder shot",
|
|
405
432
|
},
|
|
406
433
|
{
|
|
407
434
|
id: "birds-eye",
|
|
@@ -409,6 +436,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
409
436
|
category: "special",
|
|
410
437
|
description: "Direct top-down overhead view",
|
|
411
438
|
promptHint: "bird's eye view, direct overhead top-down shot looking straight down",
|
|
439
|
+
term: "bird's eye view",
|
|
412
440
|
},
|
|
413
441
|
{
|
|
414
442
|
id: "worms-eye",
|
|
@@ -416,6 +444,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
416
444
|
category: "special",
|
|
417
445
|
description: "Extreme low angle looking up",
|
|
418
446
|
promptHint: "worm's eye view, extreme low angle looking up at the subject",
|
|
447
|
+
term: "worm's eye view",
|
|
419
448
|
},
|
|
420
449
|
{
|
|
421
450
|
id: "aerial",
|
|
@@ -423,6 +452,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
423
452
|
category: "special",
|
|
424
453
|
description: "High altitude drone-style shot",
|
|
425
454
|
promptHint: "aerial drone shot, high altitude slow forward movement over the landscape",
|
|
455
|
+
term: "high altitude aerial drone shot",
|
|
426
456
|
},
|
|
427
457
|
{
|
|
428
458
|
id: "helicopter",
|
|
@@ -430,6 +460,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
430
460
|
category: "special",
|
|
431
461
|
description: "Wide high-altitude sweeping aerial",
|
|
432
462
|
promptHint: "helicopter shot, high altitude wide sweeping aerial pass with strong lateral movement",
|
|
463
|
+
term: "sweeping helicopter aerial shot",
|
|
433
464
|
},
|
|
434
465
|
{
|
|
435
466
|
id: "fly-over",
|
|
@@ -437,6 +468,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
437
468
|
category: "special",
|
|
438
469
|
description: "Low fast aerial pass over the scene",
|
|
439
470
|
promptHint: "fly over shot, low altitude drone passing quickly over the scene with strong forward motion",
|
|
471
|
+
term: "low aerial fly-over pass",
|
|
440
472
|
},
|
|
441
473
|
{
|
|
442
474
|
id: "flythrough",
|
|
@@ -444,6 +476,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
444
476
|
category: "special",
|
|
445
477
|
description: "Camera flies through space",
|
|
446
478
|
promptHint: "flythrough shot, camera moving forward through the environment, weaving through obstacles",
|
|
479
|
+
term: "flythrough shot",
|
|
447
480
|
},
|
|
448
481
|
{
|
|
449
482
|
id: "reveal",
|
|
@@ -451,6 +484,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
451
484
|
category: "special",
|
|
452
485
|
description: "Gradually reveal wider scene",
|
|
453
486
|
promptHint: "reveal shot, camera rises and tilts to gradually reveal the subject and wider scene",
|
|
487
|
+
term: "rising reveal shot",
|
|
454
488
|
},
|
|
455
489
|
{
|
|
456
490
|
id: "snorricam",
|
|
@@ -458,6 +492,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
458
492
|
category: "special",
|
|
459
493
|
description: "Body-mounted camera (subject locked to frame)",
|
|
460
494
|
promptHint: "snorricam body-mounted shot, subject locked in frame while the world moves around them",
|
|
495
|
+
term: "body-mounted snorricam shot",
|
|
461
496
|
},
|
|
462
497
|
{
|
|
463
498
|
id: "rack-focus",
|
|
@@ -474,6 +509,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
474
509
|
category: "default",
|
|
475
510
|
description: "Casual vlog-style handheld",
|
|
476
511
|
promptHint: "casual handheld vlog-style camera, slight wandering and natural shake, talking-to-camera framing",
|
|
512
|
+
term: "handheld vlog-style camera",
|
|
477
513
|
},
|
|
478
514
|
{
|
|
479
515
|
id: "pov-walk",
|
|
@@ -481,6 +517,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
481
517
|
category: "tracking",
|
|
482
518
|
description: "First-person walking POV",
|
|
483
519
|
promptHint: "first-person POV walking camera, GoPro-style head-mounted perspective with natural footstep movement",
|
|
520
|
+
term: "first person walking pov",
|
|
484
521
|
},
|
|
485
522
|
{
|
|
486
523
|
id: "velocity-edit",
|
|
@@ -488,6 +525,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
488
525
|
category: "special",
|
|
489
526
|
description: "TikTok speed-ramp pacing",
|
|
490
527
|
promptHint: "rapid speed-ramped camera move with characteristic TikTok velocity-edit pacing, dynamic acceleration into and out of the shot",
|
|
528
|
+
term: "rapid speed-ramped camera move",
|
|
491
529
|
},
|
|
492
530
|
{
|
|
493
531
|
id: "match-cut-zoom",
|
|
@@ -495,6 +533,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
495
533
|
category: "zoom",
|
|
496
534
|
description: "Beat-timed zoom for cuts",
|
|
497
535
|
promptHint: "rapid zoom timed to a beat, suggesting a match cut to the next shot",
|
|
536
|
+
term: "rapid beat-timed zoom",
|
|
498
537
|
},
|
|
499
538
|
{
|
|
500
539
|
id: "screen-tap",
|
|
@@ -502,6 +541,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
502
541
|
category: "special",
|
|
503
542
|
description: "On-screen finger-tap transition",
|
|
504
543
|
promptHint: "camera transition triggered by an on-screen finger tap, TikTok-native pacing with snap to the next subject",
|
|
544
|
+
term: "on-screen finger-tap transition",
|
|
505
545
|
},
|
|
506
546
|
{
|
|
507
547
|
id: "phone-flip",
|
|
@@ -509,6 +549,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
509
549
|
category: "special",
|
|
510
550
|
description: "Front/rear camera flip",
|
|
511
551
|
promptHint: "camera-flip transition where the phone visibly rotates between the front and rear sensors, brief blur during the swap",
|
|
552
|
+
term: "front-to-rear phone camera flip",
|
|
512
553
|
},
|
|
513
554
|
{
|
|
514
555
|
id: "gentle-drift",
|
|
@@ -516,6 +557,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
516
557
|
category: "default",
|
|
517
558
|
description: "Slow ambient floating motion",
|
|
518
559
|
promptHint: "gentle camera drift, a slow ambient floating motion with no specific direction, the camera barely moves but never sits perfectly still, evocative of contemplative atmospheric shots",
|
|
560
|
+
term: "slow gentle camera drift",
|
|
519
561
|
},
|
|
520
562
|
{
|
|
521
563
|
id: "parallax",
|
|
@@ -523,6 +565,7 @@ export const CAMERA_MOTIONS: ReadonlyArray<CameraMotion> = [
|
|
|
523
565
|
category: "default",
|
|
524
566
|
description: "Lateral motion with foreground/background depth separation",
|
|
525
567
|
promptHint: "parallax camera motion, lateral movement that emphasizes the depth separation between foreground and background elements, foreground objects appearing to move faster than distant ones",
|
|
568
|
+
term: "lateral parallax camera move",
|
|
526
569
|
},
|
|
527
570
|
]
|
|
528
571
|
|
|
@@ -578,6 +621,18 @@ export function getCameraMotionPromptHint(id: string | undefined | null): string
|
|
|
578
621
|
return getCameraMotion(id)?.promptHint ?? ""
|
|
579
622
|
}
|
|
580
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Compact counterpart of `getCameraMotionPromptHint`: the short professional
|
|
626
|
+
* term a cinematographer would write for this move ("whip pan left",
|
|
627
|
+
* "push-pull swing") where the hint is the full mechanism sentence. Same
|
|
628
|
+
* lookup and same empty-string-on-miss behavior, so the two getters can never
|
|
629
|
+
* disagree about which motion they describe; "auto" (which injects nothing)
|
|
630
|
+
* resolves to "" here too.
|
|
631
|
+
*/
|
|
632
|
+
export function getCameraMotionTerm(id: string | undefined | null): string {
|
|
633
|
+
return resolveTerm(getCameraMotion(id))
|
|
634
|
+
}
|
|
635
|
+
|
|
581
636
|
export const CAMERA_MOTION_IDS: ReadonlyArray<string> = CAMERA_MOTIONS.map((m) => m.id)
|
|
582
637
|
|
|
583
638
|
// ---------------------------------------------------------------------------
|
|
@@ -598,12 +653,21 @@ export const CAMERA_MOTION_IDS: ReadonlyArray<string> = CAMERA_MOTIONS.map((m) =
|
|
|
598
653
|
* Hints within each side are joined with " and " for grammatical flow.
|
|
599
654
|
* If multiple nodes are connected (e.g. Framing + Lighting + Tone), all
|
|
600
655
|
* three contribute their hint to the clause.
|
|
656
|
+
*
|
|
657
|
+
* @param mode `"compact"` delegates to `composeCameraMotionTermFromConnections`
|
|
658
|
+
* — the same start/end structure built from the motion's short professional
|
|
659
|
+
* `term`. The caller is expected to have resolved the connected nodes to
|
|
660
|
+
* THEIR terms too, so the whole fragment stays at one level of detail.
|
|
601
661
|
*/
|
|
602
662
|
export function composeCameraMotionHintFromConnections(
|
|
603
663
|
motionId: string | undefined,
|
|
604
664
|
startHints: ReadonlyArray<string>,
|
|
605
665
|
endHints: ReadonlyArray<string>,
|
|
666
|
+
mode: PickerHintMode = "full",
|
|
606
667
|
): string {
|
|
668
|
+
if (mode === "compact") {
|
|
669
|
+
return composeCameraMotionTermFromConnections(motionId, startHints, endHints)
|
|
670
|
+
}
|
|
607
671
|
const base = getCameraMotionPromptHint(motionId)
|
|
608
672
|
if (!base) return ""
|
|
609
673
|
const parts: string[] = [base]
|
|
@@ -613,3 +677,28 @@ export function composeCameraMotionHintFromConnections(
|
|
|
613
677
|
if (endClause) parts.push(`ending with ${endClause}`)
|
|
614
678
|
return parts.join(", ")
|
|
615
679
|
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Compact-mode mirror of `composeCameraMotionHintFromConnections`: the same
|
|
683
|
+
* start/end structure, built from the motion's short `term` and the connected
|
|
684
|
+
* parameter nodes' terms instead of their full promptHints
|
|
685
|
+
* ("dolly in, beginning with wide shot, ending with medium close-up").
|
|
686
|
+
*
|
|
687
|
+
* Same shape in every respect — empty base (an "auto" or unknown motion) still
|
|
688
|
+
* yields "", each side is joined with " and ", and the clauses are appended in
|
|
689
|
+
* start-then-end order.
|
|
690
|
+
*/
|
|
691
|
+
export function composeCameraMotionTermFromConnections(
|
|
692
|
+
motionId: string | undefined,
|
|
693
|
+
startTerms: ReadonlyArray<string>,
|
|
694
|
+
endTerms: ReadonlyArray<string>,
|
|
695
|
+
): string {
|
|
696
|
+
const base = getCameraMotionTerm(motionId)
|
|
697
|
+
if (!base) return ""
|
|
698
|
+
const parts: string[] = [base]
|
|
699
|
+
const startClause = startTerms.filter((t) => t && t.length > 0).join(" and ")
|
|
700
|
+
const endClause = endTerms.filter((t) => t && t.length > 0).join(" and ")
|
|
701
|
+
if (startClause) parts.push(`beginning with ${startClause}`)
|
|
702
|
+
if (endClause) parts.push(`ending with ${endClause}`)
|
|
703
|
+
return parts.join(", ")
|
|
704
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { PickerCatalog, PickerOption, PickerDimension } from "./picker-catalogs.js"
|
|
2
|
+
import type { LocaleId, LocaleCatalogMap } from "@nodaro/shared"
|
|
3
|
+
import { registerCatalogSidecars, resetCatalogSidecars } from "@nodaro/shared"
|
|
4
|
+
import { resolveTerm } from "./term.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Every option that leaves this module carries a RESOLVED `term`, exactly like
|
|
8
|
+
* the base registry's own options do.
|
|
9
|
+
*
|
|
10
|
+
* `PickerOption.term` is required at the type level, but a pack arrives from a
|
|
11
|
+
* separately-compiled bundle that may have been built against a `@nodaro/prompts`
|
|
12
|
+
* where the field did not exist yet — so at runtime a pack option can simply
|
|
13
|
+
* not have one. Resolving at COMPOSITION (rather than at each read) is what
|
|
14
|
+
* keeps the `/v1/catalogs` projection, the compact-hint read path and the
|
|
15
|
+
* `replace`-mode packs agreeing; a pack-added value would otherwise inject its
|
|
16
|
+
* full hint in full mode and NOTHING in compact.
|
|
17
|
+
*/
|
|
18
|
+
/** Pack-author input shapes: identical to the registry's own types except that
|
|
19
|
+
* `term` is OPTIONAL — a vendored pack may predate the field, and a new
|
|
20
|
+
* required field on a published input type would be a breaking change. The
|
|
21
|
+
* composition root resolves it (`withTerm`), so everything that LEAVES this
|
|
22
|
+
* module still satisfies `PickerOption` with a resolved `term`. */
|
|
23
|
+
export type PickerOptionInput = Omit<PickerOption, "term"> & { readonly term?: string }
|
|
24
|
+
export type PickerDimensionInput = Omit<PickerDimension, "options"> & { readonly options: readonly PickerOptionInput[] }
|
|
25
|
+
export type PickerCatalogInput = Omit<PickerCatalog, "options" | "dimensions"> & {
|
|
26
|
+
readonly options?: readonly PickerOptionInput[]
|
|
27
|
+
readonly dimensions?: readonly PickerDimensionInput[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function withTerm(o: PickerOptionInput): PickerOption {
|
|
31
|
+
return { ...o, term: resolveTerm(o) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CatalogPackMode = "replace" | "extend" | "deny"
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A vendored curation pack applied at the picker-catalog composition root.
|
|
38
|
+
* `replace` swaps a full vendored copy; `extend` appends options (single-dim)
|
|
39
|
+
* or merges dimensions by field (multi-dim); `deny` removes entry ids from the
|
|
40
|
+
* copy. Packs target EXISTING catalog ids only — a new catalog id is out of
|
|
41
|
+
* Phase-0 scope (it drags the 5-registry parameter-picker checklist).
|
|
42
|
+
*
|
|
43
|
+
* DEFERRED (do not build here): a `CatalogPolicy` (tags / deny-by-tag /
|
|
44
|
+
* override) plugs in downstream of this compose, at `getRegisteredPickerCatalogs`.
|
|
45
|
+
*/
|
|
46
|
+
export interface CatalogPack {
|
|
47
|
+
readonly id: string
|
|
48
|
+
readonly catalogId: string
|
|
49
|
+
readonly mode: CatalogPackMode
|
|
50
|
+
readonly catalog?: PickerCatalogInput
|
|
51
|
+
readonly options?: readonly PickerOptionInput[]
|
|
52
|
+
readonly dimensions?: readonly PickerDimensionInput[]
|
|
53
|
+
readonly denyIds?: readonly string[]
|
|
54
|
+
/** Localized strings for this pack's added option ids, keyed by locale → id. */
|
|
55
|
+
readonly sidecars?: Partial<Record<LocaleId, LocaleCatalogMap>>
|
|
56
|
+
/** Locales deliberately not translated for this pack (reported, never failed). */
|
|
57
|
+
readonly exemptSidecarLocales?: readonly LocaleId[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let packs: CatalogPack[] = []
|
|
61
|
+
let version = 0
|
|
62
|
+
|
|
63
|
+
export function registerCatalogPack(pack: CatalogPack): void {
|
|
64
|
+
if (packs.some((p) => p.id === pack.id)) throw new Error(`duplicate catalog pack id "${pack.id}"`)
|
|
65
|
+
packs = [...packs, pack]
|
|
66
|
+
version++
|
|
67
|
+
// Push this pack's localized sidecars into the shared app-UI localizer (G10)
|
|
68
|
+
// so pack-added entries resolve in `resolveLabel`/`resolveDescription`/search.
|
|
69
|
+
// One generic point covers every pack, incl. the person extend fan-out.
|
|
70
|
+
registerCatalogSidecars(pack.catalogId, pack.sidecars)
|
|
71
|
+
}
|
|
72
|
+
export function getRegisteredCatalogPacks(): readonly CatalogPack[] { return packs }
|
|
73
|
+
export function resetCatalogPacks(): void { packs = []; version++; resetCatalogSidecars() }
|
|
74
|
+
export function catalogPacksVersion(): number { return version }
|
|
75
|
+
|
|
76
|
+
function cloneCatalog(c: PickerCatalogInput): PickerCatalog {
|
|
77
|
+
return {
|
|
78
|
+
...c,
|
|
79
|
+
options: c.options ? c.options.map(withTerm) : undefined,
|
|
80
|
+
dimensions: c.dimensions
|
|
81
|
+
? c.dimensions.map((d) => ({ ...d, options: d.options.map(withTerm) }))
|
|
82
|
+
: undefined,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function applyExtend(c: PickerCatalog, pack: CatalogPack): PickerCatalog {
|
|
87
|
+
if (c.kind === "single") {
|
|
88
|
+
return { ...c, options: [...(c.options ?? []), ...(pack.options ?? []).map(withTerm)] }
|
|
89
|
+
}
|
|
90
|
+
const byField = new Map((c.dimensions ?? []).map((d) => [d.field, { ...d, options: [...d.options] }]))
|
|
91
|
+
for (const dim of pack.dimensions ?? []) {
|
|
92
|
+
const added = dim.options.map(withTerm)
|
|
93
|
+
const existing = byField.get(dim.field)
|
|
94
|
+
if (existing) existing.options = [...existing.options, ...added]
|
|
95
|
+
else byField.set(dim.field, { ...dim, options: added })
|
|
96
|
+
}
|
|
97
|
+
const dims = [...byField.values()]
|
|
98
|
+
return { ...c, dimensions: dims, fields: dims.map((d) => d.field) }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function applyDeny(c: PickerCatalog, denyIds: readonly string[]): PickerCatalog {
|
|
102
|
+
const deny = new Set(denyIds)
|
|
103
|
+
if (c.kind === "single") return { ...c, options: (c.options ?? []).filter((o) => !deny.has(o.id)) }
|
|
104
|
+
return { ...c, dimensions: (c.dimensions ?? []).map((d) => ({ ...d, options: d.options.filter((o) => !deny.has(o.id)) })) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function composePickerCatalogs(
|
|
108
|
+
base: readonly PickerCatalog[],
|
|
109
|
+
activePacks: readonly CatalogPack[],
|
|
110
|
+
): readonly PickerCatalog[] {
|
|
111
|
+
const known = new Set(base.map((c) => c.catalogId))
|
|
112
|
+
for (const pack of activePacks) {
|
|
113
|
+
if (!known.has(pack.catalogId)) throw new Error(`catalog pack "${pack.id}" targets unknown catalog id "${pack.catalogId}"`)
|
|
114
|
+
}
|
|
115
|
+
return base.map((c) => {
|
|
116
|
+
let out = cloneCatalog(c)
|
|
117
|
+
for (const pack of activePacks) {
|
|
118
|
+
if (pack.catalogId !== c.catalogId) continue
|
|
119
|
+
if (pack.mode === "replace" && pack.catalog) out = cloneCatalog(pack.catalog)
|
|
120
|
+
else if (pack.mode === "extend") out = applyExtend(out, pack)
|
|
121
|
+
else if (pack.mode === "deny") out = applyDeny(out, pack.denyIds ?? [])
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
})
|
|
125
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NON_EN_LOCALE_IDS, type LocaleId } from "@nodaro/shared"
|
|
2
|
+
import type { CatalogPack } from "./catalog-packs.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sidecar-localization coverage for a curation pack's added option ids. For
|
|
6
|
+
* every pack-added id, each non-English locale must either have a sidecar entry
|
|
7
|
+
* or be declared exempt (`pack.exemptSidecarLocales`). Exemptions are REPORTED,
|
|
8
|
+
* never treated as failures — a deployment may knowingly ship a pack with only
|
|
9
|
+
* a subset of the 11 locales translated.
|
|
10
|
+
*/
|
|
11
|
+
export interface SidecarCoverageReport {
|
|
12
|
+
readonly total: number
|
|
13
|
+
readonly missing: ReadonlyArray<{ catalogId: string; locale: LocaleId; id: string }>
|
|
14
|
+
readonly exempted: ReadonlyArray<{ catalogId: string; locale: LocaleId }>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function packOptionIds(pack: CatalogPack): string[] {
|
|
18
|
+
if (pack.options) return pack.options.map((o) => o.id)
|
|
19
|
+
if (pack.dimensions) return pack.dimensions.flatMap((d) => d.options.map((o) => o.id))
|
|
20
|
+
if (pack.catalog?.options) return pack.catalog.options.map((o) => o.id)
|
|
21
|
+
if (pack.catalog?.dimensions) return pack.catalog.dimensions.flatMap((d) => d.options.map((o) => o.id))
|
|
22
|
+
return []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function computePackSidecarCoverage(pack: CatalogPack): SidecarCoverageReport {
|
|
26
|
+
const ids = packOptionIds(pack)
|
|
27
|
+
const exempt = new Set<LocaleId>(pack.exemptSidecarLocales ?? [])
|
|
28
|
+
const missing: Array<{ catalogId: string; locale: LocaleId; id: string }> = []
|
|
29
|
+
const exempted: Array<{ catalogId: string; locale: LocaleId }> = []
|
|
30
|
+
for (const locale of NON_EN_LOCALE_IDS) {
|
|
31
|
+
if (exempt.has(locale)) { exempted.push({ catalogId: pack.catalogId, locale }); continue }
|
|
32
|
+
const map = pack.sidecars?.[locale] ?? {}
|
|
33
|
+
for (const id of ids) if (!map[id]) missing.push({ catalogId: pack.catalogId, locale, id })
|
|
34
|
+
}
|
|
35
|
+
return { total: ids.length * NON_EN_LOCALE_IDS.length, missing, exempted }
|
|
36
|
+
}
|