From 4312087246a2462b675da273ba49db094d993274 Mon Sep 17 00:00:00 2001 From: chmjkb Date: Sat, 24 Jan 2026 12:40:12 +0100 Subject: [PATCH 1/3] fix: ensure controller is ready before calling delete --- .../src/hooks/computer_vision/useOCR.ts | 4 +++- .../src/hooks/computer_vision/useVerticalOCR.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts b/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts index 1790023c3..8ab83d2c4 100644 --- a/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts +++ b/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts @@ -50,7 +50,9 @@ export const useOCR = ({ })(); return () => { - controllerInstance.delete(); + if (controllerInstance.isReady) { + controllerInstance.delete(); + } }; }, [ controllerInstance, diff --git a/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts b/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts index bf7da6e03..acd7c627e 100644 --- a/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts +++ b/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts @@ -53,7 +53,9 @@ export const useVerticalOCR = ({ })(); return () => { - controllerInstance.delete(); + if (controllerInstance.isReady) { + controllerInstance.delete(); + } }; }, [ controllerInstance, From aeeb0d0833ab6aee1a82098444cac78eff8f2bc0 Mon Sep 17 00:00:00 2001 From: chmjkb Date: Mon, 26 Jan 2026 09:05:05 +0100 Subject: [PATCH 2/3] fix: add a null-check for nativemodule in controllers --- .../src/hooks/computer_vision/useOCR.ts | 4 +--- .../src/hooks/computer_vision/useVerticalOCR.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts b/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts index 8ab83d2c4..1790023c3 100644 --- a/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts +++ b/packages/react-native-executorch/src/hooks/computer_vision/useOCR.ts @@ -50,9 +50,7 @@ export const useOCR = ({ })(); return () => { - if (controllerInstance.isReady) { - controllerInstance.delete(); - } + controllerInstance.delete(); }; }, [ controllerInstance, diff --git a/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts b/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts index acd7c627e..bf7da6e03 100644 --- a/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts +++ b/packages/react-native-executorch/src/hooks/computer_vision/useVerticalOCR.ts @@ -53,9 +53,7 @@ export const useVerticalOCR = ({ })(); return () => { - if (controllerInstance.isReady) { - controllerInstance.delete(); - } + controllerInstance.delete(); }; }, [ controllerInstance, From 3efa1e3aafbfa23af2b3e064fc4fbdf2ee0e25a8 Mon Sep 17 00:00:00 2001 From: chmjkb Date: Mon, 26 Jan 2026 11:12:41 +0100 Subject: [PATCH 3/3] refactor: add a base class for ocr controllers --- .../src/controllers/BaseOCRController.ts | 121 ++++++++++++++++++ .../src/controllers/OCRController.ts | 114 +++-------------- .../src/controllers/VerticalOCRController.ts | 121 +++--------------- 3 files changed, 157 insertions(+), 199 deletions(-) create mode 100644 packages/react-native-executorch/src/controllers/BaseOCRController.ts diff --git a/packages/react-native-executorch/src/controllers/BaseOCRController.ts b/packages/react-native-executorch/src/controllers/BaseOCRController.ts new file mode 100644 index 000000000..9f0d5d611 --- /dev/null +++ b/packages/react-native-executorch/src/controllers/BaseOCRController.ts @@ -0,0 +1,121 @@ +import { symbols } from '../constants/ocr/symbols'; +import { RnExecutorchErrorCode } from '../errors/ErrorCodes'; +import { RnExecutorchError, parseUnknownError } from '../errors/errorUtils'; +import { ResourceSource } from '../types/common'; +import { OCRLanguage, OCRDetection } from '../types/ocr'; +import { ResourceFetcher } from '../utils/ResourceFetcher'; + +export abstract class BaseOCRController { + protected nativeModule: any; + public isReady: boolean = false; + public isGenerating: boolean = false; + public error: RnExecutorchError | null = null; + protected isReadyCallback: (isReady: boolean) => void; + protected isGeneratingCallback: (isGenerating: boolean) => void; + protected errorCallback: (error: RnExecutorchError) => void; + + constructor({ + isReadyCallback = (_isReady: boolean) => {}, + isGeneratingCallback = (_isGenerating: boolean) => {}, + errorCallback = (_error: RnExecutorchError) => {}, + } = {}) { + this.isReadyCallback = isReadyCallback; + this.isGeneratingCallback = isGeneratingCallback; + this.errorCallback = errorCallback; + } + + protected abstract loadNativeModule( + detectorPath: string, + recognizerPath: string, + language: OCRLanguage, + extraParams?: any + ): any; + + protected internalLoad = async ( + detectorSource: ResourceSource, + recognizerSource: ResourceSource, + language: OCRLanguage, + onDownloadProgressCallback?: (downloadProgress: number) => void, + extraParams?: any + ) => { + try { + if (!detectorSource || !recognizerSource) return; + + if (!symbols[language]) { + throw new RnExecutorchError( + RnExecutorchErrorCode.LanguageNotSupported, + 'The provided language for OCR is not supported. Please try using other language.' + ); + } + + this.isReady = false; + this.isReadyCallback(false); + + const paths = await ResourceFetcher.fetch( + onDownloadProgressCallback, + detectorSource, + recognizerSource + ); + if (paths === null || paths.length < 2) { + throw new RnExecutorchError( + RnExecutorchErrorCode.DownloadInterrupted, + 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' + ); + } + this.nativeModule = this.loadNativeModule( + paths[0]!, + paths[1]!, + language, + extraParams + ); + this.isReady = true; + this.isReadyCallback(this.isReady); + } catch (e) { + if (this.errorCallback) { + this.errorCallback(parseUnknownError(e)); + } else { + throw parseUnknownError(e); + } + } + }; + + public forward = async (imageSource: string): Promise => { + if (!this.isReady) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ModuleNotLoaded, + 'The model is currently not loaded. Please load the model before calling forward().' + ); + } + if (this.isGenerating) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ModelGenerating, + 'The model is currently generating. Please wait until previous model run is complete.' + ); + } + + try { + this.isGenerating = true; + this.isGeneratingCallback(this.isGenerating); + return await this.nativeModule.generate(imageSource); + } catch (e) { + throw parseUnknownError(e); + } finally { + this.isGenerating = false; + this.isGeneratingCallback(this.isGenerating); + } + }; + + public delete() { + if (this.isGenerating) { + throw new RnExecutorchError( + RnExecutorchErrorCode.ModelGenerating, + 'The model is currently generating. Please wait until previous model run is complete.' + ); + } + if (this.nativeModule) { + this.nativeModule.unload(); + } + this.isReadyCallback(false); + this.isGeneratingCallback(false); + } +} diff --git a/packages/react-native-executorch/src/controllers/OCRController.ts b/packages/react-native-executorch/src/controllers/OCRController.ts index 57f1e3489..31523563d 100644 --- a/packages/react-native-executorch/src/controllers/OCRController.ts +++ b/packages/react-native-executorch/src/controllers/OCRController.ts @@ -1,27 +1,15 @@ import { symbols } from '../constants/ocr/symbols'; -import { RnExecutorchErrorCode } from '../errors/ErrorCodes'; -import { RnExecutorchError, parseUnknownError } from '../errors/errorUtils'; import { ResourceSource } from '../types/common'; import { OCRLanguage } from '../types/ocr'; -import { ResourceFetcher } from '../utils/ResourceFetcher'; - -export class OCRController { - private nativeModule: any; - public isReady: boolean = false; - public isGenerating: boolean = false; - public error: RnExecutorchError | null = null; - private isReadyCallback: (isReady: boolean) => void; - private isGeneratingCallback: (isGenerating: boolean) => void; - private errorCallback: (error: RnExecutorchError) => void; - - constructor({ - isReadyCallback = (_isReady: boolean) => {}, - isGeneratingCallback = (_isGenerating: boolean) => {}, - errorCallback = (_error: RnExecutorchError) => {}, - } = {}) { - this.isReadyCallback = isReadyCallback; - this.isGeneratingCallback = isGeneratingCallback; - this.errorCallback = errorCallback; +import { BaseOCRController } from './BaseOCRController'; + +export class OCRController extends BaseOCRController { + protected loadNativeModule( + detectorPath: string, + recognizerPath: string, + language: OCRLanguage + ): any { + return global.loadOCR(detectorPath, recognizerPath, symbols[language]); } public load = async ( @@ -30,83 +18,11 @@ export class OCRController { language: OCRLanguage, onDownloadProgressCallback?: (downloadProgress: number) => void ) => { - try { - if (!detectorSource || !recognizerSource) return; - - if (!symbols[language]) { - throw new RnExecutorchError( - RnExecutorchErrorCode.LanguageNotSupported, - 'The provided language for OCR is not supported. Please try using other language.' - ); - } - - this.isReady = false; - this.isReadyCallback(false); - - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - detectorSource, - recognizerSource - ); - if (paths === null || paths.length < 2) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' - ); - } - this.nativeModule = global.loadOCR( - paths[0]!, - paths[1]!, - symbols[language] - ); - this.isReady = true; - this.isReadyCallback(this.isReady); - } catch (e) { - if (this.errorCallback) { - this.errorCallback(parseUnknownError(e)); - } else { - throw parseUnknownError(e); - } - } + await this.internalLoad( + detectorSource, + recognizerSource, + language, + onDownloadProgressCallback + ); }; - - public forward = async (imageSource: string) => { - if (!this.isReady) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ModuleNotLoaded, - 'The model is currently not loaded. Please load the model before calling forward().' - ); - } - if (this.isGenerating) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ModelGenerating, - 'The model is currently generating. Please wait until previous model run is complete.' - ); - } - - try { - this.isGenerating = true; - this.isGeneratingCallback(this.isGenerating); - return await this.nativeModule.generate(imageSource); - } catch (e) { - throw parseUnknownError(e); - } finally { - this.isGenerating = false; - this.isGeneratingCallback(this.isGenerating); - } - }; - - public delete() { - if (this.isGenerating) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ModelGenerating, - 'The model is currently generating. Please wait until previous model run is complete.' - ); - } - if (this.nativeModule) { - this.nativeModule.unload(); - } - this.isReadyCallback(false); - this.isGeneratingCallback(false); - } } diff --git a/packages/react-native-executorch/src/controllers/VerticalOCRController.ts b/packages/react-native-executorch/src/controllers/VerticalOCRController.ts index eaf4b0849..2509d8331 100644 --- a/packages/react-native-executorch/src/controllers/VerticalOCRController.ts +++ b/packages/react-native-executorch/src/controllers/VerticalOCRController.ts @@ -1,27 +1,21 @@ import { symbols } from '../constants/ocr/symbols'; -import { RnExecutorchErrorCode } from '../errors/ErrorCodes'; -import { RnExecutorchError, parseUnknownError } from '../errors/errorUtils'; import { ResourceSource } from '../types/common'; import { OCRLanguage } from '../types/ocr'; -import { ResourceFetcher } from '../utils/ResourceFetcher'; +import { BaseOCRController } from './BaseOCRController'; -export class VerticalOCRController { - private ocrNativeModule: any; - public isReady: boolean = false; - public isGenerating: boolean = false; - public error: string | null = null; - private isReadyCallback: (isReady: boolean) => void; - private isGeneratingCallback: (isGenerating: boolean) => void; - private errorCallback: (error: RnExecutorchError) => void; - - constructor({ - isReadyCallback = (_isReady: boolean) => {}, - isGeneratingCallback = (_isGenerating: boolean) => {}, - errorCallback = (_error: RnExecutorchError) => {}, - } = {}) { - this.isReadyCallback = isReadyCallback; - this.isGeneratingCallback = isGeneratingCallback; - this.errorCallback = errorCallback; +export class VerticalOCRController extends BaseOCRController { + protected loadNativeModule( + detectorPath: string, + recognizerPath: string, + language: OCRLanguage, + independentCharacters?: boolean + ): any { + return global.loadVerticalOCR( + detectorPath, + recognizerPath, + symbols[language], + independentCharacters + ); } public load = async ( @@ -31,85 +25,12 @@ export class VerticalOCRController { independentCharacters: boolean, onDownloadProgressCallback: (downloadProgress: number) => void ) => { - try { - if (!detectorSource || !recognizerSource) return; - - if (!symbols[language]) { - throw new RnExecutorchError( - RnExecutorchErrorCode.LanguageNotSupported, - 'The provided language for OCR is not supported. Please try using other language.' - ); - } - - this.isReady = false; - this.isReadyCallback(this.isReady); - - const paths = await ResourceFetcher.fetch( - onDownloadProgressCallback, - detectorSource, - recognizerSource - ); - if (paths === null || paths.length < 3) { - throw new RnExecutorchError( - RnExecutorchErrorCode.DownloadInterrupted, - 'The download has been interrupted. As a result, not every file was downloaded. Please retry the download.' - ); - } - this.ocrNativeModule = global.loadVerticalOCR( - paths[0]!, - paths[1]!, - symbols[language], - independentCharacters - ); - - this.isReady = true; - this.isReadyCallback(this.isReady); - } catch (e) { - if (this.errorCallback) { - this.errorCallback(parseUnknownError(e)); - } else { - throw parseUnknownError(e); - } - } + await this.internalLoad( + detectorSource, + recognizerSource, + language, + onDownloadProgressCallback, + independentCharacters + ); }; - - public forward = async (imageSource: string) => { - if (!this.isReady) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ModuleNotLoaded, - 'The model is currently not loaded. Please load the model before calling forward().' - ); - } - if (this.isGenerating) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ModelGenerating, - 'The model is currently generating. Please wait until previous model run is complete.' - ); - } - - try { - this.isGenerating = true; - this.isGeneratingCallback(this.isGenerating); - return await this.ocrNativeModule.generate(imageSource); - } catch (e) { - throw parseUnknownError(e); - } finally { - this.isGenerating = false; - this.isGeneratingCallback(this.isGenerating); - } - }; - - public delete() { - if (this.isGenerating) { - throw new RnExecutorchError( - RnExecutorchErrorCode.ModelGenerating, - 'The model is currently generating. Please wait until previous model run is complete.' - ); - } - if (this.ocrNativeModule) { - this.ocrNativeModule.unload(); - } - this.isReadyCallback(false); - this.isGeneratingCallback(false); - } }