oxedyne/daimond/src/wasm/ocr.rs
3.7 KiB, 1 run
created by r2519314175:1512, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! `window.DaimondOcr` edge -- the authenticated POSTs that turn a PDF or an image into text. |
| 2 | //! |
| 3 | //! A thin binding, like `doc.rs`: the JS driver in `www/js/ocr.js` owns the provider keys and |
| 4 | //! the two endpoints (OpenRouter's `file-parser` chat for PDFs, Mistral's `/v1/ocr` for images, |
| 5 | //! with a vision model as the image fallback), and this side hands it a request body and reads |
| 6 | //! back the raw response. The DECISIONS that matter -- which engine, is the text empty, what did |
| 7 | //! it cost -- are made and tested in Rust (`src/tools.rs`); the driver only carries the request. |
| 8 | //! |
| 9 | //! Why the key never crosses this edge: the driver resolves it from `DaimondModels`, which keeps |
| 10 | //! a minted credits key in memory alone and seals a typed one at rest. A body in and a body out |
| 11 | //! is all that passes, so nothing here has to hold a bearer credential. |
| 12 | |
| 13 | use crate::llm::extract_json_string; |
| 14 | use crate::wasm::js_str; |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | |
| 18 | use wasm_bindgen::prelude::wasm_bindgen; |
| 19 | use wasm_bindgen::{JsCast, JsValue}; |
| 20 | use wasm_bindgen_futures::JsFuture; |
| 21 | |
| 22 | |
| 23 | #[wasm_bindgen] |
| 24 | extern "C" { |
| 25 | |
| 26 | /// The driver object `www/js/ocr.js` installs at `window.DaimondOcr`. |
| 27 | #[wasm_bindgen(js_name = DaimondOcr)] |
| 28 | type Driver; |
| 29 | |
| 30 | /// POST one `file-parser` chat body to OpenRouter; resolve the raw response text. |
| 31 | #[wasm_bindgen(method)] |
| 32 | fn chat(this: &Driver, body: &str) -> js_sys::Promise; |
| 33 | |
| 34 | /// Transcribe an image: try Mistral OCR, fall back to a vision model. Resolves a JSON |
| 35 | /// string `{"route":"mistral"|"vision","body":<raw response>}`. |
| 36 | #[wasm_bindgen(method)] |
| 37 | fn image(this: &Driver, mistral_body: &str, vision_body: &str) -> js_sys::Promise; |
| 38 | } |
| 39 | |
| 40 | |
| 41 | /// Reach the driver on `window`, or refuse in the model's language. |
| 42 | fn driver() -> Outcome<Driver> { |
| 43 | let win = res!(web_sys::window() |
| 44 | .ok_or_else(|| err!("Reading text off a file needs a browser window."; System, Missing))); |
| 45 | let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondOcr")) |
| 46 | .map_err(|e| err!("Reading window.DaimondOcr failed: {}.", js_str(&e); System, Missing))); |
| 47 | if obj.is_undefined() || obj.is_null() { |
| 48 | return Err(err!( |
| 49 | "Daimond's OCR bridge is not loaded in this page, so a PDF's or a picture's text \ |
| 50 | cannot be read here. Tell the user, and describe the file instead."; System, Missing)); |
| 51 | } |
| 52 | Ok(obj.unchecked_into::<Driver>()) |
| 53 | } |
| 54 | |
| 55 | /// The `message` of a rejected JS `Error`, verbatim: the driver writes its refusals for the |
| 56 | /// model to read and act on, so nothing here rewords one. |
| 57 | fn refusal(e: &JsValue) -> String { |
| 58 | match js_sys::Reflect::get(e, &JsValue::from_str("message")) { |
| 59 | Ok(m) => m.as_string().unwrap_or_else(|| js_str(e)), |
| 60 | Err(_) => js_str(e), |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | /// POST a `file-parser` chat body to OpenRouter and return the raw response body. |
| 65 | pub async fn openrouter_chat(body: &str) -> Outcome<String> { |
| 66 | let d = res!(driver()); |
| 67 | match JsFuture::from(d.chat(body)).await { |
| 68 | Ok(v) => Ok(v.as_string().unwrap_or_default()), |
| 69 | Err(e) => Err(err!("{}", refusal(&e); IO, Invalid)), |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | /// Transcribe an image, returning `(route, raw_response_body)` where `route` is `"mistral"` or |
| 74 | /// `"vision"` so the caller parses the body in the right shape. |
| 75 | pub async fn image_ocr(mistral_body: &str, vision_body: &str) -> Outcome<(String, String)> { |
| 76 | let d = res!(driver()); |
| 77 | let v = match JsFuture::from(d.image(mistral_body, vision_body)).await { |
| 78 | Ok(v) => v.as_string().unwrap_or_default(), |
| 79 | Err(e) => return Err(err!("{}", refusal(&e); IO, Invalid)), |
| 80 | }; |
| 81 | let route = extract_json_string(&v, "route").unwrap_or_default(); |
| 82 | let body = extract_json_string(&v, "body").unwrap_or_default(); |
| 83 | Ok((route, body)) |
| 84 | } |