endoreg-db 0.8.1__py3-none-any.whl → 0.8.2.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/helpers/download_segmentation_model.py +31 -0
- endoreg_db/migrations/0003_add_center_display_name.py +30 -0
- endoreg_db/models/administration/center/center.py +7 -1
- endoreg_db/models/media/pdf/raw_pdf.py +31 -26
- endoreg_db/models/media/video/create_from_file.py +26 -4
- endoreg_db/models/media/video/pipe_1.py +13 -1
- endoreg_db/models/media/video/video_file.py +36 -13
- endoreg_db/models/media/video/video_file_anonymize.py +2 -1
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +12 -0
- endoreg_db/models/media/video/video_file_io.py +4 -2
- endoreg_db/models/metadata/video_meta.py +2 -2
- endoreg_db/serializers/anonymization.py +3 -0
- endoreg_db/services/pdf_import.py +131 -45
- endoreg_db/services/video_import.py +427 -128
- endoreg_db/urls/__init__.py +0 -2
- endoreg_db/urls/media.py +201 -4
- endoreg_db/urls/report.py +0 -30
- endoreg_db/urls/sensitive_meta.py +0 -36
- endoreg_db/urls/video.py +30 -88
- endoreg_db/utils/paths.py +2 -10
- endoreg_db/utils/video/ffmpeg_wrapper.py +67 -4
- endoreg_db/views/anonymization/validate.py +76 -32
- endoreg_db/views/media/__init__.py +38 -2
- endoreg_db/views/media/pdf_media.py +1 -1
- endoreg_db/views/media/segments.py +71 -0
- endoreg_db/views/media/sensitive_metadata.py +314 -0
- endoreg_db/views/media/video_segments.py +596 -0
- endoreg_db/views/pdf/reimport.py +18 -8
- endoreg_db/views/video/__init__.py +0 -8
- endoreg_db/views/video/correction.py +34 -32
- endoreg_db/views/video/reimport.py +15 -12
- endoreg_db/views/video/video_stream.py +168 -50
- {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/METADATA +2 -2
- {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/RECORD +47 -43
- endoreg_db/views/video/media/__init__.py +0 -23
- /endoreg_db/{urls/pdf.py → config/__init__.py} +0 -0
- /endoreg_db/views/video/{media/task_status.py → task_status.py} +0 -0
- /endoreg_db/views/video/{media/video_analyze.py → video_analyze.py} +0 -0
- /endoreg_db/views/video/{media/video_apply_mask.py → video_apply_mask.py} +0 -0
- /endoreg_db/views/video/{media/video_correction.py → video_correction.py} +0 -0
- /endoreg_db/views/video/{media/video_download_processed.py → video_download_processed.py} +0 -0
- /endoreg_db/views/video/{media/video_media.py → video_media.py} +0 -0
- /endoreg_db/views/video/{media/video_meta.py → video_meta.py} +0 -0
- /endoreg_db/views/video/{media/video_processing_history.py → video_processing_history.py} +0 -0
- /endoreg_db/views/video/{media/video_remove_frames.py → video_remove_frames.py} +0 -0
- /endoreg_db/views/video/{media/video_reprocess.py → video_reprocess.py} +0 -0
- {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modern Media Framework - Video Segments Views
|
|
3
|
+
Migrated from legacy label_video_segment views (October 14, 2025)
|
|
4
|
+
|
|
5
|
+
Provides RESTful endpoints for video segment management:
|
|
6
|
+
- Collection: GET/POST /api/media/videos/segments/
|
|
7
|
+
- Detail: GET/PATCH/DELETE /api/media/videos/<pk>/segments/<segment_id>/
|
|
8
|
+
- Video-specific: GET/POST /api/media/videos/<pk>/segments/
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from endoreg_db.models import Label, LabelVideoSegment, VideoFile
|
|
12
|
+
from endoreg_db.serializers.label_video_segment.label_video_segment import LabelVideoSegmentSerializer
|
|
13
|
+
|
|
14
|
+
from django.db import transaction
|
|
15
|
+
from django.db.models import Count
|
|
16
|
+
from django.shortcuts import get_object_or_404
|
|
17
|
+
from rest_framework import status
|
|
18
|
+
from rest_framework.decorators import api_view, permission_classes
|
|
19
|
+
from rest_framework.response import Response
|
|
20
|
+
|
|
21
|
+
from endoreg_db.utils.permissions import EnvironmentAwarePermission
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@api_view(['GET'])
|
|
28
|
+
@permission_classes([EnvironmentAwarePermission])
|
|
29
|
+
def video_segments_stats(request):
|
|
30
|
+
"""
|
|
31
|
+
Statistics endpoint for video segments.
|
|
32
|
+
|
|
33
|
+
GET /api/media/videos/segments/stats/
|
|
34
|
+
Returns aggregated statistics about video segments.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
# Get all segments queryset
|
|
38
|
+
segments = LabelVideoSegment.objects.all()
|
|
39
|
+
|
|
40
|
+
# Calculate statistics
|
|
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
|
+
|
|
46
|
+
# Segments by label
|
|
47
|
+
label_counts = segments.values('label__name').annotate(count=Count('id'))
|
|
48
|
+
|
|
49
|
+
# Videos with segments
|
|
50
|
+
videos_with_segments = segments.values('video_id').distinct().count()
|
|
51
|
+
|
|
52
|
+
stats = {
|
|
53
|
+
'total_segments': total_segments,
|
|
54
|
+
'videos_with_segments': videos_with_segments,
|
|
55
|
+
'by_status': {item['status']: item['count'] for item in status_counts if item['status']},
|
|
56
|
+
'by_label': {item['label__name']: item['count'] for item in label_counts if item['label__name']},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Response(stats, status=status.HTTP_200_OK)
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
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
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@api_view(['GET', 'POST'])
|
|
70
|
+
@permission_classes([EnvironmentAwarePermission])
|
|
71
|
+
def video_segments_collection(request):
|
|
72
|
+
"""
|
|
73
|
+
Collection endpoint for all video segments across all videos.
|
|
74
|
+
|
|
75
|
+
GET /api/media/videos/segments/
|
|
76
|
+
- Lists all segments, optionally filtered by video_id and/or label_id
|
|
77
|
+
- Query params: video_id, label_id
|
|
78
|
+
|
|
79
|
+
POST /api/media/videos/segments/
|
|
80
|
+
- Creates a new video segment
|
|
81
|
+
- Requires: video_id, label_id, start_frame_number, end_frame_number
|
|
82
|
+
|
|
83
|
+
Modern replacement for: /api/video-segments/
|
|
84
|
+
"""
|
|
85
|
+
if request.method == 'POST':
|
|
86
|
+
logger.info(f"Creating new video segment with data: {request.data}")
|
|
87
|
+
|
|
88
|
+
with transaction.atomic():
|
|
89
|
+
serializer = LabelVideoSegmentSerializer(data=request.data)
|
|
90
|
+
if serializer.is_valid():
|
|
91
|
+
try:
|
|
92
|
+
segment = serializer.save()
|
|
93
|
+
logger.info(f"Successfully created video segment {segment.pk}")
|
|
94
|
+
return Response(
|
|
95
|
+
LabelVideoSegmentSerializer(segment).data,
|
|
96
|
+
status=status.HTTP_201_CREATED
|
|
97
|
+
)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
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
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
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
|
+
)
|
|
110
|
+
|
|
111
|
+
elif request.method == 'GET':
|
|
112
|
+
# Optional filtering by video_id
|
|
113
|
+
video_id = request.GET.get('video_id')
|
|
114
|
+
label_id = request.GET.get('label_id')
|
|
115
|
+
|
|
116
|
+
queryset = LabelVideoSegment.objects.all()
|
|
117
|
+
|
|
118
|
+
if video_id:
|
|
119
|
+
try:
|
|
120
|
+
video = VideoFile.objects.get(id=video_id)
|
|
121
|
+
queryset = queryset.filter(video_file=video)
|
|
122
|
+
except VideoFile.DoesNotExist:
|
|
123
|
+
return Response(
|
|
124
|
+
{'error': f'Video with id {video_id} not found'},
|
|
125
|
+
status=status.HTTP_404_NOT_FOUND
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if label_id:
|
|
129
|
+
try:
|
|
130
|
+
label = Label.objects.get(id=label_id)
|
|
131
|
+
queryset = queryset.filter(label=label)
|
|
132
|
+
except Label.DoesNotExist:
|
|
133
|
+
return Response(
|
|
134
|
+
{'error': f'Label with id {label_id} not found'},
|
|
135
|
+
status=status.HTTP_404_NOT_FOUND
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Order by video and start time for consistent results
|
|
139
|
+
segments = queryset.order_by('video_file__id', 'start_frame_number')
|
|
140
|
+
serializer = LabelVideoSegmentSerializer(segments, many=True)
|
|
141
|
+
return Response(serializer.data)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@api_view(['GET', 'POST'])
|
|
145
|
+
@permission_classes([EnvironmentAwarePermission])
|
|
146
|
+
def video_segments_by_video(request, pk):
|
|
147
|
+
"""
|
|
148
|
+
Video-specific segments endpoint.
|
|
149
|
+
|
|
150
|
+
GET /api/media/videos/<pk>/segments/
|
|
151
|
+
- Lists all segments for a specific video
|
|
152
|
+
- Query params: label (label name filter)
|
|
153
|
+
- Note: This was already implemented in segments.py as video_segments_by_pk
|
|
154
|
+
|
|
155
|
+
POST /api/media/videos/<pk>/segments/
|
|
156
|
+
- Creates a new segment for this video
|
|
157
|
+
- Automatically sets video_id to pk
|
|
158
|
+
- Requires: label_id, start_frame_number, end_frame_number
|
|
159
|
+
|
|
160
|
+
Modern replacement for: /api/video-segments/?video_id=<pk>
|
|
161
|
+
"""
|
|
162
|
+
# Verify video exists
|
|
163
|
+
video = get_object_or_404(VideoFile, id=pk)
|
|
164
|
+
|
|
165
|
+
if request.method == 'GET':
|
|
166
|
+
# This duplicates video_segments_by_pk functionality
|
|
167
|
+
# We keep both for compatibility during migration
|
|
168
|
+
label_name = request.GET.get('label')
|
|
169
|
+
|
|
170
|
+
queryset = LabelVideoSegment.objects.filter(video_file=video)
|
|
171
|
+
|
|
172
|
+
if label_name:
|
|
173
|
+
try:
|
|
174
|
+
label = Label.objects.get(name=label_name)
|
|
175
|
+
queryset = queryset.filter(label=label)
|
|
176
|
+
except Label.DoesNotExist:
|
|
177
|
+
return Response(
|
|
178
|
+
{'error': f'Label "{label_name}" not found'},
|
|
179
|
+
status=status.HTTP_404_NOT_FOUND
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
segments = queryset.order_by('start_frame_number')
|
|
183
|
+
serializer = LabelVideoSegmentSerializer(segments, many=True)
|
|
184
|
+
return Response(serializer.data)
|
|
185
|
+
|
|
186
|
+
elif request.method == 'POST':
|
|
187
|
+
logger.info(f"Creating new segment for video {pk} with data: {request.data}")
|
|
188
|
+
|
|
189
|
+
# Automatically set video_id to pk
|
|
190
|
+
data = request.data.copy()
|
|
191
|
+
data['video_id'] = pk
|
|
192
|
+
|
|
193
|
+
with transaction.atomic():
|
|
194
|
+
serializer = LabelVideoSegmentSerializer(data=data)
|
|
195
|
+
if serializer.is_valid():
|
|
196
|
+
try:
|
|
197
|
+
segment = serializer.save()
|
|
198
|
+
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
|
+
)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
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
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
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
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@api_view(['GET', 'PATCH', 'DELETE'])
|
|
218
|
+
@permission_classes([EnvironmentAwarePermission])
|
|
219
|
+
def video_segment_detail(request, pk, segment_id):
|
|
220
|
+
"""
|
|
221
|
+
Detail endpoint for a specific video segment.
|
|
222
|
+
|
|
223
|
+
GET /api/media/videos/<pk>/segments/<segment_id>/
|
|
224
|
+
- Returns segment details
|
|
225
|
+
|
|
226
|
+
PATCH /api/media/videos/<pk>/segments/<segment_id>/
|
|
227
|
+
- Updates segment (partial update)
|
|
228
|
+
|
|
229
|
+
DELETE /api/media/videos/<pk>/segments/<segment_id>/
|
|
230
|
+
- Deletes segment
|
|
231
|
+
|
|
232
|
+
Modern replacement for: /api/video-segments/<segment_id>/
|
|
233
|
+
"""
|
|
234
|
+
# Verify video exists
|
|
235
|
+
video = get_object_or_404(VideoFile, id=pk)
|
|
236
|
+
|
|
237
|
+
# Get segment and verify it belongs to this video
|
|
238
|
+
segment = get_object_or_404(
|
|
239
|
+
LabelVideoSegment,
|
|
240
|
+
id=segment_id,
|
|
241
|
+
video_file=video
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if request.method == 'GET':
|
|
245
|
+
serializer = LabelVideoSegmentSerializer(segment)
|
|
246
|
+
return Response(serializer.data)
|
|
247
|
+
|
|
248
|
+
elif request.method == 'PATCH':
|
|
249
|
+
logger.info(f"Updating segment {segment_id} for video {pk} with data: {request.data}")
|
|
250
|
+
|
|
251
|
+
with transaction.atomic():
|
|
252
|
+
serializer = LabelVideoSegmentSerializer(
|
|
253
|
+
segment,
|
|
254
|
+
data=request.data,
|
|
255
|
+
partial=True
|
|
256
|
+
)
|
|
257
|
+
if serializer.is_valid():
|
|
258
|
+
try:
|
|
259
|
+
segment = serializer.save()
|
|
260
|
+
logger.info(f"Successfully updated segment {segment_id}")
|
|
261
|
+
return Response(LabelVideoSegmentSerializer(segment).data)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
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
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
logger.warning(f"Invalid data for segment update: {serializer.errors}")
|
|
270
|
+
return Response(
|
|
271
|
+
{'error': 'Invalid data', 'details': serializer.errors},
|
|
272
|
+
status=status.HTTP_400_BAD_REQUEST
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
elif request.method == 'DELETE':
|
|
276
|
+
logger.info(f"Deleting segment {segment_id} from video {pk}")
|
|
277
|
+
try:
|
|
278
|
+
with transaction.atomic():
|
|
279
|
+
segment.delete()
|
|
280
|
+
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
|
+
)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
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
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ============================================================================
|
|
294
|
+
# VIDEO SEGMENT VALIDATION ENDPOINTS (Modern Framework)
|
|
295
|
+
# Migrated from /api/label-video-segment/*/validate/ (October 14, 2025)
|
|
296
|
+
# ============================================================================
|
|
297
|
+
|
|
298
|
+
@api_view(['POST'])
|
|
299
|
+
@permission_classes([EnvironmentAwarePermission])
|
|
300
|
+
def video_segment_validate(request, pk: int, segment_id: int):
|
|
301
|
+
"""
|
|
302
|
+
Validate a single video segment.
|
|
303
|
+
|
|
304
|
+
POST /api/media/videos/<pk>/segments/<segment_id>/validate/
|
|
305
|
+
|
|
306
|
+
Validates a single LabelVideoSegment and marks it as verified.
|
|
307
|
+
Used to confirm user-reviewed segment annotations.
|
|
308
|
+
|
|
309
|
+
Request Body (optional):
|
|
310
|
+
{
|
|
311
|
+
"is_validated": true, // optional, default true
|
|
312
|
+
"notes": "..." // optional, validation notes
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
Response:
|
|
316
|
+
{
|
|
317
|
+
"message": "Segment validated successfully",
|
|
318
|
+
"segment_id": 123,
|
|
319
|
+
"is_validated": true,
|
|
320
|
+
"label": "polyp",
|
|
321
|
+
"video_id": 456,
|
|
322
|
+
"start_frame": 100,
|
|
323
|
+
"end_frame": 200
|
|
324
|
+
}
|
|
325
|
+
"""
|
|
326
|
+
# Verify video exists
|
|
327
|
+
video = get_object_or_404(VideoFile, pk=pk)
|
|
328
|
+
|
|
329
|
+
# Get segment and verify it belongs to this video
|
|
330
|
+
segment = get_object_or_404(
|
|
331
|
+
LabelVideoSegment.objects.select_related('state', 'video_file', 'label'),
|
|
332
|
+
pk=segment_id,
|
|
333
|
+
video_file=video
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
# Validation status from request (default: True)
|
|
338
|
+
is_validated = request.data.get('is_validated', True)
|
|
339
|
+
notes = request.data.get('notes', '')
|
|
340
|
+
|
|
341
|
+
# Get or create state object
|
|
342
|
+
if not hasattr(segment, 'state') or segment.state is None:
|
|
343
|
+
return Response({
|
|
344
|
+
"error": "Segment has no state object. Cannot validate."
|
|
345
|
+
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
346
|
+
|
|
347
|
+
# Update state
|
|
348
|
+
with transaction.atomic():
|
|
349
|
+
segment.state.is_validated = is_validated
|
|
350
|
+
if notes and hasattr(segment.state, 'validation_notes'):
|
|
351
|
+
segment.state.validation_notes = notes
|
|
352
|
+
segment.state.save()
|
|
353
|
+
|
|
354
|
+
logger.info(f"Validated segment {segment_id} in video {pk}: {is_validated}")
|
|
355
|
+
|
|
356
|
+
return Response({
|
|
357
|
+
"message": f"Segment {segment_id} validation status updated",
|
|
358
|
+
"segment_id": segment_id,
|
|
359
|
+
"is_validated": is_validated,
|
|
360
|
+
"label": segment.label.name if segment.label else None,
|
|
361
|
+
"video_id": video.id,
|
|
362
|
+
"start_frame": segment.start_frame_number,
|
|
363
|
+
"end_frame": segment.end_frame_number
|
|
364
|
+
}, status=status.HTTP_200_OK)
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
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)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@api_view(['POST'])
|
|
374
|
+
@permission_classes([EnvironmentAwarePermission])
|
|
375
|
+
def video_segments_validate_bulk(request, pk: int):
|
|
376
|
+
"""
|
|
377
|
+
Validate multiple video segments at once.
|
|
378
|
+
|
|
379
|
+
POST /api/media/videos/<pk>/segments/validate-bulk/
|
|
380
|
+
|
|
381
|
+
Validates multiple LabelVideoSegments simultaneously.
|
|
382
|
+
Useful for batch validation after review.
|
|
383
|
+
|
|
384
|
+
Request Body:
|
|
385
|
+
{
|
|
386
|
+
"segment_ids": [1, 2, 3, ...],
|
|
387
|
+
"is_validated": true, // optional, default true
|
|
388
|
+
"notes": "..." // optional, applies to all segments
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
Response:
|
|
392
|
+
{
|
|
393
|
+
"message": "Bulk validation completed. 3 segments updated.",
|
|
394
|
+
"updated_count": 3,
|
|
395
|
+
"requested_count": 3,
|
|
396
|
+
"is_validated": true,
|
|
397
|
+
"failed_ids": [] // only present if some failed
|
|
398
|
+
}
|
|
399
|
+
"""
|
|
400
|
+
# Verify video exists
|
|
401
|
+
video = get_object_or_404(VideoFile, pk=pk)
|
|
402
|
+
|
|
403
|
+
segment_ids = request.data.get('segment_ids', [])
|
|
404
|
+
is_validated = request.data.get('is_validated', True)
|
|
405
|
+
notes = request.data.get('notes', '')
|
|
406
|
+
|
|
407
|
+
if not segment_ids:
|
|
408
|
+
return Response({
|
|
409
|
+
"error": "segment_ids is required"
|
|
410
|
+
}, status=status.HTTP_400_BAD_REQUEST)
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
# Get all segments for this video only
|
|
414
|
+
segments = LabelVideoSegment.objects.filter(
|
|
415
|
+
pk__in=segment_ids,
|
|
416
|
+
video_file=video
|
|
417
|
+
).select_related('state')
|
|
418
|
+
|
|
419
|
+
if not segments.exists():
|
|
420
|
+
return Response({
|
|
421
|
+
"error": "No segments found with provided IDs for this video"
|
|
422
|
+
}, status=status.HTTP_404_NOT_FOUND)
|
|
423
|
+
|
|
424
|
+
updated_count = 0
|
|
425
|
+
failed_ids = []
|
|
426
|
+
|
|
427
|
+
with transaction.atomic():
|
|
428
|
+
for segment in segments:
|
|
429
|
+
try:
|
|
430
|
+
if segment.state:
|
|
431
|
+
segment.state.is_validated = is_validated
|
|
432
|
+
if notes and hasattr(segment.state, 'validation_notes'):
|
|
433
|
+
segment.state.validation_notes = notes
|
|
434
|
+
segment.state.save()
|
|
435
|
+
updated_count += 1
|
|
436
|
+
else:
|
|
437
|
+
failed_ids.append(segment.id)
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error(f"Error validating segment {segment.id}: {e}")
|
|
440
|
+
failed_ids.append(segment.id)
|
|
441
|
+
|
|
442
|
+
logger.info(f"Bulk validated {updated_count} segments in video {pk}")
|
|
443
|
+
|
|
444
|
+
response_data = {
|
|
445
|
+
"message": f"Bulk validation completed. {updated_count} segments updated.",
|
|
446
|
+
"updated_count": updated_count,
|
|
447
|
+
"requested_count": len(segment_ids),
|
|
448
|
+
"is_validated": is_validated,
|
|
449
|
+
"video_id": pk
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if failed_ids:
|
|
453
|
+
response_data["failed_ids"] = failed_ids
|
|
454
|
+
response_data["warning"] = f"{len(failed_ids)} segments could not be validated"
|
|
455
|
+
|
|
456
|
+
return Response(response_data, status=status.HTTP_200_OK)
|
|
457
|
+
|
|
458
|
+
except Exception as e:
|
|
459
|
+
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)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@api_view(['GET', 'POST'])
|
|
466
|
+
@permission_classes([EnvironmentAwarePermission])
|
|
467
|
+
def video_segments_validation_status(request, pk: int):
|
|
468
|
+
"""
|
|
469
|
+
Get or update validation status for all segments of a video.
|
|
470
|
+
|
|
471
|
+
GET /api/media/videos/<pk>/segments/validation-status/
|
|
472
|
+
Returns validation statistics for all segments.
|
|
473
|
+
|
|
474
|
+
POST /api/media/videos/<pk>/segments/validation-status/
|
|
475
|
+
Marks all segments (or filtered by label) as validated.
|
|
476
|
+
|
|
477
|
+
Query Parameters (GET):
|
|
478
|
+
- label_name: filter by label (optional)
|
|
479
|
+
|
|
480
|
+
Request Body (POST, optional):
|
|
481
|
+
{
|
|
482
|
+
"label_name": "...", // optional, only validate segments with this label
|
|
483
|
+
"notes": "..." // optional
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
Response (GET):
|
|
487
|
+
{
|
|
488
|
+
"video_id": 123,
|
|
489
|
+
"total_segments": 10,
|
|
490
|
+
"validated_count": 7,
|
|
491
|
+
"unvalidated_count": 3,
|
|
492
|
+
"validation_complete": false,
|
|
493
|
+
"by_label": {...}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
Response (POST):
|
|
497
|
+
{
|
|
498
|
+
"message": "Video segment validation completed",
|
|
499
|
+
"video_id": 123,
|
|
500
|
+
"total_segments": 10,
|
|
501
|
+
"updated_count": 10,
|
|
502
|
+
"failed_count": 0
|
|
503
|
+
}
|
|
504
|
+
"""
|
|
505
|
+
# Verify video exists
|
|
506
|
+
video = get_object_or_404(VideoFile, pk=pk)
|
|
507
|
+
|
|
508
|
+
if request.method == 'GET':
|
|
509
|
+
# Get validation status
|
|
510
|
+
label_name = request.query_params.get('label_name')
|
|
511
|
+
|
|
512
|
+
segments_query = LabelVideoSegment.objects.filter(
|
|
513
|
+
video_file=video
|
|
514
|
+
).select_related('state', 'label')
|
|
515
|
+
|
|
516
|
+
if label_name:
|
|
517
|
+
segments_query = segments_query.filter(label__name=label_name)
|
|
518
|
+
|
|
519
|
+
segments = segments_query.all()
|
|
520
|
+
total_count = segments.count()
|
|
521
|
+
|
|
522
|
+
# Count validated segments
|
|
523
|
+
validated_count = sum(
|
|
524
|
+
1 for s in segments
|
|
525
|
+
if s.state and s.state.is_validated
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# By label breakdown
|
|
529
|
+
by_label = {}
|
|
530
|
+
for segment in segments:
|
|
531
|
+
label = segment.label.name if segment.label else 'unknown'
|
|
532
|
+
if label not in by_label:
|
|
533
|
+
by_label[label] = {'total': 0, 'validated': 0}
|
|
534
|
+
by_label[label]['total'] += 1
|
|
535
|
+
if segment.state and segment.state.is_validated:
|
|
536
|
+
by_label[label]['validated'] += 1
|
|
537
|
+
|
|
538
|
+
return Response({
|
|
539
|
+
"video_id": pk,
|
|
540
|
+
"total_segments": total_count,
|
|
541
|
+
"validated_count": validated_count,
|
|
542
|
+
"unvalidated_count": total_count - validated_count,
|
|
543
|
+
"validation_complete": validated_count == total_count and total_count > 0,
|
|
544
|
+
"by_label": by_label,
|
|
545
|
+
"label_filter": label_name
|
|
546
|
+
}, status=status.HTTP_200_OK)
|
|
547
|
+
|
|
548
|
+
elif request.method == 'POST':
|
|
549
|
+
# Mark all segments as validated
|
|
550
|
+
label_name = request.data.get('label_name')
|
|
551
|
+
notes = request.data.get('notes', '')
|
|
552
|
+
|
|
553
|
+
segments_query = LabelVideoSegment.objects.filter(
|
|
554
|
+
video_file=video
|
|
555
|
+
).select_related('state', 'label')
|
|
556
|
+
|
|
557
|
+
if label_name:
|
|
558
|
+
segments_query = segments_query.filter(label__name=label_name)
|
|
559
|
+
|
|
560
|
+
segments = segments_query.all()
|
|
561
|
+
|
|
562
|
+
if not segments.exists():
|
|
563
|
+
return Response({
|
|
564
|
+
"message": "No segments found to validate",
|
|
565
|
+
"video_id": pk,
|
|
566
|
+
"updated_count": 0
|
|
567
|
+
}, status=status.HTTP_200_OK)
|
|
568
|
+
|
|
569
|
+
updated_count = 0
|
|
570
|
+
failed_count = 0
|
|
571
|
+
|
|
572
|
+
with transaction.atomic():
|
|
573
|
+
for segment in segments:
|
|
574
|
+
try:
|
|
575
|
+
if segment.state:
|
|
576
|
+
segment.state.is_validated = True
|
|
577
|
+
if notes and hasattr(segment.state, 'validation_notes'):
|
|
578
|
+
segment.state.validation_notes = notes
|
|
579
|
+
segment.state.save()
|
|
580
|
+
updated_count += 1
|
|
581
|
+
else:
|
|
582
|
+
failed_count += 1
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logger.error(f"Error validating segment {segment.id}: {e}")
|
|
585
|
+
failed_count += 1
|
|
586
|
+
|
|
587
|
+
logger.info(f"Completed validation for {updated_count} segments in video {pk}")
|
|
588
|
+
|
|
589
|
+
return Response({
|
|
590
|
+
"message": f"Video segment validation completed for video {pk}",
|
|
591
|
+
"video_id": pk,
|
|
592
|
+
"total_segments": len(segments),
|
|
593
|
+
"updated_count": updated_count,
|
|
594
|
+
"failed_count": failed_count,
|
|
595
|
+
"label_filter": label_name
|
|
596
|
+
}, status=status.HTTP_200_OK)
|
endoreg_db/views/pdf/reimport.py
CHANGED
|
@@ -18,15 +18,21 @@ class PdfReimportView(APIView):
|
|
|
18
18
|
super().__init__(**kwargs)
|
|
19
19
|
self.pdf_service = PdfImportService()
|
|
20
20
|
|
|
21
|
-
def post(self, request,
|
|
21
|
+
def post(self, request, pk):
|
|
22
22
|
"""
|
|
23
23
|
Re-import a pdf file to regenerate SensitiveMeta and other metadata.
|
|
24
24
|
Instead of creating a new pdf, this updates the existing one.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
request: HTTP request object
|
|
28
|
+
pk: PDF primary key (ID)
|
|
25
29
|
"""
|
|
30
|
+
pdf_id = pk # Align with media framework naming convention
|
|
31
|
+
|
|
26
32
|
# Validate pdf_id parameter
|
|
27
33
|
if not pdf_id or not isinstance(pdf_id, int):
|
|
28
34
|
return Response(
|
|
29
|
-
{"error": "Invalid
|
|
35
|
+
{"error": "Invalid PDF ID provided."},
|
|
30
36
|
status=status.HTTP_400_BAD_REQUEST
|
|
31
37
|
)
|
|
32
38
|
|
|
@@ -94,7 +100,11 @@ class PdfReimportView(APIView):
|
|
|
94
100
|
|
|
95
101
|
logger.info(f"Starting anonymization using VideoImportService for {pdf.uuid}")
|
|
96
102
|
self.pdf_service.import_and_anonymize(
|
|
97
|
-
|
|
103
|
+
file_path=raw_file_path,
|
|
104
|
+
center_name=pdf.center.name,
|
|
105
|
+
processor_name=pdf.processor.name if pdf.processor else "Unknown",
|
|
106
|
+
save_video=True,
|
|
107
|
+
delete_source=False
|
|
98
108
|
)
|
|
99
109
|
|
|
100
110
|
logger.info(f"VideoImportService anonymization completed for {pdf.uuid}")
|
|
@@ -115,17 +125,17 @@ class PdfReimportView(APIView):
|
|
|
115
125
|
logger.exception(f"VideoImportService anonymization failed for pdf {pdf.uuid}: {e}")
|
|
116
126
|
logger.warning("Continuing without anonymization due to error")
|
|
117
127
|
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
# Refresh from database to get final state
|
|
129
|
+
pdf.refresh_from_db()
|
|
120
130
|
|
|
121
131
|
return Response({
|
|
122
|
-
"message": "
|
|
132
|
+
"message": "PDF re-import completed successfully.",
|
|
123
133
|
"pdf_id": pdf_id,
|
|
124
134
|
"uuid": str(pdf.uuid),
|
|
125
135
|
"sensitive_meta_created": pdf.sensitive_meta is not None,
|
|
126
|
-
"sensitive_meta_id": .sensitive_meta.id if pdf.sensitive_meta else None,
|
|
136
|
+
"sensitive_meta_id": pdf.sensitive_meta.id if pdf.sensitive_meta else None,
|
|
127
137
|
"updated_in_place": True,
|
|
128
|
-
"status": "done"
|
|
138
|
+
"status": "done"
|
|
129
139
|
}, status=status.HTTP_200_OK)
|
|
130
140
|
|
|
131
141
|
except Exception as e:
|
|
@@ -7,16 +7,8 @@ from .correction import (
|
|
|
7
7
|
VideoReprocessView,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
# Phase 1.2: Media Management Views ✅ IMPLEMENTED
|
|
11
10
|
from ..media.video_media import VideoMediaView
|
|
12
11
|
|
|
13
|
-
# TODO Phase 1.2+: Future views
|
|
14
|
-
# from .media import (
|
|
15
|
-
# VideoCorrectionView,
|
|
16
|
-
# TaskStatusView,
|
|
17
|
-
# VideoDownloadProcessedView,
|
|
18
|
-
# )
|
|
19
|
-
|
|
20
12
|
from .reimport import (
|
|
21
13
|
VideoReimportView
|
|
22
14
|
)
|