vidspotai-shared 1.0.116 → 1.0.117

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.
@@ -1 +1 @@
1
- {"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAqGvD;;;;GAIG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAiDD;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
1
+ {"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAwIvD;;;;GAIG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAuDD;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
@@ -78,9 +78,18 @@ const PRIMARY_MAX_ATTEMPTS = 2;
78
78
  // two layers compounded into an unbounded stall — a Vietnamese request was
79
79
  // measured at 48s in prod (2026-07-26): ~12s burning both primary attempts,
80
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;
81
+ // fallback so the worst case is primary (~12.2s) + fallback (~16s) ≈ 28s and
82
+ // the caller gets a definite answer instead of holding the request open.
83
+ //
84
+ // 2026-09-22: a single un-retried 15s shot then ALSO started losing — both
85
+ // primary AND fallback failed in prod for `id`→`en` (Slack: "translation
86
+ // failed on BOTH primary and LLM fallback"), at ~27.2s total, matching this
87
+ // budget almost exactly. One bad connection with zero recovery chance was
88
+ // enough to take the whole fallback down. Split the same total budget into
89
+ // two shorter attempts so a transient hiccup (dropped connection, slow TLS
90
+ // handshake) gets a second, fresh try instead of being fatal.
91
+ const FALLBACK_ATTEMPT_TIMEOUT_MS = 8000;
92
+ const FALLBACK_MAX_ATTEMPTS = 2;
84
93
  // gpt-4o-mini: cheap (~$0.15/1M in, $0.60/1M out → fractions of a cent per
85
94
  // prompt), reliable, and independent of the Google video quota. Swap here if a
86
95
  // cheaper effective model becomes available.
@@ -104,12 +113,27 @@ async function callPrimaryTranslate(text, from, to, version) {
104
113
  }
105
114
  return translated;
106
115
  }
107
- /**
108
- * FALLBACK: translate via a cheap LLM. Lazy-imports the provider factory so this
109
- * module stays light (utils/helpers re-exports it) and to avoid an import cycle
110
- * with the aiGen layer.
111
- */
112
- async function callLlmTranslate(text, from, to) {
116
+ /** Races `promise` against a timeout; the loser is abandoned (no AbortSignal
117
+ * on this path), but the caller is unblocked — which is the point. */
118
+ async function withTimeout(promise, ms, label) {
119
+ let timer;
120
+ try {
121
+ return await Promise.race([
122
+ promise,
123
+ new Promise((_, reject) => {
124
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
125
+ }),
126
+ ]);
127
+ }
128
+ finally {
129
+ if (timer)
130
+ clearTimeout(timer);
131
+ }
132
+ }
133
+ /** One un-retried LLM translate attempt. Lazy-imports the provider factory so
134
+ * this module stays light (utils/helpers re-exports it) and to avoid an
135
+ * import cycle with the aiGen layer. */
136
+ async function callLlmTranslateOnce(text, from, to) {
113
137
  const { getAiGenProviderService } = await Promise.resolve().then(() => __importStar(require("../aiGen/aiGenFactory.service")));
114
138
  const service = getAiGenProviderService(FALLBACK_MODEL);
115
139
  const target = languageLabel(to);
@@ -117,25 +141,13 @@ async function callLlmTranslate(text, from, to) {
117
141
  `Preserve meaning, tone, names, numbers, hashtags, emojis and line breaks. ` +
118
142
  `Output ONLY the translated text — no quotes, no language labels, no commentary. ` +
119
143
  `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);
144
+ const { text: out } = await service.generateText({
145
+ input: [
146
+ { role: "system", content: system },
147
+ { role: "user", content: text },
148
+ ],
149
+ modelKey: FALLBACK_MODEL,
150
+ options: { temperature: 0 },
139
151
  });
140
152
  const trimmed = (out ?? "").trim();
141
153
  if (!trimmed || trimmed === "No response") {
@@ -143,6 +155,20 @@ async function callLlmTranslate(text, from, to) {
143
155
  }
144
156
  return trimmed;
145
157
  }
158
+ /** FALLBACK: translate via a cheap LLM, with a real retry — see the
159
+ * FALLBACK_ATTEMPT_TIMEOUT_MS comment above for why one shot wasn't enough. */
160
+ async function callLlmTranslate(text, from, to) {
161
+ let lastErr;
162
+ for (let attempt = 1; attempt <= FALLBACK_MAX_ATTEMPTS; attempt++) {
163
+ try {
164
+ return await withTimeout(callLlmTranslateOnce(text, from, to), FALLBACK_ATTEMPT_TIMEOUT_MS, "LLM fallback");
165
+ }
166
+ catch (err) {
167
+ lastErr = err;
168
+ }
169
+ }
170
+ throw lastErr;
171
+ }
146
172
  /**
147
173
  * Translate `text` from → to (default English). Primary CF first (with a couple
148
174
  * of quick retries), then a cheap LLM. Throws Error(TRANSLATION_FAILED) only if
@@ -188,40 +214,23 @@ async function translate(text, from, to = types_1.ELANGUAGE_CODE.en, version = "
188
214
  throw e;
189
215
  }
190
216
  }
191
- /**
192
- * FALLBACK: detect via the same cheap LLM `translate` falls back to. Returns
193
- * the bare ISO 639-1 code. Throws if the reply doesn't look like a code.
194
- */
195
- async function callLlmDetect(text) {
217
+ /** One un-retried LLM detect attempt. */
218
+ async function callLlmDetectOnce(text) {
196
219
  const { getAiGenProviderService } = await Promise.resolve().then(() => __importStar(require("../aiGen/aiGenFactory.service")));
197
220
  const service = getAiGenProviderService(FALLBACK_MODEL);
198
221
  const system = "You are a language identification engine. Reply with ONLY the ISO 639-1 " +
199
222
  "code of the language the user's text is written in — two lowercase " +
200
223
  'letters, nothing else (e.g. "es", "ja", "ar"). Use "zh-cn" or "zh-tw" ' +
201
224
  "for Chinese. No punctuation, no explanation.";
202
- // Bounded the same way as callLlmTranslate (see comment above it): this is
203
- // called synchronously from POST /v1/video/generate before the BullMQ
204
- // enqueue, so an unbounded generateText here stalls the HTTP request all
205
- // the way to the Cloud Run 300s ceiling instead of failing over to the
206
- // "default to en" path below.
207
- let timer;
208
- const { text: out } = await Promise.race([
209
- service.generateText({
210
- // Only the opening slice matters for identification, and capping it
211
- // keeps this cheap when a long-form brief triggers the fallback.
212
- input: [
213
- { role: "system", content: system },
214
- { role: "user", content: text.slice(0, 1000) },
215
- ],
216
- modelKey: FALLBACK_MODEL,
217
- options: { temperature: 0 },
218
- }),
219
- new Promise((_, reject) => {
220
- timer = setTimeout(() => reject(new Error(`LLM detect timed out after ${FALLBACK_TIMEOUT_MS}ms`)), FALLBACK_TIMEOUT_MS);
221
- }),
222
- ]).finally(() => {
223
- if (timer)
224
- clearTimeout(timer);
225
+ const { text: out } = await service.generateText({
226
+ // Only the opening slice matters for identification, and capping it
227
+ // keeps this cheap when a long-form brief triggers the fallback.
228
+ input: [
229
+ { role: "system", content: system },
230
+ { role: "user", content: text.slice(0, 1000) },
231
+ ],
232
+ modelKey: FALLBACK_MODEL,
233
+ options: { temperature: 0 },
225
234
  });
226
235
  const code = (out ?? "").trim().toLowerCase().replace(/[^a-z-]/g, "");
227
236
  if (!/^[a-z]{2}(-[a-z]{2})?$/.test(code)) {
@@ -229,6 +238,27 @@ async function callLlmDetect(text) {
229
238
  }
230
239
  return code;
231
240
  }
241
+ /**
242
+ * FALLBACK: detect via the same cheap LLM `translate` falls back to, with the
243
+ * same retry treatment as callLlmTranslate. Returns the bare ISO 639-1 code.
244
+ *
245
+ * This is called synchronously from POST /v1/video/generate before the
246
+ * BullMQ enqueue, so an unbounded generateText here stalls the HTTP request
247
+ * all the way to the Cloud Run 300s ceiling instead of failing over to the
248
+ * "default to en" path below — hence the same per-attempt bound as translate.
249
+ */
250
+ async function callLlmDetect(text) {
251
+ let lastErr;
252
+ for (let attempt = 1; attempt <= FALLBACK_MAX_ATTEMPTS; attempt++) {
253
+ try {
254
+ return await withTimeout(callLlmDetectOnce(text), FALLBACK_ATTEMPT_TIMEOUT_MS, "LLM detect");
255
+ }
256
+ catch (err) {
257
+ lastErr = err;
258
+ }
259
+ }
260
+ throw lastErr;
261
+ }
232
262
  /**
233
263
  * Detect the language of `text`. Primary CF → cheap LLM → fail OPEN to English.
234
264
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidspotai-shared",
3
- "version": "1.0.116",
3
+ "version": "1.0.117",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "exports": {