mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 17:31:09 -06:00
security(segment): Object selections take at most 64 clicks at the engine API and the model (#3717)
* security(segment): Object selections take at most 64 clicks at the engine API and the model * address review: refuse over-cap Object clicks before queueing in background mode
This commit is contained in:
@@ -98,6 +98,10 @@ fn resolve_ai(s: &mut Session, shape: &mut MaskShape, c: &str) -> Result<Later>
|
||||
match shape {
|
||||
MaskShape::Object { seg: Some(_), .. } | MaskShape::Prompt { seg: Some(_), .. } => {}
|
||||
MaskShape::Object { hint, exclude, .. } if !hint.is_empty() && background => {
|
||||
// refused here, not when the worker later fails at the model's limit after "pending"
|
||||
if hint.len() + exclude.len() > crate::segment::MAX_CLICKS {
|
||||
return Err(ai_err(format!("an Object selection takes at most {} clicks", crate::segment::MAX_CLICKS)));
|
||||
}
|
||||
s.segmenter.require_model()?;
|
||||
return Ok(Later::Clicks(std::mem::take(hint), std::mem::take(exclude)));
|
||||
}
|
||||
|
||||
@@ -498,6 +498,9 @@ impl Session {
|
||||
if !clicks.iter().any(|c| c.include) {
|
||||
return Err("click the object to include it first".into());
|
||||
}
|
||||
if clicks.len() > MAX_CLICKS {
|
||||
return Err(format!("an Object selection takes at most {MAX_CLICKS} clicks"));
|
||||
}
|
||||
#[cfg(feature = "sam")]
|
||||
{
|
||||
self.segment_wait(id, worker::Kind::Clicks(Self::model_clicks(clicks)), None)?.ok_or_else(|| "nothing was selected".to_string())
|
||||
|
||||
@@ -240,6 +240,25 @@ fn object_clicks_are_capped() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// In background mode a new Object mask with too many clicks is refused up front, not queued.
|
||||
#[test]
|
||||
fn background_object_clicks_over_the_cap_are_refused_before_queueing() {
|
||||
if !Segmenter::AVAILABLE {
|
||||
return;
|
||||
}
|
||||
let mut s = Session::with_demo();
|
||||
s.segmenter.background = true;
|
||||
let dir = damaged_model("later-clicks");
|
||||
s.segmenter.dir = Some(dir.clone());
|
||||
let id = s.active().unwrap();
|
||||
let points: Vec<_> = (0..=crate::segment::MAX_CLICKS).map(|_| json!([0.5, 0.5])).collect();
|
||||
let e = s.execute("mask.add", &json!({"kind": "object", "points": points})).unwrap_err().to_string();
|
||||
assert!(e.contains("at most"), "{e}");
|
||||
assert!(s.develop_of(id).unwrap().masks.is_empty());
|
||||
assert!(!s.segmenter.busy());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[cfg(feature = "sam")]
|
||||
#[test]
|
||||
fn a_running_model_download_shows_in_activity_and_cancels() {
|
||||
|
||||
@@ -76,6 +76,8 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub const INPUT: usize = 1008;
|
||||
/// Side of the low-resolution masks the model predicts.
|
||||
pub const MASK_SIDE: usize = 288;
|
||||
/// Most clicks one Object selection may carry (the engine's `MAX_CLICKS`).
|
||||
pub const MAX_CLICKS: usize = 64;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const _: () = assert!(MASK_SIDE == tracker::LOW_RES && INPUT == vit::IMAGE);
|
||||
|
||||
@@ -203,6 +205,10 @@ impl Sam3 {
|
||||
if clicks.is_empty() {
|
||||
return Err(Error::Model("no clicks".into()));
|
||||
}
|
||||
// the prompt grows with every click: the editor takes at most this many (the engine's `MAX_CLICKS`)
|
||||
if clicks.len() > MAX_CLICKS {
|
||||
return Err(Error::Model(format!("at most {MAX_CLICKS} clicks, not {}", clicks.len())));
|
||||
}
|
||||
let (neck, tracker) = self.tracker_parts()?;
|
||||
if enc.tracker.is_none() {
|
||||
let levels = neck.forward(&enc.backbone, 3)?;
|
||||
|
||||
Reference in New Issue
Block a user