1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4use std::sync::OnceLock;
5
6use crate::expand::ExpandSettings;
7use crate::manifest::resolve_model_name;
8use crate::types::Scheduler;
9
10static RUNTIME_MODELS_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum DefaultModelSource {
15 EnvVar,
17 ConfigCustomEntry,
19 Config,
21 LastUsed,
23 OnlyDownloaded,
25 ConfigDefault,
27}
28
29#[derive(Debug, Clone)]
31pub struct DefaultModelResolution {
32 pub model: String,
33 pub source: DefaultModelSource,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize, Default)]
38pub struct ModelConfig {
39 pub transformer: Option<String>,
41 pub transformer_shards: Option<Vec<String>>,
43 pub vae: Option<String>,
44 pub spatial_upscaler: Option<String>,
46 pub temporal_upscaler: Option<String>,
48 pub distilled_lora: Option<String>,
50 pub t5_encoder: Option<String>,
51 pub clip_encoder: Option<String>,
52 pub t5_tokenizer: Option<String>,
53 pub clip_tokenizer: Option<String>,
54 pub clip_encoder_2: Option<String>,
56 pub clip_tokenizer_2: Option<String>,
58 pub text_encoder_files: Option<Vec<String>>,
60 pub text_tokenizer: Option<String>,
62 pub decoder: Option<String>,
64
65 pub default_steps: Option<u32>,
68 pub default_guidance: Option<f64>,
70 pub default_width: Option<u32>,
72 pub default_height: Option<u32>,
74 pub is_schnell: Option<bool>,
77 pub is_turbo: Option<bool>,
80 pub scheduler: Option<Scheduler>,
82 pub negative_prompt: Option<String>,
84 pub lora: Option<String>,
86 pub lora_scale: Option<f64>,
88 pub default_frames: Option<u32>,
90 pub default_fps: Option<u32>,
92
93 pub description: Option<String>,
95 pub family: Option<String>,
96}
97
98impl ModelConfig {
99 pub fn all_file_paths(&self) -> Vec<String> {
102 let mut paths = Vec::new();
103 let singles = [
104 &self.transformer,
105 &self.vae,
106 &self.spatial_upscaler,
107 &self.temporal_upscaler,
108 &self.distilled_lora,
109 &self.t5_encoder,
110 &self.clip_encoder,
111 &self.t5_tokenizer,
112 &self.clip_tokenizer,
113 &self.clip_encoder_2,
114 &self.clip_tokenizer_2,
115 &self.text_tokenizer,
116 &self.decoder,
117 ];
118 for p in singles.into_iter().flatten() {
119 paths.push(p.clone());
120 }
121 if let Some(ref shards) = self.transformer_shards {
122 paths.extend(shards.iter().cloned());
123 }
124 if let Some(ref files) = self.text_encoder_files {
125 paths.extend(files.iter().cloned());
126 }
127 paths
128 }
129
130 pub fn disk_usage(&self) -> (u64, f64) {
135 let total: u64 = self
136 .all_file_paths()
137 .iter()
138 .filter_map(|p| std::fs::metadata(p).ok())
139 .map(|m| m.len())
140 .sum();
141 (total, total as f64 / 1_073_741_824.0)
142 }
143
144 pub fn effective_steps(&self, global_cfg: &Config) -> u32 {
146 self.default_steps.unwrap_or(global_cfg.default_steps)
147 }
148
149 pub fn effective_guidance(&self) -> f64 {
151 self.default_guidance.unwrap_or(3.5)
152 }
153
154 pub fn effective_width(&self, global_cfg: &Config) -> u32 {
156 self.default_width.unwrap_or(global_cfg.default_width)
157 }
158
159 pub fn effective_height(&self, global_cfg: &Config) -> u32 {
161 self.default_height.unwrap_or(global_cfg.default_height)
162 }
163
164 pub fn effective_negative_prompt(&self, global_cfg: &Config) -> Option<String> {
166 self.negative_prompt
167 .clone()
168 .or_else(|| global_cfg.default_negative_prompt.clone())
169 }
170
171 pub fn effective_lora(&self) -> Option<(String, f64)> {
173 self.lora
174 .as_ref()
175 .map(|path| (path.clone(), self.lora_scale.unwrap_or(1.0)))
176 }
177
178 pub fn effective_frames(&self) -> Option<u32> {
180 self.default_frames
181 }
182
183 pub fn effective_fps(&self) -> Option<u32> {
185 self.default_fps
186 }
187}
188
189#[derive(Debug, Clone)]
195pub struct ModelPaths {
196 pub transformer: PathBuf,
197 pub transformer_shards: Vec<PathBuf>,
199 pub vae: PathBuf,
200 pub spatial_upscaler: Option<PathBuf>,
201 pub temporal_upscaler: Option<PathBuf>,
202 pub distilled_lora: Option<PathBuf>,
203 pub t5_encoder: Option<PathBuf>,
204 pub clip_encoder: Option<PathBuf>,
205 pub t5_tokenizer: Option<PathBuf>,
206 pub clip_tokenizer: Option<PathBuf>,
207 pub clip_encoder_2: Option<PathBuf>,
209 pub clip_tokenizer_2: Option<PathBuf>,
211 pub text_encoder_files: Vec<PathBuf>,
213 pub text_tokenizer: Option<PathBuf>,
215 pub decoder: Option<PathBuf>,
217}
218
219impl ModelPaths {
220 pub fn resolve(model_name: &str, config: &Config) -> Option<Self> {
224 if let Some(model_cfg) = config.discovered_manifest_model_config(model_name) {
225 return Self::resolve_from_model_config(Some(&model_cfg));
226 }
227
228 if crate::manifest::find_manifest(model_name).is_some() && config.has_models_dir_override()
229 {
230 return Self::resolve_from_model_config(None);
231 }
232
233 let model_cfg = config.lookup_model_config(model_name);
234 Self::resolve_from_model_config(model_cfg.as_ref())
235 }
236
237 fn resolve_from_model_config(model_cfg: Option<&ModelConfig>) -> Option<Self> {
238 let transformer = Self::resolve_path(
239 model_cfg.and_then(|m| m.transformer.as_deref()),
240 "MOLD_TRANSFORMER_PATH",
241 )?;
242 let transformer_shards = model_cfg
243 .and_then(|m| m.transformer_shards.as_ref())
244 .map(|shards| shards.iter().map(PathBuf::from).collect())
245 .unwrap_or_default();
246 let vae = Self::resolve_path(model_cfg.and_then(|m| m.vae.as_deref()), "MOLD_VAE_PATH")?;
247 let spatial_upscaler = Self::resolve_path(
248 model_cfg.and_then(|m| m.spatial_upscaler.as_deref()),
249 "MOLD_SPATIAL_UPSCALER_PATH",
250 );
251 let temporal_upscaler = Self::resolve_path(
252 model_cfg.and_then(|m| m.temporal_upscaler.as_deref()),
253 "MOLD_TEMPORAL_UPSCALER_PATH",
254 );
255 let distilled_lora = Self::resolve_path(
256 model_cfg.and_then(|m| m.distilled_lora.as_deref()),
257 "MOLD_DISTILLED_LORA_PATH",
258 );
259 let t5_encoder = Self::resolve_path(
260 model_cfg.and_then(|m| m.t5_encoder.as_deref()),
261 "MOLD_T5_PATH",
262 );
263 let clip_encoder = Self::resolve_path(
264 model_cfg.and_then(|m| m.clip_encoder.as_deref()),
265 "MOLD_CLIP_PATH",
266 );
267 let t5_tokenizer = Self::resolve_path(
268 model_cfg.and_then(|m| m.t5_tokenizer.as_deref()),
269 "MOLD_T5_TOKENIZER_PATH",
270 );
271 let clip_tokenizer = Self::resolve_path(
272 model_cfg.and_then(|m| m.clip_tokenizer.as_deref()),
273 "MOLD_CLIP_TOKENIZER_PATH",
274 );
275 let clip_encoder_2 = Self::resolve_path(
276 model_cfg.and_then(|m| m.clip_encoder_2.as_deref()),
277 "MOLD_CLIP2_PATH",
278 );
279 let clip_tokenizer_2 = Self::resolve_path(
280 model_cfg.and_then(|m| m.clip_tokenizer_2.as_deref()),
281 "MOLD_CLIP2_TOKENIZER_PATH",
282 );
283 let text_encoder_files = model_cfg
284 .and_then(|m| m.text_encoder_files.as_ref())
285 .map(|files| files.iter().map(PathBuf::from).collect())
286 .unwrap_or_default();
287 let text_tokenizer = Self::resolve_path(
288 model_cfg.and_then(|m| m.text_tokenizer.as_deref()),
289 "MOLD_TEXT_TOKENIZER_PATH",
290 );
291 let decoder = Self::resolve_path(
292 model_cfg.and_then(|m| m.decoder.as_deref()),
293 "MOLD_DECODER_PATH",
294 );
295
296 Some(Self {
297 transformer,
298 transformer_shards,
299 vae,
300 spatial_upscaler,
301 temporal_upscaler,
302 distilled_lora,
303 t5_encoder,
304 clip_encoder,
305 t5_tokenizer,
306 clip_tokenizer,
307 clip_encoder_2,
308 clip_tokenizer_2,
309 text_encoder_files,
310 text_tokenizer,
311 decoder,
312 })
313 }
314
315 fn resolve_path(config_val: Option<&str>, env_var: &str) -> Option<PathBuf> {
316 if let Ok(path) = std::env::var(env_var) {
317 return Some(PathBuf::from(path));
318 }
319 if let Some(path) = config_val {
320 return Some(PathBuf::from(path));
321 }
322 None
323 }
324}
325
326const CURRENT_CONFIG_VERSION: u32 = 1;
328
329#[derive(Debug, Clone, Deserialize, Serialize)]
330pub struct Config {
331 #[serde(default)]
334 pub config_version: u32,
335
336 #[serde(default = "default_model")]
337 pub default_model: String,
338
339 #[serde(default = "default_models_dir")]
340 pub models_dir: String,
341
342 #[serde(default = "default_port")]
343 pub server_port: u16,
344
345 #[serde(default = "default_dimension")]
346 pub default_width: u32,
347
348 #[serde(default = "default_dimension")]
349 pub default_height: u32,
350
351 #[serde(default = "default_steps")]
352 pub default_steps: u32,
353
354 #[serde(default = "default_embed_metadata")]
355 pub embed_metadata: bool,
356
357 #[serde(default)]
361 pub t5_variant: Option<String>,
362
363 #[serde(default)]
366 pub qwen3_variant: Option<String>,
367
368 #[serde(default)]
372 pub output_dir: Option<String>,
373
374 #[serde(default)]
377 pub default_negative_prompt: Option<String>,
378
379 #[serde(default)]
381 pub expand: ExpandSettings,
382
383 #[serde(default)]
385 pub logging: LoggingConfig,
386
387 #[serde(default)]
389 pub runpod: crate::runpod::RunPodSettings,
390
391 #[serde(default)]
393 pub models: HashMap<String, ModelConfig>,
394}
395
396#[derive(Debug, Clone, Deserialize, Serialize)]
398pub struct LoggingConfig {
399 #[serde(default = "default_log_level")]
401 pub level: String,
402
403 #[serde(default)]
405 pub file: bool,
406
407 #[serde(default)]
409 pub dir: Option<String>,
410
411 #[serde(default = "default_log_max_days")]
413 pub max_days: u32,
414}
415
416fn default_log_level() -> String {
417 "info".to_string()
418}
419fn default_log_max_days() -> u32 {
420 7
421}
422
423impl Default for LoggingConfig {
424 fn default() -> Self {
425 Self {
426 level: default_log_level(),
427 file: false,
428 dir: None,
429 max_days: default_log_max_days(),
430 }
431 }
432}
433
434fn default_model() -> String {
435 "flux2-klein:q8".to_string()
436}
437
438fn default_models_dir() -> String {
439 if let Ok(home) = std::env::var("MOLD_HOME") {
440 format!("{home}/models")
441 } else {
442 "~/.mold/models".to_string()
443 }
444}
445
446fn default_port() -> u16 {
447 7680
448}
449
450fn default_dimension() -> u32 {
451 768
452}
453
454fn default_steps() -> u32 {
455 4
456}
457
458fn default_embed_metadata() -> bool {
459 true
460}
461
462impl Default for Config {
463 fn default() -> Self {
464 Self {
465 config_version: CURRENT_CONFIG_VERSION,
466 default_model: default_model(),
467 models_dir: default_models_dir(),
468 server_port: default_port(),
469 default_width: default_dimension(),
470 default_height: default_dimension(),
471 default_steps: default_steps(),
472 embed_metadata: default_embed_metadata(),
473 t5_variant: None,
474 qwen3_variant: None,
475 output_dir: None,
476 default_negative_prompt: None,
477 expand: ExpandSettings::default(),
478 logging: LoggingConfig::default(),
479 runpod: crate::runpod::RunPodSettings::default(),
480 models: HashMap::new(),
481 }
482 }
483}
484
485impl Config {
486 pub fn install_runtime_models_dir_override(models_dir: PathBuf) {
487 let _ = RUNTIME_MODELS_DIR_OVERRIDE.get_or_init(|| models_dir);
488 }
489
490 pub fn load_or_default() -> Self {
491 let Some(config_path) = Self::config_path() else {
492 eprintln!("warning: could not determine home directory — using default config");
493 return Config::default();
494 };
495 let mut cfg = if config_path.exists() {
496 match std::fs::read_to_string(&config_path) {
497 Ok(contents) => match toml::from_str(&contents) {
498 Ok(cfg) => cfg,
499 Err(e) => {
500 eprintln!(
501 "warning: failed to parse config at {}: {e} — using defaults",
502 config_path.display()
503 );
504 Config::default()
505 }
506 },
507 Err(e) => {
508 eprintln!(
509 "warning: failed to read config at {}: {e} — using defaults",
510 config_path.display()
511 );
512 Config::default()
513 }
514 }
515 } else {
516 Config::default()
517 };
518
519 if cfg.config_version < CURRENT_CONFIG_VERSION {
521 Self::run_migrations(&mut cfg);
522 cfg.config_version = CURRENT_CONFIG_VERSION;
523 if let Err(e) = cfg.save() {
524 eprintln!("warning: failed to save migrated config: {e}");
525 }
526 }
527
528 cfg
529 }
530
531 pub(crate) fn run_migrations(cfg: &mut Config) {
533 if cfg.config_version < 1 {
534 Self::migrate_v0_to_v1(cfg);
535 }
536 }
539
540 fn migrate_v0_to_v1(cfg: &mut Config) {
547 let model_names: Vec<String> = cfg.models.keys().cloned().collect();
548 for name in model_names {
549 if crate::manifest::find_manifest(&name).is_some() {
550 if let Some(mc) = cfg.models.get_mut(&name) {
551 mc.default_steps = None;
552 mc.default_guidance = None;
553 mc.default_width = None;
554 mc.default_height = None;
555 mc.is_schnell = None;
556 mc.is_turbo = None;
557 mc.scheduler = None;
558 mc.negative_prompt = None;
559 mc.default_frames = None;
560 mc.default_fps = None;
561 mc.description = None;
562 mc.family = None;
563 }
564 }
565 }
566 eprintln!("config: migrated v0 → v1 (cleared stale manifest defaults)");
567 }
568
569 pub fn reload_from_disk_preserving_runtime(&self) -> Self {
571 let mut fresh = Self::load_or_default();
572 fresh.models_dir = self.models_dir.clone();
573 fresh
574 }
575
576 pub fn mold_dir() -> Option<PathBuf> {
579 if let Ok(home) = std::env::var("MOLD_HOME") {
580 return Some(PathBuf::from(home));
581 }
582 Some(
583 dirs::home_dir()
584 .unwrap_or_else(|| PathBuf::from("."))
585 .join(".mold"),
586 )
587 }
588
589 pub fn config_path() -> Option<PathBuf> {
590 Self::mold_dir().map(|d| d.join("config.toml"))
591 }
592
593 pub fn data_dir() -> Option<PathBuf> {
594 Self::mold_dir()
595 }
596
597 pub fn resolved_models_dir(&self) -> PathBuf {
598 if let Some(models_dir) = RUNTIME_MODELS_DIR_OVERRIDE.get() {
599 return models_dir.clone();
600 }
601 if let Ok(env_dir) = std::env::var("MOLD_MODELS_DIR") {
602 PathBuf::from(env_dir)
603 } else {
604 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
605 let expanded = self.models_dir.replace("~", &home.to_string_lossy());
606 PathBuf::from(expanded)
607 }
608 }
609
610 pub fn has_models_dir_override(&self) -> bool {
611 RUNTIME_MODELS_DIR_OVERRIDE.get().is_some() || std::env::var_os("MOLD_MODELS_DIR").is_some()
612 }
613
614 pub fn resolved_default_model(&self) -> String {
622 self.resolve_default_model().model
623 }
624
625 pub fn resolve_default_model(&self) -> DefaultModelResolution {
627 if let Ok(m) = std::env::var("MOLD_DEFAULT_MODEL") {
629 if !m.is_empty() {
630 return DefaultModelResolution {
631 model: m,
632 source: DefaultModelSource::EnvVar,
633 };
634 }
635 }
636 let configured = &self.default_model;
638 if self.lookup_model_config(configured).is_some() {
639 return DefaultModelResolution {
640 model: configured.clone(),
641 source: DefaultModelSource::ConfigCustomEntry,
642 };
643 }
644 if self.manifest_model_is_downloaded(configured) {
646 return DefaultModelResolution {
647 model: configured.clone(),
648 source: DefaultModelSource::Config,
649 };
650 }
651 if let Some(last) = Self::read_last_model() {
653 if self.manifest_model_is_downloaded(&last) {
654 return DefaultModelResolution {
655 model: last,
656 source: DefaultModelSource::LastUsed,
657 };
658 }
659 }
660 let downloaded: Vec<String> = crate::manifest::known_manifests()
662 .iter()
663 .filter(|m| {
664 !m.is_utility() && !m.is_upscaler() && self.manifest_model_is_downloaded(&m.name)
665 })
666 .map(|m| m.name.clone())
667 .collect();
668 if downloaded.len() == 1 {
669 return DefaultModelResolution {
670 model: downloaded.into_iter().next().unwrap(),
671 source: DefaultModelSource::OnlyDownloaded,
672 };
673 }
674 DefaultModelResolution {
677 model: crate::manifest::resolve_model_name(configured),
678 source: DefaultModelSource::ConfigDefault,
679 }
680 }
681
682 fn last_model_path() -> Option<PathBuf> {
684 Self::mold_dir().map(|d| d.join("last-model"))
685 }
686
687 pub fn read_last_model() -> Option<String> {
689 let path = Self::last_model_path()?;
690 std::fs::read_to_string(path).ok().and_then(|s| {
691 let trimmed = s.trim().to_string();
692 if trimmed.is_empty() {
693 None
694 } else {
695 Some(trimmed)
696 }
697 })
698 }
699
700 pub fn write_last_model(model: &str) {
702 if let Some(path) = Self::last_model_path() {
703 if let Some(parent) = path.parent() {
704 let _ = std::fs::create_dir_all(parent);
705 }
706 let _ = std::fs::write(path, model);
707 }
708 }
709
710 pub fn resolved_output_dir(&self) -> Option<PathBuf> {
714 let raw = if let Ok(env_dir) = std::env::var("MOLD_OUTPUT_DIR") {
715 if env_dir.is_empty() {
716 None
717 } else {
718 Some(env_dir)
719 }
720 } else {
721 self.output_dir.clone().filter(|s| !s.is_empty())
722 };
723 raw.map(|dir| {
724 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
725 if dir == "~" {
726 home
727 } else if let Some(rest) = dir.strip_prefix("~/") {
728 home.join(rest)
729 } else {
730 PathBuf::from(dir)
731 }
732 })
733 }
734
735 pub fn is_output_disabled(&self) -> bool {
738 if let Ok(env_dir) = std::env::var("MOLD_OUTPUT_DIR") {
739 return env_dir.is_empty();
740 }
741 matches!(self.output_dir.as_deref(), Some(""))
742 }
743
744 pub fn effective_output_dir(&self) -> PathBuf {
747 self.resolved_output_dir().unwrap_or_else(|| {
748 Self::mold_dir()
749 .unwrap_or_else(|| PathBuf::from(".mold"))
750 .join("output")
751 })
752 }
753
754 pub fn resolved_log_dir(&self) -> PathBuf {
756 if let Some(ref dir) = self.logging.dir {
757 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
758 if dir == "~" {
759 home
760 } else if let Some(rest) = dir.strip_prefix("~/") {
761 home.join(rest)
762 } else {
763 PathBuf::from(dir)
764 }
765 } else {
766 Self::mold_dir()
767 .unwrap_or_else(|| PathBuf::from(".mold"))
768 .join("logs")
769 }
770 }
771
772 pub fn effective_embed_metadata(&self, override_value: Option<bool>) -> bool {
773 if let Some(value) = override_value {
774 return value;
775 }
776
777 match std::env::var("MOLD_EMBED_METADATA") {
778 Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
779 "1" | "true" | "yes" | "on" => true,
780 "0" | "false" | "no" | "off" => false,
781 _ => {
782 eprintln!(
783 "warning: invalid MOLD_EMBED_METADATA value '{value}' — using config/default"
784 );
785 self.embed_metadata
786 }
787 },
788 Err(_) => self.embed_metadata,
789 }
790 }
791
792 pub fn discovered_manifest_paths(&self, name: &str) -> Option<ModelPaths> {
793 let manifest = crate::manifest::find_manifest(name)?;
794 if self.incomplete_pull_blocks_manifest(manifest) {
795 return None;
796 }
797 let models_dir = self.resolved_models_dir();
798 let downloads = manifest
799 .files
800 .iter()
801 .map(|file| {
802 crate::manifest::storage_path_candidates(manifest, file)
803 .into_iter()
804 .map(|path| models_dir.join(path))
805 .find(|path| path.exists())
806 .map(|path| (file.component, path))
807 })
808 .collect::<Option<Vec<_>>>()?;
809 crate::manifest::paths_from_downloads(&downloads, &manifest.family)
810 }
811
812 pub fn manifest_model_is_downloaded(&self, name: &str) -> bool {
813 let manifest = match crate::manifest::find_manifest(name) {
814 Some(m) => m,
815 None => return false,
816 };
817 if self.incomplete_pull_blocks_manifest(manifest) {
818 return false;
819 }
820 if manifest.is_upscaler() || manifest.is_utility() {
823 return self.manifest_files_exist(manifest);
824 }
825 self.resolved_local_manifest_model_config(name).is_some()
826 }
827
828 pub fn manifest_model_needs_download(&self, name: &str) -> bool {
831 let canonical = crate::manifest::resolve_model_name(name);
832 crate::manifest::find_manifest(&canonical).is_some()
833 && !self.manifest_model_is_downloaded(&canonical)
834 }
835
836 fn manifest_files_exist(&self, manifest: &crate::manifest::ModelManifest) -> bool {
838 let models_dir = self.resolved_models_dir();
839 manifest.files.iter().all(|file| {
840 crate::manifest::storage_path_candidates(manifest, file)
841 .into_iter()
842 .map(|path| models_dir.join(path))
843 .any(|path| path.exists())
844 })
845 }
846
847 fn incomplete_pull_blocks_manifest(&self, manifest: &crate::manifest::ModelManifest) -> bool {
854 let marker_path =
855 crate::download::pulling_marker_path_in(&self.resolved_models_dir(), &manifest.name);
856 if !marker_path.exists() {
857 return false;
858 }
859
860 if self.manifest_files_exist(manifest) {
861 let _ = std::fs::remove_file(&marker_path);
862 return false;
863 }
864
865 true
866 }
867
868 pub fn model_config(&self, name: &str) -> ModelConfig {
871 let mut cfg = self.lookup_model_config(name).unwrap_or_default();
872
873 if let Some(discovered) = self.resolved_local_manifest_model_config(name) {
874 overlay_model_paths(&mut cfg, &discovered);
875 if cfg.description.is_none() {
876 cfg.description = discovered.description;
877 }
878 if cfg.family.is_none() {
879 cfg.family = discovered.family;
880 }
881 }
882
883 cfg
884 }
885
886 pub fn resolved_model_config(&self, name: &str) -> ModelConfig {
888 let mut cfg = self.model_config(name);
889
890 if let Some(manifest) = crate::manifest::find_manifest(name) {
891 if cfg.default_steps.is_none() {
896 cfg.default_steps = Some(manifest.defaults.steps);
897 }
898 if cfg.default_guidance.is_none() {
899 cfg.default_guidance = Some(manifest.defaults.guidance);
900 }
901 if cfg.default_width.is_none() {
902 cfg.default_width = Some(manifest.defaults.width);
903 }
904 if cfg.default_height.is_none() {
905 cfg.default_height = Some(manifest.defaults.height);
906 }
907 if cfg.is_schnell.is_none() {
908 cfg.is_schnell = Some(manifest.defaults.is_schnell);
909 }
910 if cfg.scheduler.is_none() {
911 cfg.scheduler = manifest.defaults.scheduler;
912 }
913 if cfg.negative_prompt.is_none() {
914 cfg.negative_prompt = manifest.defaults.negative_prompt.clone();
915 }
916 if cfg.default_frames.is_none() {
917 cfg.default_frames = manifest.defaults.frames;
918 }
919 if cfg.default_fps.is_none() {
920 cfg.default_fps = manifest.defaults.fps;
921 }
922 cfg.description = Some(manifest.description.clone());
925 cfg.family = Some(manifest.family.clone());
926 }
927
928 cfg
929 }
930
931 pub fn upsert_model(&mut self, name: String, config: ModelConfig) {
933 self.models.insert(name, config);
934 }
935
936 pub fn remove_model(&mut self, name: &str) -> Option<ModelConfig> {
938 self.models.remove(name)
939 }
940
941 pub fn save(&self) -> anyhow::Result<()> {
946 let path = Self::config_path()
947 .ok_or_else(|| anyhow::anyhow!("cannot determine home directory for config path"))?;
948
949 let path_str = path.to_string_lossy();
954 let is_temp_config = path_str.contains("/tmp/") || path_str.contains("/mold-config-test-");
955 let has_temp_models_dir = self.models_dir.contains("/tmp/mold-")
956 || self.models_dir.contains("/mold-config-test-");
957 if has_temp_models_dir && !is_temp_config {
958 eprintln!(
959 "warning: refusing to save config with test models_dir ({}) to real config ({})",
960 self.models_dir,
961 path.display()
962 );
963 return Ok(());
964 }
965
966 if let Some(parent) = path.parent() {
967 std::fs::create_dir_all(parent)?;
968 }
969 let contents = toml::to_string_pretty(self)?;
970 std::fs::write(&path, contents)?;
971 Ok(())
972 }
973
974 pub fn exists_on_disk() -> bool {
976 Self::config_path().is_some_and(|p| p.exists())
977 }
978
979 pub fn lookup_model_config(&self, name: &str) -> Option<ModelConfig> {
982 if let Some(cfg) = self.models.get(name) {
983 return Some(cfg.clone());
984 }
985 let canonical = resolve_model_name(name);
986 if canonical != name {
987 return self.models.get(&canonical).cloned();
988 }
989 None
990 }
991
992 fn discovered_manifest_model_config(&self, name: &str) -> Option<ModelConfig> {
993 let manifest = crate::manifest::find_manifest(name)?;
994 let paths = self.discovered_manifest_paths(name)?;
995 Some(manifest.to_model_config(&paths))
996 }
997
998 fn resolved_local_manifest_model_config(&self, name: &str) -> Option<ModelConfig> {
999 let manifest = crate::manifest::find_manifest(name)?;
1000 let paths = if let Some(paths) = self.discovered_manifest_paths(name) {
1001 paths
1002 } else {
1003 let paths = ModelPaths::resolve(name, self)?;
1004 if !resolved_manifest_paths_exist(manifest, &paths) {
1005 return None;
1006 }
1007 paths
1008 };
1009 Some(manifest.to_model_config(&paths))
1010 }
1011}
1012
1013fn overlay_model_paths(target: &mut ModelConfig, source: &ModelConfig) {
1014 target.transformer = source.transformer.clone();
1015 target.transformer_shards = source.transformer_shards.clone();
1016 target.vae = source.vae.clone();
1017 if source.spatial_upscaler.is_some() {
1018 target.spatial_upscaler = source.spatial_upscaler.clone();
1019 }
1020 if source.temporal_upscaler.is_some() {
1021 target.temporal_upscaler = source.temporal_upscaler.clone();
1022 }
1023 if source.distilled_lora.is_some() {
1024 target.distilled_lora = source.distilled_lora.clone();
1025 }
1026
1027 if source.t5_encoder.is_some() {
1028 target.t5_encoder = source.t5_encoder.clone();
1029 }
1030 if source.clip_encoder.is_some() {
1031 target.clip_encoder = source.clip_encoder.clone();
1032 }
1033 if source.t5_tokenizer.is_some() {
1034 target.t5_tokenizer = source.t5_tokenizer.clone();
1035 }
1036 if source.clip_tokenizer.is_some() {
1037 target.clip_tokenizer = source.clip_tokenizer.clone();
1038 }
1039 if source.clip_encoder_2.is_some() {
1040 target.clip_encoder_2 = source.clip_encoder_2.clone();
1041 }
1042 if source.clip_tokenizer_2.is_some() {
1043 target.clip_tokenizer_2 = source.clip_tokenizer_2.clone();
1044 }
1045 if source.text_encoder_files.is_some() {
1046 target.text_encoder_files = source.text_encoder_files.clone();
1047 }
1048 if source.text_tokenizer.is_some() {
1049 target.text_tokenizer = source.text_tokenizer.clone();
1050 }
1051 if source.decoder.is_some() {
1052 target.decoder = source.decoder.clone();
1053 }
1054}
1055
1056fn resolved_manifest_paths_exist(
1057 manifest: &crate::manifest::ModelManifest,
1058 paths: &ModelPaths,
1059) -> bool {
1060 use crate::manifest::ModelComponent;
1061
1062 let mut transformer_shard_idx = 0usize;
1063 let mut text_encoder_idx = 0usize;
1064
1065 manifest.files.iter().all(|file| match file.component {
1066 ModelComponent::Transformer => paths.transformer.exists(),
1067 ModelComponent::TransformerShard => {
1068 let path = paths.transformer_shards.get(transformer_shard_idx);
1069 transformer_shard_idx += 1;
1070 path.is_some_and(|path| path.exists())
1071 }
1072 ModelComponent::Vae => paths.vae.exists(),
1073 ModelComponent::SpatialUpscaler => paths
1074 .spatial_upscaler
1075 .as_ref()
1076 .is_some_and(|path| path.exists()),
1077 ModelComponent::TemporalUpscaler => paths
1078 .temporal_upscaler
1079 .as_ref()
1080 .is_some_and(|path| path.exists()),
1081 ModelComponent::DistilledLora => paths
1082 .distilled_lora
1083 .as_ref()
1084 .is_some_and(|path| path.exists()),
1085 ModelComponent::T5Encoder => paths.t5_encoder.as_ref().is_some_and(|path| path.exists()),
1086 ModelComponent::ClipEncoder => paths
1087 .clip_encoder
1088 .as_ref()
1089 .is_some_and(|path| path.exists()),
1090 ModelComponent::T5Tokenizer => paths
1091 .t5_tokenizer
1092 .as_ref()
1093 .is_some_and(|path| path.exists()),
1094 ModelComponent::ClipTokenizer => paths
1095 .clip_tokenizer
1096 .as_ref()
1097 .is_some_and(|path| path.exists()),
1098 ModelComponent::ClipEncoder2 => paths
1099 .clip_encoder_2
1100 .as_ref()
1101 .is_some_and(|path| path.exists()),
1102 ModelComponent::ClipTokenizer2 => paths
1103 .clip_tokenizer_2
1104 .as_ref()
1105 .is_some_and(|path| path.exists()),
1106 ModelComponent::TextEncoder => {
1107 let path = paths.text_encoder_files.get(text_encoder_idx);
1108 text_encoder_idx += 1;
1109 path.is_some_and(|path| path.exists())
1110 }
1111 ModelComponent::TextTokenizer => paths
1112 .text_tokenizer
1113 .as_ref()
1114 .is_some_and(|path| path.exists()),
1115 ModelComponent::Decoder => paths.decoder.as_ref().is_some_and(|path| path.exists()),
1116 ModelComponent::Upscaler => paths.transformer.exists(),
1117 })
1118}