use std::io::Read;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct File {
pub name: &'static str,
pub size: u64,
pub sha256: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Model {
pub repo: &'static str,
pub revision: &'static str,
pub files: &'static [File],
}
const ORG: &str = "PaddlePaddle";
impl Model {
pub fn url(&self, file: &File) -> String {
format!(
"https://huggingface.co/{ORG}/{}/resolve/{}/{}",
self.repo, self.revision, file.name
)
}
pub fn dir(&self, root: &Path) -> PathBuf {
root.join(self.repo)
}
pub fn present(&self, root: &Path) -> bool {
let dir = self.dir(root);
self.files.iter().all(|f| {
std::fs::metadata(dir.join(f.name)).is_ok_and(|m| m.is_file() && m.len() == f.size)
})
}
pub fn verify(&self, root: &Path) -> Result<(), String> {
let dir = self.dir(root);
for f in self.files {
let p = dir.join(f.name);
let mut hash = Hasher::new(f);
let mut file = std::fs::File::open(&p).map_err(|e| format!("{}: {e}", p.display()))?;
let mut buf = vec![0u8; READ_CHUNK];
loop {
let n = file
.read(&mut buf)
.map_err(|e| format!("{}: {e}", p.display()))?;
if n == 0 {
break;
}
hash.update(&buf[..n]);
}
hash.finish().map_err(|e| format!("{}: {e}", p.display()))?;
}
Ok(())
}
}
const READ_CHUNK: usize = 1 << 20;
pub struct Hasher<'a> {
file: &'a File,
sha: Sha256,
size: u64,
}
impl<'a> Hasher<'a> {
pub fn new(file: &'a File) -> Self {
Self {
file,
sha: Sha256::new(),
size: 0, }
}
pub fn update(&mut self, bytes: &[u8]) {
self.sha.update(bytes);
self.size += bytes.len() as u64;
}
pub fn finish(self) -> Result<(), String> {
if self.size != self.file.size {
return Err(format!("{} bytes, expected {}", self.size, self.file.size));
}
let got: String = self
.sha
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect();
if got != self.file.sha256 {
return Err(format!("SHA-256 is {got}, expected {}", self.file.sha256));
}
Ok(())
}
}
pub fn check(f: &File, bytes: &[u8]) -> Result<(), String> {
let mut h = Hasher::new(f);
h.update(bytes);
h.finish()
}
pub fn cache_root() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CACHE_HOME")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))?;
Some(base.join("pagesift").join("models"))
}
pub const SMALL_DET: Model = Model {
repo: "PP-OCRv6_small_det_safetensors",
revision: "eae2ee920a39fb3087637d3dbb58df1896ec1f24",
files: &[
File {
name: "model.safetensors",
size: 9_938_124, sha256: "89a96a8adc4e9cd0c994098edc76022e496d35844392562b4694c8fbc583f2da",
},
File {
name: "config.json",
size: 1_096, sha256: "b42d76ab6325d5234e1afc9987b73dc592b0ec8437c8a29d46ba6209b79252eb",
},
File {
name: "preprocessor_config.json",
size: 838, sha256: "74421569a28a78d417db320f9bc039ce0997a1defe2b35f259dfc74299c9f1ed",
},
],
};
pub const SMALL_REC: Model = Model {
repo: "PP-OCRv6_small_rec_safetensors",
revision: "fe049fb103f57443fe8840c54ed06b702f3c1de5",
files: &[
File {
name: "model.safetensors",
size: 21_204_736, sha256: "f65a332afe5aa663f0b9d5706f4ae8457b5b4058a842d5c1eb22df505c27d642",
},
File {
name: "config.json",
size: 1_083, sha256: "8693fd8485e8543e13d0f6dde3891a3f4af9a47fc4e3a391fbed71f8015c899e",
},
File {
name: "preprocessor_config.json",
size: 206_177, sha256: "2b24fa36f548893f26a931cf44f1a1a6b2b14ed7e2f8f8e28e6848801a8278db",
},
],
};
pub const MEDIUM_DET: Model = Model {
repo: "PP-OCRv6_medium_det_safetensors",
revision: "4236c2b61741a259c091fd879dcc4edc339e916c",
files: &[
File {
name: "model.safetensors",
size: 88_020_412, sha256: "bd393266c02e1a680b1b34c301d5d0d81e6290440b7f8ab0f5d5032276b17eb1",
},
File {
name: "config.json",
size: 2_134, sha256: "3ea7e760f64255c152ba139e2f5f798303f7189b87c923cf498e2b2e830cadd3",
},
File {
name: "preprocessor_config.json",
size: 838, sha256: "74421569a28a78d417db320f9bc039ce0997a1defe2b35f259dfc74299c9f1ed",
},
],
};
pub const MEDIUM_REC: Model = Model {
repo: "PP-OCRv6_medium_rec_safetensors",
revision: "024cad6a831de75c2c3c26e711ba8c4a82ccd24b",
files: &[
File {
name: "model.safetensors",
size: 76_741_720, sha256: "5f43c16f2a684b1d2284662178bdb604febd3d6bfdb5ca73828d08d0f7c0c3e9",
},
File {
name: "config.json",
size: 1_131, sha256: "aa5f351b4a7df95cc4953f013d53a6d44205f1bb0aee0ec7eb243d1d8fae1c27",
},
File {
name: "preprocessor_config.json",
size: 206_177, sha256: "2b24fa36f548893f26a931cf44f1a1a6b2b14ed7e2f8f8e28e6848801a8278db",
},
],
};
pub const FORMULA: Model = Model {
repo: "PP-FormulaNet_plus-L_safetensors",
revision: "6cc004a8e91ea3f3819b05317f40b290cbe99c2b",
files: &[
File {
name: "model.safetensors",
size: 727_633_048, sha256: "582848cf9b7066bb1cf3166ad75a476da492c3661ffa61bb7439f9fd098bde13",
},
File {
name: "config.json",
size: 1_364, sha256: "3d5c513a525ebef73ba4e319e65de793d95aacc1062e5ce2dd851c7f109c060c",
},
File {
name: "generation_config.json",
size: 186, sha256: "e0dc49736b0b06f19cb2074201a072cee23707b7abff4a55a24da19b14abd707",
},
File {
name: "processor_config.json",
size: 584, sha256: "f1454999fcb5930c2d93e9dcd03f1adf046245e98e96a7f601dabc6ee01c3b78",
},
File {
name: "tokenizer.json",
size: 2_140_014, sha256: "2811d82701ec97c192fa256aa2b4516929373870ae660326cc5b1dc879b95ff2",
},
File {
name: "tokenizer_config.json",
size: 4_484, sha256: "0bf3cca5ff1a240004932dbab09373b0e8a651f5f85f2ff6f2f9dec3f0e41947",
},
],
};
pub const LAYOUT: Model = Model {
repo: "PP-DocLayoutV3_safetensors",
revision: "e4f489dc536556fc1be02b973ba06756e0d4a2ac",
files: &[
File {
name: "model.safetensors",
size: 133_270_468, sha256: "5ea422c6cc5fe759a47e1357c35639b58173508e025a3131cbe4b6ac59e2b85e",
},
File {
name: "config.json",
size: 2_460, sha256: "3cf834b91d23a756b1519bce4db42c09e852f3e35c35092dd5a3e253a50c071a",
},
File {
name: "inference.yml",
size: 1_482, sha256: "506fcfac13b3b546ae40d7886b44126420f392adb694e3f8bb6a6286a1f90fdc",
},
File {
name: "preprocessor_config.json",
size: 575, sha256: "519fe0187a43a1ca429e3ad8317bab8700f0d5e8fb3a6e3a0a413ffac078ba42",
},
],
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cached_models_verify() {
let Some(root) = cache_root() else { return };
for m in [
SMALL_DET, SMALL_REC, MEDIUM_DET, MEDIUM_REC, FORMULA, LAYOUT,
] {
if m.dir(&root).join("model.safetensors").exists() {
m.verify(&root).unwrap();
}
}
}
#[test]
fn rejects_wrong_bytes() {
let f = &SMALL_DET.files[1];
assert!(check(f, b"{}").is_err());
}
}