@llm-translate/cli 1.0.0-next.1
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/.dockerignore +51 -0
- package/.env.example +33 -0
- package/.github/workflows/docs-pages.yml +57 -0
- package/.github/workflows/release.yml +49 -0
- package/.translaterc.json +44 -0
- package/CLAUDE.md +243 -0
- package/Dockerfile +55 -0
- package/README.md +371 -0
- package/RFC.md +1595 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4494 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1152 -0
- package/dist/index.js +3841 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.yml +56 -0
- package/docs/.vitepress/config.ts +161 -0
- package/docs/api/agent.md +262 -0
- package/docs/api/engine.md +274 -0
- package/docs/api/index.md +171 -0
- package/docs/api/providers.md +304 -0
- package/docs/changelog.md +64 -0
- package/docs/cli/dir.md +243 -0
- package/docs/cli/file.md +213 -0
- package/docs/cli/glossary.md +273 -0
- package/docs/cli/index.md +129 -0
- package/docs/cli/init.md +158 -0
- package/docs/cli/serve.md +211 -0
- package/docs/glossary.json +235 -0
- package/docs/guide/chunking.md +272 -0
- package/docs/guide/configuration.md +139 -0
- package/docs/guide/cost-optimization.md +237 -0
- package/docs/guide/docker.md +371 -0
- package/docs/guide/getting-started.md +150 -0
- package/docs/guide/glossary.md +241 -0
- package/docs/guide/index.md +86 -0
- package/docs/guide/ollama.md +515 -0
- package/docs/guide/prompt-caching.md +221 -0
- package/docs/guide/providers.md +232 -0
- package/docs/guide/quality-control.md +206 -0
- package/docs/guide/vitepress-integration.md +265 -0
- package/docs/index.md +63 -0
- package/docs/ja/api/agent.md +262 -0
- package/docs/ja/api/engine.md +274 -0
- package/docs/ja/api/index.md +171 -0
- package/docs/ja/api/providers.md +304 -0
- package/docs/ja/changelog.md +64 -0
- package/docs/ja/cli/dir.md +243 -0
- package/docs/ja/cli/file.md +213 -0
- package/docs/ja/cli/glossary.md +273 -0
- package/docs/ja/cli/index.md +111 -0
- package/docs/ja/cli/init.md +158 -0
- package/docs/ja/guide/chunking.md +271 -0
- package/docs/ja/guide/configuration.md +139 -0
- package/docs/ja/guide/cost-optimization.md +30 -0
- package/docs/ja/guide/getting-started.md +150 -0
- package/docs/ja/guide/glossary.md +214 -0
- package/docs/ja/guide/index.md +32 -0
- package/docs/ja/guide/ollama.md +410 -0
- package/docs/ja/guide/prompt-caching.md +221 -0
- package/docs/ja/guide/providers.md +232 -0
- package/docs/ja/guide/quality-control.md +137 -0
- package/docs/ja/guide/vitepress-integration.md +265 -0
- package/docs/ja/index.md +58 -0
- package/docs/ko/api/agent.md +262 -0
- package/docs/ko/api/engine.md +274 -0
- package/docs/ko/api/index.md +171 -0
- package/docs/ko/api/providers.md +304 -0
- package/docs/ko/changelog.md +64 -0
- package/docs/ko/cli/dir.md +243 -0
- package/docs/ko/cli/file.md +213 -0
- package/docs/ko/cli/glossary.md +273 -0
- package/docs/ko/cli/index.md +111 -0
- package/docs/ko/cli/init.md +158 -0
- package/docs/ko/guide/chunking.md +271 -0
- package/docs/ko/guide/configuration.md +139 -0
- package/docs/ko/guide/cost-optimization.md +30 -0
- package/docs/ko/guide/getting-started.md +150 -0
- package/docs/ko/guide/glossary.md +214 -0
- package/docs/ko/guide/index.md +32 -0
- package/docs/ko/guide/ollama.md +410 -0
- package/docs/ko/guide/prompt-caching.md +221 -0
- package/docs/ko/guide/providers.md +232 -0
- package/docs/ko/guide/quality-control.md +137 -0
- package/docs/ko/guide/vitepress-integration.md +265 -0
- package/docs/ko/index.md +58 -0
- package/docs/zh/api/agent.md +262 -0
- package/docs/zh/api/engine.md +274 -0
- package/docs/zh/api/index.md +171 -0
- package/docs/zh/api/providers.md +304 -0
- package/docs/zh/changelog.md +64 -0
- package/docs/zh/cli/dir.md +243 -0
- package/docs/zh/cli/file.md +213 -0
- package/docs/zh/cli/glossary.md +273 -0
- package/docs/zh/cli/index.md +111 -0
- package/docs/zh/cli/init.md +158 -0
- package/docs/zh/guide/chunking.md +271 -0
- package/docs/zh/guide/configuration.md +139 -0
- package/docs/zh/guide/cost-optimization.md +30 -0
- package/docs/zh/guide/getting-started.md +150 -0
- package/docs/zh/guide/glossary.md +214 -0
- package/docs/zh/guide/index.md +32 -0
- package/docs/zh/guide/ollama.md +410 -0
- package/docs/zh/guide/prompt-caching.md +221 -0
- package/docs/zh/guide/providers.md +232 -0
- package/docs/zh/guide/quality-control.md +137 -0
- package/docs/zh/guide/vitepress-integration.md +265 -0
- package/docs/zh/index.md +58 -0
- package/package.json +91 -0
- package/release.config.mjs +15 -0
- package/schemas/glossary.schema.json +110 -0
- package/src/cli/commands/dir.ts +469 -0
- package/src/cli/commands/file.ts +291 -0
- package/src/cli/commands/glossary.ts +221 -0
- package/src/cli/commands/init.ts +68 -0
- package/src/cli/commands/serve.ts +60 -0
- package/src/cli/index.ts +64 -0
- package/src/cli/options.ts +59 -0
- package/src/core/agent.ts +1119 -0
- package/src/core/chunker.ts +391 -0
- package/src/core/engine.ts +634 -0
- package/src/errors.ts +188 -0
- package/src/index.ts +147 -0
- package/src/integrations/vitepress.ts +549 -0
- package/src/parsers/markdown.ts +383 -0
- package/src/providers/claude.ts +259 -0
- package/src/providers/interface.ts +109 -0
- package/src/providers/ollama.ts +379 -0
- package/src/providers/openai.ts +308 -0
- package/src/providers/registry.ts +153 -0
- package/src/server/index.ts +152 -0
- package/src/server/middleware/auth.ts +93 -0
- package/src/server/middleware/logger.ts +90 -0
- package/src/server/routes/health.ts +84 -0
- package/src/server/routes/translate.ts +210 -0
- package/src/server/types.ts +138 -0
- package/src/services/cache.ts +899 -0
- package/src/services/config.ts +217 -0
- package/src/services/glossary.ts +247 -0
- package/src/types/analysis.ts +164 -0
- package/src/types/index.ts +265 -0
- package/src/types/modes.ts +121 -0
- package/src/types/mqm.ts +157 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/tokens.ts +116 -0
- package/tests/fixtures/glossaries/ml-glossary.json +53 -0
- package/tests/fixtures/input/lynq-installation.ko.md +350 -0
- package/tests/fixtures/input/lynq-installation.md +350 -0
- package/tests/fixtures/input/simple.ko.md +27 -0
- package/tests/fixtures/input/simple.md +27 -0
- package/tests/unit/chunker.test.ts +229 -0
- package/tests/unit/glossary.test.ts +146 -0
- package/tests/unit/markdown.test.ts +205 -0
- package/tests/unit/tokens.test.ts +81 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +34 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Configuration Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export type ProviderName = 'claude' | 'openai' | 'ollama' | 'custom';
|
|
6
|
+
|
|
7
|
+
export interface TranslateConfig {
|
|
8
|
+
version: string;
|
|
9
|
+
project?: {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
purpose: string;
|
|
13
|
+
};
|
|
14
|
+
languages: {
|
|
15
|
+
source: string;
|
|
16
|
+
targets: string[];
|
|
17
|
+
/** Per-language style instructions (e.g., { "ko": "경어체", "ja": "です・ます調" }) */
|
|
18
|
+
styles?: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
provider: {
|
|
21
|
+
default: ProviderName;
|
|
22
|
+
model?: string;
|
|
23
|
+
fallback?: ProviderName[];
|
|
24
|
+
apiKeys?: Record<ProviderName, string>;
|
|
25
|
+
};
|
|
26
|
+
quality: {
|
|
27
|
+
threshold: number;
|
|
28
|
+
maxIterations: number;
|
|
29
|
+
evaluationMethod: 'llm' | 'embedding' | 'hybrid';
|
|
30
|
+
};
|
|
31
|
+
chunking: {
|
|
32
|
+
maxTokens: number;
|
|
33
|
+
overlapTokens: number;
|
|
34
|
+
preserveStructure: boolean;
|
|
35
|
+
};
|
|
36
|
+
glossary?: {
|
|
37
|
+
path: string;
|
|
38
|
+
strict: boolean;
|
|
39
|
+
};
|
|
40
|
+
paths: {
|
|
41
|
+
output: string;
|
|
42
|
+
cache?: string;
|
|
43
|
+
};
|
|
44
|
+
ignore?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Glossary Types
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
export interface Glossary {
|
|
52
|
+
metadata: {
|
|
53
|
+
name: string;
|
|
54
|
+
sourceLang: string;
|
|
55
|
+
targetLangs: string[];
|
|
56
|
+
version: string;
|
|
57
|
+
domain?: string;
|
|
58
|
+
};
|
|
59
|
+
terms: GlossaryTerm[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GlossaryTerm {
|
|
63
|
+
source: string;
|
|
64
|
+
targets: Record<string, string>;
|
|
65
|
+
context?: string;
|
|
66
|
+
caseSensitive?: boolean;
|
|
67
|
+
doNotTranslate?: boolean;
|
|
68
|
+
doNotTranslateFor?: string[];
|
|
69
|
+
partOfSpeech?: 'noun' | 'verb' | 'adjective' | 'other';
|
|
70
|
+
notes?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ResolvedGlossary {
|
|
74
|
+
metadata: {
|
|
75
|
+
name: string;
|
|
76
|
+
sourceLang: string;
|
|
77
|
+
targetLang: string;
|
|
78
|
+
version: string;
|
|
79
|
+
domain?: string;
|
|
80
|
+
};
|
|
81
|
+
terms: ResolvedGlossaryTerm[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ResolvedGlossaryTerm {
|
|
85
|
+
source: string;
|
|
86
|
+
target: string;
|
|
87
|
+
context?: string;
|
|
88
|
+
caseSensitive: boolean;
|
|
89
|
+
doNotTranslate: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Translation Types
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
export type DocumentFormat = 'markdown' | 'html' | 'text';
|
|
97
|
+
|
|
98
|
+
export interface TranslationRequest {
|
|
99
|
+
content: string;
|
|
100
|
+
sourceLang: string;
|
|
101
|
+
targetLang: string;
|
|
102
|
+
format: DocumentFormat;
|
|
103
|
+
glossary?: ResolvedGlossary;
|
|
104
|
+
context?: {
|
|
105
|
+
documentPurpose?: string;
|
|
106
|
+
/** Per-language style instruction (e.g., "경어체", "です・ます調") */
|
|
107
|
+
styleInstruction?: string;
|
|
108
|
+
previousChunks?: string[];
|
|
109
|
+
documentSummary?: string;
|
|
110
|
+
};
|
|
111
|
+
options?: {
|
|
112
|
+
qualityThreshold?: number;
|
|
113
|
+
maxIterations?: number;
|
|
114
|
+
preserveFormatting?: boolean;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface TranslationResult {
|
|
119
|
+
content: string;
|
|
120
|
+
metadata: {
|
|
121
|
+
qualityScore: number;
|
|
122
|
+
qualityThreshold: number;
|
|
123
|
+
thresholdMet: boolean;
|
|
124
|
+
iterations: number;
|
|
125
|
+
tokensUsed: {
|
|
126
|
+
input: number;
|
|
127
|
+
output: number;
|
|
128
|
+
/** Tokens read from cache (90% cost reduction) */
|
|
129
|
+
cacheRead?: number;
|
|
130
|
+
/** Tokens written to cache (25% cost increase for first write) */
|
|
131
|
+
cacheWrite?: number;
|
|
132
|
+
};
|
|
133
|
+
duration: number;
|
|
134
|
+
provider: string;
|
|
135
|
+
model: string;
|
|
136
|
+
};
|
|
137
|
+
glossaryCompliance?: {
|
|
138
|
+
applied: string[];
|
|
139
|
+
missed: string[];
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ChunkResult {
|
|
144
|
+
original: string;
|
|
145
|
+
translated: string;
|
|
146
|
+
startOffset: number;
|
|
147
|
+
endOffset: number;
|
|
148
|
+
qualityScore: number;
|
|
149
|
+
iterations?: number;
|
|
150
|
+
tokensUsed?: {
|
|
151
|
+
input: number;
|
|
152
|
+
output: number;
|
|
153
|
+
/** Number of cache hits for this chunk */
|
|
154
|
+
cacheRead?: number;
|
|
155
|
+
};
|
|
156
|
+
/** Whether this chunk was retrieved from cache */
|
|
157
|
+
cached?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface DocumentResult {
|
|
161
|
+
content: string;
|
|
162
|
+
chunks: ChunkResult[];
|
|
163
|
+
metadata: {
|
|
164
|
+
totalTokensUsed: number;
|
|
165
|
+
totalDuration: number;
|
|
166
|
+
averageQuality: number;
|
|
167
|
+
provider: string;
|
|
168
|
+
model: string;
|
|
169
|
+
totalIterations: number;
|
|
170
|
+
tokensUsed: {
|
|
171
|
+
input: number;
|
|
172
|
+
output: number;
|
|
173
|
+
/** Tokens read from cache (90% cost reduction) */
|
|
174
|
+
cacheRead?: number;
|
|
175
|
+
/** Tokens written to cache (25% cost increase for first write) */
|
|
176
|
+
cacheWrite?: number;
|
|
177
|
+
};
|
|
178
|
+
/** Cache statistics */
|
|
179
|
+
cache?: {
|
|
180
|
+
hits: number;
|
|
181
|
+
misses: number;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
glossaryCompliance?: {
|
|
185
|
+
applied: string[];
|
|
186
|
+
missed: string[];
|
|
187
|
+
compliant: boolean;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Chunking Types
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
export interface Chunk {
|
|
196
|
+
id: string;
|
|
197
|
+
content: string;
|
|
198
|
+
type: 'translatable' | 'preserve';
|
|
199
|
+
startOffset: number;
|
|
200
|
+
endOffset: number;
|
|
201
|
+
metadata?: {
|
|
202
|
+
headerHierarchy?: string[];
|
|
203
|
+
previousContext?: string;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface ChunkingConfig {
|
|
208
|
+
maxTokens: number;
|
|
209
|
+
overlapTokens: number;
|
|
210
|
+
separators: string[];
|
|
211
|
+
preservePatterns: RegExp[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Quality Evaluation Types
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Legacy simple quality evaluation (for fast mode or fallback)
|
|
220
|
+
*/
|
|
221
|
+
export interface SimpleQualityEvaluation {
|
|
222
|
+
score: number;
|
|
223
|
+
breakdown: {
|
|
224
|
+
accuracy: number;
|
|
225
|
+
fluency: number;
|
|
226
|
+
glossary: number;
|
|
227
|
+
format: number;
|
|
228
|
+
};
|
|
229
|
+
issues: string[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Re-export MQM types
|
|
233
|
+
export * from './mqm.js';
|
|
234
|
+
|
|
235
|
+
// Re-export analysis types
|
|
236
|
+
export * from './analysis.js';
|
|
237
|
+
|
|
238
|
+
// Re-export mode types
|
|
239
|
+
export * from './modes.js';
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Combined quality evaluation type (supports both MQM and simple)
|
|
243
|
+
*/
|
|
244
|
+
export type QualityEvaluation = SimpleQualityEvaluation;
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Cache Types
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
export interface CacheEntry {
|
|
251
|
+
sourceHash: string;
|
|
252
|
+
sourceLang: string;
|
|
253
|
+
targetLang: string;
|
|
254
|
+
glossaryHash: string;
|
|
255
|
+
translation: string;
|
|
256
|
+
qualityScore: number;
|
|
257
|
+
createdAt: string;
|
|
258
|
+
provider: string;
|
|
259
|
+
model: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface CacheIndex {
|
|
263
|
+
version: string;
|
|
264
|
+
entries: Record<string, CacheEntry>;
|
|
265
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Mode Configurations
|
|
3
|
+
*
|
|
4
|
+
* Defines preset configurations for different translation quality/speed tradeoffs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Available translation modes
|
|
9
|
+
*/
|
|
10
|
+
export type TranslationMode = 'fast' | 'balanced' | 'quality';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for a translation mode
|
|
14
|
+
*/
|
|
15
|
+
export interface ModeConfig {
|
|
16
|
+
/** Enable pre-translation analysis (MAPS-style) */
|
|
17
|
+
enableAnalysis: boolean;
|
|
18
|
+
|
|
19
|
+
/** Use MQM-based evaluation instead of simple scoring */
|
|
20
|
+
useMQMEvaluation: boolean;
|
|
21
|
+
|
|
22
|
+
/** Maximum refinement iterations */
|
|
23
|
+
maxIterations: number;
|
|
24
|
+
|
|
25
|
+
/** Quality threshold (0 = skip threshold check) */
|
|
26
|
+
qualityThreshold: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mode preset configurations
|
|
31
|
+
*/
|
|
32
|
+
export const MODE_PRESETS: Record<TranslationMode, ModeConfig> = {
|
|
33
|
+
/**
|
|
34
|
+
* Fast mode: Single pass, no evaluation
|
|
35
|
+
* Best for: Quick drafts, large batches, local models
|
|
36
|
+
* Speed: ~1x (fastest)
|
|
37
|
+
*/
|
|
38
|
+
fast: {
|
|
39
|
+
enableAnalysis: false,
|
|
40
|
+
useMQMEvaluation: false,
|
|
41
|
+
maxIterations: 1,
|
|
42
|
+
qualityThreshold: 0, // Skip threshold check
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Balanced mode: TEaR with MQM evaluation
|
|
47
|
+
* Best for: General use, good quality with reasonable speed
|
|
48
|
+
* Speed: ~2-3x
|
|
49
|
+
*/
|
|
50
|
+
balanced: {
|
|
51
|
+
enableAnalysis: false,
|
|
52
|
+
useMQMEvaluation: true,
|
|
53
|
+
maxIterations: 2,
|
|
54
|
+
qualityThreshold: 75,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Quality mode: Full MAPS + TEaR pipeline
|
|
59
|
+
* Best for: Production content, critical documents
|
|
60
|
+
* Speed: ~4-5x
|
|
61
|
+
*/
|
|
62
|
+
quality: {
|
|
63
|
+
enableAnalysis: true,
|
|
64
|
+
useMQMEvaluation: true,
|
|
65
|
+
maxIterations: 4,
|
|
66
|
+
qualityThreshold: 85,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get mode configuration with optional overrides
|
|
72
|
+
*/
|
|
73
|
+
export function getModeConfig(
|
|
74
|
+
mode: TranslationMode,
|
|
75
|
+
overrides?: Partial<ModeConfig>
|
|
76
|
+
): ModeConfig {
|
|
77
|
+
const preset = MODE_PRESETS[mode];
|
|
78
|
+
|
|
79
|
+
if (!overrides) {
|
|
80
|
+
return preset;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
enableAnalysis: overrides.enableAnalysis ?? preset.enableAnalysis,
|
|
85
|
+
useMQMEvaluation: overrides.useMQMEvaluation ?? preset.useMQMEvaluation,
|
|
86
|
+
maxIterations: overrides.maxIterations ?? preset.maxIterations,
|
|
87
|
+
qualityThreshold: overrides.qualityThreshold ?? preset.qualityThreshold,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Determine effective mode from CLI options
|
|
93
|
+
*/
|
|
94
|
+
export function resolveMode(options: {
|
|
95
|
+
mode?: TranslationMode;
|
|
96
|
+
quality?: number;
|
|
97
|
+
maxIterations?: number;
|
|
98
|
+
noAnalysis?: boolean;
|
|
99
|
+
noMqm?: boolean;
|
|
100
|
+
}): ModeConfig {
|
|
101
|
+
const baseMode = options.mode ?? 'balanced';
|
|
102
|
+
const preset = MODE_PRESETS[baseMode];
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
enableAnalysis:
|
|
106
|
+
options.noAnalysis !== undefined
|
|
107
|
+
? !options.noAnalysis
|
|
108
|
+
: preset.enableAnalysis,
|
|
109
|
+
useMQMEvaluation:
|
|
110
|
+
options.noMqm !== undefined ? !options.noMqm : preset.useMQMEvaluation,
|
|
111
|
+
maxIterations: options.maxIterations ?? preset.maxIterations,
|
|
112
|
+
qualityThreshold: options.quality ?? preset.qualityThreshold,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if mode allows skipping evaluation
|
|
118
|
+
*/
|
|
119
|
+
export function shouldSkipEvaluation(config: ModeConfig): boolean {
|
|
120
|
+
return config.maxIterations <= 1 && config.qualityThreshold <= 0;
|
|
121
|
+
}
|
package/src/types/mqm.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MQM (Multidimensional Quality Metrics) Types
|
|
3
|
+
* Based on https://themqm.org/ framework used in WMT evaluations
|
|
4
|
+
*
|
|
5
|
+
* Reference: TEaR (Translate, Estimate, Refine) - NAACL 2025
|
|
6
|
+
* https://arxiv.org/abs/2402.16379
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MQM Error Categories
|
|
11
|
+
*/
|
|
12
|
+
export type MQMErrorType =
|
|
13
|
+
// Accuracy errors - meaning/content issues
|
|
14
|
+
| 'accuracy/mistranslation' // Incorrect meaning
|
|
15
|
+
| 'accuracy/omission' // Missing content from source
|
|
16
|
+
| 'accuracy/addition' // Extra content not in source
|
|
17
|
+
| 'accuracy/untranslated' // Source text left unchanged
|
|
18
|
+
// Fluency errors - target language issues
|
|
19
|
+
| 'fluency/grammar' // Grammatical errors
|
|
20
|
+
| 'fluency/spelling' // Spelling/typos
|
|
21
|
+
| 'fluency/register' // Inappropriate formality level
|
|
22
|
+
| 'fluency/inconsistency' // Inconsistent terminology
|
|
23
|
+
// Style errors - quality/naturalness issues
|
|
24
|
+
| 'style/awkward' // Unnatural phrasing
|
|
25
|
+
| 'style/unidiomatic'; // Non-native expressions
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* MQM Severity Levels
|
|
29
|
+
*/
|
|
30
|
+
export type MQMSeverity = 'minor' | 'major' | 'critical';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MQM Severity Weights for score calculation
|
|
34
|
+
*/
|
|
35
|
+
export const MQM_SEVERITY_WEIGHTS: Record<MQMSeverity, number> = {
|
|
36
|
+
minor: 1, // Noticeable but doesn't affect understanding
|
|
37
|
+
major: 5, // Affects understanding or usability
|
|
38
|
+
critical: 25, // Completely wrong or unusable
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Individual MQM error annotation
|
|
43
|
+
*/
|
|
44
|
+
export interface MQMError {
|
|
45
|
+
/** Error category */
|
|
46
|
+
type: MQMErrorType;
|
|
47
|
+
|
|
48
|
+
/** Error severity */
|
|
49
|
+
severity: MQMSeverity;
|
|
50
|
+
|
|
51
|
+
/** The affected text in translation */
|
|
52
|
+
span: string;
|
|
53
|
+
|
|
54
|
+
/** Suggested correction */
|
|
55
|
+
suggestion: string;
|
|
56
|
+
|
|
57
|
+
/** Brief reason for the error */
|
|
58
|
+
explanation?: string;
|
|
59
|
+
|
|
60
|
+
/** Corresponding source text (if applicable) */
|
|
61
|
+
sourceSpan?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* MQM evaluation result
|
|
66
|
+
*/
|
|
67
|
+
export interface MQMEvaluation {
|
|
68
|
+
/** List of identified errors */
|
|
69
|
+
errors: MQMError[];
|
|
70
|
+
|
|
71
|
+
/** Quality score: 100 - sum(error weights), min 0 */
|
|
72
|
+
score: number;
|
|
73
|
+
|
|
74
|
+
/** Brief overall assessment */
|
|
75
|
+
summary: string;
|
|
76
|
+
|
|
77
|
+
/** Error count breakdown by category */
|
|
78
|
+
breakdown: {
|
|
79
|
+
accuracy: number;
|
|
80
|
+
fluency: number;
|
|
81
|
+
style: number;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Calculate MQM score from errors
|
|
87
|
+
* Score = max(0, 100 - Σ(error_weight))
|
|
88
|
+
*/
|
|
89
|
+
export function calculateMQMScore(errors: MQMError[]): number {
|
|
90
|
+
const totalPenalty = errors.reduce(
|
|
91
|
+
(sum, err) => sum + MQM_SEVERITY_WEIGHTS[err.severity],
|
|
92
|
+
0
|
|
93
|
+
);
|
|
94
|
+
return Math.max(0, 100 - totalPenalty);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Calculate error breakdown by category
|
|
99
|
+
*/
|
|
100
|
+
export function calculateMQMBreakdown(
|
|
101
|
+
errors: MQMError[]
|
|
102
|
+
): MQMEvaluation['breakdown'] {
|
|
103
|
+
return {
|
|
104
|
+
accuracy: errors.filter((e) => e.type.startsWith('accuracy/')).length,
|
|
105
|
+
fluency: errors.filter((e) => e.type.startsWith('fluency/')).length,
|
|
106
|
+
style: errors.filter((e) => e.type.startsWith('style/')).length,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse MQM evaluation JSON response from LLM
|
|
112
|
+
*/
|
|
113
|
+
export function parseMQMResponse(response: string): MQMEvaluation | null {
|
|
114
|
+
try {
|
|
115
|
+
// Extract JSON from response
|
|
116
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
117
|
+
if (!jsonMatch) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parsed = JSON.parse(jsonMatch[0]) as {
|
|
122
|
+
errors?: MQMError[];
|
|
123
|
+
score?: number;
|
|
124
|
+
summary?: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const errors = parsed.errors ?? [];
|
|
128
|
+
const score = parsed.score ?? calculateMQMScore(errors);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
errors,
|
|
132
|
+
score,
|
|
133
|
+
summary: parsed.summary ?? '',
|
|
134
|
+
breakdown: calculateMQMBreakdown(errors),
|
|
135
|
+
};
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Format MQM errors for refinement prompt
|
|
143
|
+
*/
|
|
144
|
+
export function formatMQMErrorsForPrompt(errors: MQMError[]): string {
|
|
145
|
+
if (errors.length === 0) {
|
|
146
|
+
return 'No errors identified.';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return errors
|
|
150
|
+
.map((err, i) => {
|
|
151
|
+
const severity = err.severity.toUpperCase();
|
|
152
|
+
return `${i + 1}. [${severity}] ${err.type}
|
|
153
|
+
Text: "${err.span}"
|
|
154
|
+
Fix: "${err.suggestion}"${err.explanation ? `\n Reason: ${err.explanation}` : ''}`;
|
|
155
|
+
})
|
|
156
|
+
.join('\n\n');
|
|
157
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Log Levels
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
8
|
+
|
|
9
|
+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
10
|
+
debug: 0,
|
|
11
|
+
info: 1,
|
|
12
|
+
warn: 2,
|
|
13
|
+
error: 3,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Logger Configuration
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
interface LoggerConfig {
|
|
21
|
+
level: LogLevel;
|
|
22
|
+
quiet: boolean;
|
|
23
|
+
json: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let config: LoggerConfig = {
|
|
27
|
+
level: 'info',
|
|
28
|
+
quiet: false,
|
|
29
|
+
json: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function configureLogger(options: Partial<LoggerConfig>): void {
|
|
33
|
+
config = { ...config, ...options };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Logger Implementation
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
function shouldLog(level: LogLevel): boolean {
|
|
41
|
+
if (config.quiet && level !== 'error') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[config.level];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatMessage(
|
|
48
|
+
level: LogLevel,
|
|
49
|
+
message: string,
|
|
50
|
+
data?: Record<string, unknown>
|
|
51
|
+
): string {
|
|
52
|
+
if (config.json) {
|
|
53
|
+
return JSON.stringify({
|
|
54
|
+
level,
|
|
55
|
+
message,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
...data,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const timestamp = new Date().toISOString().slice(11, 19);
|
|
62
|
+
const prefix = `[${timestamp}]`;
|
|
63
|
+
|
|
64
|
+
switch (level) {
|
|
65
|
+
case 'debug':
|
|
66
|
+
return chalk.gray(`${prefix} ${message}`);
|
|
67
|
+
case 'info':
|
|
68
|
+
return `${prefix} ${message}`;
|
|
69
|
+
case 'warn':
|
|
70
|
+
return chalk.yellow(`${prefix} ⚠ ${message}`);
|
|
71
|
+
case 'error':
|
|
72
|
+
return chalk.red(`${prefix} ✗ ${message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const logger = {
|
|
77
|
+
debug(message: string, data?: Record<string, unknown>): void {
|
|
78
|
+
if (shouldLog('debug')) {
|
|
79
|
+
console.log(formatMessage('debug', message, data));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
info(message: string, data?: Record<string, unknown>): void {
|
|
84
|
+
if (shouldLog('info')) {
|
|
85
|
+
console.log(formatMessage('info', message, data));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
warn(message: string, data?: Record<string, unknown>): void {
|
|
90
|
+
if (shouldLog('warn')) {
|
|
91
|
+
console.warn(formatMessage('warn', message, data));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
error(message: string, data?: Record<string, unknown>): void {
|
|
96
|
+
if (shouldLog('error')) {
|
|
97
|
+
console.error(formatMessage('error', message, data));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
success(message: string): void {
|
|
102
|
+
if (!config.quiet) {
|
|
103
|
+
console.log(chalk.green(`✓ ${message}`));
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
progress(current: number, total: number, message: string): void {
|
|
108
|
+
if (!config.quiet && !config.json) {
|
|
109
|
+
const percent = Math.round((current / total) * 100);
|
|
110
|
+
const bar = '█'.repeat(Math.round(percent / 5)) + '░'.repeat(20 - Math.round(percent / 5));
|
|
111
|
+
process.stdout.write(`\r[${bar}] ${percent}% ${message}`);
|
|
112
|
+
if (current === total) {
|
|
113
|
+
console.log();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Timing Utilities
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
export function createTimer(): {
|
|
124
|
+
elapsed: () => number;
|
|
125
|
+
format: () => string;
|
|
126
|
+
} {
|
|
127
|
+
const start = performance.now();
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
elapsed(): number {
|
|
131
|
+
return performance.now() - start;
|
|
132
|
+
},
|
|
133
|
+
format(): string {
|
|
134
|
+
const ms = this.elapsed();
|
|
135
|
+
if (ms < 1000) {
|
|
136
|
+
return `${ms.toFixed(0)}ms`;
|
|
137
|
+
}
|
|
138
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|