@nodaro/shared 3.11.0 → 3.12.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 (91) hide show
  1. package/dist/index.cjs +2047 -84
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1556 -27
  4. package/dist/index.d.ts +1556 -27
  5. package/dist/index.js +1861 -85
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/caption-styles.test.ts +207 -0
  9. package/src/__tests__/edl-multicam.test.ts +304 -0
  10. package/src/__tests__/edl.test.ts +822 -0
  11. package/src/__tests__/fan-out-rows.test.ts +208 -0
  12. package/src/__tests__/instagram-scrape.test.ts +66 -0
  13. package/src/__tests__/llm-models.test.ts +48 -11
  14. package/src/__tests__/meta-ads-scrape.test.ts +284 -0
  15. package/src/__tests__/node-runtime-keys.test.ts +15 -0
  16. package/src/__tests__/presentation-utils.test.ts +67 -0
  17. package/src/__tests__/producer-types.test.ts +19 -0
  18. package/src/__tests__/schedule-rules.test.ts +265 -0
  19. package/src/__tests__/speaker-layouts.test.ts +203 -0
  20. package/src/__tests__/transcribe-capabilities.test.ts +104 -0
  21. package/src/__tests__/transcribe-preflight.test.ts +60 -0
  22. package/src/__tests__/trigger-feeds.test.ts +39 -0
  23. package/src/__tests__/video-duration-auto.test.ts +65 -0
  24. package/src/__tests__/video-duration.test.ts +56 -0
  25. package/src/__tests__/video-link.test.ts +137 -0
  26. package/src/__tests__/workflow-export-strip.test.ts +59 -1
  27. package/src/caption-styles.ts +240 -0
  28. package/src/credit-identifiers.ts +31 -0
  29. package/src/edit-plan-contract.ts +96 -0
  30. package/src/edl-multicam.ts +185 -0
  31. package/src/edl.ts +747 -0
  32. package/src/entity-image-handle.ts +24 -1
  33. package/src/fan-out-rows.ts +213 -0
  34. package/src/i18n/action-fx.he.ts +8 -8
  35. package/src/i18n/aesthetic.he.ts +8 -8
  36. package/src/i18n/animals.he.ts +34 -34
  37. package/src/i18n/atmosphere.he.ts +6 -6
  38. package/src/i18n/backdrop.he.ts +7 -7
  39. package/src/i18n/camera-format.he.ts +4 -4
  40. package/src/i18n/camera-motions.he.ts +20 -20
  41. package/src/i18n/character-fx.he.ts +16 -16
  42. package/src/i18n/character-motion.he.ts +184 -184
  43. package/src/i18n/color-look.he.ts +4 -4
  44. package/src/i18n/composition-effects.he.ts +3 -3
  45. package/src/i18n/era.he.ts +9 -9
  46. package/src/i18n/exposure-settings.he.ts +2 -2
  47. package/src/i18n/framing.he.ts +13 -13
  48. package/src/i18n/furniture.he.ts +52 -52
  49. package/src/i18n/held-prop.he.ts +10 -10
  50. package/src/i18n/instrumentation.he.ts +26 -26
  51. package/src/i18n/lens.he.ts +6 -6
  52. package/src/i18n/lighting.he.ts +17 -17
  53. package/src/i18n/loop-subject.he.ts +4 -4
  54. package/src/i18n/materials.he.ts +8 -8
  55. package/src/i18n/mood.he.ts +12 -12
  56. package/src/i18n/music-genre.he.ts +9 -9
  57. package/src/i18n/music-mood.he.ts +7 -7
  58. package/src/i18n/person.he.ts +94 -94
  59. package/src/i18n/photo-genre.he.ts +24 -24
  60. package/src/i18n/photographer.he.ts +21 -21
  61. package/src/i18n/pose.he.ts +14 -14
  62. package/src/i18n/post-process-effects.he.ts +4 -4
  63. package/src/i18n/render-quality.he.ts +7 -7
  64. package/src/i18n/setting.he.ts +14 -14
  65. package/src/i18n/style.he.ts +9 -9
  66. package/src/i18n/styling.he.ts +126 -126
  67. package/src/i18n/temporal.he.ts +8 -8
  68. package/src/i18n/transitions.he.ts +9 -9
  69. package/src/i18n/vehicles.he.ts +25 -25
  70. package/src/i18n/voice-character.he.ts +14 -14
  71. package/src/i18n/voice-delivery.he.ts +11 -11
  72. package/src/i18n/weapons.he.ts +23 -23
  73. package/src/index.ts +206 -3
  74. package/src/instagram-scrape.ts +204 -0
  75. package/src/llm-models.ts +80 -3
  76. package/src/meta-ads-scrape.ts +463 -0
  77. package/src/model-catalog.ts +48 -5
  78. package/src/model-constants.ts +148 -5
  79. package/src/node-mappable-fields.ts +2 -0
  80. package/src/node-runtime-keys.ts +28 -0
  81. package/src/presentation-utils.ts +49 -0
  82. package/src/producer-types.ts +20 -0
  83. package/src/schedule-rules.ts +484 -0
  84. package/src/speaker-layouts.ts +220 -0
  85. package/src/transcribe-preflight.ts +101 -0
  86. package/src/trigger-feeds.ts +59 -0
  87. package/src/trigger-node-types.ts +20 -0
  88. package/src/video-duration-auto.ts +18 -0
  89. package/src/video-duration.ts +32 -0
  90. package/src/video-link.ts +167 -0
  91. package/src/workflow-export.ts +37 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The edit-plan node's credit-id scheme and output unwrap — split out of
3
+ * `edl.ts` (which keeps the EDL shape itself) so that module stays within the
4
+ * file-size cap. Re-exported from the package index, so importers of
5
+ * `@nodaro/shared` are unaffected.
6
+ */
7
+ import { EDL_VERSION } from "./edl.js"
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────
10
+ // edit-plan credit-id scheme (STRUCTURE only — the per-mode/tier/bucket credit
11
+ // VALUES live app-side in backend/ee/billing/credits.ts + migration 432, since
12
+ // they are probe-set placeholders and the DB row wins at runtime). Lives HERE
13
+ // so BOTH core payload-builder and ee credits.ts read one id builder — core
14
+ // may not import ee (check-ee-imports), the same reason buildVideoAnalysisCreditId
15
+ // is shared. Mirrors the plugin's own (D13-local) pricing.ts scheme.
16
+ // ─────────────────────────────────────────────────────────────────────────
17
+
18
+ export type EditPlanMode = "tighten" | "clips" | "chapters"
19
+ export type EditPlanTier = "economy" | "standard" | "premium"
20
+ export const EDIT_PLAN_MODES: readonly EditPlanMode[] = ["tighten", "clips", "chapters"]
21
+ export const EDIT_PLAN_TIERS: readonly EditPlanTier[] = ["economy", "standard", "premium"]
22
+ /** The coarse duration ladder (MINUTES) a probed source duration rounds UP to;
23
+ * the composite credit id carries the bucket. 3 modes × 3 tiers × 6 buckets =
24
+ * 54 composites (+ the bare `edit-plan`). */
25
+ export const EDIT_PLAN_BUCKET_MINUTES: readonly number[] = [15, 30, 60, 90, 120, 180]
26
+ /** Hard duration cap (design §7.4). */
27
+ export const EDIT_PLAN_MAX_MINUTES = 180
28
+ /** `clips` mode: how many clips a plan returns when the caller names no count,
29
+ * and the most it may be asked for. One source for the credit estimate, the
30
+ * orchestrated payload clamp and the request schema. */
31
+ export const EDIT_PLAN_DEFAULT_CLIP_COUNT = 8
32
+ export const EDIT_PLAN_MAX_CLIP_COUNT = 50
33
+
34
+ /** Clamp a requested clip count into `[1, EDIT_PLAN_MAX_CLIP_COUNT]`; `undefined`
35
+ * for anything that is not a positive number (the planner then uses its default). */
36
+ export function clampEditPlanClipCount(count: unknown): number | undefined {
37
+ if (typeof count !== "number" || !Number.isFinite(count) || count <= 0) return undefined
38
+ return Math.min(EDIT_PLAN_MAX_CLIP_COUNT, Math.max(1, Math.floor(count)))
39
+ }
40
+ /** The bare estimator / DB-down fallback id. */
41
+ export const EDIT_PLAN_BASE_CREDIT_ID = "edit-plan"
42
+
43
+ /** Round a source duration (seconds) UP to the smallest covering ladder bucket
44
+ * (capped at the max), in minutes. `undefined` / non-finite → the ceiling
45
+ * bucket (the safe over-reserve direction). */
46
+ export function editPlanBucketMinutes(durationSec: number | undefined): number {
47
+ const secs = typeof durationSec === "number" && Number.isFinite(durationSec) ? durationSec : EDIT_PLAN_MAX_MINUTES * 60
48
+ const mins = Math.max(1, Math.ceil(secs / 60))
49
+ const capped = Math.min(mins, EDIT_PLAN_MAX_MINUTES)
50
+ for (const b of EDIT_PLAN_BUCKET_MINUTES) if (capped <= b) return b
51
+ return EDIT_PLAN_BUCKET_MINUTES[EDIT_PLAN_BUCKET_MINUTES.length - 1]!
52
+ }
53
+
54
+ /** `edit-plan:<mode>:<tier>:<bucket>m`. `durationSec` undefined → the ceiling
55
+ * bucket. Single source of truth for the composite id shape. */
56
+ export function buildEditPlanCreditId(mode: EditPlanMode, tier: EditPlanTier, durationSec?: number): string {
57
+ return `edit-plan:${mode}:${tier}:${editPlanBucketMinutes(durationSec)}m`
58
+ }
59
+
60
+ /** Narrow an arbitrary value to a known edit-plan mode, defaulting to "tighten". */
61
+ export function asEditPlanMode(v: unknown): EditPlanMode {
62
+ return v === "clips" || v === "chapters" ? v : "tighten"
63
+ }
64
+
65
+ /** Narrow an arbitrary value to a known edit-plan tier, defaulting to "standard". */
66
+ export function asEditPlanTier(v: unknown): EditPlanTier {
67
+ return v === "economy" || v === "premium" ? v : "standard"
68
+ }
69
+
70
+ /**
71
+ * Unwrap an `edit-plan` job's `output_data` into the value stored on the node's
72
+ * `data.generatedJson`, which every output extractor then reads. This is the ONE
73
+ * place the three modes are normalized (the same rule on both engines and every
74
+ * result-application site, so audit-dag parity can't drift):
75
+ * - `clips` → the BARE `Edl[]` (T5: the `list` fan-out reads `Array.isArray`
76
+ * on `generatedJson`; each element becomes one JSON-stringified
77
+ * item a downstream `edl` input `normalizeEdl`-parses).
78
+ * - `chapters` → the `{ version, chapters }` object.
79
+ * - `tighten` → the `Edl` object at top level.
80
+ *
81
+ * The cloud relay object-spreads `output_data` and adds `viaNodaroCloud: true`;
82
+ * that key (and any other bookkeeping) is stripped here. The unwrap lives HERE —
83
+ * NEVER in `output_data` — because a bare array written into `output_data` would
84
+ * be corrupted into numeric keys by the relay's object-spread (see `EdlClipSet`).
85
+ */
86
+ export function unwrapEditPlanOutput(outputData: unknown): unknown {
87
+ if (!outputData || typeof outputData !== "object") return outputData
88
+ const o = outputData as Record<string, unknown>
89
+ // clips: EdlClipSet { version, clips: Edl[] } → the bare Edl[].
90
+ if (Array.isArray(o.clips)) return o.clips
91
+ // chapters: { version, chapters: [...] } → the object, minus bookkeeping.
92
+ if (Array.isArray(o.chapters)) return { version: EDL_VERSION, chapters: o.chapters }
93
+ // tighten: the Edl object at top level → drop the relay's viaNodaroCloud.
94
+ const { viaNodaroCloud: _viaNodaroCloud, ...rest } = o
95
+ return rest
96
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Multicam helpers over the EDL contract (`edl.ts`): folding measured source
3
+ * offsets into an EDL (D19) and resolving what each on-screen slot of a
4
+ * segment shows (D20). Pure — no I/O. Type-only imports from `edl.ts` keep the
5
+ * module graph free of runtime cycles.
6
+ */
7
+ import type { Edl, EdlRegion, EdlSegment, EdlSource } from "./edl.js"
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────
10
+ // D19 — source offsets
11
+ // ─────────────────────────────────────────────────────────────────────────
12
+
13
+ const hasOwn = (o: object, k: string): boolean => Object.prototype.hasOwnProperty.call(o, k)
14
+ const isFiniteNumber = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v)
15
+
16
+ /** audio-sync → EDL source offsets (D19: masterMs = sourceMs + offsetMs).
17
+ * `offsets` are measured against ONE common reference (audio-sync's `reference`, which need not be the master).
18
+ * ANCHORED SET: anchor = opts.anchor ?? the unique role:"master-audio" source ?? none.
19
+ * - The anchor's own offsetMs is NEVER changed (the segments are on its clock).
20
+ * - Every other provided source s: offsetMs = round((anchor.offsetMs ?? 0) + offsets[s] − (offsets[anchor] ?? 0)).
21
+ * - No anchor: offsetMs = round(offsets[s]) verbatim (offsets already relative to the master clock).
22
+ * SET, never ADD (re-running is idempotent). Returns a new Edl; never mutates; never throws.
23
+ * `ignored[].reason` is a documented open string: "unknown-source" | "not-finite" | "anchor-unknown" | "anchor-not-finite".
24
+ * opts.anchor not in edl.sources → NOTHING applied, ignored = [{ sourceId: anchor, reason: "anchor-unknown" }].
25
+ * offsets[anchor] present but non-finite → NOTHING applied, ignored = [{ sourceId: anchor, reason: "anchor-not-finite" }]. */
26
+ export function mergeEdlSourceOffsets(
27
+ edl: Edl,
28
+ offsets: Readonly<Record<string, number>>,
29
+ opts?: { readonly anchor?: string },
30
+ ): {
31
+ readonly edl: Edl
32
+ /** The source the offsets were anchored to — present whenever one resolved (it exists in `edl.sources`). */
33
+ readonly anchor?: string
34
+ /** Source ids whose `offsetMs` was set, in `offsets` key order. */
35
+ readonly applied: readonly string[]
36
+ readonly ignored: ReadonlyArray<{ readonly sourceId: string; readonly reason: string }>
37
+ } {
38
+ const sources: readonly EdlSource[] = Array.isArray(edl?.sources) ? edl.sources : []
39
+ const rows: Readonly<Record<string, unknown>> = offsets && typeof offsets === "object" ? offsets : {}
40
+ const byId = new Map<string, EdlSource>()
41
+ for (const s of sources) if (s && typeof s === "object" && !byId.has(s.id)) byId.set(s.id, s)
42
+
43
+ let anchor: string | undefined
44
+ if (opts?.anchor !== undefined) {
45
+ if (!byId.has(opts.anchor)) {
46
+ return { edl: { ...edl }, applied: [], ignored: [{ sourceId: opts.anchor, reason: "anchor-unknown" }] }
47
+ }
48
+ anchor = opts.anchor
49
+ } else {
50
+ const masters = sources.filter((s) => s && typeof s === "object" && s.role === "master-audio")
51
+ if (masters.length === 1) anchor = masters[0].id
52
+ }
53
+
54
+ // The anchor's own measurement is the rebase point; an absent one reads as 0.
55
+ let reference = 0
56
+ if (anchor !== undefined && hasOwn(rows, anchor)) {
57
+ const a = rows[anchor]
58
+ if (!isFiniteNumber(a)) {
59
+ return { edl: { ...edl }, anchor, applied: [], ignored: [{ sourceId: anchor, reason: "anchor-not-finite" }] }
60
+ }
61
+ reference = a
62
+ }
63
+ const anchorOffsetMs = anchor !== undefined ? (byId.get(anchor)?.offsetMs ?? 0) : 0
64
+
65
+ const next = new Map<string, number>()
66
+ const applied: string[] = []
67
+ const ignored: Array<{ sourceId: string; reason: string }> = []
68
+ for (const sourceId of Object.keys(rows)) {
69
+ if (sourceId === anchor) continue // never changed: the segments are on its clock
70
+ if (!byId.has(sourceId)) {
71
+ ignored.push({ sourceId, reason: "unknown-source" })
72
+ continue
73
+ }
74
+ const measured = rows[sourceId]
75
+ const offsetMs = isFiniteNumber(measured)
76
+ ? Math.round(anchor !== undefined ? anchorOffsetMs + measured - reference : measured)
77
+ : Number.NaN
78
+ // A non-finite result (the measurement, or a non-finite anchor offsetMs it
79
+ // is rebased onto) is never written.
80
+ if (!Number.isFinite(offsetMs)) {
81
+ ignored.push({ sourceId, reason: "not-finite" })
82
+ continue
83
+ }
84
+ next.set(sourceId, offsetMs)
85
+ applied.push(sourceId)
86
+ }
87
+
88
+ const merged: Edl = Array.isArray(edl?.sources)
89
+ ? { ...edl, sources: edl.sources.map((s) => (s && typeof s === "object" && next.has(s.id) ? { ...s, offsetMs: next.get(s.id)! } : s)) }
90
+ : { ...edl }
91
+ return { edl: merged, ...(anchor !== undefined ? { anchor } : {}), applied, ignored }
92
+ }
93
+
94
+ // ─────────────────────────────────────────────────────────────────────────
95
+ // D20 — slot resolution
96
+ // ─────────────────────────────────────────────────────────────────────────
97
+
98
+ export const EDL_FULL_FRAME: EdlRegion = Object.freeze({ x: 0, y: 0, w: 1, h: 1 })
99
+
100
+ export interface EdlResolvedSlot {
101
+ readonly source: string
102
+ readonly region: EdlRegion
103
+ /** "resolver" = the caller's `regionFor` (v3 per-segment tracks). */
104
+ readonly regionFrom: "slot" | "segment" | "resolver" | "speaker" | "source" | "full"
105
+ readonly speaker?: string
106
+ readonly weight?: number
107
+ }
108
+
109
+ export interface ResolveEdlSlotsOptions {
110
+ /** speaker-view's per-speaker framing table (a node SETTING, not an EDL field), keyed by (source, speaker) so a
111
+ * wide-shot region never lands on a close-up camera framing the same person. */
112
+ readonly speakerRegions?: ReadonlyArray<{ readonly source: string; readonly speaker: string; readonly region: EdlRegion }>
113
+ /** v3 hook: a per-(segment, slot) region, e.g. from a face track. Undefined = no opinion. */
114
+ readonly regionFor?: (q: { readonly segment: EdlSegment; readonly source: string; readonly speaker?: string }) => EdlRegion | undefined
115
+ }
116
+
117
+ /** A valid in-frame box: finite, 0..1, w/h > 0, x+w ≤ 1, y+h ≤ 1 — with the
118
+ * same edge tolerance `validateEdl` accepts, so the two never disagree. */
119
+ function isInFrameRegion(r: unknown): r is EdlRegion {
120
+ if (!r || typeof r !== "object") return false
121
+ const { x, y, w, h } = r as Record<string, unknown>
122
+ for (const v of [x, y, w, h]) if (!isFiniteNumber(v) || v < 0 || v > 1) return false
123
+ const box = r as EdlRegion
124
+ return box.w > 0 && box.h > 0 && box.x + box.w <= 1 + 1e-9 && box.y + box.h <= 1 + 1e-9
125
+ }
126
+
127
+ type Rung = EdlResolvedSlot["regionFrom"]
128
+
129
+ /** D20 — the ONE region-precedence implementation:
130
+ * slot.region ▷ segment.region (single-slot only) ▷ regionFor ▷ speakerRegions[(source, speaker)] ▷ source.region ▷ full frame.
131
+ * Slots = layout.slots when non-empty, else ONE implicit slot from segment.video (no video → []).
132
+ * A slot's speaker = slot.speaker ?? (single slot ? segment.speaker : undefined); no speaker → the speaker rung is skipped.
133
+ * A rung whose region is not a valid in-frame box (finite, 0..1, w/h > 0, x+w ≤ 1, y+h ≤ 1) falls through to the next.
134
+ * Unknown source id → the source rung is skipped. Pure; never throws on its own. */
135
+ export function resolveEdlSegmentSlots(edl: Edl, segment: EdlSegment, opts?: ResolveEdlSlotsOptions): readonly EdlResolvedSlot[] {
136
+ if (!segment || typeof segment !== "object") return []
137
+ const sources: readonly EdlSource[] = Array.isArray(edl?.sources) ? edl.sources : []
138
+ const layoutSlots = Array.isArray(segment.layout?.slots) ? segment.layout!.slots! : []
139
+ const slots: NonNullable<NonNullable<EdlSegment["layout"]>["slots"]> =
140
+ layoutSlots.length > 0
141
+ ? layoutSlots
142
+ : typeof segment.video === "string" && segment.video
143
+ ? [{ source: segment.video }]
144
+ : []
145
+ const single = slots.length === 1
146
+
147
+ const out: EdlResolvedSlot[] = []
148
+ for (const slot of slots) {
149
+ if (!slot || typeof slot !== "object") continue
150
+ const source = slot.source
151
+ const speaker = slot.speaker ?? (single ? segment.speaker : undefined)
152
+
153
+ // Each rung is evaluated lazily, top-down; the first valid box wins.
154
+ const rungs: ReadonlyArray<readonly [Rung, () => unknown]> = [
155
+ ["slot", () => slot.region],
156
+ ["segment", () => (single ? segment.region : undefined)],
157
+ ["resolver", () => opts?.regionFor?.({ segment, source, ...(speaker !== undefined ? { speaker } : {}) })],
158
+ ["speaker", () =>
159
+ speaker === undefined
160
+ ? undefined
161
+ : Array.isArray(opts?.speakerRegions)
162
+ ? opts.speakerRegions.find((row) => row && row.source === source && row.speaker === speaker)?.region
163
+ : undefined],
164
+ ["source", () => sources.find((s) => s && typeof s === "object" && s.id === source)?.region],
165
+ ]
166
+ let region: EdlRegion = EDL_FULL_FRAME
167
+ let regionFrom: Rung = "full"
168
+ for (const [from, read] of rungs) {
169
+ const candidate = read()
170
+ if (isInFrameRegion(candidate)) {
171
+ region = candidate
172
+ regionFrom = from
173
+ break
174
+ }
175
+ }
176
+ out.push({
177
+ source,
178
+ region,
179
+ regionFrom,
180
+ ...(speaker !== undefined ? { speaker } : {}),
181
+ ...(slot.weight !== undefined ? { weight: slot.weight } : {}),
182
+ })
183
+ }
184
+ return out
185
+ }