,
+)
+
+/**
+ * Engine variant specification (v3/v5).
+ * zh-CN: 引擎变体规范 (v3/v5).
+ */
+data class VariantSpec(
+ val name: String,
+ val supportsOpenCL: Boolean,
+ val labelAssetPath: String,
+
+ // Explicit model directories.
+ // zh-CN: 显式模型目录配置.
+
+ val modelDirCpu: String,
+ val modelDirCpuSlim: String? = null,
+ val modelDirOpenCL: String? = null,
+ val modelDirOpenCLSlim: String? = null,
+
+ val detModelFile: String,
+ val recModelFile: String,
+ val clsModelFile: String,
+
+ val checkingDrawableRes: Int,
+) {
+
+ fun resolveProfile(options: OcrOptions): ModelProfile {
+ val useSlim = options.useSlim
+ val useOpenCLRequested = options.useOpenCL && supportsOpenCL
+
+ val resolvedDir: String? = if (name == NAME_V5) {
+ // v5: let Predictor decide between CPU/OpenCL + fallback internally.
+ // zh-CN: v5: 让 Predictor 内部决定 CPU/OpenCL 并自动回退.
+ null
+ } else {
+ // v3: choose deterministic dir here (OpenCL ignored).
+ // zh-CN: v3: 在此确定性选择目录 (OpenCL 被忽略).
+ if (useSlim) (modelDirCpuSlim ?: modelDirCpu) else modelDirCpu
+ }
+
+ // Assets to check:
+ // - Always check label file.
+ // - For v3: check the resolved model dir files.
+ // - For v5: check CPU + slim dirs always, and additionally OpenCL dirs if requested.
+ // zh-CN:
+ // - 总是检查 label 文件.
+ // - v3: 检查解析出的模型目录及其文件.
+ // - v5: 总是检查 CPU/INT8 目录, 若用户请求 OpenCL 再额外检查 OpenCL 目录.
+ val assets = buildList {
+ add(labelAssetPath)
+
+ val cpuDir = modelDirCpu
+ add("$cpuDir/$detModelFile")
+ add("$cpuDir/$recModelFile")
+ add("$cpuDir/$clsModelFile")
+
+ modelDirCpuSlim?.let { slimDir ->
+ add("$slimDir/$detModelFile")
+ add("$slimDir/$recModelFile")
+ add("$slimDir/$clsModelFile")
+ }
+
+ if (useOpenCLRequested) {
+ modelDirOpenCL?.let { clDir ->
+ add("$clDir/$detModelFile")
+ add("$clDir/$recModelFile")
+ add("$clDir/$clsModelFile")
+ }
+ modelDirOpenCLSlim?.let { clSlimDir ->
+ add("$clSlimDir/$detModelFile")
+ add("$clSlimDir/$recModelFile")
+ add("$clSlimDir/$clsModelFile")
+ }
+ }
+
+ // v3 deterministic dir check (override list to minimal set).
+ // zh-CN: v3 确定性目录检查 (覆盖为最小集合).
+ if (name != NAME_V5) {
+ clear()
+ add(labelAssetPath)
+ val dir = requireNotNull(resolvedDir)
+ add("$dir/$detModelFile")
+ add("$dir/$recModelFile")
+ add("$dir/$clsModelFile")
+ }
+ }
+
+ return ModelProfile(
+ variantName = name,
+ labelAssetPath = labelAssetPath,
+ modelDir = resolvedDir,
+ useSlim = useSlim,
+ useOpenCL = useOpenCLRequested,
+ cpuThreadNum = options.cpuThreadNum,
+ detModelFile = detModelFile,
+ recModelFile = recModelFile,
+ clsModelFile = clsModelFile,
+ assetsToCheck = assets.distinct(),
+ )
+ }
+
+ fun assertAssetsExist(context: Context, profile: ModelProfile) {
+ fun assertOne(path: String) {
+ try {
+ context.assets.open(path).use { }
+ } catch (e: FileNotFoundException) {
+ throw IllegalStateException(context.getString(R.string.error_missing_required_paddle_ocr_asset, path), e)
+ }
+ }
+ profile.assetsToCheck.forEach(::assertOne)
+ }
+
+ companion object {
+ const val NAME_V5 = "v5"
+ const val NAME_V3 = "v3"
+
+ fun v5(): VariantSpec = VariantSpec(
+ name = NAME_V5,
+ supportsOpenCL = true,
+ labelAssetPath = "labels/ppocr_keys_ocrv5.txt",
+
+ modelDirCpu = "models/pp-ocrv5-arm",
+ modelDirCpuSlim = "models/pp-ocrv5-arm-int8",
+ modelDirOpenCL = "models/pp-ocrv5-arm-opencl",
+ modelDirOpenCLSlim = "models/pp-ocrv5-arm-opencl-int8",
+
+ detModelFile = "PP-OCRv5_mobile_det.nb",
+ recModelFile = "PP-OCRv5_mobile_rec.nb",
+ clsModelFile = "PP-LCNet_x1_0_textline_ori.nb",
+
+ checkingDrawableRes = R.drawable.paddle_ocr_test,
+ )
+
+ fun v3(): VariantSpec = VariantSpec(
+ name = NAME_V3,
+ supportsOpenCL = false,
+ labelAssetPath = "labels/ppocr_keys_v1.txt",
+
+ modelDirCpu = "models/ocr_v3_for_cpu",
+ modelDirCpuSlim = "models/ocr_v3_for_cpu(slim)",
+
+ modelDirOpenCL = null,
+ modelDirOpenCLSlim = null,
+
+ detModelFile = "det_opt.nb",
+ recModelFile = "rec_opt.nb",
+ clsModelFile = "cls_opt.nb",
+
+ checkingDrawableRes = R.drawable.paddle_ocr_test,
+ )
+ }
+}
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/Predictor.java b/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/Predictor.java
new file mode 100644
index 00000000..e34850b4
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/Predictor.java
@@ -0,0 +1,582 @@
+package com.baidu.paddle.lite.ocr;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.util.Base64;
+import android.util.Log;
+import androidx.preference.PreferenceManager;
+import org.opencv.BuildConfig;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Predictor for Paddle-Lite OCR engine, managing model loading, runtime configuration,
+ * and end-to-end inference flow.
+ * zh-CN: 面向 Paddle-Lite OCR 引擎的预测器, 负责模型加载/运行时配置以及端到端推理流程.
+ *
+ * @author TonyJiangWJ
+ * @see
+ * PaddlePaddle/PaddleOCR (Predictor.java)
+ * @since Aug 6, 2023
+ *
+ * Modified by TonyJiangWJ as of Aug 7, 2023.
+ * Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 17, 2026.
+ * Modified by SuperMonster003 as of Jan 18, 2026.
+ */
+@SuppressWarnings("unused")
+public class Predictor {
+
+ public static final int DEFAULT_CPU_THREAD_NUM = 4;
+ public static final boolean DEFAULT_USE_SLIM = true;
+ public static final boolean DEFAULT_USE_OPENCL = false;
+
+ private static final String TAG = Predictor.class.getSimpleName();
+
+ /**
+ * Probe bitmap cache for init-check.
+ * zh-CN: 初始化校验使用的探测位图缓存.
+ */
+ private static Bitmap checkingBitmap;
+ /**
+ * Default label file path.
+ * zh-CN: 默认字典文件路径.
+ */
+ private final String defaultLabelPath = "labels/ppocr_keys_ocrv5.txt";
+ /**
+ * Default CPU model directory (standard).
+ * zh-CN: 默认 CPU 标准模型目录.
+ */
+ private final String defaultModelPath = "models/pp-ocrv5-arm";
+ /**
+ * Default OpenCL model directory (standard).
+ * zh-CN: 默认 OpenCL 标准模型目录.
+ */
+ private final String defaultModelPathOpenCL = "models/pp-ocrv5-arm-opencl";
+ /**
+ * Default CPU model directory (INT8 slim).
+ * zh-CN: 默认 CPU INT8 slim 模型目录.
+ */
+ private final String defaultModelPathSlim = "models/pp-ocrv5-arm-int8";
+ /**
+ * Default OpenCL model directory (INT8 slim).
+ * zh-CN: 默认 OpenCL INT8 slim 模型目录.
+ */
+ private final String defaultModelPathOpenCLSlim = "models/pp-ocrv5-arm-opencl-int8";
+
+ /** Detection model. [zh-CN: 检测模型]. */
+ public String detModelFilename = "PP-OCRv5_mobile_det.nb";
+ /** Recognition model. [zh-CN: 识别模型]. */
+ public String recModelFilename = "PP-OCRv5_mobile_rec.nb";
+ /** Text direction (cls) model. [zh-CN: 方向分类 (cls) 模型]. */
+ public String clsModelFilename = "PP-LCNet_x1_0_textline_ori.nb";
+
+ /** Whether the model is loaded. [zh-CN: 模型是否已加载]. */
+ public boolean isLoaded = false;
+ /** Warm-up iteration count. [zh-CN: 预热迭代次数]. */
+ public int warmupIterNum = 1;
+ /** Inference iteration count for timing. [zh-CN: 用于计时的推理迭代次数]. */
+ public int inferIterNum = 1;
+ /** CPU thread count. [zh-CN: CPU 线程数]. */
+ public int cpuThreadNum = DEFAULT_CPU_THREAD_NUM;
+ /** CPU power mode string (Lite power hint). [zh-CN: CPU 能耗模式字符串 (Lite 电源提示)]. */
+ public String cpuPowerMode = "LITE_POWER_HIGH";
+
+ /** Selected model resolved absolute path. [zh-CN: 选定模型解析后的绝对路径]. */
+ public String modelPath = "";
+ /** Selected model directory name. [zh-CN: 选定模型目录名]. */
+ public String modelName = "";
+
+ /** Use slim (INT8) model. [zh-CN: 是否使用 slim (INT8) 模型]. */
+ public boolean useSlim = DEFAULT_USE_SLIM;
+ /** Use OpenCL backend (if available). [zh-CN: 是否启用 OpenCL 后端 (若可用)]. */
+ public boolean useOpenCL = DEFAULT_USE_OPENCL;
+ /** Validate initialization with a preset image. [zh-CN: 是否通过预设图片校验初始化]. */
+ public boolean checkModelLoaded = BuildConfig.DEBUG;
+
+ /** Enable classification (cls). [zh-CN: 启用方向分类 (cls)]. */
+ public boolean isClassificationEnabled = false;
+ /** Enable detection (det). [zh-CN: 启用文本检测 (det)]. */
+ public boolean isDetectionEnabled = false;
+ /** Enable recognition (rec). [zh-CN: 启用文本识别 (rec)]. */
+ public boolean isRecognitionEnabled = true;
+
+ /** Score threshold for filtering results. [zh-CN: 结果过滤的置信度阈值]. */
+ public float scoreThreshold = 0.1f;
+ /** Max long side for det input resize. [zh-CN: 检测输入缩放的最长边]. */
+ protected int detLongSize = 960;
+
+ /** Native predictor bridge. [zh-CN: Native 预测器桥接对象]. */
+ protected OCRPredictorNative paddlePredictor = null;
+ /** Inference time in milliseconds. [zh-CN: 推理耗时 (毫秒)]. */
+ protected float inferenceTime = 0;
+ /** Labels for recognition post-processing. [zh-CN: 识别后处理所需的字典标签]. */
+ protected List wordLabels = new ArrayList<>();
+ /** Input image buffer (ARGB_8888). [zh-CN: 输入图像缓冲 (ARGB_8888)]. */
+ protected Bitmap inputImage = null;
+ /** Preprocess time in milliseconds. [zh-CN: 预处理耗时 (毫秒)]. */
+ protected float preprocessTime = 0;
+
+ /** Validation attempt counter. [zh-CN: 初始化校验重试计数器]. */
+ private int validationAttempt = 1;
+ /** Initialization attempt counter. [zh-CN: 初始化尝试计数器]. */
+ private int initializationAttempt = 1;
+
+ // @Archived by SuperMonster003 on Nov 7, 2025.
+ // ! Legacy default paths and filenames for PP-OCRv3 are archived here.
+ // ! zh-CN: 旧版 PP-OCRv3 的默认路径与文件名在此归档保留.
+ // # private final String defaultLabelPath = "labels/ppocr_keys_v1.txt";
+ // # private final String defaultModelPath = "models/ocr_v3_for_cpu";
+ // # public String detModelFilename = "det_opt.nb";
+ // # public String recModelFilename = "rec_opt.nb";
+ // # public String clsModelFilename = "cls_opt.nb";
+ // # // Slim model converted by opt 2.10; 2.11 had issues.
+ // # // zh-CN: Slim 模型由 2.10 版 opt 转换; 2.11 存在兼容问题.
+ // # private final String defaultModelPathSlim = "models/ocr_v3_for_cpu(slim)";
+
+ public Predictor() {
+ /* Empty body. */
+ }
+
+ public static String md5(String text) {
+ MessageDigest md;
+ byte[] bytesOfMessage = text.getBytes();
+ try {
+ md = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ return Base64.encodeToString(md.digest(bytesOfMessage), Base64.DEFAULT);
+ }
+
+ public boolean init(Context appCtx) {
+ return this.init(appCtx, defaultModelPath, defaultLabelPath);
+ }
+
+ public boolean init(Context appCtx, boolean useSlim) {
+ return this.init(appCtx, useSlim, DEFAULT_USE_OPENCL);
+ }
+
+ public boolean init(Context appCtx, boolean useSlim, boolean useOpenCL) {
+ Log.d(TAG, "use slim: " + useSlim);
+ Log.d(TAG, "use opencl: " + useOpenCL);
+
+ // If already loaded and switches are consistent, reuse directly.
+ // zh-CN: 若已加载且开关一致, 直接复用.
+ if (this.isLoaded && this.useSlim == useSlim && this.useOpenCL == useOpenCL) {
+ return true;
+ }
+
+ boolean openclAvailable = false;
+ if (useOpenCL) {
+ try {
+ openclAvailable = OpenCLGuard.isOpenCLRuntimeAvailable(appCtx);
+ if (!openclAvailable) {
+ Log.w(TAG, "[OpenCL] Unavailable or fused, fallback to CPU.");
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "[OpenCL] Probe exception, fallback to CPU: " + t.getMessage());
+ }
+ }
+
+ this.useSlim = useSlim;
+ this.useOpenCL = openclAvailable;
+
+ String modelDir;
+ if (this.useSlim) {
+ modelDir = this.useOpenCL ? defaultModelPathOpenCLSlim : defaultModelPathSlim;
+ } else {
+ modelDir = this.useOpenCL ? defaultModelPathOpenCL : defaultModelPath;
+ }
+
+ boolean ok;
+ if (this.useOpenCL) {
+ OpenCLGuard.markInitStart(appCtx);
+ try {
+ ok = this.init(appCtx, modelDir, defaultLabelPath);
+ } finally {
+ OpenCLGuard.markInitEnd(appCtx);
+ }
+ if (!ok) {
+ Log.w(TAG, "[OpenCL] Init failed without crash, fallback to CPU.");
+ this.useOpenCL = false;
+ String cpuModelDir = this.useSlim ? defaultModelPathSlim : defaultModelPath;
+ ok = this.init(appCtx, cpuModelDir, defaultLabelPath);
+ }
+ return ok;
+ } else {
+ return this.init(appCtx, modelDir, defaultLabelPath);
+ }
+ }
+
+ public boolean init(Context appCtx, String modelPath, String labelPath) {
+ Log.d(TAG, "init whit model: " + modelPath + " label: " + labelPath);
+ isLoaded = loadModel(appCtx, modelPath, cpuThreadNum, cpuPowerMode);
+ if (!isLoaded) {
+ return false;
+ }
+ isLoaded = loadLabel(appCtx, labelPath);
+ if (!checkModelLoadedSuccess(appCtx)) {
+ if (initializationAttempt++ < 3) {
+ return init(appCtx, modelPath, labelPath);
+ } else {
+ return false;
+ }
+ }
+ return isLoaded;
+ }
+
+ /**
+ * Initialize and validate models by running inference on a preset test image.
+ * Retry up to several times as a workaround (deeper cause needs investigation).
+ * zh-CN:
+ * 初始化模型后通过识别预设图片校验是否初始化成功.
+ * 曲线救国, 深层的失败原因需要后续排查.
+ */
+ private boolean checkModelLoadedSuccess(Context context) {
+ if (!checkModelLoaded) {
+ return true;
+ }
+ if (!isLoaded) {
+ return false;
+ }
+ List results = runOcr(getCheckingBitmap(context));
+ StringBuilder sb = new StringBuilder();
+ for (OcrResult result : results) {
+ sb.append(result.getLabel());
+ }
+ // The image contains a single recognizable text string "测试" (Chinese word "test").
+ // zh-CN: 图片中包含唯一可识别文本 "测试".
+ boolean check = sb.toString().contains("测试");
+ Log.d(TAG, "Validation attempt " + validationAttempt + ": { initialized: " + check + ", result: " + sb + " }");
+ boolean result = check || validationAttempt++ >= 5;
+ if (!check && validationAttempt >= 5) {
+ Log.e(TAG, "Model initialization failed");
+ }
+ return result;
+ }
+
+ private Bitmap getCheckingBitmap(Context context) {
+ if (checkingBitmap == null) {
+ checkingBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.paddle_ocr_test);
+ }
+ return checkingBitmap;
+ }
+
+ public boolean init(Context appCtx, String modelPath, String labelPath, int cpuThreadNum, String cpuPowerMode) {
+ isLoaded = loadModel(appCtx, modelPath, cpuThreadNum, cpuPowerMode);
+ if (!isLoaded) {
+ return false;
+ }
+ isLoaded = loadLabel(appCtx, labelPath);
+ return isLoaded;
+ }
+
+ public boolean init(Context appCtx, String modelPath, String labelPath, int cpuThreadNum, String cpuPowerMode, int detLongSize, float scoreThreshold) {
+ boolean isLoaded = init(appCtx, modelPath, labelPath, cpuThreadNum, cpuPowerMode);
+ if (!isLoaded) {
+ return false;
+ }
+ this.detLongSize = detLongSize;
+ this.scoreThreshold = scoreThreshold;
+ return true;
+ }
+
+ protected boolean loadModel(Context appCtx, String modelPath, int cpuThreadNum, String cpuPowerMode) {
+ // Release model if exists.
+ // zh-CN: 释放模型如果存在.
+ releaseModel();
+
+ // Load model.
+ // zh-CN: 加载模型.
+ if (modelPath.isEmpty()) {
+ return false;
+ }
+ String realPath = modelPath;
+ if (modelPath.charAt(0) != '/') {
+ // Read model files from custom path if the first character of mode path is '/'
+ // otherwise copy model to cache from assets.
+ // zh-CN: 如果模型路径首字符为 '/' 则从自定义路径读取模型文件, 否则从 assets 复制模型到缓存.
+ realPath = appCtx.getCacheDir() + File.separator + modelPath;
+
+ // @SectionBegin("copyModelAssets") by TonyJiangWJ on Aug 7, 2023.
+ String key = "PADDLE_MODEL_LOADED" + md5(modelPath);
+ // Model has been updated, force override the old model.
+ // zh-CN: 进行了模型更新, 需要强制覆盖旧模型.
+ boolean loaded = PreferenceManager.getDefaultSharedPreferences(appCtx).getBoolean(key, false);
+ if (loaded) {
+ // No need to copy every time.
+ // zh-CN: 没有必要每次都复制.
+ Utils.copyDirectoryFromAssetsIfNeeded(appCtx, modelPath, realPath);
+ } else {
+ Utils.copyDirectoryFromAssets(appCtx, modelPath, realPath);
+ PreferenceManager.getDefaultSharedPreferences(appCtx).edit().putBoolean(key, true).apply();
+ }
+ // @SectionEnd("copyModelAssets")
+ }
+
+ OCRPredictorNative.Config config = new OCRPredictorNative.Config();
+
+ // Whether to use GPU (OpenCL), only set to 1 when useOpenCL is confirmed available at Java level.
+ // zh-CN: 是否使用 GPU (OpenCL), 只有在 Java 层确认 useOpenCL 可用时才真正置 1.
+ config.useOpenCL = useOpenCL ? 1 : 0;
+ config.cpuThreadNum = cpuThreadNum;
+ config.detModelFilename = realPath + File.separator + detModelFilename;
+ config.recModelFilename = realPath + File.separator + recModelFilename;
+ config.clsModelFilename = realPath + File.separator + clsModelFilename;
+ Log.i("Predictor", "model path" + config.detModelFilename + " ; " + config.recModelFilename + ";" + config.clsModelFilename);
+ config.cpuPower = cpuPowerMode;
+
+ paddlePredictor = new OCRPredictorNative(config);
+
+ this.cpuThreadNum = cpuThreadNum;
+ this.cpuPowerMode = cpuPowerMode;
+ this.modelPath = realPath;
+ this.modelName = realPath.substring(realPath.lastIndexOf(File.separator) + 1);
+
+ return true;
+ }
+
+ public void releaseModel() {
+ if (paddlePredictor != null) {
+ paddlePredictor.destroy();
+ paddlePredictor = null;
+ }
+ isLoaded = false;
+ modelPath = "";
+ modelName = "";
+ }
+
+ protected boolean loadLabel(Context appCtx, String labelPath) {
+ wordLabels.clear();
+ wordLabels.add("black");
+ // Load word labels from file.
+ // zh-CN: 从文件中加载字典标签.
+ try {
+ InputStream labelInputStream;
+ if (labelPath.startsWith(File.separator)) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ labelInputStream = Files.newInputStream(Paths.get(labelPath));
+ } else {
+ labelInputStream = new FileInputStream(labelPath);
+ }
+ } else {
+ labelInputStream = appCtx.getAssets().open(labelPath);
+ }
+ int available = labelInputStream.available();
+ byte[] lines = new byte[available];
+ if (labelInputStream.read(lines) <= 0) {
+ Log.e(TAG, "Failed to read label");
+ return false;
+ }
+ labelInputStream.close();
+ String words = new String(lines);
+ // Compatible with \r\n line endings on Windows.
+ // zh-CN: 兼容 Windows 系统下的 \r\n 换行符.
+ String[] contents = words.split("(\r)?\n");
+ wordLabels.addAll(Arrays.asList(contents));
+ wordLabels.add(" ");
+ Log.i(TAG, "Word label size: " + wordLabels.size());
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ return false;
+ }
+ return true;
+ }
+
+ public List runOcr(Bitmap inputImage) {
+ if (inputImage == null || !isLoaded()) {
+ return Collections.emptyList();
+ }
+ int run_det = isDetectionEnabled ? 1 : 0;
+ int run_cls = isClassificationEnabled ? 1 : 0;
+ int run_rec = isRecognitionEnabled ? 1 : 0;
+ // Warm up.
+ // zh-CN: 预热.
+ for (int i = 0; i < warmupIterNum; i++) {
+ paddlePredictor.runImage(inputImage, detLongSize, run_det, run_cls, run_rec);
+ }
+ // Do not need warm.
+ // zh-CN: 不需要预热.
+ warmupIterNum = 0;
+ // Run inference.
+ // zh-CN: 执行推理.
+ Date start = new Date();
+ ArrayList results = paddlePredictor.runImage(inputImage, detLongSize, run_det, run_cls, run_rec);
+ Date end = new Date();
+ inferenceTime = (end.getTime() - start.getTime()) / (float) inferIterNum;
+
+ postProcess(results);
+ Log.i(TAG, "[stat] Preprocess Time: " + preprocessTime + "; Inference Time: " + inferenceTime + "; Box Size: " + results.size());
+ List ocrResults = new ArrayList<>();
+ for (OcrResultModel resultModel : results) {
+ // Log.d(TAG, "Recognize: " + resultModel);
+ if (resultModel.getConfidence() >= scoreThreshold) {
+ ocrResults.add(new OcrResult(resultModel));
+ } else {
+ // Log.d(TAG, "Discard: " + resultModel);
+ }
+ }
+ Collections.sort(ocrResults);
+ return ocrResults;
+ }
+
+ /**
+ * Whether model is loaded and predictor is valid.
+ * zh-CN: 模型是否已加载且预测器有效.
+ */
+ public boolean isLoaded() {
+ return paddlePredictor != null && isLoaded;
+ }
+
+ public String modelPath() {
+ return modelPath;
+ }
+
+ public String modelName() {
+ return modelName;
+ }
+
+ public int cpuThreadNum() {
+ return cpuThreadNum;
+ }
+
+ public String cpuPowerMode() {
+ return cpuPowerMode;
+ }
+
+ public float inferenceTime() {
+ return inferenceTime;
+ }
+
+ public Bitmap inputImage() {
+ return inputImage;
+ }
+
+ public float preprocessTime() {
+ return preprocessTime;
+ }
+
+ public String getDefaultLabelPath() {
+ return defaultLabelPath;
+ }
+
+ public String getDefaultModelPath() {
+ return defaultModelPath;
+ }
+
+ /**
+ * Get default OpenCL model directory (standard).
+ * zh-CN: 获取默认的 OpenCL 标准模型目录.
+ */
+ public String getDefaultModelPathOpenCL() {
+ return defaultModelPathOpenCL;
+ }
+
+ /**
+ * Get default CPU model directory (INT8 slim).
+ * zh-CN: 获取默认的 CPU INT8 slim 模型目录.
+ */
+ public String getDefaultModelPathSlim() {
+ return defaultModelPathSlim;
+ }
+
+ /**
+ * Get default OpenCL model directory (INT8 slim).
+ * zh-CN: 获取默认的 OpenCL INT8 slim 模型目录.
+ */
+ public String getDefaultModelPathOpenCLSlim() {
+ return defaultModelPathOpenCLSlim;
+ }
+
+ public boolean isUseSlim() {
+ return useSlim;
+ }
+
+ public boolean isUseOpenCL() {
+ return useOpenCL;
+ }
+
+ /**
+ * Set input image buffer (copy to ARGB_8888).
+ * zh-CN: 设置输入图像缓冲 (复制为 ARGB_8888).
+ */
+ public void setInputImage(Bitmap image) {
+ if (image != null) {
+ this.inputImage = image.copy(Bitmap.Config.ARGB_8888, true);
+ }
+ }
+
+ /**
+ * Convert raw recognition outputs to text labels and metadata.
+ * zh-CN: 将识别原始输出转换为文本标签及元数据.
+ */
+ private void postProcess(ArrayList results) {
+ for (OcrResultModel r : results) {
+ StringBuilder word = new StringBuilder();
+ for (int index : r.getWordIndex()) {
+ if (index >= 0 && index < wordLabels.size()) {
+ word.append(wordLabels.get(index));
+ } else {
+ Log.e(TAG, "Word index is not in label list:" + index);
+ word.append(" ");
+ }
+ }
+ r.setLabel(word.toString());
+ r.setClsLabel(r.getClsIdx() == 1 ? "180" : "0");
+ }
+ }
+
+ /**
+ * Enable/disable classification (cls).
+ * zh-CN: 启用/禁用方向分类 (cls).
+ */
+ public void setClassificationEnabled(boolean enable) {
+ this.isClassificationEnabled = enable;
+ }
+
+ /**
+ * Enable/disable detection (det).
+ * zh-CN: 启用/禁用文本检测 (det).
+ */
+ public void setDetectionEnabled(boolean enable) {
+ this.isDetectionEnabled = enable;
+ }
+
+ /**
+ * Enable/disable recognition (rec).
+ * zh-CN: 启用/禁用文本识别 (rec).
+ */
+ public void setRecognitionEnabled(boolean enable) {
+ this.isRecognitionEnabled = enable;
+ }
+
+ /**
+ * Set max long side for detection input (smaller is usually faster, e.g., 736-960).
+ * zh-CN: 设置检测输入的最长边 (越小通常越快, 例如 736-960).
+ */
+ public void setDetLongSize(int detLongSize) {
+ this.detLongSize = detLongSize;
+ }
+
+ /**
+ * Set score threshold (slightly improves speed by pruning noisy boxes).
+ * zh-CN: 设置置信度阈值 (通过剔除噪声框可略微提速).
+ */
+ public void setScoreThreshold(float scoreThreshold) {
+ this.scoreThreshold = scoreThreshold;
+ }
+
+}
diff --git a/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/PredictorNativeBridge.kt b/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/PredictorNativeBridge.kt
new file mode 100644
index 00000000..ef92fbb4
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/PredictorNativeBridge.kt
@@ -0,0 +1,138 @@
+package com.baidu.paddle.lite.ocr
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.os.Bundle
+import android.os.Looper
+import android.util.Log
+import org.autojs.plugin.paddle.ocr.api.OcrOptions
+import org.autojs.plugin.paddle.ocr.api.OcrResult
+
+/**
+ * Native bridge based on com.baidu.paddle.lite.ocr.Predictor.
+ * zh-CN: 基于 com.baidu.paddle.lite.ocr.Predictor 的 native bridge.
+ *
+ * Created by JetBrains AI Assistant (GPT-5.2) on Jan 17, 2026.
+ * Modified by SuperMonster003 as of Jan 18, 2026.
+ */
+class PredictorNativeBridge : NativeBridge {
+
+ private val predictor = Predictor()
+
+ @Volatile
+ private var lastProfileKey: String? = null
+
+ override fun init(context: Context, profile: ModelProfile, checkingBitmap: Bitmap) {
+
+ val desiredThreadNum = profile.cpuThreadNum
+ val desiredUseSlim = profile.useSlim
+ val desiredUseOpenCL = profile.useOpenCL
+
+ // Compute a simple cache key to avoid redundant re-init.
+ // zh-CN: 计算简单缓存 key, 避免重复初始化.
+ val key = "${profile.variantName}|t=$desiredThreadNum|slim=$desiredUseSlim|opencl=$desiredUseOpenCL|dir=${profile.modelDir ?: "-"}"
+ if (predictor.isLoaded && lastProfileKey == key) return
+
+ // Predictor.init() may do heavy work and should not block main thread.
+ // zh-CN: Predictor.init() 可能较耗时, 不应阻塞主线程.
+ val ok = if (Looper.getMainLooper() == Looper.myLooper()) {
+ val lock = Object()
+ val completed = booleanArrayOf(false)
+ val initResult = booleanArrayOf(false)
+
+ Thread {
+ initResult[0] = initInternal(context, profile)
+ synchronized(lock) {
+ completed[0] = true
+ lock.notifyAll()
+ }
+ }.start()
+
+ val deadline = System.currentTimeMillis() + 60_000
+ var interrupted = false
+ synchronized(lock) {
+ try {
+ while (!completed[0]) {
+ val remaining = deadline - System.currentTimeMillis()
+ if (remaining <= 0) break
+ lock.wait(remaining)
+ }
+ } catch (_: InterruptedException) {
+ Thread.currentThread().interrupt()
+ interrupted = true
+ }
+ }
+ !interrupted && completed[0] && initResult[0]
+ } else {
+ initInternal(context, profile)
+ }
+
+ if (!ok) {
+ throw IllegalStateException(context.getString(R.string.error_failed_to_initialize_paddle_ocr_predictor))
+ }
+
+ lastProfileKey = key
+ }
+
+ private fun initInternal(context: Context, profile: ModelProfile): Boolean {
+ // Ensure cpuThreadNum updates take effect.
+ // zh-CN: 确保 cpuThreadNum 更新生效.
+ if (predictor.cpuThreadNum != profile.cpuThreadNum) {
+ predictor.releaseModel()
+ predictor.cpuThreadNum = profile.cpuThreadNum
+ }
+
+ // Apply per-variant model file names (both v3/v5).
+ // zh-CN: 应用按变体区分的模型文件名 (同时覆盖 v3/v5).
+ predictor.detModelFilename = profile.detModelFile
+ predictor.recModelFilename = profile.recModelFile
+ predictor.clsModelFilename = profile.clsModelFile
+
+ return if (profile.variantName == VariantSpec.NAME_V5) {
+ // Use v5 built-in OpenCLGuard + fallback logic.
+ // zh-CN: 使用 v5 内置的 OpenCLGuard + fallback 逻辑.
+ predictor.init(
+ context.applicationContext,
+ profile.useSlim,
+ profile.useOpenCL,
+ )
+ } else {
+ // v3: OpenCL is ignored at variant level; init by resolved modelDir/labelPath.
+ // zh-CN: v3: OpenCL 在变体层被忽略; 按解析后的 modelDir/labelPath 初始化.
+ predictor.init(
+ context.applicationContext,
+ requireNotNull(profile.modelDir) {
+ context.getString(R.string.error_missing_modeldir_for_variant, profile.variantName)
+ },
+ profile.labelAssetPath,
+ )
+ }
+ }
+
+ override fun recognizeText(bitmap: Bitmap, options: OcrOptions): List {
+ val results = predictor.runOcr(bitmap)
+ val out = ArrayList(results.size)
+ for (r in results) out.add(r.label)
+
+ // Only print summary or first several items to avoid heavy I/O.
+ // zh-CN: 仅打印摘要或前若干条, 避免大量 I/O.
+ Log.i("PaddleOcrEngine", "recognized ${out.size} items")
+ for (i in 0 until minOf(5, out.size)) {
+ Log.d("PaddleOcrEngine", "item[$i]: ${out[i]}")
+ }
+ return out
+ }
+
+ override fun detect(bitmap: Bitmap, options: OcrOptions): List {
+ val results = predictor.runOcr(bitmap)
+ return results.map { r ->
+ OcrResult().apply {
+ text = r.label
+ confidence = r.confidence
+ bounds = r.bounds
+ extras = Bundle()
+ }
+ }
+ }
+
+}
diff --git a/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/Utils.java b/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/Utils.java
new file mode 100644
index 00000000..eaee71b9
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/java/com/baidu/paddle/lite/ocr/Utils.java
@@ -0,0 +1,199 @@
+package com.baidu.paddle.lite.ocr;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.media.ExifInterface;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * @author PaddleOCR
+ * @since Aug 6, 2023
+ *
+ * Modified by TonyJiangWJ as of Aug 7, 2023.
+ * Modified by SuperMonster003 as of Jan 18, 2026.
+ */
+@SuppressWarnings({"ResultOfMethodCallIgnored", "CallToPrintStackTrace", "unused"})
+public class Utils {
+ private static final String TAG = Utils.class.getSimpleName();
+
+ public static void copyFileFromAssets(Context appCtx, String srcPath, String dstPath) {
+ if (srcPath.isEmpty() || dstPath.isEmpty()) {
+ return;
+ }
+ try (InputStream is = new BufferedInputStream(appCtx.getAssets().open(srcPath)); OutputStream os = new BufferedOutputStream(new FileOutputStream(dstPath))) {
+ try {
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = is.read(buffer)) != -1) {
+ os.write(buffer, 0, length);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void copyDirectoryFromAssets(Context appCtx, String srcDir, String dstDir) {
+ if (srcDir.isEmpty() || dstDir.isEmpty()) {
+ return;
+ }
+ try {
+ if (!new File(dstDir).exists()) {
+ new File(dstDir).mkdirs();
+ }
+ String[] list = appCtx.getAssets().list(srcDir);
+ if (list == null) {
+ return;
+ }
+ for (String fileName : list) {
+ String srcSubPath = srcDir + File.separator + fileName;
+ String dstSubPath = dstDir + File.separator + fileName;
+ if (new File(srcSubPath).isDirectory()) {
+ copyDirectoryFromAssets(appCtx, srcSubPath, dstSubPath);
+ } else {
+ Log.d(TAG, "Copy asset file: " + srcSubPath + " -> " + dstSubPath);
+ copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void copyDirectoryFromAssetsIfNeeded(Context appCtx, String srcDir, String dstDir) {
+ if (srcDir.isEmpty() || dstDir.isEmpty()) {
+ return;
+ }
+ try {
+ if (!new File(dstDir).exists()) {
+ new File(dstDir).mkdirs();
+ }
+ String[] list = appCtx.getAssets().list(srcDir);
+ if (list == null) {
+ return;
+ }
+ for (String fileName : list) {
+ String srcSubPath = srcDir + File.separator + fileName;
+ String dstSubPath = dstDir + File.separator + fileName;
+ if (new File(srcSubPath).isDirectory()) {
+ copyDirectoryFromAssetsIfNeeded(appCtx, srcSubPath, dstSubPath);
+ } else {
+ if (new File(dstSubPath).exists()) {
+ return;
+ }
+ Log.d(TAG, "Copy asset file: " + srcSubPath + " -> " + dstSubPath);
+ copyFileFromAssets(appCtx, srcSubPath, dstSubPath);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static float[] parseFloatsFromString(String string, String delimiter) {
+ String[] pieces = string.trim().toLowerCase().split(delimiter);
+ float[] floats = new float[pieces.length];
+ for (int i = 0; i < pieces.length; i++) {
+ floats[i] = Float.parseFloat(pieces[i].trim());
+ }
+ return floats;
+ }
+
+ public static long[] parseLongsFromString(String string, String delimiter) {
+ String[] pieces = string.trim().toLowerCase().split(delimiter);
+ long[] longs = new long[pieces.length];
+ for (int i = 0; i < pieces.length; i++) {
+ longs[i] = Long.parseLong(pieces[i].trim());
+ }
+ return longs;
+ }
+
+ public static String getSDCardDirectory() {
+ return Environment.getExternalStorageDirectory().getAbsolutePath();
+ }
+
+ public static boolean isSupportedNPU() {
+ return false;
+ // String hardware = android.os.Build.HARDWARE;
+ // return hardware.equalsIgnoreCase("kirin810") || hardware.equalsIgnoreCase("kirin990");
+ }
+
+ public static Bitmap resizeWithStep(Bitmap bitmap, int maxLength, int step) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int maxWH = Math.max(width, height);
+ float ratio;
+ int newWidth = width;
+ int newHeight = height;
+ if (maxWH > maxLength) {
+ ratio = maxLength * 1.0f / maxWH;
+ newWidth = (int) Math.floor(ratio * width);
+ newHeight = (int) Math.floor(ratio * height);
+ }
+
+ newWidth = newWidth - newWidth % step;
+ if (newWidth == 0) {
+ newWidth = step;
+ }
+ newHeight = newHeight - newHeight % step;
+ if (newHeight == 0) {
+ newHeight = step;
+ }
+ return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
+ }
+
+ public static Bitmap rotateBitmap(Bitmap bitmap, int orientation) {
+
+ Matrix matrix = new Matrix();
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_NORMAL:
+ return bitmap;
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ matrix.setScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ matrix.setRotate(180);
+ break;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ matrix.setRotate(180);
+ matrix.postScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ matrix.setRotate(90);
+ matrix.postScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ matrix.setRotate(90);
+ break;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ matrix.setRotate(-90);
+ matrix.postScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ matrix.setRotate(-90);
+ break;
+ default:
+ return bitmap;
+ }
+ try {
+ Bitmap bmRotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+ bitmap.recycle();
+ return bmRotated;
+ } catch (OutOfMemoryError e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/drawable/paddle_ocr_test.png b/plugin-api/paddle-ocr-engine/src/main/res/drawable/paddle_ocr_test.png
new file mode 100644
index 00000000..864676df
Binary files /dev/null and b/plugin-api/paddle-ocr-engine/src/main/res/drawable/paddle_ocr_test.png differ
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-ar/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-ar/strings.xml
new file mode 100644
index 00000000..192960bb
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-ar/strings.xml
@@ -0,0 +1,7 @@
+
+
+ تعذّرت تهيئة Paddle OCR predictor.
+ modelDir مفقود لـ variant=%1$s.
+ تعذّر فك ترميز مورد drawable الخاص بالتحقق.
+ مورد Paddle OCR المطلوب مفقود: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-en/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-en/strings.xml
new file mode 100644
index 00000000..5db29215
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-en/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Failed to initialize Paddle OCR predictor.
+ Missing modelDir for variant=%1$s.
+ Failed to decode checking drawable resource.
+ Missing required Paddle OCR asset: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-es/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..8fc5b4ec
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-es/strings.xml
@@ -0,0 +1,7 @@
+
+
+ No se pudo inicializar el predictor de Paddle OCR.
+ Falta modelDir para variant=%1$s.
+ No se pudo decodificar el recurso drawable de verificación.
+ Falta el recurso requerido de Paddle OCR: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-fr/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..963a2000
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-fr/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Échec de l\'initialisation du prédicteur Paddle OCR.
+ modelDir manquant pour variant=%1$s.
+ Échec du décodage de la ressource drawable de vérification.
+ Ressource Paddle OCR requise manquante: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-ja/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-ja/strings.xml
new file mode 100644
index 00000000..5c82180d
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-ja/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Paddle OCR predictor の初期化に失敗しました.
+ variant=%1$s の modelDir が見つかりません.
+ 確認用 drawable リソースのデコードに失敗しました.
+ 必要な Paddle OCR asset が見つかりません: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-ko/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-ko/strings.xml
new file mode 100644
index 00000000..f6523c68
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-ko/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Paddle OCR predictor 초기화에 실패했습니다.
+ variant=%1$s에 대한 modelDir가 없습니다.
+ 검사용 drawable 리소스 디코딩에 실패했습니다.
+ 필수 Paddle OCR asset이 없습니다: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-ru/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-ru/strings.xml
new file mode 100644
index 00000000..1b6387ea
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-ru/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Не удалось инициализировать predictor Paddle OCR.
+ Отсутствует modelDir для variant=%1$s.
+ Не удалось декодировать проверочный ресурс drawable.
+ Отсутствует требуемый ресурс Paddle OCR: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-zh-rHK/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-zh-rHK/strings.xml
new file mode 100644
index 00000000..f94b37b3
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-zh-rHK/strings.xml
@@ -0,0 +1,7 @@
+
+
+ 初始化 Paddle OCR predictor 失敗.
+ 變體缺少 modelDir: variant=%1$s.
+ 解碼檢查用 drawable 資源失敗.
+ 缺少 Paddle OCR 必要資源文件: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-zh-rTW/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..0ee37514
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+
+
+ 初始化 Paddle OCR predictor 失敗.
+ 變體缺少 modelDir: variant=%1$s.
+ 解碼檢查用 drawable 資源失敗.
+ 缺少 Paddle OCR 必要資原始檔: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values-zh/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values-zh/strings.xml
new file mode 100644
index 00000000..181c9b5a
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values-zh/strings.xml
@@ -0,0 +1,7 @@
+
+
+ 初始化 Paddle OCR predictor 失败.
+ 变体缺少 modelDir: variant=%1$s.
+ 解码检查用 drawable 资源失败.
+ 缺少 Paddle OCR 必要资源文件: %1$s.
+
\ No newline at end of file
diff --git a/plugin-api/paddle-ocr-engine/src/main/res/values/strings.xml b/plugin-api/paddle-ocr-engine/src/main/res/values/strings.xml
new file mode 100644
index 00000000..5db29215
--- /dev/null
+++ b/plugin-api/paddle-ocr-engine/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+ Failed to initialize Paddle OCR predictor.
+ Missing modelDir for variant=%1$s.
+ Failed to decode checking drawable resource.
+ Missing required Paddle OCR asset: %1$s.
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 92cfff64..dd024d84 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -40,7 +40,8 @@ private val libs = listOf(
)
private val pluginApi = listOf(
- "paddle-ocr",
+ "paddle-ocr-api",
+ "paddle-ocr-engine",
)
include(