def build(self) -> widgets.Widget:
"""Build and return the prediction setup layout.
Returns:
The root VBox widget for the setup screen.
"""
self._out = widgets.Output(
layout=widgets.Layout(
border="1px solid white",
height="120px",
background_color=BG_COLOR,
overflow="auto",
),
)
header = HeaderWidget(self.app, self._out, show_back_button=True)
# ── Model chooser ────────────────────────────────────────
model_dir, model_name = initial_file(
self._cfg.model_path,
purpose="model_chooser",
is_shipped_default=BackendInterface.is_shipped_default_path(
"model_path", self._cfg.model_path
),
)
self._model_chooser = AMFileChooser(
path=model_dir,
filename=model_name,
select_default=bool(model_name),
filter_pattern="*.safetensors",
title="Model checkpoint",
purpose="model_chooser",
)
self._model_chooser.register_callback(self._on_model_change)
# ── Search folder chooser ────────────────────────────────
# Resolve once so auto-validation counts the same dir the
# chooser displays — see TrainingSetupScreen.build for the
# rationale (#429 follow-up).
self._search_initial_path = initial_dir(
self._cfg.prediction_search_dir, purpose="search_chooser"
)
search_has_default = bool(
self._search_initial_path and os.path.isdir(self._search_initial_path)
)
self._search_chooser = AMFileChooser(
path=self._search_initial_path,
show_only_dirs=True,
select_default=search_has_default,
title="Search folder (images to scan)",
purpose="search_chooser",
)
self._search_chooser.register_callback(self._on_search_change)
# ── Output folder chooser ────────────────────────────────
# Decide on the same path the chooser displays, as the search chooser
# above does: deriving the flag from cfg while showing the resolved dir
# leaves Start disabled next to a perfectly valid folder. Only a real
# resolution may be pre-selected: the cwd fallback is merely where the
# chooser opens, and selecting it would make ``_on_start`` write the
# cwd over the configured ``cfg.output_dir``.
self._output_resolved_path = resolved_dir(self._cfg.output_dir, purpose="output_chooser")
self._output_chooser = AMFileChooser(
path=self._output_resolved_path or os.getcwd(),
show_only_dirs=True,
select_default=self._output_resolved_path is not None,
title="Output folder (results destination)",
purpose="output_chooser",
)
self._output_chooser.register_callback(self._on_output_change)
# ── Preview grid ──────────────────────────────────────────
self._preview_grid = PreviewGrid()
# ── Normalisation overview (read-only) ────────────────────
# Prediction must reuse the model's training normalisation (the
# subprocess overrides its decode pipeline from the checkpoint), so
# this is a read-only summary, not an editable control. Populated
# when a model is selected.
self._norm_overview = widgets.HTML(value=self._render_norm_overview())
# ── Validation panel + start button ──────────────────────
self._validation_html = widgets.HTML(
value=self._render_validation(),
layout=widgets.Layout(padding="8px 0"),
)
self._start_btn = widgets.Button(
description="Start Prediction",
button_style="success",
disabled=True,
layout=widgets.Layout(width="200px", height="40px"),
style={"font_size": "14px"},
)
self._start_btn.on_click(lambda _: self._on_start())
# ── Layout ───────────────────────────────────────────────
# Left: file choosers (constrained so preview gets space)
left_panel = VBox(
[
self._model_chooser,
self._search_chooser,
self._output_chooser,
],
layout=widgets.Layout(
background_color=BG_COLOR,
padding="12px",
gap="8px",
flex="0 1 auto",
max_width="420px",
),
)
# Centre: image preview (flex so it grows with window width)
centre_panel = VBox(
[self._preview_grid.widget],
layout=widgets.Layout(
background_color=BG_COLOR,
padding="8px",
flex="1 1 auto",
min_width="140px",
border_left=f"1px solid {ESA_BLUE_DEEP}",
border_right=f"1px solid {ESA_BLUE_DEEP}",
),
)
# Right: normalisation overview + validation + start
right_panel = VBox(
[
self._norm_overview,
self._validation_html,
self._start_btn,
],
layout=widgets.Layout(
background_color=BG_COLOR,
padding="8px 12px",
gap="8px",
width="380px",
flex="0 0 auto",
),
)
form = HBox(
[left_panel, centre_panel, right_panel],
layout=widgets.Layout(background_color=BG_COLOR),
)
return VBox(
[
header.widget,
form,
self._out,
],
layout=widgets.Layout(
background_color=BG_COLOR,
border=f"1px solid {ESA_BLUE_DEEP}",
width="100%",
),
)