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.
- package/lib/globals/aiModels/providers/google.d.ts.map +1 -1
- package/lib/globals/aiModels/providers/google.js +39 -7
- package/lib/schemas/project.schema.d.ts +3 -3
- package/lib/schemas/videoPlan.schema.d.ts +3 -3
- package/lib/services/agent/editClassifier.d.ts +2 -2
- package/lib/services/agent/eval/recorder.d.ts +13 -1
- package/lib/services/agent/eval/recorder.d.ts.map +1 -1
- package/lib/services/agent/eval/recorder.js +59 -0
- package/lib/services/agent/tools/composeScene.tool.d.ts +2 -2
- package/lib/services/agent/tools/estimateCost.tool.d.ts +1 -1
- package/lib/services/agent/tools/planVideo.tool.d.ts +1 -1
- package/lib/services/agent/tools/render.tool.d.ts +1 -1
- package/lib/services/aiGen/helpers.d.ts +8 -0
- package/lib/services/aiGen/helpers.d.ts.map +1 -1
- package/lib/services/aiGen/helpers.js +12 -0
- package/lib/services/aiGen/providers/google/google.service.d.ts +1 -0
- package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/google.service.js +100 -8
- package/lib/services/aiGen/providers/google/googleApiKeys.d.ts +71 -0
- package/lib/services/aiGen/providers/google/googleApiKeys.d.ts.map +1 -0
- package/lib/services/aiGen/providers/google/googleApiKeys.js +137 -0
- package/lib/services/aiGen/providers/google/googleKeyPool.d.ts +52 -0
- package/lib/services/aiGen/providers/google/googleKeyPool.d.ts.map +1 -0
- package/lib/services/aiGen/providers/google/googleKeyPool.js +129 -0
- package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/pixverse/pixverse.service.js +7 -1
- package/lib/services/bullmq.service.d.ts.map +1 -1
- package/lib/services/bullmq.service.js +23 -1
- package/lib/services/index.d.ts +1 -0
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +1 -0
- package/lib/services/rateLimiter/distributedRateLimiter.service.d.ts +60 -5
- package/lib/services/rateLimiter/distributedRateLimiter.service.d.ts.map +1 -1
- package/lib/services/rateLimiter/distributedRateLimiter.service.js +184 -16
- package/lib/services/translation/index.d.ts +2 -0
- package/lib/services/translation/index.d.ts.map +1 -0
- package/lib/services/translation/index.js +9 -0
- package/lib/services/translation/translation.service.d.ts +50 -0
- package/lib/services/translation/translation.service.d.ts.map +1 -0
- package/lib/services/translation/translation.service.js +211 -0
- package/lib/utils/helpers.d.ts +2 -4
- package/lib/utils/helpers.d.ts.map +1 -1
- package/lib/utils/helpers.js +9 -63
- 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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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-
|
|
99
|
-
* Use for short-lived calls that share a
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 @@
|
|
|
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;
|
package/lib/utils/helpers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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":"
|
|
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"}
|