Merge branch 'feat/ocr' into feat/ocr-translate

This commit is contained in:
icarus 2025-08-24 00:02:17 +08:00
commit 07603ae9db
21 changed files with 803 additions and 137 deletions

View File

@ -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";
+
+/// <reference types="node" />
+
declare namespace Tesseract {
- function createScheduler(): Scheduler
- function createWorker(langs?: string | string[] | Lang[], oem?: OEM, options?: Partial<WorkerOptions>, config?: string | Partial<InitOptions>): Promise<Worker>
- function setLogging(logging: boolean): void
- function recognize(image: ImageLike, langs?: string, options?: Partial<WorkerOptions>): Promise<RecognizeResult>
- function detect(image: ImageLike, options?: Partial<WorkerOptions>): any
+ function createScheduler(): Scheduler;
+ function createWorker(
+ langs?: LanguageCode | LanguageCode[] | Lang[],
+ oem?: OEM,
+ options?: Partial<WorkerOptions>,
+ config?: string | Partial<InitOptions>
+ ): Promise<Worker>;
+ function setLogging(logging: boolean): void;
+ function recognize(
+ image: ImageLike,
+ langs?: LanguageCode,
+ options?: Partial<WorkerOptions>
+ ): Promise<RecognizeResult>;
+ function detect(image: ImageLike, options?: Partial<WorkerOptions>): 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<Worker['recognize']>): Promise<RecognizeResult>
- addJob(action: 'detect', ...args: Parameters<Worker['detect']>): Promise<DetectResult>
- terminate(): Promise<any>
- getQueueLen(): number
- getNumWorkers(): number
+ addWorker(worker: Worker): string;
+ addJob(
+ action: "recognize",
+ ...args: Parameters<Worker["recognize"]>
+ ): Promise<RecognizeResult>;
+ addJob(
+ action: "detect",
+ ...args: Parameters<Worker["detect"]>
+ ): Promise<DetectResult>;
+ terminate(): Promise<any>;
+ getQueueLen(): number;
+ getNumWorkers(): number;
}
interface Worker {
- load(jobId?: string): Promise<ConfigResult>
- writeText(path: string, text: string, jobId?: string): Promise<ConfigResult>
- readText(path: string, jobId?: string): Promise<ConfigResult>
- removeText(path: string, jobId?: string): Promise<ConfigResult>
- FS(method: string, args: any[], jobId?: string): Promise<ConfigResult>
- reinitialize(langs?: string | Lang[], oem?: OEM, config?: string | Partial<InitOptions>, jobId?: string): Promise<ConfigResult>
- setParameters(params: Partial<WorkerParams>, jobId?: string): Promise<ConfigResult>
- getImage(type: imageType): string
- recognize(image: ImageLike, options?: Partial<RecognizeOptions>, output?: Partial<OutputFormats>, jobId?: string): Promise<RecognizeResult>
- detect(image: ImageLike, jobId?: string): Promise<DetectResult>
- terminate(jobId?: string): Promise<ConfigResult>
+ load(jobId?: string): Promise<ConfigResult>;
+ writeText(
+ path: string,
+ text: string,
+ jobId?: string
+ ): Promise<ConfigResult>;
+ readText(path: string, jobId?: string): Promise<ConfigResult>;
+ removeText(path: string, jobId?: string): Promise<ConfigResult>;
+ FS(method: string, args: any[], jobId?: string): Promise<ConfigResult>;
+ reinitialize(
+ langs?: string | Lang[],
+ oem?: OEM,
+ config?: string | Partial<InitOptions>,
+ jobId?: string
+ ): Promise<ConfigResult>;
+ setParameters(
+ params: Partial<WorkerParams>,
+ jobId?: string
+ ): Promise<ConfigResult>;
+ getImage(type: imageType): string;
+ recognize(
+ image: ImageLike,
+ options?: Partial<RecognizeOptions>,
+ output?: Partial<OutputFormats>,
+ jobId?: string
+ ): Promise<RecognizeResult>;
+ detect(image: ImageLike, jobId?: string): Promise<DetectResult>;
+ terminate(jobId?: string): Promise<ConfigResult>;
}
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 {

View File

@ -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": {

View File

@ -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<string, string> = {
// '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/'

View File

@ -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<BuiltinOcrProviderId, BuiltinOcrProvider>
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(BUILTIN_OCR_PROVIDERS_MAP)
export const DEFAULT_OCR_PROVIDER = {
image: tesseract

View File

@ -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<OcrProviderConfig>) => {
dispatch(updateOcrProviderConfig({ id: provider.id, update }))
}
return {
provider,
updateConfig
}
}

View File

@ -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",

View File

@ -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サービスプロバイダー",

View File

@ -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",

View File

@ -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 服务提供商",

View File

@ -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 服務提供商",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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 <OcrTesseractSettings />
}
} else {
throw new Error('Not supported OCR provider')
}
}
return (
<>
<SettingTitle>
<Flex align="center" gap={8}>
<ProviderLogo shape="square" src={getOcrProviderLogo(provider.id)} size={16} />
<ProviderName> {provider.name}</ProviderName>
</Flex>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
{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

View File

@ -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<OcrProvider>(imageProvider) // since default to image provider
const tabs: TabsProps['items'] = [
{
key: 'image',
label: t('settings.tool.ocr.image.title'),
icon: <PictureOutlined />,
children: <OcrImageProviderSettings />
children: <OcrImageSettings setProvider={setProvider} />
}
]
@ -27,6 +32,9 @@ const OcrSettings: FC = () => {
<SettingDivider />
<Tabs defaultActiveKey="image" items={tabs} />
</SettingGroup>
<SettingGroup theme={themeMode}>
<OcrProviderSettings provider={provider} />
</SettingGroup>
</>
)
}

View File

@ -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<OcrTesseractConfig['langs']>(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 (
<>
<SettingRow>
<SettingRowTitle>
<Flex align="center" gap={4}>
{t('settings.tool.ocr.image.tesseract.langs')}
<InfoTooltip title={t('settings.tool.ocr.image.tesseract.temp_tooltip')} />
</Flex>
</SettingRowTitle>
<div style={{ display: 'flex', gap: '8px' }}>
<Select
mode="multiple"
disabled
style={{ width: '100%' }}
placeholder="Please select"
value={['chi_sim', 'chi_tra', 'eng']}
options={options}
/>
</div>
</SettingRow>
</>
)
}

View File

@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { BUILTIN_OCR_PROVIDERS, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr'
import { ImageOcrProvider, OcrProvider } from '@renderer/types'
import { ImageOcrProvider, OcrProvider, OcrProviderConfig } from '@renderer/types'
export interface OcrState {
providers: OcrProvider[]
@ -22,8 +22,8 @@ const ocrSlice = createSlice({
addOcrProvider(state, action: PayloadAction<OcrProvider>) {
state.providers.push(action.payload)
},
removeOcrProvider(state, action: PayloadAction<OcrProvider>) {
state.providers = state.providers.filter((provider) => provider.id !== action.payload.id)
removeOcrProvider(state, action: PayloadAction<string>) {
state.providers = state.providers.filter((provider) => provider.id !== action.payload)
},
updateOcrProvider(state, action: PayloadAction<Partial<OcrProvider>>) {
const index = state.providers.findIndex((provider) => provider.id === action.payload.id)
@ -31,13 +31,31 @@ const ocrSlice = createSlice({
Object.assign(state.providers[index], action.payload)
}
},
updateOcrProviderConfig(
state,
action: PayloadAction<{ id: string; update: Omit<Partial<OcrProviderConfig>, '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<ImageOcrProvider>) {
state.imageProvider = action.payload
}
}
})
export const { setOcrProviders, addOcrProvider, removeOcrProvider, updateOcrProvider, setImageOcrProvider } =
ocrSlice.actions
export const {
setOcrProviders,
addOcrProvider,
removeOcrProvider,
updateOcrProvider,
updateOcrProviderConfig,
setImageOcrProvider
} = ocrSlice.actions
export default ocrSlice.reducer

View File

@ -1,3 +1,5 @@
import Tesseract from 'tesseract.js'
import { FileMetadata, ImageFileMetadata, isImageFile } from '.'
export const BuiltinOcrProviderIds = {
@ -123,3 +125,18 @@ export type OcrResult = {
export type OcrHandler = (file: SupportedOcrFile) => Promise<OcrResult>
export type OcrImageHandler = (file: ImageFileMetadata) => Promise<OcrResult>
// Tesseract Types
export type OcrTesseractConfig = OcrProviderConfig & {
langs: Record<TesseractLangCode, boolean>
}
export type OcrTesseractProvider = BuiltinOcrProvider & {
config: OcrTesseractConfig
}
export const isOcrTesseractProvider = (p: OcrProvider): p is OcrTesseractProvider => {
return p.id === BuiltinOcrProviderIds.tesseract
}
export type TesseractLangCode = Tesseract.LanguageCode

View File

@ -8579,7 +8579,7 @@ __metadata:
string-width: "npm:^7.2.0"
styled-components: "npm:^6.1.11"
tar: "npm:^7.4.3"
tesseract.js: "npm:^6.0.1"
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"
@ -20978,7 +20978,7 @@ __metadata:
languageName: node
linkType: hard
"tesseract.js@npm:*, tesseract.js@npm:^6.0.1":
"tesseract.js@npm:6.0.1":
version: 6.0.1
resolution: "tesseract.js@npm:6.0.1"
dependencies:
@ -20995,6 +20995,23 @@ __metadata:
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"