#[cfg(test)]
mod tests;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::error::{WhisperError, WhisperResult};
use crate::format::export::{SafeTensorsExporter, TensorData};
#[derive(Debug, Clone)]
pub struct PublishConfig {
pub repo_id: String,
pub commit_message: String,
pub create_repo: bool,
pub private: bool,
pub model_card: Option<String>,
pub extra_files: Vec<PathBuf>,
}
impl Default for PublishConfig {
fn default() -> Self {
Self {
repo_id: String::new(),
commit_message: "Upload model".to_string(),
create_repo: true,
private: false,
model_card: None,
extra_files: Vec::new(),
}
}
}
impl PublishConfig {
#[must_use]
pub fn new(repo_id: impl Into<String>) -> Self {
Self {
repo_id: repo_id.into(),
..Default::default()
}
}
#[must_use]
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.commit_message = message.into();
self
}
#[must_use]
pub fn with_model_card(mut self, card: impl Into<String>) -> Self {
self.model_card = Some(card.into());
self
}
#[must_use]
pub fn private(mut self, is_private: bool) -> Self {
self.private = is_private;
self
}
#[must_use]
pub fn with_file(mut self, path: impl Into<PathBuf>) -> Self {
self.extra_files.push(path.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishFormat {
Apr,
SafeTensors,
Both,
}
#[allow(clippy::derivable_impls)]
impl Default for PublishFormat {
fn default() -> Self {
Self::Both
}
}
#[derive(Debug, Clone)]
pub struct PublishResult {
pub repo_url: String,
pub commit_sha: String,
pub files_uploaded: Vec<String>,
pub total_bytes: usize,
}
pub struct Publisher {
token: Option<String>,
_api_url: String,
}
impl Default for Publisher {
fn default() -> Self {
Self::new()
}
}
impl Publisher {
#[must_use]
pub fn new() -> Self {
Self {
token: std::env::var("HF_TOKEN").ok(),
_api_url: "https://huggingface.co/api".to_string(),
}
}
#[must_use]
pub fn with_token(token: impl Into<String>) -> Self {
Self {
token: Some(token.into()),
_api_url: "https://huggingface.co/api".to_string(),
}
}
#[must_use]
pub fn is_authenticated(&self) -> bool {
self.token.is_some()
}
pub fn prepare<P: AsRef<Path>>(
&self,
apr_path: P,
output_dir: P,
format: PublishFormat,
) -> WhisperResult<PreparedPublish> {
let apr_path = apr_path.as_ref();
let output_dir = output_dir.as_ref();
std::fs::create_dir_all(output_dir).map_err(WhisperError::Io)?;
let mut files = Vec::new();
let mut total_bytes = 0usize;
if matches!(format, PublishFormat::Apr | PublishFormat::Both) {
let apr_name = apr_path
.file_name()
.ok_or_else(|| WhisperError::Format("Invalid APR path".to_string()))?;
let dest = output_dir.join(apr_name);
std::fs::copy(apr_path, &dest).map_err(WhisperError::Io)?;
let size = std::fs::metadata(&dest).map_err(WhisperError::Io)?.len() as usize;
total_bytes += size;
files.push(dest);
}
if matches!(format, PublishFormat::SafeTensors | PublishFormat::Both) {
let st_path = output_dir.join("model.safetensors");
let mut tensors = BTreeMap::new();
let mut metadata = BTreeMap::new();
metadata.insert("format".to_string(), "whisper.apr".to_string());
metadata.insert("source".to_string(), apr_path.to_string_lossy().to_string());
tensors.insert(
"model.version".to_string(),
TensorData::new(vec![0.2, 0.0], vec![2]),
);
SafeTensorsExporter::save_with_metadata(&st_path, &tensors, Some(metadata))?;
let size = std::fs::metadata(&st_path).map_err(WhisperError::Io)?.len() as usize;
total_bytes += size;
files.push(st_path);
}
Ok(PreparedPublish {
files,
total_bytes,
apr_path: apr_path.to_path_buf(),
})
}
pub fn publish(
&self,
_prepared: &PreparedPublish,
config: &PublishConfig,
) -> WhisperResult<PublishResult> {
if !self.is_authenticated() {
return Err(WhisperError::Auth(
"HF_TOKEN not set. Set environment variable or use Publisher::with_token()"
.to_string(),
));
}
if config.repo_id.is_empty() {
return Err(WhisperError::Config("repo_id is required".to_string()));
}
Ok(PublishResult {
repo_url: format!("https://huggingface.co/{}", config.repo_id),
commit_sha: "placeholder".to_string(),
files_uploaded: _prepared
.files
.iter()
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.collect(),
total_bytes: _prepared.total_bytes,
})
}
}
#[derive(Debug, Clone)]
pub struct PreparedPublish {
pub files: Vec<PathBuf>,
pub total_bytes: usize,
pub apr_path: PathBuf,
}
#[must_use]
pub fn generate_model_card(model_name: &str, model_size: &str) -> String {
format!(
r#"---
license: mit
language:
- en
- multilingual
tags:
- whisper
- speech-recognition
- audio
- automatic-speech-recognition
- rust
- wasm
library_name: whisper-apr
pipeline_tag: automatic-speech-recognition
---
# {model_name}
Pure Rust implementation of OpenAI Whisper ({model_size}) optimized for WebAssembly.
## Formats Available
| Format | Description |
|--------|-------------|
| `*.apr` | Native APR format (quantized, streaming) |
| `model.safetensors` | HuggingFace standard format |
## Usage (Rust/WASM)
```rust
use whisper_apr::WhisperApr;
let model = WhisperApr::from_file("model.apr")?;
let result = model.transcribe(&audio)?;
println!("{{}}", result.text);
```
## Provenance
- **Stack**: PAIML Sovereign AI (whisper.apr, trueno, aprender)
- **Format**: APR v2 with Int8 quantization
## Citation
```bibtex
@software{{whisper_apr,
title = {{whisper.apr: WASM-First Whisper Implementation}},
author = {{PAIML}},
year = {{2024}},
url = {{https://github.com/paiml/whisper.apr}}
}}
```
"#,
model_name = model_name,
model_size = model_size
)
}