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.
Files changed (183) hide show
  1. package/lib/globals/aiModels/enums.d.ts +1 -0
  2. package/lib/globals/aiModels/enums.d.ts.map +1 -1
  3. package/lib/globals/aiModels/enums.js +3 -0
  4. package/lib/globals/aiModels/extendCapability.d.ts +26 -0
  5. package/lib/globals/aiModels/extendCapability.d.ts.map +1 -0
  6. package/lib/globals/aiModels/extendCapability.js +42 -0
  7. package/lib/globals/aiModels/index.d.ts +1 -0
  8. package/lib/globals/aiModels/index.d.ts.map +1 -1
  9. package/lib/globals/aiModels/index.js +1 -0
  10. package/lib/globals/aiModels/providers/google.d.ts.map +1 -1
  11. package/lib/globals/aiModels/providers/google.js +6 -9
  12. package/lib/globals/aiModels/providers/hostedAvatars.js +10 -10
  13. package/lib/globals/creditUnit.d.ts +20 -33
  14. package/lib/globals/creditUnit.d.ts.map +1 -1
  15. package/lib/globals/creditUnit.js +20 -33
  16. package/lib/globals/types.d.ts +34 -2
  17. package/lib/globals/types.d.ts.map +1 -1
  18. package/lib/globals/types.js +37 -0
  19. package/lib/models/demoVideoProject.model.d.ts +213 -0
  20. package/lib/models/demoVideoProject.model.d.ts.map +1 -1
  21. package/lib/models/index.d.ts +2 -0
  22. package/lib/models/index.d.ts.map +1 -1
  23. package/lib/models/index.js +2 -0
  24. package/lib/models/mediaUpload.model.d.ts +25 -0
  25. package/lib/models/mediaUpload.model.d.ts.map +1 -1
  26. package/lib/models/productConnection.model.d.ts +57 -0
  27. package/lib/models/productConnection.model.d.ts.map +1 -0
  28. package/lib/models/productConnection.model.js +7 -0
  29. package/lib/models/video.model.d.ts +11 -0
  30. package/lib/models/video.model.d.ts.map +1 -1
  31. package/lib/models/videoQaJob.model.d.ts +61 -0
  32. package/lib/models/videoQaJob.model.d.ts.map +1 -0
  33. package/lib/models/videoQaJob.model.js +16 -0
  34. package/lib/schemas/brandKit.schema.d.ts +3 -1
  35. package/lib/schemas/brandKit.schema.d.ts.map +1 -1
  36. package/lib/schemas/brandKit.schema.js +9 -0
  37. package/lib/schemas/brief.schema.d.ts +5 -4
  38. package/lib/schemas/brief.schema.d.ts.map +1 -1
  39. package/lib/schemas/brief.schema.js +23 -1
  40. package/lib/schemas/demoQuestion.schema.d.ts +24 -4
  41. package/lib/schemas/demoQuestion.schema.d.ts.map +1 -1
  42. package/lib/schemas/demoQuestion.schema.js +8 -2
  43. package/lib/schemas/demoSpec.schema.d.ts +40 -6
  44. package/lib/schemas/demoSpec.schema.d.ts.map +1 -1
  45. package/lib/schemas/demoSpec.schema.js +46 -1
  46. package/lib/schemas/project.schema.d.ts +33 -3
  47. package/lib/schemas/project.schema.d.ts.map +1 -1
  48. package/lib/schemas/project.schema.js +22 -0
  49. package/lib/schemas/social.schema.d.ts +1 -1
  50. package/lib/schemas/videoPlan.schema.d.ts +22 -20
  51. package/lib/schemas/videoPlan.schema.d.ts.map +1 -1
  52. package/lib/schemas/videoPlan.schema.js +19 -0
  53. package/lib/services/agent/editClassifier.d.ts +10 -10
  54. package/lib/services/agent/eval/types.d.ts +3 -2
  55. package/lib/services/agent/eval/types.d.ts.map +1 -1
  56. package/lib/services/agent/executor/core.d.ts.map +1 -1
  57. package/lib/services/agent/executor/core.js +11 -0
  58. package/lib/services/agent/planner/Planner.d.ts.map +1 -1
  59. package/lib/services/agent/planner/Planner.js +5 -0
  60. package/lib/services/agent/planner/overlayRegen.d.ts.map +1 -1
  61. package/lib/services/agent/planner/overlayRegen.js +5 -0
  62. package/lib/services/agent/planner/plannerMessages.d.ts.map +1 -1
  63. package/lib/services/agent/planner/plannerMessages.js +10 -1
  64. package/lib/services/agent/planner/scriptFirstPlanner.d.ts.map +1 -1
  65. package/lib/services/agent/planner/scriptFirstPlanner.js +2 -0
  66. package/lib/services/agent/regenCore.d.ts.map +1 -1
  67. package/lib/services/agent/regenCore.js +11 -1
  68. package/lib/services/agent/runHelpers.d.ts +8 -0
  69. package/lib/services/agent/runHelpers.d.ts.map +1 -1
  70. package/lib/services/agent/runHelpers.js +5 -1
  71. package/lib/services/agent/tools/chapterOutline.tool.d.ts +8 -0
  72. package/lib/services/agent/tools/chapterOutline.tool.d.ts.map +1 -1
  73. package/lib/services/agent/tools/chapterOutline.tool.js +21 -8
  74. package/lib/services/agent/tools/composeScene.tool.d.ts +32 -2
  75. package/lib/services/agent/tools/composeScene.tool.d.ts.map +1 -1
  76. package/lib/services/agent/tools/estimateCost.tool.d.ts +11 -1
  77. package/lib/services/agent/tools/estimateCost.tool.d.ts.map +1 -1
  78. package/lib/services/agent/tools/extendVideo.tool.d.ts +9 -3
  79. package/lib/services/agent/tools/extendVideo.tool.d.ts.map +1 -1
  80. package/lib/services/agent/tools/extendVideo.tool.js +12 -27
  81. package/lib/services/agent/tools/generateAvatarVideo.tool.d.ts +1 -1
  82. package/lib/services/agent/tools/generateScript.tool.d.ts +3 -2
  83. package/lib/services/agent/tools/generateScript.tool.d.ts.map +1 -1
  84. package/lib/services/agent/tools/generateScript.tool.js +16 -7
  85. package/lib/services/agent/tools/generateVoiceover.tool.d.ts +1 -1
  86. package/lib/services/agent/tools/planVideo.tool.d.ts +11 -8
  87. package/lib/services/agent/tools/planVideo.tool.d.ts.map +1 -1
  88. package/lib/services/agent/tools/render.tool.d.ts +11 -1
  89. package/lib/services/agent/tools/render.tool.d.ts.map +1 -1
  90. package/lib/services/agent/tools/searchStock.tool.d.ts +1 -1
  91. package/lib/services/agent/tools/searchUserLibrary.tool.d.ts +4 -4
  92. package/lib/services/aiGen/providers/alibaba/alibaba.d.ts +0 -6
  93. package/lib/services/aiGen/providers/alibaba/alibaba.d.ts.map +1 -1
  94. package/lib/services/aiGen/providers/alibaba/alibaba.js +5 -103
  95. package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts.map +1 -1
  96. package/lib/services/aiGen/providers/bytedance/bytedance.service.js +1 -9
  97. package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
  98. package/lib/services/aiGen/providers/google/google.service.js +26 -142
  99. package/lib/services/aiGen/providers/google/googleErrors.d.ts.map +1 -1
  100. package/lib/services/aiGen/providers/google/googleErrors.js +7 -17
  101. package/lib/services/aiGen/providers/kling/kling.service.d.ts.map +1 -1
  102. package/lib/services/aiGen/providers/kling/kling.service.js +22 -3
  103. package/lib/services/aiGen/providers/kling/klingStatus.d.ts +7 -0
  104. package/lib/services/aiGen/providers/kling/klingStatus.d.ts.map +1 -1
  105. package/lib/services/aiGen/providers/kling/klingStatus.js +41 -4
  106. package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
  107. package/lib/services/aiGen/providers/pixverse/pixverse.service.js +25 -0
  108. package/lib/services/aiGen/providers/types.d.ts +20 -0
  109. package/lib/services/aiGen/providers/types.d.ts.map +1 -1
  110. package/lib/services/avatarGen/avatarProviderFactory.d.ts.map +1 -1
  111. package/lib/services/avatarGen/avatarProviderFactory.js +3 -0
  112. package/lib/services/avatarGen/index.d.ts +1 -0
  113. package/lib/services/avatarGen/index.d.ts.map +1 -1
  114. package/lib/services/avatarGen/index.js +1 -0
  115. package/lib/services/avatarGen/providers/synthesiaAvatarProvider.d.ts +53 -0
  116. package/lib/services/avatarGen/providers/synthesiaAvatarProvider.d.ts.map +1 -0
  117. package/lib/services/avatarGen/providers/synthesiaAvatarProvider.js +176 -0
  118. package/lib/services/demo/demoAuthor.d.ts +7 -0
  119. package/lib/services/demo/demoAuthor.d.ts.map +1 -1
  120. package/lib/services/demo/demoAuthor.js +2 -0
  121. package/lib/services/demo/demoAuthorPrompt.d.ts +6 -0
  122. package/lib/services/demo/demoAuthorPrompt.d.ts.map +1 -1
  123. package/lib/services/demo/demoAuthorPrompt.js +24 -2
  124. package/lib/services/demo/demoCaptions.d.ts +69 -0
  125. package/lib/services/demo/demoCaptions.d.ts.map +1 -0
  126. package/lib/services/demo/demoCaptions.js +152 -0
  127. package/lib/services/demo/demoMusic.d.ts +8 -1
  128. package/lib/services/demo/demoMusic.d.ts.map +1 -1
  129. package/lib/services/demo/demoMusic.js +32 -16
  130. package/lib/services/demo/demoScriptPrompt.d.ts +2 -1
  131. package/lib/services/demo/demoScriptPrompt.d.ts.map +1 -1
  132. package/lib/services/demo/demoScriptPrompt.js +29 -2
  133. package/lib/services/demo/demoStockAssets.d.ts +29 -0
  134. package/lib/services/demo/demoStockAssets.d.ts.map +1 -0
  135. package/lib/services/demo/demoStockAssets.js +107 -0
  136. package/lib/services/demo/index.d.ts +3 -0
  137. package/lib/services/demo/index.d.ts.map +1 -1
  138. package/lib/services/demo/index.js +3 -0
  139. package/lib/services/demo/productConnection.service.d.ts +78 -0
  140. package/lib/services/demo/productConnection.service.d.ts.map +1 -0
  141. package/lib/services/demo/productConnection.service.js +125 -0
  142. package/lib/services/editor/planToProject.d.ts.map +1 -1
  143. package/lib/services/editor/planToProject.helpers.d.ts +1 -0
  144. package/lib/services/editor/planToProject.helpers.d.ts.map +1 -1
  145. package/lib/services/editor/planToProject.helpers.js +3 -0
  146. package/lib/services/editor/planToProject.js +39 -0
  147. package/lib/services/editor/projectToDesign.js +13 -1
  148. package/lib/services/firestore.service.d.ts +14 -0
  149. package/lib/services/firestore.service.d.ts.map +1 -1
  150. package/lib/services/firestore.service.js +16 -0
  151. package/lib/services/translation/index.d.ts +1 -0
  152. package/lib/services/translation/index.d.ts.map +1 -1
  153. package/lib/services/translation/index.js +8 -1
  154. package/lib/services/translation/outputLanguage.d.ts +75 -0
  155. package/lib/services/translation/outputLanguage.d.ts.map +1 -0
  156. package/lib/services/translation/outputLanguage.js +190 -0
  157. package/lib/services/translation/translation.service.d.ts +13 -2
  158. package/lib/services/translation/translation.service.d.ts.map +1 -1
  159. package/lib/services/translation/translation.service.js +73 -31
  160. package/lib/services/tts/index.d.ts +1 -0
  161. package/lib/services/tts/index.d.ts.map +1 -1
  162. package/lib/services/tts/index.js +1 -0
  163. package/lib/services/tts/providers/elevenlabs.service.d.ts +7 -0
  164. package/lib/services/tts/providers/elevenlabs.service.d.ts.map +1 -1
  165. package/lib/services/tts/providers/elevenlabs.service.js +69 -13
  166. package/lib/services/tts/types.d.ts +29 -0
  167. package/lib/services/tts/types.d.ts.map +1 -1
  168. package/lib/services/tts/wordTimings.d.ts +27 -0
  169. package/lib/services/tts/wordTimings.d.ts.map +1 -0
  170. package/lib/services/tts/wordTimings.js +57 -0
  171. package/lib/utils/billableDuration.d.ts +11 -0
  172. package/lib/utils/billableDuration.d.ts.map +1 -0
  173. package/lib/utils/billableDuration.js +91 -0
  174. package/lib/utils/index.d.ts +3 -0
  175. package/lib/utils/index.d.ts.map +1 -1
  176. package/lib/utils/index.js +3 -0
  177. package/lib/utils/mp4Duration.d.ts +38 -0
  178. package/lib/utils/mp4Duration.d.ts.map +1 -0
  179. package/lib/utils/mp4Duration.js +164 -0
  180. package/lib/utils/socialEntitlements.d.ts +51 -0
  181. package/lib/utils/socialEntitlements.d.ts.map +1 -0
  182. package/lib/utils/socialEntitlements.js +65 -0
  183. 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
- // Bounded: generateText itself takes no timeout, so race it. The loser is
121
- // abandoned rather than cancelled (there is no AbortSignal on this path), but
122
- // the caller is unblocked — which is the point.
123
- let timer;
124
- const { text: out } = await Promise.race([
125
- service.generateText({
126
- input: [
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
- * Detect the language of `text`. Primary CF; on failure fails OPEN to English
193
- * (matches prior behavior language detection must never block generation).
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
- return res.data;
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
- logger_1.logger.warn("translation: detectLang failed, defaulting to en", {
205
- err: errMsg(err),
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,4 +1,5 @@
1
1
  export * from "./types";
2
+ export * from "./wordTimings";
2
3
  export * from "./providers/elevenlabs.service";
3
4
  export * from "./providers/minimax.service";
4
5
  export * from "./providers/openai.service";
@@ -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,EAAE,sBAAsB,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAiBzF,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;IA+FrD;;;;;OAKG;YACW,gBAAgB;IAsD9B,SAAS,IAAI,eAAe,EAAE;IAI9B,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAGpD"}
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
- outputFormat,
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
- outputFormat,
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(`${this.baseUrl}/text-to-speech/${voiceId}`, body, {
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: mimeType,
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
- return Buffer.from(response.data);
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;CACJ;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;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"}
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
+ }
@@ -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
@@ -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"}
@@ -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"}