Oregami
Repositories/oxedyne/daimond

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
13use crate::llm::extract_json_string;
14use crate::wasm::js_str;
15
16use oxedyne_fe2o3_core::prelude::*;
17
18use wasm_bindgen::prelude::wasm_bindgen;
19use wasm_bindgen::{JsCast, JsValue};
20use wasm_bindgen_futures::JsFuture;
21
22
23#[wasm_bindgen]
24extern "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.
42fn 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.
57fn 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.
65pub 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.
75pub 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}