@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.
- package/dist/index.cjs +2047 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1556 -27
- package/dist/index.d.ts +1556 -27
- package/dist/index.js +1861 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/caption-styles.test.ts +207 -0
- package/src/__tests__/edl-multicam.test.ts +304 -0
- package/src/__tests__/edl.test.ts +822 -0
- package/src/__tests__/fan-out-rows.test.ts +208 -0
- package/src/__tests__/instagram-scrape.test.ts +66 -0
- package/src/__tests__/llm-models.test.ts +48 -11
- package/src/__tests__/meta-ads-scrape.test.ts +284 -0
- package/src/__tests__/node-runtime-keys.test.ts +15 -0
- package/src/__tests__/presentation-utils.test.ts +67 -0
- package/src/__tests__/producer-types.test.ts +19 -0
- package/src/__tests__/schedule-rules.test.ts +265 -0
- package/src/__tests__/speaker-layouts.test.ts +203 -0
- package/src/__tests__/transcribe-capabilities.test.ts +104 -0
- package/src/__tests__/transcribe-preflight.test.ts +60 -0
- package/src/__tests__/trigger-feeds.test.ts +39 -0
- package/src/__tests__/video-duration-auto.test.ts +65 -0
- package/src/__tests__/video-duration.test.ts +56 -0
- package/src/__tests__/video-link.test.ts +137 -0
- package/src/__tests__/workflow-export-strip.test.ts +59 -1
- package/src/caption-styles.ts +240 -0
- package/src/credit-identifiers.ts +31 -0
- package/src/edit-plan-contract.ts +96 -0
- package/src/edl-multicam.ts +185 -0
- package/src/edl.ts +747 -0
- package/src/entity-image-handle.ts +24 -1
- package/src/fan-out-rows.ts +213 -0
- package/src/i18n/action-fx.he.ts +8 -8
- package/src/i18n/aesthetic.he.ts +8 -8
- package/src/i18n/animals.he.ts +34 -34
- package/src/i18n/atmosphere.he.ts +6 -6
- package/src/i18n/backdrop.he.ts +7 -7
- package/src/i18n/camera-format.he.ts +4 -4
- package/src/i18n/camera-motions.he.ts +20 -20
- package/src/i18n/character-fx.he.ts +16 -16
- package/src/i18n/character-motion.he.ts +184 -184
- package/src/i18n/color-look.he.ts +4 -4
- package/src/i18n/composition-effects.he.ts +3 -3
- package/src/i18n/era.he.ts +9 -9
- package/src/i18n/exposure-settings.he.ts +2 -2
- package/src/i18n/framing.he.ts +13 -13
- package/src/i18n/furniture.he.ts +52 -52
- package/src/i18n/held-prop.he.ts +10 -10
- package/src/i18n/instrumentation.he.ts +26 -26
- package/src/i18n/lens.he.ts +6 -6
- package/src/i18n/lighting.he.ts +17 -17
- package/src/i18n/loop-subject.he.ts +4 -4
- package/src/i18n/materials.he.ts +8 -8
- package/src/i18n/mood.he.ts +12 -12
- package/src/i18n/music-genre.he.ts +9 -9
- package/src/i18n/music-mood.he.ts +7 -7
- package/src/i18n/person.he.ts +94 -94
- package/src/i18n/photo-genre.he.ts +24 -24
- package/src/i18n/photographer.he.ts +21 -21
- package/src/i18n/pose.he.ts +14 -14
- package/src/i18n/post-process-effects.he.ts +4 -4
- package/src/i18n/render-quality.he.ts +7 -7
- package/src/i18n/setting.he.ts +14 -14
- package/src/i18n/style.he.ts +9 -9
- package/src/i18n/styling.he.ts +126 -126
- package/src/i18n/temporal.he.ts +8 -8
- package/src/i18n/transitions.he.ts +9 -9
- package/src/i18n/vehicles.he.ts +25 -25
- package/src/i18n/voice-character.he.ts +14 -14
- package/src/i18n/voice-delivery.he.ts +11 -11
- package/src/i18n/weapons.he.ts +23 -23
- package/src/index.ts +206 -3
- package/src/instagram-scrape.ts +204 -0
- package/src/llm-models.ts +80 -3
- package/src/meta-ads-scrape.ts +463 -0
- package/src/model-catalog.ts +48 -5
- package/src/model-constants.ts +148 -5
- package/src/node-mappable-fields.ts +2 -0
- package/src/node-runtime-keys.ts +28 -0
- package/src/presentation-utils.ts +49 -0
- package/src/producer-types.ts +20 -0
- package/src/schedule-rules.ts +484 -0
- package/src/speaker-layouts.ts +220 -0
- package/src/transcribe-preflight.ts +101 -0
- package/src/trigger-feeds.ts +59 -0
- package/src/trigger-node-types.ts +20 -0
- package/src/video-duration-auto.ts +18 -0
- package/src/video-duration.ts +32 -0
- package/src/video-link.ts +167 -0
- 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
|
+
}
|