@nodaro/shared 1.10.0 → 1.12.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 +69 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -14
- package/dist/index.d.ts +51 -14
- package/dist/index.js +67 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/credit-identifiers.test.ts +24 -12
- package/src/__tests__/video-analysis-pricing.test.ts +29 -3
- package/src/__tests__/video-audio-capability.test.ts +34 -6
- package/src/credit-identifiers.ts +8 -2
- package/src/index.ts +4 -0
- package/src/llm-models.ts +36 -9
- package/src/model-catalog.ts +20 -0
- package/src/model-constants.ts +36 -10
- package/src/video-analysis-pricing.ts +19 -1
package/package.json
CHANGED
|
@@ -313,31 +313,31 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
313
313
|
// --- Duration tiers for kling-3.0 ---
|
|
314
314
|
describe("kling-3.0 duration tiers", () => {
|
|
315
315
|
it("5s duration returns :5s tier", () => {
|
|
316
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 5)).toBe("kling-3.0:5s")
|
|
316
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 5, false)).toBe("kling-3.0:5s")
|
|
317
317
|
})
|
|
318
318
|
|
|
319
319
|
it("3s duration falls into 5s tier", () => {
|
|
320
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 3)).toBe("kling-3.0:5s")
|
|
320
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 3, false)).toBe("kling-3.0:5s")
|
|
321
321
|
})
|
|
322
322
|
|
|
323
323
|
it("10s duration returns :10s tier", () => {
|
|
324
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 10)).toBe("kling-3.0:10s")
|
|
324
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 10, false)).toBe("kling-3.0:10s")
|
|
325
325
|
})
|
|
326
326
|
|
|
327
327
|
it("7s duration falls into 10s tier", () => {
|
|
328
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 7)).toBe("kling-3.0:10s")
|
|
328
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 7, false)).toBe("kling-3.0:10s")
|
|
329
329
|
})
|
|
330
330
|
|
|
331
331
|
it("15s duration returns :15s tier", () => {
|
|
332
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 15)).toBe("kling-3.0:15s")
|
|
332
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 15, false)).toBe("kling-3.0:15s")
|
|
333
333
|
})
|
|
334
334
|
|
|
335
335
|
it("duration exceeding max tier clamps to last tier (20s -> 15s)", () => {
|
|
336
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 20)).toBe("kling-3.0:15s")
|
|
336
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 20, false)).toBe("kling-3.0:15s")
|
|
337
337
|
})
|
|
338
338
|
|
|
339
339
|
it("duration exceeding max tier clamps to last tier (100s -> 15s)", () => {
|
|
340
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 100)).toBe("kling-3.0:15s")
|
|
340
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 100, false)).toBe("kling-3.0:15s")
|
|
341
341
|
})
|
|
342
342
|
})
|
|
343
343
|
|
|
@@ -351,6 +351,18 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
351
351
|
expect(buildVideoCreditModelIdentifier("kling", 5, true)).toBe("kling:5s:audio")
|
|
352
352
|
})
|
|
353
353
|
|
|
354
|
+
it("kling-3.0 + sound UNDEFINED appends :audio (model default is audio ON)", () => {
|
|
355
|
+
// The provider layer generates audio when the caller expressed no intent
|
|
356
|
+
// (models.ts extraParams.sound: true + kling3-client `?? true`). Billing
|
|
357
|
+
// must mirror that via capability defaultOn — reserving the no-audio
|
|
358
|
+
// tier against an audio-on generation was a silent under-bill.
|
|
359
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 5, undefined)).toBe("kling-3.0:5s:audio")
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it("kling (2.6) + sound UNDEFINED does not append :audio (model default is OFF)", () => {
|
|
363
|
+
expect(buildVideoCreditModelIdentifier("kling", 5, undefined)).toBe("kling:5s")
|
|
364
|
+
})
|
|
365
|
+
|
|
354
366
|
it("kling-3.0 + sound=false does not append :audio", () => {
|
|
355
367
|
expect(buildVideoCreditModelIdentifier("kling-3.0", 5, false)).toBe("kling-3.0:5s")
|
|
356
368
|
})
|
|
@@ -407,7 +419,7 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
407
419
|
// --- String duration parsing ---
|
|
408
420
|
describe("duration parsing", () => {
|
|
409
421
|
it('string duration "10" is parsed as number 10', () => {
|
|
410
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", "10")).toBe("kling-3.0:10s")
|
|
422
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", "10", false)).toBe("kling-3.0:10s")
|
|
411
423
|
})
|
|
412
424
|
|
|
413
425
|
it('string duration "5" works', () => {
|
|
@@ -415,19 +427,19 @@ describe("buildVideoCreditModelIdentifier", () => {
|
|
|
415
427
|
})
|
|
416
428
|
|
|
417
429
|
it("NaN duration defaults to 5", () => {
|
|
418
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", "abc")).toBe("kling-3.0:5s")
|
|
430
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", "abc", false)).toBe("kling-3.0:5s")
|
|
419
431
|
})
|
|
420
432
|
|
|
421
433
|
it("undefined duration defaults to 5", () => {
|
|
422
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0")).toBe("kling-3.0:5s")
|
|
434
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", undefined, false)).toBe("kling-3.0:5s")
|
|
423
435
|
})
|
|
424
436
|
|
|
425
437
|
it("0 duration falls into first tier", () => {
|
|
426
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 0)).toBe("kling-3.0:5s")
|
|
438
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 0, false)).toBe("kling-3.0:5s")
|
|
427
439
|
})
|
|
428
440
|
|
|
429
441
|
it("1 duration falls into first tier", () => {
|
|
430
|
-
expect(buildVideoCreditModelIdentifier("kling-3.0", 1)).toBe("kling-3.0:5s")
|
|
442
|
+
expect(buildVideoCreditModelIdentifier("kling-3.0", 1, false)).toBe("kling-3.0:5s")
|
|
431
443
|
})
|
|
432
444
|
})
|
|
433
445
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "../video-analysis-pricing.js"
|
|
8
8
|
import {
|
|
9
9
|
VIDEO_ANALYSIS_LLM_MODELS, VIDEO_ANALYSIS_TIERS, VIDEO_ANALYSIS_TIER_ORDER,
|
|
10
|
+
VIDEO_ANALYSIS_MIXED_TIERS,
|
|
10
11
|
DEFAULT_VIDEO_ANALYSIS_TIER, DEFAULT_VIDEO_ANALYSIS_MODEL, resolveVideoAnalysisModel,
|
|
11
12
|
} from "../llm-models.js"
|
|
12
13
|
|
|
@@ -43,26 +44,51 @@ describe("video-analysis-pricing", () => {
|
|
|
43
44
|
expect(VIDEO_ANALYSIS_LLM_MODELS).toEqual(["gemini-3-flash", "gemini-3.1-pro"])
|
|
44
45
|
})
|
|
45
46
|
|
|
46
|
-
it("tier layer: every tier maps to a real model AND every model is tier-reachable (no vendor leak)", () => {
|
|
47
|
+
it("tier layer: every model-backed tier maps to a real model AND every model is tier-reachable (no vendor leak)", () => {
|
|
47
48
|
// Adding a video-analysis model without a tier would silently leave it
|
|
48
49
|
// unreachable / unnamed — this fails until a tier decision is made.
|
|
49
50
|
const tierTargets = Object.values(VIDEO_ANALYSIS_TIERS)
|
|
50
51
|
for (const m of tierTargets) expect(VIDEO_ANALYSIS_LLM_MODELS).toContain(m)
|
|
51
52
|
for (const m of VIDEO_ANALYSIS_LLM_MODELS) expect(tierTargets).toContain(m)
|
|
52
|
-
|
|
53
|
+
// TIER_ORDER = model-backed tiers + mixed roll-plan tiers, exactly.
|
|
54
|
+
expect(new Set(VIDEO_ANALYSIS_TIER_ORDER)).toEqual(
|
|
55
|
+
new Set([...Object.keys(VIDEO_ANALYSIS_TIERS), ...VIDEO_ANALYSIS_MIXED_TIERS]),
|
|
56
|
+
)
|
|
57
|
+
// Mixed tiers are SENTINELS, never model ids — a mixed id leaking into the
|
|
58
|
+
// model list would break the roll-plan dispatch in the analysis engine.
|
|
59
|
+
for (const t of VIDEO_ANALYSIS_MIXED_TIERS) expect(VIDEO_ANALYSIS_LLM_MODELS).not.toContain(t)
|
|
53
60
|
})
|
|
54
61
|
|
|
55
|
-
it("resolveVideoAnalysisModel: tier → model, raw model passthrough, default pro on empty/unknown", () => {
|
|
62
|
+
it("resolveVideoAnalysisModel: tier → model, mixed → sentinel, raw model passthrough, default pro on empty/unknown", () => {
|
|
56
63
|
expect(DEFAULT_VIDEO_ANALYSIS_TIER).toBe("pro")
|
|
57
64
|
expect(DEFAULT_VIDEO_ANALYSIS_MODEL).toBe("gemini-3.1-pro")
|
|
58
65
|
expect(resolveVideoAnalysisModel("pro")).toBe("gemini-3.1-pro")
|
|
59
66
|
expect(resolveVideoAnalysisModel("fast")).toBe("gemini-3-flash")
|
|
67
|
+
expect(resolveVideoAnalysisModel("mixed")).toBe("mixed") // roll-plan sentinel passthrough
|
|
68
|
+
expect(resolveVideoAnalysisModel("mixed-fast")).toBe("mixed-fast")
|
|
60
69
|
expect(resolveVideoAnalysisModel("gemini-3-flash")).toBe("gemini-3-flash") // raw passthrough
|
|
61
70
|
expect(resolveVideoAnalysisModel(undefined)).toBe("gemini-3.1-pro") // default → pro
|
|
62
71
|
expect(resolveVideoAnalysisModel("")).toBe("gemini-3.1-pro")
|
|
63
72
|
expect(resolveVideoAnalysisModel("nonsense")).toBe("gemini-3.1-pro") // unknown → default, never throws
|
|
64
73
|
})
|
|
65
74
|
|
|
75
|
+
it("mixed tiers price under ONE shared credit family (video-analysis:mixed:*)", () => {
|
|
76
|
+
// Both variants are the identical compute plan — a per-variant price split
|
|
77
|
+
// would be a phantom distinction and double the admin surface.
|
|
78
|
+
for (const bucketSec of VIDEO_ANALYSIS_DURATION_BUCKETS) {
|
|
79
|
+
expect(buildVideoAnalysisCreditId("mixed", bucketSec)).toBe(`video-analysis:mixed:${bucketSec}s`)
|
|
80
|
+
expect(buildVideoAnalysisCreditId("mixed-fast", bucketSec)).toBe(`video-analysis:mixed:${bucketSec}s`)
|
|
81
|
+
const credits = VIDEO_ANALYSIS_BUCKET_CREDITS[`video-analysis:mixed:${bucketSec}s`]
|
|
82
|
+
expect(credits, `missing mixed entry for ${bucketSec}s`).toBeDefined()
|
|
83
|
+
expect(Number.isInteger(credits)).toBe(true)
|
|
84
|
+
// Sanity: mixed (3 fast + 2 pro rolls + refine) must never price below
|
|
85
|
+
// the pro tier it supersets.
|
|
86
|
+
expect(credits).toBeGreaterThanOrEqual(
|
|
87
|
+
VIDEO_ANALYSIS_BUCKET_CREDITS[`video-analysis:gemini-3.1-pro:${bucketSec}s`],
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
66
92
|
// Full drift-detection against the live $-formula lives in
|
|
67
93
|
// backend/src/lib/pricing/__tests__/video-analysis-cost.test.ts (this
|
|
68
94
|
// package cannot see the formula post-S5). This is a lightweight shape
|
|
@@ -22,15 +22,33 @@ describe("getVideoAudioCapability", () => {
|
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
it("returns
|
|
25
|
+
it("returns native_speech (sound toggle, cost-affecting) for KIE Kling", () => {
|
|
26
|
+
// Probe-verified 2026-07-16: scripted quoted dialogue comes back verbatim
|
|
27
|
+
// with articulated lips on the KIE path for BOTH kling (2.6) and kling-3.0.
|
|
26
28
|
for (const m of ["kling", "kling-3.0"]) {
|
|
27
29
|
const cap = getVideoAudioCapability(m)
|
|
28
|
-
expect(cap.mode, m).toBe("
|
|
30
|
+
expect(cap.mode, m).toBe("native_speech")
|
|
29
31
|
expect(cap.field, m).toBe("sound")
|
|
30
32
|
expect(cap.affectsCost, m).toBe(true)
|
|
31
33
|
}
|
|
32
34
|
})
|
|
33
35
|
|
|
36
|
+
it("returns native_speech (generateAudio lever, flat-priced) for kling-3-omni", () => {
|
|
37
|
+
const cap = getVideoAudioCapability("kling-3-omni")
|
|
38
|
+
expect(cap.mode).toBe("native_speech")
|
|
39
|
+
expect(cap.field).toBe("generateAudio")
|
|
40
|
+
// Audio is priced into the Replicate flat per-duration rate — no :audio composite.
|
|
41
|
+
expect(cap.affectsCost).toBeUndefined()
|
|
42
|
+
expect(cap.defaultOn).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("defaultOn mirrors each model's own config default", () => {
|
|
46
|
+
// kling-3.0 generates audio unless explicitly disabled (models.ts
|
|
47
|
+
// extraParams.sound: true + kling3-client `?? true`); kling 2.6 defaults off.
|
|
48
|
+
expect(getVideoAudioCapability("kling-3.0").defaultOn).toBe(true)
|
|
49
|
+
expect(getVideoAudioCapability("kling").defaultOn).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
|
|
34
52
|
it("returns audio_driven for Seedance 2.0", () => {
|
|
35
53
|
for (const m of ["seedance-2", "seedance-2-fast"]) {
|
|
36
54
|
const cap = getVideoAudioCapability(m)
|
|
@@ -66,6 +84,7 @@ describe("videoModelSupportsAudio", () => {
|
|
|
66
84
|
it("is true for any model with an audio mode, false for silent", () => {
|
|
67
85
|
expect(videoModelSupportsAudio("veo3")).toBe(true)
|
|
68
86
|
expect(videoModelSupportsAudio("kling-3.0")).toBe(true)
|
|
87
|
+
expect(videoModelSupportsAudio("kling-3-omni")).toBe(true)
|
|
69
88
|
expect(videoModelSupportsAudio("seedance-2")).toBe(true)
|
|
70
89
|
expect(videoModelSupportsAudio("seedance")).toBe(true)
|
|
71
90
|
expect(videoModelSupportsAudio("minimax")).toBe(false)
|
|
@@ -80,9 +99,12 @@ describe("videoModelCanSpeakDialogue", () => {
|
|
|
80
99
|
expect(videoModelCanSpeakDialogue("seedance-2")).toBe(true)
|
|
81
100
|
expect(videoModelCanSpeakDialogue("seedance-2-fast")).toBe(true)
|
|
82
101
|
expect(videoModelCanSpeakDialogue("seedance-2-mini")).toBe(true)
|
|
102
|
+
// Kling 2.6 / 3.0 / Omni speak scripted dialogue natively (probe-verified
|
|
103
|
+
// 2026-07-16) — the Story→Video auto-pick uses in-model speech + revoice.
|
|
104
|
+
expect(videoModelCanSpeakDialogue("kling")).toBe(true)
|
|
105
|
+
expect(videoModelCanSpeakDialogue("kling-3.0")).toBe(true)
|
|
106
|
+
expect(videoModelCanSpeakDialogue("kling-3-omni")).toBe(true)
|
|
83
107
|
// ambient-only models are NOT dialogue-capable — their audio is SFX/ambient
|
|
84
|
-
expect(videoModelCanSpeakDialogue("kling")).toBe(false)
|
|
85
|
-
expect(videoModelCanSpeakDialogue("kling-3.0")).toBe(false)
|
|
86
108
|
expect(videoModelCanSpeakDialogue("seedance")).toBe(false)
|
|
87
109
|
expect(videoModelCanSpeakDialogue("minimax")).toBe(false)
|
|
88
110
|
expect(videoModelCanSpeakDialogue(undefined)).toBe(false)
|
|
@@ -101,10 +123,16 @@ describe("seedance-2-mini is a full Seedance 2 family member", () => {
|
|
|
101
123
|
})
|
|
102
124
|
|
|
103
125
|
describe("VIDEO_AUDIO_CAPABILITY internal consistency", () => {
|
|
104
|
-
it("every
|
|
126
|
+
it("every AUDIO_ADDON provider is cost-affecting on the canonical `sound` lever", () => {
|
|
127
|
+
// The billing-critical invariant: the `:audio` surcharge keys off `sound`,
|
|
128
|
+
// so every surcharged model must declare audio, be marked cost-affecting,
|
|
129
|
+
// and carry its toggle on that exact field (applyVideoAudioToggle then
|
|
130
|
+
// refuses the generateAudio alias for these, making a billed/generated
|
|
131
|
+
// divergence structurally impossible).
|
|
105
132
|
for (const m of AUDIO_ADDON_PROVIDERS) {
|
|
106
133
|
const cap = getVideoAudioCapability(m)
|
|
107
|
-
expect(cap.mode, m).toBe("
|
|
134
|
+
expect(cap.mode, m).not.toBe("none")
|
|
135
|
+
expect(cap.field, m).toBe("sound")
|
|
108
136
|
expect(cap.affectsCost, m).toBe(true)
|
|
109
137
|
}
|
|
110
138
|
})
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
MOTION_DURATION_TIERS,
|
|
19
19
|
T2I_TO_I2I_VARIANT,
|
|
20
20
|
isVeoProvider,
|
|
21
|
+
getVideoAudioCapability,
|
|
21
22
|
} from "./model-constants.js"
|
|
22
23
|
import { isFlux2Model } from "./flux2-pricing.js"
|
|
23
24
|
import { MODEL_CATALOG } from "./model-catalog.js"
|
|
@@ -227,8 +228,13 @@ export function buildVideoCreditModelIdentifier(
|
|
|
227
228
|
const tier = tiers.find(t => durationSec <= t.maxSeconds) ?? tiers[tiers.length - 1]
|
|
228
229
|
let identifier = `${effectiveProvider}:${tier.suffix}`
|
|
229
230
|
|
|
230
|
-
// Append audio suffix if applicable
|
|
231
|
-
|
|
231
|
+
// Append audio suffix if applicable. When the caller expressed no intent
|
|
232
|
+
// (sound === undefined), fall back to the model's own default (capability
|
|
233
|
+
// `defaultOn` — kling-3.0 generates audio unless explicitly disabled), so an
|
|
234
|
+
// intent-less request is billed for the audio it actually produces instead
|
|
235
|
+
// of reserving the cheaper no-audio tier against an audio-on generation.
|
|
236
|
+
const soundOn = sound ?? (getVideoAudioCapability(effectiveProvider).defaultOn === true)
|
|
237
|
+
if (AUDIO_ADDON_PROVIDERS.has(effectiveProvider) && soundOn) {
|
|
232
238
|
identifier += ":audio"
|
|
233
239
|
}
|
|
234
240
|
|
package/src/index.ts
CHANGED
|
@@ -255,11 +255,15 @@ export {
|
|
|
255
255
|
VIDEO_ANALYSIS_LLM_MODELS,
|
|
256
256
|
VIDEO_ANALYSIS_TIERS,
|
|
257
257
|
type VideoAnalysisTier,
|
|
258
|
+
type VideoAnalysisModelTier,
|
|
259
|
+
VIDEO_ANALYSIS_MIXED_TIERS,
|
|
260
|
+
type VideoAnalysisMixedTier,
|
|
258
261
|
VIDEO_ANALYSIS_TIER_ORDER,
|
|
259
262
|
DEFAULT_VIDEO_ANALYSIS_TIER,
|
|
260
263
|
DEFAULT_VIDEO_ANALYSIS_MODEL,
|
|
261
264
|
VIDEO_ANALYSIS_TIER_LABELS,
|
|
262
265
|
isVideoAnalysisTier,
|
|
266
|
+
isVideoAnalysisMixedTier,
|
|
263
267
|
resolveVideoAnalysisModel,
|
|
264
268
|
LLM_FEATURE_DEFAULTS,
|
|
265
269
|
LLM_MODALITY_CAPS,
|
package/src/llm-models.ts
CHANGED
|
@@ -396,26 +396,53 @@ export const VIDEO_ANALYSIS_LLM_MODELS: string[] = LLM_MODELS
|
|
|
396
396
|
* so adding a video model forces a tier decision instead of silently leaking.
|
|
397
397
|
*/
|
|
398
398
|
export const VIDEO_ANALYSIS_TIERS = { fast: "gemini-3-flash", pro: "gemini-3.1-pro" } as const
|
|
399
|
-
export type
|
|
399
|
+
export type VideoAnalysisModelTier = keyof typeof VIDEO_ANALYSIS_TIERS
|
|
400
|
+
/**
|
|
401
|
+
* MIXED tiers — advanced multi-engine analysis plans whose identifier resolves
|
|
402
|
+
* to an engine-plan SENTINEL consumed by the analysis engine, never to a single
|
|
403
|
+
* model id. Two variants, same price (one shared `video-analysis:mixed:*`
|
|
404
|
+
* credit family): `mixed` targets maximum result quality; `mixed-fast` targets
|
|
405
|
+
* run-to-run output consistency. What each plan does internally is deliberately
|
|
406
|
+
* NOT published here (Apache irrevocability; only the wire vocabulary below is
|
|
407
|
+
* contract — the engine lives in the private analysis plugin).
|
|
408
|
+
*/
|
|
409
|
+
export const VIDEO_ANALYSIS_MIXED_TIERS = ["mixed", "mixed-fast"] as const
|
|
410
|
+
export type VideoAnalysisMixedTier = (typeof VIDEO_ANALYSIS_MIXED_TIERS)[number]
|
|
400
411
|
/** UI/listing order — recommended (pro) first. */
|
|
401
|
-
export const VIDEO_ANALYSIS_TIER_ORDER = ["pro", "fast"] as const
|
|
412
|
+
export const VIDEO_ANALYSIS_TIER_ORDER = ["pro", "fast", "mixed", "mixed-fast"] as const
|
|
413
|
+
export type VideoAnalysisTier = (typeof VIDEO_ANALYSIS_TIER_ORDER)[number]
|
|
402
414
|
export const DEFAULT_VIDEO_ANALYSIS_TIER: VideoAnalysisTier = "pro"
|
|
403
415
|
export const DEFAULT_VIDEO_ANALYSIS_MODEL: string = VIDEO_ANALYSIS_TIERS[DEFAULT_VIDEO_ANALYSIS_TIER]
|
|
404
416
|
/** Neutral, vendor-free display labels for the UI. */
|
|
405
|
-
export const VIDEO_ANALYSIS_TIER_LABELS: Record<VideoAnalysisTier, string> = {
|
|
417
|
+
export const VIDEO_ANALYSIS_TIER_LABELS: Record<VideoAnalysisTier, string> = {
|
|
418
|
+
fast: "Fast",
|
|
419
|
+
pro: "Pro",
|
|
420
|
+
mixed: "Mixed",
|
|
421
|
+
"mixed-fast": "Mixed (consistent)",
|
|
422
|
+
}
|
|
406
423
|
|
|
407
424
|
export function isVideoAnalysisTier(v: string): v is VideoAnalysisTier {
|
|
408
|
-
return
|
|
425
|
+
return (VIDEO_ANALYSIS_TIER_ORDER as readonly string[]).includes(v)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function isVideoAnalysisMixedTier(v: string): v is VideoAnalysisMixedTier {
|
|
429
|
+
return (VIDEO_ANALYSIS_MIXED_TIERS as readonly string[]).includes(v)
|
|
409
430
|
}
|
|
410
431
|
|
|
411
432
|
/**
|
|
412
|
-
* Resolve a user-supplied tier
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
433
|
+
* Resolve a user-supplied tier OR a raw internal model id to the analysis
|
|
434
|
+
* ENGINE IDENTIFIER carried in the worker payload:
|
|
435
|
+
* - model-backed tiers ("fast"/"pro") → the internal model id;
|
|
436
|
+
* - mixed tiers ("mixed"/"mixed-fast") → the sentinel ITSELF (the engine
|
|
437
|
+
* expands it to a multi-model roll plan);
|
|
438
|
+
* - raw model ids pass through (back-compat for stored `llmModel` values);
|
|
439
|
+
* - empty/unknown → the default tier's model (never an error).
|
|
416
440
|
*/
|
|
417
441
|
export function resolveVideoAnalysisModel(input?: string | null): string {
|
|
418
|
-
if (input &&
|
|
442
|
+
if (input && isVideoAnalysisMixedTier(input)) return input
|
|
443
|
+
if (input && Object.prototype.hasOwnProperty.call(VIDEO_ANALYSIS_TIERS, input)) {
|
|
444
|
+
return VIDEO_ANALYSIS_TIERS[input as VideoAnalysisModelTier]
|
|
445
|
+
}
|
|
419
446
|
if (input && VIDEO_ANALYSIS_LLM_MODELS.includes(input)) return input
|
|
420
447
|
return DEFAULT_VIDEO_ANALYSIS_MODEL
|
|
421
448
|
}
|
package/src/model-catalog.ts
CHANGED
|
@@ -1839,6 +1839,26 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
1839
1839
|
{ identifier: "video-analysis:gemini-3.1-pro:600s", credits: 11, note: "10-min ceiling" },
|
|
1840
1840
|
],
|
|
1841
1841
|
},
|
|
1842
|
+
// Both mixed tiers are variants of the same advanced multi-engine analysis
|
|
1843
|
+
// and share this ONE credit family (videoAnalysisCreditSegment maps both;
|
|
1844
|
+
// plan internals live in the private analysis plugin).
|
|
1845
|
+
"mixed-video-analysis": {
|
|
1846
|
+
id: "mixed-video-analysis",
|
|
1847
|
+
kind: "video",
|
|
1848
|
+
modes: ["video-analysis"] as const,
|
|
1849
|
+
family: "Nodaro",
|
|
1850
|
+
label: "Video Analysis (Mixed)",
|
|
1851
|
+
series: "Video Analysis",
|
|
1852
|
+
description: "Our most advanced analysis tier — multiple analysis engines combined into one result for maximum completeness and accuracy. Billed per duration bucket.",
|
|
1853
|
+
useCases: ["video-analysis", "shot-list", "premium", "most-complete"],
|
|
1854
|
+
pricing: [
|
|
1855
|
+
{ identifier: "video-analysis:mixed", credits: 14, note: "10-min ceiling (no duration given)" },
|
|
1856
|
+
{ identifier: "video-analysis:mixed:60s", credits: 3 },
|
|
1857
|
+
{ identifier: "video-analysis:mixed:180s", credits: 4 },
|
|
1858
|
+
{ identifier: "video-analysis:mixed:360s", credits: 9 },
|
|
1859
|
+
{ identifier: "video-analysis:mixed:600s", credits: 14, note: "10-min ceiling" },
|
|
1860
|
+
],
|
|
1861
|
+
},
|
|
1842
1862
|
}
|
|
1843
1863
|
|
|
1844
1864
|
// =============================================================================
|
package/src/model-constants.ts
CHANGED
|
@@ -1393,8 +1393,13 @@ export const AUDIO_ADDON_PROVIDERS = new Set([
|
|
|
1393
1393
|
* lip-synced spoken dialogue. Toggle is offered; the
|
|
1394
1394
|
* pipeline still uses TTS + lip-sync for dialogue.
|
|
1395
1395
|
* "native_speech" — bakes spoken dialogue + lip movement from the prompt
|
|
1396
|
-
* (VEO 3.x
|
|
1397
|
-
*
|
|
1396
|
+
* (VEO 3.x always-on; Kling 2.6/3.0 behind the `sound`
|
|
1397
|
+
* toggle — probe-verified 2026-07-16: scripted lines come
|
|
1398
|
+
* back word-for-word with articulated lips on the KIE
|
|
1399
|
+
* path, matching the official Kling 2.6 audio guide and
|
|
1400
|
+
* Kling 3.0 prompting docs). The pipeline injects the
|
|
1401
|
+
* dialogue line, enables audio, and revoices the clip to
|
|
1402
|
+
* the character's saved voice.
|
|
1398
1403
|
* "audio_driven" — lip-syncs to a supplied reference-audio track (Seedance
|
|
1399
1404
|
* 2.0 multimodal). The pipeline synthesises the character's
|
|
1400
1405
|
* voice first, feeds it as reference audio, and skips the
|
|
@@ -1410,6 +1415,14 @@ export interface VideoAudioCapability {
|
|
|
1410
1415
|
alwaysOn?: boolean
|
|
1411
1416
|
/** Enabling audio raises the credit cost (Kling — see AUDIO_ADDON_PROVIDERS). */
|
|
1412
1417
|
affectsCost?: boolean
|
|
1418
|
+
/**
|
|
1419
|
+
* The model generates audio when the caller expresses NO intent (its own
|
|
1420
|
+
* config default is on — kling-3.0's `sound: true`, kling-3-omni's
|
|
1421
|
+
* `generate_audio: true`). The `:audio` credit suffix mirrors this default
|
|
1422
|
+
* so an intent-less request is billed for the audio it actually produces
|
|
1423
|
+
* (see buildVideoCreditModelIdentifier). Absent ⇒ default off.
|
|
1424
|
+
*/
|
|
1425
|
+
defaultOn?: boolean
|
|
1413
1426
|
}
|
|
1414
1427
|
|
|
1415
1428
|
/**
|
|
@@ -1426,9 +1439,22 @@ export const VIDEO_AUDIO_CAPABILITY: Record<string, VideoAudioCapability> = {
|
|
|
1426
1439
|
veo3: { mode: "native_speech", alwaysOn: true },
|
|
1427
1440
|
"veo3.1": { mode: "native_speech", alwaysOn: true },
|
|
1428
1441
|
veo3_lite: { mode: "native_speech", alwaysOn: true },
|
|
1429
|
-
// Kling 2.6 / 3.0 —
|
|
1430
|
-
|
|
1431
|
-
|
|
1442
|
+
// Kling 2.6 / 3.0 — native spoken dialogue + lip sync behind the `sound`
|
|
1443
|
+
// toggle (probe-verified on the KIE path 2026-07-16: scripted quoted lines
|
|
1444
|
+
// are spoken verbatim with articulated lips; the official Kling 2.6 audio
|
|
1445
|
+
// guide documents speech/dialogue/narration/singing with [Character@Voice]
|
|
1446
|
+
// binding, zh+en voices). Cost-affecting: `:audio` credit suffix.
|
|
1447
|
+
// kling-3.0's model default is sound ON (kie models.ts extraParams +
|
|
1448
|
+
// kling3-client `?? true`) → defaultOn keeps billing aligned with what an
|
|
1449
|
+
// intent-less request actually generates; kling 2.6 defaults OFF.
|
|
1450
|
+
kling: { mode: "native_speech", field: "sound", affectsCost: true },
|
|
1451
|
+
"kling-3.0": { mode: "native_speech", field: "sound", affectsCost: true, defaultOn: true },
|
|
1452
|
+
// Kling 3.0 Omni (Replicate kwaivgi/kling-v3-omni-video) — the Omni tier's
|
|
1453
|
+
// headline feature IS native dialogue (per-character voices, unified audio
|
|
1454
|
+
// timeline). Lever is Replicate's `generate_audio` (default true in our
|
|
1455
|
+
// provider config → defaultOn); audio is priced into the flat per-duration
|
|
1456
|
+
// rate, so NOT cost-affecting (no :audio composite).
|
|
1457
|
+
"kling-3-omni": { mode: "native_speech", field: "generateAudio", defaultOn: true },
|
|
1432
1458
|
// Seedance 1.x — optional ambient audio (generate_audio); not dialogue.
|
|
1433
1459
|
seedance: { mode: "ambient", field: "generateAudio" },
|
|
1434
1460
|
// Seedance 2.0 — multimodal; lip-syncs to a supplied reference-audio track.
|
|
@@ -1457,11 +1483,11 @@ export function videoModelSupportsAudio(model: string | undefined): boolean {
|
|
|
1457
1483
|
|
|
1458
1484
|
/**
|
|
1459
1485
|
* True when the model can produce lip-synced spoken DIALOGUE — either natively
|
|
1460
|
-
* (VEO) or driven by a supplied audio track (Seedance
|
|
1461
|
-
* dialogue auto-pick: in-model speech + character
|
|
1462
|
-
* reference audio (Seedance 2.0) vs.
|
|
1463
|
-
* Ambient-only models (
|
|
1464
|
-
* not speech.
|
|
1486
|
+
* (VEO 3.x, Kling 2.6/3.0/Omni) or driven by a supplied audio track (Seedance
|
|
1487
|
+
* 2.0). Drives the Story→Video dialogue auto-pick: in-model speech + character
|
|
1488
|
+
* revoice (VEO, Kling) / character-voiced reference audio (Seedance 2.0) vs.
|
|
1489
|
+
* the TTS + separate-lip-sync fallback. Ambient-only models (Seedance 1.x)
|
|
1490
|
+
* return `false` — their audio is SFX, not speech.
|
|
1465
1491
|
*/
|
|
1466
1492
|
export function videoModelCanSpeakDialogue(model: string | undefined): boolean {
|
|
1467
1493
|
const mode = getVideoAudioCapability(model).mode
|
|
@@ -54,6 +54,24 @@ export const VIDEO_ANALYSIS_BUCKET_CREDITS: Record<string, number> = {
|
|
|
54
54
|
"video-analysis:gemini-3.1-pro:180s": 3,
|
|
55
55
|
"video-analysis:gemini-3.1-pro:360s": 7,
|
|
56
56
|
"video-analysis:gemini-3.1-pro:600s": 11,
|
|
57
|
+
// Mixed tiers (`mixed` + `mixed-fast`) share ONE credit family — they are
|
|
58
|
+
// variants of the same engine plan (plan internals live in the private
|
|
59
|
+
// analysis plugin). Admin-tunable via model_pricing like every other row.
|
|
60
|
+
"video-analysis:mixed:60s": 3,
|
|
61
|
+
"video-analysis:mixed:180s": 4,
|
|
62
|
+
"video-analysis:mixed:360s": 9,
|
|
63
|
+
"video-analysis:mixed:600s": 14,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The credit-id MODEL SEGMENT for an engine identifier: both mixed-tier
|
|
68
|
+
* sentinels share the `mixed` price family (same engine plan); everything
|
|
69
|
+
* else prices under its own identifier. Single source of truth — used by
|
|
70
|
+
* `buildVideoAnalysisCreditId` below, so route/orchestrator/UI callers can
|
|
71
|
+
* never diverge on where a sentinel prices.
|
|
72
|
+
*/
|
|
73
|
+
export function videoAnalysisCreditSegment(modelOrSentinel: string): string {
|
|
74
|
+
return modelOrSentinel === "mixed-fast" ? "mixed" : modelOrSentinel
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
export function pickVideoAnalysisBucket(durationSec: number): number {
|
|
@@ -65,7 +83,7 @@ export function buildVideoAnalysisCreditId(model: string, durationSec?: number):
|
|
|
65
83
|
const bucket = durationSec !== undefined && durationSec > 0
|
|
66
84
|
? pickVideoAnalysisBucket(Math.min(durationSec, VIDEO_ANALYSIS_MAX_DURATION_SEC))
|
|
67
85
|
: VIDEO_ANALYSIS_MAX_DURATION_SEC // unknown → ceiling composite (the ONLY silent-ceiling path)
|
|
68
|
-
return `video-analysis:${model}:${bucket}s`
|
|
86
|
+
return `video-analysis:${videoAnalysisCreditSegment(model)}:${bucket}s`
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
export function bucketSecondsFromCreditId(creditId: string): number | null {
|