/** * Task Transformer * * Flexible data transformation from any source format to Label Studio tasks. * Supports nested data, predictions, and custom field mapping. */ export interface TaskTransformRule { /** * Field mapping: Label Studio field name -> source data path * Example: { "image": "image_url", "date": "metadata.timestamp" } */ dataFields: { [lsField: string]: string } /** * Prediction configuration (optional) */ predictions?: PredictionConfig /** * Metadata to attach to task (optional) */ meta?: { [key: string]: string } } export interface PredictionConfig { /** * Enable predictions */ enabled: boolean /** * Path to prediction result in source data */ resultPath: string /** * Path to confidence score (optional) */ scorePath?: string /** * Model version identifier (optional) */ modelVersion?: string /** * Transform function for prediction result */ resultTransform?: (result: any) => any } export interface LabelStudioTask { data: string // JSON string predictions?: Array<{ result: any score?: number model_version?: string }> meta?: any } export class TaskTransformer { /** * Transform source data to Label Studio tasks * * @param sourceData - Array of source data objects * @param rule - Transformation rules * @returns Array of Label Studio tasks */ static transform(sourceData: any[], rule: TaskTransformRule): LabelStudioTask[] { return sourceData.map(data => this.transformOne(data, rule)).filter(task => task !== null) as LabelStudioTask[] } /** * Transform a single data object */ static transformOne(sourceData: any, rule: TaskTransformRule): LabelStudioTask | null { try { // Build task data const taskData: any = {} for (const [lsField, sourcePath] of Object.entries(rule.dataFields)) { const value = this.getNestedValue(sourceData, sourcePath) if (value !== undefined) { taskData[lsField] = value } } // Skip if no data extracted if (Object.keys(taskData).length === 0) { return null } const task: LabelStudioTask = { data: JSON.stringify(taskData) } // Add predictions if configured if (rule.predictions?.enabled) { const prediction = this.buildPrediction(sourceData, rule.predictions) if (prediction) { task.predictions = [prediction] } } // Add metadata if configured if (rule.meta) { const meta: any = {} for (const [metaKey, sourcePath] of Object.entries(rule.meta)) { const value = this.getNestedValue(sourceData, sourcePath) if (value !== undefined) { meta[metaKey] = value } } if (Object.keys(meta).length > 0) { task.meta = meta } } return task } catch (error) { console.error('Failed to transform task:', error) return null } } /** * Build prediction object from source data */ private static buildPrediction( sourceData: any, config: PredictionConfig ): { result: any; score?: number; model_version?: string } | null { const result = this.getNestedValue(sourceData, config.resultPath) if (!result) { return null } const prediction: any = { result: config.resultTransform ? config.resultTransform(result) : result } if (config.scorePath) { const score = this.getNestedValue(sourceData, config.scorePath) if (score !== undefined) { prediction.score = score } } if (config.modelVersion) { prediction.model_version = config.modelVersion } return prediction } /** * Get nested value from object using dot notation path * Example: "metadata.user.name" -> obj.metadata.user.name */ private static getNestedValue(obj: any, path: string): any { if (!path || !obj) return undefined const keys = path.split('.') let current = obj for (const key of keys) { if (current === null || current === undefined) { return undefined } // Support array indexing: "items[0].name" const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/) if (arrayMatch) { const [, arrayKey, index] = arrayMatch current = current[arrayKey]?.[parseInt(index, 10)] } else { current = current[key] } } return current } /** * Transform with multiple rules (useful for heterogeneous data) */ static transformMultiRule(sourceData: any[], rules: TaskTransformRule[]): LabelStudioTask[] { const tasks: LabelStudioTask[] = [] for (const rule of rules) { tasks.push(...this.transform(sourceData, rule)) } return tasks } } /** * Pre-built transformation templates for common scenarios */ export class TaskTransformTemplates { /** * WBM Image Classification with AI predictions */ static wbmImageClassification(includeAiPrediction: boolean = true): TaskTransformRule { const rule: TaskTransformRule = { dataFields: { image: 'image_url', date: 'timestamp', device_id: 'device_id' }, meta: { wafer_id: 'wafer_id', lot_id: 'lot_id' } } if (includeAiPrediction) { rule.predictions = { enabled: true, resultPath: 'ai_prediction', scorePath: 'confidence', modelVersion: 'wbm-classifier-v1', resultTransform: result => { // Transform AI result to Label Studio format return [ { from_name: 'rank1', to_name: 'data', type: 'choices', value: { choices: [result.rank1] } }, { from_name: 'rank2', to_name: 'data', type: 'choices', value: { choices: [result.rank2] } }, { from_name: 'rank3', to_name: 'data', type: 'choices', value: { choices: [result.rank3] } } ] } } } return rule } /** * Simple image classification */ static imageClassification(imageField: string = 'image_url'): TaskTransformRule { return { dataFields: { image: imageField } } } /** * Text classification with metadata */ static textClassification(textField: string = 'text', metaFields: string[] = []): TaskTransformRule { const rule: TaskTransformRule = { dataFields: { text: textField } } if (metaFields.length > 0) { rule.meta = {} for (const field of metaFields) { rule.meta[field] = field } } return rule } /** * Time series data */ static timeSeries(valuesField: string, timestampField?: string): TaskTransformRule { const dataFields: any = { timeseries: valuesField } if (timestampField) { dataFields.timestamp = timestampField } return { dataFields } } /** * Object detection with bounding box predictions */ static objectDetection(imageField: string = 'image_url', predictionsField?: string): TaskTransformRule { const rule: TaskTransformRule = { dataFields: { image: imageField } } if (predictionsField) { rule.predictions = { enabled: true, resultPath: predictionsField, resultTransform: bboxes => { // Transform bounding boxes to Label Studio format return bboxes.map((bbox: any) => ({ from_name: 'bbox', to_name: 'data', type: 'rectanglelabels', value: { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height, rectanglelabels: [bbox.label] } })) } } } return rule } }