vidspotai-shared 1.0.49 → 1.0.51

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":"AAaA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA6BlB,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;IAyC3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuEjD,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAiB7G"}
@@ -16,16 +16,71 @@ const helpers_2 = require("../../helpers");
16
16
  const baseAiGenProvider_service_1 = require("../baseAiGenProvider.service");
17
17
  const fs_1 = require("fs");
18
18
  const promises_2 = require("stream/promises");
19
+ // Codes from undici / Node net layer that indicate a transient network failure
20
+ // the request never reached the server, or the server hung up mid-flight.
21
+ // Retrying these is safe (idempotent at the API layer for our use cases) and usually succeeds.
22
+ const TRANSIENT_NETWORK_CODES = new Set([
23
+ "ECONNRESET",
24
+ "ECONNREFUSED",
25
+ "ETIMEDOUT",
26
+ "ENOTFOUND",
27
+ "EAI_AGAIN",
28
+ "EPIPE",
29
+ "UND_ERR_SOCKET",
30
+ "UND_ERR_CONNECT_TIMEOUT",
31
+ "UND_ERR_HEADERS_TIMEOUT",
32
+ "UND_ERR_BODY_TIMEOUT",
33
+ ]);
34
+ function isTransientFetchError(err) {
35
+ if (!err)
36
+ return false;
37
+ // undici wraps low-level errors as `TypeError: fetch failed` with the real
38
+ // reason on `err.cause`. Treat that exact shape as transient.
39
+ if (err.message === "fetch failed")
40
+ return true;
41
+ const code = err.code ?? err.cause?.code;
42
+ return !!code && TRANSIENT_NETWORK_CODES.has(code);
43
+ }
19
44
  class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService {
20
45
  constructor() {
21
46
  super();
22
47
  this.ai = new genai_1.GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });
23
48
  }
49
+ /**
50
+ * Retries `fn` on transient network errors (undici "fetch failed", ECONNRESET, ETIMEDOUT,
51
+ * DNS hiccups, etc.) with exponential backoff: 1s → 2s → 4s. Non-network errors
52
+ * (auth, validation, quota) bypass retry and surface immediately.
53
+ */
54
+ async withTransientRetry(label, fn) {
55
+ let lastErr;
56
+ for (let attempt = 1; attempt <= GoogleService.MAX_RETRY_ATTEMPTS; attempt++) {
57
+ try {
58
+ return await fn();
59
+ }
60
+ catch (err) {
61
+ lastErr = err;
62
+ const transient = isTransientFetchError(err);
63
+ if (!transient || attempt === GoogleService.MAX_RETRY_ATTEMPTS) {
64
+ throw err;
65
+ }
66
+ const backoffMs = 1000 * 2 ** (attempt - 1); // 1s, 2s, 4s
67
+ logger_1.logger.warn(`Google ${label} transient network error — retrying`, {
68
+ attempt,
69
+ maxAttempts: GoogleService.MAX_RETRY_ATTEMPTS,
70
+ backoffMs,
71
+ message: err.message,
72
+ causeCode: err.cause?.code,
73
+ causeSyscall: err.cause?.syscall,
74
+ });
75
+ await new Promise((r) => setTimeout(r, backoffMs));
76
+ }
77
+ }
78
+ throw lastErr;
79
+ }
24
80
  async generateVideo(params) {
25
81
  (0, helpers_2.validateParams)(params);
26
82
  const modelConfig = aiModels_1.aiModelConfigs[params.modelKey];
27
83
  const modelId = modelConfig.modelId;
28
- // console.log("GoogleService.generateVideo - modelId:", modelId, params);
29
84
  const request = {
30
85
  model: modelId,
31
86
  prompt: params.prompt,
@@ -37,14 +92,14 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
37
92
  },
38
93
  };
39
94
  if (params.inputImageUrl) {
40
- const imgResp = await fetch(params.inputImageUrl);
95
+ const imgResp = await this.withTransientRetry("input-image fetch", () => fetch(params.inputImageUrl));
41
96
  const imgBuffer = Buffer.from(await imgResp.arrayBuffer());
42
97
  request.image = {
43
98
  mimeType: "image/png", // or infer from URL (jpg/png)
44
99
  imageBytes: imgBuffer.toString("base64"),
45
100
  };
46
101
  }
47
- const operation = await this.ai.models.generateVideos(request);
102
+ const operation = await this.withTransientRetry("generateVideos", () => this.ai.models.generateVideos(request));
48
103
  if (!operation || !operation.name) {
49
104
  throw new Error("Failed to initiate video generation task");
50
105
  }
@@ -53,9 +108,7 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
53
108
  async checkVideoStatus({ task, outputFilename, outputFilePath = "videos", }) {
54
109
  const operation = new genai_1.GenerateVideosOperation();
55
110
  operation.name = task;
56
- const result = await this.ai.operations.getVideosOperation({
57
- operation,
58
- });
111
+ const result = await this.withTransientRetry("getVideosOperation", () => this.ai.operations.getVideosOperation({ operation }));
59
112
  if (result.done) {
60
113
  if (result.error) {
61
114
  return {
@@ -74,10 +127,10 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
74
127
  const localPath = `${outputFilename}.mp4`; // use /tmp for cloud functions
75
128
  const filePath = `${outputFilePath}/${outputFilename}.mp4`;
76
129
  const file = (0, firebase_1.getBucket)().file(filePath);
77
- await this.ai.files.download({
130
+ await this.withTransientRetry("files.download", () => this.ai.files.download({
78
131
  file: video,
79
132
  downloadPath: localPath,
80
- });
133
+ }));
81
134
  await (0, helpers_1.waitForFile)(localPath, 20000, 500);
82
135
  const readStream = (0, fs_1.createReadStream)(localPath);
83
136
  const writeStream = file.createWriteStream({ contentType: "video/mp4" });
@@ -118,3 +171,4 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
118
171
  }
119
172
  }
120
173
  exports.GoogleService = GoogleService;
174
+ 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.49",
3
+ "version": "1.0.51",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "exports": {