Skip to content

Commit 2e817b3

Browse files
committed
Merge branch 'develop' into 'fb-leap-2036/summary'
Workflow run: https://github.com/HumanSignal/label-studio/actions/runs/14956470496
2 parents 195fed0 + 76fd73b commit 2e817b3

File tree

29 files changed

+294
-127
lines changed

29 files changed

+294
-127
lines changed

.github/CODEOWNERS

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
deploy/ @HumanSignal/devops
33
Dockerfile* @HumanSignal/devops
44
docker-compose* @HumanSignal/devops
5-
docs/.npmignore @hlomzik @Gondragos @nicholasrq
6-
docs/package.json @hlomzik @Gondragos @nicholasrq
7-
docs/yarn.lock @hlomzik @Gondragos @nicholasrq
5+
docs/.npmignore @hlomzik @Gondragos @nick-skriabin
6+
docs/package.json @hlomzik @Gondragos @nick-skriabin
7+
docs/yarn.lock @hlomzik @Gondragos @nick-skriabin
8+
web/libs/editor @hlomzik @Gondragos @nick-skriabin

label_studio/io_storages/README.md

+26-1
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,29 @@ The Storage Proxy API behavior can be configured using the following environment
184184
| `RESOLVER_PROXY_MAX_RANGE_SIZE` | Maximum size in bytes for a single range request | 7*1024*1024 |
185185
| `RESOLVER_PROXY_CACHE_TIMEOUT` | Cache TTL in seconds for proxy responses | 3600 |
186186

187-
These optimizations ensure that the Proxy API remains responsive and resource-efficient, even when handling large files or many concurrent requests.
187+
These optimizations ensure that the Proxy API remains responsive and resource-efficient, even when handling large files or many concurrent requests.
188+
189+
## Multiple Storages and URL Resolving
190+
191+
There are use cases where multiple storages can/must be used in a single project. This can cause some confusion as to which storage gets used when. Here are some common cases and how to set up mutliple storages properly.
192+
193+
### Case 1 - Tasks Referencing Other Buckets
194+
* bucket-A containing JSON tasks
195+
* bucket-B containing images/text/other data
196+
* Tasks synced from bucket-A have references to data in bucket-B
197+
198+
##### How To Setup
199+
* Add storage 1 for bucket-A
200+
* Add storage 2 for bucket-B (might be same or different credentials than bucket-A)
201+
* Sync storage 1
202+
* All references to data in bucket-B will be resolved using storage 2 automatically
203+
204+
### Case 2 - Buckets with Different Credentials
205+
* bucket-A accessible by credentials 1
206+
* bucket-B accessible by credentials 2
207+
208+
##### How To Setup
209+
* Add storage 1 for bucket-A with credentials 1
210+
* Add storage 2 for bucket-B with credentials 2
211+
* Sync both storages
212+
* The appropriate storage will be used to resolve urls/generate presigned URLs

label_studio/io_storages/base_models.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from django.utils import timezone
2828
from django.utils.translation import gettext_lazy as _
2929
from django_rq import job
30-
from io_storages.utils import get_uri_via_regex
30+
from io_storages.utils import get_uri_via_regex, parse_bucket_uri
3131
from rq.job import Job
3232
from tasks.models import Annotation, Task
3333
from tasks.serializers import AnnotationSerializer, PredictionSerializer
@@ -255,8 +255,19 @@ def can_resolve_scheme(self, url: Union[str, None]) -> bool:
255255
return False
256256
# TODO: Search for occurrences inside string, e.g. for cases like "gs://bucket/file.pdf" or "<embed src='gs://bucket/file.pdf'/>"
257257
_, prefix = get_uri_via_regex(url, prefixes=(self.url_scheme,))
258-
if prefix == self.url_scheme:
259-
return True
258+
bucket_uri = parse_bucket_uri(url, self)
259+
260+
# If there is a prefix and the bucket matches the storage's bucket/container/path
261+
if prefix == self.url_scheme and bucket_uri:
262+
# bucket is used for s3 and gcs
263+
if hasattr(self, 'bucket') and bucket_uri.bucket == self.bucket:
264+
return True
265+
# container is used for azure blob
266+
if hasattr(self, 'container') and bucket_uri.bucket == self.container:
267+
return True
268+
# path is used for redis
269+
if hasattr(self, 'path') and bucket_uri.bucket == self.path:
270+
return True
260271
# if not found any occurrences - this Storage can't resolve url
261272
return False
262273

label_studio/io_storages/functions.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,5 @@ def get_storage_by_url(url: Union[str, List, Dict], storage_objects: Iterable[Im
5454
for storage_object in storage_objects:
5555
if storage_object.can_resolve_url(url):
5656
# note: only first found storage_object will be used for link resolving
57-
# probably we need to use more advanced can_resolve_url mechanics
58-
# that takes into account not only prefixes, but bucket path too
57+
# can_resolve_url now checks both the scheme and the bucket to ensure the correct storage is used
5958
return storage_object

label_studio/io_storages/s3/serializers.py

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def validate(self, data):
6868
except TypeError as e:
6969
logger.info(f'It seems access keys are incorrect: {e}', exc_info=True)
7070
raise ValidationError('It seems access keys are incorrect')
71+
except KeyError:
72+
raise ValidationError(f'{storage.url_scheme}://{storage.bucket}/{storage.prefix} not found.')
7173
return data
7274

7375

label_studio/tasks/models.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,10 @@ def prepare_filename(filename):
420420
def resolve_storage_uri(self, url) -> Optional[Mapping[str, Any]]:
421421
from io_storages.functions import get_storage_by_url
422422

423-
storage = self.storage
424-
project = self.project
425-
426-
if not storage:
427-
storage_objects = project.get_all_import_storage_objects
428-
storage = get_storage_by_url(url, storage_objects)
423+
# Instead of using self.storage, we check all storage objects for the project to
424+
# support imported tasks that point to another bucket
425+
storage_objects = self.project.get_all_import_storage_objects
426+
storage = get_storage_by_url(url, storage_objects)
429427

430428
if storage:
431429
return {
@@ -468,10 +466,9 @@ def resolve_uri(self, task_data, project):
468466

469467
# project storage
470468
# TODO: to resolve nested lists and dicts we should improve get_storage_by_url(),
471-
# TODO: problem with current approach: it can be used only the first storage that get_storage_by_url
472-
# TODO: returns. However, maybe the second storage will resolve uris properly.
473-
# TODO: resolve_uri() already supports them
474-
storage = self.storage or get_storage_by_url(task_data[field], storage_objects)
469+
# Now always using get_storage_by_url to ensure the storage with the correct bucket is used
470+
# As a last fallback we can use self.storage which is the storage the Task was imported from
471+
storage = get_storage_by_url(task_data[field], storage_objects) or self.storage
475472
if storage:
476473
try:
477474
resolved_uri = storage.resolve_uri(task_data[field], self)

label_studio/tests/export.tavern.yml

+4
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,15 @@ stages:
139139
tags:
140140
- image segmentation
141141
- object detection
142+
- keypoints
142143
name: COCO
143144
- title: COCO with Images
144145
description: COCO format with images downloaded.
145146
link: https://labelstud.io/guide/export.html#COCO
146147
tags:
147148
- image segmentation
148149
- object detection
150+
- keypoints
149151
name: COCO_WITH_IMAGES
150152
- title: YOLO
151153
description: Popular TXT format is created for each image file. Each txt file contains
@@ -155,13 +157,15 @@ stages:
155157
tags:
156158
- image segmentation
157159
- object detection
160+
- keypoints
158161
name: YOLO
159162
- title: YOLO with Images
160163
description: YOLO format with images downloaded.
161164
link: https://labelstud.io/guide/export.html#YOLO
162165
tags:
163166
- image segmentation
164167
- object detection
168+
- keypoints
165169
name: YOLO_WITH_IMAGES
166170
- title: YOLOv8 OBB
167171
description: Popular TXT format is created for each image file. Each txt file contains

poetry.lock

+33-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ dependencies = [
7373
"djangorestframework-simplejwt[crypto] (>=5.4.0,<6.0.0)",
7474
"tldextract (>=5.1.3)",
7575
## HumanSignal repo dependencies :start
76-
"label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/1ef88e2f8afe50a738fc1b03385cb1f3d6b2dd9f.zip",
76+
"label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/c4ed0b0240c4a5e9f372393f84fdea01abe33141.zip",
7777
## HumanSignal repo dependencies :end
7878
]
7979

web/libs/datamanager/src/components/App/App.jsx renamed to web/libs/datamanager/src/components/App/App.tsx

+26-7
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,47 @@ import { DataManager } from "../DataManager/DataManager";
88
import { Labeling } from "../Label/Label";
99
import "./App.scss";
1010

11-
class ErrorBoundary extends React.Component {
12-
state = {
11+
interface ErrorBoundaryProps {
12+
children: React.ReactNode;
13+
}
14+
15+
interface ErrorBoundaryState {
16+
error: Error | null;
17+
}
18+
19+
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
20+
state: ErrorBoundaryState = {
1321
error: null,
1422
};
1523

16-
componentDidCatch(error) {
24+
componentDidCatch(error: Error): void {
1725
this.setState({ error });
1826
}
1927

20-
render() {
21-
return this.state.error ? <div className="error">{this.state.error}</div> : this.props.children;
28+
render(): React.ReactNode {
29+
return this.state.error ? <div className="error">{this.state.error.toString()}</div> : this.props.children;
2230
}
2331
}
2432

33+
interface AppComponentProps {
34+
app: {
35+
SDK: {
36+
mode: string;
37+
};
38+
crashed: boolean;
39+
loading: boolean;
40+
isLabeling: boolean;
41+
};
42+
}
43+
2544
/**
2645
* Main Component
27-
* @param {{app: import("../../stores/AppStore").AppStore} param0
2846
*/
29-
const AppComponent = ({ app }) => {
47+
const AppComponent: React.FC<AppComponentProps> = ({ app }) => {
3048
const rootCN = cn("root");
3149
const rootClassName = rootCN.mod({ mode: app.SDK.mode }).toString();
3250
const crashCN = cn("crash");
51+
3352
return (
3453
<ErrorBoundary>
3554
<Provider store={app}>

web/libs/editor/src/components/AnnotationTab/AutoAcceptToggle.scss

+28-9
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,46 @@
1919
}
2020

2121
&__action {
22-
padding: 0;
23-
margin: 0;
2422
margin: 2px;
2523
width: 28px;
2624
height: 28px;
2725
display: flex;
2826
align-items: center;
2927
justify-content: center;
3028
border-radius: 100%;
31-
color: var(--sand_700);
3229
background-color: var(--color-neutral-surface);
30+
padding: var(--spacing-tightest);
31+
transition: all 150ms ease-out;
3332

3433
&_type {
35-
&_accept svg {
36-
width: 15px;
37-
height: 10px;
34+
&_accept {
35+
border: 1px solid var(--color-positive-border);
36+
background-color: var(--color-positive-surface);
37+
color: var(--color-positive-surface-content);
38+
39+
&:hover {
40+
background-color: var(--color-positive-surface-hover);
41+
}
42+
43+
& svg {
44+
width: 20px;
45+
height: 20px;
46+
}
3847
}
3948

40-
&_reject svg {
41-
width: 12.5px;
42-
height: 12.5px;
49+
&_reject {
50+
border: 1px solid var(--color-negative-border);
51+
background-color: var(--color-negative-surface);
52+
color: var(--color-negative-surface-content);
53+
54+
&:hover {
55+
background-color: var(--color-negative-surface-hover);
56+
}
57+
58+
& svg {
59+
width: 20px;
60+
height: 20px;
61+
}
4362
}
4463
}
4564
}

web/libs/editor/src/components/SidePanels/DetailsPanel/RegionDetails.scss

+12
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@
132132
display: grid;
133133
grid-template-columns: auto 1fr;
134134
grid-column-gap: 10px;
135+
color: var(--color-neutral-content);
136+
137+
& .ant-typography {
138+
color: var(--color-neutral-content);
139+
140+
& mark {
141+
color: var(--color-neutral-content);
142+
background-color: var(--color-primary-background);
143+
padding: var(--spacing-tightest) var(--spacing-tighter);
144+
border-radius: var(--corner-radius-smaller);
145+
}
146+
}
135147
}
136148

137149
&__value {

0 commit comments

Comments
 (0)