vidspotai-shared 1.0.100 → 1.0.101-dev.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/lib/globals/aiModels/enums.d.ts +1 -0
- package/lib/globals/aiModels/enums.d.ts.map +1 -1
- package/lib/globals/aiModels/enums.js +3 -0
- package/lib/globals/aiModels/extendCapability.d.ts +26 -0
- package/lib/globals/aiModels/extendCapability.d.ts.map +1 -0
- package/lib/globals/aiModels/extendCapability.js +42 -0
- package/lib/globals/aiModels/index.d.ts +1 -0
- package/lib/globals/aiModels/index.d.ts.map +1 -1
- package/lib/globals/aiModels/index.js +1 -0
- package/lib/globals/aiModels/providers/google.d.ts.map +1 -1
- package/lib/globals/aiModels/providers/google.js +6 -9
- package/lib/globals/aiModels/providers/hostedAvatars.js +10 -10
- package/lib/globals/creditUnit.d.ts +20 -33
- package/lib/globals/creditUnit.d.ts.map +1 -1
- package/lib/globals/creditUnit.js +20 -33
- package/lib/globals/types.d.ts +34 -2
- package/lib/globals/types.d.ts.map +1 -1
- package/lib/globals/types.js +37 -0
- package/lib/models/demoVideoProject.model.d.ts +213 -0
- package/lib/models/demoVideoProject.model.d.ts.map +1 -1
- package/lib/models/index.d.ts +2 -0
- package/lib/models/index.d.ts.map +1 -1
- package/lib/models/index.js +2 -0
- package/lib/models/mediaUpload.model.d.ts +25 -0
- package/lib/models/mediaUpload.model.d.ts.map +1 -1
- package/lib/models/productConnection.model.d.ts +57 -0
- package/lib/models/productConnection.model.d.ts.map +1 -0
- package/lib/models/productConnection.model.js +7 -0
- package/lib/models/video.model.d.ts +11 -0
- package/lib/models/video.model.d.ts.map +1 -1
- package/lib/models/videoQaJob.model.d.ts +61 -0
- package/lib/models/videoQaJob.model.d.ts.map +1 -0
- package/lib/models/videoQaJob.model.js +16 -0
- package/lib/schemas/brandKit.schema.d.ts +3 -1
- package/lib/schemas/brandKit.schema.d.ts.map +1 -1
- package/lib/schemas/brandKit.schema.js +9 -0
- package/lib/schemas/brief.schema.d.ts +5 -4
- package/lib/schemas/brief.schema.d.ts.map +1 -1
- package/lib/schemas/brief.schema.js +23 -1
- package/lib/schemas/demoQuestion.schema.d.ts +24 -4
- package/lib/schemas/demoQuestion.schema.d.ts.map +1 -1
- package/lib/schemas/demoQuestion.schema.js +8 -2
- package/lib/schemas/demoSpec.schema.d.ts +40 -6
- package/lib/schemas/demoSpec.schema.d.ts.map +1 -1
- package/lib/schemas/demoSpec.schema.js +46 -1
- package/lib/schemas/project.schema.d.ts +33 -3
- package/lib/schemas/project.schema.d.ts.map +1 -1
- package/lib/schemas/project.schema.js +22 -0
- package/lib/schemas/social.schema.d.ts +1 -1
- package/lib/schemas/videoPlan.schema.d.ts +22 -20
- package/lib/schemas/videoPlan.schema.d.ts.map +1 -1
- package/lib/schemas/videoPlan.schema.js +19 -0
- package/lib/services/agent/editClassifier.d.ts +10 -10
- package/lib/services/agent/eval/types.d.ts +3 -2
- package/lib/services/agent/eval/types.d.ts.map +1 -1
- package/lib/services/agent/executor/core.d.ts.map +1 -1
- package/lib/services/agent/executor/core.js +11 -0
- package/lib/services/agent/planner/Planner.d.ts.map +1 -1
- package/lib/services/agent/planner/Planner.js +5 -0
- package/lib/services/agent/planner/overlayRegen.d.ts.map +1 -1
- package/lib/services/agent/planner/overlayRegen.js +5 -0
- package/lib/services/agent/planner/plannerMessages.d.ts.map +1 -1
- package/lib/services/agent/planner/plannerMessages.js +10 -1
- package/lib/services/agent/planner/scriptFirstPlanner.d.ts.map +1 -1
- package/lib/services/agent/planner/scriptFirstPlanner.js +2 -0
- package/lib/services/agent/regenCore.d.ts.map +1 -1
- package/lib/services/agent/regenCore.js +11 -1
- package/lib/services/agent/runHelpers.d.ts +8 -0
- package/lib/services/agent/runHelpers.d.ts.map +1 -1
- package/lib/services/agent/runHelpers.js +5 -1
- package/lib/services/agent/tools/chapterOutline.tool.d.ts +8 -0
- package/lib/services/agent/tools/chapterOutline.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/chapterOutline.tool.js +21 -8
- package/lib/services/agent/tools/composeScene.tool.d.ts +32 -2
- package/lib/services/agent/tools/composeScene.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/estimateCost.tool.d.ts +11 -1
- package/lib/services/agent/tools/estimateCost.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/extendVideo.tool.d.ts +9 -3
- package/lib/services/agent/tools/extendVideo.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/extendVideo.tool.js +12 -27
- package/lib/services/agent/tools/generateAvatarVideo.tool.d.ts +1 -1
- package/lib/services/agent/tools/generateScript.tool.d.ts +3 -2
- package/lib/services/agent/tools/generateScript.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/generateScript.tool.js +16 -7
- package/lib/services/agent/tools/generateVoiceover.tool.d.ts +1 -1
- package/lib/services/agent/tools/planVideo.tool.d.ts +11 -8
- package/lib/services/agent/tools/planVideo.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/render.tool.d.ts +11 -1
- package/lib/services/agent/tools/render.tool.d.ts.map +1 -1
- package/lib/services/agent/tools/searchStock.tool.d.ts +1 -1
- package/lib/services/agent/tools/searchUserLibrary.tool.d.ts +4 -4
- package/lib/services/aiGen/providers/alibaba/alibaba.d.ts +0 -6
- package/lib/services/aiGen/providers/alibaba/alibaba.d.ts.map +1 -1
- package/lib/services/aiGen/providers/alibaba/alibaba.js +5 -103
- package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/bytedance/bytedance.service.js +1 -9
- package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/google.service.js +26 -142
- package/lib/services/aiGen/providers/google/googleErrors.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/googleErrors.js +7 -17
- package/lib/services/aiGen/providers/kling/kling.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/kling/kling.service.js +22 -3
- package/lib/services/aiGen/providers/kling/klingStatus.d.ts +7 -0
- package/lib/services/aiGen/providers/kling/klingStatus.d.ts.map +1 -1
- package/lib/services/aiGen/providers/kling/klingStatus.js +41 -4
- package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/pixverse/pixverse.service.js +25 -0
- package/lib/services/aiGen/providers/types.d.ts +20 -0
- package/lib/services/aiGen/providers/types.d.ts.map +1 -1
- package/lib/services/avatarGen/avatarProviderFactory.d.ts.map +1 -1
- package/lib/services/avatarGen/avatarProviderFactory.js +3 -0
- package/lib/services/avatarGen/index.d.ts +1 -0
- package/lib/services/avatarGen/index.d.ts.map +1 -1
- package/lib/services/avatarGen/index.js +1 -0
- package/lib/services/avatarGen/providers/synthesiaAvatarProvider.d.ts +53 -0
- package/lib/services/avatarGen/providers/synthesiaAvatarProvider.d.ts.map +1 -0
- package/lib/services/avatarGen/providers/synthesiaAvatarProvider.js +176 -0
- package/lib/services/demo/demoAuthor.d.ts +7 -0
- package/lib/services/demo/demoAuthor.d.ts.map +1 -1
- package/lib/services/demo/demoAuthor.js +2 -0
- package/lib/services/demo/demoAuthorPrompt.d.ts +6 -0
- package/lib/services/demo/demoAuthorPrompt.d.ts.map +1 -1
- package/lib/services/demo/demoAuthorPrompt.js +24 -2
- package/lib/services/demo/demoCaptions.d.ts +69 -0
- package/lib/services/demo/demoCaptions.d.ts.map +1 -0
- package/lib/services/demo/demoCaptions.js +152 -0
- package/lib/services/demo/demoMusic.d.ts +8 -1
- package/lib/services/demo/demoMusic.d.ts.map +1 -1
- package/lib/services/demo/demoMusic.js +32 -16
- package/lib/services/demo/demoScriptPrompt.d.ts +2 -1
- package/lib/services/demo/demoScriptPrompt.d.ts.map +1 -1
- package/lib/services/demo/demoScriptPrompt.js +29 -2
- package/lib/services/demo/demoStockAssets.d.ts +29 -0
- package/lib/services/demo/demoStockAssets.d.ts.map +1 -0
- package/lib/services/demo/demoStockAssets.js +107 -0
- package/lib/services/demo/index.d.ts +3 -0
- package/lib/services/demo/index.d.ts.map +1 -1
- package/lib/services/demo/index.js +3 -0
- package/lib/services/demo/productConnection.service.d.ts +78 -0
- package/lib/services/demo/productConnection.service.d.ts.map +1 -0
- package/lib/services/demo/productConnection.service.js +125 -0
- package/lib/services/editor/planToProject.d.ts.map +1 -1
- package/lib/services/editor/planToProject.helpers.d.ts +1 -0
- package/lib/services/editor/planToProject.helpers.d.ts.map +1 -1
- package/lib/services/editor/planToProject.helpers.js +3 -0
- package/lib/services/editor/planToProject.js +39 -0
- package/lib/services/editor/projectToDesign.js +13 -1
- package/lib/services/firestore.service.d.ts +14 -0
- package/lib/services/firestore.service.d.ts.map +1 -1
- package/lib/services/firestore.service.js +16 -0
- package/lib/services/translation/index.d.ts +1 -0
- package/lib/services/translation/index.d.ts.map +1 -1
- package/lib/services/translation/index.js +8 -1
- package/lib/services/translation/outputLanguage.d.ts +75 -0
- package/lib/services/translation/outputLanguage.d.ts.map +1 -0
- package/lib/services/translation/outputLanguage.js +190 -0
- package/lib/services/translation/translation.service.d.ts +13 -2
- package/lib/services/translation/translation.service.d.ts.map +1 -1
- package/lib/services/translation/translation.service.js +73 -31
- package/lib/services/tts/index.d.ts +1 -0
- package/lib/services/tts/index.d.ts.map +1 -1
- package/lib/services/tts/index.js +1 -0
- package/lib/services/tts/providers/elevenlabs.service.d.ts +7 -0
- package/lib/services/tts/providers/elevenlabs.service.d.ts.map +1 -1
- package/lib/services/tts/providers/elevenlabs.service.js +69 -13
- package/lib/services/tts/types.d.ts +29 -0
- package/lib/services/tts/types.d.ts.map +1 -1
- package/lib/services/tts/wordTimings.d.ts +27 -0
- package/lib/services/tts/wordTimings.d.ts.map +1 -0
- package/lib/services/tts/wordTimings.js +57 -0
- package/lib/utils/billableDuration.d.ts +11 -0
- package/lib/utils/billableDuration.d.ts.map +1 -0
- package/lib/utils/billableDuration.js +91 -0
- package/lib/utils/index.d.ts +3 -0
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/index.js +3 -0
- package/lib/utils/mp4Duration.d.ts +38 -0
- package/lib/utils/mp4Duration.d.ts.map +1 -0
- package/lib/utils/mp4Duration.js +164 -0
- package/lib/utils/socialEntitlements.d.ts +51 -0
- package/lib/utils/socialEntitlements.d.ts.map +1 -0
- package/lib/utils/socialEntitlements.js +65 -0
- package/package.json +1 -1
|
@@ -74,13 +74,6 @@ const PRIMARY_BASE = "https://us-central1-translation-service-3931b.cloudfunctio
|
|
|
74
74
|
// 24s user-facing stall was itself part of the problem).
|
|
75
75
|
const PRIMARY_TIMEOUT_MS = 6000;
|
|
76
76
|
const PRIMARY_MAX_ATTEMPTS = 2;
|
|
77
|
-
// The LLM fallback had NO timeout, so when the primary CF was also failing the
|
|
78
|
-
// two layers compounded into an unbounded stall — a Vietnamese request was
|
|
79
|
-
// measured at 48s in prod (2026-07-26): ~12s burning both primary attempts,
|
|
80
|
-
// then ~36s waiting on a slow generateText that nothing was bounding. Cap the
|
|
81
|
-
// fallback so the worst case is primary (~12.2s) + fallback (15s) ≈ 27s and the
|
|
82
|
-
// caller gets a definite answer instead of holding the request open.
|
|
83
|
-
const FALLBACK_TIMEOUT_MS = 15000;
|
|
84
77
|
// gpt-4o-mini: cheap (~$0.15/1M in, $0.60/1M out → fractions of a cent per
|
|
85
78
|
// prompt), reliable, and independent of the Google video quota. Swap here if a
|
|
86
79
|
// cheaper effective model becomes available.
|
|
@@ -117,25 +110,13 @@ async function callLlmTranslate(text, from, to) {
|
|
|
117
110
|
`Preserve meaning, tone, names, numbers, hashtags, emojis and line breaks. ` +
|
|
118
111
|
`Output ONLY the translated text — no quotes, no language labels, no commentary. ` +
|
|
119
112
|
`If the text is already in ${target}, return it unchanged.`;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{ role: "system", content: system },
|
|
128
|
-
{ role: "user", content: text },
|
|
129
|
-
],
|
|
130
|
-
modelKey: FALLBACK_MODEL,
|
|
131
|
-
options: { temperature: 0 },
|
|
132
|
-
}),
|
|
133
|
-
new Promise((_, reject) => {
|
|
134
|
-
timer = setTimeout(() => reject(new Error(`LLM fallback timed out after ${FALLBACK_TIMEOUT_MS}ms`)), FALLBACK_TIMEOUT_MS);
|
|
135
|
-
}),
|
|
136
|
-
]).finally(() => {
|
|
137
|
-
if (timer)
|
|
138
|
-
clearTimeout(timer);
|
|
113
|
+
const { text: out } = await service.generateText({
|
|
114
|
+
input: [
|
|
115
|
+
{ role: "system", content: system },
|
|
116
|
+
{ role: "user", content: text },
|
|
117
|
+
],
|
|
118
|
+
modelKey: FALLBACK_MODEL,
|
|
119
|
+
options: { temperature: 0 },
|
|
139
120
|
});
|
|
140
121
|
const trimmed = (out ?? "").trim();
|
|
141
122
|
if (!trimmed || trimmed === "No response") {
|
|
@@ -189,20 +170,81 @@ async function translate(text, from, to = types_1.ELANGUAGE_CODE.en, version = "
|
|
|
189
170
|
}
|
|
190
171
|
}
|
|
191
172
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
173
|
+
* FALLBACK: detect via the same cheap LLM `translate` falls back to. Returns
|
|
174
|
+
* the bare ISO 639-1 code. Throws if the reply doesn't look like a code.
|
|
175
|
+
*/
|
|
176
|
+
async function callLlmDetect(text) {
|
|
177
|
+
const { getAiGenProviderService } = await Promise.resolve().then(() => __importStar(require("../aiGen/aiGenFactory.service")));
|
|
178
|
+
const service = getAiGenProviderService(FALLBACK_MODEL);
|
|
179
|
+
const system = "You are a language identification engine. Reply with ONLY the ISO 639-1 " +
|
|
180
|
+
"code of the language the user's text is written in — two lowercase " +
|
|
181
|
+
'letters, nothing else (e.g. "es", "ja", "ar"). Use "zh-cn" or "zh-tw" ' +
|
|
182
|
+
"for Chinese. No punctuation, no explanation.";
|
|
183
|
+
const { text: out } = await service.generateText({
|
|
184
|
+
// Only the opening slice matters for identification, and capping it keeps
|
|
185
|
+
// this cheap when a long-form brief triggers the fallback.
|
|
186
|
+
input: [
|
|
187
|
+
{ role: "system", content: system },
|
|
188
|
+
{ role: "user", content: text.slice(0, 1000) },
|
|
189
|
+
],
|
|
190
|
+
modelKey: FALLBACK_MODEL,
|
|
191
|
+
options: { temperature: 0 },
|
|
192
|
+
});
|
|
193
|
+
const code = (out ?? "").trim().toLowerCase().replace(/[^a-z-]/g, "");
|
|
194
|
+
if (!/^[a-z]{2}(-[a-z]{2})?$/.test(code)) {
|
|
195
|
+
throw new Error(`LLM detect returned a non-code reply: "${out}"`);
|
|
196
|
+
}
|
|
197
|
+
return code;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Detect the language of `text`. Primary CF → cheap LLM → fail OPEN to English.
|
|
201
|
+
*
|
|
202
|
+
* The LLM layer matters more here than it does for `translate`. The primary
|
|
203
|
+
* endpoint is the same flaky Cloud Function, but detection's failure mode is
|
|
204
|
+
* silent rather than loud: it used to return en/100% with `reliable: false`,
|
|
205
|
+
* and since every caller ignored that flag, a detect timeout quietly forced
|
|
206
|
+
* English on a non-English user. That is one of the ways the product ended up
|
|
207
|
+
* English-only regardless of what the user wrote.
|
|
208
|
+
*
|
|
209
|
+
* The terminal fallback still returns English so detection can never block a
|
|
210
|
+
* generation run — but it keeps `reliable: false`, and callers
|
|
211
|
+
* (`resolveOutputLanguage`) now honour that flag and defer to the UI locale
|
|
212
|
+
* instead of treating the guess as fact.
|
|
194
213
|
*/
|
|
195
214
|
async function detectLanguage(text) {
|
|
215
|
+
let primaryErr;
|
|
196
216
|
try {
|
|
197
217
|
const res = await axios_1.default.post(`${PRIMARY_BASE}/detectLang`, { text }, {
|
|
198
218
|
headers: { "internal-key": process.env.TRANSLATION_SERVICE_KEY || "" },
|
|
199
219
|
timeout: PRIMARY_TIMEOUT_MS,
|
|
200
220
|
});
|
|
201
|
-
|
|
221
|
+
if (res.data?.languages?.[0]?.code)
|
|
222
|
+
return res.data;
|
|
223
|
+
throw new Error("primary detectLang returned no language");
|
|
202
224
|
}
|
|
203
225
|
catch (err) {
|
|
204
|
-
|
|
205
|
-
|
|
226
|
+
primaryErr = err;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const code = await callLlmDetect(text);
|
|
230
|
+
logger_1.logger.warn("translation: detectLang primary failed, LLM recovered", {
|
|
231
|
+
fallbackModel: FALLBACK_MODEL,
|
|
232
|
+
detected: code,
|
|
233
|
+
err: errMsg(primaryErr),
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
reliable: true,
|
|
237
|
+
textBytes: Buffer.byteLength(text, "utf8"),
|
|
238
|
+
languages: [
|
|
239
|
+
{ name: languageLabel(code), code, percent: 100, score: 1 },
|
|
240
|
+
],
|
|
241
|
+
chunks: [],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
catch (fallbackErr) {
|
|
245
|
+
logger_1.logger.warn("translation: detectLang failed on both layers, defaulting to en", {
|
|
246
|
+
primaryErr: errMsg(primaryErr),
|
|
247
|
+
fallbackErr: errMsg(fallbackErr),
|
|
206
248
|
});
|
|
207
249
|
return {
|
|
208
250
|
reliable: false,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/tts/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/tts/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,sBAAsB,CAAC"}
|
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./wordTimings"), exports);
|
|
18
19
|
__exportStar(require("./providers/elevenlabs.service"), exports);
|
|
19
20
|
__exportStar(require("./providers/minimax.service"), exports);
|
|
20
21
|
__exportStar(require("./providers/openai.service"), exports);
|
|
@@ -9,6 +9,13 @@ export declare class ElevenLabsService extends BaseTtsProviderService {
|
|
|
9
9
|
* Retries: axios timeout (ECONNABORTED), network errors (ECONNRESET/ETIMEDOUT/etc.),
|
|
10
10
|
* HTTP 429 (rate limit, longer backoff), HTTP 5xx (server errors).
|
|
11
11
|
* Does NOT retry: 400/401/403/404 (caller handles 400 language_code separately).
|
|
12
|
+
*
|
|
13
|
+
* Two endpoints, one code path. The plain endpoint streams audio bytes; the
|
|
14
|
+
* `/with-timestamps` endpoint returns JSON carrying base64 audio PLUS character
|
|
15
|
+
* alignment. We keep `responseType: "arraybuffer"` for BOTH and parse the JSON
|
|
16
|
+
* out of the buffer ourselves, so error bodies keep arriving as Buffers and the
|
|
17
|
+
* caller's error-decoding (which reads `err.response.data` as a Buffer) stays
|
|
18
|
+
* correct for both modes.
|
|
12
19
|
*/
|
|
13
20
|
private postTtsWithRetry;
|
|
14
21
|
getVoices(): ITtsVoiceOption[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elevenlabs.service.d.ts","sourceRoot":"","sources":["../../../../src/services/tts/providers/elevenlabs.service.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"elevenlabs.service.d.ts","sourceRoot":"","sources":["../../../../src/services/tts/providers/elevenlabs.service.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,sBAAsB,EACtB,eAAe,EAEf,SAAS,EACT,SAAS,EACV,MAAM,UAAU,CAAC;AAiBlB,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAW1B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IAuGrD;;;;;;;;;;;;OAYG;YACW,gBAAgB;IAsE9B,SAAS,IAAI,eAAe,EAAE;IAI9B,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAGpD"}
|
|
@@ -88,12 +88,14 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
|
|
|
88
88
|
};
|
|
89
89
|
const outputFormat = params.outputFormat;
|
|
90
90
|
const { mimeType, extension } = mimeFromOutputFormat(outputFormat);
|
|
91
|
+
const postOpts = {
|
|
92
|
+
outputFormat,
|
|
93
|
+
optimizeStreamingLatency: params.optimizeStreamingLatency,
|
|
94
|
+
withTimestamps: params.withTimestamps === true,
|
|
95
|
+
};
|
|
91
96
|
try {
|
|
92
|
-
const audioBuffer = await this.postTtsWithRetry(voiceId, buildBody(languageCode),
|
|
93
|
-
|
|
94
|
-
optimizeStreamingLatency: params.optimizeStreamingLatency,
|
|
95
|
-
});
|
|
96
|
-
return { audioBuffer, mimeType, extension };
|
|
97
|
+
const { audioBuffer, alignment } = await this.postTtsWithRetry(voiceId, buildBody(languageCode), postOpts);
|
|
98
|
+
return { audioBuffer, mimeType, extension, ...(alignment ? { alignment } : {}) };
|
|
97
99
|
}
|
|
98
100
|
catch (err) {
|
|
99
101
|
// responseType:"arraybuffer" causes error bodies to arrive as Buffer/ArrayBuffer,
|
|
@@ -114,11 +116,8 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
|
|
|
114
116
|
if (err.response.status === 400 &&
|
|
115
117
|
languageCode &&
|
|
116
118
|
(detail.includes("language_code") || detail.includes("does not support"))) {
|
|
117
|
-
const audioBuffer = await this.postTtsWithRetry(voiceId, buildBody(undefined),
|
|
118
|
-
|
|
119
|
-
optimizeStreamingLatency: params.optimizeStreamingLatency,
|
|
120
|
-
});
|
|
121
|
-
return { audioBuffer, mimeType, extension };
|
|
119
|
+
const { audioBuffer, alignment } = await this.postTtsWithRetry(voiceId, buildBody(undefined), postOpts);
|
|
120
|
+
return { audioBuffer, mimeType, extension, ...(alignment ? { alignment } : {}) };
|
|
122
121
|
}
|
|
123
122
|
const wrapped = new Error(`ElevenLabs TTS ${err.response.status}: ${detail} [voiceId=${voiceId}, languageCode=${languageCode ?? "none"}, modelId=${modelId}, textLen=${params.text?.length ?? 0}]`);
|
|
124
123
|
wrapped.cause = err;
|
|
@@ -132,6 +131,13 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
|
|
|
132
131
|
* Retries: axios timeout (ECONNABORTED), network errors (ECONNRESET/ETIMEDOUT/etc.),
|
|
133
132
|
* HTTP 429 (rate limit, longer backoff), HTTP 5xx (server errors).
|
|
134
133
|
* Does NOT retry: 400/401/403/404 (caller handles 400 language_code separately).
|
|
134
|
+
*
|
|
135
|
+
* Two endpoints, one code path. The plain endpoint streams audio bytes; the
|
|
136
|
+
* `/with-timestamps` endpoint returns JSON carrying base64 audio PLUS character
|
|
137
|
+
* alignment. We keep `responseType: "arraybuffer"` for BOTH and parse the JSON
|
|
138
|
+
* out of the buffer ourselves, so error bodies keep arriving as Buffers and the
|
|
139
|
+
* caller's error-decoding (which reads `err.response.data` as a Buffer) stays
|
|
140
|
+
* correct for both modes.
|
|
135
141
|
*/
|
|
136
142
|
async postTtsWithRetry(voiceId, body, opts) {
|
|
137
143
|
const query = {};
|
|
@@ -140,21 +146,37 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
|
|
|
140
146
|
if (opts?.optimizeStreamingLatency !== undefined) {
|
|
141
147
|
query.optimize_streaming_latency = opts.optimizeStreamingLatency;
|
|
142
148
|
}
|
|
149
|
+
const withTimestamps = opts?.withTimestamps === true;
|
|
150
|
+
const url = withTimestamps
|
|
151
|
+
? `${this.baseUrl}/text-to-speech/${voiceId}/with-timestamps`
|
|
152
|
+
: `${this.baseUrl}/text-to-speech/${voiceId}`;
|
|
143
153
|
const { mimeType } = mimeFromOutputFormat(opts?.outputFormat);
|
|
154
|
+
const accept = withTimestamps ? "application/json" : mimeType;
|
|
144
155
|
let lastErr;
|
|
145
156
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
146
157
|
try {
|
|
147
|
-
const response = await axios_1.default.post(
|
|
158
|
+
const response = await axios_1.default.post(url, body, {
|
|
148
159
|
headers: {
|
|
149
160
|
"xi-api-key": this.apiKey,
|
|
150
161
|
"Content-Type": "application/json",
|
|
151
|
-
Accept:
|
|
162
|
+
Accept: accept,
|
|
152
163
|
},
|
|
153
164
|
params: Object.keys(query).length ? query : undefined,
|
|
154
165
|
responseType: "arraybuffer",
|
|
155
166
|
timeout: PER_ATTEMPT_TIMEOUT_MS,
|
|
156
167
|
});
|
|
157
|
-
|
|
168
|
+
const raw = Buffer.from(response.data);
|
|
169
|
+
if (!withTimestamps)
|
|
170
|
+
return { audioBuffer: raw };
|
|
171
|
+
// A malformed/partial timestamps payload must not lose us the audio —
|
|
172
|
+
// but there's no audio to salvage if the JSON itself won't parse, so
|
|
173
|
+
// that case is a real error. Missing/!ok alignment degrades to no
|
|
174
|
+
// alignment, and the caller falls back to estimated word spacing.
|
|
175
|
+
const parsed = parseTimestampedResponse(raw);
|
|
176
|
+
return {
|
|
177
|
+
audioBuffer: parsed.audioBuffer,
|
|
178
|
+
...(parsed.alignment ? { alignment: parsed.alignment } : {}),
|
|
179
|
+
};
|
|
158
180
|
}
|
|
159
181
|
catch (err) {
|
|
160
182
|
lastErr = err;
|
|
@@ -211,6 +233,40 @@ function mimeFromOutputFormat(fmt) {
|
|
|
211
233
|
return { mimeType: "audio/mpeg", extension: "mp3" };
|
|
212
234
|
}
|
|
213
235
|
}
|
|
236
|
+
/** True when every element of `v` is a finite number. */
|
|
237
|
+
function isNumberArray(v) {
|
|
238
|
+
return Array.isArray(v) && v.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
239
|
+
}
|
|
240
|
+
function isStringArray(v) {
|
|
241
|
+
return Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Decode a `/with-timestamps` response: base64 audio + character alignment.
|
|
245
|
+
*
|
|
246
|
+
* We deliberately read `alignment` and NOT `normalized_alignment`. Normalized
|
|
247
|
+
* alignment is timed against the text ElevenLabs actually spoke after its
|
|
248
|
+
* normaliser ran — "200,000" becomes "two hundred thousand" — so its characters
|
|
249
|
+
* no longer correspond to the text we display in a caption. `alignment` is timed
|
|
250
|
+
* against the input text verbatim, which is what gets drawn.
|
|
251
|
+
*/
|
|
252
|
+
function parseTimestampedResponse(raw) {
|
|
253
|
+
const body = JSON.parse(raw.toString("utf8"));
|
|
254
|
+
if (typeof body.audio_base64 !== "string" || !body.audio_base64) {
|
|
255
|
+
throw new Error("ElevenLabs with-timestamps response carried no audio_base64");
|
|
256
|
+
}
|
|
257
|
+
const audioBuffer = Buffer.from(body.audio_base64, "base64");
|
|
258
|
+
const a = body.alignment;
|
|
259
|
+
const characters = a?.characters;
|
|
260
|
+
const startsSec = a?.character_start_times_seconds;
|
|
261
|
+
const endsSec = a?.character_end_times_seconds;
|
|
262
|
+
if (!isStringArray(characters) || !isNumberArray(startsSec) || !isNumberArray(endsSec)) {
|
|
263
|
+
logger_1.logger.warn("ElevenLabs returned audio without a usable character alignment", {
|
|
264
|
+
hasAlignment: Boolean(a),
|
|
265
|
+
});
|
|
266
|
+
return { audioBuffer };
|
|
267
|
+
}
|
|
268
|
+
return { audioBuffer, alignment: { characters, startsSec, endsSec } };
|
|
269
|
+
}
|
|
214
270
|
function isTransientHttpError(err) {
|
|
215
271
|
if (!err)
|
|
216
272
|
return false;
|
|
@@ -41,11 +41,40 @@ export interface TtsParams {
|
|
|
41
41
|
pronunciation_dictionary_id: string;
|
|
42
42
|
version_id?: string;
|
|
43
43
|
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Ask the provider to return per-CHARACTER timings alongside the audio, so the
|
|
46
|
+
* caller can derive word timings and draw captions synced to the actual speech
|
|
47
|
+
* instead of guessing from a words-per-second estimate.
|
|
48
|
+
*
|
|
49
|
+
* Only ElevenLabs implements this (its `/with-timestamps` endpoint). Providers
|
|
50
|
+
* that don't simply ignore the flag and return `TtsResult.alignment`
|
|
51
|
+
* undefined — the audio is identical either way, so a caller can always ask.
|
|
52
|
+
*/
|
|
53
|
+
withTimestamps?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Per-character speech timings for one synthesized utterance. Parallel arrays —
|
|
57
|
+
* `characters[i]` is spoken from `startsSec[i]` to `endsSec[i]`, relative to the
|
|
58
|
+
* start of THIS utterance's audio (not the video timeline).
|
|
59
|
+
*
|
|
60
|
+
* Characters, not words, is the provider's native granularity; use
|
|
61
|
+
* `wordsFromAlignment` to group them.
|
|
62
|
+
*/
|
|
63
|
+
export interface TtsAlignment {
|
|
64
|
+
characters: string[];
|
|
65
|
+
startsSec: number[];
|
|
66
|
+
endsSec: number[];
|
|
44
67
|
}
|
|
45
68
|
export interface TtsResult {
|
|
46
69
|
audioBuffer: Buffer;
|
|
47
70
|
mimeType: string;
|
|
48
71
|
extension: string;
|
|
72
|
+
/**
|
|
73
|
+
* Character timings, present only when `TtsParams.withTimestamps` was set AND
|
|
74
|
+
* the provider supports it. Always handle absence — a caller that needs word
|
|
75
|
+
* timings must degrade (e.g. to evenly-spaced words) rather than fail.
|
|
76
|
+
*/
|
|
77
|
+
alignment?: TtsAlignment;
|
|
49
78
|
}
|
|
50
79
|
export interface ITtsVoiceOption {
|
|
51
80
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/tts/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,yGAAyG;IACzG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iGAAiG;IACjG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sGAAsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAG1B,2EAA2E;IAC3E,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,8BAA8B,CAAC,EAAE,OAAO,CAAC;IAGzC,0FAA0F;IAC1F,wBAAwB,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAE7C,qEAAqE;IACrE,+BAA+B,CAAC,EAAE,KAAK,CAAC;QACtC,2BAA2B,EAAE,MAAM,CAAC;QACpC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/tts/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,yGAAyG;IACzG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iGAAiG;IACjG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sGAAsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAG1B,2EAA2E;IAC3E,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,8BAA8B,CAAC,EAAE,OAAO,CAAC;IAGzC,0FAA0F;IAC1F,wBAAwB,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAE7C,qEAAqE;IACrE,+BAA+B,CAAC,EAAE,KAAK,CAAC;QACtC,2BAA2B,EAAE,MAAM,CAAC;QACpC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;IAGH;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,8BAAsB,sBAAsB;IAC1C,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IAExD;;;OAGG;IACH,QAAQ,CAAC,SAAS,IAAI,eAAe,EAAE;IAEvC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAC7D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TtsAlignment } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Word-level speech timings derived from a provider's character alignment.
|
|
4
|
+
* Times are relative to the start of the utterance's own audio, seconds.
|
|
5
|
+
*/
|
|
6
|
+
export interface TtsWord {
|
|
7
|
+
word: string;
|
|
8
|
+
startSec: number;
|
|
9
|
+
endSec: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Group a provider's per-CHARACTER alignment into words.
|
|
13
|
+
*
|
|
14
|
+
* TTS providers expose timings per character (that IS their native granularity);
|
|
15
|
+
* captions need them per word. A word runs from its first character's start to
|
|
16
|
+
* its last character's end, and whitespace characters are the delimiters —
|
|
17
|
+
* punctuation stays attached to the word it belongs to, because the caller
|
|
18
|
+
* displays this text verbatim.
|
|
19
|
+
*
|
|
20
|
+
* Defensive on purpose: the alignment is a parsed API response (a trust
|
|
21
|
+
* boundary), so mismatched array lengths are truncated to the shortest rather
|
|
22
|
+
* than trusted, and non-finite / out-of-order timings are repaired instead of
|
|
23
|
+
* poisoning the caption track. Returns `[]` for an empty or unusable alignment,
|
|
24
|
+
* which callers treat as "no word timings" and degrade from.
|
|
25
|
+
*/
|
|
26
|
+
export declare function wordsFromAlignment(alignment: TtsAlignment): TtsWord[];
|
|
27
|
+
//# sourceMappingURL=wordTimings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wordTimings.d.ts","sourceRoot":"","sources":["../../../src/services/tts/wordTimings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,YAAY,GAAG,OAAO,EAAE,CA6CrE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wordsFromAlignment = wordsFromAlignment;
|
|
4
|
+
/**
|
|
5
|
+
* Group a provider's per-CHARACTER alignment into words.
|
|
6
|
+
*
|
|
7
|
+
* TTS providers expose timings per character (that IS their native granularity);
|
|
8
|
+
* captions need them per word. A word runs from its first character's start to
|
|
9
|
+
* its last character's end, and whitespace characters are the delimiters —
|
|
10
|
+
* punctuation stays attached to the word it belongs to, because the caller
|
|
11
|
+
* displays this text verbatim.
|
|
12
|
+
*
|
|
13
|
+
* Defensive on purpose: the alignment is a parsed API response (a trust
|
|
14
|
+
* boundary), so mismatched array lengths are truncated to the shortest rather
|
|
15
|
+
* than trusted, and non-finite / out-of-order timings are repaired instead of
|
|
16
|
+
* poisoning the caption track. Returns `[]` for an empty or unusable alignment,
|
|
17
|
+
* which callers treat as "no word timings" and degrade from.
|
|
18
|
+
*/
|
|
19
|
+
function wordsFromAlignment(alignment) {
|
|
20
|
+
const n = Math.min(alignment.characters.length, alignment.startsSec.length, alignment.endsSec.length);
|
|
21
|
+
const words = [];
|
|
22
|
+
let text = "";
|
|
23
|
+
let start = 0;
|
|
24
|
+
let end = 0;
|
|
25
|
+
let open = false;
|
|
26
|
+
const flush = () => {
|
|
27
|
+
if (!open)
|
|
28
|
+
return;
|
|
29
|
+
const trimmed = text.trim();
|
|
30
|
+
if (trimmed) {
|
|
31
|
+
// A provider glitch can hand back end <= start; keep words strictly
|
|
32
|
+
// ordered so the renderer's `t >= start && t <= end` test can still match.
|
|
33
|
+
words.push({ word: trimmed, startSec: start, endSec: Math.max(end, start) });
|
|
34
|
+
}
|
|
35
|
+
text = "";
|
|
36
|
+
open = false;
|
|
37
|
+
};
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
const ch = alignment.characters[i] ?? "";
|
|
40
|
+
if (/^\s*$/.test(ch)) {
|
|
41
|
+
flush();
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const s = alignment.startsSec[i];
|
|
45
|
+
const e = alignment.endsSec[i];
|
|
46
|
+
const sSec = Number.isFinite(s) ? s : end;
|
|
47
|
+
const eSec = Number.isFinite(e) ? e : sSec;
|
|
48
|
+
if (!open) {
|
|
49
|
+
start = sSec;
|
|
50
|
+
open = true;
|
|
51
|
+
}
|
|
52
|
+
end = Math.max(end, eSec);
|
|
53
|
+
text += ch;
|
|
54
|
+
}
|
|
55
|
+
flush();
|
|
56
|
+
return words;
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick the duration to bill for a scene.
|
|
3
|
+
*
|
|
4
|
+
* @param modelKey Model the scene was generated with.
|
|
5
|
+
* @param deliveredSec Measured/reported length of the delivered clip, if known.
|
|
6
|
+
* @param requestedSec Length we asked the provider for.
|
|
7
|
+
* @returns The duration to price, or `undefined` when the delivered value
|
|
8
|
+
* cannot be priced and the caller should fall back to `requestedSec`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toBillableDurationSec(modelKey: string, deliveredSec: number | undefined, requestedSec: number | undefined): number | undefined;
|
|
11
|
+
//# sourceMappingURL=billableDuration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billableDuration.d.ts","sourceRoot":"","sources":["../../src/utils/billableDuration.ts"],"names":[],"mappings":"AAkEA;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,GAAG,SAAS,CA4BpB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toBillableDurationSec = toBillableDurationSec;
|
|
4
|
+
const aiModels_1 = require("../globals/aiModels");
|
|
5
|
+
const logger_1 = require("./logger");
|
|
6
|
+
/**
|
|
7
|
+
* Normalise a DELIVERED clip length into a value the credit tables can price.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS IS REQUIRED
|
|
10
|
+
* Most provider cost tables key on duration as a literal object key —
|
|
11
|
+
* `cost.table[mode][duration]` (Kling) and
|
|
12
|
+
* `cost[audio|noAudio].table[resolution][duration]` (PixVerse). Those keys are
|
|
13
|
+
* the discrete durations the model actually sells: {5, 10}, {5, 8, 10, 15}, etc.
|
|
14
|
+
*
|
|
15
|
+
* A delivered length is NOT one of those keys. A nominal 5s clip encoded at
|
|
16
|
+
* 30fps is 5.033s on the wire; an extend appends a native increment nobody
|
|
17
|
+
* asked for. Feeding a raw measured value straight into the table MISSES,
|
|
18
|
+
* which drops the calculation into its fallback branch — a flat $1.00 (10
|
|
19
|
+
* credits) for Kling, a flat 10 credits for PixVerse. That fallback is worse
|
|
20
|
+
* than the over-billing we set out to fix: it would turn a correct 7-credit
|
|
21
|
+
* charge into 10, on every scene, for every model.
|
|
22
|
+
*
|
|
23
|
+
* So delivered durations are snapped onto the model's own declared
|
|
24
|
+
* `fields.duration.allowedValues` before they are priced. That list is
|
|
25
|
+
* uniform across providers, so this needs no per-provider special-casing.
|
|
26
|
+
*
|
|
27
|
+
* SNAPPING RULE
|
|
28
|
+
* 1. Within `SNAP_TOLERANCE_SEC` of an allowed value → that value.
|
|
29
|
+
* Absorbs encoder drift (5.033 → 5) without changing what we charge.
|
|
30
|
+
* 2. Otherwise the smallest allowed value >= delivered.
|
|
31
|
+
* Rounding UP matches how providers bill partial increments (Kling
|
|
32
|
+
* lipsync explicitly bills `ceil(d / 5) * 5`) and never under-charges.
|
|
33
|
+
* 3. Longer than every allowed value → `undefined`, so the caller keeps the
|
|
34
|
+
* REQUESTED duration. This is the genuinely unknown case (e.g. an extend
|
|
35
|
+
* whose total exceeds any single-generation length the model sells); it
|
|
36
|
+
* is logged and left on the pre-existing behaviour rather than guessed.
|
|
37
|
+
*
|
|
38
|
+
* Models with no declared `allowedValues` are per-second priced (kling-avatar,
|
|
39
|
+
* kling-v3.0-turbo compute `perSecond * duration` and never touch a table), so
|
|
40
|
+
* a fractional value is both safe and more accurate there — it passes through.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* How far a delivered length may sit above/below a sold duration and still be
|
|
44
|
+
* treated as that duration. One third of a second comfortably covers
|
|
45
|
+
* frame-quantisation drift at 24/25/30/60fps (a single frame is <= 0.042s)
|
|
46
|
+
* while staying far below the gap between any two sold durations (>= 2s).
|
|
47
|
+
*/
|
|
48
|
+
const SNAP_TOLERANCE_SEC = 1 / 3;
|
|
49
|
+
/** Read the model's declared sellable durations, if it declares any. */
|
|
50
|
+
function allowedDurations(modelKey) {
|
|
51
|
+
// Typed `string[] | number[] | boolean[]` in the config schema, so it is
|
|
52
|
+
// narrowed from `unknown` here rather than assumed to be numeric.
|
|
53
|
+
// `modelKey` arrives as a plain string from the job document, so the config
|
|
54
|
+
// map is indexed through a widened view — an unknown key yields undefined
|
|
55
|
+
// rather than a type error, and is handled as "no declared durations".
|
|
56
|
+
const configs = aiModels_1.aiModelConfigs;
|
|
57
|
+
const raw = configs[modelKey]?.fields?.duration?.allowedValues;
|
|
58
|
+
if (!Array.isArray(raw))
|
|
59
|
+
return undefined;
|
|
60
|
+
const numeric = raw.filter((v) => typeof v === "number" && Number.isFinite(v) && v > 0);
|
|
61
|
+
return numeric.length > 0 ? [...numeric].sort((a, b) => a - b) : undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Pick the duration to bill for a scene.
|
|
65
|
+
*
|
|
66
|
+
* @param modelKey Model the scene was generated with.
|
|
67
|
+
* @param deliveredSec Measured/reported length of the delivered clip, if known.
|
|
68
|
+
* @param requestedSec Length we asked the provider for.
|
|
69
|
+
* @returns The duration to price, or `undefined` when the delivered value
|
|
70
|
+
* cannot be priced and the caller should fall back to `requestedSec`.
|
|
71
|
+
*/
|
|
72
|
+
function toBillableDurationSec(modelKey, deliveredSec, requestedSec) {
|
|
73
|
+
if (typeof deliveredSec !== "number" ||
|
|
74
|
+
!Number.isFinite(deliveredSec) ||
|
|
75
|
+
deliveredSec <= 0) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const allowed = allowedDurations(modelKey);
|
|
79
|
+
// Per-second models: no table to miss, so the exact measured value is both
|
|
80
|
+
// safe and the most accurate thing to bill.
|
|
81
|
+
if (!allowed)
|
|
82
|
+
return deliveredSec;
|
|
83
|
+
const withinTolerance = allowed.find((v) => Math.abs(deliveredSec - v) <= SNAP_TOLERANCE_SEC);
|
|
84
|
+
if (withinTolerance !== undefined)
|
|
85
|
+
return withinTolerance;
|
|
86
|
+
const roundedUp = allowed.find((v) => v >= deliveredSec);
|
|
87
|
+
if (roundedUp !== undefined)
|
|
88
|
+
return roundedUp;
|
|
89
|
+
logger_1.logger.warn(`toBillableDurationSec: delivered ${deliveredSec}s exceeds every sellable duration for "${modelKey}" (max ${allowed[allowed.length - 1]}s) — billing the requested duration instead`, { modelKey, deliveredSec, requestedSec, allowed });
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export * from "./billableDuration";
|
|
1
2
|
export * from "./errors";
|
|
2
3
|
export * from "./helpers";
|
|
4
|
+
export * from "./mp4Duration";
|
|
3
5
|
export * from "./logger";
|
|
4
6
|
export * from "./renderTier";
|
|
7
|
+
export * from "./socialEntitlements";
|
|
5
8
|
export * from "./ttsUtils";
|
|
6
9
|
export * from "./outreachTemplate";
|
|
7
10
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/utils/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC"}
|
package/lib/utils/index.js
CHANGED
|
@@ -14,9 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./billableDuration"), exports);
|
|
17
18
|
__exportStar(require("./errors"), exports);
|
|
18
19
|
__exportStar(require("./helpers"), exports);
|
|
20
|
+
__exportStar(require("./mp4Duration"), exports);
|
|
19
21
|
__exportStar(require("./logger"), exports);
|
|
20
22
|
__exportStar(require("./renderTier"), exports);
|
|
23
|
+
__exportStar(require("./socialEntitlements"), exports);
|
|
21
24
|
__exportStar(require("./ttsUtils"), exports);
|
|
22
25
|
__exportStar(require("./outreachTemplate"), exports);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free MP4 duration reader.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* Billing prefers the DELIVERED clip length over the REQUESTED one (see
|
|
6
|
+
* `VideoStatusResult.durationSec`). Kling reports the delivered length in its
|
|
7
|
+
* status payload, so nothing has to be measured there. PixVerse does NOT — its
|
|
8
|
+
* `/video/result` response carries `create_time, id, modify_time,
|
|
9
|
+
* negative_prompt, outputHeight, outputWidth, prompt, resolution_ratio, seed,
|
|
10
|
+
* size, status, style, url` and nothing else (verified against the official
|
|
11
|
+
* docs 2026-07-24; `size` is BYTES, not seconds). So for PixVerse the only
|
|
12
|
+
* honest source of the delivered length is the file itself.
|
|
13
|
+
*
|
|
14
|
+
* We already download the finished clip into a Buffer before uploading it to
|
|
15
|
+
* Firebase, so the bytes are in hand at zero extra network cost. This module
|
|
16
|
+
* reads the length out of that Buffer by walking the MP4 box tree to `mvhd`,
|
|
17
|
+
* following the same precedent as `mp4IsComplete()` in google.service.ts:
|
|
18
|
+
* scan the bytes rather than shell out, because `packages/shared` has no
|
|
19
|
+
* ffmpeg/ffprobe dependency and must not gain one (it ships to Firebase
|
|
20
|
+
* Functions, where no such binary exists).
|
|
21
|
+
*
|
|
22
|
+
* CONTRACT: this function never throws and never guesses. Every malformed,
|
|
23
|
+
* truncated, fragmented or otherwise unreadable input returns `undefined`, and
|
|
24
|
+
* callers fall back to the requested duration — i.e. the pre-existing billing
|
|
25
|
+
* behaviour. A wrong number here would mis-bill a real user, so "I don't know"
|
|
26
|
+
* is always the correct answer when the bytes aren't unambiguous.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Read the duration, in seconds, of an MP4/MOV held in memory.
|
|
30
|
+
*
|
|
31
|
+
* Returns `undefined` for anything it cannot read with confidence: non-MP4
|
|
32
|
+
* input, a truncated download (no `moov`), fragmented MP4 with an unknown
|
|
33
|
+
* `mvhd` duration, a zero/absent timescale, or an implausible result.
|
|
34
|
+
*
|
|
35
|
+
* @param buf Raw bytes of the complete file.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readMp4DurationSec(buf: Buffer): number | undefined;
|
|
38
|
+
//# sourceMappingURL=mp4Duration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mp4Duration.d.ts","sourceRoot":"","sources":["../../src/utils/mp4Duration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AA4EH;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAuDlE"}
|