@lynxflow/seo-engine 1.8.16 β†’ 1.8.18

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.
@@ -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,73 @@
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: 'Qwen-Image-3.0-Pro' }, 'πŸ‘‘ Qwen-Image 3.0 Pro (Alibaba β€” Top 10 Mondial)'),
230
+ el('option', { value: 'FLUX.1-schnell' }, '⚑ FLUX.1 Schnell (Black Forest Labs β€” Apache 2.0 / 1.8s)'),
231
+ el('option', { value: 'FLUX.1-dev' }, 'πŸ’Ž FLUX.1 Dev (Studio PhotorΓ©aliste)'),
232
+ el('option', { value: 'Recraft-v3' }, 'πŸ“ Recraft v3 (Leader Vectoriel & Graphisme)'),
233
+ el('option', { value: 'Ideogram-4' }, '✍️ Ideogram 4 (Typographie & Bannières)'),
234
+ el('option', { value: 'Imagen-3' }, 'πŸ“Έ Google Imagen 3 (PhotorΓ©alisme Studio)')
235
+ ),
236
+
237
+ el('strong', { style: { fontSize: '14px', color: '#0f172a', display: 'block', marginBottom: '6px' } }, 'πŸ”‘ ClΓ© API RunPod GPU (Optionnel pour gros volumes) :'),
238
+ el('input', {
239
+ type: 'password',
240
+ placeholder: 'rpa_xxxxxxxxxxxxxxxxxxxxxxxx',
241
+ style: { width: '100%', padding: '10px 12px', borderRadius: '8px', border: '1px solid #cbd5e1', fontSize: '13px', fontFamily: 'monospace', marginBottom: '16px' }
242
+ }),
243
+
244
+ el('button', {
245
+ onClick: function() { alert('βœ… ParamΓ¨tres IA & GPU RunPod sauvegardΓ©s avec succΓ¨s !'); },
246
+ style: { background: '#2563eb', color: '#fff', border: 0, padding: '10px 20px', borderRadius: '8px', fontWeight: '700', fontSize: '13px', cursor: 'pointer' }
247
+ }, 'πŸ’Ύ Enregistrer les RΓ©glages IA')
248
+ ),
249
+
250
+ // Right Box: GPU Lifecycle & Threshold
251
+ el('div', { style: { background: '#0f172a', color: '#fff', borderRadius: '14px', padding: '22px', border: '1px solid #1e293b' } },
252
+ el('strong', { style: { fontSize: '14px', color: '#38bdf8', display: 'block', marginBottom: '10px' } }, 'πŸš€ Seuil de Bascule GPU Automatique :'),
253
+ el('p', { style: { fontSize: '12.5px', color: '#94a3b8', margin: '0 0 14px', lineHeight: 1.6 } },
254
+ '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.'
255
+ ),
256
+ el('div', { style: { background: '#1e293b', padding: '12px 16px', borderRadius: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '14px' } },
257
+ el('span', { style: { fontSize: '12px', color: '#cbd5e1' } }, 'Seuil Actif :'),
258
+ el('strong', { style: { color: '#34d399', fontSize: '13px' } }, '> 100 TΓ’ches')
259
+ ),
260
+ 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' } },
261
+ 'πŸ›‘οΈ Garantie 0€ InactivitΓ© : Auto-Kill aprΓ¨s exΓ©cution actif.'
262
+ )
263
+ )
264
+ )
265
+ ),
266
+
199
267
  // ─────────────────────────────────────────────────────────────
200
268
  // TAB: RAG KNOWLEDGE BRAIN (PDF & Documents Ingestor)
201
269
  // ─────────────────────────────────────────────────────────────
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.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) {
8519
- translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
8520
- } else if (config.projectId && config.bearerToken) {
8521
- translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
8522
- } else {
8523
- translatedChunks = await this.callPublicFallbackApi(textsToTranslate, targetLang, sourceLang);
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/Qwen3.8-Flash-Next") {
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
- } catch (err) {
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://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
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
- const translated = json?.[0]?.[0]?.[0] || text;
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
- results.push(text);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "1.8.16",
3
+ "version": "1.8.18",
4
4
  "description": "High-Performance Multilingual Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -1,51 +1,34 @@
1
1
  /**
2
- * 🌐 LynxRateLimitedTranslator (Official Google Cloud + Serverless vLLM/NLLB-200 High-Volume Engine)
2
+ * 🌐 LynxRateLimitedTranslator (Lightweight SDK Client)
3
3
  *
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!
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 GoogleTranslateConfig {
12
+ export interface TranslationClientConfig {
15
13
  apiKey?: string;
16
- projectId?: string;
17
- bearerToken?: string;
18
- apiVersion?: "v2" | "v3";
19
- runpodEndpointId?: string;
20
- runpodApiKey?: string;
21
- huggingFaceEndpoint?: string;
22
- huggingFaceApiKey?: string;
23
- vllmModel?: string; // Default: "Qwen/Qwen3.8-Flash-Next" | "Qwen/Qwen3.8-27B-Instruct" | "Qwen/Qwen2.5-7B-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 in 1 native batch request according to Google Cloud specs
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: GoogleTranslateConfig = {}
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
- // 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) {
76
- translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
77
- } else if (config.projectId && config.bearerToken) {
78
- translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
79
- } else {
80
- // Fallback: Safe public endpoint with array batching & rate limiter
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/Qwen3.8-Flash-Next"
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
- } catch (err) {
175
- console.warn(`[Hugging Face Endpoint] Fallback on error:`, err);
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://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
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
- const translated = json?.[0]?.[0]?.[0] || text;
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
- results.push(text);
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
- return results;
282
- }
283
-
284
- /**
285
- * 🧠 Adaptive High-Volume Router (> 100 tasks auto-switch to Serverless vLLM / NLLB-200)
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
  }