use crate::{Result, WeightMatrix, NOVAQEngine, NOVAQModel};
use hf_hub::api::tokio::{Api, ApiBuilder, Repo, RepoType};
use reqwest::Client;
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
pub struct HuggingFaceStreamer {
api: Api,
client: Client,
}
impl HuggingFaceStreamer {
pub fn new() -> Result<Self> {
let mut builder = ApiBuilder::new();
if let Ok(token) = std::env::var("HF_TOKEN") {
builder = builder.with_token(Some(token));
} else if let Ok(token) = std::env::var("HUGGINGFACE_HUB_TOKEN") {
builder = builder.with_token(Some(token));
}
let api = builder.build().map_err(|e| format!("Failed to create HF API: {}", e))?;
let client = Client::new();
Ok(Self { api, client })
}
pub async fn stream_compress_hf_model(
&self,
repo_id: &str,
filename: Option<&str>,
novaq_engine: &mut NOVAQEngine,
progress_callback: impl Fn(f32, &str) + Send + Sync,
) -> Result<NOVAQModel> {
progress_callback(0.0, &format!("Connecting to HuggingFace: {}", repo_id));
let repo = self.api.repo(Repo::new(repo_id.to_string(), RepoType::Model));
let repo_info = repo.info().await
.map_err(|e| format!("Failed to get repo info for {}: {}", repo_id, e))?;
progress_callback(0.1, "Analyzing model files...");
let target_file = self.find_best_model_file(&repo, filename).await?;
progress_callback(0.2, &format!("Found model file: {}", target_file));
if target_file.ends_with(".safetensors") {
self.stream_safetensors_from_hf(&repo, &target_file, novaq_engine, progress_callback).await
} else if target_file.ends_with(".bin") {
self.stream_pytorch_from_hf(&repo, &target_file, novaq_engine, progress_callback).await
} else {
Err(format!("Unsupported model file format: {}", target_file).into())
}
}
async fn find_best_model_file(&self, repo: &hf_hub::api::tokio::ApiRepo, filename: Option<&str>) -> Result<String> {
if let Some(file) = filename {
return Ok(file.to_string());
}
let files = repo.get("").await
.map_err(|e| format!("Failed to list repo files: {}", e))?;
let preferred_files = [
"model.safetensors",
"pytorch_model.bin",
"model.bin",
"consolidated.00.pth",
];
for preferred in &preferred_files {
if let Ok(_) = repo.get(preferred).await {
return Ok(preferred.to_string());
}
}
Err("No compatible model file found in repository".into())
}
async fn stream_safetensors_from_hf(
&self,
repo: &hf_hub::api::tokio::ApiRepo,
filename: &str,
novaq_engine: &mut NOVAQEngine,
progress_callback: impl Fn(f32, &str) + Send + Sync,
) -> Result<NOVAQModel> {
progress_callback(0.3, "Downloading SafeTensors header...");
let file_path = repo.get(filename).await
.map_err(|e| format!("Failed to access {}: {}", filename, e))?;
let mut file = fs::File::open(&file_path).await
.map_err(|e| format!("Failed to open model file: {}", e))?;
let mut size_bytes = [0u8; 8];
file.read_exact(&mut size_bytes).await
.map_err(|e| format!("Failed to read header size: {}", e))?;
let header_size = u64::from_le_bytes(size_bytes) as usize;
let mut header_bytes = vec![0u8; header_size];
file.read_exact(&mut header_bytes).await
.map_err(|e| format!("Failed to read header: {}", e))?;
let header: HashMap<String, Value> = serde_json::from_slice(&header_bytes)
.map_err(|e| format!("Failed to parse SafeTensors header: {}", e))?;
progress_callback(0.4, "Processing tensors with NOVAQ...");
let mut weight_matrices = Vec::new();
let tensor_names: Vec<_> = header.keys().filter(|k| *k != "__metadata__").collect();
let total_tensors = tensor_names.len();
for (i, tensor_name) in tensor_names.iter().enumerate() {
let progress = 0.4 + (i as f32 / total_tensors as f32) * 0.5;
progress_callback(progress, &format!("Processing tensor: {}", tensor_name));
let tensor_info = &header[*tensor_name];
let tensor_data = self.extract_tensor_from_file(&mut file, tensor_info).await?;
let shape: Vec<usize> = tensor_info["shape"].as_array()
.ok_or("Invalid tensor shape")?
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let weight_matrix = WeightMatrix::new(tensor_data, shape, tensor_name.to_string());
weight_matrices.push(weight_matrix);
}
progress_callback(0.9, "Finalizing NOVAQ compression...");
let novaq_model = novaq_engine.quantize_model(weight_matrices)
.map_err(|e| format!("NOVAQ compression failed: {}", e))?;
progress_callback(1.0, "HuggingFace streaming compression complete!");
Ok(novaq_model)
}
async fn extract_tensor_from_file(
&self,
file: &mut fs::File,
tensor_info: &Value,
) -> Result<Vec<f32>> {
let data_offsets = tensor_info["data_offsets"].as_array()
.ok_or("Invalid tensor data offsets")?;
let start_offset = data_offsets[0].as_u64().unwrap();
let end_offset = data_offsets[1].as_u64().unwrap();
let tensor_size = (end_offset - start_offset) as usize;
let header_size = 8u64; let actual_offset = header_size + start_offset;
file.seek(SeekFrom::Start(actual_offset)).await
.map_err(|e| format!("Failed to seek to tensor data: {}", e))?;
let mut tensor_bytes = vec![0u8; tensor_size];
file.read_exact(&mut tensor_bytes).await
.map_err(|e| format!("Failed to read tensor data: {}", e))?;
let float_count = tensor_size / 4;
let mut tensor_data = Vec::with_capacity(float_count);
for chunk in tensor_bytes.chunks_exact(4) {
let float_val = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
tensor_data.push(float_val);
}
Ok(tensor_data)
}
async fn stream_pytorch_from_hf(
&self,
repo: &hf_hub::api::tokio::ApiRepo,
filename: &str,
novaq_engine: &mut NOVAQEngine,
progress_callback: impl Fn(f32, &str) + Send + Sync,
) -> Result<NOVAQModel> {
progress_callback(0.0, "PyTorch streaming not yet implemented for HF");
Err("PyTorch streaming from HuggingFace not implemented yet".into())
}
}
pub struct OllamaStreamer {
client: Client,
base_url: String,
}
impl OllamaStreamer {
pub fn new() -> Self {
Self {
client: Client::new(),
base_url: "http://localhost:11434".to_string(),
}
}
pub fn with_base_url(mut self, url: String) -> Self {
self.base_url = url;
self
}
pub async fn stream_compress_ollama_model(
&self,
model_name: &str,
novaq_engine: &mut NOVAQEngine,
progress_callback: impl Fn(f32, &str) + Send + Sync,
) -> Result<NOVAQModel> {
progress_callback(0.0, &format!("Connecting to Ollama: {}", model_name));
let models = self.list_models().await?;
let model_info = models.iter()
.find(|m| m["name"].as_str() == Some(model_name))
.ok_or_else(|| format!("Model {} not found in Ollama. Run: ollama pull {}", model_name, model_name))?;
progress_callback(0.1, "Found model in Ollama");
let model_details = self.get_model_info(model_name).await?;
progress_callback(0.2, "Retrieved model information");
let model_path = self.find_ollama_model_path(model_name).await?;
progress_callback(0.3, &format!("Located model files: {}", model_path));
self.stream_gguf_file(&model_path, novaq_engine, progress_callback).await
}
async fn list_models(&self) -> Result<Vec<Value>> {
let url = format!("{}/api/tags", self.base_url);
let response = self.client.get(&url).send().await
.map_err(|e| format!("Failed to connect to Ollama API: {}. Is Ollama running?", e))?;
if !response.status().is_success() {
return Err(format!("Ollama API returned error: {}", response.status()).into());
}
let data: Value = response.json().await
.map_err(|e| format!("Failed to parse Ollama response: {}", e))?;
Ok(data["models"].as_array().unwrap_or(&vec![]).clone())
}
async fn get_model_info(&self, model_name: &str) -> Result<Value> {
let url = format!("{}/api/show", self.base_url);
let payload = serde_json::json!({ "name": model_name });
let response = self.client.post(&url)
.json(&payload)
.send().await
.map_err(|e| format!("Failed to get model info: {}", e))?;
if !response.status().is_success() {
return Err(format!("Failed to get model info: {}", response.status()).into());
}
let data: Value = response.json().await
.map_err(|e| format!("Failed to parse model info: {}", e))?;
Ok(data)
}
async fn find_ollama_model_path(&self, model_name: &str) -> Result<String> {
let home_dir = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| "Could not determine home directory")?;
let ollama_dir = format!("{}/.ollama/models", home_dir);
let manifests_dir = format!("{}/manifests", ollama_dir);
let blobs_dir = format!("{}/blobs", ollama_dir);
Ok(format!("{}/blobs/[model-hash]", ollama_dir))
}
async fn stream_gguf_file(
&self,
model_path: &str,
novaq_engine: &mut NOVAQEngine,
progress_callback: impl Fn(f32, &str) + Send + Sync,
) -> Result<NOVAQModel> {
progress_callback(0.4, "Parsing GGUF model format...");
progress_callback(0.5, "GGUF streaming compression not fully implemented yet");
Err("GGUF streaming compression from Ollama not implemented yet".into())
}
}
impl Default for HuggingFaceStreamer {
fn default() -> Self {
Self::new().expect("Failed to create HuggingFace streamer")
}
}
impl Default for OllamaStreamer {
fn default() -> Self {
Self::new()
}
}