diff --git a/frontend/src/hooks/metadata-ai-operation.js b/frontend/src/hooks/metadata-ai-operation.js index be84f565a5d..78b9af64eb4 100644 --- a/frontend/src/hooks/metadata-ai-operation.js +++ b/frontend/src/hooks/metadata-ai-operation.js @@ -68,6 +68,38 @@ export const MetadataAIOperationsProvider = ({ }); }, [repoID]); + const genDualLayerPDF = useCallback(({ parentDir, fileName }, { success_callback, fail_callback } = {}) => { + const filePath = Utils.joinPath(parentDir, fileName); + const inProgressToaster = toaster.notifyInProgress(gettext('Making PDF searchable by AI...'), { duration: 5 }); + metadataAPI.genDualLayerPDF(repoID, filePath).then(res => { + if (res.data.task_status === 'processing') { + toaster.notifyInProgress(gettext('The task has been started, perhaps by another user'), { duration: 5 }); + } else if (res.data.task_status === 'already_ocr') { + const userConfirmed = window.confirm(gettext('The text has already been OCR processed. Do you want to proceed with OCR again?')); + if (userConfirmed) { + metadataAPI.genDualLayerPDF(repoID, filePath, { force: true }).then(res => { + if (res.data.task_status === 'processing') { + toaster.notifyInProgress(gettext('The task has been started, perhaps by another user'), { duration: 5 }); + } else { + success_callback && success_callback(); + } + }).catch(() => { + const errorMessage = gettext('Failed to make PDF searchable'); + toaster.danger(errorMessage); + fail_callback && fail_callback(); + }); + } + } else { + success_callback && success_callback(); + } + }).catch(() => { + inProgressToaster.close(); + const errorMessage = gettext('Failed to make PDF searchable'); + toaster.danger(errorMessage); + fail_callback && fail_callback(); + }); + }, [repoID]); + const extractFilesDetails = useCallback((objIds, { success_callback, fail_callback } = {}) => { const inProgressToaster = toaster.notifyInProgress(gettext('Extracting file details by AI...'), { duration: null }); metadataAPI.extractFileDetails(repoID, objIds).then(res => { @@ -102,6 +134,7 @@ export const MetadataAIOperationsProvider = ({ onOCR, OCRSuccessCallBack, generateDescription, + genDualLayerPDF, extractFilesDetails, extractFileDetails, }}> diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index 3ec64780202..d66ebde5e70 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -264,6 +264,16 @@ class MetadataManagerAPI { return this.req.post(url, params); }; + genDualLayerPDF = (repoID, filePath, options = {}) => { + const url = this.server + '/api/v2.1/ai/pdf/generate-text-layer/'; + const params = { + path: filePath, + repo_id: repoID, + ...options, + }; + return this.req.post(url, params); + }; + imageCaption = (repoID, filePath, lang) => { const url = this.server + '/api/v2.1/ai/image-caption/'; const params = { diff --git a/frontend/src/metadata/components/metadata-details/ai/index.js b/frontend/src/metadata/components/metadata-details/ai/index.js index cc28677a81e..cf6e66fa404 100644 --- a/frontend/src/metadata/components/metadata-details/ai/index.js +++ b/frontend/src/metadata/components/metadata-details/ai/index.js @@ -20,6 +20,7 @@ const OPERATION = { OCR: 'ocr', FILE_TAGS: 'file-tags', FILE_DETAIL: 'file-detail', + GEN_DUAL_LAYER_PDF: 'gen-dual-layer-pdf', }; const AI = () => { diff --git a/frontend/src/metadata/context.js b/frontend/src/metadata/context.js index 649bf3246ae..1ad863fcb20 100644 --- a/frontend/src/metadata/context.js +++ b/frontend/src/metadata/context.js @@ -235,6 +235,11 @@ class Context { return this.metadataAPI.generateDescription(repoID, filePath); }; + genDualLayerPDF = (filePath, options = {}) => { + const repoID = this.settings['repoID']; + return this.metadataAPI.genDualLayerPDF(repoID, filePath, ...options); + }; + imageCaption = (filePath) => { const repoID = this.settings['repoID']; const lang = this.settings['lang']; diff --git a/frontend/src/metadata/views/table/context-menu/index.js b/frontend/src/metadata/views/table/context-menu/index.js index 22c40237ee9..de189b08cc8 100644 --- a/frontend/src/metadata/views/table/context-menu/index.js +++ b/frontend/src/metadata/views/table/context-menu/index.js @@ -34,6 +34,7 @@ const OPERATION = { FILE_DETAIL: 'file-detail', FILE_DETAILS: 'file-details', MOVE: 'move', + GEN_DUAL_LAYER_PDF: 'gen-dual-layer-pdf', }; const ContextMenu = ({ @@ -48,7 +49,7 @@ const ContextMenu = ({ const { metadata } = useMetadataView(); const { enableOCR } = useMetadataStatus(); - const { onOCR, generateDescription, extractFilesDetails } = useMetadataAIOperations(); + const { onOCR, generateDescription, extractFilesDetails, genDualLayerPDF } = useMetadataAIOperations(); const repoID = window.sfMetadataStore.repoId; @@ -172,6 +173,7 @@ const ContextMenu = ({ const isDescribableFile = checkIsDescribableFile(record); const isImage = Utils.imageCheck(fileName); const isVideo = Utils.videoCheck(fileName); + const isPDF = Utils.pdfCheck(fileName); if (descriptionColumn && isDescribableFile) { list.push({ value: OPERATION.GENERATE_DESCRIPTION, @@ -180,6 +182,14 @@ const ContextMenu = ({ }); } + if (isPDF) { + list.push({ + value: OPERATION.GEN_DUAL_LAYER_PDF, + label: gettext('Make the PDF searchable'), + record + }); + } + if (enableOCR && isImage) { list.push({ value: OPERATION.OCR, label: gettext('OCR'), record }); } @@ -233,6 +243,14 @@ const ContextMenu = ({ }); }, [updateRecords, generateDescription]); + const handleGenerateDualLayerPDF = useCallback((record) => { + const parentDir = getParentDirFromRecord(record); + const fileName = getFileNameFromRecord(record); + if (!fileName || !parentDir) return; + if (!Utils.pdfCheck(fileName)) return; + genDualLayerPDF({ parentDir, fileName }); + }, [genDualLayerPDF]); + const toggleFileTagsRecord = useCallback((record = null) => { setFileTagsRecord(record); }, []); @@ -318,6 +336,12 @@ const ContextMenu = ({ handelGenerateDescription(record); break; } + case OPERATION.GEN_DUAL_LAYER_PDF: { + const { record } = option; + if (!record) break; + handleGenerateDualLayerPDF(record); + break; + } case OPERATION.FILE_TAGS: { const { record } = option; if (!record) break; @@ -378,7 +402,7 @@ const ContextMenu = ({ break; } } - }, [repoID, onCopySelected, onClearSelected, handelGenerateDescription, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileDetails, toggleFileTagsRecord, toggleMoveDialog]); + }, [repoID, onCopySelected, onClearSelected, handelGenerateDescription, handleGenerateDualLayerPDF, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateFileDetails, toggleFileTagsRecord, toggleMoveDialog]); const currentRecordId = getRecordIdFromRecord(currentRecord.current); const fileName = getFileNameFromRecord(currentRecord.current); diff --git a/seahub/ai/apis.py b/seahub/ai/apis.py index 8e6f8fbd93c..1f6ac910459 100644 --- a/seahub/ai/apis.py +++ b/seahub/ai/apis.py @@ -1,5 +1,6 @@ import logging import os.path +import json from pysearpc import SearpcError from seahub.repo_metadata.models import RepoMetadata @@ -15,7 +16,8 @@ from seahub.api2.authentication import TokenAuthentication, SdocJWTTokenAuthentication from seahub.utils import get_file_type_and_ext, IMAGE from seahub.views import check_folder_permission -from seahub.ai.utils import image_caption, translate, writing_assistant, verify_ai_config, generate_summary, generate_file_tags, ocr +from seahub.ai.utils import image_caption, translate, writing_assistant, \ + verify_ai_config, generate_summary, generate_file_tags, ocr, generate_dual_layer_pdf logger = logging.getLogger(__name__) @@ -343,3 +345,72 @@ def post(self, request): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return Response(resp_json, resp.status_code) + + +class GenDualLayerPDF(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def post(self, request): + if not verify_ai_config(): + return api_error(status.HTTP_400_BAD_REQUEST, 'AI server not configured') + + repo_id = request.data.get('repo_id') + path = request.data.get('path') + force = request.data.get('force') + username = request.user.username + + if not repo_id: + return api_error(status.HTTP_400_BAD_REQUEST, 'repo_id invalid') + if not path: + return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid') + + 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, os.path.dirname(path)) + if not permission: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + file_id = seafile_api.get_file_id_by_path(repo_id, path) + except SearpcError as e: + logger.error(e) + return api_error( + status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error' + ) + + if not file_id: + return api_error(status.HTTP_404_NOT_FOUND, f"File {path} not found") + + download_token = seafile_api.get_fileserver_access_token( + repo_id, file_id, 'download', username, use_onetime=True + ) + parent_dir = os.path.dirname(path) + obj_id = json.dumps({'parent_dir': parent_dir}) + upload_token = seafile_api.get_fileserver_access_token( + repo_id, obj_id, 'upload-link', username, use_onetime=True + ) + if not (download_token and upload_token): + error_msg = 'Internal Server Error, ' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + params = { + 'path': path, + 'download_token': download_token, + 'upload_token': upload_token, + 'repo_id': repo_id, + 'force': force, + } + try: + resp = generate_dual_layer_pdf(params) + resp_json = resp.json() + except Exception as e: + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response(resp_json, resp.status_code) diff --git a/seahub/ai/utils.py b/seahub/ai/utils.py index 615be743933..ea16e47b570 100644 --- a/seahub/ai/utils.py +++ b/seahub/ai/utils.py @@ -35,6 +35,13 @@ def generate_summary(params): return resp +def generate_dual_layer_pdf(params): + headers = gen_headers() + url = urljoin(SEAFILE_AI_SERVER_URL, '/api/v1/pdf/generate-text-layer/') + resp = requests.post(url, json=params, headers=headers) + return resp + + def generate_file_tags(params): headers = gen_headers() url = urljoin(SEAFILE_AI_SERVER_URL, '/api/v1/generate-file-tags/') diff --git a/seahub/urls.py b/seahub/urls.py index 111ec8e2a6d..23927fc5d43 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -2,7 +2,8 @@ from django.urls import include, path, re_path from django.views.generic import TemplateView -from seahub.ai.apis import ImageCaption, GenerateSummary, GenerateFileTags, OCR, Translate, WritingAssistant +from seahub.ai.apis import ImageCaption, GenerateSummary, GenerateFileTags, \ + OCR, Translate, WritingAssistant, GenDualLayerPDF from seahub.api2.endpoints.share_link_auth import ShareLinkUserAuthView, ShareLinkEmailAuthView from seahub.api2.endpoints.internal_api import InternalUserListView, InternalCheckShareLinkAccess, \ InternalCheckFileOperationAccess @@ -1060,4 +1061,5 @@ re_path(r'^api/v2.1/ai/ocr/$', OCR.as_view(), name='api-v2.1-ocr'), re_path(r'^api/v2.1/ai/translate/$', Translate.as_view(), name='api-v2.1-translate'), re_path(r'^api/v2.1/ai/writing-assistant/$', WritingAssistant.as_view(), name='api-v2.1-writing-assistant'), + re_path(r'^api/v2.1/ai/pdf/generate-text-layer/$', GenDualLayerPDF.as_view(), name='api-v2.1-pdf-generate-text-layer'), ]