mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-26 20:56:55 +01:00
Integration: local models off by default in builds without them (#1199)
- Preferences > Integrations: default and web builds showed the section with two single-option "Classical" dropdowns, "Local models aren't available in this build" and disabled Download buttons; main hides it (#204). The model choices are now hidden where no model can run (prefs::LOCAL_ML_ONLY), and main's hidden-section test is back. - A model choice saved by a local-ml build (the preferences file is shared) made every Select Subject, Object Selection box and Remove Background fail in a default build, with no control left to switch back. There it now means Classical; local-ml builds stay strict, and an explicit "model" param still fails. - models.download / models.remove: the automation gate let a control or MCP agent start about 1.8 GB of downloads or delete installed models outside its workspace. Both are now refused like other ambient file effects; models.list stays allowed. - Select Subject / Object Selection with a model run as jobs, so "No subject found" / "found nothing there" never showed. The job result now gets the same message as the classical path, including the Object Selection options bar's "try a rectangle" hint. - local-models.yml pins its third-party actions by SHA, as ci.yml does. - docs/development.md: Windows local-ml builds fail to link ONNX Runtime 1.28's prebuilt with MSVC 14.42 (unresolved __std_search_1 etc.), and Linux ones need glibc 2.38 / GCC 13 (Ubuntu 22.04 fails with __isoc23_strtol). Tests that fail without the fix: model_cmds a_model_saved_by_a_local_ml_build_means_classical_in_a_build_without_models, prefs_ui unimplemented_preferences_are_hidden_from_the_dialog, workspace local_model_downloads_and_removals_are_not_automation_commands, jobs_ui a_model_selection_that_finds_nothing_says_so_like_the_classical_one. Co-authored-by: mighty-programmer <83077551+mighty-programmer@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5c70cb0ee
commit
d5139d177a
11 files changed
+168
-25
No files matched your search
@@ -26,10 +26,10 @@ jobs:
|
||||
CARGO_PROFILE_TEST_DEBUG: "0"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
|
||||
with:
|
||||
key: optional-local-ml
|
||||
- name: Install Linux UI dependencies
|
||||
|
||||
Generated
+1
@@ -5146,6 +5146,7 @@ dependencies = [
|
||||
"photocraft-geom",
|
||||
"photocraft-gpu",
|
||||
"photocraft-io",
|
||||
"photocraft-ml",
|
||||
"photocraft-paint",
|
||||
"photocraft-plugins",
|
||||
"photocraft-psd",
|
||||
|
||||
@@ -297,6 +297,10 @@ pub fn authorize_engine_command(id: &str, params: &Value) -> Result<(), Automati
|
||||
| "layer.videoLayers.newVideoLayerFromFile"
|
||||
| "layer.videoLayers.replaceFootage"
|
||||
| "layer.videoLayers.reloadFrame"
|
||||
// Downloading (about 1.8 GB) or deleting local models writes the app's model
|
||||
// folder, outside any workspace; Preferences › Integrations does it for the user.
|
||||
| "models.download"
|
||||
| "models.remove"
|
||||
)
|
||||
|| command_uses_ambient_path(id, params)
|
||||
|| profile_command_may_read_ambient(id, params)
|
||||
@@ -731,6 +735,17 @@ mod tests {
|
||||
let _ = std::fs::remove_dir(link);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_model_downloads_and_removals_are_not_automation_commands() {
|
||||
for id in ["models.download", "models.remove"] {
|
||||
for model in ["birefnet-hr-matting", "sam2.1-large"] {
|
||||
assert!(authorize_engine_command(id, &serde_json::json!({"id": model})).is_err(), "{id} {model}");
|
||||
assert!(authorize_engine_step(id, &serde_json::json!({"id": model})).is_err(), "{id} {model} as an action step");
|
||||
}
|
||||
}
|
||||
assert!(authorize_engine_command("models.list", &serde_json::json!({})).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filesystem_commands_fail_closed() {
|
||||
for id in [
|
||||
|
||||
@@ -29,22 +29,29 @@ pub(crate) fn backend(s: &Session) -> Result<Arc<dyn InferenceBackend>> {
|
||||
/// An explicit command choice overrides the saved preference, allowing an action or script to
|
||||
/// request the same method every time. Inference never silently falls back or starts a download.
|
||||
pub(crate) fn subject_model(s: &Session, p: &Value) -> Result<Option<ModelId>> {
|
||||
model_choice(p, s.prefs().integrations.subject_model.name(), ModelId::BiRefNet)
|
||||
model_choice(p, saved(s, s.prefs().integrations.subject_model.name()), ModelId::BiRefNet)
|
||||
}
|
||||
|
||||
pub(crate) fn object_model(s: &Session, p: &Value) -> Result<Option<ModelId>> {
|
||||
model_choice(p, s.prefs().integrations.object_model.name(), ModelId::Sam2)
|
||||
model_choice(p, saved(s, s.prefs().integrations.object_model.name()), ModelId::Sam2)
|
||||
}
|
||||
|
||||
/// The saved choice. Default and web builds can't run a model and hide the choice, so one saved
|
||||
/// by a build with local models (the preferences file is shared) means Classical there instead of
|
||||
/// failing every Select Subject. Builds with local models keep it strict; explicit params too.
|
||||
fn saved(s: &Session, name: &'static str) -> &'static str {
|
||||
if crate::prefs::NATIVE_MODELS || s.model_backend.is_some() { name } else { "classical" }
|
||||
}
|
||||
|
||||
/// Resolve the preference before dispatch so recorded actions keep the method used, even if
|
||||
/// the preference changes before playback. Explicit choices (including invalid ones) stay intact.
|
||||
pub(crate) fn inject_model(s: &Session, command: &str, mut params: Value) -> Value {
|
||||
let model = match command {
|
||||
"select.subject" | "layer.removeBackground" => s.prefs().integrations.subject_model.name(),
|
||||
"select.subject" | "layer.removeBackground" => saved(s, s.prefs().integrations.subject_model.name()),
|
||||
// Point clicks commit the built-in Object Finder's hover result. SAM supports boxes,
|
||||
// so record Classical for this route even when the saved box preference is SAM.
|
||||
"select.object" if params.get("rect").is_none() && params.get("point").is_some() => "classical",
|
||||
"select.object" => s.prefs().integrations.object_model.name(),
|
||||
"select.object" => saved(s, s.prefs().integrations.object_model.name()),
|
||||
_ => return params,
|
||||
};
|
||||
if params.is_null() {
|
||||
@@ -375,6 +382,28 @@ mod tests {
|
||||
assert!(s.execute("select.subject", json!({"model":"birefnet-hr-matting"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_model_saved_by_a_local_ml_build_means_classical_in_a_build_without_models() {
|
||||
if crate::prefs::NATIVE_MODELS {
|
||||
return;
|
||||
}
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width":8,"height":4,"depth":8,"mode":"rgb"})).unwrap();
|
||||
s.execute("prefs.set", json!({"path":"integrations.subjectModel","value":"birefnet-hr-matting"})).unwrap();
|
||||
s.execute("prefs.set", json!({"path":"integrations.objectModel","value":"sam2.1-large"})).unwrap();
|
||||
s.execute("actions.record", json!({"name":"Saved"})).unwrap();
|
||||
for (cmd, p) in [("select.subject", json!({})), ("select.object", json!({"rect":[0,0,8,4]})), ("layer.removeBackground", json!({}))] {
|
||||
let r = s.execute(cmd, p);
|
||||
assert!(!matches!(&r, Err(EngineError::Other(m)) if m.contains("local models")), "{cmd}: {r:?}");
|
||||
}
|
||||
s.execute("actions.stop", json!({})).unwrap();
|
||||
let steps = &s.actions.list[0].steps;
|
||||
assert!(!steps.is_empty());
|
||||
assert!(steps.iter().all(|(_, p)| p["model"] == "classical"), "{steps:?}");
|
||||
// Asking for a model by name still fails: only the saved choice falls back.
|
||||
assert!(s.execute("select.subject", json!({"model":"birefnet-hr-matting"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downloads_need_no_document_never_replay_and_removal_resets_only_its_preference() {
|
||||
let mut s = Session::new();
|
||||
@@ -503,8 +532,13 @@ mod tests {
|
||||
assert_eq!(fake.calls.lock().unwrap().len(), 2);
|
||||
s.model_backend = None;
|
||||
let result = s.execute("file.automate.batch", called_action).unwrap();
|
||||
assert_eq!(result["files"], json!([]), "a called action cannot grant a model backend");
|
||||
assert_eq!(result["errors"].as_array().unwrap().len(), 1);
|
||||
if crate::prefs::NATIVE_MODELS {
|
||||
assert_eq!(result["files"], json!([]), "a called action cannot grant a model backend");
|
||||
assert_eq!(result["errors"].as_array().unwrap().len(), 1);
|
||||
} else {
|
||||
// A build without local models reads the saved model as Classical.
|
||||
assert_eq!(result["errors"], json!([]), "{result}");
|
||||
}
|
||||
assert_eq!(fake.calls.lock().unwrap().len(), 2);
|
||||
let result =
|
||||
s.execute("file.automate.batch", json!({"steps":[["select.subject",{"model":"birefnet-hr-matting"}]],"input":[input],"output":output})).unwrap();
|
||||
|
||||
@@ -1080,9 +1080,16 @@ pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
|
||||
|
||||
/// Is the preference at `path` (`"section.key"`) hidden from the Preferences dialog?
|
||||
pub fn is_hidden(path: &str) -> bool {
|
||||
HIDDEN_UNTIL_IMPLEMENTED.contains(&path) || (!cfg!(target_os = "linux") && LINUX_ONLY.contains(&path))
|
||||
HIDDEN_UNTIL_IMPLEMENTED.contains(&path) || (!cfg!(target_os = "linux") && LINUX_ONLY.contains(&path)) || (!NATIVE_MODELS && LOCAL_ML_ONLY.contains(&path))
|
||||
}
|
||||
|
||||
/// Whether this build can run local models (the `local-ml` feature, native only).
|
||||
pub const NATIVE_MODELS: bool = cfg!(all(feature = "local-ml", not(target_arch = "wasm32")));
|
||||
|
||||
/// Model choices; builds without local models don't show them (#204). A choice saved by a build
|
||||
/// with them (the preferences file is shared) still round-trips, and means Classical here.
|
||||
pub const LOCAL_ML_ONLY: &[&str] = &["integrations.subjectModel", "integrations.objectModel"];
|
||||
|
||||
/// Preferences that only do something on Linux; the dialog doesn't show them elsewhere. They
|
||||
/// still load, save and round-trip on every platform.
|
||||
pub const LINUX_ONLY: &[&str] = &["performance.linuxDisplayServer", "performance.gpuAdapter"];
|
||||
|
||||
@@ -67,6 +67,8 @@ photocraft-testkit = { workspace = true }
|
||||
# examples/perf_scenarios.rs (a 500-brush .abr for the #210 import row) and large_file_bench.rs
|
||||
# (the generated 2 GB PSD and 4.4 GB PSB of P20, `fixtures`).
|
||||
photocraft-psd = { workspace = true, features = ["testgen"] }
|
||||
# A deterministic local-model backend for the model-job tests (jobs_ui_tests.rs).
|
||||
photocraft-ml = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
# Deflates the translation catalogs and egui's default fonts (build.rs); already linked at run time to inflate them.
|
||||
|
||||
@@ -525,6 +525,22 @@ fn on_event(app: &mut PhotocraftApp, ctx: &egui::Context, e: JobEvent) {
|
||||
app.sync_views();
|
||||
crate::file_ui::print_status(app, result);
|
||||
}
|
||||
// With a local model, Select Subject and Object Selection run as jobs: say when they found
|
||||
// nothing, as `report_smart_result` does for the classical ones that finish at once.
|
||||
JobOutcome::Done(ref v)
|
||||
if matches!(e.command.as_str(), "select.subject" | "select.object") && v.get("changed").and_then(Value::as_bool) == Some(false) =>
|
||||
{
|
||||
app.sync_views();
|
||||
app.ui.status = if e.command == "select.subject" && app.active_tool() == crate::state::Tool::ObjectSelection {
|
||||
tl!("No subject found — try a rectangle around the object")
|
||||
} else if e.command == "select.subject" {
|
||||
tl!("No subject found — try Object Selection or the Lasso")
|
||||
} else {
|
||||
tl!("Object Selection found nothing there — try a tighter rectangle")
|
||||
}
|
||||
.into();
|
||||
app.ui.status_error = false;
|
||||
}
|
||||
JobOutcome::Done(_) => {
|
||||
app.sync_views();
|
||||
app.ui.status = e.label;
|
||||
|
||||
@@ -937,3 +937,48 @@ fn background_open_warns_about_missing_fonts() {
|
||||
let want = vec!["font 'NoSuchFamily4559' is not installed; text may render using a fallback face".to_string()];
|
||||
assert!(app.ui.notices.last().is_some_and(|n| n.title.contains("poster.psd") && n.lines == want), "{:?}", app.ui.notices);
|
||||
}
|
||||
|
||||
/// A local model that never finds anything (an all-transparent mask).
|
||||
struct EmptyModel;
|
||||
impl photocraft_ml::InferenceBackend for EmptyModel {
|
||||
fn status(&self) -> Vec<photocraft_ml::ModelStatus> {
|
||||
photocraft_ml::MODELS.iter().map(|m| photocraft_ml::ModelStatus { id: m.id, installed: true, busy: false }).collect()
|
||||
}
|
||||
fn download(&self, _: photocraft_ml::ModelId, _: &photocraft_raster::Interrupt<'_>) -> photocraft_ml::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn remove(&self, _: photocraft_ml::ModelId, _: &photocraft_raster::Interrupt<'_>) -> photocraft_ml::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn infer(
|
||||
&self,
|
||||
_: photocraft_ml::ModelId,
|
||||
_: &photocraft_algo::segment::RgbImage,
|
||||
_: photocraft_ml::Prompt,
|
||||
_: &photocraft_raster::Interrupt<'_>,
|
||||
) -> photocraft_ml::Result<photocraft_ml::AlphaMask> {
|
||||
Ok(photocraft_ml::AlphaMask { width: 4, height: 4, values: vec![0.0; 16], kind: photocraft_ml::MaskKind::Alpha })
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_model_selection_that_finds_nothing_says_so_like_the_classical_one() {
|
||||
use crate::state::Tool;
|
||||
// The same hints as the options bars' classical results (retouch_ui.rs).
|
||||
for (tool, cmd, params, want) in [
|
||||
(Tool::QuickSelection, "select.subject", json!({"model": "birefnet-hr-matting"}), "No subject found — try Object Selection"),
|
||||
(Tool::ObjectSelection, "select.subject", json!({"model": "birefnet-hr-matting"}), "No subject found — try a rectangle"),
|
||||
(Tool::ObjectSelection, "select.object", json!({"model": "sam2.1-large", "rect": [40, 40, 120, 90]}), "Object Selection found nothing there"),
|
||||
] {
|
||||
let mut h = app_harness();
|
||||
h.state_mut().ui.tool = tool;
|
||||
h.state_mut().session.model_backend = Some(Arc::new(EmptyModel));
|
||||
let r = h.state_mut().run(cmd, params).unwrap();
|
||||
assert_eq!(r["pending"], true, "{cmd} runs as a job: {r}");
|
||||
frames_until(&mut h, 2, |h| h.state().session.jobs().is_empty());
|
||||
let app = h.state();
|
||||
assert!(app.ui.status.contains(want), "{cmd}: {}", app.ui.status);
|
||||
assert!(!app.ui.status_error);
|
||||
assert!(app.session.active().unwrap().doc.selection.is_none(), "{cmd} left the selection alone");
|
||||
}
|
||||
}
|
||||
@@ -1759,7 +1759,10 @@ fn interface_groups(ui: &mut egui::Ui, obj: &mut Map<String, Value>, keys: &[Str
|
||||
}
|
||||
if !checks.is_empty() || !other.is_empty() {
|
||||
pref_group(ui, "Options", |ui| {
|
||||
egui::Grid::new("prefs-grid-interface-checks").num_columns(2).spacing([0.0, 7.0]).show(ui, |ui| field_rows(ui, "interface", obj, checks, lang, &Value::Null));
|
||||
egui::Grid::new("prefs-grid-interface-checks")
|
||||
.num_columns(2)
|
||||
.spacing([0.0, 7.0])
|
||||
.show(ui, |ui| field_rows(ui, "interface", obj, checks, lang, &Value::Null));
|
||||
grid("interface-options").show(ui, |ui| field_rows(ui, "interface", obj, other, lang, &Value::Null));
|
||||
});
|
||||
}
|
||||
@@ -1895,7 +1898,9 @@ fn field_rows(ui: &mut egui::Ui, section: &str, obj: &mut Map<String, Value>, ke
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|v| !matches!(path.as_str(), "integrations.subjectModel" | "integrations.objectModel") || *v == s || model_choice_available(models, v))
|
||||
.filter(|v| {
|
||||
!matches!(path.as_str(), "integrations.subjectModel" | "integrations.objectModel") || *v == s || model_choice_available(models, v)
|
||||
})
|
||||
.collect();
|
||||
let labels: Vec<String> = opts.iter().map(|o| choice_label(o)).collect();
|
||||
let pairs: Vec<(String, &str)> = opts.iter().map(|o| o.to_string()).zip(labels.iter().map(String::as_str)).collect();
|
||||
@@ -2755,9 +2760,18 @@ mod tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
let values = json!({"memoryUsageMb":50000}).as_object().unwrap().clone();
|
||||
assert!(!field_visible("performance", &values, "memoryUsageMb", ""));
|
||||
let mut h = Harness::builder()
|
||||
.with_size(vec2(680.0, 350.0))
|
||||
.build_ui_state(|ui, values| section_fields(ui, "performance", values, &["memoryUsageMb".into()], PreferenceFieldOptions { lang: crate::i18n::Lang::EN, system: None, models: &json!({"available":false}), search: "" }), values);
|
||||
let mut h = Harness::builder().with_size(vec2(680.0, 350.0)).build_ui_state(
|
||||
|ui, values| {
|
||||
section_fields(
|
||||
ui,
|
||||
"performance",
|
||||
values,
|
||||
&["memoryUsageMb".into()],
|
||||
PreferenceFieldOptions { lang: crate::i18n::Lang::EN, system: None, models: &json!({"available":false}), search: "" },
|
||||
)
|
||||
},
|
||||
values,
|
||||
);
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Memory Usage");
|
||||
h.get_by_label("Per-document RAM limit (MB)");
|
||||
@@ -2848,9 +2862,10 @@ mod tests {
|
||||
assert!(contains("placeholder", "type"));
|
||||
assert!(contains("performance", "performance"));
|
||||
assert!(contains("interface.uiScale", "interface"));
|
||||
assert!(contains("BACKGROUND REMOVAL", "integrations"));
|
||||
assert!(contains("Object selection", "integrations"));
|
||||
assert!(contains("integrations.subjectModel", "integrations"));
|
||||
// The model choices exist only in builds that can run local models.
|
||||
assert_eq!(contains("BACKGROUND REMOVAL", "integrations"), prefs::NATIVE_MODELS);
|
||||
assert_eq!(contains("Object selection", "integrations"), prefs::NATIVE_MODELS);
|
||||
assert_eq!(contains("integrations.subjectModel", "integrations"), prefs::NATIVE_MODELS);
|
||||
assert!(preference_sections(&values, "this setting does not exist").is_empty());
|
||||
assert_eq!(preference_sections(&values, "").len(), SECTIONS.len());
|
||||
}
|
||||
@@ -2858,6 +2873,9 @@ mod tests {
|
||||
#[test]
|
||||
fn searched_model_preferences_render_only_the_matching_method_without_changing_choices() {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
if !prefs::NATIVE_MODELS {
|
||||
return; // Hidden without local models (unimplemented_preferences_are_hidden_from_the_dialog).
|
||||
}
|
||||
for code in ["en", "el"] {
|
||||
let lang = crate::i18n::Lang::from_pref(code);
|
||||
let _scope = crate::i18n::language_scope(lang);
|
||||
@@ -4084,7 +4102,8 @@ mod tests {
|
||||
let values = prefs::Preferences::default().to_json();
|
||||
assert!(has_visible_fields(&values, "general"));
|
||||
assert!(has_visible_fields(&values, "fileHandling"));
|
||||
assert!(has_visible_fields(&values, "integrations"));
|
||||
// Model choices and downloads only where models can run; agent control is still unread.
|
||||
assert_eq!(has_visible_fields(&values, "integrations"), prefs::NATIVE_MODELS);
|
||||
assert!(has_visible_fields(&values, "enhancedControls"));
|
||||
// Every setting of these sections is still unimplemented.
|
||||
assert!(!has_visible_fields(&values, "scratchDisks"));
|
||||
|
||||
+5
-1
@@ -56,7 +56,11 @@ and the optional learned weights. A fresh `local-ml` build therefore requires ne
|
||||
and is **not reproducible offline from Cargo.lock alone**. `cargo --offline` controls Cargo
|
||||
resolution; it does not supply that runtime archive. Default builds and wasm omit this
|
||||
dependency. Enabled distributors must ship the runtime licence and third-party notices;
|
||||
see [native runtime packaging](local-models.md#build-and-use).
|
||||
see [native runtime packaging](local-models.md#build-and-use). On Windows that prebuilt runtime
|
||||
needs a recent MSVC toolset: with MSVC 14.42 (Visual Studio 2022 17.12) the link fails with
|
||||
unresolved `__std_search_1`, `__std_remove_1` and similar; update the Visual Studio Build Tools.
|
||||
On Linux it needs glibc 2.38 and GCC 13's libstdc++ or newer (Ubuntu 24.04, not 22.04): older
|
||||
systems fail with unresolved `__isoc23_strtol` and `_M_replace_cold`.
|
||||
|
||||
The dev profile builds with `debug = "line-tables-only"`: panic backtraces keep file and line, and test binaries link about twice as fast. To inspect local variables in a debugger, build with `CARGO_PROFILE_DEV_DEBUG=true`.
|
||||
|
||||
|
||||
+6
-6
@@ -9,10 +9,10 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
|
||||
| [Performance](#performance) | 3 | 22 | 26 | 60 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 22 not run, 4 not measurable yet (baseline 2026-10-05) |
|
||||
| [File compatibility](#file-compatibility) | 9 | 12 | 6 | io corpus: oracle ≥ 148/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 249/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 188/256, round trip ≥ 258/256 |
|
||||
| [Tools](#tools) | 36 | 8 | 0 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 14 | 6 | 3 | settings that do nothing: 24 of 171 (target 0) |
|
||||
| [Workspace and UI](#workspace-and-ui) | 14 | 6 | 3 | settings that do nothing: 24 of 173 (target 0) |
|
||||
| [Type](#type) | 5 | 3 | 1 | |
|
||||
| [Automation](#automation) | 5 | 2 | 4 | 631 of 631 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 30 of 30 crates; panic_hunt present |
|
||||
| [Automation](#automation) | 5 | 3 | 3 | 631 of 631 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 31 of 31 crates; panic_hunt present |
|
||||
| [Distribution](#distribution) | 5 | 4 | 2 | |
|
||||
| [Open bugs](#open-bugs) | 31 | 1 | 0 | |
|
||||
|
||||
@@ -181,7 +181,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
||||
|
||||
## Workspace and UI
|
||||
|
||||
**Settings that do nothing: 24 of 171** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
**Settings that do nothing: 24 of 173** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
|
||||
Unread: `general.color_picker`, `general.zoom_resizes_windows`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `performance.cache_levels`, `performance.gpu_adapter`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.text_engine`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
|
||||
|
||||
@@ -238,14 +238,14 @@ Menu wiring (`docs/parity-checklist.md`): 631 of 631 menu items live (100.0%). T
|
||||
| AUTO-219-4 | Replay tests for crop, filters, selections and styled text on a second document | done | [#219](https://github.com/storytold/photocraft/issues/219) | actions_cmds.rs: a_recorded_edit_run_replays_on_a_second_document records a crop, a Gaussian Blur, a selection fill and faux-bold type on one document and replays all of them onto a second (plus the original layer/select/fill replay) |
|
||||
| AUTO-219-5 | Batch: subfolders, open documents, naming template, progress/cancel, error log, stop | partial | [#219](https://github.com/storytold/photocraft/issues/219) | file_cmds.rs and file_cmds/batch_naming.rs: folder/array input, per-file errors, {name}/{serial}/{date} output templates and starting serial, Include All Subfolders (includeSubfolders: depth/loop/file-count bounded, results keep their subfolder; Batch and Image Processor dialogs, #3753); no open-documents source, cancel or error-log file |
|
||||
| AUTO-219-6 | Plug-in ABI v2: panel kind, progress/cancel calls, read other layers, Last Filter repeats | missing | [#219](https://github.com/storytold/photocraft/issues/219) | runtime.rs:15 ABI_VERSION = 1; manifest.rs:18 Kind::Filter only |
|
||||
| AUTO-219-7 | Local ONNX models behind a flag, managed in Preferences; subject select, background removal | missing | [#219](https://github.com/storytold/photocraft/issues/219) | No ONNX runtime dependency |
|
||||
| AUTO-219-7 | Local ONNX models behind a flag, managed in Preferences; subject select, background removal | partial | [#219](https://github.com/storytold/photocraft/issues/219) | local-ml: CPU BiRefNet alpha / SAM 2.1 box selection, pinned verified explicit downloads in Preferences; exploratory 20-target COCO comparison in docs/local-models-validation.md; representative matting quality and web inference remain open |
|
||||
| AUTO-219-8 | xtask check that every panel button routes through a command id | missing | [#219](https://github.com/storytold/photocraft/issues/219) | No such xtask check |
|
||||
| AUTO-219-9 | Headless commands for in-canvas type editing, Liquify/Puppet strokes, transform handles | done | [#219](https://github.com/storytold/photocraft/issues/219) | type.hitTest, type.caret, type.navigate; filter.liquify strokes, edit.puppetWarp pins, type.edit, edit.transform |
|
||||
| AUTO-219-10 | Agent acceptance suite grown to 30-50 workflows | partial | [#219](https://github.com/storytold/photocraft/issues/219) | automation/tests/agent_tasks.rs runs 10 workflows over MCP |
|
||||
|
||||
## Reliability
|
||||
|
||||
Never-crash attribute on 30 of 30 crate roots.
|
||||
Never-crash attribute on 31 of 31 crate roots.
|
||||
|
||||
| Id | Target | Status | Issue | Note |
|
||||
|---|---|---|---|---|
|
||||
|
||||
Reference in new issue
Block a user