mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 07:00:09 +08:00
feat: new build-in OCR provider -> intel OV(NPU) OCR (#10737)
* new build-in ocr provider intel ov Signed-off-by: Ma, Kejiang <kj.ma@intel.com> Signed-off-by: Kejiang Ma <kj.ma@intel.com> * updated base on PR's commnets Signed-off-by: Kejiang Ma <kj.ma@intel.com> * feat(OcrImageSettings): use swr to fetch available providers Add loading state and error handling when fetching available OCR providers. Display an alert when provider loading fails, showing the error message. Also optimize provider filtering logic using useMemo. * refactor(ocr): rename providers to listProviders for consistency Update method name to better reflect its functionality and maintain naming consistency across the codebase --------- Signed-off-by: Ma, Kejiang <kj.ma@intel.com> Signed-off-by: Kejiang Ma <kj.ma@intel.com> Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
parent
c4e0a6acfe
commit
0e5ebcfd00
@ -337,6 +337,7 @@ export enum IpcChannel {
|
|||||||
|
|
||||||
// OCR
|
// OCR
|
||||||
OCR_ocr = 'ocr:ocr',
|
OCR_ocr = 'ocr:ocr',
|
||||||
|
OCR_ListProviders = 'ocr:list-providers',
|
||||||
|
|
||||||
// OVMS
|
// OVMS
|
||||||
Ovms_AddModel = 'ovms:add-model',
|
Ovms_AddModel = 'ovms:add-model',
|
||||||
|
|||||||
@ -875,6 +875,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
||||||
ocrService.ocr(file, provider)
|
ocrService.ocr(file, provider)
|
||||||
)
|
)
|
||||||
|
ipcMain.handle(IpcChannel.OCR_ListProviders, () => ocrService.listProviderIds())
|
||||||
|
|
||||||
// OVMS
|
// OVMS
|
||||||
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
|
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { loggerService } from '@logger'
|
|||||||
import { isLinux } from '@main/constant'
|
import { isLinux } from '@main/constant'
|
||||||
import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types'
|
import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types'
|
||||||
|
|
||||||
|
import { ovOcrService } from './builtin/OvOcrService'
|
||||||
import { ppocrService } from './builtin/PpocrService'
|
import { ppocrService } from './builtin/PpocrService'
|
||||||
import { systemOcrService } from './builtin/SystemOcrService'
|
import { systemOcrService } from './builtin/SystemOcrService'
|
||||||
import { tesseractService } from './builtin/TesseractService'
|
import { tesseractService } from './builtin/TesseractService'
|
||||||
@ -22,6 +23,10 @@ export class OcrService {
|
|||||||
this.registry.delete(providerId)
|
this.registry.delete(providerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public listProviderIds(): string[] {
|
||||||
|
return Array.from(this.registry.keys())
|
||||||
|
}
|
||||||
|
|
||||||
public async ocr(file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> {
|
public async ocr(file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> {
|
||||||
const handler = this.registry.get(provider.id)
|
const handler = this.registry.get(provider.id)
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
@ -39,3 +44,5 @@ ocrService.register(BuiltinOcrProviderIds.tesseract, tesseractService.ocr.bind(t
|
|||||||
!isLinux && ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))
|
!isLinux && ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))
|
||||||
|
|
||||||
ocrService.register(BuiltinOcrProviderIds.paddleocr, ppocrService.ocr.bind(ppocrService))
|
ocrService.register(BuiltinOcrProviderIds.paddleocr, ppocrService.ocr.bind(ppocrService))
|
||||||
|
|
||||||
|
ovOcrService.isAvailable() && ocrService.register(BuiltinOcrProviderIds.ovocr, ovOcrService.ocr.bind(ovOcrService))
|
||||||
|
|||||||
128
src/main/services/ocr/builtin/OvOcrService.ts
Normal file
128
src/main/services/ocr/builtin/OvOcrService.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { isWin } from '@main/constant'
|
||||||
|
import { isImageFileMetadata, OcrOvConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as os from 'os'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
import { OcrBaseService } from './OcrBaseService'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('OvOcrService')
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat')
|
||||||
|
|
||||||
|
export class OvOcrService extends OcrBaseService {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAvailable(): boolean {
|
||||||
|
return (
|
||||||
|
isWin &&
|
||||||
|
os.cpus()[0].model.toLowerCase().includes('intel') &&
|
||||||
|
os.cpus()[0].model.toLowerCase().includes('ultra') &&
|
||||||
|
fs.existsSync(PATH_BAT_FILE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOvOcrPath(): string {
|
||||||
|
return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr')
|
||||||
|
}
|
||||||
|
|
||||||
|
private getImgDir(): string {
|
||||||
|
return path.join(this.getOvOcrPath(), 'img')
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOutputDir(): string {
|
||||||
|
return path.join(this.getOvOcrPath(), 'output')
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearDirectory(dirPath: string): Promise<void> {
|
||||||
|
if (fs.existsSync(dirPath)) {
|
||||||
|
const files = await fs.promises.readdir(dirPath)
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(dirPath, file)
|
||||||
|
const stats = await fs.promises.stat(filePath)
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
await this.clearDirectory(filePath)
|
||||||
|
await fs.promises.rmdir(filePath)
|
||||||
|
} else {
|
||||||
|
await fs.promises.unlink(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the directory does not exist, create it
|
||||||
|
await fs.promises.mkdir(dirPath, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyFileToImgDir(sourceFilePath: string, targetFileName: string): Promise<void> {
|
||||||
|
const imgDir = this.getImgDir()
|
||||||
|
const targetFilePath = path.join(imgDir, targetFileName)
|
||||||
|
await fs.promises.copyFile(sourceFilePath, targetFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runOcrBatch(): Promise<void> {
|
||||||
|
const ovOcrPath = this.getOvOcrPath()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute run.bat in the ov-ocr directory
|
||||||
|
await execAsync(`"${PATH_BAT_FILE}"`, {
|
||||||
|
cwd: ovOcrPath,
|
||||||
|
timeout: 60000 // 60 second timeout
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error running ovocr batch: ${error}`)
|
||||||
|
throw new Error(`Failed to run OCR batch: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ocrImage(filePath: string, options?: OcrOvConfig): Promise<OcrResult> {
|
||||||
|
logger.info(`OV OCR called on ${filePath} with options ${JSON.stringify(options)}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Clear img directory and output directory
|
||||||
|
await this.clearDirectory(this.getImgDir())
|
||||||
|
await this.clearDirectory(this.getOutputDir())
|
||||||
|
|
||||||
|
// 2. Copy file to img directory
|
||||||
|
const fileName = path.basename(filePath)
|
||||||
|
await this.copyFileToImgDir(filePath, fileName)
|
||||||
|
logger.info(`File copied to img directory: ${fileName}`)
|
||||||
|
|
||||||
|
// 3. Run run.bat
|
||||||
|
logger.info('Running OV OCR batch process...')
|
||||||
|
await this.runOcrBatch()
|
||||||
|
|
||||||
|
// 4. Check that output/[basename].txt file exists
|
||||||
|
const baseNameWithoutExt = path.basename(fileName, path.extname(fileName))
|
||||||
|
const outputFilePath = path.join(this.getOutputDir(), `${baseNameWithoutExt}.txt`)
|
||||||
|
if (!fs.existsSync(outputFilePath)) {
|
||||||
|
throw new Error(`OV OCR output file not found at: ${outputFilePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Read output/[basename].txt file content
|
||||||
|
const ocrText = await fs.promises.readFile(outputFilePath, 'utf-8')
|
||||||
|
logger.info(`OV OCR text extracted: ${ocrText.substring(0, 100)}...`)
|
||||||
|
|
||||||
|
// 6. Return result
|
||||||
|
return { text: ocrText }
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error during OV OCR process: ${error}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ocr = async (file: SupportedOcrFile, options?: OcrOvConfig): Promise<OcrResult> => {
|
||||||
|
if (isImageFileMetadata(file)) {
|
||||||
|
return this.ocrImage(file.path, options)
|
||||||
|
} else {
|
||||||
|
throw new Error('Unsupported file type, currently only image files are supported')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ovOcrService = new OvOcrService()
|
||||||
@ -480,7 +480,8 @@ const api = {
|
|||||||
},
|
},
|
||||||
ocr: {
|
ocr: {
|
||||||
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
|
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
|
||||||
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider)
|
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider),
|
||||||
|
listProviders: (): Promise<string[]> => ipcRenderer.invoke(IpcChannel.OCR_ListProviders)
|
||||||
},
|
},
|
||||||
cherryai: {
|
cherryai: {
|
||||||
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BuiltinOcrProvider,
|
BuiltinOcrProvider,
|
||||||
BuiltinOcrProviderId,
|
BuiltinOcrProviderId,
|
||||||
|
OcrOvProvider,
|
||||||
OcrPpocrProvider,
|
OcrPpocrProvider,
|
||||||
OcrProviderCapability,
|
OcrProviderCapability,
|
||||||
OcrSystemProvider,
|
OcrSystemProvider,
|
||||||
@ -50,10 +51,23 @@ const ppocrOcr: OcrPpocrProvider = {
|
|||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
const ovOcr: OcrOvProvider = {
|
||||||
|
id: 'ovocr',
|
||||||
|
name: 'Intel OV(NPU) OCR',
|
||||||
|
config: {
|
||||||
|
langs: isWin ? ['en-us', 'zh-cn'] : undefined
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
image: true
|
||||||
|
// pdf: true
|
||||||
|
}
|
||||||
|
} as const satisfies OcrOvProvider
|
||||||
|
|
||||||
export const BUILTIN_OCR_PROVIDERS_MAP = {
|
export const BUILTIN_OCR_PROVIDERS_MAP = {
|
||||||
tesseract,
|
tesseract,
|
||||||
system: systemOcr,
|
system: systemOcr,
|
||||||
paddleocr: ppocrOcr
|
paddleocr: ppocrOcr,
|
||||||
|
ovocr: ovOcr
|
||||||
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
||||||
|
|
||||||
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(BUILTIN_OCR_PROVIDERS_MAP)
|
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(BUILTIN_OCR_PROVIDERS_MAP)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import IntelLogo from '@renderer/assets/images/providers/intel.png'
|
||||||
import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png'
|
import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png'
|
||||||
import TesseractLogo from '@renderer/assets/images/providers/Tesseract.js.png'
|
import TesseractLogo from '@renderer/assets/images/providers/Tesseract.js.png'
|
||||||
import { BUILTIN_OCR_PROVIDERS_MAP, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr'
|
import { BUILTIN_OCR_PROVIDERS_MAP, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr'
|
||||||
@ -83,6 +84,8 @@ export const useOcrProviders = () => {
|
|||||||
return <MonitorIcon size={size} />
|
return <MonitorIcon size={size} />
|
||||||
case 'paddleocr':
|
case 'paddleocr':
|
||||||
return <Avatar size={size} src={PaddleocrLogo} />
|
return <Avatar size={size} src={PaddleocrLogo} />
|
||||||
|
case 'ovocr':
|
||||||
|
return <Avatar size={size} src={IntelLogo} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <FileQuestionMarkIcon size={size} />
|
return <FileQuestionMarkIcon size={size} />
|
||||||
|
|||||||
@ -340,12 +340,14 @@ export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {
|
|||||||
const builtinOcrProviderKeyMap = {
|
const builtinOcrProviderKeyMap = {
|
||||||
system: 'ocr.builtin.system',
|
system: 'ocr.builtin.system',
|
||||||
tesseract: '',
|
tesseract: '',
|
||||||
paddleocr: ''
|
paddleocr: '',
|
||||||
|
ovocr: ''
|
||||||
} as const satisfies Record<BuiltinOcrProviderId, string>
|
} as const satisfies Record<BuiltinOcrProviderId, string>
|
||||||
|
|
||||||
export const getBuiltinOcrProviderLabel = (key: BuiltinOcrProviderId) => {
|
export const getBuiltinOcrProviderLabel = (key: BuiltinOcrProviderId) => {
|
||||||
if (key === 'tesseract') return 'Tesseract'
|
if (key === 'tesseract') return 'Tesseract'
|
||||||
else if (key == 'paddleocr') return 'PaddleOCR'
|
else if (key == 'paddleocr') return 'PaddleOCR'
|
||||||
|
else if (key == 'ovocr') return 'Intel OV(NPU) OCR'
|
||||||
else return getLabel(builtinOcrProviderKeyMap, key)
|
else return getLabel(builtinOcrProviderKeyMap, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2049,6 +2049,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "Cannot delete built-in provider",
|
"cannot_remove_builtin": "Cannot delete built-in provider",
|
||||||
"existing": "The provider already exists",
|
"existing": "The provider already exists",
|
||||||
|
"get_providers": "Failed to get available providers",
|
||||||
"not_found": "OCR provider does not exist",
|
"not_found": "OCR provider does not exist",
|
||||||
"update_failed": "Failed to update configuration"
|
"update_failed": "Failed to update configuration"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2049,6 +2049,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "不能删除内置提供商",
|
"cannot_remove_builtin": "不能删除内置提供商",
|
||||||
"existing": "提供商已存在",
|
"existing": "提供商已存在",
|
||||||
|
"get_providers": "获取可用提供商失败",
|
||||||
"not_found": "OCR 提供商不存在",
|
"not_found": "OCR 提供商不存在",
|
||||||
"update_failed": "更新配置失败"
|
"update_failed": "更新配置失败"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2048,8 +2048,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "不能刪除內建提供者",
|
"cannot_remove_builtin": "不能刪除內建提供者",
|
||||||
"existing": "提供商已存在",
|
"existing": "提供者已存在",
|
||||||
"not_found": "OCR 提供商不存在",
|
"get_providers": "取得可用提供者失敗",
|
||||||
|
"not_found": "OCR 提供者不存在",
|
||||||
"update_failed": "更新配置失敗"
|
"update_failed": "更新配置失敗"
|
||||||
},
|
},
|
||||||
"unknown": "OCR過程發生錯誤"
|
"unknown": "OCR過程發生錯誤"
|
||||||
|
|||||||
@ -2041,6 +2041,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "Δεν είναι δυνατή η διαγραφή του ενσωματωμένου παρόχου",
|
"cannot_remove_builtin": "Δεν είναι δυνατή η διαγραφή του ενσωματωμένου παρόχου",
|
||||||
"existing": "Ο πάροχος υπηρεσιών υπάρχει ήδη",
|
"existing": "Ο πάροχος υπηρεσιών υπάρχει ήδη",
|
||||||
|
"get_providers": "Αποτυχία λήψης διαθέσιμων παρόχων",
|
||||||
"not_found": "Ο πάροχος OCR δεν υπάρχει",
|
"not_found": "Ο πάροχος OCR δεν υπάρχει",
|
||||||
"update_failed": "Αποτυχία ενημέρωσης της διαμόρφωσης"
|
"update_failed": "Αποτυχία ενημέρωσης της διαμόρφωσης"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2041,6 +2041,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "No se puede eliminar el proveedor integrado",
|
"cannot_remove_builtin": "No se puede eliminar el proveedor integrado",
|
||||||
"existing": "El proveedor ya existe",
|
"existing": "El proveedor ya existe",
|
||||||
|
"get_providers": "Error al obtener proveedores disponibles",
|
||||||
"not_found": "El proveedor de OCR no existe",
|
"not_found": "El proveedor de OCR no existe",
|
||||||
"update_failed": "Actualización de la configuración fallida"
|
"update_failed": "Actualización de la configuración fallida"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2041,6 +2041,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "Impossible de supprimer le fournisseur intégré",
|
"cannot_remove_builtin": "Impossible de supprimer le fournisseur intégré",
|
||||||
"existing": "Le fournisseur existe déjà",
|
"existing": "Le fournisseur existe déjà",
|
||||||
|
"get_providers": "Échec de l'obtention des fournisseurs disponibles",
|
||||||
"not_found": "Le fournisseur OCR n'existe pas",
|
"not_found": "Le fournisseur OCR n'existe pas",
|
||||||
"update_failed": "Échec de la mise à jour de la configuration"
|
"update_failed": "Échec de la mise à jour de la configuration"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2041,6 +2041,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "組み込みプロバイダーは削除できません",
|
"cannot_remove_builtin": "組み込みプロバイダーは削除できません",
|
||||||
"existing": "プロバイダーはすでに存在します",
|
"existing": "プロバイダーはすでに存在します",
|
||||||
|
"get_providers": "利用可能なプロバイダーの取得に失敗しました",
|
||||||
"not_found": "OCRプロバイダーが存在しません",
|
"not_found": "OCRプロバイダーが存在しません",
|
||||||
"update_failed": "更新構成に失敗しました"
|
"update_failed": "更新構成に失敗しました"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2041,6 +2041,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "Não é possível excluir o provedor integrado",
|
"cannot_remove_builtin": "Não é possível excluir o provedor integrado",
|
||||||
"existing": "O provedor já existe",
|
"existing": "O provedor já existe",
|
||||||
|
"get_providers": "Falha ao obter provedores disponíveis",
|
||||||
"not_found": "O provedor OCR não existe",
|
"not_found": "O provedor OCR não existe",
|
||||||
"update_failed": "Falha ao atualizar a configuração"
|
"update_failed": "Falha ao atualizar a configuração"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2041,6 +2041,7 @@
|
|||||||
"provider": {
|
"provider": {
|
||||||
"cannot_remove_builtin": "Не удается удалить встроенного поставщика",
|
"cannot_remove_builtin": "Не удается удалить встроенного поставщика",
|
||||||
"existing": "Поставщик уже существует",
|
"existing": "Поставщик уже существует",
|
||||||
|
"get_providers": "Не удалось получить доступных поставщиков",
|
||||||
"not_found": "Поставщик OCR отсутствует",
|
"not_found": "Поставщик OCR отсутствует",
|
||||||
"update_failed": "Обновление конфигурации не удалось"
|
"update_failed": "Обновление конфигурации не удалось"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
|
import { Alert, Skeleton } from '@heroui/react'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { ErrorTag } from '@renderer/components/Tags/ErrorTag'
|
import { ErrorTag } from '@renderer/components/Tags/ErrorTag'
|
||||||
import { isMac, isWin } from '@renderer/config/constant'
|
import { isMac, isWin } from '@renderer/config/constant'
|
||||||
import { useOcrProviders } from '@renderer/hooks/useOcrProvider'
|
import { useOcrProviders } from '@renderer/hooks/useOcrProvider'
|
||||||
import { BuiltinOcrProviderIds, ImageOcrProvider, isImageOcrProvider, OcrProvider } from '@renderer/types'
|
import { BuiltinOcrProviderIds, ImageOcrProvider, isImageOcrProvider, OcrProvider } from '@renderer/types'
|
||||||
|
import { getErrorMessage } from '@renderer/utils'
|
||||||
import { Select } from 'antd'
|
import { Select } from 'antd'
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useCallback, useEffect, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useSWRImmutable from 'swr/immutable'
|
||||||
|
|
||||||
import { SettingRow, SettingRowTitle } from '..'
|
import { SettingRow, SettingRowTitle } from '..'
|
||||||
|
|
||||||
@ -18,10 +21,16 @@ type Props = {
|
|||||||
const OcrImageSettings = ({ setProvider }: Props) => {
|
const OcrImageSettings = ({ setProvider }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { providers, imageProvider, getOcrProviderName, setImageProviderId } = useOcrProviders()
|
const { providers, imageProvider, getOcrProviderName, setImageProviderId } = useOcrProviders()
|
||||||
|
const fetcher = useCallback(() => {
|
||||||
|
return window.api.ocr.listProviders()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { data: validProviders, isLoading, error } = useSWRImmutable('ocr/providers', fetcher)
|
||||||
|
|
||||||
const imageProviders = providers.filter((p) => isImageOcrProvider(p))
|
const imageProviders = providers.filter((p) => isImageOcrProvider(p))
|
||||||
|
|
||||||
// 挂载时更新外部状态
|
// 挂载时更新外部状态
|
||||||
|
// FIXME: Just keep the imageProvider always valid, so we don't need update it in this component.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProvider(imageProvider)
|
setProvider(imageProvider)
|
||||||
}, [imageProvider, setProvider])
|
}, [imageProvider, setProvider])
|
||||||
@ -40,12 +49,17 @@ const OcrImageSettings = ({ setProvider }: Props) => {
|
|||||||
|
|
||||||
const platformSupport = isMac || isWin
|
const platformSupport = isMac || isWin
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
|
if (!validProviders) return []
|
||||||
const platformFilter = platformSupport ? () => true : (p: ImageOcrProvider) => p.id !== BuiltinOcrProviderIds.system
|
const platformFilter = platformSupport ? () => true : (p: ImageOcrProvider) => p.id !== BuiltinOcrProviderIds.system
|
||||||
return imageProviders.filter(platformFilter).map((p) => ({
|
const validFilter = (p: ImageOcrProvider) => validProviders.includes(p.id)
|
||||||
value: p.id,
|
return imageProviders
|
||||||
label: getOcrProviderName(p)
|
.filter(platformFilter)
|
||||||
}))
|
.filter(validFilter)
|
||||||
}, [getOcrProviderName, imageProviders, platformSupport])
|
.map((p) => ({
|
||||||
|
value: p.id,
|
||||||
|
label: getOcrProviderName(p)
|
||||||
|
}))
|
||||||
|
}, [getOcrProviderName, imageProviders, platformSupport, validProviders])
|
||||||
|
|
||||||
const isSystem = imageProvider.id === BuiltinOcrProviderIds.system
|
const isSystem = imageProvider.id === BuiltinOcrProviderIds.system
|
||||||
|
|
||||||
@ -55,12 +69,23 @@ const OcrImageSettings = ({ setProvider }: Props) => {
|
|||||||
<SettingRowTitle>{t('settings.tool.ocr.image_provider')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.tool.ocr.image_provider')}</SettingRowTitle>
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
{!platformSupport && isSystem && <ErrorTag message={t('settings.tool.ocr.error.not_system')} />}
|
{!platformSupport && isSystem && <ErrorTag message={t('settings.tool.ocr.error.not_system')} />}
|
||||||
<Select
|
<Skeleton isLoaded={!isLoading}>
|
||||||
value={imageProvider.id}
|
{!error && (
|
||||||
style={{ width: '200px' }}
|
<Select
|
||||||
onChange={(id: string) => setImageProvider(id)}
|
value={imageProvider.id}
|
||||||
options={options}
|
style={{ width: '200px' }}
|
||||||
/>
|
onChange={(id: string) => setImageProvider(id)}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Alert
|
||||||
|
color="danger"
|
||||||
|
title={t('ocr.error.provider.get_providers')}
|
||||||
|
description={getErrorMessage(error)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Skeleton>
|
||||||
</div>
|
</div>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { useOcrProvider } from '@renderer/hooks/useOcrProvider'
|
||||||
|
import { BuiltinOcrProviderIds, isOcrOVProvider } from '@renderer/types'
|
||||||
|
import { Flex, Tag } from 'antd'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { SettingRow, SettingRowTitle } from '..'
|
||||||
|
|
||||||
|
export const OcrOVSettings = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { provider } = useOcrProvider(BuiltinOcrProviderIds.ovocr)
|
||||||
|
|
||||||
|
if (!isOcrOVProvider(provider)) {
|
||||||
|
throw new Error('Not OV OCR provider.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>
|
||||||
|
<Flex align="center" gap={4}>
|
||||||
|
{t('settings.tool.ocr.common.langs')}
|
||||||
|
</Flex>
|
||||||
|
</SettingRowTitle>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<Tag>🇬🇧 {t('languages.english')}</Tag>
|
||||||
|
<Tag>🇨🇳 {t('languages.chinese')}</Tag>
|
||||||
|
<Tag>🇭🇰 {t('languages.chinese-traditional')}</Tag>
|
||||||
|
</div>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import { Divider, Flex } from 'antd'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingGroup, SettingTitle } from '..'
|
import { SettingGroup, SettingTitle } from '..'
|
||||||
|
import { OcrOVSettings } from './OcrOVSettings'
|
||||||
import { OcrPpocrSettings } from './OcrPpocrSettings'
|
import { OcrPpocrSettings } from './OcrPpocrSettings'
|
||||||
import { OcrSystemSettings } from './OcrSystemSettings'
|
import { OcrSystemSettings } from './OcrSystemSettings'
|
||||||
import { OcrTesseractSettings } from './OcrTesseractSettings'
|
import { OcrTesseractSettings } from './OcrTesseractSettings'
|
||||||
@ -35,6 +36,8 @@ const OcrProviderSettings = ({ provider }: Props) => {
|
|||||||
return <OcrSystemSettings />
|
return <OcrSystemSettings />
|
||||||
case 'paddleocr':
|
case 'paddleocr':
|
||||||
return <OcrPpocrSettings />
|
return <OcrPpocrSettings />
|
||||||
|
case 'ovocr':
|
||||||
|
return <OcrOVSettings />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2667,6 +2667,15 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 162 error', error as Error)
|
logger.error('migrate 162 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'163': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
addOcrProvider(state, BUILTIN_OCR_PROVIDERS_MAP.ovocr)
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 163 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { FileMetadata, ImageFileMetadata, isImageFileMetadata, TranslateLanguage
|
|||||||
export const BuiltinOcrProviderIds = {
|
export const BuiltinOcrProviderIds = {
|
||||||
tesseract: 'tesseract',
|
tesseract: 'tesseract',
|
||||||
system: 'system',
|
system: 'system',
|
||||||
paddleocr: 'paddleocr'
|
paddleocr: 'paddleocr',
|
||||||
|
ovocr: 'ovocr'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type BuiltinOcrProviderId = keyof typeof BuiltinOcrProviderIds
|
export type BuiltinOcrProviderId = keyof typeof BuiltinOcrProviderIds
|
||||||
@ -188,3 +189,19 @@ export type OcrPpocrProvider = {
|
|||||||
export const isOcrPpocrProvider = (p: OcrProvider): p is OcrPpocrProvider => {
|
export const isOcrPpocrProvider = (p: OcrProvider): p is OcrPpocrProvider => {
|
||||||
return p.id === BuiltinOcrProviderIds.paddleocr
|
return p.id === BuiltinOcrProviderIds.paddleocr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OV OCR Types
|
||||||
|
export type OcrOvConfig = OcrProviderBaseConfig & {
|
||||||
|
langs?: TranslateLanguageCode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OcrOvProvider = {
|
||||||
|
id: 'ovocr'
|
||||||
|
config: OcrOvConfig
|
||||||
|
} & ImageOcrProvider &
|
||||||
|
// PdfOcrProvider &
|
||||||
|
BuiltinOcrProvider
|
||||||
|
|
||||||
|
export const isOcrOVProvider = (p: OcrProvider): p is OcrOvProvider => {
|
||||||
|
return p.id === BuiltinOcrProviderIds.ovocr
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user