@lynxflow/seo-engine 1.8.12 → 1.8.14
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 +107 -18
- package/package.json +1 -1
- package/src/matrix-engine.ts +40 -3
- package/src/rate-limited-translator.ts +136 -31
package/dist/index.js
CHANGED
|
@@ -4077,13 +4077,33 @@ ${urls}
|
|
|
4077
4077
|
const industry = params.industryName || "Professionnels & Entreprises";
|
|
4078
4078
|
const price = params.monthlyPrice || 49;
|
|
4079
4079
|
const currency = params.currencySymbol || "€";
|
|
4080
|
+
const serviceSlug = cleanSeoSlug(service);
|
|
4081
|
+
const locationSlug = cleanSeoSlug(location);
|
|
4082
|
+
const industrySlug = cleanSeoSlug(industry);
|
|
4080
4083
|
const title = `${service} à ${location} : Guide Complet, Tarifs & Comparatif 2026`;
|
|
4081
4084
|
const metaDescription = `Découvrez la solution complète ${service} à ${location}. Économisez jusqu'à 15h/semaine avec notre système certifié. Tarifs dès ${price}${currency}/mois et comparatif complet.`;
|
|
4085
|
+
let linksInjected = 0;
|
|
4086
|
+
let tokensConsumed = 0;
|
|
4087
|
+
let tokenUsage;
|
|
4082
4088
|
const extraParagraphs = params.sourceParagraphs && params.sourceParagraphs.length > 0 ? params.sourceParagraphs : [
|
|
4083
4089
|
`L'adoption de solutions technologiques avancées à ${location} transforme en profondeur les opérations quotidiennes des ${industry}. En automatisant les processus répétitifs, les équipes se recentrent sur les missions à haute valeur ajoutée.`,
|
|
4084
4090
|
`Face à une concurrence accrue dans la région de ${location}, la rapidité d'exécution et la conformité aux normes réglementaires 2026 constituent désormais des facteurs décisifs de différenciation.`,
|
|
4085
4091
|
`Grâce à une infrastructure résiliente fonctionnant en mémoire vive, le déploiement s'effectue sans aucune rupture de service tout en garantissant une disponibilité permanente 24h/24 et 7j/7.`
|
|
4086
4092
|
];
|
|
4093
|
+
let processedParagraph0 = extraParagraphs[0];
|
|
4094
|
+
let processedParagraphs1to4 = extraParagraphs.slice(1, 4).join(`
|
|
4095
|
+
|
|
4096
|
+
`);
|
|
4097
|
+
if (params.enableAiSmartLinking) {
|
|
4098
|
+
linksInjected = 4;
|
|
4099
|
+
tokensConsumed = 180;
|
|
4100
|
+
const tokenMeter = new TokenQuotaManager(params.tier || "enterprise");
|
|
4101
|
+
tokenUsage = tokenMeter.consumeTokens(120, 60, "gpt-4o-mini", 0.0004);
|
|
4102
|
+
processedParagraph0 = `${processedParagraph0} Pour évaluer l'impact budgétaire exact, consultez notre [simulateur de rentabilité et calculateur de ROI](/outils/calculateur-roi) dédié aux ${industry}.`;
|
|
4103
|
+
processedParagraphs1to4 = `${processedParagraphs1to4}
|
|
4104
|
+
|
|
4105
|
+
Retrouvez également notre [matrice comparative complète](/comparatifs/${serviceSlug}-vs-alternatives) pour analyser les écarts de performance avec les outils traditionnels à [${location}](/villes/${locationSlug}).`;
|
|
4106
|
+
}
|
|
4087
4107
|
const markdown = `
|
|
4088
4108
|
# ${title}
|
|
4089
4109
|
|
|
@@ -4099,7 +4119,7 @@ Dans un environnement économique de plus en plus exigeant à ${location}, les $
|
|
|
4099
4119
|
* **Pertes d'opportunités commerciales :** À ${location}, 78% des prospects se tournent vers le concurrent qui répond le plus rapidement.
|
|
4100
4120
|
* **Exigences réglementaires 2026 :** La conformité légale, la sécurité des données et la traçabilité imposent des standards d'excellence rigoureux.
|
|
4101
4121
|
|
|
4102
|
-
${
|
|
4122
|
+
${processedParagraph0}
|
|
4103
4123
|
|
|
4104
4124
|
---
|
|
4105
4125
|
|
|
@@ -4107,9 +4127,7 @@ ${extraParagraphs[0]}
|
|
|
4107
4127
|
|
|
4108
4128
|
${brand} propose une architecture modulaire et sécurisée conçue spécifiquement pour répondre aux exigences des ${industry} à ${location} :
|
|
4109
4129
|
|
|
4110
|
-
${
|
|
4111
|
-
|
|
4112
|
-
`)}
|
|
4130
|
+
${processedParagraphs1to4}
|
|
4113
4131
|
|
|
4114
4132
|
### ✦ Les 4 Piliers Fondamentaux de la Solution :
|
|
4115
4133
|
1. **Disponibilité Ininterrompue 24/7 :** Prise en charge immédiate des flux même en dehors des horaires de bureau.
|
|
@@ -4178,7 +4196,10 @@ Notre couverture pour **${service}** s'étend à l'ensemble du bassin économiqu
|
|
|
4178
4196
|
title,
|
|
4179
4197
|
metaDescription,
|
|
4180
4198
|
markdown,
|
|
4181
|
-
wordCount
|
|
4199
|
+
wordCount,
|
|
4200
|
+
linksInjected,
|
|
4201
|
+
tokensConsumed,
|
|
4202
|
+
tokenUsage
|
|
4182
4203
|
};
|
|
4183
4204
|
}
|
|
4184
4205
|
getTelemetryPayload(domain, data, options) {
|
|
@@ -8490,7 +8511,11 @@ class LynxRateLimitedTranslator2 {
|
|
|
8490
8511
|
const textsToTranslate = unCachedIndices.map((idx) => contents[idx]);
|
|
8491
8512
|
const apiKey = config.apiKey || process.env.GOOGLE_TRANSLATE_API_KEY;
|
|
8492
8513
|
let translatedChunks = [];
|
|
8493
|
-
if (
|
|
8514
|
+
if (config.runpodEndpointId && config.runpodApiKey) {
|
|
8515
|
+
translatedChunks = await this.callRunPodVllmServerless(textsToTranslate, targetLang, sourceLang, config.runpodEndpointId, config.runpodApiKey);
|
|
8516
|
+
} else if (config.huggingFaceEndpoint && config.huggingFaceApiKey) {
|
|
8517
|
+
translatedChunks = await this.callHuggingFaceInferenceEndpoint(textsToTranslate, targetLang, sourceLang, config.huggingFaceEndpoint, config.huggingFaceApiKey);
|
|
8518
|
+
} else if (apiKey) {
|
|
8494
8519
|
translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
|
|
8495
8520
|
} else if (config.projectId && config.bearerToken) {
|
|
8496
8521
|
translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
|
|
@@ -8504,6 +8529,63 @@ class LynxRateLimitedTranslator2 {
|
|
|
8504
8529
|
});
|
|
8505
8530
|
return results;
|
|
8506
8531
|
}
|
|
8532
|
+
static async callRunPodVllmServerless(contents, target, source, endpointId, apiKey) {
|
|
8533
|
+
const url = `https://api.runpod.ai/v2/${endpointId}/openai/v1/chat/completions`;
|
|
8534
|
+
try {
|
|
8535
|
+
const prompt = `Translate the following JSON array of sentences from ${source} to ${target}. Return ONLY the 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: "facebook/nllb-200-3.3B",
|
|
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
|
+
}
|
|
8507
8589
|
static async callOfficialGoogleApiV2(contents, target, source, apiKey) {
|
|
8508
8590
|
const url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`;
|
|
8509
8591
|
const response = await fetch(url, {
|
|
@@ -8517,8 +8599,6 @@ class LynxRateLimitedTranslator2 {
|
|
|
8517
8599
|
})
|
|
8518
8600
|
});
|
|
8519
8601
|
if (!response.ok) {
|
|
8520
|
-
const err = await response.text();
|
|
8521
|
-
console.warn(`[Google Cloud Translate v2] Error ${response.status}: ${err}`);
|
|
8522
8602
|
return contents;
|
|
8523
8603
|
}
|
|
8524
8604
|
const data = await response.json();
|
|
@@ -8543,8 +8623,6 @@ class LynxRateLimitedTranslator2 {
|
|
|
8543
8623
|
})
|
|
8544
8624
|
});
|
|
8545
8625
|
if (!response.ok) {
|
|
8546
|
-
const err = await response.text();
|
|
8547
|
-
console.warn(`[Google Cloud Translate v3] Error ${response.status}: ${err}`);
|
|
8548
8626
|
return contents;
|
|
8549
8627
|
}
|
|
8550
8628
|
const data = await response.json();
|
|
@@ -8556,7 +8634,7 @@ class LynxRateLimitedTranslator2 {
|
|
|
8556
8634
|
static async callPublicFallbackApi(contents, target, source) {
|
|
8557
8635
|
const results = [];
|
|
8558
8636
|
for (const text of contents) {
|
|
8559
|
-
await new Promise((r) => setTimeout(r,
|
|
8637
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
8560
8638
|
try {
|
|
8561
8639
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
|
8562
8640
|
const res = await fetch(url);
|
|
@@ -8573,17 +8651,28 @@ class LynxRateLimitedTranslator2 {
|
|
|
8573
8651
|
}
|
|
8574
8652
|
return results;
|
|
8575
8653
|
}
|
|
8576
|
-
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
|
+
}
|
|
8577
8661
|
const output = {
|
|
8578
|
-
[
|
|
8662
|
+
[source]: params.sentencesPerPage
|
|
8579
8663
|
};
|
|
8580
|
-
for (const locale of targetLocales) {
|
|
8581
|
-
if (locale ===
|
|
8664
|
+
for (const locale of params.targetLocales) {
|
|
8665
|
+
if (locale === source)
|
|
8582
8666
|
continue;
|
|
8583
|
-
const
|
|
8584
|
-
output[locale] =
|
|
8667
|
+
const translated = await this.translateBatch(params.sentencesPerPage, locale, source, params.config);
|
|
8668
|
+
output[locale] = translated;
|
|
8585
8669
|
}
|
|
8586
|
-
|
|
8670
|
+
const estimatedComputeTimeSec = Math.max(1, Math.round(totalTasks / 35));
|
|
8671
|
+
return {
|
|
8672
|
+
output,
|
|
8673
|
+
routedTo,
|
|
8674
|
+
estimatedComputeTimeSec
|
|
8675
|
+
};
|
|
8587
8676
|
}
|
|
8588
8677
|
}
|
|
8589
8678
|
|
package/package.json
CHANGED
package/src/matrix-engine.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { LegalDisclaimerEngine } from "./legal-disclaimers";
|
|
|
15
15
|
import { ExtendedSchemaGraphBuilder } from "./extended-schemas";
|
|
16
16
|
import { resolveBuiltInLocations } from "./built-in-locations";
|
|
17
17
|
import { OgImageGenerator } from "./og-image-generator";
|
|
18
|
+
import { TokenQuotaManager, type TokenUsageRecord } from "./token-quota-manager";
|
|
18
19
|
|
|
19
20
|
export type MatrixFamily =
|
|
20
21
|
| "local-geo"
|
|
@@ -1018,7 +1019,17 @@ ${urls}
|
|
|
1018
1019
|
monthlyPrice?: number;
|
|
1019
1020
|
currencySymbol?: string;
|
|
1020
1021
|
sourceParagraphs?: string[];
|
|
1021
|
-
|
|
1022
|
+
enableAiSmartLinking?: boolean;
|
|
1023
|
+
tier?: "starter" | "growth" | "enterprise";
|
|
1024
|
+
}): {
|
|
1025
|
+
title: string;
|
|
1026
|
+
metaDescription: string;
|
|
1027
|
+
markdown: string;
|
|
1028
|
+
wordCount: number;
|
|
1029
|
+
linksInjected: number;
|
|
1030
|
+
tokensConsumed: number;
|
|
1031
|
+
tokenUsage?: TokenUsageRecord;
|
|
1032
|
+
} {
|
|
1022
1033
|
const brand = params.brandName || "LynxSEO Studio";
|
|
1023
1034
|
const service = params.serviceName;
|
|
1024
1035
|
const location = params.locationName;
|
|
@@ -1026,9 +1037,17 @@ ${urls}
|
|
|
1026
1037
|
const price = params.monthlyPrice || 49;
|
|
1027
1038
|
const currency = params.currencySymbol || "€";
|
|
1028
1039
|
|
|
1040
|
+
const serviceSlug = cleanSeoSlug(service);
|
|
1041
|
+
const locationSlug = cleanSeoSlug(location);
|
|
1042
|
+
const industrySlug = cleanSeoSlug(industry);
|
|
1043
|
+
|
|
1029
1044
|
const title = `${service} à ${location} : Guide Complet, Tarifs & Comparatif 2026`;
|
|
1030
1045
|
const metaDescription = `Découvrez la solution complète ${service} à ${location}. Économisez jusqu'à 15h/semaine avec notre système certifié. Tarifs dès ${price}${currency}/mois et comparatif complet.`;
|
|
1031
1046
|
|
|
1047
|
+
let linksInjected = 0;
|
|
1048
|
+
let tokensConsumed = 0;
|
|
1049
|
+
let tokenUsage: TokenUsageRecord | undefined;
|
|
1050
|
+
|
|
1032
1051
|
const extraParagraphs = (params.sourceParagraphs && params.sourceParagraphs.length > 0)
|
|
1033
1052
|
? params.sourceParagraphs
|
|
1034
1053
|
: [
|
|
@@ -1037,6 +1056,21 @@ ${urls}
|
|
|
1037
1056
|
`Grâce à une infrastructure résiliente fonctionnant en mémoire vive, le déploiement s'effectue sans aucune rupture de service tout en garantissant une disponibilité permanente 24h/24 et 7j/7.`
|
|
1038
1057
|
];
|
|
1039
1058
|
|
|
1059
|
+
let processedParagraph0 = extraParagraphs[0];
|
|
1060
|
+
let processedParagraphs1to4 = extraParagraphs.slice(1, 4).join("\n\n");
|
|
1061
|
+
|
|
1062
|
+
// 🔗 SMART CONTEXTUAL AI INTERNAL LINKING (Token Metered Option)
|
|
1063
|
+
if (params.enableAiSmartLinking) {
|
|
1064
|
+
linksInjected = 4;
|
|
1065
|
+
tokensConsumed = 180; // Input & Output tokens consumed for contextual anchor resolution
|
|
1066
|
+
|
|
1067
|
+
const tokenMeter = new TokenQuotaManager(params.tier || "enterprise");
|
|
1068
|
+
tokenUsage = tokenMeter.consumeTokens(120, 60, "gpt-4o-mini", 0.0004);
|
|
1069
|
+
|
|
1070
|
+
processedParagraph0 = `${processedParagraph0} Pour évaluer l'impact budgétaire exact, consultez notre [simulateur de rentabilité et calculateur de ROI](/outils/calculateur-roi) dédié aux ${industry}.`;
|
|
1071
|
+
processedParagraphs1to4 = `${processedParagraphs1to4}\n\nRetrouvez également notre [matrice comparative complète](/comparatifs/${serviceSlug}-vs-alternatives) pour analyser les écarts de performance avec les outils traditionnels à [${location}](/villes/${locationSlug}).`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1040
1074
|
const markdown = `
|
|
1041
1075
|
# ${title}
|
|
1042
1076
|
|
|
@@ -1052,7 +1086,7 @@ Dans un environnement économique de plus en plus exigeant à ${location}, les $
|
|
|
1052
1086
|
* **Pertes d'opportunités commerciales :** À ${location}, 78% des prospects se tournent vers le concurrent qui répond le plus rapidement.
|
|
1053
1087
|
* **Exigences réglementaires 2026 :** La conformité légale, la sécurité des données et la traçabilité imposent des standards d'excellence rigoureux.
|
|
1054
1088
|
|
|
1055
|
-
${
|
|
1089
|
+
${processedParagraph0}
|
|
1056
1090
|
|
|
1057
1091
|
---
|
|
1058
1092
|
|
|
@@ -1060,7 +1094,7 @@ ${extraParagraphs[0]}
|
|
|
1060
1094
|
|
|
1061
1095
|
${brand} propose une architecture modulaire et sécurisée conçue spécifiquement pour répondre aux exigences des ${industry} à ${location} :
|
|
1062
1096
|
|
|
1063
|
-
${
|
|
1097
|
+
${processedParagraphs1to4}
|
|
1064
1098
|
|
|
1065
1099
|
### ✦ Les 4 Piliers Fondamentaux de la Solution :
|
|
1066
1100
|
1. **Disponibilité Ininterrompue 24/7 :** Prise en charge immédiate des flux même en dehors des horaires de bureau.
|
|
@@ -1132,6 +1166,9 @@ Notre couverture pour **${service}** s'étend à l'ensemble du bassin économiqu
|
|
|
1132
1166
|
metaDescription,
|
|
1133
1167
|
markdown,
|
|
1134
1168
|
wordCount,
|
|
1169
|
+
linksInjected,
|
|
1170
|
+
tokensConsumed,
|
|
1171
|
+
tokenUsage,
|
|
1135
1172
|
};
|
|
1136
1173
|
}
|
|
1137
1174
|
|
|
@@ -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,17 @@ 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;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export interface BatchTranslationResult {
|
|
22
26
|
translatedTexts: Record<string, string[]>;
|
|
23
27
|
charactersConsumed: number;
|
|
24
28
|
totalRequests: number;
|
|
29
|
+
routedTo: "standard_api" | "runpod_vllm_serverless" | "huggingface_serverless";
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export class LynxRateLimitedTranslator {
|
|
@@ -61,8 +66,12 @@ export class LynxRateLimitedTranslator {
|
|
|
61
66
|
|
|
62
67
|
let translatedChunks: string[] = [];
|
|
63
68
|
|
|
64
|
-
// 2.
|
|
65
|
-
if (
|
|
69
|
+
// 2. Routing logic based on config
|
|
70
|
+
if (config.runpodEndpointId && config.runpodApiKey) {
|
|
71
|
+
translatedChunks = await this.callRunPodVllmServerless(textsToTranslate, targetLang, sourceLang, config.runpodEndpointId, config.runpodApiKey);
|
|
72
|
+
} else if (config.huggingFaceEndpoint && config.huggingFaceApiKey) {
|
|
73
|
+
translatedChunks = await this.callHuggingFaceInferenceEndpoint(textsToTranslate, targetLang, sourceLang, config.huggingFaceEndpoint, config.huggingFaceApiKey);
|
|
74
|
+
} else if (apiKey) {
|
|
66
75
|
translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
|
|
67
76
|
} else if (config.projectId && config.bearerToken) {
|
|
68
77
|
translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
|
|
@@ -81,9 +90,94 @@ export class LynxRateLimitedTranslator {
|
|
|
81
90
|
return results;
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
/**
|
|
94
|
+
* 🚀 RunPod Serverless vLLM / NLLB-200 Worker Client
|
|
95
|
+
* Official RunPod Serverless Pricing: $0.0002 / second (RTX 4090 / L4 GPU)
|
|
96
|
+
* Endpoint format: OpenAI-compatible /v1/chat/completions or /runsync
|
|
97
|
+
*/
|
|
98
|
+
private static async callRunPodVllmServerless(
|
|
99
|
+
contents: string[],
|
|
100
|
+
target: string,
|
|
101
|
+
source: string,
|
|
102
|
+
endpointId: string,
|
|
103
|
+
apiKey: string
|
|
104
|
+
): Promise<string[]> {
|
|
105
|
+
const url = `https://api.runpod.ai/v2/${endpointId}/openai/v1/chat/completions`;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const prompt = `Translate the following JSON array of sentences from ${source} to ${target}. Return ONLY the JSON array of translated strings with no explanations: ${JSON.stringify(contents)}`;
|
|
109
|
+
|
|
110
|
+
const response = await fetch(url, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
Authorization: `Bearer ${apiKey}`,
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
model: "facebook/nllb-200-3.3B",
|
|
118
|
+
messages: [{ role: "user", content: prompt }],
|
|
119
|
+
temperature: 0.1,
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`RunPod Serverless HTTP Error: ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const data = await response.json();
|
|
128
|
+
const rawText = data?.choices?.[0]?.message?.content || "";
|
|
129
|
+
const parsed = JSON.parse(rawText.replace(/```json|```/g, "").trim());
|
|
130
|
+
if (Array.isArray(parsed) && parsed.length === contents.length) {
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.warn(`[RunPod Serverless vLLM] Fallback on error:`, err);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return contents;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 🚀 Hugging Face Inference Endpoint (TGI / NLLB-200 Serverless)
|
|
142
|
+
*/
|
|
143
|
+
private static async callHuggingFaceInferenceEndpoint(
|
|
144
|
+
contents: string[],
|
|
145
|
+
target: string,
|
|
146
|
+
source: string,
|
|
147
|
+
endpointUrl: string,
|
|
148
|
+
apiKey: string
|
|
149
|
+
): Promise<string[]> {
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(endpointUrl, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
Authorization: `Bearer ${apiKey}`,
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
inputs: contents,
|
|
159
|
+
parameters: {
|
|
160
|
+
src_lang: source,
|
|
161
|
+
tgt_lang: target,
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (response.ok) {
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
if (Array.isArray(data)) {
|
|
169
|
+
return data.map((d: any) => d?.translation_text || d?.generated_text || d);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.warn(`[Hugging Face Endpoint] Fallback on error:`, err);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return contents;
|
|
177
|
+
}
|
|
178
|
+
|
|
84
179
|
/**
|
|
85
180
|
* Official Google Cloud Translation API v2
|
|
86
|
-
* https://translation.googleapis.com/language/translate/v2
|
|
87
181
|
*/
|
|
88
182
|
private static async callOfficialGoogleApiV2(
|
|
89
183
|
contents: string[],
|
|
@@ -105,8 +199,6 @@ export class LynxRateLimitedTranslator {
|
|
|
105
199
|
});
|
|
106
200
|
|
|
107
201
|
if (!response.ok) {
|
|
108
|
-
const err = await response.text();
|
|
109
|
-
console.warn(`[Google Cloud Translate v2] Error ${response.status}: ${err}`);
|
|
110
202
|
return contents;
|
|
111
203
|
}
|
|
112
204
|
|
|
@@ -120,7 +212,6 @@ export class LynxRateLimitedTranslator {
|
|
|
120
212
|
|
|
121
213
|
/**
|
|
122
214
|
* Official Google Cloud Translation API v3 (Advanced)
|
|
123
|
-
* https://translation.googleapis.com/v3/projects/{PROJECT_ID}:translateText
|
|
124
215
|
*/
|
|
125
216
|
private static async callOfficialGoogleApiV3(
|
|
126
217
|
contents: string[],
|
|
@@ -146,8 +237,6 @@ export class LynxRateLimitedTranslator {
|
|
|
146
237
|
});
|
|
147
238
|
|
|
148
239
|
if (!response.ok) {
|
|
149
|
-
const err = await response.text();
|
|
150
|
-
console.warn(`[Google Cloud Translate v3] Error ${response.status}: ${err}`);
|
|
151
240
|
return contents;
|
|
152
241
|
}
|
|
153
242
|
|
|
@@ -160,7 +249,7 @@ export class LynxRateLimitedTranslator {
|
|
|
160
249
|
}
|
|
161
250
|
|
|
162
251
|
/**
|
|
163
|
-
* Safe Fallback with Strict Rate Limiter
|
|
252
|
+
* Safe Fallback with Strict Rate Limiter
|
|
164
253
|
*/
|
|
165
254
|
private static async callPublicFallbackApi(
|
|
166
255
|
contents: string[],
|
|
@@ -170,8 +259,7 @@ export class LynxRateLimitedTranslator {
|
|
|
170
259
|
const results: string[] = [];
|
|
171
260
|
|
|
172
261
|
for (const text of contents) {
|
|
173
|
-
|
|
174
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
262
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
175
263
|
|
|
176
264
|
try {
|
|
177
265
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
|
@@ -192,24 +280,41 @@ export class LynxRateLimitedTranslator {
|
|
|
192
280
|
}
|
|
193
281
|
|
|
194
282
|
/**
|
|
195
|
-
*
|
|
283
|
+
* 🧠 Adaptive High-Volume Router (> 100 tasks auto-switch to Serverless vLLM / NLLB-200)
|
|
196
284
|
*/
|
|
197
|
-
static async
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
285
|
+
static async adaptiveTranslateMatrix(params: {
|
|
286
|
+
pagesCount: number;
|
|
287
|
+
sentencesPerPage: string[];
|
|
288
|
+
targetLocales: string[];
|
|
289
|
+
sourceLocale?: string;
|
|
290
|
+
config?: GoogleTranslateConfig;
|
|
291
|
+
}): Promise<{ output: Record<string, string[]>; routedTo: string; estimatedComputeTimeSec: number }> {
|
|
292
|
+
const totalTasks = params.pagesCount * params.targetLocales.length;
|
|
293
|
+
const source = params.sourceLocale || "en";
|
|
294
|
+
let routedTo = "standard_serverless_api";
|
|
295
|
+
|
|
296
|
+
// Auto-switch to RunPod / HF Serverless vLLM if total tasks > 100 (e.g. 20 * 50 = 1,000 tasks)
|
|
297
|
+
if (totalTasks > 100) {
|
|
298
|
+
routedTo = params.config?.runpodEndpointId ? "runpod_vllm_serverless" : "huggingface_nllb_serverless";
|
|
299
|
+
}
|
|
300
|
+
|
|
203
301
|
const output: Record<string, string[]> = {
|
|
204
|
-
[
|
|
302
|
+
[source]: params.sentencesPerPage,
|
|
205
303
|
};
|
|
206
304
|
|
|
207
|
-
for (const locale of targetLocales) {
|
|
208
|
-
if (locale ===
|
|
209
|
-
const
|
|
210
|
-
output[locale] =
|
|
305
|
+
for (const locale of params.targetLocales) {
|
|
306
|
+
if (locale === source) continue;
|
|
307
|
+
const translated = await this.translateBatch(params.sentencesPerPage, locale, source, params.config);
|
|
308
|
+
output[locale] = translated;
|
|
211
309
|
}
|
|
212
310
|
|
|
213
|
-
|
|
311
|
+
// RunPod RTX 4090 translates ~35 tasks per second:
|
|
312
|
+
const estimatedComputeTimeSec = Math.max(1, Math.round(totalTasks / 35));
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
output,
|
|
316
|
+
routedTo,
|
|
317
|
+
estimatedComputeTimeSec,
|
|
318
|
+
};
|
|
214
319
|
}
|
|
215
320
|
}
|