vidspotai-shared 1.0.50 → 1.0.52

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.
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.googleConfigs = void 0;
4
4
  exports.googleConfigs = {
5
5
  "google-veo-3.1": {
6
- modelId: "veo-3.1-generate-001",
6
+ modelId: "veo-3.1-generate-preview",
7
7
  type: ["text-to-video", "image-to-video"],
8
8
  fields: {
9
9
  prompt: { required: true },
@@ -20,7 +20,7 @@ exports.googleConfigs = {
20
20
  },
21
21
  },
22
22
  "google-veo-3.1-fast": {
23
- modelId: "veo-3.1-fast-generate-001",
23
+ modelId: "veo-3.1-fast-generate-preview",
24
24
  type: ["text-to-video", "image-to-video"],
25
25
  fields: {
26
26
  prompt: { required: true },
@@ -37,7 +37,7 @@ exports.googleConfigs = {
37
37
  },
38
38
  },
39
39
  "google-veo-3.1-lite": {
40
- modelId: "veo-3.1-lite-generate-001",
40
+ modelId: "veo-3.1-lite-generate-preview",
41
41
  type: ["text-to-video", "image-to-video"],
42
42
  fields: {
43
43
  prompt: { required: true },
@@ -1,3 +1,4 @@
1
1
  import { z } from "zod";
2
2
  export declare const urlSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
3
+ export declare const imageUrlSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
3
4
  //# sourceMappingURL=schemas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/globals/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,SAAS,wDAWpB,CAAC"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/globals/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,SAAS,wDAiBpB,CAAC;AAYH,eAAO,MAAM,cAAc,wDA2BzB,CAAC"}
@@ -1,18 +1,65 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.urlSchema = void 0;
3
+ exports.imageUrlSchema = exports.urlSchema = void 0;
4
4
  const zod_1 = require("zod");
5
+ // Accepts only http(s) URLs. Rejects blob:, data:, file:, javascript:, etc.
6
+ // `new URL()` alone treats blob:https://… as valid, so we explicitly check protocol.
5
7
  exports.urlSchema = zod_1.z.string().transform((val, ctx) => {
6
8
  try {
7
9
  if (!val)
8
10
  throw new Error("No URL");
9
- return new URL(val).toString(); // validates and normalizes
11
+ const parsed = new URL(val);
12
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
13
+ throw new Error(`Unsupported URL scheme: ${parsed.protocol}`);
14
+ }
15
+ return parsed.toString();
10
16
  }
11
- catch {
17
+ catch (err) {
12
18
  ctx.addIssue({
13
19
  code: "custom",
14
- message: "Invalid URL",
20
+ message: err?.message?.startsWith("Unsupported URL scheme")
21
+ ? "URL must start with http:// or https:// (blob: and data: URLs are not supported — please re-upload the file)"
22
+ : "Invalid URL",
15
23
  });
16
24
  return zod_1.z.NEVER;
17
25
  }
18
26
  });
27
+ // File extensions our image-input pipeline accepts. Must match common formats
28
+ // supported by all image-input video providers (ByteDance, MiniMax, Runway, etc.).
29
+ const ALLOWED_IMAGE_EXTENSIONS = new Set([
30
+ "jpg", "jpeg", "png", "webp", "gif", "bmp",
31
+ ]);
32
+ // Stricter URL schema for fields that must point to an image file. Validates
33
+ // http(s) and file extension. Use this for inputImageUrl, avatarImageUrl, etc.
34
+ // to reject PDFs, audio, video, and other accidental wrong-type uploads at the
35
+ // API boundary instead of relying on each provider's error path.
36
+ exports.imageUrlSchema = zod_1.z.string().transform((val, ctx) => {
37
+ try {
38
+ if (!val)
39
+ throw new Error("No URL");
40
+ const parsed = new URL(val);
41
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
42
+ ctx.addIssue({
43
+ code: "custom",
44
+ message: "Image URL must start with http:// or https:// (please re-upload the file)",
45
+ });
46
+ return zod_1.z.NEVER;
47
+ }
48
+ // Strip query string (e.g. Firebase Storage tokens) before checking extension
49
+ const pathname = parsed.pathname.toLowerCase();
50
+ const lastDot = pathname.lastIndexOf(".");
51
+ const extension = lastDot >= 0 ? pathname.slice(lastDot + 1) : "";
52
+ if (!extension || !ALLOWED_IMAGE_EXTENSIONS.has(extension)) {
53
+ ctx.addIssue({
54
+ code: "custom",
55
+ message: `Image must be one of: ${[...ALLOWED_IMAGE_EXTENSIONS].join(", ").toUpperCase()}. Please upload a valid image file.`,
56
+ });
57
+ return zod_1.z.NEVER;
58
+ }
59
+ return parsed.toString();
60
+ }
61
+ catch {
62
+ ctx.addIssue({ code: "custom", message: "Invalid image URL" });
63
+ return zod_1.z.NEVER;
64
+ }
65
+ });
@@ -8,8 +8,8 @@ const scriptModelBase = zod_1.z.object({
8
8
  userId: zod_1.z.string(),
9
9
  prompt: zod_1.z.string(), // original if en other wise en translated
10
10
  originalPrompt: zod_1.z.string().optional(),
11
- durationType: zod_1.z.enum(types_1.EVideoDurationType),
12
- videoModelKey: zod_1.z.enum(aiModels_1.EVideoGenModels),
11
+ durationType: zod_1.z.nativeEnum(types_1.EVideoDurationType),
12
+ videoModelKey: zod_1.z.nativeEnum(aiModels_1.EVideoGenModels),
13
13
  translatedSummary: zod_1.z.string().min(1).optional(), // translated summary
14
14
  createdAt: zod_1.z.date(),
15
15
  updatedAt: zod_1.z.date(),
@@ -1 +1 @@
1
- {"version":3,"file":"bytedance.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/bytedance/bytedance.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAUlB,qBAAa,gBAAiB,SAAQ,wBAAwB;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CACsD;IAI9E,OAAO,CAAC,cAAc;IAOhB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA6G3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA8FjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAoCjD"}
1
+ {"version":3,"file":"bytedance.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/bytedance/bytedance.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAUlB,qBAAa,gBAAiB,SAAQ,wBAAwB;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CACsD;IAI9E,OAAO,CAAC,cAAc;IAOhB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA6G3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA2HjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAoCjD"}
@@ -127,13 +127,37 @@ class ByteDanceService extends baseAiGenProvider_service_1.BaseAiGenProviderServ
127
127
  return { status: types_1.EVideoSceneStatus.PENDING };
128
128
  case "failed":
129
129
  case "expired":
130
- case "cancelled":
130
+ case "cancelled": {
131
+ const errCode = data.error?.code;
132
+ const errMessage = data.error?.message ?? "";
133
+ // Sensitive content (matches sceneMonitor.isContentPolicyError so Slack stays quiet).
134
+ if (errCode?.startsWith("OutputVideoSensitiveContentDetected") ||
135
+ errCode?.startsWith("OutputImageSensitiveContentDetected") ||
136
+ errCode?.startsWith("InputVideoSensitiveContentDetected") ||
137
+ errCode?.startsWith("InputImageSensitiveContentDetected") ||
138
+ errCode?.startsWith("InputTextSensitiveContentDetected")) {
139
+ // Keep raw JSON so the monitor's classifier still matches.
140
+ return {
141
+ status: types_1.EVideoSceneStatus.FAILED,
142
+ errorMessage: JSON.stringify(data.error),
143
+ };
144
+ }
145
+ // Generic InvalidParameter on content.text — ByteDance's moderation pipeline
146
+ // rejects the prompt asynchronously without a more specific code. Preserve the
147
+ // raw error so the classifier can suppress Slack alerts, but flag it clearly.
148
+ if (errCode === "InvalidParameter" && /content\.text/i.test(errMessage)) {
149
+ return {
150
+ status: types_1.EVideoSceneStatus.FAILED,
151
+ errorMessage: JSON.stringify(data.error),
152
+ };
153
+ }
131
154
  return {
132
155
  status: types_1.EVideoSceneStatus.FAILED,
133
156
  errorMessage: data.error
134
157
  ? JSON.stringify(data.error)
135
158
  : "ByteDance video generation failed",
136
159
  };
160
+ }
137
161
  case "succeeded":
138
162
  break;
139
163
  default:
@@ -2,7 +2,14 @@ import { BaseAiGenProviderService } from "../baseAiGenProvider.service";
2
2
  import { CreditUsageParams, VideoGenerationParams, VideoGenerationResult, VideoStatusParams, VideoStatusResult } from "../types";
3
3
  export declare class GoogleService extends BaseAiGenProviderService {
4
4
  private ai;
5
+ private static readonly MAX_RETRY_ATTEMPTS;
5
6
  constructor();
7
+ /**
8
+ * Retries `fn` on transient network errors (undici "fetch failed", ECONNRESET, ETIMEDOUT,
9
+ * DNS hiccups, etc.) with exponential backoff: 1s → 2s → 4s. Non-network errors
10
+ * (auth, validation, quota) bypass retry and surface immediately.
11
+ */
12
+ private withTransientRetry;
6
13
  generateVideo(params: VideoGenerationParams): Promise<VideoGenerationResult>;
7
14
  checkVideoStatus({ task, outputFilename, outputFilePath, }: VideoStatusParams): Promise<VideoStatusResult>;
8
15
  getCreditUsed({ modelKey, duration, resolution, multiClip }: CreditUsageParams): number;
@@ -1 +1 @@
1
- {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAIlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;;IAOlB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqC3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAmEjD,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAiB7G"}
1
+ {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA0DlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA0B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqE3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuFjD,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAiB7G"}
@@ -12,20 +12,105 @@ const types_1 = require("../../../../globals/types");
12
12
  const firebase_1 = require("../../../../libs/firebase");
13
13
  const helpers_1 = require("../../../../utils/helpers");
14
14
  const logger_1 = require("../../../../utils/logger");
15
+ const errors_1 = require("../../../../utils/errors");
15
16
  const helpers_2 = require("../../helpers");
16
17
  const baseAiGenProvider_service_1 = require("../baseAiGenProvider.service");
17
18
  const fs_1 = require("fs");
18
19
  const promises_2 = require("stream/promises");
20
+ // Codes from undici / Node net layer that indicate a transient network failure
21
+ // the request never reached the server, or the server hung up mid-flight.
22
+ // Retrying these is safe (idempotent at the API layer for our use cases) and usually succeeds.
23
+ const TRANSIENT_NETWORK_CODES = new Set([
24
+ "ECONNRESET",
25
+ "ECONNREFUSED",
26
+ "ETIMEDOUT",
27
+ "ENOTFOUND",
28
+ "EAI_AGAIN",
29
+ "EPIPE",
30
+ "UND_ERR_SOCKET",
31
+ "UND_ERR_CONNECT_TIMEOUT",
32
+ "UND_ERR_HEADERS_TIMEOUT",
33
+ "UND_ERR_BODY_TIMEOUT",
34
+ ]);
35
+ // Pick a Veo-compatible image MIME type. Veo accepts image/jpeg, image/png,
36
+ // image/webp. We prefer the upstream Content-Type header (Firebase Storage
37
+ // returns the type set at upload), then fall back to URL extension, then to
38
+ // image/jpeg as a reasonable last resort for phone-camera uploads.
39
+ const VEO_SUPPORTED_IMAGE_MIMES = new Set([
40
+ "image/jpeg",
41
+ "image/png",
42
+ "image/webp",
43
+ ]);
44
+ function pickImageMimeType(contentType, url) {
45
+ const normalized = contentType.split(";")[0]?.trim().toLowerCase();
46
+ if (normalized && VEO_SUPPORTED_IMAGE_MIMES.has(normalized)) {
47
+ return normalized;
48
+ }
49
+ // Map common but non-canonical types
50
+ if (normalized === "image/jpg")
51
+ return "image/jpeg";
52
+ // Extension fallback. Strip query string first (Firebase URLs have ?alt=media).
53
+ const pathOnly = url.split("?")[0]?.toLowerCase() ?? "";
54
+ if (pathOnly.endsWith(".png"))
55
+ return "image/png";
56
+ if (pathOnly.endsWith(".webp"))
57
+ return "image/webp";
58
+ if (pathOnly.endsWith(".jpg") || pathOnly.endsWith(".jpeg"))
59
+ return "image/jpeg";
60
+ // Phone uploads default to JPEG far more often than PNG, so it's a safer
61
+ // last-resort than the previous hardcoded "image/png".
62
+ return "image/jpeg";
63
+ }
64
+ function isTransientFetchError(err) {
65
+ if (!err)
66
+ return false;
67
+ // undici wraps low-level errors as `TypeError: fetch failed` with the real
68
+ // reason on `err.cause`. Treat that exact shape as transient.
69
+ if (err.message === "fetch failed")
70
+ return true;
71
+ const code = err.code ?? err.cause?.code;
72
+ return !!code && TRANSIENT_NETWORK_CODES.has(code);
73
+ }
19
74
  class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService {
20
75
  constructor() {
21
76
  super();
22
77
  this.ai = new genai_1.GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });
23
78
  }
79
+ /**
80
+ * Retries `fn` on transient network errors (undici "fetch failed", ECONNRESET, ETIMEDOUT,
81
+ * DNS hiccups, etc.) with exponential backoff: 1s → 2s → 4s. Non-network errors
82
+ * (auth, validation, quota) bypass retry and surface immediately.
83
+ */
84
+ async withTransientRetry(label, fn) {
85
+ let lastErr;
86
+ for (let attempt = 1; attempt <= GoogleService.MAX_RETRY_ATTEMPTS; attempt++) {
87
+ try {
88
+ return await fn();
89
+ }
90
+ catch (err) {
91
+ lastErr = err;
92
+ const transient = isTransientFetchError(err);
93
+ if (!transient || attempt === GoogleService.MAX_RETRY_ATTEMPTS) {
94
+ throw err;
95
+ }
96
+ const backoffMs = 1000 * 2 ** (attempt - 1); // 1s, 2s, 4s
97
+ logger_1.logger.warn(`Google ${label} transient network error — retrying`, {
98
+ attempt,
99
+ maxAttempts: GoogleService.MAX_RETRY_ATTEMPTS,
100
+ backoffMs,
101
+ message: err.message,
102
+ causeCode: err.cause?.code,
103
+ causeSyscall: err.cause?.syscall,
104
+ });
105
+ await new Promise((r) => setTimeout(r, backoffMs));
106
+ }
107
+ }
108
+ throw lastErr;
109
+ }
24
110
  async generateVideo(params) {
25
111
  (0, helpers_2.validateParams)(params);
26
112
  const modelConfig = aiModels_1.aiModelConfigs[params.modelKey];
27
113
  const modelId = modelConfig.modelId;
28
- // console.log("GoogleService.generateVideo - modelId:", modelId, params);
29
114
  const request = {
30
115
  model: modelId,
31
116
  prompt: params.prompt,
@@ -37,14 +122,33 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
37
122
  },
38
123
  };
39
124
  if (params.inputImageUrl) {
40
- const imgResp = await fetch(params.inputImageUrl);
125
+ const imgResp = await this.withTransientRetry("input-image fetch", () => fetch(params.inputImageUrl));
126
+ // BUG #3 fix: fetch() does NOT throw on 4xx/5xx — it returns a Response
127
+ // with the error body. Without this check we'd encode an HTML error page
128
+ // as if it were image bytes, and Veo would silently reject it.
129
+ if (!imgResp.ok) {
130
+ throw new errors_1.UserFacingError(`Input image could not be downloaded (HTTP ${imgResp.status}). The image URL may have expired or been deleted.`);
131
+ }
41
132
  const imgBuffer = Buffer.from(await imgResp.arrayBuffer());
133
+ // BUG #2 fix: a 0-byte body slips through fetch().ok if the upstream
134
+ // wrote an empty file. Veo treats empty bytes as content-filtered (no
135
+ // error, no video). Surface the real cause to the user.
136
+ if (imgBuffer.length === 0) {
137
+ throw new errors_1.UserFacingError("Input image is empty (0 bytes). Please re-upload the image.");
138
+ }
139
+ // BUG #1 fix: previously hardcoded "image/png" regardless of the actual
140
+ // upload type. Veo rejects JPEG bytes labeled as PNG, with no error
141
+ // detail. Prefer the Content-Type from the response (authoritative —
142
+ // Firebase Storage sets it from upload metadata), fall back to URL
143
+ // extension, then default to JPEG (modern phone-camera default).
144
+ const respContentType = imgResp.headers.get("content-type") ?? "";
145
+ const mimeType = pickImageMimeType(respContentType, params.inputImageUrl);
42
146
  request.image = {
43
- mimeType: "image/png", // or infer from URL (jpg/png)
147
+ mimeType,
44
148
  imageBytes: imgBuffer.toString("base64"),
45
149
  };
46
150
  }
47
- const operation = await this.ai.models.generateVideos(request);
151
+ const operation = await this.withTransientRetry("generateVideos", () => this.ai.models.generateVideos(request));
48
152
  if (!operation || !operation.name) {
49
153
  throw new Error("Failed to initiate video generation task");
50
154
  }
@@ -53,9 +157,7 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
53
157
  async checkVideoStatus({ task, outputFilename, outputFilePath = "videos", }) {
54
158
  const operation = new genai_1.GenerateVideosOperation();
55
159
  operation.name = task;
56
- const result = await this.ai.operations.getVideosOperation({
57
- operation,
58
- });
160
+ const result = await this.withTransientRetry("getVideosOperation", () => this.ai.operations.getVideosOperation({ operation }));
59
161
  if (result.done) {
60
162
  if (result.error) {
61
163
  return {
@@ -65,6 +167,22 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
65
167
  }
66
168
  const videoUri = result.response?.generatedVideos?.[0]?.video?.uri;
67
169
  if (!videoUri) {
170
+ // Veo silently filters content via RAI without setting result.error.
171
+ // When raiMediaFilteredCount > 0, the request was blocked by safety
172
+ // filters. Surface the reasons so we can show the user what to fix,
173
+ // and prefix with `user_input_error:` so sceneMonitor demotes the log
174
+ // level (these are expected user-input failures, not bugs).
175
+ const filteredCount = result.response?.raiMediaFilteredCount ?? 0;
176
+ const filteredReasons = result.response?.raiMediaFilteredReasons ?? [];
177
+ if (filteredCount > 0 || filteredReasons.length > 0) {
178
+ const reasons = filteredReasons.length > 0
179
+ ? filteredReasons.join("; ")
180
+ : "Content blocked by Google safety filters";
181
+ return {
182
+ status: types_1.EVideoSceneStatus.FAILED,
183
+ errorMessage: `user_input_error: ${reasons}`,
184
+ };
185
+ }
68
186
  return {
69
187
  status: types_1.EVideoSceneStatus.FAILED,
70
188
  errorMessage: "No video URL or VideoBytes found in response",
@@ -74,10 +192,10 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
74
192
  const localPath = `${outputFilename}.mp4`; // use /tmp for cloud functions
75
193
  const filePath = `${outputFilePath}/${outputFilename}.mp4`;
76
194
  const file = (0, firebase_1.getBucket)().file(filePath);
77
- await this.ai.files.download({
195
+ await this.withTransientRetry("files.download", () => this.ai.files.download({
78
196
  file: video,
79
197
  downloadPath: localPath,
80
- });
198
+ }));
81
199
  await (0, helpers_1.waitForFile)(localPath, 20000, 500);
82
200
  const readStream = (0, fs_1.createReadStream)(localPath);
83
201
  const writeStream = file.createWriteStream({ contentType: "video/mp4" });
@@ -118,3 +236,4 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
118
236
  }
119
237
  }
120
238
  exports.GoogleService = GoogleService;
239
+ GoogleService.MAX_RETRY_ATTEMPTS = 3;
@@ -1,5 +1,10 @@
1
1
  import { BaseAiGenProviderService } from "../baseAiGenProvider.service";
2
2
  import { CreditUsageParams, VideoGenerationParams, VideoGenerationResult, VideoStatusParams, VideoStatusResult } from "../types";
3
+ /**
4
+ * Maps Minimax base_resp status codes to user-facing error messages.
5
+ * Raw codes like "1026 input new_sensitive" are never shown to users.
6
+ */
7
+ export declare const MINIMAX_USER_INPUT_ERROR_PREFIX = "user_input_error: ";
3
8
  export declare class MinimaxService extends BaseAiGenProviderService {
4
9
  private readonly baseUrl;
5
10
  private readonly apiKey;
@@ -1 +1 @@
1
- {"version":3,"file":"minimax.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/minimax/minimax.service.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAiClB,qBAAa,cAAe,SAAQ,wBAAwB;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAEpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;YAUlB,OAAO;IAiCf,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA4B3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA6EjD,aAAa,CAAC,EACZ,QAAQ,EACR,UAAmB,EACnB,QAAY,EACZ,SAAiB,GAClB,EAAE,iBAAiB,GAAG,MAAM;CAW9B"}
1
+ {"version":3,"file":"minimax.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/minimax/minimax.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAUlB;;;GAGG;AAGH,eAAO,MAAM,+BAA+B,uBAAuB,CAAC;AAmEpE,qBAAa,cAAe,SAAQ,wBAAwB;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAEpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;YAUlB,OAAO;IAuDf,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAmD3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA6EjD,aAAa,CAAC,EACZ,QAAQ,EACR,UAAmB,EACnB,QAAY,EACZ,SAAiB,GAClB,EAAE,iBAAiB,GAAG,MAAM;CAW9B"}
@@ -3,12 +3,13 @@ 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.MinimaxService = void 0;
6
+ exports.MinimaxService = exports.MINIMAX_USER_INPUT_ERROR_PREFIX = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  const aiModels_1 = require("../../../../globals/aiModels");
9
9
  const firebase_1 = require("../../../../libs/firebase");
10
10
  const types_1 = require("../../../../globals/types");
11
11
  const helpers_1 = require("../../helpers");
12
+ const errors_1 = require("../../../../utils/errors");
12
13
  const baseAiGenProvider_service_1 = require("../baseAiGenProvider.service");
13
14
  const helpers_2 = require("../../../../utils/helpers");
14
15
  const logger_1 = require("../../../../utils/logger");
@@ -16,15 +17,33 @@ const logger_1 = require("../../../../utils/logger");
16
17
  * Maps Minimax base_resp status codes to user-facing error messages.
17
18
  * Raw codes like "1026 input new_sensitive" are never shown to users.
18
19
  */
20
+ // Marker prefix on errorMessage so sceneMonitor's classifier can suppress Slack
21
+ // alerts for user-input failures (image too small, unsupported format, etc.).
22
+ exports.MINIMAX_USER_INPUT_ERROR_PREFIX = "user_input_error: ";
19
23
  function minimaxStatusToUserMessage(code, msg) {
20
- const lower = msg.toLowerCase();
24
+ const lower = (msg || "").toLowerCase();
25
+ // Input-validation errors — user can fix these by changing their input.
26
+ // Detected by message text because Minimax does not give a stable code for these.
27
+ if (lower.includes("first_frame_image") || lower.includes("last_frame_image") || lower.includes("subject_reference")) {
28
+ if (lower.includes("minimum pixel") || lower.includes("short side")) {
29
+ return `${exports.MINIMAX_USER_INPUT_ERROR_PREFIX}Your input image is too small. Please upload an image at least 300px on the short side.`;
30
+ }
31
+ if (lower.includes("aspect ratio") || lower.includes("ratio")) {
32
+ return `${exports.MINIMAX_USER_INPUT_ERROR_PREFIX}Your input image's aspect ratio is not supported. Please use a more standard ratio (e.g. 16:9, 9:16, 1:1).`;
33
+ }
34
+ if (lower.includes("format") || lower.includes("type")) {
35
+ return `${exports.MINIMAX_USER_INPUT_ERROR_PREFIX}Your input image format is not supported. Please use JPEG or PNG.`;
36
+ }
37
+ if (lower.includes("size") || lower.includes("too large")) {
38
+ return `${exports.MINIMAX_USER_INPUT_ERROR_PREFIX}Your input image is too large. Please upload a smaller image.`;
39
+ }
40
+ return `${exports.MINIMAX_USER_INPUT_ERROR_PREFIX}Your input image was rejected by the provider: ${msg}`;
41
+ }
21
42
  switch (code) {
22
43
  case 1026:
23
- // Input (prompt or image) flagged by content moderation
24
- return "Your request was declined because the prompt or input image contains content that violates the content policy. Please modify your prompt or use a different image and try again.";
44
+ return `${exports.MINIMAX_USER_INPUT_ERROR_PREFIX}Your request was declined because the prompt or input image contains content that violates the content policy. Please modify your prompt or use a different image and try again.`;
25
45
  case 1027:
26
- // Generated output flagged by content moderation
27
- return "Your request was declined because the generated content was flagged for sensitive material. Please adjust your prompt and try again.";
46
+ return `${exports.MINIMAX_USER_INPUT_ERROR_PREFIX}Your request was declined because the generated content was flagged for sensitive material. Please adjust your prompt and try again.`;
28
47
  case 1002:
29
48
  return "The request was rejected due to a rate limit or quota issue. Please try again in a moment.";
30
49
  case 1004:
@@ -34,6 +53,39 @@ function minimaxStatusToUserMessage(code, msg) {
34
53
  return msg || `Generation failed (code ${code})`;
35
54
  }
36
55
  }
56
+ // Transient axios/network failures that warrant a retry.
57
+ const MINIMAX_TRANSIENT_NETWORK_CODES = new Set([
58
+ "ECONNRESET",
59
+ "ECONNREFUSED",
60
+ "ETIMEDOUT",
61
+ "ENOTFOUND",
62
+ "EAI_AGAIN",
63
+ "ERR_NETWORK",
64
+ "EPIPE",
65
+ "ECONNABORTED",
66
+ ]);
67
+ const MINIMAX_MAX_ATTEMPTS = 3;
68
+ function isMinimaxTransientError(err) {
69
+ if (!err)
70
+ return false;
71
+ if (err.code === "ECONNABORTED")
72
+ return true;
73
+ const code = err.code ?? err.cause?.code;
74
+ if (code && MINIMAX_TRANSIENT_NETWORK_CODES.has(code))
75
+ return true;
76
+ if (typeof err.message === "string" && err.message.toLowerCase().includes("timeout"))
77
+ return true;
78
+ // Retry 429 and 5xx — but only when the upstream is reachable enough to
79
+ // give us a status. Note: our request() uses validateStatus:()=>true so
80
+ // non-2xx is thrown via Error message; check the message string.
81
+ if (typeof err.message === "string") {
82
+ if (err.message.startsWith("Minimax API Error: 429"))
83
+ return true;
84
+ if (/^Minimax API Error: 5\d\d/.test(err.message))
85
+ return true;
86
+ }
87
+ return false;
88
+ }
37
89
  class MinimaxService extends baseAiGenProvider_service_1.BaseAiGenProviderService {
38
90
  constructor() {
39
91
  super();
@@ -57,11 +109,34 @@ class MinimaxService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
57
109
  timeout: 120000, // 2 minutes timeout
58
110
  validateStatus: () => true, // we'll manually handle non-2xx
59
111
  };
60
- const response = await axios_1.default.request(config);
61
- if (response.status < 200 || response.status >= 300) {
62
- throw new Error(`Minimax API Error: ${response.status} ${response.statusText} — ${JSON.stringify(response.data)}`);
112
+ let lastErr;
113
+ for (let attempt = 1; attempt <= MINIMAX_MAX_ATTEMPTS; attempt++) {
114
+ try {
115
+ const response = await axios_1.default.request(config);
116
+ if (response.status < 200 || response.status >= 300) {
117
+ throw new Error(`Minimax API Error: ${response.status} ${response.statusText} — ${JSON.stringify(response.data)}`);
118
+ }
119
+ return response.data;
120
+ }
121
+ catch (err) {
122
+ lastErr = err;
123
+ if (!isMinimaxTransientError(err) || attempt === MINIMAX_MAX_ATTEMPTS) {
124
+ throw err;
125
+ }
126
+ const backoffMs = 500 * Math.pow(2, attempt - 1); // 500ms, 1s
127
+ logger_1.logger.warn(`Minimax request failed transiently — retrying`, {
128
+ attempt,
129
+ maxAttempts: MINIMAX_MAX_ATTEMPTS,
130
+ backoffMs,
131
+ endpoint,
132
+ method,
133
+ code: err.code,
134
+ message: err.message,
135
+ });
136
+ await new Promise((r) => setTimeout(r, backoffMs));
137
+ }
63
138
  }
64
- return response.data;
139
+ throw lastErr;
65
140
  }
66
141
  async generateVideo(params) {
67
142
  (0, helpers_1.validateParams)(params);
@@ -77,9 +152,26 @@ class MinimaxService extends baseAiGenProvider_service_1.BaseAiGenProviderServic
77
152
  duration: params.duration || 6,
78
153
  };
79
154
  const response = await this.request("/v1/video_generation", "POST", requestBody);
80
- logger_1.logger.info("Minimax video generation task response", { taskId: response.task_id });
81
- const taskId = response.task_id;
82
- return { task: taskId, status: types_1.EVideoSceneStatus.TRIGGERED };
155
+ logger_1.logger.info("Minimax video generation task response", {
156
+ taskId: response.task_id,
157
+ statusCode: response.base_resp?.status_code,
158
+ statusMsg: response.base_resp?.status_msg,
159
+ });
160
+ // Minimax returns HTTP 200 with a non-zero base_resp.status_code on soft
161
+ // failures (sensitive content, invalid params, auth issues). In that case
162
+ // task_id is missing — surface a clear error rather than persisting a
163
+ // half-triggered scene with no task handle.
164
+ const baseStatus = response.base_resp?.status_code ?? 0;
165
+ if (baseStatus !== 0 || !response.task_id) {
166
+ const userMessage = minimaxStatusToUserMessage(baseStatus, response.base_resp?.status_msg ?? "");
167
+ const isUserInput = userMessage.startsWith(exports.MINIMAX_USER_INPUT_ERROR_PREFIX);
168
+ const cleaned = userMessage.replace(exports.MINIMAX_USER_INPUT_ERROR_PREFIX, "");
169
+ if (isUserInput) {
170
+ throw new errors_1.UserFacingError(cleaned);
171
+ }
172
+ throw new Error(`Minimax video_generation returned no task_id (code ${baseStatus}): ${cleaned}`);
173
+ }
174
+ return { task: response.task_id, status: types_1.EVideoSceneStatus.TRIGGERED };
83
175
  }
84
176
  async checkVideoStatus({ task, outputFilename, outputFilePath = "videos", }) {
85
177
  const result = await this.request(`/v1/query/video_generation`, "GET", undefined, { task_id: `${task}` });
@@ -67,7 +67,7 @@ class RunwayService extends baseAiGenProvider_service_1.BaseAiGenProviderService
67
67
  ratio: params.aspectRatio,
68
68
  duration: params.duration,
69
69
  };
70
- run = await this.request("/image-to-video", "POST", body);
70
+ run = await this.request("/image_to_video", "POST", body);
71
71
  }
72
72
  // VIDEO → VIDEO (gen4_aleph)
73
73
  else if (modelId === "gen4_aleph") {
@@ -77,7 +77,7 @@ class RunwayService extends baseAiGenProvider_service_1.BaseAiGenProviderService
77
77
  videoUri: params.inputVideoUrl,
78
78
  ratio: params.aspectRatio,
79
79
  };
80
- run = await this.request("/video-to-video", "POST", body);
80
+ run = await this.request("/video_to_video", "POST", body);
81
81
  }
82
82
  else {
83
83
  throw new Error(`Invalid params: Model ${params.modelKey} does not support this generation type`);
@@ -4,6 +4,13 @@ export declare class ElevenLabsService extends BaseTtsProviderService {
4
4
  private readonly apiKey;
5
5
  constructor();
6
6
  generate(params: TtsParams): Promise<TtsResult>;
7
+ /**
8
+ * POST to ElevenLabs with bounded timeout + retry on transient failures.
9
+ * Retries: axios timeout (ECONNABORTED), network errors (ECONNRESET/ETIMEDOUT/etc.),
10
+ * HTTP 429 (rate limit, longer backoff), HTTP 5xx (server errors).
11
+ * Does NOT retry: 400/401/403/404 (caller handles 400 language_code separately).
12
+ */
13
+ private postTtsWithRetry;
7
14
  getVoices(): ITtsVoiceOption[];
8
15
  mapLanguageCode(locale: string): string | undefined;
9
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"elevenlabs.service.d.ts","sourceRoot":"","sources":["../../../../src/services/tts/providers/elevenlabs.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEzF,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAW1B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IAuFrD,SAAS,IAAI,eAAe,EAAE;IAI9B,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAGpD"}
1
+ {"version":3,"file":"elevenlabs.service.d.ts","sourceRoot":"","sources":["../../../../src/services/tts/providers/elevenlabs.service.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAiBzF,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAW1B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IA4DrD;;;;;OAKG;YACW,gBAAgB;IAuC9B,SAAS,IAAI,eAAe,EAAE;IAI9B,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAGpD"}
@@ -7,7 +7,22 @@ exports.ElevenLabsService = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  const elevenlabs_1 = require("../../../globals/ttsModels/providers/elevenlabs");
9
9
  const voices_1 = require("../../../globals/ttsModels/voices");
10
+ const logger_1 = require("../../../utils/logger");
10
11
  const types_1 = require("../types");
12
+ // Per-attempt axios timeout. The Firebase Functions `api` umbrella runs on a 60s
13
+ // budget, so we cap a single attempt at 25s — leaves room for one retry plus
14
+ // post-TTS work (Storage upload, signed URL) inside the function window.
15
+ const PER_ATTEMPT_TIMEOUT_MS = 25000;
16
+ const MAX_ATTEMPTS = 2;
17
+ const TRANSIENT_NETWORK_CODES = new Set([
18
+ "ECONNRESET",
19
+ "ECONNREFUSED",
20
+ "ETIMEDOUT",
21
+ "ENOTFOUND",
22
+ "EAI_AGAIN",
23
+ "ERR_NETWORK",
24
+ "EPIPE",
25
+ ]);
11
26
  class ElevenLabsService extends types_1.BaseTtsProviderService {
12
27
  constructor() {
13
28
  super();
@@ -23,26 +38,19 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
23
38
  const languageCode = params.languageCode
24
39
  ? this.mapLanguageCode(params.languageCode)
25
40
  : undefined;
41
+ const buildBody = (lang) => ({
42
+ text: params.text,
43
+ model_id: elevenlabs_1.elevenlabsConfig.modelId,
44
+ language_code: lang,
45
+ voice_settings: {
46
+ stability: 0.5,
47
+ similarity_boost: 0.75,
48
+ },
49
+ });
26
50
  try {
27
- const response = await axios_1.default.post(`${this.baseUrl}/text-to-speech/${voiceId}`, {
28
- text: params.text,
29
- model_id: elevenlabs_1.elevenlabsConfig.modelId,
30
- language_code: languageCode,
31
- voice_settings: {
32
- stability: 0.5,
33
- similarity_boost: 0.75,
34
- },
35
- }, {
36
- headers: {
37
- "xi-api-key": this.apiKey,
38
- "Content-Type": "application/json",
39
- Accept: "audio/mpeg",
40
- },
41
- responseType: "arraybuffer",
42
- timeout: 60000,
43
- });
51
+ const audioBuffer = await this.postTtsWithRetry(voiceId, buildBody(languageCode));
44
52
  return {
45
- audioBuffer: Buffer.from(response.data),
53
+ audioBuffer,
46
54
  mimeType: "audio/mpeg",
47
55
  extension: "mp3",
48
56
  };
@@ -66,21 +74,9 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
66
74
  if (err.response.status === 400 &&
67
75
  languageCode &&
68
76
  (detail.includes("language_code") || detail.includes("does not support"))) {
69
- const response = await axios_1.default.post(`${this.baseUrl}/text-to-speech/${voiceId}`, {
70
- text: params.text,
71
- model_id: elevenlabs_1.elevenlabsConfig.modelId,
72
- voice_settings: { stability: 0.5, similarity_boost: 0.75 },
73
- }, {
74
- headers: {
75
- "xi-api-key": this.apiKey,
76
- "Content-Type": "application/json",
77
- Accept: "audio/mpeg",
78
- },
79
- responseType: "arraybuffer",
80
- timeout: 60000,
81
- });
77
+ const audioBuffer = await this.postTtsWithRetry(voiceId, buildBody(undefined));
82
78
  return {
83
- audioBuffer: Buffer.from(response.data),
79
+ audioBuffer,
84
80
  mimeType: "audio/mpeg",
85
81
  extension: "mp3",
86
82
  };
@@ -90,6 +86,47 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
90
86
  throw err;
91
87
  }
92
88
  }
89
+ /**
90
+ * POST to ElevenLabs with bounded timeout + retry on transient failures.
91
+ * Retries: axios timeout (ECONNABORTED), network errors (ECONNRESET/ETIMEDOUT/etc.),
92
+ * HTTP 429 (rate limit, longer backoff), HTTP 5xx (server errors).
93
+ * Does NOT retry: 400/401/403/404 (caller handles 400 language_code separately).
94
+ */
95
+ async postTtsWithRetry(voiceId, body) {
96
+ let lastErr;
97
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
98
+ try {
99
+ const response = await axios_1.default.post(`${this.baseUrl}/text-to-speech/${voiceId}`, body, {
100
+ headers: {
101
+ "xi-api-key": this.apiKey,
102
+ "Content-Type": "application/json",
103
+ Accept: "audio/mpeg",
104
+ },
105
+ responseType: "arraybuffer",
106
+ timeout: PER_ATTEMPT_TIMEOUT_MS,
107
+ });
108
+ return Buffer.from(response.data);
109
+ }
110
+ catch (err) {
111
+ lastErr = err;
112
+ if (!isTransientHttpError(err) || attempt === MAX_ATTEMPTS) {
113
+ throw err;
114
+ }
115
+ const status = err.response?.status;
116
+ const backoffMs = status === 429 ? 2000 : 500;
117
+ logger_1.logger.warn("ElevenLabs TTS request failed — retrying", {
118
+ attempt,
119
+ maxAttempts: MAX_ATTEMPTS,
120
+ backoffMs,
121
+ status,
122
+ code: err.code,
123
+ message: err.message,
124
+ });
125
+ await new Promise((r) => setTimeout(r, backoffMs));
126
+ }
127
+ }
128
+ throw lastErr;
129
+ }
93
130
  getVoices() {
94
131
  return voices_1.ELEVENLABS_VOICES;
95
132
  }
@@ -98,3 +135,25 @@ class ElevenLabsService extends types_1.BaseTtsProviderService {
98
135
  }
99
136
  }
100
137
  exports.ElevenLabsService = ElevenLabsService;
138
+ function isTransientHttpError(err) {
139
+ if (!err)
140
+ return false;
141
+ // Axios timeout
142
+ if (err.code === "ECONNABORTED")
143
+ return true;
144
+ if (typeof err.message === "string" && err.message.toLowerCase().includes("timeout"))
145
+ return true;
146
+ // Network-level codes (top-level or wrapped via cause)
147
+ const code = err.code ?? err.cause?.code;
148
+ if (code && TRANSIENT_NETWORK_CODES.has(code))
149
+ return true;
150
+ if (err.name === "AggregateError")
151
+ return true;
152
+ // Server errors / rate limit
153
+ const status = err.response?.status;
154
+ if (status === 429)
155
+ return true;
156
+ if (typeof status === "number" && status >= 500 && status < 600)
157
+ return true;
158
+ return false;
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidspotai-shared",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "exports": {