locai 2.1.0
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/README.md +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- package/package.json +80 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import ProviderFactory from "../core/provider-factory.js";
|
|
2
|
+
import rateLimiter from "./rate-limiter.js";
|
|
3
|
+
import { LRUCache } from "lru-cache";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
class AIContextAnalyzer {
|
|
7
|
+
config;
|
|
8
|
+
cache;
|
|
9
|
+
categoryKeywords;
|
|
10
|
+
stats;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.cache = new LRUCache({
|
|
14
|
+
max: config.cacheSize || 500,
|
|
15
|
+
ttl: config.cacheTTL || 1000 * 60 * 60 * 24,
|
|
16
|
+
updateAgeOnGet: true,
|
|
17
|
+
allowStale: true,
|
|
18
|
+
});
|
|
19
|
+
this.categoryKeywords = {};
|
|
20
|
+
if (config.categories) {
|
|
21
|
+
Object.entries(config.categories).forEach(([category, data]) => {
|
|
22
|
+
if (data.keywords && Array.isArray(data.keywords)) {
|
|
23
|
+
this.categoryKeywords[category] = new Set(data.keywords.map((k) => k.toLowerCase()));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
this.stats = {
|
|
28
|
+
totalAnalyzed: 0,
|
|
29
|
+
aiCalls: 0,
|
|
30
|
+
cacheHits: 0,
|
|
31
|
+
keywordMatches: 0,
|
|
32
|
+
newCategories: 0,
|
|
33
|
+
errors: 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Batch analysis for multiple texts - much more efficient
|
|
37
|
+
async analyzeBatch(texts, apiProvider = "openai") {
|
|
38
|
+
if (!texts || !Array.isArray(texts) || !this.config.enabled) {
|
|
39
|
+
return texts.map(() => null);
|
|
40
|
+
}
|
|
41
|
+
const results = [];
|
|
42
|
+
const aiTexts = [];
|
|
43
|
+
const aiIndices = [];
|
|
44
|
+
// First pass: keyword matching and cache lookup
|
|
45
|
+
for (let i = 0; i < texts.length; i++) {
|
|
46
|
+
const text = texts[i];
|
|
47
|
+
if (!text) {
|
|
48
|
+
results[i] = null;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (text.length < (this.config.minTextLength || 10)) {
|
|
52
|
+
results[i] = this._fastKeywordMatch(text);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const cacheKey = this._getCacheKey(text);
|
|
56
|
+
if (this.cache.has(cacheKey)) {
|
|
57
|
+
this.stats.cacheHits++;
|
|
58
|
+
results[i] = this.cache.get(cacheKey) || null;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const keywordMatch = this._fastKeywordMatch(text);
|
|
62
|
+
if (keywordMatch && keywordMatch.confidence > 0.85) {
|
|
63
|
+
this.stats.keywordMatches++;
|
|
64
|
+
this.cache.set(cacheKey, keywordMatch);
|
|
65
|
+
results[i] = keywordMatch;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Need AI analysis
|
|
69
|
+
if (this.config.useAI) {
|
|
70
|
+
aiTexts.push(text);
|
|
71
|
+
aiIndices.push(i);
|
|
72
|
+
results[i] = keywordMatch || this._getDefaultContext();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
results[i] = keywordMatch || this._getDefaultContext();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Batch AI analysis if needed
|
|
79
|
+
if (aiTexts.length > 0 && this.config.useAI) {
|
|
80
|
+
try {
|
|
81
|
+
const batchResults = await this._batchAIAnalysis(aiTexts, apiProvider);
|
|
82
|
+
for (let i = 0; i < batchResults.length; i++) {
|
|
83
|
+
const originalIndex = aiIndices[i];
|
|
84
|
+
const aiResult = batchResults[i];
|
|
85
|
+
if (aiResult) {
|
|
86
|
+
results[originalIndex] = aiResult;
|
|
87
|
+
const cacheKey = this._getCacheKey(aiTexts[i]);
|
|
88
|
+
this.cache.set(cacheKey, aiResult);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error("Batch AI analysis failed:", error.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
// Single text analysis (legacy support)
|
|
99
|
+
async analyzeContext(text, apiProvider = "openai") {
|
|
100
|
+
const results = await this.analyzeBatch([text], apiProvider);
|
|
101
|
+
return results[0];
|
|
102
|
+
}
|
|
103
|
+
// Efficient batch AI analysis
|
|
104
|
+
async _batchAIAnalysis(texts, apiProvider) {
|
|
105
|
+
if (texts.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
this.stats.totalAnalyzed += texts.length;
|
|
108
|
+
this.stats.aiCalls++; // Only one API call for the batch
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
+
const provider = ProviderFactory.getProvider(apiProvider, true, this.config);
|
|
111
|
+
if (!provider) {
|
|
112
|
+
throw new Error("AI provider not available for context analysis");
|
|
113
|
+
}
|
|
114
|
+
const batchPrompt = this._createBatchAnalysisPrompt(texts);
|
|
115
|
+
const result = await rateLimiter.enqueue(apiProvider.toLowerCase(), () => provider.analyze(batchPrompt, {
|
|
116
|
+
...this.config.analysisOptions,
|
|
117
|
+
maxTokens: Math.min(4000, this.config.analysisOptions?.maxTokens || 2000),
|
|
118
|
+
timeout: this.config.analysisOptions?.timeout || 10000,
|
|
119
|
+
retries: this.config.analysisOptions?.retries || 2,
|
|
120
|
+
}));
|
|
121
|
+
return this._parseBatchAnalysisResult(result, texts.length);
|
|
122
|
+
}
|
|
123
|
+
// Create batch analysis prompt for multiple texts
|
|
124
|
+
_createBatchAnalysisPrompt(texts) {
|
|
125
|
+
const categories = Object.keys(this.config.categories || {}).join(", ");
|
|
126
|
+
const maxTextLength = 200; // Shorter for batch processing
|
|
127
|
+
const truncatedTexts = texts
|
|
128
|
+
.map((text, index) => {
|
|
129
|
+
const truncated = text.length > maxTextLength ? text.substring(0, maxTextLength) + "..." : text;
|
|
130
|
+
return `${index + 1}. "${truncated}"`;
|
|
131
|
+
})
|
|
132
|
+
.join("\n");
|
|
133
|
+
return `
|
|
134
|
+
TASK: Analyze the following ${texts.length} texts and determine their context categories. Be concise.
|
|
135
|
+
|
|
136
|
+
TEXTS TO ANALYZE:
|
|
137
|
+
${truncatedTexts}
|
|
138
|
+
|
|
139
|
+
AVAILABLE CATEGORIES: ${categories}
|
|
140
|
+
|
|
141
|
+
INSTRUCTIONS:
|
|
142
|
+
1. For each text, identify the primary context category
|
|
143
|
+
2. Provide a confidence score (0.0-1.0)
|
|
144
|
+
3. Be concise - this is batch processing
|
|
145
|
+
|
|
146
|
+
FORMAT YOUR RESPONSE AS JSON ARRAY:
|
|
147
|
+
[
|
|
148
|
+
{"index": 1, "category": "category_name", "confidence": 0.0-1.0},
|
|
149
|
+
{"index": 2, "category": "category_name", "confidence": 0.0-1.0},
|
|
150
|
+
...
|
|
151
|
+
]
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
_createAnalysisPrompt(text) {
|
|
155
|
+
const categories = Object.keys(this.config.categories || {}).join(", ");
|
|
156
|
+
const maxTextLength = this.config.analysisOptions?.maxTokens
|
|
157
|
+
? Math.min(1500, this.config.analysisOptions.maxTokens * 5)
|
|
158
|
+
: 1500;
|
|
159
|
+
const truncatedText = text.length > maxTextLength ? text.substring(0, maxTextLength) + "..." : text;
|
|
160
|
+
return `
|
|
161
|
+
TASK: Analyze the following text and determine its context category.
|
|
162
|
+
|
|
163
|
+
TEXT TO ANALYZE:
|
|
164
|
+
"""
|
|
165
|
+
${truncatedText}
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
AVAILABLE CATEGORIES: ${categories}${this.config.allowNewCategories ? ", or suggest a new category if none of these fit" : ""}
|
|
169
|
+
|
|
170
|
+
INSTRUCTIONS:
|
|
171
|
+
1. Identify the primary context category of the text
|
|
172
|
+
2. Provide a confidence score (0.0-1.0)
|
|
173
|
+
3. Suggest 3-5 keywords that are relevant to this text
|
|
174
|
+
4. Provide a brief explanation of your categorization
|
|
175
|
+
|
|
176
|
+
FORMAT YOUR RESPONSE AS JSON:
|
|
177
|
+
{
|
|
178
|
+
"category": "category_name",
|
|
179
|
+
"confidence": 0.0-1.0,
|
|
180
|
+
"keywords": ["keyword1", "keyword2", "keyword3"],
|
|
181
|
+
"explanation": "Brief explanation of why this category was chosen"
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
_createSimplifiedAnalysisPrompt(text) {
|
|
186
|
+
const categories = Object.keys(this.config.categories || {}).join(", ");
|
|
187
|
+
return `
|
|
188
|
+
TASK: Categorize the following text. Be concise.
|
|
189
|
+
|
|
190
|
+
TEXT: "${text}"
|
|
191
|
+
|
|
192
|
+
CATEGORIES: ${categories}${this.config.allowNewCategories ? " (or suggest new)" : ""}
|
|
193
|
+
|
|
194
|
+
RESPONSE FORMAT:
|
|
195
|
+
{
|
|
196
|
+
"category": "category_name",
|
|
197
|
+
"confidence": 0.0-1.0,
|
|
198
|
+
"keywords": ["keyword1", "keyword2", "keyword3"]
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
// Parse batch analysis results
|
|
203
|
+
_parseBatchAnalysisResult(result, expectedCount) {
|
|
204
|
+
try {
|
|
205
|
+
const jsonMatch = result.match(/\[[\s\S]*?\]/);
|
|
206
|
+
if (!jsonMatch) {
|
|
207
|
+
throw new Error("No valid JSON array found in AI response");
|
|
208
|
+
}
|
|
209
|
+
let analysisArray;
|
|
210
|
+
try {
|
|
211
|
+
analysisArray = JSON.parse(jsonMatch[0]);
|
|
212
|
+
}
|
|
213
|
+
catch (jsonError) {
|
|
214
|
+
console.error("JSON parsing error:", jsonError);
|
|
215
|
+
const cleanedJson = jsonMatch[0]
|
|
216
|
+
.replace(/,\s*]/g, "]")
|
|
217
|
+
.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');
|
|
218
|
+
analysisArray = JSON.parse(cleanedJson);
|
|
219
|
+
}
|
|
220
|
+
if (!Array.isArray(analysisArray)) {
|
|
221
|
+
throw new Error("Response is not an array");
|
|
222
|
+
}
|
|
223
|
+
// Create results array with proper indexing
|
|
224
|
+
const results = new Array(expectedCount).fill(null);
|
|
225
|
+
for (const item of analysisArray) {
|
|
226
|
+
if (!item.category || typeof item.confidence !== "number" || !item.index) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const index = item.index - 1; // Convert to 0-based index
|
|
230
|
+
if (index < 0 || index >= expectedCount) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
let category = item.category.toLowerCase().trim();
|
|
234
|
+
if (this.config.categories &&
|
|
235
|
+
!this.config.categories[category] &&
|
|
236
|
+
this.config.allowNewCategories) {
|
|
237
|
+
this._saveNewCategory(category, item.keywords || []);
|
|
238
|
+
this.stats.newCategories++;
|
|
239
|
+
}
|
|
240
|
+
else if (this.config.categories && !this.config.categories[category]) {
|
|
241
|
+
category = this._findClosestCategory(category, item.keywords || []);
|
|
242
|
+
}
|
|
243
|
+
results[index] = {
|
|
244
|
+
category,
|
|
245
|
+
confidence: item.confidence,
|
|
246
|
+
keywords: item.keywords || [],
|
|
247
|
+
explanation: item.explanation || "Batch analysis",
|
|
248
|
+
prompt: this.config.categories?.[category]?.prompt || this.config.fallback?.prompt,
|
|
249
|
+
method: "batch_ai",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return results;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
console.error("Error parsing batch AI analysis result:", error.message);
|
|
256
|
+
return new Array(expectedCount).fill(null);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
_parseAnalysisResult(result) {
|
|
260
|
+
try {
|
|
261
|
+
const jsonMatch = result.match(/\{[\s\S]*?\}/);
|
|
262
|
+
if (!jsonMatch) {
|
|
263
|
+
throw new Error("No valid JSON found in AI response");
|
|
264
|
+
}
|
|
265
|
+
let analysisData;
|
|
266
|
+
try {
|
|
267
|
+
analysisData = JSON.parse(jsonMatch[0]);
|
|
268
|
+
}
|
|
269
|
+
catch (jsonError) {
|
|
270
|
+
console.error("JSON parsing error:", jsonError);
|
|
271
|
+
const cleanedJson = jsonMatch[0]
|
|
272
|
+
.replace(/,\s*}/g, "}")
|
|
273
|
+
.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');
|
|
274
|
+
analysisData = JSON.parse(cleanedJson);
|
|
275
|
+
}
|
|
276
|
+
if (!analysisData.category || typeof analysisData.confidence !== "number") {
|
|
277
|
+
throw new Error("Invalid analysis data structure");
|
|
278
|
+
}
|
|
279
|
+
let category = analysisData.category.toLowerCase().trim();
|
|
280
|
+
if (this.config.categories &&
|
|
281
|
+
!this.config.categories[category] &&
|
|
282
|
+
this.config.allowNewCategories) {
|
|
283
|
+
this._saveNewCategory(category, analysisData.keywords || []);
|
|
284
|
+
this.stats.newCategories++;
|
|
285
|
+
}
|
|
286
|
+
else if (this.config.categories && !this.config.categories[category]) {
|
|
287
|
+
category = this._findClosestCategory(category, analysisData.keywords || []);
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
category,
|
|
291
|
+
confidence: analysisData.confidence,
|
|
292
|
+
keywords: analysisData.keywords || [],
|
|
293
|
+
explanation: analysisData.explanation || "No explanation provided",
|
|
294
|
+
prompt: this.config.categories?.[category]?.prompt || this.config.fallback?.prompt,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.error("Error parsing AI analysis result:", error.message);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
_fastKeywordMatch(text) {
|
|
303
|
+
if (!text || !this.config?.categories) {
|
|
304
|
+
return this._getDefaultContext();
|
|
305
|
+
}
|
|
306
|
+
const textLower = text.toLowerCase();
|
|
307
|
+
const scores = {};
|
|
308
|
+
let maxScore = 0;
|
|
309
|
+
let bestCategory = this.config.fallback?.category || "general";
|
|
310
|
+
for (const [category, keywordSet] of Object.entries(this.categoryKeywords)) {
|
|
311
|
+
let score = 0;
|
|
312
|
+
for (const keyword of keywordSet) {
|
|
313
|
+
const regex = new RegExp(`\\b${keyword}\\b`, "i");
|
|
314
|
+
if (regex.test(textLower)) {
|
|
315
|
+
score += 2;
|
|
316
|
+
}
|
|
317
|
+
else if (textLower.includes(keyword)) {
|
|
318
|
+
score += 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (score > 0) {
|
|
322
|
+
const weight = this.config.categories[category]?.weight || 1.0;
|
|
323
|
+
score *= weight;
|
|
324
|
+
scores[category] = score;
|
|
325
|
+
if (score > maxScore) {
|
|
326
|
+
maxScore = score;
|
|
327
|
+
bestCategory = category;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const maxPossibleScore = Math.max(8, Object.keys(this.categoryKeywords).length * 2);
|
|
332
|
+
const baseConfidence = Math.min(0.9, maxScore / maxPossibleScore);
|
|
333
|
+
const confidence = Math.min(0.95, 1 - Math.exp(-baseConfidence * 2));
|
|
334
|
+
if (this.config.detection && confidence < (this.config.detection.minConfidence || 0.4)) {
|
|
335
|
+
return this._getDefaultContext();
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
category: bestCategory,
|
|
339
|
+
confidence,
|
|
340
|
+
keywords: Array.from(this.categoryKeywords[bestCategory] || []).slice(0, 5),
|
|
341
|
+
explanation: `Matched keywords for ${bestCategory} category`,
|
|
342
|
+
prompt: this.config.categories[bestCategory]?.prompt || this.config.fallback?.prompt,
|
|
343
|
+
method: "keyword_match",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
_getDefaultContext() {
|
|
347
|
+
return {
|
|
348
|
+
category: this.config.fallback?.category || "general",
|
|
349
|
+
confidence: 0.5,
|
|
350
|
+
keywords: [],
|
|
351
|
+
explanation: "Default category used",
|
|
352
|
+
prompt: this.config.fallback?.prompt || "Translate naturally",
|
|
353
|
+
method: "default",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
_findClosestCategory(suggestedCategory, keywords) {
|
|
357
|
+
let bestMatch = this.config.fallback?.category || "general";
|
|
358
|
+
let highestScore = 0;
|
|
359
|
+
if (!this.config.categories)
|
|
360
|
+
return bestMatch;
|
|
361
|
+
for (const [category, config] of Object.entries(this.config.categories)) {
|
|
362
|
+
let score = 0;
|
|
363
|
+
if (config.keywords && this.categoryKeywords[category]) {
|
|
364
|
+
const keywordsLower = new Set(keywords.map((k) => k.toLowerCase()));
|
|
365
|
+
for (const keyword of keywordsLower) {
|
|
366
|
+
if (this.categoryKeywords[category].has(keyword)) {
|
|
367
|
+
score += 1.5;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
for (const configKeyword of this.categoryKeywords[category]) {
|
|
371
|
+
if (configKeyword.includes(keyword) || keyword.includes(configKeyword)) {
|
|
372
|
+
score += 0.75;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (category.includes(suggestedCategory) || suggestedCategory.includes(category)) {
|
|
379
|
+
score += 2;
|
|
380
|
+
}
|
|
381
|
+
if (score > highestScore) {
|
|
382
|
+
highestScore = score;
|
|
383
|
+
bestMatch = category;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return bestMatch;
|
|
387
|
+
}
|
|
388
|
+
_saveNewCategory(category, keywords) {
|
|
389
|
+
if (this.config.categories && !this.config.categories[category]) {
|
|
390
|
+
this.config.categories[category] = {
|
|
391
|
+
keywords: keywords,
|
|
392
|
+
prompt: `Translate with awareness of ${category} context`,
|
|
393
|
+
weight: 1.0,
|
|
394
|
+
};
|
|
395
|
+
this.categoryKeywords[category] = new Set(keywords.map((k) => k.toLowerCase()));
|
|
396
|
+
log(`Added new context category: ${category}`, true);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Generate a collision-resistant cache key for text
|
|
401
|
+
*/
|
|
402
|
+
_getCacheKey(text) {
|
|
403
|
+
let keyContent;
|
|
404
|
+
if (text.length <= 100) {
|
|
405
|
+
// Short text: use as-is (lowercased for consistency)
|
|
406
|
+
keyContent = text.toLowerCase();
|
|
407
|
+
}
|
|
408
|
+
else if (text.length <= 500) {
|
|
409
|
+
// Medium text: sample from beginning, middle, and end with better distribution
|
|
410
|
+
const start = text.substring(0, 40).toLowerCase();
|
|
411
|
+
const middlePos = Math.floor(text.length / 2);
|
|
412
|
+
const middle = text.substring(middlePos - 20, middlePos + 20).toLowerCase();
|
|
413
|
+
const end = text.substring(text.length - 40).toLowerCase();
|
|
414
|
+
keyContent = `${start}|MID:${middle}|${end}`;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
// Long text: enhanced sampling with length and character diversity
|
|
418
|
+
const quarter = Math.floor(text.length / 4);
|
|
419
|
+
const start = text.substring(0, 30).toLowerCase();
|
|
420
|
+
const q1 = text.substring(quarter, quarter + 25).toLowerCase();
|
|
421
|
+
const q2 = text.substring(quarter * 2, quarter * 2 + 25).toLowerCase();
|
|
422
|
+
const q3 = text.substring(quarter * 3, quarter * 3 + 25).toLowerCase();
|
|
423
|
+
const end = text.substring(text.length - 30).toLowerCase();
|
|
424
|
+
// Include length and character diversity for additional uniqueness
|
|
425
|
+
const charSet = new Set(text.toLowerCase()).size;
|
|
426
|
+
const wordCount = text.split(/\s+/).length;
|
|
427
|
+
keyContent = `LEN:${text.length}|WORDS:${wordCount}|CHARS:${charSet}|${start}|Q1:${q1}|Q2:${q2}|Q3:${q3}|${end}`;
|
|
428
|
+
}
|
|
429
|
+
return crypto
|
|
430
|
+
.createHash("sha256")
|
|
431
|
+
.update(keyContent, "utf8")
|
|
432
|
+
.digest("hex")
|
|
433
|
+
.substring(0, 32); // Truncate for storage efficiency while maintaining uniqueness
|
|
434
|
+
}
|
|
435
|
+
getStats() {
|
|
436
|
+
return {
|
|
437
|
+
...this.stats,
|
|
438
|
+
cacheSize: this.cache.size,
|
|
439
|
+
cacheCapacity: this.cache.max,
|
|
440
|
+
hitRate: this.stats.cacheHits / Math.max(1, this.stats.totalAnalyzed),
|
|
441
|
+
aiUsageRate: this.stats.aiCalls / Math.max(1, this.stats.totalAnalyzed),
|
|
442
|
+
keywordMatchRate: this.stats.keywordMatches / Math.max(1, this.stats.totalAnalyzed),
|
|
443
|
+
errorRate: this.stats.errors / Math.max(1, this.stats.totalAnalyzed),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
resetStats() {
|
|
447
|
+
this.stats = {
|
|
448
|
+
totalAnalyzed: 0,
|
|
449
|
+
aiCalls: 0,
|
|
450
|
+
cacheHits: 0,
|
|
451
|
+
keywordMatches: 0,
|
|
452
|
+
newCategories: 0,
|
|
453
|
+
errors: 0,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
clearCache() {
|
|
457
|
+
this.cache.clear();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
export default AIContextAnalyzer;
|