Phase 6.1 — Vision OCR + ElementMatcher: LLM 추정 ~70% → deterministic 99%
사용자 dogfooding이 명확히 보여줬다: LLM target_text는 perfect (CLAUDE.md 정확), coordinates는 ~70% 빗나감. Phase 6.1이 그 gap을 OCR + fuzzy matching으로 메움. 정책 swap back lock-in — 본질 '99% 좌표는 OCR이 source' 일관.
AI 버전
사용자 두 번 dogfooding 자료가 명확:
- 첫 시도 (Phase 4.2): "정확한 위치 못 찾았어요" 에러 — LLM이 SYSTEM_PROMPT "fallback only" 룰 너무 잘 따라
coordinates생략. → v0.1 임시 fix (강제). - 두 번째 시도 (Phase 4.2 fix 후): 박스 떴음 + 빗나감 —
target_text="CLAUDE.md"perfect, coords ~70%.
→ Phase 6.1의 exactly 그 gap을 메움.
흐름 — OCR + ElementMatcher
capture (PNG) → DisplayGeometry (geometry + sentSize)
↓
async let 병렬:
- dispatcher: Gemini → AnalysisResult (target_text="CLAUDE.md")
- OCR: VNRecognizeTextRequest .accurate ko-KR+en-US → [OCRBox]
↓
ElementMatcher.match(targetText, candidates, geometry):
1. case-insensitive substring (가장 짧은 = 가장 specific)
2. fail → Levenshtein normalized similarity ≥ 0.7
↓
matchedRect: CGRect? (screen-local logical pt)
↓
AppDelegate 3-tier fallback:
1. matchedRect (OCR 99%)
2. result.coordinates LLM 추정 (~70% fallback)
3. 한국어 에러 "찾지 못했어요"Vision OCR Y-flip (R8 — Tauri Layer 6 유사 함정)
VNRecognizedTextObservation.boundingBox: normalized 0..1, bottom-left origin (Core Graphics 표준).
sent image: top-left origin (DisplayGeometry 좌표계).
→ 변환 한 곳:
let bb = obs.boundingBox
let rect = CGRect(
x: bb.minX * w,
y: (1.0 - bb.maxY) * h, // ← Y-flip 핵심
width: bb.width * w,
height: bb.height * h
)bb.maxY 사용 이유: bottom-left bbox는 위쪽 모서리가 maxY (높은 y). top-left에선 위쪽 모서리가 작은 y. 1 - bb.maxY → top-left y.
여러 곳에 흩어지면 top-left/bottom-left mix 함정 — Tauri 시절 Layer 6 유사. OCRService 안 한 곳에서.
ElementMatcher — substring → Levenshtein escalate
Substring 매칭 우선 (실측 "CLAUDE.md" 대부분 hit) + fuzzy fallback:
// 1. substring (가장 짧은 = 가장 specific)
let substringMatches = candidates.filter { normalize($0.text).contains(normalizedTarget) }
if let best = substringMatches.min(by: { normalize($0.text).count < normalize($1.text).count }) {
return geometry.logicalRectFromSentBox(toIntBox(best.rectInSentImage))
}
// 2. fuzzy
let scored = candidates.compactMap { (box, similarity(normalize(box.text), normalizedTarget)) }
.filter { $0.1 >= threshold }
guard let best = scored.max(by: { $0.1 < $1.1 }) else { return nil }Specific 우선 (test로 lock): "Sign in" 검색 시 "Sign in" 자체가 "Sign in with Google"보다 우선. 가장 짧은 매칭 박스 = 가장 specific.
Levenshtein 0.7: "CLAUDE.md" ↔ "CLAUDE.txt" = 0.78 매칭 (OCR이 .txt로 잘못 인식해도). "CLAUDE.md" ↔ "totally different" = 0.0 거부.
11 unit tests로 lock.
R9 lock-in swap back — 본질 일관성
Phase 4.2 fix에서 v0.1 임시 coordinates 강제. DECISIONS에 명시:
Phase 6.1 commit 시점에 반드시 swap 다시 — 안 그러면 본질 "99% 좌표는 OCR이 source" 자기 모순.
지킴:
- Prompts.swift coordinates 룰: "반드시 줘 (v0.1)" → "fallback only — 평소엔 키 생략. backend OCR가 99% deterministic"
- GeminiDispatcher responseSchema
required에서coordinates제거 - GeminiDispatcherTests 2 tests
requiredset swap back
verify workflow가 자기 모순 잡을 패턴 사전 차단. 본질 정확히 일관.
AppDelegate 3-tier fallback
case .done(let result, let geometry, let matchedRect):
if let rect = matchedRect {
hud.presentAnnotation(rect, on: screen) // 1. OCR 99%
return
}
if let coords = result.coordinates,
let local = geometry.logicalRectFromSentBox(coords) {
hud.presentAnnotation(local, on: screen) // 2. LLM ~70% fallback
return
}
hud.presentError("\"\(result.targetText)\"을(를) 화면에서 못 찾았어요", on: screen) // 3.LLM coords fallback 유지 이유: OCR이 visible text 못 잡는 경우 (아이콘만 / 가려진 텍스트 / OCR 인식 실패) 마지막 보루.
68 tests, build 3.86s, test 0.214s
6 AnalysisResult + 4 Prompts + 8 Env + 6 GeminiDispatcher
+ 9 DisplayGeometry + 8 HUDOverlayWindow + 2 HUDController
+ 6 AnalyzeCoordinator (OCR matched / no-match 추가)
+ 8 UserMessage + 11 ElementMatcher = 68/68 passElementMatcher tests:
- substring 매칭 — CLAUDE.md
- case-insensitive — claude.md → CLAUDE.md
- partial — Settings in "Open Settings"
- specific 우선 — Sign in vs Sign in with Google
- fuzzy — CLAUDE.md vs CLAUDE.txt (Levenshtein 0.78)
- threshold 미만 → nil
- custom threshold 0.5 vs 0.7
- 빈 target → nil
- 빈 candidates → nil
- whitespace normalize
- levenshtein basics + similarity bounds
Build warning (non-fatal)
VNRecognizeTextRequest.supportedRecognitionLanguages(for:revision:) macOS 12+ deprecated — instance method request.supportedRecognitionLanguages() 권장. Phase 6.1 cleanup으로 분리 (functional 영향 X).
사용자 검증 — 이번엔 정확해야
./dev.sh 새 binary로:
1. ⌥+Space → trigger panel
2. "CLAUDE.md 찾아줘" + Analyze
3. 빨간 박스가 *정확히* CLAUDE.md 사이드바 항목 위에 ← OCR deterministic
4. log: [match] substring hit — target="CLAUDE.md" box="CLAUDE.md"
[analyze] complete N.Ns OCR-matched
5. ⌥+Space → dismiss빗나가면 — Levenshtein threshold 튜닝 또는 OCR fixture 자료로 디버그.
다음
Phase 5.x — HUDOverlayView bubble (한글 next_action 박스 옆). target_text ("CLAUDE.md") + next_action ("여기 CLAUDE.md 보이죠? 누르세요") 둘 다 visible. 화면 가장자리 clamping (박스가 화면 우측 끝이면 bubble 좌측에).
Phase 6.1 commit 후에도 adversarial verify workflow 권장 — Y-flip 정확성 / threshold 튜닝 / OCR 다국어 edge case / 매칭 알고리즘 false positive.
리뷰 필요
내 시각이 아직 안 들어간 entry.