endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/management/commands/load_ai_model_data.py +2 -1
- endoreg_db/management/commands/setup_endoreg_db.py +11 -7
- endoreg_db/models/media/pdf/raw_pdf.py +241 -97
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/video_file.py +300 -187
- endoreg_db/models/metadata/model_meta_logic.py +15 -1
- endoreg_db/models/metadata/sensitive_meta_logic.py +391 -70
- endoreg_db/serializers/__init__.py +26 -55
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +65 -35
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/lookup_service.py +228 -58
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +585 -282
- endoreg_db/services/video_import.py +340 -101
- endoreg_db/urls/__init__.py +36 -23
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +3 -2
- endoreg_db/views/__init__.py +6 -3
- endoreg_db/views/media/pdf_media.py +3 -1
- endoreg_db/views/media/video_media.py +1 -1
- endoreg_db/views/media/video_segments.py +187 -259
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +187 -0
- endoreg_db/views/pdf/reimport.py +110 -94
- endoreg_db/views/requirement/lookup.py +171 -287
- endoreg_db/views/video/__init__.py +0 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/METADATA +1 -1
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/RECORD +33 -34
- endoreg_db/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/video/video_media.py +0 -158
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,8 +8,7 @@ Provides RESTful endpoints for video segment management:
|
|
|
8
8
|
- Video-specific: GET/POST /api/media/videos/<pk>/segments/
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
from endoreg_db.serializers.label_video_segment.label_video_segment import LabelVideoSegmentSerializer
|
|
11
|
+
import logging
|
|
13
12
|
|
|
14
13
|
from django.db import transaction
|
|
15
14
|
from django.db.models import Count
|
|
@@ -18,71 +17,65 @@ from rest_framework import status
|
|
|
18
17
|
from rest_framework.decorators import api_view, permission_classes
|
|
19
18
|
from rest_framework.response import Response
|
|
20
19
|
|
|
20
|
+
from endoreg_db.models import Label, LabelVideoSegment, VideoFile
|
|
21
|
+
from endoreg_db.serializers.label_video_segment.label_video_segment import LabelVideoSegmentSerializer
|
|
21
22
|
from endoreg_db.utils.permissions import EnvironmentAwarePermission
|
|
22
23
|
|
|
23
|
-
import logging
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
@api_view([
|
|
27
|
+
@api_view(["GET"])
|
|
28
28
|
@permission_classes([EnvironmentAwarePermission])
|
|
29
29
|
def video_segments_stats(request):
|
|
30
30
|
"""
|
|
31
31
|
Statistics endpoint for video segments.
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
GET /api/media/videos/segments/stats/
|
|
34
34
|
Returns aggregated statistics about video segments.
|
|
35
35
|
"""
|
|
36
36
|
try:
|
|
37
37
|
# Get all segments queryset
|
|
38
38
|
segments = LabelVideoSegment.objects.all()
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
# Calculate statistics
|
|
41
41
|
total_segments = segments.count()
|
|
42
|
-
|
|
43
|
-
# Segments by status (assuming status field exists)
|
|
44
|
-
status_counts = segments.values('status').annotate(count=Count('id'))
|
|
45
|
-
|
|
42
|
+
|
|
46
43
|
# Segments by label
|
|
47
|
-
label_counts = segments.values(
|
|
48
|
-
|
|
44
|
+
label_counts = segments.values("label__name").annotate(count=Count("id"))
|
|
45
|
+
|
|
49
46
|
# Videos with segments
|
|
50
|
-
videos_with_segments = segments.values(
|
|
51
|
-
|
|
47
|
+
videos_with_segments = segments.values("video_file").distinct().count()
|
|
48
|
+
|
|
52
49
|
stats = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'by_label': {item['label__name']: item['count'] for item in label_counts if item['label__name']},
|
|
50
|
+
"total_segments": total_segments,
|
|
51
|
+
"videos_with_segments": videos_with_segments,
|
|
52
|
+
"by_label": {item["label__name"]: item["count"] for item in label_counts if item["label__name"]},
|
|
57
53
|
}
|
|
58
|
-
|
|
54
|
+
|
|
59
55
|
return Response(stats, status=status.HTTP_200_OK)
|
|
60
|
-
|
|
56
|
+
|
|
61
57
|
except Exception as e:
|
|
62
58
|
logger.error(f"Error fetching video segment stats: {e}")
|
|
63
|
-
return Response(
|
|
64
|
-
{'error': 'Failed to fetch segment statistics'},
|
|
65
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
66
|
-
)
|
|
59
|
+
return Response({"error": "Failed to fetch segment statistics"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
67
60
|
|
|
68
61
|
|
|
69
|
-
@api_view([
|
|
62
|
+
@api_view(["GET", "POST"])
|
|
70
63
|
@permission_classes([EnvironmentAwarePermission])
|
|
71
64
|
def video_segments_collection(request):
|
|
72
65
|
"""
|
|
73
66
|
Collection endpoint for all video segments across all videos.
|
|
74
|
-
|
|
67
|
+
|
|
75
68
|
GET /api/media/videos/segments/
|
|
76
69
|
- Lists all segments, optionally filtered by video_id and/or label_id
|
|
77
70
|
- Query params: video_id, label_id
|
|
78
|
-
|
|
71
|
+
|
|
79
72
|
POST /api/media/videos/segments/
|
|
80
73
|
- Creates a new video segment
|
|
81
74
|
- Requires: video_id, label_id, start_frame_number, end_frame_number
|
|
82
|
-
|
|
75
|
+
|
|
83
76
|
Modern replacement for: /api/video-segments/
|
|
84
77
|
"""
|
|
85
|
-
if request.method ==
|
|
78
|
+
if request.method == "POST":
|
|
86
79
|
logger.info(f"Creating new video segment with data: {request.data}")
|
|
87
80
|
|
|
88
81
|
with transaction.atomic():
|
|
@@ -91,27 +84,18 @@ def video_segments_collection(request):
|
|
|
91
84
|
try:
|
|
92
85
|
segment = serializer.save()
|
|
93
86
|
logger.info(f"Successfully created video segment {segment.pk}")
|
|
94
|
-
return Response(
|
|
95
|
-
LabelVideoSegmentSerializer(segment).data,
|
|
96
|
-
status=status.HTTP_201_CREATED
|
|
97
|
-
)
|
|
87
|
+
return Response(LabelVideoSegmentSerializer(segment).data, status=status.HTTP_201_CREATED)
|
|
98
88
|
except Exception as e:
|
|
99
89
|
logger.error(f"Error creating video segment: {str(e)}")
|
|
100
|
-
return Response(
|
|
101
|
-
{'error': f'Failed to create segment: {str(e)}'},
|
|
102
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
103
|
-
)
|
|
90
|
+
return Response({"error": f"Failed to create segment: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
104
91
|
else:
|
|
105
92
|
logger.warning(f"Invalid data for video segment creation: {serializer.errors}")
|
|
106
|
-
return Response(
|
|
107
|
-
{'error': 'Invalid data', 'details': serializer.errors},
|
|
108
|
-
status=status.HTTP_400_BAD_REQUEST
|
|
109
|
-
)
|
|
93
|
+
return Response({"error": "Invalid data", "details": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
|
|
110
94
|
|
|
111
|
-
elif request.method ==
|
|
95
|
+
elif request.method == "GET":
|
|
112
96
|
# Optional filtering by video_id
|
|
113
|
-
video_id = request.GET.get(
|
|
114
|
-
label_id = request.GET.get(
|
|
97
|
+
video_id = request.GET.get("video_id")
|
|
98
|
+
label_id = request.GET.get("label_id")
|
|
115
99
|
|
|
116
100
|
queryset = LabelVideoSegment.objects.all()
|
|
117
101
|
|
|
@@ -120,140 +104,114 @@ def video_segments_collection(request):
|
|
|
120
104
|
video = VideoFile.objects.get(id=video_id)
|
|
121
105
|
queryset = queryset.filter(video_file=video)
|
|
122
106
|
except VideoFile.DoesNotExist:
|
|
123
|
-
return Response(
|
|
124
|
-
{'error': f'Video with id {video_id} not found'},
|
|
125
|
-
status=status.HTTP_404_NOT_FOUND
|
|
126
|
-
)
|
|
107
|
+
return Response({"error": f"Video with id {video_id} not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
127
108
|
|
|
128
109
|
if label_id:
|
|
129
110
|
try:
|
|
130
111
|
label = Label.objects.get(id=label_id)
|
|
131
112
|
queryset = queryset.filter(label=label)
|
|
132
113
|
except Label.DoesNotExist:
|
|
133
|
-
return Response(
|
|
134
|
-
{'error': f'Label with id {label_id} not found'},
|
|
135
|
-
status=status.HTTP_404_NOT_FOUND
|
|
136
|
-
)
|
|
114
|
+
return Response({"error": f"Label with id {label_id} not found"}, status=status.HTTP_404_NOT_FOUND)
|
|
137
115
|
|
|
138
116
|
# Order by video and start time for consistent results
|
|
139
|
-
segments = queryset.order_by(
|
|
117
|
+
segments = queryset.order_by("video_file__id", "start_frame_number")
|
|
140
118
|
serializer = LabelVideoSegmentSerializer(segments, many=True)
|
|
141
119
|
return Response(serializer.data)
|
|
142
120
|
|
|
143
121
|
|
|
144
|
-
@api_view([
|
|
122
|
+
@api_view(["GET", "POST"])
|
|
145
123
|
@permission_classes([EnvironmentAwarePermission])
|
|
146
124
|
def video_segments_by_video(request, pk):
|
|
147
125
|
"""
|
|
148
126
|
Video-specific segments endpoint.
|
|
149
|
-
|
|
127
|
+
|
|
150
128
|
GET /api/media/videos/<pk>/segments/
|
|
151
129
|
- Lists all segments for a specific video
|
|
152
130
|
- Query params: label (label name filter)
|
|
153
131
|
- Note: This was already implemented in segments.py as video_segments_by_pk
|
|
154
|
-
|
|
132
|
+
|
|
155
133
|
POST /api/media/videos/<pk>/segments/
|
|
156
134
|
- Creates a new segment for this video
|
|
157
135
|
- Automatically sets video_id to pk
|
|
158
136
|
- Requires: label_id, start_frame_number, end_frame_number
|
|
159
|
-
|
|
137
|
+
|
|
160
138
|
Modern replacement for: /api/video-segments/?video_id=<pk>
|
|
161
139
|
"""
|
|
162
140
|
# Verify video exists
|
|
163
141
|
video = get_object_or_404(VideoFile, id=pk)
|
|
164
|
-
|
|
165
|
-
if request.method ==
|
|
142
|
+
|
|
143
|
+
if request.method == "GET":
|
|
166
144
|
# This duplicates video_segments_by_pk functionality
|
|
167
145
|
# We keep both for compatibility during migration
|
|
168
|
-
label_name = request.GET.get(
|
|
169
|
-
|
|
146
|
+
label_name = request.GET.get("label")
|
|
147
|
+
|
|
170
148
|
queryset = LabelVideoSegment.objects.filter(video_file=video)
|
|
171
|
-
|
|
149
|
+
|
|
172
150
|
if label_name:
|
|
173
151
|
try:
|
|
174
152
|
label = Label.objects.get(name=label_name)
|
|
175
153
|
queryset = queryset.filter(label=label)
|
|
176
154
|
except Label.DoesNotExist:
|
|
177
|
-
return Response(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
segments = queryset.order_by('start_frame_number')
|
|
155
|
+
return Response({"error": f'Label "{label_name}" not found'}, status=status.HTTP_404_NOT_FOUND)
|
|
156
|
+
|
|
157
|
+
segments = queryset.order_by("start_frame_number")
|
|
183
158
|
serializer = LabelVideoSegmentSerializer(segments, many=True)
|
|
184
159
|
return Response(serializer.data)
|
|
185
|
-
|
|
186
|
-
elif request.method ==
|
|
160
|
+
|
|
161
|
+
elif request.method == "POST":
|
|
187
162
|
logger.info(f"Creating new segment for video {pk} with data: {request.data}")
|
|
188
|
-
|
|
163
|
+
|
|
189
164
|
# Automatically set video_id to pk
|
|
190
165
|
data = request.data.copy()
|
|
191
|
-
data[
|
|
192
|
-
|
|
166
|
+
data["video_id"] = pk
|
|
167
|
+
|
|
193
168
|
with transaction.atomic():
|
|
194
169
|
serializer = LabelVideoSegmentSerializer(data=data)
|
|
195
170
|
if serializer.is_valid():
|
|
196
171
|
try:
|
|
197
172
|
segment = serializer.save()
|
|
198
173
|
logger.info(f"Successfully created segment {segment.pk} for video {pk}")
|
|
199
|
-
return Response(
|
|
200
|
-
LabelVideoSegmentSerializer(segment).data,
|
|
201
|
-
status=status.HTTP_201_CREATED
|
|
202
|
-
)
|
|
174
|
+
return Response(LabelVideoSegmentSerializer(segment).data, status=status.HTTP_201_CREATED)
|
|
203
175
|
except Exception as e:
|
|
204
176
|
logger.error(f"Error creating segment for video {pk}: {str(e)}")
|
|
205
|
-
return Response(
|
|
206
|
-
{'error': f'Failed to create segment: {str(e)}'},
|
|
207
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
208
|
-
)
|
|
177
|
+
return Response({"error": f"Failed to create segment: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
209
178
|
else:
|
|
210
179
|
logger.warning(f"Invalid data for segment creation: {serializer.errors}")
|
|
211
|
-
return Response(
|
|
212
|
-
{'error': 'Invalid data', 'details': serializer.errors},
|
|
213
|
-
status=status.HTTP_400_BAD_REQUEST
|
|
214
|
-
)
|
|
180
|
+
return Response({"error": "Invalid data", "details": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
|
|
215
181
|
|
|
216
182
|
|
|
217
|
-
@api_view([
|
|
183
|
+
@api_view(["GET", "PATCH", "DELETE"])
|
|
218
184
|
@permission_classes([EnvironmentAwarePermission])
|
|
219
185
|
def video_segment_detail(request, pk, segment_id):
|
|
220
186
|
"""
|
|
221
187
|
Detail endpoint for a specific video segment.
|
|
222
|
-
|
|
188
|
+
|
|
223
189
|
GET /api/media/videos/<pk>/segments/<segment_id>/
|
|
224
190
|
- Returns segment details
|
|
225
|
-
|
|
191
|
+
|
|
226
192
|
PATCH /api/media/videos/<pk>/segments/<segment_id>/
|
|
227
193
|
- Updates segment (partial update)
|
|
228
|
-
|
|
194
|
+
|
|
229
195
|
DELETE /api/media/videos/<pk>/segments/<segment_id>/
|
|
230
196
|
- Deletes segment
|
|
231
|
-
|
|
197
|
+
|
|
232
198
|
Modern replacement for: /api/video-segments/<segment_id>/
|
|
233
199
|
"""
|
|
234
200
|
# Verify video exists
|
|
235
201
|
video = get_object_or_404(VideoFile, id=pk)
|
|
236
|
-
|
|
202
|
+
|
|
237
203
|
# Get segment and verify it belongs to this video
|
|
238
|
-
segment = get_object_or_404(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
video_file=video
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
if request.method == 'GET':
|
|
204
|
+
segment = get_object_or_404(LabelVideoSegment, id=segment_id, video_file=video)
|
|
205
|
+
|
|
206
|
+
if request.method == "GET":
|
|
245
207
|
serializer = LabelVideoSegmentSerializer(segment)
|
|
246
208
|
return Response(serializer.data)
|
|
247
|
-
|
|
248
|
-
elif request.method ==
|
|
209
|
+
|
|
210
|
+
elif request.method == "PATCH":
|
|
249
211
|
logger.info(f"Updating segment {segment_id} for video {pk} with data: {request.data}")
|
|
250
|
-
|
|
212
|
+
|
|
251
213
|
with transaction.atomic():
|
|
252
|
-
serializer = LabelVideoSegmentSerializer(
|
|
253
|
-
segment,
|
|
254
|
-
data=request.data,
|
|
255
|
-
partial=True
|
|
256
|
-
)
|
|
214
|
+
serializer = LabelVideoSegmentSerializer(segment, data=request.data, partial=True)
|
|
257
215
|
if serializer.is_valid():
|
|
258
216
|
try:
|
|
259
217
|
segment = serializer.save()
|
|
@@ -261,33 +219,21 @@ def video_segment_detail(request, pk, segment_id):
|
|
|
261
219
|
return Response(LabelVideoSegmentSerializer(segment).data)
|
|
262
220
|
except Exception as e:
|
|
263
221
|
logger.error(f"Error updating segment {segment_id}: {str(e)}")
|
|
264
|
-
return Response(
|
|
265
|
-
{'error': f'Failed to update segment: {str(e)}'},
|
|
266
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
267
|
-
)
|
|
222
|
+
return Response({"error": f"Failed to update segment: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
268
223
|
else:
|
|
269
224
|
logger.warning(f"Invalid data for segment update: {serializer.errors}")
|
|
270
|
-
return Response(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
elif request.method == 'DELETE':
|
|
225
|
+
return Response({"error": "Invalid data", "details": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
|
|
226
|
+
|
|
227
|
+
elif request.method == "DELETE":
|
|
276
228
|
logger.info(f"Deleting segment {segment_id} from video {pk}")
|
|
277
229
|
try:
|
|
278
230
|
with transaction.atomic():
|
|
279
231
|
segment.delete()
|
|
280
232
|
logger.info(f"Successfully deleted segment {segment_id}")
|
|
281
|
-
return Response(
|
|
282
|
-
{'message': f'Segment {segment_id} deleted successfully'},
|
|
283
|
-
status=status.HTTP_204_NO_CONTENT
|
|
284
|
-
)
|
|
233
|
+
return Response({"message": f"Segment {segment_id} deleted successfully"}, status=status.HTTP_204_NO_CONTENT)
|
|
285
234
|
except Exception as e:
|
|
286
235
|
logger.error(f"Error deleting segment {segment_id}: {str(e)}")
|
|
287
|
-
return Response(
|
|
288
|
-
{'error': f'Failed to delete segment: {str(e)}'},
|
|
289
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
290
|
-
)
|
|
236
|
+
return Response({"error": f"Failed to delete segment: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
291
237
|
|
|
292
238
|
|
|
293
239
|
# ============================================================================
|
|
@@ -295,23 +241,24 @@ def video_segment_detail(request, pk, segment_id):
|
|
|
295
241
|
# Migrated from /api/label-video-segment/*/validate/ (October 14, 2025)
|
|
296
242
|
# ============================================================================
|
|
297
243
|
|
|
298
|
-
|
|
244
|
+
|
|
245
|
+
@api_view(["POST"])
|
|
299
246
|
@permission_classes([EnvironmentAwarePermission])
|
|
300
247
|
def video_segment_validate(request, pk: int, segment_id: int):
|
|
301
248
|
"""
|
|
302
249
|
Validate a single video segment.
|
|
303
|
-
|
|
250
|
+
|
|
304
251
|
POST /api/media/videos/<pk>/segments/<segment_id>/validate/
|
|
305
|
-
|
|
252
|
+
|
|
306
253
|
Validates a single LabelVideoSegment and marks it as verified.
|
|
307
254
|
Used to confirm user-reviewed segment annotations.
|
|
308
|
-
|
|
255
|
+
|
|
309
256
|
Request Body (optional):
|
|
310
257
|
{
|
|
311
258
|
"is_validated": true, // optional, default true
|
|
312
259
|
"notes": "..." // optional, validation notes
|
|
313
260
|
}
|
|
314
|
-
|
|
261
|
+
|
|
315
262
|
Response:
|
|
316
263
|
{
|
|
317
264
|
"message": "Segment validated successfully",
|
|
@@ -325,69 +272,64 @@ def video_segment_validate(request, pk: int, segment_id: int):
|
|
|
325
272
|
"""
|
|
326
273
|
# Verify video exists
|
|
327
274
|
video = get_object_or_404(VideoFile, pk=pk)
|
|
328
|
-
|
|
275
|
+
|
|
329
276
|
# Get segment and verify it belongs to this video
|
|
330
|
-
segment = get_object_or_404(
|
|
331
|
-
|
|
332
|
-
pk=segment_id,
|
|
333
|
-
video_file=video
|
|
334
|
-
)
|
|
335
|
-
|
|
277
|
+
segment = get_object_or_404(LabelVideoSegment.objects.select_related("state", "video_file", "label"), pk=segment_id, video_file=video)
|
|
278
|
+
|
|
336
279
|
try:
|
|
337
280
|
# Validation status from request (default: True)
|
|
338
|
-
is_validated = request.data.get(
|
|
339
|
-
notes = request.data.get(
|
|
340
|
-
|
|
281
|
+
is_validated = request.data.get("is_validated", True)
|
|
282
|
+
notes = request.data.get("notes", "")
|
|
283
|
+
|
|
341
284
|
# Get or create state object
|
|
342
|
-
if not hasattr(segment,
|
|
343
|
-
return Response({
|
|
344
|
-
|
|
345
|
-
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
346
|
-
|
|
285
|
+
if not hasattr(segment, "state") or segment.state is None:
|
|
286
|
+
return Response({"error": "Segment has no state object. Cannot validate."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
287
|
+
|
|
347
288
|
# Update state
|
|
348
289
|
with transaction.atomic():
|
|
349
290
|
segment.state.is_validated = is_validated
|
|
350
|
-
if notes and hasattr(segment.state,
|
|
291
|
+
if notes and hasattr(segment.state, "validation_notes"):
|
|
351
292
|
segment.state.validation_notes = notes
|
|
352
293
|
segment.state.save()
|
|
353
|
-
|
|
294
|
+
|
|
354
295
|
logger.info(f"Validated segment {segment_id} in video {pk}: {is_validated}")
|
|
355
|
-
|
|
356
|
-
return Response(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
296
|
+
|
|
297
|
+
return Response(
|
|
298
|
+
{
|
|
299
|
+
"message": f"Segment {segment_id} validation status updated",
|
|
300
|
+
"segment_id": segment_id,
|
|
301
|
+
"is_validated": is_validated,
|
|
302
|
+
"label": segment.label.name if segment.label else None,
|
|
303
|
+
"video_id": video.id,
|
|
304
|
+
"start_frame": segment.start_frame_number,
|
|
305
|
+
"end_frame": segment.end_frame_number,
|
|
306
|
+
},
|
|
307
|
+
status=status.HTTP_200_OK,
|
|
308
|
+
)
|
|
309
|
+
|
|
366
310
|
except Exception as e:
|
|
367
311
|
logger.error(f"Error validating segment {segment_id} in video {pk}: {e}")
|
|
368
|
-
return Response({
|
|
369
|
-
"error": f"Validation failed: {str(e)}"
|
|
370
|
-
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
312
|
+
return Response({"error": f"Validation failed: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
371
313
|
|
|
372
314
|
|
|
373
|
-
@api_view([
|
|
315
|
+
@api_view(["POST"])
|
|
374
316
|
@permission_classes([EnvironmentAwarePermission])
|
|
375
317
|
def video_segments_validate_bulk(request, pk: int):
|
|
376
318
|
"""
|
|
377
319
|
Validate multiple video segments at once.
|
|
378
|
-
|
|
320
|
+
|
|
379
321
|
POST /api/media/videos/<pk>/segments/validate-bulk/
|
|
380
|
-
|
|
322
|
+
|
|
381
323
|
Validates multiple LabelVideoSegments simultaneously.
|
|
382
324
|
Useful for batch validation after review.
|
|
383
|
-
|
|
325
|
+
|
|
384
326
|
Request Body:
|
|
385
327
|
{
|
|
386
328
|
"segment_ids": [1, 2, 3, ...],
|
|
387
329
|
"is_validated": true, // optional, default true
|
|
388
330
|
"notes": "..." // optional, applies to all segments
|
|
389
331
|
}
|
|
390
|
-
|
|
332
|
+
|
|
391
333
|
Response:
|
|
392
334
|
{
|
|
393
335
|
"message": "Bulk validation completed. 3 segments updated.",
|
|
@@ -399,37 +341,30 @@ def video_segments_validate_bulk(request, pk: int):
|
|
|
399
341
|
"""
|
|
400
342
|
# Verify video exists
|
|
401
343
|
video = get_object_or_404(VideoFile, pk=pk)
|
|
402
|
-
|
|
403
|
-
segment_ids = request.data.get(
|
|
404
|
-
is_validated = request.data.get(
|
|
405
|
-
notes = request.data.get(
|
|
406
|
-
|
|
344
|
+
|
|
345
|
+
segment_ids = request.data.get("segment_ids", [])
|
|
346
|
+
is_validated = request.data.get("is_validated", True)
|
|
347
|
+
notes = request.data.get("notes", "")
|
|
348
|
+
|
|
407
349
|
if not segment_ids:
|
|
408
|
-
return Response({
|
|
409
|
-
|
|
410
|
-
}, status=status.HTTP_400_BAD_REQUEST)
|
|
411
|
-
|
|
350
|
+
return Response({"error": "segment_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
351
|
+
|
|
412
352
|
try:
|
|
413
353
|
# Get all segments for this video only
|
|
414
|
-
segments = LabelVideoSegment.objects.filter(
|
|
415
|
-
|
|
416
|
-
video_file=video
|
|
417
|
-
).select_related('state')
|
|
418
|
-
|
|
354
|
+
segments = LabelVideoSegment.objects.filter(pk__in=segment_ids, video_file=video).select_related("state")
|
|
355
|
+
|
|
419
356
|
if not segments.exists():
|
|
420
|
-
return Response({
|
|
421
|
-
|
|
422
|
-
}, status=status.HTTP_404_NOT_FOUND)
|
|
423
|
-
|
|
357
|
+
return Response({"error": "No segments found with provided IDs for this video"}, status=status.HTTP_404_NOT_FOUND)
|
|
358
|
+
|
|
424
359
|
updated_count = 0
|
|
425
360
|
failed_ids = []
|
|
426
|
-
|
|
361
|
+
|
|
427
362
|
with transaction.atomic():
|
|
428
363
|
for segment in segments:
|
|
429
364
|
try:
|
|
430
365
|
if segment.state:
|
|
431
366
|
segment.state.is_validated = is_validated
|
|
432
|
-
if notes and hasattr(segment.state,
|
|
367
|
+
if notes and hasattr(segment.state, "validation_notes"):
|
|
433
368
|
segment.state.validation_notes = notes
|
|
434
369
|
segment.state.save()
|
|
435
370
|
updated_count += 1
|
|
@@ -438,51 +373,49 @@ def video_segments_validate_bulk(request, pk: int):
|
|
|
438
373
|
except Exception as e:
|
|
439
374
|
logger.error(f"Error validating segment {segment.id}: {e}")
|
|
440
375
|
failed_ids.append(segment.id)
|
|
441
|
-
|
|
376
|
+
|
|
442
377
|
logger.info(f"Bulk validated {updated_count} segments in video {pk}")
|
|
443
|
-
|
|
378
|
+
|
|
444
379
|
response_data = {
|
|
445
380
|
"message": f"Bulk validation completed. {updated_count} segments updated.",
|
|
446
381
|
"updated_count": updated_count,
|
|
447
382
|
"requested_count": len(segment_ids),
|
|
448
383
|
"is_validated": is_validated,
|
|
449
|
-
"video_id": pk
|
|
384
|
+
"video_id": pk,
|
|
450
385
|
}
|
|
451
|
-
|
|
386
|
+
|
|
452
387
|
if failed_ids:
|
|
453
388
|
response_data["failed_ids"] = failed_ids
|
|
454
389
|
response_data["warning"] = f"{len(failed_ids)} segments could not be validated"
|
|
455
|
-
|
|
390
|
+
|
|
456
391
|
return Response(response_data, status=status.HTTP_200_OK)
|
|
457
|
-
|
|
392
|
+
|
|
458
393
|
except Exception as e:
|
|
459
394
|
logger.error(f"Error in bulk validation for video {pk}: {e}")
|
|
460
|
-
return Response({
|
|
461
|
-
"error": f"Bulk validation failed: {str(e)}"
|
|
462
|
-
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
395
|
+
return Response({"error": f"Bulk validation failed: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
463
396
|
|
|
464
397
|
|
|
465
|
-
@api_view([
|
|
398
|
+
@api_view(["GET", "POST"])
|
|
466
399
|
@permission_classes([EnvironmentAwarePermission])
|
|
467
400
|
def video_segments_validation_status(request, pk: int):
|
|
468
401
|
"""
|
|
469
402
|
Get or update validation status for all segments of a video.
|
|
470
|
-
|
|
403
|
+
|
|
471
404
|
GET /api/media/videos/<pk>/segments/validation-status/
|
|
472
405
|
Returns validation statistics for all segments.
|
|
473
|
-
|
|
406
|
+
|
|
474
407
|
POST /api/media/videos/<pk>/segments/validation-status/
|
|
475
408
|
Marks all segments (or filtered by label) as validated.
|
|
476
|
-
|
|
409
|
+
|
|
477
410
|
Query Parameters (GET):
|
|
478
411
|
- label_name: filter by label (optional)
|
|
479
|
-
|
|
412
|
+
|
|
480
413
|
Request Body (POST, optional):
|
|
481
414
|
{
|
|
482
415
|
"label_name": "...", // optional, only validate segments with this label
|
|
483
416
|
"notes": "..." // optional
|
|
484
417
|
}
|
|
485
|
-
|
|
418
|
+
|
|
486
419
|
Response (GET):
|
|
487
420
|
{
|
|
488
421
|
"video_id": 123,
|
|
@@ -492,7 +425,7 @@ def video_segments_validation_status(request, pk: int):
|
|
|
492
425
|
"validation_complete": false,
|
|
493
426
|
"by_label": {...}
|
|
494
427
|
}
|
|
495
|
-
|
|
428
|
+
|
|
496
429
|
Response (POST):
|
|
497
430
|
{
|
|
498
431
|
"message": "Video segment validation completed",
|
|
@@ -504,77 +437,69 @@ def video_segments_validation_status(request, pk: int):
|
|
|
504
437
|
"""
|
|
505
438
|
# Verify video exists
|
|
506
439
|
video = get_object_or_404(VideoFile, pk=pk)
|
|
507
|
-
|
|
508
|
-
if request.method ==
|
|
440
|
+
|
|
441
|
+
if request.method == "GET":
|
|
509
442
|
# Get validation status
|
|
510
|
-
label_name = request.query_params.get(
|
|
511
|
-
|
|
512
|
-
segments_query = LabelVideoSegment.objects.filter(
|
|
513
|
-
|
|
514
|
-
).select_related('state', 'label')
|
|
515
|
-
|
|
443
|
+
label_name = request.query_params.get("label_name")
|
|
444
|
+
|
|
445
|
+
segments_query = LabelVideoSegment.objects.filter(video_file=video).select_related("state", "label")
|
|
446
|
+
|
|
516
447
|
if label_name:
|
|
517
448
|
segments_query = segments_query.filter(label__name=label_name)
|
|
518
|
-
|
|
449
|
+
|
|
519
450
|
segments = segments_query.all()
|
|
520
451
|
total_count = segments.count()
|
|
521
|
-
|
|
452
|
+
|
|
522
453
|
# Count validated segments
|
|
523
|
-
validated_count = sum(
|
|
524
|
-
|
|
525
|
-
if s.state and s.state.is_validated
|
|
526
|
-
)
|
|
527
|
-
|
|
454
|
+
validated_count = sum(1 for s in segments if s.state and s.state.is_validated)
|
|
455
|
+
|
|
528
456
|
# By label breakdown
|
|
529
457
|
by_label = {}
|
|
530
458
|
for segment in segments:
|
|
531
|
-
label = segment.label.name if segment.label else
|
|
459
|
+
label = segment.label.name if segment.label else "unknown"
|
|
532
460
|
if label not in by_label:
|
|
533
|
-
by_label[label] = {
|
|
534
|
-
by_label[label][
|
|
461
|
+
by_label[label] = {"total": 0, "validated": 0}
|
|
462
|
+
by_label[label]["total"] += 1
|
|
535
463
|
if segment.state and segment.state.is_validated:
|
|
536
|
-
by_label[label][
|
|
537
|
-
|
|
538
|
-
return Response(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
464
|
+
by_label[label]["validated"] += 1
|
|
465
|
+
|
|
466
|
+
return Response(
|
|
467
|
+
{
|
|
468
|
+
"video_id": pk,
|
|
469
|
+
"total_segments": total_count,
|
|
470
|
+
"validated_count": validated_count,
|
|
471
|
+
"unvalidated_count": total_count - validated_count,
|
|
472
|
+
"validation_complete": validated_count == total_count and total_count > 0,
|
|
473
|
+
"by_label": by_label,
|
|
474
|
+
"label_filter": label_name,
|
|
475
|
+
},
|
|
476
|
+
status=status.HTTP_200_OK,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
elif request.method == "POST":
|
|
549
480
|
# Mark all segments as validated
|
|
550
|
-
label_name = request.data.get(
|
|
551
|
-
notes = request.data.get(
|
|
552
|
-
|
|
553
|
-
segments_query = LabelVideoSegment.objects.filter(
|
|
554
|
-
|
|
555
|
-
).select_related('state', 'label')
|
|
556
|
-
|
|
481
|
+
label_name = request.data.get("label_name")
|
|
482
|
+
notes = request.data.get("notes", "")
|
|
483
|
+
|
|
484
|
+
segments_query = LabelVideoSegment.objects.filter(video_file=video).select_related("state", "label")
|
|
485
|
+
|
|
557
486
|
if label_name:
|
|
558
487
|
segments_query = segments_query.filter(label__name=label_name)
|
|
559
|
-
|
|
488
|
+
|
|
560
489
|
segments = segments_query.all()
|
|
561
|
-
|
|
490
|
+
|
|
562
491
|
if not segments.exists():
|
|
563
|
-
return Response({
|
|
564
|
-
|
|
565
|
-
"video_id": pk,
|
|
566
|
-
"updated_count": 0
|
|
567
|
-
}, status=status.HTTP_200_OK)
|
|
568
|
-
|
|
492
|
+
return Response({"message": "No segments found to validate", "video_id": pk, "updated_count": 0}, status=status.HTTP_200_OK)
|
|
493
|
+
|
|
569
494
|
updated_count = 0
|
|
570
495
|
failed_count = 0
|
|
571
|
-
|
|
496
|
+
|
|
572
497
|
with transaction.atomic():
|
|
573
498
|
for segment in segments:
|
|
574
499
|
try:
|
|
575
500
|
if segment.state:
|
|
576
501
|
segment.state.is_validated = True
|
|
577
|
-
if notes and hasattr(segment.state,
|
|
502
|
+
if notes and hasattr(segment.state, "validation_notes"):
|
|
578
503
|
segment.state.validation_notes = notes
|
|
579
504
|
segment.state.save()
|
|
580
505
|
updated_count += 1
|
|
@@ -583,14 +508,17 @@ def video_segments_validation_status(request, pk: int):
|
|
|
583
508
|
except Exception as e:
|
|
584
509
|
logger.error(f"Error validating segment {segment.id}: {e}")
|
|
585
510
|
failed_count += 1
|
|
586
|
-
|
|
511
|
+
|
|
587
512
|
logger.info(f"Completed validation for {updated_count} segments in video {pk}")
|
|
588
|
-
|
|
589
|
-
return Response(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
513
|
+
|
|
514
|
+
return Response(
|
|
515
|
+
{
|
|
516
|
+
"message": f"Video segment validation completed for video {pk}",
|
|
517
|
+
"video_id": pk,
|
|
518
|
+
"total_segments": len(segments),
|
|
519
|
+
"updated_count": updated_count,
|
|
520
|
+
"failed_count": failed_count,
|
|
521
|
+
"label_filter": label_name,
|
|
522
|
+
},
|
|
523
|
+
status=status.HTTP_200_OK,
|
|
524
|
+
)
|