use std::path::Path;
use std::time::{Duration, Instant};
use crate::pipeline::{
decoded_chw_to_rgb8, latent_seq_to_packed_nchw, PipelineError, TeSource, TextToImageOut,
};
use crate::png::encode_rgb8;
use crate::sample;
use crate::te::{Qwen3Tokenizer, TeWeights, TextEncoder};
use crate::vae::{VaeDecoder, VaeWeights};
use crate::{DitForward, DitWeights};
const SEQ_TXT: usize = 512;
#[derive(Debug, Clone)]
pub struct RenderParams {
pub prompt: String,
pub seed: u64,
pub steps: usize,
pub width: usize,
pub height: usize,
pub guidance: f32,
}
impl Default for RenderParams {
fn default() -> Self {
Self {
prompt: String::new(),
seed: 42,
steps: 4,
width: 512,
height: 512,
guidance: 1.0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct StageTimings {
pub te_encode: Duration,
pub dit_sample: Duration,
pub vae_decode: Duration,
pub png_encode: Duration,
pub total: Duration,
}
pub struct RenderOutcome {
pub image: TextToImageOut,
pub timings: StageTimings,
}
pub struct ImageSession {
dit_weights: DitWeights,
vae: VaeDecoder,
te_weights: TeWeights,
tokenizer: Qwen3Tokenizer,
in_channels: usize,
joint_dim: usize,
}
impl ImageSession {
pub fn load(
dit_gguf: &Path,
vae_weights: &Path,
te_source: &TeSource,
tokenizer_dir: &Path,
) -> Result<Self, PipelineError> {
let dit_weights = DitWeights::open(dit_gguf)?;
let dcfg = dit_weights.config();
let in_channels = dcfg.in_channels as usize;
let joint_dim = dcfg.joint_attention_dim as usize;
let vae_loaded = VaeWeights::open(vae_weights)?;
let vae = VaeDecoder::from_weights(&vae_loaded)?;
drop(vae_loaded);
let te_weights = match te_source {
TeSource::Mlx4bit(p) => TeWeights::open_mlx_4bit(p)?,
TeSource::NpyDir(d) => TeWeights::open(d)?,
};
te_weights.set_resident(true);
let tokenizer = Qwen3Tokenizer::open(tokenizer_dir)?;
Ok(Self {
dit_weights,
vae,
te_weights,
tokenizer,
in_channels,
joint_dim,
})
}
pub fn warm(&self) -> Result<Duration, PipelineError> {
let t = Instant::now();
let _ = self.encode("warmup")?;
Ok(t.elapsed())
}
fn encode(&self, prompt: &str) -> Result<Vec<f32>, PipelineError> {
let toks = self.tokenizer.tokenize(prompt, SEQ_TXT)?;
let encoder = TextEncoder::new(&self.te_weights);
let out = encoder.forward(&toks.input_ids, &toks.attention_mask)?;
let cond = out.cond_7680()?;
let need = SEQ_TXT * self.joint_dim;
if cond.len() != need {
return Err(PipelineError::Shape(format!(
"TE cond len {} != SEQ_TXT*joint_dim ({SEQ_TXT}*{})",
cond.len(),
self.joint_dim
)));
}
Ok(cond)
}
pub fn render(&self, params: &RenderParams) -> Result<RenderOutcome, PipelineError> {
let t_total = Instant::now();
let (lat_h, lat_w) = sample::latent_grid(params.height, params.width);
let seq_img = lat_h * lat_w;
let t_te = Instant::now();
let cond_vec = self.encode(¶ms.prompt)?;
let te_encode = t_te.elapsed();
let t_dit = Instant::now();
let fwd = DitForward::new(&self.dit_weights);
let init = sample::create_noise(params.seed, params.height, params.width);
if init.len() != seq_img * self.in_channels {
return Err(PipelineError::Shape(format!(
"native noise len {} != {seq_img}*{} (in_channels vs PACKED_CHANNELS {})",
init.len(),
self.in_channels,
sample::PACKED_CHANNELS
)));
}
let img_ids = sample::img_ids(lat_h, lat_w);
let txt_ids = sample::txt_ids(SEQ_TXT);
let (timesteps, sigmas) = sample::flow_match_schedule(seq_img, params.steps);
let latent = fwd.sample(
&init, &cond_vec, &img_ids, &txt_ids, seq_img, SEQ_TXT, ×teps, &sigmas, None,
)?;
let dit_sample = t_dit.elapsed();
let t_vae = Instant::now();
let packed = latent_seq_to_packed_nchw(&latent, seq_img, self.in_channels, lat_h, lat_w)?;
let decoded = self
.vae
.decode_packed_latents(&packed, lat_h, lat_w, None)?;
let vae_decode = t_vae.elapsed();
let t_png = Instant::now();
let (w, h, rgb) = decoded_chw_to_rgb8(decoded.c, decoded.h, decoded.w, &decoded.data)?;
let png = encode_rgb8(w, h, &rgb)?;
let png_encode = t_png.elapsed();
Ok(RenderOutcome {
image: TextToImageOut {
png,
width: w,
height: h,
stage_cosines: Vec::new(),
},
timings: StageTimings {
te_encode,
dit_sample,
vae_decode,
png_encode,
total: t_total.elapsed(),
},
})
}
}