vidspotai-shared 1.0.81 → 1.0.82

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 (44) hide show
  1. package/lib/globals/aiModels/providers/google.d.ts.map +1 -1
  2. package/lib/globals/aiModels/providers/google.js +39 -7
  3. package/lib/schemas/project.schema.d.ts +3 -3
  4. package/lib/schemas/videoPlan.schema.d.ts +3 -3
  5. package/lib/services/agent/editClassifier.d.ts +2 -2
  6. package/lib/services/agent/eval/recorder.d.ts +13 -1
  7. package/lib/services/agent/eval/recorder.d.ts.map +1 -1
  8. package/lib/services/agent/eval/recorder.js +59 -0
  9. package/lib/services/agent/tools/composeScene.tool.d.ts +2 -2
  10. package/lib/services/agent/tools/estimateCost.tool.d.ts +1 -1
  11. package/lib/services/agent/tools/planVideo.tool.d.ts +1 -1
  12. package/lib/services/agent/tools/render.tool.d.ts +1 -1
  13. package/lib/services/aiGen/helpers.d.ts +8 -0
  14. package/lib/services/aiGen/helpers.d.ts.map +1 -1
  15. package/lib/services/aiGen/helpers.js +12 -0
  16. package/lib/services/aiGen/providers/google/google.service.d.ts +1 -0
  17. package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
  18. package/lib/services/aiGen/providers/google/google.service.js +100 -8
  19. package/lib/services/aiGen/providers/google/googleApiKeys.d.ts +71 -0
  20. package/lib/services/aiGen/providers/google/googleApiKeys.d.ts.map +1 -0
  21. package/lib/services/aiGen/providers/google/googleApiKeys.js +137 -0
  22. package/lib/services/aiGen/providers/google/googleKeyPool.d.ts +52 -0
  23. package/lib/services/aiGen/providers/google/googleKeyPool.d.ts.map +1 -0
  24. package/lib/services/aiGen/providers/google/googleKeyPool.js +129 -0
  25. package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
  26. package/lib/services/aiGen/providers/pixverse/pixverse.service.js +7 -1
  27. package/lib/services/bullmq.service.d.ts.map +1 -1
  28. package/lib/services/bullmq.service.js +23 -1
  29. package/lib/services/index.d.ts +1 -0
  30. package/lib/services/index.d.ts.map +1 -1
  31. package/lib/services/index.js +1 -0
  32. package/lib/services/rateLimiter/distributedRateLimiter.service.d.ts +60 -5
  33. package/lib/services/rateLimiter/distributedRateLimiter.service.d.ts.map +1 -1
  34. package/lib/services/rateLimiter/distributedRateLimiter.service.js +184 -16
  35. package/lib/services/translation/index.d.ts +2 -0
  36. package/lib/services/translation/index.d.ts.map +1 -0
  37. package/lib/services/translation/index.js +9 -0
  38. package/lib/services/translation/translation.service.d.ts +50 -0
  39. package/lib/services/translation/translation.service.d.ts.map +1 -0
  40. package/lib/services/translation/translation.service.js +211 -0
  41. package/lib/utils/helpers.d.ts +2 -4
  42. package/lib/utils/helpers.d.ts.map +1 -1
  43. package/lib/utils/helpers.js +9 -63
  44. package/package.json +6 -6
@@ -3,15 +3,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DistributedRateLimiter = void 0;
6
+ exports.DistributedRateLimiter = exports.DAILY_NEAR_RESET_MS = exports.MAX_ACCEPTABLE_WAIT_MS = void 0;
7
7
  exports.getAiGenModelRateLimiter = getAiGenModelRateLimiter;
8
8
  exports.getTtsProviderRateLimiter = getTtsProviderRateLimiter;
9
9
  exports.snapshotAllRateLimiters = snapshotAllRateLimiters;
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
11
  const aiModels_1 = require("../../globals/aiModels");
12
12
  const ttsModels_1 = require("../../globals/ttsModels");
13
+ const googleApiKeys_1 = require("../aiGen/providers/google/googleApiKeys");
14
+ const errors_1 = require("../../utils/errors");
13
15
  const logger_1 = require("../../utils/logger");
14
16
  const redis_service_1 = require("../redis.service");
17
+ /**
18
+ * Capacity-planning thresholds (used by previewCapacity + the bounded gate).
19
+ *
20
+ * MAX_ACCEPTABLE_WAIT_MS — a model whose projected wait for a submit slot
21
+ * exceeds this is considered "not available" by previewCapacity, so the
22
+ * job-start selector spills to a less-loaded model in the same tier chain.
23
+ * DAILY_NEAR_RESET_MS — when a model's per-day cap is exhausted, we only wait
24
+ * it out if the UTC-midnight reset is within this window; otherwise the
25
+ * bounded gate fast-fails (VIDEO_PROVIDER_RATE_LIMITED) so the caller can
26
+ * fall back rather than block for hours.
27
+ */
28
+ exports.MAX_ACCEPTABLE_WAIT_MS = 180000; // 3 min
29
+ exports.DAILY_NEAR_RESET_MS = 120000; // 2 min
15
30
  // Module-level throttle so Redis outages don't fire a Slack error every
16
31
  // request. First failure goes through; subsequent failures within the
17
32
  // window log at warn level (Console + Loki only) until the window resets.
@@ -62,9 +77,14 @@ end
62
77
 
63
78
  redis.call('ZADD', minKey, nowMs, uniqueId)
64
79
  redis.call('PEXPIRE', minKey, 90000)
65
- local newDay = redis.call('INCR', dayKey)
66
- if newDay == 1 then
67
- redis.call('EXPIRE', dayKey, secsTilMidnight)
80
+ -- Only touch the day counter when a day limit applies. With dayLimit <= 0
81
+ -- (skipDay poll path, or a model with no per-day cap) we must NOT increment —
82
+ -- otherwise polls would burn the daily SUBMIT budget they're meant to bypass.
83
+ if dayLimit > 0 then
84
+ local newDay = redis.call('INCR', dayKey)
85
+ if newDay == 1 then
86
+ redis.call('EXPIRE', dayKey, secsTilMidnight)
87
+ end
68
88
  end
69
89
 
70
90
  return { 1, 0, 'ok' }
@@ -83,11 +103,18 @@ class DistributedRateLimiter {
83
103
  * a slot in each. Caller MUST call releaseSlot() when the in-flight task
84
104
  * settles — per-min / per-day auto-expire but the concurrent counter does
85
105
  * not. Mirrors the old AiGenModelRateLimiter API.
106
+ *
107
+ * `opts.maxWaitMs` bounds the rate-budget wait: if satisfying the per-day /
108
+ * per-minute gate would take longer than this, the call throws a
109
+ * `UserFacingError(VIDEO_PROVIDER_RATE_LIMITED)` instead of sleep-looping for
110
+ * hours. This is the backstop that stops jobs from piling up in-process when
111
+ * a daily cap is hit — the submit path passes it; legacy callers that omit
112
+ * it keep the original (unbounded) behavior.
86
113
  */
87
- async waitUntilAvailable() {
114
+ async waitUntilAvailable(opts) {
88
115
  await this.acquireConcurrent();
89
116
  try {
90
- await this.consumeRedisGates();
117
+ await this.consumeRedisGates({ maxWaitMs: opts?.maxWaitMs });
91
118
  }
92
119
  catch (err) {
93
120
  this.releaseConcurrent();
@@ -95,13 +122,99 @@ class DistributedRateLimiter {
95
122
  }
96
123
  }
97
124
  /**
98
- * Bumps per-min + per-day only — does NOT count toward the concurrent cap.
99
- * Use for short-lived calls that share a provider's rate budget but aren't
100
- * "in-flight tasks" (status polls, sync image/TTS gens that complete in the
101
- * same call). No release is needed Redis auto-expires the counters.
125
+ * Bumps per-MINUTE only — does NOT count toward the concurrent cap and does
126
+ * NOT consume/await the per-day gate. Use for short-lived calls that share a
127
+ * provider's per-minute budget but must not be blocked by the daily SUBMIT
128
+ * cap: status polls of already-running tasks (failing a poll on daily quota
129
+ * would abandon a video that's already generating), plus sync image/TTS gens
130
+ * (which have no per-day limit configured anyway). No release needed — Redis
131
+ * auto-expires the counters.
102
132
  */
103
133
  async waitUntilAvailableForOneShot() {
104
- await this.consumeRedisGates();
134
+ await this.consumeRedisGates({ skipDay: true });
135
+ }
136
+ /**
137
+ * Read-only capacity estimate — does NOT consume a slot. Reads current
138
+ * per-minute / per-day usage and concurrency pressure from Redis and
139
+ * projects how long a new submission would wait. Fails open (available:true,
140
+ * wait:0) on Redis errors / no client, matching the gate's fail-open policy.
141
+ */
142
+ async previewCapacity(maxAcceptableWaitMs = exports.MAX_ACCEPTABLE_WAIT_MS) {
143
+ const secsUntilDayReset = secsUntilMidnight();
144
+ const base = {
145
+ modelKey: this.modelKey,
146
+ perMinLimit: this.perMinLimit,
147
+ perDayLimit: this.perDayLimit,
148
+ concurrentLimit: this.concurrentLimit,
149
+ concurrentActive: this.activeRequests,
150
+ concurrentQueueDepth: this.waitQueue.length,
151
+ secsUntilDayReset,
152
+ };
153
+ const failOpen = {
154
+ ...base,
155
+ perMinUsed: 0,
156
+ perDayUsed: 0,
157
+ projectedWaitMs: 0,
158
+ available: true,
159
+ dailyExhausted: false,
160
+ };
161
+ const client = redis_service_1.redis.getClient();
162
+ if (!client)
163
+ return failOpen;
164
+ const minKey = `ratelimit:${this.modelKey}:min`;
165
+ const dayKey = `ratelimit:${this.modelKey}:day:${utcDateKey()}`;
166
+ let perMinUsed = 0;
167
+ let perDayUsed = 0;
168
+ try {
169
+ const now = Date.now();
170
+ const [minCount, dayRaw] = await Promise.all([
171
+ client.zcount(minKey, now - 60000, "+inf"),
172
+ client.get(dayKey),
173
+ ]);
174
+ perMinUsed = minCount;
175
+ perDayUsed = dayRaw ? Number(dayRaw) : 0;
176
+ }
177
+ catch (err) {
178
+ logger_1.logger.warn("distributedRateLimiter: previewCapacity read failed, assuming available", {
179
+ modelKey: this.modelKey,
180
+ err: err instanceof Error ? err.message : String(err),
181
+ });
182
+ return failOpen;
183
+ }
184
+ const dailyExhausted = this.perDayLimit > 0 && perDayUsed >= this.perDayLimit;
185
+ // Project the wait for a new submit slot.
186
+ let projectedWaitMs = 0;
187
+ if (dailyExhausted) {
188
+ projectedWaitMs = secsUntilDayReset * 1000;
189
+ }
190
+ else if (this.perMinLimit > 0) {
191
+ // Requests already queued on the concurrency gate are "ahead" of a new
192
+ // arrival competing for per-minute slots. Estimate how many 60s windows
193
+ // it takes to drain them past the per-minute throughput.
194
+ const pendingAhead = this.waitQueue.length;
195
+ const freeThisWindow = Math.max(0, this.perMinLimit - perMinUsed);
196
+ if (pendingAhead < freeThisWindow) {
197
+ projectedWaitMs = 0;
198
+ }
199
+ else {
200
+ const slotsNeeded = pendingAhead - freeThisWindow + 1;
201
+ const windowsNeeded = Math.ceil(slotsNeeded / this.perMinLimit);
202
+ projectedWaitMs = windowsNeeded * 60000;
203
+ }
204
+ }
205
+ // No per-minute gate (perMinLimit === 0): wait is concurrency-only; those
206
+ // models churn fast, so we treat backlog as drainable (projectedWaitMs 0).
207
+ const available = dailyExhausted
208
+ ? secsUntilDayReset * 1000 <= exports.DAILY_NEAR_RESET_MS
209
+ : projectedWaitMs <= maxAcceptableWaitMs;
210
+ return {
211
+ ...base,
212
+ perMinUsed,
213
+ perDayUsed,
214
+ projectedWaitMs,
215
+ available,
216
+ dailyExhausted,
217
+ };
105
218
  }
106
219
  /** Free a concurrent slot acquired via waitUntilAvailable(). */
107
220
  releaseSlot() {
@@ -165,8 +278,11 @@ class DistributedRateLimiter {
165
278
  if (next)
166
279
  next();
167
280
  }
168
- async consumeRedisGates() {
169
- if (!this.perMinLimit && !this.perDayLimit)
281
+ async consumeRedisGates(opts) {
282
+ // skipDay ignores the per-day cap entirely (poll path) — see
283
+ // waitUntilAvailableForOneShot.
284
+ const effectiveDayLimit = opts?.skipDay ? 0 : this.perDayLimit;
285
+ if (!this.perMinLimit && !effectiveDayLimit)
170
286
  return;
171
287
  const client = redis_service_1.redis.getClient();
172
288
  if (!client) {
@@ -179,6 +295,8 @@ class DistributedRateLimiter {
179
295
  const minKey = `ratelimit:${this.modelKey}:min`;
180
296
  const secsTilMidnight = secsUntilMidnight();
181
297
  const uniqueId = `${Date.now()}-${crypto_1.default.randomBytes(4).toString("hex")}`;
298
+ const startedAt = Date.now();
299
+ const maxWaitMs = opts?.maxWaitMs;
182
300
  while (true) {
183
301
  const nowMs = Date.now();
184
302
  // Day key resolved per-iteration so a retry across midnight UTC
@@ -186,7 +304,7 @@ class DistributedRateLimiter {
186
304
  const dayKey = `ratelimit:${this.modelKey}:day:${utcDateKey()}`;
187
305
  let result;
188
306
  try {
189
- result = (await client.eval(CONSUME_SCRIPT, 2, minKey, dayKey, String(nowMs), String(this.perMinLimit), String(this.perDayLimit), String(secsTilMidnight), uniqueId));
307
+ result = (await client.eval(CONSUME_SCRIPT, 2, minKey, dayKey, String(nowMs), String(this.perMinLimit), String(effectiveDayLimit), String(secsTilMidnight), uniqueId));
190
308
  }
191
309
  catch (err) {
192
310
  // Redis blip — fail open rather than blocking the pipeline. Throttle
@@ -210,6 +328,43 @@ class DistributedRateLimiter {
210
328
  }
211
329
  if (result[0] === 1)
212
330
  return;
331
+ // Bounded-wait backstop. Without it, a per-day-exhausted gate sleep-loops
332
+ // (60s cap per iteration) until UTC midnight — jobs pile up in-process,
333
+ // hold a worker + concurrency slot, and block graceful shutdown. When the
334
+ // caller passes maxWaitMs, fast-fail instead so the scene can fall back to
335
+ // another model (or surface a clean rate-limit error).
336
+ if (maxWaitMs != null) {
337
+ const reason = result[2];
338
+ // A 'day' block's TRUE wait is until midnight (result[1] is 60s-capped
339
+ // for sleep granularity, not the real ETA) — decide against the real ETA.
340
+ const trueWaitMs = reason === "day" ? secsTilMidnight * 1000 : Date.now() - startedAt + result[1];
341
+ if (trueWaitMs > maxWaitMs) {
342
+ // This is the terminal case: no fallback model had headroom either, so
343
+ // the scene fails with a clean rate-limit error. logger.error → Slack
344
+ // (error-only transport). Model + provider + cause live in the MESSAGE
345
+ // because the Slack dedup key is the message's first 80 chars — so each
346
+ // model fails its own alert (deduped to ~1/5min) and `day` vs `min`
347
+ // causes don't collapse into each other. This + the spill alert are
348
+ // the only signals Ammar gets that a provider is chronically over
349
+ // quota and needs more capacity. Provider-agnostic (every model + TTS).
350
+ const provider = this.modelKey.startsWith("tts:")
351
+ ? this.modelKey.slice(4)
352
+ : this.modelKey.split("-")[0];
353
+ logger_1.logger.error(`distributedRateLimiter: ${this.modelKey} hard rate-limit ${reason === "day" ? "daily-cap" : "throughput"} fail (provider ${provider})`, {
354
+ modelKey: this.modelKey,
355
+ provider,
356
+ cause: reason,
357
+ trueWaitMs,
358
+ maxWaitMs,
359
+ perMinLimit: this.perMinLimit,
360
+ perDayLimit: this.perDayLimit,
361
+ secsUntilDayReset: secsTilMidnight,
362
+ });
363
+ throw new errors_1.UserFacingError(reason === "day"
364
+ ? "This model has reached its daily generation limit."
365
+ : "This model is at capacity right now. Please try again shortly.", errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_RATE_LIMITED);
366
+ }
367
+ }
213
368
  const waitMs = Math.max(50, Math.min(1000, result[1]));
214
369
  await sleep(waitMs);
215
370
  }
@@ -237,10 +392,23 @@ function getAiGenModelRateLimiter(modelKey) {
237
392
  if (!config) {
238
393
  throw new Error(`No model config for model key: ${modelKey}`);
239
394
  }
395
+ // Google model configs hold the SINGLE-KEY Tier-2 baseline. With a
396
+ // multi-key pool (separate GCP projects/billing accounts, see
397
+ // googleKeyPool), the real aggregate budget is the SUM of each key's
398
+ // tier-scaled budget — so scale the model-level gate up to match, else it
399
+ // would throttle at one key's worth while the pool has more headroom.
400
+ // Single/legacy key → factor 1 (unchanged).
401
+ const isGoogle = modelKey.startsWith("google-");
402
+ const rpmFactor = isGoogle ? (0, googleApiKeys_1.googleAggregateFactor)("rpm") : 1;
403
+ const rpdFactor = isGoogle ? (0, googleApiKeys_1.googleAggregateFactor)("rpd") : 1;
240
404
  aiModelLimiters[modelKey] = new DistributedRateLimiter(modelKey, {
241
405
  concurrentRequests: config.concurrentRequests,
242
- requestPerMin: config.requestPerMin,
243
- requestPerDay: config.requestPerDay,
406
+ requestPerMin: config.requestPerMin
407
+ ? Math.round(config.requestPerMin * rpmFactor)
408
+ : config.requestPerMin,
409
+ requestPerDay: config.requestPerDay
410
+ ? Math.round(config.requestPerDay * rpdFactor)
411
+ : config.requestPerDay,
244
412
  });
245
413
  }
246
414
  return aiModelLimiters[modelKey];
@@ -0,0 +1,2 @@
1
+ export { translationService } from "./translation.service";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/translation/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.translationService = void 0;
4
+ // Only the new API is exported from the services barrel. The legacy aliases
5
+ // (getTranslationWrapper / detectLang / TRANSLATION_FAILED) are re-exported via
6
+ // utils/helpers instead, so each name reaches the package root through exactly
7
+ // one star-export path (no ambiguous duplicate re-export).
8
+ var translation_service_1 = require("./translation.service");
9
+ Object.defineProperty(exports, "translationService", { enumerable: true, get: function () { return translation_service_1.translationService; } });
@@ -0,0 +1,50 @@
1
+ import { ELANGUAGE_CODE, IDetectLang } from "../../globals/types";
2
+ /**
3
+ * Translation service — single abstraction over language detection + translation.
4
+ *
5
+ * WHY THIS EXISTS
6
+ * ---------------
7
+ * Translation used to be a bare axios call to a custom Cloud Function
8
+ * (`translation-service-3931b`). That endpoint times out constantly
9
+ * (`timeout of 8000ms exceeded`, ~24s of retries), and every exhaustion threw
10
+ * `TRANSLATION_FAILED` — which broke /v1/video and /v1/text prompt generation
11
+ * AND flooded Slack with `error`-level alerts.
12
+ *
13
+ * Now there are TWO layers behind one interface:
14
+ * 1. PRIMARY — the existing Cloud Function (free; just flaky).
15
+ * 2. FALLBACK — a cheap, reliable LLM (gpt-4o-mini) via our own provider
16
+ * factory. It is NOT on the Google quota that the video pipeline competes
17
+ * for, costs a fraction of a cent per short prompt, and is a single API we
18
+ * already operate.
19
+ *
20
+ * A primary timeout that the LLM recovers from logs at WARN (Console + Loki, no
21
+ * Slack) — so the common flaky-primary case no longer floods the channel. Only
22
+ * when BOTH layers fail do we log ERROR (→ Slack) and throw, so the alert that
23
+ * survives is a real, rare outage rather than noise.
24
+ *
25
+ * Callers should use `translationService.translate()` / `.detectLanguage()`.
26
+ * The legacy `getTranslationWrapper` / `detectLang` names are kept as thin
27
+ * aliases (re-exported from utils/helpers) so existing callsites keep working.
28
+ */
29
+ export declare const TRANSLATION_FAILED = "TRANSLATION_FAILED";
30
+ /**
31
+ * Translate `text` from → to (default English). Primary CF first (with a couple
32
+ * of quick retries), then a cheap LLM. Throws Error(TRANSLATION_FAILED) only if
33
+ * BOTH layers fail; `.cause` carries the last error.
34
+ */
35
+ declare function translate(text: string, from: ELANGUAGE_CODE, to?: ELANGUAGE_CODE, version?: "v1" | "v2"): Promise<string>;
36
+ /**
37
+ * Detect the language of `text`. Primary CF; on failure fails OPEN to English
38
+ * (matches prior behavior — language detection must never block generation).
39
+ */
40
+ declare function detectLanguage(text: string): Promise<IDetectLang>;
41
+ export declare const translationService: {
42
+ translate: typeof translate;
43
+ detectLanguage: typeof detectLanguage;
44
+ };
45
+ /** @deprecated use `translationService.translate` */
46
+ export declare const getTranslationWrapper: typeof translate;
47
+ /** @deprecated use `translationService.detectLanguage` */
48
+ export declare const detectLang: typeof detectLanguage;
49
+ export {};
50
+ //# sourceMappingURL=translation.service.d.ts.map
@@ -0,0 +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;AA+EvD;;;;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;AAED;;;GAGG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwBhE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.detectLang = exports.getTranslationWrapper = exports.translationService = exports.TRANSLATION_FAILED = void 0;
40
+ const axios_1 = __importDefault(require("axios"));
41
+ const enums_1 = require("../../globals/aiModels/enums");
42
+ const types_1 = require("../../globals/types");
43
+ const logger_1 = require("../../utils/logger");
44
+ /**
45
+ * Translation service — single abstraction over language detection + translation.
46
+ *
47
+ * WHY THIS EXISTS
48
+ * ---------------
49
+ * Translation used to be a bare axios call to a custom Cloud Function
50
+ * (`translation-service-3931b`). That endpoint times out constantly
51
+ * (`timeout of 8000ms exceeded`, ~24s of retries), and every exhaustion threw
52
+ * `TRANSLATION_FAILED` — which broke /v1/video and /v1/text prompt generation
53
+ * AND flooded Slack with `error`-level alerts.
54
+ *
55
+ * Now there are TWO layers behind one interface:
56
+ * 1. PRIMARY — the existing Cloud Function (free; just flaky).
57
+ * 2. FALLBACK — a cheap, reliable LLM (gpt-4o-mini) via our own provider
58
+ * factory. It is NOT on the Google quota that the video pipeline competes
59
+ * for, costs a fraction of a cent per short prompt, and is a single API we
60
+ * already operate.
61
+ *
62
+ * A primary timeout that the LLM recovers from logs at WARN (Console + Loki, no
63
+ * Slack) — so the common flaky-primary case no longer floods the channel. Only
64
+ * when BOTH layers fail do we log ERROR (→ Slack) and throw, so the alert that
65
+ * survives is a real, rare outage rather than noise.
66
+ *
67
+ * Callers should use `translationService.translate()` / `.detectLanguage()`.
68
+ * The legacy `getTranslationWrapper` / `detectLang` names are kept as thin
69
+ * aliases (re-exported from utils/helpers) so existing callsites keep working.
70
+ */
71
+ exports.TRANSLATION_FAILED = "TRANSLATION_FAILED";
72
+ const PRIMARY_BASE = "https://us-central1-translation-service-3931b.cloudfunctions.net";
73
+ // Shorter than the old 8s so we fail over to the LLM faster (the old 3×8s =
74
+ // 24s user-facing stall was itself part of the problem).
75
+ const PRIMARY_TIMEOUT_MS = 6000;
76
+ const PRIMARY_MAX_ATTEMPTS = 2;
77
+ // gpt-4o-mini: cheap (~$0.15/1M in, $0.60/1M out → fractions of a cent per
78
+ // prompt), reliable, and independent of the Google video quota. Swap here if a
79
+ // cheaper effective model becomes available.
80
+ const FALLBACK_MODEL = enums_1.ETextGenModels.OPENAI_GPT_4O_MINI;
81
+ /** Human-readable target name for the LLM prompt; falls back to the raw code. */
82
+ const LANGUAGE_NAMES = {
83
+ en: "English",
84
+ };
85
+ function languageLabel(code) {
86
+ return LANGUAGE_NAMES[code] ?? `the language with ISO code "${code}"`;
87
+ }
88
+ /** PRIMARY: one POST to the custom translation Cloud Function. Throws on error. */
89
+ async function callPrimaryTranslate(text, from, to, version) {
90
+ const res = await axios_1.default.post(`${PRIMARY_BASE}/translate`, { text, from, to, version }, {
91
+ headers: { "internal-key": process.env.TRANSLATION_SERVICE_KEY || "" },
92
+ timeout: PRIMARY_TIMEOUT_MS,
93
+ });
94
+ const translated = res.data?.translated;
95
+ if (typeof translated !== "string" || !translated.length) {
96
+ throw new Error("primary translation returned empty payload");
97
+ }
98
+ return translated;
99
+ }
100
+ /**
101
+ * FALLBACK: translate via a cheap LLM. Lazy-imports the provider factory so this
102
+ * module stays light (utils/helpers re-exports it) and to avoid an import cycle
103
+ * with the aiGen layer.
104
+ */
105
+ async function callLlmTranslate(text, from, to) {
106
+ const { getAiGenProviderService } = await Promise.resolve().then(() => __importStar(require("../aiGen/aiGenFactory.service")));
107
+ const service = getAiGenProviderService(FALLBACK_MODEL);
108
+ const target = languageLabel(to);
109
+ const system = `You are a professional translation engine. Translate the user's text into ${target}. ` +
110
+ `Preserve meaning, tone, names, numbers, hashtags, emojis and line breaks. ` +
111
+ `Output ONLY the translated text — no quotes, no language labels, no commentary. ` +
112
+ `If the text is already in ${target}, return it unchanged.`;
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 },
120
+ });
121
+ const trimmed = (out ?? "").trim();
122
+ if (!trimmed || trimmed === "No response") {
123
+ throw new Error("LLM fallback returned empty translation");
124
+ }
125
+ return trimmed;
126
+ }
127
+ /**
128
+ * Translate `text` from → to (default English). Primary CF first (with a couple
129
+ * of quick retries), then a cheap LLM. Throws Error(TRANSLATION_FAILED) only if
130
+ * BOTH layers fail; `.cause` carries the last error.
131
+ */
132
+ async function translate(text, from, to = types_1.ELANGUAGE_CODE.en, version = "v2") {
133
+ if (!text || !text.trim())
134
+ return text;
135
+ // ── Layer 1: primary Cloud Function ──────────────────────────────────────
136
+ let primaryErr;
137
+ for (let attempt = 1; attempt <= PRIMARY_MAX_ATTEMPTS; attempt++) {
138
+ try {
139
+ return await callPrimaryTranslate(text, from, to, version);
140
+ }
141
+ catch (err) {
142
+ primaryErr = err;
143
+ if (attempt < PRIMARY_MAX_ATTEMPTS) {
144
+ await new Promise((r) => setTimeout(r, 200 * attempt));
145
+ }
146
+ }
147
+ }
148
+ // ── Layer 2: LLM fallback (the common recovery path — WARN, not Slack) ────
149
+ logger_1.logger.warn("translation: primary failed, falling back to LLM", {
150
+ sourceLang: from,
151
+ targetLang: to,
152
+ primaryAttempts: PRIMARY_MAX_ATTEMPTS,
153
+ fallbackModel: FALLBACK_MODEL,
154
+ err: errMsg(primaryErr),
155
+ });
156
+ try {
157
+ return await callLlmTranslate(text, from, to);
158
+ }
159
+ catch (fallbackErr) {
160
+ // Both layers down — this is the only case worth a Slack alert.
161
+ logger_1.logger.error("translation failed on BOTH primary and LLM fallback", {
162
+ sourceLang: from,
163
+ targetLang: to,
164
+ primaryErr: errMsg(primaryErr),
165
+ fallbackErr: errMsg(fallbackErr),
166
+ });
167
+ const e = new Error(exports.TRANSLATION_FAILED);
168
+ e.cause = fallbackErr;
169
+ throw e;
170
+ }
171
+ }
172
+ /**
173
+ * Detect the language of `text`. Primary CF; on failure fails OPEN to English
174
+ * (matches prior behavior — language detection must never block generation).
175
+ */
176
+ async function detectLanguage(text) {
177
+ try {
178
+ const res = await axios_1.default.post(`${PRIMARY_BASE}/detectLang`, { text }, {
179
+ headers: { "internal-key": process.env.TRANSLATION_SERVICE_KEY || "" },
180
+ timeout: PRIMARY_TIMEOUT_MS,
181
+ });
182
+ return res.data;
183
+ }
184
+ catch (err) {
185
+ logger_1.logger.warn("translation: detectLang failed, defaulting to en", {
186
+ err: errMsg(err),
187
+ });
188
+ return {
189
+ reliable: false,
190
+ textBytes: 0,
191
+ languages: [
192
+ { name: "English", code: types_1.ELANGUAGE_CODE.en, percent: 100, score: 0 },
193
+ ],
194
+ chunks: [],
195
+ };
196
+ }
197
+ }
198
+ function errMsg(err) {
199
+ if (err instanceof Error)
200
+ return err.stack ?? err.message;
201
+ return String(err);
202
+ }
203
+ exports.translationService = {
204
+ translate,
205
+ detectLanguage,
206
+ };
207
+ // ── Legacy aliases (kept so existing callsites need no change) ──────────────
208
+ /** @deprecated use `translationService.translate` */
209
+ exports.getTranslationWrapper = translate;
210
+ /** @deprecated use `translationService.detectLanguage` */
211
+ exports.detectLang = detectLanguage;
@@ -1,4 +1,4 @@
1
- import { ELANGUAGE_CODE, ERENEWAL_FREQUENCY, ESUBSCRIPTION_PLANS, EVideoDurationType, IDetectLang } from "../globals/types";
1
+ import { ERENEWAL_FREQUENCY, ESUBSCRIPTION_PLANS, EVideoDurationType } from "../globals/types";
2
2
  import z from "zod";
3
3
  import { ETextGenModels } from "../globals";
4
4
  export declare const getPlanTypeById: (priceId: string) => ESUBSCRIPTION_PLANS | null;
@@ -32,9 +32,7 @@ export declare const assertCostFound: (modelKey: string, cost: number | undefine
32
32
  */
33
33
  export declare const MULTI_CLIP_DISCOUNT_FACTORS: Partial<Record<EVideoDurationType, number>>;
34
34
  export declare function getMultiClipDiscountFactor(durationType: EVideoDurationType): number;
35
- export declare const TRANSLATION_FAILED = "TRANSLATION_FAILED";
36
- export declare const getTranslationWrapper: (text: string, from: ELANGUAGE_CODE, to?: ELANGUAGE_CODE, version?: "v1" | "v2") => Promise<string>;
37
- export declare const detectLang: (text: string) => Promise<IDetectLang>;
35
+ export { TRANSLATION_FAILED, getTranslationWrapper, detectLang, } from "../services/translation/translation.service";
38
36
  export declare function waitForFile(path: string, timeout?: number, interval?: number): Promise<boolean>;
39
37
  export declare function generateAndValidate<T extends z.ZodSchema<any>>(args: {
40
38
  userPrompt: string;
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAElB,WAAW,EACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,CAAC,MAAM,KAAK,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG5C,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,KACd,mBAAmB,GAAG,IASxB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,KAAG,kBAAkB,GAAG,IASlE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,MAAM,mBAAmB,EACzB,OAAM,kBAA+C,WAgBtD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,oBAAiB,KAAG,MAQpE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAU,MAAM,EAChB,MAAM,MAAM,GAAG,SAAS,EACxB,UAAU,MAAM,KACf,MASF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,EAAE,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAInF,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,kBAAkB,GAAG,MAAM,CAEnF;AAED,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,MAAM,cAAc,EACpB,KAAI,cAAkC,EACtC,UAAS,IAAI,GAAG,IAAW,KAC1B,OAAO,CAAC,MAAM,CAwChB,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,WAAW,CAuBlE,CAAC;AAEF,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,SAAQ,EACf,QAAQ,SAAM,oBAQf;AAGD,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,EAAE,CAAC,CAAC;IACb,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAGxB,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AA0DrB,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAatE"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/utils/helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAEnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,CAAC,MAAM,KAAK,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG5C,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,KACd,mBAAmB,GAAG,IASxB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,KAAG,kBAAkB,GAAG,IASlE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,MAAM,mBAAmB,EACzB,OAAM,kBAA+C,WAgBtD,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,oBAAiB,KAAG,MAQpE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAU,MAAM,EAChB,MAAM,MAAM,GAAG,SAAS,EACxB,UAAU,MAAM,KACf,MASF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,EAAE,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAInF,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,kBAAkB,GAAG,MAAM,CAEnF;AAOD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,GACX,MAAM,6CAA6C,CAAC;AAErD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,SAAQ,EACf,QAAQ,SAAM,oBAQf;AAGD,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,EAAE,CAAC,CAAC;IACb,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAGxB,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AA0DrB,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAatE"}