@nodaro/shared 2.1.0 → 2.2.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.
@@ -11,7 +11,10 @@ import { DEFAULT_LABEL_BY_SOURCE, type ReferenceSource } from "./types.js"
11
11
  export const REFERENCE_ROLE_PRESETS: Record<ReferenceSource, readonly string[]> = {
12
12
  "wired-character": ["ref-only", "person", "face", "clothes", "hair", "pose", "expression", "style"],
13
13
  "wired-face": ["face", "person", "expression", "style"],
14
- "wired-location": ["ref-only", "background", "atmosphere", "as-is", "empty background", "layout", "lighting", "style"],
14
+ // "location" sits second, mirroring wired-character's ref-only/person order
15
+ // it is this source's default. "background" keeps its place for the genuine
16
+ // backdrop case (see DEFAULT_LABEL_BY_SOURCE for why it stopped being default).
17
+ "wired-location": ["ref-only", "location", "background", "atmosphere", "as-is", "empty background", "layout", "lighting", "style"],
15
18
  "wired-object": ["object", "shape", "material", "color", "texture", "style"],
16
19
  "wired-creature": ["creature", "anatomy", "markings", "pose", "color", "style"],
17
20
  "wired-image": ["object", "person", "face", "clothes", "background", "style", "pose", "texture"],
package/src/types.ts CHANGED
@@ -224,7 +224,17 @@ export const DEFAULT_LABEL_BY_SOURCE: Record<ReferenceSource, string> = {
224
224
  // subject ("match anatomy, markings…"), not a prop. See CREATURE_LABELS in
225
225
  // prompt-builder.ts.
226
226
  "wired-creature": "creature",
227
- "wired-location": "background",
227
+ // A PLACE, NOT A BACKDROP (2026-08-05). This defaulted to "background", which
228
+ // `roleToPhrase` renders as "the background from reference image B" — and the
229
+ // image models read that as "paste this behind the subject". Measured on
230
+ // gpt-image-2 (character + location, 4 draws per arm): with "background" every
231
+ // draw was a cut-out composite — an indoor-lit subject over a stock beach, no
232
+ // cast shadow, ignoring the asked-for action. Changing ONLY this word put the
233
+ // subject inside the scene with ground contact and a matching sun. "background"
234
+ // remains a curated pick in REFERENCE_ROLE_PRESETS for the genuine backdrop
235
+ // case; it is no longer what every location silently gets. Nodes that stored a
236
+ // role explicitly are untouched — `resolveDefaultRole` prefers the explicit value.
237
+ "wired-location": "location",
228
238
  }
229
239
 
230
240
  // ---------------------------------------------------------------------------
@@ -29,6 +29,10 @@
29
29
  * against it and fails on drift. There is deliberately NO app-side formula to
30
30
  * check against — it was moved private in 2026-07 and the old backend test
31
31
  * went with it.
32
+ *
33
+ * `VIDEO_AUDIT_BUCKET_CREDITS` further down is the sibling table for the
34
+ * `video-audit` node ("AI Audit") — same module, same bucket ladder, same
35
+ * generator-authoritative pattern, its own two credit families.
32
36
  */
33
37
 
34
38
  export const VIDEO_ANALYSIS_DURATION_BUCKETS = [60, 180, 360, 600] as const
@@ -67,38 +71,65 @@ export const VIDEO_ANALYSIS_WINDOW = { LEN: WINDOW_LEN, STRIDE: WINDOW_STRIDE, O
67
71
  // Net effect: smart drops 27–47% per bucket — the 24 fps token spend was also
68
72
  // partly paying for media tokens the provider clamped and never counted — and
69
73
  // the economy rows tick up 3–6% from the prompt-token true-up.
74
+ //
75
+ // REGENERATED 2026-08-03 — V1 hybrid-smart reprice (task A3), from the
76
+ // plugin's own generator (`scripts/gen-va-buckets.mjs`) at
77
+ // nodaroai/nodaro-cloud-plugins commit eef077d (branch fix/va-cost-trueup).
78
+ // This is the V1 true-up of task P6's provisional judge/refine/frame-judge
79
+ // constants, re-derived from a 2026-08-03 staging measurement and approved by
80
+ // Tal (the constants themselves, like the rest of the $-derived formula, stay
81
+ // private in the plugin repo — never in this public package). `smart` is now
82
+ // a HYBRID plan — one native 6fps skeleton pass plus 2 fast + 2 pro donor
83
+ // rolls, always refined (`selectionMode` does not apply to `smart`; it always
84
+ // refines) — and every multi-roll tier now carries its own explicit
85
+ // judge/refine terms instead of an implicit share of a single-pass budget.
86
+ // This is the full, honest reprice Tal approved, including the economy tiers
87
+ // (fast 33->185 @180s ends a below-cost combine exposure that existed at the
88
+ // old price). Net effect, per bucket (every row rises):
89
+ //
90
+ // gemini-3-flash 60s 24->180 180s 33->185 360s 86-> 514 600s 143-> 846
91
+ // gemini-3.6-flash 60s 65->203 180s 92->218 360s 237-> 598 600s 395-> 986
92
+ // gemini-3.1-pro 60s 87->215 180s 116->231 360s 305-> 636 600s 509->1050
93
+ // mixed 60s 110->228 180s 149->249 360s 390-> 684 600s 651->1129
94
+ // mixed (2026-08-04) 60s 228->268 180s 249->289 360s 684-> 724 600s 1129->1169 (+40 flat: continuity pass joined the family)
95
+ // smart 60s 333->410 180s 470->500 360s 1135->1259 600s 1868->2064
96
+ //
97
+ // Values are pasted verbatim from the plugin generator's output — never hand
98
+ // computed. The plugin's cost test cross-checks every row, sentinels included.
70
99
  export const VIDEO_ANALYSIS_BUCKET_CREDITS: Record<string, number> = {
71
100
  // Legacy fast-tier model (pre-2026-07) — kept for stored raw-id configs.
72
- "video-analysis:gemini-3-flash:60s": 24,
73
- "video-analysis:gemini-3-flash:180s": 33,
74
- "video-analysis:gemini-3-flash:360s": 86,
75
- "video-analysis:gemini-3-flash:600s": 143,
101
+ "video-analysis:gemini-3-flash:60s": 180,
102
+ "video-analysis:gemini-3-flash:180s": 185,
103
+ "video-analysis:gemini-3-flash:360s": 514,
104
+ "video-analysis:gemini-3-flash:600s": 846,
76
105
  // Current fast tier — regenerated from the private formula for its backing
77
106
  // model; higher than the legacy fast schedule but still ≤ pro per bucket.
78
- "video-analysis:gemini-3.6-flash:60s": 65,
79
- "video-analysis:gemini-3.6-flash:180s": 92,
80
- "video-analysis:gemini-3.6-flash:360s": 237,
81
- "video-analysis:gemini-3.6-flash:600s": 395,
82
- "video-analysis:gemini-3.1-pro:60s": 87,
83
- "video-analysis:gemini-3.1-pro:180s": 116,
84
- "video-analysis:gemini-3.1-pro:360s": 305,
85
- "video-analysis:gemini-3.1-pro:600s": 509,
107
+ "video-analysis:gemini-3.6-flash:60s": 203,
108
+ "video-analysis:gemini-3.6-flash:180s": 218,
109
+ "video-analysis:gemini-3.6-flash:360s": 598,
110
+ "video-analysis:gemini-3.6-flash:600s": 986,
111
+ "video-analysis:gemini-3.1-pro:60s": 215,
112
+ "video-analysis:gemini-3.1-pro:180s": 231,
113
+ "video-analysis:gemini-3.1-pro:360s": 636,
114
+ "video-analysis:gemini-3.1-pro:600s": 1050,
86
115
  // Mixed tiers (`mixed` + `mixed-fast`) share ONE credit family — they are
87
116
  // variants of the same engine plan (plan internals live in the private
88
117
  // analysis plugin). Admin-tunable via model_pricing like every other row.
89
- "video-analysis:mixed:60s": 110,
90
- "video-analysis:mixed:180s": 149,
91
- "video-analysis:mixed:360s": 390,
92
- "video-analysis:mixed:600s": 651,
93
- // SMART — the one native-transport plan: a single pass with reasoning turned
94
- // all the way up at a measured-optimal sampling rate. Priced above the
95
- // economy tiers because it genuinely costs more to run; the only tier whose
96
- // accuracy is validated against a hand-counted edit list, re-validated at
97
- // the current sampling rate before this schedule shipped.
98
- "video-analysis:smart:60s": 333,
99
- "video-analysis:smart:180s": 470,
100
- "video-analysis:smart:360s": 1135,
101
- "video-analysis:smart:600s": 1868,
118
+ "video-analysis:mixed:60s": 268,
119
+ "video-analysis:mixed:180s": 289,
120
+ "video-analysis:mixed:360s": 724,
121
+ "video-analysis:mixed:600s": 1169,
122
+ // SMART — the accuracy tier, and since the 2026-08-03 hybrid re-plan a
123
+ // multi-roll plan like the others: one native 6fps skeleton pass plus 2
124
+ // fast + 2 pro donor rolls, always refined (`selectionMode` does not apply
125
+ // here smart always refines; it never offers a cheaper "choose" path).
126
+ // Priced above the economy tiers because it genuinely costs more to run;
127
+ // the only tier whose accuracy is validated against a hand-counted edit
128
+ // list, re-validated at the current plan before this schedule shipped.
129
+ "video-analysis:smart:60s": 410,
130
+ "video-analysis:smart:180s": 500,
131
+ "video-analysis:smart:360s": 1259,
132
+ "video-analysis:smart:600s": 2064,
102
133
  }
103
134
 
104
135
  /**
@@ -132,3 +163,92 @@ export function bucketSecondsFromCreditId(creditId: string): number | null {
132
163
  export function videoAnalysisNumWindows(bucketSec: number): number {
133
164
  return bucketSec <= VIDEO_ANALYSIS_WINDOW.SINGLE_MAX ? 1 : 1 + Math.ceil((bucketSec - WINDOW_LEN) / WINDOW_STRIDE)
134
165
  }
166
+
167
+ /**
168
+ * Precomputed credit cost for the `video-audit` node ("AI Audit") — the same
169
+ * pattern as `VIDEO_ANALYSIS_BUCKET_CREDITS` above: the OUTPUT of the private
170
+ * `videoAuditBucketCredits` formula in `@nodaroai/cloud-plugins`
171
+ * (`src/plugins/video-analysis/cost.ts`), a plain lookup table never a
172
+ * formula, cross-checked against that package's own cost test. Shares the
173
+ * SAME duration-bucket ladder as video-analysis (`VIDEO_ANALYSIS_DURATION_BUCKETS`
174
+ * / `pickVideoAnalysisBucket`) — the audit re-watches the same clip, so it
175
+ * buckets identically.
176
+ *
177
+ * Two credit families, selected by whether an analysis was wired into the
178
+ * node (see `buildVideoAuditCreditId`):
179
+ * - `video-audit:<bucket>s` — an analysis was provided; the audit re-watches
180
+ * the clip against it directly (native watcher roll + reasoner pass, at a
181
+ * safety margin — internals stay private in the plugin).
182
+ * - `video-audit:auto:<bucket>s` — no analysis wired; the node auto-runs a
183
+ * fast-tier analysis first. Exactly the base family's price PLUS the
184
+ * `gemini-3-flash` (legacy fast tier) row from `VIDEO_ANALYSIS_BUCKET_CREDITS`
185
+ * at the SAME bucket — summed in the plugin's generator/formula, never
186
+ * hand-added here (see the single-source assertion in
187
+ * video-analysis-pricing.test.ts).
188
+ *
189
+ * Bare ids (`video-audit`, `video-audit:auto` — they live in
190
+ * `model-catalog.ts`'s pricing rows, not as keys in this table) equal their
191
+ * family's 600s ceiling, the same "unknown duration → ceiling" convention
192
+ * `VIDEO_ANALYSIS_BUCKET_CREDITS`'s bare per-model catalog rows use.
193
+ * `buildVideoAuditCreditId` never returns a bare id itself — an
194
+ * unknown/invalid duration resolves to the bucketed 600s id, matching
195
+ * `buildVideoAnalysisCreditId`.
196
+ *
197
+ * Values pasted verbatim from the plugin generator's output
198
+ * (`scripts/gen-va-buckets.mjs`) at `@nodaroai/cloud-plugins` v0.102.0 —
199
+ * never hand computed. The plugin's cost test cross-checks every row.
200
+ */
201
+ export const VIDEO_AUDIT_BUCKET_CREDITS: Record<string, number> = {
202
+ "video-audit:60s": 213,
203
+ "video-audit:180s": 289,
204
+ "video-audit:360s": 659,
205
+ "video-audit:600s": 1066,
206
+ "video-audit:auto:60s": 393,
207
+ "video-audit:auto:180s": 474,
208
+ "video-audit:auto:360s": 1173,
209
+ "video-audit:auto:600s": 1912,
210
+ }
211
+
212
+ /**
213
+ * Composite credit-id builder for `video-audit`. Mirrors
214
+ * `buildVideoAnalysisCreditId`'s bucket selection and unknown/invalid-duration
215
+ * ceiling fallback exactly (same ladder, same rounding — `durationSec <=
216
+ * bucket`, clamped to the 600s max), but selects the FAMILY by whether an
217
+ * analysis was already provided instead of by model: `analysisProvided:
218
+ * true` prices under the cheaper `video-audit` family (re-audits an existing
219
+ * analysis); `false` prices under `video-audit:auto` (the node auto-runs a
220
+ * fast analysis first, hence that family's built-in fast-tier addition).
221
+ */
222
+ export function buildVideoAuditCreditId(args: { analysisProvided: boolean; durationSec?: number }): string {
223
+ const { analysisProvided, durationSec } = args
224
+ const bucket = durationSec !== undefined && durationSec > 0
225
+ ? pickVideoAnalysisBucket(Math.min(durationSec, VIDEO_ANALYSIS_MAX_DURATION_SEC))
226
+ : VIDEO_ANALYSIS_MAX_DURATION_SEC // no/invalid duration → ceiling composite, same convention as buildVideoAnalysisCreditId
227
+ const family = analysisProvided ? "video-audit" : "video-audit:auto"
228
+ return `${family}:${bucket}s`
229
+ }
230
+
231
+ /**
232
+ * Table lookup for a resolved bucket + family. Snaps `bucketSec` onto the
233
+ * ladder via `pickVideoAnalysisBucket` first, so a raw duration (not just an
234
+ * exact 60/180/360/600) is safe to pass — mirrors `buildVideoAuditCreditId`'s
235
+ * own rounding rather than requiring the caller to pre-round.
236
+ */
237
+ export function videoAuditCreditsForBucket(bucketSec: number, auto: boolean): number {
238
+ const bucket = pickVideoAnalysisBucket(bucketSec)
239
+ return VIDEO_AUDIT_BUCKET_CREDITS[`video-audit${auto ? ":auto" : ""}:${bucket}s`]!
240
+ }
241
+
242
+ /**
243
+ * Parses a `video-audit:*` credit id back to its bucket seconds — both
244
+ * families (`video-audit:<n>s` and `video-audit:auto:<n>s`); null on
245
+ * anything else (a bare id with no bucket, a `video-analysis:*` id, or
246
+ * garbage). Deliberately a SEPARATE parser from `bucketSecondsFromCreditId`
247
+ * (anchored to the `video-analysis:` prefix and a mandatory model segment) —
248
+ * mirrors the plugin-local audit parser so app code can never mis-parse an
249
+ * audit id with the VA-anchored parser, or vice versa.
250
+ */
251
+ export function bucketSecondsFromAuditCreditId(id: string): number | null {
252
+ const m = /^video-audit(?::auto)?:(\d+)s$/.exec(id)
253
+ return m ? Number(m[1]) : null
254
+ }
@@ -491,8 +491,24 @@ export function renderAnalyzedScene(scene: { visual: string }, slots: EntitySlot
491
491
  return scene.visual.replace(SLOT_TOKEN_RE, (_whole, id: string) => castMap?.[id] ?? byId.get(id)?.description ?? id)
492
492
  }
493
493
 
494
+ /**
495
+ * Float slack for the oversize comparison.
496
+ *
497
+ * Scene boundaries arrive as decimal seconds and the duration is derived by
498
+ * SUBTRACTING two of them, which does not land on the cap exactly: a real job
499
+ * produced a 12.67 → 20.67 scene whose computed length is 8.000000000000002,
500
+ * so a scene sitting precisely ON the 8s cap was flagged oversized and carried
501
+ * a `oversized: true` defect marker downstream for no reason.
502
+ *
503
+ * A microsecond is orders of magnitude below any boundary precision the
504
+ * analyzer can actually resolve (windows land on ~0.01s at best), so this can
505
+ * never mask a genuinely oversized scene — the smallest real overshoot is
506
+ * still ~10000x larger than the tolerance.
507
+ */
508
+ const OVERSIZE_TOLERANCE_SEC = 1e-6
509
+
494
510
  export function isOversizedScene(startSec: number, endSec: number): boolean {
495
- return endSec - startSec > VIDEO_ANALYSIS_MAX_SCENE_SEC
511
+ return endSec - startSec > VIDEO_ANALYSIS_MAX_SCENE_SEC + OVERSIZE_TOLERANCE_SEC
496
512
  }
497
513
 
498
514
  const STANDARD_RATIOS: Array<[string, number]> = [