1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-04 08:28:11 +00:00

face_cluster (#6470)

* face_cluster

* update

* update

* feat: update ui

* feat: optimize code

* feat: update code

* feat: optimize ui

* feat: optimize view name

---------

Co-authored-by: zheng.shen <zheng.shen@seafile.com>
Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
shenzheng-1
2024-10-16 11:09:30 +08:00
committed by GitHub
parent 24405a6600
commit 065f1584b5
24 changed files with 872 additions and 31 deletions

View File

@@ -1,3 +1,4 @@
export const LIST_MODE = 'list'; export const LIST_MODE = 'list';
export const GRID_MODE = 'grid'; export const GRID_MODE = 'grid';
export const METADATA_MODE = 'metadata'; export const METADATA_MODE = 'metadata';
export const FACE_RECOGNITION_MODE = 'person_image';

View File

@@ -9,7 +9,8 @@ import ResizeBar from '../resize-bar';
import { DRAG_HANDLER_HEIGHT, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE } from '../resize-bar/constants'; import { DRAG_HANDLER_HEIGHT, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE } from '../resize-bar/constants';
import { SeafileMetadata } from '../../metadata'; import { SeafileMetadata } from '../../metadata';
import { mediaUrl } from '../../utils/constants'; import { mediaUrl } from '../../utils/constants';
import { GRID_MODE, LIST_MODE, METADATA_MODE } from './constants'; import { GRID_MODE, LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE } from './constants';
import FaceRecognition from '../../metadata/views/face-recognition';
const propTypes = { const propTypes = {
isSidePanelFolded: PropTypes.bool, isSidePanelFolded: PropTypes.bool,
@@ -203,6 +204,9 @@ class DirColumnView extends React.Component {
renameFileCallback={this.props.renameFileCallback} renameFileCallback={this.props.renameFileCallback}
/> />
} }
{currentMode === FACE_RECOGNITION_MODE &&
<FaceRecognition repoID={this.props.repoID}/>
}
{currentMode === LIST_MODE && {currentMode === LIST_MODE &&
<DirListView <DirListView
path={this.props.path} path={this.props.path}

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
import TreeSection from '../tree-section'; import TreeSection from '../tree-section';
import { MetadataStatusManagementDialog, MetadataTreeView, useMetadata } from '../../metadata'; import { MetadataStatusManagementDialog, MetadataFaceRecognitionDialog, MetadataTreeView, useMetadata } from '../../metadata';
import ExtensionPrompts from './extension-prompts'; import ExtensionPrompts from './extension-prompts';
const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => { const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
@@ -12,18 +12,31 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
}, [window.app.pageOptions.enableMetadataManagement]); }, [window.app.pageOptions.enableMetadataManagement]);
const [showMetadataStatusManagementDialog, setShowMetadataStatusManagementDialog] = useState(false); const [showMetadataStatusManagementDialog, setShowMetadataStatusManagementDialog] = useState(false);
const { enableMetadata, updateEnableMetadata, navigation } = useMetadata(); const [showMetadataFaceRecognitionDialog, setShowMetadataFaceRecognitionDialog] = useState(false);
const { enableMetadata, updateEnableMetadata, enableFaceRecognition, updateEnableFaceRecognition, navigation } = useMetadata();
const moreOperations = useMemo(() => { const moreOperations = useMemo(() => {
if (!enableMetadataManagement || !currentRepoInfo.is_admin) return []; if (!enableMetadataManagement || !currentRepoInfo.is_admin) return [];
return [ let operations = [
{ key: 'extended-properties', value: gettext('Extended properties') } { key: 'extended-properties', value: gettext('Extended properties') }
]; ];
}, [enableMetadataManagement, currentRepoInfo]); if (enableMetadata) {
operations.push({ key: 'face-recognition', value: gettext('Face recognition') });
}
return operations;
}, [enableMetadataManagement, enableMetadata, currentRepoInfo]);
const moreOperationClick = useCallback((operationKey) => { const moreOperationClick = useCallback((operationKey) => {
if (operationKey === 'extended-properties') { switch (operationKey) {
setShowMetadataStatusManagementDialog(true); case 'extended-properties': {
return; setShowMetadataStatusManagementDialog(true);
break;
}
case 'face-recognition': {
setShowMetadataFaceRecognitionDialog(true);
break;
}
default:
break;
} }
}, []); }, []);
@@ -31,6 +44,14 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
setShowMetadataStatusManagementDialog(false); setShowMetadataStatusManagementDialog(false);
}, []); }, []);
const closeMetadataFaceRecognitionDialog = useCallback(() => {
setShowMetadataFaceRecognitionDialog(false);
}, []);
const openMetadataFaceRecognition = useCallback(() => {
updateEnableFaceRecognition(true);
}, [updateEnableFaceRecognition]);
const toggleMetadataStatus = useCallback((value) => { const toggleMetadataStatus = useCallback((value) => {
updateEnableMetadata(value); updateEnableMetadata(value);
}, [updateEnableMetadata]); }, [updateEnableMetadata]);
@@ -63,6 +84,14 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
submit={toggleMetadataStatus} submit={toggleMetadataStatus}
/> />
)} )}
{showMetadataFaceRecognitionDialog && (
<MetadataFaceRecognitionDialog
value={enableFaceRecognition}
repoID={repoID}
toggle={closeMetadataFaceRecognitionDialog}
submit={openMetadataFaceRecognition}
/>
)}
</> </>
); );
}; };

View File

@@ -52,9 +52,9 @@ class ItemDropdownMenu extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps) { // for toolbar item operation UNSAFE_componentWillReceiveProps(nextProps) { // for toolbar item operation
let { item } = nextProps; let { item } = nextProps;
if (item.name !== this.props.item.name) { const nextMenuList = nextProps.getMenuList(item);
let menuList = this.props.getMenuList(item); if (item.name !== this.props.item.name || this.state.menuList !== nextMenuList) {
this.setState({ menuList: menuList }); this.setState({ menuList: nextMenuList });
} }
} }

View File

@@ -4,7 +4,8 @@ import KeyCodes from './keyCodes';
export const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px) export const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)
export const PRIVATE_FILE_TYPE = { export const PRIVATE_FILE_TYPE = {
FILE_EXTENDED_PROPERTIES: '__file_extended_properties' FILE_EXTENDED_PROPERTIES: '__file_extended_properties',
FACE_RECOGNITION: '__face_recognition',
}; };
const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84', const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84',

View File

@@ -247,6 +247,32 @@ class MetadataManagerAPI {
}; };
return this.req.delete(url, { data }); return this.req.delete(url, { data });
} }
// face recognition
getFaceRecognitionStatus(repoID) {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-recognition/';
return this.req.get(url);
}
openFaceRecognition = (repoID) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-recognition/';
return this.req.post(url);
};
getFaceData = (repoID, start = 0, limit = 1000) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-records/?start=' + start + '&limit=' + limit;
return this.req.get(url);
};
updateFaceName = (repoID, recordID, name) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-record/';
const params = {
record_id: recordID,
name: name,
};
return this.req.put(url, params);
};
} }
const metadataAPI = new MetadataManagerAPI(); const metadataAPI = new MetadataManagerAPI();

View File

@@ -0,0 +1,51 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
import { gettext } from '../../../../utils/constants';
import metadataAPI from '../../../api';
import toaster from '../../../../components/toast';
import { Utils } from '../../../../utils/utils';
const MetadataFaceRecognitionDialog = ({ value, repoID, toggle, submit }) => {
const [submitting, setSubmitting] = useState(false);
const onToggle = useCallback(() => {
toggle();
}, [toggle]);
const onSubmit = useCallback(() => {
setSubmitting(true);
metadataAPI.openFaceRecognition(repoID).then(res => {
submit(true);
toggle();
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
setSubmitting(false);
});
}, [repoID, submit, toggle]);
return (
<Modal className="metadata-face-recognition-dialog" isOpen={true} toggle={onToggle}>
<ModalHeader toggle={onToggle}>{gettext('Face recognition')}</ModalHeader>
<ModalBody>
{value ? gettext('Face recognition enabled.') : gettext('Whether to enable face recognition?')}
</ModalBody>
{!value && (
<ModalFooter>
<Button color="secondary" onClick={onToggle}>{gettext('Cancel')}</Button>
<Button color="primary" disabled={submitting} onClick={onSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
)}
</Modal>
);
};
MetadataFaceRecognitionDialog.propTypes = {
value: PropTypes.bool.isRequired,
repoID: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
};
export default MetadataFaceRecognitionDialog;

View File

@@ -11,6 +11,7 @@ export const NOT_DISPLAY_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.FILE_DETAILS, PRIVATE_COLUMN_KEY.FILE_DETAILS,
PRIVATE_COLUMN_KEY.LOCATION, PRIVATE_COLUMN_KEY.LOCATION,
PRIVATE_COLUMN_KEY.IS_DIR, PRIVATE_COLUMN_KEY.IS_DIR,
PRIVATE_COLUMN_KEY.FACE_LINKS,
]; ];
export const VIEW_NOT_DISPLAY_COLUMN_KEYS = [ export const VIEW_NOT_DISPLAY_COLUMN_KEYS = [

View File

@@ -29,6 +29,7 @@ export const PRIVATE_COLUMN_KEY = {
CAPTURE_TIME: '_capture_time', CAPTURE_TIME: '_capture_time',
FILE_REVIEWER: '_reviewer', FILE_REVIEWER: '_reviewer',
OWNER: '_owner', OWNER: '_owner',
FACE_LINKS: '_face_links',
}; };
export const PRIVATE_COLUMN_KEYS = [ export const PRIVATE_COLUMN_KEYS = [
@@ -59,6 +60,7 @@ export const PRIVATE_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.CAPTURE_TIME, PRIVATE_COLUMN_KEY.CAPTURE_TIME,
PRIVATE_COLUMN_KEY.FILE_REVIEWER, PRIVATE_COLUMN_KEY.FILE_REVIEWER,
PRIVATE_COLUMN_KEY.OWNER, PRIVATE_COLUMN_KEY.OWNER,
PRIVATE_COLUMN_KEY.FACE_LINKS,
]; ];
export const EDITABLE_PRIVATE_COLUMN_KEYS = [ export const EDITABLE_PRIVATE_COLUMN_KEYS = [

View File

@@ -6,7 +6,7 @@ import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_CO
export const VIEW_TYPE = { export const VIEW_TYPE = {
TABLE: 'table', TABLE: 'table',
GALLERY: 'gallery' GALLERY: 'gallery',
}; };
export const VIEW_TYPE_ICON = { export const VIEW_TYPE_ICON = {

View File

@@ -15,8 +15,10 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
}, [window.app.pageOptions.enableMetadataManagement]); }, [window.app.pageOptions.enableMetadataManagement]);
const [enableMetadata, setEnableExtendedProperties] = useState(false); const [enableMetadata, setEnableExtendedProperties] = useState(false);
const [enableFaceRecognition, setEnableFaceRecognition] = useState(false);
const [showFirstView, setShowFirstView] = useState(false); const [showFirstView, setShowFirstView] = useState(false);
const [navigation, setNavigation] = useState([]); const [navigation, setNavigation] = useState([]);
const [staticView, setStaticView] = useState([]);
const [, setCount] = useState(0); const [, setCount] = useState(0);
const viewsMap = useRef({}); const viewsMap = useRef({});
@@ -55,12 +57,21 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
if (!newValue) { if (!newValue) {
hideMetadataView && hideMetadataView(); hideMetadataView && hideMetadataView();
cancelURLView(); cancelURLView();
setEnableFaceRecognition(false);
} else { } else {
setShowFirstView(true); setShowFirstView(true);
} }
setEnableExtendedProperties(newValue); setEnableExtendedProperties(newValue);
}, [enableMetadata, hideMetadataView, cancelURLView]); }, [enableMetadata, hideMetadataView, cancelURLView]);
const updateEnableFaceRecognition = useCallback((newValue) => {
if (newValue === enableFaceRecognition) return;
setEnableFaceRecognition(newValue);
if (newValue) {
toaster.success(gettext('Recognizing portraits. Please refresh the page later.'));
}
}, [enableFaceRecognition]);
// views // views
useEffect(() => { useEffect(() => {
if (enableMetadata) { if (enableMetadata) {
@@ -71,6 +82,11 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
viewsMap.current[view._id] = view; viewsMap.current[view._id] = view;
}); });
} }
viewsMap.current['_face_recognition'] = {
_id: '_face_recognition',
name: gettext('Photos - classfied by people'),
type: PRIVATE_FILE_TYPE.FACE_RECOGNITION,
};
setNavigation(navigation); setNavigation(navigation);
}).catch(error => { }).catch(error => {
const errorMsg = Utils.getErrorMsg(error); const errorMsg = Utils.getErrorMsg(error);
@@ -84,8 +100,31 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, enableMetadata]); }, [repoID, enableMetadata]);
useEffect(() => {
if (!enableMetadata) {
setStaticView([]);
setEnableFaceRecognition(false);
return;
}
metadataAPI.getFaceRecognitionStatus(repoID).then(res => {
setEnableFaceRecognition(res.data.enabled);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
});
}, [repoID, enableMetadata]);
useEffect(() => {
if (!enableFaceRecognition) {
setStaticView([]);
return;
}
setStaticView([{ _id: '_face_recognition', type: 'view' }]);
}, [enableFaceRecognition]);
const selectView = useCallback((view, isSelected) => { const selectView = useCallback((view, isSelected) => {
if (isSelected) return; if (isSelected) return;
const isFaceRecognitionView = view.type === PRIVATE_FILE_TYPE.FACE_RECOGNITION;
const node = { const node = {
children: [], children: [],
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id, path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id,
@@ -94,9 +133,9 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
isPreload: true, isPreload: true,
object: { object: {
file_tags: [], file_tags: [],
id: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES, id: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
name: gettext('File extended properties'), name: isFaceRecognitionView ? gettext('Photos - classfied by people') : gettext('File extended properties'),
type: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES, type: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
isDir: () => false, isDir: () => false,
}, },
parentNode: {}, parentNode: {},
@@ -177,9 +216,12 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
<MetadataContext.Provider value={{ <MetadataContext.Provider value={{
enableMetadata, enableMetadata,
updateEnableMetadata, updateEnableMetadata,
enableFaceRecognition,
updateEnableFaceRecognition,
showFirstView, showFirstView,
setShowFirstView, setShowFirstView,
navigation, navigation,
staticView,
viewsMap: viewsMap.current, viewsMap: viewsMap.current,
selectView, selectView,
addView, addView,

View File

@@ -1,5 +1,6 @@
import SeafileMetadata, { Context as MetadataContext } from './metadata-view'; import SeafileMetadata, { Context as MetadataContext } from './metadata-view';
import MetadataStatusManagementDialog from './components/dialog/metadata-status-manage-dialog'; import MetadataStatusManagementDialog from './components/dialog/metadata-status-manage-dialog';
import MetadataFaceRecognitionDialog from './components/dialog/metadata-face-recognition-dialog';
import MetadataDetails from './components/metadata-details'; import MetadataDetails from './components/metadata-details';
import MetadataTreeView from './metadata-tree-view'; import MetadataTreeView from './metadata-tree-view';
import metadataAPI from './api'; import metadataAPI from './api';
@@ -11,6 +12,7 @@ export {
MetadataContext, MetadataContext,
SeafileMetadata, SeafileMetadata,
MetadataStatusManagementDialog, MetadataStatusManagementDialog,
MetadataFaceRecognitionDialog,
MetadataTreeView, MetadataTreeView,
MetadataDetails, MetadataDetails,
}; };

View File

@@ -38,8 +38,10 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
}, [userPerm]); }, [userPerm]);
const [, setState] = useState(0); const [, setState] = useState(0);
const { const {
enableFaceRecognition,
showFirstView, showFirstView,
navigation, navigation,
staticView,
viewsMap, viewsMap,
selectView, selectView,
addView, addView,
@@ -196,6 +198,20 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
/> />
</div> </div>
)} )}
{enableFaceRecognition && staticView.map((item) => {
const view = viewsMap[item._id];
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id;
const isSelected = currentPath === viewPath;
return (
<ViewItem
key={view._id}
userPerm="r"
view={view}
isSelected={isSelected}
onClick={(view) => selectView(view, isSelected)}
/>
);
})}
{canAdd && ( {canAdd && (
<div id="sf-metadata-view-popover"> <div id="sf-metadata-view-popover">
<CustomizeAddTool <CustomizeAddTool

View File

@@ -207,6 +207,8 @@ export const getColumnDisplayName = (key, name) => {
return gettext('Capture time'); return gettext('Capture time');
case PRIVATE_COLUMN_KEY.OWNER: case PRIVATE_COLUMN_KEY.OWNER:
return gettext('File owner'); return gettext('File owner');
case PRIVATE_COLUMN_KEY.FACE_FEATURES:
return gettext('Face Features');
default: default:
return name; return name;
} }

View File

@@ -0,0 +1,110 @@
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Input } from 'reactstrap';
import toaster from '../../../components/toast';
import { Utils } from '../../../utils/utils';
import { gettext, siteRoot } from '../../../utils/constants';
import metadataAPI from '../../api';
import isHotkey from 'is-hotkey';
import { isEnter } from '../../utils/hotkey';
const theadData = [
{ width: '5%', text: '' },
{ width: '39%', text: gettext('Name') },
{ width: '34%', text: gettext('Original path') },
{ width: '11%', text: gettext('Size') },
{ width: '11%', text: gettext('Last Update') },
];
const FaceGroup = ({ repoID, group, onPhotoClick }) => {
const [name, setName] = useState(group.name);
const [isRenaming, setRenaming] = useState(false);
const serverName = useRef(group.name);
const showPhoto = useCallback((event, photo) => {
event.preventDefault();
onPhotoClick(photo);
}, [onPhotoClick]);
const changeName = useCallback((event) => {
const value = event.target.value;
if (name === value) return;
setName(value);
}, [name]);
const renameName = useCallback(() => {
setRenaming(true);
}, []);
const updateName = useCallback(() => {
if (name === serverName.current) {
setRenaming(false);
return;
}
metadataAPI.updateFaceName(repoID, group.record_id, name).then(res => {
serverName.current = name;
setRenaming(false);
}).catch(err => {
const errorMsg = Utils.getErrorMsg(err);
toaster.danger(errorMsg);
setName(serverName.current);
setRenaming(false);
});
}, [repoID, group, name]);
const onRenameKeyDown = useCallback((event) => {
if (isEnter(event)) {
updateName();
} else if (isHotkey('esc', event)) {
setName(serverName.current);
setRenaming(false);
}
}, [updateName]);
return (
<div key={group.record_id} className="sf-metadata-face-recognition-item">
{isRenaming ?
(<Input
autoFocus
value={name}
onChange={changeName}
onBlur={updateName}
onKeyDown={onRenameKeyDown}
/>)
:
(<div className="sf-metadata-face-recognition-name form-control" onClick={renameName}>{name}</div>)
}
<table className="table-hover">
<thead>
<tr>
{theadData.map((item, index) => {
return <th key={index} width={item.width}>{item.text}</th>;
})}
</tr>
</thead>
<tbody>
{group.photos.map((photo, index) => {
return (
<tr key={index} onClick={(event) => showPhoto(event, photo)}>
<td className="text-center"><img src={photo.src} alt="" className="thumbnail cursor-pointer" /></td>
<td><a href={`${siteRoot}lib/${repoID}/file${photo.path}`} onClick={(event) => showPhoto(event, photo)}>{photo.file_name}</a></td>
<td>{photo.parent_dir}</td>
<td>{Utils.bytesToSize(photo.size)}</td>
<td title={moment(photo.mtime).fromNow()}>{moment(photo.mtime).fromNow()}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
FaceGroup.propTypes = {
repoID: PropTypes.string,
group: PropTypes.object.isRequired,
onPhotoClick: PropTypes.func,
};
export default FaceGroup;

View File

@@ -0,0 +1,29 @@
.sf-metadata-face-recognition {
height: 100%;
width: 100%;
padding: 16px;
overflow-x: hidden;
overflow-y: scroll;
}
.sf-metadata-face-recognition .sf-metadata-face-recognition-item {
margin-bottom: 16px;
}
.sf-metadata-face-recognition .sf-metadata-face-recognition-item:last-child {
margin-bottom: 0;
}
.sf-metadata-face-recognition .sf-metadata-face-recognition-name {
border-color: transparent;
cursor: pointer;
}
.sf-metadata-face-recognition .sf-metadata-face-recognition-loading-more {
height: 30px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}

View File

@@ -0,0 +1,161 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
import toaster from '../../../components/toast';
import { Utils } from '../../../utils/utils';
import metadataAPI from '../../api';
import FaceGroup from './face-group';
import ImageDialog from '../../../components/dialog/image-dialog';
import ModalPortal from '../../../components/modal-portal';
import { siteRoot, gettext, thumbnailSizeForOriginal, thumbnailDefaultSize } from '../../../utils/constants';
import './index.css';
const LIMIT = 1000;
const FaceRecognition = ({ repoID }) => {
const [loading, setLoading] = useState(true);
const [faceOriginData, setFaceOriginData] = useState([]);
const [isLoadingMore, setLoadingMore] = useState(false);
const [isImagePopupOpen, setIsImagePopupOpen] = useState(false);
const [imageIndex, setImageIndex] = useState(-1);
const containerRef = useRef(null);
const hasMore = useRef(true);
const faceData = useMemo(() => {
if (!Array.isArray(faceOriginData) || faceOriginData.length === 0) return [];
const data = faceOriginData.map(dataItem => {
const { record_id, link_photos } = dataItem;
const linkPhotos = link_photos || [];
const name = dataItem.name || gettext('Person Image');
return {
record_id: record_id,
name: name || gettext('Person Image'),
photos: linkPhotos.map(photo => {
const { path } = photo;
return {
...photo,
name: photo.file_name,
url: `${siteRoot}lib/${repoID}/file${path}`,
default_url: `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}${path}`,
src: `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}${path}`,
thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`,
};
}),
};
});
return data;
}, [repoID, faceOriginData]);
const imageItems = useMemo(() => {
return faceData.map(group => group.photos).flat();
}, [faceData]);
useEffect(() => {
setLoading(true);
metadataAPI.getFaceData(repoID, 0, LIMIT).then(res => {
const faceOriginData = res.data.results || [];
if (faceOriginData.length < LIMIT) {
hasMore.current = false;
}
setFaceOriginData(faceOriginData);
setLoading(false);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
setLoading(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const loadMore = useCallback(() => {
if (!hasMore.current) return;
setLoadingMore(true);
metadataAPI.getFaceData(repoID, faceOriginData.length, LIMIT).then(res => {
const newFaceData = res.data.results || [];
if (newFaceData.length < LIMIT) {
hasMore.current = false;
}
setFaceOriginData([...faceOriginData, ...newFaceData]);
setLoadingMore(false);
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
setLoadingMore(false);
});
}, [repoID, faceOriginData]);
const handleScroll = useCallback(() => {
if (!containerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
if (scrollTop + clientHeight >= scrollHeight - 10) {
loadMore();
}
}, [loadMore]);
const onPhotoClick = useCallback((photo) => {
let imageIndex = imageItems.findIndex(item => item.url === photo.url);
if (imageIndex < 0) imageIndex = 0;
setImageIndex(imageIndex);
setIsImagePopupOpen(true);
}, [imageItems]);
const closeImagePopup = useCallback(() => {
setIsImagePopupOpen(false);
setImageIndex(-1);
}, []);
const moveToPrevImage = useCallback(() => {
let prevImageIndex = imageIndex - 1;
if (prevImageIndex < 0) prevImageIndex = imageItems.length - 1;
setImageIndex(prevImageIndex);
}, [imageIndex, imageItems]);
const moveToNextImage = useCallback(() => {
let nextImageIndex = imageIndex + 1;
if (nextImageIndex > imageItems.length - 1) nextImageIndex = 0;
setImageIndex(nextImageIndex);
}, [imageIndex, imageItems]);
if (loading) {
return (<CenteredLoading />);
}
return (
<>
<div className="sf-metadata-wrapper">
<div className="sf-metadata-main">
<div className="sf-metadata-container">
<div className="sf-metadata-face-recognition" ref={containerRef} onScroll={handleScroll}>
{faceData.length > 0 && faceData.map((face) => {
return (<FaceGroup key={face.record_id} group={face} repoID={repoID} onPhotoClick={onPhotoClick} />);
})}
{isLoadingMore && (
<div className="sf-metadata-face-recognition-loading-more">
<CenteredLoading />
</div>
)}
</div>
</div>
</div>
</div>
{isImagePopupOpen && (
<ModalPortal>
<ImageDialog
imageItems={imageItems}
imageIndex={imageIndex}
closeImagePopup={closeImagePopup}
moveToPrevImage={moveToPrevImage}
moveToNextImage={moveToNextImage}
/>
</ModalPortal>
)}
</>
);
};
FaceRecognition.propTypes = {
repoID: PropTypes.string.isRequired,
};
export default FaceRecognition;

View File

@@ -340,17 +340,17 @@ const Gallery = () => {
onDownload={handleDownload} onDownload={handleDownload}
onDelete={handleDelete} onDelete={handleDelete}
/> />
{isImagePopupOpen && {isImagePopupOpen && (
<ModalPortal> <ModalPortal>
<ImageDialog <ImageDialog
imageItems={imageItems} imageItems={imageItems}
imageIndex={imageIndex} imageIndex={imageIndex}
closeImagePopup={closeImagePopup} closeImagePopup={closeImagePopup}
moveToPrevImage={moveToPrevImage} moveToPrevImage={moveToPrevImage}
moveToNextImage={moveToNextImage} moveToNextImage={moveToNextImage}
/> />
</ModalPortal> </ModalPortal>
} )}
{isZipDialogOpen && {isZipDialogOpen &&
<ModalPortal> <ModalPortal>
<ZipDownloadDialog <ZipDownloadDialog

View File

@@ -22,7 +22,7 @@ import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type'; import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
import { PRIVATE_FILE_TYPE } from '../../constants'; import { PRIVATE_FILE_TYPE } from '../../constants';
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks'; import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
import { LIST_MODE, METADATA_MODE } from '../../components/dir-view-mode/constants'; import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE } from '../../components/dir-view-mode/constants';
import CurDirPath from '../../components/cur-dir-path'; import CurDirPath from '../../components/cur-dir-path';
import DirTool from '../../components/cur-dir-path/dir-tool'; import DirTool from '../../components/cur-dir-path/dir-tool';
import DetailContainer from '../../components/dirent-detail/detail-container'; import DetailContainer from '../../components/dirent-detail/detail-container';
@@ -555,6 +555,19 @@ class LibContentView extends React.Component {
window.history.pushState({ url: url, path: '' }, '', url); window.history.pushState({ url: url, path: '' }, '', url);
}; };
showFaceRecognition = (filePath, viewId) => {
const repoID = this.props.repoID;
const repoInfo = this.state.currentRepoInfo;
this.setState({
currentMode: FACE_RECOGNITION_MODE,
path: filePath,
viewId: viewId,
isDirentDetailShow: false
});
const url = `${siteRoot}library/${repoID}/${encodeURIComponent(repoInfo.repo_name)}/?view=${encodeURIComponent(viewId)}`;
window.history.pushState({ url: url, path: '' }, '', url);
};
hideFileMetadata = () => { hideFileMetadata = () => {
this.setState({ this.setState({
currentMode: LIST_MODE, currentMode: LIST_MODE,
@@ -1890,6 +1903,10 @@ class LibContentView extends React.Component {
if (node.path !== this.state.path) { if (node.path !== this.state.path) {
this.showFileMetadata(node.path, node.view_id || '0000'); this.showFileMetadata(node.path, node.view_id || '0000');
} }
} else if (Utils.isFaceRecognition(node?.object?.type)) {
if (node.path !== this.state.path) {
this.showFaceRecognition(node.path, node.view_id || '0000');
}
} else { } else {
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path); let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
let dirent = node.object; let dirent = node.object;
@@ -2027,7 +2044,7 @@ class LibContentView extends React.Component {
isDirentSelected: false, isDirentSelected: false,
isAllDirentSelected: false, isAllDirentSelected: false,
}); });
if (this.state.currentMode === METADATA_MODE) { if (this.state.currentMode === METADATA_MODE || this.state.currentMode === FACE_RECOGNITION_MODE) {
this.setState({ this.setState({
currentMode: cookie.load('seafile_view_mode') || LIST_MODE, currentMode: cookie.load('seafile_view_mode') || LIST_MODE,
}); });

View File

@@ -154,6 +154,10 @@ export const Utils = {
} }
}, },
isFaceRecognition: function (type) {
return type === PRIVATE_FILE_TYPE.FACE_RECOGNITION;
},
getShareLinkPermissionList: function (itemType, permission, path, canEdit) { getShareLinkPermissionList: function (itemType, permission, path, canEdit) {
// itemType: library, dir, file // itemType: library, dir, file
// permission: rw, r, admin, cloud-edit, preview, custom-* // permission: rw, r, admin, cloud-edit, preview, custom-*

View File

@@ -1,4 +1,5 @@
import logging import logging
import os
from datetime import datetime from datetime import datetime
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
@@ -12,7 +13,7 @@ from seahub.api2.authentication import TokenAuthentication
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
from seahub.views import check_folder_permission from seahub.views import check_folder_permission
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \ from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
get_unmodifiable_columns, can_read_metadata get_unmodifiable_columns, can_read_metadata, init_faces, add_init_face_recognition_task, get_metadata_by_faces
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.utils.timeutils import datetime_to_isoformat_timestr
from seahub.utils.repo import is_repo_admin from seahub.utils.repo import is_repo_admin
@@ -815,3 +816,248 @@ class MetadataViewsMoveView(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'navigation': results['navigation']}) return Response({'navigation': results['navigation']})
class FacesRecords(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request, repo_id):
start = request.GET.get('start', 0)
limit = request.GET.get('limit', 100)
try:
start = int(start)
limit = int(limit)
except:
start = 0
limit = 1000
if start < 0:
error_msg = 'start invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
if limit < 0:
error_msg = 'limit invalid'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not metadata or not metadata.enabled:
error_msg = f'The metadata module is disabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
permission = check_folder_permission(request, repo_id, '/')
if not permission:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
try:
metadata = metadata_server_api.get_metadata()
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
tables = metadata.get('tables', [])
faces_table_id = [table['id'] for table in tables if table['name'] == FACES_TABLE.name]
faces_table_id = faces_table_id[0] if faces_table_id else None
if not faces_table_id:
return api_error(status.HTTP_404_NOT_FOUND, 'face recognition not be used')
sql = f'SELECT * FROM `{FACES_TABLE.name}` WHERE `{FACES_TABLE.columns.photo_links.name}` IS NOT NULL LIMIT {start}, {limit}'
try:
query_result = metadata_server_api.query_rows(sql)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
faces = query_result.get('results')
if not faces:
error_msg = 'Records not found'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
query_result = get_metadata_by_faces(faces, metadata_server_api)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
if not query_result:
error_msg = 'Records not found'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
classify_result = dict()
for row in query_result:
link_row_ids = [item['row_id'] for item in row.get(METADATA_TABLE.columns.face_links.name, [])]
if not link_row_ids:
continue
for link_row_id in link_row_ids:
if link_row_id not in classify_result:
classify_result[link_row_id] = []
file_name = row.get(METADATA_TABLE.columns.file_name.name, '')
parent_dir = row.get(METADATA_TABLE.columns.parent_dir.name, '')
size = row.get(METADATA_TABLE.columns.size.name, 0)
mtime = row.get('_mtime')
classify_result[link_row_id].append({
'path': os.path.join(parent_dir, file_name),
'file_name': file_name,
'parent_dir': parent_dir,
'size': size,
'mtime': mtime
})
id_to_name = {item.get(FACES_TABLE.columns.id.name): item.get(FACES_TABLE.columns.name.name, '') for item in faces}
classify_result = [{
'record_id': key,
'name': id_to_name.get(key, ''),
'link_photos': value
} for key, value in classify_result.items()]
return Response({'results': classify_result})
class FacesRecord(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def put(self, request, repo_id):
name = request.data.get('name')
record_id = request.data.get('record_id')
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not metadata or not metadata.enabled:
error_msg = f'The metadata module is not enabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
if not is_repo_admin(request.user.username, repo_id):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
from seafevents.repo_metadata.utils import FACES_TABLE
try:
metadata = metadata_server_api.get_metadata()
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
tables = metadata.get('tables', [])
faces_table_id = [table['id'] for table in tables if table['name'] == FACES_TABLE.name]
faces_table_id = faces_table_id[0] if faces_table_id else None
if not faces_table_id:
return api_error(status.HTTP_404_NOT_FOUND, 'face recognition not be used')
sql = f'SELECT * FROM `{FACES_TABLE.name}` WHERE `{FACES_TABLE.columns.id.name}` = "{record_id}"'
try:
results = metadata_server_api.query_rows(sql).get('results', [])
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
if not results:
error_msg = 'Record not found'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
update_row = {
FACES_TABLE.columns.id.name: record_id,
FACES_TABLE.columns.name.name: name,
}
try:
metadata_server_api.update_rows(faces_table_id, [update_row])
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'success': True})
class FaceRecognitionManage(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, )
throttle_classes = (UserRateThrottle, )
def get(self, request, repo_id):
# recource check
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not metadata or not metadata.enabled:
error_msg = f'The metadata module is not enabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
if not can_read_metadata(request, repo_id):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
from seafevents.repo_metadata.utils import FACES_TABLE
try:
metadata = metadata_server_api.get_metadata()
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
tables = metadata.get('tables', [])
faces_table_id = [table['id'] for table in tables if table['name'] == FACES_TABLE.name]
is_enabled = True if faces_table_id else False
return Response({'enabled': is_enabled})
def post(self, request, repo_id):
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not metadata or not metadata.enabled:
error_msg = f'The metadata module is not enabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# resource check
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
if not is_repo_admin(request.user.username, repo_id):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
params = {
'repo_id': repo_id,
}
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
init_faces(metadata_server_api)
try:
task_id = add_init_face_recognition_task(params=params)
except Exception as e:
logger.error(e)
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
return Response({'task_id': task_id})

View File

@@ -171,3 +171,16 @@ class MetadataServerAPI:
} }
response = requests.put(url, json=data, headers=self.headers, timeout=self.timeout) response = requests.put(url, json=data, headers=self.headers, timeout=self.timeout)
return parse_response(response) return parse_response(response)
def create_table(self, table_name):
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/tables'
data = {
'name': table_name,
}
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
return parse_response(response)
def get_metadata(self):
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/metadata'
response = requests.get(url, headers=self.headers, timeout=self.timeout)
return parse_response(response)

View File

@@ -20,6 +20,42 @@ def add_init_metadata_task(params):
return json.loads(resp.content)['task_id'] return json.loads(resp.content)['task_id']
def add_init_face_recognition_task(params):
payload = {'exp': int(time.time()) + 300, }
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
headers = {"Authorization": "Token %s" % token}
url = urljoin(SEAFEVENTS_SERVER_URL, '/add-init-face-recognition-task')
resp = requests.get(url, params=params, headers=headers)
return json.loads(resp.content)['task_id']
def get_metadata_by_faces(faces, metadata_server_api):
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
parameters = []
query_result = []
for face in faces:
link_row_ids = [item['row_id'] for item in face.get(FACES_TABLE.columns.photo_links.name, [])]
if not link_row_ids:
continue
for link_row_id in link_row_ids:
sql += '?, '
parameters.append(link_row_id)
if len(parameters) >= 10000:
sql = sql.rstrip(', ') + ');'
results = metadata_server_api.query_rows(sql, parameters).get('results', [])
query_result.extend(results)
sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
parameters = []
if parameters:
sql = sql.rstrip(', ') + ');'
results = metadata_server_api.query_rows(sql, parameters).get('results', [])
query_result.extend(results)
return query_result
def generator_base64_code(length=4): def generator_base64_code(length=4):
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789' possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'
ids = random.sample(possible, length) ids = random.sample(possible, length)
@@ -57,6 +93,36 @@ def get_sys_columns():
return columns return columns
def get_link_column(face_table_id):
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
columns = [
METADATA_TABLE.columns.face_links.to_dict({
'link_id': FACES_TABLE.link_id,
'table_id': METADATA_TABLE.id,
'other_table_id': face_table_id,
'display_column_key': FACES_TABLE.columns.name.key,
}),
]
return columns
def get_face_columns(face_table_id):
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
columns = [
FACES_TABLE.columns.photo_links.to_dict({
'link_id': FACES_TABLE.link_id,
'table_id': METADATA_TABLE.id,
'other_table_id': face_table_id,
'display_column_key': METADATA_TABLE.columns.obj_id.key,
}),
FACES_TABLE.columns.vector.to_dict(),
FACES_TABLE.columns.name.to_dict(),
]
return columns
def get_unmodifiable_columns(): def get_unmodifiable_columns():
from seafevents.repo_metadata.utils import METADATA_TABLE from seafevents.repo_metadata.utils import METADATA_TABLE
columns = [ columns = [
@@ -90,6 +156,20 @@ def init_metadata(metadata_server_api):
metadata_server_api.add_columns(METADATA_TABLE.id, sys_columns) metadata_server_api.add_columns(METADATA_TABLE.id, sys_columns)
def init_faces(metadata_server_api):
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
resp = metadata_server_api.create_table(FACES_TABLE.name)
# init link column
link_column = get_link_column(resp['id'])
metadata_server_api.add_columns(METADATA_TABLE.id, link_column)
# init face column
face_columns = get_face_columns(resp['id'])
metadata_server_api.add_columns(resp['id'], face_columns)
def get_file_download_token(repo_id, file_id, username): def get_file_download_token(repo_id, file_id, username):
return seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', username, use_onetime=True) return seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', username, use_onetime=True)

View File

@@ -211,7 +211,8 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView,
Wiki2DuplicatePageView, WikiPageTrashView, Wiki2PublishView, Wiki2PublishConfigView, Wiki2PublishPageView Wiki2DuplicatePageView, WikiPageTrashView, Wiki2PublishView, Wiki2PublishConfigView, Wiki2PublishPageView
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \ from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
FaceRecognitionManage, FacesRecord
from seahub.api2.endpoints.user_list import UserListView from seahub.api2.endpoints.user_list import UserListView
from seahub.api2.endpoints.seahub_io import SeahubIOStatus from seahub.api2.endpoints.seahub_io import SeahubIOStatus
@@ -1052,6 +1053,9 @@ if settings.ENABLE_METADATA_MANAGEMENT:
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/(?P<view_id>[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'), re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/(?P<view_id>[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'), re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/duplicate-view/$', MetadataViewsDuplicateView.as_view(), name='api-v2.1-metadata-view-duplicate'), re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/duplicate-view/$', MetadataViewsDuplicateView.as_view(), name='api-v2.1-metadata-view-duplicate'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-record/$', FacesRecord.as_view(), name='api-v2.1-metadata-face-record'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-records/$', FacesRecords.as_view(), name='api-v2.1-metadata-face-records'),
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'),
] ]
# ai API # ai API