Skip to content

Commit fc26d71

Browse files
hlomziknick-skriabinniklub
authored
feat: BROS-194: Custom tags (#8108)
Co-authored-by: hlomzik <[email protected]> Co-authored-by: Nick Skriabin <[email protected]> Co-authored-by: niklub <[email protected]>
1 parent 603b16c commit fc26d71

File tree

15 files changed

+164
-85
lines changed

15 files changed

+164
-85
lines changed

label_studio/projects/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,9 @@ def validate_config(self, config_string, strict=False):
590590
diff_str = []
591591
for ann_tuple in different_annotations:
592592
from_name, to_name, t = ann_tuple.split('|')
593+
# TODO tags that operate as both object and control tags; should be special registry/logic for them
594+
if from_name == to_name and t.lower() == 'chatmessage':
595+
continue
593596
if t.lower() == 'textarea': # avoid textarea to_name check (see DEV-1598)
594597
continue
595598
if (

label_studio/tasks/validation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class SkipField(Exception):
2020
'HyperText': [str],
2121
'Image': [str, list],
2222
'Paragraphs': [list, str],
23+
'Chat': [list, str],
2324
'Table': [dict, list, str],
2425
'TimeSeries': [dict, list, str],
2526
'TimeSeriesChannel': [dict, list, str],

web/libs/core/src/lib/utils/feature-flags/flags.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,8 @@ export const FF_NEW_STORAGES = "fflag_feat_bros_193_new_cloud_storage_providers_
105105
* Datamanager filter members
106106
*/
107107
export const FF_DM_FILTER_MEMBERS = "fflag_feat_fit_449_datamanager_filter_members_short";
108+
109+
/**
110+
* Modify MST models to allow custom tags
111+
*/
112+
export const FF_CUSTOM_TAGS = "fflag_feat_front_bros_194_custom_tags_short";

web/libs/editor/src/components/Node/Node.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getType } from "mobx-state-tree";
33
import { observer } from "mobx-react";
44
import { ApartmentOutlined, AudioOutlined, LineChartOutlined, MessageOutlined } from "@ant-design/icons";
55

6+
import Registry from "../../core/Registry";
67
import "./Node.scss";
78
import {
89
IconBrushTool,
@@ -136,6 +137,8 @@ const NodeViews: Record<string, NodeViewProps> = {
136137
name: "Timeline Span",
137138
icon: IconTimelineRegion,
138139
},
140+
141+
...Object.fromEntries(Registry.customTags.map((tag) => [tag.region.name, tag.region.nodeView])),
139142
};
140143

141144
const NodeIcon: FC<any> = observer(({ node, ...props }) => {

web/libs/editor/src/components/SidePanels/DetailsPanel/RegionItem.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,17 @@ const RegionAction: FC<any> = observer(({ region, annotation, editMode, onEditMo
155155
aria-label="Unlock Region"
156156
tooltip="Unlock Region"
157157
/>
158-
<RegionActionButton
159-
aria-label={`${region.hidden ? "Show" : "Hide"} selected region`}
160-
variant="neutral"
161-
look="string"
162-
onClick={region.toggleHidden}
163-
tooltip={`${region.hidden ? "Show" : "Hide"} selected region`}
164-
>
165-
{region.hidden ? <IconEyeClosed /> : <IconEyeOpened />}
166-
</RegionActionButton>
158+
{region.hideable && (
159+
<RegionActionButton
160+
aria-label={`${region.hidden ? "Show" : "Hide"} selected region`}
161+
variant="neutral"
162+
look="string"
163+
onClick={region.toggleHidden}
164+
tooltip={`${region.hidden ? "Show" : "Hide"} selected region`}
165+
>
166+
{region.hidden ? <IconEyeClosed /> : <IconEyeOpened />}
167+
</RegionActionButton>
168+
)}
167169
<RegionActionButton
168170
variant="negative"
169171
look="string"

web/libs/editor/src/components/SidePanels/OutlinerPanel/OutlinerTree.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -424,16 +424,17 @@ const RootTitle: FC<any> = observer(
424424
</Elem>
425425
)}
426426
</Elem>
427-
<RegionControls
428-
hovered={hovered}
429-
item={item}
430-
entity={props.entity}
431-
regions={props.children}
432-
type={props.type}
433-
collapsed={collapsed}
434-
hasControls={hasControls && isArea}
435-
toggleCollapsed={toggleCollapsed}
436-
/>
427+
{item?.hideable !== false && (
428+
<RegionControls
429+
item={item}
430+
entity={props.entity}
431+
regions={props.children}
432+
type={props.type}
433+
collapsed={collapsed}
434+
hasControls={hasControls && isArea}
435+
toggleCollapsed={toggleCollapsed}
436+
/>
437+
)}
437438
</Elem>
438439

439440
{!collapsed && hasControls && isArea && (

web/libs/editor/src/core/Registry.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
1+
interface ObjectTag {
2+
name: string;
3+
}
4+
5+
interface CustomTag<ViewTag = unknown> {
6+
tag: string;
7+
model: ObjectTag;
8+
view: React.ComponentType<ViewTag>;
9+
detector?: (value: object) => boolean;
10+
region: {
11+
name: string;
12+
nodeView: {
13+
name: string;
14+
icon: any;
15+
getContent?: (node: any) => JSX.Element | null;
16+
fullContent?: (node: any) => JSX.Element | null;
17+
};
18+
};
19+
}
20+
121
/**
222
* Class for register View
323
*/
424
class _Registry {
525
tags: any[] = [];
26+
customTags: CustomTag[] = [];
627
models: Record<string, any> = {};
728
views: Record<string, any> = {};
829
regions: any[] = [];
@@ -16,7 +37,7 @@ class _Registry {
1637

1738
perRegionViews: Record<string, any> = {};
1839

19-
addTag(tag: string | number, model: { name: string | number }, view: any) {
40+
addTag(tag: string | number, model: { name: string | number }, view: JSX.Element) {
2041
this.tags.push(tag);
2142
this.models[tag] = model;
2243
this.views[tag] = view;
@@ -111,6 +132,13 @@ class _Registry {
111132
getPerRegionView(tag: string | number, mode: string | number) {
112133
return this.perRegionViews[tag]?.[mode];
113134
}
135+
136+
addCustomTag<ViewTag = unknown>(tag: string, definition: CustomTag<ViewTag>) {
137+
this.addTag(tag.toLowerCase(), definition.model, definition.view);
138+
this.addObjectType(definition.model);
139+
this.addRegionType(definition.region, definition.model.name, definition.detector);
140+
this.customTags.push(definition);
141+
}
114142
}
115143

116144
const Registry = new _Registry();

web/libs/editor/src/mixins/Regions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const RegionsMixin = types
3434
perRegionFocusRequest: null,
3535
shapeRef: null,
3636
drawingTimeout: null,
37+
hideable: true,
3738
}))
3839
.views((self) => ({
3940
get perRegionStates() {

web/libs/editor/src/regions/Area.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ const Area = types.union(
4848
dispatcher(sn) {
4949
// for some deserializations
5050
if (sn.$treenode) return sn.$treenode.type;
51+
52+
for (const customTag of Registry.customTags) {
53+
if (sn.value?.[customTag.resultName] || sn[customTag.resultName]) return customTag.region;
54+
}
55+
5156
if (
5257
!sn.points && // dirty hack to make it work with polygons, but may be the whole condition is not necessary at all
5358
// `sequence` and `ranges` are used for video regions
@@ -89,6 +94,7 @@ const Area = types.union(
8994
BitmaskRegionModel,
9095
VideoRectangleRegionModel,
9196
ClassificationArea,
97+
...Registry.customTags.map((t) => t.region),
9298
);
9399

94100
export default Area;

web/libs/editor/src/regions/Result.js

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,69 @@
11
import { getParent, getRoot, getSnapshot, types } from "mobx-state-tree";
2+
import { ff } from "@humansignal/core";
23
import { guidGenerator } from "../core/Helpers";
34
import Registry from "../core/Registry";
45
import Tree from "../core/Tree";
56
import { AnnotationMixin } from "../mixins/AnnotationMixin";
67
import { isDefined } from "../utils/utilities";
78
import { FF_LSDV_4583, isFF } from "../utils/feature-flags";
89

10+
const resultTypes = [
11+
"labels",
12+
"hypertextlabels",
13+
"paragraphlabels",
14+
"rectangle",
15+
"keypoint",
16+
"polygon",
17+
"brush",
18+
"bitmask",
19+
"ellipse",
20+
"magicwand",
21+
"rectanglelabels",
22+
"keypointlabels",
23+
"polygonlabels",
24+
"brushlabels",
25+
"bitmasklabels",
26+
"ellipselabels",
27+
"timeserieslabels",
28+
"timelinelabels",
29+
"choices",
30+
"datetime",
31+
"number",
32+
"taxonomy",
33+
"textarea",
34+
"rating",
35+
"pairwise",
36+
"videorectangle",
37+
"ranker",
38+
];
39+
40+
const resultValues = {
41+
ranker: types.union(types.array(types.string), types.frozen(), types.null),
42+
datetime: types.maybe(types.string),
43+
number: types.maybe(types.number),
44+
rating: types.maybe(types.number),
45+
item_index: types.maybeNull(types.number),
46+
text: types.maybe(types.union(types.string, types.array(types.string))),
47+
choices: types.maybe(types.array(types.union(types.string, types.array(types.string)))),
48+
// pairwise
49+
selected: types.maybe(types.enumeration(["left", "right"])),
50+
// @todo all other *labels
51+
labels: types.maybe(types.array(types.string)),
52+
htmllabels: types.maybe(types.array(types.string)),
53+
hypertextlabels: types.maybe(types.array(types.string)),
54+
paragraphlabels: types.maybe(types.array(types.string)),
55+
rectanglelabels: types.maybe(types.array(types.string)),
56+
keypointlabels: types.maybe(types.array(types.string)),
57+
polygonlabels: types.maybe(types.array(types.string)),
58+
ellipselabels: types.maybe(types.array(types.string)),
59+
brushlabels: types.maybe(types.array(types.string)),
60+
timeserieslabels: types.maybe(types.array(types.string)),
61+
timelinelabels: types.maybe(types.array(types.string)), // new one
62+
bitmasklabels: types.maybe(types.array(types.string)),
63+
taxonomy: types.frozen(), // array of arrays of strings
64+
sequence: types.frozen(),
65+
};
66+
967
const Result = types
1068
.model("Result", {
1169
id: types.optional(types.identifier, guidGenerator),
@@ -32,62 +90,20 @@ const Result = types
3290
// object tag
3391
to_name: types.late(() => types.reference(types.union(...Registry.objectTypes()))),
3492
// @todo some general type, maybe just a `string`
35-
type: types.enumeration([
36-
"labels",
37-
"hypertextlabels",
38-
"paragraphlabels",
39-
"rectangle",
40-
"keypoint",
41-
"polygon",
42-
"brush",
43-
"bitmask",
44-
"ellipse",
45-
"magicwand",
46-
"rectanglelabels",
47-
"keypointlabels",
48-
"polygonlabels",
49-
"brushlabels",
50-
"bitmasklabels",
51-
"ellipselabels",
52-
"timeserieslabels",
53-
"timelinelabels",
54-
"choices",
55-
"datetime",
56-
"number",
57-
"taxonomy",
58-
"textarea",
59-
"rating",
60-
"pairwise",
61-
"videorectangle",
62-
"ranker",
63-
]),
93+
type: ff.isActive(ff.FF_CUSTOM_TAGS)
94+
? types.late(() => types.enumeration([...resultTypes, ...Registry.customTags.map((t) => t.resultName)]))
95+
: types.enumeration([...resultTypes]),
6496
// @todo much better to have just a value, not a hash with empty fields
65-
value: types.model({
66-
ranker: types.union(types.array(types.string), types.frozen(), types.null),
67-
datetime: types.maybe(types.string),
68-
number: types.maybe(types.number),
69-
rating: types.maybe(types.number),
70-
item_index: types.maybeNull(types.number),
71-
text: types.maybe(types.union(types.string, types.array(types.string))),
72-
choices: types.maybe(types.array(types.union(types.string, types.array(types.string)))),
73-
// pairwise
74-
selected: types.maybe(types.enumeration(["left", "right"])),
75-
// @todo all other *labels
76-
labels: types.maybe(types.array(types.string)),
77-
htmllabels: types.maybe(types.array(types.string)),
78-
hypertextlabels: types.maybe(types.array(types.string)),
79-
paragraphlabels: types.maybe(types.array(types.string)),
80-
rectanglelabels: types.maybe(types.array(types.string)),
81-
keypointlabels: types.maybe(types.array(types.string)),
82-
polygonlabels: types.maybe(types.array(types.string)),
83-
ellipselabels: types.maybe(types.array(types.string)),
84-
brushlabels: types.maybe(types.array(types.string)),
85-
timeserieslabels: types.maybe(types.array(types.string)),
86-
timelinelabels: types.maybe(types.array(types.string)), // new one
87-
bitmasklabels: types.maybe(types.array(types.string)),
88-
taxonomy: types.frozen(), // array of arrays of strings
89-
sequence: types.frozen(),
90-
}),
97+
value: ff.isActive(ff.FF_CUSTOM_TAGS)
98+
? types.late(() =>
99+
types.model({
100+
...resultValues,
101+
...Object.fromEntries(Registry.customTags.map((t) => [t.resultName, types.maybe(t.result)])),
102+
}),
103+
)
104+
: types.model({
105+
...resultValues,
106+
}),
91107
// info about object and region
92108
meta: types.frozen(),
93109
})

0 commit comments

Comments
 (0)