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