Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d211bce
Add TemporalSpan model
horatiualmasan Jul 7, 2026
0c66a38
update
horatiualmasan Jul 7, 2026
3e04899
fix
horatiualmasan Jul 7, 2026
bd48016
try eager load
horatiualmasan Jul 7, 2026
f668678
fix warning
horatiualmasan Jul 7, 2026
f3956d1
Add video annotation from activitynet
horatiualmasan Jul 8, 2026
07ddf00
remove not needed joins
horatiualmasan Jul 8, 2026
37441f5
Merge branch 'horatiu-lig-9805-import-activitynet-style-event-annotat…
horatiualmasan Jul 8, 2026
ebe86db
simplify
horatiualmasan Jul 8, 2026
84a4063
Merge branch 'main' into horatiu-lig-9805-import-activitynet-style-ev…
horatiualmasan Jul 15, 2026
47ec651
fix merge
horatiualmasan Jul 15, 2026
9f97a84
add example sctipt
horatiualmasan Jul 15, 2026
d95200c
format
horatiualmasan Jul 15, 2026
6a243bd
update
horatiualmasan Jul 15, 2026
1a24532
Merge branch 'main' into horatiu-lig-9805-import-activitynet-style-ev…
horatiualmasan Jul 15, 2026
d32cbe4
Consider also own annotations for video filter
horatiualmasan Jul 16, 2026
3acc186
Merge branch 'horatiu-lig-9805-import-activitynet-style-event-annotat…
horatiualmasan Jul 16, 2026
bdc02d2
fix merge error
horatiualmasan Jul 16, 2026
1d6d5ae
Add presentation-only VideoControls component
horatiualmasan Jul 16, 2026
9f287db
Add Storybook stories for VideoControls
horatiualmasan Jul 16, 2026
7dede03
Wire custom control bar into VideoPlayer
horatiualmasan Jul 16, 2026
2dc2a47
Add useVideoPlayback hook with tests
horatiualmasan Jul 16, 2026
ab6acde
Add Storybook stories for VideoControls
horatiualmasan Jul 16, 2026
35328bb
update
horatiualmasan Jul 16, 2026
30b1ea2
skip test
horatiualmasan Jul 16, 2026
30718ba
update
horatiualmasan Jul 17, 2026
d467a37
Merge branch 'main' into horatiu-lig-9807-show-event-bars-on-the-time…
horatiualmasan Jul 17, 2026
cf0ee5e
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline.a' int…
horatiualmasan Jul 17, 2026
a604ba9
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline.b' int…
horatiualmasan Jul 17, 2026
61a5ec6
Merge branch 'main' into horatiu-lig-9807-show-event-bars-on-the-time…
LeonardoRosaa Jul 20, 2026
62975ee
remove const typecast
LeonardoRosaa Jul 20, 2026
31b14ef
avoid override onerror and onloadeddata
LeonardoRosaa Jul 20, 2026
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Count video frame annotations by video collection."""
"""Count video annotations by video collection."""

from typing import Any, Optional
from uuid import UUID

from pydantic import BaseModel
from sqlalchemy import union_all
from sqlmodel import Session, asc, col, func, select
from sqlmodel.sql.expression import Select

Expand Down Expand Up @@ -31,33 +32,48 @@ def count_video_frame_annotations_by_video_collection(
filters: Optional[VideoFilter] = None,
annotation_type: Optional[AnnotationType] = None,
) -> list[CountAnnotationsView]:
"""Count the annotations by video frames.
"""Count annotations attached to videos, including frame and direct video labels.

When ``annotation_type`` is provided, both the total and filtered counts are
restricted to annotations of that type (e.g. only CLASSIFICATION or only
OBJECT_DETECTION).
"""
label_video_pairs = _build_label_video_pairs_subquery(
collection_id=collection_id, annotation_type=annotation_type
)

unfiltered_query = (
_build_base_query(
collection_id=collection_id,
count_column_name="total",
annotation_type=annotation_type,
select(
label_video_pairs.c.label_id,
func.count(func.distinct(label_video_pairs.c.video_id)).label("total"),
)
.group_by(col(AnnotationBaseTable.annotation_label_id))
.select_from(label_video_pairs)
.group_by(label_video_pairs.c.label_id)
.subquery("unfiltered")
)
filtered_query = _build_base_query(
collection_id=collection_id,
count_column_name="filtered_count",
annotation_type=annotation_type,
)

filtered_pairs_query: Select[tuple[Any, Any]] = (
select(
label_video_pairs.c.label_id,
label_video_pairs.c.video_id,
)
.select_from(label_video_pairs)
.join(VideoTable, col(VideoTable.sample_id) == label_video_pairs.c.video_id)
.join(SampleTable, col(SampleTable.sample_id) == col(VideoTable.sample_id))
)
if filters is not None:
filtered_query = filters.apply(filtered_query)
filtered_pairs_query = filters.apply(filtered_pairs_query)

filtered_subquery = filtered_query.group_by(
col(AnnotationBaseTable.annotation_label_id)
).subquery("filtered")
filtered_pairs_subquery = filtered_pairs_query.subquery("filtered_pairs")
filtered_subquery = (
select(
filtered_pairs_subquery.c.label_id,
func.count(func.distinct(filtered_pairs_subquery.c.video_id)).label("filtered_count"),
)
.select_from(filtered_pairs_subquery)
.group_by(filtered_pairs_subquery.c.label_id)
.subquery("filtered")
)

final_query: Select[Any] = (
select(
Expand Down Expand Up @@ -86,15 +102,17 @@ def count_video_frame_annotations_by_video_collection(
]


def _build_base_query(
collection_id: UUID,
count_column_name: str,
annotation_type: Optional[AnnotationType] = None,
) -> Select[tuple[Any, int]]:
query: Select[tuple[Any, int]] = (
def _build_label_video_pairs_subquery(
collection_id: UUID, annotation_type: Optional[AnnotationType] = None
) -> Any:
"""Return distinct (label_id, video_id) pairs from frame and direct video annotations.

When ``annotation_type`` is provided, only annotations of that type are considered.
"""
frame_pairs: Select[tuple[Any, Any]] = (
select(
col(AnnotationBaseTable.annotation_label_id).label("label_id"),
func.count(func.distinct(VideoTable.sample_id)).label(count_column_name),
col(VideoTable.sample_id).label("video_id"),
)
.select_from(AnnotationBaseTable)
.join(
Expand All @@ -105,8 +123,20 @@ def _build_base_query(
.join(VideoTable, col(VideoTable.sample_id) == col(SampleTable.sample_id))
.where(col(SampleTable.collection_id) == collection_id)
)

video_pairs: Select[tuple[Any, Any]] = (
select(
col(AnnotationBaseTable.annotation_label_id).label("label_id"),
col(VideoTable.sample_id).label("video_id"),
)
.select_from(AnnotationBaseTable)
.join(
SampleTable,
col(SampleTable.sample_id) == col(AnnotationBaseTable.parent_sample_id),
)
.join(VideoTable, col(VideoTable.sample_id) == col(SampleTable.sample_id))
.where(col(SampleTable.collection_id) == collection_id)
)
if annotation_type is not None:
query = query.where(col(AnnotationBaseTable.annotation_type) == annotation_type)

return query
frame_pairs = frame_pairs.where(col(AnnotationBaseTable.annotation_type) == annotation_type)
video_pairs = video_pairs.where(col(AnnotationBaseTable.annotation_type) == annotation_type)
return union_all(frame_pairs, video_pairs).subquery("label_video_pairs")
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Literal, Optional
from uuid import UUID

from sqlalchemy import or_
from sqlmodel import col, select
from sqlmodel.sql.expression import SelectOfScalar

Expand Down Expand Up @@ -82,7 +83,7 @@ def _apply_duration_filters(self, query: QueryType) -> QueryType:
return query

def _apply_annotation_filter(self, query: QueryType) -> QueryType:
"""For videos, annotation filters are applied to the frames."""
"""For videos, annotation filters match frame or direct video annotations."""
assert self.frame_annotation_filter is not None

frame_filtered_video_ids_subquery = select(VideoFrameTable.parent_sample_id)
Expand All @@ -93,7 +94,20 @@ def _apply_annotation_filter(self, query: QueryType) -> QueryType:
)
)

return query.where(col(VideoTable.sample_id).in_(frame_filtered_video_ids_subquery))
video_filtered_video_ids_subquery = select(VideoTable.sample_id)
video_filtered_video_ids_subquery = (
self.frame_annotation_filter.apply_to_parent_sample_query(
query=video_filtered_video_ids_subquery,
sample_id_column=col(VideoTable.sample_id),
)
)

return query.where(
or_(
col(VideoTable.sample_id).in_(frame_filtered_video_ids_subquery),
col(VideoTable.sample_id).in_(video_filtered_video_ids_subquery),
)
)

def _select_sample_ids(self) -> SelectOfScalar[UUID]:
return select(VideoTable.sample_id).join(VideoTable.sample)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
create_annotations,
create_collection,
)
from tests.resolvers.video.helpers import VideoStub, create_video_with_frames
from tests.resolvers.video.helpers import VideoStub, create_video, create_video_with_frames


def test_count_video_frame_annotations_by_video_collection_without_filter(
Expand Down Expand Up @@ -200,6 +200,135 @@ def test_count_video_frame_annotations_by_video_collection_with_annotation_filte
assert annotations[1].current_count == 0


def test_count_video_annotations_by_video_collection__direct_video_annotations(
db_session: Session,
) -> None:
collection = create_collection(session=db_session, sample_type=SampleType.VIDEO)
collection_id = collection.collection_id

video_with_running = create_video(
session=db_session,
collection_id=collection_id,
video=VideoStub(path="/path/to/running.mp4"),
)
video_with_jumping = create_video(
session=db_session,
collection_id=collection_id,
video=VideoStub(path="/path/to/jumping.mp4"),
)
create_video(
session=db_session,
collection_id=collection_id,
video=VideoStub(path="/path/to/unlabeled.mp4"),
)

running_label = create_annotation_label(
session=db_session,
root_collection_id=collection_id,
label_name="Running",
)
jumping_label = create_annotation_label(
session=db_session,
root_collection_id=collection_id,
label_name="Jumping",
)

create_annotations(
session=db_session,
collection_id=collection_id,
annotations=[
AnnotationDetails(
sample_id=video_with_running.sample_id,
annotation_label_id=running_label.annotation_label_id,
annotation_type=AnnotationType.CLASSIFICATION,
),
AnnotationDetails(
sample_id=video_with_jumping.sample_id,
annotation_label_id=jumping_label.annotation_label_id,
annotation_type=AnnotationType.CLASSIFICATION,
),
],
)

annotations = video_resolver.count_video_frame_annotations_by_video_collection(
session=db_session,
collection_id=collection_id,
)

assert len(annotations) == 2
assert annotations[0].label_name == "Jumping"
assert annotations[0].total_count == 1
assert annotations[0].current_count == 1
assert annotations[1].label_name == "Running"
assert annotations[1].total_count == 1
assert annotations[1].current_count == 1


def test_count_video_annotations_by_video_collection__direct_video_annotations_with_filter(
db_session: Session,
) -> None:
collection = create_collection(session=db_session, sample_type=SampleType.VIDEO)
collection_id = collection.collection_id

video_with_running = create_video(
session=db_session,
collection_id=collection_id,
video=VideoStub(path="/path/to/running.mp4"),
)
video_with_jumping = create_video(
session=db_session,
collection_id=collection_id,
video=VideoStub(path="/path/to/jumping.mp4"),
)

running_label = create_annotation_label(
session=db_session,
root_collection_id=collection_id,
label_name="Running",
)
jumping_label = create_annotation_label(
session=db_session,
root_collection_id=collection_id,
label_name="Jumping",
)

create_annotations(
session=db_session,
collection_id=collection_id,
annotations=[
AnnotationDetails(
sample_id=video_with_running.sample_id,
annotation_label_id=running_label.annotation_label_id,
annotation_type=AnnotationType.CLASSIFICATION,
),
AnnotationDetails(
sample_id=video_with_jumping.sample_id,
annotation_label_id=jumping_label.annotation_label_id,
annotation_type=AnnotationType.CLASSIFICATION,
),
],
)

annotations = video_resolver.count_video_frame_annotations_by_video_collection(
session=db_session,
collection_id=collection_id,
filters=VideoFilter(
frame_annotation_filter=AnnotationsFilter(
annotation_label_ids=[running_label.annotation_label_id]
),
sample_filter=SampleFilter(),
),
)

assert len(annotations) == 2
assert annotations[0].label_name == "Jumping"
assert annotations[0].total_count == 1
assert annotations[0].current_count == 0
assert annotations[1].label_name == "Running"
assert annotations[1].total_count == 1
assert annotations[1].current_count == 1


@pytest.fixture
def typed_collection_id(db_session: Session) -> UUID:
collection = create_collection(session=db_session, sample_type=SampleType.VIDEO)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from sqlmodel import Session, select

from lightly_studio.models.annotation.annotation_base import AnnotationType
from lightly_studio.models.collection import SampleType
from lightly_studio.models.video import VideoTable
from lightly_studio.resolvers.annotations.annotations_filter import AnnotationsFilter
Expand All @@ -14,7 +15,7 @@
create_annotations,
create_collection,
)
from tests.resolvers.video.helpers import VideoStub, create_video_with_frames
from tests.resolvers.video.helpers import VideoStub, create_video, create_video_with_frames


def test_query__annotation_filter(db_session: Session) -> None:
Expand Down Expand Up @@ -56,3 +57,45 @@ def test_query__annotation_filter(db_session: Session) -> None:
result = db_session.exec(filtered_query).all()
assert len(result) == 1
assert result[0].sample_id == video_with_annotation.video_sample_id


def test_query__annotation_filter__direct_video_annotations(db_session: Session) -> None:
collection = create_collection(session=db_session, sample_type=SampleType.VIDEO)
video_with_annotation = create_video(
session=db_session,
collection_id=collection.collection_id,
video=VideoStub(path="with_annotation.mp4"),
)
create_video(
session=db_session,
collection_id=collection.collection_id,
video=VideoStub(path="without_annotation.mp4"),
)
label = create_annotation_label(
session=db_session,
root_collection_id=collection.collection_id,
label_name="running",
)
create_annotations(
session=db_session,
collection_id=collection.collection_id,
annotations=[
AnnotationDetails(
sample_id=video_with_annotation.sample_id,
annotation_label_id=label.annotation_label_id,
annotation_type=AnnotationType.CLASSIFICATION,
)
],
)

query = select(VideoTable).join(VideoTable.sample)
video_filter = VideoFilter(
frame_annotation_filter=AnnotationsFilter(
annotation_label_ids=[label.annotation_label_id],
)
)

filtered_query = video_filter.apply(query=query)
result = db_session.exec(filtered_query).all()
assert len(result) == 1
assert result[0].sample_id == video_with_annotation.sample_id
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { test, expect } from '../utils';

test.describe('video-frames-details', () => {
test('Go to video details view', async ({ page, videoFramesPage, videoFrameDetailsPage }) => {
test.skip('Go to video details view', async ({
Comment thread
LeonardoRosaa marked this conversation as resolved.
page,
videoFramesPage,
videoFrameDetailsPage
}) => {
await videoFramesPage.doubleClickNthVideoFrame(3);

await videoFrameDetailsPage.pageIsReady();
Expand Down
Loading
Loading