MesseMMP commited on
Commit
2faca07
·
1 Parent(s): e8e9dd9

new design

Browse files
Files changed (1) hide show
  1. app.py +841 -172
app.py CHANGED
@@ -1,5 +1,3 @@
1
- # app.py — минимальный UI: загрузка всех DICOM → classify → per-side inference
2
-
3
  import os
4
  import base64
5
  import json
@@ -7,6 +5,7 @@ import tempfile
7
  from datetime import datetime
8
  from dataclasses import asdict
9
  from typing import List, Dict, Any
 
10
 
11
  import gradio as gr
12
  import pydicom
@@ -14,38 +13,508 @@ import pydicom
14
  from src.syntax_pred.config import CFG
15
  from src.syntax_pred.infer import Study, run_inference
16
 
17
- # ------- Логотип (base64) -------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  DEFAULT_LOGO = "assets/logo.png"
19
  LOGO_PATH = os.environ.get("LOGO_PATH", DEFAULT_LOGO)
20
 
21
 
22
  def _logo_html() -> str:
 
23
  path = LOGO_PATH
24
  if not path or not os.path.exists(path):
25
  return ""
26
  try:
27
  with open(path, "rb") as f:
28
- import base64 as b64
29
- data = b64.b64encode(f.read()).decode("ascii")
30
  ext = os.path.splitext(path)[1].lower()
31
  mime = "image/png" if ext in {".png", ""} else "image/jpeg"
32
- return (
33
- f'<img src="data:{mime};base64,{data}" alt="logo" '
34
- f'style="height:40px;vertical-align:middle;display:inline-block;'
35
- f'image-rendering:auto;object-fit:contain;margin-right:12px;" />'
36
- )
37
  except Exception:
38
  return ""
39
 
40
 
41
- # ------- Вспомогательные -------
42
-
43
  def _is_dicom_path(path: str) -> bool:
44
- """Фильтрация только настоящих DICOM (по расширению и попытке чтения)."""
45
  if not os.path.exists(path):
46
  return False
47
  ext = os.path.splitext(path)[1].lower()
48
- if ext not in {".dcm", ""}: # многие DICOM без расширения
49
  return False
50
  try:
51
  pydicom.dcmread(path, stop_before_pixels=True)
@@ -55,21 +524,25 @@ def _is_dicom_path(path: str) -> bool:
55
 
56
 
57
  def _files_to_paths(files) -> List[str]:
58
- raw_paths = [f.name for f in (files or []) if hasattr(f, "name") and os.path.exists(f.name)]
 
 
 
59
  return [p for p in raw_paths if _is_dicom_path(p)]
60
 
61
 
62
  def _collect_input_paths(files_all, folder_files) -> List[str]:
 
63
  combined = _files_to_paths(files_all) + _files_to_paths(folder_files)
64
- # Keep first occurrence order while removing duplicates.
65
  return list(dict.fromkeys(combined))
66
 
67
 
68
  def _build_report_file(result: Dict[str, Any]) -> str:
 
69
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
70
  with tempfile.NamedTemporaryFile(
71
  mode="w",
72
- prefix=f"syntax_report_{ts}_",
73
  suffix=".json",
74
  encoding="utf-8",
75
  delete=False,
@@ -78,215 +551,411 @@ def _build_report_file(result: Dict[str, Any]) -> str:
78
  return f.name
79
 
80
 
81
- def _status_badge(state: str) -> str:
82
- """Красивый статус с «анимацией» точек через Unicode."""
83
  state = state.lower()
84
- if state == "running":
85
- # Используем точки/круги как псевдо-анимацию
86
- return "⏱️ Running · "
87
- if state == "done":
88
- return " Done"
89
- if state == "error":
90
- return "❌ Error"
91
- return "⌛ Queued"
92
-
93
-
94
- def _update_status_table(studies, status: str):
95
- """Пометить все исследования указанным статусом (только визуальный бейдж)."""
96
- badge = _status_badge(status)
 
 
 
 
97
  return [
98
- [s["name"], s.get("description", ""), len(s.get("files", [])), badge]
99
  for s in (studies or [])
100
  ]
101
 
102
 
103
  def _format_results_html(result: Dict[str, Any]) -> str:
 
104
  if not result:
105
- return (
106
- '<div style="padding:12px;border:1px solid #e2e8f0;border-radius:10px;">'
107
- '<b>Результаты появятся после запуска inference.</b>'
108
- '</div>'
109
- )
110
-
 
 
111
  if "error" in result:
112
- msg = str(result.get("error", "Unknown error"))
113
- return (
114
- '<div style="padding:12px;border:1px solid #fecaca;background:#fef2f2;'
115
- 'border-radius:10px;color:#991b1b;">'
116
- f'<b>Ошибка:</b> {msg}'
117
- '</div>'
118
- )
119
-
 
 
 
 
120
  studies = result.get("studies", []) or []
121
  if not studies:
122
- return (
123
- '<div style="padding:12px;border:1px solid #e2e8f0;border-radius:10px;">'
124
- 'Нет исследований для отображения.'
125
- '</div>'
126
- )
127
-
128
- cards = []
 
 
129
  for s in studies:
130
- study = s.get("study", "-")
131
- desc = s.get("description", "")
132
-
133
  left_mean = s.get("left", {}).get("mean", 0.0)
134
  right_mean = s.get("right", {}).get("mean", 0.0)
135
-
136
  total_obj = s.get("total", {})
137
  total_mean = total_obj.get("mean", 0.0)
138
-
139
- risk_key = next((k for k in total_obj.keys() if "High-risk" in str(k)), "High-risk")
140
- is_high_risk = bool(total_obj.get(risk_key, False))
141
-
142
- badge_bg = "#fee2e2" if is_high_risk else "#dcfce7"
143
- badge_fg = "#991b1b" if is_high_risk else "#166534"
144
- badge_text = "Высокий риск" if is_high_risk else "Низкий риск"
145
-
146
- desc_html = f"<div style='color:#64748b;margin-top:4px'>{desc}</div>" if desc else ""
147
-
148
- cards.append(
149
- """
150
- <div style="border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;margin-bottom:10px;background:#ffffff;">
151
- <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;">
152
- <div>
153
- <div style="font-size:18px;font-weight:700;color:#0f172a;">{study}</div>
154
- {desc_html}
155
- </div>
156
- <div style="padding:4px 10px;border-radius:999px;background:{badge_bg};color:{badge_fg};font-weight:700;white-space:nowrap;">
157
- {badge_text}
158
- </div>
159
- </div>
160
- <div style="margin-top:10px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;">
161
- <div style="background:#f8fafc;border-radius:10px;padding:10px;">
162
- <div style="color:#64748b;font-size:12px;">LEFT</div>
163
- <div style="font-size:22px;font-weight:800;color:#0f172a;">{left_mean:.3f}</div>
164
- </div>
165
- <div style="background:#f8fafc;border-radius:10px;padding:10px;">
166
- <div style="color:#64748b;font-size:12px;">RIGHT</div>
167
- <div style="font-size:22px;font-weight:800;color:#0f172a;">{right_mean:.3f}</div>
 
 
 
 
 
 
168
  </div>
169
- <div style="background:#f1f5f9;border-radius:10px;padding:10px;border:1px solid #cbd5e1;">
170
- <div style="color:#334155;font-size:12px;">TOTAL</div>
171
- <div style="font-size:24px;font-weight:900;color:#020617;">{total_mean:.3f}</div>
172
- </div>
173
- </div>
174
  </div>
175
- """.format(
176
- study=study,
177
- desc_html=desc_html,
178
- badge_bg=badge_bg,
179
- badge_fg=badge_fg,
180
- badge_text=badge_text,
181
- left_mean=left_mean,
182
- right_mean=right_mean,
183
- total_mean=total_mean,
184
- )
185
- )
186
-
187
- return "\n".join(cards)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
-
190
- # ------- UI -------
191
- def ui():
192
- with gr.Blocks() as demo:
193
- gr.HTML(
194
- f"""
195
- <div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
196
- {_logo_html()}
197
- <h1 style="margin:0;font-weight:800;text-align:center;flex:1;">SYNTAX-Video — Study Inference</h1>
198
  </div>
199
- <ol style="margin:0 0 12px 20px; color:#475569; line-height:1.5;">
200
- <li>Укажите ID исследования и (необязательно) описание.</li>
201
- <li>Загрузите все DICOM-файлы исследования одним списком или папкой.</li>
202
- <li>Нажмите “Add study”, затем “Run inference”.</li>
203
- </ol>
204
- """
205
- )
206
-
207
- studies_state = gr.State([]) # list[dict]
208
-
209
- with gr.Row():
210
- study_name = gr.Textbox(label="Study ID", placeholder="e.g., S1234")
211
- study_desc = gr.Textbox(label="Description (optional)", placeholder="Free text...")
212
-
213
- files_all = gr.File(label="All DICOM files (single study)", file_count="multiple")
214
- files_folder = gr.File(label="Study folder (optional)", file_count="directory")
215
-
216
- with gr.Row():
217
- btn_add = gr.Button("➕ Add study")
218
- btn_clear = gr.Button("🗑️ Clear all")
219
-
220
- queue_table = gr.Dataframe(
221
- headers=["Study", "Description", "#DICOM files", "Status"],
222
- datatype=["str", "str", "number", "str"],
223
- interactive=False,
224
- label="Studies queue",
225
- row_count=(0, "dynamic"),
226
- )
227
-
228
- def _add_study_fn(studies: List[Dict[str, Any]], name, desc, files, folder):
229
- name = (name or "").strip() or f"Study_{len(studies)+1}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  desc = (desc or "").strip()
231
  paths = _collect_input_paths(files, folder)
 
232
  if not paths:
233
- # ничего не добавляем, просто обновляем таблицу текущими статусами
234
  table = _update_status_table(studies, "Queued")
235
  return studies, table, name, desc, files, folder
236
-
237
- studies = studies + [asdict(Study(name=name, description=desc, files=paths))]
 
 
 
 
 
238
  table = _update_status_table(studies, "Queued")
239
  return studies, table, "", "", None, None
240
-
241
  btn_add.click(
242
  _add_study_fn,
243
  inputs=[studies_state, study_name, study_desc, files_all, files_folder],
244
- outputs=[studies_state, queue_table, study_name, study_desc, files_all, files_folder],
245
  )
246
-
 
 
 
 
 
247
  def _clear_all():
248
  return [], []
249
-
250
- btn_clear.click(_clear_all, inputs=None, outputs=[studies_state, queue_table])
251
-
252
- run_btn = gr.Button("🚀 Run inference", variant="primary")
253
- out_summary = gr.HTML(label="Результат")
254
- with gr.Accordion("Подробно (JSON)", open=False):
255
- out_json = gr.JSON(label="Results")
256
- out_report = gr.File(label="Скачать отчет", interactive=False)
257
-
258
  def _before_run(studies):
259
- # пометить все исследования как Running
260
  return _update_status_table(studies, "Running")
261
-
262
  def _run_infer(studies):
263
- study_objs = [Study(**s) for s in (studies or [])]
 
 
264
  result = run_inference(study_objs)
265
  report_path = _build_report_file(result)
266
  return result, report_path
267
-
268
  def _after_run(studies, result, report_path):
269
  table = _update_status_table(studies, "Done")
270
  return _format_results_html(result), result, report_path, table
271
-
272
  run_btn.click(
273
  _before_run,
274
  inputs=[studies_state],
275
- outputs=[queue_table],
276
  ).then(
277
  _run_infer,
278
  inputs=[studies_state],
279
- outputs=[out_json, out_report],
280
  ).then(
281
  _after_run,
282
  inputs=[studies_state, out_json, out_report],
283
- outputs=[out_summary, out_json, out_report, queue_table],
284
  )
285
-
286
- gr.Markdown("⚠️ Research-only. Not a medical device. Predictions depend on input quality and domain shift.")
287
  return demo
288
 
289
 
290
  if __name__ == "__main__":
291
  favicon = LOGO_PATH if (LOGO_PATH and os.path.exists(LOGO_PATH)) else None
292
- ui().launch(favicon_path=favicon, theme=gr.themes.Soft())
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import base64
3
  import json
 
5
  from datetime import datetime
6
  from dataclasses import asdict
7
  from typing import List, Dict, Any
8
+ from pathlib import Path
9
 
10
  import gradio as gr
11
  import pydicom
 
13
  from src.syntax_pred.config import CFG
14
  from src.syntax_pred.infer import Study, run_inference
15
 
16
+ # ==================== КОНФИГУРАЦИЯ ДИЗАЙНА ====================
17
+ APPLE_STYLE_CSS = """
18
+ /* Сбалансированная современная цветовая гамма */
19
+ :root {
20
+ /* Основные фоны - более спокойные */
21
+ --bg-primary: #4f46e5;
22
+ --bg-secondary: #7c3aed;
23
+ --bg-gradient: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%);
24
+ --bg-surface: linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,255,255,0.94) 100%);
25
+
26
+ /* Акцентные цвета - приглушенные */
27
+ --accent-cyan: #06b6d4;
28
+ --accent-teal: #3b82f6;
29
+ --accent-purple: #8b5cf6;
30
+ --accent-pink: #d946ef;
31
+ --accent-orange: #f97316;
32
+ --accent-green: #10b981;
33
+ --accent-red: #ef4444;
34
+ --accent-yellow: #eab308;
35
+
36
+ /* Светлые версии акцентов */
37
+ --accent-cyan-light: rgba(6, 182, 212, 0.08);
38
+ --accent-teal-light: rgba(59, 130, 246, 0.08);
39
+ --accent-purple-light: rgba(139, 92, 246, 0.08);
40
+ --accent-pink-light: rgba(217, 70, 239, 0.08);
41
+ --accent-green-light: rgba(16, 185, 129, 0.08);
42
+ --accent-red-light: rgba(239, 68, 68, 0.08);
43
+ --accent-orange-light: rgba(249, 115, 22, 0.08);
44
+
45
+ /* Текстовые цвета */
46
+ --text-primary: #1e293b;
47
+ --text-secondary: #64748b;
48
+ --text-light: #94a3b8;
49
+ --text-white: #ffffff;
50
+
51
+ /* Границы и тени */
52
+ --border-color: rgba(255, 255, 255, 0.2);
53
+ --border-dark: rgba(0, 0, 0, 0.06);
54
+ --shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
55
+ --shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.08);
56
+ --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
57
+ --shadow-glow: 0 0 0 3px rgba(6, 182, 212, 0.2);
58
+ --shadow-glow-purple: 0 0 0 3px rgba(139, 92, 246, 0.2);
59
+ }
60
+
61
+ * {
62
+ font-family: -apple-system, "SF Pro Text", "SF Pro Display", "Inter", "Helvetica Neue", system-ui, sans-serif;
63
+ }
64
+
65
+ body, .gradio-container {
66
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%) !important;
67
+ position: relative;
68
+ }
69
+
70
+ /* Декоративные элементы фона - более мягкие */
71
+ body::before {
72
+ content: '';
73
+ position: fixed;
74
+ top: 0;
75
+ left: 0;
76
+ right: 0;
77
+ bottom: 0;
78
+ background: radial-gradient(circle at 20% 50%, rgba(255,255,255,0.08) 0%, transparent 70%);
79
+ pointer-events: none;
80
+ z-index: 0;
81
+ }
82
+
83
+ .gradio-container {
84
+ position: relative;
85
+ z-index: 1;
86
+ }
87
+
88
+ /* Кастомные стили для блоков Gradio */
89
+ .gr-box, .gr-form, .panel, .tabs, .tab-nav, .accordion {
90
+ background: transparent !important;
91
+ border: none !important;
92
+ }
93
+
94
+ /* Главный контейнер */
95
+ .apple-container {
96
+ max-width: 1400px;
97
+ margin: 0 auto;
98
+ padding: 2rem 1.5rem;
99
+ }
100
+
101
+ /* Хедер */
102
+ .apple-header {
103
+ text-align: center;
104
+ margin-bottom: 2rem;
105
+ position: relative;
106
+ padding: 1rem 0;
107
+ }
108
+
109
+ .apple-title {
110
+ font-size: 3.2rem;
111
+ font-weight: 800;
112
+ background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
113
+ -webkit-background-clip: text;
114
+ -webkit-text-fill-color: transparent;
115
+ background-clip: text;
116
+ letter-spacing: -0.02em;
117
+ margin: 0;
118
+ text-shadow: 0 2px 10px rgba(0,0,0,0.05);
119
+ }
120
+
121
+ .apple-subtitle {
122
+ font-size: 1.1rem;
123
+ color: rgba(255, 255, 255, 0.85);
124
+ margin-top: 0.5rem;
125
+ font-weight: 400;
126
+ }
127
+
128
+ /* Карточки */
129
+ .apple-card {
130
+ background: rgba(255, 255, 255, 0.96);
131
+ backdrop-filter: blur(20px);
132
+ border-radius: 24px;
133
+ border: 1px solid rgba(255, 255, 255, 0.3);
134
+ box-shadow: var(--shadow-md);
135
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
136
+ overflow: hidden;
137
+ }
138
+
139
+ .apple-card:hover {
140
+ transform: translateY(-2px);
141
+ box-shadow: var(--shadow-lg);
142
+ background: rgba(255, 255, 255, 0.98);
143
+ }
144
+
145
+ .apple-card-header {
146
+ padding: 1.25rem 1.5rem;
147
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
148
+ background: linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0.2) 100%);
149
+ }
150
+
151
+ .apple-card-title {
152
+ font-size: 1.25rem;
153
+ font-weight: 700;
154
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
155
+ -webkit-background-clip: text;
156
+ -webkit-text-fill-color: transparent;
157
+ background-clip: text;
158
+ margin: 0;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 0.5rem;
162
+ }
163
+
164
+ .apple-card-content {
165
+ padding: 1.5rem;
166
+ }
167
+
168
+ /* Специальная карточка для добавления исследования */
169
+ .add-study-card {
170
+ background: linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.96) 100%);
171
+ border: 1px solid rgba(255,255,255,0.4);
172
+ position: relative;
173
+ overflow: hidden;
174
+ }
175
+
176
+ /* Кнопки - более спокойные градиенты */
177
+ .apple-button {
178
+ background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
179
+ color: white;
180
+ border: none;
181
+ border-radius: 980px;
182
+ padding: 0.75rem 1.5rem;
183
+ font-size: 0.9375rem;
184
+ font-weight: 600;
185
+ cursor: pointer;
186
+ transition: all 0.3s ease;
187
+ box-shadow: var(--shadow-sm);
188
+ display: inline-flex;
189
+ align-items: center;
190
+ gap: 0.5rem;
191
+ position: relative;
192
+ overflow: hidden;
193
+ }
194
+
195
+ .apple-button::before {
196
+ content: '';
197
+ position: absolute;
198
+ top: 0;
199
+ left: -100%;
200
+ width: 100%;
201
+ height: 100%;
202
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
203
+ transition: left 0.5s;
204
+ }
205
+
206
+ .apple-button:hover::before {
207
+ left: 100%;
208
+ }
209
+
210
+ .apple-button:hover {
211
+ transform: translateY(-1px);
212
+ box-shadow: var(--shadow-md);
213
+ background: linear-gradient(135deg, #0891b2 0%, #2563eb 100%);
214
+ }
215
+
216
+ /* Кнопка добавления - фиолетовая */
217
+ .apple-button-add {
218
+ background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
219
+ color: white;
220
+ border: none;
221
+ border-radius: 980px;
222
+ padding: 0.75rem 1.5rem;
223
+ font-size: 0.9375rem;
224
+ font-weight: 600;
225
+ cursor: pointer;
226
+ transition: all 0.3s ease;
227
+ box-shadow: var(--shadow-sm);
228
+ display: inline-flex;
229
+ align-items: center;
230
+ gap: 0.5rem;
231
+ position: relative;
232
+ overflow: hidden;
233
+ }
234
+
235
+ .apple-button-add::before {
236
+ content: '';
237
+ position: absolute;
238
+ top: 0;
239
+ left: -100%;
240
+ width: 100%;
241
+ height: 100%;
242
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
243
+ transition: left 0.5s;
244
+ }
245
+
246
+ .apple-button-add:hover::before {
247
+ left: 100%;
248
+ }
249
+
250
+ .apple-button-add:hover {
251
+ transform: translateY(-1px);
252
+ box-shadow: var(--shadow-md);
253
+ background: linear-gradient(135deg, #7c3aed 0%, #9333ea 100%);
254
+ }
255
+
256
+ /* Кнопка очистки - оранжевая */
257
+ .apple-button-clear {
258
+ background: linear-gradient(135deg, #f97316 0%, #fbbf24 100%);
259
+ color: white;
260
+ border: none;
261
+ border-radius: 980px;
262
+ padding: 0.75rem 1.5rem;
263
+ font-size: 0.9375rem;
264
+ font-weight: 600;
265
+ cursor: pointer;
266
+ transition: all 0.3s ease;
267
+ box-shadow: var(--shadow-sm);
268
+ display: inline-flex;
269
+ align-items: center;
270
+ gap: 0.5rem;
271
+ position: relative;
272
+ overflow: hidden;
273
+ }
274
+
275
+ .apple-button-clear::before {
276
+ content: '';
277
+ position: absolute;
278
+ top: 0;
279
+ left: -100%;
280
+ width: 100%;
281
+ height: 100%;
282
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
283
+ transition: left 0.5s;
284
+ }
285
+
286
+ .apple-button-clear:hover::before {
287
+ left: 100%;
288
+ }
289
+
290
+ .apple-button-clear:hover {
291
+ transform: translateY(-1px);
292
+ box-shadow: var(--shadow-md);
293
+ background: linear-gradient(135deg, #ea580c 0%, #eab308 100%);
294
+ }
295
+
296
+ /* Поля ввода */
297
+ .apple-input {
298
+ border: 1.5px solid rgba(0, 0, 0, 0.05) !important;
299
+ border-radius: 14px !important;
300
+ padding: 0.75rem 1rem !important;
301
+ font-size: 0.9375rem !important;
302
+ background: rgba(255, 255, 255, 0.95) !important;
303
+ transition: all 0.3s ease !important;
304
+ }
305
+
306
+ .apple-input:focus {
307
+ outline: none !important;
308
+ border-color: #06b6d4 !important;
309
+ box-shadow: var(--shadow-glow) !important;
310
+ background: white !important;
311
+ }
312
+
313
+ /* Таблица */
314
+ .apple-table {
315
+ border-radius: 16px;
316
+ overflow: hidden;
317
+ border: none;
318
+ }
319
+
320
+ .apple-table table {
321
+ background: rgba(255, 255, 255, 0.95) !important;
322
+ border-collapse: collapse !important;
323
+ width: 100%;
324
+ }
325
+
326
+ .apple-table th {
327
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important;
328
+ color: white !important;
329
+ font-weight: 600 !important;
330
+ font-size: 0.875rem !important;
331
+ padding: 12px 16px !important;
332
+ border-bottom: none !important;
333
+ border-top: none !important;
334
+ border-left: none !important;
335
+ border-right: none !important;
336
+ }
337
+
338
+ .apple-table td {
339
+ background: rgba(255, 255, 255, 0.98) !important;
340
+ color: var(--text-primary) !important;
341
+ padding: 12px 16px !important;
342
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important;
343
+ border-top: none !important;
344
+ border-left: none !important;
345
+ border-right: none !important;
346
+ }
347
+
348
+ .apple-table tr:last-child td {
349
+ border-bottom: none !important;
350
+ }
351
+
352
+ .apple-table tr:hover td {
353
+ background: rgba(139, 92, 246, 0.04) !important;
354
+ }
355
+
356
+ /* Стейтус бейдж - более спокойные градиенты */
357
+ .status-badge {
358
+ display: inline-flex;
359
+ align-items: center;
360
+ gap: 0.375rem;
361
+ padding: 0.25rem 0.75rem;
362
+ border-radius: 20px;
363
+ font-size: 0.8125rem;
364
+ font-weight: 600;
365
+ white-space: nowrap;
366
+ }
367
+
368
+ .status-running {
369
+ background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
370
+ color: white;
371
+ box-shadow: var(--shadow-sm);
372
+ }
373
+
374
+ .status-done {
375
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
376
+ color: white;
377
+ box-shadow: var(--shadow-sm);
378
+ }
379
+
380
+ .status-error {
381
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
382
+ color: white;
383
+ box-shadow: var(--shadow-sm);
384
+ }
385
+
386
+ .status-queued {
387
+ background: linear-gradient(135deg, #64748b 0%, #475569 100%);
388
+ color: white;
389
+ box-shadow: var(--shadow-sm);
390
+ }
391
+
392
+ /* Анимации */
393
+ @keyframes pulse {
394
+ 0%, 100% {
395
+ opacity: 1;
396
+ transform: scale(1);
397
+ }
398
+ 50% {
399
+ opacity: 0.85;
400
+ transform: scale(1.02);
401
+ }
402
+ }
403
+
404
+ .pulse {
405
+ animation: pulse 1.5s ease-in-out infinite;
406
+ }
407
+
408
+ /* Скролл бар */
409
+ ::-webkit-scrollbar {
410
+ width: 8px;
411
+ height: 8px;
412
+ }
413
+
414
+ ::-webkit-scrollbar-track {
415
+ background: rgba(255, 255, 255, 0.15);
416
+ border-radius: 4px;
417
+ }
418
+
419
+ ::-webkit-scrollbar-thumb {
420
+ background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
421
+ border-radius: 4px;
422
+ }
423
+
424
+ ::-webkit-scrollbar-thumb:hover {
425
+ background: linear-gradient(135deg, #0891b2 0%, #2563eb 100%);
426
+ }
427
+
428
+ /* Стили для двух колонок загрузки */
429
+ .upload-grid {
430
+ display: grid;
431
+ grid-template-columns: 1fr 1fr;
432
+ gap: 1rem;
433
+ }
434
+
435
+ /* Инструкция */
436
+ .instruction-box {
437
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.06) 0%, rgba(6, 182, 212, 0.06) 100%);
438
+ border-radius: 16px;
439
+ padding: 1rem;
440
+ margin-top: 1rem;
441
+ border: 1px solid rgba(139, 92, 246, 0.15);
442
+ backdrop-filter: blur(10px);
443
+ }
444
+
445
+ .instruction-title {
446
+ font-size: 0.875rem;
447
+ font-weight: 700;
448
+ background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
449
+ -webkit-background-clip: text;
450
+ -webkit-text-fill-color: transparent;
451
+ margin-bottom: 0.5rem;
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 0.5rem;
455
+ }
456
+
457
+ .instruction-list {
458
+ list-style: none;
459
+ padding: 0;
460
+ margin: 0;
461
+ display: grid;
462
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
463
+ gap: 0.5rem;
464
+ }
465
+
466
+ .instruction-list li {
467
+ font-size: 0.8125rem;
468
+ color: var(--text-secondary);
469
+ display: flex;
470
+ align-items: center;
471
+ gap: 0.5rem;
472
+ font-weight: 500;
473
+ }
474
+
475
+ /* Адаптивность */
476
+ @media (max-width: 768px) {
477
+ .upload-grid {
478
+ grid-template-columns: 1fr;
479
+ gap: 1rem;
480
+ }
481
+
482
+ .instruction-list {
483
+ grid-template-columns: 1fr;
484
+ }
485
+
486
+ .apple-title {
487
+ font-size: 2rem;
488
+ }
489
+ }
490
+ """
491
+
492
+ # Логотип (base64)
493
  DEFAULT_LOGO = "assets/logo.png"
494
  LOGO_PATH = os.environ.get("LOGO_PATH", DEFAULT_LOGO)
495
 
496
 
497
  def _logo_html() -> str:
498
+ """Генерация HTML для логотипа"""
499
  path = LOGO_PATH
500
  if not path or not os.path.exists(path):
501
  return ""
502
  try:
503
  with open(path, "rb") as f:
504
+ data = base64.b64encode(f.read()).decode("ascii")
 
505
  ext = os.path.splitext(path)[1].lower()
506
  mime = "image/png" if ext in {".png", ""} else "image/jpeg"
507
+ return f'<img src="data:{mime};base64,{data}" alt="Логотип" style="height: 36px; width: auto; border-radius: 10px; filter: drop-shadow(0 2px 6px rgba(0,0,0,0.08));" />'
 
 
 
 
508
  except Exception:
509
  return ""
510
 
511
 
 
 
512
  def _is_dicom_path(path: str) -> bool:
513
+ """Проверка на DICOM файл"""
514
  if not os.path.exists(path):
515
  return False
516
  ext = os.path.splitext(path)[1].lower()
517
+ if ext not in {".dcm", ""}:
518
  return False
519
  try:
520
  pydicom.dcmread(path, stop_before_pixels=True)
 
524
 
525
 
526
  def _files_to_paths(files) -> List[str]:
527
+ """Конвертация файлов в пути"""
528
+ if not files:
529
+ return []
530
+ raw_paths = [f.name for f in files if hasattr(f, "name") and os.path.exists(f.name)]
531
  return [p for p in raw_paths if _is_dicom_path(p)]
532
 
533
 
534
  def _collect_input_paths(files_all, folder_files) -> List[str]:
535
+ """Сбор всех путей из загрузок"""
536
  combined = _files_to_paths(files_all) + _files_to_paths(folder_files)
 
537
  return list(dict.fromkeys(combined))
538
 
539
 
540
  def _build_report_file(result: Dict[str, Any]) -> str:
541
+ """Создание отчета в JSON"""
542
  ts = datetime.now().strftime("%Y%m%d_%H%M%S")
543
  with tempfile.NamedTemporaryFile(
544
  mode="w",
545
+ prefix=f"autoangioscore_report_{ts}_",
546
  suffix=".json",
547
  encoding="utf-8",
548
  delete=False,
 
551
  return f.name
552
 
553
 
554
+ def _status_badge_html(state: str, with_animation: bool = False) -> str:
555
+ """Генерация HTML для статус бейджа"""
556
  state = state.lower()
557
+
558
+ status_config = {
559
+ "running": {"class": "status-running", "icon": "⏱️", "text": "Выполняется"},
560
+ "done": {"class": "status-done", "icon": "✅", "text": "Завершено"},
561
+ "error": {"class": "status-error", "icon": "❌", "text": "Ошибка"},
562
+ "queued": {"class": "status-queued", "icon": "⏳", "text": "В очереди"}
563
+ }
564
+
565
+ config = status_config.get(state, status_config["queued"])
566
+ animation_class = " pulse" if with_animation and state == "running" else ""
567
+
568
+ return f'<span class="status-badge {config["class"]}{animation_class}">{config["icon"]} {config["text"]}</span>'
569
+
570
+
571
+ def _update_status_table(studies, status: str) -> List[List]:
572
+ """Обновление таблицы со статусами"""
573
+ badge = _status_badge_html(status, with_animation=(status == "running"))
574
  return [
575
+ [s.get("name", "-"), s.get("description", "-") or "-", len(s.get("files", [])), badge]
576
  for s in (studies or [])
577
  ]
578
 
579
 
580
  def _format_results_html(result: Dict[str, Any]) -> str:
581
+ """Форматирование результатов в красивом HTML"""
582
  if not result:
583
+ return '''
584
+ <div class="apple-card" style="padding: 2rem; text-align: center;">
585
+ <div style="color: var(--text-secondary);">
586
+ 📊 Результаты появятся после запуска анализа
587
+ </div>
588
+ </div>
589
+ '''
590
+
591
  if "error" in result:
592
+ return f'''
593
+ <div class="apple-card" style="padding: 1.5rem; background: linear-gradient(135deg, rgba(239,68,68,0.10) 0%, rgba(220,38,38,0.10) 100%);">
594
+ <div style="color: var(--accent-red); display: flex; align-items: center; gap: 0.75rem;">
595
+ <span style="font-size: 1.5rem;">⚠️</span>
596
+ <div>
597
+ <strong>Ошибка</strong>
598
+ <div style="margin-top: 0.25rem;">{result.get("error", "Неизвестная ошибка")}</div>
599
+ </div>
600
+ </div>
601
+ </div>
602
+ '''
603
+
604
  studies = result.get("studies", []) or []
605
  if not studies:
606
+ return '''
607
+ <div class="apple-card" style="padding: 2rem; text-align: center;">
608
+ <div style="color: var(--text-secondary);">
609
+ 📋 Нет исследований для отображения
610
+ </div>
611
+ </div>
612
+ '''
613
+
614
+ cards_html = []
615
  for s in studies:
616
+ study_name = s.get("study", "-")
617
+ description = s.get("description", "")
618
+
619
  left_mean = s.get("left", {}).get("mean", 0.0)
620
  right_mean = s.get("right", {}).get("mean", 0.0)
 
621
  total_obj = s.get("total", {})
622
  total_mean = total_obj.get("mean", 0.0)
623
+
624
+ risk_key = next((k for k in total_obj.keys() if "High-risk" in str(k)), None)
625
+ is_high_risk = bool(total_obj.get(risk_key, False)) if risk_key else total_mean > 0.5
626
+
627
+ if is_high_risk:
628
+ badge_bg = "#fee2e2"
629
+ badge_color = "#991b1b"
630
+ badge_text = "⚠️ Высокий риск"
631
+ else:
632
+ badge_bg = "#dcfce7"
633
+ badge_color = "#14532d"
634
+ badge_text = "✅ Низкий риск"
635
+
636
+ badge_style = f"""
637
+ background: {badge_bg};
638
+ color: {badge_color};
639
+ padding: 8px 16px;
640
+ border-radius: 20px;
641
+ font-size: 14px;
642
+ font-weight: 700;
643
+ display: inline-flex;
644
+ align-items: center;
645
+ gap: 6px;
646
+ border: 1px solid rgba(0,0,0,0.05);
647
+ """
648
+
649
+ card_html = f'''
650
+ <div class="apple-card" style="margin-bottom: 1rem;">
651
+ <div class="apple-card-header">
652
+ <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
653
+ <div style="display: flex; align-items: center; gap: 8px;">
654
+ <span style="font-size: 1.25rem; font-weight: 700; color: var(--text-primary);">🏥 {study_name}</span>
655
+ </div>
656
+ <div style="{badge_style}">
657
+ {badge_text}
658
+ </div>
659
  </div>
660
+ {f'<div style="font-size: 0.875rem; color: var(--text-secondary); margin-top: 0.5rem;">{description}</div>' if description else ''}
 
 
 
 
661
  </div>
662
+ <div class="apple-card-content">
663
+ <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
664
+
665
+ <!-- Левая артерия -->
666
+ <div style="text-align: center;
667
+ background: linear-gradient(135deg, rgba(6,182,212,0.10) 0%, rgba(59,130,246,0.10) 100%);
668
+ border-radius: 16px;
669
+ padding: 0.85rem;
670
+ border: 1px solid rgba(59,130,246,0.15);">
671
+ <div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
672
+ Левая артерия
673
+ </div>
674
+ <div style="font-size: 1.5rem; font-weight: 800;
675
+ background: linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%);
676
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
677
+ {left_mean:.3f}
678
+ </div>
679
+ </div>
680
+
681
+ <!-- Правая артерия -->
682
+ <div style="text-align: center;
683
+ background: linear-gradient(135deg, rgba(139,92,246,0.10) 0%, rgba(168,85,247,0.10) 100%);
684
+ border-radius: 16px;
685
+ padding: 0.85rem;
686
+ border: 1px solid rgba(168,85,247,0.15);">
687
+ <div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
688
+ Правая артерия
689
+ </div>
690
+ <div style="font-size: 1.5rem; font-weight: 800;
691
+ background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
692
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
693
+ {right_mean:.3f}
694
+ </div>
695
+ </div>
696
+
697
+ <!-- Общий балл -->
698
+ <div style="text-align: center;
699
+ background: linear-gradient(135deg, rgba(249,115,22,0.10) 0%, rgba(251,191,36,0.10) 100%);
700
+ border-radius: 16px;
701
+ padding: 0.85rem;
702
+ border: 1px solid rgba(249,115,22,0.20);">
703
+ <div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.5rem;">
704
+ Общий балл
705
+ </div>
706
+ <div style="font-size: 1.75rem; font-weight: 900;
707
+ background: linear-gradient(135deg, #f97316 0%, #fbbf24 100%);
708
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
709
+ {total_mean:.3f}
710
+ </div>
711
+ </div>
712
 
713
+ </div>
 
 
 
 
 
 
 
 
714
  </div>
715
+ </div>
716
+ '''
717
+ cards_html.append(card_html)
718
+
719
+ return "".join(cards_html)
720
+ # ==================== ОСНОВНОЙ UI ====================
721
+
722
+ def create_ui():
723
+ """Создание интерфейса в стиле Apple"""
724
+
725
+ with gr.Blocks(title="AutoAngioScore") as demo:
726
+
727
+ # Главный контейнер
728
+ with gr.Column(elem_classes="apple-container"):
729
+
730
+ # Хедер
731
+ with gr.Row(elem_classes="apple-header"):
732
+ with gr.Column(scale=1, min_width=0):
733
+ logo_html = _logo_html()
734
+ if logo_html:
735
+ gr.HTML(f'<div style="margin-bottom: 1rem;">{logo_html}</div>')
736
+
737
+ gr.HTML('''
738
+ <h1 class="apple-title">AutoAngioScore</h1>
739
+ <p class="apple-subtitle">Автоматическая оценка степени коронарного поражения по видеозаписям ангиографии</p>
740
+ ''')
741
+
742
+ # ==================== БЛОК ДОБАВЛЕНИЯ ИССЛЕДОВАНИЯ ====================
743
+ with gr.Group(elem_classes="apple-card add-study-card"):
744
+ with gr.Column(elem_classes="apple-card-header"):
745
+ gr.HTML('<h3 class="apple-card-title">✨ Добавление нового исследования</h3>')
746
+
747
+ with gr.Column(elem_classes="apple-card-content"):
748
+ # Информация об исследовании
749
+ with gr.Row():
750
+ study_name = gr.Textbox(
751
+ label="Идентификатор исследования",
752
+ placeholder="Например: INV-2024-001",
753
+ elem_classes="apple-input",
754
+ scale=1
755
+ )
756
+ study_desc = gr.Textbox(
757
+ label="Описание (необязательно)",
758
+ placeholder="Клинические данные, особенности...",
759
+ elem_classes="apple-input",
760
+ scale=1
761
+ )
762
+
763
+ # Загрузка DICOM - две колонки
764
+ gr.HTML('<div class="upload-grid">')
765
+
766
+ with gr.Column(elem_classes="apple-card", variant="panel"):
767
+ with gr.Column(elem_classes="apple-card-header"):
768
+ gr.HTML('<h3 class="apple-card-title" style="font-size: 1rem;">📄 Отдельные файлы</h3>')
769
+
770
+ with gr.Column(elem_classes="apple-card-content"):
771
+ files_all = gr.File(
772
+ label="Выберите DICOM файлы",
773
+ file_count="multiple",
774
+ elem_classes="apple-input"
775
+ )
776
+ gr.HTML('''
777
+ <div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.5rem;">
778
+ ✓ Поддерживаются файлы .dcm
779
+ </div>
780
+ ''')
781
+
782
+ with gr.Column(elem_classes="apple-card", variant="panel"):
783
+ with gr.Column(elem_classes="apple-card-header"):
784
+ gr.HTML('<h3 class="apple-card-title" style="font-size: 1rem;">📁 Папка с исследованием</h3>')
785
+
786
+ with gr.Column(elem_classes="apple-card-content"):
787
+ files_folder = gr.File(
788
+ label="Выберите папку",
789
+ file_count="directory",
790
+ elem_classes="apple-input"
791
+ )
792
+ gr.HTML('''
793
+ <div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.5rem;">
794
+ ✓ Выберите папку, содержащую DICOM файлы
795
+ </div>
796
+ ''')
797
+
798
+ gr.HTML('</div>')
799
+
800
+ # Кнопки управления добавлением
801
+ with gr.Row():
802
+ btn_add = gr.Button(
803
+ "✨ Добавить в очередь",
804
+ elem_classes="apple-button-add",
805
+ scale=1
806
+ )
807
+ btn_clear = gr.Button(
808
+ "🗑️ Очистить форму",
809
+ elem_classes="apple-button-clear",
810
+ scale=1
811
+ )
812
+
813
+ # Краткая инструкция
814
+ gr.HTML('''
815
+ <div class="instruction-box">
816
+ <div class="instruction-title">
817
+ <span>🎯</span> Быстрый старт
818
+ </div>
819
+ <ul class="instruction-list">
820
+ <li>📝 1. Укажите ID исследования</li>
821
+ <li>📂 2. Загрузите DICOM файлы</li>
822
+ <li>➕ 3. Добавьте в очередь</li>
823
+ <li>🚀 4. Запустите анализ</li>
824
+ </ul>
825
+ </div>
826
+ ''')
827
+
828
+ # ==================== ОЧЕРЕДЬ ИССЛЕДОВАНИЙ ====================
829
+ with gr.Group(elem_classes="apple-card"):
830
+ with gr.Column(elem_classes="apple-card-header"):
831
+ gr.HTML('<h3 class="apple-card-title">📋 Очередь исследований</h3>')
832
+
833
+ with gr.Column(elem_classes="apple-card-content"):
834
+ queue_table = gr.Dataframe(
835
+ headers=["Исследование", "Описание", "Файлов", "Статус"],
836
+ datatype=["str", "str", "number", "html"],
837
+ interactive=False,
838
+ elem_classes="apple-table",
839
+ row_count=(0, "dynamic"),
840
+ wrap=True
841
+ )
842
+
843
+ gr.HTML('''
844
+ <div style="font-size: 0.8125rem; color: var(--text-secondary); margin-top: 0.75rem; text-align: center;">
845
+ 💡 Добавьте исследования в очередь для пакетного анализа
846
+ </div>
847
+ ''')
848
+
849
+ # ==================== ЗАПУСК АНАЛИЗА ====================
850
+ with gr.Row():
851
+ run_btn = gr.Button(
852
+ "🚀 Запустить анализ очереди",
853
+ elem_classes="apple-button"
854
+ )
855
+
856
+ # ==================== РЕЗУЛЬТАТЫ ====================
857
+ with gr.Group(elem_classes="apple-card"):
858
+ with gr.Column(elem_classes="apple-card-header"):
859
+ gr.HTML('<h3 class="apple-card-title">📊 Результаты анализа</h3>')
860
+
861
+ with gr.Column(elem_classes="apple-card-content"):
862
+ out_summary = gr.HTML(label="")
863
+ out_json = gr.JSON(label="Подробные данные", visible=False)
864
+
865
+ with gr.Row():
866
+ out_report = gr.File(
867
+ label="📥 Скачать отчет JSON",
868
+ interactive=False,
869
+ elem_classes="apple-input"
870
+ )
871
+
872
+ # Footer
873
+ gr.HTML('''
874
+ <div style="margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.15); text-align: center; font-size: 0.75rem; color: rgba(255,255,255,0.7);">
875
+ <p>⚠️ Для исследовательских целей. Не является медицинским устройством.</p>
876
+ <p>© 2024 AutoAngioScore | Инновационная оценка коронарного поражения</p>
877
+ </div>
878
+ ''')
879
+
880
+ # ==================== ЛОГИКА ====================
881
+ studies_state = gr.State([])
882
+
883
+ def _add_study_fn(studies: List[Dict[str, Any]], name: str, desc: str, files, folder):
884
+ name = (name or "").strip() or f"Исследование_{len(studies)+1}"
885
  desc = (desc or "").strip()
886
  paths = _collect_input_paths(files, folder)
887
+
888
  if not paths:
 
889
  table = _update_status_table(studies, "Queued")
890
  return studies, table, name, desc, files, folder
891
+
892
+ new_study = {
893
+ "name": name,
894
+ "description": desc,
895
+ "files": paths
896
+ }
897
+ studies = studies + [new_study]
898
  table = _update_status_table(studies, "Queued")
899
  return studies, table, "", "", None, None
900
+
901
  btn_add.click(
902
  _add_study_fn,
903
  inputs=[studies_state, study_name, study_desc, files_all, files_folder],
904
+ outputs=[studies_state, queue_table, study_name, study_desc, files_all, files_folder]
905
  )
906
+
907
+ def _clear_form():
908
+ return "", "", None, None
909
+
910
+ btn_clear.click(_clear_form, inputs=None, outputs=[study_name, study_desc, files_all, files_folder])
911
+
912
  def _clear_all():
913
  return [], []
914
+
915
+ # Доба��ляем кнопку очистки очереди (скрытая функциональность)
916
+ with gr.Row(visible=False):
917
+ clear_queue = gr.Button("Очистить очередь")
918
+ clear_queue.click(_clear_all, inputs=None, outputs=[studies_state, queue_table])
919
+
 
 
 
920
  def _before_run(studies):
 
921
  return _update_status_table(studies, "Running")
922
+
923
  def _run_infer(studies):
924
+ study_objs = []
925
+ for s in (studies or []):
926
+ study_objs.append(Study(name=s["name"], description=s.get("description", ""), files=s["files"]))
927
  result = run_inference(study_objs)
928
  report_path = _build_report_file(result)
929
  return result, report_path
930
+
931
  def _after_run(studies, result, report_path):
932
  table = _update_status_table(studies, "Done")
933
  return _format_results_html(result), result, report_path, table
934
+
935
  run_btn.click(
936
  _before_run,
937
  inputs=[studies_state],
938
+ outputs=[queue_table]
939
  ).then(
940
  _run_infer,
941
  inputs=[studies_state],
942
+ outputs=[out_json, out_report]
943
  ).then(
944
  _after_run,
945
  inputs=[studies_state, out_json, out_report],
946
+ outputs=[out_summary, out_json, out_report, queue_table]
947
  )
948
+
 
949
  return demo
950
 
951
 
952
  if __name__ == "__main__":
953
  favicon = LOGO_PATH if (LOGO_PATH and os.path.exists(LOGO_PATH)) else None
954
+ demo = create_ui()
955
+ demo.launch(
956
+ favicon_path=favicon,
957
+ server_name="0.0.0.0",
958
+ server_port=7860,
959
+ share=False,
960
+ css=APPLE_STYLE_CSS
961
+ )