mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-02-15 23:33:15 +08:00
feat: Add PaddleOCR as a new Preprocess provider (#12067)
* feat: Add PaddleOCR as a new Preprocess provider * Fix validateFile * fix use i18n to translate and fix tip * Fix code * Fix code * Fix migrate.ts/index.ts * Fix code * Fix update index.ts version * fix(store): reorder migration versions and improve error logging Move migration 190 to 191 and vice versa for better version ordering Add success logging for migration 191 and improve error message specificity * Fix fix code * Fix fix code * Fix fix validateFile * Fix remove migrate duplicated and add enum FileType * Fix no specific number * Fix index.ts and tailwindcss * Fix type and final file not exit condition * Fix add jsdoc * Fix lint --fix * Fix: use zod schema * Fix: fix zod schema * Fix: looseObject * Fix use formatZodError * Fix fix code * Fix migrate.ts * Fix add annotation * Fix fix header and Outpudir * Fix code * Fix fix code * Fix throw error * feat(preprocess): add return type for readPdf method Add PreprocessReadPdfResult interface and use it as return type for readPdf method to provide better type safety and clarity * refactor(preprocess): improve type safety for pdf parsing variable * refactor(PaddleocrPreprocessProvider): extract markdown filename generation to method Extract repeated filename replacement logic into a dedicated method to improve code reuse and maintainability * refactor: make createProcessedFileInfo async and await its result Use async/await pattern for file operations to improve code consistency and avoid potential blocking * fix: use English error message in PaddleOCR API failure * Fix fix doc * Fix remove ppocr tip * fix --------- Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
parent
d17b228711
commit
5366110ce1
@ -4,7 +4,7 @@ import path from 'node:path'
|
||||
import { loggerService } from '@logger'
|
||||
import { windowService } from '@main/services/WindowService'
|
||||
import { getFileExt, getTempDir } from '@main/utils/file'
|
||||
import type { FileMetadata, PreprocessProvider } from '@types'
|
||||
import type { FileMetadata, PreprocessProvider, PreprocessReadPdfResult } from '@types'
|
||||
import { PDFDocument } from 'pdf-lib'
|
||||
|
||||
const logger = loggerService.withContext('BasePreprocessProvider')
|
||||
@ -90,7 +90,7 @@ export default abstract class BasePreprocessProvider {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
public async readPdf(buffer: Buffer) {
|
||||
public async readPdf(buffer: Buffer): Promise<PreprocessReadPdfResult> {
|
||||
const pdfDoc = await PDFDocument.load(buffer, { ignoreEncryption: true })
|
||||
return {
|
||||
numPages: pdfDoc.getPageCount()
|
||||
|
||||
289
src/main/knowledge/preprocess/PaddleocrPreprocessProvider.ts
Normal file
289
src/main/knowledge/preprocess/PaddleocrPreprocessProvider.ts
Normal file
@ -0,0 +1,289 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { fileStorage } from '@main/services/FileStorage'
|
||||
import { getFileType } from '@main/utils/file'
|
||||
import { MB } from '@shared/config/constant'
|
||||
import type { FileMetadata, PreprocessProvider, PreprocessReadPdfResult } from '@types'
|
||||
import { net } from 'electron'
|
||||
import * as z from 'zod'
|
||||
|
||||
import BasePreprocessProvider from './BasePreprocessProvider'
|
||||
|
||||
const logger = loggerService.withContext('PaddleocrPreprocessProvider')
|
||||
|
||||
/**
|
||||
* 单个文件大小不超过50MB,为避免处理超时,建议每个文件不超过100页。若超过100页,API只解析前100页,后续页将被忽略。
|
||||
* 来源:PaddleOCR 官方 API 调用说明 https://aistudio.baidu.com/paddleocr
|
||||
*/
|
||||
export const PDF_SIZE_LIMIT_MB = 50
|
||||
export const PDF_PAGE_LIMIT = 100
|
||||
export const PDF_SIZE_LIMIT_BYTES = PDF_SIZE_LIMIT_MB * MB
|
||||
|
||||
enum FileType {
|
||||
PDF = 0,
|
||||
Image = 1
|
||||
}
|
||||
|
||||
const ApiResponseSchema = z.looseObject({
|
||||
result: z
|
||||
.looseObject({
|
||||
layoutParsingResults: z
|
||||
.array(
|
||||
z.looseObject({
|
||||
markdown: z.looseObject({
|
||||
text: z.string().min(1, 'Markdown text cannot be empty')
|
||||
})
|
||||
})
|
||||
)
|
||||
.min(1, 'At least one layout parsing result required')
|
||||
})
|
||||
.optional(),
|
||||
errorCode: z.number().optional(),
|
||||
errorMsg: z.string().optional()
|
||||
})
|
||||
|
||||
type ApiResponse = z.infer<typeof ApiResponseSchema>
|
||||
|
||||
const isApiSuccess = (response: ApiResponse): boolean => {
|
||||
const hasNoError = !response.errorCode || response.errorCode === 0
|
||||
const hasSuccessMsg = !response.errorMsg || /success/i.test(response.errorMsg)
|
||||
return hasNoError && hasSuccessMsg
|
||||
}
|
||||
|
||||
function formatZodError(error: z.ZodError): string {
|
||||
return error.issues
|
||||
.map((issue) => {
|
||||
const path = issue.path.join('.')
|
||||
const code = issue.code
|
||||
const message = issue.message
|
||||
return `[${code}] ${path}: ${message}`
|
||||
})
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof z.ZodError) {
|
||||
return formatZodError(error)
|
||||
} else if (error instanceof Error) {
|
||||
return error.message
|
||||
} else if (typeof error === 'string') {
|
||||
return error
|
||||
} else {
|
||||
return 'Unknown error'
|
||||
}
|
||||
}
|
||||
|
||||
export default class PaddleocrPreprocessProvider extends BasePreprocessProvider {
|
||||
constructor(provider: PreprocessProvider, userId?: string) {
|
||||
super(provider, userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件并通过 PaddleOCR 进行预处理(当前仅支持 PDF 文件)
|
||||
* @param sourceId - 源任务ID,用于进度更新/日志追踪
|
||||
* @param file - 待处理的文件元数据(仅支持 ext 为 .pdf 的文件)
|
||||
* @returns {Promise<{processedFile: FileMetadata; quota: number}>} 处理后的文件元数据 + 配额消耗(当前 PaddleOCR 配额为 0)
|
||||
* @throws {Error} 若传入非 PDF 文件、文件大小超限、页数超限等会抛出异常
|
||||
*/
|
||||
public async parseFile(
|
||||
sourceId: string,
|
||||
file: FileMetadata
|
||||
): Promise<{ processedFile: FileMetadata; quota: number }> {
|
||||
try {
|
||||
const filePath = fileStorage.getFilePathById(file)
|
||||
logger.info(`PaddleOCR preprocess processing started: ${filePath}`)
|
||||
|
||||
const fileBuffer = await this.validateFile(filePath)
|
||||
|
||||
// 进度条
|
||||
await this.sendPreprocessProgress(sourceId, 25)
|
||||
|
||||
// 1.读取pdf文件并编码为base64
|
||||
const fileData = fileBuffer.toString('base64')
|
||||
await this.sendPreprocessProgress(sourceId, 50)
|
||||
|
||||
// 2. 调用PadlleOCR文档处理API
|
||||
const apiResponse = await this.callPaddleOcrApi(fileData, FileType.PDF)
|
||||
logger.info(`PaddleOCR API call completed`)
|
||||
|
||||
await this.sendPreprocessProgress(sourceId, 75)
|
||||
|
||||
// 3. 处理 API 错误场景
|
||||
if (!isApiSuccess(apiResponse)) {
|
||||
const errorCode = apiResponse.errorCode ?? -1
|
||||
const errorMsg = apiResponse.errorMsg || 'Unknown error'
|
||||
const fullErrorMsg = `PaddleOCR API processing failed [${errorCode}]: ${errorMsg}`
|
||||
logger.error(fullErrorMsg)
|
||||
throw new Error(fullErrorMsg)
|
||||
}
|
||||
|
||||
// 4. 保存markdown文本
|
||||
const outputDir = await this.saveResults(apiResponse.result, file)
|
||||
|
||||
await this.sendPreprocessProgress(sourceId, 100)
|
||||
|
||||
const processedFile = await this.createProcessedFileInfo(file, outputDir)
|
||||
|
||||
// 5. 创建处理后数据
|
||||
return {
|
||||
processedFile,
|
||||
quota: 0
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
logger.error(`PaddleOCR preprocess processing failed for:`, error as Error)
|
||||
throw new Error(getErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
public async checkQuota(): Promise<number> {
|
||||
// PaddleOCR doesn't have quota checking, return 0
|
||||
return 0
|
||||
}
|
||||
|
||||
private getMarkdownFileName(file: FileMetadata): string {
|
||||
return file.origin_name.replace(/\.(pdf|jpg|jpeg|png)$/i, '.md')
|
||||
}
|
||||
|
||||
private async validateFile(filePath: string): Promise<Buffer> {
|
||||
// 阶段1:校验文件类型
|
||||
logger.info(`Validating PDF file: ${filePath}`)
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (ext !== '.pdf') {
|
||||
throw new Error(`File ${filePath} is not a PDF (extension: ${ext.slice(1)})`)
|
||||
}
|
||||
|
||||
// 阶段2:校验文件大小
|
||||
const stats = await fs.promises.stat(filePath)
|
||||
const fileSizeBytes = stats.size
|
||||
if (fileSizeBytes > PDF_SIZE_LIMIT_BYTES) {
|
||||
const fileSizeMB = Math.round(fileSizeBytes / MB)
|
||||
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of ${PDF_SIZE_LIMIT_MB}MB`)
|
||||
}
|
||||
|
||||
// 阶段3:校验页数(兼容 PDF 解析失败的场景)
|
||||
const pdfBuffer = await fs.promises.readFile(filePath)
|
||||
let doc: PreprocessReadPdfResult | undefined
|
||||
|
||||
try {
|
||||
doc = await this.readPdf(pdfBuffer)
|
||||
} catch (error: unknown) {
|
||||
// PDF 解析失败:抛异常,跳过页数校验
|
||||
const errorMsg = getErrorMessage(error)
|
||||
logger.error(
|
||||
`Failed to parse PDF structure (file may be corrupted or use non-standard format). ` +
|
||||
`Skipping page count validation. Will attempt to process with PaddleOCR API. ` +
|
||||
`Error details: ${errorMsg}. ` +
|
||||
`Suggestion: If processing fails, try repairing the PDF using tools like Adobe Acrobat or online PDF repair services.`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
if (doc?.numPages > PDF_PAGE_LIMIT) {
|
||||
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of ${PDF_PAGE_LIMIT} pages`)
|
||||
}
|
||||
|
||||
logger.info(`PDF validation passed: ${doc.numPages} pages, ${Math.round(fileSizeBytes / MB)}MB`)
|
||||
|
||||
return pdfBuffer
|
||||
}
|
||||
|
||||
private async createProcessedFileInfo(file: FileMetadata, outputDir: string): Promise<FileMetadata> {
|
||||
const finalMdFileName = this.getMarkdownFileName(file)
|
||||
const finalMdPath = path.join(outputDir, finalMdFileName)
|
||||
|
||||
const ext = path.extname(finalMdPath)
|
||||
const type = getFileType(ext)
|
||||
const fileSize = (await fs.promises.stat(finalMdPath)).size
|
||||
|
||||
return {
|
||||
...file,
|
||||
name: finalMdFileName,
|
||||
path: finalMdPath,
|
||||
type: type,
|
||||
ext: ext,
|
||||
size: fileSize
|
||||
}
|
||||
}
|
||||
|
||||
private async callPaddleOcrApi(fileData: string, fileType: number): Promise<ApiResponse> {
|
||||
if (!this.provider.apiHost) {
|
||||
throw new Error('PaddleOCR API host is not configured')
|
||||
}
|
||||
|
||||
const endpoint = this.provider.apiHost
|
||||
const payload = {
|
||||
file: fileData,
|
||||
fileType: fileType,
|
||||
useDocOrientationClassify: false,
|
||||
useDocUnwarping: false,
|
||||
useTextlineOrientation: false,
|
||||
useChartRecognition: false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await net.fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Client-Platform': 'cherry-studio',
|
||||
Authorization: `token ${this.provider.apiKey}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(`PaddleOCR API error: HTTP ${response.status} - ${errorText}`)
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const rawData = await response.json()
|
||||
logger.debug('PaddleOCR API response', { data: rawData })
|
||||
|
||||
// Zod 校验响应结构(不合法则直接抛错)
|
||||
const validatedData = ApiResponseSchema.parse(rawData)
|
||||
return validatedData // 返回完整响应
|
||||
} catch (error: unknown) {
|
||||
const errorMsg = getErrorMessage(error)
|
||||
logger.error(`Failed to call PaddleOCR API: ${errorMsg}`, { error })
|
||||
throw new Error(`Failed to call PaddleOCR API: ${errorMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async saveResults(result: ApiResponse['result'], file: FileMetadata): Promise<string> {
|
||||
const outputDir = path.join(this.storageDir, file.id)
|
||||
|
||||
// 确保输出目录存在且为空
|
||||
if (fs.existsSync(outputDir)) {
|
||||
fs.rmSync(outputDir, { recursive: true, force: true })
|
||||
}
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
|
||||
// 处理 result 为 undefined 的场景(API 无解析结果)
|
||||
if (!result) {
|
||||
const errorMsg = `Parsing failed: No valid parsing result from PaddleOCR API for file [ID: ${file.id}]`
|
||||
// Keep warning log for troubleshooting
|
||||
logger.error(errorMsg)
|
||||
// Throw exception to interrupt function execution (no empty file created)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
// Zod 保证:result 存在时,layoutParsingResults 必是非空数组
|
||||
const markdownText = result.layoutParsingResults
|
||||
.filter((layoutResult) => layoutResult?.markdown?.text)
|
||||
.map((layoutResult) => layoutResult.markdown.text)
|
||||
.join('\n\n')
|
||||
|
||||
// 直接构造目标文件名
|
||||
const finalMdFileName = this.getMarkdownFileName(file)
|
||||
const finalMdPath = path.join(outputDir, finalMdFileName)
|
||||
|
||||
// 保存 Markdown 文件
|
||||
fs.writeFileSync(finalMdPath, markdownText, 'utf-8')
|
||||
|
||||
logger.info(`Saved markdown file: ${finalMdPath}`)
|
||||
return outputDir
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,8 @@ import Doc2xPreprocessProvider from './Doc2xPreprocessProvider'
|
||||
import MineruPreprocessProvider from './MineruPreprocessProvider'
|
||||
import MistralPreprocessProvider from './MistralPreprocessProvider'
|
||||
import OpenMineruPreprocessProvider from './OpenMineruPreprocessProvider'
|
||||
import PaddleocrPreprocessProvider from './PaddleocrPreprocessProvider'
|
||||
|
||||
export default class PreprocessProviderFactory {
|
||||
static create(provider: PreprocessProvider, userId?: string): BasePreprocessProvider {
|
||||
switch (provider.id) {
|
||||
@ -17,6 +19,8 @@ export default class PreprocessProviderFactory {
|
||||
return new MineruPreprocessProvider(provider, userId)
|
||||
case 'open-mineru':
|
||||
return new OpenMineruPreprocessProvider(provider, userId)
|
||||
case 'paddleocr':
|
||||
return new PaddleocrPreprocessProvider(provider, userId)
|
||||
default:
|
||||
return new DefaultPreprocessProvider(provider)
|
||||
}
|
||||
|
||||
@ -67,7 +67,8 @@ export class PpocrService extends OcrBaseService {
|
||||
} satisfies OcrPayload
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Client-Platform': 'cherry-studio'
|
||||
}
|
||||
|
||||
if (options.accessToken) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Doc2xLogo from '@renderer/assets/images/ocr/doc2x.png'
|
||||
import MinerULogo from '@renderer/assets/images/ocr/mineru.jpg'
|
||||
import MistralLogo from '@renderer/assets/images/providers/mistral.png'
|
||||
import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png'
|
||||
import type { PreprocessProviderId } from '@renderer/types'
|
||||
|
||||
export function getPreprocessProviderLogo(providerId: PreprocessProviderId) {
|
||||
@ -13,6 +14,8 @@ export function getPreprocessProviderLogo(providerId: PreprocessProviderId) {
|
||||
return MinerULogo
|
||||
case 'open-mineru':
|
||||
return MinerULogo
|
||||
case 'paddleocr':
|
||||
return PaddleocrLogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
@ -44,5 +47,11 @@ export const PREPROCESS_PROVIDER_CONFIG: Record<PreprocessProviderId, Preprocess
|
||||
official: 'https://github.com/opendatalab/MinerU/',
|
||||
apiKey: 'https://github.com/opendatalab/MinerU/'
|
||||
}
|
||||
},
|
||||
paddleocr: {
|
||||
websites: {
|
||||
official: 'https://aistudio.baidu.com/paddleocr/',
|
||||
apiKey: 'https://aistudio.baidu.com/paddleocr/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => {
|
||||
label: t('settings.tool.preprocess.provider'),
|
||||
title: t('settings.tool.preprocess.provider'),
|
||||
options: preprocessProviders
|
||||
.filter((p) => p.apiKey !== '' || ['open-mineru'].includes(p.id))
|
||||
.filter((p) => p.apiKey !== '' || ['mineru', 'open-mineru', 'paddleocr'].includes(p.id))
|
||||
.map((p) => ({ value: p.id, label: p.name }))
|
||||
}
|
||||
return [preprocessOptions]
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "OCR service provider",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Access token of AI Studio Community",
|
||||
"aistudio_url_label": "AI Studio Community",
|
||||
"api_url": "API URL",
|
||||
"serving_doc_url_label": "PaddleOCR Serving Documentation",
|
||||
"tip": "You can refer to the official PaddleOCR documentation to deploy a local service, or deploy a cloud service on the PaddlePaddle AI Studio Community. For the latter case, please provide the access token of the AI Studio Community."
|
||||
"api_url_label": "Obtain Access Token and API URL"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "OCR service"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Access token of AI Studio Community",
|
||||
"api_url": "API URL",
|
||||
"api_url_label": "Obtain Access Token and API URL",
|
||||
"paddleocr_url_label": "PaddleOCR Official Website"
|
||||
},
|
||||
"provider": "Document Processing Provider",
|
||||
"provider_placeholder": "Choose a document processing provider",
|
||||
"title": "Document Processing",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "OCR 服务提供商",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "星河社区访问令牌",
|
||||
"aistudio_url_label": "星河社区",
|
||||
"api_url": "API URL",
|
||||
"serving_doc_url_label": "PaddleOCR 服务化部署文档",
|
||||
"tip": "您可以参考 PaddleOCR 官方文档部署本地服务,或者在飞桨星河社区部署云服务。对于后一种情况,请填写星河社区访问令牌。"
|
||||
"api_url_label": "获取访问令牌及API URL"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "OCR 服务"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "星河社区访问令牌",
|
||||
"api_url": "API URL",
|
||||
"api_url_label": "获取访问令牌及API URL",
|
||||
"paddleocr_url_label": "PaddleOCR 官网"
|
||||
},
|
||||
"provider": "文档处理服务商",
|
||||
"provider_placeholder": "选择一个文档处理服务商",
|
||||
"title": "文档处理",
|
||||
|
||||
@ -4946,11 +4946,9 @@
|
||||
},
|
||||
"image_provider": "OCR 服務供應商",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "星河社群存取權杖",
|
||||
"aistudio_url_label": "星河社群",
|
||||
"aistudio_access_token": "星河社區訪問令牌",
|
||||
"api_url": "API 網址",
|
||||
"serving_doc_url_label": "PaddleOCR 服務化部署文件",
|
||||
"tip": "您可以參考 PaddleOCR 官方文件來部署本機服務,或是在飛槳星河社群部署雲端服務。對於後者,請提供星河社群的存取權杖。"
|
||||
"api_url_label": "取得訪問令牌及 API 網址"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "OCR 服務"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "星河社區訪問令牌",
|
||||
"api_url": "API 網址",
|
||||
"api_url_label": "取得訪問令牌及 API 網址",
|
||||
"paddleocr_url_label": "PaddleOCR 官方網站"
|
||||
},
|
||||
"provider": "文件處理供應商",
|
||||
"provider_placeholder": "選擇一個文件處理供應商",
|
||||
"title": "文件處理",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "OCR-Anbieter",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "StarRiver Community Access Token",
|
||||
"aistudio_url_label": "StarRiver Community",
|
||||
"api_url": "API-URL",
|
||||
"serving_doc_url_label": "PaddleOCR-Dokumentation für die Bereitstellung",
|
||||
"tip": "Sie können die offizielle PaddleOCR-Dokumentation als Referenz für die lokale Bereitstellung verwenden oder in der StarRiver-Community Cloud-Dienste bereitstellen. Für letztere Option geben Sie bitte den StarRiver-Community-Zugriffstoken ein."
|
||||
"api_url_label": "Zugriffstoken und API-URL abrufen"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "OCR-Dienst"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Zugriffstoken der AI Studio Community",
|
||||
"api_url": "API-URL",
|
||||
"api_url_label": "Zugriffstoken und API-URL abrufen",
|
||||
"paddleocr_url_label": "Offizielle Website von PaddleOCR"
|
||||
},
|
||||
"provider": "Dokumentverarbeitungsanbieter",
|
||||
"provider_placeholder": "Einen Dokumentverarbeitungsanbieter auswählen",
|
||||
"title": "Dokumentverarbeitung",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "Πάροχοι υπηρεσιών OCR",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Διακριτικό πρόσβασης της κοινότητας AI Studio",
|
||||
"aistudio_url_label": "Κοινότητα AI Studio",
|
||||
"api_url": "Διεύθυνση URL API",
|
||||
"serving_doc_url_label": "Τεκμηρίωση PaddleOCR Serving",
|
||||
"tip": "Μπορείτε να ανατρέξετε στην επίσημη τεκμηρίωση του PaddleOCR για να αναπτύξετε μια τοπική υπηρεσία, ή να αναπτύξετε μια υπηρεσία στο cloud στην Κοινότητα PaddlePaddle AI Studio. Στη δεύτερη περίπτωση, παρακαλώ παρέχετε το διακριτικό πρόσβασης (access token) της Κοινότητας AI Studio."
|
||||
"api_url_label": "Λήψη Διακριτικού Πρόσβασης και URL του API"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "Υπηρεσία OCR"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Δείκτης πρόσβασης της κοινότητας AI Studio",
|
||||
"api_url": "Διεύθυνση URL του API",
|
||||
"api_url_label": "Λήψη Διακριτικού Πρόσβασης και URL API",
|
||||
"paddleocr_url_label": "Επίσημη ιστοσελίδα του PaddleOCR"
|
||||
},
|
||||
"provider": "πάροχος υπηρεσιών προεπεξεργασίας εγγράφων",
|
||||
"provider_placeholder": "Επιλέξτε έναν πάροχο υπηρεσιών προεπεξεργασίας εγγράφων",
|
||||
"title": "Προεπεξεργασία εγγράφων",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "Proveedor de servicios OCR",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Token de acceso de la comunidad de AI Studio",
|
||||
"aistudio_url_label": "Comunidad de AI Studio",
|
||||
"api_url": "URL de la API",
|
||||
"serving_doc_url_label": "Documentación de PaddleOCR Serving",
|
||||
"tip": "Puede consultar la documentación oficial de PaddleOCR para implementar un servicio local, o implementar un servicio en la nube en la Comunidad de PaddlePaddle AI Studio. En este último caso, proporcione el token de acceso de la Comunidad de AI Studio."
|
||||
"api_url_label": "Obtener el token de acceso y la URL de la API"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "Servicio OCR"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Token de acceso de la Comunidad AI Studio",
|
||||
"api_url": "URL de la API",
|
||||
"api_url_label": "Obtener el token de acceso y la URL de la API",
|
||||
"paddleocr_url_label": "Sitio web oficial de PaddleOCR"
|
||||
},
|
||||
"provider": "Proveedor de servicios de preprocesamiento de documentos",
|
||||
"provider_placeholder": "Seleccionar un proveedor de servicios de preprocesamiento de documentos",
|
||||
"title": "Preprocesamiento de documentos",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "Fournisseur de service OCR",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Jeton d’accès de la communauté AI Studio",
|
||||
"aistudio_url_label": "Communauté AI Studio",
|
||||
"api_url": "URL de l’API",
|
||||
"serving_doc_url_label": "Documentation de PaddleOCR Serving",
|
||||
"tip": "Vous pouvez consulter la documentation officielle de PaddleOCR pour déployer un service local, ou déployer un service cloud sur la Communauté PaddlePaddle AI Studio. Dans ce dernier cas, veuillez fournir le jeton d’accès de la Communauté AI Studio."
|
||||
"api_url_label": "Obtenir le jeton d’accès et l’URL de l’API"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "Service OCR"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Jeton d’accès à la communauté AI Studio",
|
||||
"api_url": "URL de l’API",
|
||||
"api_url_label": "Obtenir le jeton d’accès et l’URL de l’API",
|
||||
"paddleocr_url_label": "Site officiel de PaddleOCR"
|
||||
},
|
||||
"provider": "fournisseur de services de prétraitement de documents",
|
||||
"provider_placeholder": "Choisissez un prestataire de traitement de documents",
|
||||
"title": "Prétraitement des documents",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "OCRサービスプロバイダー",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "AI Studio Community のアクセス・トークン",
|
||||
"aistudio_url_label": "AI Studio Community",
|
||||
"api_url": "API URL",
|
||||
"serving_doc_url_label": "PaddleOCR サービング ドキュメント",
|
||||
"tip": "ローカルサービスをデプロイするには、公式の PaddleOCR ドキュメントを参照するか、PaddlePaddle AI Studio コミュニティ上でクラウドサービスをデプロイすることができます。後者の場合は、AI Studio コミュニティのアクセストークンを提供してください。"
|
||||
"api_url_label": "アクセストークンとAPI URLを取得する"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "OCRサービス"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "AI Studioコミュニティのアクセストークン",
|
||||
"api_url": "API URL",
|
||||
"api_url_label": "アクセストークンとAPI URLを取得する",
|
||||
"paddleocr_url_label": "PaddleOCR 公式ウェブサイト"
|
||||
},
|
||||
"provider": "プレプロセスプロバイダー",
|
||||
"provider_placeholder": "前処理プロバイダーを選択してください",
|
||||
"title": "前処理",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "Provedor de serviços OCR",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Token de acesso da comunidade AI Studio",
|
||||
"aistudio_url_label": "Comunidade AI Studio",
|
||||
"api_url": "URL da API",
|
||||
"serving_doc_url_label": "Documentação do PaddleOCR Serving",
|
||||
"tip": "Você pode consultar a documentação oficial do PaddleOCR para implantar um serviço local ou implantar um serviço na nuvem na Comunidade PaddlePaddle AI Studio. No último caso, forneça o token de acesso da Comunidade AI Studio."
|
||||
"api_url_label": "Obter Token de Acesso e URL da API"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "Serviço OCR"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Token de acesso da Comunidade AI Studio",
|
||||
"api_url": "URL da API",
|
||||
"api_url_label": "Obter Token de Acesso e URL da API",
|
||||
"paddleocr_url_label": "Site Oficial do PaddleOCR"
|
||||
},
|
||||
"provider": "prestador de serviços de pré-processamento de documentos",
|
||||
"provider_placeholder": "Escolha um fornecedor de pré-processamento de documentos",
|
||||
"title": "Pré-processamento de documentos",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "Furnizor serviciu OCR",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Token de acces Comunitatea AI Studio",
|
||||
"aistudio_url_label": "Comunitatea AI Studio",
|
||||
"api_url": "URL API",
|
||||
"serving_doc_url_label": "Documentație servire PaddleOCR",
|
||||
"tip": "Poți consulta documentația oficială PaddleOCR pentru a implementa un serviciu local sau poți implementa un serviciu cloud pe Comunitatea PaddlePaddle AI Studio. Pentru ultimul caz, te rugăm să furnizezi tokenul de acces al Comunității AI Studio."
|
||||
"api_url_label": "Obțineți token-ul de acces și URL-ul API"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "Serviciu OCR"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Token de acces al Comunității AI Studio",
|
||||
"api_url": "URL API",
|
||||
"api_url_label": "Obțineți token-ul de acces și URL-ul API",
|
||||
"paddleocr_url_label": "Site-ul oficial PaddleOCR"
|
||||
},
|
||||
"provider": "Furnizor procesare documente",
|
||||
"provider_placeholder": "Alege un furnizor de procesare documente",
|
||||
"title": "Procesare documente",
|
||||
|
||||
@ -4947,10 +4947,8 @@
|
||||
"image_provider": "Поставщик услуг OCR",
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Токен доступа сообщества AI Studio",
|
||||
"aistudio_url_label": "Сообщество AI Studio",
|
||||
"api_url": "URL API",
|
||||
"serving_doc_url_label": "Документация по PaddleOCR Serving",
|
||||
"tip": "Вы можете обратиться к официальной документации PaddleOCR, чтобы развернуть локальный сервис, либо развернуть облачный сервис в сообществе PaddlePaddle AI Studio. В последнем случае, пожалуйста, предоставьте токен доступа сообщества AI Studio."
|
||||
"api_url_label": "Получение токена доступа и URL API"
|
||||
},
|
||||
"system": {
|
||||
"win": {
|
||||
@ -4963,6 +4961,12 @@
|
||||
"title": "OCR-сервис"
|
||||
},
|
||||
"preprocess": {
|
||||
"paddleocr": {
|
||||
"aistudio_access_token": "Токен доступа к сообществу AI Studio",
|
||||
"api_url": "URL API",
|
||||
"api_url_label": "Получение токена доступа и URL API",
|
||||
"paddleocr_url_label": "Официальный сайт PaddleOCR"
|
||||
},
|
||||
"provider": "Поставщик обработки документов",
|
||||
"provider_placeholder": "Выберите поставщика услуг обработки документов",
|
||||
"title": "Обработка документов",
|
||||
|
||||
@ -5,13 +5,11 @@ import { Input } from 'antd'
|
||||
import { startTransition, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingRow, SettingRowTitle } from '..'
|
||||
import { SettingHelpLink, SettingHelpTextRow, SettingRow, SettingRowTitle } from '..'
|
||||
|
||||
export const OcrPpocrSettings = () => {
|
||||
// Hack: Hard-coded for now
|
||||
const SERVING_DOC_URL = 'https://www.paddleocr.ai/latest/version3.x/deployment/serving.html'
|
||||
const AISTUDIO_URL = 'https://aistudio.baidu.com/pipeline/mine'
|
||||
|
||||
const API_URL = 'https://aistudio.baidu.com/paddleocr/task'
|
||||
const { t } = useTranslation()
|
||||
const { provider, updateConfig } = useOcrProvider(BuiltinOcrProviderIds.paddleocr)
|
||||
|
||||
@ -68,13 +66,9 @@ export const OcrPpocrSettings = () => {
|
||||
</SettingRow>
|
||||
|
||||
<SettingHelpTextRow style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<SettingHelpText style={{ marginBottom: 5 }}>{t('settings.tool.ocr.paddleocr.tip')}</SettingHelpText>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<SettingHelpLink target="_blank" href={SERVING_DOC_URL}>
|
||||
{t('settings.tool.ocr.paddleocr.serving_doc_url_label')}
|
||||
</SettingHelpLink>
|
||||
<SettingHelpLink target="_blank" href={AISTUDIO_URL}>
|
||||
{t('settings.tool.ocr.paddleocr.aistudio_url_label')}
|
||||
<SettingHelpLink target="_blank" href={API_URL}>
|
||||
{t('settings.tool.ocr.paddleocr.api_url_label')}
|
||||
</SettingHelpLink>
|
||||
</div>
|
||||
</SettingHelpTextRow>
|
||||
|
||||
@ -75,31 +75,34 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
|
||||
<ProviderName> {preprocessProvider.name}</ProviderName>
|
||||
{officialWebsite && preprocessProviderConfig?.websites && (
|
||||
<Link target="_blank" href={preprocessProviderConfig.websites.official}>
|
||||
<ExportOutlined style={{ color: 'var(--color-text)', fontSize: '12px' }} />
|
||||
<ExportOutlined className="text-[--color-text] text-[12px]" />
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</SettingTitle>
|
||||
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
||||
<Divider className="my-[10px] w-full" />
|
||||
{hasObjectKey(preprocessProvider, 'apiKey') && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
{t('settings.provider.api_key.label')}
|
||||
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
|
||||
<Button type="text" size="small" onClick={openApiKeyList} icon={<List size={14} />} />
|
||||
</Tooltip>
|
||||
<SettingSubtitle className="mt-[5px] mb-[10px] flex items-center justify-between">
|
||||
{preprocessProvider.id === 'paddleocr'
|
||||
? t('settings.tool.preprocess.paddleocr.aistudio_access_token')
|
||||
: t('settings.provider.api_key.label')}
|
||||
{preprocessProvider.id !== 'paddleocr' && (
|
||||
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
|
||||
<Button type="text" size="small" onClick={openApiKeyList} icon={<List size={14} />} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingSubtitle>
|
||||
<Flex gap={8}>
|
||||
<Input.Password
|
||||
value={apiKey}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
placeholder={
|
||||
preprocessProvider.id === 'mineru'
|
||||
? t('settings.mineru.api_key')
|
||||
: preprocessProvider.id === 'paddleocr'
|
||||
? t('settings.tool.preprocess.paddleocr.aistudio_access_token')
|
||||
: t('settings.provider.api_key.label')
|
||||
}
|
||||
onChange={(e) => setApiKey(formatApiKeys(e.target.value))}
|
||||
onBlur={onUpdateApiKey}
|
||||
spellCheck={false}
|
||||
@ -107,28 +110,51 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
|
||||
autoFocus={apiKey === ''}
|
||||
/>
|
||||
</Flex>
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between', marginTop: 5 }}>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
{preprocessProvider.id !== 'paddleocr' && (
|
||||
<SettingHelpTextRow className="mt-[5px] justify-between">
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasObjectKey(preprocessProvider, 'apiHost') && (
|
||||
<>
|
||||
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
{t('settings.provider.api_host')}
|
||||
<SettingSubtitle className="mt-[5px] mb-[10px]">
|
||||
{preprocessProvider.id === 'paddleocr'
|
||||
? t('settings.tool.preprocess.paddleocr.api_url')
|
||||
: t('settings.provider.api_host')}
|
||||
</SettingSubtitle>
|
||||
<Flex>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
placeholder={
|
||||
preprocessProvider.id === 'paddleocr'
|
||||
? t('settings.tool.preprocess.paddleocr.api_url')
|
||||
: t('settings.provider.api_host')
|
||||
}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
</Flex>
|
||||
{preprocessProvider.id === 'paddleocr' && (
|
||||
<SettingHelpTextRow className="!flex-col">
|
||||
<div className="!flex !gap-3">
|
||||
<SettingHelpLink
|
||||
className="!inline-block"
|
||||
target="_blank"
|
||||
href="https://aistudio.baidu.com/paddleocr/task">
|
||||
{t('settings.tool.preprocess.paddleocr.api_url_label')}
|
||||
</SettingHelpLink>
|
||||
<SettingHelpLink className="!inline-block" target="_blank" href="https://aistudio.baidu.com/paddleocr">
|
||||
{t('settings.tool.preprocess.paddleocr.paddleocr_url_label')}
|
||||
</SettingHelpLink>
|
||||
</div>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 192,
|
||||
version: 193,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -3159,6 +3159,16 @@ const migrateConfig = {
|
||||
logger.error('migrate 192 error', error as Error)
|
||||
return state
|
||||
}
|
||||
},
|
||||
'193': (state: RootState) => {
|
||||
try {
|
||||
addPreprocessProviders(state, 'paddleocr')
|
||||
logger.info('migrate 193 success')
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 193 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -49,6 +49,12 @@ const initialState: PreprocessState = {
|
||||
name: 'Open MinerU',
|
||||
apiKey: '',
|
||||
apiHost: ''
|
||||
},
|
||||
{
|
||||
id: 'paddleocr',
|
||||
name: 'PaddleOCR',
|
||||
apiKey: '',
|
||||
apiHost: ''
|
||||
}
|
||||
],
|
||||
defaultProvider: 'mineru'
|
||||
|
||||
@ -108,7 +108,8 @@ export const PreprocessProviderIds = {
|
||||
doc2x: 'doc2x',
|
||||
mistral: 'mistral',
|
||||
mineru: 'mineru',
|
||||
'open-mineru': 'open-mineru'
|
||||
'open-mineru': 'open-mineru',
|
||||
paddleocr: 'paddleocr'
|
||||
} as const
|
||||
|
||||
export type PreprocessProviderId = keyof typeof PreprocessProviderIds
|
||||
@ -156,3 +157,7 @@ export interface KnowledgeSearchResult {
|
||||
score: number
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export interface PreprocessReadPdfResult {
|
||||
numPages: number
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user