mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 00:43:53 +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:
@@ -1,3 +1,4 @@
|
||||
export const LIST_MODE = 'list';
|
||||
export const GRID_MODE = 'grid';
|
||||
export const METADATA_MODE = 'metadata';
|
||||
export const FACE_RECOGNITION_MODE = 'person_image';
|
||||
|
@@ -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 { SeafileMetadata } from '../../metadata';
|
||||
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 = {
|
||||
isSidePanelFolded: PropTypes.bool,
|
||||
@@ -203,6 +204,9 @@ class DirColumnView extends React.Component {
|
||||
renameFileCallback={this.props.renameFileCallback}
|
||||
/>
|
||||
}
|
||||
{currentMode === FACE_RECOGNITION_MODE &&
|
||||
<FaceRecognition repoID={this.props.repoID}/>
|
||||
}
|
||||
{currentMode === LIST_MODE &&
|
||||
<DirListView
|
||||
path={this.props.path}
|
||||
|
@@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import TreeSection from '../tree-section';
|
||||
import { MetadataStatusManagementDialog, MetadataTreeView, useMetadata } from '../../metadata';
|
||||
import { MetadataStatusManagementDialog, MetadataFaceRecognitionDialog, MetadataTreeView, useMetadata } from '../../metadata';
|
||||
import ExtensionPrompts from './extension-prompts';
|
||||
|
||||
const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
||||
@@ -12,18 +12,31 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
||||
}, [window.app.pageOptions.enableMetadataManagement]);
|
||||
|
||||
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(() => {
|
||||
if (!enableMetadataManagement || !currentRepoInfo.is_admin) return [];
|
||||
return [
|
||||
let operations = [
|
||||
{ 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) => {
|
||||
if (operationKey === 'extended-properties') {
|
||||
switch (operationKey) {
|
||||
case 'extended-properties': {
|
||||
setShowMetadataStatusManagementDialog(true);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
case 'face-recognition': {
|
||||
setShowMetadataFaceRecognitionDialog(true);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -31,6 +44,14 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
||||
setShowMetadataStatusManagementDialog(false);
|
||||
}, []);
|
||||
|
||||
const closeMetadataFaceRecognitionDialog = useCallback(() => {
|
||||
setShowMetadataFaceRecognitionDialog(false);
|
||||
}, []);
|
||||
|
||||
const openMetadataFaceRecognition = useCallback(() => {
|
||||
updateEnableFaceRecognition(true);
|
||||
}, [updateEnableFaceRecognition]);
|
||||
|
||||
const toggleMetadataStatus = useCallback((value) => {
|
||||
updateEnableMetadata(value);
|
||||
}, [updateEnableMetadata]);
|
||||
@@ -63,6 +84,14 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
||||
submit={toggleMetadataStatus}
|
||||
/>
|
||||
)}
|
||||
{showMetadataFaceRecognitionDialog && (
|
||||
<MetadataFaceRecognitionDialog
|
||||
value={enableFaceRecognition}
|
||||
repoID={repoID}
|
||||
toggle={closeMetadataFaceRecognitionDialog}
|
||||
submit={openMetadataFaceRecognition}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -52,9 +52,9 @@ class ItemDropdownMenu extends React.Component {
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // for toolbar item operation
|
||||
let { item } = nextProps;
|
||||
if (item.name !== this.props.item.name) {
|
||||
let menuList = this.props.getMenuList(item);
|
||||
this.setState({ menuList: menuList });
|
||||
const nextMenuList = nextProps.getMenuList(item);
|
||||
if (item.name !== this.props.item.name || this.state.menuList !== nextMenuList) {
|
||||
this.setState({ menuList: nextMenuList });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,8 @@ import KeyCodes from './keyCodes';
|
||||
export const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)
|
||||
|
||||
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',
|
||||
|
@@ -247,6 +247,32 @@ class MetadataManagerAPI {
|
||||
};
|
||||
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();
|
||||
|
@@ -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;
|
@@ -11,6 +11,7 @@ export const NOT_DISPLAY_COLUMN_KEYS = [
|
||||
PRIVATE_COLUMN_KEY.FILE_DETAILS,
|
||||
PRIVATE_COLUMN_KEY.LOCATION,
|
||||
PRIVATE_COLUMN_KEY.IS_DIR,
|
||||
PRIVATE_COLUMN_KEY.FACE_LINKS,
|
||||
];
|
||||
|
||||
export const VIEW_NOT_DISPLAY_COLUMN_KEYS = [
|
||||
|
@@ -29,6 +29,7 @@ export const PRIVATE_COLUMN_KEY = {
|
||||
CAPTURE_TIME: '_capture_time',
|
||||
FILE_REVIEWER: '_reviewer',
|
||||
OWNER: '_owner',
|
||||
FACE_LINKS: '_face_links',
|
||||
};
|
||||
|
||||
export const PRIVATE_COLUMN_KEYS = [
|
||||
@@ -59,6 +60,7 @@ export const PRIVATE_COLUMN_KEYS = [
|
||||
PRIVATE_COLUMN_KEY.CAPTURE_TIME,
|
||||
PRIVATE_COLUMN_KEY.FILE_REVIEWER,
|
||||
PRIVATE_COLUMN_KEY.OWNER,
|
||||
PRIVATE_COLUMN_KEY.FACE_LINKS,
|
||||
];
|
||||
|
||||
export const EDITABLE_PRIVATE_COLUMN_KEYS = [
|
||||
|
@@ -6,7 +6,7 @@ import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_CO
|
||||
|
||||
export const VIEW_TYPE = {
|
||||
TABLE: 'table',
|
||||
GALLERY: 'gallery'
|
||||
GALLERY: 'gallery',
|
||||
};
|
||||
|
||||
export const VIEW_TYPE_ICON = {
|
||||
|
@@ -15,8 +15,10 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
}, [window.app.pageOptions.enableMetadataManagement]);
|
||||
|
||||
const [enableMetadata, setEnableExtendedProperties] = useState(false);
|
||||
const [enableFaceRecognition, setEnableFaceRecognition] = useState(false);
|
||||
const [showFirstView, setShowFirstView] = useState(false);
|
||||
const [navigation, setNavigation] = useState([]);
|
||||
const [staticView, setStaticView] = useState([]);
|
||||
const [, setCount] = useState(0);
|
||||
const viewsMap = useRef({});
|
||||
|
||||
@@ -55,12 +57,21 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
if (!newValue) {
|
||||
hideMetadataView && hideMetadataView();
|
||||
cancelURLView();
|
||||
setEnableFaceRecognition(false);
|
||||
} else {
|
||||
setShowFirstView(true);
|
||||
}
|
||||
setEnableExtendedProperties(newValue);
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
if (enableMetadata) {
|
||||
@@ -71,6 +82,11 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
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);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
@@ -84,8 +100,31 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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) => {
|
||||
if (isSelected) return;
|
||||
const isFaceRecognitionView = view.type === PRIVATE_FILE_TYPE.FACE_RECOGNITION;
|
||||
const node = {
|
||||
children: [],
|
||||
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id,
|
||||
@@ -94,9 +133,9 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
isPreload: true,
|
||||
object: {
|
||||
file_tags: [],
|
||||
id: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
||||
name: gettext('File extended properties'),
|
||||
type: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
||||
id: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
||||
name: isFaceRecognitionView ? gettext('Photos - classfied by people') : gettext('File extended properties'),
|
||||
type: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
||||
isDir: () => false,
|
||||
},
|
||||
parentNode: {},
|
||||
@@ -177,9 +216,12 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
||||
<MetadataContext.Provider value={{
|
||||
enableMetadata,
|
||||
updateEnableMetadata,
|
||||
enableFaceRecognition,
|
||||
updateEnableFaceRecognition,
|
||||
showFirstView,
|
||||
setShowFirstView,
|
||||
navigation,
|
||||
staticView,
|
||||
viewsMap: viewsMap.current,
|
||||
selectView,
|
||||
addView,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import SeafileMetadata, { Context as MetadataContext } from './metadata-view';
|
||||
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 MetadataTreeView from './metadata-tree-view';
|
||||
import metadataAPI from './api';
|
||||
@@ -11,6 +12,7 @@ export {
|
||||
MetadataContext,
|
||||
SeafileMetadata,
|
||||
MetadataStatusManagementDialog,
|
||||
MetadataFaceRecognitionDialog,
|
||||
MetadataTreeView,
|
||||
MetadataDetails,
|
||||
};
|
||||
|
@@ -38,8 +38,10 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
}, [userPerm]);
|
||||
const [, setState] = useState(0);
|
||||
const {
|
||||
enableFaceRecognition,
|
||||
showFirstView,
|
||||
navigation,
|
||||
staticView,
|
||||
viewsMap,
|
||||
selectView,
|
||||
addView,
|
||||
@@ -196,6 +198,20 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
/>
|
||||
</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 && (
|
||||
<div id="sf-metadata-view-popover">
|
||||
<CustomizeAddTool
|
||||
|
@@ -207,6 +207,8 @@ export const getColumnDisplayName = (key, name) => {
|
||||
return gettext('Capture time');
|
||||
case PRIVATE_COLUMN_KEY.OWNER:
|
||||
return gettext('File owner');
|
||||
case PRIVATE_COLUMN_KEY.FACE_FEATURES:
|
||||
return gettext('Face Features');
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
|
110
frontend/src/metadata/views/face-recognition/face-group.js
Normal file
110
frontend/src/metadata/views/face-recognition/face-group.js
Normal 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;
|
29
frontend/src/metadata/views/face-recognition/index.css
Normal file
29
frontend/src/metadata/views/face-recognition/index.css
Normal 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;
|
||||
}
|
161
frontend/src/metadata/views/face-recognition/index.js
Normal file
161
frontend/src/metadata/views/face-recognition/index.js
Normal 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;
|
@@ -340,7 +340,7 @@ const Gallery = () => {
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
{isImagePopupOpen &&
|
||||
{isImagePopupOpen && (
|
||||
<ModalPortal>
|
||||
<ImageDialog
|
||||
imageItems={imageItems}
|
||||
@@ -350,7 +350,7 @@ const Gallery = () => {
|
||||
moveToNextImage={moveToNextImage}
|
||||
/>
|
||||
</ModalPortal>
|
||||
}
|
||||
)}
|
||||
{isZipDialogOpen &&
|
||||
<ModalPortal>
|
||||
<ZipDownloadDialog
|
||||
|
@@ -22,7 +22,7 @@ import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
|
||||
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
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 DirTool from '../../components/cur-dir-path/dir-tool';
|
||||
import DetailContainer from '../../components/dirent-detail/detail-container';
|
||||
@@ -555,6 +555,19 @@ class LibContentView extends React.Component {
|
||||
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 = () => {
|
||||
this.setState({
|
||||
currentMode: LIST_MODE,
|
||||
@@ -1890,6 +1903,10 @@ class LibContentView extends React.Component {
|
||||
if (node.path !== this.state.path) {
|
||||
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 {
|
||||
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
||||
let dirent = node.object;
|
||||
@@ -2027,7 +2044,7 @@ class LibContentView extends React.Component {
|
||||
isDirentSelected: false,
|
||||
isAllDirentSelected: false,
|
||||
});
|
||||
if (this.state.currentMode === METADATA_MODE) {
|
||||
if (this.state.currentMode === METADATA_MODE || this.state.currentMode === FACE_RECOGNITION_MODE) {
|
||||
this.setState({
|
||||
currentMode: cookie.load('seafile_view_mode') || LIST_MODE,
|
||||
});
|
||||
|
@@ -154,6 +154,10 @@ export const Utils = {
|
||||
}
|
||||
},
|
||||
|
||||
isFaceRecognition: function (type) {
|
||||
return type === PRIVATE_FILE_TYPE.FACE_RECOGNITION;
|
||||
},
|
||||
|
||||
getShareLinkPermissionList: function (itemType, permission, path, canEdit) {
|
||||
// itemType: library, dir, file
|
||||
// permission: rw, r, admin, cloud-edit, preview, custom-*
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
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.views import check_folder_permission
|
||||
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.utils.timeutils import datetime_to_isoformat_timestr
|
||||
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 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})
|
||||
|
@@ -171,3 +171,16 @@ class MetadataServerAPI:
|
||||
}
|
||||
response = requests.put(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||
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)
|
||||
|
@@ -20,6 +20,42 @@ def add_init_metadata_task(params):
|
||||
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):
|
||||
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
ids = random.sample(possible, length)
|
||||
@@ -57,6 +93,36 @@ def get_sys_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():
|
||||
from seafevents.repo_metadata.utils import METADATA_TABLE
|
||||
columns = [
|
||||
@@ -90,6 +156,20 @@ def init_metadata(metadata_server_api):
|
||||
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):
|
||||
return seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', username, use_onetime=True)
|
||||
|
||||
|
@@ -211,7 +211,8 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView,
|
||||
Wiki2DuplicatePageView, WikiPageTrashView, Wiki2PublishView, Wiki2PublishConfigView, Wiki2PublishPageView
|
||||
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
||||
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.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/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/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
|
||||
|
Reference in New Issue
Block a user