Skip to content

Commit fa57210

Browse files
chrisryugjclaude
andcommitted
fix: OCR 초기화가 폴더 목록 조회까지 멈추게 하던 문제 해소 (v3.4.5, 이슈 #35 후속)
v3.4.4로 앱은 기동하게 됐으나, OcrEngine::new(ONNX 세션 빌드)가 끝나지 않는 환경에서 폴더 트리가 통째로 비어 보이고 get_folders_with_info가 10초 타임아웃을 반복했다. 원인: folder_service() → get_watch_manager() → get_ocr_engine() 체인. OCR이 감시 매니저와 같은 OnceCell을 blocking 초기화하는 바람에, 그 초기화를 기다리는 UI 커맨드가 전부 함께 굶었다. DB·인덱스는 정상(파일명 캐시 25만 건 정상 로드), 조회만 막힌 것. - get_watch_manager / index_service는 blocking init 대신 ocr_engine_if_ready peek 사용 - OCR 워밍업은 setup의 전용 백그라운드 스레드가 전담 - ocr_engine을 Arc<OnceCell> 공유로 전환(embedder·vector_index와 동일 패턴) — WatchManager 생성이 워밍업보다 빨라도 준비 즉시 반영, Option 캡처로 영구 고정되지 않음 - OcrEngine::new에 단계별 로그 추가 — 지연 단계(det/rec 세션) 원격 판별용 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5f9ea33 commit fa57210

9 files changed

Lines changed: 96 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22

3+
## [3.4.5] - 2026-07-23
4+
5+
**OCR 초기화가 앱 전체를 멈추게 하던 문제 해소 — 폴더 목록이 비어 보이던 증상 (이슈 #35 후속)**
6+
7+
### 🐛 수정
8+
9+
- **감시 폴더 목록이 통째로 비어 보이던 문제 해소** — v3.4.4 에서 앱은 정상 기동하게 됐으나,
10+
OCR 엔진(ONNX 세션) 초기화가 오래 걸리는 환경에서 폴더 트리가 뜨지 않고
11+
`get_folders_with_info` 가 반복 타임아웃됐다. 원인은 OCR 엔진이 감시 매니저와 같은 지연 초기화
12+
셀을 공유해, OCR 초기화가 끝날 때까지 폴더 목록 조회까지 함께 대기한 것. **인덱싱 데이터는
13+
손상되지 않았고**, 조회만 막혀 비어 보인 것이다.
14+
- 이제 OCR 엔진 초기화는 **전용 백그라운드 워밍업**에서만 수행하고, 나머지 경로는 준비된 경우에만
15+
가져다 쓴다. OCR 초기화가 지연되거나 실패해도 폴더 목록·검색·인덱싱은 그대로 동작하며,
16+
스캔 문서 OCR 만 비활성된다.
17+
18+
### 🔧 내부
19+
20+
- OCR 엔진을 `Arc<OnceCell>` 공유로 전환 — 임베더·벡터 인덱스와 동일 패턴. 감시 매니저 생성 시점에
21+
아직 워밍업 전이어도 준비되는 즉시 반영되어, 생성 순서에 따라 OCR 이 꺼진 채 굳지 않는다.
22+
- OCR 초기화에 단계별 로그(`[1/3] det 세션``[2/3] rec 세션``[3/3] 사전`) 추가 — 환경별로
23+
어느 단계에서 지연되는지 로그만으로 판별 가능.
24+
325
## [3.4.4] - 2026-07-23
426

527
**OCR 활성 시 기동 실패 잔여 해소 — DLL을 설치본 경로에서 직접 로드 (이슈 #35 후속)**

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "anything",
3-
"version": "3.4.4",
3+
"version": "3.4.5",
44
"description": "로컬 문서 검색 앱 - Everything 대항마",
55
"type": "module",
66
"scripts": {

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "docufinder"
3-
version = "3.4.4"
3+
version = "3.4.5"
44
description = "로컬 문서 검색 앱 - HWPX, Office, PDF 지원"
55
authors = ["Chris"]
66
edition = "2021"

src-tauri/src/application/container.rs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ pub struct AppContainer {
4646
vector_worker: Arc<RwLock<VectorWorker>>,
4747
tokenizer: OnceCell<Arc<dyn TextTokenizer>>,
4848
/// OCR 엔진 (PaddleOCR ONNX)
49-
ocr_engine: OnceCell<Arc<OcrEngine>>,
49+
/// embedder/vector_index 와 같은 이유로 `Arc<OnceCell>` 공유 — WatchManager 생성 시점에
50+
/// 아직 워밍업 전이라도, 이후 준비되면 WatchManager 가 매번 `.get()` 으로 최신 상태를
51+
/// 읽는다. Option 으로 캡처하면 생성 순서에 따라 OCR 이 영구히 꺼진 채로 굳는다.
52+
ocr_engine: Arc<OnceCell<Arc<OcrEngine>>>,
5053
/// 파일명 캐시 (Everything 스타일 빠른 검색)
5154
filename_cache: Arc<FilenameCache>,
5255
/// 배치 인덱싱 컨트롤러 (멀티 폴더 순차 실행 상태)
@@ -174,7 +177,7 @@ impl AppContainer {
174177
watch_manager: OnceCell::new(),
175178
vector_worker: Arc::new(RwLock::new(VectorWorker::new())),
176179
tokenizer: OnceCell::new(),
177-
ocr_engine: OnceCell::new(),
180+
ocr_engine: Arc::new(OnceCell::new()),
178181
filename_cache: Arc::new(FilenameCache::new()),
179182
batch_controller: Arc::new(BatchController::new()),
180183
indexing_cancel_flag: Arc::new(AtomicBool::new(false)),
@@ -248,9 +251,14 @@ impl AppContainer {
248251
/// IndexService 생성 - 공유된 vector_worker 사용
249252
pub fn index_service(&self) -> IndexService {
250253
let settings = self.get_settings();
251-
// OCR 엔진: ocr_enabled + 모델 파일 존재 시에만 전달
254+
// OCR 엔진: 준비된 경우에만 전달 — 여기서 blocking 초기화하지 않는다
255+
// (근거는 ocr_engine_if_ready 주석). 아직 워밍업 전이면 이번 인덱싱만 OCR 없이 진행.
252256
let ocr = if settings.ocr_enabled {
253-
self.get_ocr_engine().ok()
257+
let ready = self.ocr_engine_if_ready();
258+
if ready.is_none() {
259+
tracing::warn!("OCR 엔진이 아직 준비되지 않아 이번 인덱싱은 OCR 없이 진행합니다");
260+
}
261+
ready
254262
} else {
255263
None
256264
};
@@ -403,12 +411,11 @@ impl AppContainer {
403411
let watch_pause_handle = WatchPauseHandle::new();
404412
// 벡터 인덱싱은 AI RAG 전용 → incremental update 후 자동 벡터 트리거 비활성화
405413
let vector_trigger: Option<Arc<dyn Fn() + Send + Sync>> = None;
406-
// OCR 엔진: ocr_enabled + 모델 파일 존재 시에만 전달
407-
let ocr = if self.get_settings().ocr_enabled {
408-
self.get_ocr_engine().ok()
409-
} else {
410-
None
411-
};
414+
// OCR 엔진: OnceCell 을 **공유** — 여기서 blocking 초기화하지 않는다.
415+
// WatchManager 는 폴더 목록 조회(get_folders_with_info) 등 UI 경로가 함께 쓰는
416+
// OnceCell 이라, 여기서 OCR 빌드를 기다리면 폴더 트리가 통째로 안 뜬다(이슈 #35).
417+
// 공유이므로 생성 시점에 아직 워밍업 전이어도 준비되는 즉시 반영된다.
418+
let ocr = self.ocr_engine.clone();
412419
// 벡터/임베더 OnceCell을 **공유** — WatchManager 생성 시점에
413420
// OnceCell이 비어있어도, 이후 검색 등으로 init되면 WatchManager도
414421
// 매번 .get()으로 최신 상태를 읽는다 (orphan 벡터 방지)
@@ -458,6 +465,17 @@ impl AppContainer {
458465
.cloned()
459466
}
460467

468+
/// 이미 초기화된 OCR 엔진만 반환 — **절대 blocking 초기화하지 않는다**.
469+
///
470+
/// `get_ocr_engine` 은 OnceCell 초기화라, 초기화가 오래 걸리거나 멈추면 같은 OnceCell 을
471+
/// 기다리는 **모든** 호출자가 함께 멈춘다. OCR 은 ONNX 세션 빌드라 환경(내부망 EDR 의
472+
/// DLL 로드 검사 등)에 따라 수십 초 이상 걸릴 수 있어, UI 경로가 그 지연에 얽히면 앱이
473+
/// 멈춘 것처럼 보인다(이슈 #35). 워밍업은 setup 의 전용 스레드가 담당하고, 그 외에는
474+
/// 이 peek 으로 준비된 경우에만 사용한다.
475+
pub fn ocr_engine_if_ready(&self) -> Option<Arc<OcrEngine>> {
476+
self.ocr_engine.get().cloned()
477+
}
478+
461479
/// OCR 모델 사용 가능 여부
462480
pub fn is_ocr_available(&self) -> bool {
463481
let ocr_dir = self.models_dir.join("paddleocr");

src-tauri/src/indexer/manager.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ pub struct IndexContext {
7676
pub on_incremental_update: Option<Arc<dyn Fn(usize) + Send + Sync>>,
7777
/// 벡터 워커 트리거 콜백 (watcher 증분 인덱싱 후 벡터 백필 시작)
7878
pub on_vector_trigger: Option<Arc<dyn Fn() + Send + Sync>>,
79-
/// OCR 엔진 (이미지 파일 텍스트 인식)
80-
pub ocr_engine: Option<Arc<OcrEngine>>,
79+
/// OCR 엔진 (이미지 파일 텍스트 인식) — embedder/vector_index 와 같은 공유 OnceCell.
80+
/// WatchManager 생성 시점에 아직 워밍업 전이어도, 준비되는 즉시 `.get()` 으로 반영된다.
81+
pub ocr_engine: Arc<OnceCell<Arc<OcrEngine>>>,
8182
}
8283

8384
#[derive(Debug, Clone)]
@@ -529,7 +530,7 @@ impl WatchManager {
529530
.unwrap_or("")
530531
.to_lowercase();
531532
let is_ocr_image =
532-
ctx.ocr_engine.is_some() && OCR_IMAGE_EXTENSIONS.contains(&ext.as_str());
533+
ctx.ocr_engine.get().is_some() && OCR_IMAGE_EXTENSIONS.contains(&ext.as_str());
533534

534535
if SUPPORTED_EXTENSIONS.contains(&ext.as_str()) || is_ocr_image {
535536
// Modify 이벤트는 속성/메타데이터 변경만으로도 발생하므로,
@@ -539,7 +540,7 @@ impl WatchManager {
539540
tracing::debug!("[WatchManager] Unchanged, skip reparse: {:?}", path);
540541
continue;
541542
}
542-
let ocr_ref = ctx.ocr_engine.as_deref();
543+
let ocr_ref = ctx.ocr_engine.get().map(|a| a.as_ref());
543544
let vi_ref = ctx.vector_index.get().map(|a| a.as_ref());
544545
match pipeline::index_file_fts_only_no_tx(&conn, path, ocr_ref, vi_ref) {
545546
Ok(result) => {

src-tauri/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,35 @@ pub fn run() {
12221222
// Store app container
12231223
app.manage(RwLock::new(container));
12241224

1225+
// OCR 엔진 워밍업 — ONNX 세션 빌드를 여기서 **한 번만** 끝내둔다.
1226+
// 다른 경로(get_watch_manager·index_service)는 blocking 초기화 대신
1227+
// ocr_engine_if_ready peek 을 쓰므로, 이 빌드가 지연되거나 멈춰도 폴더 트리·검색·
1228+
// 인덱싱은 그대로 동작한다(스캔 문서 OCR 만 비활성). 컨테이너 락은 read 전용
1229+
// (쓰는 곳이 없음)이라 오래 잡아도 다른 커맨드를 굶기지 않는다.
1230+
if setup_settings.ocr_enabled {
1231+
let app_handle = app.handle().clone();
1232+
std::thread::spawn(move || {
1233+
let Some(container_state) = app_handle.try_state::<RwLock<AppContainer>>()
1234+
else {
1235+
return;
1236+
};
1237+
let Ok(container) = container_state.read() else {
1238+
return;
1239+
};
1240+
let started = std::time::Instant::now();
1241+
match container.get_ocr_engine() {
1242+
Ok(_) => tracing::info!(
1243+
"OCR 엔진 워밍업 완료 ({}ms)",
1244+
started.elapsed().as_millis()
1245+
),
1246+
Err(e) => tracing::warn!(
1247+
"OCR 엔진 워밍업 실패 — 스캔 문서 OCR 만 비활성됩니다: {}",
1248+
e
1249+
),
1250+
}
1251+
});
1252+
}
1253+
12251254
// 감시 폴더 자동 복원 (위에서 이동) — OCR 엔진 빌드가 창 표시를 막지 않도록 분리.
12261255
{
12271256
let app_handle = app.handle().clone();

src-tauri/src/ocr/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ impl OcrEngine {
130130
.map(|n| n.get().clamp(2, 4))
131131
.unwrap_or(2);
132132

133+
// 단계별 로그 — 환경에 따라 ONNX 세션 빌드(= ONNX Runtime 동적 라이브러리 로드) 에서
134+
// 멈추는 사례가 있어(이슈 #35), 어느 단계에서 걸렸는지 로그만으로 판별되게 한다.
135+
tracing::info!("OCR 초기화 [1/3] det 세션 생성 시작 (ONNX Runtime 로드)");
136+
133137
let det_session = Session::builder()
134138
.map_err(|e| OcrError::ModelLoad(e.to_string()))?
135139
.with_execution_providers([ort::ep::CPU::default().build()])
@@ -141,6 +145,8 @@ impl OcrEngine {
141145
.commit_from_file(&det_path)
142146
.map_err(|e| OcrError::ModelLoad(format!("Detection session: {}", e)))?;
143147

148+
tracing::info!("OCR 초기화 [2/3] det 세션 완료 — rec 세션 생성 시작");
149+
144150
let rec_session = Session::builder()
145151
.map_err(|e| OcrError::ModelLoad(e.to_string()))?
146152
.with_execution_providers([ort::ep::CPU::default().build()])
@@ -152,6 +158,8 @@ impl OcrEngine {
152158
.commit_from_file(&rec_path)
153159
.map_err(|e| OcrError::ModelLoad(format!("Recognition session: {}", e)))?;
154160

161+
tracing::info!("OCR 초기화 [3/3] rec 세션 완료 — 사전 로드");
162+
155163
// 사전 로드
156164
let dict_content = std::fs::read_to_string(&dict_path)
157165
.map_err(|e| OcrError::ModelLoad(format!("Dictionary read: {}", e)))?;

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Anything",
4-
"version": "3.4.4",
4+
"version": "3.4.5",
55
"identifier": "com.anything.app",
66
"build": {
77
"beforeDevCommand": "pnpm dev",

0 commit comments

Comments
 (0)