use crate::error::{AprFormatError, Result};
use crate::model_card::ModelCard;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const MAGIC: [u8; 4] = [0x41, 0x50, 0x52, 0x4E];
pub const FORMAT_VERSION: (u8, u8) = (1, 0);
pub const HEADER_SIZE: usize = 32;
pub const MAX_UNCOMPRESSED_SIZE: u32 = 1024 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u16)]
pub enum ModelType {
LinearRegression = 0x0001,
LogisticRegression = 0x0002,
DecisionTree = 0x0003,
RandomForest = 0x0004,
GradientBoosting = 0x0005,
KMeans = 0x0006,
Pca = 0x0007,
NaiveBayes = 0x0008,
Knn = 0x0009,
Svm = 0x000A,
NgramLm = 0x0010,
Tfidf = 0x0011,
CountVectorizer = 0x0012,
NeuralSequential = 0x0020,
NeuralCustom = 0x0021,
ContentRecommender = 0x0030,
MixtureOfExperts = 0x0040,
Custom = 0x00FF,
}
impl ModelType {
#[must_use]
pub fn from_u16(value: u16) -> Option<Self> {
match value {
0x0001 => Some(Self::LinearRegression),
0x0002 => Some(Self::LogisticRegression),
0x0003 => Some(Self::DecisionTree),
0x0004 => Some(Self::RandomForest),
0x0005 => Some(Self::GradientBoosting),
0x0006 => Some(Self::KMeans),
0x0007 => Some(Self::Pca),
0x0008 => Some(Self::NaiveBayes),
0x0009 => Some(Self::Knn),
0x000A => Some(Self::Svm),
0x0010 => Some(Self::NgramLm),
0x0011 => Some(Self::Tfidf),
0x0012 => Some(Self::CountVectorizer),
0x0020 => Some(Self::NeuralSequential),
0x0021 => Some(Self::NeuralCustom),
0x0030 => Some(Self::ContentRecommender),
0x0040 => Some(Self::MixtureOfExperts),
0x00FF => Some(Self::Custom),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum Compression {
None = 0x00,
#[default]
ZstdDefault = 0x01,
ZstdMax = 0x02,
Lz4 = 0x03,
}
impl Compression {
#[must_use]
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0x00 => Some(Self::None),
0x01 => Some(Self::ZstdDefault),
0x02 => Some(Self::ZstdMax),
0x03 => Some(Self::Lz4),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Flags(u8);
impl Flags {
pub const ENCRYPTED: u8 = 0b0000_0001;
pub const SIGNED: u8 = 0b0000_0010;
pub const STREAMING: u8 = 0b0000_0100;
pub const LICENSED: u8 = 0b0000_1000;
pub const TRUENO_NATIVE: u8 = 0b0001_0000;
pub const QUANTIZED: u8 = 0b0010_0000;
pub const HAS_MODEL_CARD: u8 = 0b0100_0000;
#[must_use]
pub fn new() -> Self {
Self(0)
}
#[must_use]
pub fn with_encrypted(mut self) -> Self {
self.0 |= Self::ENCRYPTED;
self
}
#[must_use]
pub fn with_signed(mut self) -> Self {
self.0 |= Self::SIGNED;
self
}
#[must_use]
pub fn with_streaming(mut self) -> Self {
self.0 |= Self::STREAMING;
self
}
#[must_use]
pub fn with_licensed(mut self) -> Self {
self.0 |= Self::LICENSED;
self
}
#[must_use]
pub fn with_trueno_native(mut self) -> Self {
self.0 |= Self::TRUENO_NATIVE;
self
}
#[must_use]
pub fn with_quantized(mut self) -> Self {
self.0 |= Self::QUANTIZED;
self
}
#[must_use]
pub fn with_model_card(mut self) -> Self {
self.0 |= Self::HAS_MODEL_CARD;
self
}
#[must_use]
pub fn is_encrypted(self) -> bool {
self.0 & Self::ENCRYPTED != 0
}
#[must_use]
pub fn is_signed(self) -> bool {
self.0 & Self::SIGNED != 0
}
#[must_use]
pub fn is_streaming(self) -> bool {
self.0 & Self::STREAMING != 0
}
#[must_use]
pub fn is_licensed(self) -> bool {
self.0 & Self::LICENSED != 0
}
#[must_use]
pub fn is_trueno_native(self) -> bool {
self.0 & Self::TRUENO_NATIVE != 0
}
#[must_use]
pub fn is_quantized(self) -> bool {
self.0 & Self::QUANTIZED != 0
}
#[must_use]
pub fn has_model_card(self) -> bool {
self.0 & Self::HAS_MODEL_CARD != 0
}
#[must_use]
pub fn bits(self) -> u8 {
self.0
}
#[must_use]
pub fn from_bits(bits: u8) -> Self {
Self(bits & 0b0111_1111)
}
}
#[derive(Debug, Clone)]
pub struct Header {
pub magic: [u8; 4],
pub version: (u8, u8),
pub model_type: ModelType,
pub metadata_size: u32,
pub payload_size: u32,
pub uncompressed_size: u32,
pub compression: Compression,
pub flags: Flags,
pub quality_score: u8,
}
impl Header {
#[must_use]
pub fn new(model_type: ModelType) -> Self {
Self {
magic: MAGIC,
version: FORMAT_VERSION,
model_type,
metadata_size: 0,
payload_size: 0,
uncompressed_size: 0,
compression: Compression::default(),
flags: Flags::default(),
quality_score: 0,
}
}
#[must_use]
pub fn to_bytes(&self) -> [u8; HEADER_SIZE] {
let mut bytes = [0u8; HEADER_SIZE];
bytes[0..4].copy_from_slice(&self.magic);
bytes[4] = self.version.0;
bytes[5] = self.version.1;
let model_type = self.model_type as u16;
bytes[6..8].copy_from_slice(&model_type.to_le_bytes());
bytes[8..12].copy_from_slice(&self.metadata_size.to_le_bytes());
bytes[12..16].copy_from_slice(&self.payload_size.to_le_bytes());
bytes[16..20].copy_from_slice(&self.uncompressed_size.to_le_bytes());
bytes[20] = self.compression as u8;
bytes[21] = self.flags.bits();
bytes[22] = self.quality_score;
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() < HEADER_SIZE {
return Err(AprFormatError::FormatError {
message: format!(
"Header too short: {} bytes, expected {}",
bytes.len(),
HEADER_SIZE
),
});
}
let magic: [u8; 4] = bytes[0..4]
.try_into()
.map_err(|_| AprFormatError::FormatError {
message: "header slice too short for magic".to_string(),
})?;
if magic != MAGIC {
return Err(AprFormatError::FormatError {
message: format!(
"Invalid magic number: {:02X}{:02X}{:02X}{:02X}, expected APRN",
magic[0], magic[1], magic[2], magic[3]
),
});
}
let version = (bytes[4], bytes[5]);
if version.0 > FORMAT_VERSION.0 {
return Err(AprFormatError::UnsupportedVersion {
found: version,
supported: FORMAT_VERSION,
});
}
let model_type_raw = u16::from_le_bytes([bytes[6], bytes[7]]);
let model_type =
ModelType::from_u16(model_type_raw).ok_or_else(|| AprFormatError::FormatError {
message: format!("Unknown model type: 0x{model_type_raw:04X}"),
})?;
let metadata_size = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
let payload_size = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
let uncompressed_size = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
if uncompressed_size > MAX_UNCOMPRESSED_SIZE {
return Err(AprFormatError::FormatError {
message: format!(
"Uncompressed size {uncompressed_size} exceeds maximum {MAX_UNCOMPRESSED_SIZE} (compression bomb protection)"
),
});
}
let compression =
Compression::from_u8(bytes[20]).ok_or_else(|| AprFormatError::FormatError {
message: format!("Unknown compression algorithm: 0x{:02X}", bytes[20]),
})?;
let flags = Flags::from_bits(bytes[21]);
let quality_score = bytes[22];
Ok(Self {
magic,
version,
model_type,
metadata_size,
payload_size,
uncompressed_size,
compression,
flags,
quality_score,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub samples: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DistillMethod {
Standard,
Progressive,
Ensemble,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeacherProvenance {
pub hash: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
pub model_type: ModelType,
pub param_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ensemble_teachers: Option<Vec<TeacherProvenance>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistillationParams {
pub temperature: f32,
pub alpha: f32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub beta: Option<f32>,
pub epochs: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub final_loss: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayerMapping {
pub student_layer: usize,
pub teacher_layer: usize,
pub weight: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistillationInfo {
pub method: DistillMethod,
pub teacher: TeacherProvenance,
pub params: DistillationParams,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub layer_mapping: Option<Vec<LayerMapping>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LicenseTier {
Personal,
Team,
Enterprise,
Academic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseInfo {
pub uuid: String,
pub hash: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiry: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seats: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub licensee: Option<String>,
pub tier: LicenseTier,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub created_at: String,
pub aprender_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub training: Option<TrainingInfo>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub hyperparameters: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metrics: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub custom: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub distillation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub distillation_info: Option<DistillationInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<LicenseInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_card: Option<ModelCard>,
}
impl Default for Metadata {
fn default() -> Self {
Self {
created_at: chrono_lite_now(),
aprender_version: env!("CARGO_PKG_VERSION").to_string(),
model_name: None,
description: None,
training: None,
hyperparameters: HashMap::new(),
metrics: HashMap::new(),
custom: HashMap::new(),
distillation: None,
distillation_info: None,
license: None,
model_card: None,
}
}
}
#[must_use]
pub fn chrono_lite_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
format!("{secs}")
}
#[derive(Debug, Clone, Default)]
pub struct SaveOptions {
pub compression: Compression,
pub metadata: Metadata,
pub quality_score: Option<u8>,
}
impl SaveOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_compression(mut self, compression: Compression) -> Self {
self.compression = compression;
self
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.metadata.model_name = Some(name.into());
self
}
#[must_use]
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.metadata.description = Some(desc.into());
self
}
#[must_use]
pub fn with_distillation_info(mut self, info: DistillationInfo) -> Self {
self.metadata.distillation_info = Some(info);
self
}
#[must_use]
pub fn with_license(mut self, license: LicenseInfo) -> Self {
self.metadata.license = Some(license);
self
}
#[must_use]
pub fn with_model_card(mut self, card: ModelCard) -> Self {
self.metadata.model_card = Some(card);
self
}
#[must_use]
pub fn with_quality_score(mut self, score: u8) -> Self {
self.quality_score = Some(score);
self
}
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct ModelInfo {
pub model_type: ModelType,
pub format_version: (u8, u8),
pub metadata: Metadata,
pub payload_size: usize,
pub uncompressed_size: usize,
pub encrypted: bool,
pub signed: bool,
pub streaming: bool,
pub licensed: bool,
pub trueno_native: bool,
pub quantized: bool,
pub has_model_card: bool,
}