diff --git a/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch b/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch new file mode 100644 index 0000000000..0cb156ee99 --- /dev/null +++ b/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch @@ -0,0 +1,348 @@ +diff --git a/src/constants/languages.d.ts b/src/constants/languages.d.ts +new file mode 100644 +index 0000000000000000000000000000000000000000..6a2ba5086187622b8ca8887bcc7406018fba8a89 +--- /dev/null ++++ b/src/constants/languages.d.ts +@@ -0,0 +1,43 @@ ++/** ++ * Languages with existing tesseract traineddata ++ * https://tesseract-ocr.github.io/tessdoc/Data-Files#data-files-for-version-400-november-29-2016 ++ */ ++ ++// Define the language codes as string literals ++type LanguageCode = ++ | 'afr' | 'amh' | 'ara' | 'asm' | 'aze' | 'aze_cyrl' | 'bel' | 'ben' | 'bod' | 'bos' ++ | 'bul' | 'cat' | 'ceb' | 'ces' | 'chi_sim' | 'chi_tra' | 'chr' | 'cym' | 'dan' | 'deu' ++ | 'dzo' | 'ell' | 'eng' | 'enm' | 'epo' | 'est' | 'eus' | 'fas' | 'fin' | 'fra' ++ | 'frk' | 'frm' | 'gle' | 'glg' | 'grc' | 'guj' | 'hat' | 'heb' | 'hin' | 'hrv' ++ | 'hun' | 'iku' | 'ind' | 'isl' | 'ita' | 'ita_old' | 'jav' | 'jpn' | 'kan' | 'kat' ++ | 'kat_old' | 'kaz' | 'khm' | 'kir' | 'kor' | 'kur' | 'lao' | 'lat' | 'lav' | 'lit' ++ | 'mal' | 'mar' | 'mkd' | 'mlt' | 'msa' | 'mya' | 'nep' | 'nld' | 'nor' | 'ori' ++ | 'pan' | 'pol' | 'por' | 'pus' | 'ron' | 'rus' | 'san' | 'sin' | 'slk' | 'slv' ++ | 'spa' | 'spa_old' | 'sqi' | 'srp' | 'srp_latn' | 'swa' | 'swe' | 'syr' | 'tam' | 'tel' ++ | 'tgk' | 'tgl' | 'tha' | 'tir' | 'tur' | 'uig' | 'ukr' | 'urd' | 'uzb' | 'uzb_cyrl' ++ | 'vie' | 'yid'; ++ ++// Define the language keys as string literals ++type LanguageKey = ++ | 'AFR' | 'AMH' | 'ARA' | 'ASM' | 'AZE' | 'AZE_CYRL' | 'BEL' | 'BEN' | 'BOD' | 'BOS' ++ | 'BUL' | 'CAT' | 'CEB' | 'CES' | 'CHI_SIM' | 'CHI_TRA' | 'CHR' | 'CYM' | 'DAN' | 'DEU' ++ | 'DZO' | 'ELL' | 'ENG' | 'ENM' | 'EPO' | 'EST' | 'EUS' | 'FAS' | 'FIN' | 'FRA' ++ | 'FRK' | 'FRM' | 'GLE' | 'GLG' | 'GRC' | 'GUJ' | 'HAT' | 'HEB' | 'HIN' | 'HRV' ++ | 'HUN' | 'IKU' | 'IND' | 'ISL' | 'ITA' | 'ITA_OLD' | 'JAV' | 'JPN' | 'KAN' | 'KAT' ++ | 'KAT_OLD' | 'KAZ' | 'KHM' | 'KIR' | 'KOR' | 'KUR' | 'LAO' | 'LAT' | 'LAV' | 'LIT' ++ | 'MAL' | 'MAR' | 'MKD' | 'MLT' | 'MSA' | 'MYA' | 'NEP' | 'NLD' | 'NOR' | 'ORI' ++ | 'PAN' | 'POL' | 'POR' | 'PUS' | 'RON' | 'RUS' | 'SAN' | 'SIN' | 'SLK' | 'SLV' ++ | 'SPA' | 'SPA_OLD' | 'SQI' | 'SRP' | 'SRP_LATN' | 'SWA' | 'SWE' | 'SYR' | 'TAM' | 'TEL' ++ | 'TGK' | 'TGL' | 'THA' | 'TIR' | 'TUR' | 'UIG' | 'UKR' | 'URD' | 'UZB' | 'UZB_CYRL' ++ | 'VIE' | 'YID'; ++ ++// Create a mapped type to ensure each key maps to its specific value ++type LanguagesMap = { ++ [K in LanguageKey]: LanguageCode; ++}; ++ ++// Declare the exported constant with the specific type ++export const LANGUAGES: LanguagesMap; ++ ++// Export the individual types for use in other modules ++export type { LanguageCode, LanguageKey, LanguagesMap }; +\ No newline at end of file +diff --git a/src/index.d.ts b/src/index.d.ts +index 1f5a9c8094fe4de7983467f9efb43bdb4de535f2..16dc95cf68663673e37e189b719cb74897b7735f 100644 +--- a/src/index.d.ts ++++ b/src/index.d.ts +@@ -1,31 +1,74 @@ ++// Import the languages types ++import { LanguagesMap } from "./constants/languages"; ++ ++/// ++ + declare namespace Tesseract { +- function createScheduler(): Scheduler +- function createWorker(langs?: string | string[] | Lang[], oem?: OEM, options?: Partial, config?: string | Partial): Promise +- function setLogging(logging: boolean): void +- function recognize(image: ImageLike, langs?: string, options?: Partial): Promise +- function detect(image: ImageLike, options?: Partial): any ++ function createScheduler(): Scheduler; ++ function createWorker( ++ langs?: LanguageCode | LanguageCode[] | Lang[], ++ oem?: OEM, ++ options?: Partial, ++ config?: string | Partial ++ ): Promise; ++ function setLogging(logging: boolean): void; ++ function recognize( ++ image: ImageLike, ++ langs?: LanguageCode, ++ options?: Partial ++ ): Promise; ++ function detect(image: ImageLike, options?: Partial): any; ++ ++ // Export languages constant ++ const languages: LanguagesMap; ++ ++ type LanguageCode = import("./constants/languages").LanguageCode; ++ type LanguageKey = import("./constants/languages").LanguageKey; + + interface Scheduler { +- addWorker(worker: Worker): string +- addJob(action: 'recognize', ...args: Parameters): Promise +- addJob(action: 'detect', ...args: Parameters): Promise +- terminate(): Promise +- getQueueLen(): number +- getNumWorkers(): number ++ addWorker(worker: Worker): string; ++ addJob( ++ action: "recognize", ++ ...args: Parameters ++ ): Promise; ++ addJob( ++ action: "detect", ++ ...args: Parameters ++ ): Promise; ++ terminate(): Promise; ++ getQueueLen(): number; ++ getNumWorkers(): number; + } + + interface Worker { +- load(jobId?: string): Promise +- writeText(path: string, text: string, jobId?: string): Promise +- readText(path: string, jobId?: string): Promise +- removeText(path: string, jobId?: string): Promise +- FS(method: string, args: any[], jobId?: string): Promise +- reinitialize(langs?: string | Lang[], oem?: OEM, config?: string | Partial, jobId?: string): Promise +- setParameters(params: Partial, jobId?: string): Promise +- getImage(type: imageType): string +- recognize(image: ImageLike, options?: Partial, output?: Partial, jobId?: string): Promise +- detect(image: ImageLike, jobId?: string): Promise +- terminate(jobId?: string): Promise ++ load(jobId?: string): Promise; ++ writeText( ++ path: string, ++ text: string, ++ jobId?: string ++ ): Promise; ++ readText(path: string, jobId?: string): Promise; ++ removeText(path: string, jobId?: string): Promise; ++ FS(method: string, args: any[], jobId?: string): Promise; ++ reinitialize( ++ langs?: string | Lang[], ++ oem?: OEM, ++ config?: string | Partial, ++ jobId?: string ++ ): Promise; ++ setParameters( ++ params: Partial, ++ jobId?: string ++ ): Promise; ++ getImage(type: imageType): string; ++ recognize( ++ image: ImageLike, ++ options?: Partial, ++ output?: Partial, ++ jobId?: string ++ ): Promise; ++ detect(image: ImageLike, jobId?: string): Promise; ++ terminate(jobId?: string): Promise; + } + + interface Lang { +@@ -34,43 +77,43 @@ declare namespace Tesseract { + } + + interface InitOptions { +- load_system_dawg: string +- load_freq_dawg: string +- load_unambig_dawg: string +- load_punc_dawg: string +- load_number_dawg: string +- load_bigram_dawg: string +- } +- +- type LoggerMessage = { +- jobId: string +- progress: number +- status: string +- userJobId: string +- workerId: string ++ load_system_dawg: string; ++ load_freq_dawg: string; ++ load_unambig_dawg: string; ++ load_punc_dawg: string; ++ load_number_dawg: string; ++ load_bigram_dawg: string; + } +- ++ ++ type LoggerMessage = { ++ jobId: string; ++ progress: number; ++ status: string; ++ userJobId: string; ++ workerId: string; ++ }; ++ + interface WorkerOptions { +- corePath: string +- langPath: string +- cachePath: string +- dataPath: string +- workerPath: string +- cacheMethod: string +- workerBlobURL: boolean +- gzip: boolean +- legacyLang: boolean +- legacyCore: boolean +- logger: (arg: LoggerMessage) => void, +- errorHandler: (arg: any) => void ++ corePath: string; ++ langPath: string; ++ cachePath: string; ++ dataPath: string; ++ workerPath: string; ++ cacheMethod: string; ++ workerBlobURL: boolean; ++ gzip: boolean; ++ legacyLang: boolean; ++ legacyCore: boolean; ++ logger: (arg: LoggerMessage) => void; ++ errorHandler: (arg: any) => void; + } + interface WorkerParams { +- tessedit_pageseg_mode: PSM +- tessedit_char_whitelist: string +- tessedit_char_blacklist: string +- preserve_interword_spaces: string +- user_defined_dpi: string +- [propName: string]: any ++ tessedit_pageseg_mode: PSM; ++ tessedit_char_whitelist: string; ++ tessedit_char_blacklist: string; ++ preserve_interword_spaces: string; ++ user_defined_dpi: string; ++ [propName: string]: any; + } + interface OutputFormats { + text: boolean; +@@ -88,36 +131,36 @@ declare namespace Tesseract { + debug: boolean; + } + interface RecognizeOptions { +- rectangle: Rectangle +- pdfTitle: string +- pdfTextOnly: boolean +- rotateAuto: boolean +- rotateRadians: number ++ rectangle: Rectangle; ++ pdfTitle: string; ++ pdfTextOnly: boolean; ++ rotateAuto: boolean; ++ rotateRadians: number; + } + interface ConfigResult { +- jobId: string +- data: any ++ jobId: string; ++ data: any; + } + interface RecognizeResult { +- jobId: string +- data: Page ++ jobId: string; ++ data: Page; + } + interface DetectResult { +- jobId: string +- data: DetectData ++ jobId: string; ++ data: DetectData; + } + interface DetectData { +- tesseract_script_id: number | null +- script: string | null +- script_confidence: number | null +- orientation_degrees: number | null +- orientation_confidence: number | null ++ tesseract_script_id: number | null; ++ script: string | null; ++ script_confidence: number | null; ++ orientation_degrees: number | null; ++ orientation_confidence: number | null; + } + interface Rectangle { +- left: number +- top: number +- width: number +- height: number ++ left: number; ++ top: number; ++ width: number; ++ height: number; + } + enum OEM { + TESSERACT_ONLY, +@@ -126,28 +169,36 @@ declare namespace Tesseract { + DEFAULT, + } + enum PSM { +- OSD_ONLY = '0', +- AUTO_OSD = '1', +- AUTO_ONLY = '2', +- AUTO = '3', +- SINGLE_COLUMN = '4', +- SINGLE_BLOCK_VERT_TEXT = '5', +- SINGLE_BLOCK = '6', +- SINGLE_LINE = '7', +- SINGLE_WORD = '8', +- CIRCLE_WORD = '9', +- SINGLE_CHAR = '10', +- SPARSE_TEXT = '11', +- SPARSE_TEXT_OSD = '12', +- RAW_LINE = '13' ++ OSD_ONLY = "0", ++ AUTO_OSD = "1", ++ AUTO_ONLY = "2", ++ AUTO = "3", ++ SINGLE_COLUMN = "4", ++ SINGLE_BLOCK_VERT_TEXT = "5", ++ SINGLE_BLOCK = "6", ++ SINGLE_LINE = "7", ++ SINGLE_WORD = "8", ++ CIRCLE_WORD = "9", ++ SINGLE_CHAR = "10", ++ SPARSE_TEXT = "11", ++ SPARSE_TEXT_OSD = "12", ++ RAW_LINE = "13", + } + const enum imageType { + COLOR = 0, + GREY = 1, +- BINARY = 2 ++ BINARY = 2, + } +- type ImageLike = string | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement +- | CanvasRenderingContext2D | File | Blob | Buffer | OffscreenCanvas; ++ type ImageLike = ++ | string ++ | HTMLImageElement ++ | HTMLCanvasElement ++ | HTMLVideoElement ++ | CanvasRenderingContext2D ++ | File ++ | Blob ++ | (typeof Buffer extends undefined ? never : Buffer) ++ | OffscreenCanvas; + interface Block { + paragraphs: Paragraph[]; + text: string; +@@ -179,7 +230,7 @@ declare namespace Tesseract { + text: string; + confidence: number; + baseline: Baseline; +- rowAttributes: RowAttributes ++ rowAttributes: RowAttributes; + bbox: Bbox; + } + interface Paragraph { diff --git a/package.json b/package.json index 472326ee65..5613c74d3f 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "officeparser": "^4.2.0", "os-proxy-config": "^1.1.2", "selection-hook": "^1.0.11", + "tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch", "turndown": "7.2.0" }, "devDependencies": { @@ -257,6 +258,7 @@ "remove-markdown": "^0.6.2", "rollup-plugin-visualizer": "^5.12.0", "sass": "^1.88.0", + "sharp": "^0.34.3", "shiki": "^3.9.1", "strict-url-sanitise": "^0.0.1", "string-width": "^7.2.0", @@ -296,7 +298,8 @@ "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", "pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch", "undici": "6.21.2", - "vite": "npm:rolldown-vite@latest" + "vite": "npm:rolldown-vite@latest", + "tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch" }, "packageManager": "yarn@4.9.1", "lint-staged": { diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 56ebfb3d58..f35db50bc6 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -281,5 +281,8 @@ export enum IpcChannel { TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage', // CodeTools - CodeTools_Run = 'code-tools:run' + CodeTools_Run = 'code-tools:run', + + // OCR + OCR_ocr = 'ocr:ocr' } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 2183c30831..3d72b67390 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -30,6 +30,7 @@ import { openTraceWindow, setTraceWindowTitle } from './services/NodeTraceServic import NotificationService from './services/NotificationService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' +import { ocrService } from './services/ocr/OcrService' import { proxyManager } from './services/ProxyManager' import { pythonService } from './services/PythonService' import { FileServiceManager } from './services/remotefile/FileServiceManager' @@ -709,4 +710,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // CodeTools ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run) + + // OCR + ipcMain.handle(IpcChannel.OCR_ocr, (_, ...args: Parameters) => ocrService.ocr(...args)) } diff --git a/src/main/services/ocr/OcrService.ts b/src/main/services/ocr/OcrService.ts new file mode 100644 index 0000000000..6ac8c311e3 --- /dev/null +++ b/src/main/services/ocr/OcrService.ts @@ -0,0 +1,34 @@ +import { loggerService } from '@logger' +import { BuiltinOcrProviderIds, OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types' + +import { tesseractService } from './tesseract/TesseractService' + +const logger = loggerService.withContext('OcrService') + +export class OcrService { + private registry: Map = new Map() + + register(providerId: string, handler: OcrHandler): void { + if (this.registry.has(providerId)) { + logger.warn(`Provider ${providerId} has existing handler. Overwrited.`) + } + this.registry.set(providerId, handler) + } + + unregister(providerId: string): void { + this.registry.delete(providerId) + } + + public async ocr(file: SupportedOcrFile, provider: OcrProvider): Promise { + const handler = this.registry.get(provider.id) + if (!handler) { + throw new Error(`Provider ${provider.id} is not registered`) + } + return handler(file) + } +} + +export const ocrService = new OcrService() + +// Register built-in providers +ocrService.register(BuiltinOcrProviderIds.tesseract, tesseractService.ocr.bind(tesseractService)) diff --git a/src/main/services/ocr/tesseract/TesseractService.ts b/src/main/services/ocr/tesseract/TesseractService.ts new file mode 100644 index 0000000000..d2ba6d2ed8 --- /dev/null +++ b/src/main/services/ocr/tesseract/TesseractService.ts @@ -0,0 +1,82 @@ +import { loggerService } from '@logger' +import { getIpCountry } from '@main/utils/ipService' +import { loadOcrImage } from '@main/utils/ocr' +import { MB } from '@shared/config/constant' +import { ImageFileMetadata, isImageFile, OcrResult, SupportedOcrFile } from '@types' +import { app } from 'electron' +import fs from 'fs' +import path from 'path' +import Tesseract, { createWorker, LanguageCode } from 'tesseract.js' + +const logger = loggerService.withContext('TesseractService') + +// config +const MB_SIZE_THRESHOLD = 50 +const tesseractLangs = ['chi_sim', 'chi_tra', 'eng'] satisfies LanguageCode[] +enum TesseractLangsDownloadUrl { + CN = 'https://gitcode.com/beyondkmp/tessdata/releases/download/4.1.0/', + GLOBAL = 'https://github.com/tesseract-ocr/tessdata/raw/main/' +} + +export class TesseractService { + private worker: Tesseract.Worker | null = null + + async getWorker(): Promise { + if (!this.worker) { + // for now, only support limited languages + this.worker = await createWorker(tesseractLangs, undefined, { + langPath: await this._getLangPath(), + cachePath: await this._getCacheDir(), + gzip: false, + logger: (m) => logger.debug('From worker', m) + }) + } + return this.worker + } + + async imageOcr(file: ImageFileMetadata): Promise { + const worker = await this.getWorker() + const stat = await fs.promises.stat(file.path) + if (stat.size > MB_SIZE_THRESHOLD * MB) { + throw new Error(`This image is too large (max ${MB_SIZE_THRESHOLD}MB)`) + } + const buffer = await loadOcrImage(file) + const result = await worker.recognize(buffer) + return { text: result.data.text } + } + + async ocr(file: SupportedOcrFile): Promise { + if (!isImageFile(file)) { + throw new Error('Only image files are supported currently') + } + return this.imageOcr(file) + } + + private async _getLangPath(): Promise { + const country = await getIpCountry() + return country.toLowerCase() === 'cn' ? TesseractLangsDownloadUrl.CN : TesseractLangsDownloadUrl.GLOBAL + } + + private async _getCacheDir(): Promise { + const cacheDir = path.join(app.getPath('userData'), 'tesseract') + // use access to check if the directory exists + if ( + !(await fs.promises + .access(cacheDir, fs.constants.F_OK) + .then(() => true) + .catch(() => false)) + ) { + await fs.promises.mkdir(cacheDir, { recursive: true }) + } + return cacheDir + } + + async dispose(): Promise { + if (this.worker) { + await this.worker.terminate() + this.worker = null + } + } +} + +export const tesseractService = new TesseractService() diff --git a/src/main/utils/ocr.ts b/src/main/utils/ocr.ts new file mode 100644 index 0000000000..b0079f2a50 --- /dev/null +++ b/src/main/utils/ocr.ts @@ -0,0 +1,29 @@ +import { ImageFileMetadata } from '@types' +import { readFile } from 'fs/promises' +import sharp from 'sharp' + +const preprocessImage = async (buffer: Buffer) => { + return await sharp(buffer) + .grayscale() // 转为灰度 + .normalize() + .sharpen() + .threshold(100) // 可能需要根据具体图片调整 + .png({ quality: 100 }) + .toBuffer() +} + +/** + * 加载并预处理OCR图像 + * @param file - 图像文件元数据 + * @returns 预处理后的图像Buffer + * @throws {Error} 当文件不存在或无法读取时抛出错误;当图像预处理失败时抛出错误 + * + * 预处理步骤: + * 1. 读取图像文件 + * 2. 转换为灰度图 + * 3. 后续可扩展其他预处理步骤 + */ +export const loadOcrImage = async (file: ImageFileMetadata): Promise => { + const buffer = await readFile(file.path) + return await preprocessImage(buffer) +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 1059826224..af4803fd50 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -17,9 +17,12 @@ import { MemoryConfig, MemoryListOptions, MemorySearchOptions, + OcrProvider, + OcrResult, Provider, S3Config, Shortcut, + SupportedOcrFile, ThemeMode, WebDavConfig } from '@types' @@ -406,6 +409,10 @@ const api = { env: Record, options?: { autoUpdateToLatest?: boolean } ) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options) + }, + ocr: { + ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise => + ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider) } } diff --git a/src/renderer/src/assets/images/providers/Tesseract.js.png b/src/renderer/src/assets/images/providers/Tesseract.js.png new file mode 100644 index 0000000000..d60b9b6878 Binary files /dev/null and b/src/renderer/src/assets/images/providers/Tesseract.js.png differ diff --git a/src/renderer/src/config/ocr.ts b/src/renderer/src/config/ocr.ts new file mode 100644 index 0000000000..b899cbb5f0 --- /dev/null +++ b/src/renderer/src/config/ocr.ts @@ -0,0 +1,32 @@ +import { + BuiltinOcrProvider, + BuiltinOcrProviderId, + ImageOcrProvider, + OcrProviderCapability, + OcrTesseractProvider +} from '@renderer/types' + +const tesseract: BuiltinOcrProvider & ImageOcrProvider & OcrTesseractProvider = { + id: 'tesseract', + name: 'Tesseract', + capabilities: { + image: true + }, + config: { + langs: { + chi_sim: true, + chi_tra: true, + eng: true + } + } +} as const satisfies OcrTesseractProvider + +export const BUILTIN_OCR_PROVIDERS_MAP = { + tesseract +} as const satisfies Record + +export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(BUILTIN_OCR_PROVIDERS_MAP) + +export const DEFAULT_OCR_PROVIDER = { + image: tesseract +} as const satisfies Record diff --git a/src/renderer/src/config/ocrProviders.ts b/src/renderer/src/config/ocrProviders.ts deleted file mode 100644 index 5e482e10ef..0000000000 --- a/src/renderer/src/config/ocrProviders.ts +++ /dev/null @@ -1,12 +0,0 @@ -import MacOSLogo from '@renderer/assets/images/providers/macos.svg' - -export function getOcrProviderLogo(providerId: string) { - switch (providerId) { - case 'system': - return MacOSLogo - default: - return undefined - } -} - -export const OCR_PROVIDER_CONFIG = {} diff --git a/src/renderer/src/hooks/useOcr.ts b/src/renderer/src/hooks/useOcr.ts new file mode 100644 index 0000000000..a1cbac0f8f --- /dev/null +++ b/src/renderer/src/hooks/useOcr.ts @@ -0,0 +1,54 @@ +import { loggerService } from '@logger' +import * as OcrService from '@renderer/services/ocr/OcrService' +import { useAppSelector } from '@renderer/store' +import { ImageFileMetadata, isImageFile, SupportedOcrFile } from '@renderer/types' +import { uuid } from '@renderer/utils' +import { formatErrorMessage } from '@renderer/utils/error' +import { useTranslation } from 'react-i18next' + +const logger = loggerService.withContext('useOcr') + +export const useOcr = () => { + const { t } = useTranslation() + const imageProvider = useAppSelector((state) => state.ocr.imageProvider) + + /** + * 对图片文件进行OCR识别 + * @param image 图片文件元数据 + * @returns OCR识别结果的Promise + * @throws OCR失败时抛出错误 + */ + const ocrImage = async (image: ImageFileMetadata) => { + return OcrService.ocr(image, imageProvider) + } + + /** + * 对支持的文件进行OCR识别. + * @param file 支持OCR的文件 + * @returns OCR识别结果的Promise + * @throws 当文件类型不支持或OCR失败时抛出错误 + */ + const ocr = async (file: SupportedOcrFile) => { + const key = uuid() + window.message.loading({ content: t('ocr.processing'), key, duration: 0 }) + // await to keep show loading message + try { + if (isImageFile(file)) { + return await ocrImage(file) + } else { + // @ts-expect-error all types should be covered + throw new Error(t('ocr.file.not_supported', { type: file.type })) + } + } catch (e) { + logger.error('Failed to ocr.', e as Error) + window.message.error(t('ocr.error.unknown') + ': ' + formatErrorMessage(e)) + throw e + } finally { + window.message.destroy(key) + } + } + + return { + ocr + } +} diff --git a/src/renderer/src/hooks/useOcrProvider.ts b/src/renderer/src/hooks/useOcrProvider.ts new file mode 100644 index 0000000000..ce2eb5b8fc --- /dev/null +++ b/src/renderer/src/hooks/useOcrProvider.ts @@ -0,0 +1,84 @@ +import { loggerService } from '@logger' +import { BUILTIN_OCR_PROVIDERS_MAP } from '@renderer/config/ocr' +import { useAppSelector } from '@renderer/store' +import { addOcrProvider, removeOcrProvider, updateOcrProviderConfig } from '@renderer/store/ocr' +import { isBuiltinOcrProviderId, OcrProvider, OcrProviderConfig } from '@renderer/types' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' + +const logger = loggerService.withContext('useOcrProvider') + +export const useOcrProviders = () => { + const providers = useAppSelector((state) => state.ocr.providers) + const dispatch = useDispatch() + const { t } = useTranslation() + + /** + * 添加一个新的OCR服务提供者 + * @param provider - OCR提供者对象,包含id和其他配置信息 + * @throws {Error} 当尝试添加一个已存在ID的提供者时抛出错误 + */ + const addProvider = (provider: OcrProvider) => { + if (providers.some((p) => p.id === provider.id)) { + const msg = `Provider with id ${provider.id} already exists` + logger.error(msg) + window.message.error(t('ocr.error.provider.existing')) + throw new Error(msg) + } + dispatch(addOcrProvider(provider)) + } + + /** + * 移除一个OCR服务提供者 + * @param id - 要移除的OCR提供者ID + * @throws {Error} 当尝试移除一个内置提供商时抛出错误 + */ + const removeProvider = (id: string) => { + if (isBuiltinOcrProviderId(id)) { + const msg = `Cannot remove builtin provider ${id}` + logger.error(msg) + window.message.error(t('ocr.error.provider.cannot_remove_builtin')) + throw new Error(msg) + } + + dispatch(removeOcrProvider(id)) + } + + return { providers, addProvider, removeProvider } +} + +export const useOcrProvider = (id: string) => { + const { t } = useTranslation() + const dispatch = useDispatch() + const { providers, addProvider } = useOcrProviders() + let provider = providers.find((p) => p.id === id) + + // safely fallback + if (!provider) { + logger.error(`Ocr Provider ${id} not found`) + window.message.error(t('ocr.error.provider.not_found')) + if (isBuiltinOcrProviderId(id)) { + try { + addProvider(BUILTIN_OCR_PROVIDERS_MAP[id]) + } catch (e) { + logger.warn(`Add ${BUILTIN_OCR_PROVIDERS_MAP[id].name} failed. Just use temp provider from config.`) + window.message.warning(t('ocr.warning.provider.fallback', { name: BUILTIN_OCR_PROVIDERS_MAP[id].name })) + } finally { + provider = BUILTIN_OCR_PROVIDERS_MAP[id] + } + } else { + logger.warn(`Fallback to tesseract`) + window.message.warning(t('ocr.warning.provider.fallback', { name: 'Tesseract' })) + provider = BUILTIN_OCR_PROVIDERS_MAP.tesseract + } + } + + const updateConfig = (update: Partial) => { + dispatch(updateOcrProviderConfig({ id: provider.id, update })) + } + + return { + provider, + updateConfig + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 48bb9664a1..9dbf612fa5 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1574,6 +1574,26 @@ }, "tip": "If the response is successful, then only messages exceeding 30 seconds will trigger a reminder" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "Cannot delete built-in provider", + "existing": "The provider already exists", + "not_found": "OCR provider does not exist", + "update_failed": "Failed to update configuration" + }, + "unknown": "An error occurred during the OCR process" + }, + "file": { + "not_supported": "Unsupported file type {{type}}" + }, + "processing": "OCR processing...", + "warning": { + "provider": { + "fallback": "Reverted to {{name}}, which may cause issues" + } + } + }, "ollama": { "keep_alive_time": { "description": "The time in minutes to keep the connection alive, default is 5 minutes.", @@ -3498,6 +3518,20 @@ }, "title": "Settings", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "The provider does not exist" + }, + "tesseract": { + "langs": "Supported languages", + "temp_tooltip": "Currently only Chinese and English are supported" + }, + "title": "Image" + }, + "image_provider": "OCR service provider", + "title": "OCR service" + }, "preprocess": { "provider": "Document Processing Provider", "provider_placeholder": "Choose a document processing provider", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index d731da1934..f3a819565b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1574,6 +1574,26 @@ }, "tip": "応答が成功した場合、30秒を超えるメッセージのみに通知を行います" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "組み込みプロバイダーは削除できません", + "existing": "プロバイダーはすでに存在します", + "not_found": "OCRプロバイダーが存在しません", + "update_failed": "更新構成に失敗しました" + }, + "unknown": "OCR処理中にエラーが発生しました" + }, + "file": { + "not_supported": "サポートされていないファイルタイプ {{type}}" + }, + "processing": "OCR処理中...", + "warning": { + "provider": { + "fallback": "{{name}} に戻されました。これにより問題が発生する可能性があります。" + } + } + }, "ollama": { "keep_alive_time": { "description": "モデルがメモリに保持される時間(デフォルト:5分)", @@ -3498,6 +3518,20 @@ }, "title": "設定", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "該提供者は存在しません" + }, + "tesseract": { + "langs": "サポートされている言語", + "temp_tooltip": "現在のところ、中国語と英語のみをサポートしています" + }, + "title": "画像" + }, + "image_provider": "OCRサービスプロバイダー", + "title": "OCRサービス" + }, "preprocess": { "provider": "プレプロセスプロバイダー", "provider_placeholder": "前処理プロバイダーを選択してください", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 21251f332d..e5a7323bcc 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1574,6 +1574,26 @@ }, "tip": "Если ответ успешен, уведомление выдается только по сообщениям, превышающим 30 секунд" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "Не удается удалить встроенного поставщика", + "existing": "Поставщик уже существует", + "not_found": "Поставщик OCR отсутствует", + "update_failed": "Обновление конфигурации не удалось" + }, + "unknown": "Произошла ошибка в процессе распознавания текста" + }, + "file": { + "not_supported": "Неподдерживаемый тип файла {{type}}" + }, + "processing": "Обработка OCR...", + "warning": { + "provider": { + "fallback": "Возвращено к {{name}}, это может вызвать проблемы" + } + } + }, "ollama": { "keep_alive_time": { "description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.", @@ -3498,6 +3518,20 @@ }, "title": "Настройки", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "Поставщик не существует" + }, + "tesseract": { + "langs": "Поддерживаемые языки", + "temp_tooltip": "На данный момент поддерживаются только китайский и английский языки" + }, + "title": "Изображение" + }, + "image_provider": "Поставщик услуг OCR", + "title": "OCR-сервис" + }, "preprocess": { "provider": "Поставщик обработки документов", "provider_placeholder": "Выберите поставщика услуг обработки документов", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 4307fb1208..4ba42ba646 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1574,6 +1574,26 @@ }, "tip": "如果响应成功,则只针对超过30秒的消息进行提醒" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "不能删除内置提供商", + "existing": "提供商已存在", + "not_found": "OCR 提供商不存在", + "update_failed": "更新配置失败" + }, + "unknown": "OCR 过程发生错误" + }, + "file": { + "not_supported": "不支持的文件类型 {{type}}" + }, + "processing": "OCR 处理中...", + "warning": { + "provider": { + "fallback": "已回退到 {{name}},这可能导致问题" + } + } + }, "ollama": { "keep_alive_time": { "description": "对话后模型在内存中保持的时间(默认:5 分钟)", @@ -3498,6 +3518,20 @@ }, "title": "设置", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "该提供商不存在" + }, + "tesseract": { + "langs": "支持的语言", + "temp_tooltip": "目前暂时只支持中文和英文" + }, + "title": "图片" + }, + "image_provider": "OCR 服务提供商", + "title": "OCR 服务" + }, "preprocess": { "provider": "文档处理服务商", "provider_placeholder": "选择一个文档处理服务商", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e9a41e2813..6d25a814b0 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1574,6 +1574,26 @@ }, "tip": "如果回應成功,則只針對超過30秒的訊息發出提醒" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "不能刪除內建提供者", + "existing": "提供商已存在", + "not_found": "OCR 提供商不存在", + "update_failed": "更新配置失敗" + }, + "unknown": "OCR過程發生錯誤" + }, + "file": { + "not_supported": "不支持的文件類型 {{type}}" + }, + "processing": "OCR 處理中...", + "warning": { + "provider": { + "fallback": "已回退到 {{name}},這可能導致問題" + } + } + }, "ollama": { "keep_alive_time": { "description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)", @@ -3498,6 +3518,20 @@ }, "title": "設定", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "該提供商不存在" + }, + "tesseract": { + "langs": "支援的語言", + "temp_tooltip": "目前暫時只支援中文和英文" + }, + "title": "圖片" + }, + "image_provider": "OCR 服務提供商", + "title": "OCR 服務" + }, "preprocess": { "provider": "文件處理供應商", "provider_placeholder": "選擇一個文件處理供應商", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index b0c96f2aa7..43bdc945ac 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -1574,6 +1574,26 @@ }, "tip": "Εάν η απάντηση είναι επιτυχής, η ειδοποίηση εμφανίζεται μόνο για μηνύματα που υπερβαίνουν τα 30 δευτερόλεπτα" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "Δεν είναι δυνατή η διαγραφή του ενσωματωμένου παρόχου", + "existing": "Ο πάροχος υπηρεσιών υπάρχει ήδη", + "not_found": "Ο πάροχος OCR δεν υπάρχει", + "update_failed": "Αποτυχία ενημέρωσης της διαμόρφωσης" + }, + "unknown": "Η διαδικασία OCR εμφάνισε σφάλμα" + }, + "file": { + "not_supported": "Μη υποστηριζόμενος τύπος αρχείου {{type}}" + }, + "processing": "Η επεξεργασία OCR βρίσκεται σε εξέλιξη...", + "warning": { + "provider": { + "fallback": "Επαναφέρθηκε στο {{name}}, το οποίο μπορεί να προκαλέσει προβλήματα" + } + } + }, "ollama": { "keep_alive_time": { "description": "Χρόνος που ο μοντέλος διατηρείται στη μνήμη μετά τη συζήτηση (προεπιλογή: 5 λεπτά)", @@ -3498,6 +3518,20 @@ }, "title": "Ρυθμίσεις", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "Ο πάροχος δεν υπάρχει" + }, + "tesseract": { + "langs": "Υποστηριζόμενες γλώσσες", + "temp_tooltip": "Προς το παρόν υποστηρίζονται μόνο η κινεζική και η αγγλική γλώσσα" + }, + "title": "Εικόνα" + }, + "image_provider": "Πάροχοι υπηρεσιών OCR", + "title": "Υπηρεσία OCR" + }, "preprocess": { "provider": "πάροχος υπηρεσιών προεπεξεργασίας εγγράφων", "provider_placeholder": "Επιλέξτε έναν πάροχο υπηρεσιών προεπεξεργασίας εγγράφων", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index efd6643820..e0d86e7a37 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -1574,6 +1574,26 @@ }, "tip": "Si la respuesta es exitosa, solo se enviará un recordatorio para mensajes que excedan los 30 segundos" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "No se puede eliminar el proveedor integrado", + "existing": "El proveedor ya existe", + "not_found": "El proveedor de OCR no existe", + "update_failed": "Actualización de la configuración fallida" + }, + "unknown": "El proceso OCR ha fallado" + }, + "file": { + "not_supported": "Tipo de archivo no compatible {{type}}" + }, + "processing": "Procesando OCR...", + "warning": { + "provider": { + "fallback": "Se ha revertido a {{name}}, lo que podría causar problemas" + } + } + }, "ollama": { "keep_alive_time": { "description": "Tiempo que el modelo permanece en memoria después de la conversación (por defecto: 5 minutos)", @@ -3498,6 +3518,20 @@ }, "title": "Configuración", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "El proveedor no existe" + }, + "tesseract": { + "langs": "Idiomas compatibles", + "temp_tooltip": "Actualmente solo se admiten chino e inglés." + }, + "title": "Imagen" + }, + "image_provider": "Proveedor de servicios OCR", + "title": "Servicio OCR" + }, "preprocess": { "provider": "Proveedor de servicios de preprocesamiento de documentos", "provider_placeholder": "Seleccionar un proveedor de servicios de preprocesamiento de documentos", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 37008d5c4f..646e2b28a4 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -1574,6 +1574,26 @@ }, "tip": "Si la réponse est réussie, un rappel est envoyé uniquement pour les messages dépassant 30 secondes" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "Impossible de supprimer le fournisseur intégré", + "existing": "Le fournisseur existe déjà", + "not_found": "Le fournisseur OCR n'existe pas", + "update_failed": "Échec de la mise à jour de la configuration" + }, + "unknown": "Une erreur s'est produite lors du processus OCR" + }, + "file": { + "not_supported": "Type de fichier non pris en charge {{type}}" + }, + "processing": "Traitement OCR en cours...", + "warning": { + "provider": { + "fallback": "Revenu à {{name}}, ce qui pourrait entraîner des problèmes" + } + } + }, "ollama": { "keep_alive_time": { "description": "Le temps pendant lequel le modèle reste en mémoire après la conversation (par défaut : 5 minutes)", @@ -3498,6 +3518,20 @@ }, "title": "Paramètres", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "Ce fournisseur n'existe pas" + }, + "tesseract": { + "langs": "Langues prises en charge", + "temp_tooltip": "Pour le moment, seuls le chinois et l'anglais sont pris en charge." + }, + "title": "Image" + }, + "image_provider": "Fournisseur de service OCR", + "title": "Service OCR" + }, "preprocess": { "provider": "fournisseur de services de prétraitement de documents", "provider_placeholder": "Choisissez un prestataire de traitement de documents", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index f245c4a463..e1828408f0 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -1574,6 +1574,26 @@ }, "tip": "Se a resposta for bem-sucedida, lembrete apenas para mensagens que excedam 30 segundos" }, + "ocr": { + "error": { + "provider": { + "cannot_remove_builtin": "Não é possível excluir o provedor integrado", + "existing": "O provedor já existe", + "not_found": "O provedor OCR não existe", + "update_failed": "Falha ao atualizar a configuração" + }, + "unknown": "O processo OCR apresentou um erro" + }, + "file": { + "not_supported": "Tipo de arquivo não suportado {{type}}" + }, + "processing": "Processamento OCR em andamento...", + "warning": { + "provider": { + "fallback": "Revertido para {{name}}, o que pode causar problemas" + } + } + }, "ollama": { "keep_alive_time": { "description": "Tempo que o modelo permanece na memória após a conversa (padrão: 5 minutos)", @@ -3498,6 +3518,20 @@ }, "title": "Configurações", "tool": { + "ocr": { + "image": { + "error": { + "provider_not_found": "O provedor não existe" + }, + "tesseract": { + "langs": "Idiomas suportados", + "temp_tooltip": "No momento, apenas chinês e inglês são suportados." + }, + "title": "Imagem" + }, + "image_provider": "Provedor de serviços OCR", + "title": "Serviço OCR" + }, "preprocess": { "provider": "prestador de serviços de pré-processamento de documentos", "provider_placeholder": "Escolha um fornecedor de pré-processamento de documentos", diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx new file mode 100644 index 0000000000..3efdf94fa0 --- /dev/null +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx @@ -0,0 +1,62 @@ +import { loggerService } from '@logger' +import { useAppSelector } from '@renderer/store' +import { setImageOcrProvider } from '@renderer/store/ocr' +import { isImageOcrProvider, OcrProvider } from '@renderer/types' +import { Select } from 'antd' +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' + +import { SettingRow, SettingRowTitle } from '..' + +const logger = loggerService.withContext('OcrImageSettings') + +type Props = { + setProvider: (provider: OcrProvider) => void +} + +const OcrImageSettings = ({ setProvider }: Props) => { + const { t } = useTranslation() + const providers = useAppSelector((state) => state.ocr.providers) + const imageProvider = useAppSelector((state) => state.ocr.imageProvider) + const imageProviders = providers.filter((p) => isImageOcrProvider(p)) + const dispatch = useDispatch() + + // 挂载时更新外部状态 + useEffect(() => { + setProvider(imageProvider) + }, [imageProvider, setProvider]) + + const updateImageProvider = (id: string) => { + const provider = imageProviders.find((p) => p.id === id) + if (!provider) { + logger.error(`Failed to find image provider by id: ${id}`) + window.message.error(t('settings.tool.ocr.image.error.provider_not_found')) + return + } + + setProvider(provider) + dispatch(setImageOcrProvider(provider)) + } + + return ( + <> + + {t('settings.tool.ocr.image_provider')} +
+ +
+
+ + ) +} diff --git a/src/renderer/src/pages/settings/PreprocessSettings/PreprocessSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/PreprocessProviderSettings.tsx similarity index 100% rename from src/renderer/src/pages/settings/PreprocessSettings/PreprocessSettings.tsx rename to src/renderer/src/pages/settings/DocProcessSettings/PreprocessProviderSettings.tsx diff --git a/src/renderer/src/pages/settings/PreprocessSettings/index.tsx b/src/renderer/src/pages/settings/DocProcessSettings/PreprocessSettings.tsx similarity index 90% rename from src/renderer/src/pages/settings/PreprocessSettings/index.tsx rename to src/renderer/src/pages/settings/DocProcessSettings/PreprocessSettings.tsx index f80c0cd679..a09265a637 100644 --- a/src/renderer/src/pages/settings/PreprocessSettings/index.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/PreprocessSettings.tsx @@ -5,8 +5,8 @@ import { Select } from 'antd' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' -import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' -import PreprocessProviderSettings from './PreprocessSettings' +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import PreprocessProviderSettings from './PreprocessProviderSettings' const PreprocessSettings: FC = () => { const { preprocessProviders } = usePreprocessProviders() @@ -25,7 +25,7 @@ const PreprocessSettings: FC = () => { } return ( - + <> {t('settings.tool.preprocess.title')} @@ -52,7 +52,7 @@ const PreprocessSettings: FC = () => { )} - + ) } export default PreprocessSettings diff --git a/src/renderer/src/pages/settings/DocProcessSettings/index.tsx b/src/renderer/src/pages/settings/DocProcessSettings/index.tsx new file mode 100644 index 0000000000..526f507fff --- /dev/null +++ b/src/renderer/src/pages/settings/DocProcessSettings/index.tsx @@ -0,0 +1,18 @@ +import { useTheme } from '@renderer/context/ThemeProvider' +import { FC } from 'react' + +import { SettingContainer } from '..' +import OcrSettings from './OcrSettings' +import PreprocessSettings from './PreprocessSettings' + +const DocProcessSettings: FC = () => { + const { theme: themeMode } = useTheme() + + return ( + + + + + ) +} +export default DocProcessSettings diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index 3a72865d63..b8666a8f7d 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -26,10 +26,10 @@ import styled from 'styled-components' import AboutSettings from './AboutSettings' import DataSettings from './DataSettings/DataSettings' import DisplaySettings from './DisplaySettings/DisplaySettings' +import DocProcessSettings from './DocProcessSettings' import GeneralSettings from './GeneralSettings' import MCPSettings from './MCPSettings' import MemorySettings from './MemorySettings' -import PreprocessSettings from './PreprocessSettings' import ProvidersList from './ProviderSettings' import QuickAssistantSettings from './QuickAssistantSettings' import QuickPhraseSettings from './QuickPhraseSettings' @@ -100,8 +100,8 @@ const SettingsPage: FC = () => { {t('memory.title')} - - + + {t('settings.tool.preprocess.title')} @@ -144,7 +144,7 @@ const SettingsPage: FC = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/renderer/src/services/ocr/OcrService.ts b/src/renderer/src/services/ocr/OcrService.ts new file mode 100644 index 0000000000..3d8339f6e3 --- /dev/null +++ b/src/renderer/src/services/ocr/OcrService.ts @@ -0,0 +1,23 @@ +import { loggerService } from '@logger' +import { isOcrApiProvider, OcrProvider, OcrResult, SupportedOcrFile } from '@renderer/types' + +import { OcrApiClientFactory } from './clients/OcrApiClientFactory' + +const logger = loggerService.withContext('renderer:OcrService') + +/** + * ocr a file + * @param file any supported file + * @param provider ocr provider + * @returns ocr result + * @throws {Error} + */ +export const ocr = async (file: SupportedOcrFile, provider: OcrProvider): Promise => { + logger.info(`ocr file ${file.path}`) + if (isOcrApiProvider(provider)) { + const client = OcrApiClientFactory.create(provider) + return client.ocr(file) + } else { + return window.api.ocr.ocr(file, provider) + } +} diff --git a/src/renderer/src/services/ocr/clients/OcrApiClientFactory.ts b/src/renderer/src/services/ocr/clients/OcrApiClientFactory.ts new file mode 100644 index 0000000000..e685c0e3f9 --- /dev/null +++ b/src/renderer/src/services/ocr/clients/OcrApiClientFactory.ts @@ -0,0 +1,28 @@ +import { loggerService } from '@logger' +import { OcrApiProvider } from '@renderer/types' + +import { OcrBaseApiClient } from './OcrBaseApiClient' +import { OcrExampleApiClient } from './OcrExampleApiClient' + +const logger = loggerService.withContext('OcrApiClientFactory') + +export class OcrApiClientFactory { + /** + * Create an ApiClient instance for the given provider + * 为给定的提供者创建ApiClient实例 + */ + static create(provider: OcrApiProvider): OcrBaseApiClient { + logger.debug(`Creating ApiClient for provider:`, { + id: provider.id, + config: provider.config + }) + + let instance: OcrBaseApiClient + + // Extend other clients here + // eslint-disable-next-line prefer-const + instance = new OcrExampleApiClient(provider) + + return instance + } +} diff --git a/src/renderer/src/services/ocr/clients/OcrBaseApiClient.ts b/src/renderer/src/services/ocr/clients/OcrBaseApiClient.ts new file mode 100644 index 0000000000..c9605671ae --- /dev/null +++ b/src/renderer/src/services/ocr/clients/OcrBaseApiClient.ts @@ -0,0 +1,43 @@ +import { OcrApiProvider, OcrHandler } from '@renderer/types' + +export abstract class OcrBaseApiClient { + public provider: OcrApiProvider + protected host: string + protected apiKey: string + + constructor(provider: OcrApiProvider) { + this.provider = provider + this.host = this.getHost() + this.apiKey = this.getApiKey() + } + + abstract ocr: OcrHandler + + // copy from BaseApiClient + public getHost(): string { + return this.provider.config.api.apiHost + } + + // copy from BaseApiClient + public getApiKey() { + const keys = this.provider.config.api.apiKey.split(',').map((key) => key.trim()) + const keyName = `ocr_provider:${this.provider.id}:last_used_key` + + if (keys.length === 1) { + return keys[0] + } + + const lastUsedKey = window.keyv.get(keyName) + if (!lastUsedKey) { + window.keyv.set(keyName, keys[0]) + return keys[0] + } + + const currentIndex = keys.indexOf(lastUsedKey) + const nextIndex = (currentIndex + 1) % keys.length + const nextKey = keys[nextIndex] + window.keyv.set(keyName, nextKey) + + return nextKey + } +} diff --git a/src/renderer/src/services/ocr/clients/OcrExampleApiClient.ts b/src/renderer/src/services/ocr/clients/OcrExampleApiClient.ts new file mode 100644 index 0000000000..34d28173bb --- /dev/null +++ b/src/renderer/src/services/ocr/clients/OcrExampleApiClient.ts @@ -0,0 +1,15 @@ +import { OcrApiProvider, SupportedOcrFile } from '@renderer/types' + +import { OcrBaseApiClient } from './OcrBaseApiClient' + +export type OcrExampleProvider = OcrApiProvider + +export class OcrExampleApiClient extends OcrBaseApiClient { + constructor(provider: OcrApiProvider) { + super(provider) + } + + public ocr = async (file: SupportedOcrFile) => { + return { text: `Example output: ${file.path}` } + } +} diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index d90ee7282d..cdd3be560d 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -20,6 +20,7 @@ import migrate from './migrate' import minapps from './minapps' import newMessagesReducer from './newMessage' import nutstore from './nutstore' +import ocr from './ocr' import paintings from './paintings' import preprocess from './preprocess' import runtime from './runtime' @@ -55,14 +56,15 @@ const rootReducer = combineReducers({ messages: newMessagesReducer, messageBlocks: messageBlocksReducer, inputTools: inputToolsReducer, - translate + translate, + ocr }) const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 136, + version: 137, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index ee677ead39..f1f170eafb 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -3,6 +3,7 @@ import { nanoid } from '@reduxjs/toolkit' import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE, isMac } from '@renderer/config/constant' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { isFunctionCallingModel, isNotSupportedTextDelta, SYSTEM_MODELS } from '@renderer/config/models' +import { BUILTIN_OCR_PROVIDERS, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr' import { TRANSLATE_PROMPT } from '@renderer/config/prompts' import { isSupportArrayContentProvider, @@ -2174,6 +2175,18 @@ const migrateConfig = { logger.error('migrate 136 error', error as Error) return state } + }, + '137': (state: RootState) => { + try { + state.ocr = { + providers: BUILTIN_OCR_PROVIDERS, + imageProvider: DEFAULT_OCR_PROVIDER.image + } + return state + } catch (error) { + logger.error('migrate 137 error', error as Error) + return state + } } } diff --git a/src/renderer/src/store/ocr.ts b/src/renderer/src/store/ocr.ts new file mode 100644 index 0000000000..7e4ba3d348 --- /dev/null +++ b/src/renderer/src/store/ocr.ts @@ -0,0 +1,61 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { BUILTIN_OCR_PROVIDERS, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr' +import { ImageOcrProvider, OcrProvider, OcrProviderConfig } from '@renderer/types' + +export interface OcrState { + providers: OcrProvider[] + imageProvider: ImageOcrProvider +} + +const initialState: OcrState = { + providers: BUILTIN_OCR_PROVIDERS, + imageProvider: DEFAULT_OCR_PROVIDER.image +} + +const ocrSlice = createSlice({ + name: 'ocr', + initialState, + reducers: { + setOcrProviders(state, action: PayloadAction) { + state.providers = action.payload + }, + addOcrProvider(state, action: PayloadAction) { + state.providers.push(action.payload) + }, + removeOcrProvider(state, action: PayloadAction) { + state.providers = state.providers.filter((provider) => provider.id !== action.payload) + }, + updateOcrProvider(state, action: PayloadAction>) { + const index = state.providers.findIndex((provider) => provider.id === action.payload.id) + if (index !== -1) { + Object.assign(state.providers[index], action.payload) + } + }, + updateOcrProviderConfig( + state, + action: PayloadAction<{ id: string; update: Omit, 'id'> }> + ) { + const index = state.providers.findIndex((provider) => provider.id === action.payload.id) + if (index !== -1) { + if (!state.providers[index].config) { + state.providers[index].config = {} + } + Object.assign(state.providers[index].config, action.payload.update) + } + }, + setImageOcrProvider(state, action: PayloadAction) { + state.imageProvider = action.payload + } + } +}) + +export const { + setOcrProviders, + addOcrProvider, + removeOcrProvider, + updateOcrProvider, + updateOcrProviderConfig, + setImageOcrProvider +} = ocrSlice.actions + +export default ocrSlice.reducer diff --git a/src/renderer/src/types/file.ts b/src/renderer/src/types/file.ts index db998c60d6..db5c51e5b3 100644 --- a/src/renderer/src/types/file.ts +++ b/src/renderer/src/types/file.ts @@ -100,3 +100,16 @@ export enum FileTypes { DOCUMENT = 'document', OTHER = 'other' } + +export type ImageFileMetadata = FileMetadata & { + type: FileTypes.IMAGE +} + +/** + * 类型守卫函数,用于检查一个 FileMetadata 是否为图片文件元数据 + * @param file - 要检查的文件元数据 + * @returns 如果文件是图片类型则返回 true + */ +export const isImageFile = (file: FileMetadata): file is ImageFileMetadata => { + return file.type === FileTypes.IMAGE +} diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index edb81bd969..ee35d7202f 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -9,6 +9,8 @@ export * from './file' import type { FileMetadata } from './file' import type { Message } from './newMessage' +export * from './ocr' + export type Assistant = { id: string name: string diff --git a/src/renderer/src/types/ocr.ts b/src/renderer/src/types/ocr.ts new file mode 100644 index 0000000000..c537191318 --- /dev/null +++ b/src/renderer/src/types/ocr.ts @@ -0,0 +1,142 @@ +import Tesseract from 'tesseract.js' + +import { FileMetadata, ImageFileMetadata, isImageFile } from '.' + +export const BuiltinOcrProviderIds = { + tesseract: 'tesseract' +} as const + +export type BuiltinOcrProviderId = keyof typeof BuiltinOcrProviderIds + +export const isBuiltinOcrProviderId = (id: string): id is BuiltinOcrProviderId => { + return Object.hasOwn(BuiltinOcrProviderIds, id) +} + +// extensible +export const OcrProviderCapabilities = { + image: 'image' +} as const + +export type OcrProviderCapability = keyof typeof OcrProviderCapabilities + +export const isOcrProviderCapability = (cap: string): cap is OcrProviderCapability => { + return Object.hasOwn(OcrProviderCapabilities, cap) +} + +export type OcrProviderCapabilityRecord = Partial> + +// OCR models and providers share the same type definition. +// A provider can offer capabilities to process multiple file types, +// while a model belonging to that provider may be limited to processing only one specific file type. +export type OcrModelCapabilityRecord = OcrProviderCapabilityRecord + +export interface OcrModel { + id: string + name: string + providerId: string + capabilities: OcrModelCapabilityRecord +} + +/** + * Extend this type to define provider-specefic config types. + */ +export type OcrProviderApiConfig = { + apiKey: string + apiHost: string + apiVersion?: string +} + +export const isOcrProviderApiConfig = (config: unknown): config is OcrProviderApiConfig => { + return ( + typeof config === 'object' && + config !== null && + 'apiKey' in config && + typeof config.apiKey === 'string' && + 'apiHost' in config && + typeof config.apiHost === 'string' && + (!('apiVersion' in config) || typeof config.apiVersion === 'string') + ) +} + +/** + * For future. Model based ocr, api based ocr. May different api client. + * + * Extend this type to define provider-specific config types. + */ +export type OcrProviderConfig = { + /** Not used for now. Could safely remove. */ + api?: OcrProviderApiConfig + /** Not used for now. Could safely remove. */ + models?: OcrModel[] + /** Not used for now. Could safely remove. */ + enabled?: boolean +} + +export type OcrProvider = { + id: string + name: string + capabilities: OcrProviderCapabilityRecord + config?: OcrProviderConfig +} + +export type OcrApiProvider = OcrProvider & { + config: OcrProviderConfig & { + api: OcrProviderApiConfig + } +} + +export const isOcrApiProvider = (p: OcrProvider): p is OcrApiProvider => { + return !!(p.config && p.config.api && isOcrProviderApiConfig(p.config.api)) +} + +export type BuiltinOcrProvider = OcrProvider & { + id: BuiltinOcrProviderId +} + +export const isBuiltinOcrProvider = (p: OcrProvider): p is BuiltinOcrProvider => { + return isBuiltinOcrProviderId(p.id) +} + +// Not sure compatiable api endpoint exists. May not support custom ocr provider +export type CustomOcrProvider = OcrProvider & { + id: Exclude +} + +export type ImageOcrProvider = OcrProvider & { + capabilities: OcrProviderCapabilityRecord & { + [OcrProviderCapabilities.image]: true + } +} + +export const isImageOcrProvider = (p: OcrProvider): p is ImageOcrProvider => { + return p.capabilities.image === true +} + +export type SupportedOcrFile = ImageFileMetadata + +export const isSupportedOcrFile = (file: FileMetadata): file is SupportedOcrFile => { + return isImageFile(file) +} + +export type OcrResult = { + text: string +} + +export type OcrHandler = (file: SupportedOcrFile) => Promise + +export type OcrImageHandler = (file: ImageFileMetadata) => Promise + +// Tesseract Types +export type OcrTesseractConfig = OcrProviderConfig & { + langs: Partial> +} + +export type OcrTesseractProvider = BuiltinOcrProvider & { + config: OcrTesseractConfig +} + +export const isOcrTesseractProvider = (p: OcrProvider): p is OcrTesseractProvider => { + return p.id === BuiltinOcrProviderIds.tesseract +} + +export type TesseractLangCode = Tesseract.LanguageCode diff --git a/src/renderer/src/utils/ocr.ts b/src/renderer/src/utils/ocr.ts new file mode 100644 index 0000000000..1c4e6628d3 --- /dev/null +++ b/src/renderer/src/utils/ocr.ts @@ -0,0 +1,12 @@ +import TesseractLogo from '@renderer/assets/images/providers/Tesseract.js.png' +import { isBuiltinOcrProviderId } from '@renderer/types' + +export function getOcrProviderLogo(providerId: string) { + if (isBuiltinOcrProviderId(providerId)) { + switch (providerId) { + case 'tesseract': + return TesseractLogo + } + } + return undefined +} diff --git a/yarn.lock b/yarn.lock index 6c1b8a4bb7..db442ef6f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2953,7 +2953,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:^1.4.5": +"@emnapi/runtime@npm:^1.4.4, @emnapi/runtime@npm:^1.4.5": version: 1.4.5 resolution: "@emnapi/runtime@npm:1.4.5" dependencies: @@ -3524,6 +3524,207 @@ __metadata: languageName: node linkType: hard +"@img/sharp-darwin-arm64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-darwin-arm64@npm:0.34.3" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-darwin-x64@npm:0.34.3" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-linux-arm@npm:1.2.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-ppc64@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-linux-x64@npm:1.2.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.2.0": + version: 1.2.0 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-linux-arm64@npm:0.34.3" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-linux-arm@npm:0.34.3" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-ppc64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-linux-ppc64@npm:0.34.3" + dependencies: + "@img/sharp-libvips-linux-ppc64": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-linux-ppc64": + optional: true + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-linux-s390x@npm:0.34.3" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-linux-x64@npm:0.34.3" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.3" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-linuxmusl-x64@npm:0.34.3" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.0" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-wasm32@npm:0.34.3" + dependencies: + "@emnapi/runtime": "npm:^1.4.4" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-arm64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-win32-arm64@npm:0.34.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-win32-ia32@npm:0.34.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.34.3": + version: 0.34.3 + resolution: "@img/sharp-win32-x64@npm:0.34.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -8631,11 +8832,13 @@ __metadata: rollup-plugin-visualizer: "npm:^5.12.0" sass: "npm:^1.88.0" selection-hook: "npm:^1.0.11" + sharp: "npm:^0.34.3" shiki: "npm:^3.9.1" strict-url-sanitise: "npm:^0.0.1" string-width: "npm:^7.2.0" styled-components: "npm:^6.1.11" tar: "npm:^7.4.3" + tesseract.js: "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch" tiny-pinyin: "npm:^1.3.2" tokenx: "npm:^1.1.0" tsx: "npm:^4.20.3" @@ -9371,6 +9574,13 @@ __metadata: languageName: node linkType: hard +"bmp-js@npm:^0.1.0": + version: 0.1.0 + resolution: "bmp-js@npm:0.1.0" + checksum: 10c0/c651bd5936dcf8d67900050fac14dcbe30baf87c3d21c58f4934fcdf46172e152a87d8c0c3ca25caa2b4b2c7780ef3b5fcc6cd20afd8f0351856cadb1bef9694 + languageName: node + linkType: hard + "body-parser@npm:^2.2.0": version: 2.2.0 resolution: "body-parser@npm:2.2.0" @@ -10139,7 +10349,7 @@ __metadata: languageName: node linkType: hard -"color-string@npm:^1.6.0": +"color-string@npm:^1.6.0, color-string@npm:^1.9.0": version: 1.9.1 resolution: "color-string@npm:1.9.1" dependencies: @@ -10168,6 +10378,16 @@ __metadata: languageName: node linkType: hard +"color@npm:^4.2.3": + version: 4.2.3 + resolution: "color@npm:4.2.3" + dependencies: + color-convert: "npm:^2.0.1" + color-string: "npm:^1.9.0" + checksum: 10c0/7fbe7cfb811054c808349de19fb380252e5e34e61d7d168ec3353e9e9aacb1802674bddc657682e4e9730c2786592a4de6f8283e7e0d3870b829bb0b7b2f6118 + languageName: node + linkType: hard + "color@npm:^5.0.0": version: 5.0.0 resolution: "color@npm:5.0.0" @@ -11280,7 +11500,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.3": +"detect-libc@npm:^2.0.3, detect-libc@npm:^2.0.4": version: 2.0.4 resolution: "detect-libc@npm:2.0.4" checksum: 10c0/c15541f836eba4b1f521e4eecc28eefefdbc10a94d3b8cb4c507689f332cc111babb95deda66f2de050b22122113189986d5190be97d51b5a2b23b938415e67c @@ -14050,6 +14270,13 @@ __metadata: languageName: node linkType: hard +"idb-keyval@npm:^6.2.0": + version: 6.2.2 + resolution: "idb-keyval@npm:6.2.2" + checksum: 10c0/b52f0d2937cc2ec9f1da536b0b5c0875af3043ca210714beaffead4ec1f44f2ad322220305fd024596203855224d9e3523aed83e971dfb62ddc21b5b1721aeef + languageName: node + linkType: hard + "ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" @@ -14441,6 +14668,13 @@ __metadata: languageName: node linkType: hard +"is-url@npm:^1.2.4": + version: 1.2.4 + resolution: "is-url@npm:1.2.4" + checksum: 10c0/0157a79874f8f95fdd63540e3f38c8583c2ef572661cd0693cda80ae3e42dfe8e9a4a972ec1b827f861d9a9acf75b37f7d58a37f94a8a053259642912c252bc3 + languageName: node + linkType: hard + "is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" @@ -17550,6 +17784,15 @@ __metadata: languageName: node linkType: hard +"opencollective-postinstall@npm:^2.0.3": + version: 2.0.3 + resolution: "opencollective-postinstall@npm:2.0.3" + bin: + opencollective-postinstall: index.js + checksum: 10c0/8a0104a218bc1afaae943f0af378461eeb2836f9848bad872bbd067ec5d1d9791636f307454ab77d0746f10341366f295384656a340ebdb87a2585058e8567e5 + languageName: node + linkType: hard + "option@npm:~0.2.1": version: 0.2.4 resolution: "option@npm:0.2.4" @@ -19454,6 +19697,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.13.3": + version: 0.13.11 + resolution: "regenerator-runtime@npm:0.13.11" + checksum: 10c0/12b069dc774001fbb0014f6a28f11c09ebfe3c0d984d88c9bced77fdb6fedbacbca434d24da9ae9371bfbf23f754869307fb51a4c98a8b8b18e5ef748677ca24 + languageName: node + linkType: hard + "regex-recursion@npm:^6.0.2": version: 6.0.2 resolution: "regex-recursion@npm:6.0.2" @@ -20145,6 +20395,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.2": + version: 7.7.2 + resolution: "semver@npm:7.7.2" + bin: + semver: bin/semver.js + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea + languageName: node + linkType: hard + "send@npm:^1.1.0, send@npm:^1.2.0": version: 1.2.0 resolution: "send@npm:1.2.0" @@ -20213,6 +20472,84 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.34.3": + version: 0.34.3 + resolution: "sharp@npm:0.34.3" + dependencies: + "@img/sharp-darwin-arm64": "npm:0.34.3" + "@img/sharp-darwin-x64": "npm:0.34.3" + "@img/sharp-libvips-darwin-arm64": "npm:1.2.0" + "@img/sharp-libvips-darwin-x64": "npm:1.2.0" + "@img/sharp-libvips-linux-arm": "npm:1.2.0" + "@img/sharp-libvips-linux-arm64": "npm:1.2.0" + "@img/sharp-libvips-linux-ppc64": "npm:1.2.0" + "@img/sharp-libvips-linux-s390x": "npm:1.2.0" + "@img/sharp-libvips-linux-x64": "npm:1.2.0" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.0" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.0" + "@img/sharp-linux-arm": "npm:0.34.3" + "@img/sharp-linux-arm64": "npm:0.34.3" + "@img/sharp-linux-ppc64": "npm:0.34.3" + "@img/sharp-linux-s390x": "npm:0.34.3" + "@img/sharp-linux-x64": "npm:0.34.3" + "@img/sharp-linuxmusl-arm64": "npm:0.34.3" + "@img/sharp-linuxmusl-x64": "npm:0.34.3" + "@img/sharp-wasm32": "npm:0.34.3" + "@img/sharp-win32-arm64": "npm:0.34.3" + "@img/sharp-win32-ia32": "npm:0.34.3" + "@img/sharp-win32-x64": "npm:0.34.3" + color: "npm:^4.2.3" + detect-libc: "npm:^2.0.4" + semver: "npm:^7.7.2" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-ppc64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-ppc64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-arm64": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/df9e6645e3db6ed298a0ac956ba74e468c367fc038b547936fbdddc6a29fce9af40413acbef73b3716291530760f311a20e45c8983f20ee5ea69dd2f21464a2b + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -21001,6 +21338,47 @@ __metadata: languageName: node linkType: hard +"tesseract.js-core@npm:^6.0.0": + version: 6.0.0 + resolution: "tesseract.js-core@npm:6.0.0" + checksum: 10c0/c04be8bbaa296be658664496754f21e857bdffff84113f08adf02f03a1f84596d68b3542ed2fda4a6dc138abb84b09b30ab07c04ee5950879e780876d343955f + languageName: node + linkType: hard + +"tesseract.js@npm:6.0.1": + version: 6.0.1 + resolution: "tesseract.js@npm:6.0.1" + dependencies: + bmp-js: "npm:^0.1.0" + idb-keyval: "npm:^6.2.0" + is-url: "npm:^1.2.4" + node-fetch: "npm:^2.6.9" + opencollective-postinstall: "npm:^2.0.3" + regenerator-runtime: "npm:^0.13.3" + tesseract.js-core: "npm:^6.0.0" + wasm-feature-detect: "npm:^1.2.11" + zlibjs: "npm:^0.3.1" + checksum: 10c0/1d73bb1fbc00c8629756d9594989d8bbfabda657a8cad84922ad68eb0f073148c82845bf71a882e5d2427a46edb5a470356864e60562c7a8442bddd70251435a + languageName: node + linkType: hard + +"tesseract.js@patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch": + version: 6.0.1 + resolution: "tesseract.js@patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch::version=6.0.1&hash=a9cf7b" + dependencies: + bmp-js: "npm:^0.1.0" + idb-keyval: "npm:^6.2.0" + is-url: "npm:^1.2.4" + node-fetch: "npm:^2.6.9" + opencollective-postinstall: "npm:^2.0.3" + regenerator-runtime: "npm:^0.13.3" + tesseract.js-core: "npm:^6.0.0" + wasm-feature-detect: "npm:^1.2.11" + zlibjs: "npm:^0.3.1" + checksum: 10c0/8a94fcc688ff21a9e82b721563d8fa174837ba807d0f01290fe9a1bb6a1c96ecaf7dc1c83510510f3d5185abd15f1cc5fc3cb7ad6c0eee0c4b3e278106f8a5da + languageName: node + linkType: hard + "test-exclude@npm:^7.0.1": version: 7.0.1 resolution: "test-exclude@npm:7.0.1" @@ -22173,6 +22551,13 @@ __metadata: languageName: node linkType: hard +"wasm-feature-detect@npm:^1.2.11": + version: 1.8.0 + resolution: "wasm-feature-detect@npm:1.8.0" + checksum: 10c0/2cb43e91bbf7aa7c121bc76b3133de3ab6dc4f482acc1d2dc46c528e8adb7a51c72df5c2aacf1d219f113c04efd1706f18274d5790542aa5dd49e0644e3ee665 + languageName: node + linkType: hard + "wcwidth@npm:^1.0.1": version: 1.0.1 resolution: "wcwidth@npm:1.0.1" @@ -22678,6 +23063,13 @@ __metadata: languageName: node linkType: hard +"zlibjs@npm:^0.3.1": + version: 0.3.1 + resolution: "zlibjs@npm:0.3.1" + checksum: 10c0/2d110bfcb0f8b8dbf225423f6556da9c5bca95c8b849c1218983676158a24b5cd0350357e0c4d504e27f8c7e18d471d9712576f35114a81a51bcf83453f02beb + languageName: node + linkType: hard + "zod-to-json-schema@npm:^3.22.3, zod-to-json-schema@npm:^3.22.4, zod-to-json-schema@npm:^3.22.5, zod-to-json-schema@npm:^3.24.1": version: 3.24.5 resolution: "zod-to-json-schema@npm:3.24.5"