diff --git a/client/dive-common/apispec.ts b/client/dive-common/apispec.ts index 9ac26db83..d5b8ef232 100644 --- a/client/dive-common/apispec.ts +++ b/client/dive-common/apispec.ts @@ -149,6 +149,24 @@ interface FrameImage { timestamp?: number; } +/** One metadata attachment loaded by a platform implementation. */ +interface FrameMetadataAttachmentText { + /** Preserved original name, falling back to the resolved item/path basename. */ + name: string; + /** Present for a readable TXT/CSV attachment. */ + text?: string; + /** Present when the selected locator could not be read. */ + error?: string; +} + +/** Complete normalized attachment response for one dataset. */ +interface FrameMetadataSourcesResponse { + /** Single-camera dataset attachment or multicamera parent attachment. */ + shared?: FrameMetadataAttachmentText; + /** Camera-local attachments only, keyed by camera name. */ + cameras: Record; +} + export interface MultiCamImportFolderArgs { datasetName?: string; // Girder parent folder name (required on web) defaultDisplay: string; // In multicam the default camera to display @@ -163,6 +181,8 @@ export interface MultiCamImportFolderArgs { * dataset's saved camera registration. */ transformFile?: string; + /** Optional camera-local metadata attachment. */ + metadataFile?: string; /** Per-camera media type when cameras differ (e.g. EO JPG + IR TIFF on web). */ type?: 'image-sequence' | 'video' | 'large-image'; /** @@ -208,6 +228,10 @@ interface MediaImportResponse { globPattern: string; mediaConvertList: string[]; } + +/** User-editable datasetInfo stored on the dataset's backing metadata object. */ +type DatasetInfoFields = Record; + /** * The parts of metadata a user should be able to modify. */ @@ -219,7 +243,7 @@ interface DatasetMetaMutable { imageEnhancements?: ImageEnhancements; attributes?: Readonly>; attributeTrackFilters?: Readonly>; - datasetInfo?: Record; + datasetInfo?: DatasetInfoFields; cameraHomographies?: CameraHomographies; cameraCorrespondences?: CameraCorrespondences; cameraTransformTypes?: CameraTransformTypes; @@ -331,6 +355,7 @@ interface Api { loadMetadata(datasetId: string): Promise; loadDetections(datasetId: string, revision?: number, set?: string): Promise; + loadFrameMetadata(datasetId: string): Promise; saveDetections(datasetId: string, args: SaveDetectionsArgs): Promise; saveMetadata(datasetId: string, metadata: DatasetMetaMutable): Promise; @@ -339,7 +364,13 @@ interface Api { args: SaveAttributeTrackFilterArgs): Promise; // Non-Endpoint shared functions openFromDisk(datasetType: DatasetType | 'bulk' | 'calibration' | 'annotation' | 'text' | 'zip' | 'transform' | 'metadata', directory?: boolean): - Promise<{canceled?: boolean; filePaths: string[]; fileList?: File[]; root?: string}>; + Promise<{ + canceled?: boolean; + filePaths: string[]; + fileList?: File[]; + root?: string; + selectionId?: string; + }>; /** Desktop: immediate child directory names under a parent folder (multicam subfolder import). */ listImmediateSubfolders?(parentPath: string): Promise; /** Desktop: subfolders or root-level video files under a parent folder (multicam import). */ @@ -591,6 +622,7 @@ export { AnnotationSchema, Api, DatasetMeta, + DatasetInfoFields, DatasetMetaMutable, DatasetMetaMutableKeys, MulticamSharedMutableKeys, @@ -602,6 +634,8 @@ export { SubType, PipelineParamType, FrameImage, + FrameMetadataAttachmentText, + FrameMetadataSourcesResponse, MultiTrackRecord, MultiGroupRecord, Pipe, diff --git a/client/dive-common/components/ImportAnnotations.vue b/client/dive-common/components/ImportAnnotations.vue index f70721b9d..9424b6cff 100644 --- a/client/dive-common/components/ImportAnnotations.vue +++ b/client/dive-common/components/ImportAnnotations.vue @@ -5,6 +5,7 @@ import { import { useApi } from 'dive-common/apispec'; import { usePrompt } from 'dive-common/vue-utilities/prompt-service'; import { clientSettings, isStereoInteractiveModeEnabled } from 'dive-common/store/settings'; +import isFrameMetadataSourceName from 'dive-common/frameMetadata/naming'; import clearLengthAttributes from 'dive-common/utils/clearLengthAttributes'; import warpAnnotationsAcrossCameras from 'dive-common/utils/warpAnnotationsAcrossCameras'; import { cloneDeep } from 'lodash'; @@ -188,6 +189,22 @@ export default defineComponent({ if (!ret.canceled) { menuOpen.value = false; const path = ret.filePaths[0]; + // A reserved-name sidecar is frame metadata, not annotations. Reject before + // the import call: web uploads the file and postprocesses afterwards, so a + // later check would leave it sitting in the dataset folder. + const name = ret.fileList?.[0]?.name ?? path; + if (isFrameMetadataSourceName(name)) { + await prompt({ + title: 'Not an Annotation File', + text: [ + `"${name.replace(/^.*[\\/]/, '')}" is a frame metadata file, not annotations.`, + 'Attach frame metadata with the "Metadata File (Optional)" field when the dataset is created.', + 'Attached that way, a frame metadata file may have any name.', + ], + positiveButton: 'OK', + }); + return; + } let importFile: boolean | string[] = false; processing.value = true; const set = currentSet.value === 'default' ? undefined : currentSet.value; diff --git a/client/dive-common/components/ImportMultiCamDialog/ImportMultiCamChooseMetadata.vue b/client/dive-common/components/ImportMultiCamDialog/ImportMultiCamChooseMetadata.vue new file mode 100644 index 000000000..99050bddf --- /dev/null +++ b/client/dive-common/components/ImportMultiCamDialog/ImportMultiCamChooseMetadata.vue @@ -0,0 +1,51 @@ + + + diff --git a/client/dive-common/components/ImportMultiCamDialog/ImportMultiCamMetadata.vue b/client/dive-common/components/ImportMultiCamDialog/ImportMultiCamMetadata.vue index f8fbad63f..20de2c793 100644 --- a/client/dive-common/components/ImportMultiCamDialog/ImportMultiCamMetadata.vue +++ b/client/dive-common/components/ImportMultiCamDialog/ImportMultiCamMetadata.vue @@ -1,6 +1,6 @@