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;
|
|
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 (
|
|
82
|
-
// caller gets a definite answer instead of holding the request open.
|
|
83
|
-
|
|
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
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{ role: "system", content: system },
|
|
128
|
-
{ role: "user", content: text },
|
|
129
|
-
],
|
|
130
|
-
modelKey: FALLBACK_MODEL,
|
|
131
|
-
options: { temperature: 0 },
|
|
132
|
-
}),
|
|
133
|
-
new Promise((_, reject) => {
|
|
134
|
-
timer = setTimeout(() => reject(new Error(`LLM fallback timed out after ${FALLBACK_TIMEOUT_MS}ms`)), FALLBACK_TIMEOUT_MS);
|
|
135
|
-
}),
|
|
136
|
-
]).finally(() => {
|
|
137
|
-
if (timer)
|
|
138
|
-
clearTimeout(timer);
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
*
|