vidspotai-shared 1.0.80 → 1.0.82-dev.0

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 (31) hide show
  1. package/lib/globals/aiModels/enums.d.ts +5 -0
  2. package/lib/globals/aiModels/enums.d.ts.map +1 -1
  3. package/lib/globals/aiModels/enums.js +12 -1
  4. package/lib/globals/aiModels/providers/alibaba.d.ts.map +1 -1
  5. package/lib/globals/aiModels/providers/alibaba.js +159 -39
  6. package/lib/globals/aiModels/providers/google.d.ts.map +1 -1
  7. package/lib/globals/aiModels/providers/google.js +12 -3
  8. package/lib/services/aiGen/aiGenFactory.service.d.ts +4 -1
  9. package/lib/services/aiGen/aiGenFactory.service.d.ts.map +1 -1
  10. package/lib/services/aiGen/aiGenFactory.service.js +13 -1
  11. package/lib/services/aiGen/index.d.ts +1 -0
  12. package/lib/services/aiGen/index.d.ts.map +1 -1
  13. package/lib/services/aiGen/index.js +1 -0
  14. package/lib/services/aiGen/providers/alibaba/alibaba.d.ts +34 -7
  15. package/lib/services/aiGen/providers/alibaba/alibaba.d.ts.map +1 -1
  16. package/lib/services/aiGen/providers/alibaba/alibaba.js +193 -75
  17. package/lib/services/aiGen/providers/google/google.service.d.ts +1 -0
  18. package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
  19. package/lib/services/aiGen/providers/google/google.service.js +66 -1
  20. package/lib/services/aiGen/providers/pixverse/pixverse.service.d.ts.map +1 -1
  21. package/lib/services/aiGen/providers/pixverse/pixverse.service.js +71 -40
  22. package/lib/services/aiGen/transientRetry.d.ts +35 -0
  23. package/lib/services/aiGen/transientRetry.d.ts.map +1 -0
  24. package/lib/services/aiGen/transientRetry.js +106 -0
  25. package/package.json +1 -1
  26. package/lib/services/aiGen/providers/azure/azure.service.d.ts +0 -14
  27. package/lib/services/aiGen/providers/azure/azure.service.d.ts.map +0 -1
  28. package/lib/services/aiGen/providers/azure/azure.service.js +0 -108
  29. package/lib/services/aiGen/providers/azure/index.d.ts +0 -2
  30. package/lib/services/aiGen/providers/azure/index.d.ts.map +0 -1
  31. package/lib/services/aiGen/providers/azure/index.js +0 -17
@@ -13,32 +13,83 @@ const helpers_1 = require("./helpers");
13
13
  const helpers_2 = require("../../helpers");
14
14
  const utils_1 = require("../../../../utils");
15
15
  const logger_1 = require("../../../../utils/logger");
16
- // DashScope hosts Wan endpoints under three task-typed paths. We pick at submit
17
- // time based on which inputs are present (audio s2v, image i2v, else t2v).
18
- const ALIBABA_BASE = "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc";
19
- const ENDPOINT_T2V = `${ALIBABA_BASE}/video-generation/video-synthesis`;
20
- const ENDPOINT_I2V = `${ALIBABA_BASE}/image2video/video-synthesis`;
21
- const ENDPOINT_S2V = `${ALIBABA_BASE}/sound2video/video-synthesis`;
22
- // All three share the same /tasks/{task_id} status endpoint.
23
- const ENDPOINT_TASK_STATUS = "https://dashscope-intl.aliyuncs.com/api/v1/tasks";
16
+ const errors_1 = require("../../../../utils/errors");
17
+ // Endpoint topology (verified 2026-06-06 against intl docs + live probes):
18
+ //
19
+ // T2V + I2V → /services/aigc/video-generation/video-synthesis (async; X-DashScope-Async: enable)
20
+ // Wan/Qwen async → /services/aigc/text2image/image-synthesis (async; legacy qwen-image, qwen-image-plus)
21
+ // Wan async image → /services/aigc/image-generation/generation (async; wan2.7-image*, wan2.6-image)
22
+ // Qwen/Wan SYNC → /services/aigc/multimodal-generation/generation (sync; new qwen-image-2.0*, qwen-image-edit*, wan2.7-image* + edit)
23
+ //
24
+ // Both T2V and I2V use the SAME video endpoint — the model + presence of
25
+ // img_url determines mode. The legacy split into /image2video/... and
26
+ // /sound2video/... endpoints was wrong (Model not exist errors).
27
+ //
28
+ // S2V (wan2.2-s2v) is China-region only and cannot be reached with an intl
29
+ // DashScope key; the model key is intentionally kept but rejected at submit
30
+ // with a UserFacingError. Same for kolors-v2 (Kuaishou model, not Alibaba).
31
+ const ALIBABA_BASE_URL = "https://dashscope-intl.aliyuncs.com/api/v1";
32
+ const ALIBABA_BASE = `${ALIBABA_BASE_URL}/services/aigc`;
33
+ const ENDPOINT_VIDEO_ASYNC = `${ALIBABA_BASE}/video-generation/video-synthesis`;
34
+ const ENDPOINT_IMAGE_T2I_ASYNC = `${ALIBABA_BASE}/text2image/image-synthesis`;
35
+ const ENDPOINT_IMAGE_GEN_ASYNC = `${ALIBABA_BASE}/image-generation/generation`;
36
+ const ENDPOINT_MULTIMODAL_SYNC = `${ALIBABA_BASE}/multimodal-generation/generation`;
37
+ const ENDPOINT_TASK_STATUS = `${ALIBABA_BASE_URL}/tasks`;
38
+ // Model IDs that don't run on the intl `dashscope-intl.aliyuncs.com` endpoint.
39
+ // We surface a clean PROVIDER_AUTH_ERROR rather than letting them fall through
40
+ // and hit a 404 "Model not exist."
41
+ const INTL_UNAVAILABLE_MODEL_IDS = new Set([
42
+ "wan2.2-s2v", // China region only (dashscope.aliyuncs.com)
43
+ "kolors-v2", // Kuaishou model; not hosted on DashScope intl
44
+ ]);
45
+ // Image model classification — drives sync vs async endpoint dispatch.
46
+ // Sync (multimodal-generation/generation, returns inline image URL):
47
+ // - Qwen 2.x sync line: qwen-image-2.0, qwen-image-2.0-pro, qwen-image-max,
48
+ // qwen-image-edit*, plus wan2.7-image / wan2.7-image-pro when used inline.
49
+ // Async-via-image-generation (returns task_id, poll /tasks):
50
+ // - Wan image-gen line: wan2.7-image*, wan2.6-image.
51
+ // Async-via-text2image (returns task_id, poll /tasks):
52
+ // - Legacy qwen-image, qwen-image-plus.
53
+ function isSyncMultimodalImageModel(modelId) {
54
+ return (modelId.startsWith("qwen-image-2") ||
55
+ modelId.startsWith("qwen-image-max") ||
56
+ modelId.startsWith("qwen-image-edit") ||
57
+ modelId.startsWith("wan2.7-image"));
58
+ }
59
+ function isAsyncWanImageModel(modelId) {
60
+ return modelId.startsWith("wan2.7-image") || modelId.startsWith("wan2.6-image");
61
+ }
24
62
  class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderService {
25
63
  constructor() {
26
64
  super();
27
- this.baseUrl = ENDPOINT_T2V; // legacy default; used by getCreditUsed only
65
+ // Used by getCreditUsed only. Real request paths are chosen per-call.
66
+ this.baseUrl = ENDPOINT_VIDEO_ASYNC;
28
67
  this.timeout = 60000; // 60 seconds
29
68
  if (!process.env.ALIBABA_API_KEY) {
30
- throw new Error("Missing ALIBABA_API_KEY in environment variables");
69
+ // Classify as PROVIDER_AUTH_ERROR (UserFacingError warn log, no Slack
70
+ // page per job). The DashScope key is single-Bearer and distinct from
71
+ // ALIBABA_CLOUD_ACCESS_KEY/SECRET (those are general Alibaba Cloud
72
+ // creds, not DashScope) — surface a hint so an operator knows what to
73
+ // provision instead of chasing a generic 500.
74
+ throw new errors_1.UserFacingError("Alibaba (DashScope) API key is not configured. Set ALIBABA_API_KEY (Bearer sk-* token from dashscope.aliyuncs.com) in the runtime env.", errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
31
75
  }
32
76
  }
33
- async request(body, method = "POST", url = this.baseUrl) {
77
+ /**
78
+ * DashScope API call. `async` toggles the `X-DashScope-Async: enable` header
79
+ * — required for async endpoints (video-generation, image-generation,
80
+ * text2image), MUST be omitted for sync multimodal-generation.
81
+ */
82
+ async request(body, method = "POST", url = this.baseUrl, asyncMode = true) {
83
+ const headers = {
84
+ Authorization: `Bearer ${process.env.ALIBABA_API_KEY}`,
85
+ "Content-Type": "application/json",
86
+ };
87
+ if (asyncMode)
88
+ headers["X-DashScope-Async"] = "enable";
34
89
  const config = {
35
90
  method,
36
91
  url,
37
- headers: {
38
- Authorization: `Bearer ${process.env.ALIBABA_API_KEY}`,
39
- "Content-Type": "application/json",
40
- "X-DashScope-Async": "enable",
41
- },
92
+ headers,
42
93
  timeout: this.timeout,
43
94
  data: method === "POST" ? body : undefined,
44
95
  };
@@ -51,10 +102,12 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
51
102
  const modelId = modelConfig?.modelId;
52
103
  if (!modelId)
53
104
  throw new Error(`Unknown modelKey: ${params.modelKey}`);
54
- // Endpoint selection: audio wins (S2V), then image (I2V), else T2V.
55
- const isS2V = !!params.inputAudioUrl;
56
- const isI2V = !isS2V && !!params.inputImageUrl;
57
- const endpoint = isS2V ? ENDPOINT_S2V : isI2V ? ENDPOINT_I2V : ENDPOINT_T2V;
105
+ if (INTL_UNAVAILABLE_MODEL_IDS.has(modelId)) {
106
+ throw new errors_1.UserFacingError(`Model "${modelId}" is not available on the intl DashScope endpoint (dashscope-intl.aliyuncs.com). It is hosted only in the China region and requires a separate Beijing-region API key.`, errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
107
+ }
108
+ // T2V and I2V share a single endpoint. The model + presence of img_url
109
+ // determines mode. S2V is unavailable on intl (gated above).
110
+ const isI2V = !!params.inputImageUrl;
58
111
  const input = {};
59
112
  if (params.prompt)
60
113
  input.prompt = params.prompt;
@@ -62,21 +115,16 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
62
115
  input.negative_prompt = params.negativePrompt;
63
116
  if (isI2V) {
64
117
  input.img_url = params.inputImageUrl;
65
- // Wan 2.7 i2v supports first-last-frame interpolation
118
+ // wan2.7-i2v supports first-last-frame interpolation
66
119
  if (params.lastFrameImageUrl)
67
120
  input.last_frame_url = params.lastFrameImageUrl;
68
121
  }
69
- if (isS2V) {
70
- input.audio_url = params.inputAudioUrl;
71
- if (params.inputImageUrl)
72
- input.image_url = params.inputImageUrl;
73
- }
74
122
  const parameters = {
75
123
  duration: params.duration || 5,
76
124
  prompt_extend: params.promptOptimizer ?? true,
77
125
  };
78
- // T2V requires explicit size; I2V/S2V derive from inputs.
79
- if (!isI2V && !isS2V) {
126
+ // T2V requires explicit size; I2V derives dimensions from the input image.
127
+ if (!isI2V) {
80
128
  const size = (0, helpers_1.getAlibabaDimensions)(params.resolution, params.aspectRatio);
81
129
  if (!size) {
82
130
  throw new Error(`Invalid resolution/aspect ratio combination: ${params.resolution} ${params.aspectRatio}`);
@@ -88,7 +136,7 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
88
136
  if (params.watermark !== undefined)
89
137
  parameters.watermark = params.watermark;
90
138
  const body = { model: modelId, input, parameters };
91
- const result = await this.request(body, "POST", endpoint);
139
+ const result = await this.request(body, "POST", ENDPOINT_VIDEO_ASYNC, true);
92
140
  // DashScope returns { output: { task_id, ... } } on async submit.
93
141
  const taskId = result?.output?.task_id || result?.request_id;
94
142
  if (!result || !taskId) {
@@ -161,38 +209,127 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
161
209
  return { status: types_1.EVideoSceneStatus.PENDING };
162
210
  }
163
211
  /**
164
- * DashScope image generation. Both Kolors and Qwen-Image are async on
165
- * DashScope — submit returns a task_id, then poll /tasks/{id}. Image jobs
166
- * typically finish in 5–15s, well inside our HTTP timeout, so we poll
167
- * inline rather than going through BullMQ.
212
+ * DashScope image generation. Three dispatch paths (verified live 2026-06-06):
213
+ *
214
+ * 1. SYNC multimodal-generation/generation
215
+ * Models: qwen-image-2.0, qwen-image-2.0-pro, qwen-image-max,
216
+ * qwen-image-edit*, wan2.7-image, wan2.7-image-pro
217
+ * Response: output.choices[0].message.content[].image (inline URLs)
218
+ * Note: NO X-DashScope-Async header. Edit mode is triggered by including
219
+ * {image: refUrl} entries in the user message content array.
220
+ *
221
+ * 2. ASYNC image-generation/generation (Wan async image)
222
+ * Models: wan2.7-image*, wan2.6-image (when caller prefers async; we
223
+ * currently route these through path 1 since they're available
224
+ * there too).
168
225
  *
169
- * Endpoint selection:
170
- * - Qwen-Image edit (modelId qwen-image-edit*): /image2image/image-synthesis
171
- * - Everything else (Qwen T2I, Kolors): /text2image/image-synthesis
226
+ * 3. ASYNC text2image/image-synthesis (legacy)
227
+ * Models: qwen-image, qwen-image-plus
228
+ * Response: output.results[].url
229
+ *
230
+ * Image jobs typically finish in 5–15s, well inside our HTTP timeout, so
231
+ * we poll inline rather than going through BullMQ.
172
232
  */
173
233
  async generateImage(params) {
174
234
  const modelConfig = aiModels_1.aiModelConfigs[params.modelKey];
175
235
  const modelId = modelConfig?.modelId;
176
236
  if (!modelId)
177
237
  throw new Error(`Unknown image modelKey: ${params.modelKey}`);
178
- const isEdit = modelId.startsWith("qwen-image-edit");
179
- const submitUrl = isEdit
180
- ? `${ALIBABA_BASE}/image2image/image-synthesis`
181
- : `${ALIBABA_BASE}/text2image/image-synthesis`;
238
+ if (INTL_UNAVAILABLE_MODEL_IDS.has(modelId)) {
239
+ throw new errors_1.UserFacingError(`Model "${modelId}" is not available on the intl DashScope endpoint. Use a different image provider.`, errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
240
+ }
241
+ const refs = [
242
+ ...(params.inputImageUrl ? [params.inputImageUrl] : []),
243
+ ...(params.inputImageUrls ?? []),
244
+ ];
245
+ const isEdit = modelId.startsWith("qwen-image-edit") ||
246
+ (refs.length > 0 && (modelId.startsWith("wan2.7-image") || modelId.startsWith("qwen-image-2")));
247
+ if (isEdit && !refs.length) {
248
+ throw new Error(`${modelId}: edit/reference mode requires at least one input image`);
249
+ }
250
+ let remoteUrls;
251
+ let providerRequestId;
252
+ if (isSyncMultimodalImageModel(modelId)) {
253
+ const result = await this.generateImageSync(modelId, params, refs);
254
+ remoteUrls = result.urls;
255
+ providerRequestId = result.requestId;
256
+ }
257
+ else if (isAsyncWanImageModel(modelId)) {
258
+ const result = await this.generateImageAsync(modelId, params, ENDPOINT_IMAGE_GEN_ASYNC);
259
+ remoteUrls = result.urls;
260
+ providerRequestId = result.taskId;
261
+ }
262
+ else {
263
+ // Legacy async path: qwen-image, qwen-image-plus.
264
+ const result = await this.generateImageAsync(modelId, params, ENDPOINT_IMAGE_T2I_ASYNC);
265
+ remoteUrls = result.urls;
266
+ providerRequestId = result.taskId;
267
+ }
268
+ if (!remoteUrls.length) {
269
+ throw new Error("DashScope image returned no URLs");
270
+ }
271
+ const bucket = (0, firebase_1.getBucket)();
272
+ const ts = Date.now();
273
+ const urls = [];
274
+ for (let i = 0; i < remoteUrls.length; i++) {
275
+ const remoteUrl = remoteUrls[i];
276
+ const bytes = Buffer.from(await (await axios_1.default.get(remoteUrl, { responseType: "arraybuffer", timeout: this.timeout })).data);
277
+ const path = `images/dashscope/${ts}-${Math.random().toString(36).slice(2, 8)}-${i}.png`;
278
+ const file = bucket.file(path);
279
+ await file.save(bytes, { contentType: "image/png" });
280
+ const [signed] = await file.getSignedUrl({ action: "read", expires: "03-09-2491" });
281
+ urls.push(signed);
282
+ }
283
+ return {
284
+ imageUrl: urls[0],
285
+ imageUrls: urls.length > 1 ? urls : undefined,
286
+ providerRequestId,
287
+ };
288
+ }
289
+ /**
290
+ * Sync multimodal-generation. Returns inline image URLs in
291
+ * `output.choices[0].message.content[].image`. No polling needed.
292
+ */
293
+ async generateImageSync(modelId, params, refs) {
294
+ const userContent = [];
295
+ // Image refs come first (DashScope convention); each as its own content entry.
296
+ for (const ref of refs.slice(0, 3)) {
297
+ userContent.push({ image: ref });
298
+ }
299
+ if (params.prompt)
300
+ userContent.push({ text: params.prompt });
301
+ const input = {
302
+ messages: [{ role: "user", content: userContent }],
303
+ };
304
+ const parameters = {};
305
+ if (params.negativePrompt)
306
+ parameters.negative_prompt = params.negativePrompt;
307
+ if (params.watermark !== undefined)
308
+ parameters.watermark = params.watermark;
309
+ if (params.promptOptimizer !== undefined)
310
+ parameters.prompt_extend = params.promptOptimizer;
311
+ if (params.seed !== undefined)
312
+ parameters.seed = params.seed;
313
+ if (params.aspectRatio)
314
+ parameters.size = params.aspectRatio;
315
+ if (params.imageSize)
316
+ parameters.size = params.imageSize;
317
+ const result = await this.request({ model: modelId, input, parameters }, "POST", ENDPOINT_MULTIMODAL_SYNC, false);
318
+ const choices = result?.output?.choices ?? [];
319
+ const content = choices[0]?.message?.content ?? [];
320
+ const urls = content
321
+ .map((c) => c?.image)
322
+ .filter((u) => typeof u === "string" && !!u);
323
+ return { urls, requestId: result?.request_id ?? "" };
324
+ }
325
+ /**
326
+ * Async submit + poll. Used by both image-generation/generation (Wan async)
327
+ * and text2image/image-synthesis (legacy Qwen).
328
+ */
329
+ async generateImageAsync(modelId, params, submitUrl) {
182
330
  const input = { prompt: params.prompt };
183
331
  if (params.negativePrompt)
184
332
  input.negative_prompt = params.negativePrompt;
185
- if (isEdit) {
186
- const refs = [
187
- ...(params.inputImageUrl ? [params.inputImageUrl] : []),
188
- ...(params.inputImageUrls ?? []),
189
- ];
190
- if (!refs.length) {
191
- throw new Error("Qwen-Image edit requires at least one input image");
192
- }
193
- // Qwen edit accepts 1–3 refs as base_image_url.
194
- input.base_image_url = refs.length === 1 ? refs[0] : refs.slice(0, 3);
195
- }
196
333
  const parameters = {
197
334
  n: params.numImages ?? 1,
198
335
  ...(params.aspectRatio ? { size: params.aspectRatio } : {}),
@@ -203,18 +340,18 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
203
340
  ? { prompt_extend: params.promptOptimizer }
204
341
  : { prompt_extend: true }),
205
342
  };
206
- const submit = await this.request({ model: modelId, input, parameters }, "POST", submitUrl);
343
+ const submit = await this.request({ model: modelId, input, parameters }, "POST", submitUrl, true);
207
344
  const taskId = submit?.output?.task_id;
208
345
  if (!taskId) {
209
346
  throw new Error("DashScope image submit returned no task_id");
210
347
  }
211
- // Poll up to 90s, every 2s. Image jobs are quick.
348
+ // Poll up to 90s, every 2s.
212
349
  const maxAttempts = 45;
213
350
  const intervalMs = 2000;
214
351
  let lastResult = null;
215
352
  for (let i = 0; i < maxAttempts; i++) {
216
353
  await new Promise((r) => setTimeout(r, intervalMs));
217
- const poll = await this.request(null, "GET", `${ENDPOINT_TASK_STATUS}/${taskId}`);
354
+ const poll = await this.request(null, "GET", `${ENDPOINT_TASK_STATUS}/${taskId}`, false);
218
355
  const status = poll?.output?.task_status ?? poll?.status;
219
356
  if (status === "SUCCEEDED" || status === "succeeded") {
220
357
  lastResult = poll;
@@ -228,27 +365,8 @@ class AlibabaService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
228
365
  throw new Error("DashScope image task timed out after 90s");
229
366
  }
230
367
  const results = lastResult.output?.results ?? [];
231
- const remoteUrls = results.map((r) => r.url).filter((u) => !!u);
232
- if (!remoteUrls.length) {
233
- throw new Error("DashScope image task returned no URLs");
234
- }
235
- const bucket = (0, firebase_1.getBucket)();
236
- const ts = Date.now();
237
- const urls = [];
238
- for (let i = 0; i < remoteUrls.length; i++) {
239
- const remoteUrl = remoteUrls[i];
240
- const bytes = Buffer.from(await (await axios_1.default.get(remoteUrl, { responseType: "arraybuffer", timeout: this.timeout })).data);
241
- const path = `images/dashscope/${ts}-${Math.random().toString(36).slice(2, 8)}-${i}.png`;
242
- const file = bucket.file(path);
243
- await file.save(bytes, { contentType: "image/png" });
244
- const [signed] = await file.getSignedUrl({ action: "read", expires: "03-09-2491" });
245
- urls.push(signed);
246
- }
247
- return {
248
- imageUrl: urls[0],
249
- imageUrls: urls.length > 1 ? urls : undefined,
250
- providerRequestId: taskId,
251
- };
368
+ const urls = results.map((r) => r.url).filter((u) => !!u);
369
+ return { urls, taskId };
252
370
  }
253
371
  getCreditUsed({ modelKey, resolution, aspectRatio, duration, multiClip = false, numImages = 1, }) {
254
372
  const modelConfig = aiModels_1.aiModelConfigs[modelKey];
@@ -13,6 +13,7 @@ export declare class GoogleService extends BaseAiGenProviderService {
13
13
  generateVideo(params: VideoGenerationParams): Promise<VideoGenerationResult>;
14
14
  checkVideoStatus({ task, outputFilename, outputFilePath, }: VideoStatusParams): Promise<VideoStatusResult>;
15
15
  generateImage(params: ImageGenerationParams): Promise<ImageGenerationResult>;
16
+ private _generateImage;
16
17
  /**
17
18
  * Lyria 2 (Vertex AI). Sync — POST {region}-aiplatform.googleapis.com/.../lyria-002:predict
18
19
  * returns base64-encoded WAV audio inline. Auth via ADC on the function service
@@ -1 +1 @@
1
- {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAoJlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA+B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyG3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0F3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAsGjC;;;;;;OAMG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqEjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BvI"}
1
+ {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA2MlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA+B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyG3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAoG3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAiBnB,cAAc;IAwG5B;;;;;;OAMG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqEjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BvI"}
@@ -141,6 +141,43 @@ function classifyGoogleApiError(err) {
141
141
  /use case is currently not supported/i.test(msg)) {
142
142
  return new errors_1.UserFacingError(msg, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
143
143
  }
144
+ // Imagen + Nano-Banana surface Responsible-AI filter rejections as
145
+ // INVALID_ARGUMENT 400 with the literal text "filtered out because they
146
+ // violated Google's Responsible AI practices" (and a recommendation to
147
+ // rephrase). This is user content moderation, not a system bug — show the
148
+ // user the rephrase hint and skip the Slack page. Also matches the Veo
149
+ // RAI message ("violated Google's content policies") for the same reason.
150
+ if ((status === "INVALID_ARGUMENT" || httpCode === 400) &&
151
+ /(filtered out because they violated|violated Google's (?:Responsible AI|content) (?:practices|policies))/i.test(msg)) {
152
+ return new errors_1.UserFacingError("Your prompt was flagged by Google's safety filters. Please rephrase and try again.", errors_1.USER_FACING_ERROR_CODES.CONTENT_POLICY_VIOLATION);
153
+ }
154
+ // Generic INVALID_ARGUMENT 400 on a provided string field. Veo echoes the
155
+ // offending value back ("The string value `<prompt>` ...") and the only
156
+ // large free-text field we send is the prompt, so attribute this to the
157
+ // prompt: it's either over the model's length limit or otherwise rejected
158
+ // by the validator. Either way it's user input, not a platform bug —
159
+ // surface a typed, actionable, non-retryable error (logged warn, no Slack
160
+ // page) instead of leaking the echoed prompt into the error channel.
161
+ if ((status === "INVALID_ARGUMENT" || httpCode === 400) &&
162
+ /string value/i.test(msg)) {
163
+ const tooLong = /(exceed|too long|maximum length|length limit|\blimit\b)/i.test(msg);
164
+ return tooLong
165
+ ? new errors_1.UserFacingError("Your prompt is too long for this model. Please shorten it and try again.", errors_1.USER_FACING_ERROR_CODES.PROMPT_TOO_LONG)
166
+ : new errors_1.UserFacingError("Your prompt was rejected by the model. Please simplify or rephrase it and try again.", errors_1.USER_FACING_ERROR_CODES.PROMPT_INVALID);
167
+ }
168
+ // gRPC code 13 = INTERNAL. Veo returns this for transient backend failures
169
+ // ("Video generation failed due to an internal server issue. Please try
170
+ // again in a few minutes."). It is NOT our bug and NOT moderation — a
171
+ // Google-side flake. Surface as a transient PROVIDER_UNAVAILABLE so the
172
+ // user gets a "try again" message and it logs warn (Loki) instead of
173
+ // paging Slack as a platform error. (Egregiously-blocked content that Veo
174
+ // masks as code 13 also lands here; we can't distinguish it from a real
175
+ // internal flake, and "try again" is an acceptable fallback message.)
176
+ if (httpCode === 13 ||
177
+ status === "INTERNAL" ||
178
+ /internal (server|error)/i.test(msg)) {
179
+ return new errors_1.UserFacingError("Google's video service had a temporary problem. Please try again in a few minutes.", errors_1.USER_FACING_ERROR_CODES.PROVIDER_UNAVAILABLE);
180
+ }
144
181
  }
145
182
  catch {
146
183
  // Not JSON — fall through to non-JSON checks.
@@ -274,9 +311,19 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
274
311
  const result = await this.withTransientRetry("getVideosOperation", () => this.ai.operations.getVideosOperation({ operation }));
275
312
  if (result.done) {
276
313
  if (result.error) {
314
+ // A long-running operation can finish with an error (e.g. gRPC 13
315
+ // INTERNAL) instead of throwing. Route it through the same classifier
316
+ // as thrown API errors so transient INTERNAL and invalid-prompt
317
+ // failures get a typed `{code, message}` (which sceneMonitor demotes to
318
+ // warn) instead of leaking raw gRPC JSON that pages Slack as a bug.
319
+ const classified = classifyGoogleApiError({
320
+ message: JSON.stringify(result.error),
321
+ });
277
322
  return {
278
323
  status: types_1.EVideoSceneStatus.FAILED,
279
- errorMessage: JSON.stringify(result.error),
324
+ errorMessage: classified
325
+ ? JSON.stringify(classified.toJSON())
326
+ : JSON.stringify(result.error),
280
327
  };
281
328
  }
282
329
  const videoUri = result.response?.generatedVideos?.[0]?.video?.uri;
@@ -338,6 +385,24 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
338
385
  return { status: types_1.EVideoSceneStatus.PENDING };
339
386
  }
340
387
  async generateImage(params) {
388
+ try {
389
+ return await this._generateImage(params);
390
+ }
391
+ catch (err) {
392
+ // The SDK's generateImages / generateContent throws ApiError instances
393
+ // whose .message is a JSON string. Route through classifyGoogleApiError
394
+ // so Imagen RAI safety filter rejections (the most common failure mode
395
+ // for image gen — "filtered out because they violated Google's
396
+ // Responsible AI practices") become UserFacingError(CONTENT_POLICY_VIOLATION)
397
+ // instead of leaking as raw provider JSON into the worker's Slack
398
+ // error channel.
399
+ const userFacing = classifyGoogleApiError(err);
400
+ if (userFacing)
401
+ throw userFacing;
402
+ throw err;
403
+ }
404
+ }
405
+ async _generateImage(params) {
341
406
  const modelConfig = aiModels_1.aiModelConfigs[params.modelKey];
342
407
  const modelId = modelConfig?.modelId;
343
408
  if (!modelId)
@@ -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;AASlB,qBAAa,eAAgB,SAAQ,wBAAwB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAKpE,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAsL3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAmGjD,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;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"}
@@ -12,7 +12,70 @@ const types_1 = require("../../../../globals/types");
12
12
  const firebase_1 = require("../../../../libs/firebase");
13
13
  const helpers_2 = require("../../../../utils/helpers");
14
14
  const logger_1 = require("../../../../utils/logger");
15
+ const errors_1 = require("../../../../utils/errors");
16
+ const transientRetry_1 = require("../../transientRetry");
15
17
  const crypto_1 = __importDefault(require("crypto"));
18
+ // PixVerse soft-failure ErrCodes (HTTP 200 body). 0 = success.
19
+ // 500090 — insufficient balance on the openapi account (operator must top up)
20
+ // 401xx — auth (apiKey empty/invalid)
21
+ const PIXVERSE_ERR_INSUFFICIENT_BALANCE = 500090;
22
+ /**
23
+ * Classify a PixVerse HTTP failure (non-2xx). Returns:
24
+ * - TransientHttpError for retryable cases (5xx, 429, and 404 — PixVerse's
25
+ * CloudFlare edge has been observed serving "404 page not found" for
26
+ * transiently-misrouted requests that succeed on retry, 2026-06-06 prod log)
27
+ * - UserFacingError for 401/403 auth (deploy/config bug — fail fast, no Slack)
28
+ * - raw Error for true 4xx (validation, etc.) — surface to Slack as bug
29
+ */
30
+ function classifyPixVerseHttpError(status, body, op) {
31
+ if (status === 401 || status === 403) {
32
+ return new errors_1.UserFacingError(`PixVerse rejected the API key (HTTP ${status}). Ask an operator to verify PIXVERSE_API_KEY.`, errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
33
+ }
34
+ if (status >= 500 || status === 429 || status === 404) {
35
+ return new transientRetry_1.TransientHttpError(status, `PixVerse ${op} transient HTTP ${status}: ${body.slice(0, 200)}`);
36
+ }
37
+ return new Error(`PixVerse ${op} failed (${status}): ${body}`);
38
+ }
39
+ /**
40
+ * Classify a PixVerse 200-response with non-zero ErrCode. Insufficient-balance
41
+ * is an operator concern (account top-up), not a per-user bug — we surface it
42
+ * as UserFacingError(ACCOUNT_QUOTA_EXCEEDED) so it logs as warn (no Slack
43
+ * page-storm per job) AND the daily ops-channel digest still picks up that
44
+ * the account is empty. Other ErrCodes are real provider/protocol bugs.
45
+ */
46
+ function classifyPixVerseApiError(errCode, errMsg, op) {
47
+ if (errCode === PIXVERSE_ERR_INSUFFICIENT_BALANCE) {
48
+ return new errors_1.UserFacingError("Video provider is temporarily unavailable. Please try a different model or retry shortly.", errors_1.USER_FACING_ERROR_CODES.ACCOUNT_QUOTA_EXCEEDED);
49
+ }
50
+ return new Error(`PixVerse ${op} API error (code ${errCode}): ${errMsg || "Unknown error"}`);
51
+ }
52
+ /**
53
+ * One-shot PixVerse POST/GET helper with shared transient-retry behavior.
54
+ * Returns the parsed JSON body, or throws a classified error.
55
+ */
56
+ async function pixverseFetch(url, init, op) {
57
+ return (0, transientRetry_1.withTransientRetry)(`pixverse:${op}`, async () => {
58
+ const resp = await fetch(url, init);
59
+ if (!resp.ok) {
60
+ const errText = await resp.text();
61
+ throw classifyPixVerseHttpError(resp.status, errText, op);
62
+ }
63
+ const data = await resp.json();
64
+ if (data.ErrCode !== undefined && data.ErrCode !== 0) {
65
+ throw classifyPixVerseApiError(data.ErrCode, data.ErrMsg ?? "", op);
66
+ }
67
+ return data;
68
+ }, {
69
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => {
70
+ logger_1.logger.warn(`PixVerse ${op} transient error — retrying`, {
71
+ attempt,
72
+ maxAttempts,
73
+ backoffMs,
74
+ message: err.message,
75
+ });
76
+ },
77
+ });
78
+ }
16
79
  class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderService {
17
80
  constructor() {
18
81
  super(...arguments);
@@ -43,7 +106,7 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
43
106
  extendBody.negative_prompt = params.negativePrompt;
44
107
  if (params.seed !== undefined)
45
108
  extendBody.seed = params.seed;
46
- const resp = await fetch(`${this.baseUrl}/extend/generate`, {
109
+ const data = await pixverseFetch(`${this.baseUrl}/extend/generate`, {
47
110
  method: "POST",
48
111
  headers: {
49
112
  "Content-Type": "application/json",
@@ -51,15 +114,7 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
51
114
  "Ai-trace-id": traceId,
52
115
  },
53
116
  body: JSON.stringify(extendBody),
54
- });
55
- if (!resp.ok) {
56
- const errText = await resp.text();
57
- throw new Error(`PixVerse extendVideo failed (${resp.status}): ${errText}`);
58
- }
59
- const data = await resp.json();
60
- if (data.ErrCode !== 0) {
61
- throw new Error(`PixVerse API error: ${data.ErrMsg || "Unknown error"}`);
62
- }
117
+ }, "extendVideo");
63
118
  const videoId = data?.Resp?.video_id;
64
119
  if (!videoId)
65
120
  throw new Error("PixVerse extend did not return video_id");
@@ -94,7 +149,7 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
94
149
  effectBody.water_mark = !params.watermark;
95
150
  if (params.motionMode)
96
151
  effectBody.motion_mode = params.motionMode;
97
- const resp = await fetch(`${this.baseUrl}/template/generate`, {
152
+ const data = await pixverseFetch(`${this.baseUrl}/template/generate`, {
98
153
  method: "POST",
99
154
  headers: {
100
155
  "Content-Type": "application/json",
@@ -102,15 +157,7 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
102
157
  "Ai-trace-id": traceId,
103
158
  },
104
159
  body: JSON.stringify(effectBody),
105
- });
106
- if (!resp.ok) {
107
- const errText = await resp.text();
108
- throw new Error(`PixVerse effect generation failed (${resp.status}): ${errText}`);
109
- }
110
- const data = await resp.json();
111
- if (data.ErrCode !== 0) {
112
- throw new Error(`PixVerse API error: ${data.ErrMsg || "Unknown error"}`);
113
- }
160
+ }, "effectGeneration");
114
161
  const videoId = data?.Resp?.video_id;
115
162
  if (!videoId)
116
163
  throw new Error("PixVerse effect did not return video_id");
@@ -165,7 +212,7 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
165
212
  body.image_url = params.inputImageUrl;
166
213
  }
167
214
  const endpoint = isImageToVideo ? `${this.baseUrl}/image/generate` : `${this.baseUrl}/text/generate`;
168
- const resp = await fetch(endpoint, {
215
+ const data = await pixverseFetch(endpoint, {
169
216
  method: "POST",
170
217
  headers: {
171
218
  "Content-Type": "application/json",
@@ -173,15 +220,7 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
173
220
  "Ai-trace-id": traceId,
174
221
  },
175
222
  body: JSON.stringify(body),
176
- });
177
- if (!resp.ok) {
178
- const errText = await resp.text();
179
- throw new Error(`PixVerse generateVideo failed (${resp.status}): ${errText}`);
180
- }
181
- const data = await resp.json();
182
- if (data.ErrCode !== 0) {
183
- throw new Error(`PixVerse API error: ${data.ErrMsg || "Unknown error"}`);
184
- }
223
+ }, "generateVideo");
185
224
  const videoId = data?.Resp?.video_id;
186
225
  if (!videoId) {
187
226
  throw new Error("PixVerse API did not return video_id");
@@ -196,21 +235,13 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
196
235
  // =========================================================
197
236
  async checkVideoStatus({ task, outputFilename, outputFilePath = "videos", }) {
198
237
  const traceId = crypto_1.default.randomUUID();
199
- const resp = await fetch(`${this.baseUrl}/result/${task}`, {
238
+ const data = await pixverseFetch(`${this.baseUrl}/result/${task}`, {
200
239
  method: "GET",
201
240
  headers: {
202
241
  "API-KEY": process.env.PIXVERSE_API_KEY,
203
242
  "Ai-trace-id": traceId,
204
243
  },
205
- });
206
- if (!resp.ok) {
207
- const errText = await resp.text();
208
- throw new Error(`PixVerse checkVideoStatus failed (${resp.status}): ${errText}`);
209
- }
210
- const data = await resp.json();
211
- if (data.ErrCode !== 0) {
212
- throw new Error(`PixVerse API error: ${data.ErrMsg || "Unknown error"}`);
213
- }
244
+ }, "checkVideoStatus");
214
245
  const status = data?.Resp?.status;
215
246
  // ---- Status Mapping ----
216
247
  switch (status) {