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