Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
05c07b4
Removes unused or commented-out code
Tomaz-Vieira Jun 10, 2025
92548ac
[WIP] Implementing support for partial/broken models
Tomaz-Vieira Jun 11, 2025
3261ca7
Revert changes to project_data.rs in favor of using AsPartial
Tomaz-Vieira Jun 12, 2025
3e015ae
[WIP] derive AsPartial kinda works
Tomaz-Vieira Jun 13, 2025
5af01df
Fixes derive of AsPartial to also work on tuples. Impl AsPartial fo m…
Tomaz-Vieira Jun 16, 2025
12e020f
Removes debug write from AsPartial derive
Tomaz-Vieira Jun 16, 2025
932b4c4
impl AsPartial for BoundedString and suffixed_file_ref
Tomaz-Vieira Jun 25, 2025
8b1fef3
impl ASaPartial for Arc<str>, SlashlessString
Tomaz-Vieira Jun 27, 2025
3ff0f4a
derive AsPartial: ignore orioginal attributes
Tomaz-Vieira Jun 30, 2025
573f604
derive AsPartial: impl AsPartial<Partial=Self> for generated PartialS…
Tomaz-Vieira Jun 30, 2025
a4be95b
impl AsPartial for CiteEntry2Msg
Tomaz-Vieira Jun 30, 2025
ed6950b
AsPartial::Partial must implement AsPartial. Adds docs.
Tomaz-Vieira Jun 30, 2025
a6efc13
impl AsPartial for Vec<T>
Tomaz-Vieira Jun 30, 2025
b820c9e
impl AsPartial for NonEmptyList
Tomaz-Vieira Jun 30, 2025
124e3b1
impl AsPartial for serde_json::Map<String, serde_json::Value>
Tomaz-Vieira Jun 30, 2025
9a78d62
impl Aspartial for Orcid
Tomaz-Vieira Jun 30, 2025
b095b28
impl AsPartial for Maintainer
Tomaz-Vieira Jun 30, 2025
5bd8f5e
impl AsPartial for Icon
Tomaz-Vieira Jun 30, 2025
23bec0a
impl AsPartial for Tag
Tomaz-Vieira Jun 30, 2025
a40f9e0
Remove some boilerplate via derive_more
Tomaz-Vieira Jun 30, 2025
8174dce
impl AsPartial for bool
Tomaz-Vieira Jun 30, 2025
47e9b72
impl AsPartial for Version
Tomaz-Vieira Jun 30, 2025
8d9eb84
impl AsPartial for Author2
Tomaz-Vieira Jun 30, 2025
d31f44d
impl Aspartial for RdfTypeModel
Tomaz-Vieira Jun 30, 2025
1abf641
impl AsPartial for NonBatchAxisId
Tomaz-Vieira Jun 30, 2025
5ea5a3a
impl AsPartial for ClipDescr
Tomaz-Vieira Jun 30, 2025
4ea4ea4
impl AsPartial for Binarize
Tomaz-Vieira Jun 30, 2025
da812cb
derive(AsPartial) implements Clone for generated partial struct
Tomaz-Vieira Jun 30, 2025
324bb20
derive(AsPartial): removes original struct attributes for now
Tomaz-Vieira Jun 30, 2025
7f9a896
impl AsPartial for DataType and UintDataType
Tomaz-Vieira Jun 30, 2025
fa3ed71
derive(AsPartial): use #[serde(default)] when available
Tomaz-Vieira Jul 2, 2025
cd02d01
impl AsPartial fopr ScaleLinear*Descr
Tomaz-Vieira Jul 2, 2025
21eada3
impl AsPartial for EnsureDtype
Tomaz-Vieira Jul 2, 2025
5de2864
impls AsPartial for (f32, f32)
Tomaz-Vieira Jul 2, 2025
43e3947
Adds Debug to auto AsPartial
Tomaz-Vieira Jul 4, 2025
b3e67c6
impl AsPartial for preprocessing
Tomaz-Vieira Jul 4, 2025
775c43e
impl AsPartial for postprocessing
Tomaz-Vieira Jul 4, 2025
2adc8e1
Adds missing serde(try_from='serde_json::JsonValue') to pre- and post…
Tomaz-Vieira Jul 4, 2025
f1f633e
impl AsPartial for LiteralInt, LitStr, usize, NonZeroUsize
Tomaz-Vieira Jul 4, 2025
06e35a7
impl AsPartial for SpaceUnit and TimeUnit
Tomaz-Vieira Jul 4, 2025
22ea810
impl AsPartial for axis sizes
Tomaz-Vieira Jul 4, 2025
3592efa
impl AsPartial for input axes
Tomaz-Vieira Jul 4, 2025
3dc6aa7
impl AsPartial for AxisScale, BatchAxis, ChannelAxis, IndexAxis
Tomaz-Vieira Jul 4, 2025
46821fd
[WIP] moving stuff out of the workspace
Tomaz-Vieira Jul 9, 2025
b823f26
[WIP] using aspartial as a separate crate
Tomaz-Vieira Jul 15, 2025
5bdf938
AsPartial errors fixed, compiles again
Tomaz-Vieira Jul 16, 2025
5f5055f
More AsPartial implementations
Tomaz-Vieira Jul 16, 2025
1861e80
[WIP] more aspartial
Tomaz-Vieira Jul 16, 2025
2871866
Impls AsPartial for weights, is compilable again
Tomaz-Vieira Jul 17, 2025
d70d1b3
Removes serde_tokenstream
Tomaz-Vieira Jul 17, 2025
79a2217
Fixes AsPartial implementations
Tomaz-Vieira Jul 21, 2025
1e3c2dc
Implements LocalFileSourceWidgetRawData::from_partial
Tomaz-Vieira Jul 29, 2025
1ccaf0f
Implements from_partial to more project data structs
Tomaz-Vieira Jul 29, 2025
0ee107d
Actually do some emoji validation
Tomaz-Vieira Jul 29, 2025
338485c
spec: use workspace aspartial dependency
Tomaz-Vieira Jul 29, 2025
890dc0c
Remove leftover debug prints
Tomaz-Vieira Jul 29, 2025
f70be7c
Fixes notices not resetting opacity on hover
Tomaz-Vieira Jul 30, 2025
eebfedd
Version::Partial is now VersionMsg instead of String to support float…
Tomaz-Vieira Jul 30, 2025
b559aaf
Format imports in author_widget.rs
Tomaz-Vieira Jul 30, 2025
0339b61
impl Display for rdf::FsPath
Tomaz-Vieira Jul 31, 2025
738cb5e
Adds more restoration form partial file
Tomaz-Vieira Jul 31, 2025
efb0e11
impl Display for VersionMsg
Tomaz-Vieira Aug 1, 2025
8b55cbd
impl restore for weights from partial
Tomaz-Vieira Aug 1, 2025
3b06c28
Go back to using aspartial from crates.io
Tomaz-Vieira Aug 1, 2025
103e20a
Cleanup exit logic. Double exit command discards and quits
Tomaz-Vieira Aug 4, 2025
c21d1da
Handle overlapping fields of pytorch archiveture gracefully
Tomaz-Vieira Aug 4, 2025
2ecaf27
Partial support is complete but untested
Tomaz-Vieira Aug 5, 2025
7cd6d90
More carefully select axis size by inspecting which fields are present
Tomaz-Vieira Aug 5, 2025
b2078ae
Pass in a writeable 'warnings' stream to partial loading
Tomaz-Vieira Aug 12, 2025
20af667
task__build_webapp: Checks execution result of 'trunk'
Tomaz-Vieira Aug 12, 2025
23b7416
notifications: reworks Notification and NotificationWidget
Tomaz-Vieira Aug 12, 2025
5ead8c3
[WIP] comparing partial to raw
Tomaz-Vieira Aug 13, 2025
9e8bc67
Clean up partial recovery
Tomaz-Vieira Aug 19, 2025
a8589f3
Merge branch 'loading_partial'
Tomaz-Vieira Aug 19, 2025
84c396c
Removes leftover test code
Tomaz-Vieira Aug 19, 2025
387ba61
removes unused import
Tomaz-Vieira Aug 19, 2025
56f6c17
Streamlines notice widget, unties fade time from framerate
Tomaz-Vieira Aug 19, 2025
11773a0
Adds warning about importing broken models
Tomaz-Vieira Aug 19, 2025
67d090e
Fix LocalFileSOurceWIdgetRawData::from_partial reading wrong path
Tomaz-Vieira Aug 19, 2025
7eacbaf
Fix model recovery in async contexts
Tomaz-Vieira Aug 26, 2025
a5e2756
Stop hashing file data in file input. Compare data via ptr equality i…
Tomaz-Vieira Aug 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 395 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ url = "2.5.2"
uuid = "1.10.0"
http = "1.1.0"
sha2 = "0.10.9"
aspartial = { version="0.0.4", features = ["iso8601"] }
unic = "0.9.0"


# FIXME: this is from the egui example app
Expand Down
1 change: 1 addition & 0 deletions bioimg_codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ proc-macro = true
proc-macro2 = "1.0.78"
quote = "1.0.35"
syn = { version = "2.0.52", features = ["full"] }
heck = { version = "0.5.0" }
1 change: 1 addition & 0 deletions bioimg_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use proc_macro::TokenStream;
mod str_marker;
mod syn_extensions;
mod restore;
mod serde_attributes;

////////////////////////////////////////////

Expand Down
Empty file.
6 changes: 6 additions & 0 deletions bioimg_codegen/src/syn_extensions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// pub trait IFieldExt{
// fn to_partial_field()
// }


// use quote::quote;

// pub trait AttributeExt {
Expand All @@ -20,3 +25,4 @@
// syn::LitStr::new(&quote!(#self).to_string(), self.span())
// }
// }
//
3 changes: 3 additions & 0 deletions bioimg_gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ bioimg_runtime = {path = "../bioimg_runtime"}
bioimg_zoo = {path = "../bioimg_zoo"}

serde = { workspace = true, features = ["derive"] }
serde_yaml = { workspace = true }
image = { workspace = true }
url = { workspace=true, features = ["serde"] }
uuid = { workspace = true, features = ["v4"] }
Expand All @@ -22,6 +23,7 @@ ndarray = { workspace = true }
ndarray-npy = { workspace = true }
thiserror = { workspace = true }
strum = { workspace = true }
aspartial = { workspace = true }

egui = { version = "0.31.0", features = ["serde"] }
eframe = { version = "0.31.0" }
Expand All @@ -35,6 +37,7 @@ bson = "2.11.0"
indoc = "2.0.5"
itertools = "0.14.0"
sha2 = { workspace = true }
serde_path_to_error = "0.1.17"

# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
Expand Down
160 changes: 123 additions & 37 deletions bioimg_gui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::path::Path;
use std::sync::Arc;
use std::thread::JoinHandle;

use bioimg_runtime::zip_archive_ext::SharedZipArchive;
use bioimg_spec::rdf::model::model_rdf_0_5::PartialModelRdfV0_5;
use bioimg_spec::rdf::model::ModelRdfName;
use bioimg_zoo::collection::ZooNickname;
use indoc::indoc;
Expand All @@ -14,20 +16,19 @@ use bioimg_spec::rdf::ResourceId;
use bioimg_spec::rdf::bounded_string::BoundedString;
use bioimg_spec::rdf::non_empty_list::NonEmptyList;

use crate::project_data::{AppStateRawData, ProjectLoadError};
use crate::project_data::{AppState1RawData, AppStateRawData, ProjectLoadError};
use crate::result::{GuiError, Result, VecResultExt};
use crate::widgets::attachments_widget::AttachmentsWidget;

use crate::widgets::code_editor_widget::MarkdwownLang;
use crate::widgets::collapsible_widget::SummarizableWidget;
use crate::widgets::cover_image_widget::CoverImageItemConf;
// use crate::widgets::cover_image_widget::CoverImageWidget;
use crate::widgets::icon_widget::IconWidgetValue;
use crate::widgets::image_widget_2::SpecialImageWidget;
use crate::widgets::json_editor_widget::JsonObjectEditorWidget;
use crate::widgets::model_interface_widget::ModelInterfaceWidget;
use crate::widgets::model_links_widget::ModelLinksWidget;
use crate::widgets::notice_widget::NotificationsWidget;
use crate::widgets::notice_widget::{Notification, NotificationsWidget};
use crate::widgets::pipeline_widget::PipelineWidget;
use crate::widgets::search_and_pick_widget::SearchAndPickWidget;
use crate::widgets::staging_opt::StagingOpt;
Expand All @@ -46,10 +47,16 @@ use crate::widgets::{
util::group_frame, StatefulWidget,
};

pub struct AppStateFromPartial{
state: AppState1RawData,
warnings: String,
}

#[must_use]
pub enum TaskResult{
Notification(Result<String, String>),
ModelImport(Box<rt::zoo_model::ZooModel>),
PartialModelLoad(AppStateFromPartial),
}

impl TaskResult{
Expand All @@ -61,6 +68,14 @@ impl TaskResult{
}
}

#[derive(Default, Copy, Clone)]
enum ExitingStatus{
#[default]
NotExiting,
Confirming,
Exiting,
}

#[derive(Restore)]
pub struct AppState1 {
pub staging_name: StagingString<ModelRdfName>,
Expand Down Expand Up @@ -103,9 +118,7 @@ pub struct AppState1 {
#[restore_default]
pub notifications_channel: TaskChannel<TaskResult>,
#[restore_default]
close_confirmed: bool,
#[restore_default]
show_confirmation_dialog: bool,
exiting_status: ExitingStatus,
}

impl ValueWidget for AppState1{
Expand Down Expand Up @@ -193,8 +206,7 @@ impl Default for AppState1 {
zoo_model_creation_task: Default::default(),
pipeline_widget: Default::default(),

close_confirmed: false,
show_confirmation_dialog: false,
exiting_status: Default::default(),
}
}
}
Expand Down Expand Up @@ -401,6 +413,30 @@ impl AppState1{
#[cfg(not(target_arch="wasm32"))]
std::thread::spawn(move || smol::block_on(fut));
}

fn load_partial_model(archive: &SharedZipArchive) -> Result<AppStateFromPartial>{
let model_rdf_bytes: Vec<u8> = 'model_rdf: {
for file_name in ["rdf.yaml", "bioimageio.yaml"]{
match archive.read_full_entry(file_name) {
Ok(bytes) => break 'model_rdf bytes,
Err(zip_err) => match zip_err{
zip::result::ZipError::FileNotFound => continue,
err => return Err(GuiError::new(format!("Could not read rdf file: {err}")))
}
};
}
return Err(GuiError::new("Could not find rdf file inside archive"))
};
let yaml_deserializer = serde_yaml::Deserializer::from_slice(&model_rdf_bytes);
let partial: PartialModelRdfV0_5 = ::serde_path_to_error::deserialize(yaml_deserializer)?;
let mut warnings = String::with_capacity(16 * 1024);
let state = AppState1RawData::from_partial(&archive, partial, &mut warnings); //FIXME: retrieve errors and notify
warnings += indoc!("
PLEASE BE AWARE THAT RECOVERING AND THEN RE-EXPORTING A MODEL MIGHT PRODUCE A NEW, VALID MODEL THAT DOES NOT
BEHAVE LIKE THE ORIGINAL\n"
);
Ok(AppStateFromPartial { state, warnings})
}
}


Expand Down Expand Up @@ -430,7 +466,7 @@ impl eframe::App for AppState1 {
let model = match self.create_model(){
Ok(model) => model,
Err(err) => {
self.notifications_widget.push_message(Err(err.to_string()));
self.notifications_widget.push(Notification::error(err.to_string(), None));
return;
}
};
Expand All @@ -452,11 +488,11 @@ impl eframe::App for AppState1 {
return;
}
match packing_task.join().unwrap(){
Ok(nickname) => self.notifications_widget.push_message(
Ok(format!("Model successfully uploaded: {nickname}"))
Ok(nickname) => self.notifications_widget.push(
Notification::info(format!("Model successfully uploaded: {nickname}"), None)
),
Err(upload_err) => self.notifications_widget.push_message(
Err(format!("Could not upload model: {upload_err}"))
Err(upload_err) => self.notifications_widget.push(
Notification::error(format!("Could not upload model: {upload_err}"), None)
),
};
})});
Expand Down Expand Up @@ -493,6 +529,43 @@ impl eframe::App for AppState1 {
sender.send(message).unwrap();
}
}
if ui.button("♻📦⤴ Recover Model")
.on_hover_text(
"Import data from a model .zip archive that is potentially broken or incompatible with this application"
)
.clicked()
{
ui.close_menu();
let sender = self.notifications_channel.sender().clone();
let fut = async move {
// On web, file picker is always async, so we make a future.
// Also, we don't want to pass in an entire Arc<Mutex<App>> to the future,
// so it gets a sender: Sender<TaskResult> instead to report its result back.
let Some(handle) = rfd::AsyncFileDialog::new().add_filter("bioimage model", &["zip"]).pick_file().await else {
return
};
// #[cfg(target_arch="wasm32")]
let archive = SharedZipArchive::from_raw_data(handle.read().await, handle.file_name());
// #[cfg(not(target_arch="wasm32"))]
// let archive = match SharedZipArchive::open(handle.path()){
// Ok(archive) => archive,
// Err(e) => {
// let err = Err(format!("Could not open {}: {e}", handle.path().to_string_lossy()));
// _ = sender.send(TaskResult::Notification(err));
// return
// }
// };
let message = match Self::load_partial_model(&archive) {
Err(err) => TaskResult::Notification(Err(format!("Could not recover model: {err}"))),
Ok(state_from_partial) => TaskResult::PartialModelLoad(state_from_partial),
};
sender.send(message).unwrap();
};
#[cfg(target_arch="wasm32")]
wasm_bindgen_futures::spawn_local(fut);
#[cfg(not(target_arch="wasm32"))]
std::thread::spawn(move || smol::block_on(fut));
}
#[cfg(not(target_arch="wasm32"))]
if ui.button("🗊⤵ Save Draft ")
.on_hover_text("Save your current work as-is, even with unresolved errors")
Expand All @@ -503,7 +576,7 @@ impl eframe::App for AppState1 {
break 'save_project;
};
let result = self.save_project(&path);
self.notifications_widget.push_message(result);
self.notifications_widget.push(result.into());
}}
#[cfg(not(target_arch="wasm32"))]
if ui.button("🗊⤴ Load Draft")
Expand All @@ -515,7 +588,7 @@ impl eframe::App for AppState1 {
break 'load_project;
};
if let Err(err) = self.load_project(&path){
self.notifications_widget.push_message(Err(err));
self.notifications_widget.push(Notification::error(err, None));
}
}}
});
Expand All @@ -530,8 +603,12 @@ impl eframe::App for AppState1 {
egui::CentralPanel::default().show(ctx, |ui| {
while let Ok(msg) = self.notifications_channel.receiver().try_recv(){
match msg{
TaskResult::Notification(msg) => self.notifications_widget.push_message(msg),
TaskResult::Notification(msg) => self.notifications_widget.push(msg.into()),
TaskResult::ModelImport(model) => self.set_value(*model),
TaskResult::PartialModelLoad(AppStateFromPartial{state, warnings}) => {
self.restore(state);
self.notifications_widget.push(Notification::warning(warnings, None));
}
}
}
if let Some(error_rect) = self.notifications_widget.draw(ui, egui::Id::from("messages_widget")){
Expand Down Expand Up @@ -766,50 +843,59 @@ impl eframe::App for AppState1 {
if save_button_clicked {
match self.create_model(){
Ok(zoo_model) => self.launch_model_saving(zoo_model),
Err(err) => self.notifications_widget.push_gui_error(
GuiError::new(format!("Could not create zoo model: {err}"))
Err(err) => self.notifications_widget.push(
Notification::error(format!("Could not create zoo model: {err}"), None)
),
}
}
});
});

if ctx.input(|i| i.viewport().close_requested()) {
if self.close_confirmed {
// do nothing - we will close
} else {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
self.show_confirmation_dialog = true;
}
}
let close_requested = ctx.input(|i| i.viewport().close_requested());
self.exiting_status = match self.exiting_status {
ExitingStatus::NotExiting => {
if close_requested {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
ExitingStatus::Confirming
} else {
ExitingStatus::NotExiting
}
},
ExitingStatus::Confirming => {
if close_requested {
ExitingStatus::Exiting
} else {
ExitingStatus::Confirming
}
},
ExitingStatus::Exiting => {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
ExitingStatus::Exiting
},
};

#[cfg(not(target_arch="wasm32"))]
if self.show_confirmation_dialog {
if matches!(self.exiting_status, ExitingStatus::Confirming) {
egui::Modal::new(egui::Id::from("confirmation dialog"))
.show(ctx, |ui| {
ui.label("Save draft before quitting?");
ui.horizontal(|ui| {
if ui.button("Yes 💾").clicked() || ui.input(|i| i.key_pressed(egui::Key::Enter)){ 'save_draft: {
self.show_confirmation_dialog = false;
let Some(path) = rfd::FileDialog::new().set_file_name("MyDraft.bmb").save_file() else {
self.exiting_status = ExitingStatus::NotExiting;
break 'save_draft;
};
let result = self.save_project(&path);
if result.is_ok(){
self.show_confirmation_dialog = false;
self.close_confirmed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
self.exiting_status = ExitingStatus::Exiting;
}
self.notifications_widget.push_message(result);
self.notifications_widget.push(result.into());
}}
if ui.button("No 🗑").clicked() {
self.show_confirmation_dialog = false;
self.close_confirmed = true;
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
self.exiting_status = ExitingStatus::Exiting;
}
if ui.button("Cancel 🗙").clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
self.show_confirmation_dialog = false;
self.close_confirmed = false;
self.exiting_status = ExitingStatus::NotExiting;
}
});
});
Expand Down
Loading
Loading