vidspotai-shared 1.0.99 → 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/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/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 +66 -5
- 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
|
@@ -170,20 +170,81 @@ async function translate(text, from, to = types_1.ELANGUAGE_CODE.en, version = "
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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.
|
|
175
213
|
*/
|
|
176
214
|
async function detectLanguage(text) {
|
|
215
|
+
let primaryErr;
|
|
177
216
|
try {
|
|
178
217
|
const res = await axios_1.default.post(`${PRIMARY_BASE}/detectLang`, { text }, {
|
|
179
218
|
headers: { "internal-key": process.env.TRANSLATION_SERVICE_KEY || "" },
|
|
180
219
|
timeout: PRIMARY_TIMEOUT_MS,
|
|
181
220
|
});
|
|
182
|
-
|
|
221
|
+
if (res.data?.languages?.[0]?.code)
|
|
222
|
+
return res.data;
|
|
223
|
+
throw new Error("primary detectLang returned no language");
|
|
183
224
|
}
|
|
184
225
|
catch (err) {
|
|
185
|
-
|
|
186
|
-
|
|
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),
|
|
187
248
|
});
|
|
188
249
|
return {
|
|
189
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"}
|