fix: select supported CPU compute devices for modern Vision OCR

This commit is contained in:
Codex committed 2026-10-12 12:25:46 +00:00
1 parent 96255a37df
commit baa6cb22c9
5 files changed
+122 -7

No files matched your search

+19
View File
@@ -31,6 +31,7 @@ jobs:
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
- name: Cache current macOS OCR dependencies
id: ocr-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
@@ -51,6 +52,24 @@ jobs:
run: |
cargo test --manifest-path src-tauri/Cargo.toml ocr:: -- --nocapture
- name: Diagnose native Vision if OCR fails
if: failure()
run: |
fixture_dir="$RUNNER_TEMP/ttree-ocr-diagnostics"
mkdir -p "$fixture_dir"
swift src-tauri/src/ocr/tests/generate-fixtures.swift "$fixture_dir"
swift src-tauri/src/ocr/tests/diagnose-vision.swift "$fixture_dir/english.png"
- name: Save compiled OCR dependencies
if: always() && steps.ocr-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: macos-27-ocr-${{ hashFiles('src-tauri/Cargo.lock') }}
verify:
needs: ocr-current-macos
runs-on: macos-14
+1
View File
@@ -4472,6 +4472,7 @@ dependencies = [
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-ml",
"objc2-foundation",
"objc2-image-io",
"objc2-vision",
+2
View File
@@ -38,6 +38,7 @@ core-foundation = "0.10"
security-framework = "3"
objc2 = "0.6"
objc2-core-foundation = "0.3"
objc2-core-ml = { version = "0.3", features = ["MLCPUComputeDevice", "MLComputeDeviceProtocol"] }
objc2-image-io = { version = "0.3", features = ["CGImageSource"] }
objc2-app-kit = { version = "0.3", features = [
"NSPasteboard",
@@ -62,6 +63,7 @@ objc2-foundation = { version = "0.3", features = [
] }
objc2-vision = { version = "0.3", features = [
"objc2-core-graphics",
"objc2-core-ml",
"VNRequest",
"VNRecognizeTextRequest",
"VNObservation",
@@ -0,0 +1,62 @@
// CI-only comparison against Apple's minimal Vision request; synthetic text only.
import Foundation
import ImageIO
import Vision
import CoreML
let file = URL(fileURLWithPath: CommandLine.arguments[1])
let data = try Data(contentsOf: file)
let source = CGImageSourceCreateWithData(data as CFData, nil)!
let image = CGImageSourceCreateImageAtIndex(source, 0, nil)!
print("Vision default revision: \(VNRecognizeTextRequest.defaultRevision)")
print("Vision supported revisions: \(VNRecognizeTextRequest.supportedRevisions)")
func assignCPU(_ request: VNRecognizeTextRequest) throws {
if #available(macOS 14.0, *) {
let stages = try request.supportedComputeStageDevices()
for (stage, devices) in stages {
print("compute stage \(stage): \(devices)")
if let cpu = devices.first(where: { $0 is MLCPUComputeDevice }) {
request.setComputeDevice(cpu, for: stage)
}
}
}
}
let profiles: [(String, (VNRecognizeTextRequest) throws -> Void)] = [
("apple-default", { _ in }),
("default-explicit-cpu", { try assignCPU($0) }),
("rev3-english-cpu", { request in
request.revision = 3
request.recognitionLanguages = ["en-US"]
try assignCPU(request)
}),
("rev3-chinese-english-cpu", { request in
request.revision = 3
request.recognitionLanguages = ["zh-Hans", "zh-Hant", "en-US"]
try assignCPU(request)
}),
("rev2-chinese-english-cpu", { request in
request.revision = 2
request.recognitionLanguages = ["zh-Hans", "zh-Hant", "en-US"]
try assignCPU(request)
}),
("fast-english-cpu", { request in
request.recognitionLevel = .fast
request.recognitionLanguages = ["en-US"]
try assignCPU(request)
}),
]
for (name, configure) in profiles {
do {
let request = VNRecognizeTextRequest()
try configure(request)
let handler = VNImageRequestHandler(cgImage: image, options: [:])
try handler.perform([request])
let text = request.results?.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n") ?? ""
print("\(name): OK \(text)")
} catch {
let error = error as NSError
print("\(name): ERROR domain=\(error.domain) code=\(error.code) details=\(error.userInfo)")
}
}
+38 -7
View File
@@ -6,9 +6,10 @@
#![cfg(target_os = "macos")]
use objc2::rc::{autoreleasepool, Retained};
use objc2::AnyThread;
use objc2::{AnyThread, ClassType};
use objc2_core_graphics::CGImage;
use objc2_foundation::{NSArray, NSString};
use objc2_core_ml::MLCPUComputeDevice;
use objc2_foundation::{NSArray, NSObjectProtocol, NSString};
use objc2_vision::{VNImageRequestHandler, VNRecognizeTextRequest, VNRequestTextRecognitionLevel};
/// 对指定图片文件路径执行 OCR,返回识别文本(按行拼接)。
@@ -85,11 +86,6 @@ fn configure_request(request: &VNRecognizeTextRequest, compatibility: bool) -> R
unsafe {
request.setRevision(revision);
request.setPreferBackgroundProcessing(true);
if compatibility {
// macOS 11 也必须支持回退,因此保留兼容的 CPU-only API。
#[allow(deprecated)]
request.setUsesCPUOnly(true);
}
}
if objc2::available!(macos = 13.0) {
request.setAutomaticallyDetectsLanguage(!compatibility);
@@ -116,6 +112,41 @@ fn configure_request(request: &VNRecognizeTextRequest, compatibility: bool) -> R
if !languages.is_empty() {
request.setRecognitionLanguages(&NSArray::from_retained_slice(&languages));
}
// 新系统后台 OCR 不能依赖 ANE 自动选择。旧 usesCPUOnly 属性在新
// Vision 上不足以保证 CPU 执行,必须按模型支持的计算阶段显式选择。
if compatibility || objc2::available!(macos = 27.0) {
configure_cpu(request)?;
}
Ok(())
}
fn configure_cpu(request: &VNRecognizeTextRequest) -> Result<(), String> {
// SAFETY: 新计算设备 API 和 MLCPUComputeDevice 从 macOS 14 才可用。
unsafe {
if objc2::available!(macos = 14.0) {
let stages = request
.supportedComputeStageDevicesAndReturnError()
.map_err(|error| format!("读取 OCR 计算设备失败: {error}"))?;
let mut assigned = false;
for stage in stages.allKeys().iter() {
if let Some(devices) = stages.objectForKey(stage) {
if let Some(cpu) = devices
.iter()
.find(|device| device.isKindOfClass(MLCPUComputeDevice::class()))
{
request.setComputeDevice_forComputeStage(Some(cpu), stage);
assigned = true;
}
}
}
if !assigned {
return Err("此 OCR 模型没有可用的 CPU 计算设备".to_string());
}
} else {
#[allow(deprecated)]
request.setUsesCPUOnly(true);
}
}
Ok(())
}