label-studio-converter 1.1.0 → 1.3.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.
@@ -38,12 +38,11 @@ var init_cjs_shims = __esm({
38
38
  });
39
39
 
40
40
  // src/constants.ts
41
- var OUTPUT_BASE_DIR, DEFAULT_LABEL_NAME, DEFAULT_LABEL_STUDIO_FULL_JSON, DEFAULT_CREATE_FILE_PER_IMAGE, DEFAULT_CREATE_FILE_LIST_FOR_SERVING, DEFAULT_FILE_LIST_NAME, DEFAULT_BASE_SERVER_URL, DEFAULT_PPOCR_FILE_NAME, SORT_VERTICAL_NONE, SORT_VERTICAL_TOP_BOTTOM, SORT_VERTICAL_BOTTOM_TOP, DEFAULT_SORT_VERTICAL, SORT_HORIZONTAL_NONE, SORT_HORIZONTAL_LTR, SORT_HORIZONTAL_RTL, DEFAULT_SORT_HORIZONTAL, SHAPE_NORMALIZE_NONE, SHAPE_NORMALIZE_RECTANGLE, DEFAULT_SHAPE_NORMALIZE, DEFAULT_WIDTH_INCREMENT, DEFAULT_HEIGHT_INCREMENT, DEFAULT_LABEL_STUDIO_PRECISION, DEFAULT_PPOCR_PRECISION;
41
+ var DEFAULT_LABEL_NAME, DEFAULT_LABEL_STUDIO_FULL_JSON, DEFAULT_CREATE_FILE_PER_IMAGE, DEFAULT_CREATE_FILE_LIST_FOR_SERVING, DEFAULT_FILE_LIST_NAME, DEFAULT_BASE_SERVER_URL, DEFAULT_PPOCR_FILE_NAME, SORT_VERTICAL_NONE, SORT_VERTICAL_TOP_BOTTOM, SORT_VERTICAL_BOTTOM_TOP, DEFAULT_SORT_VERTICAL, SORT_HORIZONTAL_NONE, SORT_HORIZONTAL_LTR, SORT_HORIZONTAL_RTL, DEFAULT_SORT_HORIZONTAL, SHAPE_NORMALIZE_NONE, SHAPE_NORMALIZE_RECTANGLE, DEFAULT_SHAPE_NORMALIZE, DEFAULT_WIDTH_INCREMENT, DEFAULT_HEIGHT_INCREMENT, DEFAULT_LABEL_STUDIO_PRECISION, DEFAULT_PPOCR_PRECISION, DEFAULT_RECURSIVE, DEFAULT_PPOCR_FILE_PATTERN, DEFAULT_LABEL_STUDIO_FILE_PATTERN, OUTPUT_MODE_ANNOTATIONS, OUTPUT_MODE_PREDICTIONS, DEFAULT_OUTPUT_MODE, DEFAULT_BACKUP, BACKUP_SUFFIX_PREFIX;
42
42
  var init_constants = __esm({
43
43
  "src/constants.ts"() {
44
44
  "use strict";
45
45
  init_cjs_shims();
46
- OUTPUT_BASE_DIR = "./output";
47
46
  DEFAULT_LABEL_NAME = "Text";
48
47
  DEFAULT_LABEL_STUDIO_FULL_JSON = true;
49
48
  DEFAULT_CREATE_FILE_PER_IMAGE = false;
@@ -66,6 +65,86 @@ var init_constants = __esm({
66
65
  DEFAULT_HEIGHT_INCREMENT = 0;
67
66
  DEFAULT_LABEL_STUDIO_PRECISION = -1;
68
67
  DEFAULT_PPOCR_PRECISION = 0;
68
+ DEFAULT_RECURSIVE = false;
69
+ DEFAULT_PPOCR_FILE_PATTERN = ".*\\.txt$";
70
+ DEFAULT_LABEL_STUDIO_FILE_PATTERN = ".*\\.json$";
71
+ OUTPUT_MODE_ANNOTATIONS = "annotations";
72
+ OUTPUT_MODE_PREDICTIONS = "predictions";
73
+ DEFAULT_OUTPUT_MODE = OUTPUT_MODE_ANNOTATIONS;
74
+ DEFAULT_BACKUP = false;
75
+ BACKUP_SUFFIX_PREFIX = "backup-";
76
+ }
77
+ });
78
+
79
+ // src/lib/backup-utils.ts
80
+ async function backupFileIfExists(filePath) {
81
+ if (!(0, import_fs.existsSync)(filePath)) {
82
+ return null;
83
+ }
84
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").split(".")[0];
85
+ const backupPath = `${filePath}.${BACKUP_SUFFIX_PREFIX}${timestamp}`;
86
+ await copyFileAsync(filePath, backupPath);
87
+ return backupPath;
88
+ }
89
+ var import_fs, import_util, copyFileAsync;
90
+ var init_backup_utils = __esm({
91
+ "src/lib/backup-utils.ts"() {
92
+ "use strict";
93
+ init_cjs_shims();
94
+ import_fs = require("fs");
95
+ import_util = require("util");
96
+ init_constants();
97
+ copyFileAsync = (0, import_util.promisify)(import_fs.copyFile);
98
+ }
99
+ });
100
+
101
+ // src/lib/file-utils.ts
102
+ async function findFiles(dirs, pattern, recursive) {
103
+ const allFiles = [];
104
+ const regex = new RegExp(pattern);
105
+ async function scanDirectory(dirPath) {
106
+ const entries = await (0, import_promises.readdir)(dirPath, { withFileTypes: true });
107
+ for (const entry of entries) {
108
+ const fullPath = (0, import_path.join)(dirPath, entry.name);
109
+ if (entry.isDirectory()) {
110
+ if (recursive) {
111
+ await scanDirectory(fullPath);
112
+ }
113
+ } else if (entry.isFile()) {
114
+ if (regex.test(entry.name)) {
115
+ allFiles.push(fullPath);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ for (const dir of dirs) {
121
+ const dirStat = await (0, import_promises.stat)(dir);
122
+ if (dirStat.isDirectory()) {
123
+ await scanDirectory(dir);
124
+ } else if (dirStat.isFile()) {
125
+ if (regex.test(dir)) {
126
+ allFiles.push(dir);
127
+ }
128
+ }
129
+ }
130
+ return allFiles;
131
+ }
132
+ function getRelativePathFromInputs(filePath, inputDirs) {
133
+ for (const inputDir of inputDirs) {
134
+ const rel = (0, import_path.relative)(inputDir, filePath);
135
+ if (!rel.startsWith("..") && !rel.startsWith("/")) {
136
+ return rel;
137
+ }
138
+ }
139
+ return (0, import_path.relative)(process.cwd(), filePath);
140
+ }
141
+ var import_promises, import_path;
142
+ var init_file_utils = __esm({
143
+ "src/lib/file-utils.ts"() {
144
+ "use strict";
145
+ init_cjs_shims();
146
+ import_promises = require("fs/promises");
147
+ import_path = require("path");
69
148
  }
70
149
  });
71
150
 
@@ -155,273 +234,533 @@ var init_geometry = __esm({
155
234
  }
156
235
  });
157
236
 
158
- // src/lib/ppocr-label.ts
159
- var import_node_crypto, import_node_fs, import_node_path, import_image_size, ppocrToLabelStudio, ppocrToFullLabelStudio, ppocrToMinLabelStudio;
160
- var init_ppocr_label = __esm({
161
- "src/lib/ppocr-label.ts"() {
237
+ // src/lib/sort.ts
238
+ function getBoundingBoxCenter(points) {
239
+ let minX = Infinity;
240
+ let minY = Infinity;
241
+ let maxX = -Infinity;
242
+ let maxY = -Infinity;
243
+ for (const [x, y] of points) {
244
+ if (x !== void 0 && y !== void 0) {
245
+ minX = Math.min(minX, x);
246
+ minY = Math.min(minY, y);
247
+ maxX = Math.max(maxX, x);
248
+ maxY = Math.max(maxY, y);
249
+ }
250
+ }
251
+ return {
252
+ x: (minX + maxX) / 2,
253
+ y: (minY + maxY) / 2,
254
+ width: maxX - minX,
255
+ height: maxY - minY
256
+ };
257
+ }
258
+ function sortBoundingBoxes(annotations, verticalSort, horizontalSort) {
259
+ if (verticalSort === SORT_VERTICAL_NONE && horizontalSort === SORT_HORIZONTAL_NONE) {
260
+ return annotations;
261
+ }
262
+ const sorted = [...annotations];
263
+ const isVerticalText = sorted.length > 0 && (() => {
264
+ const verticalCount = sorted.filter((ann) => {
265
+ const center = getBoundingBoxCenter(ann.points);
266
+ return center.height > center.width * 1.5;
267
+ }).length;
268
+ return verticalCount > sorted.length / 2;
269
+ })();
270
+ if (horizontalSort === SORT_HORIZONTAL_RTL && verticalSort !== SORT_VERTICAL_NONE && isVerticalText) {
271
+ const annotationsWithCenters = sorted.map((ann) => ({
272
+ annotation: ann,
273
+ center: getBoundingBoxCenter(ann.points)
274
+ }));
275
+ const columns = [];
276
+ for (const item of annotationsWithCenters) {
277
+ let addedToColumn = false;
278
+ for (const column of columns) {
279
+ const avgX = column.reduce((sum, c) => sum + c.center.x, 0) / column.length;
280
+ if (Math.abs(item.center.x - avgX) < GROUPING_TOLERANCE) {
281
+ column.push(item);
282
+ addedToColumn = true;
283
+ break;
284
+ }
285
+ }
286
+ if (!addedToColumn) {
287
+ columns.push([item]);
288
+ }
289
+ }
290
+ columns.sort((colA, colB) => {
291
+ const avgXA = colA.reduce((sum, c) => sum + c.center.x, 0) / colA.length;
292
+ const avgXB = colB.reduce((sum, c) => sum + c.center.x, 0) / colB.length;
293
+ return avgXB - avgXA;
294
+ });
295
+ for (const column of columns) {
296
+ column.sort((a, b) => {
297
+ return verticalSort === SORT_VERTICAL_TOP_BOTTOM ? a.center.y - b.center.y : b.center.y - a.center.y;
298
+ });
299
+ }
300
+ return columns.flat().map((item) => item.annotation);
301
+ }
302
+ sorted.sort((a, b) => {
303
+ const centerA = getBoundingBoxCenter(a.points);
304
+ const centerB = getBoundingBoxCenter(b.points);
305
+ if (verticalSort !== SORT_VERTICAL_NONE) {
306
+ const yDiff = verticalSort === SORT_VERTICAL_TOP_BOTTOM ? centerA.y - centerB.y : centerB.y - centerA.y;
307
+ if (Math.abs(yDiff) > GROUPING_TOLERANCE) {
308
+ return yDiff;
309
+ }
310
+ }
311
+ if (horizontalSort !== SORT_HORIZONTAL_NONE) {
312
+ return horizontalSort === SORT_HORIZONTAL_LTR ? centerA.x - centerB.x : centerB.x - centerA.x;
313
+ }
314
+ return 0;
315
+ });
316
+ return sorted;
317
+ }
318
+ var GROUPING_TOLERANCE;
319
+ var init_sort = __esm({
320
+ "src/lib/sort.ts"() {
162
321
  "use strict";
163
322
  init_cjs_shims();
164
- import_node_crypto = require("crypto");
165
- import_node_fs = require("fs");
166
- import_node_path = require("path");
167
- import_image_size = __toESM(require("image-size"), 1);
168
323
  init_constants();
324
+ GROUPING_TOLERANCE = 50;
325
+ }
326
+ });
327
+
328
+ // src/lib/enhance.ts
329
+ function enhancePPOCRLabel(data, options) {
330
+ const {
331
+ sortVertical,
332
+ sortHorizontal,
333
+ normalizeShape: normalizeShape2,
334
+ widthIncrement = 0,
335
+ heightIncrement = 0,
336
+ precision = 0
337
+ } = options;
338
+ let enhanced = data;
339
+ if (sortVertical && sortHorizontal) {
340
+ enhanced = sortBoundingBoxes(enhanced, sortVertical, sortHorizontal);
341
+ }
342
+ if (normalizeShape2 || widthIncrement !== 0 || heightIncrement !== 0) {
343
+ enhanced = enhanced.map((annotation) => {
344
+ let points = transformPoints(annotation.points, {
345
+ normalizeShape: normalizeShape2,
346
+ widthIncrement,
347
+ heightIncrement
348
+ });
349
+ points = roundPoints(points, precision);
350
+ return {
351
+ ...annotation,
352
+ points
353
+ };
354
+ });
355
+ }
356
+ return enhanced;
357
+ }
358
+ var init_enhance = __esm({
359
+ "src/lib/enhance.ts"() {
360
+ "use strict";
361
+ init_cjs_shims();
362
+ init_geometry();
363
+ init_sort();
364
+ }
365
+ });
366
+
367
+ // src/lib/label-studio.ts
368
+ var turf, labelStudioToPPOCR, minLabelStudioToPPOCR, processAnnotationOrPrediction, enhanceLabelStudioData;
369
+ var init_label_studio = __esm({
370
+ "src/lib/label-studio.ts"() {
371
+ "use strict";
372
+ init_cjs_shims();
373
+ turf = __toESM(require("@turf/turf"), 1);
374
+ init_constants();
375
+ init_enhance();
169
376
  init_geometry();
170
- ppocrToLabelStudio = async (data, options) => {
377
+ labelStudioToPPOCR = async (data, options) => {
171
378
  const {
172
- imagePath,
173
- baseServerUrl,
174
- inputDir,
175
- toFullJson = true,
176
- taskId = 1,
177
- labelName = DEFAULT_LABEL_NAME,
379
+ baseImageDir,
178
380
  normalizeShape: normalizeShape2,
179
381
  widthIncrement = 0,
180
382
  heightIncrement = 0,
181
- precision = DEFAULT_LABEL_STUDIO_PRECISION
383
+ precision = 0
182
384
  } = options || {};
183
- if (toFullJson) {
184
- return ppocrToFullLabelStudio(
185
- data,
186
- imagePath,
187
- baseServerUrl,
188
- inputDir,
189
- taskId,
190
- labelName,
191
- normalizeShape2,
192
- widthIncrement,
193
- heightIncrement,
194
- precision
195
- );
196
- } else {
197
- return ppocrToMinLabelStudio(
198
- data,
199
- imagePath,
200
- baseServerUrl,
201
- inputDir,
202
- labelName,
203
- normalizeShape2,
204
- widthIncrement,
205
- heightIncrement,
206
- precision
207
- );
208
- }
209
- };
210
- ppocrToFullLabelStudio = (data, imagePath, baseServerUrl, inputDir, taskId = 1, labelName = DEFAULT_LABEL_NAME, normalizeShape2, widthIncrement = 0, heightIncrement = 0, precision = DEFAULT_LABEL_STUDIO_PRECISION) => {
211
- const newBaseServerUrl = baseServerUrl.replace(/\/+$/, "") + (baseServerUrl === "" ? "" : "/");
212
- const now = (/* @__PURE__ */ new Date()).toISOString();
213
- let original_width = 1920;
214
- let original_height = 1080;
215
- const resolvedImagePath = inputDir ? (0, import_node_path.join)(inputDir, imagePath) : imagePath;
216
- if (!(0, import_node_fs.existsSync)(resolvedImagePath)) {
217
- throw new Error(`Image file not found: ${resolvedImagePath}`);
218
- }
219
- const buffer = (0, import_node_fs.readFileSync)(resolvedImagePath);
220
- const dimensions = (0, import_image_size.default)(buffer);
221
- if (!dimensions.width || !dimensions.height) {
222
- throw new Error(
223
- `Failed to read image dimensions from: ${resolvedImagePath}`
224
- );
225
- }
226
- original_width = dimensions.width;
227
- original_height = dimensions.height;
228
- const fileName = imagePath.split("/").pop() || imagePath;
229
- const result = [
230
- {
231
- id: taskId,
232
- annotations: [
233
- {
234
- id: taskId,
235
- completed_by: 1,
236
- result: data.map((item) => {
237
- let { points } = item;
238
- points = transformPoints(points, {
239
- normalizeShape: normalizeShape2,
240
- widthIncrement,
241
- heightIncrement
242
- });
243
- const annotationId = (0, import_node_crypto.randomUUID)().slice(0, 10);
244
- const polygonPoints = points.map(([x, y]) => [
245
- roundToPrecision((x ?? 0) / original_width * 100, precision),
246
- roundToPrecision((y ?? 0) / original_height * 100, precision)
385
+ const resultMap = /* @__PURE__ */ new Map();
386
+ for (const task of data) {
387
+ let imagePath = task.file_upload || "";
388
+ if (task.data.ocr) {
389
+ const urlPath = task.data.ocr.replace(/^https?:\/\/[^/]+\//, "");
390
+ imagePath = decodeURIComponent(urlPath);
391
+ }
392
+ if (baseImageDir) {
393
+ imagePath = `${baseImageDir}/${task.file_upload || imagePath.split("/").pop() || imagePath}`;
394
+ }
395
+ const imageAnnotations = [];
396
+ for (const annotation of task.annotations) {
397
+ const groupedById = /* @__PURE__ */ new Map();
398
+ for (const resultItem of annotation.result) {
399
+ const { id } = resultItem;
400
+ if (!groupedById.has(id)) {
401
+ groupedById.set(id, []);
402
+ }
403
+ groupedById.get(id).push(resultItem);
404
+ }
405
+ for (const [_, resultItems] of groupedById) {
406
+ let points;
407
+ let transcription = "";
408
+ for (const resultItem of resultItems) {
409
+ if ("points" in resultItem.value && resultItem.value.points) {
410
+ const { points: valuePoints } = resultItem.value;
411
+ const { original_width, original_height } = resultItem;
412
+ points = valuePoints.map(([x, y]) => [
413
+ (x ?? 0) * original_width / 100,
414
+ (y ?? 0) * original_height / 100
247
415
  ]);
248
- return [
249
- // 1. Polygon geometry only
250
- {
251
- original_width,
252
- original_height,
253
- image_rotation: 0,
254
- value: {
255
- points: polygonPoints,
256
- closed: true
257
- },
258
- id: annotationId,
259
- from_name: "poly",
260
- to_name: "image",
261
- type: "polygon",
262
- origin: "manual"
263
- },
264
- // 2. Labels with polygon geometry
265
- {
266
- original_width,
267
- original_height,
268
- image_rotation: 0,
269
- value: {
270
- points: polygonPoints,
271
- closed: true,
272
- labels: [labelName]
273
- },
274
- id: annotationId,
275
- from_name: "label",
276
- to_name: "image",
277
- type: "labels",
278
- origin: "manual"
279
- },
280
- // 3. Textarea with polygon geometry and text
281
- {
282
- original_width,
283
- original_height,
284
- image_rotation: 0,
285
- value: {
286
- points: polygonPoints,
287
- closed: true,
288
- text: [item.transcription]
289
- },
290
- id: annotationId,
291
- from_name: "transcription",
292
- to_name: "image",
293
- type: "textarea",
294
- origin: "manual"
295
- }
416
+ } else if ("x" in resultItem.value && "y" in resultItem.value && "width" in resultItem.value && "height" in resultItem.value) {
417
+ const { x, y, width, height } = resultItem.value;
418
+ const { original_width, original_height } = resultItem;
419
+ const absX = x * original_width / 100;
420
+ const absY = y * original_height / 100;
421
+ const absWidth = width * original_width / 100;
422
+ const absHeight = height * original_height / 100;
423
+ points = [
424
+ [absX, absY],
425
+ [absX + absWidth, absY],
426
+ [absX + absWidth, absY + absHeight],
427
+ [absX, absY + absHeight]
296
428
  ];
297
- }).flat(),
298
- was_cancelled: false,
299
- ground_truth: false,
300
- created_at: now,
301
- updated_at: now,
302
- draft_created_at: now,
303
- lead_time: 0,
304
- prediction: {},
305
- result_count: data.length * 3,
306
- unique_id: (0, import_node_crypto.randomUUID)(),
307
- import_id: null,
308
- last_action: null,
309
- bulk_created: false,
310
- task: taskId,
311
- project: 1,
312
- updated_by: 1,
313
- parent_prediction: null,
314
- parent_annotation: null,
315
- last_created_by: null
429
+ }
430
+ if ("text" in resultItem.value && Array.isArray(resultItem.value.text)) {
431
+ transcription = resultItem.value.text[0] || "";
432
+ }
316
433
  }
317
- ],
318
- file_upload: fileName,
319
- drafts: [],
320
- predictions: [],
321
- data: { ocr: `${newBaseServerUrl}${imagePath}` },
322
- meta: {},
323
- created_at: now,
324
- updated_at: now,
325
- allow_skip: false,
326
- inner_id: taskId,
327
- total_annotations: 1,
328
- cancelled_annotations: 0,
329
- total_predictions: 0,
330
- comment_count: 0,
331
- unresolved_comment_count: 0,
332
- last_comment_updated_at: null,
333
- project: 1,
334
- updated_by: 1,
335
- comment_authors: []
434
+ if (points && points.length > 0) {
435
+ points = transformPoints(points, {
436
+ normalizeShape: normalizeShape2,
437
+ widthIncrement,
438
+ heightIncrement
439
+ });
440
+ points = roundPoints(points, precision);
441
+ let dt_score = 1;
442
+ try {
443
+ const firstPoint = points[0];
444
+ if (firstPoint) {
445
+ const polygon2 = turf.polygon([points.concat([firstPoint])]);
446
+ const area2 = turf.area(polygon2);
447
+ dt_score = Math.min(1, Math.max(0.5, area2 / 1e4));
448
+ }
449
+ } catch {
450
+ dt_score = 0.8;
451
+ }
452
+ imageAnnotations.push({
453
+ transcription,
454
+ points,
455
+ dt_score
456
+ });
457
+ }
458
+ }
459
+ }
460
+ if (imageAnnotations.length > 0) {
461
+ resultMap.set(imagePath, imageAnnotations);
336
462
  }
337
- ];
338
- return result;
339
- };
340
- ppocrToMinLabelStudio = (data, imagePath, baseServerUrl, inputDir, labelName = "text", normalizeShape2, widthIncrement = 0, heightIncrement = 0, precision = DEFAULT_LABEL_STUDIO_PRECISION) => {
341
- const newBaseServerUrl = baseServerUrl.replace(/\/+$/, "") + (baseServerUrl === "" ? "" : "/");
342
- const now = (/* @__PURE__ */ new Date()).toISOString();
343
- let original_width = 1920;
344
- let original_height = 1080;
345
- const resolvedImagePath = inputDir ? (0, import_node_path.join)(inputDir, imagePath) : imagePath;
346
- if (!(0, import_node_fs.existsSync)(resolvedImagePath)) {
347
- throw new Error(`Image file not found: ${resolvedImagePath}`);
348
463
  }
349
- const buffer = (0, import_node_fs.readFileSync)(resolvedImagePath);
350
- const dimensions = (0, import_image_size.default)(buffer);
351
- if (!dimensions.width || !dimensions.height) {
352
- throw new Error(
353
- `Failed to read image dimensions from: ${resolvedImagePath}`
464
+ return resultMap;
465
+ };
466
+ minLabelStudioToPPOCR = async (data, options) => {
467
+ const {
468
+ baseImageDir,
469
+ normalizeShape: normalizeShape2,
470
+ widthIncrement = 0,
471
+ heightIncrement = 0,
472
+ precision = 0
473
+ } = options || {};
474
+ const resultMap = /* @__PURE__ */ new Map();
475
+ for (const item of data) {
476
+ let imagePath = item.ocr || "";
477
+ if (imagePath) {
478
+ imagePath = decodeURIComponent(
479
+ imagePath.replace(/^https?:\/\/[^/]+\//, "")
480
+ );
481
+ }
482
+ if (baseImageDir) {
483
+ imagePath = `${baseImageDir}/${imagePath.split("/").pop() || imagePath}`;
484
+ }
485
+ const numAnnotations = Math.max(
486
+ item.poly?.length || 0,
487
+ item.bbox?.length || 0,
488
+ item.transcription?.length || 0
354
489
  );
490
+ for (let i = 0; i < numAnnotations; i++) {
491
+ let points;
492
+ if (item.poly && item.poly.length > i && item.poly[i]) {
493
+ const poly = item.poly[i];
494
+ if (poly) {
495
+ const { points: polyPoints } = poly;
496
+ points = polyPoints;
497
+ }
498
+ } else if (item.bbox && item.bbox.length > i && item.bbox[i]) {
499
+ const bbox = item.bbox[i];
500
+ if (bbox) {
501
+ const { x, y, width, height } = bbox;
502
+ points = [
503
+ [x, y],
504
+ [x + width, y],
505
+ [x + width, y + height],
506
+ [x, y + height]
507
+ ];
508
+ }
509
+ }
510
+ if (!points) {
511
+ continue;
512
+ }
513
+ points = transformPoints(points, {
514
+ normalizeShape: normalizeShape2,
515
+ widthIncrement,
516
+ heightIncrement
517
+ });
518
+ points = roundPoints(points, precision);
519
+ const transcription = item.transcription && item.transcription.length > i ? item.transcription[i] : "";
520
+ let dt_score = 1;
521
+ try {
522
+ const firstPoint = points[0];
523
+ if (firstPoint) {
524
+ const polygon2 = turf.polygon([points.concat([firstPoint])]);
525
+ const area2 = turf.area(polygon2);
526
+ dt_score = Math.min(1, Math.max(0.5, area2 / 1e4));
527
+ }
528
+ } catch {
529
+ dt_score = 0.8;
530
+ }
531
+ const annotation = {
532
+ transcription: transcription ?? "",
533
+ points,
534
+ dt_score
535
+ };
536
+ if (!resultMap.has(imagePath)) {
537
+ resultMap.set(imagePath, []);
538
+ }
539
+ resultMap.get(imagePath).push(annotation);
540
+ }
355
541
  }
356
- original_width = dimensions.width;
357
- original_height = dimensions.height;
358
- return data.map((item, index) => {
359
- let { points } = item;
360
- points = transformPoints(points, {
361
- normalizeShape: normalizeShape2,
362
- widthIncrement,
363
- heightIncrement
364
- });
365
- const roundedPoints = points.map(
366
- ([x, y]) => [
367
- roundToPrecision(x ?? 0, precision),
368
- roundToPrecision(y ?? 0, precision)
369
- ]
370
- );
371
- let minX = Infinity;
372
- let minY = Infinity;
373
- let maxX = -Infinity;
374
- let maxY = -Infinity;
375
- for (const point of roundedPoints) {
376
- const [x, y] = point;
377
- if (x !== void 0 && y !== void 0) {
378
- minX = Math.min(minX, x);
379
- minY = Math.min(minY, y);
380
- maxX = Math.max(maxX, x);
381
- maxY = Math.max(maxY, y);
542
+ return resultMap;
543
+ };
544
+ processAnnotationOrPrediction = (annotation, options) => {
545
+ const {
546
+ sortVertical,
547
+ sortHorizontal,
548
+ normalizeShape: normalizeShape2,
549
+ widthIncrement,
550
+ heightIncrement,
551
+ precision
552
+ } = options;
553
+ const groupedById = /* @__PURE__ */ new Map();
554
+ for (const resultItem of annotation.result) {
555
+ const { id } = resultItem;
556
+ if (!groupedById.has(id)) {
557
+ groupedById.set(id, []);
558
+ }
559
+ groupedById.get(id).push(resultItem);
560
+ }
561
+ const enhancedResult = [];
562
+ for (const [, resultItems] of groupedById) {
563
+ let ppocrAnnotations = [];
564
+ for (const resultItem of resultItems) {
565
+ let points;
566
+ if ("points" in resultItem.value && resultItem.value.points) {
567
+ const { points: valuePoints } = resultItem.value;
568
+ const { original_width, original_height } = resultItem;
569
+ points = valuePoints.map(([x, y]) => [
570
+ (x ?? 0) * original_width / 100,
571
+ (y ?? 0) * original_height / 100
572
+ ]);
573
+ } else if ("x" in resultItem.value && "y" in resultItem.value && "width" in resultItem.value && "height" in resultItem.value) {
574
+ const { x, y, width, height } = resultItem.value;
575
+ const { original_width, original_height } = resultItem;
576
+ const absX = x * original_width / 100;
577
+ const absY = y * original_height / 100;
578
+ const absWidth = width * original_width / 100;
579
+ const absHeight = height * original_height / 100;
580
+ points = [
581
+ [absX, absY],
582
+ [absX + absWidth, absY],
583
+ [absX + absWidth, absY + absHeight],
584
+ [absX, absY + absHeight]
585
+ ];
586
+ }
587
+ if (points) {
588
+ ppocrAnnotations.push({
589
+ transcription: "",
590
+ points,
591
+ dt_score: 1
592
+ });
382
593
  }
383
594
  }
384
- const width = maxX - minX;
385
- const height = maxY - minY;
386
- return {
387
- ocr: encodeURI(`${newBaseServerUrl}${imagePath}`),
388
- id: index + 1,
389
- bbox: [
390
- {
391
- x: minX,
392
- y: minY,
393
- width,
394
- height,
395
- rotation: 0,
396
- original_width,
397
- original_height
595
+ if (ppocrAnnotations.length > 0) {
596
+ ppocrAnnotations = enhancePPOCRLabel(ppocrAnnotations, {
597
+ sortVertical,
598
+ sortHorizontal,
599
+ normalizeShape: normalizeShape2,
600
+ widthIncrement,
601
+ heightIncrement,
602
+ precision
603
+ });
604
+ for (let i = 0; i < resultItems.length; i++) {
605
+ const resultItem = resultItems[i];
606
+ const enhanced = ppocrAnnotations[i];
607
+ if (!enhanced) {
608
+ enhancedResult.push(resultItem);
609
+ continue;
398
610
  }
399
- ],
400
- label: [
401
- {
402
- points: roundedPoints,
403
- closed: true,
404
- labels: [labelName],
405
- original_width,
406
- original_height
611
+ if ("points" in resultItem.value && resultItem.value.points) {
612
+ const { original_width, original_height } = resultItem;
613
+ enhancedResult.push({
614
+ ...resultItem,
615
+ value: {
616
+ ...resultItem.value,
617
+ points: enhanced.points.map(
618
+ ([x, y]) => [
619
+ (x ?? 0) / original_width * 100,
620
+ (y ?? 0) / original_height * 100
621
+ ]
622
+ )
623
+ }
624
+ });
625
+ } else if ("x" in resultItem.value && "y" in resultItem.value && "width" in resultItem.value && "height" in resultItem.value) {
626
+ const { original_width, original_height } = resultItem;
627
+ const xs = enhanced.points.map(([x]) => x ?? 0);
628
+ const ys = enhanced.points.map(([, y]) => y ?? 0);
629
+ const minX = Math.min(...xs);
630
+ const maxX = Math.max(...xs);
631
+ const minY = Math.min(...ys);
632
+ const maxY = Math.max(...ys);
633
+ enhancedResult.push({
634
+ ...resultItem,
635
+ value: {
636
+ ...resultItem.value,
637
+ x: minX / original_width * 100,
638
+ y: minY / original_height * 100,
639
+ width: (maxX - minX) / original_width * 100,
640
+ height: (maxY - minY) / original_height * 100
641
+ }
642
+ });
643
+ } else {
644
+ enhancedResult.push(resultItem);
407
645
  }
408
- ],
409
- transcription: [item.transcription],
410
- poly: [
411
- {
412
- points: roundedPoints,
413
- closed: true,
414
- original_width,
415
- original_height
646
+ }
647
+ } else {
648
+ enhancedResult.push(...resultItems);
649
+ }
650
+ }
651
+ return {
652
+ ...annotation,
653
+ result: enhancedResult
654
+ };
655
+ };
656
+ enhanceLabelStudioData = async (data, isFull, options) => {
657
+ const {
658
+ sortVertical,
659
+ sortHorizontal,
660
+ normalizeShape: normalizeShape2,
661
+ widthIncrement = 0,
662
+ heightIncrement = 0,
663
+ precision = 0,
664
+ outputMode
665
+ } = options;
666
+ if (isFull) {
667
+ const fullData = data;
668
+ const isPredictions = outputMode === OUTPUT_MODE_PREDICTIONS;
669
+ return fullData.map((task) => {
670
+ const processOptions = {
671
+ sortVertical,
672
+ sortHorizontal,
673
+ normalizeShape: normalizeShape2,
674
+ widthIncrement,
675
+ heightIncrement,
676
+ precision
677
+ };
678
+ const allItems = [
679
+ ...task.annotations.map((a) => ({
680
+ item: a,
681
+ source: "annotations"
682
+ })),
683
+ ...(task.predictions || []).map((p) => ({
684
+ item: p,
685
+ source: "predictions"
686
+ }))
687
+ ];
688
+ const processedItems = allItems.map(
689
+ ({ item }) => (
690
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
691
+ processAnnotationOrPrediction(item, processOptions)
692
+ )
693
+ );
694
+ if (isPredictions) {
695
+ return {
696
+ ...task,
697
+ annotations: [],
698
+ predictions: processedItems,
699
+ total_annotations: 0,
700
+ total_predictions: processedItems.length
701
+ };
702
+ } else {
703
+ return {
704
+ ...task,
705
+ annotations: processedItems,
706
+ predictions: [],
707
+ total_annotations: processedItems.length,
708
+ total_predictions: 0
709
+ };
710
+ }
711
+ });
712
+ } else {
713
+ const minData = data;
714
+ return minData.map((item) => {
715
+ let ppocrAnnotations = [];
716
+ const numAnnotations = Math.max(
717
+ item.poly?.length || 0,
718
+ item.bbox?.length || 0,
719
+ item.transcription?.length || 0
720
+ );
721
+ for (let i = 0; i < numAnnotations; i++) {
722
+ let points;
723
+ if (item.poly && item.poly.length > i && item.poly[i]) {
724
+ const { points: polyPoints } = item.poly[i];
725
+ points = polyPoints;
726
+ } else if (item.bbox && item.bbox.length > i && item.bbox[i]) {
727
+ const { x, y, width, height } = item.bbox[i];
728
+ points = [
729
+ [x, y],
730
+ [x + width, y],
731
+ [x + width, y + height],
732
+ [x, y + height]
733
+ ];
416
734
  }
417
- ],
418
- annotator: 1,
419
- annotation_id: index + 1,
420
- created_at: now,
421
- updated_at: now,
422
- lead_time: 0
423
- };
424
- });
735
+ if (points) {
736
+ ppocrAnnotations.push({
737
+ transcription: item.transcription && item.transcription.length > i ? item.transcription[i] ?? "" : "",
738
+ points,
739
+ dt_score: 1
740
+ });
741
+ }
742
+ }
743
+ if (ppocrAnnotations.length > 0) {
744
+ ppocrAnnotations = enhancePPOCRLabel(ppocrAnnotations, {
745
+ sortVertical,
746
+ sortHorizontal,
747
+ normalizeShape: normalizeShape2,
748
+ widthIncrement,
749
+ heightIncrement,
750
+ precision
751
+ });
752
+ const newPoly = ppocrAnnotations.map((ann) => ({
753
+ points: ann.points
754
+ }));
755
+ const { bbox: _, ...itemWithoutBbox } = item;
756
+ return {
757
+ ...itemWithoutBbox,
758
+ poly: newPoly
759
+ };
760
+ }
761
+ return item;
762
+ });
763
+ }
425
764
  };
426
765
  }
427
766
  });
@@ -763,507 +1102,860 @@ var init_schema = __esm({
763
1102
  import_zod.default.object({
764
1103
  transcription: import_zod.default.string(),
765
1104
  points: import_zod.default.array(import_zod.default.array(import_zod.default.number())),
766
- dt_score: import_zod.default.number()
1105
+ dt_score: import_zod.default.number().optional(),
1106
+ // Detection score (from PaddleOCR)
1107
+ difficult: import_zod.default.boolean().optional()
1108
+ // Difficult flag (from PPOCRLabel tool)
767
1109
  })
768
1110
  );
769
1111
  }
770
1112
  });
771
1113
 
772
- // src/lib/sort.ts
773
- function getBoundingBoxCenter(points) {
774
- let minX = Infinity;
775
- let minY = Infinity;
776
- let maxX = -Infinity;
777
- let maxY = -Infinity;
778
- for (const [x, y] of points) {
779
- if (x !== void 0 && y !== void 0) {
780
- minX = Math.min(minX, x);
781
- minY = Math.min(minY, y);
782
- maxX = Math.max(maxX, x);
783
- maxY = Math.max(maxY, y);
784
- }
785
- }
786
- return {
787
- x: (minX + maxX) / 2,
788
- y: (minY + maxY) / 2,
789
- width: maxX - minX,
790
- height: maxY - minY
791
- };
792
- }
793
- function sortBoundingBoxes(annotations, verticalSort, horizontalSort) {
794
- if (verticalSort === SORT_VERTICAL_NONE && horizontalSort === SORT_HORIZONTAL_NONE) {
795
- return annotations;
1114
+ // src/commands/enhance-labelstudio/impl.ts
1115
+ var impl_exports = {};
1116
+ __export(impl_exports, {
1117
+ enhanceLabelStudio: () => enhanceLabelStudio
1118
+ });
1119
+ async function enhanceLabelStudio(flags, ...inputDirs) {
1120
+ const {
1121
+ outDir,
1122
+ fileName,
1123
+ backup = false,
1124
+ sortVertical = DEFAULT_SORT_VERTICAL,
1125
+ sortHorizontal = DEFAULT_SORT_HORIZONTAL,
1126
+ normalizeShape: normalizeShape2 = DEFAULT_SHAPE_NORMALIZE,
1127
+ widthIncrement = DEFAULT_WIDTH_INCREMENT,
1128
+ heightIncrement = DEFAULT_HEIGHT_INCREMENT,
1129
+ precision = DEFAULT_LABEL_STUDIO_PRECISION,
1130
+ recursive = DEFAULT_RECURSIVE,
1131
+ filePattern = DEFAULT_LABEL_STUDIO_FILE_PATTERN,
1132
+ outputMode = DEFAULT_OUTPUT_MODE
1133
+ } = flags;
1134
+ console.log(import_chalk.default.blue("Finding files..."));
1135
+ const filePaths = await findFiles(inputDirs, filePattern, recursive);
1136
+ if (filePaths.length === 0) {
1137
+ console.log(import_chalk.default.yellow("No files found matching the pattern."));
1138
+ return;
796
1139
  }
797
- const sorted = [...annotations];
798
- const isVerticalText = sorted.length > 0 && (() => {
799
- const verticalCount = sorted.filter((ann) => {
800
- const center = getBoundingBoxCenter(ann.points);
801
- return center.height > center.width * 1.5;
802
- }).length;
803
- return verticalCount > sorted.length / 2;
804
- })();
805
- if (horizontalSort === SORT_HORIZONTAL_RTL && verticalSort !== SORT_VERTICAL_NONE && isVerticalText) {
806
- const annotationsWithCenters = sorted.map((ann) => ({
807
- annotation: ann,
808
- center: getBoundingBoxCenter(ann.points)
809
- }));
810
- const columns = [];
811
- for (const item of annotationsWithCenters) {
812
- let addedToColumn = false;
813
- for (const column of columns) {
814
- const avgX = column.reduce((sum, c) => sum + c.center.x, 0) / column.length;
815
- if (Math.abs(item.center.x - avgX) < GROUPING_TOLERANCE) {
816
- column.push(item);
817
- addedToColumn = true;
818
- break;
819
- }
820
- }
821
- if (!addedToColumn) {
822
- columns.push([item]);
1140
+ console.log(import_chalk.default.blue(`Found ${filePaths.length} files to process
1141
+ `));
1142
+ for (const filePath of filePaths) {
1143
+ const file = (0, import_path2.basename)(filePath);
1144
+ console.log(import_chalk.default.gray(`Processing file: ${filePath}`));
1145
+ try {
1146
+ const fileData = await (0, import_promises2.readFile)(filePath, "utf-8");
1147
+ const labelStudioData = JSON.parse(fileData);
1148
+ const { data, isFull } = isLabelStudioFullJSON(labelStudioData);
1149
+ if (outputMode !== DEFAULT_OUTPUT_MODE && !isFull) {
1150
+ console.log(
1151
+ import_chalk.default.red(
1152
+ ` Skipping file: ${filePath}
1153
+ Error: --outputMode can only be used with Full JSON format. This file is in Min JSON format which does not support annotations/predictions distinction.`
1154
+ )
1155
+ );
1156
+ continue;
823
1157
  }
824
- }
825
- columns.sort((colA, colB) => {
826
- const avgXA = colA.reduce((sum, c) => sum + c.center.x, 0) / colA.length;
827
- const avgXB = colB.reduce((sum, c) => sum + c.center.x, 0) / colB.length;
828
- return avgXB - avgXA;
829
- });
830
- for (const column of columns) {
831
- column.sort((a, b) => {
832
- return verticalSort === SORT_VERTICAL_TOP_BOTTOM ? a.center.y - b.center.y : b.center.y - a.center.y;
1158
+ const enhanced = await enhanceLabelStudioData(data, isFull, {
1159
+ sortVertical,
1160
+ sortHorizontal,
1161
+ normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
1162
+ widthIncrement,
1163
+ heightIncrement,
1164
+ precision,
1165
+ outputMode
833
1166
  });
834
- }
835
- return columns.flat().map((item) => item.annotation);
836
- }
837
- sorted.sort((a, b) => {
838
- const centerA = getBoundingBoxCenter(a.points);
839
- const centerB = getBoundingBoxCenter(b.points);
840
- if (verticalSort !== SORT_VERTICAL_NONE) {
841
- const yDiff = verticalSort === SORT_VERTICAL_TOP_BOTTOM ? centerA.y - centerB.y : centerB.y - centerA.y;
842
- if (Math.abs(yDiff) > GROUPING_TOLERANCE) {
843
- return yDiff;
1167
+ const outputSubDir = outDir ? (() => {
1168
+ const relativePath = getRelativePathFromInputs(filePath, inputDirs);
1169
+ const relativeDir = (0, import_path2.dirname)(relativePath);
1170
+ return (0, import_path2.join)(outDir, relativeDir);
1171
+ })() : (0, import_path2.dirname)(filePath);
1172
+ await (0, import_promises2.mkdir)(outputSubDir, { recursive: true });
1173
+ const outputFileName = fileName || file;
1174
+ const outputFilePath = (0, import_path2.join)(outputSubDir, outputFileName);
1175
+ if (backup) {
1176
+ const backupPath = await backupFileIfExists(outputFilePath);
1177
+ if (backupPath) {
1178
+ console.log(import_chalk.default.gray(` Backed up to: ${backupPath}`));
1179
+ }
844
1180
  }
1181
+ await (0, import_promises2.writeFile)(
1182
+ outputFilePath,
1183
+ JSON.stringify(enhanced, null, 2),
1184
+ "utf-8"
1185
+ );
1186
+ console.log(import_chalk.default.green(`\u2713 Enhanced file saved: ${outputFilePath}`));
1187
+ } catch (error) {
1188
+ console.error(
1189
+ import_chalk.default.red(`Error processing file ${file}:`),
1190
+ error instanceof Error ? error.message : String(error)
1191
+ );
845
1192
  }
846
- if (horizontalSort !== SORT_HORIZONTAL_NONE) {
847
- return horizontalSort === SORT_HORIZONTAL_LTR ? centerA.x - centerB.x : centerB.x - centerA.x;
848
- }
849
- return 0;
850
- });
851
- return sorted;
1193
+ }
1194
+ console.log(import_chalk.default.green("\n\u2713 Enhancement complete!"));
852
1195
  }
853
- var GROUPING_TOLERANCE;
854
- var init_sort = __esm({
855
- "src/lib/sort.ts"() {
1196
+ var import_promises2, import_path2, import_chalk, isLabelStudioFullJSON;
1197
+ var init_impl = __esm({
1198
+ "src/commands/enhance-labelstudio/impl.ts"() {
856
1199
  "use strict";
857
1200
  init_cjs_shims();
1201
+ import_promises2 = require("fs/promises");
1202
+ import_path2 = require("path");
1203
+ import_chalk = __toESM(require("chalk"), 1);
858
1204
  init_constants();
859
- GROUPING_TOLERANCE = 50;
1205
+ init_backup_utils();
1206
+ init_file_utils();
1207
+ init_label_studio();
1208
+ init_schema();
1209
+ isLabelStudioFullJSON = (data) => {
1210
+ const parsedFull = FullOCRLabelStudioSchema.safeParse(data);
1211
+ if (parsedFull.success) {
1212
+ return { isFull: true, data: parsedFull.data };
1213
+ }
1214
+ if (!Array.isArray(data) && typeof data === "object" && data !== null) {
1215
+ const parsedSingleFull = FullOCRLabelStudioSchema.safeParse([data]);
1216
+ if (parsedSingleFull.success) {
1217
+ return { isFull: true, data: parsedSingleFull.data };
1218
+ }
1219
+ }
1220
+ const parsedMin = MinOCRLabelStudioSchema.safeParse(data);
1221
+ if (parsedMin.success) {
1222
+ return { isFull: false, data: parsedMin.data };
1223
+ }
1224
+ throw new Error("Input data is not valid Label Studio JSON format.");
1225
+ };
860
1226
  }
861
1227
  });
862
1228
 
863
- // src/commands/toLabelStudio/impl.ts
864
- var impl_exports = {};
865
- __export(impl_exports, {
866
- convertToLabelStudio: () => convertToLabelStudio
1229
+ // src/commands/enhance-ppocr/impl.ts
1230
+ var impl_exports2 = {};
1231
+ __export(impl_exports2, {
1232
+ enhancePPOCR: () => enhancePPOCR
867
1233
  });
868
- async function convertToLabelStudio(flags, ...inputDirs) {
1234
+ async function enhancePPOCR(flags, ...inputDirs) {
869
1235
  const {
870
- outDir = OUTPUT_BASE_DIR,
871
- defaultLabelName = DEFAULT_LABEL_NAME,
872
- toFullJson = DEFAULT_LABEL_STUDIO_FULL_JSON,
873
- createFilePerImage = DEFAULT_CREATE_FILE_PER_IMAGE,
874
- createFileListForServing = DEFAULT_CREATE_FILE_LIST_FOR_SERVING,
875
- fileListName = DEFAULT_FILE_LIST_NAME,
876
- baseServerUrl = DEFAULT_BASE_SERVER_URL,
1236
+ outDir,
1237
+ fileName,
1238
+ backup = false,
877
1239
  sortVertical = DEFAULT_SORT_VERTICAL,
878
1240
  sortHorizontal = DEFAULT_SORT_HORIZONTAL,
879
1241
  normalizeShape: normalizeShape2 = DEFAULT_SHAPE_NORMALIZE,
880
1242
  widthIncrement = DEFAULT_WIDTH_INCREMENT,
881
1243
  heightIncrement = DEFAULT_HEIGHT_INCREMENT,
882
- precision = DEFAULT_LABEL_STUDIO_PRECISION
1244
+ precision = DEFAULT_PPOCR_PRECISION,
1245
+ recursive = DEFAULT_RECURSIVE,
1246
+ filePattern = DEFAULT_PPOCR_FILE_PATTERN
883
1247
  } = flags;
884
- const newBaseServerUrl = baseServerUrl.replace(/\/+$/, "") + (baseServerUrl === "" ? "" : "/");
885
- await (0, import_promises.mkdir)(outDir, { recursive: true });
886
- for (const inputDir of inputDirs) {
887
- console.log(import_chalk.default.blue(`Processing input directory: ${inputDir}`));
888
- const files = await (0, import_promises.readdir)(inputDir);
889
- for (const file of files) {
890
- if (!file.endsWith(".txt")) {
891
- continue;
892
- }
893
- const filePath = (0, import_path.join)(inputDir, file);
894
- console.log(import_chalk.default.gray(`Processing file: ${file}`));
895
- try {
896
- const fileData = await (0, import_promises.readFile)(filePath, "utf-8");
897
- const lines = fileData.trim().split("\n");
898
- const imageDataMap = /* @__PURE__ */ new Map();
899
- for (const line of lines) {
900
- const parts = line.split(" ");
901
- if (parts.length !== 2) {
902
- throw new Error(`Invalid PPOCRLabelV2 format in line: ${line}`);
903
- }
904
- const [imagePath, annotationsStr] = parts;
905
- const annotations = JSON.parse(annotationsStr);
906
- PPOCRLabelSchema.parse(annotations);
907
- imageDataMap.set(imagePath, annotations);
908
- }
909
- const allLabelStudioData = [];
910
- const fileList = [];
911
- let taskId = 1;
912
- for (const [imagePath, ppocrData] of imageDataMap.entries()) {
913
- const sortedPpocrData = sortBoundingBoxes(
914
- ppocrData,
915
- sortVertical,
916
- sortHorizontal
917
- );
918
- const finalImagePath = createFileListForServing ? encodeURI(`${newBaseServerUrl}${imagePath}`) : imagePath;
919
- const labelStudioData = await ppocrToLabelStudio(sortedPpocrData, {
920
- toFullJson,
921
- imagePath,
922
- baseServerUrl: newBaseServerUrl,
923
- inputDir,
924
- taskId,
925
- labelName: defaultLabelName,
926
- normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
927
- widthIncrement,
928
- heightIncrement,
929
- precision
930
- });
931
- if (toFullJson) {
932
- allLabelStudioData.push(labelStudioData[0]);
933
- } else {
934
- allLabelStudioData.push(...labelStudioData);
935
- }
936
- if (createFilePerImage) {
937
- const imageBaseName = imagePath.replace(/\//g, "_").replace(/\.[^.]+$/, "");
938
- const individualOutputPath = (0, import_path.join)(
939
- outDir,
940
- `${imageBaseName}_${toFullJson ? "full" : "min"}.json`
941
- );
942
- await (0, import_promises.writeFile)(
943
- individualOutputPath,
944
- JSON.stringify(
945
- toFullJson ? labelStudioData[0] : labelStudioData,
946
- null,
947
- 2
948
- ),
949
- "utf-8"
950
- );
951
- console.log(
952
- import_chalk.default.gray(
953
- ` \u2713 Created individual file: ${individualOutputPath}`
954
- )
955
- );
956
- }
957
- if (createFileListForServing) {
958
- fileList.push(finalImagePath);
959
- }
960
- taskId++;
1248
+ console.log(import_chalk2.default.blue("Finding files..."));
1249
+ const filePaths = await findFiles(inputDirs, filePattern, recursive);
1250
+ if (filePaths.length === 0) {
1251
+ console.log(import_chalk2.default.yellow("No files found matching the pattern."));
1252
+ return;
1253
+ }
1254
+ console.log(import_chalk2.default.blue(`Found ${filePaths.length} files to process
1255
+ `));
1256
+ for (const filePath of filePaths) {
1257
+ const file = (0, import_path3.basename)(filePath);
1258
+ console.log(import_chalk2.default.gray(`Processing file: ${filePath}`));
1259
+ try {
1260
+ const fileData = await (0, import_promises3.readFile)(filePath, "utf-8");
1261
+ const lines = fileData.trim().split("\n");
1262
+ const enhancedLines = [];
1263
+ for (const line of lines) {
1264
+ const parts = line.split(" ");
1265
+ if (parts.length !== 2) {
1266
+ throw new Error(`Invalid PPOCRLabelV2 format in line: ${line}`);
961
1267
  }
962
- const baseName = file.replace(".txt", "");
963
- const outputPath = (0, import_path.join)(
964
- outDir,
965
- `${baseName}_${toFullJson ? "full" : "min"}.json`
966
- );
967
- await (0, import_promises.writeFile)(
968
- outputPath,
969
- JSON.stringify(allLabelStudioData, null, 2),
970
- "utf-8"
971
- );
972
- console.log(import_chalk.default.green(`\u2713 Converted ${file} -> ${outputPath}`));
973
- if (createFileListForServing && fileList.length > 0) {
974
- const fileListPath = (0, import_path.join)(outDir, fileListName);
975
- await (0, import_promises.writeFile)(fileListPath, fileList.join("\n"), "utf-8");
976
- console.log(
977
- import_chalk.default.green(
978
- `\u2713 Created file list: ${fileListPath} (${fileList.length} files)`
979
- )
980
- );
1268
+ const [imagePath, annotationsStr] = parts;
1269
+ const annotations = JSON.parse(annotationsStr);
1270
+ PPOCRLabelSchema.parse(annotations);
1271
+ const enhanced = enhancePPOCRLabel(annotations, {
1272
+ sortVertical,
1273
+ sortHorizontal,
1274
+ normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
1275
+ widthIncrement,
1276
+ heightIncrement,
1277
+ precision
1278
+ });
1279
+ PPOCRLabelSchema.parse(enhanced);
1280
+ const jsonArray = JSON.stringify(enhanced);
1281
+ enhancedLines.push(`${imagePath} ${jsonArray}`);
1282
+ }
1283
+ const outputSubDir = outDir ? (() => {
1284
+ const relativePath = getRelativePathFromInputs(filePath, inputDirs);
1285
+ const relativeDir = (0, import_path3.dirname)(relativePath);
1286
+ return (0, import_path3.join)(outDir, relativeDir);
1287
+ })() : (0, import_path3.dirname)(filePath);
1288
+ await (0, import_promises3.mkdir)(outputSubDir, { recursive: true });
1289
+ const outputFileName = fileName || file;
1290
+ const outputFilePath = (0, import_path3.join)(outputSubDir, outputFileName);
1291
+ if (backup) {
1292
+ const backupPath = await backupFileIfExists(outputFilePath);
1293
+ if (backupPath) {
1294
+ console.log(import_chalk2.default.gray(` Backed up to: ${backupPath}`));
981
1295
  }
982
- } catch (error) {
983
- console.error(
984
- import_chalk.default.red(`\u2717 Failed to process ${file}:`),
985
- error instanceof Error ? error.message : error
986
- );
987
1296
  }
1297
+ await (0, import_promises3.writeFile)(outputFilePath, enhancedLines.join("\n"), "utf-8");
1298
+ console.log(import_chalk2.default.green(`\u2713 Enhanced file saved: ${outputFilePath}`));
1299
+ } catch (error) {
1300
+ console.error(
1301
+ import_chalk2.default.red(`Error processing file ${file}:`),
1302
+ error instanceof Error ? error.message : String(error)
1303
+ );
988
1304
  }
989
1305
  }
990
- console.log(import_chalk.default.green("\n\u2713 Conversion completed!"));
1306
+ console.log(import_chalk2.default.green("\n\u2713 Enhancement complete!"));
991
1307
  }
992
- var import_promises, import_path, import_chalk;
993
- var init_impl = __esm({
994
- "src/commands/toLabelStudio/impl.ts"() {
1308
+ var import_promises3, import_path3, import_chalk2;
1309
+ var init_impl2 = __esm({
1310
+ "src/commands/enhance-ppocr/impl.ts"() {
995
1311
  "use strict";
996
1312
  init_cjs_shims();
997
- import_promises = require("fs/promises");
998
- import_path = require("path");
999
- import_chalk = __toESM(require("chalk"), 1);
1313
+ import_promises3 = require("fs/promises");
1314
+ import_path3 = require("path");
1315
+ import_chalk2 = __toESM(require("chalk"), 1);
1000
1316
  init_constants();
1001
- init_ppocr_label();
1317
+ init_backup_utils();
1318
+ init_enhance();
1319
+ init_file_utils();
1002
1320
  init_schema();
1003
- init_sort();
1004
1321
  }
1005
1322
  });
1006
1323
 
1007
- // src/lib/label-studio.ts
1008
- var turf, labelStudioToPPOCR, minLabelStudioToPPOCR;
1009
- var init_label_studio = __esm({
1010
- "src/lib/label-studio.ts"() {
1324
+ // src/lib/ppocr-label.ts
1325
+ var import_node_crypto, import_node_fs, import_node_path, import_image_size, ppocrToLabelStudio, ppocrToFullLabelStudio, ppocrToMinLabelStudio;
1326
+ var init_ppocr_label = __esm({
1327
+ "src/lib/ppocr-label.ts"() {
1011
1328
  "use strict";
1012
1329
  init_cjs_shims();
1013
- turf = __toESM(require("@turf/turf"), 1);
1330
+ import_node_crypto = require("crypto");
1331
+ import_node_fs = require("fs");
1332
+ import_node_path = require("path");
1333
+ import_image_size = __toESM(require("image-size"), 1);
1334
+ init_constants();
1014
1335
  init_geometry();
1015
- labelStudioToPPOCR = async (data, options) => {
1336
+ ppocrToLabelStudio = async (data, options) => {
1016
1337
  const {
1017
- baseImageDir,
1338
+ imagePath,
1339
+ baseServerUrl,
1340
+ inputDir,
1341
+ relativeDir,
1342
+ toFullJson = true,
1343
+ taskId = 1,
1344
+ labelName = DEFAULT_LABEL_NAME,
1018
1345
  normalizeShape: normalizeShape2,
1019
1346
  widthIncrement = 0,
1020
1347
  heightIncrement = 0,
1021
- precision = 0
1348
+ precision = DEFAULT_LABEL_STUDIO_PRECISION,
1349
+ outputMode = DEFAULT_OUTPUT_MODE
1022
1350
  } = options || {};
1023
- const resultMap = /* @__PURE__ */ new Map();
1024
- for (const task of data) {
1025
- let imagePath = task.file_upload || "";
1026
- if (task.data.ocr) {
1027
- const urlPath = task.data.ocr.replace(/^https?:\/\/[^/]+\//, "");
1028
- imagePath = decodeURIComponent(urlPath);
1351
+ if (toFullJson) {
1352
+ return ppocrToFullLabelStudio(
1353
+ data,
1354
+ imagePath,
1355
+ baseServerUrl,
1356
+ inputDir,
1357
+ taskId,
1358
+ labelName,
1359
+ normalizeShape2,
1360
+ widthIncrement,
1361
+ heightIncrement,
1362
+ precision,
1363
+ relativeDir,
1364
+ outputMode
1365
+ );
1366
+ } else {
1367
+ return ppocrToMinLabelStudio(
1368
+ data,
1369
+ imagePath,
1370
+ baseServerUrl,
1371
+ inputDir,
1372
+ labelName,
1373
+ normalizeShape2,
1374
+ widthIncrement,
1375
+ heightIncrement,
1376
+ precision,
1377
+ relativeDir
1378
+ );
1379
+ }
1380
+ };
1381
+ ppocrToFullLabelStudio = (data, imagePath, baseServerUrl, inputDir, taskId = 1, labelName = DEFAULT_LABEL_NAME, normalizeShape2, widthIncrement = 0, heightIncrement = 0, precision = DEFAULT_LABEL_STUDIO_PRECISION, relativeDir, outputMode = DEFAULT_OUTPUT_MODE) => {
1382
+ const newBaseServerUrl = baseServerUrl.replace(/\/+$/, "") + (baseServerUrl === "" ? "" : "/");
1383
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1384
+ let original_width = 1920;
1385
+ let original_height = 1080;
1386
+ let resolvedImagePath;
1387
+ if (inputDir) {
1388
+ const lastDirComponent = (0, import_node_path.basename)(inputDir);
1389
+ const imagePathParts = imagePath.split("/");
1390
+ if (imagePathParts[0] === lastDirComponent && imagePathParts.length > 1) {
1391
+ resolvedImagePath = (0, import_node_path.join)(inputDir, imagePathParts.slice(1).join("/"));
1392
+ } else {
1393
+ resolvedImagePath = (0, import_node_path.join)(inputDir, imagePath);
1029
1394
  }
1030
- if (baseImageDir) {
1031
- imagePath = `${baseImageDir}/${task.file_upload || imagePath.split("/").pop() || imagePath}`;
1395
+ } else {
1396
+ resolvedImagePath = imagePath;
1397
+ }
1398
+ if ((0, import_node_fs.existsSync)(resolvedImagePath)) {
1399
+ try {
1400
+ const buffer = (0, import_node_fs.readFileSync)(resolvedImagePath);
1401
+ const dimensions = (0, import_image_size.default)(buffer);
1402
+ if (dimensions.width && dimensions.height) {
1403
+ original_width = dimensions.width;
1404
+ original_height = dimensions.height;
1405
+ } else {
1406
+ console.warn(
1407
+ `Warning: Failed to read dimensions from ${resolvedImagePath}, using defaults`
1408
+ );
1409
+ }
1410
+ } catch (error) {
1411
+ console.warn(
1412
+ `Warning: Error reading image ${resolvedImagePath}, using defaults:`,
1413
+ error instanceof Error ? error.message : error
1414
+ );
1032
1415
  }
1033
- const imageAnnotations = [];
1034
- for (const annotation of task.annotations) {
1035
- const groupedById = /* @__PURE__ */ new Map();
1036
- for (const resultItem of annotation.result) {
1037
- const { id } = resultItem;
1038
- if (!groupedById.has(id)) {
1039
- groupedById.set(id, []);
1040
- }
1041
- groupedById.get(id).push(resultItem);
1416
+ } else {
1417
+ console.warn(
1418
+ `Warning: Image file not found: ${resolvedImagePath}, using default dimensions`
1419
+ );
1420
+ }
1421
+ const fileName = imagePath.split("/").pop() || imagePath;
1422
+ let normalizedImagePath = imagePath;
1423
+ if (inputDir && relativeDir) {
1424
+ const lastDirComponent = (0, import_node_path.basename)(inputDir);
1425
+ const imagePathParts = imagePath.split("/");
1426
+ if (imagePathParts[0] === lastDirComponent && imagePathParts.length > 1) {
1427
+ normalizedImagePath = imagePathParts.slice(1).join("/");
1428
+ }
1429
+ }
1430
+ const isPredictions = outputMode === OUTPUT_MODE_PREDICTIONS;
1431
+ const resultItems = data.map((item) => {
1432
+ let { points } = item;
1433
+ points = transformPoints(points, {
1434
+ normalizeShape: normalizeShape2,
1435
+ widthIncrement,
1436
+ heightIncrement
1437
+ });
1438
+ const annotationId = (0, import_node_crypto.randomUUID)().slice(0, 10);
1439
+ const polygonPoints = points.map(([x, y]) => [
1440
+ roundToPrecision((x ?? 0) / original_width * 100, precision),
1441
+ roundToPrecision((y ?? 0) / original_height * 100, precision)
1442
+ ]);
1443
+ const scoreField = isPredictions && item.dt_score !== void 0 ? { score: item.dt_score } : {};
1444
+ return [
1445
+ // 1. Polygon geometry only
1446
+ {
1447
+ original_width,
1448
+ original_height,
1449
+ image_rotation: 0,
1450
+ value: {
1451
+ points: polygonPoints,
1452
+ closed: true
1453
+ },
1454
+ id: annotationId,
1455
+ from_name: "poly",
1456
+ to_name: "image",
1457
+ type: "polygon",
1458
+ origin: "manual",
1459
+ ...scoreField
1460
+ },
1461
+ // 2. Labels with polygon geometry
1462
+ {
1463
+ original_width,
1464
+ original_height,
1465
+ image_rotation: 0,
1466
+ value: {
1467
+ points: polygonPoints,
1468
+ closed: true,
1469
+ labels: [labelName]
1470
+ },
1471
+ id: annotationId,
1472
+ from_name: "label",
1473
+ to_name: "image",
1474
+ type: "labels",
1475
+ origin: "manual",
1476
+ ...scoreField
1477
+ },
1478
+ // 3. Textarea with polygon geometry and text
1479
+ {
1480
+ original_width,
1481
+ original_height,
1482
+ image_rotation: 0,
1483
+ value: {
1484
+ points: polygonPoints,
1485
+ closed: true,
1486
+ text: [item.transcription]
1487
+ },
1488
+ id: annotationId,
1489
+ from_name: "transcription",
1490
+ to_name: "image",
1491
+ type: "textarea",
1492
+ origin: "manual",
1493
+ ...scoreField
1042
1494
  }
1043
- for (const [_, resultItems] of groupedById) {
1044
- let points;
1045
- let transcription = "";
1046
- for (const resultItem of resultItems) {
1047
- if ("points" in resultItem.value && resultItem.value.points) {
1048
- const { points: valuePoints } = resultItem.value;
1049
- const { original_width, original_height } = resultItem;
1050
- points = valuePoints.map(([x, y]) => [
1051
- (x ?? 0) * original_width / 100,
1052
- (y ?? 0) * original_height / 100
1053
- ]);
1054
- } else if ("x" in resultItem.value && "y" in resultItem.value && "width" in resultItem.value && "height" in resultItem.value) {
1055
- const { x, y, width, height } = resultItem.value;
1056
- const { original_width, original_height } = resultItem;
1057
- const absX = x * original_width / 100;
1058
- const absY = y * original_height / 100;
1059
- const absWidth = width * original_width / 100;
1060
- const absHeight = height * original_height / 100;
1061
- points = [
1062
- [absX, absY],
1063
- [absX + absWidth, absY],
1064
- [absX + absWidth, absY + absHeight],
1065
- [absX, absY + absHeight]
1066
- ];
1067
- }
1068
- if ("text" in resultItem.value && Array.isArray(resultItem.value.text)) {
1069
- transcription = resultItem.value.text[0] || "";
1070
- }
1495
+ ];
1496
+ }).flat();
1497
+ const result = [
1498
+ {
1499
+ id: taskId,
1500
+ annotations: isPredictions ? [] : [
1501
+ {
1502
+ id: taskId,
1503
+ completed_by: 1,
1504
+ result: resultItems,
1505
+ was_cancelled: false,
1506
+ ground_truth: false,
1507
+ created_at: now,
1508
+ updated_at: now,
1509
+ draft_created_at: now,
1510
+ lead_time: 0,
1511
+ prediction: {},
1512
+ result_count: data.length * 3,
1513
+ unique_id: (0, import_node_crypto.randomUUID)(),
1514
+ import_id: null,
1515
+ last_action: null,
1516
+ bulk_created: false,
1517
+ task: taskId,
1518
+ project: 1,
1519
+ updated_by: 1,
1520
+ parent_prediction: null,
1521
+ parent_annotation: null,
1522
+ last_created_by: null
1071
1523
  }
1072
- if (points && points.length > 0) {
1073
- points = transformPoints(points, {
1074
- normalizeShape: normalizeShape2,
1075
- widthIncrement,
1076
- heightIncrement
1077
- });
1078
- points = roundPoints(points, precision);
1079
- let dt_score = 1;
1080
- try {
1081
- const firstPoint = points[0];
1082
- if (firstPoint) {
1083
- const polygon2 = turf.polygon([points.concat([firstPoint])]);
1084
- const area2 = turf.area(polygon2);
1085
- dt_score = Math.min(1, Math.max(0.5, area2 / 1e4));
1086
- }
1087
- } catch {
1088
- dt_score = 0.8;
1089
- }
1090
- imageAnnotations.push({
1091
- transcription,
1092
- points,
1093
- dt_score
1094
- });
1524
+ ],
1525
+ file_upload: fileName,
1526
+ drafts: [],
1527
+ predictions: isPredictions ? [
1528
+ {
1529
+ model_version: "ppocr-v1",
1530
+ result: resultItems,
1531
+ created_at: now,
1532
+ task: taskId,
1533
+ project: 1
1095
1534
  }
1096
- }
1535
+ ] : [],
1536
+ data: {
1537
+ ocr: relativeDir ? `${newBaseServerUrl}${relativeDir}/${normalizedImagePath}` : `${newBaseServerUrl}${normalizedImagePath}`
1538
+ },
1539
+ meta: {},
1540
+ created_at: now,
1541
+ updated_at: now,
1542
+ allow_skip: false,
1543
+ inner_id: taskId,
1544
+ total_annotations: isPredictions ? 0 : 1,
1545
+ cancelled_annotations: 0,
1546
+ total_predictions: isPredictions ? 1 : 0,
1547
+ comment_count: 0,
1548
+ unresolved_comment_count: 0,
1549
+ last_comment_updated_at: null,
1550
+ project: 1,
1551
+ updated_by: 1,
1552
+ comment_authors: []
1097
1553
  }
1098
- if (imageAnnotations.length > 0) {
1099
- resultMap.set(imagePath, imageAnnotations);
1554
+ ];
1555
+ return result;
1556
+ };
1557
+ ppocrToMinLabelStudio = (data, imagePath, baseServerUrl, inputDir, labelName = "text", normalizeShape2, widthIncrement = 0, heightIncrement = 0, precision = DEFAULT_LABEL_STUDIO_PRECISION, relativeDir) => {
1558
+ const newBaseServerUrl = baseServerUrl.replace(/\/+$/, "") + (baseServerUrl === "" ? "" : "/");
1559
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1560
+ let original_width = 1920;
1561
+ let original_height = 1080;
1562
+ let resolvedImagePath;
1563
+ if (inputDir) {
1564
+ const lastDirComponent = (0, import_node_path.basename)(inputDir);
1565
+ const imagePathParts = imagePath.split("/");
1566
+ if (imagePathParts[0] === lastDirComponent && imagePathParts.length > 1) {
1567
+ resolvedImagePath = (0, import_node_path.join)(inputDir, imagePathParts.slice(1).join("/"));
1568
+ } else {
1569
+ resolvedImagePath = (0, import_node_path.join)(inputDir, imagePath);
1100
1570
  }
1571
+ } else {
1572
+ resolvedImagePath = imagePath;
1101
1573
  }
1102
- return resultMap;
1103
- };
1104
- minLabelStudioToPPOCR = async (data, options) => {
1105
- const {
1106
- baseImageDir,
1107
- normalizeShape: normalizeShape2,
1108
- widthIncrement = 0,
1109
- heightIncrement = 0,
1110
- precision = 0
1111
- } = options || {};
1112
- const resultMap = /* @__PURE__ */ new Map();
1113
- for (const item of data) {
1114
- let imagePath = item.ocr || "";
1115
- if (imagePath) {
1116
- imagePath = decodeURIComponent(
1117
- imagePath.replace(/^https?:\/\/[^/]+\//, "")
1574
+ if ((0, import_node_fs.existsSync)(resolvedImagePath)) {
1575
+ try {
1576
+ const buffer = (0, import_node_fs.readFileSync)(resolvedImagePath);
1577
+ const dimensions = (0, import_image_size.default)(buffer);
1578
+ if (dimensions.width && dimensions.height) {
1579
+ original_width = dimensions.width;
1580
+ original_height = dimensions.height;
1581
+ } else {
1582
+ console.warn(
1583
+ `Warning: Failed to read dimensions from ${resolvedImagePath}, using defaults`
1584
+ );
1585
+ }
1586
+ } catch (error) {
1587
+ console.warn(
1588
+ `Warning: Error reading image ${resolvedImagePath}, using defaults:`,
1589
+ error instanceof Error ? error.message : error
1118
1590
  );
1119
1591
  }
1120
- if (baseImageDir) {
1121
- imagePath = `${baseImageDir}/${imagePath.split("/").pop() || imagePath}`;
1592
+ } else {
1593
+ console.warn(
1594
+ `Warning: Image file not found: ${resolvedImagePath}, using default dimensions`
1595
+ );
1596
+ }
1597
+ let normalizedImagePath = imagePath;
1598
+ if (inputDir && relativeDir) {
1599
+ const lastDirComponent = (0, import_node_path.basename)(inputDir);
1600
+ const imagePathParts = imagePath.split("/");
1601
+ if (imagePathParts[0] === lastDirComponent && imagePathParts.length > 1) {
1602
+ normalizedImagePath = imagePathParts.slice(1).join("/");
1122
1603
  }
1123
- const numAnnotations = Math.max(
1124
- item.poly?.length || 0,
1125
- item.bbox?.length || 0,
1126
- item.transcription?.length || 0
1604
+ }
1605
+ return data.map((item, index) => {
1606
+ let { points } = item;
1607
+ points = transformPoints(points, {
1608
+ normalizeShape: normalizeShape2,
1609
+ widthIncrement,
1610
+ heightIncrement
1611
+ });
1612
+ const roundedPoints = points.map(
1613
+ ([x, y]) => [
1614
+ roundToPrecision(x ?? 0, precision),
1615
+ roundToPrecision(y ?? 0, precision)
1616
+ ]
1127
1617
  );
1128
- for (let i = 0; i < numAnnotations; i++) {
1129
- let points;
1130
- if (item.poly && item.poly.length > i && item.poly[i]) {
1131
- const poly = item.poly[i];
1132
- if (poly) {
1133
- const { points: polyPoints } = poly;
1134
- points = polyPoints;
1618
+ let minX = Infinity;
1619
+ let minY = Infinity;
1620
+ let maxX = -Infinity;
1621
+ let maxY = -Infinity;
1622
+ for (const point of roundedPoints) {
1623
+ const [x, y] = point;
1624
+ if (x !== void 0 && y !== void 0) {
1625
+ minX = Math.min(minX, x);
1626
+ minY = Math.min(minY, y);
1627
+ maxX = Math.max(maxX, x);
1628
+ maxY = Math.max(maxY, y);
1629
+ }
1630
+ }
1631
+ const width = maxX - minX;
1632
+ const height = maxY - minY;
1633
+ return {
1634
+ ocr: relativeDir ? encodeURI(`${newBaseServerUrl}${relativeDir}/${normalizedImagePath}`) : encodeURI(`${newBaseServerUrl}${normalizedImagePath}`),
1635
+ id: index + 1,
1636
+ bbox: [
1637
+ {
1638
+ x: minX,
1639
+ y: minY,
1640
+ width,
1641
+ height,
1642
+ rotation: 0,
1643
+ original_width,
1644
+ original_height
1135
1645
  }
1136
- } else if (item.bbox && item.bbox.length > i && item.bbox[i]) {
1137
- const bbox = item.bbox[i];
1138
- if (bbox) {
1139
- const { x, y, width, height } = bbox;
1140
- points = [
1141
- [x, y],
1142
- [x + width, y],
1143
- [x + width, y + height],
1144
- [x, y + height]
1145
- ];
1646
+ ],
1647
+ label: [
1648
+ {
1649
+ points: roundedPoints,
1650
+ closed: true,
1651
+ labels: [labelName],
1652
+ original_width,
1653
+ original_height
1146
1654
  }
1147
- }
1148
- if (!points) {
1149
- continue;
1150
- }
1151
- points = transformPoints(points, {
1152
- normalizeShape: normalizeShape2,
1153
- widthIncrement,
1154
- heightIncrement
1655
+ ],
1656
+ transcription: [item.transcription],
1657
+ poly: [
1658
+ {
1659
+ points: roundedPoints,
1660
+ closed: true,
1661
+ original_width,
1662
+ original_height
1663
+ }
1664
+ ],
1665
+ annotator: 1,
1666
+ annotation_id: index + 1,
1667
+ created_at: now,
1668
+ updated_at: now,
1669
+ lead_time: 0
1670
+ };
1671
+ });
1672
+ };
1673
+ }
1674
+ });
1675
+
1676
+ // src/commands/toLabelStudio/impl.ts
1677
+ var impl_exports3 = {};
1678
+ __export(impl_exports3, {
1679
+ convertToLabelStudio: () => convertToLabelStudio
1680
+ });
1681
+ async function convertToLabelStudio(flags, ...inputDirs) {
1682
+ const {
1683
+ outDir,
1684
+ fileName,
1685
+ backup = false,
1686
+ defaultLabelName = DEFAULT_LABEL_NAME,
1687
+ toFullJson = DEFAULT_LABEL_STUDIO_FULL_JSON,
1688
+ createFilePerImage = DEFAULT_CREATE_FILE_PER_IMAGE,
1689
+ createFileListForServing = DEFAULT_CREATE_FILE_LIST_FOR_SERVING,
1690
+ fileListName = DEFAULT_FILE_LIST_NAME,
1691
+ baseServerUrl = DEFAULT_BASE_SERVER_URL,
1692
+ sortVertical = DEFAULT_SORT_VERTICAL,
1693
+ sortHorizontal = DEFAULT_SORT_HORIZONTAL,
1694
+ normalizeShape: normalizeShape2 = DEFAULT_SHAPE_NORMALIZE,
1695
+ widthIncrement = DEFAULT_WIDTH_INCREMENT,
1696
+ heightIncrement = DEFAULT_HEIGHT_INCREMENT,
1697
+ precision = DEFAULT_LABEL_STUDIO_PRECISION,
1698
+ recursive = DEFAULT_RECURSIVE,
1699
+ filePattern = DEFAULT_PPOCR_FILE_PATTERN,
1700
+ outputMode = DEFAULT_OUTPUT_MODE
1701
+ } = flags;
1702
+ if (outputMode !== DEFAULT_OUTPUT_MODE && !toFullJson) {
1703
+ console.log(
1704
+ import_chalk3.default.red(
1705
+ "Error: --outputMode can only be used with --toFullJson (Full JSON format). Min JSON format does not support annotations/predictions distinction."
1706
+ )
1707
+ );
1708
+ return;
1709
+ }
1710
+ const newBaseServerUrl = baseServerUrl.replace(/\/+$/, "") + (baseServerUrl === "" ? "" : "");
1711
+ console.log(import_chalk3.default.blue("Finding files..."));
1712
+ const filePaths = await findFiles(inputDirs, filePattern, recursive);
1713
+ if (filePaths.length === 0) {
1714
+ console.log(import_chalk3.default.yellow("No files found matching the pattern."));
1715
+ return;
1716
+ }
1717
+ console.log(import_chalk3.default.blue(`Found ${filePaths.length} files to process
1718
+ `));
1719
+ let fileListPath = null;
1720
+ if (createFileListForServing && outDir) {
1721
+ fileListPath = (0, import_path4.join)(outDir, fileListName);
1722
+ await (0, import_promises4.mkdir)(outDir, { recursive: true });
1723
+ await (0, import_promises4.writeFile)(fileListPath, "", "utf-8");
1724
+ }
1725
+ for (const filePath of filePaths) {
1726
+ const file = (0, import_path4.basename)(filePath);
1727
+ const baseImageDir = (0, import_path4.dirname)(filePath);
1728
+ const relativePath = getRelativePathFromInputs(filePath, inputDirs);
1729
+ const relativeDir = (0, import_path4.dirname)(relativePath);
1730
+ console.log(import_chalk3.default.gray(`Processing file: ${filePath}`));
1731
+ try {
1732
+ const fileData = await (0, import_promises4.readFile)(filePath, "utf-8");
1733
+ const trimmedData = fileData.trim();
1734
+ if (trimmedData === "") {
1735
+ console.log(import_chalk3.default.yellow(` Skipping empty file: ${filePath}`));
1736
+ continue;
1737
+ }
1738
+ const lines = trimmedData.split("\n");
1739
+ const imageDataMap = /* @__PURE__ */ new Map();
1740
+ for (const line of lines) {
1741
+ if (line.trim() === "") {
1742
+ continue;
1743
+ }
1744
+ const parts = line.split(" ");
1745
+ if (parts.length !== 2) {
1746
+ throw new Error(`Invalid PPOCRLabelV2 format in line: ${line}`);
1747
+ }
1748
+ const [imagePath, annotationsStr] = parts;
1749
+ const annotations = JSON.parse(annotationsStr);
1750
+ PPOCRLabelSchema.parse(annotations);
1751
+ imageDataMap.set(imagePath, annotations);
1752
+ }
1753
+ if (imageDataMap.size === 0) {
1754
+ console.log(
1755
+ import_chalk3.default.yellow(` Skipping file with no valid data: ${filePath}`)
1756
+ );
1757
+ continue;
1758
+ }
1759
+ const allLabelStudioData = [];
1760
+ let taskId = 1;
1761
+ for (const [imagePath, ppocrData] of imageDataMap.entries()) {
1762
+ const sortedPpocrData = sortBoundingBoxes(
1763
+ ppocrData,
1764
+ sortVertical,
1765
+ sortHorizontal
1766
+ );
1767
+ const finalImagePath = createFileListForServing ? encodeURI(`${newBaseServerUrl}${relativeDir}/${imagePath}`) : imagePath;
1768
+ const labelStudioData = await ppocrToLabelStudio(sortedPpocrData, {
1769
+ toFullJson,
1770
+ imagePath,
1771
+ baseServerUrl: newBaseServerUrl,
1772
+ inputDir: baseImageDir,
1773
+ relativeDir,
1774
+ taskId,
1775
+ labelName: defaultLabelName,
1776
+ normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
1777
+ widthIncrement,
1778
+ heightIncrement,
1779
+ precision,
1780
+ outputMode
1781
+ });
1782
+ if (toFullJson) {
1783
+ allLabelStudioData.push(labelStudioData[0]);
1784
+ } else {
1785
+ allLabelStudioData.push(...labelStudioData);
1786
+ }
1787
+ if (createFilePerImage) {
1788
+ const imageBaseName = imagePath.replace(/\//g, "_").replace(/\.[^.]+$/, "");
1789
+ const outputSubDir2 = outDir ? (0, import_path4.join)(outDir, relativeDir) : (0, import_path4.dirname)(filePath);
1790
+ await (0, import_promises4.mkdir)(outputSubDir2, { recursive: true });
1791
+ const individualOutputPath = (0, import_path4.join)(
1792
+ outputSubDir2,
1793
+ `${imageBaseName}_${toFullJson ? "full" : "min"}.json`
1794
+ );
1795
+ await (0, import_promises4.writeFile)(
1796
+ individualOutputPath,
1797
+ JSON.stringify(
1798
+ toFullJson ? labelStudioData[0] : labelStudioData,
1799
+ null,
1800
+ 2
1801
+ ),
1802
+ "utf-8"
1803
+ );
1804
+ console.log(
1805
+ import_chalk3.default.gray(` \u2713 Created individual file: ${individualOutputPath}`)
1806
+ );
1807
+ }
1808
+ if (fileListPath) {
1809
+ await (0, import_promises4.writeFile)(fileListPath, `${finalImagePath}
1810
+ `, {
1811
+ encoding: "utf-8",
1812
+ flag: "a"
1155
1813
  });
1156
- points = roundPoints(points, precision);
1157
- const transcription = item.transcription && item.transcription.length > i ? item.transcription[i] : "";
1158
- let dt_score = 1;
1159
- try {
1160
- const firstPoint = points[0];
1161
- if (firstPoint) {
1162
- const polygon2 = turf.polygon([points.concat([firstPoint])]);
1163
- const area2 = turf.area(polygon2);
1164
- dt_score = Math.min(1, Math.max(0.5, area2 / 1e4));
1165
- }
1166
- } catch {
1167
- dt_score = 0.8;
1168
- }
1169
- const annotation = {
1170
- transcription: transcription ?? "",
1171
- points,
1172
- dt_score
1173
- };
1174
- if (!resultMap.has(imagePath)) {
1175
- resultMap.set(imagePath, []);
1176
- }
1177
- resultMap.get(imagePath).push(annotation);
1178
1814
  }
1815
+ taskId++;
1179
1816
  }
1180
- return resultMap;
1181
- };
1817
+ const baseName = fileName || file.replace(".txt", "");
1818
+ const outputSubDir = outDir ? (0, import_path4.join)(outDir, relativeDir) : (0, import_path4.dirname)(filePath);
1819
+ await (0, import_promises4.mkdir)(outputSubDir, { recursive: true });
1820
+ const outputPath = (0, import_path4.join)(
1821
+ outputSubDir,
1822
+ fileName ? `${fileName}.json` : `${baseName}_${toFullJson ? "full" : "min"}.json`
1823
+ );
1824
+ if (backup) {
1825
+ const backupPath = await backupFileIfExists(outputPath);
1826
+ if (backupPath) {
1827
+ console.log(import_chalk3.default.gray(` \u2713 Created backup: ${backupPath}`));
1828
+ }
1829
+ }
1830
+ await (0, import_promises4.writeFile)(
1831
+ outputPath,
1832
+ JSON.stringify(allLabelStudioData, null, 2),
1833
+ "utf-8"
1834
+ );
1835
+ console.log(import_chalk3.default.green(`\u2713 Converted ${file} -> ${outputPath}`));
1836
+ } catch (error) {
1837
+ console.error(
1838
+ import_chalk3.default.red(`\u2717 Failed to process ${file}:`),
1839
+ error instanceof Error ? error.message : error
1840
+ );
1841
+ }
1842
+ }
1843
+ console.log(import_chalk3.default.green("\n\u2713 Conversion completed!"));
1844
+ }
1845
+ var import_promises4, import_path4, import_chalk3;
1846
+ var init_impl3 = __esm({
1847
+ "src/commands/toLabelStudio/impl.ts"() {
1848
+ "use strict";
1849
+ init_cjs_shims();
1850
+ import_promises4 = require("fs/promises");
1851
+ import_path4 = require("path");
1852
+ import_chalk3 = __toESM(require("chalk"), 1);
1853
+ init_constants();
1854
+ init_backup_utils();
1855
+ init_file_utils();
1856
+ init_ppocr_label();
1857
+ init_schema();
1858
+ init_sort();
1182
1859
  }
1183
1860
  });
1184
1861
 
1185
1862
  // src/commands/toPPOCR/impl.ts
1186
- var impl_exports2 = {};
1187
- __export(impl_exports2, {
1863
+ var impl_exports4 = {};
1864
+ __export(impl_exports4, {
1188
1865
  convertToPPOCR: () => convertToPPOCR
1189
1866
  });
1190
1867
  async function convertToPPOCR(flags, ...inputDirs) {
1191
1868
  const {
1192
- outDir = `${OUTPUT_BASE_DIR}`,
1869
+ outDir,
1193
1870
  fileName = DEFAULT_PPOCR_FILE_NAME,
1871
+ backup = false,
1194
1872
  baseImageDir,
1195
1873
  sortVertical = DEFAULT_SORT_VERTICAL,
1196
1874
  sortHorizontal = DEFAULT_SORT_HORIZONTAL,
1197
1875
  normalizeShape: normalizeShape2 = DEFAULT_SHAPE_NORMALIZE,
1198
1876
  widthIncrement = DEFAULT_WIDTH_INCREMENT,
1199
1877
  heightIncrement = DEFAULT_HEIGHT_INCREMENT,
1200
- precision = DEFAULT_PPOCR_PRECISION
1878
+ precision = DEFAULT_PPOCR_PRECISION,
1879
+ recursive = DEFAULT_RECURSIVE,
1880
+ filePattern = DEFAULT_LABEL_STUDIO_FILE_PATTERN
1201
1881
  } = flags;
1202
- await (0, import_promises2.mkdir)(outDir, { recursive: true });
1203
- for (const inputDir of inputDirs) {
1204
- console.log(import_chalk2.default.blue(`Processing input directory: ${inputDir}`));
1205
- const files = await (0, import_promises2.readdir)(inputDir);
1206
- for (const file of files) {
1207
- if (!file.endsWith(".json")) {
1208
- continue;
1882
+ console.log(import_chalk4.default.blue("Finding files..."));
1883
+ const filePaths = await findFiles(inputDirs, filePattern, recursive);
1884
+ if (filePaths.length === 0) {
1885
+ console.log(import_chalk4.default.yellow("No files found matching the pattern."));
1886
+ return;
1887
+ }
1888
+ console.log(import_chalk4.default.blue(`Found ${filePaths.length} files to process
1889
+ `));
1890
+ for (const filePath of filePaths) {
1891
+ const file = (0, import_path5.basename)(filePath);
1892
+ const relativePath = getRelativePathFromInputs(filePath, inputDirs);
1893
+ const relativeDir = (0, import_path5.dirname)(relativePath);
1894
+ console.log(import_chalk4.default.gray(`Processing file: ${filePath}`));
1895
+ try {
1896
+ const fileData = await (0, import_promises5.readFile)(filePath, "utf-8");
1897
+ const labelStudioData = JSON.parse(fileData);
1898
+ const { data, isFull } = isLabelStudioFullJSON2(labelStudioData);
1899
+ const ppocrDataMap = isFull ? await labelStudioToPPOCR(data, {
1900
+ baseImageDir,
1901
+ normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
1902
+ widthIncrement,
1903
+ heightIncrement,
1904
+ precision
1905
+ }) : await minLabelStudioToPPOCR(data, {
1906
+ baseImageDir,
1907
+ normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
1908
+ widthIncrement,
1909
+ heightIncrement,
1910
+ precision
1911
+ });
1912
+ const outputLines = [];
1913
+ for (const [imagePath, annotations] of ppocrDataMap.entries()) {
1914
+ const sortedAnnotations = sortBoundingBoxes(
1915
+ annotations,
1916
+ sortVertical,
1917
+ sortHorizontal
1918
+ );
1919
+ PPOCRLabelSchema.parse(sortedAnnotations);
1920
+ const jsonArray = JSON.stringify(sortedAnnotations);
1921
+ outputLines.push(`${imagePath} ${jsonArray}`);
1209
1922
  }
1210
- const filePath = (0, import_path2.join)(inputDir, file);
1211
- console.log(import_chalk2.default.gray(`Processing file: ${file}`));
1212
- try {
1213
- const fileData = await (0, import_promises2.readFile)(filePath, "utf-8");
1214
- const labelStudioData = JSON.parse(fileData);
1215
- const { data, isFull } = isLabelStudioFullJSON(labelStudioData);
1216
- const ppocrDataMap = isFull ? await labelStudioToPPOCR(data, {
1217
- baseImageDir,
1218
- normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
1219
- widthIncrement,
1220
- heightIncrement,
1221
- precision
1222
- }) : await minLabelStudioToPPOCR(data, {
1223
- baseImageDir,
1224
- normalizeShape: normalizeShape2 !== SHAPE_NORMALIZE_NONE ? normalizeShape2 : void 0,
1225
- widthIncrement,
1226
- heightIncrement,
1227
- precision
1228
- });
1229
- const outputLines = [];
1230
- for (const [imagePath, annotations] of ppocrDataMap.entries()) {
1231
- const sortedAnnotations = sortBoundingBoxes(
1232
- annotations,
1233
- sortVertical,
1234
- sortHorizontal
1235
- );
1236
- PPOCRLabelSchema.parse(sortedAnnotations);
1237
- const jsonArray = JSON.stringify(sortedAnnotations);
1238
- outputLines.push(`${imagePath} ${jsonArray}`);
1923
+ const baseName = file.replace(".json", "");
1924
+ const outputSubDir = outDir ? (0, import_path5.join)(outDir, relativeDir) : (0, import_path5.dirname)(filePath);
1925
+ await (0, import_promises5.mkdir)(outputSubDir, { recursive: true });
1926
+ const outputPath = (0, import_path5.join)(outputSubDir, `${baseName}_${fileName}`);
1927
+ if (backup) {
1928
+ const backupPath = await backupFileIfExists(outputPath);
1929
+ if (backupPath) {
1930
+ console.log(import_chalk4.default.gray(` Backed up to: ${backupPath}`));
1239
1931
  }
1240
- const baseName = file.replace(".json", "");
1241
- const outputPath = (0, import_path2.join)(outDir, `${baseName}_${fileName}`);
1242
- await (0, import_promises2.writeFile)(outputPath, outputLines.join("\n"), "utf-8");
1243
- console.log(import_chalk2.default.green(`\u2713 Converted ${file} -> ${outputPath}`));
1244
- } catch (error) {
1245
- console.error(
1246
- import_chalk2.default.red(`\u2717 Failed to process ${file}:`),
1247
- error instanceof Error ? error.message : error
1248
- );
1249
1932
  }
1933
+ await (0, import_promises5.writeFile)(outputPath, outputLines.join("\n"), "utf-8");
1934
+ console.log(import_chalk4.default.green(`\u2713 Converted ${file} -> ${outputPath}`));
1935
+ } catch (error) {
1936
+ console.error(
1937
+ import_chalk4.default.red(`\u2717 Failed to process ${file}:`),
1938
+ error instanceof Error ? error.message : error
1939
+ );
1250
1940
  }
1251
1941
  }
1252
- console.log(import_chalk2.default.green("\n\u2713 Conversion completed!"));
1942
+ console.log(import_chalk4.default.green("\n\u2713 Conversion completed!"));
1253
1943
  }
1254
- var import_promises2, import_path2, import_chalk2, isLabelStudioFullJSON;
1255
- var init_impl2 = __esm({
1944
+ var import_promises5, import_path5, import_chalk4, isLabelStudioFullJSON2;
1945
+ var init_impl4 = __esm({
1256
1946
  "src/commands/toPPOCR/impl.ts"() {
1257
1947
  "use strict";
1258
1948
  init_cjs_shims();
1259
- import_promises2 = require("fs/promises");
1260
- import_path2 = require("path");
1261
- import_chalk2 = __toESM(require("chalk"), 1);
1949
+ import_promises5 = require("fs/promises");
1950
+ import_path5 = require("path");
1951
+ import_chalk4 = __toESM(require("chalk"), 1);
1262
1952
  init_constants();
1953
+ init_backup_utils();
1954
+ init_file_utils();
1263
1955
  init_label_studio();
1264
1956
  init_schema();
1265
1957
  init_sort();
1266
- isLabelStudioFullJSON = (data) => {
1958
+ isLabelStudioFullJSON2 = (data) => {
1267
1959
  const parsedFull = FullOCRLabelStudioSchema.safeParse(data);
1268
1960
  if (parsedFull.success) {
1269
1961
  return { isFull: true, data: parsedFull.data };
@@ -1285,24 +1977,210 @@ var init_impl2 = __esm({
1285
1977
 
1286
1978
  // src/bin/bash-complete.ts
1287
1979
  init_cjs_shims();
1288
- var import_core4 = require("@stricli/core");
1980
+ var import_core6 = require("@stricli/core");
1289
1981
 
1290
1982
  // src/app.ts
1291
1983
  init_cjs_shims();
1292
1984
  var import_auto_complete = require("@stricli/auto-complete");
1293
- var import_core3 = require("@stricli/core");
1985
+ var import_core5 = require("@stricli/core");
1294
1986
 
1295
1987
  // package.json
1296
- var version = "1.1.0";
1988
+ var version = "1.3.0";
1297
1989
  var description = "Convert between Label Studio OCR format and PPOCRLabelv2 format";
1298
1990
 
1299
- // src/commands/toLabelStudio/command.ts
1991
+ // src/commands/enhance-labelstudio/command.ts
1300
1992
  init_cjs_shims();
1301
1993
  var import_core = require("@stricli/core");
1302
1994
  init_constants();
1303
- var toLabelStudioCommand = (0, import_core.buildCommand)({
1995
+ var enhanceLabelStudioCommand = (0, import_core.buildCommand)({
1996
+ loader: async () => {
1997
+ const { enhanceLabelStudio: enhanceLabelStudio2 } = await Promise.resolve().then(() => (init_impl(), impl_exports));
1998
+ return enhanceLabelStudio2;
1999
+ },
2000
+ parameters: {
2001
+ positional: {
2002
+ kind: "array",
2003
+ parameter: {
2004
+ brief: "Input directories containing Label Studio JSON files",
2005
+ parse: String
2006
+ },
2007
+ minimum: 1
2008
+ },
2009
+ flags: {
2010
+ outDir: {
2011
+ kind: "parsed",
2012
+ brief: "Output directory. If not specified, files are saved in the same directory as the source files",
2013
+ parse: String,
2014
+ optional: true
2015
+ },
2016
+ fileName: {
2017
+ kind: "parsed",
2018
+ brief: "Custom output filename. If not specified, uses the same name as the source file",
2019
+ parse: String,
2020
+ optional: true
2021
+ },
2022
+ backup: {
2023
+ kind: "boolean",
2024
+ brief: `Create backup of existing files before overwriting. Default: ${DEFAULT_BACKUP}`,
2025
+ optional: true
2026
+ },
2027
+ sortVertical: {
2028
+ kind: "parsed",
2029
+ brief: `Sort bounding boxes vertically. Options: "${SORT_VERTICAL_NONE}", "${SORT_VERTICAL_TOP_BOTTOM}", "${SORT_VERTICAL_BOTTOM_TOP}". Default: "${DEFAULT_SORT_VERTICAL}"`,
2030
+ parse: String,
2031
+ optional: true
2032
+ },
2033
+ sortHorizontal: {
2034
+ kind: "parsed",
2035
+ brief: `Sort bounding boxes horizontally. Options: "${SORT_HORIZONTAL_NONE}", "${SORT_HORIZONTAL_LTR}", "${SORT_HORIZONTAL_RTL}". Default: "${DEFAULT_SORT_HORIZONTAL}"`,
2036
+ parse: String,
2037
+ optional: true
2038
+ },
2039
+ normalizeShape: {
2040
+ kind: "parsed",
2041
+ brief: `Normalize diamond-like shapes to axis-aligned rectangles. Options: "${SHAPE_NORMALIZE_NONE}", "${SHAPE_NORMALIZE_RECTANGLE}". Default: "${DEFAULT_SHAPE_NORMALIZE}"`,
2042
+ parse: String,
2043
+ optional: true
2044
+ },
2045
+ widthIncrement: {
2046
+ kind: "parsed",
2047
+ brief: `Increase bounding box width by this amount (in pixels). Can be negative to decrease. Default: ${DEFAULT_WIDTH_INCREMENT}`,
2048
+ parse: Number,
2049
+ optional: true
2050
+ },
2051
+ heightIncrement: {
2052
+ kind: "parsed",
2053
+ brief: `Increase bounding box height by this amount (in pixels). Can be negative to decrease. Default: ${DEFAULT_HEIGHT_INCREMENT}`,
2054
+ parse: Number,
2055
+ optional: true
2056
+ },
2057
+ precision: {
2058
+ kind: "parsed",
2059
+ brief: `Number of decimal places for coordinates. Use -1 for full precision (no rounding). Default: ${DEFAULT_LABEL_STUDIO_PRECISION}`,
2060
+ parse: Number,
2061
+ optional: true
2062
+ },
2063
+ recursive: {
2064
+ kind: "boolean",
2065
+ brief: `Recursively search directories for files. Default: ${DEFAULT_RECURSIVE}`,
2066
+ optional: true
2067
+ },
2068
+ filePattern: {
2069
+ kind: "parsed",
2070
+ brief: `Regex pattern to match Label Studio files (should match .json files). Default: "${DEFAULT_LABEL_STUDIO_FILE_PATTERN}"`,
2071
+ parse: String,
2072
+ optional: true
2073
+ },
2074
+ outputMode: {
2075
+ kind: "parsed",
2076
+ brief: `Output mode: "${OUTPUT_MODE_ANNOTATIONS}" for editable annotations (ground truth) or "${OUTPUT_MODE_PREDICTIONS}" for read-only predictions (pre-annotations). Default: "${DEFAULT_OUTPUT_MODE}"`,
2077
+ parse: String,
2078
+ optional: true
2079
+ }
2080
+ }
2081
+ },
2082
+ docs: {
2083
+ brief: "Enhance Label Studio files with sorting, normalization, and resizing"
2084
+ }
2085
+ });
2086
+
2087
+ // src/commands/enhance-ppocr/command.ts
2088
+ init_cjs_shims();
2089
+ var import_core2 = require("@stricli/core");
2090
+ init_constants();
2091
+ var enhancePPOCRCommand = (0, import_core2.buildCommand)({
2092
+ loader: async () => {
2093
+ const { enhancePPOCR: enhancePPOCR2 } = await Promise.resolve().then(() => (init_impl2(), impl_exports2));
2094
+ return enhancePPOCR2;
2095
+ },
2096
+ parameters: {
2097
+ positional: {
2098
+ kind: "array",
2099
+ parameter: {
2100
+ brief: "Input directories containing PPOCRLabel files",
2101
+ parse: String
2102
+ },
2103
+ minimum: 1
2104
+ },
2105
+ flags: {
2106
+ outDir: {
2107
+ kind: "parsed",
2108
+ brief: "Output directory. If not specified, files are saved in the same directory as the source files",
2109
+ parse: String,
2110
+ optional: true
2111
+ },
2112
+ fileName: {
2113
+ kind: "parsed",
2114
+ brief: "Custom output filename. If not specified, uses the same name as the source file",
2115
+ parse: String,
2116
+ optional: true
2117
+ },
2118
+ backup: {
2119
+ kind: "boolean",
2120
+ brief: `Create backup of existing files before overwriting. Default: ${DEFAULT_BACKUP}`,
2121
+ optional: true
2122
+ },
2123
+ sortVertical: {
2124
+ kind: "parsed",
2125
+ brief: `Sort bounding boxes vertically. Options: "${SORT_VERTICAL_NONE}", "${SORT_VERTICAL_TOP_BOTTOM}", "${SORT_VERTICAL_BOTTOM_TOP}". Default: "${DEFAULT_SORT_VERTICAL}"`,
2126
+ parse: String,
2127
+ optional: true
2128
+ },
2129
+ sortHorizontal: {
2130
+ kind: "parsed",
2131
+ brief: `Sort bounding boxes horizontally. Options: "${SORT_HORIZONTAL_NONE}", "${SORT_HORIZONTAL_LTR}", "${SORT_HORIZONTAL_RTL}". Default: "${DEFAULT_SORT_HORIZONTAL}"`,
2132
+ parse: String,
2133
+ optional: true
2134
+ },
2135
+ normalizeShape: {
2136
+ kind: "parsed",
2137
+ brief: `Normalize diamond-like shapes to axis-aligned rectangles. Options: "${SHAPE_NORMALIZE_NONE}", "${SHAPE_NORMALIZE_RECTANGLE}". Default: "${DEFAULT_SHAPE_NORMALIZE}"`,
2138
+ parse: String,
2139
+ optional: true
2140
+ },
2141
+ widthIncrement: {
2142
+ kind: "parsed",
2143
+ brief: `Increase bounding box width by this amount (in pixels). Can be negative to decrease. Default: ${DEFAULT_WIDTH_INCREMENT}`,
2144
+ parse: Number,
2145
+ optional: true
2146
+ },
2147
+ heightIncrement: {
2148
+ kind: "parsed",
2149
+ brief: `Increase bounding box height by this amount (in pixels). Can be negative to decrease. Default: ${DEFAULT_HEIGHT_INCREMENT}`,
2150
+ parse: Number,
2151
+ optional: true
2152
+ },
2153
+ precision: {
2154
+ kind: "parsed",
2155
+ brief: `Number of decimal places for coordinates. Use -1 for full precision (no rounding). Default: ${DEFAULT_PPOCR_PRECISION} (integers)`,
2156
+ parse: Number,
2157
+ optional: true
2158
+ },
2159
+ recursive: {
2160
+ kind: "boolean",
2161
+ brief: `Recursively search directories for files. Default: ${DEFAULT_RECURSIVE}`,
2162
+ optional: true
2163
+ },
2164
+ filePattern: {
2165
+ kind: "parsed",
2166
+ brief: `Regex pattern to match PPOCRLabel files (should match .txt files). Default: "${DEFAULT_PPOCR_FILE_PATTERN}"`,
2167
+ parse: String,
2168
+ optional: true
2169
+ }
2170
+ }
2171
+ },
2172
+ docs: {
2173
+ brief: "Enhance PPOCRLabel files with sorting, normalization, and resizing"
2174
+ }
2175
+ });
2176
+
2177
+ // src/commands/toLabelStudio/command.ts
2178
+ init_cjs_shims();
2179
+ var import_core3 = require("@stricli/core");
2180
+ init_constants();
2181
+ var toLabelStudioCommand = (0, import_core3.buildCommand)({
1304
2182
  loader: async () => {
1305
- const { convertToLabelStudio: convertToLabelStudio2 } = await Promise.resolve().then(() => (init_impl(), impl_exports));
2183
+ const { convertToLabelStudio: convertToLabelStudio2 } = await Promise.resolve().then(() => (init_impl3(), impl_exports3));
1306
2184
  return convertToLabelStudio2;
1307
2185
  },
1308
2186
  parameters: {
@@ -1317,58 +2195,69 @@ var toLabelStudioCommand = (0, import_core.buildCommand)({
1317
2195
  flags: {
1318
2196
  outDir: {
1319
2197
  kind: "parsed",
1320
- brief: `Output directory. Default to "${OUTPUT_BASE_DIR}"`,
2198
+ brief: "Output directory. If not specified, files are saved in the same directory as the source files",
2199
+ parse: String,
2200
+ optional: true
2201
+ },
2202
+ fileName: {
2203
+ kind: "parsed",
2204
+ brief: "Custom output filename (without extension). If not specified, uses source filename with format suffix",
1321
2205
  parse: String,
1322
2206
  optional: true
1323
2207
  },
2208
+ backup: {
2209
+ kind: "boolean",
2210
+ brief: `Create backup of existing files before overwriting. Default: ${DEFAULT_BACKUP}`,
2211
+ optional: true
2212
+ },
1324
2213
  defaultLabelName: {
1325
2214
  kind: "parsed",
1326
- brief: `Default label name for text annotations. Default to "${DEFAULT_LABEL_NAME}"`,
2215
+ brief: `Default label name for text annotations. Default: "${DEFAULT_LABEL_NAME}"`,
1327
2216
  parse: String,
1328
2217
  optional: true
1329
2218
  },
1330
2219
  toFullJson: {
1331
2220
  kind: "boolean",
1332
- brief: `Convert to Full OCR Label Studio format. Default to "${DEFAULT_LABEL_STUDIO_FULL_JSON}"`,
2221
+ brief: `Convert to Full OCR Label Studio format. Default: "${DEFAULT_LABEL_STUDIO_FULL_JSON}"`,
1333
2222
  optional: true
1334
2223
  },
1335
2224
  createFilePerImage: {
1336
2225
  kind: "boolean",
1337
- brief: `Create a separate Label Studio JSON file for each image. Default to "${DEFAULT_CREATE_FILE_PER_IMAGE}"`,
2226
+ brief: `Create a separate Label Studio JSON file for each image. Default: "${DEFAULT_CREATE_FILE_PER_IMAGE}"`,
1338
2227
  optional: true
1339
2228
  },
1340
2229
  createFileListForServing: {
1341
2230
  kind: "boolean",
1342
- brief: `Create a file list for serving in Label Studio. Default to "${DEFAULT_CREATE_FILE_LIST_FOR_SERVING}"`,
2231
+ brief: `Create a file list for serving in Label Studio. Default: "${DEFAULT_CREATE_FILE_LIST_FOR_SERVING}"`,
1343
2232
  optional: true
1344
2233
  },
1345
2234
  fileListName: {
1346
2235
  kind: "parsed",
1347
- brief: `Name of the file list for serving. Default to "${DEFAULT_FILE_LIST_NAME}"`,
2236
+ brief: `Name of the file list for serving. Default: "${DEFAULT_FILE_LIST_NAME}"`,
1348
2237
  parse: String,
1349
2238
  optional: true
1350
2239
  },
1351
2240
  baseServerUrl: {
1352
2241
  kind: "parsed",
1353
- brief: `Base server URL for constructing image URLs in the file list. Default to "${DEFAULT_BASE_SERVER_URL}"`,
2242
+ brief: `Base server URL for constructing image URLs in the file list. Default: "${DEFAULT_BASE_SERVER_URL}"`,
1354
2243
  parse: String,
1355
2244
  optional: true
1356
2245
  },
1357
2246
  sortVertical: {
1358
2247
  kind: "parsed",
1359
- brief: `Sort bounding boxes vertically. Options: "${SORT_VERTICAL_NONE}" (default), "${SORT_VERTICAL_TOP_BOTTOM}", "${SORT_VERTICAL_BOTTOM_TOP}"`,
2248
+ brief: `Sort bounding boxes vertically. Options: "${SORT_VERTICAL_NONE}", "${SORT_VERTICAL_TOP_BOTTOM}", "${SORT_VERTICAL_BOTTOM_TOP}". Default: "${DEFAULT_SORT_VERTICAL}"`,
1360
2249
  parse: String,
1361
2250
  optional: true
1362
2251
  },
1363
2252
  sortHorizontal: {
1364
2253
  kind: "parsed",
1365
- brief: `Sort bounding boxes horizontally. Options: "${SORT_HORIZONTAL_NONE}" (default), "${SORT_HORIZONTAL_LTR}", "${SORT_HORIZONTAL_RTL}"`,
2254
+ brief: `Sort bounding boxes horizontally. Options: "${SORT_HORIZONTAL_NONE}", "${SORT_HORIZONTAL_LTR}", "${SORT_HORIZONTAL_RTL}". Default: "${DEFAULT_SORT_HORIZONTAL}"`,
1366
2255
  parse: String,
1367
2256
  optional: true
1368
2257
  },
1369
2258
  normalizeShape: {
1370
2259
  kind: "parsed",
1371
- brief: `Normalize diamond-like shapes to axis-aligned rectangles. Options: "${SHAPE_NORMALIZE_NONE}" (default), "${SHAPE_NORMALIZE_RECTANGLE}"`,
2260
+ brief: `Normalize diamond-like shapes to axis-aligned rectangles. Options: "${SHAPE_NORMALIZE_NONE}", "${SHAPE_NORMALIZE_RECTANGLE}". Default: "${DEFAULT_SHAPE_NORMALIZE}"`,
1372
2261
  parse: String,
1373
2262
  optional: true
1374
2263
  },
@@ -1389,6 +2278,23 @@ var toLabelStudioCommand = (0, import_core.buildCommand)({
1389
2278
  brief: `Number of decimal places for coordinates. Use -1 for full precision (no rounding). Default: ${DEFAULT_LABEL_STUDIO_PRECISION}`,
1390
2279
  parse: Number,
1391
2280
  optional: true
2281
+ },
2282
+ recursive: {
2283
+ kind: "boolean",
2284
+ brief: `Recursively search directories for files. Default: ${DEFAULT_RECURSIVE}`,
2285
+ optional: true
2286
+ },
2287
+ filePattern: {
2288
+ kind: "parsed",
2289
+ brief: `Regex pattern to match PPOCRLabel files (should match .txt files). Default: "${DEFAULT_PPOCR_FILE_PATTERN}"`,
2290
+ parse: String,
2291
+ optional: true
2292
+ },
2293
+ outputMode: {
2294
+ kind: "parsed",
2295
+ brief: `Output mode: "${OUTPUT_MODE_ANNOTATIONS}" for editable annotations (ground truth) or "${OUTPUT_MODE_PREDICTIONS}" for read-only predictions (pre-annotations). Default: "${DEFAULT_OUTPUT_MODE}"`,
2296
+ parse: String,
2297
+ optional: true
1392
2298
  }
1393
2299
  }
1394
2300
  },
@@ -1399,11 +2305,11 @@ var toLabelStudioCommand = (0, import_core.buildCommand)({
1399
2305
 
1400
2306
  // src/commands/toPPOCR/commands.ts
1401
2307
  init_cjs_shims();
1402
- var import_core2 = require("@stricli/core");
2308
+ var import_core4 = require("@stricli/core");
1403
2309
  init_constants();
1404
- var toPPOCRCommand = (0, import_core2.buildCommand)({
2310
+ var toPPOCRCommand = (0, import_core4.buildCommand)({
1405
2311
  loader: async () => {
1406
- const { convertToPPOCR: convertToPPOCR2 } = await Promise.resolve().then(() => (init_impl2(), impl_exports2));
2312
+ const { convertToPPOCR: convertToPPOCR2 } = await Promise.resolve().then(() => (init_impl4(), impl_exports4));
1407
2313
  return convertToPPOCR2;
1408
2314
  },
1409
2315
  parameters: {
@@ -1418,16 +2324,21 @@ var toPPOCRCommand = (0, import_core2.buildCommand)({
1418
2324
  flags: {
1419
2325
  outDir: {
1420
2326
  kind: "parsed",
1421
- brief: `Output directory. Default to "${OUTPUT_BASE_DIR}"`,
2327
+ brief: "Output directory. If not specified, files are saved in the same directory as the source files",
1422
2328
  parse: String,
1423
2329
  optional: true
1424
2330
  },
1425
2331
  fileName: {
1426
2332
  kind: "parsed",
1427
- brief: `Output PPOCR file name. Default to "${DEFAULT_PPOCR_FILE_NAME}"`,
2333
+ brief: `Output PPOCR file name. Default: "${DEFAULT_PPOCR_FILE_NAME}"`,
1428
2334
  parse: String,
1429
2335
  optional: true
1430
2336
  },
2337
+ backup: {
2338
+ kind: "boolean",
2339
+ brief: `Create backup of existing files before overwriting. Default: ${DEFAULT_BACKUP}`,
2340
+ optional: true
2341
+ },
1431
2342
  baseImageDir: {
1432
2343
  kind: "parsed",
1433
2344
  brief: 'Base directory path to prepend to image filenames in output (e.g., "ch" or "images/ch")',
@@ -1436,19 +2347,19 @@ var toPPOCRCommand = (0, import_core2.buildCommand)({
1436
2347
  },
1437
2348
  sortVertical: {
1438
2349
  kind: "parsed",
1439
- brief: `Sort bounding boxes vertically. Options: "${SORT_VERTICAL_NONE}" (default), "${SORT_VERTICAL_TOP_BOTTOM}", "${SORT_VERTICAL_BOTTOM_TOP}"`,
2350
+ brief: `Sort bounding boxes vertically. Options: "${SORT_VERTICAL_NONE}", "${SORT_VERTICAL_TOP_BOTTOM}", "${SORT_VERTICAL_BOTTOM_TOP}". Default: "${DEFAULT_SORT_VERTICAL}"`,
1440
2351
  parse: String,
1441
2352
  optional: true
1442
2353
  },
1443
2354
  sortHorizontal: {
1444
2355
  kind: "parsed",
1445
- brief: `Sort bounding boxes horizontally. Options: "${SORT_HORIZONTAL_NONE}" (default), "${SORT_HORIZONTAL_LTR}", "${SORT_HORIZONTAL_RTL}"`,
2356
+ brief: `Sort bounding boxes horizontally. Options: "${SORT_HORIZONTAL_NONE}", "${SORT_HORIZONTAL_LTR}", "${SORT_HORIZONTAL_RTL}". Default: "${DEFAULT_SORT_HORIZONTAL}"`,
1446
2357
  parse: String,
1447
2358
  optional: true
1448
2359
  },
1449
2360
  normalizeShape: {
1450
2361
  kind: "parsed",
1451
- brief: `Normalize diamond-like shapes to axis-aligned rectangles. Options: "${SHAPE_NORMALIZE_NONE}" (default), "${SHAPE_NORMALIZE_RECTANGLE}"`,
2362
+ brief: `Normalize diamond-like shapes to axis-aligned rectangles. Options: "${SHAPE_NORMALIZE_NONE}", "${SHAPE_NORMALIZE_RECTANGLE}". Default: "${DEFAULT_SHAPE_NORMALIZE}"`,
1452
2363
  parse: String,
1453
2364
  optional: true
1454
2365
  },
@@ -1469,6 +2380,17 @@ var toPPOCRCommand = (0, import_core2.buildCommand)({
1469
2380
  brief: `Number of decimal places for coordinates. Use -1 for full precision (no rounding). Default: ${DEFAULT_PPOCR_PRECISION} (integers)`,
1470
2381
  parse: Number,
1471
2382
  optional: true
2383
+ },
2384
+ recursive: {
2385
+ kind: "boolean",
2386
+ brief: `Recursively search directories for files. Default: ${DEFAULT_RECURSIVE}`,
2387
+ optional: true
2388
+ },
2389
+ filePattern: {
2390
+ kind: "parsed",
2391
+ brief: `Regex pattern to match Label Studio files (should match .json files). Default: "${DEFAULT_LABEL_STUDIO_FILE_PATTERN}"`,
2392
+ parse: String,
2393
+ optional: true
1472
2394
  }
1473
2395
  }
1474
2396
  },
@@ -1478,10 +2400,12 @@ var toPPOCRCommand = (0, import_core2.buildCommand)({
1478
2400
  });
1479
2401
 
1480
2402
  // src/app.ts
1481
- var routes = (0, import_core3.buildRouteMap)({
2403
+ var routes = (0, import_core5.buildRouteMap)({
1482
2404
  routes: {
1483
2405
  toLabelStudio: toLabelStudioCommand,
1484
2406
  toPPOCR: toPPOCRCommand,
2407
+ "enhance-labelstudio": enhanceLabelStudioCommand,
2408
+ "enhance-ppocr": enhancePPOCRCommand,
1485
2409
  install: (0, import_auto_complete.buildInstallCommand)("label-studio-converter", {
1486
2410
  bash: "__label-studio-converter_bash_complete"
1487
2411
  }),
@@ -1495,7 +2419,7 @@ var routes = (0, import_core3.buildRouteMap)({
1495
2419
  }
1496
2420
  }
1497
2421
  });
1498
- var app = (0, import_core3.buildApplication)(routes, {
2422
+ var app = (0, import_core5.buildApplication)(routes, {
1499
2423
  name: "label-studio-converter",
1500
2424
  versionInfo: {
1501
2425
  currentVersion: version
@@ -1522,9 +2446,9 @@ function buildContext(process2) {
1522
2446
  if (process.env.COMP_LINE?.endsWith(" ")) {
1523
2447
  inputs.push("");
1524
2448
  }
1525
- await (0, import_core4.proposeCompletions)(app, inputs, buildContext(process));
2449
+ await (0, import_core6.proposeCompletions)(app, inputs, buildContext(process));
1526
2450
  try {
1527
- for (const { completion } of await (0, import_core4.proposeCompletions)(
2451
+ for (const { completion } of await (0, import_core6.proposeCompletions)(
1528
2452
  app,
1529
2453
  inputs,
1530
2454
  buildContext(process)