diff --git a/app.py b/app.py
index 6a92774..ef6d4ba 100644
--- a/app.py
+++ b/app.py
@@ -39,6 +39,27 @@ MODEL_ARTIFACT_DIR = BASE_DIR / "artifacts" / MODEL_VERSION
VIEW_OVERVIEW = "Przegląd"
VIEW_DETAIL = "Szczegóły cylindra"
VIEW_MODEL = "Model"
+PLOTLY_CONFIG = {"displayModeBar": False, "displaylogo": False}
+DIAGNOSTIC_COLUMN_DISPLAY = {
+ "engine_id": "Silnik",
+ "cylinder": "Cylinder",
+ "label": "Diagnoza",
+ "severity": "Nasilenie",
+ "raw_model_label": "Surowa diagnoza modelu",
+ "decision_source": "Źródło decyzji",
+ "label_confidence": "Wynik diagnozy",
+ "raw_model_confidence": "Surowy wynik diagnozy",
+ "label_margin": "Margines decyzji",
+ "severity_confidence": "Wynik oceny nasilenia",
+ "anomaly_score_mean_abs_mv": "Średnie odchylenie bezwzględne [mV]",
+ "anomaly_ratio_to_engine_median": "Odchylenie względem mediany silnika",
+ "ood_absolute_threshold_mv": "Bezwzględny próg anomalii [mV]",
+ "ood_ratio_threshold": "Względny próg anomalii",
+ "top_anomalous_frequencies_khz": "Anomalne częstotliwości [kHz]",
+ "missing_spectral_cells": "Brakujące pomiary widma",
+ "model_version": "Wersja modelu",
+ "model_artifact_sha256": "Suma SHA-256 artefaktu",
+}
@dataclass(frozen=True)
@@ -83,7 +104,7 @@ def build_dependencies() -> AppDependencies:
"Artefakt modelu nie został załadowany. Aplikacja działa w bezpiecznym "
"trybie demonstracyjnym na zapisanych predykcjach."
),
- model_version="demo-precomputed",
+ model_version="tryb-demonstracyjny",
)
@@ -110,17 +131,40 @@ def _format_confidence(value: float | int | None) -> str:
return f"{100 * float(value):.0f}%"
+def _diagnostics_for_display(frame: pd.DataFrame) -> pd.DataFrame:
+ display = frame.copy()
+ for column in ("label", "raw_model_label"):
+ if column in display:
+ display[column] = display[column].map(LABEL_DISPLAY).fillna(display[column])
+ if "severity" in display:
+ display["severity"] = (
+ display["severity"].map(SEVERITY_DISPLAY).fillna(display["severity"])
+ )
+ if "decision_source" in display:
+ display["decision_source"] = display["decision_source"].replace(
+ {
+ "classifier": "Klasyfikator spektralny",
+ "ood_override": "Reguła anomalii",
+ }
+ )
+ if "model_version" in display:
+ display["model_version"] = display["model_version"].replace(
+ {"demo-precomputed": "tryb demonstracyjny"}
+ )
+ return display.rename(columns=DIAGNOSTIC_COLUMN_DISPLAY)
+
+
def _render_header(live_inference: bool) -> None:
- mode = "LIVE INFERENCE" if live_inference else "DEMO FALLBACK"
+ mode = "MODEL AKTYWNY" if live_inference else "TRYB DEMONSTRACYJNY"
st.markdown(
f"""
""",
unsafe_allow_html=True,
@@ -134,7 +178,7 @@ def _render_summary(summary) -> None:
STATUS SILNIKA{html.escape(summary.status)}
NAJWYŻSZE NASILENIE{html.escape(summary.highest_severity_display)}
WYMAGA UWAGI{summary.attention} / {summary.cylinders}
- ŚR. SCORE MODELU{_format_confidence(summary.mean_confidence)}
+ ŚR. WYNIK MODELU{_format_confidence(summary.mean_confidence)}
""",
unsafe_allow_html=True,
@@ -171,24 +215,29 @@ def _render_cylinder_grid(analysis, session_key: str, view_key: str) -> None:
def _render_engine_overview(analysis) -> None:
- left, right = st.columns([1.45, 1.0], gap="large")
+ left, right = st.columns(2, gap="large")
with left:
- st.plotly_chart(engine_heatmap(analysis), width="stretch", key=f"heatmap_{analysis.engine_id}")
+ st.markdown("### Mapa odchyleń")
+ st.plotly_chart(
+ engine_heatmap(analysis),
+ width="stretch",
+ key=f"heatmap_{analysis.engine_id}",
+ config=PLOTLY_CONFIG,
+ )
with right:
st.markdown("### Priorytet kontroli")
ranking = rank_cylinders(analysis).head(6).copy()
ranking["Cylinder"] = ranking["cylinder"].map(lambda value: f"C{int(value):02d}")
ranking["Diagnoza"] = ranking["label_display"]
ranking["Nasilenie"] = ranking["severity_display"]
- ranking["Score modelu"] = ranking["label_confidence"].map(_format_confidence)
+ ranking["Wynik modelu"] = ranking["label_confidence"].map(_format_confidence)
ranking["Priorytet"] = ranking["priority_display"]
st.dataframe(
- ranking[["Cylinder", "Diagnoza", "Nasilenie", "Score modelu", "Priorytet"]],
+ ranking[["Cylinder", "Diagnoza", "Nasilenie", "Wynik modelu", "Priorytet"]],
hide_index=True,
width="stretch",
- height=315,
+ height=360,
)
- st.caption("Priorytet: nasilenie → odchylenie → score modelu.")
def _render_cylinder_detail(
@@ -209,8 +258,8 @@ def _render_cylinder_detail(
{html.escape(explanation.severity_display)}
-
Score label{_format_confidence(explanation.confidence)}
-
Score severity{_format_confidence(severity_confidence)}
+
Wynik diagnozy{_format_confidence(explanation.confidence)}
+
Wynik oceny nasilenia{_format_confidence(severity_confidence)}
Średnie odchylenie{explanation.anomaly_score:.1f} mV
@@ -225,6 +274,7 @@ def _render_cylinder_detail(
f"spectrum_{analysis.engine_id}_"
+ "_".join(str(value) for value in comparison_cylinders)
),
+ config=PLOTLY_CONFIG,
)
st.markdown("#### Odchylenie cylindra głównego")
@@ -232,6 +282,7 @@ def _render_cylinder_detail(
deviation_chart(analysis, cylinder),
width="stretch",
key=f"deviation_{analysis.engine_id}_{cylinder}",
+ config=PLOTLY_CONFIG,
)
why, next_step = st.columns(2, gap="large")
@@ -242,30 +293,37 @@ def _render_cylinder_detail(
st.markdown("#### Następny krok")
st.write(explanation.recommendation)
source_label = (
- "Reguła OOD"
+ "Reguła anomalii"
if explanation.decision_source == "ood_override"
else "Klasyfikator spektralny"
)
- st.caption(f"Źródło: {source_label} · score nie jest prawdopodobieństwem.")
+ st.caption(
+ f"Źródło: {source_label} · wynik modelu nie jest prawdopodobieństwem."
+ )
def _render_technical(result: DiagnosisResult) -> None:
st.markdown("### Walidacja i model")
c1, c2, c3, c4 = st.columns(4)
- c1.metric("Grouped Macro F1", "0.981")
- c2.metric("Severity accuracy", "0.930")
- c3.metric("Walidacyjne ML", "33.65 / 40")
+ c1.metric("Makro F1 (grupowe)", "0.981")
+ c2.metric("Trafność nasilenia", "0.930")
+ c3.metric("Punkty walidacyjne", "33.65 / 40")
c4.metric("Testy", "38 / 38")
st.caption(
- "Grouped CV po engine_id · 5 seedów · Macro F1 przy 5% braków: 0.983 · CPU."
+ "Walidacja grupowa według silników · średnia z 5 uruchomień · "
+ "makro F1 przy 5% braków: 0.983 · CPU."
)
with st.expander("Pokaż dane diagnostyczne"):
- st.dataframe(result.diagnostics, width="stretch", hide_index=True)
+ st.dataframe(
+ _diagnostics_for_display(result.diagnostics),
+ width="stretch",
+ hide_index=True,
+ )
def render_app(dependencies: AppDependencies | None = None) -> None:
st.set_page_config(
- page_title="ENGIN Diagnostic Console",
+ page_title="Konsola diagnostyczna ENGIN",
page_icon="⚙️",
layout="wide",
initial_sidebar_state="expanded",
@@ -284,7 +342,7 @@ def render_app(dependencies: AppDependencies | None = None) -> None:
payload: bytes | None
if source == "Dane demonstracyjne":
payload = deps.demo_payload
- st.success("Demo załadowane")
+ st.success("Dane demonstracyjne załadowane")
else:
uploaded = st.file_uploader("Plik pomiarowy CSV", type=["csv"])
payload = uploaded.getvalue() if uploaded is not None else None
@@ -316,14 +374,14 @@ def render_app(dependencies: AppDependencies | None = None) -> None:
with st.sidebar:
engine_id = st.selectbox("Aktywny silnik", result.engine_ids)
st.download_button(
- "Pobierz predictions.csv",
+ "Pobierz wyniki (CSV)",
data=result.submission.to_csv(index=False).encode("utf-8"),
file_name="predictions.csv",
mime="text/csv",
width="stretch",
)
st.download_button(
- "Pobierz diagnostykę",
+ "Pobierz dane diagnostyczne (CSV)",
data=result.diagnostics.to_csv(index=False).encode("utf-8"),
file_name="prediction_diagnostics.csv",
mime="text/csv",
@@ -331,7 +389,8 @@ def render_app(dependencies: AppDependencies | None = None) -> None:
)
st.divider()
st.caption(
- f"Model {deps.model_version} · CPU inference · brak połączeń zewnętrznych"
+ f"Wersja modelu: {deps.model_version} · wnioskowanie na CPU · "
+ "bez połączeń zewnętrznych"
)
analysis = analyze_engine(result, str(engine_id))
diff --git a/assets/app.css b/assets/app.css
index 4307f41..df6231f 100644
--- a/assets/app.css
+++ b/assets/app.css
@@ -98,6 +98,22 @@
[data-testid="stMetric"] { padding: .7rem; border: 1px solid var(--line); background: var(--panel); }
[data-testid="stDataFrame"], [data-testid="stPlotlyChart"] { border: 1px solid var(--line); overflow: hidden; }
+/* Streamlit 1.62 has no interface locale setting, so localize its uploader chrome. */
+[data-testid="stFileUploaderDropzone"] button [data-testid="stMarkdownContainer"] { display: none; }
+[data-testid="stFileUploaderDropzone"] button::after { content: "Wybierz plik"; }
+[data-testid="stFileUploaderDropzoneInstructions"] > div { display: none; }
+[data-testid="stFileUploaderDropzoneInstructions"]::after {
+ content: "Maks. 10 MB na plik · CSV";
+ color: var(--muted);
+ font-size: .875rem;
+ white-space: nowrap;
+}
+[data-testid="stFileUploaderDropzone"] > div:not([data-testid]) > span { font-size: 0; }
+[data-testid="stFileUploaderDropzone"] > div:not([data-testid]) > span::after {
+ content: "Upuść plik tutaj";
+ font-size: .875rem;
+}
+
@media (max-width: 1000px) {
.product-header { flex-direction: column; }
.diagnosis-card { grid-template-columns: 1fr; }
diff --git a/engin/charts.py b/engin/charts.py
index ee4fb98..3f2afb0 100644
--- a/engin/charts.py
+++ b/engin/charts.py
@@ -57,10 +57,9 @@ def engine_heatmap(analysis: EngineAnalysis) -> go.Figure:
hovertemplate="Cylinder %{y}
%{x} kHz
Odchylenie %{z:.1f} mV",
)
)
- fig.update_layout(title="Mapa odchyleń")
fig.update_yaxes(autorange="reversed", title="Cylinder")
fig.update_xaxes(title="Częstotliwość [kHz]", dtick=2)
- return _base_layout(fig, height=420)
+ return _base_layout(fig, height=360, margin_top=18, margin_bottom=42)
def _cylinder_position(analysis: EngineAnalysis, cylinder: int) -> int:
diff --git a/tests/test_app_smoke.py b/tests/test_app_smoke.py
index 3b3b452..3cd15af 100644
--- a/tests/test_app_smoke.py
+++ b/tests/test_app_smoke.py
@@ -19,6 +19,31 @@ class StreamlitSmokeTests(unittest.TestCase):
self.assertFalse(
any(selector.label == "Cylinder główny" for selector in app.selectbox)
)
+ rendered = "\n".join(markdown.value for markdown in app.markdown)
+ self.assertIn("Konsola diagnostyczna ENGIN", rendered)
+ for english_fragment in (
+ "Diagnostic Console",
+ "LIVE INFERENCE",
+ "DEMO FALLBACK",
+ "LEAKAGE-SAFE",
+ "Grouped Macro F1",
+ "Severity accuracy",
+ ):
+ self.assertNotIn(english_fragment, rendered)
+
+ app.segmented_control[0].set_value("Model").run()
+ self.assertEqual(
+ [metric.label for metric in app.metric],
+ [
+ "Makro F1 (grupowe)",
+ "Trafność nasilenia",
+ "Punkty walidacyjne",
+ "Testy",
+ ],
+ )
+ diagnostic_columns = set(app.dataframe[0].value.columns)
+ self.assertIn("Źródło decyzji", diagnostic_columns)
+ self.assertNotIn("decision_source", diagnostic_columns)
def test_upload_mode_has_safe_empty_state(self) -> None:
app = AppTest.from_file(str(ROOT / "app.py"), default_timeout=30).run()
diff --git a/tests/test_explainability.py b/tests/test_explainability.py
index 40e12b2..9791fab 100644
--- a/tests/test_explainability.py
+++ b/tests/test_explainability.py
@@ -63,7 +63,9 @@ class ExplainabilityTests(unittest.TestCase):
def test_all_chart_factories_return_populated_figures(self) -> None:
cylinder = int(self.analysis.measurements["cylinder"].iloc[0])
compared = self.analysis.measurements["cylinder"].astype(int).head(4).tolist()
- self.assertEqual(len(engine_heatmap(self.analysis).data), 1)
+ heatmap = engine_heatmap(self.analysis)
+ self.assertEqual(len(heatmap.data), 1)
+ self.assertEqual(heatmap.layout.height, 360)
self.assertEqual(len(cylinder_spectrum(self.analysis, cylinder).data), 2)
self.assertEqual(
len(cylinder_spectrum(self.analysis, cylinder, compared).data),