@things-factory/integration-label-studio 9.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/EXTERNAL_DATA_SOURCING.md +484 -0
  3. package/IMPLEMENTATION_GUIDE.md +469 -0
  4. package/INTEGRATION.md +279 -0
  5. package/README.md +1014 -0
  6. package/SETUP_GUIDE.md +577 -0
  7. package/TEST_GUIDE.md +387 -0
  8. package/UI_CUSTOMIZATION.md +395 -0
  9. package/USER_SYNC_GUIDE.md +514 -0
  10. package/client/bootstrap.ts +1 -0
  11. package/client/index.ts +1 -0
  12. package/client/label-studio-label-page.ts +52 -0
  13. package/client/label-studio-project-create.ts +216 -0
  14. package/client/label-studio-project-list.ts +214 -0
  15. package/client/label-studio-wrapper.ts +294 -0
  16. package/client/route.ts +15 -0
  17. package/client/tsconfig.json +13 -0
  18. package/config/config.development.js +124 -0
  19. package/config/config.production.js +182 -0
  20. package/dist-client/bootstrap.d.ts +1 -0
  21. package/dist-client/bootstrap.js +2 -0
  22. package/dist-client/bootstrap.js.map +1 -0
  23. package/dist-client/index.d.ts +1 -0
  24. package/dist-client/index.js +2 -0
  25. package/dist-client/index.js.map +1 -0
  26. package/dist-client/label-studio-label-page.d.ts +8 -0
  27. package/dist-client/label-studio-label-page.js +54 -0
  28. package/dist-client/label-studio-label-page.js.map +1 -0
  29. package/dist-client/label-studio-project-create.d.ts +16 -0
  30. package/dist-client/label-studio-project-create.js +235 -0
  31. package/dist-client/label-studio-project-create.js.map +1 -0
  32. package/dist-client/label-studio-project-list.d.ts +16 -0
  33. package/dist-client/label-studio-project-list.js +222 -0
  34. package/dist-client/label-studio-project-list.js.map +1 -0
  35. package/dist-client/label-studio-wrapper.d.ts +57 -0
  36. package/dist-client/label-studio-wrapper.js +304 -0
  37. package/dist-client/label-studio-wrapper.js.map +1 -0
  38. package/dist-client/route.d.ts +1 -0
  39. package/dist-client/route.js +14 -0
  40. package/dist-client/route.js.map +1 -0
  41. package/dist-client/tsconfig.tsbuildinfo +1 -0
  42. package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
  43. package/dist-server/controller/label-studio-role-mapper.js +65 -0
  44. package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
  45. package/dist-server/controller/user-provisioning-service.d.ts +66 -0
  46. package/dist-server/controller/user-provisioning-service.js +264 -0
  47. package/dist-server/controller/user-provisioning-service.js.map +1 -0
  48. package/dist-server/index.d.ts +7 -0
  49. package/dist-server/index.js +19 -0
  50. package/dist-server/index.js.map +1 -0
  51. package/dist-server/route/label-studio-sso.d.ts +2 -0
  52. package/dist-server/route/label-studio-sso.js +156 -0
  53. package/dist-server/route/label-studio-sso.js.map +1 -0
  54. package/dist-server/route/webhook.d.ts +65 -0
  55. package/dist-server/route/webhook.js +248 -0
  56. package/dist-server/route/webhook.js.map +1 -0
  57. package/dist-server/route.d.ts +1 -0
  58. package/dist-server/route.js +21 -0
  59. package/dist-server/route.js.map +1 -0
  60. package/dist-server/service/ai-prediction-service.d.ts +27 -0
  61. package/dist-server/service/ai-prediction-service.js +222 -0
  62. package/dist-server/service/ai-prediction-service.js.map +1 -0
  63. package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
  64. package/dist-server/service/dataset-labeling-integration.js +512 -0
  65. package/dist-server/service/dataset-labeling-integration.js.map +1 -0
  66. package/dist-server/service/external-data-source-service.d.ts +78 -0
  67. package/dist-server/service/external-data-source-service.js +415 -0
  68. package/dist-server/service/external-data-source-service.js.map +1 -0
  69. package/dist-server/service/index.d.ts +12 -0
  70. package/dist-server/service/index.js +27 -0
  71. package/dist-server/service/index.js.map +1 -0
  72. package/dist-server/service/label-studio-sso-service.d.ts +38 -0
  73. package/dist-server/service/label-studio-sso-service.js +98 -0
  74. package/dist-server/service/label-studio-sso-service.js.map +1 -0
  75. package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
  76. package/dist-server/service/ml/ml-backend-service.js +153 -0
  77. package/dist-server/service/ml/ml-backend-service.js.map +1 -0
  78. package/dist-server/service/prediction/prediction-management.d.ts +32 -0
  79. package/dist-server/service/prediction/prediction-management.js +299 -0
  80. package/dist-server/service/prediction/prediction-management.js.map +1 -0
  81. package/dist-server/service/project/project-management.d.ts +36 -0
  82. package/dist-server/service/project/project-management.js +309 -0
  83. package/dist-server/service/project/project-management.js.map +1 -0
  84. package/dist-server/service/task/task-management.d.ts +42 -0
  85. package/dist-server/service/task/task-management.js +372 -0
  86. package/dist-server/service/task/task-management.js.map +1 -0
  87. package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
  88. package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
  89. package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
  90. package/dist-server/service/webhook/webhook-management.d.ts +21 -0
  91. package/dist-server/service/webhook/webhook-management.js +134 -0
  92. package/dist-server/service/webhook/webhook-management.js.map +1 -0
  93. package/dist-server/tsconfig.tsbuildinfo +1 -0
  94. package/dist-server/types/dataset-labeling-types.d.ts +71 -0
  95. package/dist-server/types/dataset-labeling-types.js +259 -0
  96. package/dist-server/types/dataset-labeling-types.js.map +1 -0
  97. package/dist-server/types/label-studio-types.d.ts +128 -0
  98. package/dist-server/types/label-studio-types.js +494 -0
  99. package/dist-server/types/label-studio-types.js.map +1 -0
  100. package/dist-server/types/prediction-types.d.ts +39 -0
  101. package/dist-server/types/prediction-types.js +121 -0
  102. package/dist-server/types/prediction-types.js.map +1 -0
  103. package/dist-server/utils/annotation-exporter.d.ts +104 -0
  104. package/dist-server/utils/annotation-exporter.js +261 -0
  105. package/dist-server/utils/annotation-exporter.js.map +1 -0
  106. package/dist-server/utils/label-config-builder.d.ts +117 -0
  107. package/dist-server/utils/label-config-builder.js +286 -0
  108. package/dist-server/utils/label-config-builder.js.map +1 -0
  109. package/dist-server/utils/label-studio-api-client.d.ts +180 -0
  110. package/dist-server/utils/label-studio-api-client.js +401 -0
  111. package/dist-server/utils/label-studio-api-client.js.map +1 -0
  112. package/dist-server/utils/media-url-extractor.d.ts +45 -0
  113. package/dist-server/utils/media-url-extractor.js +152 -0
  114. package/dist-server/utils/media-url-extractor.js.map +1 -0
  115. package/dist-server/utils/task-transformer.d.ts +108 -0
  116. package/dist-server/utils/task-transformer.js +260 -0
  117. package/dist-server/utils/task-transformer.js.map +1 -0
  118. package/package.json +47 -0
  119. package/server/SERVER_STRUCTURE.md +351 -0
  120. package/server/controller/label-studio-role-mapper.ts +76 -0
  121. package/server/controller/user-provisioning-service.ts +340 -0
  122. package/server/index.ts +19 -0
  123. package/server/route/label-studio-sso.ts +194 -0
  124. package/server/route/webhook.ts +304 -0
  125. package/server/route.ts +35 -0
  126. package/server/service/ai-prediction-service.ts +239 -0
  127. package/server/service/dataset-labeling-integration.ts +590 -0
  128. package/server/service/external-data-source-service.ts +438 -0
  129. package/server/service/index.ts +24 -0
  130. package/server/service/label-studio-sso-service.ts +108 -0
  131. package/server/service/labeling-scenario-service.ts.deprecated +566 -0
  132. package/server/service/ml/ml-backend-service.ts +127 -0
  133. package/server/service/prediction/prediction-management.ts +281 -0
  134. package/server/service/project/project-management.ts +284 -0
  135. package/server/service/task/task-management.ts +363 -0
  136. package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
  137. package/server/service/webhook/webhook-management.ts +109 -0
  138. package/server/tsconfig.json +11 -0
  139. package/server/types/dataset-labeling-types.ts +181 -0
  140. package/server/types/global.d.ts +23 -0
  141. package/server/types/label-studio-types.ts +346 -0
  142. package/server/types/prediction-types.ts +86 -0
  143. package/server/types/scenario-types.ts.deprecated +362 -0
  144. package/server/utils/annotation-exporter.ts +340 -0
  145. package/server/utils/label-config-builder.ts +340 -0
  146. package/server/utils/label-studio-api-client.ts +487 -0
  147. package/server/utils/media-url-extractor.ts +193 -0
  148. package/server/utils/task-transformer.ts +342 -0
  149. package/test-ai-prediction.js +268 -0
  150. package/test-dataset-integration.js +449 -0
  151. package/test-simple.js +89 -0
  152. package/things-factory.config.js +12 -0
@@ -0,0 +1,590 @@
1
+ import { Resolver, Query, Mutation, Arg, Ctx, Int, Directive } from 'type-graphql'
2
+ import { getRepository } from '@things-factory/shell'
3
+ import { AIModelClientFactory } from '@things-factory/ai-inference'
4
+ import { DataSample, DataSet } from '@things-factory/dataset'
5
+ import { labelStudioApi } from '../utils/label-studio-api-client.js'
6
+ import {
7
+ CreateLabelingTasksRequest,
8
+ TaskCreationResult,
9
+ SyncAnnotationsRequest,
10
+ SyncAnnotationsResult,
11
+ DatasetLabelingStatus,
12
+ DatasetLabelingStatusRequest,
13
+ GeneratePredictionsForDatasetRequest
14
+ } from '../types/dataset-labeling-types.js'
15
+ import { BatchPredictionResult } from '../types/prediction-types.js'
16
+ import { extractMediaUrls, getBaseUrl } from '../utils/media-url-extractor.js'
17
+
18
+ /**
19
+ * Dataset Labeling Integration Service
20
+ *
21
+ * Integrates Things-Factory Dataset module with Label Studio for AI-assisted labeling
22
+ *
23
+ * Features:
24
+ * - Create Label Studio tasks from DataSamples
25
+ * - Auto-generate AI predictions for labeling tasks
26
+ * - Sync human annotations back to DataSamples
27
+ * - Track labeling progress and status
28
+ */
29
+ @Resolver()
30
+ export class DatasetLabelingIntegration {
31
+ /**
32
+ * Create Label Studio labeling tasks from DataSamples in a DataSet
33
+ * Optionally generates AI predictions automatically
34
+ */
35
+ @Mutation(returns => TaskCreationResult, {
36
+ description:
37
+ 'Create Label Studio labeling tasks from DataSamples in a DataSet. Optionally auto-generates AI predictions for each task.'
38
+ })
39
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
40
+ async createLabelingTasksFromDataset(
41
+ @Arg('input') input: CreateLabelingTasksRequest,
42
+ @Ctx() context: ResolverContext
43
+ ): Promise<TaskCreationResult> {
44
+ const { domain } = context.state
45
+
46
+ console.log(
47
+ `[Dataset Labeling] Creating tasks for DataSet ${input.dataSetId} in LS project ${input.projectId}`
48
+ )
49
+
50
+ try {
51
+ // 1. Get DataSet configuration
52
+ const dataSet = await getRepository(DataSet).findOne({
53
+ where: { domain: { id: domain.id }, id: input.dataSetId }
54
+ })
55
+
56
+ if (!dataSet) {
57
+ throw new Error(`DataSet not found: ${input.dataSetId}`)
58
+ }
59
+
60
+ // 2. Query DataSamples
61
+ const queryBuilder = getRepository(DataSample)
62
+ .createQueryBuilder('sample')
63
+ .where('sample.domainId = :domainId', { domainId: domain.id })
64
+ .andWhere('sample.dataSetId = :dataSetId', { dataSetId: input.dataSetId })
65
+
66
+ if (input.sinceDate) {
67
+ queryBuilder.andWhere('sample.collectedAt >= :sinceDate', { sinceDate: input.sinceDate })
68
+ }
69
+
70
+ if (input.limit) {
71
+ queryBuilder.limit(input.limit)
72
+ }
73
+
74
+ queryBuilder.orderBy('sample.collectedAt', 'DESC')
75
+
76
+ const samples = await queryBuilder.getMany()
77
+
78
+ console.log(`[Dataset Labeling] Found ${samples.length} DataSamples to process`)
79
+
80
+ // 3. Create tasks for each sample
81
+ const result: TaskCreationResult = {
82
+ totalSamples: samples.length,
83
+ tasksCreated: 0,
84
+ tasksFailed: 0,
85
+ tasksSkipped: 0,
86
+ taskIds: [],
87
+ predictionsCreated: 0
88
+ }
89
+
90
+ // Get base URL for converting relative attachment paths to absolute URLs
91
+ const baseUrl = getBaseUrl(context)
92
+
93
+ for (const sample of samples) {
94
+ try {
95
+ // Extract all media URLs from DataSample (image, video, audio)
96
+ const mediaUrls = extractMediaUrls(sample, baseUrl)
97
+
98
+ if (Object.keys(mediaUrls).length === 0) {
99
+ console.log(`[Dataset Labeling] Skipping sample ${sample.id}: no media found`)
100
+ result.tasksSkipped++
101
+ continue
102
+ }
103
+
104
+ // Create Label Studio task with all media URLs
105
+ const task = await labelStudioApi.createTask(input.projectId, {
106
+ data: mediaUrls,
107
+ meta: {
108
+ dataSampleId: sample.id,
109
+ dataSampleName: sample.name,
110
+ dataSetId: input.dataSetId,
111
+ dataSetName: dataSet.name,
112
+ collectedAt: sample.collectedAt?.toISOString(),
113
+ source: 'things-factory-dataset'
114
+ }
115
+ })
116
+
117
+ result.taskIds.push(task.id)
118
+ result.tasksCreated++
119
+
120
+ console.log(`[Dataset Labeling] Created task ${task.id} for sample ${sample.id}`)
121
+
122
+ // 4. Auto-generate AI prediction if requested (for image detection)
123
+ if (input.autoGeneratePredictions && mediaUrls.image) {
124
+ try {
125
+ const modelClient = input.modelId
126
+ ? AIModelClientFactory.getClient(input.modelId)
127
+ : AIModelClientFactory.getDefaultClient()
128
+
129
+ // Use first image URL if multiple images
130
+ const imageUrl = Array.isArray(mediaUrls.image) ? mediaUrls.image[0] : mediaUrls.image
131
+
132
+ const objects = await modelClient.detectObjects(imageUrl, {
133
+ confidenceThreshold: input.confidenceThreshold
134
+ })
135
+
136
+ if (objects.length > 0) {
137
+ const labelStudioResult = this.convertToLabelStudioFormat(objects)
138
+ const avgConfidence = objects.reduce((sum, obj) => sum + obj.confidence, 0) / objects.length
139
+
140
+ await labelStudioApi.createPrediction({
141
+ task: task.id,
142
+ result: labelStudioResult,
143
+ score: avgConfidence,
144
+ model_version: input.modelId || 'default-model-v1.0'
145
+ })
146
+
147
+ result.predictionsCreated = (result.predictionsCreated || 0) + 1
148
+
149
+ console.log(`[Dataset Labeling] Created prediction for task ${task.id} (${objects.length} objects)`)
150
+ }
151
+ } catch (predError) {
152
+ console.error(`[Dataset Labeling] Failed to create prediction for task ${task.id}:`, predError)
153
+ // Continue even if prediction fails
154
+ }
155
+ }
156
+ } catch (taskError) {
157
+ console.error(`[Dataset Labeling] Failed to create task for sample ${sample.id}:`, taskError)
158
+ result.tasksFailed++
159
+ }
160
+ }
161
+
162
+ console.log(
163
+ `[Dataset Labeling] Completed: ${result.tasksCreated} created, ${result.tasksFailed} failed, ${result.tasksSkipped} skipped`
164
+ )
165
+
166
+ return result
167
+ } catch (error) {
168
+ console.error('[Dataset Labeling] Failed to create tasks:', error)
169
+ throw new Error(`Failed to create labeling tasks: ${error.message}`)
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Sync completed annotations from Label Studio back to DataSamples
175
+ */
176
+ @Mutation(returns => SyncAnnotationsResult, {
177
+ description: 'Sync Label Studio annotations back to DataSamples, updating judgment fields'
178
+ })
179
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
180
+ async syncAnnotationsToDataset(
181
+ @Arg('input') input: SyncAnnotationsRequest,
182
+ @Ctx() context: ResolverContext
183
+ ): Promise<SyncAnnotationsResult> {
184
+ const { domain } = context.state
185
+
186
+ console.log(`[Dataset Labeling] Syncing annotations from LS project ${input.projectId} to DataSet ${input.dataSetId}`)
187
+
188
+ try {
189
+ // 1. Get all tasks from Label Studio project
190
+ const tasksResponse = await labelStudioApi.getTasks(input.projectId, {
191
+ page_size: 1000 // Adjust as needed
192
+ })
193
+
194
+ const tasks = tasksResponse.tasks || tasksResponse.results || []
195
+
196
+ console.log(`[Dataset Labeling] Found ${tasks.length} tasks in Label Studio`)
197
+
198
+ // 2. Filter tasks related to this DataSet
199
+ const dataSetTasks = tasks.filter((task: any) => task.meta?.dataSetId === input.dataSetId)
200
+
201
+ console.log(`[Dataset Labeling] ${dataSetTasks.length} tasks belong to DataSet ${input.dataSetId}`)
202
+
203
+ const result: SyncAnnotationsResult = {
204
+ totalAnnotations: 0,
205
+ samplesUpdated: 0,
206
+ updatesFailed: 0,
207
+ skipped: 0
208
+ }
209
+
210
+ // 3. Process each task
211
+ for (const task of dataSetTasks) {
212
+ const dataSampleId = task.meta?.dataSampleId
213
+
214
+ if (!dataSampleId) {
215
+ result.skipped++
216
+ continue
217
+ }
218
+
219
+ // Get annotations for this task
220
+ const annotations = task.annotations || []
221
+
222
+ if (annotations.length === 0) {
223
+ result.skipped++
224
+ continue
225
+ }
226
+
227
+ // Filter by completion status if requested
228
+ let relevantAnnotations = annotations
229
+ if (input.completedOnly) {
230
+ relevantAnnotations = annotations.filter((ann: any) => ann.was_cancelled === false)
231
+ }
232
+
233
+ // Filter by date if requested
234
+ if (input.sinceDate) {
235
+ relevantAnnotations = relevantAnnotations.filter(
236
+ (ann: any) => new Date(ann.updated_at) >= input.sinceDate
237
+ )
238
+ }
239
+
240
+ if (relevantAnnotations.length === 0) {
241
+ result.skipped++
242
+ continue
243
+ }
244
+
245
+ result.totalAnnotations += relevantAnnotations.length
246
+
247
+ try {
248
+ // 4. Get DataSample
249
+ const sample = await getRepository(DataSample).findOne({
250
+ where: { domain: { id: domain.id }, id: dataSampleId }
251
+ })
252
+
253
+ if (!sample) {
254
+ console.log(`[Dataset Labeling] DataSample not found: ${dataSampleId}`)
255
+ result.updatesFailed++
256
+ continue
257
+ }
258
+
259
+ // 5. Convert annotations to judgment data
260
+ const latestAnnotation = relevantAnnotations[relevantAnnotations.length - 1]
261
+ const judgment = this.convertAnnotationToJudgment(latestAnnotation, task)
262
+
263
+ // 6. Update DataSample
264
+ sample.judgment = judgment
265
+
266
+ // Optionally update OOC/OOS flags based on annotation
267
+ // This is application-specific logic
268
+ if (judgment.defectsFound > 0) {
269
+ sample.ooc = true
270
+ }
271
+
272
+ await getRepository(DataSample).save(sample)
273
+
274
+ result.samplesUpdated++
275
+
276
+ console.log(`[Dataset Labeling] Updated DataSample ${dataSampleId} with annotation ${latestAnnotation.id}`)
277
+ } catch (updateError) {
278
+ console.error(`[Dataset Labeling] Failed to update DataSample ${dataSampleId}:`, updateError)
279
+ result.updatesFailed++
280
+ }
281
+ }
282
+
283
+ console.log(
284
+ `[Dataset Labeling] Sync completed: ${result.samplesUpdated} updated, ${result.updatesFailed} failed, ${result.skipped} skipped`
285
+ )
286
+
287
+ return result
288
+ } catch (error) {
289
+ console.error('[Dataset Labeling] Failed to sync annotations:', error)
290
+ throw new Error(`Failed to sync annotations: ${error.message}`)
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Generate AI predictions for existing DataSet samples
296
+ */
297
+ @Mutation(returns => BatchPredictionResult, {
298
+ description: 'Generate AI predictions for DataSet samples that already have Label Studio tasks'
299
+ })
300
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
301
+ async generatePredictionsForDataset(
302
+ @Arg('input') input: GeneratePredictionsForDatasetRequest,
303
+ @Ctx() context: ResolverContext
304
+ ): Promise<BatchPredictionResult> {
305
+ const { domain } = context.state
306
+
307
+ console.log(
308
+ `[Dataset Labeling] Generating predictions for DataSet ${input.dataSetId} in LS project ${input.projectId}`
309
+ )
310
+
311
+ try {
312
+ // 1. Get all tasks from Label Studio project
313
+ const tasksResponse = await labelStudioApi.getTasks(input.projectId, {
314
+ page_size: 1000
315
+ })
316
+
317
+ const tasks = tasksResponse.tasks || tasksResponse.results || []
318
+ const dataSetTasks = tasks.filter((task: any) => task.meta?.dataSetId === input.dataSetId)
319
+
320
+ console.log(`[Dataset Labeling] Found ${dataSetTasks.length} tasks for DataSet`)
321
+
322
+ const result: BatchPredictionResult = {
323
+ total: dataSetTasks.length,
324
+ succeeded: 0,
325
+ failed: 0,
326
+ results: [],
327
+ modelVersion: input.modelId || 'default-model-v1.0'
328
+ }
329
+
330
+ // 2. Get AI model client
331
+ const modelClient = input.modelId
332
+ ? AIModelClientFactory.getClient(input.modelId)
333
+ : AIModelClientFactory.getDefaultClient()
334
+
335
+ // 3. Process tasks in batches
336
+ const BATCH_SIZE = 5
337
+ for (let i = 0; i < dataSetTasks.length; i += BATCH_SIZE) {
338
+ const batch = dataSetTasks.slice(i, i + BATCH_SIZE)
339
+
340
+ const batchPromises = batch.map(async (task: any) => {
341
+ try {
342
+ // Check if prediction already exists
343
+ if (!input.forceRegenerate && task.predictions && task.predictions.length > 0) {
344
+ result.results.push({
345
+ taskId: task.id,
346
+ success: true,
347
+ objectCount: 0,
348
+ error: 'Prediction already exists (skipped)'
349
+ })
350
+ return
351
+ }
352
+
353
+ const imageUrl = task.data?.image
354
+
355
+ if (!imageUrl) {
356
+ result.failed++
357
+ result.results.push({
358
+ taskId: task.id,
359
+ success: false,
360
+ objectCount: 0,
361
+ error: 'No image URL in task data'
362
+ })
363
+ return
364
+ }
365
+
366
+ // Run AI inference
367
+ const objects = await modelClient.detectObjects(imageUrl, {
368
+ confidenceThreshold: input.confidenceThreshold
369
+ })
370
+
371
+ if (objects.length === 0) {
372
+ result.succeeded++
373
+ result.results.push({
374
+ taskId: task.id,
375
+ success: true,
376
+ objectCount: 0,
377
+ error: 'No objects detected'
378
+ })
379
+ return
380
+ }
381
+
382
+ // Create prediction
383
+ const labelStudioResult = this.convertToLabelStudioFormat(objects)
384
+ const avgConfidence = objects.reduce((sum, obj) => sum + obj.confidence, 0) / objects.length
385
+
386
+ const prediction = await labelStudioApi.createPrediction({
387
+ task: task.id,
388
+ result: labelStudioResult,
389
+ score: avgConfidence,
390
+ model_version: result.modelVersion
391
+ })
392
+
393
+ result.succeeded++
394
+ result.results.push({
395
+ taskId: task.id,
396
+ predictionId: prediction.id,
397
+ success: true,
398
+ objectCount: objects.length,
399
+ avgConfidence
400
+ })
401
+
402
+ console.log(`[Dataset Labeling] Created prediction for task ${task.id} (${objects.length} objects)`)
403
+ } catch (error) {
404
+ result.failed++
405
+ result.results.push({
406
+ taskId: task.id,
407
+ success: false,
408
+ objectCount: 0,
409
+ error: error.message
410
+ })
411
+ }
412
+ })
413
+
414
+ await Promise.all(batchPromises)
415
+ }
416
+
417
+ console.log(`[Dataset Labeling] Prediction generation completed: ${result.succeeded}/${result.total} succeeded`)
418
+
419
+ return result
420
+ } catch (error) {
421
+ console.error('[Dataset Labeling] Failed to generate predictions:', error)
422
+ throw new Error(`Failed to generate predictions: ${error.message}`)
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Query labeling status for a DataSet
428
+ */
429
+ @Query(returns => DatasetLabelingStatus, {
430
+ description: 'Get labeling status and progress for a DataSet'
431
+ })
432
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
433
+ async datasetLabelingStatus(
434
+ @Arg('input') input: DatasetLabelingStatusRequest,
435
+ @Ctx() context: ResolverContext
436
+ ): Promise<DatasetLabelingStatus> {
437
+ const { domain } = context.state
438
+
439
+ try {
440
+ // 1. Get DataSet
441
+ const dataSet = await getRepository(DataSet).findOne({
442
+ where: { domain: { id: domain.id }, id: input.dataSetId }
443
+ })
444
+
445
+ if (!dataSet) {
446
+ throw new Error(`DataSet not found: ${input.dataSetId}`)
447
+ }
448
+
449
+ // 2. Count total DataSamples
450
+ const totalSamples = await getRepository(DataSample).count({
451
+ where: { domain: { id: domain.id }, dataSetId: input.dataSetId }
452
+ })
453
+
454
+ // 3. Get Label Studio tasks if projectId provided
455
+ let tasksCreated = 0
456
+ let withPredictions = 0
457
+ let withAnnotations = 0
458
+ let annotationsCompleted = 0
459
+
460
+ if (input.projectId) {
461
+ const tasksResponse = await labelStudioApi.getTasks(input.projectId, {
462
+ page_size: 1000
463
+ })
464
+
465
+ const tasks = tasksResponse.tasks || tasksResponse.results || []
466
+ const dataSetTasks = tasks.filter((task: any) => task.meta?.dataSetId === input.dataSetId)
467
+
468
+ tasksCreated = dataSetTasks.length
469
+
470
+ for (const task of dataSetTasks) {
471
+ if (task.predictions && task.predictions.length > 0) {
472
+ withPredictions++
473
+ }
474
+
475
+ if (task.annotations && task.annotations.length > 0) {
476
+ withAnnotations++
477
+
478
+ const completedAnnotations = task.annotations.filter((ann: any) => ann.was_cancelled === false)
479
+ if (completedAnnotations.length > 0) {
480
+ annotationsCompleted++
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ const notProcessed = totalSamples - tasksCreated
487
+ const completionRate = totalSamples > 0 ? annotationsCompleted / totalSamples : 0
488
+
489
+ return {
490
+ dataSetId: input.dataSetId,
491
+ dataSetName: dataSet.name,
492
+ totalSamples,
493
+ tasksCreated,
494
+ withPredictions,
495
+ withAnnotations,
496
+ annotationsCompleted,
497
+ notProcessed,
498
+ completionRate,
499
+ projectId: input.projectId,
500
+ lastSyncedAt: new Date()
501
+ }
502
+ } catch (error) {
503
+ console.error('[Dataset Labeling] Failed to get status:', error)
504
+ throw new Error(`Failed to get labeling status: ${error.message}`)
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Helper: Extract image URL from DataSample
510
+ */
511
+ private extractImageUrl(sample: DataSample, imageField: string): string | null {
512
+ // Try data field first
513
+ if (sample.data && typeof sample.data === 'object') {
514
+ if (sample.data[imageField]) {
515
+ return sample.data[imageField]
516
+ }
517
+ }
518
+
519
+ // Try rawData field
520
+ if (sample.rawData) {
521
+ try {
522
+ const rawData = typeof sample.rawData === 'string' ? JSON.parse(sample.rawData) : sample.rawData
523
+ if (rawData[imageField]) {
524
+ return rawData[imageField]
525
+ }
526
+ // If rawData is just a string URL
527
+ if (typeof rawData === 'string' && rawData.startsWith('http')) {
528
+ return rawData
529
+ }
530
+ } catch (e) {
531
+ // If rawData is a plain URL string
532
+ if (typeof sample.rawData === 'string' && sample.rawData.startsWith('http')) {
533
+ return sample.rawData
534
+ }
535
+ }
536
+ }
537
+
538
+ return null
539
+ }
540
+
541
+ /**
542
+ * Helper: Convert AI detection results to Label Studio format
543
+ */
544
+ private convertToLabelStudioFormat(objects: any[]) {
545
+ return objects.map(obj => ({
546
+ from_name: 'label',
547
+ to_name: 'image',
548
+ type: 'rectanglelabels',
549
+ value: {
550
+ x: obj.bbox.x,
551
+ y: obj.bbox.y,
552
+ width: obj.bbox.width,
553
+ height: obj.bbox.height,
554
+ rectanglelabels: [obj.className]
555
+ }
556
+ }))
557
+ }
558
+
559
+ /**
560
+ * Helper: Convert Label Studio annotation to DataSample judgment format
561
+ */
562
+ private convertAnnotationToJudgment(annotation: any, task: any): any {
563
+ const result = annotation.result || []
564
+
565
+ const objects = result
566
+ .filter((item: any) => item.type === 'rectanglelabels')
567
+ .map((item: any) => ({
568
+ type: item.value.rectanglelabels?.[0] || 'unknown',
569
+ bbox: {
570
+ x: item.value.x,
571
+ y: item.value.y,
572
+ width: item.value.width,
573
+ height: item.value.height
574
+ },
575
+ source: 'human-annotation'
576
+ }))
577
+
578
+ return {
579
+ annotationId: annotation.id,
580
+ taskId: task.id,
581
+ annotatedBy: annotation.completed_by,
582
+ annotatedAt: annotation.updated_at,
583
+ objects,
584
+ objectCount: objects.length,
585
+ defectsFound: objects.length, // Application-specific logic
586
+ verified: true,
587
+ source: 'label-studio'
588
+ }
589
+ }
590
+ }