@nodaro/shared 2.5.0 → 2.7.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.
- package/dist/index.cjs +138 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -19
- package/dist/index.d.ts +169 -19
- package/dist/index.js +133 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/entity-image-handle.test.ts +33 -1
- package/src/__tests__/group-aggregation.test.ts +39 -0
- package/src/__tests__/infer-music-video.test.ts +76 -0
- package/src/__tests__/llm-models.test.ts +3 -2
- package/src/__tests__/seedance-2-5-catalog.test.ts +16 -8
- package/src/entity-image-handle.ts +37 -3
- package/src/group-aggregation.ts +32 -0
- package/src/index.ts +2 -0
- package/src/llm-models.ts +5 -0
- package/src/model-catalog.ts +48 -3
- package/src/model-constants.ts +23 -0
- package/src/node-default-mappings.ts +3 -2
- package/src/reduce-strategy-registry.ts +31 -13
- package/src/video-analysis.ts +70 -0
package/src/video-analysis.ts
CHANGED
|
@@ -100,6 +100,21 @@ export type VideoAnalysisTransition = (typeof VIDEO_ANALYSIS_TRANSITIONS)[number
|
|
|
100
100
|
* the majority of shots.
|
|
101
101
|
*/
|
|
102
102
|
export const VIDEO_ANALYSIS_SPEED_EFFECTS = ["slow-motion", "ramp-in", "ramp-out", "timelapse", "freeze", "reverse"] as const
|
|
103
|
+
|
|
104
|
+
/** CHRONICLE TIME (2026-08-17): the STORY clock, per scene, as read from the
|
|
105
|
+
* pictures — light, sky, practicals. "ambiguous" is the honest answer for a
|
|
106
|
+
* windowless interior; guessing day is exactly the kind of tidy inference
|
|
107
|
+
* the analysis doctrine forbids. */
|
|
108
|
+
export const VIDEO_ANALYSIS_TIMES_OF_DAY = ["dawn", "day", "dusk", "night", "ambiguous"] as const
|
|
109
|
+
|
|
110
|
+
/** STORY JUMP since the PREVIOUS scene in the list: how much narrative time
|
|
111
|
+
* passed across the cut, judged from evidence (wardrobe change, aged
|
|
112
|
+
* subjects, season, a title card), not from the cut itself. Time outranks
|
|
113
|
+
* location for continuity judgements (same person, new place, continuous
|
|
114
|
+
* time ⇒ same outfit; same place, years later ⇒ anything may differ), which
|
|
115
|
+
* is why this is a structured field and not prose. "unclear" is the honest
|
|
116
|
+
* default; the FIRST scene of a clip is "continuous" by convention. */
|
|
117
|
+
export const VIDEO_ANALYSIS_STORY_JUMPS = ["continuous", "same-day", "another-day", "years-later", "unclear"] as const
|
|
103
118
|
export type VideoAnalysisSpeedEffect = (typeof VIDEO_ANALYSIS_SPEED_EFFECTS)[number]
|
|
104
119
|
|
|
105
120
|
/**
|
|
@@ -268,6 +283,12 @@ const windowSceneBase = z.object({
|
|
|
268
283
|
angle: z.enum(VIDEO_ANALYSIS_SHOT_ANGLES).optional(),
|
|
269
284
|
/** Time manipulation. Absent ⇒ normal speed. */
|
|
270
285
|
speed: z.enum(VIDEO_ANALYSIS_SPEED_EFFECTS).optional(),
|
|
286
|
+
/** CHRONICLE TIME (2026-08-17) — see the consts' docstrings. Both optional:
|
|
287
|
+
* absent on every pre-2.6.0 analysis, and legitimately absent when the
|
|
288
|
+
* analyser cannot read the clock. Enum + optional keeps the window decode
|
|
289
|
+
* grammar congruence-safe (no ints, no maxItems). */
|
|
290
|
+
timeOfDay: z.enum(VIDEO_ANALYSIS_TIMES_OF_DAY).optional(),
|
|
291
|
+
storyJump: z.enum(VIDEO_ANALYSIS_STORY_JUMPS).optional(),
|
|
271
292
|
visual: z.string().min(1),
|
|
272
293
|
/**
|
|
273
294
|
* Text burned into the PICTURE of this shot — titles, captions, lower-thirds,
|
|
@@ -521,3 +542,52 @@ export function aspectRatioFromDims(w: number, h: number): string {
|
|
|
521
542
|
const g = gcd(Math.round(w), Math.round(h))
|
|
522
543
|
return `${Math.round(w) / g}:${Math.round(h) / g}`
|
|
523
544
|
}
|
|
545
|
+
|
|
546
|
+
/** Sung-vocal evidence in a music layer's gen-ready description. Word-bounded
|
|
547
|
+
* and deliberately WITHOUT bare "song"/"music" (an instrumental bed is
|
|
548
|
+
* routinely described as a "pop song"). Quoted text of some length inside a
|
|
549
|
+
* music layer counts too — analysers quote lyrics. */
|
|
550
|
+
const MUSIC_VOCAL_RE = /\b(?:lyrics?|sung|sings?|singing|vocals?|chorus|verse|rap(?:ping|ped)?|a cappella)\b/i
|
|
551
|
+
const MUSIC_VOCAL_QUOTE_RE = /["\u201c][^"\u201d]{6,}["\u201d]/
|
|
552
|
+
/** Negated-vocal phrasing — a layer matching this contributes NO vocal
|
|
553
|
+
* evidence (it does not veto other layers). */
|
|
554
|
+
const MUSIC_NO_VOCAL_RE = /\b(?:no|without|non)[- ](?:vocals?|lyrics?|singing)\b|\binstrumental\b|\bwordless\b/i
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* MUSIC-VIDEO INFERENCE (2026-08-17): is this clip a music video — one whose
|
|
558
|
+
* soundtrack IS the content, to be taken as-is with no stem separation?
|
|
559
|
+
*
|
|
560
|
+
* Lives in SHARED because two sides must agree BYTE-FOR-BYTE on the answer:
|
|
561
|
+
* the recast route derives `music.mode` from it server-side, and the client
|
|
562
|
+
* both prices the original-audio prep and GUARDS on the server's derived mode
|
|
563
|
+
* at generate time — two hand-written copies of this heuristic would drift
|
|
564
|
+
* into that guard firing on honest runs. Deterministic, throw-proof on any
|
|
565
|
+
* malformed analysis (absent fields ⇒ false).
|
|
566
|
+
*
|
|
567
|
+
* The rule is conservative toward FALSE (a false positive keeps unwanted
|
|
568
|
+
* dialogue in the render; a false negative merely runs the separation, which
|
|
569
|
+
* was yesterday's default): at least 4 scenes, at least 80% of scenes carry a
|
|
570
|
+
* music layer, and at least one music layer carries sung-vocal evidence that
|
|
571
|
+
* is not negated ("instrumental", "no vocals").
|
|
572
|
+
*
|
|
573
|
+
* An EXPLICIT analyze-time flag always wins — callers use
|
|
574
|
+
* `flag === true || inferMusicVideo(analysis)` and never let a cached false
|
|
575
|
+
* suppress the inference (the flag can only ever be set true; false means
|
|
576
|
+
* "unset", not "denied").
|
|
577
|
+
*/
|
|
578
|
+
export function inferMusicVideo(analysis: {
|
|
579
|
+
scenes?: ReadonlyArray<{ audio?: ReadonlyArray<{ mode?: string; content?: string }> }>
|
|
580
|
+
} | undefined | null): boolean {
|
|
581
|
+
const scenes = analysis?.scenes ?? []
|
|
582
|
+
if (scenes.length < 4) return false
|
|
583
|
+
const musicLayers = (sc: (typeof scenes)[number]) => (sc.audio ?? []).filter((a) => a?.mode === "music")
|
|
584
|
+
const withMusic = scenes.filter((sc) => musicLayers(sc).length > 0).length
|
|
585
|
+
if (withMusic / scenes.length < 0.8) return false
|
|
586
|
+
return scenes.some((sc) =>
|
|
587
|
+
musicLayers(sc).some((a) => {
|
|
588
|
+
const content = typeof a.content === "string" ? a.content : ""
|
|
589
|
+
if (MUSIC_NO_VOCAL_RE.test(content)) return false
|
|
590
|
+
return MUSIC_VOCAL_RE.test(content) || MUSIC_VOCAL_QUOTE_RE.test(content)
|
|
591
|
+
}),
|
|
592
|
+
)
|
|
593
|
+
}
|