use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::card::field::FieldSpec;
use crate::reference::Ref;
pub mod validate;
pub use validate::ModelCardError;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct ModelSpec {
pub interface: ModelInterface,
pub task_type: TaskType,
pub signature: ModelSignature,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sample_input: Option<SampleInput>,
#[serde(default)]
pub card_refs: Vec<Ref>,
}
impl ModelSpec {
pub fn new(
interface: ModelInterface,
task_type: TaskType,
signature: ModelSignature,
sample_input: Option<SampleInput>,
card_refs: Vec<Ref>,
) -> Result<Self, ModelCardError> {
let spec = Self {
interface,
task_type,
signature,
sample_input,
card_refs,
};
spec.validate()?;
Ok(spec)
}
pub fn validate(&self) -> Result<(), ModelCardError> {
validate::validate_model_spec(self)
}
#[must_use]
pub fn interface_kind(&self) -> &'static str {
self.interface.kind()
}
#[must_use]
pub const fn is_generation(&self) -> bool {
matches!(self.task_type, TaskType::Generation)
}
pub fn card_refs(&self) -> impl Iterator<Item = &Ref> {
self.card_refs.iter()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "kind", content = "meta")]
pub enum ModelInterface {
Sklearn(SklearnMeta),
Xgboost(XgboostMeta),
Lightgbm(LightgbmMeta),
Catboost(CatboostMeta),
Torch(TorchMeta),
Lightning(LightningMeta),
Tensorflow(TensorflowMeta),
Huggingface(HuggingfaceMeta),
Custom(CustomMeta),
}
impl ModelInterface {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Sklearn(_) => "Sklearn",
Self::Xgboost(_) => "Xgboost",
Self::Lightgbm(_) => "Lightgbm",
Self::Catboost(_) => "Catboost",
Self::Torch(_) => "Torch",
Self::Lightning(_) => "Lightning",
Self::Tensorflow(_) => "Tensorflow",
Self::Huggingface(_) => "Huggingface",
Self::Custom(_) => "Custom",
}
}
#[must_use]
pub const fn loader_family(&self) -> &'static str {
match self {
Self::Sklearn(_) => "sklearn",
Self::Xgboost(_) => "xgboost",
Self::Lightgbm(_) => "lightgbm",
Self::Catboost(_) => "catboost",
Self::Torch(_) => "torch",
Self::Lightning(_) => "pytorch-lightning",
Self::Tensorflow(_) => "tensorflow",
Self::Huggingface(_) => "huggingface-transformers",
Self::Custom(_) => "custom-python-loader",
}
}
#[must_use]
pub const fn default_media_type(&self) -> &'static str {
match self {
Self::Sklearn(_) | Self::Xgboost(_) | Self::Lightgbm(_) | Self::Catboost(_) => {
"application/x-joblib"
}
Self::Torch(_) => "application/vnd.safetensors",
Self::Lightning(_) => "application/x-pytorch-ckpt",
Self::Tensorflow(_) => "application/vnd.keras+zip",
Self::Huggingface(_) => "application/vnd.huggingface+bundle",
Self::Custom(_) => "application/octet-stream",
}
}
#[must_use]
pub const fn default_extension(&self) -> &'static str {
match self {
Self::Sklearn(_) | Self::Xgboost(_) | Self::Lightgbm(_) | Self::Catboost(_) => "joblib",
Self::Torch(meta) => match meta.save_format {
TorchSaveFormat::Safetensors => "safetensors",
TorchSaveFormat::Pickle => "pt",
},
Self::Lightning(_) => "ckpt",
Self::Tensorflow(meta) => match meta.save_format {
TfSaveFormat::Keras => "keras",
TfSaveFormat::SavedModel => "savedmodel",
},
Self::Huggingface(_) => "huggingface",
Self::Custom(_) => "bin",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct SklearnMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct XgboostMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct LightgbmMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct CatboostMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct TorchMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
pub save_format: TorchSaveFormat,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct LightningMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct TensorflowMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
pub save_format: TfSaveFormat,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct HuggingfaceMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
pub hf_task: HuggingFaceTask,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct CustomMeta {
pub framework_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_subtype: Option<String>,
pub loader_module: String,
pub loader_class: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub extra: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum TaskType {
BinaryClassification,
MultiClassClassification,
Regression,
Clustering,
AnomalyDetection,
Forecasting,
Generation,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum TorchSaveFormat {
Safetensors,
Pickle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum TfSaveFormat {
Keras,
SavedModel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum HuggingFaceTask {
TextClassification,
TokenClassification,
QuestionAnswering,
Summarization,
Translation,
TextGeneration,
FillMask,
ZeroShotClassification,
ImageClassification,
ObjectDetection,
ImageSegmentation,
ImageToText,
ImageToImage,
TextToImage,
DepthEstimation,
AudioClassification,
AutomaticSpeechRecognition,
AudioToAudio,
TextToSpeech,
TabularClassification,
TabularRegression,
FeatureExtraction,
SentenceSimilarity,
Conversational,
DocumentQuestionAnswering,
VisualQuestionAnswering,
TableQuestionAnswering,
Embedding,
MultipleChoice,
Other,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct ModelSignature {
pub inputs: Vec<FieldSpec>,
pub outputs: Vec<FieldSpec>,
}
impl ModelSignature {
#[must_use]
pub fn new(inputs: Vec<FieldSpec>, outputs: Vec<FieldSpec>) -> Self {
Self { inputs, outputs }
}
pub fn from_fields(
inputs: Vec<FieldSpec>,
outputs: Vec<FieldSpec>,
) -> Result<Self, ModelCardError> {
let signature = Self::new(inputs, outputs);
signature.validate()?;
Ok(signature)
}
#[must_use]
pub fn inputs(&self) -> &[FieldSpec] {
&self.inputs
}
#[must_use]
pub fn outputs(&self) -> &[FieldSpec] {
&self.outputs
}
pub fn validate(&self) -> Result<(), ModelCardError> {
validate::validate_signature(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct SampleInput {
pub kind: SampleInputKind,
}
impl SampleInput {
#[must_use]
pub const fn new(kind: SampleInputKind) -> Self {
Self { kind }
}
#[must_use]
pub const fn kind(&self) -> SampleInputKind {
self.kind
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum SampleInputKind {
Pandas,
Polars,
Arrow,
Numpy,
Torch,
Tf,
Dict,
List,
Tuple,
Str,
None,
}