Skip to content
Open
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
e1b39eb
Serializers for video chapters and integration of chapters in job and…
DE-AI Oct 15, 2025
f9def16
* Visualize chapter marks in the player slider component
DE-AI Oct 16, 2025
fe46b06
Use the start timestamp to check if the chapter is inside the segment
DE-AI Oct 20, 2025
844c18e
Update openapi schema with chapter class and field.
DE-AI Oct 20, 2025
cfc34f9
Rest API test for chapters
DE-AI Oct 21, 2025
266852b
Undo browser change for cypress
DE-AI Oct 21, 2025
fef0935
Extend manifest file with chapters
DE-AI Oct 21, 2025
96997f9
Use manifest file as source for video chapters in the rest api.
DE-AI Oct 22, 2025
beb51ce
Handled Typescript code reviews.
DE-AI Oct 23, 2025
b1fcd5e
Move chapter marks above the player slider and bring the step dots of…
MhhhxX Oct 23, 2025
661bfaf
Fix more code style issues.
MhhhxX Oct 23, 2025
4b08264
Type information for get_video_chapaters method
MhhhxX Oct 23, 2025
9400f93
Remove time_base field from the chapter api response as it is not nee…
MhhhxX Oct 23, 2025
825b909
Ensure backwards compatibility of new "chapters" field.
MhhhxX Oct 23, 2025
5b57a3b
Remove and replace deprecated imports from the typing module.
MhhhxX Oct 23, 2025
5487ce7
Remove unused import
MhhhxX Oct 23, 2025
2175684
Refactor find closest pts method with bisect module.
MhhhxX Oct 24, 2025
ae64ff7
Don't copy time base field from chapters as it isn't needed in the ap…
MhhhxX Oct 24, 2025
867ae0a
Remove Fraction from the api and frontend code.
MhhhxX Oct 24, 2025
e80a036
Add keyboard shortcut for chapter navigations.
MhhhxX Oct 24, 2025
14e184e
Add chapter menu icon.
MhhhxX Oct 28, 2025
0cda387
Copy only required fields of the chapter dictionary.
MhhhxX Oct 28, 2025
7752f87
Remove dev tag from api schema version.
MhhhxX Oct 28, 2025
4882df9
Copy metadata and id
MhhhxX Oct 28, 2025
c39d982
Squashed commit of the following:
MhhhxX Nov 2, 2025
3ec7e7f
Mark prop as readonly
MhhhxX Nov 2, 2025
64ff04f
Merge branch 'develop' into chapters
klakhov Nov 6, 2025
13b301a
Move css to class, make list scrollable
MhhhxX Nov 6, 2025
6813da5
add licence header
MhhhxX Nov 6, 2025
d4155d6
fix imports
MhhhxX Nov 6, 2025
0433cad
Replace for loop with find.
MhhhxX Nov 6, 2025
f9c2af4
Use reduce to collect SliderMarks, fix css style for antd slider marks
MhhhxX Nov 6, 2025
d8b1942
Refactored useState with redux actions.
MhhhxX Nov 6, 2025
172dbbf
Make sethoveredchapter readonly
MhhhxX Nov 6, 2025
53f7bc7
Add correct licence header.
Nov 12, 2025
ef21f69
Hide horizontal scrollbar on hover.
Nov 12, 2025
903d10d
Improve chapter menu icon
Nov 12, 2025
2a96235
Shrink chapter menu icon.
MhhhxX Nov 15, 2025
94a7bb0
Merge branch 'develop' into chapters
klakhov Nov 24, 2025
af7af4b
Format python files
MhhhxX Nov 24, 2025
d137ae0
Fix eslint and stylint errors
MhhhxX Nov 24, 2025
b169771
Fixed get chapter for task metadata.
MhhhxX Nov 25, 2025
7f03528
Make Chapter not required
MhhhxX Nov 25, 2025
eab4313
Use mkv instead avi because avi doesn't support chapters in the metadata
MhhhxX Nov 25, 2025
865d98f
Add changelog fragment
MhhhxX Nov 25, 2025
8a591fc
Fix changelog fragment
MhhhxX Nov 26, 2025
86e15d6
Update schema.xml
MhhhxX Nov 26, 2025
8a0e6f2
Chapters field returned by the serializer defaults to None in case of…
MhhhxX Nov 26, 2025
b77d5f5
Merge branch 'develop' into chapters
zhiltsov-max Nov 27, 2025
c99623f
Fix file name
MhhhxX Nov 27, 2025
2f928b4
Merge branch 'develop' into chapters
MhhhxX Nov 28, 2025
fb1172f
Merge branch 'develop' into chapters
MhhhxX Nov 28, 2025
11b4d90
Merge branch 'develop' into chapters
zhiltsov-max Nov 28, 2025
574366a
fix shortcut test
MhhhxX Dec 1, 2025
2ee857c
Add cypress tests for video chapters
MhhhxX Dec 2, 2025
d93047f
Merge branch 'develop' into chapters
MhhhxX Dec 2, 2025
f51cb21
Fix code duplication.
MhhhxX Dec 2, 2025
17fae39
Use better function to check the slider value
MhhhxX Dec 2, 2025
89da2bb
fix sonar issues
MhhhxX Dec 2, 2025
d9b6ea6
Use dom element to wait for page change.
MhhhxX Dec 2, 2025
1f279ec
Add licence header
MhhhxX Dec 2, 2025
d00d8ad
Fix more sonar issues
MhhhxX Dec 2, 2025
e6d4576
Merge branch 'develop' into chapters
MhhhxX Dec 2, 2025
94fb506
Move searchAcrossPages to outer scope
MhhhxX Dec 2, 2025
903a469
Use correct selector to click on the correct list item
MhhhxX Dec 3, 2025
ac025d2
Merge branch 'develop' into chapters
MhhhxX Dec 3, 2025
7a9e56c
Merge branch 'develop' into chapters
MhhhxX Dec 4, 2025
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
4 changes: 4 additions & 0 deletions changelog.d/20251125_154952_max.christoph_chapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Show video chapters in the player progress as marks. Seek to chapters with a menu and with player navigation buttons.
(<https://github.com/cvat-ai/cvat/pull/9924>)
37 changes: 35 additions & 2 deletions cvat-core/src/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from 'cvat-data';
import PluginRegistry from './plugins';
import serverProxy from './server-proxy';
import { SerializedFramesMetaData } from './server-response-types';
import { SerializedChapterMetaData, SerializedFramesMetaData } from './server-response-types';
import { ArgumentError } from './exceptions';
import { FieldUpdateTrigger } from './common';
import config from './config';
Expand Down Expand Up @@ -83,8 +83,28 @@ interface FramesMetaDataUpdatedData {
deletedFrames: Record<number, DeletedFrameState>;
}

export class ChapterMetaData {
readonly #title: string;

constructor(initialData: SerializedChapterMetaData) {
this.#title = initialData.title;
}

get title(): string {
return this.#title;
}
}

export class Chapter {
public id: number;
public start: number;
public stop: number;
public metadata: ChapterMetaData;
}

export class FramesMetaData {
public chunkSize: number;
public chapters: Chapter[] | null;
public deletedFrames: Record<number, boolean>;
public includedFrames: number[] | null;
public frameFilter: string;
Expand All @@ -109,6 +129,7 @@ export class FramesMetaData {
constructor(initialData: Omit<SerializedFramesMetaData, 'deleted_frames'> & { deleted_frames: Record<number, boolean> }) {
const data: typeof initialData = {
chunk_size: undefined,
chapters: [],
deleted_frames: {},
included_frames: null,
frame_filter: undefined,
Expand Down Expand Up @@ -174,6 +195,9 @@ export class FramesMetaData {
chunkSize: {
get: () => data.chunk_size,
},
chapters: {
get: () => data.chapters,
},
deletedFrames: {
get: () => data.deleted_frames,
},
Expand Down Expand Up @@ -971,9 +995,13 @@ export async function patchMeta(id: number, meta?: FramesMetaData, session: 'job
}

export async function findFrame(
jobID: number, frameFrom: number, frameTo: number, filters: { offset?: number, notDeleted: boolean },
jobID: number,
frameFrom: number,
frameTo: number,
filters: { offset?: number, notDeleted: boolean, chapterMark?: boolean },
): Promise<number | null> {
const offset = filters.offset || 1;
const chapterMark = filters.chapterMark || false;
const meta = await getFramesMeta('job', jobID);

const sign = Math.sign(frameTo - frameFrom);
Expand All @@ -993,6 +1021,11 @@ export async function findFrame(
if (filters.notDeleted) {
return !(frame in meta.deletedFrames);
}

if (chapterMark) {
return meta.chapters.some((chapter) => chapter.start === frame);
}

return true;
};
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
Expand Down
12 changes: 12 additions & 0 deletions cvat-core/src/server-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,20 @@ export interface SerializedCloudStorage {
manifests?: string[];
}

export interface SerializedChapterMetaData {
title: string;
}

export interface SerializedChapter {
id: number;
start: number;
end: number;
metadata: SerializedChapterMetaData;
}

export interface SerializedFramesMetaData {
chunk_size: number;
chapters: SerializedChapter[] | null
deleted_frames: number[];
included_frames: number[] | null;
frame_filter: string;
Expand Down
1 change: 1 addition & 0 deletions cvat-core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ export class Session {
filters: {
offset?: number,
notDeleted: boolean,
chapterMark?: boolean,
},
frameFrom: number,
frameTo: number,
Expand Down
45 changes: 45 additions & 0 deletions cvat-ui/src/actions/annotation-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
SEARCH_CHAPTERS_FAILED = 'SEARCH_CHAPTERS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
Expand All @@ -171,6 +172,16 @@ export enum AnnotationActionTypes {
RESTORE_FRAME_FAILED = 'RESTORE_FRAME_FAILED',
UPDATE_BRUSH_TOOLS_CONFIG = 'UPDATE_BRUSH_TOOLS_CONFIG',
HIGHLIGHT_CONFLICT = 'HIGHLIGHT_CONFCLICT',
HOVERED_CHAPTER = 'HOVERED_CHAPTER',
}

export function setHoveredChapter(id: number | null): AnyAction {
return {
type: AnnotationActionTypes.HOVERED_CHAPTER,
payload: {
id,
},
};
}

export function saveLogsAsync(): ThunkAction {
Expand Down Expand Up @@ -1324,6 +1335,40 @@ export function searchAnnotationsAsync(
};
}

export function searchChaptersAsync(
sessionInstance: NonNullable<CombinedState['annotation']['job']['instance']>,
frameFrom: number,
frameTo: number,
) {
return async (dispatch: ThunkDispatch, getState: () => CombinedState): Promise<void> => {
try {
const {
settings: {
player: { showDeletedFrames },
},
} = getState();

const frame = await sessionInstance.frames
.search(
{
notDeleted: showDeletedFrames,
chapterMark: true,
},
frameFrom,
frameTo,
);
if (frame !== null) {
dispatch(changeFrameAsync(frame));
}
} catch (error) {
dispatch({
type: AnnotationActionTypes.SEARCH_CHAPTERS_FAILED,
payload: { error },
});
}
};
}

export const ShapeTypeToControl: Record<ShapeType, ActiveControl> = {
[ShapeType.RECTANGLE]: ActiveControl.DRAW_RECTANGLE,
[ShapeType.POLYLINE]: ActiveControl.DRAW_POLYLINE,
Expand Down
8 changes: 8 additions & 0 deletions cvat-ui/src/assets/chapter-menu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions cvat-ui/src/assets/next_chapter_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions cvat-ui/src/assets/previous_chapter_icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 52 additions & 1 deletion cvat-ui/src/components/annotation-page/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,33 @@
height: $grid-unit-size;
background-color: $player-slider-color;
}

> .ant-slider-mark {
z-index: 2;
top: -16px;

> .ant-slider-mark-text {
> .ant-slider-mark-chapter {
display: inline-block;
width: 2px;
height: 10px;
background-color: #ccc;
border-radius: 2px;
transition: all 0.1s ease;
transform-origin: center center;
transform: scaleX(1);
}

> .ant-slider-mark-chapter.active {
transform: scaleX(1.5);
background-color: #ff4136;
}
}
}

> .ant-slider-step > .ant-slider-dot {
z-index: 2;
}
}

.cvat-player-slider-progress {
Expand Down Expand Up @@ -457,6 +484,28 @@
}
}

.cvat-player-chapter-menu-wrapper {
max-height: 37 * $grid-unit-size;
max-width: 22 * $grid-unit-size;
overflow: auto;
}

.cvat-player-chapter-menu-list {
.cvat-player-chapter-menu-list-item {
margin-right: 2px;

&:hover {
transform: translateX(2px);
box-shadow: 0 2px 5px rgba(0,0,0,10%);
background-color: #f5f5f5;
}
}
}

.cvat-player-chapters-menu-button svg {
transform: scale(0.8);
}

.cvat-annotations-filters-input.ant-select {
> .ant-select-selector {
height: 32px;
Expand All @@ -473,7 +522,9 @@
.cvat-player-previous-filtered-inlined-button,
.cvat-player-next-filtered-inlined-button,
.cvat-player-previous-empty-inlined-button,
.cvat-player-next-empty-inlined-button {
.cvat-player-next-empty-inlined-button,
.cvat-player-previous-chapter-inlined-button,
.cvat-player-next-chapter-inlined-button {
color: $player-buttons-color;

&:not(:first-child) {
Expand Down
87 changes: 87 additions & 0 deletions cvat-ui/src/components/annotation-page/top-bar/chapter-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React from 'react';
import Icon from '@ant-design/icons';
import Popover from 'antd/lib/popover';
import List from 'antd/lib/list';
import CvatTooltip from 'components/common/cvat-tooltip';
import { Chapter } from 'cvat-core/src/frames';
import { ChapterMenuIcon } from 'icons';

interface Props {
chapters: Chapter[];
onSelectChapter: (id: number) => void;
onHoveredChapter?: (id: number | null) => void;
}

function ChapterMenu(props: Readonly<Props>): JSX.Element {
const {
chapters,
onSelectChapter,
onHoveredChapter,
} = props;

const content = (
<div className='cvat-player-chapter-menu-wrapper'>
<List
className='cvat-player-chapter-menu-list'
size='small'
dataSource={chapters}
renderItem={(chapter: Chapter) => {
const itemClass = 'cvat-player-chapter-menu-list-item';

return (
<List.Item
className={itemClass}
key={chapter.id}
onClick={() => onSelectChapter(chapter.id)}
onMouseEnter={() => onHoveredChapter?.(chapter.id)}
onMouseLeave={() => onHoveredChapter?.(null)}
>
<div>
<strong>
<span style={{ color: '#aaa' }}>
{chapter.id}
{': '}
</span>
{chapter.metadata.title}
</strong>
<div>
Frames
{' '}
{chapter.start}
-
{chapter.stop}
</div>
</div>
</List.Item>

);
}}
/>
</div>
);

return (
<Popover
trigger='click'
content={content}
title='Chapters'
placement='bottom'
className='cvat-player-chapter-menu'
>

<CvatTooltip title='Select chapter'>
<Icon
className='cvat-player-chapters-menu-button'
component={ChapterMenuIcon}
/>
</CvatTooltip>

</Popover>
);
}

export default React.memo(ChapterMenu);
Loading
Loading