gptrans 2.2.0 → 2.2.4
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/.agents/skills/modelmix/SKILL.md +17 -8
- package/README.md +9 -6
- package/demo/case_1.js +4 -2
- package/demo/case_2.js +3 -1
- package/demo/case_3.js +3 -1
- package/demo/case_4.js +5 -8
- package/demo/case_language_info.js +3 -1
- package/demo/case_parallelism.js +3 -10
- package/demo/case_references.js +3 -10
- package/demo/case_refine.js +3 -1
- package/demo/example-image-translation.js +4 -8
- package/index.js +41 -53
- package/package.json +3 -3
- package/pnpm-workspace.yaml +1 -0
- package/prompt/refine.md +4 -4
- package/prompt/translate.md +7 -7
- package/skills/gptrans/SKILL.md +9 -8
- package/test/gptrans.tAsync.test.js +93 -1
- package/test-image-translation.js +2 -6
|
@@ -66,10 +66,18 @@ const model = ModelMix.new()
|
|
|
66
66
|
|
|
67
67
|
If `sonnet45` fails, it automatically tries `gpt5mini`, then `gemini3flash`.
|
|
68
68
|
|
|
69
|
+
The equivalent `chain()` syntax accepts `shortcut@<effort>` to override unified effort for one model. `<effort>` must be an integer from `0` to `100`; models without the suffix keep their configured or provider-default effort:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const model = ModelMix.new()
|
|
73
|
+
.chain('sonnet5@50', 'gpt56luna@100');
|
|
74
|
+
```
|
|
75
|
+
|
|
69
76
|
## Available Model Shorthands
|
|
70
77
|
|
|
71
78
|
- **OpenAI**: `gpt52` `gpt51` `gpt5` `gpt5mini` `gpt5nano` `gpt41` `gpt41mini` `gpt41nano`
|
|
72
|
-
- **Anthropic**: `opus46` `opus45` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
|
|
79
|
+
- **Anthropic**: `opus46` `opus45` `sonnet5` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
|
|
80
|
+
- **OpenAI Codex**: `gpt56sol` `gpt56terra` `gpt56luna`
|
|
73
81
|
- **Google**: `gemini3pro` `gemini3flash` `gemini25pro` `gemini25flash`
|
|
74
82
|
- **Grok**: `grok4` `grok41` (thinking variant available)
|
|
75
83
|
- **Perplexity**: `sonar` `sonarPro`
|
|
@@ -158,11 +166,11 @@ const description = await model.message();
|
|
|
158
166
|
const model = ModelMix.new().gpt5mini();
|
|
159
167
|
model.setSystemFromFile('./prompts/system.md');
|
|
160
168
|
model.addTextFromFile('./prompts/task.md');
|
|
161
|
-
model.
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
model.assign({
|
|
170
|
+
role: 'data analyst',
|
|
171
|
+
language: 'Spanish'
|
|
164
172
|
});
|
|
165
|
-
model.
|
|
173
|
+
model.assignKeyFromFile('code', './src/utils.js');
|
|
166
174
|
console.log(await model.message());
|
|
167
175
|
```
|
|
168
176
|
|
|
@@ -268,7 +276,7 @@ const reply = await chat.message(); // "Martin"
|
|
|
268
276
|
- Use `.json()` for structured output instead of parsing text manually.
|
|
269
277
|
- Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
|
|
270
278
|
- For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
|
|
271
|
-
- Template placeholders use
|
|
279
|
+
- Template placeholders use EJS syntax such as `<%- key %>` in system prompts and user messages.
|
|
272
280
|
- The library uses CommonJS internally (`require`) but supports ESM import via `{ ModelMix }`.
|
|
273
281
|
- Available provider Mix classes for custom setups: `MixOpenAI`, `MixAnthropic`, `MixGoogle`, `MixPerplexity`, `MixGroq`, `MixTogether`, `MixGrok`, `MixOpenRouter`, `MixOllama`, `MixLMStudio`, `MixCustom`, `MixCerebras`, `MixFireworks`, `MixMiniMax`.
|
|
274
282
|
|
|
@@ -282,8 +290,9 @@ const reply = await chat.message(); // "Martin"
|
|
|
282
290
|
| `.setSystemFromFile(path)` | `this` | Set system prompt from file |
|
|
283
291
|
| `.addImage(path)` | `this` | Add image from file |
|
|
284
292
|
| `.addImageFromUrl(url)` | `this` | Add image from URL or data URI |
|
|
285
|
-
| `.
|
|
286
|
-
| `.
|
|
293
|
+
| `.assign({})` | `this` | Assign EJS template data |
|
|
294
|
+
| `.assignKeyFromFile(key, path)` | `this` | Assign rendered file content to one template key |
|
|
295
|
+
| `.chain(...models)` | `this` | Attach ordered shortcuts; each may use `@<effort>` from `0` to `100` |
|
|
287
296
|
| `.message()` | `Promise<string>` | Get text response |
|
|
288
297
|
| `.json(example, desc?, opts?)` | `Promise<object>` | Get structured JSON |
|
|
289
298
|
| `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ npm install gptrans
|
|
|
30
30
|
|
|
31
31
|
### 🌐 Environment Setup
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
GPTrans reads provider credentials from `process.env` and does not load `.env` files itself. If you use a `.env` file, load it in your application before constructing `GPTrans`. Create the file in your project root and add your API keys:
|
|
34
34
|
|
|
35
35
|
```env
|
|
36
36
|
OPENAI_API_KEY=your_openai_api_key
|
|
@@ -44,13 +44,15 @@ Here's a simple example to get you started:
|
|
|
44
44
|
|
|
45
45
|
```javascript
|
|
46
46
|
import GPTrans from 'gptrans';
|
|
47
|
+
import { join } from 'node:path';
|
|
48
|
+
import { loadEnvFile } from 'node:process';
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
loadEnvFile(join(import.meta.dirname, '.env'));
|
|
51
|
+
const dbPath = join(import.meta.dirname, 'db');
|
|
49
52
|
const gptrans = new GPTrans({
|
|
50
53
|
path: dbPath,
|
|
51
54
|
from: 'en-US',
|
|
52
|
-
target: 'es-AR'
|
|
53
|
-
model: 'sonnet45'
|
|
55
|
+
target: 'es-AR'
|
|
54
56
|
});
|
|
55
57
|
|
|
56
58
|
// Translate text with parameter substitution
|
|
@@ -79,7 +81,7 @@ When creating a new instance of GPTrans, you can customize:
|
|
|
79
81
|
| `path` | Absolute directory path for the DeepBase translation cache | Required |
|
|
80
82
|
| `from` | Source language locale (BCP 47) | `en-US` |
|
|
81
83
|
| `target` | Target language locale (BCP 47) | `es` |
|
|
82
|
-
| `model` |
|
|
84
|
+
| `model` | ModelMix shortcut or array for an ordered fallback chain; append `@<effort>` (`0`–`100`) to override one model | `['sonnet5@50', 'gpt56luna@100']` |
|
|
83
85
|
| `batchThreshold` | Maximum number of characters to accumulate before triggering batch processing | `1500` |
|
|
84
86
|
| `debounceTimeout` | Time in milliseconds to wait before processing translations | `500` |
|
|
85
87
|
| `instruction` | Additional instruction for the translator (e.g., tone, style). Does not affect the cache key | `''` |
|
|
@@ -241,7 +243,7 @@ GPTrans supports a fallback mechanism for translation models. Instead of providi
|
|
|
241
243
|
```javascript
|
|
242
244
|
const translator = new GPTrans({
|
|
243
245
|
path: dbPath,
|
|
244
|
-
model: ['
|
|
246
|
+
model: ['sonnet5@50', 'gpt56luna@100'],
|
|
245
247
|
// ... other options
|
|
246
248
|
});
|
|
247
249
|
```
|
|
@@ -249,6 +251,7 @@ const translator = new GPTrans({
|
|
|
249
251
|
When using multiple models:
|
|
250
252
|
- The first model in the array is used as the primary translation service
|
|
251
253
|
- If the primary model fails (due to API errors, rate limits, etc.), GPTrans automatically falls back to the next model
|
|
254
|
+
- Use `model@<effort>` with an integer from `0` to `100` to set ModelMix's unified effort for that model only. By default, `sonnet5` uses effort `50`; if it fails, `gpt56luna` uses effort `100`. Models without the suffix keep their configured or provider-default effort.
|
|
252
255
|
- This ensures higher availability and resilience of your translation service
|
|
253
256
|
|
|
254
257
|
## ✏️ Refining Translations
|
package/demo/case_1.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
+
import { join } from 'path';
|
|
2
3
|
|
|
3
|
-
const dbPath =
|
|
4
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
5
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
4
6
|
const gptrans = new GPTrans({ path: dbPath, model: 'sonnet45' });
|
|
5
7
|
|
|
6
8
|
console.log(gptrans.t('Hello, {name}!', { name: 'Anya' }));
|
|
@@ -30,7 +32,7 @@ const ar2es = new GPTrans({
|
|
|
30
32
|
path: dbPath,
|
|
31
33
|
from: 'es-AR',
|
|
32
34
|
target: 'es-ES',
|
|
33
|
-
model: '
|
|
35
|
+
model: 'gpt56terra@20'
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
console.log(ar2es.t('¿Tenés fuego?'));
|
package/demo/case_2.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
+
import { join } from 'path';
|
|
2
3
|
|
|
3
|
-
const dbPath =
|
|
4
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
5
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
4
6
|
|
|
5
7
|
try {
|
|
6
8
|
const gptrans = new GPTrans({
|
package/demo/case_3.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
+
import { join } from 'path';
|
|
2
3
|
|
|
3
|
-
const dbPath =
|
|
4
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
5
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
4
6
|
|
|
5
7
|
try {
|
|
6
8
|
const gptrans = new GPTrans({
|
package/demo/case_4.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
2
|
+
import { join } from 'path';
|
|
4
3
|
import { promises as fs } from 'fs';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
5
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
6
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
9
7
|
|
|
10
8
|
// Initialize translator
|
|
11
9
|
const model = new GPTrans({
|
|
12
10
|
path: dbPath,
|
|
13
|
-
model: ['
|
|
11
|
+
model: ['sonnet50', 'gpt56luna@80'],
|
|
14
12
|
from: 'es', // Assuming the source file is in Spanish
|
|
15
13
|
target: 'en',
|
|
16
14
|
});
|
|
@@ -19,10 +17,9 @@ const model = new GPTrans({
|
|
|
19
17
|
await model.preload();
|
|
20
18
|
|
|
21
19
|
// Read and translate the file
|
|
22
|
-
const filePath =
|
|
20
|
+
const filePath = join(import.meta.dirname, 'georgia_incident.md');
|
|
23
21
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
24
22
|
|
|
25
23
|
// Translate the content
|
|
26
24
|
const translatedContent = model.t(content);
|
|
27
25
|
console.log(translatedContent);
|
|
28
|
-
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
+
import { join } from 'path';
|
|
2
3
|
|
|
3
|
-
const dbPath =
|
|
4
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
5
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
4
6
|
|
|
5
7
|
// Ejemplo: Cómo obtener la información completa del idioma
|
|
6
8
|
|
package/demo/case_parallelism.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from 'path';
|
|
2
|
+
import { join } from 'path';
|
|
4
3
|
|
|
5
4
|
// Cargar .env desde la carpeta demo
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
9
|
-
try {
|
|
10
|
-
process.loadEnvFile(join(__dirname, '.env'));
|
|
11
|
-
} catch {
|
|
12
|
-
/* optional .env missing or unreadable */
|
|
13
|
-
}
|
|
5
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
6
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
14
7
|
|
|
15
8
|
console.log('🚀 Prueba de Paralelismo en GPTrans');
|
|
16
9
|
console.log('⚠️ Múltiples instancias con MISMO NOMBRE y MISMO PAR DE IDIOMAS\n');
|
package/demo/case_references.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from 'path';
|
|
2
|
+
import { join } from 'path';
|
|
4
3
|
|
|
5
4
|
// Load .env from demo folder
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
9
|
-
try {
|
|
10
|
-
process.loadEnvFile(join(__dirname, '.env'));
|
|
11
|
-
} catch {
|
|
12
|
-
/* optional .env missing or unreadable */
|
|
13
|
-
}
|
|
5
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
6
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
14
7
|
|
|
15
8
|
console.log('🚀 Testing GPTrans with Reference Translations\n');
|
|
16
9
|
console.log('='.repeat(70));
|
package/demo/case_refine.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
|
+
import { join } from 'path';
|
|
2
3
|
|
|
3
|
-
const dbPath =
|
|
4
|
+
const dbPath = join(import.meta.dirname, '../db');
|
|
5
|
+
try { process.loadEnvFile(join(import.meta.dirname, '.env')); } catch { }
|
|
4
6
|
|
|
5
7
|
console.log('🚀 Testing GPTrans Refine\n');
|
|
6
8
|
console.log('='.repeat(70));
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
import GPTrans from '../index.js';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
5
|
|
|
10
6
|
async function main() {
|
|
11
7
|
try {
|
|
12
8
|
// Initialize GPTrans with Spanish as target
|
|
13
9
|
const gptrans = new GPTrans({
|
|
14
|
-
path: path.join(
|
|
10
|
+
path: path.join(import.meta.dirname, 'db'),
|
|
15
11
|
from: 'en-US',
|
|
16
12
|
target: 'es'
|
|
17
13
|
});
|
|
@@ -24,7 +20,7 @@ async function main() {
|
|
|
24
20
|
console.log(' Input: en/image.jpg');
|
|
25
21
|
console.log(' Expected output: es/image.jpg (sibling folder)\n');
|
|
26
22
|
|
|
27
|
-
const imageInLangFolder = path.join(
|
|
23
|
+
const imageInLangFolder = path.join(import.meta.dirname, 'en', 'camera_4126.jpg');
|
|
28
24
|
if (fs.existsSync(imageInLangFolder)) {
|
|
29
25
|
const result1 = await gptrans.img(imageInLangFolder);
|
|
30
26
|
console.log(' Result:', result1);
|
|
@@ -39,7 +35,7 @@ async function main() {
|
|
|
39
35
|
// console.log(' Input: ./image.jpg');
|
|
40
36
|
// console.log(' Expected output: ./es/image.jpg (subfolder)\n');
|
|
41
37
|
|
|
42
|
-
// const imageInRoot = path.join(
|
|
38
|
+
// const imageInRoot = path.join(import.meta.dirname, 'camera_4126.jpg');
|
|
43
39
|
// if (fs.existsSync(imageInRoot)) {
|
|
44
40
|
// const result2 = await gptrans.img(imageInRoot);
|
|
45
41
|
// console.log(' Result:', result2);
|
|
@@ -54,7 +50,7 @@ async function main() {
|
|
|
54
50
|
// console.log(' Input: images/photo.jpg');
|
|
55
51
|
// console.log(' Expected output: images/es/photo.jpg (subfolder)\n');
|
|
56
52
|
|
|
57
|
-
// const imageInCustomFolder = path.join(
|
|
53
|
+
// const imageInCustomFolder = path.join(import.meta.dirname, 'images', 'photo.jpg');
|
|
58
54
|
// if (fs.existsSync(imageInCustomFolder)) {
|
|
59
55
|
// const result3 = await gptrans.img(imageInCustomFolder);
|
|
60
56
|
// console.log(' Result:', result3);
|
package/index.js
CHANGED
|
@@ -7,15 +7,20 @@ import { GeminiGenerator } from 'genmix';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import pathModule from 'path';
|
|
9
9
|
|
|
10
|
+
const TRANSLATE_PROMPT_FILE = pathModule.join(import.meta.dirname, 'prompt', 'translate.md');
|
|
11
|
+
const REFINE_PROMPT_FILE = pathModule.join(import.meta.dirname, 'prompt', 'refine.md');
|
|
12
|
+
const DEFAULT_MODEL_CHAIN = Object.freeze(['sonnet5@50', 'gpt56luna@100']);
|
|
13
|
+
|
|
10
14
|
class GPTrans {
|
|
11
15
|
static #mmixInstances = new Map();
|
|
12
16
|
static #translationLocks = new Map();
|
|
13
17
|
|
|
14
|
-
static mmix(models =
|
|
15
|
-
const
|
|
18
|
+
static mmix(models = DEFAULT_MODEL_CHAIN, { debug = 0 } = {}) {
|
|
19
|
+
const modelChain = Array.isArray(models) ? models : [models];
|
|
20
|
+
const key = modelChain.join(',');
|
|
16
21
|
|
|
17
22
|
if (!this.#mmixInstances.has(key)) {
|
|
18
|
-
|
|
23
|
+
const instance = ModelMix.new({
|
|
19
24
|
config: {
|
|
20
25
|
debug,
|
|
21
26
|
bottleneck: {
|
|
@@ -23,18 +28,7 @@ class GPTrans {
|
|
|
23
28
|
maxConcurrent: 1
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
|
-
});
|
|
27
|
-
const modelArray = Array.isArray(models) ? models : [models];
|
|
28
|
-
|
|
29
|
-
for (const model of modelArray) {
|
|
30
|
-
if (typeof instance[model] !== 'function') {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Model "${model}" is not available. Please check the model name. ` +
|
|
33
|
-
`Available models include: gpt51, gpt52, sonnet46, sonnet45, opus46, haiku45, etc.`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
instance = instance[model]();
|
|
37
|
-
}
|
|
31
|
+
}).chain(...modelChain);
|
|
38
32
|
|
|
39
33
|
this.#mmixInstances.set(key, instance);
|
|
40
34
|
}
|
|
@@ -63,7 +57,7 @@ class GPTrans {
|
|
|
63
57
|
return isLanguageAvailable(langCode);
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
constructor({ from = 'en-US', target = 'es', model =
|
|
60
|
+
constructor({ from = 'en-US', target = 'es', model = DEFAULT_MODEL_CHAIN, batchThreshold = 1500, debounceTimeout = 500, promptFile = null, name = '', context = '', instruction = '', freeze = false, debug = false, path: dbPath } = {}) {
|
|
67
61
|
|
|
68
62
|
if (typeof dbPath !== 'string' || !pathModule.isAbsolute(dbPath)) {
|
|
69
63
|
throw new TypeError('GPTrans requires an absolute "path" option.');
|
|
@@ -72,12 +66,6 @@ class GPTrans {
|
|
|
72
66
|
target = this.normalizeBCP47(target);
|
|
73
67
|
from = this.normalizeBCP47(from);
|
|
74
68
|
|
|
75
|
-
try {
|
|
76
|
-
process.loadEnvFile();
|
|
77
|
-
} catch {
|
|
78
|
-
/* optional .env missing or unreadable */
|
|
79
|
-
}
|
|
80
|
-
|
|
81
69
|
const namePrefix = name ? '_' + name : '';
|
|
82
70
|
this.dbPath = dbPath;
|
|
83
71
|
this.instanceName = name;
|
|
@@ -100,7 +88,7 @@ class GPTrans {
|
|
|
100
88
|
this.isProcessingBatch = false; // Track if a batch is currently being processed
|
|
101
89
|
this.modelMixOptions = { debug };
|
|
102
90
|
this.modelKey = model;
|
|
103
|
-
this.promptFile = promptFile
|
|
91
|
+
this.promptFile = promptFile ? pathModule.resolve(promptFile) : TRANSLATE_PROMPT_FILE;
|
|
104
92
|
this.context = context;
|
|
105
93
|
this.instruction = instruction;
|
|
106
94
|
this.freeze = freeze;
|
|
@@ -335,7 +323,7 @@ class GPTrans {
|
|
|
335
323
|
try {
|
|
336
324
|
const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
|
|
337
325
|
|
|
338
|
-
model.setSystem("You are an expert translator specialized in literary translation between
|
|
326
|
+
model.setSystem("You are an expert translator specialized in literary translation between <%- FROM_LANG %> and <%- TARGET_DENONYM %> <%- TARGET_LANG %>.");
|
|
339
327
|
|
|
340
328
|
// Build references section (includes header when references exist, empty otherwise)
|
|
341
329
|
let referencesText = '';
|
|
@@ -379,21 +367,21 @@ class GPTrans {
|
|
|
379
367
|
}
|
|
380
368
|
}
|
|
381
369
|
|
|
382
|
-
// Use ModelMix templates: addTextFromFile +
|
|
370
|
+
// Use ModelMix templates: addTextFromFile + assign + block
|
|
383
371
|
model.addTextFromFile(this.promptFile);
|
|
384
|
-
model.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
372
|
+
model.assign({
|
|
373
|
+
INPUT: text,
|
|
374
|
+
CONTEXT: this.context,
|
|
375
|
+
INSTRUCTION: this.instruction,
|
|
376
|
+
REFERENCES: referencesText,
|
|
377
|
+
TARGET_ISO: this.replaceTarget.TARGET_ISO,
|
|
378
|
+
TARGET_LANG: this.replaceTarget.TARGET_LANG,
|
|
379
|
+
TARGET_COUNTRY: this.replaceTarget.TARGET_COUNTRY,
|
|
380
|
+
TARGET_DENONYM: this.replaceTarget.TARGET_DENONYM,
|
|
381
|
+
FROM_ISO: fromReplace.FROM_ISO,
|
|
382
|
+
FROM_LANG: fromReplace.FROM_LANG,
|
|
383
|
+
FROM_COUNTRY: fromReplace.FROM_COUNTRY,
|
|
384
|
+
FROM_DENONYM: fromReplace.FROM_DENONYM,
|
|
397
385
|
});
|
|
398
386
|
|
|
399
387
|
return await model.block({ addSystemExtra: false });
|
|
@@ -557,7 +545,7 @@ class GPTrans {
|
|
|
557
545
|
}
|
|
558
546
|
|
|
559
547
|
const finalInstruction = instructions.length > 1 ? `- ${merged}` : merged;
|
|
560
|
-
const refinePromptFile = promptFile
|
|
548
|
+
const refinePromptFile = promptFile ? pathModule.resolve(promptFile) : REFINE_PROMPT_FILE;
|
|
561
549
|
|
|
562
550
|
// Collect all existing translations grouped by contextHash
|
|
563
551
|
const allBatches = [];
|
|
@@ -652,22 +640,22 @@ class GPTrans {
|
|
|
652
640
|
try {
|
|
653
641
|
const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
|
|
654
642
|
|
|
655
|
-
model.setSystem("You are an expert translator and editor specialized in refining
|
|
643
|
+
model.setSystem("You are an expert translator and editor specialized in refining <%- TARGET_DENONYM %> <%- TARGET_LANG %> translations.");
|
|
656
644
|
|
|
657
|
-
// Use ModelMix templates: addTextFromFile +
|
|
645
|
+
// Use ModelMix templates: addTextFromFile + assign + block
|
|
658
646
|
model.addTextFromFile(refinePromptFile);
|
|
659
|
-
model.
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
647
|
+
model.assign({
|
|
648
|
+
INPUT: text,
|
|
649
|
+
INSTRUCTION: instruction,
|
|
650
|
+
CONTEXT: this.context,
|
|
651
|
+
TARGET_ISO: this.replaceTarget.TARGET_ISO,
|
|
652
|
+
TARGET_LANG: this.replaceTarget.TARGET_LANG,
|
|
653
|
+
TARGET_COUNTRY: this.replaceTarget.TARGET_COUNTRY,
|
|
654
|
+
TARGET_DENONYM: this.replaceTarget.TARGET_DENONYM,
|
|
655
|
+
FROM_ISO: this.replaceFrom.FROM_ISO,
|
|
656
|
+
FROM_LANG: this.replaceFrom.FROM_LANG,
|
|
657
|
+
FROM_COUNTRY: this.replaceFrom.FROM_COUNTRY,
|
|
658
|
+
FROM_DENONYM: this.replaceFrom.FROM_DENONYM,
|
|
671
659
|
});
|
|
672
660
|
|
|
673
661
|
return await model.block({ addSystemExtra: false });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gptrans",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.4",
|
|
5
5
|
"description": "🚆 GPTrans - The smarter AI-powered way to translate.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"translate",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/clasen/GPTrans#readme",
|
|
34
34
|
"engines": {
|
|
35
|
-
"node": ">=20.
|
|
35
|
+
"node": ">=20.12.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"deepbase": "^3.9.0",
|
|
39
39
|
"deepbase-json": "3.9.0",
|
|
40
40
|
"genmix": "^1.2.5",
|
|
41
|
-
"modelmix": "^
|
|
41
|
+
"modelmix": "^5.1.6",
|
|
42
42
|
"string-hash": "^1.1.3"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
package/pnpm-workspace.yaml
CHANGED
package/prompt/refine.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Goal
|
|
2
|
-
Refine existing translations in
|
|
2
|
+
Refine existing translations in <%- TARGET_ISO %> (<%- TARGET_DENONYM %> <%- TARGET_LANG %>) based on the following instruction.
|
|
3
3
|
|
|
4
4
|
## Refinement Instruction
|
|
5
|
-
|
|
5
|
+
<%- INSTRUCTION %>
|
|
6
6
|
|
|
7
7
|
## Current Translations to Evaluate
|
|
8
8
|
```
|
|
9
|
-
|
|
9
|
+
<%- INPUT %>
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
# Return Format
|
|
@@ -23,4 +23,4 @@ Refine existing translations in {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) ba
|
|
|
23
23
|
- **Consistency:** Maintain consistent terminology across all translations in the batch.
|
|
24
24
|
|
|
25
25
|
# Context
|
|
26
|
-
|
|
26
|
+
<%- CONTEXT %>
|
package/prompt/translate.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Goal
|
|
2
|
-
Translation from
|
|
3
|
-
|
|
2
|
+
Translation from <%- FROM_ISO %> to <%- TARGET_ISO %> (<%- TARGET_DENONYM %> <%- TARGET_LANG %>) with cultural adaptations.
|
|
3
|
+
<%- INSTRUCTION %>
|
|
4
4
|
|
|
5
5
|
## Text to translate
|
|
6
6
|
```
|
|
7
|
-
|
|
7
|
+
<%- INPUT %>
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
<%- REFERENCES %>
|
|
11
11
|
|
|
12
12
|
# Return Format
|
|
13
13
|
- The input may contain multiple texts separated by `------`. Translate each one independently and return them in the same order, separated by `------`. The number of segments in your output must exactly match the number of segments in the input.
|
|
@@ -15,13 +15,13 @@ Translation from {FROM_ISO} to {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) wit
|
|
|
15
15
|
- Do not include alternative translations, only provide the best translation.
|
|
16
16
|
|
|
17
17
|
# Warnings
|
|
18
|
-
- **Context:** I will provide you with a text in
|
|
18
|
+
- **Context:** I will provide you with a text in <%- FROM_DENONYM %> <%- FROM_LANG %>. The goal is to translate it to <%- TARGET_ISO %> (<%- TARGET_DENONYM %> <%- TARGET_LANG %>) while maintaining the essence, style, intention, and tone of the original.
|
|
19
19
|
- **Proper names:** Do not translate proper names (people, places, brands, etc.) unless they have an officially recognized translation in the target language.
|
|
20
|
-
- **Cultural references:** Adapt or explain references that are not familiar in
|
|
20
|
+
- **Cultural references:** Adapt or explain references that are not familiar in <%- TARGET_DENONYM %> culture, whenever necessary.
|
|
21
21
|
- **Wordplay and humor:** When it's impossible to directly translate wordplay, find a resource that recreates the playful effect.
|
|
22
22
|
- **Idioms:** Do not introduce new idioms or expressions that are not present in the original text.
|
|
23
23
|
- **Variables:** Do not translate content between curly braces. These are system variables and must remain exactly the same.
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
# Context
|
|
27
|
-
|
|
27
|
+
<%- CONTEXT %>
|
package/skills/gptrans/SKILL.md
CHANGED
|
@@ -81,13 +81,13 @@ You can manually edit translation files to override specific entries.
|
|
|
81
81
|
|
|
82
82
|
```javascript
|
|
83
83
|
import GPTrans from 'gptrans';
|
|
84
|
+
import path from 'node:path';
|
|
84
85
|
|
|
85
|
-
const dbPath =
|
|
86
|
+
const dbPath = path.join(import.meta.dirname, 'db');
|
|
86
87
|
const gptrans = new GPTrans({
|
|
87
88
|
path: dbPath,
|
|
88
89
|
from: 'en-US',
|
|
89
|
-
target: 'es-AR'
|
|
90
|
-
model: 'sonnet45'
|
|
90
|
+
target: 'es-AR'
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
// Translate text — returns original on first call, cached translation after
|
|
@@ -106,7 +106,7 @@ console.log(gptrans.setContext().t('Welcome back'));
|
|
|
106
106
|
| --- | --- | --- | --- |
|
|
107
107
|
| `from` | `string` | `'en-US'` | Source language (BCP 47) |
|
|
108
108
|
| `target` | `string` | `'es'` | Target language (BCP 47) |
|
|
109
|
-
| `model` | `string \| string[]` | `'
|
|
109
|
+
| `model` | `string \| string[]` | `['sonnet5@50', 'gpt56luna@100']` | ModelMix shortcut or ordered fallback chain; supports `@<effort>` from `0` to `100` |
|
|
110
110
|
| `batchThreshold` | `number` | `1500` | Max characters before triggering batch |
|
|
111
111
|
| `debounceTimeout` | `number` | `500` | Milliseconds to wait before processing |
|
|
112
112
|
| `freeze` | `boolean` | `false` | Prevent new translations from being queued |
|
|
@@ -134,10 +134,12 @@ const gptrans = new GPTrans({
|
|
|
134
134
|
path: dbPath,
|
|
135
135
|
from: 'en',
|
|
136
136
|
target: 'fr',
|
|
137
|
-
model: ['
|
|
137
|
+
model: ['sonnet5@50', 'gpt56luna@100']
|
|
138
138
|
});
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
Use `model@<effort>` with an integer from `0` to `100` to override the unified effort for one model. The default chain uses `sonnet5` at effort `50`, then falls back to `gpt56luna` at effort `100`.
|
|
142
|
+
|
|
141
143
|
### Pre-translate all pending texts
|
|
142
144
|
|
|
143
145
|
```javascript
|
|
@@ -268,8 +270,7 @@ await gptrans.purge();
|
|
|
268
270
|
const es2ar = new GPTrans({
|
|
269
271
|
path: dbPath,
|
|
270
272
|
from: 'es-ES',
|
|
271
|
-
target: 'es-AR'
|
|
272
|
-
model: 'sonnet45'
|
|
273
|
+
target: 'es-AR'
|
|
273
274
|
});
|
|
274
275
|
|
|
275
276
|
console.log(es2ar.t('Eres muy bueno'));
|
|
@@ -295,7 +296,7 @@ if (GPTrans.isLanguageAvailable('pt-BR')) {
|
|
|
295
296
|
- Use `setContext()` for gender-aware or domain-specific translations. Context is captured per-batch and auto-resets when changed.
|
|
296
297
|
- Use the `instruction` constructor option for style/tone guidance (e.g., "Use a more natural tone"). Unlike `context`, `instruction` does NOT affect the cache key — different instructions for the same text overwrite the same translation entry.
|
|
297
298
|
- Prefer passing an array of instructions to `refine()` over multiple calls — it processes everything in a single API pass.
|
|
298
|
-
- Use model arrays (`model: ['
|
|
299
|
+
- Use model arrays (`model: ['sonnet5@50', 'gpt56luna@100']`) for production resilience with automatic fallback. The optional `@<effort>` suffix accepts integers from `0` to `100` and applies only to that model.
|
|
299
300
|
- Translation caches live in `db/gptrans_<locale>.json`. These files can be manually edited to override specific translations.
|
|
300
301
|
- The `name` constructor option isolates cache files (`db/gptrans_<name>_<locale>.json`), useful for multiple independent translation contexts in the same project.
|
|
301
302
|
- When translating images, ensure `GEMINI_API_KEY` is set. The `img()` method auto-creates sibling language folders.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { access, mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import GPTrans from '../index.js';
|
|
@@ -161,6 +161,87 @@ test('constructor requires an absolute database path', () => {
|
|
|
161
161
|
assert.throws(() => new GPTrans({ path: './db' }), /requires an absolute "path" option/);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
test('the default model chain uses the configured efforts and renders the EJS prompt', async () => {
|
|
165
|
+
const previousAnthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
166
|
+
const previousOpenAiKey = process.env.OPENAI_API_KEY;
|
|
167
|
+
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
|
168
|
+
process.env.OPENAI_API_KEY = 'test-openai-key';
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const model = GPTrans.mmix();
|
|
172
|
+
assert.deepEqual(
|
|
173
|
+
model.models.map(({ key }) => key),
|
|
174
|
+
['claude-sonnet-5', 'gpt-5.6-luna']
|
|
175
|
+
);
|
|
176
|
+
assert.equal(model.models[0].provider.config.effort, 50);
|
|
177
|
+
assert.equal(model.models[1].provider.config.effort, 100);
|
|
178
|
+
|
|
179
|
+
const gp = new GPTrans({ path: tmpdir(), freeze: true, name: 'modelmix-render' });
|
|
180
|
+
assert.deepEqual(gp.modelKey, ['sonnet5@50', 'gpt56luna@100']);
|
|
181
|
+
model.addTextFromFile(gp.promptFile);
|
|
182
|
+
model.assign({
|
|
183
|
+
INPUT: 'Hello',
|
|
184
|
+
CONTEXT: 'Greeting',
|
|
185
|
+
INSTRUCTION: 'Be friendly',
|
|
186
|
+
REFERENCES: '',
|
|
187
|
+
TARGET_ISO: 'es',
|
|
188
|
+
TARGET_LANG: 'Spanish',
|
|
189
|
+
TARGET_COUNTRY: 'Spain',
|
|
190
|
+
TARGET_DENONYM: 'Spanish',
|
|
191
|
+
FROM_ISO: 'en-US',
|
|
192
|
+
FROM_LANG: 'English',
|
|
193
|
+
FROM_COUNTRY: 'United States',
|
|
194
|
+
FROM_DENONYM: 'American'
|
|
195
|
+
});
|
|
196
|
+
const messages = await model.prepareMessages();
|
|
197
|
+
const renderedPrompt = messages[0].content[0].text;
|
|
198
|
+
assert.match(renderedPrompt, /Translation from en-US to es/);
|
|
199
|
+
assert.match(renderedPrompt, /```\nHello\n```/);
|
|
200
|
+
assert.doesNotMatch(renderedPrompt, /<%-\s*[A-Z_]+\s*%>/);
|
|
201
|
+
} finally {
|
|
202
|
+
if (previousAnthropicKey === undefined) delete process.env.ANTHROPIC_API_KEY;
|
|
203
|
+
else process.env.ANTHROPIC_API_KEY = previousAnthropicKey;
|
|
204
|
+
if (previousOpenAiKey === undefined) delete process.env.OPENAI_API_KEY;
|
|
205
|
+
else process.env.OPENAI_API_KEY = previousOpenAiKey;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('_translate uses assign() with the absolute built-in prompt path', async () => {
|
|
210
|
+
const gp = createTestInstance();
|
|
211
|
+
const originalMmix = GPTrans.mmix;
|
|
212
|
+
const calls = {};
|
|
213
|
+
const model = {
|
|
214
|
+
setSystem(value) {
|
|
215
|
+
calls.system = value;
|
|
216
|
+
},
|
|
217
|
+
addTextFromFile(value) {
|
|
218
|
+
calls.promptFile = value;
|
|
219
|
+
},
|
|
220
|
+
assign(value) {
|
|
221
|
+
calls.templateData = value;
|
|
222
|
+
},
|
|
223
|
+
async block(options) {
|
|
224
|
+
calls.blockOptions = options;
|
|
225
|
+
return 'Hola';
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
GPTrans.mmix = () => model;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
assert.equal(await gp._translate('Hello'), 'Hola');
|
|
232
|
+
assert.equal(path.isAbsolute(calls.promptFile), true);
|
|
233
|
+
assert.equal(path.basename(calls.promptFile), 'translate.md');
|
|
234
|
+
await access(calls.promptFile);
|
|
235
|
+
assert.equal(calls.templateData.INPUT, 'Hello');
|
|
236
|
+
assert.equal(calls.templateData.FROM_ISO, 'en-US');
|
|
237
|
+
assert.equal(calls.templateData.TARGET_ISO, 'es');
|
|
238
|
+
assert.match(calls.system, /<%- FROM_LANG %>/);
|
|
239
|
+
assert.deepEqual(calls.blockOptions, { addSystemExtra: false });
|
|
240
|
+
} finally {
|
|
241
|
+
GPTrans.mmix = originalMmix;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
164
245
|
test('DeepBase 3.9 cache is available to synchronous translations', async () => {
|
|
165
246
|
const directory = await mkdtemp(path.join(tmpdir(), 'gptrans-deepbase-'));
|
|
166
247
|
const originalMmix = GPTrans.mmix;
|
|
@@ -193,6 +274,17 @@ test('DeepBase 3.9 cache is available to synchronous translations', async () =>
|
|
|
193
274
|
|
|
194
275
|
assert.equal(gp.dbFrom.getSync('', sourceKey), sourceText);
|
|
195
276
|
assert.equal(gp.dbTarget.getSync(contextHash, sourceKey), 'Nuevo texto fuente');
|
|
277
|
+
|
|
278
|
+
let refinePromptFile;
|
|
279
|
+
gp._processRefineBatch = async (entries, instruction, promptFile) => {
|
|
280
|
+
assert.equal(entries.length, 2);
|
|
281
|
+
assert.equal(instruction, 'Use a formal tone');
|
|
282
|
+
refinePromptFile = promptFile;
|
|
283
|
+
};
|
|
284
|
+
await gp.refine('Use a formal tone');
|
|
285
|
+
assert.equal(path.isAbsolute(refinePromptFile), true);
|
|
286
|
+
assert.equal(path.basename(refinePromptFile), 'refine.md');
|
|
287
|
+
await access(refinePromptFile);
|
|
196
288
|
} finally {
|
|
197
289
|
GPTrans.mmix = originalMmix;
|
|
198
290
|
if (gp.debounceTimer) {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import GPTrans from './index.js';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
4
|
|
|
9
5
|
async function testImageTranslation() {
|
|
10
6
|
try {
|
|
@@ -12,14 +8,14 @@ async function testImageTranslation() {
|
|
|
12
8
|
|
|
13
9
|
// Initialize GPTrans with Portuguese as target language
|
|
14
10
|
const gptrans = new GPTrans({
|
|
15
|
-
path: path.join(
|
|
11
|
+
path: path.join(import.meta.dirname, 'db'),
|
|
16
12
|
from: 'en-US',
|
|
17
13
|
target: 'pt-BR',
|
|
18
14
|
model: 'sonnet45'
|
|
19
15
|
});
|
|
20
16
|
|
|
21
17
|
// Test image path (you'll need to provide an actual image)
|
|
22
|
-
const testImagePath = path.join(
|
|
18
|
+
const testImagePath = path.join(import.meta.dirname, 'test-image.jpg');
|
|
23
19
|
|
|
24
20
|
// Check if test image exists
|
|
25
21
|
if (!fs.existsSync(testImagePath)) {
|