gptrans 1.9.8 → 2.0.2
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 +303 -0
- package/README.md +24 -1
- package/demo/case_refine.js +61 -0
- package/index.js +169 -59
- package/package.json +5 -2
- package/prompt/refine.md +26 -0
- package/prompt/translate.md +7 -10
- package/skills/gptrans/SKILL.md +306 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modelmix
|
|
3
|
+
description: Instructions for using the ModelMix Node.js library to interact with multiple AI LLM providers through a unified interface. Use when integrating AI models (OpenAI, Anthropic, Google, Groq, Perplexity, Grok, etc.), chaining models with fallback, getting structured JSON from LLMs, adding MCP tools, streaming responses, or managing multi-provider AI workflows in Node.js.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ModelMix Library Skill
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
ModelMix is a Node.js library that provides a unified fluent API to interact with multiple AI LLM providers. It handles automatic fallback between models, round-robin load balancing, structured JSON output, streaming, MCP tool integration, rate limiting, and token tracking.
|
|
11
|
+
|
|
12
|
+
Use this skill when:
|
|
13
|
+
- Integrating one or more AI models into a Node.js project
|
|
14
|
+
- Chaining models with automatic fallback
|
|
15
|
+
- Extracting structured JSON from LLMs
|
|
16
|
+
- Adding MCP tools or custom tools to models
|
|
17
|
+
- Working with templates and file-based prompts
|
|
18
|
+
|
|
19
|
+
Do NOT use this skill for:
|
|
20
|
+
- Python or non-Node.js projects
|
|
21
|
+
- Direct HTTP calls to LLM APIs (use ModelMix instead)
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install modelmix
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Core Concepts
|
|
30
|
+
|
|
31
|
+
### Import
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { ModelMix } from 'modelmix';
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Creating an Instance
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
// Static factory (preferred)
|
|
41
|
+
const model = ModelMix.new();
|
|
42
|
+
|
|
43
|
+
// With global options
|
|
44
|
+
const model = ModelMix.new({
|
|
45
|
+
options: { max_tokens: 4096, temperature: 0.7 },
|
|
46
|
+
config: {
|
|
47
|
+
system: "You are a helpful assistant.",
|
|
48
|
+
max_history: 5,
|
|
49
|
+
debug: 0, // 0=silent, 1=minimal, 2=summary, 3=full
|
|
50
|
+
roundRobin: false // false=fallback, true=rotate models
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Attaching Models (Fluent Chain)
|
|
56
|
+
|
|
57
|
+
Chain shorthand methods to attach providers. First model is primary; others are fallbacks:
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const model = ModelMix.new()
|
|
61
|
+
.sonnet45() // primary
|
|
62
|
+
.gpt5mini() // fallback 1
|
|
63
|
+
.gemini3flash() // fallback 2
|
|
64
|
+
.addText("Hello!")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If `sonnet45` fails, it automatically tries `gpt5mini`, then `gemini3flash`.
|
|
68
|
+
|
|
69
|
+
## Available Model Shorthands
|
|
70
|
+
|
|
71
|
+
- **OpenAI**: `gpt52` `gpt51` `gpt5` `gpt5mini` `gpt5nano` `gpt41` `gpt41mini` `gpt41nano`
|
|
72
|
+
- **Anthropic**: `opus46` `opus45` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
|
|
73
|
+
- **Google**: `gemini3pro` `gemini3flash` `gemini25pro` `gemini25flash`
|
|
74
|
+
- **Grok**: `grok4` `grok41` (thinking variant available)
|
|
75
|
+
- **Perplexity**: `sonar` `sonarPro`
|
|
76
|
+
- **Groq**: `scout` `maverick`
|
|
77
|
+
- **Together**: `qwen3` `kimiK2`
|
|
78
|
+
- **Multi-provider**: `deepseekR1` `gptOss`
|
|
79
|
+
- **MiniMax**: `minimaxM21`
|
|
80
|
+
- **Fireworks**: `deepseekV32` `GLM47`
|
|
81
|
+
|
|
82
|
+
Each method is called as `mix.methodName()` and accepts optional `{ options, config }` to override per-model settings.
|
|
83
|
+
|
|
84
|
+
## Common Tasks
|
|
85
|
+
|
|
86
|
+
### Get a text response
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const answer = await ModelMix.new()
|
|
90
|
+
.gpt5mini()
|
|
91
|
+
.addText("What is the capital of France?")
|
|
92
|
+
.message();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Get structured JSON
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const result = await ModelMix.new()
|
|
99
|
+
.gpt5mini()
|
|
100
|
+
.addText("Name and capital of 3 South American countries.")
|
|
101
|
+
.json(
|
|
102
|
+
{ countries: [{ name: "", capital: "" }] }, // schema example
|
|
103
|
+
{ countries: [{ name: "country name", capital: "in uppercase" }] }, // descriptions
|
|
104
|
+
{ addNote: true } // options
|
|
105
|
+
);
|
|
106
|
+
// result.countries → [{ name: "Brazil", capital: "BRASILIA" }, ...]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`json()` signature: `json(schemaExample, schemaDescription?, { addSchema, addExample, addNote }?)`
|
|
110
|
+
|
|
111
|
+
### Stream a response
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
await ModelMix.new()
|
|
115
|
+
.gpt5mini()
|
|
116
|
+
.addText("Tell me a story.")
|
|
117
|
+
.stream(({ delta, message }) => {
|
|
118
|
+
process.stdout.write(delta);
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Get raw response (tokens, thinking, tool calls)
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
const raw = await ModelMix.new()
|
|
126
|
+
.sonnet45think()
|
|
127
|
+
.addText("Solve this step by step: 2+2*3")
|
|
128
|
+
.raw();
|
|
129
|
+
// raw.message, raw.think, raw.tokens, raw.toolCalls, raw.response
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Access full response after `message()` or `json()` with `lastRaw`
|
|
133
|
+
|
|
134
|
+
After calling `message()`, `json()`, `block()`, or `stream()`, use `lastRaw` to access the complete response (tokens, thinking, tool calls, etc.). It has the same structure as `raw()`.
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const model = ModelMix.new().gpt5mini().addText("Hello!");
|
|
138
|
+
const text = await model.message();
|
|
139
|
+
console.log(model.lastRaw.tokens);
|
|
140
|
+
// { input: 122, output: 86, total: 541, cost: 0.000319 }
|
|
141
|
+
console.log(model.lastRaw.think); // reasoning content (if available)
|
|
142
|
+
console.log(model.lastRaw.response); // raw API response
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Add images
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const model = ModelMix.new().sonnet45();
|
|
149
|
+
model.addImage('./photo.jpg'); // from file
|
|
150
|
+
model.addImageFromUrl('https://example.com/img.png'); // from URL
|
|
151
|
+
model.addText('Describe this image.');
|
|
152
|
+
const description = await model.message();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Use templates with placeholders
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
const model = ModelMix.new().gpt5mini();
|
|
159
|
+
model.setSystemFromFile('./prompts/system.md');
|
|
160
|
+
model.addTextFromFile('./prompts/task.md');
|
|
161
|
+
model.replace({
|
|
162
|
+
'{role}': 'data analyst',
|
|
163
|
+
'{language}': 'Spanish'
|
|
164
|
+
});
|
|
165
|
+
model.replaceKeyFromFile('{code}', './src/utils.js');
|
|
166
|
+
console.log(await model.message());
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Round-robin load balancing
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
const pool = ModelMix.new({ config: { roundRobin: true } })
|
|
173
|
+
.gpt5mini()
|
|
174
|
+
.sonnet45()
|
|
175
|
+
.gemini3flash();
|
|
176
|
+
|
|
177
|
+
// Each call rotates to the next model
|
|
178
|
+
const r1 = await pool.new().addText("Request 1").message();
|
|
179
|
+
const r2 = await pool.new().addText("Request 2").message();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### MCP integration (external tools)
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
const model = ModelMix.new({ config: { max_history: 10 } }).gpt5nano();
|
|
186
|
+
model.setSystem('You are an assistant. Today is ' + new Date().toISOString());
|
|
187
|
+
await model.addMCP('@modelcontextprotocol/server-brave-search');
|
|
188
|
+
model.addText('Use Internet: What is the latest news about AI?');
|
|
189
|
+
console.log(await model.message());
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Requires `BRAVE_API_KEY` in `.env` for Brave Search MCP.
|
|
193
|
+
|
|
194
|
+
### Custom local tools (addTool)
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
const model = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
|
|
198
|
+
|
|
199
|
+
model.addTool({
|
|
200
|
+
name: "get_weather",
|
|
201
|
+
description: "Get weather for a city",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: { city: { type: "string" } },
|
|
205
|
+
required: ["city"]
|
|
206
|
+
}
|
|
207
|
+
}, async ({ city }) => {
|
|
208
|
+
return `The weather in ${city} is sunny, 25C`;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
model.addText("What's the weather in Tokyo?");
|
|
212
|
+
console.log(await model.message());
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Rate limiting (Bottleneck)
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
const model = ModelMix.new({
|
|
219
|
+
config: {
|
|
220
|
+
bottleneck: {
|
|
221
|
+
maxConcurrent: 4,
|
|
222
|
+
minTime: 1000
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}).gpt5mini();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Debug mode
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const model = ModelMix.new({
|
|
232
|
+
config: { debug: 2 } // 0=silent, 1=minimal, 2=summary, 3=full
|
|
233
|
+
}).gpt5mini();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
For full debug output, also set the env: `DEBUG=ModelMix* node script.js`
|
|
237
|
+
|
|
238
|
+
### Use free-tier models
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
// These use providers with free quotas (OpenRouter, Groq, Cerebras)
|
|
242
|
+
const model = ModelMix.new()
|
|
243
|
+
.gptOss()
|
|
244
|
+
.kimiK2()
|
|
245
|
+
.deepseekR1()
|
|
246
|
+
.hermes3()
|
|
247
|
+
.addText("What is the capital of France?");
|
|
248
|
+
console.log(await model.message());
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Conversation history
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
const chat = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
|
|
255
|
+
chat.addText("My name is Martin.");
|
|
256
|
+
await chat.message();
|
|
257
|
+
chat.addText("What's my name?");
|
|
258
|
+
const reply = await chat.message(); // "Martin"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Agent Usage Rules
|
|
262
|
+
|
|
263
|
+
- Always check `package.json` for `modelmix` before running `npm install`.
|
|
264
|
+
- Use `ModelMix.new()` static factory to create instances (not `new ModelMix()`).
|
|
265
|
+
- Store API keys in `.env` and load with `dotenv/config` or `process.loadEnvFile()`. Never hardcode keys.
|
|
266
|
+
- Chain models for resilience: primary model first, fallbacks after.
|
|
267
|
+
- When using MCP tools or `addTool()`, set `max_history` to at least 3.
|
|
268
|
+
- Use `.json()` for structured output instead of parsing text manually.
|
|
269
|
+
- Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
|
|
270
|
+
- For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
|
|
271
|
+
- Template placeholders use `{key}` syntax in both system prompts and user messages.
|
|
272
|
+
- The library uses CommonJS internally (`require`) but supports ESM import via `{ ModelMix }`.
|
|
273
|
+
- Available provider Mix classes for custom setups: `MixOpenAI`, `MixAnthropic`, `MixGoogle`, `MixPerplexity`, `MixGroq`, `MixTogether`, `MixGrok`, `MixOpenRouter`, `MixOllama`, `MixLMStudio`, `MixCustom`, `MixCerebras`, `MixFireworks`, `MixMiniMax`.
|
|
274
|
+
|
|
275
|
+
## API Quick Reference
|
|
276
|
+
|
|
277
|
+
| Method | Returns | Description |
|
|
278
|
+
| --- | --- | --- |
|
|
279
|
+
| `.addText(text)` | `this` | Add user message |
|
|
280
|
+
| `.addTextFromFile(path)` | `this` | Add user message from file |
|
|
281
|
+
| `.setSystem(text)` | `this` | Set system prompt |
|
|
282
|
+
| `.setSystemFromFile(path)` | `this` | Set system prompt from file |
|
|
283
|
+
| `.addImage(path)` | `this` | Add image from file |
|
|
284
|
+
| `.addImageFromUrl(url)` | `this` | Add image from URL or data URI |
|
|
285
|
+
| `.replace({})` | `this` | Set placeholder replacements |
|
|
286
|
+
| `.replaceKeyFromFile(key, path)` | `this` | Replace placeholder with file content |
|
|
287
|
+
| `.message()` | `Promise<string>` | Get text response |
|
|
288
|
+
| `.json(example, desc?, opts?)` | `Promise<object>` | Get structured JSON |
|
|
289
|
+
| `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
|
|
290
|
+
| `.lastRaw` | `object \| null` | Full response from last `message()`/`json()`/`block()`/`stream()` call |
|
|
291
|
+
| `.stream(callback)` | `Promise` | Stream response |
|
|
292
|
+
| `.block()` | `Promise<string>` | Extract code block from response |
|
|
293
|
+
| `.addMCP(package)` | `Promise` | Add MCP server tools |
|
|
294
|
+
| `.addTool(def, callback)` | `this` | Register custom local tool |
|
|
295
|
+
| `.addTools([{tool, callback}])` | `this` | Register multiple tools |
|
|
296
|
+
| `.removeTool(name)` | `this` | Remove a tool |
|
|
297
|
+
| `.listTools()` | `{local, mcp}` | List registered tools |
|
|
298
|
+
| `.new()` | `ModelMix` | Clone instance sharing models |
|
|
299
|
+
| `.attach(key, provider)` | `this` | Attach custom provider |
|
|
300
|
+
|
|
301
|
+
## References
|
|
302
|
+
|
|
303
|
+
- [GitHub Repository](https://github.com/clasen/ModelMix)
|
package/README.md
CHANGED
|
@@ -21,7 +21,10 @@ Whether you're building a multilingual website, a mobile app, or a localization
|
|
|
21
21
|
```bash
|
|
22
22
|
npm install gptrans
|
|
23
23
|
```
|
|
24
|
-
|
|
24
|
+
> **AI Skill**: You can also add GPTrans as a skill for AI agentic development:
|
|
25
|
+
> ```bash
|
|
26
|
+
> npx skills add https://github.com/clasen/GPTrans --skill gptrans
|
|
27
|
+
> ```
|
|
25
28
|
### 🌐 Environment Setup
|
|
26
29
|
|
|
27
30
|
GPTrans uses dotenv for environment configuration. Create a `.env` file in your project root and add your API keys:
|
|
@@ -214,6 +217,26 @@ When using multiple models:
|
|
|
214
217
|
- If the primary model fails (due to API errors, rate limits, etc.), GPTrans automatically falls back to the next model
|
|
215
218
|
- This ensures higher availability and resilience of your translation service
|
|
216
219
|
|
|
220
|
+
## ✏️ Refining Translations
|
|
221
|
+
|
|
222
|
+
The `refine()` method lets you improve existing translations by running them through the AI again with a specific instruction. It processes translations in batches (same as the translation flow) and only updates entries that genuinely benefit from the refinement.
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
226
|
+
|
|
227
|
+
// After translations already exist...
|
|
228
|
+
// Refine with a single instruction
|
|
229
|
+
await gptrans.refine('Use a more natural and colloquial tone');
|
|
230
|
+
|
|
231
|
+
// Or pass multiple instructions at once (single API pass, saves tokens)
|
|
232
|
+
await gptrans.refine([
|
|
233
|
+
'Shorten texts where possible without losing meaning',
|
|
234
|
+
'Use a more colloquial tone'
|
|
235
|
+
]);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
> **Note:** The refine method uses the **target translation** as input (not the original source text). If the AI determines a translation is already good, it keeps it unchanged. Passing an array of instructions is preferred over multiple `refine()` calls since it processes everything in a single API pass.
|
|
239
|
+
|
|
217
240
|
# 🖼️ Image Translation
|
|
218
241
|
|
|
219
242
|
Intelligent image translation using Google's Gemini AI.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import GPTrans from '../index.js';
|
|
2
|
+
|
|
3
|
+
console.log('🚀 Testing GPTrans Refine\n');
|
|
4
|
+
console.log('='.repeat(70));
|
|
5
|
+
|
|
6
|
+
async function testRefine() {
|
|
7
|
+
// Step 1: Create initial translations
|
|
8
|
+
const gptrans = new GPTrans({
|
|
9
|
+
from: 'en-US',
|
|
10
|
+
target: 'es-AR',
|
|
11
|
+
model: 'sonnet45',
|
|
12
|
+
name: 'refine_test'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const texts = [
|
|
16
|
+
'You have exceeded the maximum number of attempts',
|
|
17
|
+
'Your session has expired, please log in again',
|
|
18
|
+
'The operation was completed successfully',
|
|
19
|
+
'An unexpected error occurred, please try again later',
|
|
20
|
+
'Are you sure you want to delete this item?'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
console.log('\n📝 Step 1: Creating initial translations...\n');
|
|
24
|
+
texts.forEach(text => {
|
|
25
|
+
console.log(` EN: ${text}`);
|
|
26
|
+
console.log(` ES: ${gptrans.t(text)}\n`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await gptrans.preload();
|
|
30
|
+
|
|
31
|
+
console.log('='.repeat(70));
|
|
32
|
+
console.log('\n📝 Step 2: Translations before refine:\n');
|
|
33
|
+
texts.forEach(text => {
|
|
34
|
+
console.log(` EN: ${text}`);
|
|
35
|
+
console.log(` ES: ${gptrans.t(text)}\n`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Step 2: Refine with multiple instructions in a single pass
|
|
39
|
+
console.log('='.repeat(70));
|
|
40
|
+
console.log('\n🔄 Step 3: Refining with multiple instructions (single API pass)...\n');
|
|
41
|
+
|
|
42
|
+
await gptrans.refine([
|
|
43
|
+
'Use "vos" instead of "tú" for all second-person references',
|
|
44
|
+
'Use a more friendly and colloquial tone, less robotic',
|
|
45
|
+
'Shorten messages where possible without losing clarity'
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
console.log('📝 Translations after refine:\n');
|
|
49
|
+
texts.forEach(text => {
|
|
50
|
+
console.log(` EN: ${text}`);
|
|
51
|
+
console.log(` ES: ${gptrans.t(text)}\n`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log('='.repeat(70));
|
|
55
|
+
console.log('\n✅ Refine test completed!\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
testRefine().catch(error => {
|
|
59
|
+
console.error('\n❌ Error:', error.message);
|
|
60
|
+
console.error(error.stack);
|
|
61
|
+
});
|
package/index.js
CHANGED
|
@@ -11,11 +11,11 @@ class GPTrans {
|
|
|
11
11
|
static #mmixInstances = new Map();
|
|
12
12
|
static #translationLocks = new Map();
|
|
13
13
|
|
|
14
|
-
static mmix(models = 'sonnet45', { debug =
|
|
14
|
+
static mmix(models = 'sonnet45', { debug = 0 } = {}) {
|
|
15
15
|
const key = Array.isArray(models) ? models.join(',') : models;
|
|
16
16
|
|
|
17
17
|
if (!this.#mmixInstances.has(key)) {
|
|
18
|
-
|
|
18
|
+
let instance = ModelMix.new({
|
|
19
19
|
config: {
|
|
20
20
|
max_history: 1,
|
|
21
21
|
debug,
|
|
@@ -25,9 +25,6 @@ class GPTrans {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
|
-
|
|
29
|
-
// Chain models dynamically
|
|
30
|
-
let instance = mmix;
|
|
31
28
|
const modelArray = Array.isArray(models) ? models : [models];
|
|
32
29
|
|
|
33
30
|
for (const model of modelArray) {
|
|
@@ -218,15 +215,15 @@ class GPTrans {
|
|
|
218
215
|
const textsToTranslate = batch.map(([_, text]) => text).join(`\n${this.divider}\n`);
|
|
219
216
|
try {
|
|
220
217
|
const translations = await this._translate(textsToTranslate, batch, batchReferences, this.preloadBaseLanguage);
|
|
221
|
-
|
|
218
|
+
|
|
222
219
|
// Try different split strategies to be more robust
|
|
223
220
|
let translatedTexts = translations.split(`\n${this.divider}\n`);
|
|
224
|
-
|
|
221
|
+
|
|
225
222
|
// If split doesn't match batch size, try alternative separators
|
|
226
223
|
if (translatedTexts.length !== batch.length) {
|
|
227
224
|
// Try without newlines around divider
|
|
228
225
|
translatedTexts = translations.split(this.divider);
|
|
229
|
-
|
|
226
|
+
|
|
230
227
|
// If still doesn't match, try with just newline
|
|
231
228
|
if (translatedTexts.length !== batch.length) {
|
|
232
229
|
translatedTexts = translations.split(/\n{2,}/); // Split by multiple newlines
|
|
@@ -234,7 +231,7 @@ class GPTrans {
|
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
const contextHash = this._hash(context);
|
|
237
|
-
|
|
234
|
+
|
|
238
235
|
// Validate we have the right number of translations
|
|
239
236
|
if (translatedTexts.length !== batch.length) {
|
|
240
237
|
console.error(`❌ Translation count mismatch:`);
|
|
@@ -242,7 +239,7 @@ class GPTrans {
|
|
|
242
239
|
console.error(` Received: ${translatedTexts.length} translations`);
|
|
243
240
|
console.error(` Batch keys: ${batch.map(([key]) => key).join(', ')}`);
|
|
244
241
|
console.error(`\n📝 Full response from model:\n${translations}\n`);
|
|
245
|
-
|
|
242
|
+
|
|
246
243
|
// Try to save what we can
|
|
247
244
|
const minLength = Math.min(translatedTexts.length, batch.length);
|
|
248
245
|
for (let i = 0; i < minLength; i++) {
|
|
@@ -276,18 +273,14 @@ class GPTrans {
|
|
|
276
273
|
try {
|
|
277
274
|
const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
|
|
278
275
|
|
|
279
|
-
model.setSystem("You are an expert translator specialized in literary translation between FROM_LANG and TARGET_DENONYM TARGET_LANG.");
|
|
276
|
+
model.setSystem("You are an expert translator specialized in literary translation between {FROM_LANG} and {TARGET_DENONYM} {TARGET_LANG}.");
|
|
280
277
|
|
|
281
|
-
//
|
|
282
|
-
let promptContent = fs.readFileSync(this.promptFile, 'utf-8');
|
|
283
|
-
|
|
284
|
-
// Format references if available
|
|
278
|
+
// Build references section (includes header when references exist, empty otherwise)
|
|
285
279
|
let referencesText = '';
|
|
286
280
|
if (Object.keys(batchReferences).length > 0 && batch.length > 0) {
|
|
287
|
-
// Group all references by language first
|
|
288
281
|
const refsByLang = {};
|
|
289
|
-
|
|
290
|
-
batch.forEach(([key]
|
|
282
|
+
|
|
283
|
+
batch.forEach(([key]) => {
|
|
291
284
|
if (batchReferences[key]) {
|
|
292
285
|
Object.entries(batchReferences[key]).forEach(([lang, translation]) => {
|
|
293
286
|
if (!refsByLang[lang]) {
|
|
@@ -297,8 +290,7 @@ class GPTrans {
|
|
|
297
290
|
});
|
|
298
291
|
}
|
|
299
292
|
});
|
|
300
|
-
|
|
301
|
-
// Format: one language header, then all its translations with bullets
|
|
293
|
+
|
|
302
294
|
const refBlocks = Object.entries(refsByLang).map(([lang, translations]) => {
|
|
303
295
|
try {
|
|
304
296
|
const langInfo = isoAssoc(lang);
|
|
@@ -311,17 +303,8 @@ class GPTrans {
|
|
|
311
303
|
return `${header}\n${content}`;
|
|
312
304
|
}
|
|
313
305
|
});
|
|
314
|
-
|
|
315
|
-
referencesText = refBlocks.join(`\n\n`);
|
|
316
|
-
}
|
|
317
306
|
|
|
318
|
-
|
|
319
|
-
if (!referencesText) {
|
|
320
|
-
// Remove the entire "Reference Translations" section
|
|
321
|
-
promptContent = promptContent.replace(
|
|
322
|
-
/## Reference Translations \(for context\)[\s\S]*?(?=\n#|$)/,
|
|
323
|
-
''
|
|
324
|
-
);
|
|
307
|
+
referencesText = `## Reference Translations (for context)\nThese are existing translations in other languages that may help you provide a more accurate translation. Use them as reference but do not simply copy them:\n\n${refBlocks.join('\n\n')}`;
|
|
325
308
|
}
|
|
326
309
|
|
|
327
310
|
// Determine which FROM_ values to use
|
|
@@ -334,30 +317,23 @@ class GPTrans {
|
|
|
334
317
|
}
|
|
335
318
|
}
|
|
336
319
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
320
|
+
// Use ModelMix templates: addTextFromFile + replace + block
|
|
321
|
+
model.addTextFromFile(this.promptFile);
|
|
322
|
+
model.replace({
|
|
323
|
+
'{INPUT}': text,
|
|
324
|
+
'{CONTEXT}': this.context,
|
|
325
|
+
'{REFERENCES}': referencesText,
|
|
326
|
+
'{TARGET_ISO}': this.replaceTarget.TARGET_ISO,
|
|
327
|
+
'{TARGET_LANG}': this.replaceTarget.TARGET_LANG,
|
|
328
|
+
'{TARGET_COUNTRY}': this.replaceTarget.TARGET_COUNTRY,
|
|
329
|
+
'{TARGET_DENONYM}': this.replaceTarget.TARGET_DENONYM,
|
|
330
|
+
'{FROM_ISO}': fromReplace.FROM_ISO,
|
|
331
|
+
'{FROM_LANG}': fromReplace.FROM_LANG,
|
|
332
|
+
'{FROM_COUNTRY}': fromReplace.FROM_COUNTRY,
|
|
333
|
+
'{FROM_DENONYM}': fromReplace.FROM_DENONYM,
|
|
346
334
|
});
|
|
347
|
-
Object.entries(fromReplace).forEach(([key, value]) => {
|
|
348
|
-
promptContent = promptContent.replace(new RegExp(key, 'g'), value);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
model.addText(promptContent);
|
|
352
|
-
|
|
353
|
-
const response = await model.message();
|
|
354
335
|
|
|
355
|
-
|
|
356
|
-
const codeBlockRegex = /```(?:\w*\n)?([\s\S]*?)```/;
|
|
357
|
-
const match = response.match(codeBlockRegex);
|
|
358
|
-
const translatedText = match ? match[1].trim() : response.trim();
|
|
359
|
-
|
|
360
|
-
return translatedText;
|
|
336
|
+
return await model.block({ addSystemExtra: false });
|
|
361
337
|
|
|
362
338
|
} finally {
|
|
363
339
|
// Always release the lock
|
|
@@ -431,14 +407,14 @@ class GPTrans {
|
|
|
431
407
|
|
|
432
408
|
// Track which keys need translation
|
|
433
409
|
const keysNeedingTranslation = [];
|
|
434
|
-
|
|
410
|
+
|
|
435
411
|
for (const [context, pairs] of this.dbFrom.entries()) {
|
|
436
412
|
// Skip the _context metadata
|
|
437
413
|
if (context === '_context') continue;
|
|
438
|
-
|
|
414
|
+
|
|
439
415
|
this.setContext(context);
|
|
440
416
|
const contextHash = this._hash(context);
|
|
441
|
-
|
|
417
|
+
|
|
442
418
|
for (const [key, text] of Object.entries(pairs)) {
|
|
443
419
|
// Check if translation already exists
|
|
444
420
|
if (!this.dbTarget.get(contextHash, key)) {
|
|
@@ -462,7 +438,7 @@ class GPTrans {
|
|
|
462
438
|
const checkInterval = setInterval(() => {
|
|
463
439
|
// Check if there are still pending translations or batch being processed
|
|
464
440
|
const hasPending = this.pendingTranslations.size > 0 || this.isProcessingBatch;
|
|
465
|
-
|
|
441
|
+
|
|
466
442
|
// Check if all needed translations are now complete
|
|
467
443
|
let allTranslated = true;
|
|
468
444
|
for (const { contextHash, key } of keysNeedingTranslation) {
|
|
@@ -471,7 +447,7 @@ class GPTrans {
|
|
|
471
447
|
break;
|
|
472
448
|
}
|
|
473
449
|
}
|
|
474
|
-
|
|
450
|
+
|
|
475
451
|
if (allTranslated && !hasPending) {
|
|
476
452
|
clearInterval(checkInterval);
|
|
477
453
|
resolve();
|
|
@@ -507,6 +483,140 @@ class GPTrans {
|
|
|
507
483
|
return this;
|
|
508
484
|
}
|
|
509
485
|
|
|
486
|
+
async refine(instruction, { promptFile = null } = {}) {
|
|
487
|
+
// Accept string or array of instructions
|
|
488
|
+
const instructions = Array.isArray(instruction) ? instruction : [instruction];
|
|
489
|
+
const merged = instructions.filter(i => i && i.trim()).join('\n- ');
|
|
490
|
+
|
|
491
|
+
if (!merged) {
|
|
492
|
+
throw new Error('Refinement instruction is required');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const finalInstruction = instructions.length > 1 ? `- ${merged}` : merged;
|
|
496
|
+
const refinePromptFile = promptFile ?? new URL('./prompt/refine.md', import.meta.url).pathname;
|
|
497
|
+
|
|
498
|
+
// Collect all existing translations grouped by contextHash
|
|
499
|
+
const allBatches = [];
|
|
500
|
+
let currentBatch = [];
|
|
501
|
+
let currentCharCount = 0;
|
|
502
|
+
|
|
503
|
+
for (const [contextHash, pairs] of this.dbTarget.entries()) {
|
|
504
|
+
for (const [key, translation] of Object.entries(pairs)) {
|
|
505
|
+
const entryCharCount = translation.length;
|
|
506
|
+
|
|
507
|
+
// If adding this entry exceeds threshold, flush current batch
|
|
508
|
+
if (currentBatch.length > 0 && currentCharCount + entryCharCount >= this.batchThreshold) {
|
|
509
|
+
allBatches.push({ entries: currentBatch, contextHash });
|
|
510
|
+
currentBatch = [];
|
|
511
|
+
currentCharCount = 0;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
currentBatch.push({ key, translation, contextHash });
|
|
515
|
+
currentCharCount += entryCharCount;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Push remaining entries
|
|
520
|
+
if (currentBatch.length > 0) {
|
|
521
|
+
allBatches.push({ entries: currentBatch, contextHash: currentBatch[0].contextHash });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (allBatches.length === 0) {
|
|
525
|
+
return this;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Process each batch sequentially (respecting rate limits via locks)
|
|
529
|
+
for (const batch of allBatches) {
|
|
530
|
+
await this._processRefineBatch(batch.entries, finalInstruction, refinePromptFile);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return this;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async _processRefineBatch(entries, instruction, refinePromptFile) {
|
|
537
|
+
const charCount = entries.reduce((sum, e) => sum + e.translation.length, 0);
|
|
538
|
+
this.modelConfig.options.max_tokens = charCount + 1000;
|
|
539
|
+
const minTime = Math.floor((60000 / (8000 / charCount)) * 1.4);
|
|
540
|
+
GPTrans.mmix(this.modelKey, this.modelMixOptions).limiter.updateSettings({ minTime });
|
|
541
|
+
|
|
542
|
+
const textsToRefine = entries.map(e => e.translation).join(`\n${this.divider}\n`);
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const refined = await this._refineTranslate(textsToRefine, instruction, refinePromptFile);
|
|
546
|
+
|
|
547
|
+
// Split with same strategy as _processBatch
|
|
548
|
+
let refinedTexts = refined.split(`\n${this.divider}\n`);
|
|
549
|
+
|
|
550
|
+
if (refinedTexts.length !== entries.length) {
|
|
551
|
+
refinedTexts = refined.split(this.divider);
|
|
552
|
+
|
|
553
|
+
if (refinedTexts.length !== entries.length) {
|
|
554
|
+
refinedTexts = refined.split(/\n{2,}/);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (refinedTexts.length !== entries.length) {
|
|
559
|
+
console.error(`❌ Refine count mismatch:`);
|
|
560
|
+
console.error(` Expected: ${entries.length} translations`);
|
|
561
|
+
console.error(` Received: ${refinedTexts.length} translations`);
|
|
562
|
+
console.error(`\n📝 Full response from model:\n${refined}\n`);
|
|
563
|
+
|
|
564
|
+
// Save what we can
|
|
565
|
+
const minLength = Math.min(refinedTexts.length, entries.length);
|
|
566
|
+
for (let i = 0; i < minLength; i++) {
|
|
567
|
+
if (refinedTexts[i] && refinedTexts[i].trim()) {
|
|
568
|
+
this.dbTarget.set(entries[i].contextHash, entries[i].key, refinedTexts[i].trim());
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
entries.forEach((entry, index) => {
|
|
575
|
+
const refinedText = refinedTexts[index]?.trim();
|
|
576
|
+
if (!refinedText) {
|
|
577
|
+
console.error(`❌ No refined text for ${entry.key} at index ${index}`);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
this.dbTarget.set(entry.contextHash, entry.key, refinedText);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
} catch (e) {
|
|
584
|
+
console.error('❌ Error in _processRefineBatch:', e.message);
|
|
585
|
+
console.error(e.stack);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async _refineTranslate(text, instruction, refinePromptFile) {
|
|
590
|
+
const releaseLock = await GPTrans.#acquireTranslationLock(this.modelKey);
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
|
|
594
|
+
|
|
595
|
+
model.setSystem("You are an expert translator and editor specialized in refining {TARGET_DENONYM} {TARGET_LANG} translations.");
|
|
596
|
+
|
|
597
|
+
// Use ModelMix templates: addTextFromFile + replace + block
|
|
598
|
+
model.addTextFromFile(refinePromptFile);
|
|
599
|
+
model.replace({
|
|
600
|
+
'{INPUT}': text,
|
|
601
|
+
'{INSTRUCTION}': instruction,
|
|
602
|
+
'{CONTEXT}': this.context,
|
|
603
|
+
'{TARGET_ISO}': this.replaceTarget.TARGET_ISO,
|
|
604
|
+
'{TARGET_LANG}': this.replaceTarget.TARGET_LANG,
|
|
605
|
+
'{TARGET_COUNTRY}': this.replaceTarget.TARGET_COUNTRY,
|
|
606
|
+
'{TARGET_DENONYM}': this.replaceTarget.TARGET_DENONYM,
|
|
607
|
+
'{FROM_ISO}': this.replaceFrom.FROM_ISO,
|
|
608
|
+
'{FROM_LANG}': this.replaceFrom.FROM_LANG,
|
|
609
|
+
'{FROM_COUNTRY}': this.replaceFrom.FROM_COUNTRY,
|
|
610
|
+
'{FROM_DENONYM}': this.replaceFrom.FROM_DENONYM,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return await model.block({ addSystemExtra: false });
|
|
614
|
+
|
|
615
|
+
} finally {
|
|
616
|
+
releaseLock();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
510
620
|
async img(imagePath, options = {}) {
|
|
511
621
|
const {
|
|
512
622
|
quality = '1K',
|
|
@@ -586,7 +696,7 @@ class GPTrans {
|
|
|
586
696
|
// Save translated image - preserve original file format
|
|
587
697
|
const filename = path.basename(targetPath, path.extname(targetPath));
|
|
588
698
|
const formatOptions = generator.getReferenceMetadata();
|
|
589
|
-
|
|
699
|
+
|
|
590
700
|
// Apply default quality settings for JPEG images
|
|
591
701
|
if (formatOptions && (formatOptions.format === 'jpeg' || formatOptions.format === 'jpg')) {
|
|
592
702
|
// Apply custom quality if specified, otherwise use defaults
|
|
@@ -595,7 +705,7 @@ class GPTrans {
|
|
|
595
705
|
formatOptions.chromaSubsampling = '4:4:4'; // Better color quality
|
|
596
706
|
formatOptions.optimiseCoding = true; // Lossless size reduction
|
|
597
707
|
}
|
|
598
|
-
|
|
708
|
+
|
|
599
709
|
await generator.save({ directory: targetDir, filename, formatOptions });
|
|
600
710
|
} else {
|
|
601
711
|
throw new Error('No translated image was generated');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gptrans",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.2",
|
|
5
5
|
"description": "🚆 GPTrans - The smarter AI-powered way to translate.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"translate",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"translation",
|
|
16
16
|
"translator",
|
|
17
17
|
"magic",
|
|
18
|
+
"agentic",
|
|
19
|
+
"skill",
|
|
20
|
+
"image",
|
|
18
21
|
"clasen"
|
|
19
22
|
],
|
|
20
23
|
"repository": {
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
"dotenv": "^16.4.7",
|
|
38
41
|
"form-data": "^4.0.4",
|
|
39
42
|
"genmix": "^1.0.4",
|
|
40
|
-
"modelmix": "^4.
|
|
43
|
+
"modelmix": "^4.3.6",
|
|
41
44
|
"string-hash": "^1.1.3"
|
|
42
45
|
}
|
|
43
46
|
}
|
package/prompt/refine.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Goal
|
|
2
|
+
Refine existing translations in {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) based on the following instruction.
|
|
3
|
+
|
|
4
|
+
## Refinement Instruction
|
|
5
|
+
{INSTRUCTION}
|
|
6
|
+
|
|
7
|
+
## Current Translations to Evaluate
|
|
8
|
+
```
|
|
9
|
+
{INPUT}
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
# Return Format
|
|
13
|
+
- Provide the refined translations within a code block using ```.
|
|
14
|
+
- Return each translation separated by `------` (same divider as the input).
|
|
15
|
+
- If a translation is already good and does not need improvement, return it exactly as-is.
|
|
16
|
+
- Do not add, remove, or reorder translations. The output must have the exact same number of entries as the input.
|
|
17
|
+
|
|
18
|
+
# Warnings
|
|
19
|
+
- **Preserve meaning:** The refined translation must preserve the original meaning. Only improve style, naturalness, or clarity as instructed.
|
|
20
|
+
- **Minimal changes:** Only modify translations that genuinely benefit from the refinement instruction. If the current translation is already good, keep it unchanged.
|
|
21
|
+
- **Proper names:** Do not translate proper names (people, places, brands, etc.) unless they have an officially recognized translation in the target language.
|
|
22
|
+
- **Variables:** Do not translate content between curly braces. These are system variables and must remain exactly the same.
|
|
23
|
+
- **Consistency:** Maintain consistent terminology across all translations in the batch.
|
|
24
|
+
|
|
25
|
+
# Context
|
|
26
|
+
{CONTEXT}
|
package/prompt/translate.md
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
# Goal
|
|
2
|
-
Translation from FROM_ISO to TARGET_ISO (TARGET_DENONYM TARGET_LANG) with cultural adaptations.
|
|
2
|
+
Translation from {FROM_ISO} to {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) with cultural adaptations.
|
|
3
3
|
|
|
4
4
|
## Text to translate
|
|
5
5
|
```
|
|
6
|
-
INPUT
|
|
6
|
+
{INPUT}
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
These are existing translations in other languages that may help you provide a more accurate translation. Use them as reference but do not simply copy them:
|
|
11
|
-
|
|
12
|
-
REFERENCES
|
|
9
|
+
{REFERENCES}
|
|
13
10
|
|
|
14
11
|
# Return Format
|
|
15
12
|
- Provide the final translation within a code block using ```.
|
|
16
13
|
- Do not include alternative translations, only provide the best translation.
|
|
17
14
|
|
|
18
15
|
# Warnings
|
|
19
|
-
- **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.
|
|
16
|
+
- **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.
|
|
20
17
|
- **Proper names:** Do not translate proper names (people, places, brands, etc.) unless they have an officially recognized translation in the target language.
|
|
21
|
-
- **Cultural references:** Adapt or explain references that are not familiar in TARGET_DENONYM culture, whenever necessary.
|
|
18
|
+
- **Cultural references:** Adapt or explain references that are not familiar in {TARGET_DENONYM} culture, whenever necessary.
|
|
22
19
|
- **Wordplay and humor:** When it's impossible to directly translate wordplay, find a resource that recreates the playful effect.
|
|
23
20
|
- **Idioms:** Do not introduce new idioms or expressions that are not present in the original text.
|
|
24
|
-
- **Variables:** Do not translate content between curly braces
|
|
21
|
+
- **Variables:** Do not translate content between curly braces. These are system variables and must remain exactly the same.
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
# Context
|
|
28
|
-
CONTEXT
|
|
25
|
+
{CONTEXT}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gptrans
|
|
3
|
+
description: Instructions for using the GPTrans Node.js library for AI-powered translations with smart batching, caching, context-aware translations, refinement, and image translation. Use when adding i18n/localization to a Node.js project, translating text or images between languages, or managing multilingual content.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
tags: [nodejs, translation, i18n, localization, ai, gpt, multilingual]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GPTrans Library Skill
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Overview](#overview)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Core Concepts](#core-concepts)
|
|
15
|
+
- [Basic Usage](#basic-usage)
|
|
16
|
+
- [Common Tasks](#common-tasks)
|
|
17
|
+
- [Agent Usage Rules](#agent-usage-rules)
|
|
18
|
+
- [API Quick Reference](#api-quick-reference)
|
|
19
|
+
- [References](#references)
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
GPTrans is a Node.js library that provides AI-powered translations with smart batching, caching, and context awareness. It uses LLMs (via ModelMix) to produce high-quality, culturally-adapted translations while minimizing API calls through intelligent debouncing and batch processing.
|
|
24
|
+
|
|
25
|
+
Use this skill when:
|
|
26
|
+
- Adding internationalization (i18n) or localization to a Node.js application
|
|
27
|
+
- Translating text content between languages using AI
|
|
28
|
+
- Translating images between languages (via Gemini)
|
|
29
|
+
- Managing multilingual content with caching and manual correction support
|
|
30
|
+
- Building tools that need context-aware translations (gender, tone, domain)
|
|
31
|
+
- Pre-translating or bulk-translating content with `preload()`
|
|
32
|
+
- Refining existing translations with specific style instructions
|
|
33
|
+
|
|
34
|
+
Do NOT use this skill for:
|
|
35
|
+
- Python or non-Node.js projects
|
|
36
|
+
- Simple key-value i18n (use `i18next` or similar instead)
|
|
37
|
+
- Projects that don't need AI-quality translations
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install gptrans
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Environment Setup
|
|
46
|
+
|
|
47
|
+
GPTrans requires API keys for the underlying LLM providers. Create a `.env` file:
|
|
48
|
+
|
|
49
|
+
```env
|
|
50
|
+
OPENAI_API_KEY=your_openai_api_key
|
|
51
|
+
ANTHROPIC_API_KEY=your_anthropic_api_key
|
|
52
|
+
GEMINI_API_KEY=your_gemini_api_key # only for image translation
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Dependencies
|
|
56
|
+
|
|
57
|
+
GPTrans uses [ModelMix](https://github.com/clasen/ModelMix) internally for LLM calls. It is installed automatically as a dependency.
|
|
58
|
+
|
|
59
|
+
## Core Concepts
|
|
60
|
+
|
|
61
|
+
### How It Works
|
|
62
|
+
|
|
63
|
+
1. **First call to `t()`**: Returns the original text immediately. The translation is queued in the background.
|
|
64
|
+
2. **Batching**: Queued translations are grouped by character count (`batchThreshold`) and debounce timer (`debounceTimeout`) to optimize API usage and provide better context to the LLM.
|
|
65
|
+
3. **Caching**: Completed translations are stored in `db/gptrans_<locale>.json`. Subsequent calls return instantly from cache.
|
|
66
|
+
4. **Second call to `t()`**: Returns the cached translation.
|
|
67
|
+
|
|
68
|
+
### Language Tags
|
|
69
|
+
|
|
70
|
+
GPTrans uses BCP 47 language tags: `en-US`, `es-AR`, `pt-BR`, `fr`, `de`, etc. Region can be omitted for universal variants (e.g., `es` for generic Spanish).
|
|
71
|
+
|
|
72
|
+
### Translation Cache Files
|
|
73
|
+
|
|
74
|
+
Translations are stored as JSON in `db/` directory:
|
|
75
|
+
- `db/gptrans_<target>.json` — cached translations for target locale
|
|
76
|
+
- `db/gptrans_from_<source>.json` — source text registry
|
|
77
|
+
|
|
78
|
+
You can manually edit translation files to override specific entries.
|
|
79
|
+
|
|
80
|
+
## Basic Usage
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
import GPTrans from 'gptrans';
|
|
84
|
+
|
|
85
|
+
const gptrans = new GPTrans({
|
|
86
|
+
from: 'en-US',
|
|
87
|
+
target: 'es-AR',
|
|
88
|
+
model: 'sonnet45'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Translate text — returns original on first call, cached translation after
|
|
92
|
+
console.log(gptrans.t('Hello, {name}!', { name: 'John' }));
|
|
93
|
+
|
|
94
|
+
// Context-aware translation (e.g., gender)
|
|
95
|
+
console.log(gptrans.setContext('Message is for a woman').t('You are very good'));
|
|
96
|
+
|
|
97
|
+
// Context auto-clears — pass empty to reset
|
|
98
|
+
console.log(gptrans.setContext().t('Welcome back'));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Constructor Options
|
|
102
|
+
|
|
103
|
+
| Option | Type | Default | Description |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| `from` | `string` | `'en-US'` | Source language (BCP 47) |
|
|
106
|
+
| `target` | `string` | `'es'` | Target language (BCP 47) |
|
|
107
|
+
| `model` | `string \| string[]` | `'sonnet45'` | Model key or array for fallback chain |
|
|
108
|
+
| `batchThreshold` | `number` | `1500` | Max characters before triggering batch |
|
|
109
|
+
| `debounceTimeout` | `number` | `500` | Milliseconds to wait before processing |
|
|
110
|
+
| `freeze` | `boolean` | `false` | Prevent new translations from being queued |
|
|
111
|
+
| `name` | `string` | `''` | Instance name (isolates cache files) |
|
|
112
|
+
| `context` | `string` | `''` | Default context for translations |
|
|
113
|
+
| `promptFile` | `string` | `null` | Custom prompt file path (overrides built-in) |
|
|
114
|
+
| `debug` | `boolean` | `false` | Enable debug output |
|
|
115
|
+
|
|
116
|
+
## Common Tasks
|
|
117
|
+
|
|
118
|
+
### Translate text with parameter substitution
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
122
|
+
console.log(gptrans.t('Hello, {name}!', { name: 'Martin' }));
|
|
123
|
+
// First call: "Hello, Martin!" (original)
|
|
124
|
+
// After caching: "Hola, Martin!" (translated)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Use model fallback for resilience
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const gptrans = new GPTrans({
|
|
131
|
+
from: 'en',
|
|
132
|
+
target: 'fr',
|
|
133
|
+
model: ['sonnet45', 'gpt41'] // falls back to gpt41 if sonnet45 fails
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Pre-translate all pending texts
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
const gptrans = new GPTrans({ from: 'en', target: 'es' });
|
|
141
|
+
|
|
142
|
+
// Register texts
|
|
143
|
+
gptrans.t('Welcome');
|
|
144
|
+
gptrans.t('Sign in');
|
|
145
|
+
gptrans.t('Sign out');
|
|
146
|
+
|
|
147
|
+
// Wait for all translations to complete
|
|
148
|
+
await gptrans.preload();
|
|
149
|
+
|
|
150
|
+
// Now all calls return cached translations
|
|
151
|
+
console.log(gptrans.t('Welcome')); // "Bienvenido"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Pre-translate with reference languages
|
|
155
|
+
|
|
156
|
+
Use existing translations in other languages as context for better accuracy:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const gptrans = new GPTrans({ from: 'es', target: 'fr' });
|
|
160
|
+
await gptrans.preload({
|
|
161
|
+
references: ['en', 'pt'] // AI sees English and Portuguese as reference
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Pre-translate using an alternate base language
|
|
166
|
+
|
|
167
|
+
Translate from an intermediate language instead of the original (useful for gender-neutral intermediaries):
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
const gptrans = new GPTrans({ from: 'es', target: 'pt' });
|
|
171
|
+
await gptrans.preload({
|
|
172
|
+
baseLanguage: 'en', // translate FROM English instead of Spanish
|
|
173
|
+
references: ['es'] // show original Spanish for context
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Set context for gender-aware translations
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
181
|
+
|
|
182
|
+
// Context applies to next translation(s) in the batch
|
|
183
|
+
console.log(gptrans.setContext('The user is female').t('You are welcome'));
|
|
184
|
+
|
|
185
|
+
// Reset context
|
|
186
|
+
console.log(gptrans.setContext().t('Thank you'));
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Refine existing translations
|
|
190
|
+
|
|
191
|
+
Improve cached translations with specific instructions:
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
195
|
+
|
|
196
|
+
// Single instruction
|
|
197
|
+
await gptrans.refine('Use a more colloquial tone');
|
|
198
|
+
|
|
199
|
+
// Multiple instructions (single API pass — preferred)
|
|
200
|
+
await gptrans.refine([
|
|
201
|
+
'Use "vos" instead of "tu"',
|
|
202
|
+
'Shorten texts where possible without losing meaning',
|
|
203
|
+
'Use a more friendly and natural tone'
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// Custom refine prompt
|
|
207
|
+
await gptrans.refine('More formal', { promptFile: './my-refine-prompt.md' });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Translate an image
|
|
211
|
+
|
|
212
|
+
Requires `GEMINI_API_KEY`. Auto-detects language folders for output path:
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
const gptrans = new GPTrans({ from: 'en', target: 'es' });
|
|
216
|
+
|
|
217
|
+
// en/banner.jpg → es/banner.jpg (auto sibling folder)
|
|
218
|
+
const translatedPath = await gptrans.img('en/banner.jpg');
|
|
219
|
+
|
|
220
|
+
// With options
|
|
221
|
+
const result = await gptrans.img('en/hero.jpg', {
|
|
222
|
+
quality: '1K',
|
|
223
|
+
jpegQuality: 92,
|
|
224
|
+
prompt: 'Translate all visible text to Spanish. Keep layout and style.'
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Freeze mode (prevent new translations)
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const gptrans = new GPTrans({ from: 'en', target: 'es', freeze: true });
|
|
232
|
+
|
|
233
|
+
// Returns original text, logs "[key] text" — nothing queued
|
|
234
|
+
console.log(gptrans.t('New text'));
|
|
235
|
+
|
|
236
|
+
// Toggle at runtime
|
|
237
|
+
gptrans.setFreeze(false);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Purge orphaned translations
|
|
241
|
+
|
|
242
|
+
Remove cached translations whose source text no longer exists:
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
await gptrans.purge();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Translate between regional variants
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
// Spain Spanish → Argentina Spanish
|
|
252
|
+
const es2ar = new GPTrans({
|
|
253
|
+
from: 'es-ES',
|
|
254
|
+
target: 'es-AR',
|
|
255
|
+
model: 'sonnet45'
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
console.log(es2ar.t('Eres muy bueno'));
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Check language availability
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
import GPTrans from 'gptrans';
|
|
265
|
+
|
|
266
|
+
if (GPTrans.isLanguageAvailable('pt-BR')) {
|
|
267
|
+
// Language is supported
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Agent Usage Rules
|
|
272
|
+
|
|
273
|
+
- Always check `package.json` for `gptrans` before running `npm install`.
|
|
274
|
+
- Store API keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`) in `.env`. Never hardcode keys.
|
|
275
|
+
- Use BCP 47 language tags for `from` and `target` (e.g., `en-US`, `es-AR`, `pt-BR`, or just `es`).
|
|
276
|
+
- The `t()` method is synchronous and non-blocking. It returns the original text on first call and the cached translation on subsequent calls. Do NOT `await` it.
|
|
277
|
+
- To ensure translations are complete before using them, call `await gptrans.preload()` after registering texts with `t()`.
|
|
278
|
+
- Use `setContext()` for gender-aware or domain-specific translations. Context is captured per-batch and auto-resets when changed.
|
|
279
|
+
- Prefer passing an array of instructions to `refine()` over multiple calls — it processes everything in a single API pass.
|
|
280
|
+
- Use model arrays (`model: ['sonnet45', 'gpt41']`) for production resilience with automatic fallback.
|
|
281
|
+
- Translation caches live in `db/gptrans_<locale>.json`. These files can be manually edited to override specific translations.
|
|
282
|
+
- The `name` constructor option isolates cache files (`db/gptrans_<name>_<locale>.json`), useful for multiple independent translation contexts in the same project.
|
|
283
|
+
- When translating images, ensure `GEMINI_API_KEY` is set. The `img()` method auto-creates sibling language folders.
|
|
284
|
+
- Do NOT use `freeze: true` during initial translation — it prevents all new translations from being queued.
|
|
285
|
+
- Variables in curly braces (`{name}`, `{count}`) in source text are preserved through translation. Parameter substitution happens at `t()` call time.
|
|
286
|
+
|
|
287
|
+
## API Quick Reference
|
|
288
|
+
|
|
289
|
+
| Method | Returns | Description |
|
|
290
|
+
| --- | --- | --- |
|
|
291
|
+
| `new GPTrans(options)` | `GPTrans` | Create instance with `from`, `target`, `model`, etc. |
|
|
292
|
+
| `.t(text, params?)` | `string` | Translate text (sync). Returns cached or original. |
|
|
293
|
+
| `.get(key, text)` | `string \| undefined` | Get translation by key, queue if missing. |
|
|
294
|
+
| `.setContext(context?)` | `this` | Set context for next batch (gender, tone, etc.). |
|
|
295
|
+
| `.setFreeze(freeze?)` | `this` | Enable/disable freeze mode at runtime. |
|
|
296
|
+
| `await .preload(options?)` | `this` | Pre-translate all pending texts. Options: `{ references, baseLanguage }`. |
|
|
297
|
+
| `await .refine(instruction, options?)` | `this` | Refine existing translations. Accepts string or array. |
|
|
298
|
+
| `await .img(path, options?)` | `string` | Translate image text. Returns output path. |
|
|
299
|
+
| `await .purge()` | `this` | Remove orphaned translations from cache. |
|
|
300
|
+
| `GPTrans.isLanguageAvailable(code)` | `boolean` | Check if a language code is supported. |
|
|
301
|
+
|
|
302
|
+
## References
|
|
303
|
+
|
|
304
|
+
- [GitHub Repository](https://github.com/clasen/GPTrans)
|
|
305
|
+
- [npm Package](https://www.npmjs.com/package/gptrans)
|
|
306
|
+
- [ModelMix (underlying LLM library)](https://github.com/clasen/ModelMix)
|