diff --git a/frontend/src/hooks/metadata-ai-operation.js b/frontend/src/hooks/metadata-ai-operation.js
index be84f565a5..9b02574c7d 100644
--- a/frontend/src/hooks/metadata-ai-operation.js
+++ b/frontend/src/hooks/metadata-ai-operation.js
@@ -92,6 +92,20 @@ export const MetadataAIOperationsProvider = ({
});
}, [extractFilesDetails]);
+ const faceRecognition = useCallback((objIds, { success_callback, fail_callback } = {}) => {
+ const inProgressToaster = toaster.notifyInProgress(gettext('Detecting faces by AI...'), { duration: null });
+ metadataAPI.recognizeFaces(repoID, objIds).then(res => {
+ inProgressToaster.close();
+ toaster.success(gettext('Faces detected'));
+ success_callback && success_callback();
+ }).catch(error => {
+ inProgressToaster.close();
+ const errorMessage = gettext('Failed to detect faces');
+ toaster.danger(errorMessage);
+ fail_callback && fail_callback();
+ });
+ }, [repoID]);
+
return (
{children}
diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js
index 9bc38cb9a4..190f19e840 100644
--- a/frontend/src/metadata/api.js
+++ b/frontend/src/metadata/api.js
@@ -334,6 +334,14 @@ class MetadataManagerAPI {
return this.req.delete(url);
};
+ recognizeFaces = (repoID, objIds) => {
+ const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/recognize-faces/';
+ const params = {
+ obj_ids: objIds,
+ };
+ return this.req.post(url, params);
+ };
+
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);
diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js
index 2d977a37bb..f171f1262d 100644
--- a/frontend/src/metadata/constants/event-bus-type.js
+++ b/frontend/src/metadata/constants/event-bus-type.js
@@ -44,6 +44,7 @@ export const EVENT_BUS_TYPE = {
MOVE_RECORD: 'move_record',
DELETE_RECORDS: 'delete_records',
UPDATE_RECORD_DETAILS: 'update_record_details',
+ UPDATE_FACE_RECOGNITION: 'update_face_recognition',
GENERATE_DESCRIPTION: 'generate_description',
OCR: 'ocr',
diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js
index e2f1617cc1..772ac0730e 100644
--- a/frontend/src/metadata/hooks/metadata-view.js
+++ b/frontend/src/metadata/hooks/metadata-view.js
@@ -38,7 +38,7 @@ export const MetadataViewProvider = ({
const { collaborators } = useCollaborators();
const { isBeingBuilt, setIsBeingBuilt } = useMetadata();
- const { onOCR, generateDescription, extractFilesDetails } = useMetadataAIOperations();
+ const { onOCR, generateDescription, extractFilesDetails, faceRecognition } = useMetadataAIOperations();
const tableChanged = useCallback(() => {
setMetadata(storeRef.current.data);
@@ -348,6 +348,15 @@ export const MetadataViewProvider = ({
});
}, [metadata, extractFilesDetails, modifyRecords]);
+ const updateFaceRecognition = useCallback((records) => {
+ const recordObjIds = records.map(record => getFileObjIdFromRecord(record));
+ if (recordObjIds.length > 50) {
+ toaster.danger(gettext('Select up to 50 files'));
+ return;
+ }
+ faceRecognition(recordObjIds);
+ }, [faceRecognition]);
+
const updateRecordDescription = useCallback((record) => {
const parentDir = getParentDirFromRecord(record);
const fileName = getFileNameFromRecord(record);
@@ -429,6 +438,7 @@ export const MetadataViewProvider = ({
const unsubscribeMoveRecord = eventBus.subscribe(EVENT_BUS_TYPE.MOVE_RECORD, moveRecord);
const unsubscribeDeleteRecords = eventBus.subscribe(EVENT_BUS_TYPE.DELETE_RECORDS, deleteRecords);
const unsubscribeUpdateDetails = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_RECORD_DETAILS, updateRecordDetails);
+ const unsubscribeUpdateFaceRecognition = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_FACE_RECOGNITION, updateFaceRecognition);
const unsubscribeUpdateDescription = eventBus.subscribe(EVENT_BUS_TYPE.GENERATE_DESCRIPTION, updateRecordDescription);
const unsubscribeOCR = eventBus.subscribe(EVENT_BUS_TYPE.OCR, ocr);
@@ -454,6 +464,7 @@ export const MetadataViewProvider = ({
unsubscribeMoveRecord();
unsubscribeDeleteRecords();
unsubscribeUpdateDetails();
+ unsubscribeUpdateFaceRecognition();
unsubscribeUpdateDescription();
unsubscribeOCR();
delayReloadDataTimer.current && clearTimeout(delayReloadDataTimer.current);
@@ -493,6 +504,7 @@ export const MetadataViewProvider = ({
updateCurrentPath: params.updateCurrentPath,
updateSelectedRecordIds,
updateRecordDetails,
+ updateFaceRecognition,
updateRecordDescription,
ocr,
}}
diff --git a/frontend/src/metadata/views/table/context-menu/index.js b/frontend/src/metadata/views/table/context-menu/index.js
index 98df10fa79..95167d8a7a 100644
--- a/frontend/src/metadata/views/table/context-menu/index.js
+++ b/frontend/src/metadata/views/table/context-menu/index.js
@@ -30,13 +30,14 @@ const OPERATION = {
RENAME_FILE: 'rename-file',
FILE_DETAIL: 'file-detail',
FILE_DETAILS: 'file-details',
+ DETECT_FACES: 'detect-faces',
MOVE: 'move',
};
const ContextMenu = ({
isGroupView, selectedRange, selectedPosition, recordMetrics, recordGetterByIndex, onClearSelected, onCopySelected,
getTableContentRect, getTableCanvasContainerRect, deleteRecords, selectNone, updateFileTags, moveRecord, addFolder, updateRecordDetails,
- updateRecordDescription, ocr,
+ updateFaceRecognition, updateRecordDescription, ocr,
}) => {
const currentRecord = useRef(null);
@@ -119,6 +120,13 @@ const ContextMenu = ({
if (imageOrVideoRecords.length > 0) {
list.push({ value: OPERATION.FILE_DETAILS, label: gettext('Extract file details'), records: imageOrVideoRecords });
}
+ const imageRecords = records.filter(record => {
+ const fileName = getFileNameFromRecord(record);
+ return Utils.imageCheck(fileName);
+ });
+ if (imageRecords.length > 0) {
+ list.push({ value: OPERATION.DETECT_FACES, label: gettext('Detect faces'), records: imageRecords });
+ }
return list;
}
@@ -148,6 +156,17 @@ const ContextMenu = ({
if (imageOrVideoRecords.length > 0) {
list.push({ value: OPERATION.FILE_DETAILS, label: gettext('Extract file details'), records: imageOrVideoRecords });
}
+ const imageRecords = records.filter(record => {
+ const isFolder = checkIsDir(record);
+ if (isFolder) return false;
+ const canModifyRow = checkCanModifyRow(record);
+ if (!canModifyRow) return false;
+ const fileName = getFileNameFromRecord(record);
+ return Utils.imageCheck(fileName);
+ });
+ if (imageRecords.length > 0) {
+ list.push({ value: OPERATION.DETECT_FACES, label: gettext('Detect faces'), records: imageRecords });
+ }
return list;
}
@@ -193,6 +212,9 @@ const ContextMenu = ({
if (isImage || isVideo) {
aiOptions.push({ value: OPERATION.FILE_DETAIL, label: gettext('Extract file detail'), record: record });
}
+ if (isImage) {
+ aiOptions.push({ value: OPERATION.DETECT_FACES, label: gettext('Detect faces'), records: [record] });
+ }
if (descriptionColumn && isDescribableFile) {
aiOptions.push({
@@ -305,6 +327,11 @@ const ContextMenu = ({
updateRecordDetails([record]);
break;
}
+ case OPERATION.DETECT_FACES: {
+ const { records } = option;
+ updateFaceRecognition(records);
+ break;
+ }
case OPERATION.MOVE: {
const { record } = option;
if (!record) break;
@@ -315,7 +342,7 @@ const ContextMenu = ({
break;
}
}
- }, [repoID, onCopySelected, onClearSelected, updateRecordDescription, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateRecordDetails, toggleFileTagsRecord, toggleMoveDialog]);
+ }, [repoID, onCopySelected, onClearSelected, updateRecordDescription, toggleFileTagsRecord, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateRecordDetails, updateFaceRecognition, toggleMoveDialog]);
useEffect(() => {
const unsubscribeToggleMoveDialog = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, toggleMoveDialog);
diff --git a/frontend/src/metadata/views/table/index.js b/frontend/src/metadata/views/table/index.js
index ce25223e26..a340277b75 100644
--- a/frontend/src/metadata/views/table/index.js
+++ b/frontend/src/metadata/views/table/index.js
@@ -30,6 +30,7 @@ const Table = () => {
addFolder,
updateSelectedRecordIds,
updateRecordDetails,
+ updateFaceRecognition,
updateRecordDescription,
ocr,
} = useMetadataView();
@@ -182,6 +183,7 @@ const Table = () => {
addFolder={addFolder}
updateSelectedRecordIds={updateSelectedRecordIds}
updateRecordDetails={updateRecordDetails}
+ updateFaceRecognition={updateFaceRecognition}
updateRecordDescription={updateRecordDescription}
ocr={ocr}
/>
diff --git a/frontend/src/metadata/views/table/table-main/records/index.js b/frontend/src/metadata/views/table/table-main/records/index.js
index 02879c105b..0c5d5ee574 100644
--- a/frontend/src/metadata/views/table/table-main/records/index.js
+++ b/frontend/src/metadata/views/table/table-main/records/index.js
@@ -646,6 +646,7 @@ class Records extends Component {
addFolder={this.props.addFolder}
selectNone={this.selectNone}
updateRecordDetails={this.props.updateRecordDetails}
+ updateFaceRecognition={this.props.updateFaceRecognition}
updateRecordDescription={this.props.updateRecordDescription}
ocr={this.props.ocr}
/>
diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py
index 19d6f35077..7199e5157b 100644
--- a/seahub/repo_metadata/apis.py
+++ b/seahub/repo_metadata/apis.py
@@ -13,7 +13,7 @@ from seahub.api2.throttling import UserRateThrottle
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, \
+from seahub.repo_metadata.utils import add_init_metadata_task, recognize_faces, gen_unique_id, init_metadata, \
get_unmodifiable_columns, can_read_metadata, init_faces, \
extract_file_details, get_table_by_name, remove_faces_table, FACES_SAVE_PATH, \
init_tags, init_tag_self_link_columns, remove_tags_table, add_init_face_recognition_task, init_ocr, \
@@ -1799,6 +1799,46 @@ class MetadataExtractFileDetails(APIView):
return Response({'details': resp})
+class MetadataRecognizeFaces(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request, repo_id):
+ obj_ids = request.data.get('obj_ids')
+ if not obj_ids or not isinstance(obj_ids, list) or len(obj_ids) > 50:
+ error_msg = 'obj_ids is invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ record = RepoMetadata.objects.filter(repo_id=repo_id).first()
+ if not record or not record.enabled:
+ error_msg = f'The metadata module is disabled for repo {repo_id}.'
+ return api_error(status.HTTP_404_NOT_FOUND, 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)
+
+ permission = check_folder_permission(request, repo_id, '/')
+ if permission != 'rw':
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ params = {
+ 'obj_ids': obj_ids,
+ 'repo_id': repo_id
+ }
+ try:
+ resp = recognize_faces(params=params)
+ resp_json = resp.json()
+ except Exception as e:
+ logger.exception(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response(resp_json, resp.status_code)
+
+
# tags
class MetadataTagsStatusManage(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
diff --git a/seahub/repo_metadata/urls.py b/seahub/repo_metadata/urls.py
index 4aa97f4f34..3c59b414ee 100644
--- a/seahub/repo_metadata/urls.py
+++ b/seahub/repo_metadata/urls.py
@@ -1,5 +1,5 @@
from django.urls import re_path
-from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecord, \
+from .apis import MetadataRecognizeFaces, MetadataRecords, MetadataManage, MetadataColumns, MetadataRecord, \
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataTagsFiles, MetadataDetailsSettingsView, \
@@ -24,6 +24,7 @@ urlpatterns = [
re_path(r'^people-photos/(?P.+)/$', PeoplePhotos.as_view(), name='api-v2.1-metadata-people-photos-get-delete'),
re_path(r'^people-photos/$', PeoplePhotos.as_view(), name='api-v2.1-metadata-people-photos-post'),
re_path(r'^face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'),
+ re_path(r'^recognize-faces/$', MetadataRecognizeFaces.as_view(), name='api-v2.1-metadata-recognize-faces'),
re_path(r'^people-cover-photo/(?P.+)/$', PeopleCoverPhoto.as_view(), name='api-v2.1-metadata-people-cover-photo'),
re_path(r'^extract-file-details/$', MetadataExtractFileDetails.as_view(), name='api-v2.1-metadata-extract-file-details'),
diff --git a/seahub/repo_metadata/utils.py b/seahub/repo_metadata/utils.py
index 18583f510c..845548c40e 100644
--- a/seahub/repo_metadata/utils.py
+++ b/seahub/repo_metadata/utils.py
@@ -41,6 +41,16 @@ def extract_file_details(params):
resp = requests.post(url, json=params, headers=headers, timeout=30)
return json.loads(resp.content)['details']
+
+def recognize_faces(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, '/recognize-faces')
+ resp = requests.post(url, json=params, headers=headers, timeout=30)
+ return resp
+
+
def update_people_cover_photo(params):
payload = {'exp': int(time.time()) + 300, }
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')