@nodaro/shared 2.11.0 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/shared",
3
- "version": "2.11.0",
3
+ "version": "2.12.1",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ VIDEO_ANALYSIS_TEXT_KINDS, VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN, VIDEO_ANALYSIS_TRANSITIONS,
4
+ windowAnalysisSchema, videoAnalysisResultSchema,
5
+ } from "../video-analysis.js"
6
+
7
+ const scene = { startSec: 0, endSec: 4, label: "hook", shotType: "Wide", camera: "static", visual: "{slot:a} waits", audio: [] }
8
+ const slot = { slotId: "a", label: "A", source: "wired-character", role: "person", description: "d" }
9
+
10
+ describe("shot-craft fields (keys only)", () => {
11
+ it("adds a text-kind vocabulary and a clip transition-in vocabulary without widening the transition enum", () => {
12
+ expect(VIDEO_ANALYSIS_TEXT_KINDS).toEqual(["title", "caption", "lower-third", "subtitle", "logo", "other"])
13
+ expect(VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN).toEqual(["fade"])
14
+ expect(VIDEO_ANALYSIS_TRANSITIONS).toEqual(["cut", "fade", "dissolve", "wipe", "whip"])
15
+ })
16
+ it("window and result accept transitionIn at the CLIP level and onScreenTextKind on a scene", () => {
17
+ const w = windowAnalysisSchema.parse({ transitionIn: "fade", slots: [slot], scenes: [{ ...scene, onScreenText: "HELLO", onScreenTextKind: "title", transitionOut: "cut" }] })
18
+ expect(w.transitionIn).toBe("fade")
19
+ expect(w.scenes[0]!.onScreenTextKind).toBe("title")
20
+ const r = videoAnalysisResultSchema.parse({
21
+ meta: { durationSec: 4, width: 16, height: 9, aspectRatio: "16:9" }, transitionIn: "fade", slots: [slot],
22
+ scenes: [{ ...scene, sceneNumber: 1, visualResolved: "A waits", slotRefs: ["a"], onScreenTextKind: "subtitle", onScreenText: "hi" }],
23
+ })
24
+ expect(r.transitionIn).toBe("fade")
25
+ })
26
+ it("a pre-change result still parses, and an unknown key is stripped (readers never break)", () => {
27
+ const r = videoAnalysisResultSchema.parse({
28
+ meta: { durationSec: 4, width: 16, height: 9, aspectRatio: "16:9" }, slots: [slot],
29
+ scenes: [{ ...scene, sceneNumber: 1, visualResolved: "A waits", slotRefs: ["a"], futureKey: 1 }],
30
+ })
31
+ expect(r.transitionIn).toBeUndefined()
32
+ expect((r.scenes[0] as Record<string, unknown>).futureKey).toBeUndefined()
33
+ })
34
+ it("rejects a value outside the vocabularies", () => {
35
+ expect(() => windowAnalysisSchema.parse({ transitionIn: "wipe", slots: [], scenes: [] })).toThrow()
36
+ expect(() => windowAnalysisSchema.parse({ slots: [], scenes: [{ ...scene, onScreenTextKind: "banner" }] })).toThrow()
37
+ })
38
+ })
@@ -18,7 +18,6 @@ const map: LocaleCatalogMap = {
18
18
  "macro": { label: "ماكرو", description: "تفصيل قريب جدا لموضوع صغير" },
19
19
  "full-shot": { label: "لقطة كاملة", description: "الجسم بالكامل من الرأس إلى القدم في الإطار" },
20
20
  "cowboy-shot": { label: "لقطة Cowboy", description: "من منتصف الفخذ فأعلى، تأطير غربي كلاسيكي" },
21
- "head-to-knees": { label: "من الرأس للركبتين", description: "من الرأس نزولا للركبتين" },
22
21
  "head-to-hip": { label: "من الرأس للخصر", description: "من الرأس نزولا للوركين" },
23
22
  "half-body": { label: "نصف جسم", description: "بورتريه نظيف من الخصر فأعلى" },
24
23
 
@@ -18,7 +18,6 @@ const map: LocaleCatalogMap = {
18
18
  "macro": { label: "Makro", description: "Extremes Nahdetail eines kleinen Motivs" },
19
19
  "full-shot": { label: "Ganzkörper-Aufnahme", description: "Ganzer Körper von Kopf bis Fuß im Bild" },
20
20
  "cowboy-shot": { label: "Cowboy Shot", description: "Ab Mitte Oberschenkel, klassische Western-Einstellung" },
21
- "head-to-knees": { label: "Kopf bis Knie", description: "Vom Kopf bis zu den Knien" },
22
21
  "head-to-hip": { label: "Kopf bis Hüfte", description: "Vom Kopf bis zur Hüftlinie" },
23
22
  "half-body": { label: "Halbporträt", description: "Sauberes Porträt ab der Hüfte aufwärts" },
24
23
  "eye-level": { label: "Augenhöhe", description: "Kamera auf Augenhöhe des Motivs" },
@@ -18,7 +18,6 @@ const map: LocaleCatalogMap = {
18
18
  "macro": { label: "Macro", description: "Detalle extremamente cercano de un sujeto pequeño" },
19
19
  "full-shot": { label: "Plano Entero", description: "Cuerpo entero de cabeza a pies en el cuadro" },
20
20
  "cowboy-shot": { label: "Plano Vaquero", description: "Desde mitad del muslo hacia arriba, encuadre clásico del Oeste" },
21
- "head-to-knees": { label: "De Cabeza a Rodillas", description: "Desde la cabeza hasta las rodillas" },
22
21
  "head-to-hip": { label: "De Cabeza a Cadera", description: "Desde la cabeza hasta las caderas" },
23
22
  "half-body": { label: "Medio Cuerpo", description: "Retrato limpio de cintura para arriba" },
24
23
 
@@ -19,7 +19,6 @@ const map: LocaleCatalogMap = {
19
19
  "macro": { description: "Détail extrêmement rapproché d'un petit sujet" },
20
20
  "full-shot": { label: "Plan en pied", description: "Corps entier de la tête aux pieds dans le cadre" },
21
21
  "cowboy-shot": { description: "Du milieu des cuisses, cadrage western classique" },
22
- "head-to-knees": { label: "De la tête aux genoux", description: "De la tête jusqu'aux genoux" },
23
22
  "head-to-hip": { label: "De la tête à la hanche", description: "De la tête jusqu'aux hanches" },
24
23
  "half-body": { label: "Demi-corps", description: "Portrait propre à partir de la taille" },
25
24
  // Angle
@@ -18,7 +18,6 @@ const map: LocaleCatalogMap = {
18
18
  "macro": { label: "Macro", description: "פרט קרוב קיצוני של סובייקט קטן" },
19
19
  "full-shot": { label: "Full Shot", description: "כל הגוף מראש עד רגליים בפריים" },
20
20
  "cowboy-shot": { label: "Cowboy Shot", description: "מאמצע הירך ומעלה, מסגור Western קלאסי" },
21
- "head-to-knees": { label: "ראש עד ברכיים", description: "מהראש למטה עד הברכיים" },
22
21
  "head-to-hip": { label: "ראש עד מותן", description: "מהראש למטה עד המותניים" },
23
22
  "half-body": { label: "חצי גוף", description: "פורטרט נקי מהמותניים ומעלה" },
24
23
 
@@ -19,7 +19,6 @@ const map: LocaleCatalogMap = {
19
19
  "macro": { label: "Macro", description: "छोटे subject का अत्यधिक नज़दीकी detail" },
20
20
  "full-shot": { label: "Full Shot", description: "Frame में सिर-से-पैर तक पूरा बदन" },
21
21
  "cowboy-shot": { label: "Cowboy Shot", description: "जाँघ के बीच से ऊपर, क्लासिक Western framing" },
22
- "head-to-knees": { label: "सिर से घुटनों तक", description: "सिर से लेकर घुटनों तक" },
23
22
  "head-to-hip": { label: "सिर से कूल्हे तक", description: "सिर से लेकर कूल्हे तक" },
24
23
  "half-body": { label: "आधा बदन", description: "साफ़ कमर-से-ऊपर portrait" },
25
24
 
@@ -19,7 +19,6 @@ const map: LocaleCatalogMap = {
19
19
  "macro": { label: "マクロ", description: "小さな被写体の極端な拡大" },
20
20
  "full-shot": { label: "フルショット", description: "頭からつま先まで全身がフレーム内に収まる" },
21
21
  "cowboy-shot": { label: "カウボーイショット", description: "太もも中央から上、クラシックな西部劇のフレーミング" },
22
- "head-to-knees": { label: "頭から膝まで", description: "頭から膝までを写す" },
23
22
  "head-to-hip": { label: "頭から腰まで", description: "頭から腰までを写す" },
24
23
  "half-body": { label: "ハーフボディ", description: "クリーンな腰上のポートレート" },
25
24
 
@@ -19,7 +19,6 @@ const map: LocaleCatalogMap = {
19
19
  "macro": { label: "매크로", description: "작은 피사체의 극단적인 클로즈 디테일입니다" },
20
20
  "full-shot": { label: "풀 샷", description: "머리부터 발끝까지 전신이 프레임에 들어갑니다" },
21
21
  "cowboy-shot": { label: "카우보이 샷", description: "허벅지 중간부터 위로, 클래식한 서부극 프레이밍입니다" },
22
- "head-to-knees": { label: "머리부터 무릎까지", description: "머리부터 무릎까지의 프레이밍입니다" },
23
22
  "head-to-hip": { label: "머리부터 엉덩이까지", description: "머리부터 엉덩이까지의 프레이밍입니다" },
24
23
  "half-body": { label: "반신", description: "허리 위의 깔끔한 인물 사진입니다" },
25
24
 
@@ -19,7 +19,6 @@ const map: LocaleCatalogMap = {
19
19
  "macro": { description: "Detalhe extremo de um pequeno sujeito" },
20
20
  "full-shot": { label: "Plano Inteiro", description: "Corpo inteiro da cabeça aos pés no quadro" },
21
21
  "cowboy-shot": { description: "Da metade da coxa para cima, enquadramento clássico de Western" },
22
- "head-to-knees": { label: "Da Cabeça aos Joelhos", description: "Da cabeça até os joelhos" },
23
22
  "head-to-hip": { label: "Da Cabeça ao Quadril", description: "Da cabeça até o quadril" },
24
23
  "half-body": { label: "Meio Corpo", description: "Retrato limpo da cintura para cima" },
25
24
 
@@ -19,7 +19,6 @@ const map: LocaleCatalogMap = {
19
19
  "macro": { label: "Макро", description: "Сверхблизкая деталь маленького объекта" },
20
20
  "full-shot": { label: "Полный план", description: "Всё тело с головы до пят в кадре" },
21
21
  "cowboy-shot": { label: "Ковбойский план", description: "От середины бедра, классическое вестерн-кадрирование" },
22
- "head-to-knees": { label: "Голова до колен", description: "От головы до колен" },
23
22
  "head-to-hip": { label: "Голова до бёдер", description: "От головы до бёдер" },
24
23
  "half-body": { label: "Поясной портрет", description: "Чистый поясной портрет" },
25
24
 
@@ -19,7 +19,6 @@ const map: LocaleCatalogMap = {
19
19
  "macro": { label: "微距", description: "对小型主体的极近距细节" },
20
20
  "full-shot": { label: "全身镜头", description: "全身入画" },
21
21
  "cowboy-shot": { label: "牛仔镜头", description: "大腿中部以上,经典西部片取景" },
22
- "head-to-knees": { label: "头到膝盖", description: "从头部到膝盖" },
23
22
  "head-to-hip": { label: "头到髋", description: "从头部到髋部" },
24
23
  "half-body": { label: "半身像", description: "干净的腰部以上人像" },
25
24
 
@@ -88,6 +88,21 @@ export type VideoAnalysisVisualEffect = (typeof VIDEO_ANALYSIS_VISUAL_EFFECTS)[n
88
88
  export const VIDEO_ANALYSIS_TRANSITIONS = ["cut", "fade", "dissolve", "wipe", "whip"] as const
89
89
  export type VideoAnalysisTransition = (typeof VIDEO_ANALYSIS_TRANSITIONS)[number]
90
90
 
91
+ /**
92
+ * What burned-in text IS (2026-08-28, recast shot craft). Decides render-vs-omit
93
+ * downstream: a `subtitle` is a transcription of the piece's own speech — a
94
+ * recreation re-speaks it and must not render it — while a `title`, `caption`,
95
+ * `lower-third` or `logo` is picture content. Absent when a scene carries no
96
+ * text. A new KEY on the scene: strip-mode readers drop it harmlessly.
97
+ */
98
+ export const VIDEO_ANALYSIS_TEXT_KINDS = ["title", "caption", "lower-third", "subtitle", "logo", "other"] as const
99
+ export type VideoAnalysisTextKind = (typeof VIDEO_ANALYSIS_TEXT_KINDS)[number]
100
+
101
+ /** How the CLIP opens (clip-level, not per scene — a window's first scene is
102
+ * not the clip's). Absent ⇒ a hard open. Only `fade` today. */
103
+ export const VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN = ["fade"] as const
104
+ export type VideoAnalysisClipTransitionIn = (typeof VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN)[number]
105
+
91
106
  /**
92
107
  * Time manipulation — slow motion, ramps, timelapse, freeze, reverse.
93
108
  *
@@ -302,6 +317,8 @@ const windowSceneBase = z.object({
302
317
  * earlier scene's speech means the shot is replayed footage under a voice-over.
303
318
  */
304
319
  onScreenText: z.string().optional(),
320
+ /** What the on-screen text IS. Meaningful only when `onScreenText` is non-empty. */
321
+ onScreenTextKind: z.enum(VIDEO_ANALYSIS_TEXT_KINDS).optional(),
305
322
  /** Effects on this shot's PICTURE. Absent ⇒ a clean image. */
306
323
  effects: z.array(z.enum(VIDEO_ANALYSIS_VISUAL_EFFECTS)).optional(),
307
324
  transitionOut: z.enum(VIDEO_ANALYSIS_TRANSITIONS).optional(),
@@ -323,6 +340,8 @@ export const windowAnalysisSchema = z.object({
323
340
  /** The clip-level look as read from THIS window. Merge folds the windows
324
341
  * field-by-field (first non-empty wins), like `language`. */
325
342
  look: clipLookSchema.optional(),
343
+ /** The clip opens with a fade-in. Folded from window 0 only. */
344
+ transitionIn: z.enum(VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN).optional(),
326
345
  slots: z.array(entitySlotSchema),
327
346
  scenes: z.array(windowSceneSchema),
328
347
  })
@@ -349,6 +368,8 @@ export const videoAnalysisResultSchema = z.object({
349
368
  * rather than a member: `meta` is probed fact (ffprobe dimensions, probed
350
369
  * duration), while this is the model's reading of the photography. */
351
370
  look: clipLookSchema.optional(),
371
+ /** The clip opens with a fade-in. Folded from window 0 only. */
372
+ transitionIn: z.enum(VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN).optional(),
352
373
  slots: z.array(entitySlotSchema),
353
374
  scenes: z.array(analyzedSceneSchema).min(1),
354
375
  /** CAST VARIATIONS (§4 cap handling): looks the analyzer's merge FOLDED into