@@ -376,6 +376,150 @@ class VideoFile(SpyglassMixin, dj.Imported):
376376
377377 _nwb_table = Nwbfile
378378
379+ def _prepare_video_entry (self , key , video_obj , cam_device_str ):
380+ """Prepare a VideoFile entry dict for a given video object.
381+
382+ Parameters
383+ ----------
384+ key : dict
385+ The primary key for the VideoFile entry
386+ video_obj : pynwb.image.ImageSeries
387+ The video object from the NWB file
388+ cam_device_str : str
389+ Regular expression pattern to extract camera device number
390+ (e.g., r"camera_device (\d+)") as an integer.
391+
392+ Returns
393+ -------
394+ dict
395+ Prepared entry dict ready for insertion
396+
397+ Raises
398+ ------
399+ KeyError
400+ If camera_name is not found in CameraDevice table
401+ """
402+ nwb_cam_device = video_obj .device .name
403+ key ["video_file_num" ] = int (re .match (cam_device_str , nwb_cam_device )[1 ])
404+
405+ camera_name = video_obj .device .camera_name
406+ if not (CameraDevice & {"camera_name" : camera_name }):
407+ raise KeyError (
408+ f"No camera with camera_name: { camera_name } found "
409+ "in CameraDevice table."
410+ )
411+
412+ key ["camera_name" ] = camera_name
413+ key ["video_file_object_id" ] = video_obj .object_id
414+ return key
415+
416+ def _process_video_timestamps (
417+ self , video_obj , valid_times , key , cam_device_str
418+ ):
419+ """Process video timestamps and collect VideoFile entries.
420+
421+ Handles both single-file and multi-file ImageSeries. For multi-file
422+ ImageSeries (indicated by starting_frame attribute), segments
423+ timestamps per file and validates each segment separately.
424+
425+ Parameters
426+ ----------
427+ video_obj : pynwb.image.ImageSeries
428+ The video object from the NWB file
429+ valid_times : Interval
430+ Valid time intervals for the current epoch
431+ key : dict
432+ The primary key for the VideoFile entry
433+ cam_device_str : str
434+ Regular expression pattern to extract camera device number
435+ (e.g., r"camera_device (\d+)") as an integer.
436+
437+ Returns
438+ -------
439+ list
440+ List of entry dicts ready for insertion (may be empty)
441+ """
442+ timestamps = video_obj .timestamps
443+ starting_frame = getattr (video_obj , "starting_frame" , None )
444+
445+ # Check for multi-file ImageSeries
446+ if starting_frame is not None and len (starting_frame ) > 1 :
447+ return self ._process_multifile_video (
448+ video_obj ,
449+ timestamps ,
450+ starting_frame ,
451+ valid_times ,
452+ key ,
453+ cam_device_str ,
454+ )
455+
456+ # Single-file ImageSeries: original logic for backward compatibility
457+ these_times = valid_times .contains (timestamps )
458+ if len (these_times ) <= (0.9 * len (timestamps )):
459+ return []
460+
461+ entry = self ._prepare_video_entry (key , video_obj , cam_device_str )
462+ return [entry ]
463+
464+ def _process_multifile_video (
465+ self ,
466+ video_obj ,
467+ timestamps ,
468+ starting_frame ,
469+ valid_times ,
470+ key ,
471+ cam_device_str ,
472+ ):
473+ """Process multi-file ImageSeries with starting_frame parameter.
474+
475+ Parameters
476+ ----------
477+ video_obj : pynwb.image.ImageSeries
478+ The video object from the NWB file
479+ timestamps : array
480+ All timestamps for the ImageSeries
481+ starting_frame : array
482+ Frame indices indicating where each external file begins
483+ valid_times : Interval
484+ Valid time intervals for the current epoch
485+ key : dict
486+ The primary key for the VideoFile entry
487+ cam_device_str : str
488+ Regular expression pattern to extract camera device number
489+ (e.g., r"camera_device (\d+)") as an integer.
490+
491+ Returns
492+ -------
493+ list
494+ List of entry dicts ready for insertion (may be empty)
495+ """
496+ entries = []
497+
498+ for file_idx in range (len (starting_frame )):
499+ # Determine timestamp range for this file segment
500+ start_idx = starting_frame [file_idx ]
501+ end_idx = (
502+ starting_frame [file_idx + 1 ]
503+ if file_idx + 1 < len (starting_frame )
504+ else len (timestamps )
505+ )
506+
507+ # Extract timestamps for this specific file
508+ file_timestamps = timestamps [start_idx :end_idx ]
509+
510+ # Check if 90% of this file's timestamps overlap with epoch
511+ these_times = valid_times .contains (file_timestamps )
512+ if len (these_times ) <= (0.9 * len (file_timestamps )):
513+ continue
514+
515+ # This file segment matches the epoch - prepare VideoFile entry
516+ entry = self ._prepare_video_entry (
517+ key .copy (), video_obj , cam_device_str
518+ )
519+ entries .append (entry )
520+
521+ return entries
522+
379523 def make (self , key , verbose = True , skip_duplicates = False ):
380524 """Make without optional transaction"""
381525 if not self .connection .in_transaction :
@@ -387,7 +531,8 @@ def make(self, key, verbose=True, skip_duplicates=False):
387531 nwb_file_name = key ["nwb_file_name" ]
388532 nwb_file_abspath = Nwbfile .get_abs_path (nwb_file_name )
389533 nwbf = get_nwb_file (nwb_file_abspath )
390- # get all ImageSeries objects in the NWB file
534+
535+ # Get all ImageSeries objects in the NWB file
391536 videos = {
392537 obj .name : obj
393538 for obj in nwbf .objects .values ()
@@ -399,7 +544,7 @@ def make(self, key, verbose=True, skip_duplicates=False):
399544 )
400545 return
401546
402- # get the interval for the current TaskEpoch
547+ # Get the interval for the current TaskEpoch
403548 interval_list_name = (TaskEpoch () & key ).fetch1 ("interval_list_name" )
404549 valid_times = (
405550 IntervalList
@@ -410,47 +555,31 @@ def make(self, key, verbose=True, skip_duplicates=False):
410555 ).fetch_interval ()
411556
412557 cam_device_str = r"camera_device (\d+)"
413- is_found = False
414- for ind , video in enumerate (videos .values ()):
415- if isinstance (video , pynwb .image .ImageSeries ):
416- video = [video ]
417- for video_obj in video :
418- # check to see if the times for this video_object are largely
419- # overlapping with the task epoch times
420-
421- timestamps = video_obj .timestamps
422- these_times = valid_times .contains (timestamps )
423- if not len (these_times ) > (0.9 * len (timestamps )):
424- continue
425-
426- nwb_cam_device = video_obj .device .name
427-
428- # returns whatever was captured in the first group (within the
429- # parentheses) of the regular expression - in this case, 0
430-
431- key ["video_file_num" ] = int (
432- re .match (cam_device_str , nwb_cam_device )[1 ]
433- )
434- camera_name = video_obj .device .camera_name
435- if CameraDevice & {"camera_name" : camera_name }:
436- key ["camera_name" ] = video_obj .device .camera_name
437- else :
438- raise KeyError (
439- f"No camera with camera_name: { camera_name } found "
440- + "in CameraDevice table."
441- )
442- key ["video_file_object_id" ] = video_obj .object_id
443- self .insert1 (
444- key ,
445- skip_duplicates = skip_duplicates ,
446- allow_direct_insert = True ,
558+ video_inserts = []
559+
560+ for video in videos .values ():
561+ video_list = (
562+ [video ] if isinstance (video , pynwb .image .ImageSeries ) else video
563+ )
564+ for video_obj in video_list :
565+ entries = self ._process_video_timestamps (
566+ video_obj ,
567+ valid_times ,
568+ key .copy (),
569+ cam_device_str ,
447570 )
448- is_found = True
571+ video_inserts . extend ( entries )
449572
450- if not is_found and verbose :
573+ if video_inserts :
574+ self .insert (
575+ video_inserts ,
576+ skip_duplicates = skip_duplicates ,
577+ allow_direct_insert = True ,
578+ )
579+ elif verbose :
451580 logger .info (
452581 f"No video found corresponding to file { nwb_file_name } , "
453- + f"epoch { interval_list_name } "
582+ f"epoch { interval_list_name } "
454583 )
455584
456585 @classmethod
0 commit comments