@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,487 @@
1
+ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
2
+ import { config } from '@things-factory/env'
3
+
4
+ /**
5
+ * Label Studio API Client
6
+ * Provides methods to interact with Label Studio REST API
7
+ */
8
+ export class LabelStudioApiClient {
9
+ private client: AxiosInstance | null = null
10
+ private serverUrl: string | null = null
11
+ private apiToken: string | null = null
12
+
13
+ constructor() {
14
+ // Defer initialization until first use
15
+ }
16
+
17
+ private ensureInitialized() {
18
+ if (this.client) {
19
+ return
20
+ }
21
+
22
+ const labelStudioConfig = config.get('labelStudio', {})
23
+ this.serverUrl = labelStudioConfig.serverUrl || 'http://localhost:8080'
24
+ this.apiToken = labelStudioConfig.apiToken || ''
25
+
26
+ this.client = axios.create({
27
+ baseURL: this.serverUrl,
28
+ headers: {
29
+ Authorization: `Token ${this.apiToken}`,
30
+ 'Content-Type': 'application/json'
31
+ },
32
+ timeout: 30000
33
+ })
34
+ }
35
+
36
+ // ============================================================================
37
+ // Project Methods
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Get all projects
42
+ */
43
+ async getProjects(): Promise<any[]> {
44
+ this.ensureInitialized()
45
+ const response = await this.client!.get('/api/projects')
46
+ return response.data.results || response.data
47
+ }
48
+
49
+ /**
50
+ * Get project by ID
51
+ */
52
+ async getProject(projectId: number): Promise<any> {
53
+ this.ensureInitialized()
54
+ const response = await this.client!.get(`/api/projects/${projectId}`)
55
+ return response.data
56
+ }
57
+
58
+ /**
59
+ * Create new project
60
+ */
61
+ async createProject(data: {
62
+ title: string
63
+ description?: string
64
+ label_config: string
65
+ expert_instruction?: string
66
+ }): Promise<any> {
67
+ this.ensureInitialized()
68
+ const response = await this.client!.post('/api/projects', data)
69
+ return response.data
70
+ }
71
+
72
+ /**
73
+ * Update project
74
+ */
75
+ async updateProject(projectId: number, data: any): Promise<any> {
76
+ this.ensureInitialized()
77
+ const response = await this.client!.patch(`/api/projects/${projectId}`, data)
78
+ return response.data
79
+ }
80
+
81
+ /**
82
+ * Delete project
83
+ */
84
+ async deleteProject(projectId: number): Promise<void> {
85
+ this.ensureInitialized()
86
+ await this.client!.delete(`/api/projects/${projectId}`)
87
+ }
88
+
89
+ // ============================================================================
90
+ // Task Methods
91
+ // ============================================================================
92
+
93
+ /**
94
+ * Get tasks for a project
95
+ */
96
+ async getTasks(projectId: number, params?: { page?: number; page_size?: number }): Promise<any> {
97
+ this.ensureInitialized()
98
+ const response = await this.client!.get(`/api/projects/${projectId}/tasks`, { params })
99
+ return response.data
100
+ }
101
+
102
+ /**
103
+ * Get single task
104
+ */
105
+ async getTask(taskId: number): Promise<any> {
106
+ this.ensureInitialized()
107
+ const response = await this.client!.get(`/api/tasks/${taskId}`)
108
+ return response.data
109
+ }
110
+
111
+ /**
112
+ * Create task
113
+ */
114
+ async createTask(projectId: number, data: any): Promise<any> {
115
+ this.ensureInitialized()
116
+ const response = await this.client!.post(`/api/projects/${projectId}/tasks`, data)
117
+ return response.data
118
+ }
119
+
120
+ /**
121
+ * Import tasks in bulk
122
+ */
123
+ async importTasks(projectId: number, tasks: any[]): Promise<any> {
124
+ this.ensureInitialized()
125
+ const response = await this.client!.post(`/api/projects/${projectId}/import`, tasks, {
126
+ headers: {
127
+ 'Content-Type': 'application/json'
128
+ }
129
+ })
130
+ return response.data
131
+ }
132
+
133
+ /**
134
+ * Delete task
135
+ */
136
+ async deleteTask(taskId: number): Promise<void> {
137
+ this.ensureInitialized()
138
+ await this.client!.delete(`/api/tasks/${taskId}`)
139
+ }
140
+
141
+ // ============================================================================
142
+ // Annotation Methods
143
+ // ============================================================================
144
+
145
+ /**
146
+ * Get annotations for a task
147
+ */
148
+ async getAnnotations(taskId: number): Promise<any[]> {
149
+ this.ensureInitialized()
150
+ const response = await this.client!.get(`/api/tasks/${taskId}/annotations`)
151
+ return response.data
152
+ }
153
+
154
+ /**
155
+ * Create annotation
156
+ */
157
+ async createAnnotation(taskId: number, annotation: any): Promise<any> {
158
+ this.ensureInitialized()
159
+ const response = await this.client!.post(`/api/tasks/${taskId}/annotations`, annotation)
160
+ return response.data
161
+ }
162
+
163
+ /**
164
+ * Update annotation
165
+ */
166
+ async updateAnnotation(annotationId: number, data: any): Promise<any> {
167
+ this.ensureInitialized()
168
+ const response = await this.client!.patch(`/api/annotations/${annotationId}`, data)
169
+ return response.data
170
+ }
171
+
172
+ /**
173
+ * Delete annotation
174
+ */
175
+ async deleteAnnotation(annotationId: number): Promise<void> {
176
+ this.ensureInitialized()
177
+ await this.client!.delete(`/api/annotations/${annotationId}`)
178
+ }
179
+
180
+ // ============================================================================
181
+ // Prediction Methods
182
+ // ============================================================================
183
+
184
+ /**
185
+ * Get all predictions for a task
186
+ */
187
+ async getPredictions(taskId: number): Promise<any[]> {
188
+ this.ensureInitialized()
189
+ const response = await this.client!.get('/api/predictions', {
190
+ params: { task: taskId }
191
+ })
192
+ return response.data.results || response.data
193
+ }
194
+
195
+ /**
196
+ * Get predictions for a project
197
+ */
198
+ async getProjectPredictions(projectId: number): Promise<any[]> {
199
+ this.ensureInitialized()
200
+ const response = await this.client!.get('/api/predictions', {
201
+ params: { project: projectId }
202
+ })
203
+ return response.data.results || response.data
204
+ }
205
+
206
+ /**
207
+ * Get single prediction by ID
208
+ */
209
+ async getPrediction(predictionId: number): Promise<any> {
210
+ this.ensureInitialized()
211
+ const response = await this.client!.get(`/api/predictions/${predictionId}`)
212
+ return response.data
213
+ }
214
+
215
+ /**
216
+ * Create a prediction for a task
217
+ */
218
+ async createPrediction(data: {
219
+ task: number
220
+ result: any[]
221
+ score?: number
222
+ model_version?: string
223
+ }): Promise<any> {
224
+ this.ensureInitialized()
225
+ const response = await this.client!.post('/api/predictions', data)
226
+ return response.data
227
+ }
228
+
229
+ /**
230
+ * Create predictions in bulk
231
+ */
232
+ async createPredictions(predictions: Array<{
233
+ task: number
234
+ result: any[]
235
+ score?: number
236
+ model_version?: string
237
+ }>): Promise<any> {
238
+ this.ensureInitialized()
239
+ const results = await Promise.all(
240
+ predictions.map(prediction => this.createPrediction(prediction))
241
+ )
242
+ return {
243
+ created: results.length,
244
+ predictions: results
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Update prediction
250
+ */
251
+ async updatePrediction(predictionId: number, data: any): Promise<any> {
252
+ this.ensureInitialized()
253
+ const response = await this.client!.patch(`/api/predictions/${predictionId}`, data)
254
+ return response.data
255
+ }
256
+
257
+ /**
258
+ * Delete prediction
259
+ */
260
+ async deletePrediction(predictionId: number): Promise<void> {
261
+ this.ensureInitialized()
262
+ await this.client!.delete(`/api/predictions/${predictionId}`)
263
+ }
264
+
265
+ /**
266
+ * Import predictions for a project
267
+ * This is useful for bulk importing AI model predictions
268
+ */
269
+ async importPredictions(projectId: number, predictions: any[]): Promise<any> {
270
+ this.ensureInitialized()
271
+ const response = await this.client!.post(`/api/projects/${projectId}/import/predictions`, predictions)
272
+ return response.data
273
+ }
274
+
275
+ // ============================================================================
276
+ // Export Methods
277
+ // ============================================================================
278
+
279
+ /**
280
+ * Export annotations
281
+ */
282
+ async exportAnnotations(
283
+ projectId: number,
284
+ format: 'JSON' | 'JSON_MIN' | 'CSV' | 'TSV' | 'CONLL2003' | 'COCO' | 'VOC' | 'YOLO' = 'JSON'
285
+ ): Promise<any> {
286
+ this.ensureInitialized()
287
+ const response = await this.client!.get(`/api/projects/${projectId}/export`, {
288
+ params: { exportType: format }
289
+ })
290
+ return response.data
291
+ }
292
+
293
+ /**
294
+ * Get export files
295
+ */
296
+ async getExportFiles(projectId: number): Promise<any[]> {
297
+ this.ensureInitialized()
298
+ const response = await this.client!.get(`/api/projects/${projectId}/exports`)
299
+ return response.data
300
+ }
301
+
302
+ // ============================================================================
303
+ // ML Backend Methods
304
+ // ============================================================================
305
+
306
+ /**
307
+ * Get ML backends for project
308
+ */
309
+ async getMLBackends(projectId: number): Promise<any[]> {
310
+ this.ensureInitialized()
311
+ const response = await this.client!.get(`/api/ml`, {
312
+ params: { project: projectId }
313
+ })
314
+ return response.data.results || response.data
315
+ }
316
+
317
+ /**
318
+ * Add ML backend to project
319
+ */
320
+ async addMLBackend(data: {
321
+ project: number
322
+ url: string
323
+ title: string
324
+ is_interactive?: boolean
325
+ }): Promise<any> {
326
+ this.ensureInitialized()
327
+ const response = await this.client!.post('/api/ml', data)
328
+ return response.data
329
+ }
330
+
331
+ /**
332
+ * Delete ML backend
333
+ */
334
+ async deleteMLBackend(mlBackendId: number): Promise<void> {
335
+ this.ensureInitialized()
336
+ await this.client!.delete(`/api/ml/${mlBackendId}`)
337
+ }
338
+
339
+ /**
340
+ * Trigger predictions for tasks
341
+ */
342
+ async triggerPredictions(projectId: number, taskIds?: number[]): Promise<any> {
343
+ this.ensureInitialized()
344
+ const payload = taskIds ? { task_ids: taskIds } : {}
345
+ const response = await this.client!.post(`/api/projects/${projectId}/predict`, payload)
346
+ return response.data
347
+ }
348
+
349
+ /**
350
+ * Train ML model
351
+ */
352
+ async trainModel(mlBackendId: number): Promise<any> {
353
+ this.ensureInitialized()
354
+ const response = await this.client!.post(`/api/ml/${mlBackendId}/train`)
355
+ return response.data
356
+ }
357
+
358
+ // ============================================================================
359
+ // Statistics Methods
360
+ // ============================================================================
361
+
362
+ /**
363
+ * Get project statistics
364
+ */
365
+ async getProjectStats(projectId: number): Promise<any> {
366
+ this.ensureInitialized()
367
+ const response = await this.client!.get(`/api/projects/${projectId}`)
368
+ const project = response.data
369
+
370
+ // Get detailed statistics
371
+ const tasksResponse = await this.client!.get(`/api/projects/${projectId}/tasks`, {
372
+ params: { page_size: 1 }
373
+ })
374
+
375
+ return {
376
+ totalTasks: project.task_number || 0,
377
+ completedTasks: project.finished_task_number || 0,
378
+ totalAnnotations: project.total_annotations_number || 0,
379
+ avgAnnotationsPerTask:
380
+ project.task_number > 0 ? (project.total_annotations_number || 0) / project.task_number : 0,
381
+ completionRate: project.task_number > 0 ? (project.finished_task_number || 0) / project.task_number : 0
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Get annotator statistics
387
+ */
388
+ async getAnnotatorStats(projectId: number): Promise<any[]> {
389
+ this.ensureInitialized()
390
+ const response = await this.client!.get(`/api/projects/${projectId}/annotations`)
391
+ const annotations = response.data.results || response.data
392
+
393
+ // Group by annotator
394
+ const statsByUser: { [email: string]: any } = {}
395
+
396
+ for (const annotation of annotations) {
397
+ const email = annotation.completed_by?.email || 'unknown'
398
+
399
+ if (!statsByUser[email]) {
400
+ statsByUser[email] = {
401
+ email,
402
+ annotationCount: 0,
403
+ totalTime: 0,
404
+ lastAnnotationDate: null
405
+ }
406
+ }
407
+
408
+ statsByUser[email].annotationCount++
409
+ if (annotation.lead_time) {
410
+ statsByUser[email].totalTime += annotation.lead_time
411
+ }
412
+
413
+ const annotationDate = new Date(annotation.created_at)
414
+ if (
415
+ !statsByUser[email].lastAnnotationDate ||
416
+ annotationDate > new Date(statsByUser[email].lastAnnotationDate)
417
+ ) {
418
+ statsByUser[email].lastAnnotationDate = annotation.created_at
419
+ }
420
+ }
421
+
422
+ return Object.values(statsByUser).map(stat => ({
423
+ ...stat,
424
+ avgTime: stat.annotationCount > 0 ? stat.totalTime / stat.annotationCount : 0
425
+ }))
426
+ }
427
+
428
+ // ============================================================================
429
+ // Webhook Methods
430
+ // ============================================================================
431
+
432
+ /**
433
+ * Create webhook
434
+ */
435
+ async createWebhook(data: {
436
+ project: number
437
+ url: string
438
+ send_payload?: boolean
439
+ send_for_all_actions?: boolean
440
+ headers?: { [key: string]: string }
441
+ }): Promise<any> {
442
+ this.ensureInitialized()
443
+ const response = await this.client!.post('/api/webhooks', data)
444
+ return response.data
445
+ }
446
+
447
+ /**
448
+ * Get webhooks for project
449
+ */
450
+ async getWebhooks(projectId: number): Promise<any[]> {
451
+ this.ensureInitialized()
452
+ const response = await this.client!.get('/api/webhooks', {
453
+ params: { project: projectId }
454
+ })
455
+ return response.data.results || response.data
456
+ }
457
+
458
+ /**
459
+ * Delete webhook
460
+ */
461
+ async deleteWebhook(webhookId: number): Promise<void> {
462
+ this.ensureInitialized()
463
+ await this.client!.delete(`/api/webhooks/${webhookId}`)
464
+ }
465
+ }
466
+
467
+ // Singleton instance (lazy initialization)
468
+ let _instance: LabelStudioApiClient | null = null
469
+
470
+ function getLabelStudioApiClient(): LabelStudioApiClient {
471
+ if (!_instance) {
472
+ _instance = new LabelStudioApiClient()
473
+ }
474
+ return _instance
475
+ }
476
+
477
+ // Export singleton accessor
478
+ export const labelStudioApi = new Proxy({} as LabelStudioApiClient, {
479
+ get(_target, prop) {
480
+ const instance = getLabelStudioApiClient()
481
+ const value = (instance as any)[prop]
482
+ if (typeof value === 'function') {
483
+ return value.bind(instance)
484
+ }
485
+ return value
486
+ }
487
+ })
@@ -0,0 +1,193 @@
1
+ import { DataSample } from '@things-factory/dataset'
2
+
3
+ /**
4
+ * Media URL Extractor
5
+ *
6
+ * Extracts media URLs (image, video, audio) from DataSample for Label Studio tasks
7
+ */
8
+
9
+ export interface MediaUrls {
10
+ [fieldName: string]: string | string[]
11
+ }
12
+
13
+ export interface AttachmentInfo {
14
+ id: string
15
+ mimetype: string
16
+ name: string
17
+ fullpath: string
18
+ }
19
+
20
+ /**
21
+ * Extract all media URLs from a DataSample
22
+ * Returns an object with field names as keys and URLs as values
23
+ *
24
+ * @param sample - DataSample containing media attachments
25
+ * @param baseUrl - Base URL for converting relative paths to absolute URLs (e.g., 'https://tf.example.com')
26
+ * @param mediaFields - Optional array of field names to extract. If not provided, extracts all fields.
27
+ * @returns Object with media URLs keyed by field name
28
+ */
29
+ export function extractMediaUrls(
30
+ sample: DataSample,
31
+ baseUrl: string,
32
+ mediaFields?: string[]
33
+ ): MediaUrls {
34
+ const urls: MediaUrls = {}
35
+
36
+ if (!sample.data || typeof sample.data !== 'object') {
37
+ return urls
38
+ }
39
+
40
+ const fieldsToExtract = mediaFields || Object.keys(sample.data)
41
+
42
+ for (const fieldName of fieldsToExtract) {
43
+ const fieldValue = sample.data[fieldName]
44
+
45
+ if (!fieldValue) continue
46
+
47
+ // Handle array of attachments
48
+ if (Array.isArray(fieldValue)) {
49
+ const extractedUrls = extractUrlsFromAttachmentArray(fieldValue, baseUrl)
50
+ if (extractedUrls.length > 0) {
51
+ urls[fieldName] = extractedUrls.length === 1 ? extractedUrls[0] : extractedUrls
52
+ }
53
+ }
54
+ // Handle single URL string
55
+ else if (typeof fieldValue === 'string') {
56
+ const url = normalizeUrl(fieldValue, baseUrl)
57
+ if (url) {
58
+ urls[fieldName] = url
59
+ }
60
+ }
61
+ // Handle single attachment object
62
+ else if (typeof fieldValue === 'object' && 'fullpath' in fieldValue) {
63
+ const url = normalizeUrl(fieldValue.fullpath, baseUrl)
64
+ if (url) {
65
+ urls[fieldName] = url
66
+ }
67
+ }
68
+ }
69
+
70
+ return urls
71
+ }
72
+
73
+ /**
74
+ * Extract URL from DataSample for a specific field
75
+ * Handles both single values and arrays
76
+ */
77
+ export function extractMediaUrl(
78
+ sample: DataSample,
79
+ fieldName: string,
80
+ baseUrl: string
81
+ ): string | string[] | null {
82
+ if (!sample.data || typeof sample.data !== 'object') {
83
+ return null
84
+ }
85
+
86
+ const fieldValue = sample.data[fieldName]
87
+
88
+ if (!fieldValue) return null
89
+
90
+ // Handle array of attachments
91
+ if (Array.isArray(fieldValue)) {
92
+ const urls = extractUrlsFromAttachmentArray(fieldValue, baseUrl)
93
+ return urls.length > 0 ? (urls.length === 1 ? urls[0] : urls) : null
94
+ }
95
+
96
+ // Handle single URL string
97
+ if (typeof fieldValue === 'string') {
98
+ return normalizeUrl(fieldValue, baseUrl)
99
+ }
100
+
101
+ // Handle single attachment object
102
+ if (typeof fieldValue === 'object' && 'fullpath' in fieldValue) {
103
+ return normalizeUrl(fieldValue.fullpath, baseUrl)
104
+ }
105
+
106
+ return null
107
+ }
108
+
109
+ /**
110
+ * Extract URLs from an array of attachments
111
+ * Handles nested arrays created by create-data-sample.ts
112
+ */
113
+ function extractUrlsFromAttachmentArray(arr: any[], baseUrl: string): string[] {
114
+ const urls: string[] = []
115
+
116
+ for (const item of arr) {
117
+ if (Array.isArray(item)) {
118
+ // Nested array - recursively extract
119
+ urls.push(...extractUrlsFromAttachmentArray(item, baseUrl))
120
+ } else if (item && typeof item === 'object' && 'fullpath' in item) {
121
+ // Attachment object
122
+ const url = normalizeUrl(item.fullpath, baseUrl)
123
+ if (url) urls.push(url)
124
+ } else if (typeof item === 'string') {
125
+ // Direct URL string
126
+ const url = normalizeUrl(item, baseUrl)
127
+ if (url) urls.push(url)
128
+ }
129
+ }
130
+
131
+ return urls
132
+ }
133
+
134
+ /**
135
+ * Normalize URL - convert relative paths to absolute URLs
136
+ *
137
+ * @param path - Relative path (e.g., '/attachments/2024/01/image.jpg') or absolute URL
138
+ * @param baseUrl - Base URL (e.g., 'https://tf.example.com')
139
+ * @returns Absolute URL or null if invalid
140
+ */
141
+ function normalizeUrl(path: string | undefined, baseUrl: string): string | null {
142
+ if (!path) return null
143
+
144
+ // Already absolute URL
145
+ if (path.startsWith('http://') || path.startsWith('https://')) {
146
+ return path
147
+ }
148
+
149
+ // Relative path - convert to absolute
150
+ if (path.startsWith('/')) {
151
+ const cleanBaseUrl = baseUrl.replace(/\/+$/, '') // Remove trailing slashes
152
+ return `${cleanBaseUrl}${path}`
153
+ }
154
+
155
+ // Invalid format
156
+ return null
157
+ }
158
+
159
+ /**
160
+ * Build Label Studio task data from DataSample
161
+ * Automatically extracts all media fields and converts to absolute URLs
162
+ *
163
+ * @param sample - DataSample with media attachments
164
+ * @param baseUrl - Base URL for the Things-Factory server
165
+ * @param mediaFields - Optional list of media field names to include
166
+ * @returns Object ready for Label Studio task creation
167
+ */
168
+ export function buildLabelStudioTaskData(
169
+ sample: DataSample,
170
+ baseUrl: string,
171
+ mediaFields?: string[]
172
+ ): Record<string, any> {
173
+ return extractMediaUrls(sample, baseUrl, mediaFields)
174
+ }
175
+
176
+ /**
177
+ * Helper: Get base URL from context or environment
178
+ * Falls back to environment variable or localhost
179
+ */
180
+ export function getBaseUrl(context?: any): string {
181
+ // Try from context (request)
182
+ if (context?.state?.domain?.systemUrl) {
183
+ return context.state.domain.systemUrl
184
+ }
185
+
186
+ // Try from environment
187
+ if (process.env.THINGS_FACTORY_URL) {
188
+ return process.env.THINGS_FACTORY_URL
189
+ }
190
+
191
+ // Fallback
192
+ return 'http://localhost:3000'
193
+ }