@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.
- package/CHANGELOG.md +85 -0
- package/EXTERNAL_DATA_SOURCING.md +484 -0
- package/IMPLEMENTATION_GUIDE.md +469 -0
- package/INTEGRATION.md +279 -0
- package/README.md +1014 -0
- package/SETUP_GUIDE.md +577 -0
- package/TEST_GUIDE.md +387 -0
- package/UI_CUSTOMIZATION.md +395 -0
- package/USER_SYNC_GUIDE.md +514 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +1 -0
- package/client/label-studio-label-page.ts +52 -0
- package/client/label-studio-project-create.ts +216 -0
- package/client/label-studio-project-list.ts +214 -0
- package/client/label-studio-wrapper.ts +294 -0
- package/client/route.ts +15 -0
- package/client/tsconfig.json +13 -0
- package/config/config.development.js +124 -0
- package/config/config.production.js +182 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/label-studio-label-page.d.ts +8 -0
- package/dist-client/label-studio-label-page.js +54 -0
- package/dist-client/label-studio-label-page.js.map +1 -0
- package/dist-client/label-studio-project-create.d.ts +16 -0
- package/dist-client/label-studio-project-create.js +235 -0
- package/dist-client/label-studio-project-create.js.map +1 -0
- package/dist-client/label-studio-project-list.d.ts +16 -0
- package/dist-client/label-studio-project-list.js +222 -0
- package/dist-client/label-studio-project-list.js.map +1 -0
- package/dist-client/label-studio-wrapper.d.ts +57 -0
- package/dist-client/label-studio-wrapper.js +304 -0
- package/dist-client/label-studio-wrapper.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +14 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
- package/dist-server/controller/label-studio-role-mapper.js +65 -0
- package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
- package/dist-server/controller/user-provisioning-service.d.ts +66 -0
- package/dist-server/controller/user-provisioning-service.js +264 -0
- package/dist-server/controller/user-provisioning-service.js.map +1 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +19 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/route/label-studio-sso.d.ts +2 -0
- package/dist-server/route/label-studio-sso.js +156 -0
- package/dist-server/route/label-studio-sso.js.map +1 -0
- package/dist-server/route/webhook.d.ts +65 -0
- package/dist-server/route/webhook.js +248 -0
- package/dist-server/route/webhook.js.map +1 -0
- package/dist-server/route.d.ts +1 -0
- package/dist-server/route.js +21 -0
- package/dist-server/route.js.map +1 -0
- package/dist-server/service/ai-prediction-service.d.ts +27 -0
- package/dist-server/service/ai-prediction-service.js +222 -0
- package/dist-server/service/ai-prediction-service.js.map +1 -0
- package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
- package/dist-server/service/dataset-labeling-integration.js +512 -0
- package/dist-server/service/dataset-labeling-integration.js.map +1 -0
- package/dist-server/service/external-data-source-service.d.ts +78 -0
- package/dist-server/service/external-data-source-service.js +415 -0
- package/dist-server/service/external-data-source-service.js.map +1 -0
- package/dist-server/service/index.d.ts +12 -0
- package/dist-server/service/index.js +27 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/label-studio-sso-service.d.ts +38 -0
- package/dist-server/service/label-studio-sso-service.js +98 -0
- package/dist-server/service/label-studio-sso-service.js.map +1 -0
- package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
- package/dist-server/service/ml/ml-backend-service.js +153 -0
- package/dist-server/service/ml/ml-backend-service.js.map +1 -0
- package/dist-server/service/prediction/prediction-management.d.ts +32 -0
- package/dist-server/service/prediction/prediction-management.js +299 -0
- package/dist-server/service/prediction/prediction-management.js.map +1 -0
- package/dist-server/service/project/project-management.d.ts +36 -0
- package/dist-server/service/project/project-management.js +309 -0
- package/dist-server/service/project/project-management.js.map +1 -0
- package/dist-server/service/task/task-management.d.ts +42 -0
- package/dist-server/service/task/task-management.js +372 -0
- package/dist-server/service/task/task-management.js.map +1 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
- package/dist-server/service/webhook/webhook-management.d.ts +21 -0
- package/dist-server/service/webhook/webhook-management.js +134 -0
- package/dist-server/service/webhook/webhook-management.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/types/dataset-labeling-types.d.ts +71 -0
- package/dist-server/types/dataset-labeling-types.js +259 -0
- package/dist-server/types/dataset-labeling-types.js.map +1 -0
- package/dist-server/types/label-studio-types.d.ts +128 -0
- package/dist-server/types/label-studio-types.js +494 -0
- package/dist-server/types/label-studio-types.js.map +1 -0
- package/dist-server/types/prediction-types.d.ts +39 -0
- package/dist-server/types/prediction-types.js +121 -0
- package/dist-server/types/prediction-types.js.map +1 -0
- package/dist-server/utils/annotation-exporter.d.ts +104 -0
- package/dist-server/utils/annotation-exporter.js +261 -0
- package/dist-server/utils/annotation-exporter.js.map +1 -0
- package/dist-server/utils/label-config-builder.d.ts +117 -0
- package/dist-server/utils/label-config-builder.js +286 -0
- package/dist-server/utils/label-config-builder.js.map +1 -0
- package/dist-server/utils/label-studio-api-client.d.ts +180 -0
- package/dist-server/utils/label-studio-api-client.js +401 -0
- package/dist-server/utils/label-studio-api-client.js.map +1 -0
- package/dist-server/utils/media-url-extractor.d.ts +45 -0
- package/dist-server/utils/media-url-extractor.js +152 -0
- package/dist-server/utils/media-url-extractor.js.map +1 -0
- package/dist-server/utils/task-transformer.d.ts +108 -0
- package/dist-server/utils/task-transformer.js +260 -0
- package/dist-server/utils/task-transformer.js.map +1 -0
- package/package.json +47 -0
- package/server/SERVER_STRUCTURE.md +351 -0
- package/server/controller/label-studio-role-mapper.ts +76 -0
- package/server/controller/user-provisioning-service.ts +340 -0
- package/server/index.ts +19 -0
- package/server/route/label-studio-sso.ts +194 -0
- package/server/route/webhook.ts +304 -0
- package/server/route.ts +35 -0
- package/server/service/ai-prediction-service.ts +239 -0
- package/server/service/dataset-labeling-integration.ts +590 -0
- package/server/service/external-data-source-service.ts +438 -0
- package/server/service/index.ts +24 -0
- package/server/service/label-studio-sso-service.ts +108 -0
- package/server/service/labeling-scenario-service.ts.deprecated +566 -0
- package/server/service/ml/ml-backend-service.ts +127 -0
- package/server/service/prediction/prediction-management.ts +281 -0
- package/server/service/project/project-management.ts +284 -0
- package/server/service/task/task-management.ts +363 -0
- package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
- package/server/service/webhook/webhook-management.ts +109 -0
- package/server/tsconfig.json +11 -0
- package/server/types/dataset-labeling-types.ts +181 -0
- package/server/types/global.d.ts +23 -0
- package/server/types/label-studio-types.ts +346 -0
- package/server/types/prediction-types.ts +86 -0
- package/server/types/scenario-types.ts.deprecated +362 -0
- package/server/utils/annotation-exporter.ts +340 -0
- package/server/utils/label-config-builder.ts +340 -0
- package/server/utils/label-studio-api-client.ts +487 -0
- package/server/utils/media-url-extractor.ts +193 -0
- package/server/utils/task-transformer.ts +342 -0
- package/test-ai-prediction.js +268 -0
- package/test-dataset-integration.js +449 -0
- package/test-simple.js +89 -0
- 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
|
+
}
|