vidspotai-shared 1.0.82-dev.0 → 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 +33 -10
  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 +62 -7
  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 +1 -1
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * Pure (SDK-free, Redis-free) helpers for the Google AI Studio API key pool.
4
+ *
5
+ * ── DECISION RECORD (2026-06-15) ────────────────────────────────────────────
6
+ * Problem: Veo on a single Gemini Developer API key is quota-starved — Tier 2
7
+ * is ~4 req/min and ~50/day per Veo model — which serialized bursts of jobs
8
+ * into a multi-hour queue (~370 min observed avg latency).
9
+ *
10
+ * Chosen fix (today): a multi-key pool over TWO Google AI Studio keys that live
11
+ * in SEPARATE GCP projects / billing accounts, so their quotas are independent
12
+ * and ADD UP. Keys are used in PRIORITY order:
13
+ * - key[0] = the NEW key (vidspotai project), currently **Tier 1** (2/min,
14
+ * 10/day per Veo model). Used FIRST, deliberately, to drive usage and
15
+ * promote its billing account up the tier ladder.
16
+ * - key[1] = the CURRENT key, **Tier 2** (4/min, 50/day). Used once key[0]
17
+ * is out of per-minute / per-day budget.
18
+ * Aggregate Veo budget = T1 + T2 = 6/min, 60/day. When BOTH are exhausted, the
19
+ * job-start capacity selector (videoJobProcessor) spills to another provider.
20
+ *
21
+ * When key[1]'s account is billed it moves to Tier 3; bump its tier in
22
+ * GOOGLE_API_KEY_TIERS then (no code change — the ladder below handles it).
23
+ *
24
+ * Vertex AI (DEFERRED, on record for the future): Veo is also available via
25
+ * Vertex, where quota is **per-project** and the current billing account is
26
+ * Tier-2-per-project — so we could create multiple projects under one account
27
+ * to scale further. We are NOT doing Vertex now: the two AI-Studio keys across
28
+ * two billing accounts already cover our needed headroom, and the Vertex path
29
+ * needs a different output flow (GCS `gs://` URIs rather than the Files API)
30
+ * that we'd rather build + test deliberately when the extra capacity is needed.
31
+ *
32
+ * ── CONFIG ──────────────────────────────────────────────────────────────────
33
+ * GOOGLE_API_KEYS — comma-separated keys in PRIORITY order. Falls back
34
+ * to the single legacy GOOGLE_API_KEY when unset.
35
+ * GOOGLE_API_KEY_TIERS — comma-separated tier numbers (1/2/3) aligned to
36
+ * GOOGLE_API_KEYS. Missing/extra entries default to 2.
37
+ * GOOGLE_API_KEY — legacy single key; also the client used to poll
38
+ * tasks submitted before the pool existed (un-tagged).
39
+ *
40
+ * Per-key budget is derived from the model config's Tier-2 baseline
41
+ * (requestPerMin / requestPerDay) scaled by each key's tier (see TIER_FACTORS);
42
+ * the pool gives us the SUM across keys.
43
+ */
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.parseGoogleApiKeys = parseGoogleApiKeys;
46
+ exports.parseGoogleApiKeyTiers = parseGoogleApiKeyTiers;
47
+ exports.googleApiKeyCount = googleApiKeyCount;
48
+ exports.scaleLimitForTier = scaleLimitForTier;
49
+ exports.googleAggregateFactor = googleAggregateFactor;
50
+ exports.googleKeyId = googleKeyId;
51
+ exports.encodeVeoTask = encodeVeoTask;
52
+ exports.decodeVeoTask = decodeVeoTask;
53
+ /**
54
+ * Per-Veo-model rate ladder, expressed as a factor of the Tier-2 baseline that
55
+ * the model configs encode. Verified against Google's Veo quota
56
+ * (predict_long_running_requests_per_model): RPM 2/4/10 and RPD 10/50/500 for
57
+ * Tier 1/2/3. Only Veo configures per-key min/day caps today, so these factors
58
+ * only materially affect Veo; models with no caps (factor × 0 = 0) are
59
+ * unaffected.
60
+ */
61
+ const TIER_FACTORS = {
62
+ 1: { rpm: 2 / 4, rpd: 10 / 50 }, // 0.5×, 0.2×
63
+ 2: { rpm: 1, rpd: 1 },
64
+ 3: { rpm: 10 / 4, rpd: 500 / 50 }, // 2.5×, 10×
65
+ };
66
+ function tierFactor(tier) {
67
+ return TIER_FACTORS[tier] ?? TIER_FACTORS[2];
68
+ }
69
+ /** Parse the configured keys in priority order, de-duped, empties removed. */
70
+ function parseGoogleApiKeys() {
71
+ const multi = (process.env.GOOGLE_API_KEYS ?? "").trim();
72
+ const raw = multi || (process.env.GOOGLE_API_KEY ?? "").trim();
73
+ const seen = new Set();
74
+ const keys = [];
75
+ for (const part of raw.split(",")) {
76
+ const k = part.trim();
77
+ if (k && !seen.has(k)) {
78
+ seen.add(k);
79
+ keys.push(k);
80
+ }
81
+ }
82
+ return keys;
83
+ }
84
+ /** Tier per key (aligned to parseGoogleApiKeys order); defaults to 2. */
85
+ function parseGoogleApiKeyTiers() {
86
+ const keys = parseGoogleApiKeys();
87
+ const raw = (process.env.GOOGLE_API_KEY_TIERS ?? "").trim();
88
+ const parsed = raw
89
+ ? raw.split(",").map((s) => Number(s.trim()))
90
+ : [];
91
+ return keys.map((_, i) => {
92
+ const t = parsed[i];
93
+ return t === 1 || t === 2 || t === 3 ? t : 2;
94
+ });
95
+ }
96
+ /** Number of configured Google keys (>= 1 so callers can multiply safely). */
97
+ function googleApiKeyCount() {
98
+ return Math.max(1, parseGoogleApiKeys().length);
99
+ }
100
+ /** Per-key rate cap for a given Tier-2 baseline and tier. */
101
+ function scaleLimitForTier(baseline, tier, kind) {
102
+ if (!baseline)
103
+ return 0;
104
+ return Math.max(1, Math.round(baseline * tierFactor(tier)[kind]));
105
+ }
106
+ /**
107
+ * Aggregate multiplier across all configured keys, for scaling a Google
108
+ * model's Tier-2 baseline into the pool-wide budget. Returns 1 for a single
109
+ * default key (legacy behavior unchanged).
110
+ */
111
+ function googleAggregateFactor(kind) {
112
+ const tiers = parseGoogleApiKeyTiers();
113
+ if (tiers.length <= 1)
114
+ return 1; // legacy single key → baseline unchanged
115
+ return tiers.reduce((sum, t) => sum + tierFactor(t)[kind], 0);
116
+ }
117
+ /** Stable id for the key at a given priority index. */
118
+ function googleKeyId(index) {
119
+ return `k${index}`;
120
+ }
121
+ const TASK_TAG_RE = /^gk:([a-z0-9]+)::([\s\S]+)$/;
122
+ /**
123
+ * Tag a Veo operation name with the id of the key that created it, so polling
124
+ * + download can re-select the same project-scoped client. With a single key
125
+ * we return the bare operation name (no tag) — byte-for-byte the legacy format,
126
+ * so nothing changes until a real pool is configured.
127
+ */
128
+ function encodeVeoTask(keyId, operationName, poolSize) {
129
+ return poolSize > 1 ? `gk:${keyId}::${operationName}` : operationName;
130
+ }
131
+ /** Reverse of encodeVeoTask. `keyId` is undefined for legacy/un-tagged tasks. */
132
+ function decodeVeoTask(task) {
133
+ const m = TASK_TAG_RE.exec(task);
134
+ if (m)
135
+ return { keyId: m[1], operationName: m[2] };
136
+ return { operationName: task };
137
+ }
@@ -0,0 +1,52 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+ import { googleApiKeyCount } from "./googleApiKeys";
3
+ /**
4
+ * Google AI Studio multi-key pool.
5
+ *
6
+ * Each configured key is a separate GCP project / billing account with its own
7
+ * Tier-2 quota, so N keys give us N× the per-model daily + per-minute budget.
8
+ * The model-level rate limiter (DistributedRateLimiter) enforces the AGGREGATE
9
+ * (per-key config × key count — see getAiGenModelRateLimiter); this pool decides
10
+ * WHICH key serves each individual submit, in priority order, so we drain the
11
+ * first key's budget before leaning on the next.
12
+ *
13
+ * Veo operations are project-scoped — a task submitted with key K can only be
14
+ * polled / downloaded with key K — so submit tags the task with its key id
15
+ * (encodeVeoTask) and the poll path re-selects the client via clientById().
16
+ *
17
+ * Single-key / unconfigured pool: behaves exactly like the legacy single
18
+ * `GoogleGenAI({ apiKey })` client (no tagging, no Redis routing).
19
+ */
20
+ interface KeyEntry {
21
+ id: string;
22
+ apiKey: string;
23
+ client: GoogleGenAI;
24
+ /** Gemini API tier (1/2/3) of this key's billing account. */
25
+ tier: number;
26
+ }
27
+ declare class GoogleKeyPool {
28
+ private readonly entries;
29
+ constructor();
30
+ get size(): number;
31
+ /** The highest-priority key's client (index 0). */
32
+ get primaryClient(): GoogleGenAI;
33
+ /** Resolve a tagged task's key id back to its client (undefined if unknown). */
34
+ clientById(id: string | undefined): GoogleGenAI | undefined;
35
+ /**
36
+ * Pick the key that should serve a new submission. Walks keys in priority
37
+ * order and returns the first whose per-key minute + day budgets still have
38
+ * headroom, consuming a slot on it. The caps are the model's Tier-2 baseline
39
+ * (baselineMin / baselineDay) scaled to each key's own tier — so a Tier-1
40
+ * key gets a smaller share than a Tier-2 one. Falls back to the primary key
41
+ * if Redis is down or all are at cap (the aggregate model-level gate should
42
+ * have prevented the latter).
43
+ */
44
+ pickForSubmit(modelId: string, baselineMin: number, baselineDay: number): Promise<KeyEntry>;
45
+ /** Best-effort increment of a key's per-day + per-minute routing counters. */
46
+ private consume;
47
+ }
48
+ /** Lazily-built process-wide pool. */
49
+ export declare function getGoogleKeyPool(): GoogleKeyPool;
50
+ export { googleApiKeyCount };
51
+ export type { GoogleKeyPool };
52
+ //# sourceMappingURL=googleKeyPool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"googleKeyPool.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/googleKeyPool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EACL,iBAAiB,EAKlB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;;;;;;GAgBG;AAEH,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,CAAC;IACpB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;CACd;AAcD,cAAM,aAAa;IACjB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;;IA4BrC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,mDAAmD;IACnD,IAAI,aAAa,IAAI,WAAW,CAE/B;IAED,gFAAgF;IAChF,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS;IAK3D;;;;;;;;OAQG;IACG,aAAa,CACjB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC;IA2CpB,8EAA8E;YAChE,OAAO;CAUtB;AAID,sCAAsC;AACtC,wBAAgB,gBAAgB,IAAI,aAAa,CAGhD;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,YAAY,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.googleApiKeyCount = void 0;
4
+ exports.getGoogleKeyPool = getGoogleKeyPool;
5
+ const genai_1 = require("@google/genai");
6
+ const logger_1 = require("../../../../utils/logger");
7
+ const redis_service_1 = require("../../../redis.service");
8
+ const googleApiKeys_1 = require("./googleApiKeys");
9
+ Object.defineProperty(exports, "googleApiKeyCount", { enumerable: true, get: function () { return googleApiKeys_1.googleApiKeyCount; } });
10
+ function utcDateKey() {
11
+ return new Date().toISOString().slice(0, 10);
12
+ }
13
+ function secsUntilMidnight() {
14
+ const now = new Date();
15
+ const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
16
+ return Math.ceil((midnight.getTime() - now.getTime()) / 1000);
17
+ }
18
+ class GoogleKeyPool {
19
+ constructor() {
20
+ const keys = (0, googleApiKeys_1.parseGoogleApiKeys)();
21
+ const tiers = (0, googleApiKeys_1.parseGoogleApiKeyTiers)();
22
+ this.entries = keys.map((apiKey, i) => ({
23
+ id: (0, googleApiKeys_1.googleKeyId)(i),
24
+ apiKey,
25
+ client: new genai_1.GoogleGenAI({ apiKey }),
26
+ tier: tiers[i] ?? 2,
27
+ }));
28
+ if (!this.entries.length) {
29
+ // Mirror the legacy constructor: a bang-asserted GOOGLE_API_KEY. Building
30
+ // a client with undefined defers the failure to first call, same as before.
31
+ this.entries.push({
32
+ id: (0, googleApiKeys_1.googleKeyId)(0),
33
+ apiKey: process.env.GOOGLE_API_KEY ?? "",
34
+ client: new genai_1.GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY }),
35
+ tier: 2,
36
+ });
37
+ }
38
+ if (this.entries.length > 1) {
39
+ logger_1.logger.info("googleKeyPool: multi-key pool active", {
40
+ keys: this.entries.map((e) => ({ id: e.id, tier: e.tier })),
41
+ });
42
+ }
43
+ }
44
+ get size() {
45
+ return this.entries.length;
46
+ }
47
+ /** The highest-priority key's client (index 0). */
48
+ get primaryClient() {
49
+ return this.entries[0].client;
50
+ }
51
+ /** Resolve a tagged task's key id back to its client (undefined if unknown). */
52
+ clientById(id) {
53
+ if (!id)
54
+ return undefined;
55
+ return this.entries.find((e) => e.id === id)?.client;
56
+ }
57
+ /**
58
+ * Pick the key that should serve a new submission. Walks keys in priority
59
+ * order and returns the first whose per-key minute + day budgets still have
60
+ * headroom, consuming a slot on it. The caps are the model's Tier-2 baseline
61
+ * (baselineMin / baselineDay) scaled to each key's own tier — so a Tier-1
62
+ * key gets a smaller share than a Tier-2 one. Falls back to the primary key
63
+ * if Redis is down or all are at cap (the aggregate model-level gate should
64
+ * have prevented the latter).
65
+ */
66
+ async pickForSubmit(modelId, baselineMin, baselineDay) {
67
+ if (this.entries.length === 1)
68
+ return this.entries[0];
69
+ const client = redis_service_1.redis.getClient();
70
+ if (!client)
71
+ return this.entries[0];
72
+ const date = utcDateKey();
73
+ for (const entry of this.entries) {
74
+ try {
75
+ const perKeyMinLimit = (0, googleApiKeys_1.scaleLimitForTier)(baselineMin, entry.tier, "rpm");
76
+ const perKeyDayLimit = (0, googleApiKeys_1.scaleLimitForTier)(baselineDay, entry.tier, "rpd");
77
+ const dayKey = `gkpool:${entry.id}:${modelId}:day:${date}`;
78
+ const minKey = `gkpool:${entry.id}:${modelId}:min`;
79
+ const [dayRaw, minRaw] = await Promise.all([
80
+ client.get(dayKey),
81
+ client.get(minKey),
82
+ ]);
83
+ const dayUsed = dayRaw ? Number(dayRaw) : 0;
84
+ const minUsed = minRaw ? Number(minRaw) : 0;
85
+ const dayOk = perKeyDayLimit <= 0 || dayUsed < perKeyDayLimit;
86
+ const minOk = perKeyMinLimit <= 0 || minUsed < perKeyMinLimit;
87
+ if (dayOk && minOk) {
88
+ await this.consume(entry, modelId, date);
89
+ return entry;
90
+ }
91
+ }
92
+ catch (err) {
93
+ logger_1.logger.warn("googleKeyPool: routing read failed, trying next key", {
94
+ keyId: entry.id,
95
+ err: err instanceof Error ? err.message : String(err),
96
+ });
97
+ }
98
+ }
99
+ // All at cap / errored — use the primary and let Google's own 429 + the
100
+ // provider-fallback chain handle it.
101
+ logger_1.logger.warn("googleKeyPool: no key with headroom — using primary", {
102
+ modelId,
103
+ poolSize: this.entries.length,
104
+ });
105
+ await this.consume(this.entries[0], modelId, date).catch(() => undefined);
106
+ return this.entries[0];
107
+ }
108
+ /** Best-effort increment of a key's per-day + per-minute routing counters. */
109
+ async consume(entry, modelId, date) {
110
+ const client = redis_service_1.redis.getClient();
111
+ if (!client)
112
+ return;
113
+ const dayKey = `gkpool:${entry.id}:${modelId}:day:${date}`;
114
+ const minKey = `gkpool:${entry.id}:${modelId}:min`;
115
+ const newDay = await client.incr(dayKey);
116
+ if (newDay === 1)
117
+ await client.expire(dayKey, secsUntilMidnight());
118
+ const newMin = await client.incr(minKey);
119
+ if (newMin === 1)
120
+ await client.expire(minKey, 60);
121
+ }
122
+ }
123
+ let pool = null;
124
+ /** Lazily-built process-wide pool. */
125
+ function getGoogleKeyPool() {
126
+ if (!pool)
127
+ pool = new GoogleKeyPool();
128
+ return pool;
129
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"pixverse.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/pixverse/pixverse.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA0FlB,qBAAa,eAAgB,SAAQ,wBAAwB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAKpE,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAmK3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0FjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAkDjD"}
1
+ {"version":3,"file":"pixverse.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/pixverse/pixverse.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA0FlB,qBAAa,eAAgB,SAAQ,wBAAwB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAKpE,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyK3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0FjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAkDjD"}
@@ -211,7 +211,13 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
211
211
  if (isImageToVideo) {
212
212
  body.image_url = params.inputImageUrl;
213
213
  }
214
- const endpoint = isImageToVideo ? `${this.baseUrl}/image/generate` : `${this.baseUrl}/text/generate`;
214
+ // I2V path is `/img/generate` NOT `/image/generate`, which returns a bare
215
+ // "404 page not found" (verified 2026-06-15 via PixVerse OpenAPI smoke test
216
+ // + docs https://docs.platform.pixverse.ai/image-to-video-generation-13016633e0).
217
+ // This affected ALL PixVerse image-to-video models; it surfaced in prod as
218
+ // "pixverse-v6 404" only because v6 sits low in the I2V fallback chain and
219
+ // was the model selected when the rare fallback fired.
220
+ const endpoint = isImageToVideo ? `${this.baseUrl}/img/generate` : `${this.baseUrl}/text/generate`;
215
221
  const data = await pixverseFetch(endpoint, {
216
222
  method: "POST",
217
223
  headers: {
@@ -1 +1 @@
1
- {"version":3,"file":"bullmq.service.d.ts","sourceRoot":"","sources":["../../src/services/bullmq.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAgB,EAAwB,YAAY,EAAE,MAAM,SAAS,CAAC;AAItE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAc1C,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgB;IACvC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAgB;IAIpC,OAAO,CAAC,UAAU,CAAC,CAAc;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO;WAQO,WAAW,CAAC,OAAO,EAAE,oBAAoB;IAOvD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,aAAa;IAOrB,sDAAsD;IACzC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBlC,gBAAgB;IACH,MAAM,CACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAUpC,YAAY,CACjB,UAAU,EAAE,MAAM,EAAE,EACpB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QACE,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CACF;CAkHJ;AASD,eAAO,MAAM,MAAM,eAGjB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC"}
1
+ {"version":3,"file":"bullmq.service.d.ts","sourceRoot":"","sources":["../../src/services/bullmq.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAgB,EAAwB,YAAY,EAAE,MAAM,SAAS,CAAC;AAItE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAc1C,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgB;IACvC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAgB;IAIpC,OAAO,CAAC,UAAU,CAAC,CAAc;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO;WAQO,WAAW,CAAC,OAAO,EAAE,oBAAoB;IAOvD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,aAAa;IAOrB,sDAAsD;IACzC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBlC,gBAAgB;IACH,MAAM,CACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAUpC,YAAY,CACjB,UAAU,EAAE,MAAM,EAAE,EACpB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QACE,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CACF;CAyIJ;AASD,eAAO,MAAM,MAAM,eAGjB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC"}
@@ -166,7 +166,28 @@ class BullMQService {
166
166
  if (this.shuttingDown)
167
167
  return;
168
168
  this.shuttingDown = true;
169
- logger_1.logger.info("Shutting down BullMQ workers gracefully...");
169
+ // Hard backstop: worker.close() waits for the in-flight job's processor
170
+ // to finish, and a video job can legitimately run for the full poll
171
+ // window (~15 min) + finalization. If a job is wedged (e.g. an
172
+ // unforeseen hang — the rate-limiter pile-up that used to cause this is
173
+ // now bounded, see DistributedRateLimiter.waitUntilAvailable), an
174
+ // unbounded close() would hold the old Railway container alive
175
+ // indefinitely on a rolling deploy. Force-exit after a generous bound
176
+ // so a stuck process can never block a deploy. Default 20 min (> the
177
+ // 15-min legit job ceiling); override with SHUTDOWN_HARD_TIMEOUT_MS to
178
+ // match the Railway service's drain/grace window.
179
+ const hardTimeoutMs = Number(process.env.SHUTDOWN_HARD_TIMEOUT_MS) || 20 * 60000;
180
+ const hardExit = setTimeout(() => {
181
+ logger_1.logger.error("Graceful shutdown exceeded hard timeout — force-exiting", {
182
+ hardTimeoutMs,
183
+ activeWorkers: this.workers.length,
184
+ });
185
+ process.exit(1);
186
+ }, hardTimeoutMs);
187
+ // Don't let this timer itself keep the event loop alive once everything
188
+ // else has settled.
189
+ hardExit.unref();
190
+ logger_1.logger.info("Shutting down BullMQ workers gracefully...", { hardTimeoutMs });
170
191
  for (const worker of this.workers) {
171
192
  await worker.close();
172
193
  logger_1.logger.info(`Worker for queue closed`, { queueName: worker.name });
@@ -180,6 +201,7 @@ class BullMQService {
180
201
  if (this.connection) {
181
202
  await this.connection.quit();
182
203
  }
204
+ clearTimeout(hardExit);
183
205
  logger_1.logger.info("All workers closed. Exiting process.");
184
206
  process.exit(0);
185
207
  };
@@ -10,6 +10,7 @@ export * from "./notification.service";
10
10
  export * from "./credits/pricing";
11
11
  export * from "./analytics.service";
12
12
  export * from "./tts";
13
+ export * from "./translation";
13
14
  export * from "./musicGen";
14
15
  export * from "./audioAnalysis";
15
16
  export * from "./asr";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,OAAO,CAAC;AACtB,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC"}
@@ -26,6 +26,7 @@ __exportStar(require("./notification.service"), exports);
26
26
  __exportStar(require("./credits/pricing"), exports);
27
27
  __exportStar(require("./analytics.service"), exports);
28
28
  __exportStar(require("./tts"), exports);
29
+ __exportStar(require("./translation"), exports);
29
30
  __exportStar(require("./musicGen"), exports);
30
31
  __exportStar(require("./audioAnalysis"), exports);
31
32
  __exportStar(require("./asr"), exports);
@@ -1,5 +1,18 @@
1
1
  import { TAiGenModel } from "../../globals/aiModels";
2
2
  import { TtsProvider } from "../../globals/ttsModels/types";
3
+ /**
4
+ * Capacity-planning thresholds (used by previewCapacity + the bounded gate).
5
+ *
6
+ * MAX_ACCEPTABLE_WAIT_MS — a model whose projected wait for a submit slot
7
+ * exceeds this is considered "not available" by previewCapacity, so the
8
+ * job-start selector spills to a less-loaded model in the same tier chain.
9
+ * DAILY_NEAR_RESET_MS — when a model's per-day cap is exhausted, we only wait
10
+ * it out if the UTC-midnight reset is within this window; otherwise the
11
+ * bounded gate fast-fails (VIDEO_PROVIDER_RATE_LIMITED) so the caller can
12
+ * fall back rather than block for hours.
13
+ */
14
+ export declare const MAX_ACCEPTABLE_WAIT_MS = 180000;
15
+ export declare const DAILY_NEAR_RESET_MS = 120000;
3
16
  /**
4
17
  * DistributedRateLimiter — three-tier provider quota gate.
5
18
  *
@@ -39,6 +52,29 @@ export interface LimiterSnapshot {
39
52
  perMinLimit?: number;
40
53
  perDayLimit?: number;
41
54
  }
55
+ /**
56
+ * Read-only capacity estimate for a model — does NOT consume a slot. Used by
57
+ * the job-start model selector to decide whether to spill to another model in
58
+ * the tier chain before triggering. `available` folds the projected-wait and
59
+ * daily-reset thresholds into a single go/no-go.
60
+ */
61
+ export interface CapacityPreview {
62
+ modelKey: string;
63
+ /** Rough estimate of how long until a NEW submit can proceed, in ms. */
64
+ projectedWaitMs: number;
65
+ /** Whether a new submission can proceed within the acceptable wait window. */
66
+ available: boolean;
67
+ /** Per-day cap is configured and currently exhausted. */
68
+ dailyExhausted: boolean;
69
+ secsUntilDayReset: number;
70
+ perMinUsed: number;
71
+ perMinLimit: number;
72
+ perDayUsed: number;
73
+ perDayLimit: number;
74
+ concurrentActive: number;
75
+ concurrentLimit: number;
76
+ concurrentQueueDepth: number;
77
+ }
42
78
  export declare class DistributedRateLimiter {
43
79
  private readonly modelKey;
44
80
  private readonly concurrentLimit;
@@ -52,15 +88,34 @@ export declare class DistributedRateLimiter {
52
88
  * a slot in each. Caller MUST call releaseSlot() when the in-flight task
53
89
  * settles — per-min / per-day auto-expire but the concurrent counter does
54
90
  * not. Mirrors the old AiGenModelRateLimiter API.
91
+ *
92
+ * `opts.maxWaitMs` bounds the rate-budget wait: if satisfying the per-day /
93
+ * per-minute gate would take longer than this, the call throws a
94
+ * `UserFacingError(VIDEO_PROVIDER_RATE_LIMITED)` instead of sleep-looping for
95
+ * hours. This is the backstop that stops jobs from piling up in-process when
96
+ * a daily cap is hit — the submit path passes it; legacy callers that omit
97
+ * it keep the original (unbounded) behavior.
55
98
  */
56
- waitUntilAvailable(): Promise<void>;
99
+ waitUntilAvailable(opts?: {
100
+ maxWaitMs?: number;
101
+ }): Promise<void>;
57
102
  /**
58
- * Bumps per-min + per-day only — does NOT count toward the concurrent cap.
59
- * Use for short-lived calls that share a provider's rate budget but aren't
60
- * "in-flight tasks" (status polls, sync image/TTS gens that complete in the
61
- * same call). No release is needed Redis auto-expires the counters.
103
+ * Bumps per-MINUTE only — does NOT count toward the concurrent cap and does
104
+ * NOT consume/await the per-day gate. Use for short-lived calls that share a
105
+ * provider's per-minute budget but must not be blocked by the daily SUBMIT
106
+ * cap: status polls of already-running tasks (failing a poll on daily quota
107
+ * would abandon a video that's already generating), plus sync image/TTS gens
108
+ * (which have no per-day limit configured anyway). No release needed — Redis
109
+ * auto-expires the counters.
62
110
  */
63
111
  waitUntilAvailableForOneShot(): Promise<void>;
112
+ /**
113
+ * Read-only capacity estimate — does NOT consume a slot. Reads current
114
+ * per-minute / per-day usage and concurrency pressure from Redis and
115
+ * projects how long a new submission would wait. Fails open (available:true,
116
+ * wait:0) on Redis errors / no client, matching the gate's fail-open policy.
117
+ */
118
+ previewCapacity(maxAcceptableWaitMs?: number): Promise<CapacityPreview>;
64
119
  /** Free a concurrent slot acquired via waitUntilAvailable(). */
65
120
  releaseSlot(): void;
66
121
  get activeConcurrentCount(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"distributedRateLimiter.service.d.ts","sourceRoot":"","sources":["../../../src/services/rateLimiter/distributedRateLimiter.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAU5D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,UAAU,4BAA4B;IACpC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAuDD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,SAAS,CAAyB;gBAE9B,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B;IAOlE;;;;;OAKG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAUzC;;;;;OAKG;IACG,4BAA4B,IAAI,OAAO,CAAC,IAAI,CAAC;IAInD,gEAAgE;IAChE,WAAW,IAAI,IAAI;IAInB,IAAI,qBAAqB,IAAI,MAAM,CAElC;IAEK,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC;YAoC5B,iBAAiB;IAc/B,OAAO,CAAC,iBAAiB;YAQX,iBAAiB;CA6DhC;AAwBD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,WAAW,GACpB,sBAAsB,CAaxB;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,WAAW,GACpB,sBAAsB,CAexB;AAED;4DAC4D;AAC5D,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAM1E"}
1
+ {"version":3,"file":"distributedRateLimiter.service.d.ts","sourceRoot":"","sources":["../../../src/services/rateLimiter/distributedRateLimiter.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAM5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAC9C,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAQ3C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,UAAU,4BAA4B;IACpC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,SAAS,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,cAAc,EAAE,OAAO,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AA4DD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,SAAS,CAAyB;gBAE9B,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B;IAOlE;;;;;;;;;;;;OAYG;IACG,kBAAkB,CAAC,IAAI,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtE;;;;;;;;OAQG;IACG,4BAA4B,IAAI,OAAO,CAAC,IAAI,CAAC;IAInD;;;;;OAKG;IACG,eAAe,CACnB,mBAAmB,GAAE,MAA+B,GACnD,OAAO,CAAC,eAAe,CAAC;IAgF3B,gEAAgE;IAChE,WAAW,IAAI,IAAI;IAInB,IAAI,qBAAqB,IAAI,MAAM,CAElC;IAEK,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC;YAoC5B,iBAAiB;IAc/B,OAAO,CAAC,iBAAiB;YAQX,iBAAiB;CAoHhC;AAwBD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,WAAW,GACpB,sBAAsB,CA0BxB;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,WAAW,GACpB,sBAAsB,CAexB;AAED;4DAC4D;AAC5D,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAM1E"}