@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,362 @@
|
|
|
1
|
+
import { Field, InputType, ObjectType, Int, Float, registerEnumType } from 'type-graphql'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Labeling Workflow Scenario Types
|
|
5
|
+
*
|
|
6
|
+
* Defines end-to-end labeling workflows with multiple steps
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Enums
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export enum ScenarioStepType {
|
|
14
|
+
ImportData = 'import_data',
|
|
15
|
+
GeneratePredictions = 'generate_predictions',
|
|
16
|
+
WaitForAnnotations = 'wait_for_annotations',
|
|
17
|
+
SyncAnnotations = 'sync_annotations',
|
|
18
|
+
ValidateQuality = 'validate_quality',
|
|
19
|
+
ExportResults = 'export_results',
|
|
20
|
+
Notification = 'notification'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
registerEnumType(ScenarioStepType, {
|
|
24
|
+
name: 'ScenarioStepType',
|
|
25
|
+
description: 'Type of workflow step'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export enum ScenarioStatus {
|
|
29
|
+
Draft = 'draft',
|
|
30
|
+
Active = 'active',
|
|
31
|
+
Paused = 'paused',
|
|
32
|
+
Completed = 'completed',
|
|
33
|
+
Failed = 'failed'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
registerEnumType(ScenarioStatus, {
|
|
37
|
+
name: 'ScenarioStatus',
|
|
38
|
+
description: 'Status of workflow scenario'
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export enum TriggerType {
|
|
42
|
+
Manual = 'manual',
|
|
43
|
+
Schedule = 'schedule',
|
|
44
|
+
DatasetUpdate = 'dataset_update',
|
|
45
|
+
ExternalEvent = 'external_event'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
registerEnumType(TriggerType, {
|
|
49
|
+
name: 'TriggerType',
|
|
50
|
+
description: 'How the scenario is triggered'
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Scenario Step Types
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
@InputType()
|
|
58
|
+
export class ImportDataStepConfig {
|
|
59
|
+
@Field({ description: 'Data source type: dataset, api, file' })
|
|
60
|
+
sourceType: 'dataset' | 'api' | 'file'
|
|
61
|
+
|
|
62
|
+
@Field({ nullable: true, description: 'DataSet ID if sourceType is dataset' })
|
|
63
|
+
dataSetId?: string
|
|
64
|
+
|
|
65
|
+
@Field({ nullable: true, description: 'External source URL if sourceType is api/file' })
|
|
66
|
+
sourceUrl?: string
|
|
67
|
+
|
|
68
|
+
@Field({ nullable: true, description: 'Image field name' })
|
|
69
|
+
imageField?: string
|
|
70
|
+
|
|
71
|
+
@Field({ nullable: true, description: 'Maximum items to import' })
|
|
72
|
+
limit?: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@InputType()
|
|
76
|
+
export class GeneratePredictionsStepConfig {
|
|
77
|
+
@Field({ nullable: true, description: 'AI model ID to use' })
|
|
78
|
+
modelId?: string
|
|
79
|
+
|
|
80
|
+
@Field(type => Float, { nullable: true, description: 'Confidence threshold' })
|
|
81
|
+
confidenceThreshold?: number
|
|
82
|
+
|
|
83
|
+
@Field({ nullable: true, defaultValue: false, description: 'Force regenerate existing predictions' })
|
|
84
|
+
forceRegenerate?: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@InputType()
|
|
88
|
+
export class WaitForAnnotationsStepConfig {
|
|
89
|
+
@Field(type => Int, { nullable: true, description: 'Minimum number of annotations required' })
|
|
90
|
+
minAnnotations?: number
|
|
91
|
+
|
|
92
|
+
@Field(type => Float, { nullable: true, description: 'Minimum completion rate (0-1)' })
|
|
93
|
+
minCompletionRate?: number
|
|
94
|
+
|
|
95
|
+
@Field(type => Int, { nullable: true, description: 'Maximum wait time in minutes' })
|
|
96
|
+
maxWaitMinutes?: number
|
|
97
|
+
|
|
98
|
+
@Field({ nullable: true, defaultValue: true, description: 'Auto-proceed when criteria met' })
|
|
99
|
+
autoProceed?: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@InputType()
|
|
103
|
+
export class SyncAnnotationsStepConfig {
|
|
104
|
+
@Field({ nullable: true, defaultValue: true, description: 'Only sync completed annotations' })
|
|
105
|
+
completedOnly?: boolean
|
|
106
|
+
|
|
107
|
+
@Field({ nullable: true, description: 'Sync annotations updated after this date' })
|
|
108
|
+
sinceDate?: Date
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@InputType()
|
|
112
|
+
export class ValidateQualityStepConfig {
|
|
113
|
+
@Field(type => Float, { nullable: true, description: 'Minimum required quality score (0-1)' })
|
|
114
|
+
minQualityScore?: number
|
|
115
|
+
|
|
116
|
+
@Field({ nullable: true, description: 'Validation rules in JSON format' })
|
|
117
|
+
validationRules?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@InputType()
|
|
121
|
+
export class NotificationStepConfig {
|
|
122
|
+
@Field({ description: 'Notification message template' })
|
|
123
|
+
message: string
|
|
124
|
+
|
|
125
|
+
@Field(type => [String], { nullable: true, description: 'Email recipients' })
|
|
126
|
+
recipients?: string[]
|
|
127
|
+
|
|
128
|
+
@Field({ nullable: true, description: 'Webhook URL to call' })
|
|
129
|
+
webhookUrl?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Scenario Step Definition
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
@InputType()
|
|
137
|
+
export class ScenarioStepInput {
|
|
138
|
+
@Field({ description: 'Step name' })
|
|
139
|
+
name: string
|
|
140
|
+
|
|
141
|
+
@Field(type => ScenarioStepType, { description: 'Step type' })
|
|
142
|
+
type: ScenarioStepType
|
|
143
|
+
|
|
144
|
+
@Field({ description: 'Step configuration as JSON string' })
|
|
145
|
+
config: string
|
|
146
|
+
|
|
147
|
+
@Field({ nullable: true, description: 'Condition to execute this step (JSON logic)' })
|
|
148
|
+
condition?: string
|
|
149
|
+
|
|
150
|
+
@Field({ nullable: true, defaultValue: true, description: 'Continue on error' })
|
|
151
|
+
continueOnError?: boolean
|
|
152
|
+
|
|
153
|
+
@Field(type => Int, { nullable: true, description: 'Max retry attempts' })
|
|
154
|
+
maxRetries?: number
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@ObjectType()
|
|
158
|
+
export class ScenarioStep {
|
|
159
|
+
@Field({ description: 'Step name' })
|
|
160
|
+
name: string
|
|
161
|
+
|
|
162
|
+
@Field(type => ScenarioStepType, { description: 'Step type' })
|
|
163
|
+
type: ScenarioStepType
|
|
164
|
+
|
|
165
|
+
@Field({ description: 'Step configuration' })
|
|
166
|
+
config: string
|
|
167
|
+
|
|
168
|
+
@Field({ nullable: true, description: 'Execution condition' })
|
|
169
|
+
condition?: string
|
|
170
|
+
|
|
171
|
+
@Field({ defaultValue: true, description: 'Continue on error' })
|
|
172
|
+
continueOnError: boolean
|
|
173
|
+
|
|
174
|
+
@Field(type => Int, { nullable: true, description: 'Max retry attempts' })
|
|
175
|
+
maxRetries?: number
|
|
176
|
+
|
|
177
|
+
@Field(type => Int, { description: 'Step order' })
|
|
178
|
+
order: number
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Scenario Definition
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
@InputType()
|
|
186
|
+
export class CreateScenarioRequest {
|
|
187
|
+
@Field({ description: 'Scenario name' })
|
|
188
|
+
name: string
|
|
189
|
+
|
|
190
|
+
@Field({ nullable: true, description: 'Scenario description' })
|
|
191
|
+
description?: string
|
|
192
|
+
|
|
193
|
+
@Field(type => Int, { description: 'Label Studio project ID' })
|
|
194
|
+
projectId: number
|
|
195
|
+
|
|
196
|
+
@Field(type => TriggerType, { description: 'How to trigger this scenario' })
|
|
197
|
+
triggerType: TriggerType
|
|
198
|
+
|
|
199
|
+
@Field({ nullable: true, description: 'Trigger configuration (schedule, event, etc.)' })
|
|
200
|
+
triggerConfig?: string
|
|
201
|
+
|
|
202
|
+
@Field(type => [ScenarioStepInput], { description: 'Workflow steps' })
|
|
203
|
+
steps: ScenarioStepInput[]
|
|
204
|
+
|
|
205
|
+
@Field({ nullable: true, defaultValue: true, description: 'Auto-start scenario after creation' })
|
|
206
|
+
autoStart?: boolean
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@ObjectType()
|
|
210
|
+
export class LabelingScenario {
|
|
211
|
+
@Field({ description: 'Scenario ID' })
|
|
212
|
+
id: string
|
|
213
|
+
|
|
214
|
+
@Field({ description: 'Scenario name' })
|
|
215
|
+
name: string
|
|
216
|
+
|
|
217
|
+
@Field({ nullable: true, description: 'Description' })
|
|
218
|
+
description?: string
|
|
219
|
+
|
|
220
|
+
@Field(type => Int, { description: 'Label Studio project ID' })
|
|
221
|
+
projectId: number
|
|
222
|
+
|
|
223
|
+
@Field(type => TriggerType, { description: 'Trigger type' })
|
|
224
|
+
triggerType: TriggerType
|
|
225
|
+
|
|
226
|
+
@Field({ nullable: true, description: 'Trigger configuration' })
|
|
227
|
+
triggerConfig?: string
|
|
228
|
+
|
|
229
|
+
@Field(type => [ScenarioStep], { description: 'Workflow steps' })
|
|
230
|
+
steps: ScenarioStep[]
|
|
231
|
+
|
|
232
|
+
@Field(type => ScenarioStatus, { description: 'Current status' })
|
|
233
|
+
status: ScenarioStatus
|
|
234
|
+
|
|
235
|
+
@Field({ nullable: true, description: 'Last execution time' })
|
|
236
|
+
lastExecutedAt?: Date
|
|
237
|
+
|
|
238
|
+
@Field({ nullable: true, description: 'Next scheduled execution' })
|
|
239
|
+
nextExecutionAt?: Date
|
|
240
|
+
|
|
241
|
+
@Field({ description: 'Created at' })
|
|
242
|
+
createdAt: Date
|
|
243
|
+
|
|
244
|
+
@Field({ description: 'Updated at' })
|
|
245
|
+
updatedAt: Date
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Scenario Execution
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
@InputType()
|
|
253
|
+
export class ExecuteScenarioRequest {
|
|
254
|
+
@Field({ description: 'Scenario ID to execute' })
|
|
255
|
+
scenarioId: string
|
|
256
|
+
|
|
257
|
+
@Field({ nullable: true, description: 'Override parameters as JSON' })
|
|
258
|
+
parameters?: string
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@ObjectType()
|
|
262
|
+
export class ScenarioExecutionStep {
|
|
263
|
+
@Field({ description: 'Step name' })
|
|
264
|
+
stepName: string
|
|
265
|
+
|
|
266
|
+
@Field(type => ScenarioStepType, { description: 'Step type' })
|
|
267
|
+
stepType: ScenarioStepType
|
|
268
|
+
|
|
269
|
+
@Field({ description: 'Step status: pending, running, completed, failed, skipped' })
|
|
270
|
+
status: string
|
|
271
|
+
|
|
272
|
+
@Field({ nullable: true, description: 'Step output data' })
|
|
273
|
+
output?: string
|
|
274
|
+
|
|
275
|
+
@Field({ nullable: true, description: 'Error message if failed' })
|
|
276
|
+
error?: string
|
|
277
|
+
|
|
278
|
+
@Field({ nullable: true, description: 'Started at' })
|
|
279
|
+
startedAt?: Date
|
|
280
|
+
|
|
281
|
+
@Field({ nullable: true, description: 'Completed at' })
|
|
282
|
+
completedAt?: Date
|
|
283
|
+
|
|
284
|
+
@Field(type => Int, { nullable: true, description: 'Duration in milliseconds' })
|
|
285
|
+
durationMs?: number
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@ObjectType()
|
|
289
|
+
export class ScenarioExecution {
|
|
290
|
+
@Field({ description: 'Execution ID' })
|
|
291
|
+
id: string
|
|
292
|
+
|
|
293
|
+
@Field({ description: 'Scenario ID' })
|
|
294
|
+
scenarioId: string
|
|
295
|
+
|
|
296
|
+
@Field({ description: 'Scenario name' })
|
|
297
|
+
scenarioName: string
|
|
298
|
+
|
|
299
|
+
@Field({ description: 'Execution status: running, completed, failed' })
|
|
300
|
+
status: string
|
|
301
|
+
|
|
302
|
+
@Field(type => [ScenarioExecutionStep], { description: 'Step executions' })
|
|
303
|
+
steps: ScenarioExecutionStep[]
|
|
304
|
+
|
|
305
|
+
@Field({ nullable: true, description: 'Overall result summary' })
|
|
306
|
+
summary?: string
|
|
307
|
+
|
|
308
|
+
@Field({ description: 'Started at' })
|
|
309
|
+
startedAt: Date
|
|
310
|
+
|
|
311
|
+
@Field({ nullable: true, description: 'Completed at' })
|
|
312
|
+
completedAt?: Date
|
|
313
|
+
|
|
314
|
+
@Field(type => Int, { nullable: true, description: 'Total duration in milliseconds' })
|
|
315
|
+
totalDurationMs?: number
|
|
316
|
+
|
|
317
|
+
@Field({ nullable: true, description: 'Error message if failed' })
|
|
318
|
+
error?: string
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@ObjectType()
|
|
322
|
+
export class ScenarioExecutionResult {
|
|
323
|
+
@Field({ description: 'Execution ID' })
|
|
324
|
+
executionId: string
|
|
325
|
+
|
|
326
|
+
@Field({ description: 'Execution status' })
|
|
327
|
+
status: string
|
|
328
|
+
|
|
329
|
+
@Field({ nullable: true, description: 'Execution summary' })
|
|
330
|
+
summary?: string
|
|
331
|
+
|
|
332
|
+
@Field({ nullable: true, description: 'Error if failed' })
|
|
333
|
+
error?: string
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Scenario List and Status
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
@ObjectType()
|
|
341
|
+
export class LabelingScenarioList {
|
|
342
|
+
@Field(type => [LabelingScenario], { description: 'List of scenarios' })
|
|
343
|
+
items: LabelingScenario[]
|
|
344
|
+
|
|
345
|
+
@Field(type => Int, { description: 'Total count' })
|
|
346
|
+
total: number
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@ObjectType()
|
|
350
|
+
export class ScenarioExecutionList {
|
|
351
|
+
@Field(type => [ScenarioExecution], { description: 'List of executions' })
|
|
352
|
+
items: ScenarioExecution[]
|
|
353
|
+
|
|
354
|
+
@Field(type => Int, { description: 'Total count' })
|
|
355
|
+
total: number
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@InputType()
|
|
359
|
+
export class ScenarioStatusRequest {
|
|
360
|
+
@Field({ description: 'Scenario ID' })
|
|
361
|
+
scenarioId: string
|
|
362
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotation Exporter
|
|
3
|
+
*
|
|
4
|
+
* Flexible annotation export system with pluggable format converters.
|
|
5
|
+
* Supports custom export formats for different use cases.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface LabelStudioAnnotation {
|
|
9
|
+
id: number
|
|
10
|
+
task: number
|
|
11
|
+
project: number
|
|
12
|
+
completed_by: {
|
|
13
|
+
id: number
|
|
14
|
+
email: string
|
|
15
|
+
first_name: string
|
|
16
|
+
last_name: string
|
|
17
|
+
}
|
|
18
|
+
result: Array<{
|
|
19
|
+
from_name: string
|
|
20
|
+
to_name: string
|
|
21
|
+
type: string
|
|
22
|
+
value: any
|
|
23
|
+
}>
|
|
24
|
+
created_at: string
|
|
25
|
+
updated_at: string
|
|
26
|
+
lead_time?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ExportContext {
|
|
30
|
+
projectId: number
|
|
31
|
+
projectTitle?: string
|
|
32
|
+
exportedAt: string
|
|
33
|
+
metadata?: any
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Export format converter function type
|
|
38
|
+
* Converts Label Studio annotations to custom format
|
|
39
|
+
*/
|
|
40
|
+
export type FormatConverter<T = any> = (
|
|
41
|
+
annotations: LabelStudioAnnotation[],
|
|
42
|
+
context: ExportContext
|
|
43
|
+
) => T | Promise<T>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registry for custom export formats
|
|
47
|
+
*/
|
|
48
|
+
const formatConverters: Map<string, FormatConverter> = new Map()
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a custom export format converter
|
|
52
|
+
*
|
|
53
|
+
* @param formatName - Unique format identifier (e.g., "csv", "coco", "yolo")
|
|
54
|
+
* @param converter - Converter function
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* registerExportFormat('csv', (annotations, context) => {
|
|
58
|
+
* const rows = annotations.map(a => ({
|
|
59
|
+
* id: a.id,
|
|
60
|
+
* task: a.task,
|
|
61
|
+
* result: JSON.stringify(a.result)
|
|
62
|
+
* }))
|
|
63
|
+
* return Papa.unparse(rows)
|
|
64
|
+
* })
|
|
65
|
+
*/
|
|
66
|
+
export function registerExportFormat<T = any>(formatName: string, converter: FormatConverter<T>): void {
|
|
67
|
+
formatConverters.set(formatName.toLowerCase(), converter)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unregister an export format
|
|
72
|
+
*/
|
|
73
|
+
export function unregisterExportFormat(formatName: string): void {
|
|
74
|
+
formatConverters.delete(formatName.toLowerCase())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get all registered format names
|
|
79
|
+
*/
|
|
80
|
+
export function getRegisteredFormats(): string[] {
|
|
81
|
+
return Array.from(formatConverters.keys())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Export annotations to specified format
|
|
86
|
+
*
|
|
87
|
+
* @param annotations - Label Studio annotations
|
|
88
|
+
* @param format - Export format name
|
|
89
|
+
* @param context - Export context
|
|
90
|
+
* @returns Converted data in requested format
|
|
91
|
+
*/
|
|
92
|
+
export async function exportAnnotations<T = any>(
|
|
93
|
+
annotations: LabelStudioAnnotation[],
|
|
94
|
+
format: string,
|
|
95
|
+
context: ExportContext
|
|
96
|
+
): Promise<T> {
|
|
97
|
+
const converter = formatConverters.get(format.toLowerCase())
|
|
98
|
+
|
|
99
|
+
if (!converter) {
|
|
100
|
+
throw new Error(`Unknown export format: ${format}. Available formats: ${getRegisteredFormats().join(', ')}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return await converter(annotations, context)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Pre-built export formats
|
|
108
|
+
*/
|
|
109
|
+
export class ExportFormats {
|
|
110
|
+
/**
|
|
111
|
+
* Export as JSON (default Label Studio format)
|
|
112
|
+
*/
|
|
113
|
+
static json(): FormatConverter<string> {
|
|
114
|
+
return (annotations, context) => {
|
|
115
|
+
return JSON.stringify(
|
|
116
|
+
{
|
|
117
|
+
meta: context,
|
|
118
|
+
annotations
|
|
119
|
+
},
|
|
120
|
+
null,
|
|
121
|
+
2
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Export as JSON Lines (one annotation per line)
|
|
128
|
+
*/
|
|
129
|
+
static jsonl(): FormatConverter<string> {
|
|
130
|
+
return annotations => {
|
|
131
|
+
return annotations.map(a => JSON.stringify(a)).join('\n')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Export image classification results as CSV
|
|
137
|
+
*/
|
|
138
|
+
static imageClassificationCSV(): FormatConverter<string> {
|
|
139
|
+
return annotations => {
|
|
140
|
+
const rows: string[] = ['task_id,annotation_id,label,annotator,lead_time,created_at']
|
|
141
|
+
|
|
142
|
+
for (const annotation of annotations) {
|
|
143
|
+
const label = annotation.result.find(r => r.type === 'choices')
|
|
144
|
+
if (label && label.value.choices) {
|
|
145
|
+
const choice = label.value.choices[0]
|
|
146
|
+
rows.push(
|
|
147
|
+
[
|
|
148
|
+
annotation.task,
|
|
149
|
+
annotation.id,
|
|
150
|
+
choice,
|
|
151
|
+
annotation.completed_by?.email || 'unknown',
|
|
152
|
+
annotation.lead_time || 0,
|
|
153
|
+
annotation.created_at
|
|
154
|
+
].join(',')
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return rows.join('\n')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Export Rank N classification results
|
|
165
|
+
*/
|
|
166
|
+
static rankNClassificationCSV(ranks: number = 3): FormatConverter<string> {
|
|
167
|
+
return annotations => {
|
|
168
|
+
const rankHeaders = Array.from({ length: ranks }, (_, i) => `rank${i + 1}`).join(',')
|
|
169
|
+
const rows: string[] = [`task_id,annotation_id,${rankHeaders},annotator,lead_time,created_at`]
|
|
170
|
+
|
|
171
|
+
for (const annotation of annotations) {
|
|
172
|
+
const rankValues: string[] = []
|
|
173
|
+
|
|
174
|
+
for (let i = 1; i <= ranks; i++) {
|
|
175
|
+
const rankResult = annotation.result.find(r => r.from_name === `rank${i}`)
|
|
176
|
+
rankValues.push(rankResult?.value?.choices?.[0] || '')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
rows.push(
|
|
180
|
+
[
|
|
181
|
+
annotation.task,
|
|
182
|
+
annotation.id,
|
|
183
|
+
...rankValues,
|
|
184
|
+
annotation.completed_by?.email || 'unknown',
|
|
185
|
+
annotation.lead_time || 0,
|
|
186
|
+
annotation.created_at
|
|
187
|
+
].join(',')
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return rows.join('\n')
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Export object detection results as COCO format
|
|
197
|
+
*/
|
|
198
|
+
static coco(): FormatConverter<any> {
|
|
199
|
+
return (annotations, context) => {
|
|
200
|
+
const images: any[] = []
|
|
201
|
+
const cocoAnnotations: any[] = []
|
|
202
|
+
const categories: Map<string, number> = new Map()
|
|
203
|
+
|
|
204
|
+
let imageId = 1
|
|
205
|
+
let annotationId = 1
|
|
206
|
+
let categoryId = 1
|
|
207
|
+
|
|
208
|
+
for (const annotation of annotations) {
|
|
209
|
+
// Add image
|
|
210
|
+
images.push({
|
|
211
|
+
id: imageId,
|
|
212
|
+
file_name: `task_${annotation.task}.jpg`,
|
|
213
|
+
width: 0, // TODO: extract from annotation if available
|
|
214
|
+
height: 0
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Process bounding boxes
|
|
218
|
+
for (const result of annotation.result) {
|
|
219
|
+
if (result.type === 'rectanglelabels') {
|
|
220
|
+
const label = result.value.rectanglelabels[0]
|
|
221
|
+
|
|
222
|
+
// Register category
|
|
223
|
+
if (!categories.has(label)) {
|
|
224
|
+
categories.set(label, categoryId++)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add annotation
|
|
228
|
+
cocoAnnotations.push({
|
|
229
|
+
id: annotationId++,
|
|
230
|
+
image_id: imageId,
|
|
231
|
+
category_id: categories.get(label),
|
|
232
|
+
bbox: [result.value.x, result.value.y, result.value.width, result.value.height],
|
|
233
|
+
area: result.value.width * result.value.height,
|
|
234
|
+
iscrowd: 0
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
imageId++
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
info: {
|
|
244
|
+
description: context.projectTitle || 'Label Studio Export',
|
|
245
|
+
date_created: context.exportedAt
|
|
246
|
+
},
|
|
247
|
+
images,
|
|
248
|
+
annotations: cocoAnnotations,
|
|
249
|
+
categories: Array.from(categories.entries()).map(([name, id]) => ({
|
|
250
|
+
id,
|
|
251
|
+
name,
|
|
252
|
+
supercategory: 'none'
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Export object detection results as YOLO format
|
|
260
|
+
*/
|
|
261
|
+
static yolo(): FormatConverter<Map<number, string>> {
|
|
262
|
+
return annotations => {
|
|
263
|
+
const taskAnnotations: Map<number, string> = new Map()
|
|
264
|
+
const categoryIndex: Map<string, number> = new Map()
|
|
265
|
+
let nextCategoryId = 0
|
|
266
|
+
|
|
267
|
+
for (const annotation of annotations) {
|
|
268
|
+
const lines: string[] = []
|
|
269
|
+
|
|
270
|
+
for (const result of annotation.result) {
|
|
271
|
+
if (result.type === 'rectanglelabels') {
|
|
272
|
+
const label = result.value.rectanglelabels[0]
|
|
273
|
+
|
|
274
|
+
// Register category
|
|
275
|
+
if (!categoryIndex.has(label)) {
|
|
276
|
+
categoryIndex.set(label, nextCategoryId++)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const classId = categoryIndex.get(label)!
|
|
280
|
+
|
|
281
|
+
// Convert to YOLO format (normalized center x, center y, width, height)
|
|
282
|
+
const x = result.value.x / 100
|
|
283
|
+
const y = result.value.y / 100
|
|
284
|
+
const w = result.value.width / 100
|
|
285
|
+
const h = result.value.height / 100
|
|
286
|
+
|
|
287
|
+
const centerX = x + w / 2
|
|
288
|
+
const centerY = y + h / 2
|
|
289
|
+
|
|
290
|
+
lines.push(`${classId} ${centerX} ${centerY} ${w} ${h}`)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (lines.length > 0) {
|
|
295
|
+
taskAnnotations.set(annotation.task, lines.join('\n'))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return taskAnnotations
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Export NER (Named Entity Recognition) results
|
|
305
|
+
*/
|
|
306
|
+
static nerJSON(): FormatConverter<any[]> {
|
|
307
|
+
return annotations => {
|
|
308
|
+
return annotations.map(annotation => {
|
|
309
|
+
const entities: any[] = []
|
|
310
|
+
|
|
311
|
+
for (const result of annotation.result) {
|
|
312
|
+
if (result.type === 'labels') {
|
|
313
|
+
entities.push({
|
|
314
|
+
start: result.value.start,
|
|
315
|
+
end: result.value.end,
|
|
316
|
+
text: result.value.text,
|
|
317
|
+
label: result.value.labels[0]
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
task_id: annotation.task,
|
|
324
|
+
annotation_id: annotation.id,
|
|
325
|
+
entities,
|
|
326
|
+
annotator: annotation.completed_by?.email
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Register default formats
|
|
334
|
+
registerExportFormat('json', ExportFormats.json())
|
|
335
|
+
registerExportFormat('jsonl', ExportFormats.jsonl())
|
|
336
|
+
registerExportFormat('csv', ExportFormats.imageClassificationCSV())
|
|
337
|
+
registerExportFormat('rank-csv', ExportFormats.rankNClassificationCSV())
|
|
338
|
+
registerExportFormat('coco', ExportFormats.coco())
|
|
339
|
+
registerExportFormat('yolo', ExportFormats.yolo())
|
|
340
|
+
registerExportFormat('ner-json', ExportFormats.nerJSON())
|