Skip to content

Commit 5c26846

Browse files
authored
Merge pull request #23 from vogelbam/add-clips-table
Add clip table
2 parents 59d689a + 8651db0 commit 5c26846

37 files changed

+588
-81
lines changed

back/src/whombat/filters/annotation_tasks.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from uuid import UUID
44

55
from soundevent import data
6-
from sqlalchemy import Select
6+
from sqlalchemy import Select, or_
77

88
from whombat import models
99
from whombat.filters import base
@@ -12,6 +12,7 @@
1212
"AnnotationProjectFilter",
1313
"DatasetFilter",
1414
"AnnotationTaskFilter",
15+
"SearchRecordingsFilter",
1516
]
1617

1718

@@ -118,6 +119,25 @@ def filter(self, query: Select) -> Select:
118119
)
119120

120121

122+
class IsCompletedFilter(base.Filter):
123+
"""Filter for tasks if rejected."""
124+
125+
eq: bool | None = None
126+
127+
def filter(self, query: Select) -> Select:
128+
"""Filter the query."""
129+
if self.eq is None:
130+
return query
131+
132+
return query.where(
133+
models.AnnotationTask.status_badges.any(
134+
models.AnnotationStatusBadge.state
135+
== data.AnnotationState.completed,
136+
)
137+
== self.eq,
138+
)
139+
140+
121141
class IsAssignedFilter(base.Filter):
122142
"""Filter for tasks if assigned."""
123143

@@ -206,6 +226,34 @@ def filter(self, query: Select) -> Select:
206226
)
207227

208228

229+
class SearchRecordingsFilter(base.Filter):
230+
"""Filter recordings by the dataset they are in."""
231+
232+
search_recordings: str | None = None
233+
234+
def filter(self, query: Select) -> Select:
235+
"""Filter the query."""
236+
query = (
237+
query.join(
238+
models.ClipAnnotation,
239+
models.AnnotationTask.clip_annotation_id
240+
== models.ClipAnnotation.id,
241+
)
242+
.join(
243+
models.Clip,
244+
models.ClipAnnotation.clip_id == models.Clip.id,
245+
)
246+
.join(
247+
models.Recording,
248+
models.Recording.id == models.Clip.recording_id,
249+
)
250+
)
251+
fields = [models.Recording.path]
252+
253+
term = f"%{self.search_recordings}%"
254+
return query.where(or_(*[field.ilike(term) for field in fields]))
255+
256+
209257
class SoundEventAnnotationTagFilter(base.Filter):
210258
"""Filter for tasks by sound event annotation tag."""
211259

@@ -258,10 +306,12 @@ def filter(self, query: Select) -> Select:
258306

259307

260308
AnnotationTaskFilter = base.combine(
309+
SearchRecordingsFilter,
261310
assigned_to=AssignedToFilter,
262311
pending=PendingFilter,
263312
verified=IsVerifiedFilter,
264313
rejected=IsRejectedFilter,
314+
completed=IsCompletedFilter,
265315
assigned=IsAssignedFilter,
266316
annotation_project=AnnotationProjectFilter,
267317
dataset=DatasetFilter,

back/src/whombat/models/annotation_task.py

+2
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,14 @@ class AnnotationTask(Base):
100100
clip: orm.Mapped[Clip] = orm.relationship(
101101
init=False,
102102
repr=False,
103+
lazy="joined",
103104
)
104105
clip_annotation: orm.Mapped[ClipAnnotation] = orm.relationship(
105106
back_populates="annotation_task",
106107
cascade="all, delete-orphan",
107108
init=False,
108109
single_parent=True,
110+
lazy="joined",
109111
)
110112
status_badges: orm.Mapped[list["AnnotationStatusBadge"]] = (
111113
orm.relationship(

back/src/whombat/schemas/annotation_tasks.py

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from soundevent.data import AnnotationState
77

88
from whombat.schemas.base import BaseSchema
9+
from whombat.schemas.clip_annotations import ClipAnnotation
10+
from whombat.schemas.clips import Clip
911
from whombat.schemas.users import SimpleUser
1012

1113
__all__ = [
@@ -50,6 +52,12 @@ class AnnotationTask(BaseSchema):
5052
status_badges: list[AnnotationStatusBadge]
5153
"""Status badges for the task."""
5254

55+
clip: Clip
56+
"""Clip of the task."""
57+
58+
clip_annotation: ClipAnnotation
59+
"""Clip annotation for the task."""
60+
5361

5462
class AnnotationTaskUpdate(BaseModel):
5563
"""Schema for updating a task."""

front/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
9+
"format": "prettier --write .",
910
"lint": "next lint",
1011
"lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src"
1112
},

front/postcss.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ module.exports = {
33
tailwindcss: {},
44
autoprefixer: {},
55
},
6-
}
6+
};

front/src/api/annotation_tasks.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export const AnnotationTaskFilterSchema = z.object({
3232
assigned: z.boolean().optional(),
3333
verified: z.boolean().optional(),
3434
rejected: z.boolean().optional(),
35+
completed: z.boolean().optional(),
3536
assigned_to: UserSchema.optional(),
37+
search_recordings: z.string().optional(),
3638
});
3739

3840
export type AnnotationTaskFilter = z.input<typeof AnnotationTaskFilterSchema>;
@@ -90,12 +92,15 @@ export function registerAnnotationTasksAPI(
9092
recording_tag__key: params.recording_tag?.key,
9193
recording_tag__value: params.recording_tag?.value,
9294
sound_event_annotation_tag__key: params.sound_event_annotation_tag?.key,
93-
sound_event_annotation_tag__value: params.sound_event_annotation_tag?.value,
95+
sound_event_annotation_tag__value:
96+
params.sound_event_annotation_tag?.value,
9497
pending__eq: params.pending,
9598
assigned__eq: params.assigned,
9699
verified__eq: params.verified,
97100
rejected__eq: params.rejected,
101+
completed__eq: params.completed,
98102
assigned_to__eq: params.assigned_to?.id,
103+
search_recordings: params.search_recordings,
99104
},
100105
});
101106
return AnnotationTaskPageSchema.parse(response.data);

front/src/api/clip_predictions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
PredictionTagSchema,
1010
RecordingSchema,
1111
UserRunSchema,
12-
PredictedTagFilterSchema
12+
PredictedTagFilterSchema,
1313
} from "@/schemas";
1414

1515
import type { ClipPrediction, Tag } from "@/types";

front/src/app/(auth)/first/page.tsx

+4-7
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,10 @@ export default function Page() {
2828
[router],
2929
);
3030

31-
const handleAuthenticationError = useCallback(
32-
() => {
33-
toast.error("This is not your first time here, is it?");
34-
router.push("/login");
35-
},
36-
[router],
37-
);
31+
const handleAuthenticationError = useCallback(() => {
32+
toast.error("This is not your first time here, is it?");
33+
router.push("/login");
34+
}, [router]);
3835

3936
return (
4037
<div className="flex flex-col items-center justify-center min-h-screen">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.resizer {
2+
position: absolute;
3+
right: 0;
4+
top: 0;
5+
height: 100%;
6+
width: 5px;
7+
cursor: col-resize;
8+
user-select: none;
9+
touch-action: none;
10+
}
11+
12+
.resizer.isResizing {
13+
opacity: 1;
14+
}
15+
16+
@media (hover: hover) {
17+
.resizer {
18+
opacity: 0;
19+
}
20+
21+
*:hover > .resizer {
22+
opacity: 1;
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
import { notFound } from "next/navigation";
3+
import { useContext } from "react";
4+
5+
import AnnotationProjectTaskClips from "@/components/annotation_projects/AnnotationProjectTaskClips";
6+
import AnnotationProject from "../context";
7+
8+
import type { AnnotationTask } from "@/types";
9+
10+
import "./page.css";
11+
12+
function getAnnotationTaskLink(annotationTask: AnnotationTask): string {
13+
return `detail/annotation/?annotation_task_uuid=${annotationTask.uuid}`;
14+
}
15+
16+
export default function Page() {
17+
const annotationProject = useContext(AnnotationProject);
18+
19+
if (annotationProject == null) {
20+
return notFound();
21+
}
22+
23+
return (
24+
<div className="w-full">
25+
<AnnotationProjectTaskClips
26+
annotationProject={annotationProject}
27+
getAnnotationTaskLink={getAnnotationTaskLink}
28+
/>
29+
</div>
30+
);
31+
}

front/src/app/(base)/annotation_projects/detail/tasks/page.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ export default function Page() {
2323

2424
return (
2525
<Center>
26-
<AnnotationProjectTasks annotationProject={project} onAddTasks={onCreateTasks} />
26+
<AnnotationProjectTasks
27+
annotationProject={project}
28+
onAddTasks={onCreateTasks}
29+
/>
2730
</Center>
2831
);
2932
}

front/src/app/(base)/exploration/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default function Page() {
1111
<div className="flex flex-col gap-4">
1212
<h1 className="text-center text-5xl">Exploration Tools</h1>
1313
<h2 className="text-center text-3xl text-stone-500 dark:text-stone-500">
14-
Utilize the provided tools to delve into your data
14+
Utilize the provided tools to delve into your data
1515
</h2>
1616
</div>
1717
<div className="flex flex-row gap-8 w-full justify-center">

front/src/app/(base)/exploration/recordings/page.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,3 @@ export default function Page() {
2525
/>
2626
);
2727
}
28-

front/src/app/(base)/page.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ export default function Page() {
88
<div className="container mx-auto p-16">
99
<div className="flex flex-col gap-4">
1010
<h1 className="text-center text-7xl">
11-
<span className="text-6xl font-thin">
12-
Welcome to
13-
</span>
11+
<span className="text-6xl font-thin">Welcome to</span>
1412
<br />
1513
<Image
1614
src="/whombat.svg"

front/src/components/annotation_projects/AnnotationProjectHeader.tsx

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66

77
import Header from "@/components/Header";
88
import { H1 } from "@/components/Headings";
9-
import { DatasetIcon, EditIcon, TagsIcon, TasksIcon } from "@/components/icons";
9+
import {
10+
DatasetIcon,
11+
EditIcon,
12+
ClipsIcon,
13+
TagsIcon,
14+
TasksIcon,
15+
} from "@/components/icons";
1016
import Tabs from "@/components/Tabs";
1117

1218
import type { AnnotationProject } from "@/types";
@@ -39,6 +45,17 @@ export default function AnnotationProjectHeader({
3945
);
4046
},
4147
},
48+
{
49+
id: "clips",
50+
title: "Clips",
51+
isActive: selectedLayoutSegment === "clips",
52+
icon: <ClipsIcon className="w-5 h-5 align-middle" />,
53+
onClick: () => {
54+
router.push(
55+
`/annotation_projects/detail/clips/?${params.toString()}`,
56+
);
57+
},
58+
},
4259
{
4360
id: "annotate",
4461
title: "Annotate",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useMemo } from "react";
2+
3+
import AnnotationTaskTable from "@/components/annotation_tasks/AnnotationTaskTable";
4+
5+
import type { AnnotationProject, AnnotationTask } from "@/types";
6+
7+
export default function AnnotationProjectTaskClips({
8+
annotationProject,
9+
getAnnotationTaskLink: getAnnotationTaskLinkFn,
10+
}: {
11+
annotationProject: AnnotationProject;
12+
getAnnotationTaskLink?: (annotationTask: AnnotationTask) => string;
13+
}) {
14+
const getAnnotationTaskLink = useMemo(() => {
15+
if (getAnnotationTaskLinkFn == null) return undefined;
16+
17+
return (annotationTask: AnnotationTask) => {
18+
const url = getAnnotationTaskLinkFn(annotationTask);
19+
return `${url}&annotation_project_uuid=${annotationProject.uuid}`;
20+
};
21+
}, [getAnnotationTaskLinkFn, annotationProject.uuid]);
22+
const filter = useMemo(
23+
() => ({ annotation_project: annotationProject }),
24+
[annotationProject],
25+
);
26+
27+
return (
28+
<AnnotationTaskTable
29+
filter={filter}
30+
fixed={["annotation_project"]}
31+
getAnnotationTaskLink={getAnnotationTaskLink}
32+
// pathFormatter={pathFormatter} TODO: if there was a dataset column, the path could be formatted as in the recordings table
33+
/>
34+
);
35+
}

0 commit comments

Comments
 (0)