hybridtm 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/hybridtm.js +1 -1
  2. package/dist/hybridtm.js.map +1 -1
  3. package/dist/match.d.ts +7 -2
  4. package/dist/match.js +14 -2
  5. package/dist/match.js.map +1 -1
  6. package/package.json +13 -7
  7. package/dist/batchimporter.d.ts +0 -22
  8. package/dist/batchimporter.js +0 -203
  9. package/dist/constants.d.ts +0 -15
  10. package/dist/constants.js +0 -16
  11. package/dist/constants.js.map +0 -1
  12. package/dist/debug-progress.d.ts +0 -12
  13. package/dist/debug-progress.js +0 -49
  14. package/dist/debug-progress.js.map +0 -1
  15. package/dist/ngramIndex.d.ts +0 -31
  16. package/dist/ngramIndex.js +0 -132
  17. package/dist/ngramIndex.js.map +0 -1
  18. package/dist/ngrams.d.ts +0 -21
  19. package/dist/ngrams.js +0 -73
  20. package/dist/ngrams.js.map +0 -1
  21. package/dist/package.json +0 -35
  22. package/dist/progressProvider.d.ts +0 -17
  23. package/dist/progressProvider.js +0 -14
  24. package/dist/progressProvider.js.map +0 -1
  25. package/dist/simplexlifftest.d.ts +0 -1
  26. package/dist/simplexlifftest.js +0 -23
  27. package/dist/simplexlifftest.js.map +0 -1
  28. package/dist/test-import.d.ts +0 -12
  29. package/dist/test-import.js +0 -101
  30. package/dist/test-import.js.map +0 -1
  31. package/dist/test-progress-fix.d.ts +0 -1
  32. package/dist/test-progress-fix.js +0 -32
  33. package/dist/test-progress-fix.js.map +0 -1
  34. package/dist/test-progress.d.ts +0 -12
  35. package/dist/test-progress.js +0 -32
  36. package/dist/test-progress.js.map +0 -1
  37. package/dist/tests/vectorSimilarityComparison.d.ts +0 -12
  38. package/dist/tests/vectorSimilarityComparison.js +0 -153
  39. package/dist/tests/vectorSimilarityComparison.js.map +0 -1
  40. package/dist/tmxreader.d.ts +0 -25
  41. package/dist/tmxreader.js +0 -51
  42. package/dist/tmxtest.d.ts +0 -16
  43. package/dist/tmxtest.js +0 -316
  44. package/dist/tmxtest.js.map +0 -1
  45. package/dist/ts/batchImporter.js.map +0 -1
  46. package/dist/ts/hybridtm.d.ts +0 -67
  47. package/dist/ts/hybridtm.js +0 -932
  48. package/dist/ts/hybridtm.js.map +0 -1
  49. package/dist/ts/hybridtmFactory.d.ts +0 -43
  50. package/dist/ts/hybridtmFactory.js +0 -165
  51. package/dist/ts/hybridtmFactory.js.map +0 -1
  52. package/dist/ts/importOptions.d.ts +0 -21
  53. package/dist/ts/importOptions.js +0 -24
  54. package/dist/ts/importOptions.js.map +0 -1
  55. package/dist/ts/index.d.ts +0 -25
  56. package/dist/ts/index.js +0 -22
  57. package/dist/ts/index.js.map +0 -1
  58. package/dist/ts/langEntry.d.ts +0 -64
  59. package/dist/ts/langEntry.js +0 -13
  60. package/dist/ts/langEntry.js.map +0 -1
  61. package/dist/ts/match.d.ts +0 -22
  62. package/dist/ts/match.js +0 -38
  63. package/dist/ts/match.js.map +0 -1
  64. package/dist/ts/matchQuality.d.ts +0 -16
  65. package/dist/ts/matchQuality.js +0 -83
  66. package/dist/ts/matchQuality.js.map +0 -1
  67. package/dist/ts/pendingEntry.d.ts +0 -24
  68. package/dist/ts/pendingEntry.js +0 -13
  69. package/dist/ts/pendingEntry.js.map +0 -1
  70. package/dist/ts/searchFilters.d.ts +0 -24
  71. package/dist/ts/searchFilters.js +0 -13
  72. package/dist/ts/searchFilters.js.map +0 -1
  73. package/dist/ts/tmxHandler.d.ts +0 -61
  74. package/dist/ts/tmxHandler.js +0 -347
  75. package/dist/ts/tmxHandler.js.map +0 -1
  76. package/dist/ts/tmxReader.js.map +0 -1
  77. package/dist/ts/tmxtest.d.ts +0 -16
  78. package/dist/ts/tmxtest.js +0 -316
  79. package/dist/ts/tmxtest.js.map +0 -1
  80. package/dist/ts/utils.d.ts +0 -17
  81. package/dist/ts/utils.js +0 -49
  82. package/dist/ts/utils.js.map +0 -1
  83. package/dist/ts/xliffHandler.d.ts +0 -67
  84. package/dist/ts/xliffHandler.js +0 -522
  85. package/dist/ts/xliffHandler.js.map +0 -1
  86. package/dist/ts/xliffReader.js.map +0 -1
  87. package/dist/ts/xlifftest.d.ts +0 -54
  88. package/dist/ts/xlifftest.js +0 -245
  89. package/dist/ts/xlifftest.js.map +0 -1
  90. package/dist/vectorSimilarityTester.d.ts +0 -56
  91. package/dist/vectorSimilarityTester.js +0 -164
  92. package/dist/vectorSimilarityTester.js.map +0 -1
  93. package/dist/xliffreader.d.ts +0 -25
  94. package/dist/xliffreader.js +0 -50
  95. package/dist/xlifftest.d.ts +0 -54
  96. package/dist/xlifftest.js +0 -245
  97. package/dist/xlifftest.js.map +0 -1
  98. /package/dist/{ts/batchImporter.d.ts → batchImporter.d.ts} +0 -0
  99. /package/dist/{ts/batchImporter.js → batchImporter.js} +0 -0
  100. /package/dist/{batchimporter.js.map → batchImporter.js.map} +0 -0
  101. /package/dist/{tmxhandler.d.ts → tmxHandler.d.ts} +0 -0
  102. /package/dist/{tmxhandler.js → tmxHandler.js} +0 -0
  103. /package/dist/{tmxhandler.js.map → tmxHandler.js.map} +0 -0
  104. /package/dist/{ts/tmxReader.d.ts → tmxReader.d.ts} +0 -0
  105. /package/dist/{ts/tmxReader.js → tmxReader.js} +0 -0
  106. /package/dist/{tmxreader.js.map → tmxReader.js.map} +0 -0
  107. /package/dist/{xliffhandler.d.ts → xliffHandler.d.ts} +0 -0
  108. /package/dist/{xliffhandler.js → xliffHandler.js} +0 -0
  109. /package/dist/{xliffhandler.js.map → xliffHandler.js.map} +0 -0
  110. /package/dist/{ts/xliffReader.d.ts → xliffReader.d.ts} +0 -0
  111. /package/dist/{ts/xliffReader.js → xliffReader.js} +0 -0
  112. /package/dist/{xliffreader.js.map → xliffReader.js.map} +0 -0
@@ -1,932 +0,0 @@
1
- /*******************************************************************************
2
- * Copyright (c) 2025-2026 Maxprograms.
3
- *
4
- * This program and the accompanying materials
5
- * are made available under the terms of the Eclipse License 1.0
6
- * which accompanies this distribution, and is available at
7
- * https://www.eclipse.org/org/documents/epl-v10.html
8
- *
9
- * Contributors:
10
- * Maxprograms - initial API and implementation
11
- *******************************************************************************/
12
- import { connect } from '@lancedb/lancedb';
13
- import { pipeline } from '@xenova/transformers';
14
- import { Field, FixedSizeList, Float32, Int32, Schema, Utf8 } from 'apache-arrow';
15
- import { unlinkSync } from 'node:fs';
16
- import { tmpdir } from "node:os";
17
- import { join } from 'node:path';
18
- import { TMReader } from 'sdltm';
19
- import { BatchImporter } from './batchImporter.js';
20
- import { resolveImportOptions } from './importOptions.js';
21
- import { Match } from './match.js';
22
- import { MatchQuality } from './matchQuality.js';
23
- import { TMXReader } from './tmxReader.js';
24
- import { Utils } from './utils.js';
25
- import { XLIFFReader } from './xliffReader.js';
26
- export class HybridTM {
27
- // OPTIMIZED MODELS
28
- static SPEED_MODEL = 'Xenova/bge-small-en-v1.5'; // 384-dim, optimized for real-time
29
- static QUALITY_MODEL = 'Xenova/LaBSE'; // 768-dim, optimized for accuracy
30
- static RESOURCE_MODEL = 'Xenova/multilingual-e5-small'; // 384-dim, optimized for modest hardware
31
- name;
32
- db = null;
33
- table = null;
34
- dbPath = '';
35
- embedder = null;
36
- modelName = '';
37
- initialized = false;
38
- initializationPromise = null;
39
- constructor(name, filePath, modelName = HybridTM.QUALITY_MODEL) {
40
- this.name = name;
41
- this.dbPath = filePath;
42
- this.modelName = modelName;
43
- }
44
- // ============================
45
- // INITIALIZATION METHODS
46
- // ============================
47
- async detectModelDimensions() {
48
- try {
49
- // Ensure embedder is initialized
50
- if (!this.embedder) {
51
- await this.initializeEmbedder();
52
- }
53
- if (!this.embedder) {
54
- throw new Error('Failed to initialize embedder for dimension detection');
55
- }
56
- // Generate a small test embedding
57
- const testResult = await this.embedder('test', {
58
- pooling: 'mean',
59
- normalize: true
60
- });
61
- return Array.from(testResult.data).length;
62
- }
63
- catch (err) {
64
- console.error('Error detecting model dimensions:', err);
65
- throw new Error('Unable to detect model dimensions for ' + this.modelName + ': ' + (err instanceof Error ? err.message : String(err)));
66
- }
67
- }
68
- async initialize() {
69
- try {
70
- // Initialize database and embedder in parallel
71
- await Promise.all([
72
- this.initializeDatabase(),
73
- this.initializeEmbedder()
74
- ]);
75
- }
76
- catch (err) {
77
- console.error('Failed to initialize HybridTM:', err);
78
- throw new Error('HybridTM initialization failed: ' + (err instanceof Error ? err.message : String(err)));
79
- }
80
- }
81
- async initializeDatabase() {
82
- try {
83
- // Connect to LanceDB
84
- this.db = await connect(this.dbPath);
85
- // Check if table exists, if not create it
86
- const tableNames = await this.db.tableNames();
87
- if (!tableNames.includes('langEntry')) {
88
- // Detect model dimensions first
89
- const dimensions = await this.detectModelDimensions();
90
- // Create Arrow schema for the langEntry table
91
- const schema = new Schema([
92
- Field.new('id', new Utf8(), false),
93
- Field.new('language', new Utf8(), false),
94
- Field.new('pureText', new Utf8(), false),
95
- Field.new('element', new Utf8(), false),
96
- Field.new('fileId', new Utf8(), false),
97
- Field.new('original', new Utf8(), false),
98
- Field.new('unitId', new Utf8(), false),
99
- Field.new('segmentIndex', new Int32(), false),
100
- Field.new('segmentCount', new Int32(), false),
101
- Field.new('metadataState', new Utf8(), true),
102
- Field.new('metadataSubState', new Utf8(), true),
103
- Field.new('metadataQuality', new Int32(), true),
104
- Field.new('metadataCreationDate', new Utf8(), true),
105
- Field.new('metadataCreationId', new Utf8(), true),
106
- Field.new('metadataChangeDate', new Utf8(), true),
107
- Field.new('metadataChangeId', new Utf8(), true),
108
- Field.new('metadataCreationTool', new Utf8(), true),
109
- Field.new('metadataCreationToolVersion', new Utf8(), true),
110
- Field.new('metadataContext', new Utf8(), true),
111
- Field.new('metadataNotes', new Utf8(), true),
112
- Field.new('metadataUsageCount', new Int32(), true),
113
- Field.new('metadataLastUsageDate', new Utf8(), true),
114
- Field.new('metadataProperties', new Utf8(), true),
115
- Field.new('metadataSegment', new Utf8(), true),
116
- Field.new('vector', new FixedSizeList(dimensions, Field.new('item', new Float32(), false)), false),
117
- ]);
118
- // Create table with the schema
119
- this.table = await this.db.createEmptyTable('langEntry', schema);
120
- }
121
- else {
122
- this.table = await this.db.openTable('langEntry');
123
- }
124
- }
125
- catch (err) {
126
- console.error('Error initializing LanceDB:', err);
127
- throw err;
128
- }
129
- }
130
- async initializeEmbedder() {
131
- try {
132
- this.embedder = await pipeline('feature-extraction', this.modelName);
133
- }
134
- catch (err) {
135
- console.error('Error initializing embedder:', err);
136
- throw err;
137
- }
138
- }
139
- async generateEmbedding(text) {
140
- try {
141
- // Ensure embedder is initialized
142
- if (!this.embedder) {
143
- await this.initializeEmbedder();
144
- }
145
- if (!this.embedder) {
146
- throw new Error('Failed to initialize embedder');
147
- }
148
- // Generate embeddings using the transformer model
149
- const result = await this.embedder(text, {
150
- pooling: 'mean',
151
- normalize: true
152
- });
153
- // Convert tensor to array
154
- const embedding = Array.from(result.data);
155
- return embedding;
156
- }
157
- catch (err) {
158
- console.error('Error generating embedding:', err);
159
- throw err;
160
- }
161
- }
162
- async ensureInitialized() {
163
- if (this.initialized) {
164
- return;
165
- }
166
- if (this.initializationPromise) {
167
- await this.initializationPromise;
168
- return;
169
- }
170
- this.initializationPromise = this.initialize();
171
- await this.initializationPromise;
172
- this.initialized = true;
173
- }
174
- async ensureTable() {
175
- await this.ensureInitialized();
176
- if (!this.table) {
177
- throw new Error('Database table is not available');
178
- }
179
- return this.table;
180
- }
181
- hydrateEntries(rows) {
182
- if (!Array.isArray(rows)) {
183
- return [];
184
- }
185
- return rows.map((row) => this.hydrateEntry((row ?? {})));
186
- }
187
- hydrateEntry(row) {
188
- const metadata = this.unflattenMetadata(row);
189
- const vectorData = row.vector;
190
- const vector = Array.isArray(vectorData)
191
- ? vectorData
192
- : ArrayBuffer.isView(vectorData)
193
- ? Array.from(vectorData)
194
- : typeof vectorData === 'object' && vectorData !== null && typeof vectorData.toArray === 'function'
195
- ? Array.from(vectorData.toArray())
196
- : [];
197
- const hydrated = { ...row };
198
- hydrated.id = typeof row.id === 'string' ? row.id : '';
199
- hydrated.language = typeof row.language === 'string' ? row.language : '';
200
- hydrated.pureText = typeof row.pureText === 'string' ? row.pureText : '';
201
- hydrated.element = typeof row.element === 'string' ? row.element : '';
202
- hydrated.fileId = typeof row.fileId === 'string' ? row.fileId : '';
203
- hydrated.original = typeof row.original === 'string' ? row.original : '';
204
- hydrated.unitId = typeof row.unitId === 'string' ? row.unitId : '';
205
- hydrated.vector = vector;
206
- hydrated.segmentIndex = this.parseNumber(row.segmentIndex, 0);
207
- hydrated.segmentCount = this.parseNumber(row.segmentCount, 1);
208
- hydrated.metadata = metadata;
209
- return hydrated;
210
- }
211
- flattenEntry(entry) {
212
- const metadataFields = this.flattenMetadata(entry.metadata);
213
- return {
214
- id: entry.id,
215
- language: entry.language,
216
- pureText: entry.pureText,
217
- element: entry.element,
218
- fileId: entry.fileId,
219
- original: entry.original,
220
- unitId: entry.unitId,
221
- segmentIndex: entry.segmentIndex,
222
- segmentCount: entry.segmentCount,
223
- vector: entry.vector,
224
- ...metadataFields,
225
- };
226
- }
227
- mapToSearchResult(entry) {
228
- if (!entry.metadata) {
229
- entry.metadata = {};
230
- }
231
- return {
232
- id: entry.id,
233
- language: entry.language,
234
- pureText: entry.pureText,
235
- element: entry.element,
236
- fileId: entry.fileId,
237
- original: entry.original,
238
- unitId: entry.unitId,
239
- segmentIndex: entry.segmentIndex,
240
- segmentCount: entry.segmentCount,
241
- metadata: entry.metadata,
242
- };
243
- }
244
- buildEntryId(fileId, unitId, segmentIndex, lang) {
245
- return fileId + ':' + unitId + ':' + segmentIndex + ':' + lang;
246
- }
247
- flattenMetadata(metadata) {
248
- const safeMetadata = metadata ? metadata : {};
249
- return {
250
- metadataState: safeMetadata.state ?? null,
251
- metadataSubState: safeMetadata.subState ?? null,
252
- metadataQuality: typeof safeMetadata.quality === 'number' ? safeMetadata.quality : null,
253
- metadataCreationDate: safeMetadata.creationDate ?? null,
254
- metadataCreationId: safeMetadata.creationId ?? null,
255
- metadataChangeDate: safeMetadata.changeDate ?? null,
256
- metadataChangeId: safeMetadata.changeId ?? null,
257
- metadataCreationTool: safeMetadata.creationTool ?? null,
258
- metadataCreationToolVersion: safeMetadata.creationToolVersion ?? null,
259
- metadataContext: safeMetadata.context ?? null,
260
- metadataNotes: safeMetadata.notes ? JSON.stringify(safeMetadata.notes) : null,
261
- metadataUsageCount: typeof safeMetadata.usageCount === 'number' ? safeMetadata.usageCount : null,
262
- metadataLastUsageDate: safeMetadata.lastUsageDate ?? null,
263
- metadataProperties: safeMetadata.properties ? JSON.stringify(safeMetadata.properties) : null,
264
- metadataSegment: safeMetadata.segment ? JSON.stringify(safeMetadata.segment) : null,
265
- };
266
- }
267
- unflattenMetadata(row) {
268
- const metadata = {};
269
- const str = (value) => {
270
- if (typeof value === 'string' && value.length > 0) {
271
- return value;
272
- }
273
- return undefined;
274
- };
275
- const num = (value) => {
276
- if (typeof value === 'number' && !Number.isNaN(value)) {
277
- return value;
278
- }
279
- if (typeof value === 'string' && value.length > 0) {
280
- const parsed = Number(value);
281
- if (!Number.isNaN(parsed)) {
282
- return parsed;
283
- }
284
- }
285
- return undefined;
286
- };
287
- const stateValue = str(row.metadataState);
288
- const validStatuses = ['initial', 'translated', 'reviewed', 'final'];
289
- if (stateValue && validStatuses.includes(stateValue)) {
290
- metadata.state = stateValue;
291
- }
292
- const subStateValue = str(row.metadataSubState);
293
- if (subStateValue) {
294
- metadata.subState = subStateValue;
295
- }
296
- const qualityValue = num(row.metadataQuality);
297
- if (qualityValue !== undefined) {
298
- metadata.quality = qualityValue;
299
- }
300
- const creationDateValue = str(row.metadataCreationDate);
301
- if (creationDateValue) {
302
- metadata.creationDate = creationDateValue;
303
- }
304
- const creationIdValue = str(row.metadataCreationId);
305
- if (creationIdValue) {
306
- metadata.creationId = creationIdValue;
307
- }
308
- const changeDateValue = str(row.metadataChangeDate);
309
- if (changeDateValue) {
310
- metadata.changeDate = changeDateValue;
311
- }
312
- const changeIdValue = str(row.metadataChangeId);
313
- if (changeIdValue) {
314
- metadata.changeId = changeIdValue;
315
- }
316
- const creationToolValue = str(row.metadataCreationTool);
317
- if (creationToolValue) {
318
- metadata.creationTool = creationToolValue;
319
- }
320
- const creationToolVersionValue = str(row.metadataCreationToolVersion);
321
- if (creationToolVersionValue) {
322
- metadata.creationToolVersion = creationToolVersionValue;
323
- }
324
- const contextValue = str(row.metadataContext);
325
- if (contextValue) {
326
- metadata.context = contextValue;
327
- }
328
- const notesValue = this.parseNotes(row.metadataNotes);
329
- if (notesValue && notesValue.length > 0) {
330
- metadata.notes = notesValue;
331
- }
332
- const usageCountValue = num(row.metadataUsageCount);
333
- if (usageCountValue !== undefined) {
334
- metadata.usageCount = usageCountValue;
335
- }
336
- const lastUsageDateValue = str(row.metadataLastUsageDate);
337
- if (lastUsageDateValue) {
338
- metadata.lastUsageDate = lastUsageDateValue;
339
- }
340
- const properties = this.parseProperties(row.metadataProperties);
341
- if (properties && Object.keys(properties).length > 0) {
342
- metadata.properties = properties;
343
- }
344
- const segment = this.parseSegment(row.metadataSegment);
345
- if (segment) {
346
- metadata.segment = segment;
347
- }
348
- return metadata;
349
- }
350
- parseNotes(value) {
351
- if (typeof value !== 'string' || value.length === 0) {
352
- return undefined;
353
- }
354
- try {
355
- const parsed = JSON.parse(value);
356
- if (Array.isArray(parsed)) {
357
- return parsed.filter((item) => typeof item === 'string');
358
- }
359
- }
360
- catch {
361
- return undefined;
362
- }
363
- return undefined;
364
- }
365
- parseSegment(value) {
366
- if (typeof value !== 'string' || value.length === 0) {
367
- return undefined;
368
- }
369
- try {
370
- const parsed = JSON.parse(value);
371
- if (parsed && typeof parsed === 'object') {
372
- return parsed;
373
- }
374
- }
375
- catch (err) {
376
- console.warn('Failed to parse metadata.segment:', err);
377
- }
378
- return undefined;
379
- }
380
- parseProperties(value) {
381
- if (typeof value !== 'string' || value.length === 0) {
382
- return undefined;
383
- }
384
- try {
385
- const parsed = JSON.parse(value);
386
- if (parsed && typeof parsed === 'object') {
387
- const result = {};
388
- Object.entries(parsed).forEach(([key, val]) => {
389
- if (typeof val === 'string') {
390
- result[key] = val;
391
- }
392
- });
393
- return result;
394
- }
395
- }
396
- catch {
397
- return undefined;
398
- }
399
- return undefined;
400
- }
401
- parseNumber(value, fallback) {
402
- const parsed = typeof value === 'number' && !Number.isNaN(value)
403
- ? value
404
- : typeof value === 'string' && value.length > 0 && !Number.isNaN(Number(value))
405
- ? Number(value)
406
- : undefined;
407
- return typeof parsed === 'number' ? parsed : fallback;
408
- }
409
- async close() {
410
- try {
411
- if (this.db) {
412
- // LanceDB connections are automatically managed
413
- }
414
- }
415
- catch (err) {
416
- console.error('Error closing database:', err);
417
- }
418
- }
419
- // ============================
420
- // SEARCH METHODS
421
- // ============================
422
- async concordanceSearch(textFragment, language, limit = 100, filters) {
423
- // Enhanced concordance search: finds text fragments and returns all language variants for matching units
424
- try {
425
- const table = await this.ensureTable();
426
- const escapeLiteral = (value) => value.replaceAll("'", "''");
427
- const escapedFragment = escapeLiteral(textFragment);
428
- const whereFragment = 'language = ' + '\'' + language + '\'' + ' AND contains(pureText, ' + '\'' + escapedFragment + '\')';
429
- const fragmentEntries = this.hydrateEntries(await table
430
- .query()
431
- .where(whereFragment)
432
- .limit(limit)
433
- .toArray());
434
- const filteredEntries = filters
435
- ? fragmentEntries.filter((entry) => this.metadataMatches(entry.metadata, filters))
436
- : fragmentEntries;
437
- if (filteredEntries.length === 0) {
438
- return [];
439
- }
440
- const segmentDescriptors = new Map();
441
- filteredEntries.forEach((entry) => {
442
- const segmentIndex = typeof entry.segmentIndex === 'number' ? entry.segmentIndex : 0;
443
- const descriptorKey = entry.fileId + '|' + entry.unitId + '|' + segmentIndex;
444
- if (!segmentDescriptors.has(descriptorKey)) {
445
- segmentDescriptors.set(descriptorKey, {
446
- fileId: entry.fileId,
447
- unitId: entry.unitId,
448
- segmentIndex
449
- });
450
- }
451
- });
452
- const result = [];
453
- for (const descriptor of segmentDescriptors.values()) {
454
- const segmentPrefix = descriptor.fileId + ':' + descriptor.unitId + ':' + descriptor.segmentIndex + ':';
455
- const sanitizedPrefix = escapeLiteral(segmentPrefix);
456
- const segmentVariants = this.hydrateEntries(await table
457
- .query()
458
- .where('starts_with(id, ' + '\'' + sanitizedPrefix + '\')')
459
- .toArray());
460
- if (segmentVariants.length === 0) {
461
- continue;
462
- }
463
- const languageMap = new Map();
464
- for (const variant of segmentVariants) {
465
- try {
466
- const xmlElement = Utils.buildXMLElement(variant.element);
467
- languageMap.set(variant.language, xmlElement);
468
- }
469
- catch (parseErr) {
470
- const errorMessage = parseErr instanceof Error ? parseErr.message : String(parseErr);
471
- throw new Error('Failed to create XML element for ' + variant.id + ': ' + errorMessage);
472
- }
473
- }
474
- if (languageMap.size > 0) {
475
- result.push(languageMap);
476
- if (result.length >= limit) {
477
- break;
478
- }
479
- }
480
- }
481
- return result;
482
- }
483
- catch (err) {
484
- console.error('Error performing concordance search:', err);
485
- throw err;
486
- }
487
- }
488
- async semanticSearch(queryText, language, limit = 10, filters) {
489
- try {
490
- const table = await this.ensureTable();
491
- const queryEmbedding = await this.generateEmbedding(queryText);
492
- const results = this.hydrateEntries(await table
493
- .vectorSearch(queryEmbedding)
494
- .where('language = ' + '\'' + language + '\'')
495
- .limit(limit)
496
- .toArray());
497
- const filtered = filters
498
- ? results.filter((entry) => this.metadataMatches(entry.metadata, filters))
499
- : results;
500
- return filtered.map((entry) => this.mapToSearchResult(entry));
501
- }
502
- catch (err) {
503
- console.error('Error performing semantic search:', err);
504
- throw err;
505
- }
506
- }
507
- async semanticTranslationSearch(searchStr, srcLang, tgtLang, similarity, limit = 100, filters) {
508
- try {
509
- const table = await this.ensureTable();
510
- // Generate embedding for the search string
511
- const queryEmbedding = await this.generateEmbedding(searchStr);
512
- // Get all entries for the source language
513
- const sourceEntries = this.hydrateEntries(await table
514
- .vectorSearch(queryEmbedding)
515
- .where('language = ' + '\'' + srcLang + '\'')
516
- .toArray());
517
- const rankedMatches = [];
518
- const sourceCriteria = filters?.source;
519
- const targetCriteria = filters?.target ?? filters?.source;
520
- for (const sourceEntry of sourceEntries) {
521
- if (!sourceEntry.pureText || !sourceEntry.vector) {
522
- continue;
523
- }
524
- if (sourceCriteria && !this.metadataMatches(sourceEntry.metadata, sourceCriteria)) {
525
- continue;
526
- }
527
- // Linear mapping (more intuitive)
528
- const rawDistance = sourceEntry._distance;
529
- const l2Distance = typeof rawDistance === 'number'
530
- ? rawDistance
531
- : typeof rawDistance === 'string' && rawDistance.length > 0 && !Number.isNaN(Number(rawDistance))
532
- ? Number(rawDistance)
533
- : 0;
534
- const semanticScore = Math.max(0, Math.round((2 - l2Distance) / 2 * 100));
535
- const fuzzyScore = MatchQuality.similarity(searchStr, sourceEntry.pureText);
536
- const hybridScore = Math.round((semanticScore + fuzzyScore) / 2);
537
- // Only include matches that meet the minimum similarity threshold
538
- if (hybridScore >= similarity) {
539
- const targetEntry = await this.findTargetEntry(table, sourceEntry, tgtLang, targetCriteria);
540
- if (!targetEntry) {
541
- continue;
542
- }
543
- if (targetCriteria && !this.metadataMatches(targetEntry.metadata, targetCriteria)) {
544
- continue;
545
- }
546
- try {
547
- const sourceElement = Utils.buildXMLElement(sourceEntry.element);
548
- const targetElement = Utils.buildXMLElement(targetEntry.element);
549
- const match = new Match(sourceElement, targetElement, this.name, semanticScore, fuzzyScore);
550
- const rankingScore = this.computeRankingScore(match.hybridScore(), sourceEntry, targetEntry);
551
- rankedMatches.push({ match, score: rankingScore });
552
- }
553
- catch (parseErr) {
554
- console.error('Error creating Match for semantic translation search: ' + (parseErr instanceof Error ? parseErr.message : String(parseErr)));
555
- }
556
- }
557
- }
558
- rankedMatches.sort((a, b) => b.score - a.score);
559
- return rankedMatches.slice(0, limit).map((item) => item.match);
560
- }
561
- catch (err) {
562
- console.error('Error performing semantic search with quality:', err);
563
- return [];
564
- }
565
- }
566
- async findTargetEntry(table, sourceEntry, tgtLang, filters) {
567
- if (!sourceEntry.fileId || !sourceEntry.unitId) {
568
- return null;
569
- }
570
- const exactId = this.buildEntryId(sourceEntry.fileId, sourceEntry.unitId, sourceEntry.segmentIndex, tgtLang);
571
- const exactMatches = this.hydrateEntries(await table
572
- .query()
573
- .where('id = ' + '\'' + Utils.replaceQuotes(exactId) + '\'')
574
- .toArray());
575
- const exactMatch = this.selectPreferredCandidate(exactMatches, sourceEntry.segmentIndex, filters);
576
- if (exactMatch) {
577
- return exactMatch;
578
- }
579
- const unitPrefix = sourceEntry.fileId + ':' + sourceEntry.unitId + ':';
580
- const sanitizedPrefix = Utils.replaceQuotes(unitPrefix);
581
- const sanitizedLang = Utils.replaceQuotes(tgtLang);
582
- const unitMatches = this.hydrateEntries(await table
583
- .query()
584
- .where('starts_with(id, ' + '\'' + sanitizedPrefix + '\') AND language = ' + '\'' + sanitizedLang + '\'')
585
- .limit(50)
586
- .toArray());
587
- if (unitMatches.length === 0) {
588
- return null;
589
- }
590
- const preferred = this.selectPreferredCandidate(unitMatches, sourceEntry.segmentIndex, filters);
591
- return preferred ?? null;
592
- }
593
- computeRankingScore(baseScore, sourceEntry, targetEntry) {
594
- let score = baseScore;
595
- if (sourceEntry.segmentIndex > 0 && targetEntry.segmentIndex > 0) {
596
- score += sourceEntry.segmentIndex === targetEntry.segmentIndex ? 10 : 5;
597
- }
598
- const quality = targetEntry.metadata && typeof targetEntry.metadata.quality === 'number'
599
- ? targetEntry.metadata.quality
600
- : undefined;
601
- if (quality !== undefined && !Number.isNaN(quality)) {
602
- score += Math.min(Math.max(quality, 0), 100) / 20;
603
- }
604
- const recencyTimestamp = this.parseMetadataTimestamp(targetEntry.metadata?.changeDate || targetEntry.metadata?.creationDate);
605
- if (recencyTimestamp !== undefined) {
606
- const ageDays = (Date.now() - recencyTimestamp) / 86400000;
607
- if (Number.isFinite(ageDays) && ageDays >= 0) {
608
- const normalized = Math.max(0, 1 - Math.min(ageDays, 365) / 365);
609
- score += normalized * 5;
610
- }
611
- }
612
- if (targetEntry.metadata?.state === 'final') {
613
- score += 3;
614
- }
615
- else if (targetEntry.metadata?.state === 'reviewed') {
616
- score += 2;
617
- }
618
- else if (targetEntry.metadata?.state === 'translated') {
619
- score += 1;
620
- }
621
- return score;
622
- }
623
- parseMetadataTimestamp(value) {
624
- if (!value) {
625
- return undefined;
626
- }
627
- const parsed = Date.parse(value);
628
- return Number.isNaN(parsed) ? undefined : parsed;
629
- }
630
- selectPreferredCandidate(entries, desiredSegmentIndex, filters) {
631
- if (!entries || entries.length === 0) {
632
- return undefined;
633
- }
634
- const filtered = filters
635
- ? entries.filter((entry) => this.metadataMatches(entry.metadata, filters))
636
- : entries.slice();
637
- if (filtered.length === 0) {
638
- return undefined;
639
- }
640
- if (desiredSegmentIndex > 0) {
641
- const exact = filtered.find((entry) => entry.segmentIndex === desiredSegmentIndex);
642
- if (exact) {
643
- return exact;
644
- }
645
- }
646
- const segmentEntry = filtered.find((entry) => entry.segmentIndex > 0);
647
- if (segmentEntry) {
648
- return segmentEntry;
649
- }
650
- return filtered[0];
651
- }
652
- metadataMatches(metadata, filter) {
653
- if (!filter) {
654
- return true;
655
- }
656
- if (!metadata) {
657
- return false;
658
- }
659
- if (filter.states && filter.states.length > 0) {
660
- if (!metadata.state || !filter.states.includes(metadata.state)) {
661
- return false;
662
- }
663
- }
664
- if (filter.minState) {
665
- const metadataRank = this.getStateRank(metadata.state);
666
- if (metadataRank === 0 || metadataRank < this.getStateRank(filter.minState)) {
667
- return false;
668
- }
669
- }
670
- if (typeof filter.minQuality === 'number') {
671
- if (typeof metadata.quality !== 'number' || metadata.quality < filter.minQuality) {
672
- return false;
673
- }
674
- }
675
- if (filter.contextIncludes && filter.contextIncludes.length > 0) {
676
- const contextValue = (metadata.context || '').toLowerCase();
677
- const matchesAll = filter.contextIncludes.every((needle) => contextValue.includes(needle.toLowerCase()));
678
- if (!matchesAll) {
679
- return false;
680
- }
681
- }
682
- if (filter.requiredProperties) {
683
- if (!metadata.properties) {
684
- return false;
685
- }
686
- for (const [key, expected] of Object.entries(filter.requiredProperties)) {
687
- if (metadata.properties[key] !== expected) {
688
- return false;
689
- }
690
- }
691
- }
692
- if (filter.provider) {
693
- const provider = metadata.segment?.provider;
694
- if (!provider || provider !== filter.provider) {
695
- return false;
696
- }
697
- }
698
- return true;
699
- }
700
- getStateRank(value) {
701
- if (!value) {
702
- return 0;
703
- }
704
- switch (value) {
705
- case 'initial':
706
- return 1;
707
- case 'translated':
708
- return 2;
709
- case 'reviewed':
710
- return 3;
711
- case 'final':
712
- return 4;
713
- default:
714
- return 0;
715
- }
716
- }
717
- // ============================
718
- // DATA MANAGEMENT METHODS
719
- // ============================
720
- async storeLangEntry(fileId, original, unitId, lang, pureText, element, embeddings, segmentIndex = 0, segmentCount = 1, metadata) {
721
- try {
722
- const table = await this.ensureTable();
723
- const sanitizedFileId = Utils.replaceQuotes(fileId);
724
- const sanitizedUnitId = Utils.replaceQuotes(unitId);
725
- const safeSegmentIndex = typeof segmentIndex === 'number' ? segmentIndex : 0;
726
- const safeSegmentCount = typeof segmentCount === 'number' && segmentCount > 0 ? segmentCount : 1;
727
- const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, safeSegmentIndex, lang);
728
- const existingEntries = this.hydrateEntries(await table
729
- .query()
730
- .where('id = ' + '\'' + entryId + '\'')
731
- .toArray());
732
- if (existingEntries.length > 0) {
733
- const existingEntry = existingEntries[0];
734
- const contentChanged = (existingEntry.pureText !== pureText ||
735
- existingEntry.element !== element.toString() ||
736
- existingEntry.original !== original);
737
- if (!contentChanged) {
738
- return;
739
- }
740
- }
741
- // Generate embeddings if not provided
742
- let vectorEmbeddings;
743
- if (embeddings && embeddings.length > 0) {
744
- vectorEmbeddings = embeddings;
745
- }
746
- else {
747
- vectorEmbeddings = await this.generateEmbedding(pureText);
748
- }
749
- const entry = {
750
- id: entryId,
751
- language: lang,
752
- pureText: pureText,
753
- element: element.toString(),
754
- fileId: sanitizedFileId,
755
- original: original,
756
- unitId: sanitizedUnitId,
757
- vector: vectorEmbeddings,
758
- segmentIndex: safeSegmentIndex,
759
- segmentCount: safeSegmentCount,
760
- metadata: metadata ?? {}
761
- };
762
- // Delete existing entry first (LanceDB upsert approach)
763
- try {
764
- await table.delete('id = ' + '\'' + entryId + '\'');
765
- }
766
- catch (deleteErr) {
767
- // Entry might not exist, which is fine
768
- }
769
- await table.add([this.flattenEntry(entry)]);
770
- }
771
- catch (err) {
772
- console.error('Error storing language entry:', err);
773
- throw err;
774
- }
775
- }
776
- async storeBatchEntries(entries) {
777
- try {
778
- const table = await this.ensureTable();
779
- const langEntries = [];
780
- // Collect all entry IDs for bulk deletion
781
- const entryIds = [];
782
- // Generate embeddings one by one and build LangEntry objects
783
- for (const entry of entries) {
784
- const sanitizedFileId = Utils.replaceQuotes(entry.fileId);
785
- const sanitizedUnitId = Utils.replaceQuotes(entry.unitId);
786
- const vectorEmbeddings = await this.generateEmbedding(entry.pureText);
787
- const safeSegmentIndex = typeof entry.segmentIndex === 'number' ? entry.segmentIndex : 0;
788
- const safeSegmentCount = typeof entry.segmentCount === 'number' && entry.segmentCount > 0 ? entry.segmentCount : 1;
789
- const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, safeSegmentIndex, entry.language);
790
- const langEntry = {
791
- id: entryId,
792
- language: entry.language,
793
- pureText: entry.pureText,
794
- element: entry.element.toString(),
795
- fileId: sanitizedFileId,
796
- original: entry.original,
797
- unitId: sanitizedUnitId,
798
- vector: vectorEmbeddings,
799
- segmentIndex: safeSegmentIndex,
800
- segmentCount: safeSegmentCount,
801
- metadata: entry.metadata ?? {}
802
- };
803
- langEntries.push(langEntry);
804
- entryIds.push(entryId);
805
- }
806
- // Delete any existing entries with these IDs to prevent duplicates
807
- // This ensures that if the same file is imported twice, entries are overwritten
808
- if (entryIds.length > 0) {
809
- const idsFilter = entryIds.map(id => '\'' + id + '\'').join(',');
810
- try {
811
- await table.delete('id IN (' + idsFilter + ')');
812
- }
813
- catch (deleteErr) {
814
- // Entries might not exist, which is fine for first import
815
- }
816
- }
817
- // Bulk insert all entries at once
818
- const flattenedEntries = langEntries.map((item) => this.flattenEntry(item));
819
- await table.add(flattenedEntries);
820
- }
821
- catch (err) {
822
- console.error('Error storing batch entries:', err);
823
- throw err;
824
- }
825
- }
826
- async entryExists(fileId, unitId, lang, segmentIndex = 0) {
827
- try {
828
- if (!this.table) {
829
- await this.initializeDatabase();
830
- }
831
- if (!this.table) {
832
- throw new Error('Failed to initialize database table');
833
- }
834
- const sanitizedFileId = Utils.replaceQuotes(fileId);
835
- const sanitizedUnitId = Utils.replaceQuotes(unitId);
836
- const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, segmentIndex, lang);
837
- const existingEntries = this.hydrateEntries(await this.table
838
- .query()
839
- .where('id = ' + '\'' + entryId + '\'')
840
- .toArray());
841
- return existingEntries.length > 0;
842
- }
843
- catch (err) {
844
- console.error('Error checking entry existence:', err);
845
- return false;
846
- }
847
- }
848
- async getLangEntry(fileId, unitId, lang, segmentIndex = 0) {
849
- try {
850
- if (!this.table) {
851
- await this.initializeDatabase();
852
- }
853
- if (!this.table) {
854
- throw new Error('Failed to initialize database table');
855
- }
856
- const sanitizedFileId = Utils.replaceQuotes(fileId);
857
- const sanitizedUnitId = Utils.replaceQuotes(unitId);
858
- const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, segmentIndex, lang);
859
- const existingEntries = this.hydrateEntries(await this.table
860
- .query()
861
- .where('id = ' + '\'' + entryId + '\'')
862
- .toArray());
863
- return existingEntries.length > 0 ? existingEntries[0] : null;
864
- }
865
- catch (err) {
866
- console.error('Error retrieving language entry:', err);
867
- return null;
868
- }
869
- }
870
- async deleteLangEntry(fileId, unitId, lang, segmentIndex = 0) {
871
- try {
872
- if (!this.table) {
873
- await this.initializeDatabase();
874
- }
875
- if (!this.table) {
876
- throw new Error('Failed to initialize database table');
877
- }
878
- const sanitizedFileId = Utils.replaceQuotes(fileId);
879
- const sanitizedUnitId = Utils.replaceQuotes(unitId);
880
- const entryId = this.buildEntryId(sanitizedFileId, sanitizedUnitId, segmentIndex, lang);
881
- // Check if entry exists first
882
- const exists = await this.entryExists(fileId, unitId, lang, segmentIndex);
883
- if (!exists) {
884
- return false;
885
- }
886
- // Delete the entry
887
- await this.table.delete('id = \'' + entryId + '\'');
888
- return true;
889
- }
890
- catch (err) {
891
- console.error('Error deleting language entry:', err);
892
- return false;
893
- }
894
- }
895
- // ============================
896
- // DATA IMPORT METHODS
897
- // ============================
898
- async importXLIFF(filePath, options) {
899
- // Phase 1: Parse XLIFF and write to temporary JSONL file
900
- const resolvedOptions = resolveImportOptions(options);
901
- const reader = new XLIFFReader(filePath, resolvedOptions);
902
- await reader.parse();
903
- // Phase 2: Batch import from JSONL file (asynchronous)
904
- const importer = new BatchImporter(this, reader.getTempFilePath(), reader.getEntryCount());
905
- await importer.import();
906
- }
907
- async importTMX(filePath, options) {
908
- // Phase 1: Parse TMX and write to temporary JSONL file
909
- const resolvedOptions = resolveImportOptions(options);
910
- const reader = new TMXReader(filePath, resolvedOptions);
911
- await reader.parse();
912
- // Phase 2: Batch import from JSONL file (asynchronous)
913
- const importer = new BatchImporter(this, reader.getTempFilePath(), reader.getEntryCount());
914
- await importer.import();
915
- }
916
- async importSDLTM(filePath, options) {
917
- const tempDir = tmpdir();
918
- const tempFileName = 'tmx_' + Date.now() + '_' + Math.random().toString(36).substring(7) + '.tmx';
919
- const tempFilePath = join(tempDir, tempFileName);
920
- const packageJson = await import('../package.json', { assert: { type: 'json' } });
921
- const productName = packageJson.default.productName;
922
- const version = packageJson.default.version;
923
- const tmReader = new TMReader({ 'productName': productName, 'version': version });
924
- const data = await tmReader.convert(filePath, tempFilePath);
925
- if (data.status !== 'Success') {
926
- throw new Error('SDL TM conversion failed for file ' + filePath);
927
- }
928
- await this.importTMX(tempFilePath, options);
929
- unlinkSync(tempFilePath);
930
- }
931
- }
932
- //# sourceMappingURL=hybridtm.js.map