npm install 한 번으로 Node.js에서 이미지 속 글자를 뽑아낸다.
arcships/light-ocr · 코어 C++17 + ONNX Runtime 1.22 · N-API v8 어댑터 · 빌드 순수 CMake(node-gyp 없음) · Node.js 22/24 · npm @arcships/light-ocr · 버전 0.3.2(2026-07-22) · 별점 372 · 라이선스 Apache-2.0 · 2026-07-14 생성된 신생 급상승 레포 · 클론 소스 직접 분석)
대부분의 고품질 OCR(PaddleOCR·EasyOCR 등)은 파이썬 + 무거운 딥러닝 프레임워크를 깔아야 하고, 클라우드 OCR API는 이미지를 남의 서버로 전송한다. Node.js 진영의 순수 JS 대안(Tesseract.js)은 설치는 쉽지만 정확도·속도가 아쉽다.
light-ocr은 그 사이를 파고든다. 정확도 좋은 PaddleOCR PP-OCRv6 Small 모델을 C++17 코어가 ONNX Runtime으로 돌리되, 그걸 Node.js에서 await 한 줄로 부르게 만들었다. 설치 순간에도, 실행 순간에도 인터넷이 필요 없다(모델·바이너리가 패키지에 들어 있음).
가장 작은 사용 예시가 이 라이브러리의 성격을 압축한다. 파이썬도, 별도 서버도, 모델 다운로드 스크립트도 없다:
import { createEngine } from "@arcships/light-ocr";
import { readFile } from "node:fs/promises";
const engine = await createEngine(); // 모델·네이티브 로드(오프라인)
const result = await engine.recognizeEncoded(await readFile("image.jpg"));
for (const line of result.lines)
console.log(line.text, line.confidence, line.box); // 글자 · 신뢰도 · 사각형 좌표
await engine.close(); // 명시적 정리
.onnx 파일)을 파이썬 없이 C++에서 실행해 주는 마이크로소프트의 추론 엔진이다. light-ocr은 이 셋을 묶어 "파이썬 없는 PaddleOCR"을 만든 셈이다.정리하면 light-ocr의 정체는 두 겹이다. ① C++ OCR 코어(light_ocr::core 정적 라이브러리): 이미지 디코딩 → 글자 영역 검출 → 인식 → 후처리까지 손으로 짠 파이프라인. ② 비동기 N-API 어댑터: 그 C++ 코어를 Node.js의 이벤트 루프를 막지 않고 비동기 Promise로 노출하는 얇지만 정교한 다리. 여기에 CLI(light-ocr)와 AI 에이전트용 스킬까지 얹었다. OCR "정확도"는 바이두 모델에서 오고, 이 레포의 진짜 가치는 그 모델을 Node 생태계에 매끄럽게·안전하게 심는 엔지니어링에 있다.
저자는 GitHub 조직 arcships(커밋 작성자 chenghan)이며, Apache-2.0으로 공개했다. 2026년 7월 14일 생성된 아주 신생 레포인데 별점이 빠르게 붙어 TrendShift Daily 상위에 올랐다. 흥미롭게도 AGENTS.md는 모든 커밋·PR·이슈 제목에 "중국 고전 시구 · 영어 제목" 형식을 요구하는 독특한 규약을 둔다(예: 修正平台標識 · fix platform id).
Node.js/자바스크립트 개발자가 "이미지에서 글자 좀 뽑자"고 마음먹으면, 지금까지 선택지는 셋 다 어딘가 불편했다. light-ocr은 그 셋의 약점을 정확히 겨냥한다.
| 방식 | 대표 | 장점 | 불편한 점 |
|---|---|---|---|
| 순수 JS | Tesseract.js | 설치 쉬움, 브라우저도 됨 | 정확도·속도 아쉬움, WASM 로딩 무거움 |
| 파이썬 엔진 | PaddleOCR, EasyOCR | 정확도 최상 | 파이썬+딥러닝 스택 필수 → Node 앱에서 쓰려면 별도 서버·IPC |
| 클라우드 API | Google Vision 등 | 서버 부담 0, 정확 | 이미지 외부 전송(프라이버시), 요금, 오프라인 불가 |
| 네이티브 임베드 (light-ocr) | light-ocr | 프리빌트 설치 1회, 오프라인, 좋은 모델, 하드웨어 가속 | 플랫폼 한정(glibc·특정 OS/arch), 라인 단위만 |
npm install @arcships/light-ocr 하면 끝이다. 컴파일러도, 파이썬도, post-install 다운로드도 없다. 비결은 npm 패키징 방식이다. 얇은 자바스크립트 파사드 패키지 하나가, 운영체제·CPU에 맞는 프리빌트 네이티브 패키지(6종)와 모델 패키지를 optionalDependencies로 두고 런타임에 자동 선택한다. 사용자는 이 복잡함을 전혀 못 느낀다.
.node 파일)이다. 보통은 설치할 때 사용자 PC에서 직접 컴파일(node-gyp)하는데, 컴파일러가 없으면 실패한다. 프리빌트는 이걸 미리 빌드해 npm에 올려 두는 방식이라 설치가 빠르고 안정적이다. N-API(Node-API)는 Node 버전이 올라가도 다시 컴파일 안 해도 되게 하는 안정적 C 인터페이스로, 한 번 빌드한 바이너리가 Node 22·24에서 그대로 돈다.설치 후에는 네트워크가 전혀 필요 없다. 이미지도, 인식 결과도 전부 로컬에서 처리된다. 개인정보가 담긴 문서·영수증·신분증을 다루는 앱이라면 이건 기능이 아니라 규정 준수(compliance) 요건이다. 클라우드 OCR로는 넘기 어려운 벽을, 라이브러리 하나로 넘는다.
번역기를 쓰는 두 방법. 클라우드 OCR은 편지를 통째로 번역 회사에 우편으로 보내 결과를 받는 것 — 빠르고 정확하지만 편지 내용을 남이 본다. 파이썬 OCR은 집에 번역가를 상주시키는 것 — 정확하지만 그 사람 먹이고 재우는(파이썬 환경 관리) 비용이 크다. light-ocr은 번역 사전 한 권을 책장에 꽂아 두는 것과 같다. 설치 한 번이면 그 뒤로는 집 안에서(오프라인) 언제든 펴 보고, 누구에게도 편지를 보이지 않는다.
기본 모드(createEngine())는 플랫폼에 맞는 가속기를 자동 선택한다: Apple Silicon macOS 15+는 Core ML(뉴럴 엔진), Linux/Windows x64는 WebGPU. 실측(v0.3.0)으로 Apple M4 Max에서 최대 2.85배, NVIDIA RTX 5060 Ti(WebGPU/Vulkan)에서 5.70배 빨라졌다. 인상적인 건 WebGPU 결과가 CPU FP32와 바이트 단위로 동일했다는 점 — "빠른데 결과가 달라지는" 흔한 함정을 품질 게이트로 막았다.
JS 생태계에는 "쓸 만한 정확도 + 쉬운 설치 + 오프라인"을 동시에 주는 OCR이 사실상 없었다. light-ocr은 그 빈자리를 프리빌트 네이티브 모듈이라는 정공법으로 메웠고, 여기에 하드웨어 가속·SHA-256 무결성 검증·에이전트 스킬까지 얹어 "장난감이 아니라 프로덕션용"이라는 인상을 준다. 신생 레포임에도 빠르게 별이 붙은 이유다.
인식 정확도의 원천은 바이두의 PP-OCRv6 Small 모델이다. light-ocr은 그 모델을 실행·포장하는 엔지니어링 레이어다. 또한 지원 범위에 한계가 있다: JPEG/PNG만(WebP·GIF·PDF·TIFF 불가), 라인 단위 좌표만(글자 단위 아님), glibc 리눅스만(musl/Alpine 불가), Bun 미검증. "만능 OCR"이 아니라 "특정 환경에서 아주 잘 도는 OCR"로 이해해야 한다.
light-ocr은 "프론트/백엔드"로 나눌 웹 앱이 아니다. 스택을 이해하려면 세 개의 층으로 보는 게 빠르다: 실제 OCR을 계산하는 C++ 코어, 그걸 JS에 잇는 N-API 어댑터, 그리고 이 모든 걸 사용자에게 배달하는 npm 패키징. 여기에 가속기(Core ML/WebGPU)가 코어 밑에 끼어든다.
손으로 짠 C++17 정적 라이브러리가 심장이다. 외부 딥러닝 프레임워크 없이, 추론은 ONNX Runtime에 맡기고 이미지 처리·후처리는 직접 구현했다. 의존성은 전부 버전 고정(models/deps.lock.json)이다.
| 구성요소 | 버전 | 역할 |
|---|---|---|
| ONNX Runtime (CPU) | 1.22.0 | 모든 플랫폼 공통 추론 엔진. .onnx 모델 실행 |
| ONNX Runtime WebGPU EP | 1.24.4 / 플러그인 0.1.0 | Linux·Windows x64 GPU 가속 실행 공급자 |
| OpenCV | 4.10.0 (core+imgproc만, 정적) | 이미지 리사이즈·크롭·원근 변환(warp) |
| Clipper | 6.4.2 | DBNet 후처리에서 폴리곤 확장(unclip) — 글자 박스 넓히기 |
| stb (stb_image) | 고정 커밋 | JPEG/PNG 디코딩(헤더 온리) |
| nlohmann/json | 3.11.3 | 디스크립터·툴·테스트의 JSON 파싱 |
CPUExecutionProvider는 CPU에서, CoreMLExecutionProvider는 애플 뉴럴 엔진에서, WebGpuExecutionProvider는 GPU에서 돌린다. light-ocr의 execution.provider: 'auto'는 기기에 맞는 EP를 자동 선택하고, 실패하면 CPU로 안전하게 되돌아간다(fallback).| 구성요소 | 내용 | 역할 |
|---|---|---|
| addon.cpp | 2321줄 | N-API 바인딩·엔진별 워커 스레드·요청 큐·완료 디스패처 |
| Node-API | 버전 8 | Node 버전 독립 ABI. 한 번 빌드로 22/24 공용 |
| js/index.cjs·.mjs·.d.ts | — | Promise·AbortSignal 래퍼, 타입 정의 |
| load-native.cjs | — | 플랫폼 감지 + SHA-256·ABI 계약 검증 후 로드 |
| bin/light-ocr.cjs | — | 무의존 CLI(recognize/detect/info) |
이 층의 핵심은 뒤(4장)에서 자세히 볼 비동기 모델이다. 무거운 C++ 연산을 JS 메인 스레드 밖에서 돌리면서도, 결과를 안전하게 Promise로 돌려준다.
가장 영리한 부분. 하나의 얇은 파사드 패키지가, 무거운 바이너리·모델을 별도 패키지로 쪼개 두고 런타임에 필요한 것만 고른다. binding.gyp도, node-gyp도, post-install 스크립트도 전혀 없다 — 빌드는 순수 CMake다.
@arcships/light-ocr 하나만 설치하지만, 그 뒤에서 OS·CPU에 맞는 바이너리가 골라진다. optionalDependencies는 npm 기능으로, "설치하다 실패해도 전체가 안 죽는" 의존성이다. 그래서 내 맥에는 맥용 바이너리만 깔리고 윈도우용은 조용히 건너뛴다. 리눅스 x64용 .node를 굳이 내 맥에 받을 필요가 없다.기본 모델은 CPU용 FP32다. WebGPU 가속을 위해 저자는 이 모델을 FP16으로 변환한 파생 모델을 따로 만들어 두되(검출 4.97MB·인식 10.6MB), GPU가 잘 못 하는 연산(NonMaxSuppression·TopK 등)은 FP32·CPU에 남기는 "op 블록 리스트"를 적용했다. 그 결과가 앞서 말한 "GPU인데 CPU와 바이트 동일"의 비결이다.
| 플랫폼 | Auto 모드 가속 |
|---|---|
| macOS Apple Silicon | Core ML(macOS 15+) → 실패 시 CPU |
| macOS Intel | CPU(인텔 Core ML은 정확도 검증 실패로 제외) |
| Linux x64 (glibc) | WebGPU(Vulkan) → CPU |
| Windows x64 | WebGPU(D3D12) → CPU |
| Linux·Windows arm64 | CPU (WebGPU 플러그인 미제공) |
이 레포에서 가장 배울 게 많은 부분이 바로 여기다. "무거운 C++ 작업을 Node의 이벤트 루프를 막지 않고 어떻게 돌리는가"라는 오래된 문제를, light-ocr은 교과서와 다른 방식으로 푼다.
보통 네이티브 애드온은 Napi::AsyncWorker로 작업을 libuv 스레드풀에 던진다. light-ocr은 대신 엔진 하나당 전용 std::thread 워커 한 개를 띄우고, 그 스레드가 FIFO 큐를 blocking으로 지키며 요청을 하나씩 처리한다.
// addon.cpp — 엔진 워커 스레드의 메인 루프 (개념 축약)
void EngineState::run() {
load_bundle(); // 모델 1회 로드
while (true) {
std::shared_ptr<Request> request;
{
std::unique_lock<std::mutex> lock(mutex);
changed.wait(lock, [this]{ // 큐에 일 생길 때까지 잠듦
return environment_closing || !queue.empty() || close_requested; });
if (environment_closing) break;
if (!queue.empty()) { request = queue.front(); queue.pop_front(); }
else if (close_requested) break;
}
// 디코드 → (ROI 크롭) → core->recognize()/detect() → 결과를 JS 스레드로
post_completion(context, run_ocr(request));
}
}
워커 스레드에서 계산한 결과를 JS 스레드로 넘기는 건 까다롭다. JS 엔진(V8) 객체는 다른 스레드에서 만지면 안 되기 때문이다. light-ocr은 환경당 단 하나의 napi_threadsafe_function(완료 디스패처)을 두어, 어느 워커든 결과를 이 통로로 밀어 넣으면 안전하게 JS 스레드에서 Promise가 resolve된다. 놀지 않을 땐 이벤트 루프를 잡아 두지 않도록 unref 처리한다.
// 모듈 초기화: 환경당 완료 디스패처 1개 생성
napi_create_threadsafe_function(env, nullptr, nullptr, resource_name,
kCompletionQueueCapacity /*64*/, 1, context.get(),
finalize_dispatcher, nullptr, call_js, &context->dispatcher);
napi_unref_threadsafe_function(env, context->dispatcher); // 유휴 시 루프 안 붙잡음
단순 비동기가 아니라 프로덕션급 흐름 제어가 들어 있다. 큐는 기본 4개(최대 64)로 경계가 있고(backpressure), 대기 중 입력 바이트가 기본 256MiB를 넘으면 거절한다. 각 요청은 네이티브 Promise + BigInt requestId를 돌려주고, JS의 AbortSignal로 native.cancel(requestId)를 불러 진행 중 작업을 취소할 수 있다. 이미지 스냅샷은 napi_adjust_external_memory로 회계 처리해 GC가 메모리 압력을 알게 한다.
// js/index.cjs — AbortSignal로 취소 가능한 Promise 래퍼
#recognize(nativeMethod, image, options) {
const operation = this.#native[nativeMethod](image, nativeOptions); // {requestId, promise}
return new Promise((resolve, reject) => {
const onAbort = () => {
try { this.#native.cancel(operation.requestId); } catch {}
settle(reject, abortReason(signal));
};
operation.promise.then(v => settle(resolve, v),
e => settle(reject, normalizeNativeError(e)));
if (signal) signal.addEventListener('abort', onAbort, { once: true });
});
}
워커의 run()은 모든 걸 try/catch로 감싸, C++ 예외가 JS 쪽으로 새어 나가 프로세스를 죽이는 일을 원천 차단한다. 치명적 오류는 napi_fatal_error로만 처리하고, 환경 종료 시 cleanup 훅이 모든 엔진 워커를 안전하게 배수(drain)한다. "네이티브 모듈은 크래시 위험"이라는 통념을, 방어적 설계로 정면 반박하는 부분이다.
실제 core->recognize() 내부는 고전적 2-스테이지 OCR이다. 글자가 어디 있는지 찾고(검출, DBNet), 그 자리를 잘라 무슨 글자인지 읽는다(인식, CRNN/SVTR + CTC).
네이티브 바이너리·모델을 npm으로 배포하는 건 공급망 공격의 표적이 될 수 있다. light-ocr은 load-native.cjs가 로드 전에 runtime-descriptor.json(스키마 2.0)을 검증한다: 아티팩트마다 SHA-256 해시·바이트 수 대조, 경로 탈출·심볼릭 링크 거부, 그리고 JS가 계산한 정책과 네이티브가 내보낸 runtimeContract를 맞춰 보는 ABI 계약 핸드셰이크까지. "신뢰할 수 있는 바이너리만 로드"를 코드로 강제한다.
구조가 말하는 핵심 세 가지. 첫째, C++ 코어(src/)와 Node 어댑터(bindings/node/)가 깔끔히 분리돼 있어, C++ 라이브러리만 떼어 다른 언어에 붙일 수도 있다. 둘째, src/inference/가 추상 인터페이스(backend.hpp) + 구체 구현(onnxruntime·coreml)으로 갈려 있어 가속기 추가가 쉽다. 셋째, 신생 레포치고 검증 자료(reports/·corpus/·fuzz)가 이례적으로 무겁다 — 품질에 진심이라는 신호다.
| 핵심 파일 | 왜 중요한가 |
|---|---|
| src/core/engine.cpp (1286줄) | 디코드→검출→정렬→인식→디코드 전 과정을 엮는 지휘자 |
| bindings/node/src/addon.cpp (2321줄) | N-API 표면·워커 스레드·큐·완료 디스패처 — 이 레포의 백미 |
| src/inference/onnxruntime/backend.cpp | ORT 세션 생성, WebGPU EP 라이브러리 등록·해시 재검증 |
| src/detection/db_postprocess.cpp | 확률맵 → 윤곽 → Clipper unclip → 사각형 박스 |
| js/load-native.cjs | 플랫폼 감지 + SHA-256/ABI 계약 검증 후 .node 로드 |
| include/light_ocr/core.hpp | public API. 모든 함수가 예외 대신 Result<T> 반환 |
이 레포엔 신경망 학습 코드도, 모델 구조 정의도 전혀 없다. 모델은 이미 학습된 .onnx 파일(별도 패키지)이고, 코드는 그걸 실행하고 전후처리할 뿐이다. "OCR 정확도 로직"을 찾지 말고, 추론 엔진 호출 + 이미지 처리 + Node 바인딩이라는 세 축으로 머릿속 모델을 잡아야 길을 잃지 않는다.
이 레포 최고의 학습 자산. AsyncWorker/libuv 스레드풀 대신 엔진당 전용 스레드 + 참조 카운트된 단일 threadsafe function 디스패처를 쓰는 방식은, 오래 도는 C++ 작업을 JS에 노출하는 정교한 실전 레퍼런스다. 네이티브 Promise, BigInt requestId 취소, 경계 있는 FIFO 백프레셔, napi_adjust_external_memory 회계 — 하나하나가 프로덕션 네이티브 모듈의 필수 요소다.
피보나치·소수 판정처럼 CPU를 오래 먹는 C++ 함수를, 전용 스레드 + threadsafe function으로 감싸 await addon.compute(n)이 이벤트 루프를 막지 않게 만들어 보라. light-ocr의 addon.cpp 워커 루프를 축소 복제하면, "왜 그냥 동기 함수를 export하면 안 되는가"를 몸으로 이해하게 된다.
파사드 + 플랫폼별 optionalDependency 바이너리 + 별도 모델 패키지, 전부 프리빌트(node-gyp·binding.gyp 없이 CMake만), Electron용 delay-load 훅까지. 이는 "설치할 때 컴파일"하던 옛 방식과 극명히 대비되는, npm에 네이티브 코드를 배달하는 모범 청사진이다. C/C++/Rust로 짠 무언가를 JS 생태계에 팔고 싶은 사람에게 그대로 참고서가 된다.
InferenceSession이라는 순수 추상 인터페이스 아래 ORT·Core ML 구현이 갈려 있고, execution.provider: 'auto'가 기기에 맞는 걸 골라 실패 시 CPU로 안전 강등한다. "하드웨어마다 다른 백엔드를, 상위 코드는 모르게 추상화하는" 이 구조는 플러그형 아키텍처의 좋은 표본이다.
WebGPU FP16로 5배 빨라지면서도 CPU FP32와 바이트 동일을 품질 게이트로 요구한 점은 인상적이다. GPU가 약한 연산은 CPU에 남기는 "경계 있는 CPU 파티션", 14장 이미지 전수 대조 — "빠른데 슬그머니 결과가 달라지는" 가속의 함정을 어떻게 규율하는지 보여 준다.
아티팩트별 SHA-256 검증, 버전 있는 디스크립터 스키마, 경로 탈출·심링크 방어, JS↔네이티브 ABI 계약 핸드셰이크. npm으로 신뢰 가능한 바이너리를 배포하는 법의 레퍼런스다. 여기에 예외 대신 Result<T>, noexcept 추론 경계, 오버플로 안전 checked_math까지 — 방어적 시스템 C++의 표본이기도 하다.
.node가 실제 제공하는 규격이 어긋나면 조용히 crash가 나는데, light-ocr은 로드 시 둘을 명시적으로 대조(핸드셰이크)해 그 사고를 막는다. Result<T>는 "성공값 T 또는 오류"를 한 타입에 담아 반환하는 패턴으로, 예외를 던지지 않아 C++ 예외가 언어 경계를 넘는 위험 자체를 없앤다.| 용도 | 요구사항 |
|---|---|
| 설치·실행(사용자) | Node.js 22 또는 24. 프리빌트라 컴파일러·파이썬·다운로드 불필요. 플랫폼 바이너리+모델 자동 선택 |
| 지원 플랫폼 | macOS arm64/x64 · Linux x64/arm64(glibc만, musl/Alpine 불가) · Windows x64/arm64 |
| 하드웨어 가속 | Core ML: Apple Silicon + macOS 15+ · WebGPU: Linux x64(Vulkan)·Windows x64(D3D12). arm64·인텔맥은 CPU |
| 입력 형식 | JPEG · PNG 또는 원시 픽셀(gray8/rgb8/bgr8/rgba8). WebP·GIF·PDF·TIFF 미지원 |
| 소스 빌드 | CMake ≥ 3.24 · Ninja · C++17 컴파일러. ONNX Runtime 1.22.0/OpenCV 4.10.0 등은 의존성 캐시로 페치. Node 애드온은 -DLIGHT_OCR_BUILD_NODE=ON |
| GPU 메모리 | 경량 모델(FP16 검출 4.97MB·인식 10.6MB)이라 부담 작음. WebGPU는 Linux에서 DRM 렌더 노드, Windows에서 dxcompiler.dll 필요 |
실행 옵션이 이 라이브러리의 "손잡이"를 보여 준다. createEngine({ execution: { provider: 'auto'|'cpu'|'apple'|'webgpu', precision: 'auto'|'fp32'|'fp16', sessionFallback: 'error'|'cpu' } })로 가속을 세밀 제어하고, detection: { strategy: 'bounded'|'tiled' }로 고해상도 속 작은 글자를 타일 방식으로 잡는다. 인식은 recognitionBatchSize·recognitionScoreThreshold로, 리소스는 queueCapacity·maxPendingInputBytes로 조인다.
가장 흔한 실전 함정은 musl 리눅스(Alpine 도커 이미지)다. 많은 컨테이너가 Alpine 기반인데 light-ocr은 glibc만 지원해 unsupported_platform을 던진다. 도커로 배포한다면 node:22-slim(Debian/glibc) 계열을 써야 한다. 또 arm64 서버·인텔 맥은 GPU 가속이 없어 CPU로만 돈다는 점도 성능 계획에 반영해야 한다.
npm install @arcships/light-ocr(Node 22/24) 후, 1장의 코드 예시 그대로 아무 영수증·명함 JPEG를 넣어 result.lines를 출력한다. 각 줄의 text·confidence·box(4점 좌표)를 눈으로 확인하고, optionalDependencies가 내 OS용 바이너리만 깔았는지 node_modules/@arcships/를 열어 확인한다.
내장 CLI(npx light-ocr recognize --format jsonl img.png)로 폴더 속 이미지를 순회해 줄 단위 JSONL로 내보내고, jq로 신뢰도 0.9 이상만 거른다. --region x,y,w,h로 특정 영역만, --provider cpu|webgpu로 백엔드를 바꿔 종료 코드(0/64~72)가 어떻게 오류를 구분하는지 관찰한다.
같은 이미지 세트를 provider: 'cpu'와 'auto'로 각각 돌려 속도(result.timingUs)를 비교하고, 두 결과의 text가 완전히 같은지 대조한다. light-ocr이 자랑하는 "가속하되 CPU와 바이트 동일"을 직접 재현하며, 왜 그게 어려운 약속인지 체감한다.
엔진 하나에 수십 개 recognize를 동시에 던져 큐 백프레셔(기본 4, queueCapacity)와 maxPendingInputBytes 거절을 유발해 본다. 일부 요청에 AbortSignal을 걸어 중간 취소하고, 그 사이 이벤트 루프가 안 막히는지(다른 타이머가 계속 도는지) 확인한다. light-ocr의 비동기 설계가 왜 필요한지 몸으로 아는 과제.
light-ocr을 참고해, 무거운 C++ 함수를 엔진당 전용 std::thread + 단일 napi_threadsafe_function으로 감싸는 최소 네이티브 모듈을 CMake만으로(node-gyp 없이) 빌드한다. 네이티브 Promise 반환, requestId 취소, 경계 있는 큐까지 넣고, SHA-256으로 .node 무결성을 검증한 뒤 로드하는 load 게이트를 흉내 내 본다. 이 레포의 핵심 엔지니어링을 스스로 재현하는 캡스톤.
| 주차 | 주제 · 목표 | 핵심 자료 |
|---|---|---|
| 1주차 | OCR 파이프라인 이해. 검출(DBNet)과 인식(CRNN/SVTR)의 2단계 구조, CTC 디코드, 원근 변환 크롭, 읽기 순서 정렬, 타일 검출·NMS | src/core/engine.cpp · db_postprocess.cpp · PaddleOCR PP-OCRv6 문서 |
| 2주차 | ONNX Runtime 추론. .onnx 모델 실행, 실행 공급자(CPU/CoreML/WebGPU), FP16 변환·op 블록 리스트, NCHW 텐서화 |
src/inference/ · models/*.lock.json · ONNX Runtime 공식 문서 |
| 3주차 | Node-API 비동기. 워커 스레드, napi_threadsafe_function, 네이티브 Promise, AbortSignal 취소, 외부 메모리 회계, 예외 안전 경계 |
bindings/node/src/addon.cpp · js/index.cjs · Node-API 공식 문서 |
| 4주차 | 네이티브 모듈 배포. CMake로 .node 빌드, 파사드+플랫폼별 프리빌트 패키징, SHA-256·ABI 계약 검증, Electron delay-load |
bindings/node/CMakeLists.txt · load-native.cjs · prebuildify/napi 생태계 |
네이티브 애드온은 빠르지만 플랫폼 매트릭스(OS×arch×libc)라는 배포 부담을 진다. 브라우저·엣지가 목표면 WASM(Tesseract.js·ONNX Runtime Web)이, 확장성·무관리가 목표면 클라우드 OCR이 낫다. light-ocr이 택한 "서버·데스크톱 Node + 오프라인" 자리를 세 대안과 비교해 보면, 기술 선택이 곧 트레이드오프 선택임이 선명해진다.
.onnx 모델을 파이썬 없이 C++에서 실행하는 마이크로소프트 추론 엔진. EP는 연산을 어디서 계산할지 정하는 백엔드(CPU/CoreML/WebGPU)로, light-ocr의 provider: 'auto'가 자동 선택·CPU 폴백한다..node 확장. 프리빌트는 이를 미리 빌드해 npm에 올려 설치 시 컴파일을 없앤 방식.unref로 이벤트 루프를 안 붙잡는다..node가 제공하는 규격(ABI)을 대조하는 핸드셰이크, 그리고 아티팩트마다 SHA-256 해시로 위변조를 검증하는 게이트. 신뢰 가능한 바이너리만 로드하도록 강제한다.