vidspotai-shared 1.0.50 → 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.
- package/lib/globals/aiModels/providers/google.js +3 -3
- package/lib/globals/schemas.d.ts +1 -0
- package/lib/globals/schemas.d.ts.map +1 -1
- package/lib/globals/schemas.js +51 -4
- package/lib/models/script.model.js +2 -2
- package/lib/services/aiGen/providers/bytedance/bytedance.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/bytedance/bytedance.service.js +25 -1
- package/lib/services/aiGen/providers/google/google.service.d.ts +7 -0
- package/lib/services/aiGen/providers/google/google.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/google/google.service.js +62 -8
- package/lib/services/aiGen/providers/minimax/minimax.service.d.ts +5 -0
- package/lib/services/aiGen/providers/minimax/minimax.service.d.ts.map +1 -1
- package/lib/services/aiGen/providers/minimax/minimax.service.js +105 -13
- package/lib/services/aiGen/providers/runway/runway.service.js +2 -2
- package/lib/services/tts/providers/elevenlabs.service.d.ts +7 -0
- package/lib/services/tts/providers/elevenlabs.service.d.ts.map +1 -1
- package/lib/services/tts/providers/elevenlabs.service.js +91 -32
- package/package.json +1 -1
|
@@ -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-
|
|
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-
|
|
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-
|
|
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 },
|
package/lib/globals/schemas.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/lib/globals/schemas.js
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
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.
|
|
12
|
-
videoModelKey: zod_1.z.
|
|
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;
|
|
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;
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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", {
|
|
81
|
-
|
|
82
|
-
|
|
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("/
|
|
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("/
|
|
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":"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|