@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,304 @@
|
|
|
1
|
+
import Koa from 'koa'
|
|
2
|
+
import Router from 'koa-router'
|
|
3
|
+
import { getRepository } from 'typeorm'
|
|
4
|
+
|
|
5
|
+
const webhookRouter = new Router()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Label Studio Webhook Event Types
|
|
9
|
+
*/
|
|
10
|
+
export enum WebhookAction {
|
|
11
|
+
ANNOTATION_CREATED = 'ANNOTATION_CREATED',
|
|
12
|
+
ANNOTATION_UPDATED = 'ANNOTATION_UPDATED',
|
|
13
|
+
ANNOTATION_DELETED = 'ANNOTATION_DELETED',
|
|
14
|
+
TASK_CREATED = 'TASK_CREATED',
|
|
15
|
+
TASK_UPDATED = 'TASK_UPDATED',
|
|
16
|
+
TASK_DELETED = 'TASK_DELETED',
|
|
17
|
+
PROJECT_UPDATED = 'PROJECT_UPDATED'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WebhookPayload {
|
|
21
|
+
action: WebhookAction
|
|
22
|
+
project: {
|
|
23
|
+
id: number
|
|
24
|
+
title: string
|
|
25
|
+
}
|
|
26
|
+
task?: {
|
|
27
|
+
id: number
|
|
28
|
+
data: any
|
|
29
|
+
annotations: any[]
|
|
30
|
+
}
|
|
31
|
+
annotation?: {
|
|
32
|
+
id: number
|
|
33
|
+
result: any[]
|
|
34
|
+
completed_by: {
|
|
35
|
+
id: number
|
|
36
|
+
email: string
|
|
37
|
+
}
|
|
38
|
+
lead_time: number
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Webhook handler function type
|
|
44
|
+
* Applications can register custom handlers for any webhook action
|
|
45
|
+
*/
|
|
46
|
+
export type WebhookHandler = (payload: WebhookPayload, context: Koa.Context) => Promise<void>
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Registry for custom webhook handlers
|
|
50
|
+
* Multiple handlers can be registered per action
|
|
51
|
+
*/
|
|
52
|
+
const customHandlers: Map<WebhookAction, WebhookHandler[]> = new Map()
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register a custom webhook handler
|
|
56
|
+
* Handlers are executed in registration order
|
|
57
|
+
*
|
|
58
|
+
* @param action - Webhook action to handle
|
|
59
|
+
* @param handler - Handler function
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* registerWebhookHandler(WebhookAction.ANNOTATION_CREATED, async (payload, ctx) => {
|
|
63
|
+
* console.log('Custom handler:', payload.annotation?.id)
|
|
64
|
+
* // Store annotation in database
|
|
65
|
+
* // Trigger ML training
|
|
66
|
+
* // Send notifications
|
|
67
|
+
* })
|
|
68
|
+
*/
|
|
69
|
+
export function registerWebhookHandler(action: WebhookAction, handler: WebhookHandler): void {
|
|
70
|
+
if (!customHandlers.has(action)) {
|
|
71
|
+
customHandlers.set(action, [])
|
|
72
|
+
}
|
|
73
|
+
customHandlers.get(action)!.push(handler)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Unregister a webhook handler
|
|
78
|
+
*/
|
|
79
|
+
export function unregisterWebhookHandler(action: WebhookAction, handler: WebhookHandler): void {
|
|
80
|
+
const handlers = customHandlers.get(action)
|
|
81
|
+
if (handlers) {
|
|
82
|
+
const index = handlers.indexOf(handler)
|
|
83
|
+
if (index > -1) {
|
|
84
|
+
handlers.splice(index, 1)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clear all custom handlers for an action
|
|
91
|
+
*/
|
|
92
|
+
export function clearWebhookHandlers(action?: WebhookAction): void {
|
|
93
|
+
if (action) {
|
|
94
|
+
customHandlers.delete(action)
|
|
95
|
+
} else {
|
|
96
|
+
customHandlers.clear()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Execute all registered handlers for an action
|
|
102
|
+
*/
|
|
103
|
+
async function executeCustomHandlers(action: WebhookAction, payload: WebhookPayload, ctx: Koa.Context): Promise<void> {
|
|
104
|
+
const handlers = customHandlers.get(action)
|
|
105
|
+
if (!handlers || handlers.length === 0) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Execute all handlers in sequence
|
|
110
|
+
for (const handler of handlers) {
|
|
111
|
+
try {
|
|
112
|
+
await handler(payload, ctx)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`[Webhook] Custom handler error for ${action}:`, error)
|
|
115
|
+
// Continue executing other handlers even if one fails
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handle annotation created event
|
|
122
|
+
*/
|
|
123
|
+
async function handleAnnotationCreated(payload: WebhookPayload) {
|
|
124
|
+
console.log(`[Webhook] Annotation created:`, {
|
|
125
|
+
projectId: payload.project.id,
|
|
126
|
+
taskId: payload.task?.id,
|
|
127
|
+
annotationId: payload.annotation?.id,
|
|
128
|
+
completedBy: payload.annotation?.completed_by?.email
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// TODO: Implement business logic
|
|
132
|
+
// 1. Store annotation in Things-Factory database
|
|
133
|
+
// 2. Trigger downstream processes (e.g., model training)
|
|
134
|
+
// 3. Send notifications to stakeholders
|
|
135
|
+
// 4. Update task status in external systems
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Handle annotation updated event
|
|
140
|
+
*/
|
|
141
|
+
async function handleAnnotationUpdated(payload: WebhookPayload) {
|
|
142
|
+
console.log(`[Webhook] Annotation updated:`, {
|
|
143
|
+
projectId: payload.project.id,
|
|
144
|
+
annotationId: payload.annotation?.id
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// TODO: Update annotation in database
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Handle annotation deleted event
|
|
152
|
+
*/
|
|
153
|
+
async function handleAnnotationDeleted(payload: WebhookPayload) {
|
|
154
|
+
console.log(`[Webhook] Annotation deleted:`, {
|
|
155
|
+
projectId: payload.project.id,
|
|
156
|
+
annotationId: payload.annotation?.id
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// TODO: Remove annotation from database
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle task created event
|
|
164
|
+
*/
|
|
165
|
+
async function handleTaskCreated(payload: WebhookPayload) {
|
|
166
|
+
console.log(`[Webhook] Task created:`, {
|
|
167
|
+
projectId: payload.project.id,
|
|
168
|
+
taskId: payload.task?.id
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// TODO: Store task reference
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handle project updated event
|
|
176
|
+
*/
|
|
177
|
+
async function handleProjectUpdated(payload: WebhookPayload) {
|
|
178
|
+
console.log(`[Webhook] Project updated:`, {
|
|
179
|
+
projectId: payload.project.id,
|
|
180
|
+
projectTitle: payload.project.title
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// TODO: Update project metadata
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Main webhook endpoint
|
|
188
|
+
* Label Studio will POST events here
|
|
189
|
+
*/
|
|
190
|
+
webhookRouter.post('/label-studio/webhook', async (ctx: Koa.Context) => {
|
|
191
|
+
try {
|
|
192
|
+
const payload = ctx.request.body as WebhookPayload
|
|
193
|
+
|
|
194
|
+
console.log(`[Webhook] Received event: ${payload.action}`)
|
|
195
|
+
|
|
196
|
+
// Execute default handlers first
|
|
197
|
+
switch (payload.action) {
|
|
198
|
+
case WebhookAction.ANNOTATION_CREATED:
|
|
199
|
+
await handleAnnotationCreated(payload)
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
case WebhookAction.ANNOTATION_UPDATED:
|
|
203
|
+
await handleAnnotationUpdated(payload)
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
case WebhookAction.ANNOTATION_DELETED:
|
|
207
|
+
await handleAnnotationDeleted(payload)
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
case WebhookAction.TASK_CREATED:
|
|
211
|
+
await handleTaskCreated(payload)
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
case WebhookAction.TASK_UPDATED:
|
|
215
|
+
// Optional: handle task updates
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
case WebhookAction.TASK_DELETED:
|
|
219
|
+
// Optional: handle task deletions
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
case WebhookAction.PROJECT_UPDATED:
|
|
223
|
+
await handleProjectUpdated(payload)
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
default:
|
|
227
|
+
console.warn(`[Webhook] Unknown action: ${payload.action}`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Execute custom handlers
|
|
231
|
+
await executeCustomHandlers(payload.action, payload, ctx)
|
|
232
|
+
|
|
233
|
+
ctx.status = 200
|
|
234
|
+
ctx.body = { success: true }
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error('[Webhook] Error processing webhook:', error)
|
|
237
|
+
ctx.status = 500
|
|
238
|
+
ctx.body = { error: error.message }
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Webhook registration helper
|
|
244
|
+
* Call this to register webhook with Label Studio
|
|
245
|
+
*/
|
|
246
|
+
webhookRouter.post('/label-studio/webhook/register', async (ctx: Koa.Context) => {
|
|
247
|
+
try {
|
|
248
|
+
const { projectId } = ctx.request.body as { projectId: number }
|
|
249
|
+
|
|
250
|
+
// Get Things-Factory server URL
|
|
251
|
+
const serverUrl = process.env.SERVER_URL || 'http://localhost:3000'
|
|
252
|
+
const webhookUrl = `${serverUrl}/label-studio/webhook`
|
|
253
|
+
|
|
254
|
+
// Register webhook using Label Studio API
|
|
255
|
+
const { labelStudioApi } = await import('../utils/label-studio-api-client.js')
|
|
256
|
+
|
|
257
|
+
const webhook = await labelStudioApi.createWebhook({
|
|
258
|
+
project: projectId,
|
|
259
|
+
url: webhookUrl,
|
|
260
|
+
send_payload: true,
|
|
261
|
+
send_for_all_actions: true,
|
|
262
|
+
headers: {
|
|
263
|
+
'Content-Type': 'application/json'
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
ctx.status = 200
|
|
268
|
+
ctx.body = {
|
|
269
|
+
success: true,
|
|
270
|
+
webhook
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error('[Webhook] Error registering webhook:', error)
|
|
274
|
+
ctx.status = 500
|
|
275
|
+
ctx.body = { error: error.message }
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get webhooks for a project
|
|
281
|
+
*/
|
|
282
|
+
webhookRouter.get('/label-studio/webhook/:projectId', async (ctx: Koa.Context) => {
|
|
283
|
+
try {
|
|
284
|
+
const projectId = parseInt(ctx.params.projectId)
|
|
285
|
+
|
|
286
|
+
const { labelStudioApi } = await import('../utils/label-studio-api-client.js')
|
|
287
|
+
const webhooks = await labelStudioApi.getWebhooks(projectId)
|
|
288
|
+
|
|
289
|
+
ctx.status = 200
|
|
290
|
+
ctx.body = webhooks
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error('[Webhook] Error fetching webhooks:', error)
|
|
293
|
+
ctx.status = 500
|
|
294
|
+
ctx.body = { error: error.message }
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// Register routes with Things-Factory
|
|
299
|
+
process.on('bootstrap-module-domain-private-route', (_app: Koa, router: Router) => {
|
|
300
|
+
router.use(webhookRouter.routes())
|
|
301
|
+
router.use(webhookRouter.allowedMethods())
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
export { webhookRouter }
|
package/server/route.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label Studio Integration Routes
|
|
3
|
+
*
|
|
4
|
+
* This module registers all Label Studio-related routes:
|
|
5
|
+
* - Webhook handler for Label Studio events (PUBLIC)
|
|
6
|
+
* - SSO authentication endpoint (PRIVATE - requires authentication)
|
|
7
|
+
*
|
|
8
|
+
* ## Subdomain Cookie Sharing Architecture:
|
|
9
|
+
* This integration uses subdomain-based cookie sharing instead of proxying.
|
|
10
|
+
* - Backend gets JWT token from Label Studio and sets shared domain cookie
|
|
11
|
+
* - Frontend loads Label Studio directly (not through proxy)
|
|
12
|
+
* - Cookie is automatically sent with all subdomain requests
|
|
13
|
+
*/
|
|
14
|
+
import Koa from 'koa'
|
|
15
|
+
import Router from 'koa-router'
|
|
16
|
+
import { webhookRouter } from './route/webhook'
|
|
17
|
+
import { ssoRouter } from './route/label-studio-sso'
|
|
18
|
+
|
|
19
|
+
// Public routes - No authentication required
|
|
20
|
+
process.on('bootstrap-module-global-public-route' as any, (_app: Koa, routes: Router) => {
|
|
21
|
+
/*
|
|
22
|
+
* Register webhook routes (Label Studio → Things Factory)
|
|
23
|
+
* These must be public so Label Studio can send events
|
|
24
|
+
*/
|
|
25
|
+
routes.use(webhookRouter.routes(), webhookRouter.allowedMethods())
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Private routes - Authentication required
|
|
29
|
+
process.on('bootstrap-module-domain-private-route' as any, (_app: Koa, routes: Router) => {
|
|
30
|
+
/*
|
|
31
|
+
* Register Label Studio SSO routes
|
|
32
|
+
* This requires authentication - ctx.state.user will be available
|
|
33
|
+
*/
|
|
34
|
+
routes.use(ssoRouter.routes(), ssoRouter.allowedMethods())
|
|
35
|
+
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Resolver, Mutation, Arg, Ctx, Int, Float, Directive } from 'type-graphql'
|
|
2
|
+
import { AIModelClientFactory, DetectedObject } from '@things-factory/ai-inference'
|
|
3
|
+
import { labelStudioApi } from '../utils/label-studio-api-client.js'
|
|
4
|
+
import {
|
|
5
|
+
GeneratePredictionRequest,
|
|
6
|
+
BatchGeneratePredictionRequest,
|
|
7
|
+
PredictionGenerationResult,
|
|
8
|
+
BatchPredictionResult
|
|
9
|
+
} from '../types/prediction-types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Label Studio AI Prediction Service
|
|
13
|
+
*
|
|
14
|
+
* Integrates AI inference with Label Studio prediction system
|
|
15
|
+
* Uses ai-inference module for pure AI operations
|
|
16
|
+
*/
|
|
17
|
+
@Resolver()
|
|
18
|
+
export class LabelStudioAIPredictionService {
|
|
19
|
+
/**
|
|
20
|
+
* Generate Label Studio prediction for a task
|
|
21
|
+
* Runs AI model and creates prediction in Label Studio
|
|
22
|
+
*/
|
|
23
|
+
@Mutation(returns => PredictionGenerationResult, {
|
|
24
|
+
description: 'Generate Label Studio prediction for a task using AI model'
|
|
25
|
+
})
|
|
26
|
+
@Directive('@privilege(category: "label-studio", privilege: "mutation")')
|
|
27
|
+
async generatePrediction(
|
|
28
|
+
@Arg('input') input: GeneratePredictionRequest,
|
|
29
|
+
@Ctx() context: ResolverContext
|
|
30
|
+
): Promise<PredictionGenerationResult> {
|
|
31
|
+
try {
|
|
32
|
+
// 1. Run AI model using ai-inference module
|
|
33
|
+
const modelClient = input.modelId
|
|
34
|
+
? AIModelClientFactory.getClient(input.modelId)
|
|
35
|
+
: AIModelClientFactory.getDefaultClient()
|
|
36
|
+
|
|
37
|
+
const objects = await modelClient.detectObjects(input.imageUrl, {
|
|
38
|
+
confidenceThreshold: input.confidenceThreshold
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (objects.length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
taskId: input.taskId,
|
|
44
|
+
success: true,
|
|
45
|
+
objectCount: 0,
|
|
46
|
+
error: 'No objects detected'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Convert AI results to Label Studio format
|
|
51
|
+
const labelStudioResult = this.convertToLabelStudioFormat(objects)
|
|
52
|
+
|
|
53
|
+
// 3. Calculate average confidence
|
|
54
|
+
const avgConfidence = objects.reduce((sum, obj) => sum + obj.confidence, 0) / objects.length
|
|
55
|
+
|
|
56
|
+
// 4. Create prediction in Label Studio
|
|
57
|
+
const prediction = await labelStudioApi.createPrediction({
|
|
58
|
+
task: input.taskId,
|
|
59
|
+
result: labelStudioResult,
|
|
60
|
+
score: avgConfidence,
|
|
61
|
+
model_version: input.modelId || 'default-model-v1.0'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
console.log(
|
|
65
|
+
`[LS AI Prediction] Created prediction ${prediction.id} for task ${input.taskId} with ${objects.length} objects`
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
taskId: input.taskId,
|
|
70
|
+
predictionId: prediction.id,
|
|
71
|
+
success: true,
|
|
72
|
+
objectCount: objects.length,
|
|
73
|
+
avgConfidence
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`[LS AI Prediction] Failed for task ${input.taskId}:`, error)
|
|
77
|
+
return {
|
|
78
|
+
taskId: input.taskId,
|
|
79
|
+
success: false,
|
|
80
|
+
objectCount: 0,
|
|
81
|
+
error: error.message
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate predictions for multiple tasks in batch
|
|
88
|
+
*/
|
|
89
|
+
@Mutation(returns => BatchPredictionResult, {
|
|
90
|
+
description: 'Generate Label Studio predictions for multiple tasks in batch'
|
|
91
|
+
})
|
|
92
|
+
@Directive('@privilege(category: "label-studio", privilege: "mutation")')
|
|
93
|
+
async generateBatchPredictions(
|
|
94
|
+
@Arg('input') input: BatchGeneratePredictionRequest,
|
|
95
|
+
@Ctx() context: ResolverContext
|
|
96
|
+
): Promise<BatchPredictionResult> {
|
|
97
|
+
const results: PredictionGenerationResult[] = []
|
|
98
|
+
let succeeded = 0
|
|
99
|
+
let failed = 0
|
|
100
|
+
|
|
101
|
+
const modelId = input.modelId || 'default-model-v1.0'
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Process tasks in parallel (limit concurrency to avoid overload)
|
|
105
|
+
const BATCH_SIZE = 5
|
|
106
|
+
for (let i = 0; i < input.taskIds.length; i += BATCH_SIZE) {
|
|
107
|
+
const batch = input.taskIds.slice(i, i + BATCH_SIZE)
|
|
108
|
+
|
|
109
|
+
const batchPromises = batch.map(async taskId => {
|
|
110
|
+
try {
|
|
111
|
+
// Get task data from Label Studio
|
|
112
|
+
const task = await labelStudioApi.getTask(taskId)
|
|
113
|
+
|
|
114
|
+
if (!task || !task.data || !task.data.image) {
|
|
115
|
+
failed++
|
|
116
|
+
return {
|
|
117
|
+
taskId,
|
|
118
|
+
success: false,
|
|
119
|
+
objectCount: 0,
|
|
120
|
+
error: 'Task has no image data'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Generate prediction
|
|
125
|
+
const result = await this.generatePrediction(
|
|
126
|
+
{
|
|
127
|
+
taskId,
|
|
128
|
+
imageUrl: task.data.image,
|
|
129
|
+
modelId: input.modelId,
|
|
130
|
+
confidenceThreshold: input.confidenceThreshold
|
|
131
|
+
},
|
|
132
|
+
context
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (result.success) {
|
|
136
|
+
succeeded++
|
|
137
|
+
} else {
|
|
138
|
+
failed++
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
} catch (error) {
|
|
143
|
+
failed++
|
|
144
|
+
return {
|
|
145
|
+
taskId,
|
|
146
|
+
success: false,
|
|
147
|
+
objectCount: 0,
|
|
148
|
+
error: error.message
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const batchResults = await Promise.all(batchPromises)
|
|
154
|
+
results.push(...batchResults)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(
|
|
158
|
+
`[LS AI Prediction] Batch completed: ${succeeded} succeeded, ${failed} failed out of ${input.taskIds.length} tasks`
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
total: input.taskIds.length,
|
|
163
|
+
succeeded,
|
|
164
|
+
failed,
|
|
165
|
+
results,
|
|
166
|
+
modelVersion: modelId
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('[LS AI Prediction] Batch processing failed:', error)
|
|
170
|
+
throw new Error(`Batch prediction generation failed: ${error.message}`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Auto-generate predictions for all unlabeled tasks in a project
|
|
176
|
+
*/
|
|
177
|
+
@Mutation(returns => BatchPredictionResult, {
|
|
178
|
+
description: 'Auto-generate predictions for all unlabeled tasks in a Label Studio project'
|
|
179
|
+
})
|
|
180
|
+
@Directive('@privilege(category: "label-studio", privilege: "mutation")')
|
|
181
|
+
async autoGeneratePredictions(
|
|
182
|
+
@Arg('projectId', type => Int) projectId: number,
|
|
183
|
+
@Arg('modelId', { nullable: true }) modelId: string,
|
|
184
|
+
@Arg('confidenceThreshold', type => Float, { nullable: true }) confidenceThreshold: number,
|
|
185
|
+
@Ctx() context: ResolverContext
|
|
186
|
+
): Promise<BatchPredictionResult> {
|
|
187
|
+
try {
|
|
188
|
+
// Get all tasks from project
|
|
189
|
+
const tasksResponse = await labelStudioApi.getTasks(projectId, {
|
|
190
|
+
page_size: 1000 // Adjust as needed
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const tasks = tasksResponse.tasks || tasksResponse.results || []
|
|
194
|
+
|
|
195
|
+
// Filter unlabeled tasks (no annotations)
|
|
196
|
+
const unlabeledTasks = tasks.filter((task: any) => {
|
|
197
|
+
const annotationCount = task.annotations?.length || task.total_annotations || 0
|
|
198
|
+
return annotationCount === 0
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
console.log(
|
|
202
|
+
`[LS AI Prediction] Auto-generating predictions for ${unlabeledTasks.length} unlabeled tasks in project ${projectId}`
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// Generate predictions in batch
|
|
206
|
+
return await this.generateBatchPredictions(
|
|
207
|
+
{
|
|
208
|
+
projectId,
|
|
209
|
+
taskIds: unlabeledTasks.map((t: any) => t.id),
|
|
210
|
+
modelId,
|
|
211
|
+
confidenceThreshold
|
|
212
|
+
},
|
|
213
|
+
context
|
|
214
|
+
)
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error(`[LS AI Prediction] Auto-generation failed for project ${projectId}:`, error)
|
|
217
|
+
throw new Error(`Auto prediction generation failed: ${error.message}`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Convert AI detection results to Label Studio format
|
|
223
|
+
* This handles the Label Studio-specific data structure
|
|
224
|
+
*/
|
|
225
|
+
private convertToLabelStudioFormat(objects: DetectedObject[]) {
|
|
226
|
+
return objects.map(obj => ({
|
|
227
|
+
from_name: 'label',
|
|
228
|
+
to_name: 'image',
|
|
229
|
+
type: 'rectanglelabels',
|
|
230
|
+
value: {
|
|
231
|
+
x: obj.bbox.x,
|
|
232
|
+
y: obj.bbox.y,
|
|
233
|
+
width: obj.bbox.width,
|
|
234
|
+
height: obj.bbox.height,
|
|
235
|
+
rectanglelabels: [obj.className]
|
|
236
|
+
}
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
}
|