-
Notifications
You must be signed in to change notification settings - Fork 3
feat: LEAP-2012: Add official docs remaining plugins to the repo #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,101 @@ | ||
| /** | ||
| * Automatically creates all the text regions containing all instances of the selected text. | ||
| * Automatically creates text regions for all instances of the selected text and deletes existing regions | ||
| * when the shift key is pressed. | ||
| */ | ||
|
|
||
| // It will be triggered when a text selection happens | ||
| LSI.on("entityCreate", (region) => { | ||
| // Track the state of the shift key | ||
| let isShiftKeyPressed = false; | ||
|
|
||
| window.addEventListener("keydown", (e) => { | ||
| if (e.key === "Shift") { | ||
| isShiftKeyPressed = true; | ||
| } | ||
| }); | ||
|
|
||
| window.addEventListener("keyup", (e) => { | ||
| if (e.key === "Shift") { | ||
| isShiftKeyPressed = false; | ||
| } | ||
| }); | ||
|
|
||
| LSI.on("entityDelete", (region) => { | ||
| if (!isShiftKeyPressed) return; // Only proceed if the shift key is pressed | ||
|
|
||
| if (window.BULK_REGIONS) return; | ||
| window.BULK_REGIONS = true; | ||
| setTimeout(() => { | ||
| window.BULK_REGIONS = false; | ||
| }, 1000); | ||
|
|
||
| const existingEntities = Htx.annotationStore.selected.regions; | ||
| const regionsToDelete = existingEntities.filter((entity) => { | ||
| const deletedText = region.text.toLowerCase().replace("\\\\n", " "); | ||
| const otherText = entity.text.toLowerCase().replace("\\\\n", " "); | ||
| return deletedText === otherText && region.labels[0] === entity.labels[0]; | ||
| }); | ||
|
|
||
| for (const region of regionsToDelete) { | ||
| Htx.annotationStore.selected.deleteRegion(region); | ||
| } | ||
|
|
||
| Htx.annotationStore.selected.updateObjects(); | ||
| }); | ||
|
|
||
| LSI.on("entityCreate", (region) => { | ||
| if (!isShiftKeyPressed) return; | ||
|
|
||
| if (window.BULK_REGIONS) return; | ||
| window.BULK_REGIONS = true; | ||
| setTimeout(() => { | ||
| window.BULK_REGIONS = false; | ||
| }, 1000); | ||
|
|
||
| const existingEntities = Htx.annotationStore.selected.regions; | ||
|
|
||
| setTimeout(() => { | ||
| // Find all the text regions matching the selection | ||
| const matches = Array.from( | ||
| region.object._value.matchAll(new RegExp(region.text, "gi")), | ||
| // Prevent tagging a single character | ||
| if (region.text.length < 2) return; | ||
| regexp = new RegExp( | ||
| region.text.replace("\\\\n", "\\\\s+").replace(" ", "\\\\s+"), | ||
| "gi", | ||
| ); | ||
| for (const m of matches) { | ||
| if (m.index === region.startOffset) continue; | ||
|
|
||
| // Include them in the results as new selections | ||
| Htx.annotationStore.selected.createResult( | ||
| { | ||
| text: region.text, | ||
| start: "/span[1]/text()[1]", | ||
| startOffset: m.index, | ||
| end: "/span[1]/text()[1]", | ||
| endOffset: m.index + region.text.length, | ||
| }, | ||
| { labels: [...region.labeling.value.labels] }, | ||
| region.labeling.from_name, | ||
| region.object, | ||
| ); | ||
| const matches = Array.from(region.object._value.matchAll(regexp)); | ||
| for (const match of matches) { | ||
| if (match.index === region.startOffset) continue; | ||
|
|
||
| const startOffset = match.index; | ||
| const endOffset = match.index + region.text.length; | ||
|
|
||
| // Check for existing entities with overlapping start and end offset | ||
| let isDuplicate = false; | ||
| for (const entity of existingEntities) { | ||
| if ( | ||
| startOffset <= entity.globalOffsets.end && | ||
| entity.globalOffsets.start <= endOffset | ||
| ) { | ||
| isDuplicate = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (!isDuplicate) { | ||
| Htx.annotationStore.selected.createResult( | ||
| { | ||
| text: region.text, | ||
| start: "/span[1]/text()[1]", | ||
| startOffset: startOffset, | ||
| end: "/span[1]/text()[1]", | ||
| endOffset: endOffset, | ||
| }, | ||
| { | ||
| labels: [...region.labeling.value.labels], | ||
| }, | ||
| region.labeling.from_name, | ||
| region.object, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Htx.annotationStore.selected.updateObjects(); | ||
| }, 100); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "data": { | ||
| "video_url": "https://example.com/path/to/video.mp4" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| /** | ||
| * Multi-frame video view plugin | ||
| * | ||
| * This plugin synchronizes three video views to display a video with three frames: | ||
| * -1 frame, 0 frame, and +1 frame. | ||
| * | ||
| * It also synchronizes the timeline labels to the 0 frame. | ||
| */ | ||
|
|
||
| async function initMultiFrameVideoView() { | ||
| // Wait for the Label Studio Interface to be ready | ||
| await LSI; | ||
|
|
||
| // Get references to the video objects by their names | ||
| const videoMinus1 = LSI.annotation.names.get("videoMinus1"); | ||
| const video0 = LSI.annotation.names.get("video0"); | ||
| const videoPlus1 = LSI.annotation.names.get("videoPlus1"); | ||
|
|
||
| if (!videoMinus1 || !video0 || !videoPlus1) return; | ||
|
|
||
| // Convert frameRate to a number and ensure it's valid | ||
| const frameRate = Number.parseFloat(video0.framerate) || 24; | ||
| const frameDuration = 1 / frameRate; | ||
|
|
||
| // Function to adjust video sync with offset and guard against endless loops | ||
| function adjustVideoSync(video, offsetFrames) { | ||
| video.isSyncing = false; | ||
|
|
||
| for (const event of ["seek", "play", "pause"]) { | ||
| video.syncHandlers.set(event, (data) => { | ||
| if (!video.isSyncing) { | ||
| video.isSyncing = true; | ||
|
|
||
| if (video.ref.current && video !== video0) { | ||
| const videoElem = video.ref.current; | ||
|
|
||
| adjustedTime = | ||
| (video0.ref.current.currentFrame + offsetFrames) * frameDuration; | ||
| adjustedTime = Math.max( | ||
| 0, | ||
| Math.min(adjustedTime, video.ref.current.duration), | ||
| ); | ||
|
|
||
| if (data.playing) { | ||
| if (!videoElem.playing) videoElem.play(); | ||
| } else { | ||
| if (videoElem.playing) videoElem.pause(); | ||
| } | ||
|
|
||
| if (data.speed) { | ||
| video.speed = data.speed; | ||
| } | ||
|
|
||
| videoElem.currentTime = adjustedTime; | ||
| if ( | ||
| Math.abs(videoElem.currentTime - adjustedTime) > | ||
| frameDuration / 2 | ||
| ) { | ||
| videoElem.currentTime = adjustedTime; | ||
| } | ||
| } | ||
|
|
||
| video.isSyncing = false; | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Adjust offsets for each video | ||
| adjustVideoSync(videoMinus1, -1); | ||
| adjustVideoSync(videoPlus1, 1); | ||
| adjustVideoSync(video0, 0); | ||
| } | ||
|
|
||
| // Initialize the plugin | ||
| initMultiFrameVideoView(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <View> | ||
| <View style="display: flex"> | ||
| <View style="width: 100%"> | ||
| <Header value="Video -1 Frame"/> | ||
| <Video name="videoMinus1" value="$video_url" | ||
| height="200" sync="lag" frameRate="29.97"/> | ||
| </View> | ||
| <View style="width: 100%"> | ||
| <Header value="Video +1 Frame"/> | ||
| <Video name="videoPlus1" value="$video_url" | ||
| height="200" sync="lag" frameRate="29.97"/> | ||
| </View> | ||
| </View> | ||
| <View style="width: 100%; margin-bottom: 1em;"> | ||
| <Header value="Video 0 Frame"/> | ||
| <Video name="video0" value="$video_url" | ||
| height="400" sync="lag" frameRate="29.97"/> | ||
| </View> | ||
| <TimelineLabels name="timelinelabels" toName="video0"> | ||
| <Label value="class1"/> | ||
| <Label value="class2"/> | ||
| </TimelineLabels> | ||
| </View> | ||
|
|
||
| <!-- | ||
| { | ||
| "data": { | ||
| "video_url": "https://example.com/path/to/video.mp4" | ||
| } | ||
| } | ||
| --> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| [ | ||
| { | ||
| "audio": "https://data.heartex.net/librispeech/dev-clean/3536/8226/3536-8226-0024.flac.wav" | ||
| } | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /** | ||
| * Simple content moderation plugin that prevents saving annotations containing hate speech | ||
| * | ||
| * This plugin monitors text entered into TextArea regions and checks for the word "hate" | ||
| * before allowing the annotation to be saved. If found, it shows an error message and | ||
| * prevents submission. This would happen only once, if user clicks Submit again it would | ||
| * work with no errors. | ||
| * | ||
| * The plugin uses Label Studio's beforeSaveAnnotation event which is triggered before | ||
| * an annotation is saved. Returning false from this event handler prevents the save | ||
| * operation from completing. | ||
| */ | ||
|
|
||
| let dismissed = false; | ||
|
|
||
| LSI.on("beforeSaveAnnotation", (store, ann) => { | ||
| // text in TextArea is always an array | ||
| const obscene = ann.results.find( | ||
| (r) => | ||
| r.type === "textarea" && r.value.text.some((t) => t.includes("hate")), | ||
| ); | ||
| if (!obscene || dismissed) return true; | ||
|
|
||
| // select region to see textarea | ||
| if (!obscene.area.classification) ann.selectArea(obscene.area); | ||
|
|
||
| Htx.showModal("The word 'hate' is disallowed", "error"); | ||
| dismissed = true; | ||
|
|
||
| return false; | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <View> | ||
| <Labels name="labels" toName="audio"> | ||
| <Label value="Speech" /> | ||
| <Label value="Noise" /> | ||
| </Labels> | ||
|
|
||
| <Audio name="audio" value="$audio"/> | ||
|
|
||
| <TextArea name="transcription" toName="audio" | ||
| editable="true" | ||
| perRegion="true" | ||
| required="true" | ||
| /> | ||
| </View> | ||
|
|
||
| <!-- | ||
| [ | ||
| { | ||
| "audio": "https://data.heartex.net/librispeech/dev-clean/3536/8226/3536-8226-0024.flac.wav" | ||
| } | ||
| ] | ||
| --> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.