fix(desktop): one subject, one photo - a rebuilt card keeps its picture
Watching a run frame by frame: a real Bali temple photo landed, then halfway through it turned into a plain blue sky. The model had rebuilt the section (fresh node ids), the same query searched again, and the session-wide dedup - there to stop two cards sharing one picture - now skipped the very photo that query had already chosen, leaving the junk below it. A query this session already answered now resolves from a memo: no network call, and no downgrade. Dedup still guards different subjects from sharing a photo.
This commit is contained in:
parent
c5feaeec01
commit
f1178c76c1
|
|
@ -1,6 +1,6 @@
|
|||
//! Background image-search enrichment for generated image nodes.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::mpsc::{self, Receiver, TryRecvError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
|
@ -229,6 +229,21 @@ pub(crate) struct ImageSearchSession {
|
|||
/// their Openverse top hit, which filled three different cards with the
|
||||
/// SAME photo (measured: test0711-22). Selection skips these best-effort.
|
||||
used_urls: Arc<Mutex<HashSet<String>>>,
|
||||
/// Query → the photo that query already resolved to, this session.
|
||||
///
|
||||
/// The dedup above must NOT fire when the SAME subject comes back: the
|
||||
/// model rebuilds a section mid-run (fresh node ids), the same query
|
||||
/// ("Bali Indonesia") searches again, and its own good photo is now in
|
||||
/// `used_urls` — so the second search skipped it and took a junk result
|
||||
/// instead (measured 2026-07-12: a real Bali temple photo turned into a
|
||||
/// plain blue sky halfway through a run). One subject, one photo: a repeat
|
||||
/// query resolves from this memo with no network call at all.
|
||||
resolved: Arc<Mutex<HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
/// Memo key — the subject, not its spelling.
|
||||
fn query_key(query: &str) -> String {
|
||||
query.trim().to_ascii_lowercase()
|
||||
}
|
||||
|
||||
impl ImageSearchSession {
|
||||
|
|
@ -254,6 +269,7 @@ impl ImageSearchSession {
|
|||
self.jobs.clear();
|
||||
self.invalidate_scan_gate();
|
||||
self.used_urls.lock().unwrap().clear();
|
||||
self.resolved.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
pub(crate) fn is_pending(&self) -> bool {
|
||||
|
|
@ -312,7 +328,12 @@ impl ImageSearchSession {
|
|||
self.in_flight.insert(id);
|
||||
let job = match &gen_profile {
|
||||
Some(profile) => spawn_gen_job(target, profile.clone()),
|
||||
None => spawn_job(target, credentials.clone(), Arc::clone(&self.used_urls)),
|
||||
None => spawn_job(
|
||||
target,
|
||||
credentials.clone(),
|
||||
Arc::clone(&self.used_urls),
|
||||
Arc::clone(&self.resolved),
|
||||
),
|
||||
};
|
||||
self.jobs.push(job);
|
||||
}
|
||||
|
|
@ -370,17 +391,30 @@ fn spawn_job(
|
|||
target: ImageSearchTarget,
|
||||
credentials: Option<OpenverseCredentials>,
|
||||
used_urls: Arc<Mutex<HashSet<String>>>,
|
||||
resolved: Arc<Mutex<HashMap<String, String>>>,
|
||||
) -> ImageSearchJob {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let node_id = target.node_id.clone();
|
||||
let aspect_ratio = target.aspect_ratio;
|
||||
let key = query_key(&target.query);
|
||||
// One subject, one photo: a query this session already answered resolves
|
||||
// from the memo — no network, and (crucially) no dedup-forced downgrade
|
||||
// when a rebuilt section asks for the same picture again.
|
||||
if let Some(url) = resolved.lock().unwrap().get(&key).cloned() {
|
||||
let _ = tx.send(Some(url));
|
||||
return ImageSearchJob { node_id, rx };
|
||||
}
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(fetch_first_image_url_blocking(
|
||||
let url = fetch_first_image_url_blocking(
|
||||
&target.query,
|
||||
aspect_ratio,
|
||||
credentials.as_ref(),
|
||||
&used_urls,
|
||||
));
|
||||
);
|
||||
if let Some(found) = url.as_ref() {
|
||||
resolved.lock().unwrap().insert(key, found.clone());
|
||||
}
|
||||
let _ = tx.send(url);
|
||||
});
|
||||
ImageSearchJob { node_id, rx }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1015,3 +1015,45 @@ fn anonymous_cover_slot_uses_sibling_text_as_query() {
|
|||
slot.query
|
||||
);
|
||||
}
|
||||
|
||||
/// The measured churn: the model rebuilds a section mid-run, the same subject
|
||||
/// searches again, and the session-wide dedup skips the very photo it picked
|
||||
/// the first time — so a real Bali temple photo became a plain blue sky. One
|
||||
/// subject, one photo: a repeat query resolves from the memo, and the dedup
|
||||
/// only ever guards DIFFERENT subjects from sharing a picture.
|
||||
#[test]
|
||||
fn a_repeat_query_gets_the_same_photo_back_not_a_dedup_downgrade() {
|
||||
use super::{query_key, spawn_job, ImageSearchTarget};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let used_urls: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
|
||||
let resolved: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
// The first search already answered "Bali Indonesia" and marked its photo used.
|
||||
let good = "https://example.org/bali-temple.jpg".to_string();
|
||||
resolved
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(query_key("Bali Indonesia"), good.clone());
|
||||
used_urls.lock().unwrap().insert(good.clone());
|
||||
|
||||
// The rebuilt card asks again — differently spelled, same subject.
|
||||
let target = ImageSearchTarget {
|
||||
node_id: op_editor_core::NodeId::new("n99".to_string()),
|
||||
query: " bali indonesia ".to_string(),
|
||||
prompt: None,
|
||||
aspect_ratio: None,
|
||||
width: None,
|
||||
height: None,
|
||||
};
|
||||
let job = spawn_job(target, None, Arc::clone(&used_urls), Arc::clone(&resolved));
|
||||
let answer = job
|
||||
.rx
|
||||
.recv_timeout(std::time::Duration::from_secs(1))
|
||||
.expect("the memo answers without touching the network");
|
||||
assert_eq!(
|
||||
answer,
|
||||
Some(good),
|
||||
"the rebuilt card gets ITS photo back, not the next-best junk result"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue