Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ name: core

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

concurrency:
group: core-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/npm-promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
version:
description: Already-published lockstep version to promote
required: true
default: 0.2.0
default: 0.2.1
type: string
release_run_id:
description: npm release run containing the verified release artifact
Expand Down
39 changes: 35 additions & 4 deletions .github/workflows/npm-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
version:
description: Lockstep version for all six packages
required: true
default: 0.2.0
default: 0.2.1
type: string
publish_to_registry:
description: Publish the fully gated candidate to npm after preflight
Expand Down Expand Up @@ -42,6 +42,7 @@ jobs:
test "$RELEASE_VERSION" = "$source_version"
if [[ "${{ inputs.publish_to_registry }}" == "true" ]]; then
test -f contracts/tiled-platform-baselines.json
test -f contracts/apple-provider-baselines.json
fi

build-native:
Expand Down Expand Up @@ -182,8 +183,32 @@ jobs:
if-no-files-found: error
retention-days: 90

derive-apple-models:
needs: validate
runs-on: macos-15
timeout-minutes: 90
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- name: Install the hash-locked Apple model toolchain
run: python -m pip install --require-hashes -r tools/apple/requirements.lock
- name: Bootstrap and derive the deterministic Apple models
shell: bash
run: |
python tools/bootstrap_models.py --cache-dir .cache/models
python tools/package_model_bundle.py
python tools/apple/convert_models.py
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: npm-apple-fp16-models
path: models/generated/apple-fp16-20260715.1
if-no-files-found: error
retention-days: 30

assemble:
needs: build-native
needs: [build-native, derive-apple-models]
runs-on: ubuntu-24.04
env:
RELEASE_VERSION: ${{ inputs.version }}
Expand All @@ -199,17 +224,23 @@ jobs:
pattern: native-*
path: dist/native-input
merge-multiple: false
- name: Bootstrap and verify the model bundle
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: npm-apple-fp16-models
path: models/generated/apple-fp16-20260715.1
- name: Bootstrap and package the self-contained Apple model bundle
shell: bash
run: |
python tools/bootstrap_models.py --cache-dir .cache/models
python tools/package_model_bundle.py
python tools/apple/package_bundle.py \
--qualification-report contracts/apple-provider-baselines.json
- name: Assemble and deterministically pack six packages
shell: bash
run: |
python tools/npm_release.py assemble \
--version "$RELEASE_VERSION" \
--bundle models/generated/ppocrv6-small-onnx-20260714.2 \
--bundle models/generated/ppocrv6-small-apple-20260715.1 \
--native-root dist/native-input \
--output-dir dist/npm/staging
python tools/npm_release.py pack \
Expand Down
42 changes: 40 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ if(APPLE AND (NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET OR
"Minimum macOS deployment target" FORCE)
endif()

project(light_ocr VERSION 0.2.0 LANGUAGES CXX)
project(light_ocr VERSION 0.2.1 LANGUAGES CXX)

if(APPLE)
enable_language(OBJCXX)
endif()

include(CTest)
include(GNUInstallDirs)
Expand Down Expand Up @@ -73,6 +77,9 @@ add_library(light_ocr_core STATIC
src/result/result.cpp
src/util/sha256.cpp
)
if(APPLE)
target_sources(light_ocr_core PRIVATE src/inference/coreml/backend.mm)
endif()
add_library(light_ocr::core ALIAS light_ocr_core)

target_include_directories(light_ocr_core
Expand All @@ -93,6 +100,17 @@ target_link_libraries(light_ocr_core
Threads::Threads
)

if(APPLE)
find_library(LIGHT_OCR_COREML_FRAMEWORK CoreML REQUIRED)
find_library(LIGHT_OCR_FOUNDATION_FRAMEWORK Foundation REQUIRED)
target_link_libraries(light_ocr_core PRIVATE
"${LIGHT_OCR_COREML_FRAMEWORK}"
"${LIGHT_OCR_FOUNDATION_FRAMEWORK}")
target_compile_options(light_ocr_core PRIVATE
$<$<COMPILE_LANGUAGE:OBJCXX>:-fobjc-arc>)
target_compile_definitions(light_ocr_core PRIVATE LIGHT_OCR_HAS_COREML=1)
endif()

target_compile_definitions(light_ocr_core PRIVATE LIGHT_OCR_VERSION="${PROJECT_VERSION}")

if(MSVC)
Expand Down Expand Up @@ -128,7 +146,8 @@ if(LIGHT_OCR_BUILD_TOOLS)

add_executable(light_ocr_leak_check tools/leak_check/main.cpp tools/common/bundle_files.cpp)
target_link_libraries(light_ocr_leak_check PRIVATE light_ocr::core nlohmann_json::nlohmann_json)
target_include_directories(light_ocr_leak_check PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tools)
target_include_directories(light_ocr_leak_check PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/tools)
light_ocr_stage_onnxruntime(light_ocr_leak_check)

add_executable(light_ocr_stage_probe tools/stage_probe/main.cpp tools/common/bundle_files.cpp)
Expand Down Expand Up @@ -172,6 +191,25 @@ if(LIGHT_OCR_BUILD_TESTS)
light_ocr_stage_onnxruntime(light_ocr_integration_tests)
add_test(NAME light_ocr_integration_tests COMMAND light_ocr_integration_tests)
set_tests_properties(light_ocr_integration_tests PROPERTIES SKIP_RETURN_CODE 77)
if(APPLE)
add_executable(light_ocr_apple_integration_tests
tests/integration/apple.cpp tools/common/bundle_files.cpp)
target_link_libraries(light_ocr_apple_integration_tests PRIVATE
light_ocr::core nlohmann_json::nlohmann_json opencv_core opencv_imgproc
Threads::Threads)
target_include_directories(light_ocr_apple_integration_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/tools)
light_ocr_stage_onnxruntime(light_ocr_apple_integration_tests)
add_test(NAME light_ocr_apple_integration_tests
COMMAND light_ocr_apple_integration_tests)
set_tests_properties(light_ocr_apple_integration_tests PROPERTIES
SKIP_RETURN_CODE 77 LABELS "acceptance;apple" TIMEOUT 180)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/models/generated/ppocrv6-small-apple-20260715.1/manifest.json")
set_tests_properties(light_ocr_apple_integration_tests PROPERTIES
ENVIRONMENT
"LIGHT_OCR_APPLE_MODEL_BUNDLE=${CMAKE_CURRENT_SOURCE_DIR}/models/generated/ppocrv6-small-apple-20260715.1;LIGHT_OCR_APPLE_TEST_PIXELS=${CMAKE_CURRENT_SOURCE_DIR}/corpus/fixtures/generated-hello-123/pixels.bin")
endif()
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/models/generated/ppocrv6-small-onnx-20260714.2/manifest.json")
set_tests_properties(light_ocr_integration_tests PROPERTIES
ENVIRONMENT "LIGHT_OCR_MODEL_BUNDLE=${CMAKE_CURRENT_SOURCE_DIR}/models/generated/ppocrv6-small-onnx-20260714.2")
Expand Down
12 changes: 10 additions & 2 deletions bindings/node/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,21 @@ if(LIGHT_OCR_BUILD_TESTS)
"${PROJECT_SOURCE_DIR}/models/generated/ppocrv6-small-onnx-20260714.2")
if(LIGHT_OCR_NODE_EXECUTABLE AND
EXISTS "${_light_ocr_test_bundle}/manifest.json")
set(_light_ocr_node_test_environment
"LIGHT_OCR_NODE_BINARY=$<TARGET_FILE:light_ocr_node>"
"LIGHT_OCR_MODEL_BUNDLE=${_light_ocr_test_bundle}")
set(_light_ocr_apple_test_bundle
"${PROJECT_SOURCE_DIR}/models/generated/ppocrv6-small-apple-20260715.1")
if(APPLE AND EXISTS "${_light_ocr_apple_test_bundle}/manifest.json")
list(APPEND _light_ocr_node_test_environment
"LIGHT_OCR_APPLE_MODEL_BUNDLE=${_light_ocr_apple_test_bundle}")
endif()
add_test(NAME light_ocr_node_tests
COMMAND "${LIGHT_OCR_NODE_EXECUTABLE}" --test --test-concurrency=1
"${CMAKE_CURRENT_SOURCE_DIR}/test/adapter.test.cjs")
set_tests_properties(light_ocr_node_tests PROPERTIES
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
ENVIRONMENT
"LIGHT_OCR_NODE_BINARY=$<TARGET_FILE:light_ocr_node>;LIGHT_OCR_MODEL_BUNDLE=${_light_ocr_test_bundle}"
ENVIRONMENT "${_light_ocr_node_test_environment}"
LABELS "node;integration")
else()
message(STATUS
Expand Down
16 changes: 14 additions & 2 deletions bindings/node/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# light-ocr Node-API adapter

状态:`@arcships/light-ocr@0.2.0` 已发布;tiled detection 和内存 JPEG/PNG 输入可用。macOS arm64/x64、Linux x64 glibc、Windows x64 的 Node.js 22/24 package matrix、真实 PP-OCRv6 和禁网运行均已通过
状态:`@arcships/light-ocr@0.2.0` 已发布;当前 0.2.1 源码候选加入开放 macOS 兼容的 Apple/Core ML provider。tiled detection 和内存 JPEG/PNG 输入继续可用,默认推理仍为 CPU

推荐直接安装公开 package:

Expand All @@ -21,6 +21,7 @@ npm install @arcships/light-ocr
- 支持 `AbortSignal` 协作式取消:queued 请求会从队列移除;running 请求立即拒绝 public Promise,但 Core 会安全运行到返回并丢弃结果。
- native addon 只接收现有绝对 bundle 目录。当前源码开发调用显式传 `bundlePath`;发布后的 facade 默认使用随 npm 安装的 model package 路径。
- 产品 engine 默认报告 `detectionStrategy: 'bounded'`、`detectionMaxSide: 960` 和 `defaultRecognitionBatchSize: 1`。0.2.0 可通过 `detection: {strategy: 'tiled'}` 显式选择 `tiled-v1`;`upstreamExact` 只用于上游对照,单次 `recognize({detectionMaxSide})` 只能继续降低 bounded engine 的 side。
- `createEngine({execution})` 接受 `cpu` 或 `apple`。macOS 15+ 默认开放:Apple Silicon interactive 使用 FP16 ANE + 宽文本 FP16 GPU,strict 使用全 GPU;Intel Mac 使用 Core ML CPU+GPU 且只接受 `cpuPartition: 'allow'`。显式 CPU fallback 会报告稳定原因;`deviceValidated` 区分已有 M4 证据与其他 Mac 的实验兼容,`engine.info.execution.sessions` 和逐批 diagnostics 还提供模型、设备、缓存、qualification ID、shape bucket 与实际 compute unit。

不支持 WebP、GIF、PDF、EXIF orientation 自动旋转、zero-copy/transfer、运行中 inference 硬中断、Electron 或 Bun。详细契约见 [Node-API 设计](../../docs/napi-design.md)。

Expand Down Expand Up @@ -51,6 +52,7 @@ macOS/Linux 产物在 `build-node/bin/light_ocr_node.node`,锁定的 ONNX Runt
```bash
export LIGHT_OCR_NODE_BINARY="$PWD/build-node/bin/light_ocr_node.node"
export LIGHT_OCR_MODEL_BUNDLE="$PWD/models/generated/ppocrv6-small-onnx-20260714.2"
export LIGHT_OCR_APPLE_MODEL_BUNDLE="$PWD/models/generated/ppocrv6-small-apple-20260715.1"

node --test --test-concurrency=1 bindings/node/test/adapter.test.cjs
# 或:ctest --test-dir build-node -R '^light_ocr_node_tests$' --output-on-failure
Expand All @@ -65,7 +67,17 @@ node --test --test-concurrency=1 bindings/node/test/adapter.test.cjs
```js
const { createEngine, OcrError } = require('@arcships/light-ocr');

const engine = await createEngine({ queueCapacity: 4 });
const engine = await createEngine({
queueCapacity: 4,
execution: {
provider: 'apple',
precision: 'fp16',
sessionFallback: 'cpu',
},
});

console.log(engine.info.execution.sessions.detection.actualProviderChain);
console.log(engine.info.execution.sessions.detection.deviceValidated);
```

当前源码开发用法仍需显式 bundle:
Expand Down
15 changes: 10 additions & 5 deletions bindings/node/js/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const { loadNative } = require('./load-native.cjs');

const DEFAULT_MODEL = 'ppocrv6-small';
const MODEL_PACKAGE = '@arcships/light-ocr-model-ppocrv6-small';
const EXPECTED_BUNDLE_ID = 'ppocrv6-small-onnx-20260714.2';
const CPU_BUNDLE_ID = 'ppocrv6-small-onnx-20260714.2';
const APPLE_BUNDLE_ID = 'ppocrv6-small-apple-20260715.1';

class OcrError extends Error {
constructor(code, message, detail) {
Expand Down Expand Up @@ -49,7 +50,7 @@ function abortReason(signal) {
: signal.reason;
}

function resolveBuiltInBundle(model) {
function resolveBuiltInBundle(model, requireApple) {
if (model !== DEFAULT_MODEL) {
throw new OcrError(
'invalid_argument',
Expand All @@ -76,11 +77,14 @@ function resolveBuiltInBundle(model) {
cause instanceof Error ? cause.message : String(cause),
);
}
if (manifest.bundleId !== EXPECTED_BUNDLE_ID) {
const compatibleBundleIds = requireApple
? [APPLE_BUNDLE_ID]
: [CPU_BUNDLE_ID, APPLE_BUNDLE_ID];
if (!compatibleBundleIds.includes(manifest.bundleId)) {
throw new OcrError(
'package_load_failed',
'The installed model package is incompatible with this light-ocr release',
`expected ${EXPECTED_BUNDLE_ID}, received ${String(manifest.bundleId)}`,
`expected ${compatibleBundleIds.join(' or ')}, received ${String(manifest.bundleId)}`,
);
}
return path.dirname(manifestPath);
Expand All @@ -101,7 +105,8 @@ function resolveCreateOptions(options) {
}
if (hasBundlePath) return options;
const model = hasModel ? options.model : DEFAULT_MODEL;
const resolved = { ...options, bundlePath: resolveBuiltInBundle(model) };
const requireApple = options.execution?.provider === 'apple';
const resolved = { ...options, bundlePath: resolveBuiltInBundle(model, requireApple) };
delete resolved.model;
return resolved;
}
Expand Down
61 changes: 61 additions & 0 deletions bindings/node/js/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,27 @@
export type PixelFormat = 'gray8' | 'rgb8' | 'bgr8' | 'rgba8';
export type DetectionStrategy = 'bounded' | 'tiled' | 'upstreamExact';
export type BuiltInModel = 'ppocrv6-small';
export type ExecutionProvider = 'cpu' | 'apple';
export type SessionFallback = 'error' | 'cpu';
export type CpuPartition = 'allow' | 'forbid';
export type PerformanceHint = 'latency' | 'throughput';
export type Precision = 'auto' | 'fp32' | 'fp16';

export interface DetectionOptions {
readonly strategy?: DetectionStrategy;
readonly maxSide?: number;
}

export interface ExecutionOptions {
/** Only providers shipped and qualified by this release appear in this union. */
readonly provider?: ExecutionProvider;
readonly sessionFallback?: SessionFallback;
readonly cpuPartition?: CpuPartition;
readonly deviceId?: number;
readonly performanceHint?: PerformanceHint;
readonly precision?: Precision;
}

export interface RawImage {
readonly data: Uint8Array;
readonly width: number;
Expand Down Expand Up @@ -43,6 +58,7 @@ export interface CreateEngineOptions {
readonly queueCapacity?: number;
readonly maxPendingInputBytes?: number;
readonly detection?: DetectionOptions;
readonly execution?: ExecutionOptions;
}

export interface RecognizeOptions {
Expand All @@ -67,6 +83,9 @@ export interface RecognitionBatchShape {
readonly batchSize: number;
readonly height: number;
readonly width: number;
readonly computeUnit: 'cpu' | 'ane' | 'gpu';
readonly modelId: string;
readonly shapeBucket: string;
}
export interface DetectionPassShape {
readonly tileOrdinal: number;
Expand Down Expand Up @@ -121,13 +140,55 @@ export interface TiledDetectionInfo {
readonly mergeIouThreshold: 0.5;
readonly mergeIosThreshold: 0.8;
}
export interface ProviderCapabilityInfo {
readonly provider: string;
readonly packageIncluded: boolean;
readonly deviceAvailable: boolean;
/** True only when this exact hardware family has reviewed qualification evidence. */
readonly deviceValidated: boolean;
}
export interface SessionExecutionInfo {
readonly requestedProvider: string;
readonly actualProviderChain: readonly string[];
readonly device: string;
readonly deviceFamily: string;
readonly operatingSystem: string;
readonly precision: string;
readonly shapePolicy: string;
readonly modelId: string;
readonly modelSha256: string;
readonly runtime: string;
readonly runtimeVersion: string;
readonly providerVersion: string;
readonly modelCacheStatus: string;
readonly qualificationId: string;
/** False means the open macOS compatibility path is experimental on this device. */
readonly deviceValidated: boolean;
readonly sessionFallback: boolean;
readonly fallbackReason?: string;
}
export interface ExecutionInfo {
readonly requestedProvider: ExecutionProvider;
readonly sessionFallback: SessionFallback;
readonly cpuPartition: CpuPartition;
readonly deviceId?: number;
readonly performanceHint: PerformanceHint;
readonly requestedPrecision: Precision;
readonly providerCapabilities: readonly ProviderCapabilityInfo[];
readonly sessions: {
readonly detection: SessionExecutionInfo;
readonly recognition: SessionExecutionInfo;
};
}
export interface EngineInfo {
readonly coreVersion: string;
readonly modelBundleId: string;
readonly modelBundleSchemaVersion: string;
readonly normalizedConfigSchemaVersion: string;
readonly backend: string;
/** @deprecated Use execution.sessions for stage-specific provider details. */
readonly executionProvider: string;
readonly execution: ExecutionInfo;
readonly capabilities: {
readonly detection: boolean;
readonly recognition: boolean;
Expand Down
Loading
Loading