use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::manifest::resolve_model_name;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ModelConfig {
pub transformer: Option<String>,
pub transformer_shards: Option<Vec<String>>,
pub vae: Option<String>,
pub t5_encoder: Option<String>,
pub clip_encoder: Option<String>,
pub t5_tokenizer: Option<String>,
pub clip_tokenizer: Option<String>,
pub clip_encoder_2: Option<String>,
pub clip_tokenizer_2: Option<String>,
pub text_encoder_files: Option<Vec<String>>,
pub text_tokenizer: Option<String>,
pub default_steps: Option<u32>,
pub default_guidance: Option<f64>,
pub default_width: Option<u32>,
pub default_height: Option<u32>,
pub is_schnell: Option<bool>,
pub scheduler: Option<String>,
pub description: Option<String>,
pub family: Option<String>,
}
impl ModelConfig {
pub fn all_file_paths(&self) -> Vec<String> {
let mut paths = Vec::new();
let singles = [
&self.transformer,
&self.vae,
&self.t5_encoder,
&self.clip_encoder,
&self.t5_tokenizer,
&self.clip_tokenizer,
&self.clip_encoder_2,
&self.clip_tokenizer_2,
&self.text_tokenizer,
];
for p in singles.into_iter().flatten() {
paths.push(p.clone());
}
if let Some(ref shards) = self.transformer_shards {
paths.extend(shards.iter().cloned());
}
if let Some(ref files) = self.text_encoder_files {
paths.extend(files.iter().cloned());
}
paths
}
pub fn effective_steps(&self, global_cfg: &Config) -> u32 {
self.default_steps.unwrap_or(global_cfg.default_steps)
}
pub fn effective_guidance(&self) -> f64 {
self.default_guidance.unwrap_or(3.5)
}
pub fn effective_width(&self, global_cfg: &Config) -> u32 {
self.default_width.unwrap_or(global_cfg.default_width)
}
pub fn effective_height(&self, global_cfg: &Config) -> u32 {
self.default_height.unwrap_or(global_cfg.default_height)
}
}
#[derive(Debug, Clone)]
pub struct ModelPaths {
pub transformer: PathBuf,
pub transformer_shards: Vec<PathBuf>,
pub vae: PathBuf,
pub t5_encoder: Option<PathBuf>,
pub clip_encoder: Option<PathBuf>,
pub t5_tokenizer: Option<PathBuf>,
pub clip_tokenizer: Option<PathBuf>,
pub clip_encoder_2: Option<PathBuf>,
pub clip_tokenizer_2: Option<PathBuf>,
pub text_encoder_files: Vec<PathBuf>,
pub text_tokenizer: Option<PathBuf>,
}
impl ModelPaths {
pub fn resolve(model_name: &str, config: &Config) -> Option<Self> {
let model_cfg = config.models.get(model_name);
let transformer = Self::resolve_path(
model_cfg.and_then(|m| m.transformer.as_deref()),
"MOLD_TRANSFORMER_PATH",
)?;
let transformer_shards = model_cfg
.and_then(|m| m.transformer_shards.as_ref())
.map(|shards| shards.iter().map(PathBuf::from).collect())
.unwrap_or_default();
let vae = Self::resolve_path(model_cfg.and_then(|m| m.vae.as_deref()), "MOLD_VAE_PATH")?;
let t5_encoder = Self::resolve_path(
model_cfg.and_then(|m| m.t5_encoder.as_deref()),
"MOLD_T5_PATH",
);
let clip_encoder = Self::resolve_path(
model_cfg.and_then(|m| m.clip_encoder.as_deref()),
"MOLD_CLIP_PATH",
);
let t5_tokenizer = Self::resolve_path(
model_cfg.and_then(|m| m.t5_tokenizer.as_deref()),
"MOLD_T5_TOKENIZER_PATH",
);
let clip_tokenizer = Self::resolve_path(
model_cfg.and_then(|m| m.clip_tokenizer.as_deref()),
"MOLD_CLIP_TOKENIZER_PATH",
);
let clip_encoder_2 = Self::resolve_path(
model_cfg.and_then(|m| m.clip_encoder_2.as_deref()),
"MOLD_CLIP2_PATH",
);
let clip_tokenizer_2 = Self::resolve_path(
model_cfg.and_then(|m| m.clip_tokenizer_2.as_deref()),
"MOLD_CLIP2_TOKENIZER_PATH",
);
let text_encoder_files = model_cfg
.and_then(|m| m.text_encoder_files.as_ref())
.map(|files| files.iter().map(PathBuf::from).collect())
.unwrap_or_default();
let text_tokenizer = Self::resolve_path(
model_cfg.and_then(|m| m.text_tokenizer.as_deref()),
"MOLD_TEXT_TOKENIZER_PATH",
);
Some(Self {
transformer,
transformer_shards,
vae,
t5_encoder,
clip_encoder,
t5_tokenizer,
clip_tokenizer,
clip_encoder_2,
clip_tokenizer_2,
text_encoder_files,
text_tokenizer,
})
}
fn resolve_path(config_val: Option<&str>, env_var: &str) -> Option<PathBuf> {
if let Some(path) = config_val {
return Some(PathBuf::from(path));
}
if let Ok(path) = std::env::var(env_var) {
return Some(PathBuf::from(path));
}
None
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
#[serde(default = "default_model")]
pub default_model: String,
#[serde(default = "default_models_dir")]
pub models_dir: String,
#[serde(default = "default_port")]
pub server_port: u16,
#[serde(default = "default_dimension")]
pub default_width: u32,
#[serde(default = "default_dimension")]
pub default_height: u32,
#[serde(default = "default_steps")]
pub default_steps: u32,
#[serde(default)]
pub t5_variant: Option<String>,
#[serde(default)]
pub qwen3_variant: Option<String>,
#[serde(default)]
pub models: HashMap<String, ModelConfig>,
}
fn default_model() -> String {
"flux-schnell".to_string()
}
fn default_models_dir() -> String {
"~/.mold/models".to_string()
}
fn default_port() -> u16 {
7680
}
fn default_dimension() -> u32 {
768
}
fn default_steps() -> u32 {
4
}
impl Default for Config {
fn default() -> Self {
Self {
default_model: default_model(),
models_dir: default_models_dir(),
server_port: default_port(),
default_width: default_dimension(),
default_height: default_dimension(),
default_steps: default_steps(),
t5_variant: None,
qwen3_variant: None,
models: HashMap::new(),
}
}
}
impl Config {
pub fn load_or_default() -> Self {
let config_path = Self::config_path();
if config_path.exists() {
match std::fs::read_to_string(&config_path) {
Ok(contents) => match toml::from_str(&contents) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!(
"warning: failed to parse config at {}: {e} — using defaults",
config_path.display()
);
Config::default()
}
},
Err(e) => {
eprintln!(
"warning: failed to read config at {}: {e} — using defaults",
config_path.display()
);
Config::default()
}
}
} else {
Config::default()
}
}
pub fn mold_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".mold")
}
pub fn config_path() -> PathBuf {
Self::mold_dir().join("config.toml")
}
pub fn data_dir() -> PathBuf {
Self::mold_dir()
}
pub fn resolved_models_dir(&self) -> PathBuf {
if let Ok(env_dir) = std::env::var("MOLD_MODELS_DIR") {
PathBuf::from(env_dir)
} else {
let expanded = self.models_dir.replace(
"~",
&dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.to_string_lossy(),
);
PathBuf::from(expanded)
}
}
pub fn model_config(&self, name: &str) -> ModelConfig {
if let Some(cfg) = self.models.get(name) {
return cfg.clone();
}
let canonical = resolve_model_name(name);
if canonical != name {
if let Some(cfg) = self.models.get(&canonical) {
return cfg.clone();
}
}
ModelConfig::default()
}
pub fn upsert_model(&mut self, name: String, config: ModelConfig) {
self.models.insert(name, config);
}
pub fn remove_model(&mut self, name: &str) -> Option<ModelConfig> {
self.models.remove(name)
}
pub fn save(&self) -> anyhow::Result<()> {
let path = Self::config_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let contents = toml::to_string_pretty(self)?;
std::fs::write(&path, contents)?;
Ok(())
}
pub fn exists_on_disk() -> bool {
Self::config_path().exists()
}
}