Skip to main content

semantic_operators/
locate.rs

1//! Presence-gated localization in one provider invocation.
2use crate::{
3    Answer, AnswerValue, Boolean, BoxQuestion, Error, ErrorKind, Provider, Region, Request, Result,
4    ask,
5};
6use indexmap::IndexMap;
7
8/// Instructions and independent thresholds for presence and localization.
9#[derive(Clone, Debug)]
10pub struct LocateOptions {
11    /// Description of the image placement, such as "`photo`".
12    pub where_: String,
13    /// Minimum confidence for the Boolean presence answer.
14    pub min_presence: f64,
15    /// Minimum confidence for the Box answer.
16    pub min_box_confidence: f64,
17}
18impl Default for LocateOptions {
19    fn default() -> Self {
20        Self {
21            where_: "the image".into(),
22            min_presence: 0.5,
23            min_box_confidence: 0.0,
24        }
25    }
26}
27/// Localization result retaining both answers, even when the target is absent.
28#[derive(Clone, Debug)]
29pub struct Located {
30    /// True or false when presence is decided, otherwise absent.
31    pub found: Option<bool>,
32    /// Decided region only when presence is confidently true.
33    pub region: Option<Region>,
34    /// Full presence answer and provenance.
35    pub presence: Answer<AnswerValue>,
36    /// Full Box answer and provenance, whether or not the target is present.
37    pub box_answer: Answer<AnswerValue>,
38}
39
40/// Ask presence and Box together, retaining the request's state and image assets.
41///
42/// The original questions are replaced by `present` and `box`. A Box answer alone
43/// is never interpreted as evidence that the target exists. Provider capabilities
44/// and validation are checked through the ordinary request boundary.
45///
46/// # Errors
47/// Returns `InvalidArgument` for a blank target, `InvalidQuestion` for an
48/// invalid confidence threshold, and propagates request/image, provider, or
49/// response-validation errors. Uncertain presence or location remains a
50/// successful result with the corresponding optional value absent.
51pub async fn locate(
52    provider: &dyn Provider,
53    request: &Request,
54    target: &str,
55    options: LocateOptions,
56) -> Result<Located> {
57    if crate::question::is_blank(target) {
58        return Err(Error::new(
59            ErrorKind::InvalidArgument,
60            "target must be a non-empty description",
61        ));
62    }
63    let presence = Boolean::new(format!("Is there {target} in {}?", options.where_))?
64        .with_min_confidence(options.min_presence)?;
65    let region = BoxQuestion::new(format!("{target} in {}", options.where_))?
66        .with_min_confidence(options.min_box_confidence)?;
67    let questions = IndexMap::from([
68        ("present".into(), presence.into()),
69        ("box".into(), region.into()),
70    ]);
71    let request = request.with_questions(questions)?;
72    let mut answers = ask(provider, &request).await?;
73    let presence = answers
74        .shift_remove("present")
75        .ok_or_else(|| Error::new(ErrorKind::InvalidResponse, "missing presence answer"))?;
76    let box_answer = answers
77        .shift_remove("box")
78        .ok_or_else(|| Error::new(ErrorKind::InvalidResponse, "missing box answer"))?;
79    let found = match presence.value() {
80        Some(AnswerValue::Boolean(value)) => Some(*value),
81        _ => None,
82    };
83    let region = if found == Some(true) {
84        match box_answer.value() {
85            Some(AnswerValue::Box(region)) => Some(*region),
86            _ => None,
87        }
88    } else {
89        None
90    };
91    Ok(Located {
92        found,
93        region,
94        presence,
95        box_answer,
96    })
97}