From c115c5ec573b33654add23b8a328ba7ad97a1478 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Tue, 21 Jul 2026 13:56:34 -0400 Subject: [PATCH 1/2] when default fps was Video FPS (-1) psot process had issues setting FPS for image sequences to 1 --- client/platform/web-girder/views/Upload.vue | 6 ++- server/dive_server/crud_dataset.py | 8 ++-- server/tests/test_create_multicam.py | 43 +++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/client/platform/web-girder/views/Upload.vue b/client/platform/web-girder/views/Upload.vue index 0549b2cac..83118d33e 100644 --- a/client/platform/web-girder/views/Upload.vue +++ b/client/platform/web-girder/views/Upload.vue @@ -440,9 +440,13 @@ export default defineComponent({ const registrationSeed = transformEntries.length ? await parseRegistrationSeed(transformEntries, Object.keys(args.sourceList)) : null; + // annotationFPS of -1 ("Video FPS") is truthy in JS; for image / + // large-image multicam resolve it to 1 so create_multicam matches + // child folders after post-process (which also maps -1 → 1). + const annotationFps = clientSettings.annotationFPS; const fps = args.type === VideoType ? DefaultVideoFPS - : (clientSettings.annotationFPS || 1); + : (annotationFps > 0 ? annotationFps : 1); setMulticamImportProgress(MULTICAM_PROGRESS_START, `${labelPrefix}Creating dataset folder…`); const { data: datasetFolder } = await createGirderFolder({ folderId: props.location._id, diff --git a/server/dive_server/crud_dataset.py b/server/dive_server/crud_dataset.py index d10029b28..1c548d0f1 100644 --- a/server/dive_server/crud_dataset.py +++ b/server/dive_server/crud_dataset.py @@ -1335,12 +1335,14 @@ def create_multicam( loaded_children[name] = child camera_types_by_name[name] = cam_type - use_video_fps = validated.type == constants.VideoType and validated.fps == -1 - if use_video_fps: + # fps == -1 means auto: take the children's resolved rates (video native + # fps, or image-sequence/large-image default of 1 after post-process). + use_auto_fps = validated.fps == -1 + if use_auto_fps: unique_fps = set(child_fps_by_name.values()) if len(unique_fps) > 1: raise RestException( - 'All cameras must have the same fps when using video-derived frame rate', + 'All cameras must have the same fps when using auto frame rate', code=400, ) else: diff --git a/server/tests/test_create_multicam.py b/server/tests/test_create_multicam.py index 896fd8762..d4df251e4 100644 --- a/server/tests/test_create_multicam.py +++ b/server/tests/test_create_multicam.py @@ -146,6 +146,49 @@ def test_create_multicam_accepts_video_fps_sentinel(_verify, folder_cls, _aux, i assert saved_meta[constants.FPSMarker] == 10.0 +@patch('dive_server.crud_dataset.crud.get_or_create_auxiliary_folder') +@patch('dive_server.crud_dataset.Folder') +@patch('dive_server.crud_dataset.crud.valid_images') +@patch('dive_server.crud_dataset.crud.verify_dataset') +def test_create_multicam_accepts_image_sequence_fps_sentinel( + _verify, valid_images_mock, folder_cls, _aux +): + """Image-sequence children resolve fps -1 → 1 in post-process; create_multicam + must accept request fps=-1 (auto) and take the children's rate.""" + user = {'login': 'tester'} + dataset_parent = _dataset_parent() + left = _child_folder('left-id', 'left', fps=1.0, media_type='image-sequence') + right = _child_folder('right-id', 'right', fps=1.0, media_type='image-sequence') + star = _child_folder('star-id', 'STAR', fps=1.0, media_type='image-sequence') + + folder_cls.return_value.load.side_effect = lambda fid, **kwargs: { + 'left-id': left, + 'right-id': right, + 'star-id': star, + }[fid] + valid_images_mock.return_value = [MagicMock(), MagicMock()] + + data = { + 'name': 'caton-set', + 'fps': -1, + 'type': 'image-sequence', + 'subType': 'multicam', + 'defaultDisplay': 'STAR', + 'cameraOrder': ['left', 'right', 'STAR'], + 'cameras': { + 'left': {'folderId': 'left-id'}, + 'right': {'folderId': 'right-id'}, + 'STAR': {'folderId': 'star-id'}, + }, + } + + result = crud_dataset.create_multicam(user, dataset_parent, data) + + assert result == dataset_parent + saved_meta = folder_cls.return_value.save.call_args_list[-1][0][0]['meta'] + assert saved_meta[constants.FPSMarker] == 1.0 + + @patch('dive_server.crud_dataset.crud.valid_images') @patch('dive_server.crud_dataset.Folder') @patch('dive_server.crud_dataset.crud.verify_dataset') From 9a3002947ac537b58cc2741c099db2769a16d00e Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Tue, 21 Jul 2026 14:24:49 -0400 Subject: [PATCH 2/2] linting/test fixes --- client/platform/web-girder/views/Upload.vue | 7 ++++--- server/tests/test_create_multicam.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/platform/web-girder/views/Upload.vue b/client/platform/web-girder/views/Upload.vue index 83118d33e..209963f56 100644 --- a/client/platform/web-girder/views/Upload.vue +++ b/client/platform/web-girder/views/Upload.vue @@ -444,9 +444,10 @@ export default defineComponent({ // large-image multicam resolve it to 1 so create_multicam matches // child folders after post-process (which also maps -1 → 1). const annotationFps = clientSettings.annotationFPS; - const fps = args.type === VideoType - ? DefaultVideoFPS - : (annotationFps > 0 ? annotationFps : 1); + let fps = DefaultVideoFPS; + if (args.type !== VideoType) { + fps = annotationFps > 0 ? annotationFps : 1; + } setMulticamImportProgress(MULTICAM_PROGRESS_START, `${labelPrefix}Creating dataset folder…`); const { data: datasetFolder } = await createGirderFolder({ folderId: props.location._id, diff --git a/server/tests/test_create_multicam.py b/server/tests/test_create_multicam.py index d4df251e4..ed4849e55 100644 --- a/server/tests/test_create_multicam.py +++ b/server/tests/test_create_multicam.py @@ -169,7 +169,7 @@ def test_create_multicam_accepts_image_sequence_fps_sentinel( valid_images_mock.return_value = [MagicMock(), MagicMock()] data = { - 'name': 'caton-set', + 'name': 'stereo-set', 'fps': -1, 'type': 'image-sequence', 'subType': 'multicam',