@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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Transformer
|
|
3
|
+
*
|
|
4
|
+
* Flexible data transformation from any source format to Label Studio tasks.
|
|
5
|
+
* Supports nested data, predictions, and custom field mapping.
|
|
6
|
+
*/
|
|
7
|
+
export interface TaskTransformRule {
|
|
8
|
+
/**
|
|
9
|
+
* Field mapping: Label Studio field name -> source data path
|
|
10
|
+
* Example: { "image": "image_url", "date": "metadata.timestamp" }
|
|
11
|
+
*/
|
|
12
|
+
dataFields: {
|
|
13
|
+
[lsField: string]: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Prediction configuration (optional)
|
|
17
|
+
*/
|
|
18
|
+
predictions?: PredictionConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Metadata to attach to task (optional)
|
|
21
|
+
*/
|
|
22
|
+
meta?: {
|
|
23
|
+
[key: string]: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface PredictionConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Enable predictions
|
|
29
|
+
*/
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Path to prediction result in source data
|
|
33
|
+
*/
|
|
34
|
+
resultPath: string;
|
|
35
|
+
/**
|
|
36
|
+
* Path to confidence score (optional)
|
|
37
|
+
*/
|
|
38
|
+
scorePath?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Model version identifier (optional)
|
|
41
|
+
*/
|
|
42
|
+
modelVersion?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Transform function for prediction result
|
|
45
|
+
*/
|
|
46
|
+
resultTransform?: (result: any) => any;
|
|
47
|
+
}
|
|
48
|
+
export interface LabelStudioTask {
|
|
49
|
+
data: string;
|
|
50
|
+
predictions?: Array<{
|
|
51
|
+
result: any;
|
|
52
|
+
score?: number;
|
|
53
|
+
model_version?: string;
|
|
54
|
+
}>;
|
|
55
|
+
meta?: any;
|
|
56
|
+
}
|
|
57
|
+
export declare class TaskTransformer {
|
|
58
|
+
/**
|
|
59
|
+
* Transform source data to Label Studio tasks
|
|
60
|
+
*
|
|
61
|
+
* @param sourceData - Array of source data objects
|
|
62
|
+
* @param rule - Transformation rules
|
|
63
|
+
* @returns Array of Label Studio tasks
|
|
64
|
+
*/
|
|
65
|
+
static transform(sourceData: any[], rule: TaskTransformRule): LabelStudioTask[];
|
|
66
|
+
/**
|
|
67
|
+
* Transform a single data object
|
|
68
|
+
*/
|
|
69
|
+
static transformOne(sourceData: any, rule: TaskTransformRule): LabelStudioTask | null;
|
|
70
|
+
/**
|
|
71
|
+
* Build prediction object from source data
|
|
72
|
+
*/
|
|
73
|
+
private static buildPrediction;
|
|
74
|
+
/**
|
|
75
|
+
* Get nested value from object using dot notation path
|
|
76
|
+
* Example: "metadata.user.name" -> obj.metadata.user.name
|
|
77
|
+
*/
|
|
78
|
+
private static getNestedValue;
|
|
79
|
+
/**
|
|
80
|
+
* Transform with multiple rules (useful for heterogeneous data)
|
|
81
|
+
*/
|
|
82
|
+
static transformMultiRule(sourceData: any[], rules: TaskTransformRule[]): LabelStudioTask[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Pre-built transformation templates for common scenarios
|
|
86
|
+
*/
|
|
87
|
+
export declare class TaskTransformTemplates {
|
|
88
|
+
/**
|
|
89
|
+
* WBM Image Classification with AI predictions
|
|
90
|
+
*/
|
|
91
|
+
static wbmImageClassification(includeAiPrediction?: boolean): TaskTransformRule;
|
|
92
|
+
/**
|
|
93
|
+
* Simple image classification
|
|
94
|
+
*/
|
|
95
|
+
static imageClassification(imageField?: string): TaskTransformRule;
|
|
96
|
+
/**
|
|
97
|
+
* Text classification with metadata
|
|
98
|
+
*/
|
|
99
|
+
static textClassification(textField?: string, metaFields?: string[]): TaskTransformRule;
|
|
100
|
+
/**
|
|
101
|
+
* Time series data
|
|
102
|
+
*/
|
|
103
|
+
static timeSeries(valuesField: string, timestampField?: string): TaskTransformRule;
|
|
104
|
+
/**
|
|
105
|
+
* Object detection with bounding box predictions
|
|
106
|
+
*/
|
|
107
|
+
static objectDetection(imageField?: string, predictionsField?: string): TaskTransformRule;
|
|
108
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Task Transformer
|
|
4
|
+
*
|
|
5
|
+
* Flexible data transformation from any source format to Label Studio tasks.
|
|
6
|
+
* Supports nested data, predictions, and custom field mapping.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TaskTransformTemplates = exports.TaskTransformer = void 0;
|
|
10
|
+
class TaskTransformer {
|
|
11
|
+
/**
|
|
12
|
+
* Transform source data to Label Studio tasks
|
|
13
|
+
*
|
|
14
|
+
* @param sourceData - Array of source data objects
|
|
15
|
+
* @param rule - Transformation rules
|
|
16
|
+
* @returns Array of Label Studio tasks
|
|
17
|
+
*/
|
|
18
|
+
static transform(sourceData, rule) {
|
|
19
|
+
return sourceData.map(data => this.transformOne(data, rule)).filter(task => task !== null);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Transform a single data object
|
|
23
|
+
*/
|
|
24
|
+
static transformOne(sourceData, rule) {
|
|
25
|
+
try {
|
|
26
|
+
// Build task data
|
|
27
|
+
const taskData = {};
|
|
28
|
+
for (const [lsField, sourcePath] of Object.entries(rule.dataFields)) {
|
|
29
|
+
const value = this.getNestedValue(sourceData, sourcePath);
|
|
30
|
+
if (value !== undefined) {
|
|
31
|
+
taskData[lsField] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Skip if no data extracted
|
|
35
|
+
if (Object.keys(taskData).length === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const task = {
|
|
39
|
+
data: JSON.stringify(taskData)
|
|
40
|
+
};
|
|
41
|
+
// Add predictions if configured
|
|
42
|
+
if (rule.predictions?.enabled) {
|
|
43
|
+
const prediction = this.buildPrediction(sourceData, rule.predictions);
|
|
44
|
+
if (prediction) {
|
|
45
|
+
task.predictions = [prediction];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Add metadata if configured
|
|
49
|
+
if (rule.meta) {
|
|
50
|
+
const meta = {};
|
|
51
|
+
for (const [metaKey, sourcePath] of Object.entries(rule.meta)) {
|
|
52
|
+
const value = this.getNestedValue(sourceData, sourcePath);
|
|
53
|
+
if (value !== undefined) {
|
|
54
|
+
meta[metaKey] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (Object.keys(meta).length > 0) {
|
|
58
|
+
task.meta = meta;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return task;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('Failed to transform task:', error);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build prediction object from source data
|
|
70
|
+
*/
|
|
71
|
+
static buildPrediction(sourceData, config) {
|
|
72
|
+
const result = this.getNestedValue(sourceData, config.resultPath);
|
|
73
|
+
if (!result) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const prediction = {
|
|
77
|
+
result: config.resultTransform ? config.resultTransform(result) : result
|
|
78
|
+
};
|
|
79
|
+
if (config.scorePath) {
|
|
80
|
+
const score = this.getNestedValue(sourceData, config.scorePath);
|
|
81
|
+
if (score !== undefined) {
|
|
82
|
+
prediction.score = score;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (config.modelVersion) {
|
|
86
|
+
prediction.model_version = config.modelVersion;
|
|
87
|
+
}
|
|
88
|
+
return prediction;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get nested value from object using dot notation path
|
|
92
|
+
* Example: "metadata.user.name" -> obj.metadata.user.name
|
|
93
|
+
*/
|
|
94
|
+
static getNestedValue(obj, path) {
|
|
95
|
+
if (!path || !obj)
|
|
96
|
+
return undefined;
|
|
97
|
+
const keys = path.split('.');
|
|
98
|
+
let current = obj;
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
if (current === null || current === undefined) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
// Support array indexing: "items[0].name"
|
|
104
|
+
const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/);
|
|
105
|
+
if (arrayMatch) {
|
|
106
|
+
const [, arrayKey, index] = arrayMatch;
|
|
107
|
+
current = current[arrayKey]?.[parseInt(index, 10)];
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
current = current[key];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Transform with multiple rules (useful for heterogeneous data)
|
|
117
|
+
*/
|
|
118
|
+
static transformMultiRule(sourceData, rules) {
|
|
119
|
+
const tasks = [];
|
|
120
|
+
for (const rule of rules) {
|
|
121
|
+
tasks.push(...this.transform(sourceData, rule));
|
|
122
|
+
}
|
|
123
|
+
return tasks;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.TaskTransformer = TaskTransformer;
|
|
127
|
+
/**
|
|
128
|
+
* Pre-built transformation templates for common scenarios
|
|
129
|
+
*/
|
|
130
|
+
class TaskTransformTemplates {
|
|
131
|
+
/**
|
|
132
|
+
* WBM Image Classification with AI predictions
|
|
133
|
+
*/
|
|
134
|
+
static wbmImageClassification(includeAiPrediction = true) {
|
|
135
|
+
const rule = {
|
|
136
|
+
dataFields: {
|
|
137
|
+
image: 'image_url',
|
|
138
|
+
date: 'timestamp',
|
|
139
|
+
device_id: 'device_id'
|
|
140
|
+
},
|
|
141
|
+
meta: {
|
|
142
|
+
wafer_id: 'wafer_id',
|
|
143
|
+
lot_id: 'lot_id'
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
if (includeAiPrediction) {
|
|
147
|
+
rule.predictions = {
|
|
148
|
+
enabled: true,
|
|
149
|
+
resultPath: 'ai_prediction',
|
|
150
|
+
scorePath: 'confidence',
|
|
151
|
+
modelVersion: 'wbm-classifier-v1',
|
|
152
|
+
resultTransform: result => {
|
|
153
|
+
// Transform AI result to Label Studio format
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
from_name: 'rank1',
|
|
157
|
+
to_name: 'data',
|
|
158
|
+
type: 'choices',
|
|
159
|
+
value: {
|
|
160
|
+
choices: [result.rank1]
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
from_name: 'rank2',
|
|
165
|
+
to_name: 'data',
|
|
166
|
+
type: 'choices',
|
|
167
|
+
value: {
|
|
168
|
+
choices: [result.rank2]
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
from_name: 'rank3',
|
|
173
|
+
to_name: 'data',
|
|
174
|
+
type: 'choices',
|
|
175
|
+
value: {
|
|
176
|
+
choices: [result.rank3]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return rule;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Simple image classification
|
|
187
|
+
*/
|
|
188
|
+
static imageClassification(imageField = 'image_url') {
|
|
189
|
+
return {
|
|
190
|
+
dataFields: {
|
|
191
|
+
image: imageField
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Text classification with metadata
|
|
197
|
+
*/
|
|
198
|
+
static textClassification(textField = 'text', metaFields = []) {
|
|
199
|
+
const rule = {
|
|
200
|
+
dataFields: {
|
|
201
|
+
text: textField
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
if (metaFields.length > 0) {
|
|
205
|
+
rule.meta = {};
|
|
206
|
+
for (const field of metaFields) {
|
|
207
|
+
rule.meta[field] = field;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return rule;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Time series data
|
|
214
|
+
*/
|
|
215
|
+
static timeSeries(valuesField, timestampField) {
|
|
216
|
+
const dataFields = {
|
|
217
|
+
timeseries: valuesField
|
|
218
|
+
};
|
|
219
|
+
if (timestampField) {
|
|
220
|
+
dataFields.timestamp = timestampField;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
dataFields
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Object detection with bounding box predictions
|
|
228
|
+
*/
|
|
229
|
+
static objectDetection(imageField = 'image_url', predictionsField) {
|
|
230
|
+
const rule = {
|
|
231
|
+
dataFields: {
|
|
232
|
+
image: imageField
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
if (predictionsField) {
|
|
236
|
+
rule.predictions = {
|
|
237
|
+
enabled: true,
|
|
238
|
+
resultPath: predictionsField,
|
|
239
|
+
resultTransform: bboxes => {
|
|
240
|
+
// Transform bounding boxes to Label Studio format
|
|
241
|
+
return bboxes.map((bbox) => ({
|
|
242
|
+
from_name: 'bbox',
|
|
243
|
+
to_name: 'data',
|
|
244
|
+
type: 'rectanglelabels',
|
|
245
|
+
value: {
|
|
246
|
+
x: bbox.x,
|
|
247
|
+
y: bbox.y,
|
|
248
|
+
width: bbox.width,
|
|
249
|
+
height: bbox.height,
|
|
250
|
+
rectanglelabels: [bbox.label]
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return rule;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
exports.TaskTransformTemplates = TaskTransformTemplates;
|
|
260
|
+
//# sourceMappingURL=task-transformer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-transformer.js","sourceRoot":"","sources":["../../server/utils/task-transformer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Task Transformer\n *\n * Flexible data transformation from any source format to Label Studio tasks.\n * Supports nested data, predictions, and custom field mapping.\n */\n\nexport interface TaskTransformRule {\n /**\n * Field mapping: Label Studio field name -> source data path\n * Example: { \"image\": \"image_url\", \"date\": \"metadata.timestamp\" }\n */\n dataFields: { [lsField: string]: string }\n\n /**\n * Prediction configuration (optional)\n */\n predictions?: PredictionConfig\n\n /**\n * Metadata to attach to task (optional)\n */\n meta?: { [key: string]: string }\n}\n\nexport interface PredictionConfig {\n /**\n * Enable predictions\n */\n enabled: boolean\n\n /**\n * Path to prediction result in source data\n */\n resultPath: string\n\n /**\n * Path to confidence score (optional)\n */\n scorePath?: string\n\n /**\n * Model version identifier (optional)\n */\n modelVersion?: string\n\n /**\n * Transform function for prediction result\n */\n resultTransform?: (result: any) => any\n}\n\nexport interface LabelStudioTask {\n data: string // JSON string\n predictions?: Array<{\n result: any\n score?: number\n model_version?: string\n }>\n meta?: any\n}\n\nexport class TaskTransformer {\n /**\n * Transform source data to Label Studio tasks\n *\n * @param sourceData - Array of source data objects\n * @param rule - Transformation rules\n * @returns Array of Label Studio tasks\n */\n static transform(sourceData: any[], rule: TaskTransformRule): LabelStudioTask[] {\n return sourceData.map(data => this.transformOne(data, rule)).filter(task => task !== null) as LabelStudioTask[]\n }\n\n /**\n * Transform a single data object\n */\n static transformOne(sourceData: any, rule: TaskTransformRule): LabelStudioTask | null {\n try {\n // Build task data\n const taskData: any = {}\n for (const [lsField, sourcePath] of Object.entries(rule.dataFields)) {\n const value = this.getNestedValue(sourceData, sourcePath)\n if (value !== undefined) {\n taskData[lsField] = value\n }\n }\n\n // Skip if no data extracted\n if (Object.keys(taskData).length === 0) {\n return null\n }\n\n const task: LabelStudioTask = {\n data: JSON.stringify(taskData)\n }\n\n // Add predictions if configured\n if (rule.predictions?.enabled) {\n const prediction = this.buildPrediction(sourceData, rule.predictions)\n if (prediction) {\n task.predictions = [prediction]\n }\n }\n\n // Add metadata if configured\n if (rule.meta) {\n const meta: any = {}\n for (const [metaKey, sourcePath] of Object.entries(rule.meta)) {\n const value = this.getNestedValue(sourceData, sourcePath)\n if (value !== undefined) {\n meta[metaKey] = value\n }\n }\n if (Object.keys(meta).length > 0) {\n task.meta = meta\n }\n }\n\n return task\n } catch (error) {\n console.error('Failed to transform task:', error)\n return null\n }\n }\n\n /**\n * Build prediction object from source data\n */\n private static buildPrediction(\n sourceData: any,\n config: PredictionConfig\n ): { result: any; score?: number; model_version?: string } | null {\n const result = this.getNestedValue(sourceData, config.resultPath)\n if (!result) {\n return null\n }\n\n const prediction: any = {\n result: config.resultTransform ? config.resultTransform(result) : result\n }\n\n if (config.scorePath) {\n const score = this.getNestedValue(sourceData, config.scorePath)\n if (score !== undefined) {\n prediction.score = score\n }\n }\n\n if (config.modelVersion) {\n prediction.model_version = config.modelVersion\n }\n\n return prediction\n }\n\n /**\n * Get nested value from object using dot notation path\n * Example: \"metadata.user.name\" -> obj.metadata.user.name\n */\n private static getNestedValue(obj: any, path: string): any {\n if (!path || !obj) return undefined\n\n const keys = path.split('.')\n let current = obj\n\n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined\n }\n\n // Support array indexing: \"items[0].name\"\n const arrayMatch = key.match(/^(\\w+)\\[(\\d+)\\]$/)\n if (arrayMatch) {\n const [, arrayKey, index] = arrayMatch\n current = current[arrayKey]?.[parseInt(index, 10)]\n } else {\n current = current[key]\n }\n }\n\n return current\n }\n\n /**\n * Transform with multiple rules (useful for heterogeneous data)\n */\n static transformMultiRule(sourceData: any[], rules: TaskTransformRule[]): LabelStudioTask[] {\n const tasks: LabelStudioTask[] = []\n\n for (const rule of rules) {\n tasks.push(...this.transform(sourceData, rule))\n }\n\n return tasks\n }\n}\n\n/**\n * Pre-built transformation templates for common scenarios\n */\nexport class TaskTransformTemplates {\n /**\n * WBM Image Classification with AI predictions\n */\n static wbmImageClassification(includeAiPrediction: boolean = true): TaskTransformRule {\n const rule: TaskTransformRule = {\n dataFields: {\n image: 'image_url',\n date: 'timestamp',\n device_id: 'device_id'\n },\n meta: {\n wafer_id: 'wafer_id',\n lot_id: 'lot_id'\n }\n }\n\n if (includeAiPrediction) {\n rule.predictions = {\n enabled: true,\n resultPath: 'ai_prediction',\n scorePath: 'confidence',\n modelVersion: 'wbm-classifier-v1',\n resultTransform: result => {\n // Transform AI result to Label Studio format\n return [\n {\n from_name: 'rank1',\n to_name: 'data',\n type: 'choices',\n value: {\n choices: [result.rank1]\n }\n },\n {\n from_name: 'rank2',\n to_name: 'data',\n type: 'choices',\n value: {\n choices: [result.rank2]\n }\n },\n {\n from_name: 'rank3',\n to_name: 'data',\n type: 'choices',\n value: {\n choices: [result.rank3]\n }\n }\n ]\n }\n }\n }\n\n return rule\n }\n\n /**\n * Simple image classification\n */\n static imageClassification(imageField: string = 'image_url'): TaskTransformRule {\n return {\n dataFields: {\n image: imageField\n }\n }\n }\n\n /**\n * Text classification with metadata\n */\n static textClassification(textField: string = 'text', metaFields: string[] = []): TaskTransformRule {\n const rule: TaskTransformRule = {\n dataFields: {\n text: textField\n }\n }\n\n if (metaFields.length > 0) {\n rule.meta = {}\n for (const field of metaFields) {\n rule.meta[field] = field\n }\n }\n\n return rule\n }\n\n /**\n * Time series data\n */\n static timeSeries(valuesField: string, timestampField?: string): TaskTransformRule {\n const dataFields: any = {\n timeseries: valuesField\n }\n\n if (timestampField) {\n dataFields.timestamp = timestampField\n }\n\n return {\n dataFields\n }\n }\n\n /**\n * Object detection with bounding box predictions\n */\n static objectDetection(imageField: string = 'image_url', predictionsField?: string): TaskTransformRule {\n const rule: TaskTransformRule = {\n dataFields: {\n image: imageField\n }\n }\n\n if (predictionsField) {\n rule.predictions = {\n enabled: true,\n resultPath: predictionsField,\n resultTransform: bboxes => {\n // Transform bounding boxes to Label Studio format\n return bboxes.map((bbox: any) => ({\n from_name: 'bbox',\n to_name: 'data',\n type: 'rectanglelabels',\n value: {\n x: bbox.x,\n y: bbox.y,\n width: bbox.width,\n height: bbox.height,\n rectanglelabels: [bbox.label]\n }\n }))\n }\n }\n }\n\n return rule\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@things-factory/integration-label-studio",
|
|
3
|
+
"version": "9.1.19",
|
|
4
|
+
"main": "dist-server/index.js",
|
|
5
|
+
"browser": "dist-client/index.js",
|
|
6
|
+
"things-factory": true,
|
|
7
|
+
"author": "Things-Factory Team",
|
|
8
|
+
"description": "Integration module for embedding Label Studio in Things-Factory with SSO support",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"@things-factory:registry": "https://registry.npmjs.org"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/hatiolab/things-factory.git",
|
|
17
|
+
"directory": "packages/integration-label-studio"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npm run build:server && npm run build:client",
|
|
21
|
+
"copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
|
|
22
|
+
"build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
|
|
23
|
+
"build:server": "npx tsc --p ./server/tsconfig.json",
|
|
24
|
+
"clean:client": "npx rimraf dist-client",
|
|
25
|
+
"clean:server": "npx rimraf dist-server",
|
|
26
|
+
"clean": "npm run clean:server && npm run clean:client"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@operato/graphql": "^9.0.0",
|
|
30
|
+
"@operato/layout": "^9.0.0",
|
|
31
|
+
"@things-factory/ai-inference": "^9.1.19",
|
|
32
|
+
"@things-factory/auth-base": "^9.1.19",
|
|
33
|
+
"@things-factory/dataset": "^9.1.19",
|
|
34
|
+
"@things-factory/env": "^9.1.0",
|
|
35
|
+
"@things-factory/menu-base": "^9.1.19",
|
|
36
|
+
"@things-factory/shell": "^9.1.19"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"things-factory",
|
|
40
|
+
"label-studio",
|
|
41
|
+
"integration",
|
|
42
|
+
"iframe",
|
|
43
|
+
"sso",
|
|
44
|
+
"data-labeling"
|
|
45
|
+
],
|
|
46
|
+
"gitHead": "078438034dbe19915108e89ff24024f7044a85a9"
|
|
47
|
+
}
|