1use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct ModelCard {
35 pub model_id: String,
38
39 pub name: String,
41
42 pub version: String,
44
45 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub author: Option<String>,
49
50 pub created_at: String,
52
53 pub framework_version: String,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub rust_version: Option<String>,
59
60 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub description: Option<String>,
64
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub license: Option<String>,
68
69 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub training_data: Option<TrainingDataInfo>,
73
74 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
76 pub hyperparameters: HashMap<String, serde_json::Value>,
77
78 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
80 pub metrics: HashMap<String, serde_json::Value>,
81
82 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub architecture: Option<String>,
86
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub param_count: Option<u64>,
90
91 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub target_hardware: Vec<String>,
94
95 #[serde(default, skip_serializing_if = "HashMap::is_empty", flatten)]
97 pub extra: HashMap<String, serde_json::Value>,
98}
99
100impl ModelCard {
101 #[must_use]
103 pub fn new(model_id: impl Into<String>, version: impl Into<String>) -> Self {
104 let model_id = model_id.into();
105 let name = model_id.clone();
106
107 Self {
108 model_id,
109 name,
110 version: version.into(),
111 author: None,
112 created_at: Self::now_iso8601(),
113 framework_version: format!("aprender {}", env!("CARGO_PKG_VERSION")),
114 rust_version: option_env!("RUSTC_VERSION").map(String::from),
115 description: None,
116 license: None,
117 training_data: None,
118 hyperparameters: HashMap::new(),
119 metrics: HashMap::new(),
120 architecture: None,
121 param_count: None,
122 target_hardware: vec!["cpu".to_string()],
123 extra: HashMap::new(),
124 }
125 }
126
127 #[must_use]
129 pub fn with_name(mut self, name: impl Into<String>) -> Self {
130 self.name = name.into();
131 self
132 }
133
134 #[must_use]
136 pub fn with_author(mut self, author: impl Into<String>) -> Self {
137 self.author = Some(author.into());
138 self
139 }
140
141 #[must_use]
143 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
144 self.description = Some(desc.into());
145 self
146 }
147
148 #[must_use]
150 pub fn with_license(mut self, license: impl Into<String>) -> Self {
151 self.license = Some(license.into());
152 self
153 }
154
155 #[must_use]
157 pub fn with_architecture(mut self, arch: impl Into<String>) -> Self {
158 self.architecture = Some(arch.into());
159 self
160 }
161
162 #[must_use]
164 pub fn with_param_count(mut self, count: u64) -> Self {
165 self.param_count = Some(count);
166 self
167 }
168
169 #[must_use]
171 pub fn with_training_data(mut self, data: TrainingDataInfo) -> Self {
172 self.training_data = Some(data);
173 self
174 }
175
176 #[must_use]
178 pub fn with_hyperparameter(
179 mut self,
180 key: impl Into<String>,
181 value: impl Into<serde_json::Value>,
182 ) -> Self {
183 self.hyperparameters.insert(key.into(), value.into());
184 self
185 }
186
187 #[must_use]
189 pub fn with_metric(
190 mut self,
191 key: impl Into<String>,
192 value: impl Into<serde_json::Value>,
193 ) -> Self {
194 self.metrics.insert(key.into(), value.into());
195 self
196 }
197
198 fn now_iso8601() -> String {
200 let now = std::time::SystemTime::now();
203 let duration = now
204 .duration_since(std::time::UNIX_EPOCH)
205 .unwrap_or_default();
206 let secs = duration.as_secs();
207
208 let days = secs / 86400;
210 let time_secs = secs % 86400;
211 let hours = time_secs / 3600;
212 let minutes = (time_secs % 3600) / 60;
213 let seconds = time_secs % 60;
214
215 let (year, month, day) = days_to_ymd(days);
217
218 format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
219 }
220
221 #[must_use]
223 pub fn to_huggingface(&self) -> String {
224 use std::fmt::Write;
225
226 let mut output = String::from("---\n");
227
228 if let Some(license) = &self.license {
230 let _ = writeln!(output, "license: {}", license.to_lowercase());
231 }
232
233 output.push_str("pipeline_tag: text-generation\n");
235
236 output.push_str("tags:\n");
238 if let Some(arch) = &self.architecture {
239 let _ = writeln!(output, " - {}", arch.to_lowercase());
240 }
241 output.push_str(" - aprender\n");
242 output.push_str(" - rust\n");
243
244 if !self.metrics.is_empty() {
251 output.push_str("model-index:\n");
252 let _ = writeln!(output, " - name: {}", self.model_id);
253 output.push_str(" results:\n");
254 output.push_str(" - task:\n");
255 output.push_str(" type: text-generation\n");
256 output.push_str(" metrics:\n");
257 for (key, value) in &self.metrics {
258 let _ = writeln!(output, " - name: {key}");
259 output.push_str(" type: custom\n");
260 let _ = writeln!(output, " value: {value}");
261 }
262 }
263
264 output.push_str("---\n\n");
265
266 let _ = writeln!(output, "# {}\n", self.name);
268
269 if let Some(desc) = &self.description {
271 let _ = writeln!(output, "{desc}\n");
272 }
273
274 if let Some(data) = &self.training_data {
276 output.push_str("## Training Data\n\n");
277 let _ = writeln!(output, "- **Source:** {}", data.name);
278 if let Some(samples) = data.samples {
279 let _ = writeln!(output, "- **Samples:** {samples}");
280 }
281 if let Some(hash) = &data.hash {
282 let _ = writeln!(output, "- **Hash:** `{hash}`");
283 }
284 output.push('\n');
285 }
286
287 output.push_str("## Framework\n\n");
289 let _ = writeln!(output, "- **Version:** {}", self.framework_version);
290 if let Some(rust) = &self.rust_version {
291 let _ = writeln!(output, "- **Rust:** {rust}");
292 }
293
294 output
295 }
296
297 pub fn to_json(&self) -> Result<String, serde_json::Error> {
299 serde_json::to_string_pretty(self)
300 }
301
302 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
304 serde_json::from_str(json)
305 }
306}
307
308impl Default for ModelCard {
309 fn default() -> Self {
310 Self::new("unnamed", "0.0.0")
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
316pub struct TrainingDataInfo {
317 pub name: String,
319
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub samples: Option<u64>,
323
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub hash: Option<String>,
327}
328
329impl TrainingDataInfo {
330 #[must_use]
332 pub fn new(name: impl Into<String>) -> Self {
333 Self {
334 name: name.into(),
335 samples: None,
336 hash: None,
337 }
338 }
339
340 #[must_use]
342 pub fn with_samples(mut self, count: u64) -> Self {
343 self.samples = Some(count);
344 self
345 }
346
347 #[must_use]
349 pub fn with_hash(mut self, hash: impl Into<String>) -> Self {
350 self.hash = Some(hash.into());
351 self
352 }
353}
354
355fn days_to_ymd(days: u64) -> (u32, u32, u32) {
357 let mut remaining = days as i64;
359 let mut year = 1970i32;
360
361 loop {
362 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
363 if remaining < days_in_year {
364 break;
365 }
366 remaining -= days_in_year;
367 year += 1;
368 }
369
370 let leap = is_leap_year(year);
371 let months = if leap {
372 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
373 } else {
374 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
375 };
376
377 let mut month = 1u32;
378 for days_in_month in months {
379 if remaining < days_in_month {
380 break;
381 }
382 remaining -= days_in_month;
383 month += 1;
384 }
385
386 let day = remaining as u32 + 1;
387
388 (year as u32, month, day)
389}
390
391fn is_leap_year(year: i32) -> bool {
393 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
394}
395
396include!("generating.rs");