Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add video and audio datatypes #22

Merged
merged 4 commits into from
Jan 12, 2023
Merged
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
6 changes: 6 additions & 0 deletions annotto-front/src/assets/locales/en/configurationProject.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"permissions": "Users permissions",
"client": "Client",
"type": "Project type",
"types": {
"text": "Text",
"image":"Image",
"video": "Video",
"audio": "Audio"
},
"name": "Project name",
"deadline": "Project deadline",
"description": "Project description",
Expand Down
6 changes: 6 additions & 0 deletions annotto-front/src/assets/locales/en/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@
"dataset": "No dataset to annotate",
"item": "No other item available"
},
"errors": {
"videoUnsupported": "Your browser doesn't support HTML video. Here is a <0>link to the video</0> instead.",
"videoUnsupportedLink": "link to the video",
"audioUnsupported": "Your browser doesn't support HTML audio. Here is a <0>link to the audio</0> instead.",
"audioUnsupportedLink": "link to the audio"
},
"guide": {
"glossary": "Glossary",
"guidelines": "Annotation guide",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const ConfigStepPage = () => {
[defaultInitialTags, t]
)

const projectOptions = projectType.map((value) => ({ value, label: value }))
const projectOptions = projectType.map((value) => ({ value, label: t(`config.types.${value}`) }))
const userOptions = users?.map(({ email }) => ({ value: email, label: email }))
const clientOptions = client?.data?.map(({ name }) => ({ value: name, label: name }))
const today = useMemo(() => moment(), [])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import PropTypes from 'prop-types'
import React from 'react'

import projectTypes from 'shared/enums/projectTypes'
import { NER, TEXT, ZONE } from 'shared/enums/annotationTypes'
import { AUDIO, NER, TEXT, VIDEO, ZONE } from 'shared/enums/annotationTypes'

import { findAnnotationItemType } from 'modules/project/services/annotationServices'

import NerContainer from 'modules/project/components/common/NerContainer'
import TextItemContainer from 'modules/project/components/common/TextItemContainer'
import ImageMarker from 'modules/project/components/common/ImageMarker'
import VideoItem from 'modules/project/components/common/VideoItem'
import AudioItem from 'modules/project/components/common/AudioItem'

const AnnotationItemWrapper = ({ projectType, tasks, currentItem, options }) => {
const annotationType = findAnnotationItemType(projectType, tasks)
Expand All @@ -21,6 +23,14 @@ const AnnotationItemWrapper = ({ projectType, tasks, currentItem, options }) =>
return <TextItemContainer content={body} highlights={highlights} />
}

case VIDEO: {
return <VideoItem content={data?.url} />
}

case AUDIO: {
return <AudioItem content={data?.url} />
}

case NER: {
return <NerContainer {...options} content={body} highlights={highlights} tasks={filteredTasks} />
}
Expand All @@ -46,6 +56,10 @@ const getProptypes = (props) => {
return NerContainer.propTypes
case ZONE:
return ImageMarker.propTypes
case VIDEO:
return VideoItem.propTypes
case AUDIO:
return AudioItem.propTypes
default:
return null
}
Expand All @@ -57,7 +71,11 @@ AnnotationItemWrapper.propTypes = {
body: PropTypes.oneOfType([TextItemContainer.propTypes.content, NerContainer.propTypes.content]),
highlights: PropTypes.oneOfType([TextItemContainer.propTypes.highlights, NerContainer.propTypes.highlights]),
data: PropTypes.shape({
url: ImageMarker.propTypes.content,
url: PropTypes.oneOfType([
ImageMarker.propTypes.content,
VideoItem.propTypes.content,
AudioItem.propTypes.content,
]),
}),
}),
tasks: PropTypes.oneOfType([ImageMarker.propTypes.tasks, NerContainer.propTypes.tasks]),
Expand Down
36 changes: 36 additions & 0 deletions annotto-front/src/modules/project/components/common/AudioItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import PropTypes from 'prop-types'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'

import * as Styled from 'modules/project/components/common/__styles__/AudioItem.styles'

const AudioItem = ({ content }) => {
const { t } = useTranslation('project')

return (
<Styled.Root data-testid="__audio-item__">
<Styled.Audio loop controls src={content}>
<Trans
t={t}
i18nKey="project:errors.audioUnsupported"
alexandredljn marked this conversation as resolved.
Show resolved Hide resolved
components={[
<a key="link" target="_blank" href={content} rel="noopener noreferrer">
{t('project:errors.audioUnsupportedLink')}
</a>,
]}
/>
</Styled.Audio>
</Styled.Root>
)
}

export default AudioItem

AudioItem.propTypes = {
/** Defines the path of the audio. */
content: PropTypes.string,
}

AudioItem.defaultProps = {
content: null,
}
36 changes: 36 additions & 0 deletions annotto-front/src/modules/project/components/common/VideoItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import PropTypes from 'prop-types'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'

import * as Styled from 'modules/project/components/common/__styles__/VideoItem.styles'

const VideoItem = ({ content }) => {
const { t } = useTranslation('project')

return (
<Styled.Root data-testid="__video-item__">
<Styled.Video loop controls src={content}>
<Trans
t={t}
i18nKey="project:errors.videoUnsupported"
components={[
<a key="link" target="_blank" href={content} rel="noopener noreferrer">
{t('project:errors.videoUnsupportedLink')}
</a>,
]}
/>
</Styled.Video>
</Styled.Root>
)
}

export default VideoItem

VideoItem.propTypes = {
/** Defines the path of the video. */
content: PropTypes.string,
}

VideoItem.defaultProps = {
content: null,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import styled from '@xstyled/styled-components'

export const Root = styled.div`
width: 100%;
display: flex;
height: 100%;
align-items: center;
`

export const Audio = styled.audio`
width: 100%;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from '@xstyled/styled-components'

export const Root = styled.div`
width: 100%;
display: flex;
height: 100%;
align-items: center;
`

export const Video = styled.video`
width: 100%;
object-fit: fill;
`
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React, { Suspense } from 'react'
import { render } from '@testing-library/react'
import { ThemeProvider } from 'styled-components'

import { IMAGE as PROJECT_IMAGE, TEXT as PROJECT_TEXT } from 'shared/enums/projectTypes'
import { NER, TEXT, ZONE } from 'shared/enums/annotationTypes'
import { PROJECT_IMAGE, PROJECT_TEXT, PROJECT_VIDEO, PROJECT_AUDIO } from 'shared/enums/projectTypes'
import { AUDIO, NER, TEXT, VIDEO, ZONE } from 'shared/enums/annotationTypes'

import theme from '__theme__'

Expand All @@ -23,6 +23,13 @@ const getInstance = (props = {}) => (
</ThemeProvider>
)

jest.mock('react-i18next', () => ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: this could be abstracted in a setting to be used everywhere in the project.

Implementation from another project:
../__mocks__/react_i18nextMock.js :

module.exports = {
	useTranslation: () => ({
		t: key => key,
		i18n: {
			changeLanguage: () => new Promise(() => {}),
		},
	}),
	initReactI18next: {
		type: '3rdParty',
		init: jest.fn(),
	},
}

documentation: https://jestjs.io/docs/manual-mocks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created an issue to do this in another commit

Trans: ({ components }) => {
return components
},
useTranslation: () => ({ t: (key) => key }),
}))

describe('AnnotationItemWrapper component', () => {
it('should render text container for text annotation type', () => {
const { getByTestId } = render(getInstance())
Expand Down Expand Up @@ -53,4 +60,32 @@ describe('AnnotationItemWrapper component', () => {

expect(imageMarker).toBeInTheDocument()
})

it('should render video component for video annotation type', () => {
const props = {
projectType: PROJECT_VIDEO,
tasks: [{ type: VIDEO, value: 'foo', label: 'foo' }],
currentItem: { data: { url: 'some video url' }, highlights: [] },
options: { someOption: 'some value' },
}
const { getByTestId } = render(getInstance(props))

const videoContainer = getByTestId('__video-item__')

expect(videoContainer).toBeInTheDocument()
})

it('should render audio component for audio annotation type', () => {
const props = {
projectType: PROJECT_AUDIO,
tasks: [{ type: AUDIO, value: 'foo', label: 'foo' }],
currentItem: { data: { url: 'some audio url' }, highlights: [] },
options: { someOption: 'some value' },
}
const { getByTestId } = render(getInstance(props))

const audioContainer = getByTestId('__audio-item__')

expect(audioContainer).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import { render } from '@testing-library/react'

import AudioItem from 'modules/project/components/common/AudioItem'

const getInstance = (props = {}) => render(<AudioItem {...props} />)

jest.mock('react-i18next', () => ({
Trans: ({ components }) => {
return components
},
useTranslation: () => ({ t: (key) => key }),
}))

describe('AudioItem', () => {
const content = 'https://example.com/audio.mp3'
it('renders the audio element with the correct src', () => {
const { getByTestId } = getInstance({ content })

const audioElement = getByTestId('__audio-item__').firstChild
expect(audioElement).toHaveAttribute('src', content)
})

it('renders a fallback message when the audio is not supported', () => {
const { getByText } = getInstance({ content })

const fallbackElement = getByText('project:errors.audioUnsupportedLink')
expect(fallbackElement).toBeInTheDocument()
expect(fallbackElement).toHaveAttribute('href', content)
expect(fallbackElement).toHaveAttribute('rel', 'noopener noreferrer')
expect(fallbackElement).toHaveAttribute('target', '_blank')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import { render } from '@testing-library/react'

import VideoItem from '../VideoItem'

const getInstance = (props = {}) => render(<VideoItem {...props} />)

jest.mock('react-i18next', () => ({
Trans: ({ components }) => {
return components
},
useTranslation: () => ({ t: (key) => key }),
}))

describe('VideoItem', () => {
const content = 'https://example.com/video.mp4'
it('renders the video element with the correct src', () => {
const { getByTestId } = getInstance({ content })

const videoElement = getByTestId('__video-item__').firstChild
expect(videoElement).toHaveAttribute('src', content)
})

it('renders a fallback message when the video is not supported', () => {
const { getByText } = getInstance({ content })

const fallbackElement = getByText('project:errors.videoUnsupportedLink')
expect(fallbackElement).toBeInTheDocument()
expect(fallbackElement).toHaveAttribute('href', content)
expect(fallbackElement).toHaveAttribute('rel', 'noopener noreferrer')
expect(fallbackElement).toHaveAttribute('target', '_blank')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import ZoneTools from 'modules/project/components/common/ZoneTools'
import AnnotationItemWrapper from 'modules/project/components/common/AnnotationItemWrapper'

import { CLASSIFICATIONS, NER, TEXT as ANNOTATION_TEXT, ZONE } from 'shared/enums/annotationTypes'
import { IMAGE, TEXT } from 'shared/enums/projectTypes'
import { PROJECT_IMAGE, PROJECT_TEXT, PROJECT_VIDEO, PROJECT_AUDIO } from 'shared/enums/projectTypes'
import { ITEM, PREDICTIONS, RAW } from 'shared/enums/itemTypes'
import { TASKS } from 'shared/enums/projectStatsTypes'
import { TWO_POINTS, WORD } from 'shared/enums/markerTypes'
Expand Down Expand Up @@ -184,7 +184,7 @@ const AnnotationPage = ({ setHeaderActions }) => {

useEffect(() => {
if (!selectedMode) {
setSelectedMode(projectType === TEXT ? WORD : TWO_POINTS)
setSelectedMode(projectType === PROJECT_TEXT ? WORD : TWO_POINTS)
}
}, [projectType, selectedMode])

Expand Down Expand Up @@ -339,7 +339,7 @@ const AnnotationPage = ({ setHeaderActions }) => {
<Row gutter={['11', '11']}>
<Col span={14}>
<Styled.Container>
<Styled.Space direction="vertical" $isTextContent={projectType === TEXT}>
<Styled.Space direction="vertical" $isTextContent={projectType === PROJECT_TEXT}>
<ActionBar
tags={currentItem?.tags}
availableTags={availableTags}
Expand Down Expand Up @@ -394,7 +394,7 @@ const AnnotationPage = ({ setHeaderActions }) => {
)}
/>
)}
{projectType === TEXT && currentItem && (
{[PROJECT_TEXT, PROJECT_VIDEO, PROJECT_AUDIO].includes(projectType) && currentItem && (
<LogsContainer logs={currentItemLogs?.data || []} isProjectContext={false} />
)}
</Styled.Space>
Expand All @@ -408,9 +408,11 @@ const AnnotationPage = ({ setHeaderActions }) => {
selectedSection={selectedSection}
selectedRelation={selectedRelation}
selectedMode={selectedMode}
isProjectTypeText={projectType === TEXT}
isProjectTypeText={projectType === PROJECT_TEXT}
entitiesRelationsGroup={projectEntitiesRelationsGroup}
tasks={projectTasks.filter(({ type }) => type === (projectType === TEXT ? NER : ZONE))}
tasks={projectTasks.filter(
({ type }) => type === (projectType === PROJECT_TEXT ? NER : ZONE)
)}
onToolChange={_onToolChange}
onSelectionChange={_onSelectionChange}
onRelationChange={_onRelationChange}
Expand All @@ -429,7 +431,7 @@ const AnnotationPage = ({ setHeaderActions }) => {
/>
</Styled.Content>
)}
{projectTasks.some((task) => task.type === TEXT) && (
{projectTasks.some((task) => task.type === PROJECT_TEXT) && (
<Styled.Content>
<TextAnnotationsContainer
annotations={annotations}
Expand All @@ -440,7 +442,7 @@ const AnnotationPage = ({ setHeaderActions }) => {
/>
</Styled.Content>
)}
{projectType === IMAGE && currentItem && projectId && (
{projectType === PROJECT_IMAGE && currentItem && projectId && (
<Styled.Content>
<LogsContainer logs={currentItemLogs?.data || []} isProjectContext={false} />
</Styled.Content>
Expand Down
Loading