Skip to content

Commit 48fcae9

Browse files
caitlinwheelesscaitlinwheeless
and
caitlinwheeless
authored
docs: DOC-282: Pause an annotator (#7147)
Co-authored-by: caitlinwheeless <[email protected]>
1 parent d98964a commit 48fcae9

9 files changed

+251
-1
lines changed

docs/source/guide/quality.md

+28
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,34 @@ For specific labels, you can see in a donut chart how many labels of each type w
116116

117117
For example, if you're developing a dataset of OCR images, and 90% of your tasks have **Text** labels and 10% have **Handwriting** labels, you might want to increase the number of images of handwriting in your dataset, to improve the eventual accuracy of a machine learning model trained on this dataset.
118118

119+
## Pause an annotator
120+
121+
For organizations with a large number of annotators, it might prove useful to pause an annotator's progress. This might be useful for annotators that are performing poorly or exhibiting behavior that might indicate they have automated their work (bot behavior).
122+
123+
You can pause annotators from the Members dashboard in a project. This action is only available next to users in the Annotator and Reviewer roles:
124+
125+
![Screenshot of pause](/images/review/pause.png)
126+
127+
When a user is paused, the following occurs:
128+
129+
* They immediately see a message informing them that they have been paused.
130+
131+
![Screenshot of message](/images/review/paused-message.png)
132+
* Their progress within their current task is saved as a draft, but they cannot make any further changes.
133+
* When they click **Go Back**, they are returned to the Projects page. If they attempt to re-enter the project, they are shown the error message above.
134+
135+
!!! info Tip
136+
137+
If you have [custom scripts](scripts) enabled, you can automatically pause an annotator based on certain behaviors and then customize the message that appears on their screen.
138+
139+
For more information, see [Custom script examples - Pause an annotator](script_examples#Pause-an-annotator).
140+
141+
!!! info Tip
142+
143+
If you hover over the **Paused** indicator, you can see the message that was shown to the user when they were paused. If a user was manually paused, it also shows who initiated the action.
144+
145+
![Screenshot of hover](/images/review/paused-tooltip.png)
146+
119147
## Verify model and annotator performance
120148

121149
To verify the performance of specific annotators, review the **Members** section for a specific project. If you don't see an annotator's activity reflected, make sure they have been added as a member to the project.

docs/source/guide/script_examples.md

+223-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ The following examples work when [custom scripts](scripts) are enabled.
1515

1616
For details on implementing your own custom scripts, see [Label Studio Interface (LSI)](scripts#Label-Studio-Interface-LSI) and [Frontend API implementation details](scripts#Frontend-API-implementation-details).
1717

18+
!!! info Tip
19+
You can find additional script examples in our [label-studio-custom-scripts repo](https://github.com/HumanSignal/label-studio-custom-scripts).
20+
1821
## Plotly
1922

2023
Use [Plotly](https://plotly.com/) to insert charts and graphs into your labeling interface. Charts are rendered in every annotation opened by a user.
@@ -597,4 +600,223 @@ The `TimelineLabels` tag is connected to the second video (`video0`), allowing a
597600
"video_url": "https://example.com/path/to/video.mp4"
598601
}
599602
}
600-
```
603+
```
604+
605+
## Pause an annotator
606+
607+
You can manually [pause an annotator](quality#Pause-an-annotator) to prevent stop them from completing tasks and revoke their project access.
608+
609+
This script automatically pauses an annotator who breaks any of the following rules and customizes the message that appears:
610+
611+
* Too many duplicate values `timesInARow(3)`:
612+
613+
Checks if the last three submitted annotations in the `TextArea` field (`comment`) all have the same value. If they do, it returns a custom warning message.
614+
615+
![Screenshot of warning](/images/project/scripts_pause1.png)
616+
617+
* Too many similar values `tooSimilar()`:
618+
619+
For the `Choices` options (`sentiment`), it computes a deviation over the past values. If the deviation is below a threshold (meaning the values are too uniform/similar), it returns a custom warning message.
620+
621+
![Screenshot of warning](/images/project/scripts_pause2.png)
622+
623+
* Too many submissions over a period of time `tooFast()`:
624+
625+
Monitors the overall speed of annotations. It checks if, for example, 20 annotations were submitted in less than 10 minutes. If so, a custom warning appears.
626+
627+
![Screenshot of warning](/images/project/scripts_pause3.png)
628+
629+
To unpause an annotator, use the [Members dashboard](quality#Pause-an-annotator).
630+
631+
!!! info Tip
632+
633+
If you hover over the **Paused** indicator, you can see the message that was shown to the user when they were paused. If a user was manually paused, it also shows who initiated the action.
634+
635+
![Screenshot of hover](/images/project/scripts_pause_hover.png)
636+
637+
#### Script
638+
639+
```javascript
640+
/****** CONFIGURATION FOR PAUSING RULES ******/
641+
/**
642+
* `fields` describe per-field rules in a format
643+
* <field-name>: [<rule>(<optional params for the rule>)]
644+
* `global` is for rules applied to the whole annotation
645+
*/
646+
const RULES = {
647+
fields: {
648+
comment: [timesInARow(3)],
649+
sentiment: [tooSimilar()],
650+
},
651+
global: [tooFast()],
652+
}
653+
/**
654+
* Messages for users when they are paused.
655+
* Each message is a function with the same name as original rule and it receives an object with
656+
* `items` and `field`.
657+
*/
658+
const MESSAGES = {
659+
timesInARow: ({ field }) => `Too many similar values for ${field}`,
660+
tooSimilar: ({ field }) => `Too similar values for ${field}`,
661+
tooFast: () => `Too fast annotations`,
662+
}
663+
664+
665+
666+
/****** ALL AVAILABLE RULES ******/
667+
/**
668+
* They recieve params and return function which recieves `items` and optional `field`.
669+
* If condition is met it returns warning message. If not — returns `false`.
670+
*/
671+
672+
// check if values for the `field` in last `times` items are the same
673+
function timesInARow(times) {
674+
return (items, field) => {
675+
if (items.length < times) return false
676+
const last = String(items.at(-1).values[field])
677+
return items.slice(-times).every((item) => String(item.values[field]) === last)
678+
? MESSAGES.timesInARow({ items, field })
679+
: false
680+
};
681+
}
682+
function tooSimilar(deviation = 0.1, max_count = 10) {
683+
return (items, field) => {
684+
if (items.length < max_count) return false
685+
const values = items.map((item) => item.values[field])
686+
const points = values.map((v) => values.indexOf(v))
687+
return calcDeviation(points) < deviation
688+
? MESSAGES.tooSimilar({ items, field })
689+
: false
690+
};
691+
}
692+
function tooFast(minutes = 10, times = 20) {
693+
return (items) => {
694+
if (items.length < times) return false
695+
const last = items.at(-1)
696+
const first = items.at(-times)
697+
return last.created_at - first.created_at < minutes * 60
698+
? MESSAGES.tooFast({ items })
699+
: false
700+
};
701+
}
702+
703+
/****** INTERNAL CODE ******/
704+
const project = DM.project.id
705+
if (!DM.project) return;
706+
707+
const key = ["__pause_stats", project].join("|")
708+
const fields = Object.keys(RULES.fields)
709+
// { sentiment: ["positive", ...], comment: undefined }
710+
const values = Object.fromEntries(fields.map(
711+
(field) => [field, DM.project.parsed_label_config[field]?.labels],
712+
))
713+
714+
// simplified version of MSE with normalized x-axis
715+
function calcDeviation(data) {
716+
const n = data.length;
717+
// we normalize indices from -n/2 to n/2 so meanX is 0
718+
const mid = n / 2;
719+
const mean = data.reduce((a, b) => a + b) / n;
720+
721+
const k = data.reduce((a, b, i) => a + (b - mean) * (i - mid), 0) / data.reduce((a, b, i) => a + (i - mid) ** 2, 0);
722+
const mse = data.reduce((a, b, i) => a + (b - (k * (i - mid) + mean)) ** 2, 0) / n;
723+
724+
return Math.abs(mse);
725+
}
726+
727+
LSI.on("submitAnnotation", (_store, ann) => {
728+
const results = ann.serializeAnnotation()
729+
// { sentiment: "positive", comment: "good" }
730+
const values = {}
731+
fields.forEach((field) => {
732+
const value = results.find((r) => r.from_name === field)?.value
733+
if (!value) return;
734+
if (value.choices) values[field] = value.choices.join("|")
735+
else if (value.text) values[field] = value.text
736+
})
737+
let stats = []
738+
try {
739+
stats = JSON.parse(localStorage.getItem(key)) ?? []
740+
} catch(e) {}
741+
stats.push({ values, created_at: Date.now() / 1000 })
742+
743+
for (const rule of RULES.global) {
744+
const result = rule(stats)
745+
if (result) {
746+
localStorage.setItem(key, "[]");
747+
pause(result);
748+
return;
749+
}
750+
}
751+
752+
for (const field of fields) {
753+
if (!values[field]) continue;
754+
for (const rule of RULES.fields[field]) {
755+
const result = rule(stats, field)
756+
if (result) {
757+
localStorage.setItem(key, "[]");
758+
pause(result);
759+
return;
760+
}
761+
}
762+
}
763+
764+
localStorage.setItem(key, JSON.stringify(stats));
765+
});
766+
767+
function pause(verbose_reason) {
768+
const body = {
769+
reason: "CUSTOM_SCRIPT",
770+
verbose_reason,
771+
}
772+
const options = {
773+
method: "POST",
774+
headers: { "Content-Type": "application/json" },
775+
body: JSON.stringify(body),
776+
}
777+
fetch(`/api/projects/${project}/members/${Htx.user.id}/pauses`, options)
778+
}
779+
```
780+
781+
**Related LSI instance methods:**
782+
783+
* [on(eventName, handler)](scripts#on-eventName-handler)
784+
785+
**Related frontend events:**
786+
787+
* [submitAnnotation](frontend_reference#submitAnnotationn)
788+
789+
#### Labeling config
790+
791+
This labeling config presents users with text and asks them to:
792+
793+
* Provide a sentiment value using `<Choices>`
794+
* Comment on their reasoning using `<TextArea>`
795+
796+
```xml
797+
<View>
798+
<Text name="text" value="$text"/>
799+
<View style="box-shadow: 2px 2px 5px #999; padding: 20px; margin-top: 2em; border-radius: 5px;">
800+
801+
<Header value="What is the sentiment of this text?" />
802+
<Choices name="sentiment" toName="text" choice="single" showInLine="true">
803+
<Choice value="positive" hotkey="1" />
804+
<Choice value="negative" hotkey="2" />
805+
<Choice value="neutral" hotkey="3" />
806+
</Choices>
807+
808+
<Header value="Why?" />
809+
<TextArea name="comment" toName="text" rows="4" placeholder="Add your comment here..." />
810+
811+
</View>
812+
</View>
813+
814+
```
815+
816+
**Related tags:**
817+
818+
* [View](/tags/view.html)
819+
* [Text](/tags/text.html)
820+
* [Header](/tags/header.html)
821+
* [Choices](/tags/choices.html)
822+
* [TextArea](/tags/textarea.html)
Loading
Loading
Loading
Loading
276 KB
Loading
Loading
Loading

0 commit comments

Comments
 (0)