feat: OCR 模型按需下载 + 三层独立开关 + 日志持久化 + 原生浮层主题同步 + 文档重构
OCR 模型按需下载(P8 瘦身): - 移除 plugins/ppocr/assets/ 内置模型(det 9.8MB + rec 21MB + dict),APK 减包 ~31MB - 新增 modelDownloader.ts:优先从 HuggingFace/CDN 下载,兜底从 APK assets 拷贝 - OcrModule.kt 新增 setModelDir,支持从 filesystem 加载模型,回退 assets 兼容旧用户 - settingsStore 持久化 ocrModelVersion / ocrModelDir - 自动化页集成模型状态检查与一键下载 UI OCR 三层独立控制: - OcrProcessorConfig 从单一 aiVisionEnabled 拆分为 layer1/2/3 三个独立开关 - 设置页可按层启停(L1 正则规则 / L2 本地 OCR / L3 AI Vision) - OCR 处理增加耗时与字符数日志 日志系统升级: - logger.ts 新增 LogFileBackend 抽象,支持磁盘持久化(按日期 app-YYYY-MM-DD.log) - 新增 logBackend.ts(ExpoLogFileBackend)+ 日志中心页 settings/logs.tsx - 日志中心:实时缓冲 + 历史文件、4 级过滤、Tag/关键词搜索、JSON 展开、分享导出、7 天过期清理 - _layout.tsx 启动时初始化文件后端 + 日志脱敏(验证码/卡号) 原生浮层 UI 主题同步(P6): - 新增 floatingUiConfig.ts:JS 侧从 theme tokens + i18n 构建 FloatingUiConfig 推送原生 - 新增 FloatingUiConfigStore.kt:SharedPreferences 存储,三浮层组件读取 - FloatingBillView 重设计:颜色/文案走配置、新增币种 chip、金额校验改 BigDecimal - FloatingHelper / FloatingTip 同步适配 - _layout.tsx 新增 FloatingUiConfigSyncer,主题/语言切换自动推送 UI 与组件增强: - FormModal 新增 select/dropdown 控件、行内布局(row/flex)、联动回调 onValuesChange - 新增 Touchable 通用触摸组件、AccountCreateModal 快速建账弹窗 - 信用卡页展示账单周期/到期还款日/本期应还/剩余可用额度,关联账户改下拉选择 - AI 设置页重做:OpenAI/Gemini/DeepSeek 预设 + 默认 URL/模型 - 引导页新增 Android 权限检查步骤(无障碍/通知/短信/存储/悬浮窗) 去重优化: - 对手方匹配改为模糊包含(includes),双方均无对手方时判定低置信度重复 - DedupResult 新增 matchedItem 返回匹配对比项 文档重构: - README.md 重写为入口索引(品牌更新 + 模块概览 + 文档导航表) - 新增 AGENTS.md(AI 助手贡献指南)、docs/architecture.md(Mermaid 数据流/分层/OCR 级联图) - 新增 docs/development.md(环境/命令/编码规范/测试/提交规范)、plugins/README.md - UI 重设计文档(design spec + p1-p8)移入 docs/design/ 其他: - i18n 新增权限/信用卡详情/日志中心/AI 设置等翻译键 - ppocr Config Plugin 修复 import 注入去重;size-optimization 增强 - 新增测试:logger.test.ts、floating-ui-config.test.ts
This commit is contained in:
@@ -62,6 +62,9 @@ class OcrModule(private val context: ReactApplicationContext) :
|
||||
@Volatile private var initialized = false
|
||||
@Volatile private var initFailed = false
|
||||
|
||||
/** 模型文件目录(filesystem 绝对路径)。非空时从该目录加载模型,否则回退到 assets。 */
|
||||
@Volatile private var modelDir: String? = null
|
||||
|
||||
override fun getName(): String = OCR_MODULE_NAME
|
||||
|
||||
override fun initialize() {
|
||||
@@ -70,7 +73,7 @@ class OcrModule(private val context: ReactApplicationContext) :
|
||||
scope.launch { initEngine() }
|
||||
}
|
||||
|
||||
/** 从 assets 加载 det/rec ONNX 模型与字典。 */
|
||||
/** 从 assets 或 filesystem 加载 det/rec ONNX 模型与字典。 */
|
||||
private fun initEngine() {
|
||||
lock.lock()
|
||||
try {
|
||||
@@ -83,11 +86,29 @@ class OcrModule(private val context: ReactApplicationContext) :
|
||||
// 移动端关闭内存优化里的图优化级别过高(部分模型会崩)
|
||||
setOptimizationLevel(OrtSession.SessionOptions.OptLevel.BASIC_OPT)
|
||||
}
|
||||
val detBytes = context.assets.open(ASSET_DET_MODEL).use { it.readBytes() }
|
||||
val recBytes = context.assets.open(ASSET_REC_MODEL).use { it.readBytes() }
|
||||
val det = env.createSession(detBytes, opts)
|
||||
val rec = env.createSession(recBytes, opts)
|
||||
val dict = loadDictionary()
|
||||
val dir = modelDir
|
||||
val (det, rec, dict) = if (dir != null) {
|
||||
// 从 filesystem 加载(P8:模型按需下载到本地目录)
|
||||
val detPath = dir + java.io.File.separator + ASSET_DET_MODEL
|
||||
val recPath = dir + java.io.File.separator + ASSET_REC_MODEL
|
||||
val dictPath = dir + java.io.File.separator + ASSET_DICT
|
||||
Log.i(OCR_MODULE_NAME, "从 filesystem 加载模型: det=$detPath, rec=$recPath")
|
||||
Triple(
|
||||
env.createSession(detPath, opts),
|
||||
env.createSession(recPath, opts),
|
||||
loadDictionaryFromFile(dictPath)
|
||||
)
|
||||
} else {
|
||||
// 回退:从 assets 加载(兼容未迁移的老用户)
|
||||
val detBytes = context.assets.open(ASSET_DET_MODEL).use { it.readBytes() }
|
||||
val recBytes = context.assets.open(ASSET_REC_MODEL).use { it.readBytes() }
|
||||
Log.i(OCR_MODULE_NAME, "从 assets 加载模型(兼容模式)")
|
||||
Triple(
|
||||
env.createSession(detBytes, opts),
|
||||
env.createSession(recBytes, opts),
|
||||
loadDictionaryFromAssets()
|
||||
)
|
||||
}
|
||||
|
||||
detSession = det
|
||||
recSession = rec
|
||||
@@ -98,7 +119,12 @@ class OcrModule(private val context: ReactApplicationContext) :
|
||||
} catch (e: Exception) {
|
||||
initFailed = true
|
||||
Log.e(OCR_MODULE_NAME, "OCR 初始化失败: ${e.message}", e)
|
||||
Log.e(OCR_MODULE_NAME, "请确认 assets 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT")
|
||||
val dir = modelDir
|
||||
if (dir != null) {
|
||||
Log.e(OCR_MODULE_NAME, "请确认 $dir 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT")
|
||||
} else {
|
||||
Log.e(OCR_MODULE_NAME, "请确认 assets 下存在 $ASSET_DET_MODEL / $ASSET_REC_MODEL / $ASSET_DICT")
|
||||
}
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
@@ -108,13 +134,13 @@ class OcrModule(private val context: ReactApplicationContext) :
|
||||
private val recEnv: OrtEnvironment? get() = ortEnv
|
||||
|
||||
/**
|
||||
* 加载 ppocrv6_dict.txt 字典。
|
||||
* 从 assets 加载 ppocrv6_dict.txt 字典。
|
||||
*
|
||||
* PaddleOCR CTC 约定:模型输出 logits 的 index 0 是 blank,字符从 index 1 开始;
|
||||
* 字典条目 dictionary[i] 对应模型输出 index i+1。解码时 dictIdx = argmaxIdx - 1。
|
||||
* 字典本身不含 blank,运行时固定用 index 0 作 blank(见 ctcGreedyDecode)。
|
||||
*/
|
||||
private fun loadDictionary(): List<String> {
|
||||
private fun loadDictionaryFromAssets(): List<String> {
|
||||
val words = mutableListOf<String>()
|
||||
context.assets.open(ASSET_DICT).use { stream ->
|
||||
BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).useLines { lines ->
|
||||
@@ -127,6 +153,17 @@ class OcrModule(private val context: ReactApplicationContext) :
|
||||
return words
|
||||
}
|
||||
|
||||
/** 从 filesystem 路径加载字典文件。 */
|
||||
private fun loadDictionaryFromFile(path: String): List<String> {
|
||||
val words = mutableListOf<String>()
|
||||
java.io.File(path).bufferedReader(Charsets.UTF_8).useLines { lines ->
|
||||
lines.forEach { line ->
|
||||
words.add(line.trimEnd('\r', '\n'))
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别图片文本。
|
||||
* @param imageBase64 base64 编码的图片(JPEG/PNG)
|
||||
@@ -209,6 +246,18 @@ class OcrModule(private val context: ReactApplicationContext) :
|
||||
promise.resolve(initialized)
|
||||
}
|
||||
|
||||
/** 设置模型文件目录(绝对路径)。若引擎已初始化则释放并重新加载。 */
|
||||
@ReactMethod
|
||||
fun setModelDir(dir: String, promise: Promise) {
|
||||
modelDir = dir
|
||||
if (initialized) {
|
||||
release()
|
||||
initFailed = false
|
||||
scope.launch { initEngine() }
|
||||
}
|
||||
promise.resolve(true)
|
||||
}
|
||||
|
||||
// ============== 推理流水线 ==============
|
||||
|
||||
private fun ensureReady() {
|
||||
|
||||
Reference in New Issue
Block a user