Browse Source

Merge pull request #2197 from doccano/enhancement/frontend-seq2seq

[Enhancement] frontend
pull/2194/head
Hiroki Nakayama 2 years ago
committed by GitHub
parent
commit
e511ac6431
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 293 additions and 423 deletions
  1. 77
      frontend/composables/useTextLabel.ts
  2. 4
      frontend/domain/models/tasks/textLabel.ts
  3. 240
      frontend/pages/projects/_id/image-captioning/index.vue
  4. 2
      frontend/pages/projects/_id/labels/import.vue
  5. 169
      frontend/pages/projects/_id/sequence-to-sequence/index.vue
  6. 182
      frontend/pages/projects/_id/speech-to-text/index.vue
  7. 3
      frontend/plugins/services.ts
  8. 26
      frontend/services/application/tasks/seq2seq/seq2seqApplicationService.ts
  9. 13
      frontend/services/application/tasks/seq2seq/seq2seqData.ts

77
frontend/composables/useTextLabel.ts

@ -0,0 +1,77 @@
import { reactive } from '@nuxtjs/composition-api'
import { TextLabel } from '~/domain/models/tasks/textLabel'
export const useTextLabel = (repository: any, projectId: string) => {
const state = reactive({
labels: [] as TextLabel[],
error: ''
})
const validateText = (text: string) => {
if (state.labels.some((label) => label.text === text)) {
state.error = 'The label already exists.'
return false
}
return true
}
const add = async (exampleId: number, text: string) => {
const textLabel = TextLabel.create(text)
if (!validateText(textLabel.text)) {
return
}
try {
await repository.create(projectId, exampleId, textLabel)
await list(exampleId)
} catch (e: any) {
state.error = e.response.data.detail
}
}
const list = async (exampleId: number) => {
state.labels = await repository.list(projectId, exampleId)
}
const update = async (exampleId: number, labelId: number, text: string) => {
if (!validateText(text)) {
return
}
const label = state.labels.find((label) => label.id === labelId)!
label.updateText(text)
try {
await repository.update(projectId, exampleId, labelId, label)
await list(exampleId)
} catch (e: any) {
state.error = e.response.data.detail
}
}
const remove = async (exampleId: number, labelId: number) => {
await repository.delete(projectId, exampleId, labelId)
state.labels = state.labels.filter((label) => label.id !== labelId)
}
const clear = async (exampleId: number) => {
await repository.clear(projectId, exampleId)
state.labels = []
}
const autoLabel = async (exampleId: number) => {
try {
await repository.autoLabel(projectId, exampleId)
await list(exampleId)
} catch (e: any) {
state.error = e.response.data.detail
}
}
return {
state,
add,
list,
update,
remove,
clear,
autoLabel
}
}

4
frontend/domain/models/tasks/textLabel.ts

@ -1,6 +1,10 @@
export class TextLabel {
constructor(readonly id: number, private _text: string, readonly user: number) {}
public static create(text: string): TextLabel {
return new TextLabel(0, text, 0)
}
get text(): string {
return this._text
}

240
frontend/pages/projects/_id/image-captioning/index.vue

@ -1,56 +1,50 @@
<template>
<layout-text v-if="image.id">
<layout-text v-if="example.id">
<template #header>
<toolbar-laptop
:doc-id="image.id"
:doc-id="example.id"
:enable-auto-labeling.sync="enableAutoLabeling"
:guideline-text="project.guideline"
:is-reviewd="image.isConfirmed"
:total="images.count"
:is-reviewd="example.isConfirmed"
:total="totalExample"
class="d-none d-sm-block"
@click:clear-label="clear"
@click:review="confirm"
>
<v-btn-toggle v-model="labelOption" mandatory class="ms-2">
<v-btn icon>
<v-icon>{{ mdiFormatListBulleted }}</v-icon>
</v-btn>
<v-btn icon>
<v-icon>{{ mdiText }}</v-icon>
</v-btn>
</v-btn-toggle>
</toolbar-laptop>
<toolbar-mobile :total="images.count" class="d-flex d-sm-none" />
@click:clear-label="clear(example.id)"
@click:review="confirm(projectId)"
/>
<toolbar-mobile :total="totalExample" class="d-flex d-sm-none" />
</template>
<template #content>
<v-snackbar :value="!!error" color="error" timeout="2000">
{{ error }}
</v-snackbar>
<v-card>
<v-img contain :src="image.url" :max-height="imageSize.height" class="grey lighten-2" />
<v-img contain :src="example.url" :max-height="imageSize.height" class="grey lighten-2" />
<seq2seq-box
:annotations="annotations"
@delete:annotation="remove"
@update:annotation="update"
@create:annotation="add"
:annotations="labels"
@delete:annotation="(labelId) => remove(example.id, labelId)"
@update:annotation="(labelId, text) => update(example.id, labelId, text)"
@create:annotation="(text) => add(example.id, text)"
/>
</v-card>
</template>
<template #sidebar>
<annotation-progress :progress="progress" />
<list-metadata :metadata="image.meta" class="mt-4" />
<list-metadata :metadata="example.meta" class="mt-4" />
</template>
</layout-text>
</template>
<script>
import { mdiFormatListBulleted, mdiText } from '@mdi/js'
import { toRefs, useContext } from '@nuxtjs/composition-api'
import _ from 'lodash'
import { reactive, ref, toRefs, useContext, useFetch, watch } from '@nuxtjs/composition-api'
import LayoutText from '@/components/tasks/layout/LayoutText'
import ListMetadata from '@/components/tasks/metadata/ListMetadata'
import AnnotationProgress from '@/components/tasks/sidebar/AnnotationProgress.vue'
import ToolbarLaptop from '@/components/tasks/toolbar/ToolbarLaptop'
import ToolbarMobile from '@/components/tasks/toolbar/ToolbarMobile'
import { useLabelList } from '@/composables/useLabelList'
import Seq2seqBox from '~/components/tasks/seq2seq/Seq2seqBox'
import { useExampleItem } from '~/composables/useExampleItem'
import { useProjectItem } from '~/composables/useProjectItem'
import { useTextLabel } from '~/composables/useTextLabel'
export default {
components: {
@ -61,6 +55,7 @@ export default {
ToolbarLaptop,
ToolbarMobile
},
layout: 'workspace',
validate({ params, query }) {
@ -68,148 +63,71 @@ export default {
},
setup() {
const { app } = useContext()
const { state, getLabelList, shortKeys } = useLabelList(app.$services.categoryType)
return {
...toRefs(state),
getLabelList,
shortKeys
const { app, params, query } = useContext()
const projectId = params.value.id
const { state: projectState, getProjectById } = useProjectItem()
const {
state: labelState,
autoLabel,
list,
clear,
remove,
add,
update
} = useTextLabel(app.$repositories.textLabel, projectId)
const { state: exampleState, confirm, getExample, updateProgress } = useExampleItem()
const enableAutoLabeling = ref(false)
const imageSize = reactive({
height: 0,
width: 0
})
getProjectById(projectId)
updateProgress(projectId)
const setImageSize = (val) => {
const img = new Image()
img.onload = function () {
imageSize.height = this.height
imageSize.width = this.width
}
img.src = val.url
}
},
data() {
return {
annotations: [],
images: [],
project: {},
enableAutoLabeling: false,
labelOption: 0,
imageSize: {
height: 0,
width: 0
},
mdiText,
mdiFormatListBulleted,
progress: {},
headers: [
{
text: 'Text',
align: 'left',
value: 'text'
},
{
text: 'Actions',
align: 'right',
value: 'action'
const { fetch } = useFetch(async () => {
await getExample(projectId, query.value)
setImageSize(exampleState.example)
if (enableAutoLabeling.value) {
try {
await autoLabel(projectId, exampleState.example.id)
} catch (e) {
enableAutoLabeling.value = false
}
]
}
},
async fetch() {
this.images = await this.$services.example.fetchOne(
this.projectId,
this.$route.query.page,
this.$route.query.q,
this.$route.query.isChecked,
this.$route.query.ordering
)
const image = this.images.items[0]
this.setImageSize(image)
if (this.enableAutoLabeling) {
await this.autoLabel(image.id)
}
await this.list(image.id)
},
computed: {
projectId() {
return this.$route.params.id
},
image() {
if (_.isEmpty(this.images) || this.images.items.length === 0) {
return {}
} else {
return this.images.items[0]
}
}
},
watch: {
'$route.query': '$fetch',
async enableAutoLabeling(val) {
if (val && !this.image.isConfirmed) {
await this.autoLabel(this.image.id)
await this.list(this.image.id)
await list(exampleState.example.id)
}
}
},
async created() {
this.getLabelList(this.projectId)
this.project = await this.$services.project.findById(this.projectId)
this.progress = await this.$repositories.metrics.fetchMyProgress(this.projectId)
},
methods: {
async list(imageId) {
this.annotations = await this.$services.seq2seq.list(this.projectId, imageId)
},
async remove(id) {
await this.$services.seq2seq.delete(this.projectId, this.image.id, id)
await this.list(this.image.id)
},
async add(text) {
await this.$services.seq2seq.create(this.projectId, this.image.id, text)
await this.list(this.image.id)
},
async update(annotationId, text) {
await this.$services.seq2seq.changeText(this.projectId, this.image.id, annotationId, text)
await this.list(this.image.id)
},
async clear() {
await this.$services.seq2seq.clear(this.projectId, this.image.id)
await this.list(this.image.id)
},
async autoLabel(imageId) {
try {
await this.$services.seq2seq.autoLabel(this.projectId, imageId)
} catch (e) {
console.log(e.response.data.detail)
})
watch(query, fetch)
watch(enableAutoLabeling, async (val) => {
if (val && !exampleState.example.isConfirmed) {
await autoLabel(exampleState.example.id)
}
},
})
async updateProgress() {
this.progress = await this.$repositories.metrics.fetchMyProgress(this.projectId)
},
async confirm() {
await this.$services.example.confirm(this.projectId, this.image.id)
await this.$fetch()
this.updateProgress()
},
setImageSize(val) {
const img = new Image()
const self = this
img.onload = function () {
self.imageSize.height = this.height
self.imageSize.width = this.width
}
img.src = val.url
return {
...toRefs(labelState),
...toRefs(exampleState),
...toRefs(projectState),
add,
list,
clear,
remove,
update,
confirm,
enableAutoLabeling,
imageSize,
projectId
}
}
}
</script>
<style scoped>
.text-pre-wrap {
white-space: pre-wrap !important;
}
</style>

2
frontend/pages/projects/_id/labels/import.vue

@ -54,7 +54,7 @@ export default Vue.extend({
try {
await this.service.upload(this.projectId, file)
this.$router.push(`/projects/${this.projectId}/labels`)
} catch (e) {
} catch (e: any) {
this.errorMessage = e.message
}
},

169
frontend/pages/projects/_id/sequence-to-sequence/index.vue

@ -1,45 +1,48 @@
<template>
<layout-text v-if="doc.id">
<layout-text v-if="example.id">
<template #header>
<toolbar-laptop
:doc-id="doc.id"
:doc-id="example.id"
:enable-auto-labeling.sync="enableAutoLabeling"
:guideline-text="project.guideline"
:is-reviewd="doc.isConfirmed"
:total="docs.count"
:is-reviewd="example.isConfirmed"
:total="totalExample"
class="d-none d-sm-block"
@click:clear-label="clear"
@click:review="confirm"
@click:clear-label="clear(example.id)"
@click:review="confirm(projectId)"
/>
<toolbar-mobile :total="docs.count" class="d-flex d-sm-none" />
<toolbar-mobile :total="totalExample" class="d-flex d-sm-none" />
</template>
<template #content>
<v-card class="mb-5">
<v-card-text class="title text-pre-wrap" v-text="doc.text" />
<v-card-text class="title text-pre-wrap">{{ example.text }}</v-card-text>
</v-card>
<seq2seq-box
:text="doc.text"
:annotations="annotations"
@delete:annotation="remove"
@update:annotation="update"
@create:annotation="add"
:text="example.text"
:annotations="labels"
@delete:annotation="(labelId) => remove(example.id, labelId)"
@update:annotation="(labelId, text) => update(example.id, labelId, text)"
@create:annotation="(text) => add(example.id, text)"
/>
</template>
<template #sidebar>
<annotation-progress :progress="progress" />
<list-metadata :metadata="doc.meta" class="mt-4" />
<list-metadata :metadata="example.meta" class="mt-4" />
</template>
</layout-text>
</template>
<script>
import _ from 'lodash'
import { ref, toRefs, useContext, useFetch, watch } from '@nuxtjs/composition-api'
import LayoutText from '@/components/tasks/layout/LayoutText'
import ListMetadata from '@/components/tasks/metadata/ListMetadata'
import AnnotationProgress from '@/components/tasks/sidebar/AnnotationProgress.vue'
import ToolbarLaptop from '@/components/tasks/toolbar/ToolbarLaptop'
import ToolbarMobile from '@/components/tasks/toolbar/ToolbarMobile'
import Seq2seqBox from '~/components/tasks/seq2seq/Seq2seqBox'
import { useExampleItem } from '~/composables/useExampleItem'
import { useProjectItem } from '~/composables/useProjectItem'
import { useTextLabel } from '~/composables/useTextLabel'
export default {
components: {
@ -56,100 +59,56 @@ export default {
return /^\d+$/.test(params.id) && /^\d+$/.test(query.page)
},
data() {
return {
annotations: [],
docs: [],
project: {},
enableAutoLabeling: false,
progress: {}
}
},
async fetch() {
this.docs = await this.$services.example.fetchOne(
this.projectId,
this.$route.query.page,
this.$route.query.q,
this.$route.query.isChecked,
this.$route.query.ordering
)
const doc = this.docs.items[0]
if (this.enableAutoLabeling) {
await this.autoLabel(doc.id)
}
await this.list(doc.id)
},
computed: {
projectId() {
return this.$route.params.id
},
doc() {
if (_.isEmpty(this.docs) || this.docs.items.length === 0) {
return {}
setup() {
const { app, params, query } = useContext()
const projectId = params.value.id
const { state: projectState, getProjectById } = useProjectItem()
const {
state: labelState,
autoLabel,
list,
clear,
remove,
add,
update
} = useTextLabel(app.$repositories.textLabel, projectId)
const { state: exampleState, confirm, getExample, updateProgress } = useExampleItem()
const enableAutoLabeling = ref(false)
getProjectById(projectId)
updateProgress(projectId)
const { fetch } = useFetch(async () => {
await getExample(projectId, query.value)
if (enableAutoLabeling.value) {
try {
await autoLabel(projectId, exampleState.example.id)
} catch (e) {
enableAutoLabeling.value = false
}
} else {
return this.docs.items[0]
await list(exampleState.example.id)
}
}
},
watch: {
'$route.query': '$fetch',
async enableAutoLabeling(val) {
if (val && !this.doc.isConfirmed) {
await this.autoLabel(this.doc.id)
await this.list(this.doc.id)
})
watch(query, fetch)
watch(enableAutoLabeling, async (val) => {
if (val && !exampleState.example.isConfirmed) {
await autoLabel(exampleState.example.id)
}
}
},
})
async created() {
this.project = await this.$services.project.findById(this.projectId)
this.progress = await this.$$repositories.metrics.fetchMyProgress(this.projectId)
},
methods: {
async list(docId) {
this.annotations = await this.$services.seq2seq.list(this.projectId, docId)
},
async remove(id) {
await this.$services.seq2seq.delete(this.projectId, this.doc.id, id)
await this.list(this.doc.id)
},
async add(text) {
await this.$services.seq2seq.create(this.projectId, this.doc.id, text)
await this.list(this.doc.id)
},
async update(annotationId, text) {
await this.$services.seq2seq.changeText(this.projectId, this.doc.id, annotationId, text)
await this.list(this.doc.id)
},
async clear() {
await this.$services.seq2seq.clear(this.projectId, this.doc.id)
await this.list(this.doc.id)
},
async autoLabel(docId) {
try {
await this.$services.seq2seq.autoLabel(this.projectId, docId)
} catch (e) {
console.log(e.response.data.detail)
}
},
async updateProgress() {
this.progress = await this.$repositories.metrics.fetchMyProgress(this.projectId)
},
async confirm() {
await this.$services.example.confirm(this.projectId, this.doc.id)
await this.$fetch()
this.updateProgress()
return {
...toRefs(labelState),
...toRefs(exampleState),
...toRefs(projectState),
add,
list,
clear,
remove,
update,
confirm,
enableAutoLabeling,
projectId
}
}
}

182
frontend/pages/projects/_id/speech-to-text/index.vue

@ -1,40 +1,40 @@
<template>
<layout-text v-if="item.id">
<layout-text v-if="example.id">
<template #header>
<toolbar-laptop
:doc-id="item.id"
:doc-id="example.id"
:enable-auto-labeling.sync="enableAutoLabeling"
:guideline-text="project.guideline"
:is-reviewd="item.isConfirmed"
:total="items.count"
:is-reviewd="example.isConfirmed"
:total="totalExample"
class="d-none d-sm-block"
@click:clear-label="clear"
@click:review="confirm"
@click:clear-label="clear(example.id)"
@click:review="confirm(projectId)"
/>
<toolbar-mobile :total="items.count" class="d-flex d-sm-none" />
<toolbar-mobile :total="totalExample" class="d-flex d-sm-none" />
</template>
<template #content>
<v-overlay :value="isLoading">
<v-progress-circular indeterminate size="64" />
</v-overlay>
<audio-viewer :source="item.url" class="mb-5" />
<audio-viewer :source="example.url" class="mb-5" />
<seq2seq-box
:text="item.text"
:annotations="annotations"
@delete:annotation="remove"
@update:annotation="update"
@create:annotation="add"
:text="example.text"
:annotations="labels"
@delete:annotation="(labelId) => remove(example.id, labelId)"
@update:annotation="(labelId, text) => update(example.id, labelId, text)"
@create:annotation="(text) => add(example.id, text)"
/>
</template>
<template #sidebar>
<annotation-progress :progress="progress" />
<list-metadata :metadata="item.meta" class="mt-4" />
<list-metadata :metadata="example.meta" class="mt-4" />
</template>
</layout-text>
</template>
<script>
import _ from 'lodash'
import { ref, toRefs, useContext, useFetch, watch } from '@nuxtjs/composition-api'
import LayoutText from '@/components/tasks/layout/LayoutText'
import ListMetadata from '@/components/tasks/metadata/ListMetadata'
import AnnotationProgress from '@/components/tasks/sidebar/AnnotationProgress.vue'
@ -42,6 +42,9 @@ import ToolbarLaptop from '@/components/tasks/toolbar/ToolbarLaptop'
import ToolbarMobile from '@/components/tasks/toolbar/ToolbarMobile'
import AudioViewer from '~/components/tasks/audio/AudioViewer'
import Seq2seqBox from '~/components/tasks/seq2seq/Seq2seqBox'
import { useExampleItem } from '~/composables/useExampleItem'
import { useProjectItem } from '~/composables/useProjectItem'
import { useTextLabel } from '~/composables/useTextLabel'
export default {
components: {
@ -59,110 +62,61 @@ export default {
return /^\d+$/.test(params.id) && /^\d+$/.test(query.page)
},
data() {
return {
annotations: [],
items: [],
project: {},
enableAutoLabeling: false,
isLoading: false,
progress: {}
}
},
async fetch() {
this.isLoading = true
this.items = await this.$services.example.fetchOne(
this.projectId,
this.$route.query.page,
this.$route.query.q,
this.$route.query.isChecked,
this.$route.query.ordering
)
const item = this.items.items[0]
if (this.enableAutoLabeling) {
await this.autoLabel(item.id)
}
await this.list(item.id)
this.isLoading = false
},
computed: {
projectId() {
return this.$route.params.id
},
item() {
if (_.isEmpty(this.items) || this.items.items.length === 0) {
return {}
setup() {
const { app, params, query } = useContext()
const projectId = params.value.id
const { state: projectState, getProjectById } = useProjectItem()
const {
state: labelState,
autoLabel,
list,
clear,
remove,
add,
update
} = useTextLabel(app.$repositories.textLabel, projectId)
const { state: exampleState, confirm, getExample, updateProgress } = useExampleItem()
const enableAutoLabeling = ref(false)
const isLoading = ref(false)
getProjectById(projectId)
updateProgress(projectId)
const { fetch } = useFetch(async () => {
isLoading.value = true
await getExample(projectId, query.value)
if (enableAutoLabeling.value) {
try {
await autoLabel(projectId, exampleState.example.id)
} catch (e) {
enableAutoLabeling.value = false
}
} else {
return this.items.items[0]
await list(exampleState.example.id)
}
}
},
watch: {
'$route.query': '$fetch',
async enableAutoLabeling(val) {
if (val && !this.item.isConfirmed) {
await this.autoLabel(this.item.id)
await this.list(this.item.id)
isLoading.value = false
})
watch(query, fetch)
watch(enableAutoLabeling, async (val) => {
if (val && !exampleState.example.isConfirmed) {
await autoLabel(exampleState.example.id)
}
}
},
})
async created() {
this.project = await this.$services.project.findById(this.projectId)
this.progress = await this.$repositories.metrics.fetchMyProgress(this.projectId)
},
methods: {
async list(itemId) {
this.annotations = await this.$services.seq2seq.list(this.projectId, itemId)
},
async remove(id) {
await this.$services.seq2seq.delete(this.projectId, this.item.id, id)
await this.list(this.item.id)
},
async add(text) {
await this.$services.seq2seq.create(this.projectId, this.item.id, text)
await this.list(this.item.id)
},
async update(annotationId, text) {
await this.$services.seq2seq.changeText(this.projectId, this.item.id, annotationId, text)
await this.list(this.item.id)
},
async clear() {
await this.$services.seq2seq.clear(this.projectId, this.item.id)
await this.list(this.item.id)
},
async autoLabel(itemId) {
try {
await this.$services.seq2seq.autoLabel(this.projectId, itemId)
} catch (e) {
console.log(e.response.data.detail)
}
},
async updateProgress() {
this.progress = await this.$repositories.metrics.fetchMyProgress(this.projectId)
},
async confirm() {
await this.$services.example.confirm(this.projectId, this.item.id)
await this.$fetch()
this.updateProgress()
return {
...toRefs(labelState),
...toRefs(exampleState),
...toRefs(projectState),
add,
list,
clear,
remove,
update,
confirm,
enableAutoLabeling,
projectId,
isLoading
}
}
}
</script>
<style scoped>
.text-pre-wrap {
white-space: pre-wrap !important;
}
</style>

3
frontend/plugins/services.ts

@ -7,7 +7,6 @@ import { ProjectApplicationService } from '@/services/application/project/projec
import { TagApplicationService } from '@/services/application/tag/tagApplicationService'
import { BoundingBoxApplicationService } from '@/services/application/tasks/boundingBox/boundingBoxApplicationService'
import { SegmentationApplicationService } from '@/services/application/tasks/segmentation/segmentationApplicationService'
import { Seq2seqApplicationService } from '@/services/application/tasks/seq2seq/seq2seqApplicationService'
import { SequenceLabelingApplicationService } from '@/services/application/tasks/sequenceLabeling/sequenceLabelingApplicationService'
export interface Services {
@ -17,7 +16,6 @@ export interface Services {
project: ProjectApplicationService
example: ExampleApplicationService
sequenceLabeling: SequenceLabelingApplicationService
seq2seq: Seq2seqApplicationService
option: OptionApplicationService
tag: TagApplicationService
bbox: BoundingBoxApplicationService
@ -41,7 +39,6 @@ const plugin: Plugin = (_, inject) => {
repositories.span,
repositories.relation
),
seq2seq: new Seq2seqApplicationService(repositories.textLabel),
option: new OptionApplicationService(repositories.option),
tag: new TagApplicationService(repositories.tag),
bbox: new BoundingBoxApplicationService(repositories.boundingBox),

26
frontend/services/application/tasks/seq2seq/seq2seqApplicationService.ts

@ -1,26 +0,0 @@
import { AnnotationApplicationService } from '../annotationApplicationService'
import { Seq2seqDTO } from './seq2seqData'
import { TextLabel } from '@/domain/models/tasks/textLabel'
export class Seq2seqApplicationService extends AnnotationApplicationService<TextLabel> {
public async list(projectId: string, exampleId: number): Promise<Seq2seqDTO[]> {
const items = await this.repository.list(projectId, exampleId)
return items.map((item) => new Seq2seqDTO(item))
}
public async create(projectId: string, exampleId: number, text: string): Promise<void> {
const item = new TextLabel(0, text, 0)
await this.repository.create(projectId, exampleId, item)
}
public async changeText(
projectId: string,
exampleId: number,
labelId: number,
text: string
): Promise<void> {
const textLabel = await this.repository.find(projectId, exampleId, labelId)
textLabel.updateText(text)
await this.repository.update(projectId, exampleId, labelId, textLabel)
}
}

13
frontend/services/application/tasks/seq2seq/seq2seqData.ts

@ -1,13 +0,0 @@
import { TextLabel } from '@/domain/models/tasks/textLabel'
export class Seq2seqDTO {
id: number
text: string
user: number
constructor(item: TextLabel) {
this.id = item.id
this.text = item.text
this.user = item.user
}
}
Loading…
Cancel
Save