Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ services:
image: heartexlabs/label-studio:latest
restart: unless-stopped
ports:
- "8080:8085"
- "8081:8086"
- "8085:8085"
depends_on:
- app
environment:
Expand All @@ -19,6 +18,7 @@ services:
# - NGINX_SSL_CERT_KEY=/certs/cert.key
volumes:
- ./mydata:/label-studio/data:rw
- E:/data:/label-studio/data/external:ro
- ./deploy/nginx/certs:/certs:ro
# Optional: Override nginx default conf
# - ./deploy/my.conf:/etc/nginx/nginx.conf
Expand All @@ -43,9 +43,12 @@ services:
- POSTGRE_HOST=db
- LABEL_STUDIO_HOST=${LABEL_STUDIO_HOST:-}
- JSON_LOG=1
- LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=/label-studio/data
- LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED=true
# - LOG_LEVEL=DEBUG
volumes:
- ./mydata:/label-studio/data:rw
- E:/data:/label-studio/data/external:ro
command: label-studio-uwsgi

db:
Expand Down
5 changes: 3 additions & 2 deletions web/apps/labelstudio/src/pages/DataManager/DataManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { isDefined } from "../../utils/helpers";
import { ImportModal } from "../CreateProject/Import/ImportModal";
import { ExportPage } from "../ExportPage/ExportPage";
import { APIConfig } from "./api-config";
import { getInteractiveContextResult } from "./interactive-context";

import "./DataManager.prefix.css";

Expand Down Expand Up @@ -157,8 +158,8 @@ export const DataManagerPage = ({ ...props }) => {
if (interactiveBacked) {
dataManager.on("lsf:regionFinishedDrawing", (reg, group) => {
const { lsf, task, currentAnnotation: annotation } = dataManager.lsf;
const ids = group.map((r) => r.cleanId);
const result = annotation.serializeAnnotation().filter((res) => ids.includes(res.id));
const serializedAnnotation = annotation.serializeAnnotation();
const result = getInteractiveContextResult(serializedAnnotation, reg, group);

const suggestionsRequest = api.callApi("mlInteractive", {
params: { pk: interactiveBacked.id },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const getInteractiveContextResult = (serializedAnnotation, region, group) => {
const control = region.results?.[0];
const fromName = control?.from_name?.name ?? control?.from_name;
const toName = control?.to_name?.name ?? control?.to_name;

if (region.type === "rectangleregion" && fromName && toName) {
const currentRegion = region.serialize?.();
const annotationResults = currentRegion
? serializedAnnotation.some((result) => result.id === currentRegion.id)
? serializedAnnotation
: [...serializedAnnotation, currentRegion]
: serializedAnnotation;

return annotationResults.filter(
(result) =>
result.type === "rectanglelabels" &&
result.from_name === fromName &&
result.to_name === toName,
);
}

const ids = group.map((item) => item.cleanId);

return serializedAnnotation.filter((result) => ids.includes(result.id));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { getInteractiveContextResult } from "./interactive-context";

const smartRectangle = {
type: "rectangleregion",
results: [{ from_name: { name: "box_target" }, to_name: { name: "image" } }],
};

describe("getInteractiveContextResult", () => {
it("includes every current rectangle for the smart rectangle control", () => {
const firstBox = {
id: "first",
type: "rectanglelabels",
from_name: "box_target",
to_name: "image",
};
const secondBox = {
id: "second",
type: "rectanglelabels",
from_name: "box_target",
to_name: "image",
};

const result = getInteractiveContextResult(
[firstBox, secondBox],
smartRectangle,
[{ cleanId: secondBox.id }],
);

expect(result).toEqual([firstBox, secondBox]);
});

it("excludes deleted and unrelated results from the smart rectangle context", () => {
const currentBox = {
id: "current",
type: "rectanglelabels",
from_name: "box_target",
to_name: "image",
};
const otherControlBox = {
id: "other-control",
type: "rectanglelabels",
from_name: "other_box_target",
to_name: "image",
};
const brushResult = {
id: "brush",
type: "brushlabels",
from_name: "brush_target",
to_name: "image",
};

const result = getInteractiveContextResult(
[currentBox, otherControlBox, brushResult],
smartRectangle,
[{ cleanId: currentBox.id }],
);

expect(result).toEqual([currentBox]);
});

it("includes the event region before annotation serialization catches up", () => {
const currentBox = {
id: "current",
type: "rectanglelabels",
from_name: "box_target",
to_name: "image",
};
const region = {
...smartRectangle,
serialize: () => currentBox,
};

const result = getInteractiveContextResult([], region, [{ cleanId: "current" }]);

expect(result).toEqual([currentBox]);
});

it("preserves group-based behavior for non-rectangle smart regions", () => {
const point = { id: "point", type: "keypointlabels" };
const unrelatedPoint = { id: "other-point", type: "keypointlabels" };

const result = getInteractiveContextResult(
[point, unrelatedPoint],
{ type: "keypointregion", results: [{}] },
[{ cleanId: point.id }],
);

expect(result).toEqual([point]);
});
});
33 changes: 12 additions & 21 deletions web/libs/datamanager/src/stores/Assignee.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
import { types } from "mobx-state-tree";
import { User } from "./Users";
import { StringOrNumberID } from "./types";
import { FF_DISABLE_GLOBAL_USER_FETCHING, isFF } from "../utils/feature-flags";

// Create a union type that can handle both user references and direct user objects
const UserOrReference = types.union({
dispatcher: (snapshot) => {
// If it's a full user object (has firstName, email, etc.), use User model
if (snapshot && typeof snapshot === "object" && (snapshot.firstName || snapshot.email || snapshot.username)) {
return User;
}
// Otherwise, it's a reference to a user ID
return types.reference(User);
},
cases: {
[User.name]: User,
reference: types.reference(User),
},
const userSnapshot = (id, user = {}) => ({
id,
firstName: "",
lastName: "",
username: "",
email: "",
lastActivity: "",
initials: "",
...user,
});

export const Assignee = types
.model("Assignee", {
id: StringOrNumberID,
user: types.late(() => UserOrReference),
user: User,
review: types.maybeNull(types.enumeration(["accepted", "rejected", "fixed"])),
reviewed: types.maybeNull(types.boolean),
annotated: types.maybeNull(types.boolean),
Expand Down Expand Up @@ -59,7 +53,7 @@ export const Assignee = types
if (typeof sn === "number") {
result = {
id: sn,
user: sn,
user: userSnapshot(sn),
annotated: true,
review: null,
reviewed: false,
Expand All @@ -68,12 +62,9 @@ export const Assignee = types
const { user_id, annotated, review, reviewed, ...user } = sn;
const id = user_id ?? sn.id;

// When global user fetching is disabled, always create user objects, otherwise use references via user id
// If we only have user_id and no other user properties, just use the user_id as reference
const hasUserProperties = Object.keys(user).length > 0;
result = {
id,
user: isFF(FF_DISABLE_GLOBAL_USER_FETCHING) && hasUserProperties ? { id, ...user } : id, // Use user_id as reference
user: userSnapshot(id, user),
annotated,
review,
reviewed,
Expand Down
40 changes: 7 additions & 33 deletions web/libs/editor/src/components/ImageView/SuggestionControls.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { Circle, Group, Image, Layer, Rect } from "react-konva";
import { Circle, Group, Image, Rect } from "react-konva";
import { IconCheck, IconCross } from "@humansignal/icons";
import Konva from "konva";
import chroma from "chroma-js";
Expand Down Expand Up @@ -28,7 +28,7 @@ const getItemPosition = (item) => {
};
};

export const SuggestionControls = observer(({ item, useLayer }) => {
export const SuggestionControls = observer(({ item }) => {
const position = getItemPosition(item);
const [hovered, setHovered] = useState(false);
const scale = 1 / item.parent.zoomScale;
Expand All @@ -39,33 +39,13 @@ export const SuggestionControls = observer(({ item, useLayer }) => {
height: 32,
};

const groupPosition = useLayer
? {
x: 0,
y: 0,
scaleX: 1,
scaleY: 1,
}
: {
x: position.x,
y: position.y,
scaleX: scale,
scaleY: scale,
};

const layerPosition = useLayer
? {
x: position.x,
y: position.y,
scaleX: scale,
scaleY: scale,
}
: {};

const content = (
<Group
{...size}
{...groupPosition}
x={position.x}
y={position.y}
scaleX={scale}
scaleY={scale}
opacity={item.highlighted || hovered ? 1 : 0.5}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
Expand All @@ -87,13 +67,7 @@ export const SuggestionControls = observer(({ item, useLayer }) => {
</Group>
);

return useLayer ? (
<Layer {...size} {...layerPosition}>
{content}
</Layer>
) : (
content
);
return content;
}
return null;
});
Expand Down
9 changes: 2 additions & 7 deletions web/libs/editor/src/regions/RegionWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { observer } from "mobx-react";
import { Fragment, useContext } from "react";
import { ImageViewContext } from "../components/ImageView/ImageViewContext";
import { SuggestionControls } from "../components/ImageView/SuggestionControls";

export const RegionWrapper = observer(({ item, children }) => {
const { suggestion } = useContext(ImageViewContext) ?? {};
import { Fragment } from "react";

export const RegionWrapper = observer(({ children }) => {
return (
<Fragment>
{children}
{suggestion && <SuggestionControls item={item} useLayer={item.type === "brushregion"} />}
</Fragment>
Comment on lines +2 to 8
);
});
3 changes: 2 additions & 1 deletion web/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ module.exports = composePlugins(
config.output = {
...config.output,
uniqueName: "labelstudio",
publicPath:
chunkFilename: isDevelopment ? "[name].js" : "[name].[contenthash].js",
publicPath:
isDevelopment && FRONTEND_HOSTNAME
? `${FRONTEND_HOSTNAME}/react-app/`
: process.env.MODE === "standalone-playground"
Expand Down
Loading