# Conflicts:
#	apps/lightcraft/src/main.rs
#	crates/engine/src/config.rs
This commit is contained in:
Claude committed 2026-10-11 11:50:35 +00:00
commit 44acde3a41
3 files changed
+56 -9

No files matched your search

+2 -5
View File
@@ -827,11 +827,8 @@ fn main() -> eframe::Result {
let (mut session, problem) = open_session(in_memory, library_dir, seed_demo && files.is_empty());
// AI masks: the SAM 3 checkpoint (facebook/sam3) in <config>/models/sam3, or LIGHTCRAFT_SAM3_DIR
// (never required: without it, AI masks offer to download it; see docs/ai-masks.md)
session.segmenter.dir = std::env::var_os("LIGHTCRAFT_SAM3_DIR")
.map(std::path::PathBuf::from)
.or_else(|| config_dir().map(|d| d.join("models").join("sam3")));
// the user's own download locations, one base URL per line (LIGHTCRAFT_SAM3_MIRRORS too)
session.segmenter.mirrors_file = config_dir().map(|d| d.join("models").join("sam3-mirrors.txt"));
// and the user's own download locations, one base URL per line (LIGHTCRAFT_SAM3_MIRRORS too)
session = session.with_default_sam3_model();
let mut app = LightcraftApp::new(session, services(cc, app_log_file.as_deref()));
if let Some(ui) = prefs {
app.ui = ui;
+36 -2
View File
@@ -3,8 +3,8 @@
//!
//! Hosts that have a file system (the desktop app, the CLI, the MCP server) share these defaults so a model
//! installed from one is there for the others. Nothing here is applied automatically: a [`Session`] has no
//! face- or denoise-models folder until a host asks for one ([`Session::with_default_face_models`],
//! [`Session::with_default_denoise_models`]), so tests stay hermetic.
//! face-, denoise- or SAM 3 model folder until a host asks for one ([`Session::with_default_face_models`],
//! [`Session::with_default_denoise_models`], [`Session::with_default_sam3_model`]), so tests stay hermetic.
use std::ffi::OsString;
use std::path::PathBuf;
@@ -58,7 +58,25 @@ pub fn default_denoise_models_dir() -> Option<PathBuf> {
.or_else(|| config_dir().map(|d| d.join("denoise-models")))
}
/// Where the SAM 3 model is kept: `$LIGHTCRAFT_SAM3_DIR` if set, else `<config>/models/sam3`.
pub fn default_sam3_dir() -> Option<PathBuf> {
sam3_dir(std::env::var_os("LIGHTCRAFT_SAM3_DIR"), config_dir())
}
/// [`default_sam3_dir`] for a given override and config folder: a non-empty override wins.
fn sam3_dir(var: Option<std::ffi::OsString>, config: Option<PathBuf>) -> Option<PathBuf> {
var.filter(|v| !v.is_empty()).map(PathBuf::from).or_else(|| config.map(|d| d.join("models").join("sam3")))
}
impl Session {
/// Look for the SAM 3 model in the shared default folder ([`default_sam3_dir`]), and for the user's
/// download mirrors in `<config>/models/sam3-mirrors.txt`.
pub fn with_default_sam3_model(mut self) -> Self {
self.segmenter.dir = default_sam3_dir();
self.segmenter.mirrors_file = config_dir().map(|d| d.join("models").join("sam3-mirrors.txt"));
self
}
/// Keep face models in the shared default folder ([`default_face_models_dir`]).
pub fn with_default_face_models(mut self) -> Self {
self.face_models_dir = default_face_models_dir();
@@ -130,3 +148,19 @@ mod config_tests {
assert_eq!(config_dir_with(Platform::Mac, |_| Some(value.clone())), Some(PathBuf::from(value)));
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A non-empty `LIGHTCRAFT_SAM3_DIR` wins; unset or empty falls back to `<config>/models/sam3` (issue #619).
#[test]
fn sam3_dir_prefers_the_override() {
let config = PathBuf::from("cfg");
let fallback = Some(config.join("models").join("sam3"));
assert_eq!(sam3_dir(Some("elsewhere".into()), Some(config.clone())), Some(PathBuf::from("elsewhere")));
assert_eq!(sam3_dir(None, Some(config.clone())), fallback);
assert_eq!(sam3_dir(Some(std::ffi::OsString::new()), Some(config)), fallback);
assert_eq!(sam3_dir(None, None), None);
}
}
+18 -2
View File
@@ -32,7 +32,8 @@ impl Drop for Headless {
impl Default for Headless {
fn default() -> Self {
Self::new(Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models())
let session = Session::new().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models();
Self::new(session.with_default_sam3_model())
}
}
@@ -43,7 +44,8 @@ impl Headless {
/// A headless session with the procedurally generated demo library.
pub fn demo() -> Self {
Self::new(Session::with_demo().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models())
let session = Session::with_demo().with_fs().with_default_denoise_models().with_system_clock().with_default_face_models();
Self::new(session.with_default_sam3_model())
}
fn photo_or_active(&self, p: &Value) -> Result<PhotoId, String> {
@@ -201,6 +203,20 @@ pub fn write_image(path: &Path, img: &Rgba8, quality: u8) -> Result<(), String>
mod tests {
use super::*;
/// Headless sessions look for the SAM 3 model where the desktop app does, and `segment.model.status` reports it (issue #619).
#[test]
fn headless_sessions_get_the_sam3_model_folder() {
let dir = lightcraft_engine::config::default_sam3_dir();
assert_eq!(Headless::default().session.segmenter.dir, dir);
assert_eq!(Headless::demo().session.segmenter.dir, dir);
let dir = std::env::temp_dir().join("lc-mcp-sam3-dir");
let mut session = Session::new();
session.segmenter.dir = Some(dir.clone());
let mut h = Headless::new(session);
let r = h.call("engine.execute", json!({"command": "segment.model.status", "params": {}})).unwrap();
assert_eq!(r["dir"], json!(dir.display().to_string()), "{r}");
}
/// A headless session stamps imports and edits with the system clock, not the engine's fixed test clock.
#[test]
fn headless_uses_the_system_clock() {