gptrans 1.8.4 → 1.8.6
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/demo/case_parallelism.js +156 -0
- package/index.js +43 -15
- package/package.json +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import GPTrans from '../index.js';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
|
|
6
|
+
// Cargar .env desde la carpeta demo
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
dotenv.config({ path: join(__dirname, '.env') });
|
|
10
|
+
|
|
11
|
+
console.log('🚀 Prueba de Paralelismo en GPTrans');
|
|
12
|
+
console.log('⚠️ Múltiples instancias con MISMO NOMBRE y MISMO PAR DE IDIOMAS\n');
|
|
13
|
+
console.log('=' .repeat(70));
|
|
14
|
+
|
|
15
|
+
// Test: Múltiples instancias con el MISMO NOMBRE y MISMO PAR DE IDIOMAS
|
|
16
|
+
async function testParallelTranslations() {
|
|
17
|
+
const instanceName = 'parallel_test'; // UN SOLO NOMBRE
|
|
18
|
+
const sourceLang = 'en'; // UN SOLO IDIOMA ORIGEN
|
|
19
|
+
const targetLang = 'es'; // UN SOLO IDIOMA DESTINO
|
|
20
|
+
const instanceCount = 20; // MUCHAS INSTANCIAS
|
|
21
|
+
|
|
22
|
+
console.log(`📋 Configuración de la prueba:`);
|
|
23
|
+
console.log(` Nombre de instancia: "${instanceName}"`);
|
|
24
|
+
console.log(` Par de idiomas: ${sourceLang} → ${targetLang}`);
|
|
25
|
+
console.log(` Número de instancias: ${instanceCount}`);
|
|
26
|
+
console.log(` Modo: Promise.all() - Todas simultáneas`);
|
|
27
|
+
console.log('-'.repeat(70));
|
|
28
|
+
|
|
29
|
+
// Generar textos únicos para cada instancia
|
|
30
|
+
const texts = Array.from({ length: instanceCount }, (_, i) =>
|
|
31
|
+
`Parallel translation test number ${i + 1}`
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
console.log(`\n🚀 Iniciando ${instanceCount} traducciones simultáneas...\n`);
|
|
35
|
+
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
|
|
38
|
+
// TODAS las instancias se crean y solicitan traducciones SIMULTÁNEAMENTE
|
|
39
|
+
const results = await Promise.all(
|
|
40
|
+
texts.map(async (text, index) => {
|
|
41
|
+
// Todas las instancias comparten el MISMO NOMBRE
|
|
42
|
+
const gptrans = new GPTrans({
|
|
43
|
+
from: sourceLang,
|
|
44
|
+
target: targetLang,
|
|
45
|
+
model: 'sonnet45',
|
|
46
|
+
name: instanceName // ⚠️ MISMO NOMBRE = MISMA DB
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Mostrar solo las primeras y últimas para no saturar consola
|
|
50
|
+
if (index < 5 || index >= instanceCount - 2) {
|
|
51
|
+
console.log(` [${index + 1}/${instanceCount}] Instancia creada, traduciendo: "${text}"`);
|
|
52
|
+
} else if (index === 5) {
|
|
53
|
+
console.log(` ... (${instanceCount - 7} instancias más) ...`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Primera lectura (inmediata - probablemente sin traducir)
|
|
57
|
+
const immediate = await gptrans.t(text);
|
|
58
|
+
|
|
59
|
+
// Esperar para que se procesen las traducciones
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
61
|
+
|
|
62
|
+
// Segunda lectura (después del procesamiento)
|
|
63
|
+
const final = await gptrans.t(text);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
index: index + 1,
|
|
67
|
+
original: text,
|
|
68
|
+
immediate: immediate,
|
|
69
|
+
final: final
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
75
|
+
|
|
76
|
+
console.log('\n' + '='.repeat(70));
|
|
77
|
+
console.log('📊 RESULTADOS');
|
|
78
|
+
console.log('='.repeat(70));
|
|
79
|
+
|
|
80
|
+
// Mostrar solo algunos resultados para no saturar
|
|
81
|
+
console.log('\n📋 Traducciones (primeras 5):');
|
|
82
|
+
results.slice(0, 5).forEach(({ index, original, immediate, final }) => {
|
|
83
|
+
console.log(`\n [${index}] Original: "${original}"`);
|
|
84
|
+
console.log(` Inmediato: "${immediate}"`);
|
|
85
|
+
console.log(` Final: "${final}"`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (instanceCount > 5) {
|
|
89
|
+
console.log(`\n ... (${instanceCount - 5} traducciones más) ...`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\n⏱️ Tiempo total: ${duration.toFixed(2)}s`);
|
|
93
|
+
console.log(`⚡ Velocidad promedio: ${(duration / instanceCount).toFixed(3)}s por traducción`);
|
|
94
|
+
|
|
95
|
+
// Análisis de integridad
|
|
96
|
+
console.log('\n' + '='.repeat(70));
|
|
97
|
+
console.log('🔍 ANÁLISIS DE INTEGRIDAD');
|
|
98
|
+
console.log('='.repeat(70));
|
|
99
|
+
|
|
100
|
+
const allTranslated = results.every(r => r.final !== r.original);
|
|
101
|
+
const someImmediate = results.filter(r => r.immediate !== r.original).length;
|
|
102
|
+
const uniqueFinals = new Set(results.map(r => r.final));
|
|
103
|
+
const uniqueOriginals = new Set(results.map(r => r.original));
|
|
104
|
+
|
|
105
|
+
console.log(`\n✓ Textos originales únicos: ${uniqueOriginals.size}/${instanceCount}`);
|
|
106
|
+
console.log(`✓ Traducciones inmediatas: ${someImmediate}/${instanceCount}`);
|
|
107
|
+
console.log(`✓ Traducciones finales completadas: ${results.filter(r => r.final !== r.original).length}/${instanceCount}`);
|
|
108
|
+
console.log(`✓ Traducciones finales únicas: ${uniqueFinals.size}/${instanceCount}`);
|
|
109
|
+
|
|
110
|
+
console.log('\n📈 Estado:');
|
|
111
|
+
console.log(` Todas traducidas correctamente: ${allTranslated ? '✅ SÍ' : '❌ NO'}`);
|
|
112
|
+
console.log(` Sin duplicados: ${uniqueFinals.size === instanceCount ? '✅ SÍ' : '❌ NO'}`);
|
|
113
|
+
|
|
114
|
+
// Detectar problemas
|
|
115
|
+
const hasRaceCondition = uniqueFinals.size !== instanceCount;
|
|
116
|
+
const hasUntranslated = !allTranslated;
|
|
117
|
+
|
|
118
|
+
if (hasRaceCondition) {
|
|
119
|
+
console.log(`\n⚠️ RACE CONDITION DETECTADA:`);
|
|
120
|
+
console.log(` Se esperaban ${instanceCount} traducciones únicas`);
|
|
121
|
+
console.log(` Se obtuvieron ${uniqueFinals.size} traducciones únicas`);
|
|
122
|
+
console.log(` Duplicados: ${instanceCount - uniqueFinals.size}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasUntranslated) {
|
|
126
|
+
const untranslated = results.filter(r => r.final === r.original);
|
|
127
|
+
console.log(`\n⚠️ TRADUCCIONES INCOMPLETAS:`);
|
|
128
|
+
console.log(` ${untranslated.length} textos no fueron traducidos`);
|
|
129
|
+
untranslated.slice(0, 3).forEach(({ index, original }) => {
|
|
130
|
+
console.log(` [${index}] "${original}"`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!hasRaceCondition && !hasUntranslated) {
|
|
135
|
+
console.log(`\n✅ PRUEBA EXITOSA:`);
|
|
136
|
+
console.log(` Todas las traducciones se completaron correctamente`);
|
|
137
|
+
console.log(` No se detectaron race conditions`);
|
|
138
|
+
console.log(` No se detectaron duplicados`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('\n' + '='.repeat(70));
|
|
142
|
+
console.log('💡 CONCLUSIÓN');
|
|
143
|
+
console.log('='.repeat(70));
|
|
144
|
+
console.log(`\n${instanceCount} instancias con:`);
|
|
145
|
+
console.log(` • MISMO nombre: "${instanceName}"`);
|
|
146
|
+
console.log(` • MISMO par de idiomas: ${sourceLang} → ${targetLang}`);
|
|
147
|
+
console.log(` • Solicitudes SIMULTÁNEAS con Promise.all()`);
|
|
148
|
+
console.log(` • Resultado: ${hasRaceCondition || hasUntranslated ? '🔴 PROBLEMAS DETECTADOS' : '✅ EXITOSO'}`);
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Ejecutar prueba
|
|
153
|
+
testParallelTranslations().catch(error => {
|
|
154
|
+
console.error('\n❌ Error durante la prueba:', error.message);
|
|
155
|
+
console.error(error.stack);
|
|
156
|
+
});
|
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'path';
|
|
|
9
9
|
|
|
10
10
|
class GPTrans {
|
|
11
11
|
static #mmixInstances = new Map();
|
|
12
|
+
static #translationLocks = new Map();
|
|
12
13
|
|
|
13
14
|
static mmix(models = 'sonnet45') {
|
|
14
15
|
const key = Array.isArray(models) ? models.join(',') : models;
|
|
@@ -44,6 +45,24 @@ class GPTrans {
|
|
|
44
45
|
return this.#mmixInstances.get(key);
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
static async #acquireTranslationLock(modelKey) {
|
|
49
|
+
if (!this.#translationLocks.has(modelKey)) {
|
|
50
|
+
this.#translationLocks.set(modelKey, Promise.resolve());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const previousLock = this.#translationLocks.get(modelKey);
|
|
54
|
+
let releaseLock;
|
|
55
|
+
|
|
56
|
+
const currentLock = new Promise(resolve => {
|
|
57
|
+
releaseLock = resolve;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.#translationLocks.set(modelKey, previousLock.then(() => currentLock));
|
|
61
|
+
|
|
62
|
+
await previousLock;
|
|
63
|
+
return releaseLock;
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
static isLanguageAvailable(langCode) {
|
|
48
67
|
return isLanguageAvailable(langCode);
|
|
49
68
|
}
|
|
@@ -100,7 +119,8 @@ class GPTrans {
|
|
|
100
119
|
setContext(context = '') {
|
|
101
120
|
if (this.context !== context && this.pendingTranslations.size > 0) {
|
|
102
121
|
clearTimeout(this.debounceTimer);
|
|
103
|
-
this.
|
|
122
|
+
const capturedContext = this.context;
|
|
123
|
+
this._processBatch(capturedContext);
|
|
104
124
|
}
|
|
105
125
|
this.context = context;
|
|
106
126
|
return this;
|
|
@@ -150,10 +170,11 @@ class GPTrans {
|
|
|
150
170
|
clearTimeout(this.debounceTimer);
|
|
151
171
|
}
|
|
152
172
|
|
|
153
|
-
// Set new timer
|
|
173
|
+
// Set new timer - capture context at scheduling time
|
|
174
|
+
const capturedContext = this.context;
|
|
154
175
|
this.debounceTimer = setTimeout(() => {
|
|
155
176
|
if (this.pendingTranslations.size > 0) {
|
|
156
|
-
this._processBatch(
|
|
177
|
+
this._processBatch(capturedContext);
|
|
157
178
|
}
|
|
158
179
|
}, this.debounceTimeout);
|
|
159
180
|
|
|
@@ -203,24 +224,31 @@ class GPTrans {
|
|
|
203
224
|
}
|
|
204
225
|
|
|
205
226
|
async _translate(text) {
|
|
227
|
+
// Acquire lock to ensure atomic model configuration and translation
|
|
228
|
+
const releaseLock = await GPTrans.#acquireTranslationLock(this.modelKey);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const model = GPTrans.mmix(this.modelKey);
|
|
206
232
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
model.setSystem("You are an expert translator specialized in literary translation between FROM_LANG and TARGET_DENONYM TARGET_LANG.");
|
|
233
|
+
model.setSystem("You are an expert translator specialized in literary translation between FROM_LANG and TARGET_DENONYM TARGET_LANG.");
|
|
210
234
|
|
|
211
|
-
|
|
235
|
+
model.addTextFromFile(this.promptFile);
|
|
212
236
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
237
|
+
model.replace({ INPUT: text, CONTEXT: this.context });
|
|
238
|
+
model.replace(this.replaceTarget);
|
|
239
|
+
model.replace(this.replaceFrom);
|
|
216
240
|
|
|
217
|
-
|
|
241
|
+
const response = await model.message();
|
|
218
242
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
const codeBlockRegex = /```(?:\w*\n)?([\s\S]*?)```/;
|
|
244
|
+
const match = response.match(codeBlockRegex);
|
|
245
|
+
const translatedText = match ? match[1].trim() : response;
|
|
222
246
|
|
|
223
|
-
|
|
247
|
+
return translatedText;
|
|
248
|
+
} finally {
|
|
249
|
+
// Always release the lock
|
|
250
|
+
releaseLock();
|
|
251
|
+
}
|
|
224
252
|
}
|
|
225
253
|
|
|
226
254
|
_textToKey(text, tokens = 5, maxlen = 6) {
|