@nodaro/shared 2.22.0 → 2.24.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.
@@ -0,0 +1,344 @@
1
+ /**
2
+ * The wire contract of `/v1/studio/productions` — TYPES ONLY.
3
+ *
4
+ * A studio production is a Nodaro workflow whose `settings.studio` holds the
5
+ * shots. Its CODE — the codec that reads and writes that document, the plan
6
+ * format, the catalogs and the reducers — lives in `@nodaro/studio-production`,
7
+ * which is FSL-licensed. What lives here is the ENVELOPE the routes return and
8
+ * the bodies they take, because the SDK is typed against this package and an
9
+ * SDK caller has to know the shape of a reply.
10
+ *
11
+ * The document's own sub-objects (the cast, the looks, the scene plan, the
12
+ * cuts) are therefore named JSON aliases rather than re-declared shapes. That
13
+ * is deliberate on both counts:
14
+ *
15
+ * - **Named**, not one anonymous `unknown`, so a `.d.ts` reader can still see
16
+ * which field is which and the SDK's surface documents itself.
17
+ * - **Not re-declared**, because a second definition of the document is exactly
18
+ * the disagreement this contract exists to end — and publishing the studio's
19
+ * domain types under Apache would be an irrevocable grant of code that was
20
+ * deliberately placed one tier down.
21
+ *
22
+ * `@nodaro/studio-production` narrows every one of them to its real type and
23
+ * pins the narrowed view against this one at build time, so the two cannot
24
+ * drift apart in silence. A consumer that wants the narrow types depends on
25
+ * that package; a consumer that only reads the wire uses these.
26
+ */
27
+
28
+ /**
29
+ * How a result is ADDRESSED: its job id when it has one, its url otherwise.
30
+ *
31
+ * Never a position. An index is meaningless the moment another writer inserts
32
+ * a result — and two writers is the normal case here, since an agent and the
33
+ * editor hold the same production open. Uploaded and hand-attached media have
34
+ * no job, which is why the url is the fallback rather than the key.
35
+ */
36
+ export type ResultKey = string
37
+
38
+ // ── the document's own vocabulary, as JSON ──────────────────────────────────
39
+ // Each alias names the `@nodaro/studio-production` type that defines it.
40
+
41
+ /** `LookSelectionMap` — cinematic picks by dimension key. */
42
+ export type StudioLookMapJson = Record<string, unknown>
43
+ /** `Cast` — the production's roles, keyed by role slug. */
44
+ export type StudioCastJson = Record<string, unknown>
45
+ /** `CastLookMap` — which view of each actor a scene pins. */
46
+ export type StudioCastLookMapJson = Record<string, unknown>
47
+ /** `ProductionFolder` — a named timeline folder. */
48
+ export type StudioFolderJson = Record<string, unknown>
49
+ /** `StoryboardSettings` — the Storyboard tab's persisted state (`brief` lives here). */
50
+ export type StudioStoryboardJson = Record<string, unknown>
51
+ /** `ProductionMusic` — the rendered soundtrack muxed over the export. */
52
+ export type StudioMusicJson = Record<string, unknown>
53
+ /** `PlanMusic` — the soundtrack PLAN (prompt + pickers), kept beside the track. */
54
+ export type StudioMusicPlanJson = Record<string, unknown>
55
+ /** `ProductionCut` — one exported cut of the film. */
56
+ export type StudioCutJson = Record<string, unknown>
57
+ /** `ScenePlan` — a scene's authored framing / motion / voice, before it renders. */
58
+ export type StudioPlanJson = Record<string, unknown>
59
+ /** `ShotBeat` — one timed motion window inside a scene. */
60
+ export type StudioBeatJson = Record<string, unknown>
61
+ /** `ShotTransition` — how a scene's last frames go out. */
62
+ export type StudioTransitionJson = Record<string, unknown>
63
+ /** `ShotVoice` — the scene's generated voiceover. */
64
+ export type StudioVoiceJson = Record<string, unknown>
65
+ /** `DirectionFields` / `SubjectFields` — platform catalog ids carried on a result. */
66
+ export type StudioIdFieldsJson = Record<string, unknown>
67
+ /** `ConnectedReference` — a bound `@`-entity chip. */
68
+ export type StudioReferenceJson = Record<string, unknown>
69
+ /** `TrashedItem` — one deleted shot, still or clip, restorable by id. */
70
+ export type StudioTrashItemJson = Record<string, unknown>
71
+
72
+ // ── results ────────────────────────────────────────────────────────────────
73
+
74
+ /**
75
+ * One generated STILL, with the context that regenerates it.
76
+ *
77
+ * Result histories ACCUMULATE — a generate appends, it never replaces — so a
78
+ * shot's stills are every framing candidate it has ever had, and each one
79
+ * carries what it was made with. That is what makes "go back to the second
80
+ * one" a read rather than a re-run.
81
+ */
82
+ export interface StudioResultView {
83
+ key: ResultKey
84
+ url: string
85
+ jobId?: string
86
+ name?: string
87
+ prompt?: string
88
+ negativePrompt?: string
89
+ provider?: string
90
+ referenceImageUrls?: string[]
91
+ references?: StudioReferenceJson[]
92
+ aspectRatio?: string
93
+ resolution?: string
94
+ /** The look layers this generation was sent with — film, scene, then the shot's own. */
95
+ filmLook?: StudioLookMapJson
96
+ sceneLook?: StudioLookMapJson
97
+ look?: StudioLookMapJson
98
+ subject?: StudioIdFieldsJson
99
+ }
100
+
101
+ /**
102
+ * One generated CLIP, with the frames it was animated from.
103
+ *
104
+ * The frames matter more here than they look: selecting a past clip restores
105
+ * the start and end frames THAT clip was made from, which is why they are
106
+ * stored per result rather than read off the shot.
107
+ */
108
+ export interface StudioClipResultView {
109
+ key: ResultKey
110
+ url: string
111
+ jobId?: string
112
+ name?: string
113
+ prompt?: string
114
+ provider?: string
115
+ negativePrompt?: string
116
+ duration?: number
117
+ /** The frames THIS clip was animated from — restored with it, never derived. */
118
+ startFrameUrl?: string
119
+ endFrameUrl?: string
120
+ referenceImageUrls?: string[]
121
+ references?: StudioReferenceJson[]
122
+ beats?: StudioBeatJson[]
123
+ scenePrompt?: string
124
+ endTransition?: StudioTransitionJson
125
+ filmLook?: StudioLookMapJson
126
+ sceneLook?: StudioLookMapJson
127
+ look?: StudioLookMapJson
128
+ subject?: StudioIdFieldsJson
129
+ }
130
+
131
+ /**
132
+ * A framing batch that is STILL RUNNING, with everything needed to land it.
133
+ *
134
+ * A marker rather than a promise: any client — or none — can finish the job,
135
+ * because the context that turns a finished job into a result is written down
136
+ * on the production instead of living in the browser tab that started it.
137
+ */
138
+ export interface PendingStillView {
139
+ jobId: string
140
+ batchId?: string
141
+ provider?: string
142
+ prompt?: string
143
+ count?: number
144
+ startedAt?: string
145
+ }
146
+
147
+ /** An animate that is still running. The clip mirror of {@link PendingStillView}. */
148
+ export interface PendingClipView {
149
+ jobId: string
150
+ provider?: string
151
+ prompt?: string
152
+ startedAt?: string
153
+ }
154
+
155
+ // ── the shot ───────────────────────────────────────────────────────────────
156
+
157
+ /** A shot's framed STILL: the active frame, and (at `detail: "full"`) its history. */
158
+ export interface StudioStillView {
159
+ nodeId: string
160
+ provider: string
161
+ prompt: string
162
+ active: ResultKey | null
163
+ activeUrl: string
164
+ count: number
165
+ /** Cinematic direction as PLATFORM catalog ids — never baked hint text. */
166
+ direction?: StudioIdFieldsJson
167
+ subject?: StudioIdFieldsJson
168
+ /** Present only at `detail: "full"` — a list read returns counts, not histories. */
169
+ results?: StudioResultView[]
170
+ pending: PendingStillView[]
171
+ }
172
+
173
+ /** A shot's animated CLIP. Independent of the still: deleting one never touches the other. */
174
+ export interface StudioClipView {
175
+ nodeId: string
176
+ provider: string
177
+ prompt: string
178
+ duration?: number
179
+ active: ResultKey | null
180
+ activeUrl: string
181
+ count: number
182
+ direction?: StudioIdFieldsJson
183
+ /** The voice this clip was revoiced into, when it was. */
184
+ revoicedVoiceId?: string
185
+ revoicedVoiceName?: string
186
+ /** Present only at `detail: "full"`. */
187
+ results?: StudioClipResultView[]
188
+ pending: PendingClipView[]
189
+ }
190
+
191
+ /** One shot, in timeline order. */
192
+ export interface StudioShotView {
193
+ id: string
194
+ index: number
195
+ name?: string
196
+ folderId?: string
197
+ still?: StudioStillView
198
+ clip?: StudioClipView
199
+ /** Explicit and sticky: selecting a still never moves them. */
200
+ startFrame?: string
201
+ endFrame?: string
202
+ directingReferences?: {
203
+ images?: string[]
204
+ videos?: string[]
205
+ audio?: string[]
206
+ }
207
+ plan?: StudioPlanJson
208
+ scenePrompt?: string
209
+ beats?: StudioBeatJson[]
210
+ endTransition?: StudioTransitionJson
211
+ look?: StudioLookMapJson
212
+ castLook?: StudioCastLookMapJson
213
+ voice?: StudioVoiceJson
214
+ }
215
+
216
+ // ── the production ─────────────────────────────────────────────────────────
217
+
218
+ /** What is in flight, at a glance — the reason a `get` can reconcile before it reads. */
219
+ export interface StudioPendingView {
220
+ stills: number
221
+ clips: number
222
+ music: boolean
223
+ draft: { jobId: string; mode: "replace" | "append" } | null
224
+ }
225
+
226
+ /** The bin: a count always, the items only at `detail: "full"`. */
227
+ export interface StudioTrashView {
228
+ count: number
229
+ items?: StudioTrashItemJson[]
230
+ }
231
+
232
+ /** The read shape of every `/v1/studio/productions` route and every studio MCP tool. */
233
+ export interface StudioProductionView {
234
+ id: string
235
+ name: string
236
+ version: number
237
+ updatedAt: string
238
+ thumbnailUrl: string | null
239
+ shared: boolean
240
+ archived: boolean
241
+ film?: StudioLookMapJson
242
+ cast?: StudioCastJson
243
+ folders: StudioFolderJson[]
244
+ storyboard?: StudioStoryboardJson
245
+ music?: StudioMusicJson
246
+ musicPlan?: StudioMusicPlanJson
247
+ cuts: StudioCutJson[]
248
+ trash: StudioTrashView
249
+ pending: StudioPendingView
250
+ /** In timeline order. */
251
+ shots: StudioShotView[]
252
+ }
253
+
254
+ /** A dashboard row — what a list returns, with no shot bodies at all. */
255
+ export interface StudioProductionSummary {
256
+ id: string
257
+ name: string
258
+ version: number
259
+ updatedAt: string
260
+ thumbnailUrl: string | null
261
+ shared: boolean
262
+ archived: boolean
263
+ shotCount: number
264
+ }
265
+
266
+ // ── request / response bodies ───────────────────────────────────────────────
267
+ // Phase 0's five routes. The operation, generation and lifecycle bodies land
268
+ // with the routes that take them, so this file never describes a route the
269
+ // platform does not serve.
270
+
271
+ /** `GET …/skill` — the authoring skill, rendered from the package at request time. */
272
+ export interface StudioSkillResponse {
273
+ /** SKILL.md — the authoring guide. */
274
+ skill: string
275
+ /** references/catalog.md — every picker, model and enum, in full. */
276
+ catalog: string
277
+ /** schema.json — the strict JSON Schema a plan is validated against. */
278
+ schema: Record<string, unknown>
279
+ /** The operating guide: the tool map, the loops, the rules. */
280
+ operating: string
281
+ /** The catalog versions the three were rendered from. */
282
+ generatedFrom: { prompts: string; shared: string }
283
+ }
284
+
285
+ /** One thing wrong with a plan, addressed at the field that is wrong. */
286
+ export interface StudioPlanIssue {
287
+ path: string
288
+ message: string
289
+ hint?: string
290
+ }
291
+
292
+ /** `POST …/validate` — free, persists nothing, and resolves against the caller's library. */
293
+ export interface StudioValidatePlanRequest {
294
+ plan: Record<string, unknown>
295
+ }
296
+
297
+ export interface StudioValidatePlanResponse {
298
+ valid: boolean
299
+ errors: StudioPlanIssue[]
300
+ warnings: StudioPlanIssue[]
301
+ summary?: {
302
+ name?: string
303
+ scenes: number
304
+ shots: number
305
+ cast: number
306
+ /** Cast entries that matched a row in the caller's library. */
307
+ bound: number
308
+ }
309
+ }
310
+
311
+ /** `GET …?limit&cursor` — the caller's "Studio" project, archived and hidden filtered. */
312
+ export interface StudioListProductionsResponse {
313
+ data: StudioProductionSummary[]
314
+ nextCursor?: string
315
+ }
316
+
317
+ /** `POST …` — a new production, optionally landed from a plan in the same call. */
318
+ export interface StudioCreateProductionRequest {
319
+ name?: string
320
+ plan?: Record<string, unknown>
321
+ }
322
+
323
+ /** `POST …/:id/import` — add a plan's scenes to a production that already exists. */
324
+ export interface StudioImportPlanRequest {
325
+ plan: Record<string, unknown>
326
+ mode?: "append"
327
+ }
328
+
329
+ /** What an import did, in the words a receipt would use. */
330
+ export interface StudioImportSummary {
331
+ shotsAdded: number
332
+ castEnrolled: number
333
+ /** Cast entries that resolved to a row in the caller's library. */
334
+ castBound: number
335
+ }
336
+
337
+ export interface StudioProductionResponse {
338
+ production: StudioProductionView
339
+ warnings?: StudioPlanIssue[]
340
+ summary?: StudioImportSummary
341
+ }
342
+
343
+ /** How much of a production a read returns. */
344
+ export type StudioProductionDetail = "summary" | "full"
package/src/types.ts CHANGED
@@ -214,6 +214,49 @@ export interface ConnectedReference {
214
214
  * The single new structured-shape field for external (API/MCP/SDK) parity.
215
215
  */
216
216
  readonly identityLock?: { enabled: boolean; text?: string }
217
+ /**
218
+ * PER-USE identity description for this reference — what the caller wants the
219
+ * model told about this subject THIS TIME, overriding whatever the entity's
220
+ * library record says. Wherever a reference's identity description already
221
+ * renders (the legacy identity bullet's `— <desc>` part, a `{image:N:label}`
222
+ * directive's descriptor) it WINS over `characterCanonicalDescription` /
223
+ * `locationCanonicalDescription` / `description`; where the HYBRID format
224
+ * renders no description at all, it adds one trailing `<binding> — <override>.`
225
+ * line instead.
226
+ *
227
+ * The two formats are deliberately ASYMMETRIC where a reference renders no
228
+ * directive at all — a plain upload (`manual` / `wired-image`, non-extra) that
229
+ * is neither `@`-mentioned nor `{image:N}`-covered: the LEGACY format stays
230
+ * silent, and the override is not surfaced there (a bullet would force a
231
+ * fidelity verb onto an opaque image); the HYBRID format — the production
232
+ * default — adds the trailing line above for it.
233
+ *
234
+ * DISTINCT from `description`, which stays the reference's own label slot (the
235
+ * free text an extra-ref row / an upstream node carries) — a caller that edits
236
+ * the wording for ONE use writes it here, so the label is untouched and
237
+ * dropping the override falls back to the library wording. Absent →
238
+ * byte-identical to a caller that never sent it.
239
+ */
240
+ readonly descriptionOverride?: string
241
+ }
242
+
243
+ /**
244
+ * A reference the caller can NAME and DESCRIBE but has no media for — a cast
245
+ * role no entity has been bound to yet, an analysis slot, a character the author
246
+ * has only written down. It carries no `url`, so it attaches no reference image
247
+ * and claims no `@image_N` seat; it reaches the model purely as prose
248
+ * (`<Name> — <description>.`), rendered in ONE place per lane by
249
+ * `@nodaro/prompts`' `renderDescribedReferenceLines`.
250
+ *
251
+ * Correlation with the prompt body is BY NAME: the caller leaves the name in the
252
+ * prose (never an indexed `@slug:N` mention — that grammar is url-gated) and the
253
+ * described line tells the model who that name is.
254
+ */
255
+ export interface DescribedReference {
256
+ /** Display name exactly as it appears in the prompt prose (e.g. "Natalie"). */
257
+ readonly name: string
258
+ /** What the model should picture when it reads that name. */
259
+ readonly description: string
217
260
  }
218
261
 
219
262
  /** Default label per source — used by `@` autocomplete and inventory fallback. */
@@ -105,39 +105,46 @@ export const VIDEO_ANALYSIS_WINDOW = { LEN: WINDOW_LEN, STRIDE: WINDOW_STRIDE, O
105
105
  // analysis rows and 6 of 8 audit rows tick up by 1–5 credits (~0.1–0.6%); unlike
106
106
  // the 8_482 → 8_706 round, the legacy `gemini-3-flash` family moves too.
107
107
  // Output of the plugin's `scripts/gen-va-buckets.mjs`, pasted verbatim.
108
+ //
109
+ // REGENERATED 2026-09-06 — the zoom-through doctrine round (cloud-plugins
110
+ // 0.221.0 → 0.221.7): the analyser's system prompt grew across the day's
111
+ // releases, moving the plugin's system-prompt token pin 9_082 → 9_434. 17 of
112
+ // 20 analysis rows and 6 of 8 audit rows tick up by 1–5 credits (~0.1–0.5%);
113
+ // the legacy `gemini-3-flash` family moves too.
114
+ // Output of the plugin's `scripts/gen-va-buckets.mjs`, pasted verbatim.
108
115
  export const VIDEO_ANALYSIS_BUCKET_CREDITS: Record<string, number> = {
109
116
  // Legacy fast-tier model (pre-2026-07) — kept for stored raw-id configs.
110
117
  "video-analysis:gemini-3-flash:60s": 181,
111
- "video-analysis:gemini-3-flash:180s": 185,
112
- "video-analysis:gemini-3-flash:360s": 515,
113
- "video-analysis:gemini-3-flash:600s": 848,
118
+ "video-analysis:gemini-3-flash:180s": 186,
119
+ "video-analysis:gemini-3-flash:360s": 516,
120
+ "video-analysis:gemini-3-flash:600s": 849,
114
121
  // Current fast tier — regenerated from the private formula for its backing
115
122
  // model; higher than the legacy fast schedule but still ≤ pro per bucket.
116
- "video-analysis:gemini-3.6-flash:60s": 204,
123
+ "video-analysis:gemini-3.6-flash:60s": 205,
117
124
  "video-analysis:gemini-3.6-flash:180s": 219,
118
- "video-analysis:gemini-3.6-flash:360s": 602,
119
- "video-analysis:gemini-3.6-flash:600s": 992,
120
- "video-analysis:gemini-3.1-pro:60s": 216,
121
- "video-analysis:gemini-3.1-pro:180s": 232,
122
- "video-analysis:gemini-3.1-pro:360s": 640,
123
- "video-analysis:gemini-3.1-pro:600s": 1056,
125
+ "video-analysis:gemini-3.6-flash:360s": 603,
126
+ "video-analysis:gemini-3.6-flash:600s": 995,
127
+ "video-analysis:gemini-3.1-pro:60s": 217,
128
+ "video-analysis:gemini-3.1-pro:180s": 233,
129
+ "video-analysis:gemini-3.1-pro:360s": 642,
130
+ "video-analysis:gemini-3.1-pro:600s": 1059,
124
131
  // Mixed tiers (`mixed` + `mixed-fast`) share ONE credit family — they are
125
132
  // variants of the same engine plan (plan internals live in the private
126
133
  // analysis plugin). Admin-tunable via model_pricing like every other row.
127
134
  "video-analysis:mixed:60s": 270,
128
- "video-analysis:mixed:180s": 291,
129
- "video-analysis:mixed:360s": 729,
130
- "video-analysis:mixed:600s": 1177,
135
+ "video-analysis:mixed:180s": 292,
136
+ "video-analysis:mixed:360s": 731,
137
+ "video-analysis:mixed:600s": 1181,
131
138
  // SMART — the accuracy tier, and since the 2026-08-03 re-plan a multi-roll
132
139
  // plan like the others, always refined (`selectionMode` does not apply
133
140
  // here — smart always refines; it never offers a cheaper "choose" path).
134
141
  // Priced above the economy tiers because it genuinely costs more to run;
135
142
  // the only tier whose accuracy is validated against a hand-counted edit
136
143
  // list, re-validated at the current plan before this schedule shipped.
137
- "video-analysis:smart:60s": 413,
138
- "video-analysis:smart:180s": 503,
139
- "video-analysis:smart:360s": 1267,
140
- "video-analysis:smart:600s": 2076,
144
+ "video-analysis:smart:60s": 414,
145
+ "video-analysis:smart:180s": 504,
146
+ "video-analysis:smart:360s": 1270,
147
+ "video-analysis:smart:600s": 2081,
141
148
  }
142
149
 
143
150
  /**
@@ -208,13 +215,13 @@ export function videoAnalysisNumWindows(bucketSec: number): number {
208
215
  */
209
216
  export const VIDEO_AUDIT_BUCKET_CREDITS: Record<string, number> = {
210
217
  "video-audit:60s": 215,
211
- "video-audit:180s": 290,
212
- "video-audit:360s": 663,
213
- "video-audit:600s": 1073,
218
+ "video-audit:180s": 291,
219
+ "video-audit:360s": 664,
220
+ "video-audit:600s": 1075,
214
221
  "video-audit:auto:60s": 396,
215
- "video-audit:auto:180s": 475,
216
- "video-audit:auto:360s": 1178,
217
- "video-audit:auto:600s": 1921,
222
+ "video-audit:auto:180s": 477,
223
+ "video-audit:auto:360s": 1180,
224
+ "video-audit:auto:600s": 1924,
218
225
  }
219
226
 
220
227
  /**