Iamgm commited on
Commit
3050255
·
1 Parent(s): 20f0014

Deploy Code + Weights via LFS

Browse files
.gitattributes CHANGED
@@ -1,3 +1,4 @@
 
1
  *.gif filter=lfs diff=lfs merge=lfs -text
2
  *.mp4 filter=lfs diff=lfs merge=lfs -text
3
  *.ipynb filter=lfs diff=lfs merge=lfs -text
 
1
+ *.pt filter=lfs diff=lfs merge=lfs -text
2
  *.gif filter=lfs diff=lfs merge=lfs -text
3
  *.mp4 filter=lfs diff=lfs merge=lfs -text
4
  *.ipynb filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -14,8 +14,8 @@ data/
14
  *.tar
15
 
16
  # Models (веса тяжелые)
17
- weights/
18
- *.pt
19
 
20
  # Environment
21
  .DS_Store
 
14
  *.tar
15
 
16
  # Models (веса тяжелые)
17
+ # weights/
18
+ # *.pt
19
 
20
  # Environment
21
  .DS_Store
deploy/Dockerfile DELETED
@@ -1,27 +0,0 @@
1
- # используем легкий Python 3.11
2
- FROM python:3.11-slim
3
-
4
- # устанавливаем системные зависимости для OpenCV (важно для YOLO)
5
- RUN apt-get update && apt-get install -y \
6
- libgl1-mesa-glx \
7
- libglib2.0-0 \
8
- && rm -rf /var/lib/apt/lists/*
9
-
10
- # создаем рабочую директорию
11
- WORKDIR /app
12
-
13
- # копируем файлы зависимостей и устанавливаем их
14
- COPY requirements.txt .
15
- RUN pip install --no-cache-dir -r requirements.txt
16
-
17
- # копируем весь проект в контейнер
18
- COPY . .
19
-
20
- # даем права на выполнение скрипта запуска
21
- RUN chmod +x start.sh
22
-
23
- # открываем порт 7860 (Hugging Face)
24
- EXPOSE 7860
25
-
26
- # команда запуска
27
- CMD ["./start.sh"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/README.md DELETED
@@ -1,12 +0,0 @@
1
- ---
2
- title: Powerline Defect Detection
3
- emoji: 🦀
4
- colorFrom: gray
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: powerline defect detection
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/src/__init__.py DELETED
File without changes
deploy/src/api/__init__.py DELETED
File without changes
deploy/src/api/main.py DELETED
@@ -1,64 +0,0 @@
1
- import io
2
- from fastapi import FastAPI, UploadFile, File, Query, HTTPException
3
- from PIL import Image
4
- from typing import Literal
5
-
6
- # импорты из наших модулей
7
- from src.api.schemas import PredictionResponse
8
- from src.ml.predictor import DefectPredictor
9
-
10
- app = FastAPI(
11
- title="PowerLine Defect Detection API",
12
- description="API для детекции дефектов ЛЭП (YOLO OBB)",
13
- version="1.0.0"
14
- )
15
-
16
- # предиктор подгрузит модели только при первом запросе
17
- predictor = DefectPredictor()
18
-
19
- @app.get("/")
20
- def health_check():
21
- return {
22
- "status": "ok",
23
- "version": "1.0.0",
24
- "models_available": list(predictor.weights_map.keys())
25
- }
26
-
27
- @app.post("/predict", response_model=PredictionResponse)
28
- async def predict_endpoint(
29
- file: UploadFile = File(...),
30
- # параметр выбора модели
31
- model_type: Literal["fast", "accurate"] = Query("fast", description="Выбор модели: fast (YOLO-S) или accurate (YOLO-L)"),
32
- # параметр порога уверенности (от 0.0 до 1.0)
33
- conf_threshold: float = Query(0.4, ge=0.0, le=1.0, description="Порог уверенности (Confidence Threshold)")
34
- ):
35
- """
36
- Принимает изображение и возвращает найденные объекты (OBB полигоны).
37
- """
38
- # валидация файла
39
- if not file.content_type.startswith("image/"):
40
- raise HTTPException(status_code=400, detail="Файл должен быть изображением")
41
-
42
- try:
43
- # чтение картинки
44
- image_bytes = await file.read()
45
- image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
46
-
47
- # передаем параметры в ML модуль
48
- detections = predictor.predict(
49
- image=image,
50
- model_key=model_type,
51
- conf_threshold=conf_threshold
52
- )
53
-
54
- # формирование ответа
55
- return {
56
- "filename": file.filename,
57
- "image_size": [image.width, image.height],
58
- "model_used": model_type,
59
- "detections": detections
60
- }
61
-
62
- except Exception as e:
63
- print(f"Error processing image: {e}")
64
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/src/api/schemas.py DELETED
@@ -1,27 +0,0 @@
1
- from pydantic import BaseModel
2
- from typing import List, Optional, Tuple
3
-
4
- class BoundingBox(BaseModel):
5
- # обычный прямоугольник для совместимости
6
- x1: float
7
- y1: float
8
- x2: float
9
- y2: float
10
-
11
- class Detection(BaseModel):
12
- class_name: str
13
- class_id: int
14
- confidence: float
15
-
16
- # OBB - это список точек [[x,y], [x,y], [x,y], [x,y]]
17
- # делаем Optional, чтобы не ломать старый код
18
- polygon: Optional[List[Tuple[float, float]]] = None
19
-
20
- # оставляем box для обратной совместимости
21
- box: BoundingBox
22
-
23
- class PredictionResponse(BaseModel):
24
- filename: str
25
- image_size: List[int] # [width, height]
26
- model_used: str # тип модели (small/large)
27
- detections: List[Detection]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/src/config.py DELETED
@@ -1,7 +0,0 @@
1
- import os
2
-
3
- class Settings:
4
- PROJECT_NAME = 'PowerLine Defects'
5
- REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
6
-
7
- settings = Settings()
 
 
 
 
 
 
 
 
deploy/src/core/__init__.py DELETED
File without changes
deploy/src/ml/__init__.py DELETED
File without changes
deploy/src/ml/predictor.py DELETED
@@ -1,85 +0,0 @@
1
- import os
2
- from PIL import Image
3
- from ultralytics import YOLO
4
- import torch
5
-
6
- class DefectPredictor:
7
- def __init__(self):
8
- self.models = {}
9
- self.active_model_name = None
10
-
11
- # пути к весам (локальные)
12
- self.weights_map = {
13
- "fast": "weights/yolo26s_obb_best.pt",
14
- "accurate": "weights/yolo26l_obb_best.pt"
15
- }
16
-
17
- # проверяем наличие GPU локально
18
- self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
19
- print(f"🚀 ML Service initialized on {self.device}")
20
-
21
- def load_model(self, model_key: str):
22
- """Ленивая загрузка модели"""
23
- if model_key not in self.weights_map:
24
- raise ValueError(f"Unknown model key: {model_key}")
25
-
26
- # если модель уже загружена - возвращаем её
27
- if model_key in self.models:
28
- return self.models[model_key]
29
-
30
- # если грузим новую, а памяти мало - опционально можно выгрузить старую
31
- # self.models.clear()
32
- # torch.cuda.empty_cache()
33
-
34
- print(f"🔄 Loading model: {model_key}...")
35
- path = self.weights_map[model_key]
36
-
37
- if not os.path.exists(path):
38
- raise FileNotFoundError(f"Model weights not found at {path}")
39
-
40
- model = YOLO(path)
41
- model.to(self.device)
42
- self.models[model_key] = model
43
- return model
44
-
45
- def predict(self, image: Image.Image, model_key: str = "fast", conf_threshold: float = 0.4):
46
- """
47
- Инференс
48
- """
49
- model = self.load_model(model_key)
50
-
51
- # инференс
52
- # imgsz можно меньше локально, но лучше 1024 как учили
53
- results = model.predict(image, conf=conf_threshold, imgsz=1024, verbose=False)
54
- result = results[0]
55
-
56
- formatted_detections = []
57
-
58
- # парсим OBB результаты
59
- if result.obb is not None:
60
- for i, cls_id in enumerate(result.obb.cls):
61
- cls_id = int(cls_id)
62
- conf = float(result.obb.conf[i])
63
-
64
- # xyxyxyxy - координаты 4 углов (полигон)
65
- # переводим тензор в список списков
66
- poly_tensor = result.obb.xyxyxyxy[i]
67
- # [[x1,y1], [x2,y2], ...]
68
- polygon = poly_tensor.cpu().numpy().tolist()
69
-
70
-
71
- # xyxy - описывающий прямоугольник (для совместимости)
72
- box_tensor = result.obb.xyxy[i]
73
- x1, y1, x2, y2 = map(float, box_tensor.cpu().numpy())
74
-
75
- class_name = result.names[cls_id]
76
-
77
- formatted_detections.append({
78
- "class_name": class_name,
79
- "class_id": cls_id,
80
- "confidence": conf,
81
- "polygon": polygon,
82
- "box": {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
83
- })
84
-
85
- return formatted_detections
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/src/ml/trainer.py DELETED
@@ -1 +0,0 @@
1
- # Training pipeline with ClearML
 
 
deploy/src/ui/__init__.py DELETED
File without changes
deploy/src/ui/app.py DELETED
@@ -1,379 +0,0 @@
1
- import streamlit as st
2
- import requests
3
- from PIL import Image, ImageDraw
4
- import io
5
- import base64
6
- import random
7
-
8
- #-----------------------------------------------------------------------------
9
- # config
10
- API_URL = "http://127.0.0.1:8000/predict"
11
-
12
- # цветовая схема
13
- THEME_COLOR = "#0078D7"
14
- BG_COLOR = "#F0F2F6"
15
-
16
- CLASS_COLORS = {
17
- "bad_insulator": "#FF2B2B",
18
- "damaged_insulator": "#D02090",
19
- "nest": "#FF8C00",
20
- "festoon_insulators": "#00C853",
21
- "polymer_insulators": "#00C853",
22
- "vibration_damper": "#00BFFF",
23
- "traverse": "#FFD700",
24
- "safety_sign+": "#4169E1"
25
- }
26
- ALL_CLASSES = list(CLASS_COLORS.keys())
27
-
28
- # фразы для загрузки
29
- SPINNER_PHRASES = [
30
- "Надеваем диэлектрические перчатки... 🧤",
31
- "Прозваниваем нейронные связи на предмет КЗ... ⚡",
32
- "Считаем воробьев на проводах... 🐦",
33
- "Ищем косинус фи в стоге сена... 🌾",
34
- "Заземляем ожидания... ⏚",
35
- "Протираем линзы виртуальных очков... 👓",
36
- "Торгуемся с трансформаторной будкой... 🏗️",
37
- "Выпрямляем синусоиду вручную... 〰️",
38
- "Уговариваем веса не улетать в бесконечность... 📉",
39
- "Объясняем нейронке, что птица — это не дефект... 🦅",
40
- "Матрицы перемножаются, искры летят... ✨",
41
- "Пытаемся найти глобальный минимум в чашке кофе... ☕",
42
- "Бэкпропагейтим до состояния просветления... 🧘",
43
- "GPU просит пощады, но мы продолжаем... 🔥",
44
- "Нормализуем данные и самооценку... 📏",
45
- "Слой за слоем, как бабушкин торт... 🍰",
46
- "Загружаем пиксели в ведро... 🪣",
47
- "Скармливаем данные тензорам. Кажется, им нравится... 😋",
48
- "Подождите, нейросеть пошла за синей изолентой... 🟦",
49
- "Спрашиваем мнение у ChatGPT, но он не отвечает... 🤖",
50
- "Генерируем оправдания для ложных срабатываний... 😅",
51
- "Квантуем пространство и время... 🌌",
52
- "Взламываем реальность через 443 порт... 🔓",
53
- "Вспоминаем формулу градиентного спуска... 📉",
54
- "Исправляем баги, созданные вчерашним мной... 🐛",
55
- "Молимся богам CUDA... 🙏",
56
- "Проверка пройдена на 99%. Остался 1% неопределенности... 🎲"
57
- ]
58
-
59
- #-----------------------------------------------------------------------------
60
- # zoom
61
- def render_zoomable_image(image_pil, caption=""):
62
- img_copy = image_pil.copy()
63
- img_copy.thumbnail((800, 800))
64
-
65
- buffered = io.BytesIO()
66
- img_copy.save(buffered, format="PNG")
67
- img_str = base64.b64encode(buffered.getvalue()).decode()
68
-
69
- html_code = f"""
70
- <style>
71
- .zoom-container {{
72
- position: relative;
73
- overflow: hidden;
74
- border-radius: 8px;
75
- cursor: crosshair;
76
- width: 100%;
77
- }}
78
- .zoom-img {{
79
- width: 100%;
80
- height: auto;
81
- display: block;
82
- transition: transform 0.2s ease;
83
- }}
84
- .zoom-container:hover .zoom-img {{
85
- transform: scale(2.5);
86
- transform-origin: center center;
87
- }}
88
- </style>
89
-
90
- <div class="zoom-container" onmousemove="zoom(event)" onmouseleave="reset(event)">
91
- <img src="data:image/png;base64,{img_str}" class="zoom-img" id="img-{caption}">
92
- </div>
93
- <div style="margin-top: 5px; color: #555; font-size: 0.9em;">{caption}</div>
94
-
95
- <script>
96
- function zoom(e) {{
97
- var zoomer = e.currentTarget;
98
- var img = zoomer.querySelector('.zoom-img');
99
- var rect = zoomer.getBoundingClientRect();
100
- var x = e.clientX - rect.left;
101
- var y = e.clientY - rect.top;
102
-
103
- var xPercent = (x / rect.width) * 100;
104
- var yPercent = (y / rect.height) * 100;
105
-
106
- img.style.transformOrigin = xPercent + "% " + yPercent + "%";
107
- }}
108
- function reset(e) {{
109
- var img = e.currentTarget.querySelector('.zoom-img');
110
- img.style.transformOrigin = "center center";
111
- }}
112
- </script>
113
- """
114
- st.components.v1.html(html_code, height=400, scrolling=False)
115
-
116
- #-----------------------------------------------------------------------------
117
- # setup page
118
- st.set_page_config(page_title="PowerLine Defect Detection", page_icon="⚡", layout="wide")
119
-
120
- # cSS HACKS
121
- st.markdown(f"""
122
- <style>
123
- :root {{ --primary-color: {THEME_COLOR}; }}
124
- div.stButton > button {{
125
- background-color: {THEME_COLOR};
126
- color: white;
127
- border-radius: 8px;
128
- border: none;
129
- padding: 10px 24px;
130
- transition: all 0.3s;
131
- }}
132
- div.stButton > button:hover {{
133
- background-color: #005A9E;
134
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
135
- }}
136
- .css-164nlkn {{ display: none; }}
137
- .streamlit-expanderHeader {{
138
- font-weight: bold;
139
- background-color: white;
140
- border-radius: 8px;
141
- }}
142
- </style>
143
- """, unsafe_allow_html=True)
144
-
145
- #-----------------------------------------------------------------------------
146
- # state
147
- if 'results' not in st.session_state:
148
- st.session_state.results = {}
149
- if 'uploader_key' not in st.session_state:
150
- st.session_state.uploader_key = 0
151
- if 'clean_expanded' not in st.session_state:
152
- st.session_state.clean_expanded = False
153
-
154
- def reset_uploader():
155
- st.session_state.uploader_key += 1
156
- st.session_state.results = {}
157
-
158
- #-----------------------------------------------------------------------------
159
- # обрабатка 1 файла
160
- def process_single_file(file_obj, model_key, conf):
161
- try:
162
- file_obj.seek(0)
163
- params = {"model_type": model_key, "conf_threshold": conf}
164
- files = {"file": ("image", file_obj, file_obj.type)}
165
- response = requests.post(API_URL, params=params, files=files)
166
- if response.status_code == 200:
167
- return response.json()
168
- else:
169
- return {"error": response.text}
170
- except Exception as e:
171
- return {"error": str(e)}
172
-
173
- def draw_detections(file_obj, detections, selected_classes):
174
- image = Image.open(file_obj).convert("RGB")
175
- draw = ImageDraw.Draw(image)
176
-
177
- count_defects = 0
178
- count_visible = 0
179
-
180
- for det in detections:
181
- cls = det['class_name']
182
- if cls not in selected_classes:
183
- continue
184
-
185
- count_visible += 1
186
- if cls in ["bad_insulator", "damaged_insulator", "nest"]:
187
- count_defects += 1
188
-
189
- color = CLASS_COLORS.get(cls, "#FFFFFF")
190
-
191
- if det.get('polygon'):
192
- poly = [c for p in det['polygon'] for c in p]
193
- draw.polygon(poly, outline=color, width=4)
194
- txt_pos = tuple(det['polygon'][0])
195
- else:
196
- b = det['box']
197
- draw.rectangle([b['x1'], b['y1'], b['x2'], b['y2']], outline=color, width=4)
198
- txt_pos = (b['x1'], b['y1'])
199
-
200
- label = f"{cls} {det['confidence']:.2f}"
201
- bbox = draw.textbbox(txt_pos, label)
202
- draw.rectangle(bbox, fill=color)
203
- draw.text(txt_pos, label, fill="black")
204
-
205
- return image, count_defects, count_visible
206
-
207
- #-----------------------------------------------------------------------------
208
- # sidebar
209
- with st.sidebar:
210
-
211
- #-----------------------------------------------------------------------------
212
- # Ссылка на профиль github
213
-
214
-
215
- # SVG икончка github
216
- svg_code = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.399 1.02 0 2.047.133 3.006.4 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>"""
217
-
218
- # кодируем в строку Base64
219
- b64_str = base64.b64encode(svg_code.encode("utf-8")).decode("utf-8")
220
- img_src = f"data:image/svg+xml;base64,{b64_str}"
221
- github_url = "https://github.com/iamgm"
222
-
223
- st.markdown(f"""
224
- <a href="{github_url}" target="_blank" style="text-decoration: none; display: block; margin-bottom: 20px;">
225
- <div style="
226
- display: flex;
227
- align-items: center;
228
- justify-content: center;
229
- background-color: white;
230
- border: 1px solid #e0e0e0;
231
- border-radius: 8px;
232
- padding: 8px 16px;
233
- color: #333;
234
- transition: 0.3s;
235
- box-shadow: 0 1px 2px rgba(0,0,0,0.05);
236
- ">
237
- <img src="{img_src}" width="24" height="24" style="margin-right: 12px; display: block;">
238
- <span style="font-weight: 600; font-size: 16px;">GitHub Profile</span>
239
- </div>
240
- </a>
241
- """, unsafe_allow_html=True)
242
-
243
-
244
- #-----------------------------------------------------------------------------
245
-
246
- st.title("⚙️ Настройки")
247
-
248
- model_choice = st.radio("Модель:", ("Fast (Small)", "Accurate (Large)"))
249
- model_key = "fast" if "Small" in model_choice else "accurate"
250
-
251
- st.divider()
252
- conf_threshold = st.slider("Порог уверенности:", 0.1, 0.9, 0.4, 0.05)
253
-
254
- st.divider()
255
- st.write("Фильтр классов:")
256
-
257
- try:
258
- selected_classes = st.pills(
259
- "Классы", options=ALL_CLASSES, default=ALL_CLASSES, selection_mode="multi", label_visibility="collapsed"
260
- )
261
- except AttributeError:
262
- selected_classes = st.multiselect("Показать:", ALL_CLASSES, default=ALL_CLASSES)
263
-
264
- #-----------------------------------------------------------------------------
265
- # main page
266
- st.title("⚡ PowerLine Defect Detection")
267
-
268
- # загрузка
269
- with st.container():
270
- col_up, col_btn = st.columns([4, 1])
271
- with col_up:
272
- uploaded_files = st.file_uploader(
273
- "Перетащите файлы сюда:",
274
- type=["jpg", "png", "jpeg"],
275
- accept_multiple_files=True,
276
- key=f"uploader_{st.session_state.uploader_key}"
277
- )
278
- with col_btn:
279
- st.write("")
280
- st.write("")
281
- if st.button("🗑️ Clear All"):
282
- reset_uploader()
283
- st.rerun()
284
- #-----------------------------------------------------------------------------
285
- # запуск первичного анализа
286
- if uploaded_files:
287
- if st.button(f"▶️ ЗАПУСТИТЬ АНАЛИЗ ({len(uploaded_files)} ФОТО)", type="primary"):
288
- progress = st.progress(0)
289
- total_files = len(uploaded_files)
290
-
291
- for i, f in enumerate(uploaded_files):
292
- phrase = random.choice(SPINNER_PHRASES)
293
- spinner_text = f"[{i+1}/{total_files}] {phrase}"
294
-
295
- with st.spinner(spinner_text):
296
- res = process_single_file(f, model_key, conf_threshold)
297
- st.session_state.results[f.name] = {
298
- "file_obj": f,
299
- "data": res,
300
- "model": model_key,
301
- "checked_again": False
302
- }
303
-
304
- progress.progress((i+1)/total_files)
305
- st.rerun()
306
-
307
- #-----------------------------------------------------------------------------
308
- # отображение результатов
309
- if st.session_state.results:
310
- st.divider()
311
-
312
- defects_list = []
313
- clean_list = []
314
-
315
- for name, item in st.session_state.results.items():
316
- detections = item['data'].get('detections', [])
317
- visible_dets = [d for d in detections if d['class_name'] in selected_classes]
318
- has_defects = any(d['class_name'] in ["bad_insulator", "damaged_insulator", "nest"] for d in visible_dets)
319
-
320
- if has_defects:
321
- defects_list.append((name, item))
322
- else:
323
- clean_list.append((name, item))
324
-
325
- # блок 1. найдены дефекты
326
- if defects_list:
327
- st.subheader(f"🔴 Обнаружены дефекты ({len(defects_list)})")
328
- for name, item in defects_list:
329
- detections = item['data'].get('detections', [])
330
- img_res, cnt_def, cnt_vis = draw_detections(item['file_obj'], detections, selected_classes)
331
-
332
- with st.expander(f"⚠️ {name} | Дефектов: {cnt_def} | Модель: {item['model']}", expanded=True):
333
- st.caption("Наведите для увеличения 🔍")
334
- render_zoomable_image(img_res, caption="Результат")
335
-
336
- # блок 2. допроверка
337
- if clean_list:
338
-
339
- # заголовок и кнопка Expand All
340
- col_head, col_toggle = st.columns([3, 1])
341
- with col_head:
342
- st.subheader(f"🟢 Не найдено ({len(clean_list)})")
343
- with col_toggle:
344
- btn_label = "📂 Раскрыть все" if not st.session_state.clean_expanded else "📂 Свернуть все"
345
- if st.button(btn_label):
346
- st.session_state.clean_expanded = not st.session_state.clean_expanded
347
- st.rerun()
348
-
349
- need_check_names = [name for name, item in clean_list if not item.get('checked_again')]
350
-
351
- if need_check_names:
352
- st.info(f"Есть {len(need_check_names)} файлов, где ничего не найдено. Попробовать Accurate модель?")
353
- if st.button("🕵️ Перепроверить 'Accurate' моделью"):
354
- prog_bar = st.progress(0)
355
- total_check = len(need_check_names)
356
-
357
- for i, name in enumerate(need_check_names):
358
- phrase = random.choice(SPINNER_PHRASES)
359
- with st.spinner(f"[{i+1}/{total_check}] {phrase}"):
360
- item = st.session_state.results[name]
361
- new_res = process_single_file(item['file_obj'], "accurate", conf_threshold)
362
- st.session_state.results[name]['data'] = new_res
363
- st.session_state.results[name]['model'] = "accurate (re-check)"
364
- st.session_state.results[name]['checked_again'] = True
365
- prog_bar.progress((i+1)/total_check)
366
- st.rerun()
367
-
368
- with st.container(height=500):
369
- for name, item in clean_list:
370
- detections = item['data'].get('detections', [])
371
- img_res, _, cnt_vis = draw_detections(item['file_obj'], detections, selected_classes)
372
-
373
- icon = "✅" if not item.get('checked_again') else "🕵️✅"
374
- status = "Чисто" if cnt_vis == 0 else f"Объектов: {cnt_vis} (Норма)"
375
-
376
- # используем состояние для expanded
377
- with st.expander(f"{icon} {name} | {status} | {item['model']}", expanded=st.session_state.clean_expanded):
378
- st.caption("Наведите для увеличения 🔍")
379
- render_zoomable_image(img_res, caption=name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/src/worker/__init__.py DELETED
File without changes
deploy/src/worker/celery_app.py DELETED
@@ -1 +0,0 @@
1
- # Celery app configuration
 
 
deploy/src/worker/tasks.py DELETED
@@ -1 +0,0 @@
1
- # Async tasks definitions
 
 
deploy/start.sh DELETED
@@ -1,12 +0,0 @@
1
- #!/bin/bash
2
-
3
- # запускаем FastAPI в фоновом режиме на порту 8000
4
- echo "🚀 Starting FastAPI Backend..."
5
- uvicorn src.api.main:app --host 0.0.0.0 --port 8000 &
6
-
7
- # ждем пару секунд, чтобы сервер успел подняться
8
- sleep 5
9
-
10
- # запускаем Streamlit на порту 7860 (Требование Hugging Face)
11
- echo "🚀 Starting Streamlit Frontend..."
12
- streamlit run src/ui/app.py --server.port 7860 --server.address 0.0.0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
weights/yolo26l_obb_best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8633e81c7cf8c7a309c8b5f1ebea572ceb9d1df87cb603c279c0af0e62ba2e70
3
+ size 56454801
weights/yolo26s_obb_best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05cf4de4f60c83cfa7e32b45854e44b0c0985154e0e08b577bd6f49d2e5fc32c
3
+ size 21684828