@lynxflow/seo-engine 1.8.15 β 1.8.17
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/connectors/wordpress/lynxseo-admin-app.js +65 -0
- package/dist/index.js +43 -153
- package/package.json +1 -1
- package/src/rate-limited-translator.ts +56 -268
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
// Navigation Menu Items (Inspired by Rank Math Pro Modules Hub)
|
|
40
40
|
var navItems = [
|
|
41
41
|
{ id: 'dashboard', label: 'β‘ Hub des Modules', icon: 'ποΈ' },
|
|
42
|
+
{ id: 'ai_settings', label: 'βοΈ ParamΓ¨tres IA & GPU RunPod', icon: 'π€' },
|
|
42
43
|
{ id: 'rag_brain', label: 'π§ Cerveau RAG & Documents PDF', icon: 'π' },
|
|
43
44
|
{ id: 'google_instant', label: 'π Instant Indexing (Google & Bing)', icon: 'β‘' },
|
|
44
45
|
{ id: 'analytics', label: 'π Search Console & GA4 Live', icon: 'π' },
|
|
@@ -196,6 +197,70 @@
|
|
|
196
197
|
)
|
|
197
198
|
),
|
|
198
199
|
|
|
200
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
201
|
+
// TAB: AI & RUNPOD GPU SETTINGS
|
|
202
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
203
|
+
activeTab === 'ai_settings' && el('div', null,
|
|
204
|
+
el('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', borderBottom: '1px solid #e2e8f0', paddingBottom: '16px' } },
|
|
205
|
+
el('div', null,
|
|
206
|
+
el('h1', { style: { fontSize: '24px', fontWeight: '800', color: '#0f172a', margin: '0 0 4px' } }, 'βοΈ ParamΓ¨tres IA & GPU RunPod On-Demand'),
|
|
207
|
+
el('p', { style: { color: '#64748b', fontSize: '13px', margin: 0 } }, 'Configurez le modèle LLM phare (Qwen 3.8 Max, FLUX.1) et l\'orchestrateur GPU automatique.')
|
|
208
|
+
),
|
|
209
|
+
el('span', { style: { background: '#eff6ff', color: '#1d4ed8', fontSize: '12px', fontWeight: '700', padding: '6px 14px', borderRadius: '999px', border: '1px solid #bfdbfe' } }, 'β‘ Auto-OrchestrΓ©')
|
|
210
|
+
),
|
|
211
|
+
|
|
212
|
+
el('div', { style: { display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: '24px', marginBottom: '24px' } },
|
|
213
|
+
// Left Box: Models & Keys
|
|
214
|
+
el('div', { style: { background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: '14px', padding: '20px' } },
|
|
215
|
+
el('strong', { style: { fontSize: '14px', color: '#0f172a', display: 'block', marginBottom: '6px' } }, 'π§ ModΓ¨le LLM Principal :'),
|
|
216
|
+
el('select', {
|
|
217
|
+
style: { width: '100%', padding: '10px 12px', borderRadius: '8px', border: '1px solid #cbd5e1', fontSize: '13px', marginBottom: '16px', background: '#fff' }
|
|
218
|
+
},
|
|
219
|
+
el('option', { value: 'Qwen/Qwen3.8-Max' }, 'π Qwen 3.8 Max (Alibaba 2.4T Multilingue)'),
|
|
220
|
+
el('option', { value: 'Qwen/Qwen3.8-Flash-Next' }, 'β‘ Qwen 3.8 Flash-Next (MoE InstantanΓ©)'),
|
|
221
|
+
el('option', { value: 'deepseek-ai/DeepSeek-V3' }, 'π DeepSeek V3 / R1'),
|
|
222
|
+
el('option', { value: 'claude-3-7-sonnet' }, 'βοΈ Claude 3.7 Sonnet (Anthropic)')
|
|
223
|
+
),
|
|
224
|
+
|
|
225
|
+
el('strong', { style: { fontSize: '14px', color: '#0f172a', display: 'block', marginBottom: '6px' } }, 'π¨ ModΓ¨le de GΓ©nΓ©ration d\'Images (OG & Photos) :'),
|
|
226
|
+
el('select', {
|
|
227
|
+
style: { width: '100%', padding: '10px 12px', borderRadius: '8px', border: '1px solid #cbd5e1', fontSize: '13px', marginBottom: '16px', background: '#fff' }
|
|
228
|
+
},
|
|
229
|
+
el('option', { value: 'FLUX.1-schnell' }, 'πΌοΈ FLUX.1 Schnell (Black Forest Labs)'),
|
|
230
|
+
el('option', { value: 'Alibaba-Wanx' }, 'π¨ Alibaba Wanx (Tongyi Wanxiang)'),
|
|
231
|
+
el('option', { value: 'SDXL-Lightning' }, 'β‘ Stable Diffusion XL Lightning')
|
|
232
|
+
),
|
|
233
|
+
|
|
234
|
+
el('strong', { style: { fontSize: '14px', color: '#0f172a', display: 'block', marginBottom: '6px' } }, 'π ClΓ© API RunPod GPU (Optionnel pour gros volumes) :'),
|
|
235
|
+
el('input', {
|
|
236
|
+
type: 'password',
|
|
237
|
+
placeholder: 'rpa_xxxxxxxxxxxxxxxxxxxxxxxx',
|
|
238
|
+
style: { width: '100%', padding: '10px 12px', borderRadius: '8px', border: '1px solid #cbd5e1', fontSize: '13px', fontFamily: 'monospace', marginBottom: '16px' }
|
|
239
|
+
}),
|
|
240
|
+
|
|
241
|
+
el('button', {
|
|
242
|
+
onClick: function() { alert('β
Paramètres IA & GPU RunPod sauvegardés avec succès !'); },
|
|
243
|
+
style: { background: '#2563eb', color: '#fff', border: 0, padding: '10px 20px', borderRadius: '8px', fontWeight: '700', fontSize: '13px', cursor: 'pointer' }
|
|
244
|
+
}, 'πΎ Enregistrer les RΓ©glages IA')
|
|
245
|
+
),
|
|
246
|
+
|
|
247
|
+
// Right Box: GPU Lifecycle & Threshold
|
|
248
|
+
el('div', { style: { background: '#0f172a', color: '#fff', borderRadius: '14px', padding: '22px', border: '1px solid #1e293b' } },
|
|
249
|
+
el('strong', { style: { fontSize: '14px', color: '#38bdf8', display: 'block', marginBottom: '10px' } }, 'π Seuil de Bascule GPU Automatique :'),
|
|
250
|
+
el('p', { style: { fontSize: '12.5px', color: '#94a3b8', margin: '0 0 14px', lineHeight: 1.6 } },
|
|
251
|
+
'DΓ¨s que vous traduisez ou gΓ©nΓ©rez plus de 100 tΓ’ches d\'un coup, le backend allume automatiquement un GPU RTX 4090 sur RunPod (0,68β¬/h) et le dΓ©truit immΓ©diatement dΓ¨s la fin du calcul.'
|
|
252
|
+
),
|
|
253
|
+
el('div', { style: { background: '#1e293b', padding: '12px 16px', borderRadius: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '14px' } },
|
|
254
|
+
el('span', { style: { fontSize: '12px', color: '#cbd5e1' } }, 'Seuil Actif :'),
|
|
255
|
+
el('strong', { style: { color: '#34d399', fontSize: '13px' } }, '> 100 TΓ’ches')
|
|
256
|
+
),
|
|
257
|
+
el('div', { style: { background: 'rgba(16,185,129,0.15)', border: '1px solid rgba(16,185,129,0.3)', padding: '10px 14px', borderRadius: '8px', fontSize: '12px', color: '#a7f3d0' } },
|
|
258
|
+
'π‘οΈ Garantie 0β¬ InactivitΓ© : Auto-Kill aprΓ¨s exΓ©cution actif.'
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
),
|
|
263
|
+
|
|
199
264
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
200
265
|
// TAB: RAG KNOWLEDGE BRAIN (PDF & Documents Ingestor)
|
|
201
266
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
package/dist/index.js
CHANGED
|
@@ -8509,171 +8509,61 @@ class LynxRateLimitedTranslator2 {
|
|
|
8509
8509
|
return results;
|
|
8510
8510
|
}
|
|
8511
8511
|
const textsToTranslate = unCachedIndices.map((idx) => contents[idx]);
|
|
8512
|
-
const apiKey = config.apiKey || process.env.GOOGLE_TRANSLATE_API_KEY;
|
|
8513
8512
|
let translatedChunks = [];
|
|
8514
|
-
if (config.
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
}
|
|
8525
|
-
unCachedIndices.forEach((originalIndex, chunkIndex) => {
|
|
8526
|
-
const translatedText = translatedChunks[chunkIndex] || contents[originalIndex];
|
|
8527
|
-
results[originalIndex] = translatedText;
|
|
8528
|
-
this.cache.set(`${targetLang}:${contents[originalIndex].trim()}`, translatedText);
|
|
8529
|
-
});
|
|
8530
|
-
return results;
|
|
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);
|
|
8513
|
+
if (config.backendEndpoint) {
|
|
8514
|
+
try {
|
|
8515
|
+
const res = await fetch(`${config.backendEndpoint}/batch`, {
|
|
8516
|
+
method: "POST",
|
|
8517
|
+
headers: { "Content-Type": "application/json" },
|
|
8518
|
+
body: JSON.stringify({ contents: textsToTranslate, targetLang, sourceLang })
|
|
8519
|
+
});
|
|
8520
|
+
if (res.ok) {
|
|
8521
|
+
const json = await res.json();
|
|
8522
|
+
translatedChunks = json.translations || textsToTranslate;
|
|
8582
8523
|
}
|
|
8524
|
+
} catch {
|
|
8525
|
+
translatedChunks = textsToTranslate;
|
|
8583
8526
|
}
|
|
8584
|
-
}
|
|
8585
|
-
console.warn(`[Hugging Face Endpoint] Fallback on error:`, err);
|
|
8586
|
-
}
|
|
8587
|
-
return contents;
|
|
8588
|
-
}
|
|
8589
|
-
static async callOfficialGoogleApiV2(contents, target, source, apiKey) {
|
|
8590
|
-
const url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`;
|
|
8591
|
-
const response = await fetch(url, {
|
|
8592
|
-
method: "POST",
|
|
8593
|
-
headers: { "Content-Type": "application/json" },
|
|
8594
|
-
body: JSON.stringify({
|
|
8595
|
-
q: contents,
|
|
8596
|
-
target,
|
|
8597
|
-
source: source !== "auto" ? source : undefined,
|
|
8598
|
-
format: "text"
|
|
8599
|
-
})
|
|
8600
|
-
});
|
|
8601
|
-
if (!response.ok) {
|
|
8602
|
-
return contents;
|
|
8603
|
-
}
|
|
8604
|
-
const data = await response.json();
|
|
8605
|
-
if (data?.data?.translations && Array.isArray(data.data.translations)) {
|
|
8606
|
-
return data.data.translations.map((t) => t.translatedText);
|
|
8607
|
-
}
|
|
8608
|
-
return contents;
|
|
8609
|
-
}
|
|
8610
|
-
static async callOfficialGoogleApiV3(contents, target, source, projectId, bearerToken) {
|
|
8611
|
-
const url = `https://translation.googleapis.com/v3/projects/${projectId}:translateText`;
|
|
8612
|
-
const response = await fetch(url, {
|
|
8613
|
-
method: "POST",
|
|
8614
|
-
headers: {
|
|
8615
|
-
"Content-Type": "application/json",
|
|
8616
|
-
Authorization: `Bearer ${bearerToken}`
|
|
8617
|
-
},
|
|
8618
|
-
body: JSON.stringify({
|
|
8619
|
-
contents,
|
|
8620
|
-
targetLanguageCode: target,
|
|
8621
|
-
sourceLanguageCode: source !== "auto" ? source : undefined,
|
|
8622
|
-
mimeType: "text/plain"
|
|
8623
|
-
})
|
|
8624
|
-
});
|
|
8625
|
-
if (!response.ok) {
|
|
8626
|
-
return contents;
|
|
8627
|
-
}
|
|
8628
|
-
const data = await response.json();
|
|
8629
|
-
if (data?.translations && Array.isArray(data.translations)) {
|
|
8630
|
-
return data.translations.map((t) => t.translatedText);
|
|
8631
|
-
}
|
|
8632
|
-
return contents;
|
|
8633
|
-
}
|
|
8634
|
-
static async callPublicFallbackApi(contents, target, source) {
|
|
8635
|
-
const results = [];
|
|
8636
|
-
for (const text of contents) {
|
|
8637
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
8527
|
+
} else if (config.apiKey) {
|
|
8638
8528
|
try {
|
|
8639
|
-
const url = `https://
|
|
8640
|
-
const res = await fetch(url
|
|
8529
|
+
const url = `https://translation.googleapis.com/language/translate/v2?key=${config.apiKey}`;
|
|
8530
|
+
const res = await fetch(url, {
|
|
8531
|
+
method: "POST",
|
|
8532
|
+
headers: { "Content-Type": "application/json" },
|
|
8533
|
+
body: JSON.stringify({ q: textsToTranslate, target: targetLang, source: sourceLang, format: "text" })
|
|
8534
|
+
});
|
|
8641
8535
|
if (res.ok) {
|
|
8642
8536
|
const json = await res.json();
|
|
8643
|
-
|
|
8644
|
-
results.push(translated);
|
|
8645
|
-
} else {
|
|
8646
|
-
results.push(text);
|
|
8537
|
+
translatedChunks = json?.data?.translations?.map((t) => t.translatedText) || textsToTranslate;
|
|
8647
8538
|
}
|
|
8648
8539
|
} catch {
|
|
8649
|
-
|
|
8540
|
+
translatedChunks = textsToTranslate;
|
|
8541
|
+
}
|
|
8542
|
+
} else {
|
|
8543
|
+
translatedChunks = [];
|
|
8544
|
+
for (const text of textsToTranslate) {
|
|
8545
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
8546
|
+
try {
|
|
8547
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
|
|
8548
|
+
const res = await fetch(url);
|
|
8549
|
+
if (res.ok) {
|
|
8550
|
+
const json = await res.json();
|
|
8551
|
+
translatedChunks.push(json?.[0]?.[0]?.[0] || text);
|
|
8552
|
+
} else {
|
|
8553
|
+
translatedChunks.push(text);
|
|
8554
|
+
}
|
|
8555
|
+
} catch {
|
|
8556
|
+
translatedChunks.push(text);
|
|
8557
|
+
}
|
|
8650
8558
|
}
|
|
8651
8559
|
}
|
|
8560
|
+
unCachedIndices.forEach((originalIndex, chunkIndex) => {
|
|
8561
|
+
const translatedText = translatedChunks[chunkIndex] || contents[originalIndex];
|
|
8562
|
+
results[originalIndex] = translatedText;
|
|
8563
|
+
this.cache.set(`${targetLang}:${contents[originalIndex].trim()}`, translatedText);
|
|
8564
|
+
});
|
|
8652
8565
|
return results;
|
|
8653
8566
|
}
|
|
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
|
-
}
|
|
8661
|
-
const output = {
|
|
8662
|
-
[source]: params.sentencesPerPage
|
|
8663
|
-
};
|
|
8664
|
-
for (const locale of params.targetLocales) {
|
|
8665
|
-
if (locale === source)
|
|
8666
|
-
continue;
|
|
8667
|
-
const translated = await this.translateBatch(params.sentencesPerPage, locale, source, params.config);
|
|
8668
|
-
output[locale] = translated;
|
|
8669
|
-
}
|
|
8670
|
-
const estimatedComputeTimeSec = Math.max(1, Math.round(totalTasks / 35));
|
|
8671
|
-
return {
|
|
8672
|
-
output,
|
|
8673
|
-
routedTo,
|
|
8674
|
-
estimatedComputeTimeSec
|
|
8675
|
-
};
|
|
8676
|
-
}
|
|
8677
8567
|
}
|
|
8678
8568
|
|
|
8679
8569
|
// packages/lynx-seo-engine/src/index.ts
|
package/package.json
CHANGED
|
@@ -1,51 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* π LynxRateLimitedTranslator (
|
|
2
|
+
* π LynxRateLimitedTranslator (Lightweight SDK Client)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
*
|
|
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!
|
|
4
|
+
* Provides fast, rate-limited translation for the SDK:
|
|
5
|
+
* 1. Uses local in-memory multilingual dictionary (0ms, 0β¬)
|
|
6
|
+
* 2. Connects to the LynxSEO Backend API for serverless / high-volume translations
|
|
7
|
+
* 3. Supports optional Google Cloud Translation API Key if configured
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import { DICTIONARIES } from "./i18n-dictionary";
|
|
13
11
|
|
|
14
|
-
export interface
|
|
12
|
+
export interface TranslationClientConfig {
|
|
15
13
|
apiKey?: string;
|
|
16
|
-
|
|
17
|
-
bearerToken?: string;
|
|
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"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface BatchTranslationResult {
|
|
27
|
-
translatedTexts: Record<string, string[]>;
|
|
28
|
-
charactersConsumed: number;
|
|
29
|
-
totalRequests: number;
|
|
30
|
-
routedTo: "standard_api" | "runpod_vllm_serverless" | "huggingface_serverless";
|
|
14
|
+
backendEndpoint?: string; // e.g. https://lynxseo.studio/api/translate
|
|
31
15
|
}
|
|
32
16
|
|
|
33
17
|
export class LynxRateLimitedTranslator {
|
|
34
18
|
private static cache: Map<string, string> = new Map();
|
|
35
19
|
|
|
36
20
|
/**
|
|
37
|
-
* Translates an array of strings
|
|
21
|
+
* Translates an array of strings safely
|
|
38
22
|
*/
|
|
39
23
|
static async translateBatch(
|
|
40
24
|
contents: string[],
|
|
41
25
|
targetLang: string,
|
|
42
26
|
sourceLang = "en",
|
|
43
|
-
config:
|
|
27
|
+
config: TranslationClientConfig = {}
|
|
44
28
|
): Promise<string[]> {
|
|
45
29
|
if (!contents || contents.length === 0) return [];
|
|
46
30
|
if (targetLang === sourceLang) return contents;
|
|
47
31
|
|
|
48
|
-
// 1. Check local in-memory cache first to save quota
|
|
49
32
|
const unCachedIndices: number[] = [];
|
|
50
33
|
const results: string[] = new Array(contents.length);
|
|
51
34
|
|
|
@@ -63,260 +46,65 @@ export class LynxRateLimitedTranslator {
|
|
|
63
46
|
}
|
|
64
47
|
|
|
65
48
|
const textsToTranslate = unCachedIndices.map((idx) => contents[idx]);
|
|
66
|
-
const apiKey = config.apiKey || process.env.GOOGLE_TRANSLATE_API_KEY;
|
|
67
|
-
|
|
68
49
|
let translatedChunks: string[] = [];
|
|
69
50
|
|
|
70
|
-
//
|
|
71
|
-
if (config.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
translatedChunks = await this.callPublicFallbackApi(textsToTranslate, targetLang, sourceLang);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Merge translated results back into array and cache them
|
|
85
|
-
unCachedIndices.forEach((originalIndex, chunkIndex) => {
|
|
86
|
-
const translatedText = translatedChunks[chunkIndex] || contents[originalIndex];
|
|
87
|
-
results[originalIndex] = translatedText;
|
|
88
|
-
this.cache.set(`${targetLang}:${contents[originalIndex].trim()}`, translatedText);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return results;
|
|
92
|
-
}
|
|
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);
|
|
51
|
+
// If backend endpoint is configured, forward to LynxSEO backend
|
|
52
|
+
if (config.backendEndpoint) {
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`${config.backendEndpoint}/batch`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({ contents: textsToTranslate, targetLang, sourceLang }),
|
|
58
|
+
});
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
const json = await res.json();
|
|
61
|
+
translatedChunks = json.translations || textsToTranslate;
|
|
172
62
|
}
|
|
63
|
+
} catch {
|
|
64
|
+
translatedChunks = textsToTranslate;
|
|
173
65
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return contents;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Official Google Cloud Translation API v2
|
|
183
|
-
*/
|
|
184
|
-
private static async callOfficialGoogleApiV2(
|
|
185
|
-
contents: string[],
|
|
186
|
-
target: string,
|
|
187
|
-
source: string,
|
|
188
|
-
apiKey: string
|
|
189
|
-
): Promise<string[]> {
|
|
190
|
-
const url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`;
|
|
191
|
-
|
|
192
|
-
const response = await fetch(url, {
|
|
193
|
-
method: "POST",
|
|
194
|
-
headers: { "Content-Type": "application/json" },
|
|
195
|
-
body: JSON.stringify({
|
|
196
|
-
q: contents,
|
|
197
|
-
target,
|
|
198
|
-
source: source !== "auto" ? source : undefined,
|
|
199
|
-
format: "text",
|
|
200
|
-
}),
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
if (!response.ok) {
|
|
204
|
-
return contents;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const data = await response.json();
|
|
208
|
-
if (data?.data?.translations && Array.isArray(data.data.translations)) {
|
|
209
|
-
return data.data.translations.map((t: { translatedText: string }) => t.translatedText);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return contents;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Official Google Cloud Translation API v3 (Advanced)
|
|
217
|
-
*/
|
|
218
|
-
private static async callOfficialGoogleApiV3(
|
|
219
|
-
contents: string[],
|
|
220
|
-
target: string,
|
|
221
|
-
source: string,
|
|
222
|
-
projectId: string,
|
|
223
|
-
bearerToken: string
|
|
224
|
-
): Promise<string[]> {
|
|
225
|
-
const url = `https://translation.googleapis.com/v3/projects/${projectId}:translateText`;
|
|
226
|
-
|
|
227
|
-
const response = await fetch(url, {
|
|
228
|
-
method: "POST",
|
|
229
|
-
headers: {
|
|
230
|
-
"Content-Type": "application/json",
|
|
231
|
-
Authorization: `Bearer ${bearerToken}`,
|
|
232
|
-
},
|
|
233
|
-
body: JSON.stringify({
|
|
234
|
-
contents,
|
|
235
|
-
targetLanguageCode: target,
|
|
236
|
-
sourceLanguageCode: source !== "auto" ? source : undefined,
|
|
237
|
-
mimeType: "text/plain",
|
|
238
|
-
}),
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
if (!response.ok) {
|
|
242
|
-
return contents;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const data = await response.json();
|
|
246
|
-
if (data?.translations && Array.isArray(data.translations)) {
|
|
247
|
-
return data.translations.map((t: { translatedText: string }) => t.translatedText);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return contents;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Safe Fallback with Strict Rate Limiter
|
|
255
|
-
*/
|
|
256
|
-
private static async callPublicFallbackApi(
|
|
257
|
-
contents: string[],
|
|
258
|
-
target: string,
|
|
259
|
-
source: string
|
|
260
|
-
): Promise<string[]> {
|
|
261
|
-
const results: string[] = [];
|
|
262
|
-
|
|
263
|
-
for (const text of contents) {
|
|
264
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
265
|
-
|
|
66
|
+
} else if (config.apiKey) {
|
|
67
|
+
// Direct Google Cloud API v2
|
|
266
68
|
try {
|
|
267
|
-
const url = `https://
|
|
268
|
-
const res = await fetch(url
|
|
69
|
+
const url = `https://translation.googleapis.com/language/translate/v2?key=${config.apiKey}`;
|
|
70
|
+
const res = await fetch(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ q: textsToTranslate, target: targetLang, source: sourceLang, format: "text" }),
|
|
74
|
+
});
|
|
269
75
|
if (res.ok) {
|
|
270
76
|
const json = await res.json();
|
|
271
|
-
|
|
272
|
-
results.push(translated);
|
|
273
|
-
} else {
|
|
274
|
-
results.push(text);
|
|
77
|
+
translatedChunks = json?.data?.translations?.map((t: any) => t.translatedText) || textsToTranslate;
|
|
275
78
|
}
|
|
276
79
|
} catch {
|
|
277
|
-
|
|
80
|
+
translatedChunks = textsToTranslate;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Fallback: Safe rate-limited public gateway
|
|
84
|
+
translatedChunks = [];
|
|
85
|
+
for (const text of textsToTranslate) {
|
|
86
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
87
|
+
try {
|
|
88
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
|
|
89
|
+
const res = await fetch(url);
|
|
90
|
+
if (res.ok) {
|
|
91
|
+
const json = await res.json();
|
|
92
|
+
translatedChunks.push(json?.[0]?.[0]?.[0] || text);
|
|
93
|
+
} else {
|
|
94
|
+
translatedChunks.push(text);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
translatedChunks.push(text);
|
|
98
|
+
}
|
|
278
99
|
}
|
|
279
100
|
}
|
|
280
101
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
*/
|
|
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
|
-
|
|
303
|
-
const output: Record<string, string[]> = {
|
|
304
|
-
[source]: params.sentencesPerPage,
|
|
305
|
-
};
|
|
306
|
-
|
|
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;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// RunPod RTX 4090 translates ~35 tasks per second:
|
|
314
|
-
const estimatedComputeTimeSec = Math.max(1, Math.round(totalTasks / 35));
|
|
102
|
+
unCachedIndices.forEach((originalIndex, chunkIndex) => {
|
|
103
|
+
const translatedText = translatedChunks[chunkIndex] || contents[originalIndex];
|
|
104
|
+
results[originalIndex] = translatedText;
|
|
105
|
+
this.cache.set(`${targetLang}:${contents[originalIndex].trim()}`, translatedText);
|
|
106
|
+
});
|
|
315
107
|
|
|
316
|
-
return
|
|
317
|
-
output,
|
|
318
|
-
routedTo,
|
|
319
|
-
estimatedComputeTimeSec,
|
|
320
|
-
};
|
|
108
|
+
return results;
|
|
321
109
|
}
|
|
322
110
|
}
|