@toloka-platform/sdk 0.11.2 → 0.11.4

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.
Files changed (76) hide show
  1. package/dist/cli/toloka.js +158 -154
  2. package/dist/cli/toloka.js.jsc +0 -0
  3. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  4. package/dist/cli/toloka.js.sha256 +1 -1
  5. package/docs/authoring.md +6 -4
  6. package/docs/cli.md +30 -11
  7. package/docs/limitations.md +2 -2
  8. package/docs/testing.md +4 -2
  9. package/examples/README.md +11 -0
  10. package/examples/app-crowdtesting/README.md +41 -0
  11. package/examples/app-crowdtesting/fields.ts +6 -0
  12. package/examples/app-crowdtesting/nodes/app-flow-test/node.ts +381 -0
  13. package/examples/app-crowdtesting/nodes/collection/node.ts +8 -0
  14. package/examples/app-crowdtesting/pipeline.browser.integration.spec.ts +77 -0
  15. package/examples/app-crowdtesting/pipeline.ts +17 -0
  16. package/examples/egocentric-video-collection/README.md +35 -0
  17. package/examples/egocentric-video-collection/fields.ts +24 -0
  18. package/examples/egocentric-video-collection/nodes/collection/node.ts +5 -0
  19. package/examples/egocentric-video-collection/nodes/record-activity/node.ts +760 -0
  20. package/examples/egocentric-video-collection/pipeline.browser.integration.spec.ts +65 -0
  21. package/examples/egocentric-video-collection/pipeline.ts +17 -0
  22. package/examples/product-moderation/README.md +49 -0
  23. package/examples/product-moderation/fields.ts +37 -0
  24. package/examples/product-moderation/nodes/collection/node.ts +16 -0
  25. package/examples/product-moderation/nodes/moderate-product/instruction.ts +77 -0
  26. package/examples/product-moderation/nodes/moderate-product/node.ts +368 -0
  27. package/examples/product-moderation/pipeline.browser.integration.spec.ts +48 -0
  28. package/examples/product-moderation/pipeline.ts +33 -0
  29. package/examples/video-pair-rating/fields.ts +16 -4
  30. package/examples/video-pair-rating/nodes/adjudicate/node.ts +78 -3
  31. package/examples/video-pair-rating/nodes/adjudicate_op/node.ts +28 -3
  32. package/examples/video-pair-rating/nodes/overall_preference_pick/node.ts +28 -3
  33. package/examples/video-pair-rating/nodes/rate_pair/node.ts +65 -2
  34. package/openapi/gen/api.ts +22 -0
  35. package/openapi/gen/models/generationBudget.ts +8 -0
  36. package/openapi/gen/models/generationBudgetReasoningEffort.ts +2 -0
  37. package/openapi/gen/models/generationModel.ts +12 -0
  38. package/openapi/gen/models/generationModelDefaultReasoningEffort.ts +2 -0
  39. package/openapi/gen/models/generationModelId.ts +2 -0
  40. package/openapi/gen/models/index.ts +8 -0
  41. package/openapi/gen/models/invalidFieldSchema.ts +6 -0
  42. package/openapi/gen/models/listGenerationModels200.ts +7 -0
  43. package/openapi/gen/models/listGenerationModels200DefaultGenerationModelId.ts +2 -0
  44. package/openapi/gen/models/pipelineGraphSnapshot.ts +2 -0
  45. package/package.json +2 -2
  46. package/skills/catalog.json +16 -12
  47. package/skills/toloka-build/SKILL.md +44 -58
  48. package/skills/toloka-build/references/human-work.md +156 -0
  49. package/skills/toloka-build/references/local-setup.md +136 -0
  50. package/skills/toloka-build/references/node-checks.md +11 -0
  51. package/skills/toloka-build/references/node-semantics.md +22 -213
  52. package/skills/toloka-build/references/task-ui.md +61 -0
  53. package/skills/toloka-build/references/workflow-and-cli.md +34 -228
  54. package/skills/toloka-fine-tuning/SKILL.md +1 -1
  55. package/skills/toloka-get-started/SKILL.md +1 -1
  56. package/skills/toloka-gisting/SKILL.md +1 -1
  57. package/src/authoring/index.ts +26 -7
  58. package/src/checks/task-ui-typecheck.ts +3 -3
  59. package/src/cli/analytics.ts +1 -0
  60. package/src/cli/check.ts +83 -12
  61. package/src/cli/commands/generation-model.ts +18 -0
  62. package/src/cli/commands/pipeline.ts +1 -1
  63. package/src/cli/commands/source.ts +14 -3
  64. package/src/cli/data.ts +5 -2
  65. package/src/cli/format-error.ts +3 -0
  66. package/src/cli/generation-models.ts +46 -0
  67. package/src/cli/program.ts +2 -0
  68. package/src/cli/project.ts +288 -138
  69. package/src/cli/trainings.ts +12 -0
  70. package/src/cli/ui.ts +3 -1
  71. package/src/client.ts +5 -0
  72. package/src/graph-rules.json +11 -0
  73. package/src/importer.ts +46 -40
  74. package/src/index.ts +4 -0
  75. package/src/transport.ts +23 -5
  76. package/src/validate.ts +183 -0
@@ -0,0 +1,381 @@
1
+ import { generation } from "@toloka-platform/sdk";
2
+
3
+ import { appTestVideo, deviceModel, itemId, osVersion } from "../../fields";
4
+
5
+ const installLink = "https://example.com/replace-with-real-build-link";
6
+
7
+ const installSteps = `1. **Uninstall** any existing copy of Acme Fitness
8
+
9
+ 2. **Open the install link** below
10
+
11
+ 3. If Google Play shows an "Unknown app" warning, tap **Install Anyway** — this is expected for a pre-release build
12
+
13
+ 4. **Open the app** once it installs`;
14
+
15
+ const recordingChecklist = `**Your recording must show:**
16
+
17
+ - **Installation** — start recording BEFORE installing
18
+ - **Workout scan ×2** — each run through to the form analysis results screen
19
+ - **Meal scan ×2** — each run through to the nutrition breakdown page
20
+ - **Today**, **Programs** and **Insights** screens, each clearly visible
21
+
22
+ Dismiss any paywall or premium upgrade screens — do not complete them.`;
23
+
24
+ export const appFlowTest = generation("app_flow_test", {
25
+ about: {
26
+ description:
27
+ "Collect unique screen recordings of testers exercising the full Acme Fitness journey on Android, one per person, to test app stability and flow correctness across real consumer devices.",
28
+ projectType: "data_collection",
29
+ },
30
+ publicName: "Full app flow test recording (Android)",
31
+ audienceAndPricing: {
32
+ audienceType: "human",
33
+ verifiedExperts: false,
34
+ audience: {
35
+ languages: { type: "and", values: [{ value: "English" }] },
36
+ specializations: { type: "and", values: [] },
37
+ countries: { type: "or", values: [] },
38
+ },
39
+ pricePerTask: 3,
40
+ timeForTask: 1800,
41
+ perExpertCapEnabled: true,
42
+ perExpertCapValue: 1,
43
+ },
44
+ entities: {
45
+ input: [
46
+ {
47
+ field: itemId,
48
+ entityType: "METADATA",
49
+ description: "Placeholder row identifier for this data collection task.",
50
+ },
51
+ ],
52
+ output: [
53
+ {
54
+ field: appTestVideo,
55
+ entityType: "VIDEO",
56
+ jsonPath: "$.app_test_video[0].sasLink",
57
+ description: "Screen recording of the full Acme Fitness test session on Android.",
58
+ relations: [{ field: itemId, rel_type: "context" }],
59
+ minDurationSeconds: 300,
60
+ maxDurationSeconds: 1800,
61
+ },
62
+ {
63
+ field: deviceModel,
64
+ entityType: "SHORT_ANSWER",
65
+ jsonPath: "$.device_model",
66
+ description: "Device manufacturer and model, as free text.",
67
+ relations: [{ field: appTestVideo, rel_type: "context" }],
68
+ },
69
+ {
70
+ field: osVersion,
71
+ entityType: "SHORT_ANSWER",
72
+ jsonPath: "$.os_version",
73
+ description: "Android version number, as free text.",
74
+ relations: [{ field: appTestVideo, rel_type: "context" }],
75
+ },
76
+ ],
77
+ },
78
+ qualityCriteria: [
79
+ {
80
+ id: "2a604228-ea59-49b4-81ba-7b51f7acd718",
81
+ qualityElement: "Installation captured",
82
+ thresholds: {
83
+ pass: "The recording starts before installation and shows the install link being opened, the install progress, and the app launching. If Google Play shows an 'Unknown app' warning, the recording also shows the tester confirming it with Install Anyway. This is what identifies which build was tested.",
84
+ fail: "The recording begins with the app already installed, or the installation is cut. The build under test cannot be identified.",
85
+ },
86
+ targetEntity: appTestVideo,
87
+ },
88
+ {
89
+ id: "42c05854-cdd5-4ea5-aa98-b43de315d493",
90
+ qualityElement: "Workout scan — at least 2 complete runs",
91
+ thresholds: {
92
+ pass: "The video shows at least 2 Workout scan runs attempted end to end. A run counts when the recording shows: opening the workout scanner and pointing the camera at yourself, the 'Analyzing form...' processing state, and then either the form analysis results screen OR a visible app failure (crash / error message) on that run. Count each attempted cycle you can see.",
93
+ fail: "Fewer than 2 complete Workout scan runs are visible — either too few were attempted, or runs were started but abandoned before the form analysis results screen appeared. Attempts that the app itself failed or errored on still count as attempted-and-shown cycles when the recording shows the user trying to complete them.",
94
+ },
95
+ targetEntity: appTestVideo,
96
+ },
97
+ {
98
+ id: "3b58bd9c-c65c-4445-8234-c87dbf0e053d",
99
+ qualityElement: "Meal scan — at least 2 complete runs",
100
+ thresholds: {
101
+ pass: "The video shows at least 2 Meal scan runs attempted end to end. A run counts when the recording shows: opening the meal scanner and pointing the camera at any food item, the 'Identifying food...' processing state, and then either the nutrition breakdown page OR a visible app failure (crash / error message) on that run. Count each attempted cycle you can see.",
102
+ fail: "Fewer than 2 complete Meal scan runs are visible — either too few were attempted, or runs were started but abandoned before the nutrition breakdown page appeared. Attempts that the app itself failed or errored on still count as attempted-and-shown cycles when the recording shows the user trying to complete them.",
103
+ },
104
+ targetEntity: appTestVideo,
105
+ },
106
+ {
107
+ id: "d4892764-b0c5-439f-acf9-4a29e70f301f",
108
+ qualityElement: "All required screens shown",
109
+ thresholds: {
110
+ pass: "Every required screen appears clearly at least once, identified by its on-screen text: Today, identified on screen by 'Today', 'Daily Plan', 'Activity Log'; Programs, identified on screen by 'Programs', 'Search programs', 'My Plan'; Insights, identified on screen by 'Insights', 'Trends', 'Weekly Summary'. Judge only what is visible on screen — never infer from which control was tapped.",
111
+ fail: "One or more required screens never appear. Check for the distinctive text of each before failing.",
112
+ },
113
+ targetEntity: appTestVideo,
114
+ },
115
+ {
116
+ id: "a870e710-ab78-42b4-a46f-aa8609255680",
117
+ qualityElement: "Recording legibility",
118
+ thresholds: {
119
+ pass: "Text, buttons and result screens are readable throughout, well enough to identify the on-screen text named in the other criteria (install warning, processing states, result screens, screen titles).",
120
+ fail: "The recording is blurred, cropped, over-compressed or obstructed to the point where the on-screen text named in the other criteria cannot be read.",
121
+ },
122
+ targetEntity: appTestVideo,
123
+ },
124
+ {
125
+ id: "93ef853c-740b-4d31-955e-721efeed10ac",
126
+ qualityElement: "Device model stated",
127
+ thresholds: {
128
+ pass: "A real device manufacturer and model is given, specific enough to identify the hardware (for example 'Samsung Galaxy S23' or 'Pixel 7a').",
129
+ fail: "Blank, placeholder, or too vague to identify the hardware — for example 'phone', 'android', 'mine', or random characters.",
130
+ },
131
+ targetEntity: deviceModel,
132
+ },
133
+ {
134
+ id: "d5394490-f327-4ace-a789-996668fdd458",
135
+ qualityElement: "OS version stated",
136
+ thresholds: {
137
+ pass: "A plausible Android version number is given — a positive integer major version, optionally followed by a minor version (for example '14' or '12.1'), that fits the Android release numbering. Do not reject a version only because it is newer than the releases you know about.",
138
+ fail: "Blank, placeholder, a word instead of a number (for example 'latest' or 'newest'), a codename without a number, random characters, or a number that cannot be an Android release version (for example '0', a year such as '2025', or a build number such as 'UP1A.231005').",
139
+ },
140
+ targetEntity: osVersion,
141
+ },
142
+ ],
143
+ instruction: `# Acme Fitness full app flow test (Android)
144
+
145
+ ## What you will do
146
+ Install Acme Fitness on your own Android device, screen-record yourself
147
+ using it, and upload the recording. Your recording tests app stability and
148
+ flow correctness on real hardware. Finding a bug is a good outcome, not a
149
+ failed task.
150
+
151
+ ## Steps
152
+ 1. START YOUR SCREEN RECORDING FIRST, before you open the install link.
153
+ 2. Uninstall any existing copy of Acme Fitness.
154
+ 3. Open the install link and install the build: ${installLink}
155
+ If Google Play shows an "Unknown app" warning, tap Install Anyway. This is expected for a pre-release build.
156
+ 4. Launch the app and sign in. Any supported method works. Answer any
157
+ onboarding questions so you reach the main app.
158
+ 5. Run each of these to completion, the required number of times:
159
+ - Workout scan (at least 2 complete runs): open the workout scanner and point the camera at yourself -> "Analyzing form..." -> the form analysis results screen
160
+ - Meal scan (at least 2 complete runs): open the meal scanner and point the camera at any food item -> "Identifying food..." -> the nutrition breakdown page
161
+ 6. Make sure each of these screens appears clearly at least once:
162
+ - Today, identified on screen by "Today", "Daily Plan", "Activity Log"
163
+ - Programs, identified on screen by "Programs", "Search programs", "My Plan"
164
+ - Insights, identified on screen by "Insights", "Trends", "Weekly Summary"
165
+ 7. Dismiss any paywall or premium upgrade screens — do not complete them. Nothing can be purchased.
166
+ 8. Stop the recording and upload it, then enter your device model and OS version.
167
+
168
+ ## Before you submit
169
+ - Is the installation visible at the start of the recording?
170
+ - Did you run Workout scan at least 2 times, each run ending on the result screen or a visible crash?
171
+ - Did you run Meal scan at least 2 times, each run ending on the result screen or a visible crash?
172
+ - Does Today appear clearly?
173
+ - Does Programs appear clearly?
174
+ - Does Insights appear clearly?
175
+ - Is the video between 5 and 30 minutes?
176
+ - Is the screen readable throughout, with no editing or trimming?
177
+
178
+ ## Examples
179
+ - Pass: the recording begins on your home screen, shows the install link, the
180
+ installation, and the app launching; two Workout scans reach the form analysis
181
+ results screen; the first Meal scan crashes the app on "Identifying food...", you
182
+ relaunch, and the next two reach the nutrition breakdown page; Today, Programs and
183
+ Insights each appear clearly. Device model "Pixel 7a", Android version "14".
184
+ - Fail: the recording starts with Acme Fitness already installed and shows only one
185
+ Meal scan. The build cannot be identified and the Meal scan count is short, so
186
+ the submission is rejected even though every screen appears.
187
+
188
+ ## If the app misbehaves
189
+ Keep recording. Relaunch if it crashes and carry on from where you were, then
190
+ submit the recording including the crash. A recording that captures a real bug
191
+ is more valuable than a clean one.
192
+
193
+ ## Your privacy
194
+ Turn on Do Not Disturb before recording so your notifications stay private, and
195
+ use a throwaway or test account to sign in where the app allows it. Recordings
196
+ are used only to assess app stability and flow correctness: https://example.com/privacy-policy`,
197
+ ui: {
198
+ code: `<View.Flex vertical={true} gap={'1.25rem'}>
199
+ <View.Card title={'Step 1: Install Acme Fitness'} variant={'info'}>
200
+ <View.Flex vertical={true} gap={'1rem'}>
201
+ <View.Alert
202
+ variant={'warning'}
203
+ title={'Start your screen recording first'}
204
+ description={'Begin recording BEFORE you tap the install link. We need to see the installation.'}
205
+ />
206
+ <View.Markdown content={CONST.INSTALL_STEPS} />
207
+ <View.Link url={CONST.INSTALL_LINK}>Open the install link</View.Link>
208
+ </View.Flex>
209
+ </View.Card>
210
+ <View.Card title={'Step 2: Record the full test'}>
211
+ <View.Markdown content={CONST.RECORDING_CHECKLIST} />
212
+ </View.Card>
213
+ <View.Card title={'Step 3: Upload and describe your device'}>
214
+ <View.Flex vertical={true} gap={'1rem'}>
215
+ <Field.VideoUpload
216
+ path={'app_test_video'}
217
+ label={'Screen recording'}
218
+ multiple={false}
219
+ linkType={'sasLink'}
220
+ minimumFiles={1}
221
+ minDurationSec={300}
222
+ maxDurationSec={1800}
223
+ />
224
+ <Field.Text
225
+ path={'device_model'}
226
+ label={'Device model'}
227
+ placeholder={'e.g. Samsung Galaxy S23'}
228
+ validation={[
229
+ { required: true, message: 'Enter your device manufacturer and model.' },
230
+ { max: 200, message: 'Keep this under 200 characters — just the manufacturer and model.' },
231
+ ]}
232
+ />
233
+ <Field.Text
234
+ path={'os_version'}
235
+ label={'Android version'}
236
+ placeholder={'e.g. 14'}
237
+ validation={[
238
+ { required: true, message: 'Enter your Android version.' },
239
+ { max: 50, message: 'Keep this under 50 characters — just the version number.' },
240
+ ]}
241
+ />
242
+ </View.Flex>
243
+ </View.Card>
244
+ </View.Flex>`,
245
+ data: '{"item_id":1}',
246
+ preprocess: "return {}",
247
+ postprocess: "return {};",
248
+ constants: JSON.stringify({
249
+ INSTALL_LINK: installLink,
250
+ INSTALL_STEPS: installSteps,
251
+ RECORDING_CHECKLIST: recordingChecklist,
252
+ }),
253
+ },
254
+ qualityAssurance: {
255
+ qaMode: "llm_only",
256
+ llmQaModelId: "gpt-5.4-mini",
257
+ },
258
+ onboarding: {
259
+ enabled: true,
260
+ steps: [
261
+ {
262
+ id: "step-1",
263
+ blocks: [
264
+ {
265
+ id: "intro",
266
+ type: "text",
267
+ title: "Testing Acme Fitness on Android",
268
+ content: [
269
+ {
270
+ type: "markdown",
271
+ value: `You will install Acme Fitness, record yourself using it, and upload the video.
272
+
273
+ You need: an Android device, free storage for a 30-minute recording, and a stable connection.
274
+
275
+ **If the app crashes or misbehaves, keep recording and carry on.** Capturing a
276
+ real bug is the most valuable thing you can do here — it will not cost you the task.`,
277
+ },
278
+ ],
279
+ },
280
+ ],
281
+ },
282
+ {
283
+ id: "step-2",
284
+ blocks: [
285
+ {
286
+ id: "requirements",
287
+ type: "text",
288
+ title: "What your recording must show",
289
+ content: [
290
+ {
291
+ type: "markdown",
292
+ value: `**1. The installation.** Start recording before you open the install link.
293
+ Without it we cannot tell which build you tested.
294
+
295
+ **2. Each flow, repeated.**
296
+ - Workout scan (at least 2 complete runs): open the workout scanner and point the camera at yourself -> "Analyzing form..." -> the form analysis results screen
297
+ - Meal scan (at least 2 complete runs): open the meal scanner and point the camera at any food item -> "Identifying food..." -> the nutrition breakdown page
298
+
299
+ **3. Every required screen, clearly visible.**
300
+ - Today, identified on screen by "Today", "Daily Plan", "Activity Log"
301
+ - Programs, identified on screen by "Programs", "Search programs", "My Plan"
302
+ - Insights, identified on screen by "Insights", "Trends", "Weekly Summary"
303
+
304
+ **4. Nothing purchased.** Dismiss any paywall or premium upgrade screens.
305
+
306
+ We check what is visible on screen, not which buttons you pressed — so make
307
+ sure each screen actually appears.`,
308
+ },
309
+ ],
310
+ },
311
+ ],
312
+ },
313
+ {
314
+ id: "step-3",
315
+ blocks: [
316
+ {
317
+ id: "quiz",
318
+ type: "quiz",
319
+ title: "Quick check",
320
+ questions: [
321
+ {
322
+ id: "q1",
323
+ type: "single-choice",
324
+ question: "When should you start your screen recording?",
325
+ options: [
326
+ { value: "before_install", label: "Before opening the install link" },
327
+ { value: "after_install", label: "Once the app is installed" },
328
+ { value: "after_signin", label: "After signing in" },
329
+ ],
330
+ correctAnswer: "before_install",
331
+ },
332
+ {
333
+ id: "q2",
334
+ type: "single-choice",
335
+ question: "How many times must you complete Workout scan?",
336
+ options: [
337
+ { value: "one", label: "Once is enough" },
338
+ { value: "required", label: "At least 2 complete runs" },
339
+ { value: "many", label: "As many as possible, at least ten" },
340
+ ],
341
+ correctAnswer: "required",
342
+ },
343
+ {
344
+ id: "q3",
345
+ type: "single-choice",
346
+ question: "The app crashes halfway through your session. What do you do?",
347
+ options: [
348
+ { value: "keep", label: "Keep recording, relaunch, carry on, and submit it" },
349
+ { value: "restart", label: "Delete the recording and start over" },
350
+ {
351
+ value: "abandon",
352
+ label: "Abandon the task — a crash means it cannot be submitted",
353
+ },
354
+ ],
355
+ correctAnswer: "keep",
356
+ },
357
+ {
358
+ id: "q4",
359
+ type: "single-choice",
360
+ question: "How do we confirm you visited Today?",
361
+ options: [
362
+ { value: "screen", label: 'The screen itself appears, showing "Today"' },
363
+ { value: "tap", label: "We detect which tab you tapped" },
364
+ { value: "time", label: "We measure how long you spent there" },
365
+ ],
366
+ correctAnswer: "screen",
367
+ },
368
+ ],
369
+ },
370
+ ],
371
+ },
372
+ ],
373
+ info: {
374
+ estimatedTime: "5-30 minutes",
375
+ role: "Android app tester",
376
+ goal: "Record a full Acme Fitness test session",
377
+ successCriteria:
378
+ "Installation captured, every flow completed the required number of times, every required screen shown",
379
+ },
380
+ },
381
+ });
@@ -0,0 +1,8 @@
1
+ import { root } from "@toloka-platform/sdk";
2
+
3
+ import { appTestVideo, deviceModel, itemId, osVersion } from "../../fields";
4
+
5
+ export const collection = root({
6
+ started: [itemId],
7
+ completed: [appTestVideo, deviceModel, osVersion],
8
+ });
@@ -0,0 +1,77 @@
1
+ import { expect, test } from "@toloka-platform/sdk/testing";
2
+ import { describe } from "vitest";
3
+ import { assert } from "es-toolkit";
4
+ import type { TaskUiSession } from "@toloka-platform/sdk/testing";
5
+
6
+ import pipeline from "./pipeline";
7
+ import { appFlowTest } from "./nodes/app-flow-test/node";
8
+
9
+ const item = { item_id: 7 };
10
+ const device = { device_model: "Samsung Galaxy S23", os_version: "14" };
11
+
12
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
13
+ typeof value === "object" && value !== null && !Array.isArray(value);
14
+
15
+ const documentedRecording = async (session: TaskUiSession): Promise<Record<string, unknown>> => {
16
+ const documentation = await session.componentDocumentation(["Field.VideoUpload"]);
17
+ const output = documentation["Field.VideoUpload"]?.example?.output;
18
+ assert(output, "Field.VideoUpload has no documented example output");
19
+ const example: unknown = JSON.parse(output);
20
+ assert(isRecord(example), "Field.VideoUpload example output is not an object");
21
+ const [upload] = Object.values(example);
22
+ assert(Array.isArray(upload), "Field.VideoUpload example output is not an array");
23
+ const [recording] = upload;
24
+ assert(isRecord(recording), "Field.VideoUpload example contains a non-object value");
25
+ assert(typeof recording.sasLink === "string", "Field.VideoUpload example has no sasLink");
26
+ return {
27
+ ...recording,
28
+ userFileName: "acme-fitness-session.mp4",
29
+ durationSec: 912.4,
30
+ videoWidth: 1080,
31
+ videoHeight: 2400,
32
+ };
33
+ };
34
+
35
+ describe("app crowdtesting example", () => {
36
+ test.ui(pipeline, appFlowTest, "submits a recording with device metadata", async ({ render }) => {
37
+ const session = await render({ data: item, allowPartialCoverage: true });
38
+ const recording = await documentedRecording(session);
39
+ await session.setValue("app_test_video", [recording]);
40
+
41
+ expect(await session.getValue("app_test_video")).toEqual([recording]);
42
+ expect(await session.validationErrors()).toEqual(
43
+ expect.arrayContaining([
44
+ expect.objectContaining({ path: "device_model" }),
45
+ expect.objectContaining({ path: "os_version" }),
46
+ ]),
47
+ );
48
+ await session.setValue("device_model", device.device_model);
49
+ await session.setValue("os_version", device.os_version);
50
+ await expect(session.submit()).resolves.toMatchObject({
51
+ ...item,
52
+ app_test_video: [recording],
53
+ ...device,
54
+ });
55
+ expect(session.coverage.complete).toBe(false);
56
+ await session.close();
57
+ });
58
+
59
+ test.pipeline(pipeline, "collects one recording per row", async ({ renderUi }) => {
60
+ const session = await renderUi(appFlowTest, { data: item, allowPartialCoverage: true });
61
+ const recording = await documentedRecording(session);
62
+ await session.setValue("app_test_video", [recording]);
63
+ await session.setValue("device_model", device.device_model);
64
+ await session.setValue("os_version", device.os_version);
65
+ const submission = await session.submit();
66
+ const uploads = submission.app_test_video;
67
+ assert(Array.isArray(uploads) && isRecord(uploads[0]), "No recording was submitted");
68
+ const completed = {
69
+ app_test_video: uploads[0].sasLink,
70
+ device_model: submission.device_model,
71
+ os_version: submission.os_version,
72
+ };
73
+
74
+ expect(completed).toEqual({ app_test_video: recording.sasLink, ...device });
75
+ await session.close();
76
+ });
77
+ });
@@ -0,0 +1,17 @@
1
+ import { connect, definePipeline } from "@toloka-platform/sdk";
2
+
3
+ import { appTestVideo, deviceModel, itemId, osVersion } from "./fields";
4
+ import { appFlowTest } from "./nodes/app-flow-test/node";
5
+ import { collection } from "./nodes/collection/node";
6
+
7
+ export const pipeline = definePipeline({
8
+ dataset: "app-test-runs",
9
+ fields: [itemId, appTestVideo, deviceModel, osVersion],
10
+ nodes: [collection, appFlowTest],
11
+ connections: [
12
+ connect(collection.started, appFlowTest.input),
13
+ connect(appFlowTest.output, collection.completed),
14
+ ],
15
+ });
16
+
17
+ export default pipeline;
@@ -0,0 +1,35 @@
1
+ # Egocentric video collection
2
+
3
+ A data-collection pipeline: each dataset item is an empty collection slot, and one participant fills it by recording a first-person video of a household activity with a head-mounted phone, then tagging it with one of 13 activity categories. LLM QA grades every submission against eight quality criteria before it is written back.
4
+
5
+ ## Shape
6
+
7
+ ```
8
+ root.started id (one row per collection slot)
9
+ └─> record_activity generation data_collection: record a video, pick its category
10
+ └─> root.completed recorded_video, task_category
11
+ ```
12
+
13
+ Two nodes: one root, one `generation`. Training, QA, redo, and the per-expert cap all live inside `record_activity`; none of them is a graph node.
14
+
15
+ ## What to read first
16
+
17
+ | file | why |
18
+ | -------------------------------------- | -------------------------------------------------------------------------- |
19
+ | `fields.ts` | the category list is declared once and feeds the enum, labels, and Task UI |
20
+ | `nodes/record-activity/node.ts` | a complete human collection node: entities, criteria, UI, Training, checks |
21
+ | `pipeline.browser.integration.spec.ts` | how to drive a hardware-backed recorder in the headless Task UI |
22
+
23
+ ## Patterns this example demonstrates
24
+
25
+ **Collection with a recording field.** `about.projectType` is `data_collection` and the screen is built around `Field.VideoRecord` with `nativeCamera={true}`, which the component contract requires for egocentric capture: the take is recorded with the phone's native camera in the Toloka app, and hosts without native camera support block the task instead of falling back to the in-browser recorder. The recorder's value is an array of takes, but the task yields one logical video, so `recorded_video` is a scalar URI field and its `VIDEO` entity stays static, reading `$.video_file[0].sasLink`. The entity's duration bounds match the recorder's `minDurationSec` and `maxDurationSec`.
26
+
27
+ **A category label output.** `task_category` is a `SINGLE_LABEL` entity that depends on the video. `taskCategories` drives the field `enum`, the entity's `allowedLabels`, and the `Field.Select` options through `ui.constants`, so the three can never disagree. The instruction, Training, quiz, and category criterion name the categories with the same labels.
28
+
29
+ **LLM QA with one criterion per concern.** Seven criteria judge the video (stability, hands, valid task, residential setting, POV angle, PII, originality) and one judges whether the category matches the footage. Where two criteria could overlap, such as setting and originality, each states what it does not judge.
30
+
31
+ **Training with quizzes and practice.** `onboarding` teaches the setup, checks it with two quizzes, grades a free-text practice review with validators, and ends with a consent block.
32
+
33
+ **Stored `annotation_qa` checks.** Two platform-side test cases hold a task input and its expected output by field name, built with `JSON.stringify` rather than hand-escaped strings.
34
+
35
+ **Hardware-backed UI tests.** The headless browser has no camera, so the tests feed the recorder the documented example take through `setValue` and render with `allowPartialCoverage: true`. Local evidence for this node is therefore `partial`; a real device run is still needed to verify recording itself.
@@ -0,0 +1,24 @@
1
+ import { field } from "@toloka-platform/sdk";
2
+
3
+ export const taskCategories = [
4
+ { label: "Food Preparation", value: "food_preparation" },
5
+ { label: "Repair & Assembly", value: "repair_assembly" },
6
+ { label: "Home Organization", value: "home_organization" },
7
+ { label: "Desk & Office Tasks", value: "desk_office_tasks" },
8
+ { label: "Craft / Hobby / Fine Manipulation", value: "craft_hobby_fine_manipulation" },
9
+ { label: "Personal Care (Bathroom)", value: "personal_care_bathroom" },
10
+ { label: "Cleaning", value: "cleaning" },
11
+ { label: "Laundry & Garment Care", value: "laundry_garment_care" },
12
+ { label: "Miscellaneous Object Interaction", value: "miscellaneous_object_interaction" },
13
+ { label: "Dining Table Tasks", value: "dining_table_tasks" },
14
+ { label: "Plant Care Activities", value: "plant_care_activities" },
15
+ { label: "Animal Care Activities", value: "animal_care_activities" },
16
+ { label: "Yard & Landscaping Operations", value: "yard_landscaping_operations" },
17
+ ];
18
+
19
+ export const slotId = field("id", { type: "integer" });
20
+ export const recordedVideo = field("recorded_video", { type: "string", format: "uri" });
21
+ export const taskCategory = field("task_category", {
22
+ type: "string",
23
+ enum: taskCategories.map((category) => category.value),
24
+ });
@@ -0,0 +1,5 @@
1
+ import { root } from "@toloka-platform/sdk";
2
+
3
+ import { recordedVideo, slotId, taskCategory } from "../../fields";
4
+
5
+ export const collection = root({ started: [slotId], completed: [recordedVideo, taskCategory] });