Skip to content

Commit d0759ef

Browse files
Adding last two plugins and updating bulk labeling
1 parent 789ea77 commit d0759ef

File tree

11 files changed

+561
-147
lines changed

11 files changed

+561
-147
lines changed

docs/source/plugins/bulk_labeling.md

Lines changed: 119 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
2-
title: Bulk Labeling for Text Spans
2+
title: Bulk Labeling for Text Spans with Keyboard Shortcut
3+
short: Bulk Labeling for Text Spans
34
type: plugins
45
category: Automation
56
cat: automation
@@ -18,68 +19,143 @@ tier: enterprise
1819

1920
## About
2021

21-
This plugin automatically applies the same label to all matching text spans.
22+
This plugin automatically applies the same label to all matching text spans when you press the **Shift** key.
2223

2324
For example, if you apply the `PER` label to the text span `Smith`, this plugin will automatically find all instances of `Smith` in the text and apply the `PER` label to them.
2425

25-
![Screenshot of bulk text labeling](/images/plugins/bulk-labeling.gif)
26+
![Screenshot of bulk text labeling](/images/plugins/bulk_actions.gif)
27+
28+
1. **Shift key tracking**
29+
- The plugin tracks the state of the Shift key using `keydown` and `keyup` event listeners. A boolean variable `isShiftKeyPressed` is set to `true` when the Shift key is pressed and `false` when it is released.
30+
2. **Bulk deletion**
31+
- When a region (annotation) is deleted and the Shift key is pressed, the plugin identifies all regions with the same text and label as the deleted region.
32+
- It then deletes all these matching regions to facilitate bulk deletion.
33+
3. **Bulk creation**
34+
- When a region is created and the Shift key is pressed, the plugin searches for all occurrences of the created region's text within the document.
35+
- It creates new regions for each occurrence of the text, ensuring that no duplicate regions are created (i.e., regions with overlapping start and end offsets are avoided).
36+
- The plugin also prevents tagging of single characters to avoid unnecessary annotations.
37+
4. **Timeout**
38+
- To prevent rapid consecutive bulk operations, the plugin sets a timeout of 1 second. This ensures that bulk operations are not triggered too frequently.
2639

2740
## Plugin
2841

2942
```javascript
3043
/**
31-
* Automatically creates all the text regions containing all instances of the selected text.
44+
* Automatically creates text regions for all instances of the selected text and deletes existing regions
45+
* when the shift key is pressed.
3246
*/
3347

34-
// It will be triggered when a text selection happens
35-
LSI.on('entityCreate', region => {
36-
if (window.BULK_REGIONS) return;
37-
window.BULK_REGIONS = true;
38-
39-
const regionTextLength = region.text.length;
40-
const regex = new RegExp(region.text, "gi");
41-
const matches = Array.from(region.object._value.matchAll(regex));
42-
43-
setTimeout(() => window.BULK_REGIONS = false, 1000);
44-
45-
if (matches.length > 1) {
46-
const results = matches.reduce((acc, m) => {
47-
if (m.index !== region.startOffset) {
48-
acc.push({
49-
id: String(Htx.annotationStore.selected.results.length + acc.length + 1),
50-
from_name: region.labeling.from_name.name,
51-
to_name: region.object.name,
52-
type: "labels",
53-
value: {
54-
text: region.text,
55-
start: "/span[1]/text()[1]",
56-
startOffset: m.index,
57-
end: "/span[1]/text()[1]",
58-
endOffset: m.index + regionTextLength,
59-
labels: [...region.labeling.value.labels],
60-
},
61-
origin: "manual",
62-
63-
});
64-
}
65-
return acc;
66-
}, []);
67-
68-
if (results.length > 0) {
69-
Htx.annotationStore.selected.deserializeResults(results);
70-
Htx.annotationStore.selected.updateObjects();
71-
}
72-
}
48+
// Track the state of the shift key
49+
let isShiftKeyPressed = false;
50+
51+
window.addEventListener("keydown", (e) => {
52+
if (e.key === "Shift") {
53+
isShiftKeyPressed = true;
54+
}
55+
});
56+
57+
window.addEventListener("keyup", (e) => {
58+
if (e.key === "Shift") {
59+
isShiftKeyPressed = false;
60+
}
61+
});
62+
63+
LSI.on("entityDelete", (region) => {
64+
if (!isShiftKeyPressed) return; // Only proceed if the shift key is pressed
65+
66+
if (window.BULK_REGIONS) return;
67+
window.BULK_REGIONS = true;
68+
setTimeout(() => {
69+
window.BULK_REGIONS = false;
70+
}, 1000);
71+
72+
const existingEntities = Htx.annotationStore.selected.regions;
73+
const regionsToDelete = existingEntities.filter((entity) => {
74+
const deletedText = region.text.toLowerCase().replace("\\\\n", " ");
75+
const otherText = entity.text.toLowerCase().replace("\\\\n", " ");
76+
return deletedText === otherText && region.labels[0] === entity.labels[0];
77+
});
78+
79+
for (const region of regionsToDelete) {
80+
Htx.annotationStore.selected.deleteRegion(region);
81+
}
82+
83+
Htx.annotationStore.selected.updateObjects();
84+
});
85+
86+
LSI.on("entityCreate", (region) => {
87+
if (!isShiftKeyPressed) return;
88+
89+
if (window.BULK_REGIONS) return;
90+
window.BULK_REGIONS = true;
91+
setTimeout(() => {
92+
window.BULK_REGIONS = false;
93+
}, 1000);
94+
95+
const existingEntities = Htx.annotationStore.selected.regions;
96+
97+
setTimeout(() => {
98+
// Prevent tagging a single character
99+
if (region.text.length < 2) return;
100+
regexp = new RegExp(
101+
region.text.replace("\\\\n", "\\\\s+").replace(" ", "\\\\s+"),
102+
"gi",
103+
);
104+
const matches = Array.from(region.object._value.matchAll(regexp));
105+
for (const match of matches) {
106+
if (match.index === region.startOffset) continue;
107+
108+
const startOffset = match.index;
109+
const endOffset = match.index + region.text.length;
110+
111+
// Check for existing entities with overlapping start and end offset
112+
let isDuplicate = false;
113+
for (const entity of existingEntities) {
114+
if (
115+
startOffset <= entity.globalOffsets.end &&
116+
entity.globalOffsets.start <= endOffset
117+
) {
118+
isDuplicate = true;
119+
break;
120+
}
121+
}
122+
123+
if (!isDuplicate) {
124+
Htx.annotationStore.selected.createResult(
125+
{
126+
text: region.text,
127+
start: "/span[1]/text()[1]",
128+
startOffset: startOffset,
129+
end: "/span[1]/text()[1]",
130+
endOffset: endOffset,
131+
},
132+
{
133+
labels: [...region.labeling.value.labels],
134+
},
135+
region.labeling.from_name,
136+
region.object,
137+
);
138+
}
139+
}
140+
141+
Htx.annotationStore.selected.updateObjects();
142+
}, 100);
73143
});
74144
```
75145

76146
**Related LSI instance methods:**
77147

78148
* [on(eventName, handler)](custom#LSI-on-eventName-handler)
79149

150+
**Related frontend APIs:**
151+
152+
* [regions](custom#regions)
153+
80154
**Related frontend events:**
81155

82156
* [entityCreate](/guide/frontend_reference#entityCreate)
157+
* [entityDelete](/guide/frontend_reference#entityDelete)
158+
83159

84160
## Labeling config
85161

docs/source/plugins/frame_offset.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
---
2+
title: Multi-Frame Video View
3+
type: plugins
4+
category: Visualization
5+
cat: visualization
6+
order: 240
7+
meta_title: Multi-Frame Video View
8+
meta_description: Synchronizes multiple video views to display a video with different frame offsets
9+
tier: enterprise
10+
---
11+
12+
<img src="/images/plugins/frame-offset-thumb.png" alt="" class="gif-border" style="max-width: 552px !important;" />
13+
14+
!!! note
15+
For information about modifying this plugin or creating your own custom plugins, see [Customize and Build Your Own Plugins](custom).
16+
17+
For general plugin information, see [Plugins for projects](/guide/plugins) and [Plugin FAQ](faq).
18+
19+
## About
20+
21+
This labeling configuration arranges three video players vertically, making it easier to view and annotate each video frame.
22+
23+
The plugin ensures the videos are synced, with one player showing one frame forward, and another player the previous frame.
24+
25+
![Screenshot of video sync labeling config](/images/plugins/video_sync.png)
26+
27+
## Plugin
28+
29+
```javascript
30+
/**
31+
* Multi-frame video view plugin
32+
*
33+
* This plugin synchronizes three video views to display a video with three frames:
34+
* -1 frame, 0 frame, and +1 frame.
35+
*
36+
* It also synchronizes the timeline labels to the 0 frame.
37+
*/
38+
39+
async function initMultiFrameVideoView() {
40+
// Wait for the Label Studio Interface to be ready
41+
await LSI;
42+
43+
// Get references to the video objects by their names
44+
const videoMinus1 = LSI.annotation.names.get("videoMinus1");
45+
const video0 = LSI.annotation.names.get("video0");
46+
const videoPlus1 = LSI.annotation.names.get("videoPlus1");
47+
48+
if (!videoMinus1 || !video0 || !videoPlus1) return;
49+
50+
// Convert frameRate to a number and ensure it's valid
51+
const frameRate = Number.parseFloat(video0.framerate) || 24;
52+
const frameDuration = 1 / frameRate;
53+
54+
// Function to adjust video sync with offset and guard against endless loops
55+
function adjustVideoSync(video, offsetFrames) {
56+
video.isSyncing = false;
57+
58+
for (const event of ["seek", "play", "pause"]) {
59+
video.syncHandlers.set(event, (data) => {
60+
if (!video.isSyncing) {
61+
video.isSyncing = true;
62+
63+
if (video.ref.current && video !== video0) {
64+
const videoElem = video.ref.current;
65+
66+
adjustedTime =
67+
(video0.ref.current.currentFrame + offsetFrames) * frameDuration;
68+
adjustedTime = Math.max(
69+
0,
70+
Math.min(adjustedTime, video.ref.current.duration),
71+
);
72+
73+
if (data.playing) {
74+
if (!videoElem.playing) videoElem.play();
75+
} else {
76+
if (videoElem.playing) videoElem.pause();
77+
}
78+
79+
if (data.speed) {
80+
video.speed = data.speed;
81+
}
82+
83+
videoElem.currentTime = adjustedTime;
84+
if (
85+
Math.abs(videoElem.currentTime - adjustedTime) >
86+
frameDuration / 2
87+
) {
88+
videoElem.currentTime = adjustedTime;
89+
}
90+
}
91+
92+
video.isSyncing = false;
93+
}
94+
});
95+
}
96+
}
97+
98+
// Adjust offsets for each video
99+
adjustVideoSync(videoMinus1, -1);
100+
adjustVideoSync(videoPlus1, 1);
101+
adjustVideoSync(video0, 0);
102+
}
103+
104+
// Initialize the plugin
105+
initMultiFrameVideoView();
106+
```
107+
108+
**Related LSI instance methods:**
109+
110+
* [annotation](custom#LSI-annotation)
111+
112+
## Labeling config
113+
114+
Each video is wrapped in a `<View>` tag with a width of 100% to ensure they stack on top of each other. The `Header` tag provides a title for
115+
each video, indicating which frame is being displayed.
116+
117+
The `Video` tags are used to load the video content, with the `name` attribute uniquely identifying each video player.
118+
119+
The `TimelineLabels` tag is connected to the second video (`video0`), allowing annotators to label specific segments of that video. The labels `class1` and `class2` can be used to categorize the content of the video, enhancing the annotation process.
120+
121+
```xml
122+
<View>
123+
<View style="display: flex">
124+
<View style="width: 100%">
125+
<Header value="Video -1 Frame"/>
126+
<Video name="videoMinus1" value="$video_url"
127+
height="200" sync="lag" frameRate="29.97"/>
128+
</View>
129+
<View style="width: 100%">
130+
<Header value="Video +1 Frame"/>
131+
<Video name="videoPlus1" value="$video_url"
132+
height="200" sync="lag" frameRate="29.97"/>
133+
</View>
134+
</View>
135+
<View style="width: 100%; margin-bottom: 1em;">
136+
<Header value="Video 0 Frame"/>
137+
<Video name="video0" value="$video_url"
138+
height="400" sync="lag" frameRate="29.97"/>
139+
</View>
140+
<TimelineLabels name="timelinelabels" toName="video0">
141+
<Label value="class1"/>
142+
<Label value="class2"/>
143+
</TimelineLabels>
144+
</View>
145+
```
146+
147+
**Related tags:**
148+
149+
* [View](/tags/view.html)
150+
* [Video](/tags/video.html)
151+
* [TimelineLabels](/tags/timelinelabels.html)
152+
* [Label](/tags/label.html)
153+
154+
## Sample data
155+
156+
```json
157+
[
158+
{
159+
"video": "/static/samples/opossum_snow.mp4"
160+
}
161+
]
162+
```

docs/source/plugins/index.ejs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ cards:
3030
image: "/images/plugins/markdown-thumb.png"
3131
url: "/plugins/markdown_to_html"
3232

33+
- title: Simple Content Moderation
34+
categories:
35+
- Validation
36+
- Transcription
37+
image: "/images/plugins/moderation-thumb.png"
38+
url: "/plugins/moderation"
39+
3340
- title: Data Visualization with Plotly
3441
categories:
3542
- Visualization
@@ -46,6 +53,13 @@ cards:
4653
image: "/images/plugins/ner-overlap-thumb.png"
4754
url: "/plugins/span_overlap"
4855

56+
- title: Multi-Frame Video View
57+
categories:
58+
- Visualization
59+
- Video
60+
image: "/images/plugins/frame-offset-thumb.png"
61+
url: "/plugins/frame_offset"
62+
4963
- title: Redact Annotator PII
5064
categories:
5165
- Visualization

0 commit comments

Comments
 (0)