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 49f7340559..c734271de5 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "officeparser": "^4.2.0", "os-proxy-config": "^1.1.2", "selection-hook": "^1.0.11", - "tesseract.js": "^6.0.1", + "tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch", "turndown": "7.2.0" }, "devDependencies": { @@ -293,7 +293,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/src/main/services/ocr/tesseract/TesseractService.ts b/src/main/services/ocr/tesseract/TesseractService.ts index 77225d4afe..d354f85428 100644 --- a/src/main/services/ocr/tesseract/TesseractService.ts +++ b/src/main/services/ocr/tesseract/TesseractService.ts @@ -5,118 +5,13 @@ import { ImageFileMetadata, isImageFile, OcrResult, SupportedOcrFile } from '@ty import { app } from 'electron' import fs from 'fs' import path from 'path' -import Tesseract, { createWorker } from 'tesseract.js' +import Tesseract, { createWorker, LanguageCode } from 'tesseract.js' const logger = loggerService.withContext('TesseractService') -// const languageCodeMap: Record = { -// 'af-za': 'afr', -// 'am-et': 'amh', -// 'ar-sa': 'ara', -// 'as-in': 'asm', -// 'az-az': 'aze', -// 'az-cyrl-az': 'aze_cyrl', -// 'be-by': 'bel', -// 'bn-bd': 'ben', -// 'bo-cn': 'bod', -// 'bs-ba': 'bos', -// 'bg-bg': 'bul', -// 'ca-es': 'cat', -// 'ceb-ph': 'ceb', -// 'cs-cz': 'ces', -// 'zh-cn': 'chi_sim', -// 'zh-tw': 'chi_tra', -// 'chr-us': 'chr', -// 'cy-gb': 'cym', -// 'da-dk': 'dan', -// 'de-de': 'deu', -// 'dz-bt': 'dzo', -// 'el-gr': 'ell', -// 'en-us': 'eng', -// 'enm-gb': 'enm', -// 'eo-world': 'epo', -// 'et-ee': 'est', -// 'eu-es': 'eus', -// 'fa-ir': 'fas', -// 'fi-fi': 'fin', -// 'fr-fr': 'fra', -// 'frk-de': 'frk', -// 'frm-fr': 'frm', -// 'ga-ie': 'gle', -// 'gl-es': 'glg', -// 'grc-gr': 'grc', -// 'gu-in': 'guj', -// 'ht-ht': 'hat', -// 'he-il': 'heb', -// 'hi-in': 'hin', -// 'hr-hr': 'hrv', -// 'hu-hu': 'hun', -// 'iu-ca': 'iku', -// 'id-id': 'ind', -// 'is-is': 'isl', -// 'it-it': 'ita', -// 'ita-it': 'ita_old', -// 'jv-id': 'jav', -// 'ja-jp': 'jpn', -// 'kn-in': 'kan', -// 'ka-ge': 'kat', -// 'kat-ge': 'kat_old', -// 'kk-kz': 'kaz', -// 'km-kh': 'khm', -// 'ky-kg': 'kir', -// 'ko-kr': 'kor', -// 'ku-tr': 'kur', -// 'la-la': 'lao', -// 'la-va': 'lat', -// 'lv-lv': 'lav', -// 'lt-lt': 'lit', -// 'ml-in': 'mal', -// 'mr-in': 'mar', -// 'mk-mk': 'mkd', -// 'mt-mt': 'mlt', -// 'ms-my': 'msa', -// 'my-mm': 'mya', -// 'ne-np': 'nep', -// 'nl-nl': 'nld', -// 'no-no': 'nor', -// 'or-in': 'ori', -// 'pa-in': 'pan', -// 'pl-pl': 'pol', -// 'pt-pt': 'por', -// 'ps-af': 'pus', -// 'ro-ro': 'ron', -// 'ru-ru': 'rus', -// 'sa-in': 'san', -// 'si-lk': 'sin', -// 'sk-sk': 'slk', -// 'sl-si': 'slv', -// 'es-es': 'spa', -// 'spa-es': 'spa_old', -// 'sq-al': 'sqi', -// 'sr-rs': 'srp', -// 'sr-latn-rs': 'srp_latn', -// 'sw-tz': 'swa', -// 'sv-se': 'swe', -// 'syr-sy': 'syr', -// 'ta-in': 'tam', -// 'te-in': 'tel', -// 'tg-tj': 'tgk', -// 'tl-ph': 'tgl', -// 'th-th': 'tha', -// 'ti-er': 'tir', -// 'tr-tr': 'tur', -// 'ug-cn': 'uig', -// 'uk-ua': 'ukr', -// 'ur-pk': 'urd', -// 'uz-uz': 'uzb', -// 'uz-cyrl-uz': 'uzb_cyrl', -// 'vi-vn': 'vie', -// 'yi-us': 'yid' -// } - // config const MB_SIZE_THRESHOLD = 50 -const tesseractLangs = ['chi_sim', 'chi_tra', 'eng'] +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/' diff --git a/src/renderer/src/config/ocr.ts b/src/renderer/src/config/ocr.ts index 1187b49dc0..b899cbb5f0 100644 --- a/src/renderer/src/config/ocr.ts +++ b/src/renderer/src/config/ocr.ts @@ -1,14 +1,31 @@ -import { BuiltinOcrProvider, ImageOcrProvider, OcrProviderCapability } from '@renderer/types' +import { + BuiltinOcrProvider, + BuiltinOcrProviderId, + ImageOcrProvider, + OcrProviderCapability, + OcrTesseractProvider +} from '@renderer/types' -const tesseract: BuiltinOcrProvider & ImageOcrProvider = { +const tesseract: BuiltinOcrProvider & ImageOcrProvider & OcrTesseractProvider = { id: 'tesseract', name: 'Tesseract', capabilities: { image: true + }, + config: { + langs: { + chi_sim: true, + chi_tra: true, + eng: true + } } -} as const +} as const satisfies OcrTesseractProvider -export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = [tesseract] as const +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 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 e84b350625..2acbf399c8 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1566,12 +1566,23 @@ }, "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..." + "processing": "OCR processing...", + "warning": { + "provider": { + "fallback": "Reverted to {{name}}, which may cause issues" + } + } }, "ollama": { "keep_alive_time": { @@ -3498,6 +3509,10 @@ "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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 260e3fd258..92a3742373 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1566,12 +1566,23 @@ }, "ocr": { "error": { + "provider": { + "cannot_remove_builtin": "組み込みプロバイダーは削除できません", + "existing": "プロバイダーはすでに存在します", + "not_found": "OCRプロバイダーが存在しません", + "update_failed": "更新構成に失敗しました" + }, "unknown": "OCR処理中にエラーが発生しました" }, "file": { "not_supported": "サポートされていないファイルタイプ {{type}}" }, - "processing": "OCR処理中..." + "processing": "OCR処理中...", + "warning": { + "provider": { + "fallback": "{{name}} に戻されました。これにより問題が発生する可能性があります。" + } + } }, "ollama": { "keep_alive_time": { @@ -3498,6 +3509,10 @@ "error": { "provider_not_found": "該提供者は存在しません" }, + "tesseract": { + "langs": "サポートされている言語", + "temp_tooltip": "現在のところ、中国語と英語のみをサポートしています" + }, "title": "画像" }, "image_provider": "OCRサービスプロバイダー", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index ffdddd710d..1cc850db67 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1566,12 +1566,23 @@ }, "ocr": { "error": { + "provider": { + "cannot_remove_builtin": "Не удается удалить встроенного поставщика", + "existing": "Поставщик уже существует", + "not_found": "Поставщик OCR отсутствует", + "update_failed": "Обновление конфигурации не удалось" + }, "unknown": "Произошла ошибка в процессе распознавания текста" }, "file": { "not_supported": "Неподдерживаемый тип файла {{type}}" }, - "processing": "Обработка OCR..." + "processing": "Обработка OCR...", + "warning": { + "provider": { + "fallback": "Возвращено к {{name}}, это может вызвать проблемы" + } + } }, "ollama": { "keep_alive_time": { @@ -3498,6 +3509,10 @@ "error": { "provider_not_found": "Поставщик не существует" }, + "tesseract": { + "langs": "Поддерживаемые языки", + "temp_tooltip": "На данный момент поддерживаются только китайский и английский языки" + }, "title": "Изображение" }, "image_provider": "Поставщик услуг OCR", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5c196d4aaf..90acff50ac 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1566,12 +1566,23 @@ }, "ocr": { "error": { - "unknown": "OCR过程发生错误" + "provider": { + "cannot_remove_builtin": "不能删除内置提供商", + "existing": "提供商已存在", + "not_found": "OCR 提供商不存在", + "update_failed": "更新配置失败" + }, + "unknown": "OCR 过程发生错误" }, "file": { "not_supported": "不支持的文件类型 {{type}}" }, - "processing": "OCR 处理中..." + "processing": "OCR 处理中...", + "warning": { + "provider": { + "fallback": "已回退到 {{name}},这可能导致问题" + } + } }, "ollama": { "keep_alive_time": { @@ -3498,6 +3509,10 @@ "error": { "provider_not_found": "该提供商不存在" }, + "tesseract": { + "langs": "支持的语言", + "temp_tooltip": "目前暂时只支持中文和英文" + }, "title": "图片" }, "image_provider": "OCR 服务提供商", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 5e81d73688..b6f5456b17 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1566,12 +1566,23 @@ }, "ocr": { "error": { + "provider": { + "cannot_remove_builtin": "不能刪除內建提供者", + "existing": "提供商已存在", + "not_found": "OCR 提供商不存在", + "update_failed": "更新配置失敗" + }, "unknown": "OCR過程發生錯誤" }, "file": { "not_supported": "不支持的文件類型 {{type}}" }, - "processing": "OCR 處理中..." + "processing": "OCR 處理中...", + "warning": { + "provider": { + "fallback": "已回退到 {{name}},這可能導致問題" + } + } }, "ollama": { "keep_alive_time": { @@ -3498,6 +3509,10 @@ "error": { "provider_not_found": "該提供商不存在" }, + "tesseract": { + "langs": "支援的語言", + "temp_tooltip": "目前暫時只支援中文和英文" + }, "title": "圖片" }, "image_provider": "OCR 服務提供商", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 7a24908b09..4a8ef581a6 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -888,6 +888,9 @@ }, "history": { "continue_chat": "Συνεχίστε το συνομιλημένο", + "error": { + "topic_not_found": "Το θέμα δεν υπάρχει" + }, "locate": { "message": "Εφαρμογή στο μήνυμα" }, @@ -1563,12 +1566,23 @@ }, "ocr": { "error": { + "provider": { + "cannot_remove_builtin": "Δεν είναι δυνατή η διαγραφή του ενσωματωμένου παρόχου", + "existing": "Ο πάροχος υπηρεσιών υπάρχει ήδη", + "not_found": "Ο πάροχος OCR δεν υπάρχει", + "update_failed": "Αποτυχία ενημέρωσης της διαμόρφωσης" + }, "unknown": "Η διαδικασία OCR εμφάνισε σφάλμα" }, "file": { "not_supported": "Μη υποστηριζόμενος τύπος αρχείου {{type}}" }, - "processing": "Η επεξεργασία OCR βρίσκεται σε εξέλιξη..." + "processing": "Η επεξεργασία OCR βρίσκεται σε εξέλιξη...", + "warning": { + "provider": { + "fallback": "Επαναφέρθηκε στο {{name}}, το οποίο μπορεί να προκαλέσει προβλήματα" + } + } }, "ollama": { "keep_alive_time": { @@ -3495,6 +3509,10 @@ "error": { "provider_not_found": "Ο πάροχος δεν υπάρχει" }, + "tesseract": { + "langs": "Υποστηριζόμενες γλώσσες", + "temp_tooltip": "Προς το παρόν υποστηρίζονται μόνο η κινεζική και η αγγλική γλώσσα" + }, "title": "Εικόνα" }, "image_provider": "Πάροχοι υπηρεσιών OCR", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 8fd6b2e14e..e426fca943 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -888,6 +888,9 @@ }, "history": { "continue_chat": "Continuar chat", + "error": { + "topic_not_found": "El tema no existe" + }, "locate": { "message": "Localizar mensaje" }, @@ -1563,12 +1566,23 @@ }, "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..." + "processing": "Procesando OCR...", + "warning": { + "provider": { + "fallback": "Se ha revertido a {{name}}, lo que podría causar problemas" + } + } }, "ollama": { "keep_alive_time": { @@ -3495,6 +3509,10 @@ "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", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 510ded6983..ad64477c78 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -888,6 +888,9 @@ }, "history": { "continue_chat": "Continuer la conversation", + "error": { + "topic_not_found": "Le sujet n'existe pas" + }, "locate": { "message": "Localiser le message" }, @@ -1563,12 +1566,23 @@ }, "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..." + "processing": "Traitement OCR en cours...", + "warning": { + "provider": { + "fallback": "Revenu à {{name}}, ce qui pourrait entraîner des problèmes" + } + } }, "ollama": { "keep_alive_time": { @@ -3495,6 +3509,10 @@ "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", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index c5b1e28015..789f5163cd 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -888,6 +888,9 @@ }, "history": { "continue_chat": "Continuar conversando", + "error": { + "topic_not_found": "Tópico inexistente" + }, "locate": { "message": "Localizar mensagem" }, @@ -1563,12 +1566,23 @@ }, "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..." + "processing": "Processamento OCR em andamento...", + "warning": { + "provider": { + "fallback": "Revertido para {{name}}, o que pode causar problemas" + } + } }, "ollama": { "keep_alive_time": { @@ -3495,6 +3509,10 @@ "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", diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrImageProviderSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx similarity index 76% rename from src/renderer/src/pages/settings/DocProcessSettings/OcrImageProviderSettings.tsx rename to src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx index ad150cc666..3efdf94fa0 100644 --- a/src/renderer/src/pages/settings/DocProcessSettings/OcrImageProviderSettings.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx @@ -1,22 +1,32 @@ import { loggerService } from '@logger' import { useAppSelector } from '@renderer/store' import { setImageOcrProvider } from '@renderer/store/ocr' -import { isImageOcrProvider } from '@renderer/types' +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('OcrImageProviderSettings') +const logger = loggerService.withContext('OcrImageSettings') -const OcrImageProviderSettings = () => { +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) { @@ -25,6 +35,7 @@ const OcrImageProviderSettings = () => { return } + setProvider(provider) dispatch(setImageOcrProvider(provider)) } @@ -48,4 +59,4 @@ const OcrImageProviderSettings = () => { ) } -export default OcrImageProviderSettings +export default OcrImageSettings diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrProviderSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrProviderSettings.tsx new file mode 100644 index 0000000000..a9ba128d7a --- /dev/null +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrProviderSettings.tsx @@ -0,0 +1,51 @@ +// import { loggerService } from '@logger' +import { isBuiltinOcrProvider, OcrProvider } from '@renderer/types' +import { getOcrProviderLogo } from '@renderer/utils/ocr' +import { Avatar, Divider, Flex } from 'antd' +import styled from 'styled-components' + +import { SettingTitle } from '..' +import { OcrTesseractSettings } from './OcrTesseractSettings' + +// const logger = loggerService.withContext('OcrTesseractSettings') + +type Props = { + provider: OcrProvider +} + +const OcrProviderSettings = ({ provider }: Props) => { + // const { t } = useTranslation() + const getProviderSettings = () => { + if (isBuiltinOcrProvider(provider)) { + switch (provider.id) { + case 'tesseract': + return + } + } else { + throw new Error('Not supported OCR provider') + } + } + + return ( + <> + + + + {provider.name} + + + + {getProviderSettings()} + + ) +} + +const ProviderName = styled.span` + font-size: 14px; + font-weight: 500; +` +const ProviderLogo = styled(Avatar)` + border: 0.5px solid var(--color-border); +` + +export default OcrProviderSettings diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx index d8763f0e2f..9ad2d111ad 100644 --- a/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrSettings.tsx @@ -1,22 +1,27 @@ import { PictureOutlined } from '@ant-design/icons' import { useTheme } from '@renderer/context/ThemeProvider' +import { useAppSelector } from '@renderer/store' +import { OcrProvider } from '@renderer/types' import { Tabs, TabsProps } from 'antd' -import { FC } from 'react' +import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import { SettingDivider, SettingGroup, SettingTitle } from '..' -import OcrImageProviderSettings from './OcrImageProviderSettings' +import OcrImageSettings from './OcrImageSettings' +import OcrProviderSettings from './OcrProviderSettings' const OcrSettings: FC = () => { const { t } = useTranslation() const { theme: themeMode } = useTheme() + const imageProvider = useAppSelector((state) => state.ocr.imageProvider) + const [provider, setProvider] = useState(imageProvider) // since default to image provider const tabs: TabsProps['items'] = [ { key: 'image', label: t('settings.tool.ocr.image.title'), icon: , - children: + children: } ] @@ -27,6 +32,9 @@ const OcrSettings: FC = () => { + + + ) } diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrTesseractSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrTesseractSettings.tsx new file mode 100644 index 0000000000..7e94a31194 --- /dev/null +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrTesseractSettings.tsx @@ -0,0 +1,52 @@ +// import { loggerService } from '@logger' +import InfoTooltip from '@renderer/components/InfoTooltip' +import { useOcrProvider } from '@renderer/hooks/useOcrProvider' +import { BuiltinOcrProviderIds, isOcrTesseractProvider } from '@renderer/types' +import { Flex, Select } from 'antd' +import { useTranslation } from 'react-i18next' + +import { SettingRow, SettingRowTitle } from '..' + +// const logger = loggerService.withContext('OcrTesseractSettings') + +export const OcrTesseractSettings = () => { + const { t } = useTranslation() + const { provider } = useOcrProvider(BuiltinOcrProviderIds.tesseract) + + // TODO: use error boundary + if (!isOcrTesseractProvider(provider)) { + throw new Error('Not tesseract provider.') + } + + // const [langs, setLangs] = useState(provider.config?.langs ?? {}) + + // currently static + const options = [ + { value: 'chi_sim', label: t('languages.chinese') }, + { value: 'chi_tra', label: t('languages.chinese-traditional') }, + { value: 'eng', label: t('languages.english') } + ] + + return ( + <> + + + + {t('settings.tool.ocr.image.tesseract.langs')} + + + +
+