/**
* Label Config Builder
*
* Flexible, declarative Label Studio configuration generator.
* Supports any data type and control combination without hardcoding templates.
*/
export interface LabelConfigSpec {
dataType: 'image' | 'text' | 'audio' | 'video' | 'timeseries' | 'html' | 'hypertext'
dataName?: string // default: 'data'
controls: ControlSpec[]
}
export interface ControlSpec {
type: 'choices' | 'labels' | 'textarea' | 'rating' | 'rectangles' | 'polygon' | 'keypoint' | 'brush'
name: string
toName: string
config: any
}
export interface ChoicesConfig {
choices: string[]
choice?: 'single' | 'multiple'
required?: boolean
}
export interface LabelsConfig {
labels: Array<{
value: string
background?: string
hotkey?: string
}>
}
export interface RatingConfig {
maxRating?: number
icon?: string
size?: 'small' | 'medium' | 'large'
}
export class LabelConfigBuilder {
/**
* Build Label Studio XML configuration from specification
*/
static build(spec: LabelConfigSpec): string {
const dataName = spec.dataName || 'data'
const dataTag = this.buildDataTag(spec.dataType, dataName)
const controlTags = spec.controls.map(c => this.buildControlTag(c))
return `
${dataTag}
${controlTags.join('\n ')}
`.trim()
}
/**
* Build data source tag (Image, Text, Audio, etc.)
*/
private static buildDataTag(type: string, name: string): string {
const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1)
return `<${capitalizedType} name="${name}" value="$${name}"/>`
}
/**
* Build control tag based on type
*/
private static buildControlTag(control: ControlSpec): string {
switch (control.type) {
case 'choices':
return this.buildChoices(control)
case 'labels':
return this.buildLabels(control)
case 'textarea':
return this.buildTextArea(control)
case 'rating':
return this.buildRating(control)
case 'rectangles':
return this.buildRectangles(control)
case 'polygon':
return this.buildPolygon(control)
case 'keypoint':
return this.buildKeypoint(control)
case 'brush':
return this.buildBrush(control)
default:
throw new Error(`Unknown control type: ${control.type}`)
}
}
/**
* Build Choices control (single or multiple selection)
*/
private static buildChoices(control: ControlSpec): string {
const config = control.config as ChoicesConfig
const choice = config.choice || 'single'
const required = config.required ? 'required="true"' : ''
const choiceElements = config.choices
.map(c => ` `)
.join('\n')
return `
${choiceElements}
`
}
/**
* Build Labels control (for NER, classification, etc.)
*/
private static buildLabels(control: ControlSpec): string {
const config = control.config as LabelsConfig
const labelElements = config.labels
.map(l => {
const background = l.background ? `background="${l.background}"` : ''
const hotkey = l.hotkey ? `hotkey="${l.hotkey}"` : ''
return ` `
})
.join('\n')
return `
${labelElements}
`
}
/**
* Build TextArea control
*/
private static buildTextArea(control: ControlSpec): string {
const placeholder = control.config.placeholder ? `placeholder="${control.config.placeholder}"` : ''
const required = control.config.required ? 'required="true"' : ''
const maxSubmissions = control.config.maxSubmissions ? `maxSubmissions="${control.config.maxSubmissions}"` : ''
return ``
}
/**
* Build Rating control
*/
private static buildRating(control: ControlSpec): string {
const config = control.config as RatingConfig
const maxRating = config.maxRating || 5
const icon = config.icon || 'star'
const size = config.size || 'medium'
return ``
}
/**
* Build Rectangle control (bounding boxes)
*/
private static buildRectangles(control: ControlSpec): string {
const labels = control.config.labels ? `labels="${control.config.labels.join(',')}"` : ''
return ``
}
/**
* Build Polygon control
*/
private static buildPolygon(control: ControlSpec): string {
const labels = control.config.labels ? `labels="${control.config.labels.join(',')}"` : ''
return ``
}
/**
* Build Keypoint control
*/
private static buildKeypoint(control: ControlSpec): string {
const labels = control.config.labels ? `labels="${control.config.labels.join(',')}"` : ''
return ``
}
/**
* Build Brush control (semantic segmentation)
*/
private static buildBrush(control: ControlSpec): string {
const labels = control.config.labels ? `labels="${control.config.labels.join(',')}"` : ''
return ``
}
/**
* Escape XML special characters
*/
private static escapeXml(str: string): string {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
}
/**
* Pre-built template generators for common use cases
*/
export class LabelConfigTemplates {
/**
* Generate Rank N image classification config
* Example: WBM Rank 3 classification
*/
static imageRankN(ranks: number, classes: string[]): LabelConfigSpec {
const controls: ControlSpec[] = []
for (let i = 1; i <= ranks; i++) {
controls.push({
type: 'choices',
name: `rank${i}`,
toName: 'data',
config: {
choices: classes,
choice: 'single',
required: i === 1 // First rank is required
}
})
}
return {
dataType: 'image',
controls
}
}
/**
* Simple image classification
*/
static imageClassification(classes: string[], multiLabel: boolean = false): LabelConfigSpec {
return {
dataType: 'image',
controls: [
{
type: 'choices',
name: 'choice',
toName: 'data',
config: {
choices: classes,
choice: multiLabel ? 'multiple' : 'single',
required: true
}
}
]
}
}
/**
* Text classification (sentiment, intent, etc.)
*/
static textClassification(classes: string[]): LabelConfigSpec {
return {
dataType: 'text',
controls: [
{
type: 'choices',
name: 'choice',
toName: 'data',
config: {
choices: classes,
choice: 'single',
required: true
}
}
]
}
}
/**
* Named Entity Recognition
*/
static namedEntityRecognition(labels: Array<{ value: string; background: string }>): LabelConfigSpec {
return {
dataType: 'text',
controls: [
{
type: 'labels',
name: 'label',
toName: 'data',
config: {
labels
}
}
]
}
}
/**
* Object detection (bounding boxes)
*/
static objectDetection(labels: string[]): LabelConfigSpec {
return {
dataType: 'image',
controls: [
{
type: 'rectangles',
name: 'bbox',
toName: 'data',
config: {
labels
}
},
{
type: 'labels',
name: 'label',
toName: 'data',
config: {
labels: labels.map(l => ({ value: l }))
}
}
]
}
}
/**
* Semantic segmentation
*/
static semanticSegmentation(labels: string[]): LabelConfigSpec {
return {
dataType: 'image',
controls: [
{
type: 'brush',
name: 'segmentation',
toName: 'data',
config: {
labels
}
},
{
type: 'labels',
name: 'label',
toName: 'data',
config: {
labels: labels.map(l => ({ value: l }))
}
}
]
}
}
}