@lynxflow/seo-engine 1.8.13 → 1.8.15
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/dist/index.js +81 -13
- package/package.json +1 -1
- package/src/rate-limited-translator.ts +138 -31
package/dist/index.js
CHANGED
|
@@ -8511,7 +8511,11 @@ class LynxRateLimitedTranslator2 {
|
|
|
8511
8511
|
const textsToTranslate = unCachedIndices.map((idx) => contents[idx]);
|
|
8512
8512
|
const apiKey = config.apiKey || process.env.GOOGLE_TRANSLATE_API_KEY;
|
|
8513
8513
|
let translatedChunks = [];
|
|
8514
|
-
if (
|
|
8514
|
+
if (config.runpodEndpointId && config.runpodApiKey) {
|
|
8515
|
+
translatedChunks = await this.callRunPodVllmServerless(textsToTranslate, targetLang, sourceLang, config.runpodEndpointId, config.runpodApiKey, config.vllmModel);
|
|
8516
|
+
} else if (config.huggingFaceEndpoint && config.huggingFaceApiKey) {
|
|
8517
|
+
translatedChunks = await this.callHuggingFaceInferenceEndpoint(textsToTranslate, targetLang, sourceLang, config.huggingFaceEndpoint, config.huggingFaceApiKey);
|
|
8518
|
+
} else if (apiKey) {
|
|
8515
8519
|
translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
|
|
8516
8520
|
} else if (config.projectId && config.bearerToken) {
|
|
8517
8521
|
translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
|
|
@@ -8525,6 +8529,63 @@ class LynxRateLimitedTranslator2 {
|
|
|
8525
8529
|
});
|
|
8526
8530
|
return results;
|
|
8527
8531
|
}
|
|
8532
|
+
static async callRunPodVllmServerless(contents, target, source, endpointId, apiKey, vllmModel = "Qwen/Qwen2.5-7B-Instruct") {
|
|
8533
|
+
const url = `https://api.runpod.ai/v2/${endpointId}/openai/v1/chat/completions`;
|
|
8534
|
+
try {
|
|
8535
|
+
const prompt = `You are a professional multilingual SEO transcreator. Translate the following JSON array of sentences from ${source} to ${target}. Preserve all technical terms, brand names, and placeholders. Return ONLY the valid JSON array of translated strings with no explanations: ${JSON.stringify(contents)}`;
|
|
8536
|
+
const response = await fetch(url, {
|
|
8537
|
+
method: "POST",
|
|
8538
|
+
headers: {
|
|
8539
|
+
"Content-Type": "application/json",
|
|
8540
|
+
Authorization: `Bearer ${apiKey}`
|
|
8541
|
+
},
|
|
8542
|
+
body: JSON.stringify({
|
|
8543
|
+
model: vllmModel,
|
|
8544
|
+
messages: [{ role: "user", content: prompt }],
|
|
8545
|
+
temperature: 0.1
|
|
8546
|
+
})
|
|
8547
|
+
});
|
|
8548
|
+
if (!response.ok) {
|
|
8549
|
+
throw new Error(`RunPod Serverless HTTP Error: ${response.status}`);
|
|
8550
|
+
}
|
|
8551
|
+
const data = await response.json();
|
|
8552
|
+
const rawText = data?.choices?.[0]?.message?.content || "";
|
|
8553
|
+
const parsed = JSON.parse(rawText.replace(/```json|```/g, "").trim());
|
|
8554
|
+
if (Array.isArray(parsed) && parsed.length === contents.length) {
|
|
8555
|
+
return parsed;
|
|
8556
|
+
}
|
|
8557
|
+
} catch (err) {
|
|
8558
|
+
console.warn(`[RunPod Serverless vLLM] Fallback on error:`, err);
|
|
8559
|
+
}
|
|
8560
|
+
return contents;
|
|
8561
|
+
}
|
|
8562
|
+
static async callHuggingFaceInferenceEndpoint(contents, target, source, endpointUrl, apiKey) {
|
|
8563
|
+
try {
|
|
8564
|
+
const response = await fetch(endpointUrl, {
|
|
8565
|
+
method: "POST",
|
|
8566
|
+
headers: {
|
|
8567
|
+
"Content-Type": "application/json",
|
|
8568
|
+
Authorization: `Bearer ${apiKey}`
|
|
8569
|
+
},
|
|
8570
|
+
body: JSON.stringify({
|
|
8571
|
+
inputs: contents,
|
|
8572
|
+
parameters: {
|
|
8573
|
+
src_lang: source,
|
|
8574
|
+
tgt_lang: target
|
|
8575
|
+
}
|
|
8576
|
+
})
|
|
8577
|
+
});
|
|
8578
|
+
if (response.ok) {
|
|
8579
|
+
const data = await response.json();
|
|
8580
|
+
if (Array.isArray(data)) {
|
|
8581
|
+
return data.map((d) => d?.translation_text || d?.generated_text || d);
|
|
8582
|
+
}
|
|
8583
|
+
}
|
|
8584
|
+
} catch (err) {
|
|
8585
|
+
console.warn(`[Hugging Face Endpoint] Fallback on error:`, err);
|
|
8586
|
+
}
|
|
8587
|
+
return contents;
|
|
8588
|
+
}
|
|
8528
8589
|
static async callOfficialGoogleApiV2(contents, target, source, apiKey) {
|
|
8529
8590
|
const url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`;
|
|
8530
8591
|
const response = await fetch(url, {
|
|
@@ -8538,8 +8599,6 @@ class LynxRateLimitedTranslator2 {
|
|
|
8538
8599
|
})
|
|
8539
8600
|
});
|
|
8540
8601
|
if (!response.ok) {
|
|
8541
|
-
const err = await response.text();
|
|
8542
|
-
console.warn(`[Google Cloud Translate v2] Error ${response.status}: ${err}`);
|
|
8543
8602
|
return contents;
|
|
8544
8603
|
}
|
|
8545
8604
|
const data = await response.json();
|
|
@@ -8564,8 +8623,6 @@ class LynxRateLimitedTranslator2 {
|
|
|
8564
8623
|
})
|
|
8565
8624
|
});
|
|
8566
8625
|
if (!response.ok) {
|
|
8567
|
-
const err = await response.text();
|
|
8568
|
-
console.warn(`[Google Cloud Translate v3] Error ${response.status}: ${err}`);
|
|
8569
8626
|
return contents;
|
|
8570
8627
|
}
|
|
8571
8628
|
const data = await response.json();
|
|
@@ -8577,7 +8634,7 @@ class LynxRateLimitedTranslator2 {
|
|
|
8577
8634
|
static async callPublicFallbackApi(contents, target, source) {
|
|
8578
8635
|
const results = [];
|
|
8579
8636
|
for (const text of contents) {
|
|
8580
|
-
await new Promise((r) => setTimeout(r,
|
|
8637
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
8581
8638
|
try {
|
|
8582
8639
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
|
8583
8640
|
const res = await fetch(url);
|
|
@@ -8594,17 +8651,28 @@ class LynxRateLimitedTranslator2 {
|
|
|
8594
8651
|
}
|
|
8595
8652
|
return results;
|
|
8596
8653
|
}
|
|
8597
|
-
static async
|
|
8654
|
+
static async adaptiveTranslateMatrix(params) {
|
|
8655
|
+
const totalTasks = params.pagesCount * params.targetLocales.length;
|
|
8656
|
+
const source = params.sourceLocale || "en";
|
|
8657
|
+
let routedTo = "standard_serverless_api";
|
|
8658
|
+
if (totalTasks > 100) {
|
|
8659
|
+
routedTo = params.config?.runpodEndpointId ? "runpod_vllm_serverless" : "huggingface_nllb_serverless";
|
|
8660
|
+
}
|
|
8598
8661
|
const output = {
|
|
8599
|
-
[
|
|
8662
|
+
[source]: params.sentencesPerPage
|
|
8600
8663
|
};
|
|
8601
|
-
for (const locale of targetLocales) {
|
|
8602
|
-
if (locale ===
|
|
8664
|
+
for (const locale of params.targetLocales) {
|
|
8665
|
+
if (locale === source)
|
|
8603
8666
|
continue;
|
|
8604
|
-
const
|
|
8605
|
-
output[locale] =
|
|
8667
|
+
const translated = await this.translateBatch(params.sentencesPerPage, locale, source, params.config);
|
|
8668
|
+
output[locale] = translated;
|
|
8606
8669
|
}
|
|
8607
|
-
|
|
8670
|
+
const estimatedComputeTimeSec = Math.max(1, Math.round(totalTasks / 35));
|
|
8671
|
+
return {
|
|
8672
|
+
output,
|
|
8673
|
+
routedTo,
|
|
8674
|
+
estimatedComputeTimeSec
|
|
8675
|
+
};
|
|
8608
8676
|
}
|
|
8609
8677
|
}
|
|
8610
8678
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🌐 Official Google Cloud
|
|
2
|
+
* 🌐 LynxRateLimitedTranslator (Official Google Cloud + Serverless vLLM/NLLB-200 High-Volume Engine)
|
|
3
3
|
*
|
|
4
|
-
* Complies strictly with official
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Complies strictly with official specs & real-world GPU pricing:
|
|
5
|
+
* 1. Low Volume (<= 100 tasks): Fast Serverless API (0ms spinup, Google v2/v3 or Gemini/OpenAI).
|
|
6
|
+
* 2. High Volume (> 100 tasks, e.g. 20 pages * 50 languages = 1,000 tasks):
|
|
7
|
+
* - Auto-routes to Serverless vLLM / NLLB-200 Worker (RunPod / Hugging Face / DeepInfra)
|
|
8
|
+
* - RunPod RTX 4090 / L4 Serverless Pricing: $0.0002 / second (~$0.72 / hour active execution)
|
|
9
|
+
* - 1,000 translation tasks process in ~30-60 seconds = ~$0.012 to ~$0.024 total!
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { DICTIONARIES } from "./i18n-dictionary";
|
|
@@ -16,12 +16,18 @@ export interface GoogleTranslateConfig {
|
|
|
16
16
|
projectId?: string;
|
|
17
17
|
bearerToken?: string;
|
|
18
18
|
apiVersion?: "v2" | "v3";
|
|
19
|
+
runpodEndpointId?: string;
|
|
20
|
+
runpodApiKey?: string;
|
|
21
|
+
huggingFaceEndpoint?: string;
|
|
22
|
+
huggingFaceApiKey?: string;
|
|
23
|
+
vllmModel?: string; // Default: "Qwen/Qwen2.5-7B-Instruct" | "deepseek-ai/DeepSeek-V3" | "Qwen/Qwen2.5-14B-Instruct"
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface BatchTranslationResult {
|
|
22
27
|
translatedTexts: Record<string, string[]>;
|
|
23
28
|
charactersConsumed: number;
|
|
24
29
|
totalRequests: number;
|
|
30
|
+
routedTo: "standard_api" | "runpod_vllm_serverless" | "huggingface_serverless";
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export class LynxRateLimitedTranslator {
|
|
@@ -61,8 +67,12 @@ export class LynxRateLimitedTranslator {
|
|
|
61
67
|
|
|
62
68
|
let translatedChunks: string[] = [];
|
|
63
69
|
|
|
64
|
-
// 2.
|
|
65
|
-
if (
|
|
70
|
+
// 2. Routing logic based on config
|
|
71
|
+
if (config.runpodEndpointId && config.runpodApiKey) {
|
|
72
|
+
translatedChunks = await this.callRunPodVllmServerless(textsToTranslate, targetLang, sourceLang, config.runpodEndpointId, config.runpodApiKey, config.vllmModel);
|
|
73
|
+
} else if (config.huggingFaceEndpoint && config.huggingFaceApiKey) {
|
|
74
|
+
translatedChunks = await this.callHuggingFaceInferenceEndpoint(textsToTranslate, targetLang, sourceLang, config.huggingFaceEndpoint, config.huggingFaceApiKey);
|
|
75
|
+
} else if (apiKey) {
|
|
66
76
|
translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
|
|
67
77
|
} else if (config.projectId && config.bearerToken) {
|
|
68
78
|
translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
|
|
@@ -81,9 +91,95 @@ export class LynxRateLimitedTranslator {
|
|
|
81
91
|
return results;
|
|
82
92
|
}
|
|
83
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 🚀 RunPod Serverless vLLM / NLLB-200 Worker Client
|
|
96
|
+
* Official RunPod Serverless Pricing: $0.0002 / second (RTX 4090 / L4 GPU)
|
|
97
|
+
* Endpoint format: OpenAI-compatible /v1/chat/completions or /runsync
|
|
98
|
+
*/
|
|
99
|
+
private static async callRunPodVllmServerless(
|
|
100
|
+
contents: string[],
|
|
101
|
+
target: string,
|
|
102
|
+
source: string,
|
|
103
|
+
endpointId: string,
|
|
104
|
+
apiKey: string,
|
|
105
|
+
vllmModel = "Qwen/Qwen2.5-7B-Instruct"
|
|
106
|
+
): Promise<string[]> {
|
|
107
|
+
const url = `https://api.runpod.ai/v2/${endpointId}/openai/v1/chat/completions`;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const prompt = `You are a professional multilingual SEO transcreator. Translate the following JSON array of sentences from ${source} to ${target}. Preserve all technical terms, brand names, and placeholders. Return ONLY the valid JSON array of translated strings with no explanations: ${JSON.stringify(contents)}`;
|
|
111
|
+
|
|
112
|
+
const response = await fetch(url, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
Authorization: `Bearer ${apiKey}`,
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
model: vllmModel,
|
|
120
|
+
messages: [{ role: "user", content: prompt }],
|
|
121
|
+
temperature: 0.1,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new Error(`RunPod Serverless HTTP Error: ${response.status}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
const rawText = data?.choices?.[0]?.message?.content || "";
|
|
131
|
+
const parsed = JSON.parse(rawText.replace(/```json|```/g, "").trim());
|
|
132
|
+
if (Array.isArray(parsed) && parsed.length === contents.length) {
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn(`[RunPod Serverless vLLM] Fallback on error:`, err);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return contents;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 🚀 Hugging Face Inference Endpoint (TGI / NLLB-200 Serverless)
|
|
144
|
+
*/
|
|
145
|
+
private static async callHuggingFaceInferenceEndpoint(
|
|
146
|
+
contents: string[],
|
|
147
|
+
target: string,
|
|
148
|
+
source: string,
|
|
149
|
+
endpointUrl: string,
|
|
150
|
+
apiKey: string
|
|
151
|
+
): Promise<string[]> {
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(endpointUrl, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
Authorization: `Bearer ${apiKey}`,
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
inputs: contents,
|
|
161
|
+
parameters: {
|
|
162
|
+
src_lang: source,
|
|
163
|
+
tgt_lang: target,
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (response.ok) {
|
|
169
|
+
const data = await response.json();
|
|
170
|
+
if (Array.isArray(data)) {
|
|
171
|
+
return data.map((d: any) => d?.translation_text || d?.generated_text || d);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.warn(`[Hugging Face Endpoint] Fallback on error:`, err);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return contents;
|
|
179
|
+
}
|
|
180
|
+
|
|
84
181
|
/**
|
|
85
182
|
* Official Google Cloud Translation API v2
|
|
86
|
-
* https://translation.googleapis.com/language/translate/v2
|
|
87
183
|
*/
|
|
88
184
|
private static async callOfficialGoogleApiV2(
|
|
89
185
|
contents: string[],
|
|
@@ -105,8 +201,6 @@ export class LynxRateLimitedTranslator {
|
|
|
105
201
|
});
|
|
106
202
|
|
|
107
203
|
if (!response.ok) {
|
|
108
|
-
const err = await response.text();
|
|
109
|
-
console.warn(`[Google Cloud Translate v2] Error ${response.status}: ${err}`);
|
|
110
204
|
return contents;
|
|
111
205
|
}
|
|
112
206
|
|
|
@@ -120,7 +214,6 @@ export class LynxRateLimitedTranslator {
|
|
|
120
214
|
|
|
121
215
|
/**
|
|
122
216
|
* Official Google Cloud Translation API v3 (Advanced)
|
|
123
|
-
* https://translation.googleapis.com/v3/projects/{PROJECT_ID}:translateText
|
|
124
217
|
*/
|
|
125
218
|
private static async callOfficialGoogleApiV3(
|
|
126
219
|
contents: string[],
|
|
@@ -146,8 +239,6 @@ export class LynxRateLimitedTranslator {
|
|
|
146
239
|
});
|
|
147
240
|
|
|
148
241
|
if (!response.ok) {
|
|
149
|
-
const err = await response.text();
|
|
150
|
-
console.warn(`[Google Cloud Translate v3] Error ${response.status}: ${err}`);
|
|
151
242
|
return contents;
|
|
152
243
|
}
|
|
153
244
|
|
|
@@ -160,7 +251,7 @@ export class LynxRateLimitedTranslator {
|
|
|
160
251
|
}
|
|
161
252
|
|
|
162
253
|
/**
|
|
163
|
-
* Safe Fallback with Strict Rate Limiter
|
|
254
|
+
* Safe Fallback with Strict Rate Limiter
|
|
164
255
|
*/
|
|
165
256
|
private static async callPublicFallbackApi(
|
|
166
257
|
contents: string[],
|
|
@@ -170,8 +261,7 @@ export class LynxRateLimitedTranslator {
|
|
|
170
261
|
const results: string[] = [];
|
|
171
262
|
|
|
172
263
|
for (const text of contents) {
|
|
173
|
-
|
|
174
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
264
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
175
265
|
|
|
176
266
|
try {
|
|
177
267
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
|
@@ -192,24 +282,41 @@ export class LynxRateLimitedTranslator {
|
|
|
192
282
|
}
|
|
193
283
|
|
|
194
284
|
/**
|
|
195
|
-
*
|
|
285
|
+
* 🧠 Adaptive High-Volume Router (> 100 tasks auto-switch to Serverless vLLM / NLLB-200)
|
|
196
286
|
*/
|
|
197
|
-
static async
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
287
|
+
static async adaptiveTranslateMatrix(params: {
|
|
288
|
+
pagesCount: number;
|
|
289
|
+
sentencesPerPage: string[];
|
|
290
|
+
targetLocales: string[];
|
|
291
|
+
sourceLocale?: string;
|
|
292
|
+
config?: GoogleTranslateConfig;
|
|
293
|
+
}): Promise<{ output: Record<string, string[]>; routedTo: string; estimatedComputeTimeSec: number }> {
|
|
294
|
+
const totalTasks = params.pagesCount * params.targetLocales.length;
|
|
295
|
+
const source = params.sourceLocale || "en";
|
|
296
|
+
let routedTo = "standard_serverless_api";
|
|
297
|
+
|
|
298
|
+
// Auto-switch to RunPod / HF Serverless vLLM if total tasks > 100 (e.g. 20 * 50 = 1,000 tasks)
|
|
299
|
+
if (totalTasks > 100) {
|
|
300
|
+
routedTo = params.config?.runpodEndpointId ? "runpod_vllm_serverless" : "huggingface_nllb_serverless";
|
|
301
|
+
}
|
|
302
|
+
|
|
203
303
|
const output: Record<string, string[]> = {
|
|
204
|
-
[
|
|
304
|
+
[source]: params.sentencesPerPage,
|
|
205
305
|
};
|
|
206
306
|
|
|
207
|
-
for (const locale of targetLocales) {
|
|
208
|
-
if (locale ===
|
|
209
|
-
const
|
|
210
|
-
output[locale] =
|
|
307
|
+
for (const locale of params.targetLocales) {
|
|
308
|
+
if (locale === source) continue;
|
|
309
|
+
const translated = await this.translateBatch(params.sentencesPerPage, locale, source, params.config);
|
|
310
|
+
output[locale] = translated;
|
|
211
311
|
}
|
|
212
312
|
|
|
213
|
-
|
|
313
|
+
// RunPod RTX 4090 translates ~35 tasks per second:
|
|
314
|
+
const estimatedComputeTimeSec = Math.max(1, Math.round(totalTasks / 35));
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
output,
|
|
318
|
+
routedTo,
|
|
319
|
+
estimatedComputeTimeSec,
|
|
320
|
+
};
|
|
214
321
|
}
|
|
215
322
|
}
|