fix: select supported CPU compute devices for modern Vision OCR
This commit is contained in:
1 parent
96255a37df
commit
baa6cb22c9
5 files changed
+122
-7
No files matched your search
@@ -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
|
||||
|
||||
Generated
+1
@@ -4472,6 +4472,7 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-ml",
|
||||
"objc2-foundation",
|
||||
"objc2-image-io",
|
||||
"objc2-vision",
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user