@nodaro/shared 1.10.0 → 1.11.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 +18 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -7
- package/dist/index.d.ts +20 -7
- package/dist/index.js +18 -4
- 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-audio-capability.test.ts +34 -6
- package/src/credit-identifiers.ts +8 -2
- package/src/model-constants.ts +36 -10
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
|
|
|
@@ -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/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
|