Browse / github.com/storytold/lightcraft / patch
LR-MASK-OBJECTS: recover from SAM worker accept errors
LR-MASK-OBJECTS: recover from SAM worker accept errors ## Summary The SAM3 remote worker now survives transient TCP accept errors by logging the failure, waiting 100 ms, and continuing to accept clients. This covers only the accept-error subproblem in #2574 (`Refs #2574`); it does not change the issue's separate timeout concerns. ## Root cause `crates/segment/src/remote.rs::serve` propagated every `listener.incoming()` error with `?`. A single accept failure therefore returned from `serve`, dropped the listener, and stopped the worker. Errors and panics inside client sessions were already contained and remain so. ## Changes - Extracted a private
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- ronibalthazar (imported from pull request #3023, open)
- Base
8020cd081ce9ccd5faf87072d277c4eb966a9572- Size
- +152 −33 in 24 files
- Review
- pending
- Shared
- 2026-10-28
- Id
0b9ce0088cd9a595ad6d8b5b
How it went
No other operator has reported using it yet.
Diff
raw · apps/lightcraft/src/native_menu.rs crates/segment/src/remote.rs crates/ui-egui/locales/cs.json crates/ui-egui/locales/de.json crates/ui-egui/locales/es.json crates/ui-egui/locales/fi.json crates/ui-egui/locales/fr.json crates/ui-egui/locales/id.json crates/ui-egui/locales/it.json crates/ui-egui/locales/ja.json crates/ui-egui/locales/ka.json crates/ui-egui/locales/pl.json crates/ui-egui/locales/pt-br.json crates/ui-egui/locales/ru.json crates/ui-egui/locales/sv.json crates/ui-egui/locales/uk.json crates/ui-egui/locales/vi.json crates/ui-egui/locales/zh-hans.json crates/ui-egui/locales/zh-hant.json crates/ui-egui/src/panels/compare.rs docs/ai-masks.md docs/gaps.md docs/parity-checklist.md docs/whats-new.md
@@ -489,9 +489,7 @@ impl NativeMenu { // events never reach `shortcuts::handle`, so this is the one guard for AppKit's // accelerators and clicks: while the blocking library window or a quit/settings notice // owns input, a queued command is dropped (Quit stays for the existing confirmation).- let (cmd, params) = if key == QUIT {- (key, serde_json::json!({}))- } else if key == "app.about" || key == SETTINGS {+ let (cmd, params) = if key == QUIT || key == "app.about" || key == SETTINGS { (key, serde_json::json!({})) } else if let Some(it) = self.items.get(&key) { (it.cmd.clone(), it.params.clone())@@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; const CONTROL_LIMIT: usize = 16 * 1024; const RESPONSE_LIMIT: usize = 4 * 1024 * 1024; const TIMEOUT: Duration = Duration::from_secs(9 * 60);+const ACCEPT_RETRY_DELAY: Duration = Duration::from_millis(100); pub const ENV: &str = "LIGHTCRAFT_SAM3_REMOTE"; #[derive(Serialize, Deserialize)]@@ -185,6 +186,30 @@ fn serve_client(stream: &mut TcpStream, dir: &Path, device: candle_core::Device) } } +fn serve_connections<I, C, H, S>(incoming: I, mut handle_client: H, mut retry_sleep: S)+where+ I: IntoIterator<Item = io::Result<C>>,+ H: FnMut(&mut C) -> Result<(), String>,+ S: FnMut(Duration),+{+ for connection in incoming {+ let mut stream = match connection {+ Ok(stream) => stream,+ Err(error) => {+ eprintln!("SAM3 accept failed: {error}");+ retry_sleep(ACCEPT_RETRY_DELAY);+ continue;+ }+ };+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handle_client(&mut stream)));+ match result {+ Ok(Err(error)) => eprintln!("SAM3 session closed: {error}"),+ Err(_) => eprintln!("SAM3 session panicked; model released"),+ Ok(Ok(())) => {}+ }+ }+}+ pub fn serve(address: SocketAddr, dir: &Path, device: candle_core::Device) -> Result<(), String> { if !address.ip().is_loopback() || !device.is_metal() { return Err("SAM worker requires a loopback address and a Metal device".into());@@ -194,14 +219,97 @@ pub fn serve(address: SocketAddr, dir: &Path, device: candle_core::Device) -> Re } let listener = TcpListener::bind(address).map_err(|e| e.to_string())?; eprintln!("SAM3 worker listening on {address}; device=Metal; idle unload=600s");- for stream in listener.incoming() {- let mut stream = stream.map_err(|e| e.to_string())?;- let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| serve_client(&mut stream, dir, device.clone())));- match result {- Ok(Err(error)) => eprintln!("SAM3 session closed: {error}"),- Err(_) => eprintln!("SAM3 session panicked; model released"),- Ok(Ok(())) => {}- }- }+ serve_connections(listener.incoming(), |stream| serve_client(stream, dir, device.clone()), std::thread::sleep); Ok(()) }++#[cfg(test)]+mod tests {+ use super::*;++ fn accept_error<T>() -> io::Result<T> {+ Err(io::Error::new(io::ErrorKind::ConnectionAborted, "injected accept failure"))+ }++ #[test]+ fn sam_accept_error_before_client_keeps_serving() {+ let mut handled = Vec::new();+ serve_connections(+ [accept_error(), Ok(1_u8)],+ |stream| {+ handled.push(*stream);+ Ok(())+ },+ |_| {},+ );++ assert_eq!(handled, [1]);+ }++ #[test]+ fn sam_accept_error_between_clients_keeps_serving() {+ let mut handled = Vec::new();+ serve_connections(+ [Ok(1_u8), accept_error(), Ok(2_u8)],+ |stream| {+ handled.push(*stream);+ Ok(())+ },+ |_| {},+ );++ assert_eq!(handled, [1, 2]);+ }++ #[test]+ fn sam_accept_repeated_errors_back_off() {+ let mut handled = Vec::new();+ let mut delays = Vec::new();+ serve_connections(+ [accept_error(), accept_error(), Ok(1_u8)],+ |stream| {+ handled.push(*stream);+ Ok(())+ },+ |delay| delays.push(delay),+ );++ assert_eq!(handled, [1]);+ assert_eq!(delays, [Duration::from_millis(100), Duration::from_millis(100)]);+ }++ #[test]+ fn sam_accept_client_failure_keeps_serving() {+ let mut handled = Vec::new();+ let mut delays = Vec::new();+ serve_connections(+ [Ok(1_u8), Ok(2_u8)],+ |stream| {+ handled.push(*stream);+ if *stream == 1 { Err("injected client failure".into()) } else { Ok(()) }+ },+ |delay| delays.push(delay),+ );++ assert_eq!(handled, [1, 2]);+ assert!(delays.is_empty());+ }++ #[test]+ fn sam_accept_client_panic_keeps_serving() {+ let mut handled = Vec::new();+ serve_connections(+ [Ok(1_u8), Ok(2_u8)],+ |stream| {+ handled.push(*stream);+ if *stream == 1 {+ std::panic::resume_unwind(Box::new("injected client panic"));+ }+ Ok(())+ },+ |_| {},+ );++ assert_eq!(handled, [1, 2]);+ }+}@@ -1630,7 +1630,7 @@ "Revert": "Vrátit změny", "Review": "Zkontrolovat", "Review Import": "Zkontrolovat import",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Pravé tlačítko na fotografii → Nastavit jako referenční fotografii, pak Zobrazení → Referenční zobrazení (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Pravé tlačítko na fotografii → Nastavit jako referenční fotografii, pak Zobrazení → Referenční zobrazení (Shift+R)", "Right-click a preset to delete it": "Přednastavení odstraníte kliknutím pravým tlačítkem", "Rights Usage Terms": "Podmínky použití práv", "Rotate": "Otočit",@@ -1295,7 +1295,7 @@ "Revert": "Zurück zum letzten Stand", "Review": "Überprüfung", "Review Import": "Import überprüfen",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Rechtsklick auf ein Foto → Als Referenzfoto festlegen, dann Ansicht → Referenzansicht (Umschalt+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Rechtsklick auf ein Foto → Als Referenzfoto festlegen, dann Ansicht → Referenzansicht (Umschalt+R)", "Right-click a preset to delete it": "Rechtsklick auf eine Vorgabe, um sie zu löschen", "Rights Usage Terms": "Nutzungsbedingungen", "Rotate": "Drehen",@@ -1302,7 +1302,7 @@ "Revert": "Revertir", "Review": "Revisión", "Review Import": "Revisar importación",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clic derecho en una foto → Definir como foto de referencia y, después, Vista → Vista de referencia (Mayús+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clic derecho en una foto → Definir como foto de referencia y, después, Vista → Vista de referencia (Mayús+R)", "Right-click a preset to delete it": "Clic derecho en un ajuste preestablecido para eliminarlo", "Rights Usage Terms": "Condiciones de uso de los derechos", "Rotate": "Rotar",@@ -1677,7 +1677,7 @@ "Review": "Tarkistus", "Review Import": "Tarkista tuonti", "Right": "Oikea",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Napsauta kuvaa hiiren kakkospainikkeella → Aseta vertailukuvaksi, sitten Näytä → Vertailunäkymä (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Napsauta kuvaa hiiren kakkospainikkeella → Aseta vertailukuvaksi, sitten Näytä → Vertailunäkymä (Shift+R)", "Right-click a preset to delete it": "Poista esiasetus napsauttamalla sitä hiiren kakkospainikkeella", "Rights Usage Terms": "Oikeuksien käyttöehdot", "Rotate": "Kierrä",@@ -1230,7 +1230,7 @@ "Revert": "Rétablir l'original", "Review": "Révision", "Review Import": "Vérifier l'importation",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clic droit sur une photo → Définir comme photo de référence, puis Affichage → Vue de référence (Maj+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clic droit sur une photo → Définir comme photo de référence, puis Affichage → Vue de référence (Maj+R)", "Right-click a preset to delete it": "Clic droit sur un préréglage pour le supprimer", "Rights Usage Terms": "Conditions d'utilisation des droits", "Rotate": "Pivoter",@@ -1668,7 +1668,7 @@ "Review": "Tinjauan", "Review Import": "Tinjau impor", "Right": "Kanan",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Klik kanan foto → Jadikan foto referensi, lalu Tampilan → Tampilan referensi (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Klik kanan foto → Jadikan foto referensi, lalu Tampilan → Tampilan referensi (Shift+R)", "Right-click a preset to delete it": "Klik kanan preset untuk menghapusnya", "Rights Usage Terms": "Ketentuan penggunaan", "Rotate": "Putar",@@ -1266,7 +1266,7 @@ "Revert": "Ripristina", "Review": "Revisione", "Review Import": "Rivedi importazione",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clic destro su una foto → Imposta come foto di riferimento, poi Visualizza → Vista di riferimento (Maiusc+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clic destro su una foto → Imposta come foto di riferimento, poi Visualizza → Vista di riferimento (Maiusc+R)", "Right-click a preset to delete it": "Clic destro su un preset per eliminarlo", "Rights Usage Terms": "Condizioni d'uso dei diritti", "Rotate": "Ruota",@@ -1180,7 +1180,7 @@ "Reverse the order": "逆順にする", "Review": "レビュー", "Review Import": "読み込み内容を確認",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "写真を右クリックして参照写真に設定し、「表示 → 参照表示」(⇧R)を選択してください",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "写真を右クリックして参照写真に設定し、「表示 → 参照表示」(⇧R)を選択してください", "Right-click a preset to delete it": "プリセットを右クリックして削除", "Rights Usage Terms": "利用条件", "Rotate": "回転",@@ -1754,7 +1754,7 @@ "Right-click a folder (or a disk other than the startup disk) in the sidebar's Folders section ▸ Synchronize": "მარჯვენა დაწკაპუნება საქაღალდეზე (ან სასტარტოს გარდა სხვა დისკზე) გვერდითი ზოლის საქაღალდეების სექციაში → საქაღალდის", "Right-click a folder ▸ New ▸ Create Album… / Create Smart Album… / Create Smart Album from Filter… / Create Folder…": "მარჯვენა დაწკაპუნება საქაღალდეზე → ახალი → ალბომის შექმნა… / სმარტ-ალბომის შექმნა… / სმარტ-ალბომის შექმნა ფილტრიდან… / საქაღალდის შექმნა…", "Right-click a photo ▸ Edit In ▸ Edit in PhotoCraft (or Photo ▸ Edit In, ⇧⌘E) renders a copy with your edits, stacks it": "მარჯვენა დაწკაპუნება ფოტოზე → რედაქტირება სხვა პროგრამაში → რედაქტირება PhotoCraft-ში (ან ფოტო → რედაქტირება სხვა პროგრამაში, ⇧⌘E) არენდერებს ასლს თქვენი ცვლილებებით, ათავსებს მას",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "დაწკაპეთ ფოტოზე მარჯვენა ღილაკით → ეტალონურ ფოტოდ დაყენება, შემდეგ ხედი → ეტალონური ხედი (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "დაწკაპეთ ფოტოზე მარჯვენა ღილაკით → ეტალონურ ფოტოდ დაყენება, შემდეგ ხედი → ეტალონური ხედი (Shift+R)", "Right-click a preset to delete it": "პრესეტის წასაშლელად დაწკაპეთ მასზე მარჯვენა ღილაკით", "Right-click ▸ Put New Keywords Inside This Keyword: new keywords, made with + or typed in the box above, go inside": "მარჯვენა დაწკაპუნება → ახალი საკვანძო სიტყვების ამ საკვანძო სიტყვაში ჩასმა: + ღილაკით ან ზემოთ ველში აკრეფილი ახალი საკვანძო სიტყვები", "Rights Usage Terms": "უფლებების გამოყენების პირობები",@@ -1205,7 +1205,7 @@ "Revert": "Cofnij zmiany", "Review": "Przegląd", "Review Import": "Sprawdź import",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Kliknij zdjęcie prawym przyciskiem → Ustaw jako zdjęcie referencyjne, a potem Widok → Widok referencyjny (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Kliknij zdjęcie prawym przyciskiem → Ustaw jako zdjęcie referencyjne, a potem Widok → Widok referencyjny (Shift+R)", "Right-click a preset to delete it": "Kliknij ustawienie wstępne prawym przyciskiem, aby je usunąć", "Rights Usage Terms": "Warunki korzystania z praw", "Rotate": "Obróć",@@ -1253,7 +1253,7 @@ "Revert": "Reverter", "Review": "Revisão", "Review Import": "Revisar importação",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Clique com o botão direito em uma foto → Definir como foto de referência, depois Exibir → Exibição de referência (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Clique com o botão direito em uma foto → Definir como foto de referência, depois Exibir → Exibição de referência (Shift+R)", "Right-click a preset to delete it": "Clique com o botão direito em uma predefinição para excluí-la", "Rights Usage Terms": "Termos de uso dos direitos", "Rotate": "Girar",@@ -1040,7 +1040,7 @@ "Retouch": "Ретушь", "Revert": "Вернуть", "Review Import": "Проверить импорт",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Щёлкните фото правой кнопкой → Назначить эталонным фото, затем Вид → Режим эталона (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Щёлкните фото правой кнопкой → Назначить эталонным фото, затем Вид → Режим эталона (Shift+R)", "Right-click a preset to delete it": "Щёлкните пресет правой кнопкой, чтобы удалить его", "Rights Usage Terms": "Условия использования прав", "Rotate": "Повернуть",@@ -1826,7 +1826,7 @@ "Review": "Granskning", "Review Import": "Granska import", "Right": "Höger",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Högerklicka på ett foto → Ange som referensfoto, sedan Visa → Referensvy (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Högerklicka på ett foto → Ange som referensfoto, sedan Visa → Referensvy (Shift+R)", "Right-click a preset to delete it": "Högerklicka på en förinställning för att radera den", "Rights Usage Terms": "Villkor för användningsrätt", "Rotate": "Rotera",@@ -1273,7 +1273,7 @@ "Restore them": "Відновити їх", "Retouch": "Ретушування", "Review Import": "Перевірити імпорт",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Клацніть фото правою кнопкою → Задати як еталонне фото, потім Вигляд → Еталонне подання (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Клацніть фото правою кнопкою → Задати як еталонне фото, потім Вигляд → Еталонне подання (Shift+R)", "Right-click a preset to delete it": "Клацніть набір налаштувань правою кнопкою, щоб видалити його", "Rights Usage Terms": "Умови використання прав", "Rotate Crop": "Обертати кадрування",@@ -1543,7 +1543,7 @@ "Revert": "Hoàn tác", "Review": "Xem lại", "Review Import": "Xem lại nhập",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "Nhấp phải ảnh → Đặt làm ảnh tham chiếu, rồi Xem → Chế độ tham chiếu (Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "Nhấp phải ảnh → Đặt làm ảnh tham chiếu, rồi Xem → Chế độ tham chiếu (Shift+R)", "Right-click a preset to delete it": "Nhấp phải thiết lập sẵn để xóa", "Rights Usage Terms": "Điều khoản sử dụng quyền", "Rotate": "Xoay",@@ -1237,7 +1237,7 @@ "Revert": "恢复", "Review": "审阅", "Review Import": "查看导入内容",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "右键单击照片 » 设为参考照片,然后选择“视图 → 参考视图”(Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "右键单击照片 » 设为参考照片,然后选择“视图 → 参考视图”(Shift+R)", "Right-click a preset to delete it": "右键单击预设即可将其删除", "Rights Usage Terms": "使用条款", "Rotate": "旋转",@@ -1238,7 +1238,7 @@ "Revert": "回復至上次儲存", "Review": "審閱", "Review Import": "檢視匯入",- "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)": "在照片上按右鍵 → 設為參考照片,再選擇「檢視 → 參考檢視」(Shift+R)",+ "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)": "在照片上按右鍵 → 設為參考照片,再選擇「檢視 → 參考檢視」(Shift+R)", "Right-click a preset to delete it": "在預設集上按右鍵即可刪除", "Rights Usage Terms": "使用權利條款", "Rotate": "旋轉",@@ -601,7 +601,7 @@ pub fn show_reference(app: &mut LightcraftApp, ui: &mut egui::Ui) { let canvas = area_and_filmstrip(app, ui); let reference = app.ui.reference.map(PhotoId).filter(|r| live(app, *r)); let (Some(r), Some(active)) = (reference, app.session.active()) else {- super::empty_message(ui, canvas, "No reference photo", "Right-click a photo ▸ Set as Reference Photo, then View → Reference View (Shift+R)");+ super::empty_message(ui, canvas, "No reference photo", "Right-click a photo → Set as Reference Photo, then View → Reference View (Shift+R)"); return; }; let area = canvas.shrink(18.0);@@ -71,6 +71,8 @@ stay in the local library, so later adjustments and exports work without the con The worker requires Metal and accepts one editing connection at a time. It loads weights on demand and releases them when the client disconnects or remains idle for ten minutes.+If the listener reports an accept error, the worker logs it, waits 100 ms, and retries; startup+validation and bind errors still stop the worker. Both endpoints require loopback addresses: SSH provides authentication and encryption. The listener has no independent authentication; do not expose it through a public proxy. Any process of any user on the worker Mac can connect to it, and on the editing computer a local@@ -1,6 +1,6 @@ # Gaps: where LightCraft falls short of Lightroom -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-26 · **Change:** patch (LR-MASK-OBJECTS: remote SAM worker recovers from listener accept errors; gap ranking and counts unchanged) Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is@@ -84,6 +84,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w | Date | Change | Summary | |---|---|---|+| 2026-10-26 | patch | LR-MASK-OBJECTS: remote SAM worker logs and retries listener accept errors after 100 ms (#2574); gap ranking and counts unchanged | | 2026-10-24 | minor | Gap 28: ⌘⌥⇧C / ⌘⌥⇧V (Ctrl+Alt+Shift+C / V on Windows and Linux) copy and paste metadata; no key leaves the entry; hours unchanged | | 2026-10-24 | minor | Gap 30 shrinks: the Info panel's Show Metadata For (Target Photo / Selected Photos, `view.infoSelectedPhotos`) shows the selected photos' shared values and marks fields that differ as Mixed; the All Metadata dialog remains; 1–3 h (was 4–8) | | 2026-10-23 | minor | Gap 13: before a Lightroom catalog import the app asks whether it merges into the open library or goes into a new one (opt-out in Settings, #2672); hours unchanged |@@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** patch (LR-MASK-OBJECTS: remote SAM worker retries listener accept errors after 100 ms, #2574; feature counts unchanged) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good@@ -293,7 +293,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-MASK-SUBJECT | Select subject | P2 | 🟡 | `cmd:mask.add` (`subject`), `crates/pipeline/src/masks.rs` | saliency heuristic, no segmentation model; a photo that carries person mattes (DNG semantic masks: iPhone ProRAW portrait / skin / hair / teeth / glasses mattes) uses them instead (synthetic files only: no corpus file carries person mattes) | | LR-MASK-SKY | Select sky | P2 | 🟡 | `cmd:mask.add` (`sky`), `crates/pipeline/src/masks.rs` (`Mattes`), `crates/raw/src/semantic.rs` | heuristic; a photo that carries a sky matte (DNG semantic mask, e.g. iPhone ProRAW) uses it instead, through orientation / crop / lens corrections, preview = export (checked on one CC0 iPhone 12 Pro ProRAW and synthetic files) | | LR-MASK-BACKGROUND | Select background | P2 | 🟡 | `cmd:mask.add` (`background`) | inverse of the subject mask (heuristic, or the photo's person mattes) |-| LR-MASK-OBJECTS | Object selection | P2 | 🟡 | `cmd:mask.add` (`object`, `prompt`), `cmd:mask.objectPoint`, `cmd:mask.refineDetail`, `cmd:segment.prepare`, `cmd:segment.model.status`, `cmd:segment.model.download`, `cmd:segment.model.cancel`, `cmd:app.openSamModelFolder`, `crates/segment`, `crates/engine/src/segment/mod.rs` | SAM 3 in pure Rust (candle; Metal on macOS, CPU elsewhere): Object tile → click to include, ⌥-click to leave out; Describe tile → a text prompt selects every instance ("sky", "the red car"); both also as Add/Subtract/Intersect components; + / − under the selected mask; comma lists (`car, road`); a zoomed-in detail pass for 5–10× finer edges on small objects; per-selection Edge (hard ↔ soft). The model runs on its own worker thread (the UI never waits; panics become errors; a job that fails on the Mac's GPU runs again on the CPU, which then serves the rest of the session, #1974), is unloaded after 10 min idle, and is optional: the segmentation is stored with the mask (288² logits), so renders and exports never need it. Weights are never bundled (SAM License): without them the app offers a consented background download (mirrors, resume, timeouts, SHA-256) — **but no default download location is configured yet** (users set `LIGHTCRAFT_SAM3_MIRRORS` or install by hand: the dialog names the three files, opens the configured model folder with the host opener, preserves setup on an open error, and links docs/ai-masks.md). Not verified against Lightroom's Select Object; no brush/box object mode; the first click on a photo waits for its analysis (~4 s on an M4 Pro, much longer on CPU) |+| LR-MASK-OBJECTS | Object selection | P2 | 🟡 | `cmd:mask.add` (`object`, `prompt`), `cmd:mask.objectPoint`, `cmd:mask.refineDetail`, `cmd:segment.prepare`, `cmd:segment.model.status`, `cmd:segment.model.download`, `cmd:segment.model.cancel`, `cmd:app.openSamModelFolder`, `crates/segment`, `crates/engine/src/segment/mod.rs` | SAM 3 in pure Rust (candle; Metal on macOS, CPU elsewhere): Object tile → click to include, ⌥-click to leave out; Describe tile → a text prompt selects every instance ("sky", "the red car"); both also as Add/Subtract/Intersect components; + / − under the selected mask; comma lists (`car, road`); a zoomed-in detail pass for 5–10× finer edges on small objects; per-selection Edge (hard ↔ soft). The model runs on its own worker thread (the UI never waits; panics become errors; a job that fails on the Mac's GPU runs again on the CPU, which then serves the rest of the session, #1974), is unloaded after 10 min idle. The remote worker logs listener accept errors, waits 100 ms, and retries; failed or panicking client sessions stay isolated (#2574). The model is optional: the segmentation is stored with the mask (288² logits), so renders and exports never need it. Weights are never bundled (SAM License): without them the app offers a consented background download (mirrors, resume, timeouts, SHA-256) — **but no default download location is configured yet** (users set `LIGHTCRAFT_SAM3_MIRRORS` or install by hand: the dialog names the three files, opens the configured model folder with the host opener, preserves setup on an open error, and links docs/ai-masks.md). Not verified against Lightroom's Select Object; no brush/box object mode; the first click on a photo waits for its analysis (~4 s on an M4 Pro, much longer on CPU) | | LR-MASK-PEOPLE | People parts | P2 | ⬜ | | shape exists (no UI): uses the photo's person / skin / hair / teeth / glasses mattes when it carries them (DNG semantic masks), else the subject heuristic | | LR-MASK-LANDSCAPE | Landscape classes | P2 | ⬜ | | shape exists, evaluates empty | | LR-MASK-BRUSH | Brush mask | P0 | 🟡 | `cmd:tool.brush`, `cmd:mask.brushStroke` (`autoMask`), `crates/pipeline/src/masks.rs`, `crates/ui-egui/src/brush_live.rs` | size/feather/flow/density/erase; Create New Mask starts a separate brush in paint mode; the stroke shows while the button is held with the committed stroke's dab spacing, feather and flow, so fast strokes have no gaps (#517); coverage builds up by passes, not hold time (holding the brush still adds nothing, live or committed, #518); Auto Mask: dabs weighted by similarity to the colour under the dab centre, refined by a guided filter on luminance (CPU + GPU); ⇧-click draws a straight stroke from where the last stroke ended (`connect`), tapering size, feather and flow from that stroke's values when they changed; no A/B brushes; pen pressure (Brush ▸ Pen Pressure, on by default; `pressure` per point in `mask.brushStroke`) scales each dab's size (a tenth to all of it) and flow, live and committed alike, where winit reports a pen force (Windows; not macOS / Linux tablets yet); no tilt |@@ -799,6 +799,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | Date | Change | Summary | |---|---|---|+| 2026-10-26 | patch | LR-MASK-OBJECTS: remote SAM worker logs and retries listener accept errors after 100 ms; client failures remain isolated (#2574); feature counts unchanged | | 2026-10-24 | minor | KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 (`keyword.nextSet`, `keyword.previousSet`) go to the next / previous keyword set; status counts unchanged (KEYC-METADATA stays 🟡) | | 2026-10-24 | minor | KEYC-METADATA: ⌘K / Ctrl+K (`panel.addKeywords`, Add Keywords) shows the Keywords panel, opens Keywording and puts the cursor in the Add keyword box; status counts unchanged (KEYC-METADATA stays 🟡) | | 2026-10-24 | patch | LR-LIB-RATING, LR-LIB-DELETE: a slider drag still open when a rating, flag or label takes the photo out of the view, or when the photo is deleted, ends as its own undo step before the selection moves on, as moving to the next photo does; LR-BEHAV-LOCALIZE lists all 18 languages; status unchanged |@@ -1,5 +1,14 @@ # What's new in LightCraft +## 2026-10-26++### Remote SAM worker recovers from accept errors+- A transient connection-accept error is logged and retried after a short pause, so one failed accept no longer+ stops the remote Metal worker (#2574). Startup validation and bind failures still report an error.++### Reference View's empty state paints on every platform+- Its instruction now uses the supported menu arrow symbol, avoiding a missing-glyph panic while the view is drawn.+ ## 2026-10-24 ### Relative-date filters and smart albums follow the clock