car-ffi-common 0.19.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
Documentation
//! FFI wrappers for `car-vision` (Apple Vision.framework OCR).
//!
//! JSON in / JSON out, mirroring the rest of `car-ffi-common`.

use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use car_vision::ocr::{self, OcrConfig};
use car_vision::Observation;

#[derive(Debug, Deserialize)]
pub struct OcrArgs {
    pub image_path: String,
    #[serde(default)]
    pub fast_path: bool,
    #[serde(default)]
    pub languages: Vec<String>,
    #[serde(default = "default_true")]
    pub language_correction: bool,
    #[serde(default)]
    pub minimum_text_height: f32,
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Serialize)]
pub struct OcrResponseJson {
    pub available: bool,
    pub observations: Vec<Observation>,
}

/// Run on-device OCR over an image file. Returns
/// `{"available": bool, "observations": [...]}`.
///
/// `available` is false when the Vision shim wasn't built into this
/// binary (non-macOS host, or build skipped); in that case
/// `observations` is empty rather than an error so callers can
/// distinguish "OCR unavailable here" from "OCR ran and found no
/// text."
pub async fn ocr(args_json: &str) -> Result<String, String> {
    let args: OcrArgs =
        serde_json::from_str(args_json).map_err(|e| format!("invalid args: {e}"))?;

    let config = OcrConfig {
        fast_path: args.fast_path,
        languages: args.languages,
        language_correction: args.language_correction,
        minimum_text_height: args.minimum_text_height,
    };
    let path = PathBuf::from(&args.image_path);

    let response = if !car_vision::is_available() {
        OcrResponseJson {
            available: false,
            observations: Vec::new(),
        }
    } else {
        // The shim is sync and CPU/Neural-Engine bound; hop to a
        // blocking thread so we don't stall an async runtime.
        let observations = tokio::task::spawn_blocking(move || ocr::recognize(&path, &config))
            .await
            .map_err(|e| format!("ocr task panicked: {e}"))?
            .map_err(|e| format!("{e}"))?;
        OcrResponseJson {
            available: true,
            observations,
        }
    };
    serde_json::to_string(&response).map_err(|e| format!("serialize: {e}"))
}