From 87b3c195a6653c5ed536a815d492faf21b90d0e1 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Fri, 21 Aug 2026 13:22:23 -0400 Subject: [PATCH] swap from video.fps to info.video_annotation_fps --- .../desktop/backend/serializers/coco.spec.ts | 42 +++++++++---- .../desktop/backend/serializers/coco.ts | 42 +++++++++---- .../desktop/backend/serializers/dive.ts | 2 +- docs/DataFormats.md | 42 ++++++++----- server/dive_server/crud_dataset.py | 4 +- server/dive_utils/serializers/dive.py | 2 +- server/dive_utils/serializers/kwcoco.py | 63 ++++++++++++------- server/dive_utils/serializers/viame.py | 7 +-- server/tests/test_deserialize_kwcoco_json.py | 51 ++++++++++----- server/tests/test_multicam_export_clone.py | 3 +- server/tests/test_update_metadata.py | 3 +- 11 files changed, 174 insertions(+), 87 deletions(-) diff --git a/client/platform/desktop/backend/serializers/coco.spec.ts b/client/platform/desktop/backend/serializers/coco.spec.ts index f8db20442..56123b745 100644 --- a/client/platform/desktop/backend/serializers/coco.spec.ts +++ b/client/platform/desktop/backend/serializers/coco.spec.ts @@ -316,9 +316,9 @@ describe('COCO serializer', () => { expect(parsedMeta).not.toHaveProperty('datasetInfo'); }); - // --- annotation fps on videos[] --- + // --- annotation fps in info.video_annotation_fps --- - it('writes videos[].fps for video datasets and restores it on re-import', async () => { + it('writes info.video_annotation_fps for video datasets and restores it on re-import', async () => { const videoMeta = { ...imageMeta, type: 'video' as const, @@ -327,7 +327,9 @@ describe('COCO serializer', () => { }; await serializeFile('/output/video.coco.json', annotationSchema, videoMeta); const out = await fs.readJSON('/output/video.coco.json'); - expect(out.videos).toEqual([{ id: 1, name: 'clip', fps: 5 }]); + expect(out.videos).toEqual([{ id: 1, name: 'clip' }]); + expect(out.info.video_annotation_fps).toEqual({ 1: 5 }); + expect(out.info.dive_extensions).toContain('video_annotation_fps'); expect(out.images.every((image: { video_id?: number }) => image.video_id === 1)).toBe(true); mockfs({ @@ -342,6 +344,7 @@ describe('COCO serializer', () => { await serializeFile('/output/seq.coco.json', annotationSchema, { ...imageMeta, fps: 5 }); const out = await fs.readJSON('/output/seq.coco.json'); expect(out).not.toHaveProperty('videos'); + expect(out.info).not.toHaveProperty('video_annotation_fps'); expect(out.images.every((image: { video_id?: number }) => image.video_id === undefined)).toBe(true); }); @@ -353,6 +356,7 @@ describe('COCO serializer', () => { }); const out = await fs.readJSON('/output/zero.coco.json'); expect(out).not.toHaveProperty('videos'); + expect(out.info).not.toHaveProperty('video_annotation_fps'); }); it('imports a pruned KWCOCO probability vector by raw category position', async () => { @@ -715,26 +719,40 @@ describe('COCO serializer', () => { expect(parsed.tracks[4].confidencePairs).toEqual([['root', 0.2]]); }); - it('imports the frame rate a video records, as the CSV header path does', async () => { - const document = (videos: unknown) => JSON.stringify({ + it('imports the frame rate from info.video_annotation_fps only', async () => { + const document = (opts: { + videos?: unknown; + video_annotation_fps?: Record; + }) => JSON.stringify({ + ...(opts.video_annotation_fps + ? { info: { video_annotation_fps: opts.video_annotation_fps } } + : {}), images: [{ id: 1, file_name: 'frame_000000.png', frame_index: 0 }], annotations: [{ id: 1, image_id: 1, category_id: 1, bbox: [0, 0, 1, 1], track_id: 1, }], categories: [{ id: 1, name: 'fish' }], - ...(videos === undefined ? {} : { videos }), + ...(opts.videos === undefined ? {} : { videos: opts.videos }), }); mockfs({ '/input': { - 'video.json': document([{ id: 1, name: 'clip', fps: 5 }]), - 'image-list.json': document(undefined), - 'unusable.json': document([{ id: 1, name: 'clip', fps: 0 }]), - 'not-a-number.json': document([{ id: 1, name: 'clip', fps: '5' }]), + 'info-map.json': document({ + videos: [{ id: 1, name: 'clip' }], + video_annotation_fps: { 1: 5 }, + }), + 'videos-fps-only.json': document({ videos: [{ id: 1, name: 'clip', fps: 5 }] }), + 'image-list.json': document({}), + 'unusable.json': document({ video_annotation_fps: { 1: 0 } }), + 'not-a-number.json': document({ video_annotation_fps: { 1: '5' } }), }, }); - const [, videoMeta] = await parseFile('/input/video.json'); - expect(videoMeta.fps).toBe(5); + const [, infoMeta] = await parseFile('/input/info-map.json'); + expect(infoMeta.fps).toBe(5); + + // fps on videos[] is ignored. + const [, videosFpsMeta] = await parseFile('/input/videos-fps-only.json'); + expect(videosFpsMeta.fps).toBeUndefined(); // An image sequence describes no video, so it carries no rate to import. const [, listMeta] = await parseFile('/input/image-list.json'); diff --git a/client/platform/desktop/backend/serializers/coco.ts b/client/platform/desktop/backend/serializers/coco.ts index b9b63b67e..c81f81a23 100644 --- a/client/platform/desktop/backend/serializers/coco.ts +++ b/client/platform/desktop/backend/serializers/coco.ts @@ -234,7 +234,6 @@ type CocoAnnotation = { type CocoVideo = { id: number; name?: string; - fps?: unknown; }; type CocoDocument = { @@ -245,20 +244,38 @@ type CocoDocument = { videos?: CocoVideo[]; }; +function usableFpsRate(rate: unknown): number | undefined { + if (typeof rate === 'number' && Number.isFinite(rate) && rate > 0) { + return rate; + } + return undefined; +} + /** - * Frame rate recorded on the video, the COCO counterpart of the VIAME CSV - * header's fps. Neither COCO nor kwcoco define one, so this reads the field - * VIAME writes on the video entry; an image sequence describes no video and - * carries none, which is not an error. + * Annotation FPS for a COCO/KWCOCO document, if usable. + * Reads `info.video_annotation_fps` (video_id → rate). Image sequences + * describe no video and carry none, which is not an error. */ function frameRateFromDocument(document: CocoDocument): number | undefined { + const { info } = document; + const fpsMap = info?.video_annotation_fps; + if (!fpsMap || typeof fpsMap !== 'object' || Array.isArray(fpsMap)) { + return undefined; + } + const map = fpsMap as Record; const videos = Array.isArray(document.videos) ? document.videos : []; for (let i = 0; i < videos.length; i += 1) { - const rate = videos[i]?.fps; - if (typeof rate === 'number' && Number.isFinite(rate) && rate > 0) { - return rate; + const videoId = videos[i]?.id; + if (videoId !== undefined) { + const rate = usableFpsRate(map[videoId]) ?? usableFpsRate(map[String(videoId)]); + if (rate !== undefined) return rate; } } + const values = Object.values(map); + for (let i = 0; i < values.length; i += 1) { + const rate = usableFpsRate(values[i]); + if (rate !== undefined) return rate; + } return undefined; } @@ -560,8 +577,9 @@ async function serializeFile( Array.from(new Set(Object.values(hierarchy))).sort().forEach(addCategoryName); const categories = new Map(categoryNames.map((name, index) => [name, index + 1])); - // Video datasets record annotation FPS on a one-entry `videos` table (VIAME - // convention). Image sequences omit it so re-import does not treat them as video. + // Video datasets record annotation FPS under info.video_annotation_fps keyed + // by video_id, with a one-entry videos table. Image sequences omit videos so + // re-import does not treat them as video. const emitVideo = ( meta.type === 'video' && typeof meta.fps === 'number' @@ -622,8 +640,10 @@ async function serializeFile( 'dive_notes', 'dive_confidence_pairs', ...(datasetInfo ? ['dive_dataset_info'] : []), + ...(emitVideo ? ['video_annotation_fps'] : []), ], ...(datasetInfo ? { dive_dataset_info: datasetInfo } : {}), + ...(emitVideo ? { video_annotation_fps: { 1: meta.fps } } : {}), }; const output: CocoDocument = { info, @@ -631,7 +651,7 @@ async function serializeFile( annotations, categories: categoryDocs, ...(emitVideo ? { - videos: [{ id: 1, name: meta.name, fps: meta.fps }], + videos: [{ id: 1, name: meta.name }], } : {}), }; await fs.writeJSON(path, output, { spaces: 2 }); diff --git a/client/platform/desktop/backend/serializers/dive.ts b/client/platform/desktop/backend/serializers/dive.ts index f4b5729f0..6ce2a41c1 100644 --- a/client/platform/desktop/backend/serializers/dive.ts +++ b/client/platform/desktop/backend/serializers/dive.ts @@ -14,7 +14,7 @@ function makeEmptyAnnotationFile(): AnnotationSchema { /** * Annotation FPS recorded on a DIVE JSON document, if usable. - * Same rules as the VIAME CSV `fps:` header and COCO `videos[].fps`. + * Same rules as the VIAME CSV `fps:` header and COCO `info.video_annotation_fps`. */ function frameRateFromDocument(data: unknown): number | undefined { if (!data || typeof data !== 'object') { diff --git a/docs/DataFormats.md b/docs/DataFormats.md index eb7de720e..c865fd607 100644 --- a/docs/DataFormats.md +++ b/docs/DataFormats.md @@ -34,7 +34,7 @@ interface AnnotationSchema { version: 2; /** * Annotation frame rate when present. Omitted when absent or unusable. - * Same role as the VIAME CSV `# metadata` `fps` field and COCO `videos[].fps`. + * Same role as the VIAME CSV `# metadata` `fps` field and COCO `info.video_annotation_fps`. */ fps?: number; } @@ -120,7 +120,8 @@ The full source [TrackData definition can be found here](https://github.com/Kitw ### Annotation frame rate (`fps`) Optional top-level `fps` carries the dataset annotation frame rate — the same value -VIAME CSV writes in the `# metadata` header and COCO/KWCOCO records on `videos[].fps`. +VIAME CSV writes in the `# metadata` header and COCO/KWCOCO records in +`info.video_annotation_fps`. ```json { @@ -208,7 +209,8 @@ This information provides the specification for an individual dataset. It consi * Annotation frame rate is stored as dataset `fps`. * Included in [DIVE Annotation JSON](#annotation-frame-rate-fps) as top-level `fps`. * Included in [VIAME CSV](#dataset-metadata-in-the-header) as the `# metadata` `fps` field. - * Included in [COCO / KWCOCO](#annotation-frame-rate-videosfps) as `videos[].fps` for video datasets. + * Included in [COCO / KWCOCO](#annotation-frame-rate-infovideo_annotation_fps) as + `info.video_annotation_fps` for video datasets. * A track type hierarchy is stored in `typeHierarchy` as a child-type to immediate-parent-type map. For example, this configuration makes `fish` a heading-only parent (it does not need to be an @@ -443,17 +445,22 @@ advertised in `info.dive_extensions`: * `info.dive_dataset_info = { "gfishsite_id": "2024TXN012", "year": "2024", ... }` -### Annotation frame rate (`videos[].fps`) +### Annotation frame rate (`info.video_annotation_fps`) -Neither MS-COCO nor KWCOCO define a frame-rate field. On import, DIVE reads the -annotation FPS the same way VIAME writes it: a positive numeric `fps` on an entry -in the top-level `videos` table (the COCO counterpart of the VIAME CSV `# metadata` -`fps` header). Image-sequence documents typically omit `videos` and carry no rate. +Neither MS-COCO nor KWCOCO define a frame-rate field. DIVE stores the annotation +FPS under `info.video_annotation_fps` as a map of KWCOCO `video_id` → rate (the +COCO counterpart of the VIAME CSV `# metadata` `fps` header). Image-sequence +documents typically omit `videos` and carry no rate. ```json { + "info": { + "description": "DIVE export for clip", + "dive_extensions": ["video_annotation_fps"], + "video_annotation_fps": { "1": 5 } + }, "videos": [ - { "id": 1, "name": "clip", "fps": 5 } + { "id": 1, "name": "clip" } ], "images": [ { "id": 1, "file_name": "frame_000000.jpg", "frame_index": 0, "video_id": 1 } @@ -463,10 +470,12 @@ in the top-level `videos` table (the COCO counterpart of the VIAME CSV `# metada * A usable value (finite number greater than zero) is restored into dataset metadata as `fps`. Unusable values (`0`, negative, non-numeric, `inf`/`nan`) are ignored. -* When multiple video entries are present, the first usable `fps` wins. -* On export of a **video** dataset, DIVE writes a one-entry `videos` table with the - annotation FPS and sets `images[].video_id`. Image-sequence exports omit `videos` - so re-import does not treat them as video. +* Keys are stringified `video_id` values (JSON object keys are always strings). +* When multiple rates are present, the first usable value wins (videos-table order, + then map insertion order). +* On export of a **video** dataset, DIVE writes a one-entry `videos` table, sets + `images[].video_id`, and records the rate under `info.video_annotation_fps`. + Image-sequence exports omit `videos` so re-import does not treat them as video. ### Extension Field Details @@ -502,9 +511,10 @@ For COCO files produced by DIVE: * DIVE writes category-aligned `prob` plus exact `dive_confidence_pairs` on each annotation. * Re-importing that file into DIVE preserves hierarchy edges, track IDs, complete confidence vectors, attributes, and notes. -* For video datasets, DIVE also writes `videos[].fps` (and `images[].video_id`) so annotation - FPS round-trips. Image-sequence exports omit `videos`. See - [Annotation frame rate (`videos[].fps`)](#annotation-frame-rate-videosfps). +* For video datasets, DIVE also writes `info.video_annotation_fps` (with `videos[]` + and `images[].video_id`) so annotation FPS round-trips. Image-sequence exports + omit `videos`. See + [Annotation frame rate (`info.video_annotation_fps`)](#annotation-frame-rate-infovideo_annotation_fps). For COCO files not produced by DIVE: diff --git a/server/dive_server/crud_dataset.py b/server/dive_server/crud_dataset.py index d0bd8ba9f..832e0f069 100644 --- a/server/dive_server/crud_dataset.py +++ b/server/dive_server/crud_dataset.py @@ -829,8 +829,8 @@ def _coco_json_export_text( for feature in track_data.get('features', []): max_frame = max(max_frame, feature.get('frame', -1)) image_filenames = {i: f'frame_{i:06d}.jpg' for i in range(max_frame + 1)} - # Annotation FPS rides on videos[].fps for video datasets only; image sequences - # omit the table so re-import does not treat them as video. + # Annotation FPS rides in info.video_annotation_fps for video datasets only; + # image sequences omit the videos table so re-import does not treat them as video. export_fps = None if dataset_type == constants.VideoType: fps = fromMeta(dsFolder, constants.FPSMarker, None) diff --git a/server/dive_utils/serializers/dive.py b/server/dive_utils/serializers/dive.py index ac45cafd0..d1c0b3b54 100644 --- a/server/dive_utils/serializers/dive.py +++ b/server/dive_utils/serializers/dive.py @@ -7,7 +7,7 @@ def frame_rate_from_dive(data: Any) -> Optional[float]: """Annotation FPS recorded on a DIVE JSON document, if usable. - Same rules as the VIAME CSV ``fps:`` header and COCO ``videos[].fps``: + Same rules as the VIAME CSV ``fps:`` header and COCO ``info.video_annotation_fps``: a finite number greater than zero. Absent or unusable values are not an error. """ if not isinstance(data, dict): diff --git a/server/dive_utils/serializers/kwcoco.py b/server/dive_utils/serializers/kwcoco.py index d2e40877d..8f026740d 100644 --- a/server/dive_utils/serializers/kwcoco.py +++ b/server/dive_utils/serializers/kwcoco.py @@ -240,21 +240,42 @@ def _validate_annotation_bounds(annotations: List[dict]) -> None: raise ValueError(_missing_bounds_error(missing_ids)) +def _usable_fps_rate(rate: Any) -> Optional[float]: + """Return rate when it is a finite number greater than zero; otherwise None.""" + if isinstance(rate, bool) or not isinstance(rate, (int, float)): + return None + if math.isfinite(rate) and rate > 0: + return float(rate) + return None + + def frame_rate_from_coco(coco: Dict[str, Any]) -> Optional[float]: - """Frame rate recorded on the video, the COCO counterpart of the CSV header's fps. + """Annotation FPS for a COCO/KWCOCO document, if usable. + + Reads ``info.video_annotation_fps``, a map of ``video_id`` → rate (the DIVE + export shape). Image-sequence documents describe no video and carry none, + which is not an error. - Neither MS-COCO nor KWCOCO define a frame rate, so this reads the field VIAME - writes on the video entry. Image-sequence documents describe no video and - carry none, which is not an error. + When multiple rates are present, the first usable value wins: videos-table + order for the map (looking up each ``video_id``), then map insertion order. """ + info = coco.get('info') or {} + fps_map = info.get('video_annotation_fps') if isinstance(info, dict) else None + if not isinstance(fps_map, dict): + return None for video in coco.get('videos') or []: - if not isinstance(video, dict): - continue - rate = video.get('fps') - if isinstance(rate, bool) or not isinstance(rate, (int, float)): + if not isinstance(video, dict) or 'id' not in video: continue - if math.isfinite(rate) and rate > 0: - return float(rate) + video_id = video['id'] + rate = _usable_fps_rate(fps_map.get(video_id)) + if rate is None: + rate = _usable_fps_rate(fps_map.get(str(video_id))) + if rate is not None: + return rate + for rate in fps_map.values(): + usable = _usable_fps_rate(rate) + if usable is not None: + return usable return None @@ -600,10 +621,11 @@ def export_dive_as_coco( typeHierarchy: DIVE child-to-parent category hierarchy, emitted through KWCOCO's ``categories[].supercategory`` field. fps: Annotation frame rate for a video dataset. When usable (finite and - greater than zero), written on a one-entry ``videos`` table with - ``images[].video_id`` set — the same convention VIAME uses and DIVE - imports. Callers should pass this only for video datasets; image - sequences omit ``videos`` so re-import does not treat them as video. + greater than zero), written under ``info.video_annotation_fps`` keyed + by ``video_id``, with a one-entry ``videos`` table and + ``images[].video_id`` set. Callers should pass this only for video + datasets; image sequences omit ``videos`` so re-import does not treat + them as video. """ parsed_tracks = [Track(**track_doc) for track_doc in tracks] category_names: List[str] = [] @@ -624,12 +646,7 @@ def add_category_name(name: str) -> None: coco_annotations: List[dict] = [] images: Dict[int, dict] = {} annotation_id = 1 - emit_video = ( - isinstance(fps, (int, float)) - and not isinstance(fps, bool) - and math.isfinite(fps) - and fps > 0 - ) + emit_video = _usable_fps_rate(fps) is not None for track in parsed_tracks: for feature in track.features: @@ -708,6 +725,10 @@ def add_category_name(name: str) -> None: if datasetInfo: info['dive_dataset_info'] = datasetInfo info['dive_extensions'].append('dive_dataset_info') + if emit_video: + # JSON object keys are strings; video_id 1 → "1". + info['video_annotation_fps'] = {'1': float(fps)} + info['dive_extensions'].append('video_annotation_fps') coco: Dict[str, Any] = { 'info': info, @@ -716,5 +737,5 @@ def add_category_name(name: str) -> None: 'categories': categories_doc, } if emit_video: - coco['videos'] = [{'id': 1, 'name': dataset_name, 'fps': float(fps)}] + coco['videos'] = [{'id': 1, 'name': dataset_name}] return coco diff --git a/server/dive_utils/serializers/viame.py b/server/dive_utils/serializers/viame.py index 500818d1f..22bea0d1b 100644 --- a/server/dive_utils/serializers/viame.py +++ b/server/dive_utils/serializers/viame.py @@ -635,9 +635,7 @@ def export_tracks_as_csv( if not confidence_pairs: continue - sorted_confidence_pairs = sorted( - confidence_pairs, key=lambda item: item[1], reverse=True - ) + sorted_confidence_pairs = sorted(confidence_pairs, key=lambda item: item[1], reverse=True) for index, keyframe in enumerate(track.features): features = [keyframe] @@ -709,8 +707,7 @@ def export_tracks_as_csv( for item in sublist # type: ignore ] columns.append( - "(poly) " - + ' '.join(map(lambda x: str(round(x)), outer_coords)) + "(poly) " + ' '.join(map(lambda x: str(round(x)), outer_coords)) ) # Write holes (additional rings) diff --git a/server/tests/test_deserialize_kwcoco_json.py b/server/tests/test_deserialize_kwcoco_json.py index 57da260e0..140c66cf0 100644 --- a/server/tests/test_deserialize_kwcoco_json.py +++ b/server/tests/test_deserialize_kwcoco_json.py @@ -807,11 +807,13 @@ def test_export_dive_as_coco_omits_empty_dataset_info(datasetInfo): def test_export_dive_as_coco_writes_video_fps(): - """Video annotation FPS lands on videos[].fps with images linked by video_id.""" + """Video annotation FPS lands in info.video_annotation_fps keyed by video_id.""" coco = kwcoco.export_dive_as_coco( _EXPORT_TRACKS, {0: "frame_000000.jpg"}, dataset_name="clip", fps=5 ) - assert coco["videos"] == [{"id": 1, "name": "clip", "fps": 5.0}] + assert coco["videos"] == [{"id": 1, "name": "clip"}] + assert coco["info"]["video_annotation_fps"] == {"1": 5.0} + assert "video_annotation_fps" in coco["info"]["dive_extensions"] assert all(image.get("video_id") == 1 for image in coco["images"]) assert kwcoco.frame_rate_from_coco(coco) == 5.0 @@ -826,6 +828,7 @@ def test_export_dive_as_coco_omits_unusable_or_absent_fps(fps): _EXPORT_TRACKS, {0: "frame_000000.jpg"}, dataset_name="demo" ) assert "videos" not in coco + assert "video_annotation_fps" not in coco["info"] assert all("video_id" not in image for image in coco["images"]) assert coco == baseline @@ -1208,7 +1211,7 @@ def test_shared_empty_dive_confidence_pairs_profile(): assert warnings == [kwcoco.DIVE_CONFIDENCE_PAIRS_WARNING] -def _fps_document(videos=None): +def _fps_document(videos=None, video_annotation_fps=None): document = { 'images': [{'id': 1, 'file_name': 'frame_000000.png', 'frame_index': 0}], 'annotations': [ @@ -1218,24 +1221,44 @@ def _fps_document(videos=None): } if videos is not None: document['videos'] = videos + if video_annotation_fps is not None: + document['info'] = {'video_annotation_fps': video_annotation_fps} return document -def test_frame_rate_read_from_video(): - """The COCO counterpart of the VIAME CSV header's fps.""" - assert kwcoco.frame_rate_from_coco( - _fps_document([{'id': 1, 'name': 'clip', 'fps': 5}]) - ) == 5.0 - assert kwcoco.frame_rate_from_coco( - _fps_document([{'id': 1}, {'id': 2, 'name': 'clip', 'fps': 29.97}]) - ) == 29.97 +def test_frame_rate_read_from_info_map(): + """info.video_annotation_fps keyed by video_id is the DIVE export shape.""" + assert ( + kwcoco.frame_rate_from_coco( + _fps_document( + videos=[{'id': 1, 'name': 'clip'}], + video_annotation_fps={'1': 5}, + ) + ) + == 5.0 + ) + assert ( + kwcoco.frame_rate_from_coco( + _fps_document( + videos=[{'id': 1}, {'id': 2, 'name': 'clip'}], + video_annotation_fps={'1': 0, '2': 29.97}, + ) + ) + == 29.97 + ) + # Numeric keys (as in an in-memory dict before JSON round-trip) also work. + assert kwcoco.frame_rate_from_coco(_fps_document(video_annotation_fps={1: 12.5})) == 12.5 + + +def test_frame_rate_ignores_videos_fps_field(): + """FPS on videos[] is not read; only info.video_annotation_fps is.""" + assert kwcoco.frame_rate_from_coco(_fps_document([{'id': 1, 'name': 'clip', 'fps': 5}])) is None def test_frame_rate_absent_or_unusable(): """An image sequence carries no rate, and no caller should see fps: 0.""" assert kwcoco.frame_rate_from_coco(_fps_document()) is None assert kwcoco.frame_rate_from_coco(_fps_document([])) is None + assert kwcoco.frame_rate_from_coco(_fps_document(video_annotation_fps={'1': 0})) is None for fps in [0, -5, '5', True, float('inf'), float('nan'), None]: - assert kwcoco.frame_rate_from_coco( - _fps_document([{'id': 1, 'fps': fps}]) - ) is None + assert kwcoco.frame_rate_from_coco(_fps_document(video_annotation_fps={'1': fps})) is None diff --git a/server/tests/test_multicam_export_clone.py b/server/tests/test_multicam_export_clone.py index 6b5603e91..1ab02f78d 100644 --- a/server/tests/test_multicam_export_clone.py +++ b/server/tests/test_multicam_export_clone.py @@ -743,7 +743,6 @@ def test_export_multicam_annotations_preflights_invalid_coco_hierarchy(zip_gen_c ) assert str(error_info.value) == ( - 'Type hierarchy is invalid: self edge "fish -> fish". ' - 'No COCO file was exported.' + 'Type hierarchy is invalid: self edge "fish -> fish". ' 'No COCO file was exported.' ) zip_gen_cls.assert_not_called() diff --git a/server/tests/test_update_metadata.py b/server/tests/test_update_metadata.py index 24c66abc9..055207312 100644 --- a/server/tests/test_update_metadata.py +++ b/server/tests/test_update_metadata.py @@ -451,8 +451,7 @@ def test_type_hierarchy_for_export_names_the_coco_artifact(): crud_dataset.type_hierarchy_for_export(folder, artifact='COCO file') assert str(error_info.value) == ( - 'Type hierarchy is invalid: self edge "fish -> fish". ' - 'No COCO file was exported.' + 'Type hierarchy is invalid: self edge "fish -> fish". ' 'No COCO file was exported.' )