From 65e4f403c5bbc174988086380df1b612bf6acfc2 Mon Sep 17 00:00:00 2001 From: Balearica Date: Fri, 6 Sep 2024 09:05:50 -0700 Subject: [PATCH] Refactored scheduler Node.js vs. browser code --- js/containers/imageContainer.js | 4 +- js/debug.js | 24 +---- js/fontContainerMain.js | 6 +- js/fontEval.js | 32 ++---- js/generalWorkerMain.js | 178 ++++++++++++++++++-------------- js/recognizeConvert.js | 32 +++--- 6 files changed, 128 insertions(+), 148 deletions(-) diff --git a/js/containers/imageContainer.js b/js/containers/imageContainer.js index edf76f7..694e54f 100644 --- a/js/containers/imageContainer.js +++ b/js/containers/imageContainer.js @@ -256,12 +256,12 @@ export class ImageCache { // If no preference is specified for upscaling, default to false. const upscaleArg = props?.upscaled || false; - const scheduler = await gs.getGeneralScheduler(); + await gs.getGeneralScheduler(); const resPromise = (async () => { // Wait for non-rotated version before replacing with promise if (typeof process === 'undefined') await gs.initTesseract({ anyOk: true }); - return scheduler.recognize({ + return gs.recognize({ image: inputImage.src, options: { rotateRadians: angleArg, upscale: upscaleArg }, output: { diff --git a/js/debug.js b/js/debug.js index daa7ac0..314dcc6 100644 --- a/js/debug.js +++ b/js/debug.js @@ -114,25 +114,11 @@ export async function drawDebugImages(args) { export async function renderPageStatic(page) { const image = await ImageCache.getNative(page.n, { rotated: opt.autoRotate, upscaled: false }); - // The Node.js canvas package does not currently support worker threads - // https://github.com/Automattic/node-canvas/issues/1394 - let res; - if (!(typeof process === 'undefined')) { - const { renderPageStaticImp } = await import('./worker/compareOCRModule.js'); - res = await renderPageStaticImp({ - page, - image, - angle: pageMetricsArr[page.n].angle, - }); - // Browser case - } else { - if (!gs.scheduler) throw new Error('GeneralScheduler must be defined before this function can run.'); - res = await gs.scheduler.renderPageStaticImp({ - page, - image, - angle: pageMetricsArr[page.n].angle, - }); - } + const res = gs.renderPageStaticImp({ + page, + image, + angle: pageMetricsArr[page.n].angle, + }); return res; } diff --git a/js/fontContainerMain.js b/js/fontContainerMain.js index e8eb625..e599c26 100644 --- a/js/fontContainerMain.js +++ b/js/fontContainerMain.js @@ -321,8 +321,6 @@ export function setDefaultFontAuto(fontMetricsObj) { * @param {Object.} fontMetricsObj */ export async function optimizeFontContainerFamily(fontFamily, fontMetricsObj) { - if (!gs.scheduler) throw new Error('GeneralScheduler must be defined before this function can run.'); - // When we have metrics for individual fonts families, those are used to optimize the appropriate fonts. // Otherwise, the "default" metric is applied to whatever font the user has selected as the default font. const multiFontMode = checkMultiFontMode(fontMetricsObj); @@ -342,7 +340,7 @@ export async function optimizeFontContainerFamily(fontFamily, fontMetricsObj) { } const metricsNormal = fontMetricsObj[fontMetricsType][fontFamily.normal.style]; - const normalOptFont = gs.scheduler.optimizeFont({ fontData: fontFamily.normal.src, fontMetricsObj: metricsNormal, style: fontFamily.normal.style }) + const normalOptFont = gs.optimizeFont({ fontData: fontFamily.normal.src, fontMetricsObj: metricsNormal, style: fontFamily.normal.style }) .then(async (x) => { const font = await loadOpentype(x.fontData, x.kerningPairs); return new FontContainerFont(fontFamily.normal.family, fontFamily.normal.style, x.fontData, true, font); @@ -352,7 +350,7 @@ export async function optimizeFontContainerFamily(fontFamily, fontMetricsObj) { /** @type {?FontContainerFont|Promise} */ let italicOptFont = null; if (metricsItalic && metricsItalic.obs >= 200) { - italicOptFont = gs.scheduler.optimizeFont({ fontData: fontFamily.italic.src, fontMetricsObj: metricsItalic, style: fontFamily.italic.style }) + italicOptFont = gs.optimizeFont({ fontData: fontFamily.italic.src, fontMetricsObj: metricsItalic, style: fontFamily.italic.style }) .then(async (x) => { const font = await loadOpentype(x.fontData, x.kerningPairs); return new FontContainerFont(fontFamily.italic.family, fontFamily.italic.style, x.fontData, true, font); diff --git a/js/fontEval.js b/js/fontEval.js index 4a5c8be..39748ed 100644 --- a/js/fontEval.js +++ b/js/fontEval.js @@ -16,8 +16,6 @@ import { gs } from './generalWorkerMain.js'; * @param {number} n - Number of words to compare */ export async function evalPagesFont(font, pageArr, opt, n = 500) { - if (!gs.scheduler) throw new Error('GeneralScheduler must be defined before this function can run.'); - let metricTotal = 0; let wordsTotal = 0; @@ -26,29 +24,13 @@ export async function evalPagesFont(font, pageArr, opt, n = 500) { const imageI = await ImageCache.getBinary(i); - // The Node.js canvas package does not currently support worker threads - // https://github.com/Automattic/node-canvas/issues/1394 - let res; - if (!(typeof process === 'undefined')) { - const { evalPageFont } = await import('./worker/compareOCRModule.js'); - - res = await evalPageFont({ - font, - page: pageArr[i], - binaryImage: imageI, - pageMetricsObj: pageMetricsArr[i], - opt, - }); - // Browser case - } else { - res = await gs.scheduler.evalPageFont({ - font, - page: pageArr[i], - binaryImage: imageI, - pageMetricsObj: pageMetricsArr[i], - opt, - }); - } + const res = await gs.evalPageFont({ + font, + page: pageArr[i], + binaryImage: imageI, + pageMetricsObj: pageMetricsArr[i], + opt, + }); metricTotal += res.metricTotal; wordsTotal += res.wordsTotal; diff --git a/js/generalWorkerMain.js b/js/generalWorkerMain.js index a06ca35..55b2f8a 100644 --- a/js/generalWorkerMain.js +++ b/js/generalWorkerMain.js @@ -102,62 +102,6 @@ export async function initGeneralWorker() { }); } -export class GeneralScheduler { - constructor(scheduler) { - this.scheduler = scheduler; - /** - * @param {Parameters[0]} args - * @returns {ReturnType} - */ - this.compareOCRPageImp = async (args) => (await this.scheduler.addJob('compareOCRPageImp', args)); - /** - * @param {Parameters[0]} args - * @returns {ReturnType} - */ - this.optimizeFont = async (args) => (await this.scheduler.addJob('optimizeFont', args)); - /** - * @template {Partial} TO - * @param {Object} args - * @param {Parameters[0]} args.image - * @param {Parameters[1]} args.options - * @param {TO} args.output - * @returns {Promise>} - * Exported for type inference purposes, should not be imported anywhere. - */ - this.recognize = async (args) => (await this.scheduler.addJob('recognize', args)); - /** - * @param {Parameters[0]} args - * @returns {ReturnType} - */ - this.recognizeAndConvert = async (args) => (await this.scheduler.addJob('recognizeAndConvert', args)); - /** - * @param {Parameters[0]} args - * @returns {Promise<[ReturnType, ReturnType]>} - */ - this.recognizeAndConvert2 = async (args) => (await this.scheduler.addJob('recognizeAndConvert2', args)); - /** - * @param {Parameters[0]} args - * @returns {ReturnType} - */ - this.evalPageBase = async (args) => (await this.scheduler.addJob('evalPageBase', args)); - /** - * @param {Parameters[0]} args - * @returns {ReturnType} - */ - this.evalWords = async (args) => (await this.scheduler.addJob('evalWords', args)); - /** - * @param {Parameters[0]} args - * @returns {ReturnType} - */ - this.evalPageFont = async (args) => (await this.scheduler.addJob('evalPageFont', args)); - /** - * @param {Parameters[0]} args - * @returns {ReturnType} - */ - this.renderPageStaticImp = async (args) => (await this.scheduler.addJob('renderPageStaticImp', args)); - } -} - /** * This class stores the scheduler and related promises. */ @@ -177,37 +121,112 @@ export class gs { static loadedBuiltInOptWorker = false; /** @type {?GeneralScheduler} */ - static scheduler = null; + // static scheduler = null; /** @type {?import('../tess/tesseract.esm.min.js').default} */ static schedulerInner = null; /** @type {?Function} */ - static resReady = null; + static #resReady = null; /** @type {?Promise} */ static schedulerReady = null; - static setSchedulerReady = () => { - gs.schedulerReady = new Promise((resolve, reject) => { - gs.resReady = resolve; - }); - }; - /** @type {?Function} */ - static resReadyTesseract = null; + static #resReadyTesseract = null; /** @type {?Promise} */ static schedulerReadyTesseract = null; - static setSchedulerReadyTesseract = () => { - gs.schedulerReadyTesseract = new Promise((resolve, reject) => { - gs.resReadyTesseract = resolve; - }); + /** + * @param {Parameters[0]} args + * @returns {ReturnType} + */ + static compareOCRPageImp = async (args) => { + if (typeof process === 'undefined') { + return await gs.schedulerInner.addJob('compareOCRPageImp', args); + // eslint-disable-next-line no-else-return + } else { + // The Node.js canvas package does not currently support worker threads + // https://github.com/Automattic/node-canvas/issues/1394 + const compareOCRPageImp = (await import('./worker/compareOCRModule.js')).compareOCRPageImp; + return await compareOCRPageImp(args); + } + }; + + /** + * @param {Parameters[0]} args + * @returns {ReturnType} + */ + static optimizeFont = async (args) => (await gs.schedulerInner.addJob('optimizeFont', args)); + + /** + * @template {Partial} TO + * @param {Object} args + * @param {Parameters[0]} args.image + * @param {Parameters[1]} args.options + * @param {TO} args.output + * @returns {Promise>} + * Exported for type inference purposes, should not be imported anywhere. + */ + static recognize = async (args) => (await gs.schedulerInner.addJob('recognize', args)); + + /** + * @param {Parameters[0]} args + * @returns {ReturnType} + */ + static recognizeAndConvert = async (args) => (await gs.schedulerInner.addJob('recognizeAndConvert', args)); + + /** + * @param {Parameters[0]} args + * @returns {Promise<[ReturnType, ReturnType]>} + */ + static recognizeAndConvert2 = async (args) => (await gs.schedulerInner.addJob('recognizeAndConvert2', args)); + + /** + * @param {Parameters[0]} args + * @returns {ReturnType} + */ + static evalPageBase = async (args) => { + if (typeof process === 'undefined') { + return await gs.schedulerInner.addJob('evalPageBase', args); + // eslint-disable-next-line no-else-return + } else { + const evalPageBase = (await import('./worker/compareOCRModule.js')).evalPageBase; + return await evalPageBase(args); + } + }; + + /** + * @param {Parameters[0]} args + * @returns {ReturnType} + */ + static evalWords = async (args) => (await gs.schedulerInner.addJob('evalWords', args)); + + /** + * @param {Parameters[0]} args + * @returns {ReturnType} + */ + static evalPageFont = async (args) => { + if (typeof process === 'undefined') { + return await gs.schedulerInner.addJob('evalPageFont', args); + // eslint-disable-next-line no-else-return + } else { + const evalPageFont = (await import('./worker/compareOCRModule.js')).evalPageFont; + return await evalPageFont(args); + } }; + /** + * @param {Parameters[0]} args + * @returns {ReturnType} + */ + static renderPageStaticImp = async (args) => (await gs.schedulerInner.addJob('renderPageStaticImp', args)); + static init = async () => { - gs.setSchedulerReady(); + gs.schedulerReady = new Promise((resolve, reject) => { + gs.#resReady = resolve; + }); // Determine number of workers to use in the browser. // This is the minimum of: @@ -240,10 +259,8 @@ export class gs { await Promise.all(resArr); - gs.scheduler = new GeneralScheduler(gs.schedulerInner); - // @ts-ignore - gs.resReady(true); + gs.#resReady(true); }; /** @@ -263,7 +280,9 @@ export class gs { if (gs.schedulerReadyTesseract) await gs.schedulerReadyTesseract; - gs.setSchedulerReadyTesseract(); + gs.schedulerReadyTesseract = new Promise((resolve, reject) => { + gs.#resReadyTesseract = resolve; + }); // Wait for the first worker to load. // A behavior (likely bug) was observed where, if the workers are loaded in parallel, @@ -276,7 +295,7 @@ export class gs { await Promise.allSettled(resArr); } // @ts-ignore - gs.resReadyTesseract(true); + gs.#resReadyTesseract(true); return gs.schedulerReadyTesseract; }; @@ -286,12 +305,12 @@ export class gs { static getGeneralScheduler = async () => { if (gs.schedulerReady) { await gs.schedulerReady; - return /** @type {GeneralScheduler} */ (gs.scheduler); + return; } await gs.init(); - return /** @type {GeneralScheduler} */ (gs.scheduler); + return; }; static clear = () => { @@ -300,12 +319,11 @@ export class gs { static terminate = async () => { gs.clear(); - gs.scheduler = null; await gs.schedulerInner.terminate(); gs.schedulerInner = null; - gs.resReady = null; + gs.#resReady = null; gs.schedulerReady = null; - gs.resReadyTesseract = null; + gs.#resReadyTesseract = null; gs.schedulerReadyTesseract = null; gs.loadedBuiltInRawWorker = false; }; diff --git a/js/recognizeConvert.js b/js/recognizeConvert.js index f442b55..5fe1b45 100644 --- a/js/recognizeConvert.js +++ b/js/recognizeConvert.js @@ -23,8 +23,6 @@ import { replaceObjectProperties } from './utils/miscUtils.js'; * Additionally, this function adds arguments to the function call that are not available in the worker thread. */ export const compareOCRPage = async (pageA, pageB, options) => { - const func = typeof process !== 'undefined' ? (await import('./worker/compareOCRModule.js')).compareOCRPageImp : gs.scheduler.compareOCRPageImp; - // Some combinations of options require the image to be provided, and some do not. // We skip sending the image for those that do not, as in addition to helping performance, // this is also necessary to run basic comparison scripts (e.g. benchmarking accuracy) without providing the image. @@ -38,7 +36,7 @@ export const compareOCRPage = async (pageA, pageB, options) => { const binaryImage = skipImage ? null : await ImageCache.getBinary(pageA.n); const pageMetricsObj = pageMetricsArr[pageA.n]; - return func({ + return gs.compareOCRPageImp({ pageA, pageB, binaryImage, pageMetricsObj, options, }); }; @@ -50,11 +48,10 @@ export const compareOCRPage = async (pageA, pageB, options) => { * @param {boolean} [params.view=false] - Draw results on debugging canvases */ export const evalOCRPage = async (params) => { - const func = typeof process !== 'undefined' ? (await import('./worker/compareOCRModule.js')).evalPageBase : gs.scheduler.evalPageBase; const n = 'page' in params.page ? params.page.page.n : params.page.n; const binaryImage = await ImageCache.getBinary(n); const pageMetricsObj = pageMetricsArr[n]; - return func({ + return gs.evalPageBase({ page: params.page, binaryImage, pageMetricsObj, func: params.func, view: params.view, }); }; @@ -195,9 +192,9 @@ export const recognizePage = async (n, legacy, lstm, areaMode, tessOptions = {}, // is to get debugging images for layout analysis rather than get text. const runRecognition = legacy || lstm; - const scheduler = await gs.getGeneralScheduler(); + await gs.getGeneralScheduler(); - const resArr = await scheduler.recognizeAndConvert2({ + const resArr = await gs.recognizeAndConvert2({ image: nativeN.src, options: config, output: { @@ -515,7 +512,7 @@ export async function recognizeAllPages(legacy = true, lstm = true, mainData = f * @param {'speed'|'quality'} [options.mode='quality'] - Recognition mode. * @param {Array} [options.langs=['eng']] - Language(s) in document. * @param {'lstm'|'legacy'|'combined'} [options.modeAdv='combined'] - Alternative method of setting recognition mode. - * @param {'conf'|'data'} [options.combineMode='data'] - Method of combining OCR results. Used if OCR data already exists. + * @param {'conf'|'data'|'none'} [options.combineMode='data'] - Method of combining OCR results. Used if OCR data already exists. * @param {boolean} [options.vanillaMode=false] - Whether to use the vanilla Tesseract.js model. */ export async function recognize(options = {}) { @@ -541,15 +538,14 @@ export async function recognize(options = {}) { if (langs.includes('rus') || langs.includes('ukr') || langs.includes('ell')) fontPromiseArr.push(loadBuiltInFontsRaw('all')); await Promise.all(fontPromiseArr); - // Whether user uploaded data will be compared against in addition to both Tesseract engines - const userUploadMode = Boolean(ocrAll['User Upload']); - const existingOCR = Object.keys(ocrAll).filter((x) => x !== 'active').length > 0; + /** @type {?OcrPage[]} */ + const existingOCR = ocrAll['User Upload'] || ocrAll.pdf; // A single Tesseract engine can be used (Legacy or LSTM) or the results from both can be used and combined. if (oemMode === 'legacy' || oemMode === 'lstm') { // Tesseract is used as the "main" data unless user-uploaded data exists and only the LSTM model is being run. // This is because Tesseract Legacy provides very strong metrics, and Abbyy often does not. - await recognizeAllPages(oemMode === 'legacy', oemMode === 'lstm', !(oemMode === 'lstm' && existingOCR), langs, vanillaMode); + await recognizeAllPages(oemMode === 'legacy', oemMode === 'lstm', !(oemMode === 'lstm' && !!existingOCR), langs, vanillaMode); // Metrics from the LSTM model are so inaccurate they are not worth using. if (oemMode === 'legacy') { @@ -566,7 +562,7 @@ export async function recognize(options = {}) { } } - if (userUploadMode) { + if (existingOCR) { const oemText = 'Tesseract Combined'; if (!ocrAll[oemText]) ocrAll[oemText] = Array(inputData.pageCount); ocrAll.active = ocrAll[oemText]; @@ -612,7 +608,7 @@ export async function recognize(options = {}) { ocrAll.active = ocrAll[oemText]; { - const tessCombinedLabel = userUploadMode ? 'Tesseract Combined' : 'Combined'; + const tessCombinedLabel = existingOCR ? 'Tesseract Combined' : 'Combined'; /** @type {Parameters[2]} */ const compOptions = { @@ -632,7 +628,7 @@ export async function recognize(options = {}) { replaceObjectProperties(ocrAll[tessCombinedLabel], res.ocr); } - if (userUploadMode) { + if (existingOCR) { if (combineMode === 'conf') { /** @type {Parameters[2]} */ const compOptions = { @@ -648,12 +644,12 @@ export async function recognize(options = {}) { editConf: true, }; - const res = await compareOCR(ocrAll['User Upload'], ocrAll['Tesseract Combined'], compOptions); + const res = await compareOCR(existingOCR, ocrAll['Tesseract Combined'], compOptions); if (DebugData.debugImg.Combined) DebugData.debugImg.Combined = res.debug; replaceObjectProperties(ocrAll.Combined, res.ocr); - } else { + } else if (combineMode === 'data') { /** @type {Parameters[2]} */ const compOptions = { mode: 'comb', @@ -668,7 +664,7 @@ export async function recognize(options = {}) { confThreshMed: opt.confThreshMed, }; - const res = await compareOCR(ocrAll['User Upload'], ocrAll['Tesseract Combined'], compOptions); + const res = await compareOCR(existingOCR, ocrAll['Tesseract Combined'], compOptions); if (DebugData.debugImg.Combined) DebugData.debugImg.Combined = res.debug;