DocUA commited on
Commit
d89a860
·
1 Parent(s): bd86d90

рефакторинг: розділення застосунку на компоненти класифікатора та інтерфейсу, покращення організації коду

Browse files
Files changed (3) hide show
  1. app.py +58 -340
  2. app_old.py +452 -0
  3. sdc_classifier.py +214 -0
app.py CHANGED
@@ -1,307 +1,25 @@
1
- import os
2
  import gradio as gr
3
- import pandas as pd
4
- import numpy as np
5
- import json
6
- from typing import Dict, List
7
-
8
- from openai import OpenAI
9
  from dotenv import load_dotenv
10
 
11
  # Load environment variables
12
  load_dotenv()
13
 
14
- # OpenAI setup
15
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
16
- client = OpenAI(api_key=OPENAI_API_KEY)
17
-
18
-
19
- DEFAULT_CLASSES_FILE = "classes_short.json" # Файл за замовчуванням
20
- DEFAULT_SIGNATURES_FILE = "signatures.npz" # Файл для збереження signatures
21
-
22
-
23
- ##############################################################################
24
- # 1. Функції для роботи з класами та signatures
25
- ##############################################################################
26
- def load_classes(json_path: str) -> dict:
27
- """
28
- Завантаження класів та їх хінтів з JSON файлу
29
- """
30
- try:
31
- with open(json_path, 'r', encoding='utf-8') as f:
32
- classes = json.load(f)
33
- return classes
34
- except FileNotFoundError:
35
- print(f"Файл {json_path} не знайдено! Використовуємо пустий словник класів.")
36
- return {}
37
- except json.JSONDecodeError:
38
- print(f"Помилка читання JSON з файлу {json_path}! Використовуємо пустий словник класів.")
39
- return {}
40
-
41
- def save_signatures(signatures: Dict[str, np.ndarray], filename: str = "signatures.npz") -> None:
42
- """
43
- Зберігає signatures у NPZ файл
44
- """
45
- if signatures:
46
- np.savez(filename, **signatures)
47
-
48
- def load_signatures(filename: str = "signatures.npz") -> Dict[str, np.ndarray]:
49
- """
50
- Завантажує signatures з NPZ файлу
51
- """
52
- try:
53
- with np.load(filename) as data:
54
- return {key: data[key] for key in data.files}
55
- except (FileNotFoundError, IOError):
56
- return None
57
-
58
- def reload_classes_and_signatures(json_path: str, model_name: str, force_rebuild: bool) -> str:
59
- """
60
- Перезавантажує класи з нового JSON файлу та оновлює signatures
61
- """
62
- global classes_json, class_signatures
63
-
64
- try:
65
- new_classes = load_classes(json_path)
66
- if not new_classes:
67
- return "Помилка: Файл класів порожній або має неправильний формат"
68
-
69
- classes_json = new_classes
70
- print(f"Завантажено {len(classes_json)} класів з {json_path}")
71
-
72
- # Зберігаємо новий файл класів як файл за замовчуванням
73
- try:
74
- with open(DEFAULT_CLASSES_FILE, 'w', encoding='utf-8') as f:
75
- json.dump(classes_json, f, ensure_ascii=False, indent=2)
76
- print(f"Збережено як {DEFAULT_CLASSES_FILE}")
77
- except Exception as e:
78
- print(f"Помилка при збереженні файлу класів: {str(e)}")
79
-
80
- result = initialize_signatures(
81
- model_name=model_name,
82
- signatures_file=DEFAULT_SIGNATURES_FILE,
83
- force_rebuild=force_rebuild
84
- )
85
- return f"Класи оновлено. {result}"
86
-
87
- except Exception as e:
88
- return f"Помилка при оновленні класів: {str(e)}"
89
-
90
-
91
- def initialize_signatures(model_name: str = "text-embedding-3-large",
92
- signatures_file: str = "signatures.npz",
93
- force_rebuild: bool = False) -> str:
94
- """
95
- Ініціалізує signatures: завантажує існуючі або створює нові
96
- """
97
- global class_signatures, classes_json
98
-
99
- if not classes_json:
100
- return "Помилка: Не знайдено жодного класу в classes.json"
101
-
102
- print(f"Знайдено {len(classes_json)} класів")
103
-
104
- # Спробуємо завантажити існуючі signatures
105
- if not force_rebuild and os.path.exists(signatures_file):
106
- try:
107
- loaded_signatures = load_signatures(signatures_file)
108
- # Перевіряємо, чи всі класи з classes_json є в signatures
109
- if loaded_signatures and all(cls in loaded_signatures for cls in classes_json):
110
- class_signatures = loaded_signatures
111
- print("Успішно завантажено збережені signatures")
112
- return f"Завантажено існуючі signatures для {len(class_signatures)} класів"
113
- except Exception as e:
114
- print(f"Помилка при завантаженні signatures: {str(e)}")
115
-
116
- # Якщо немає файлу або примусова перебудова - створюємо нові
117
- try:
118
- class_signatures = {}
119
- total_classes = len(classes_json)
120
- print(f"Починаємо створення нових signatures для {total_classes} класів...")
121
-
122
- for idx, (cls_name, hints) in enumerate(classes_json.items(), 1):
123
- if not hints:
124
- print(f"Пропускаємо клас {cls_name} - немає хінтів")
125
- continue
126
-
127
- print(f"Обробка класу {cls_name} ({idx}/{total_classes})...")
128
- try:
129
- arr = embed_hints(hints, model_name=model_name)
130
- class_signatures[cls_name] = arr.mean(axis=0)
131
- print(f"Успішно створено signature для {cls_name}")
132
- except Exception as e:
133
- print(f"Помилка при створенні signature для {cls_name}: {str(e)}")
134
- continue
135
-
136
- if not class_signatures:
137
- return "Помилка: Не вдалося створити жодного signature"
138
-
139
- # Зберігаємо нові signatures
140
- try:
141
- save_signatures(class_signatures, signatures_file)
142
- print("Signatures збережено у файл")
143
- except Exception as e:
144
- print(f"Помилка при збереженні signatures: {str(e)}")
145
-
146
- return f"Створено та збережено нові signatures для {len(class_signatures)} класів"
147
- except Exception as e:
148
- return f"Помилка при створенні signatures: {str(e)}"
149
-
150
- # Ініціалізація глобальних змінних
151
- classes_json = {}
152
- df = None
153
- embeddings = None
154
- class_signatures = None
155
- embeddings_mean = None
156
- embeddings_std = None
157
-
158
- ##############################################################################
159
- # 2. Функції для роботи з даними та класифікації
160
- ##############################################################################
161
- def load_data(csv_path: str = "messages.csv", emb_path: str = "embeddings.npy"):
162
- global df, embeddings, embeddings_mean, embeddings_std
163
- df_local = pd.read_csv(csv_path)
164
- emb_local = np.load(emb_path)
165
- assert len(df_local) == len(emb_local), "CSV і embeddings різної довжини!"
166
-
167
- df_local["Target"] = "Unlabeled"
168
-
169
- embeddings_mean = emb_local.mean(axis=0)
170
- embeddings_std = emb_local.std(axis=0)
171
- emb_local = (emb_local - embeddings_mean) / embeddings_std
172
-
173
- df = df_local
174
- embeddings = emb_local
175
-
176
- return f"Завантажено {len(df)} рядків"
177
-
178
- def get_openai_embedding(text: str, model_name: str = "text-embedding-3-large") -> list:
179
- response = client.embeddings.create(
180
- input=text,
181
- model=model_name
182
- )
183
- return response.data[0].embedding
184
-
185
- def embed_hints(hint_list: List[str], model_name: str) -> np.ndarray:
186
- emb_list = []
187
- total_hints = len(hint_list)
188
-
189
- for idx, hint in enumerate(hint_list, 1):
190
- try:
191
- print(f" Отримання embedding {idx}/{total_hints}: '{hint}'")
192
- emb = get_openai_embedding(hint, model_name=model_name)
193
- emb_list.append(emb)
194
- except Exception as e:
195
- print(f" Помилка при отриманні embedding для '{hint}': {str(e)}")
196
- continue
197
-
198
- if not emb_list:
199
- raise ValueError("Не вдалося отримати жодного embedding")
200
-
201
- return np.array(emb_list, dtype=np.float32)
202
-
203
- def predict_classes(text_embedding: np.ndarray,
204
- signatures: Dict[str, np.ndarray],
205
- threshold: float = 0.0) -> Dict[str, float]:
206
- """
207
- Повертає словник класів та їх scores для одного тексту.
208
- Scores - це значення dot product між embedding тексту та signature класу
209
- """
210
- results = {}
211
- for cls, sign in signatures.items():
212
- score = float(np.dot(text_embedding, sign))
213
- if score > threshold:
214
- results[cls] = score
215
-
216
- # Сортуємо за спаданням score
217
- results = dict(sorted(results.items(),
218
- key=lambda x: x[1],
219
- reverse=True))
220
-
221
- return results
222
-
223
- def process_single_text(text: str, threshold: float = 0.3) -> dict:
224
- if class_signatures is None:
225
- return {"error": "Спочатку збудуйте signatures!"}
226
-
227
- emb = get_openai_embedding(text)
228
-
229
- if embeddings_mean is not None and embeddings_std is not None:
230
- emb = (emb - embeddings_mean) / embeddings_std
231
-
232
- predictions = predict_classes(emb, class_signatures, threshold)
233
-
234
- if not predictions:
235
- return {"message": text, "result": "Жодного класу не знайдено"}
236
-
237
- formatted_results = []
238
- for cls, score in sorted(predictions.items(), key=lambda x: x[1], reverse=True):
239
- formatted_results.append(f"{cls}: {score:.2%}")
240
-
241
- return {
242
- "message": text,
243
- "result": "\n".join(formatted_results)
244
- }
245
-
246
- def classify_rows(filter_substring: str = "", threshold: float = 0.3):
247
- global df, embeddings, class_signatures
248
-
249
- if class_signatures is None:
250
- return "Спочатку збудуйте signatures!"
251
-
252
- if df is None or embeddings is None:
253
- return "Дані не завантажені! Спочатку викличте load_data."
254
-
255
- if filter_substring:
256
- filtered_idx = df[df["Message"].str.contains(filter_substring,
257
- case=False,
258
- na=False)].index
259
- else:
260
- filtered_idx = df.index
261
-
262
- for cls in class_signatures.keys():
263
- df[f"Score_{cls}"] = 0.0
264
-
265
- for i in filtered_idx:
266
- emb_vec = embeddings[i]
267
- predictions = predict_classes(emb_vec,
268
- class_signatures,
269
- threshold=threshold)
270
-
271
- for cls, score in predictions.items():
272
- df.at[i, f"Score_{cls}"] = score
273
-
274
- main_classes = [cls for cls, score in predictions.items()
275
- if score > threshold]
276
- df.at[i, "Target"] = "|".join(main_classes) if main_classes else "None"
277
-
278
- result_columns = ["Message", "Target"] + [f"Score_{cls}"
279
- for cls in class_signatures.keys()]
280
- result_df = df.loc[filtered_idx, result_columns].copy()
281
- return result_df.reset_index(drop=True)
282
-
283
- ##############################################################################
284
- # 3. Головний інтерфейс
285
- ##############################################################################
286
  def main():
287
- # Ініціалізуємо класи та signatures при запуску
288
- print("Завантаження класів...")
289
 
290
- # Спроба завантажити класи з файлу за замовчуванням
291
- global classes_json
292
- if os.path.exists(DEFAULT_CLASSES_FILE):
293
- classes_json = load_classes(DEFAULT_CLASSES_FILE)
294
- if not classes_json:
295
- print(f"ПОПЕРЕДЖЕННЯ: Файл {DEFAULT_CLASSES_FILE} порожній або має неправильний формат")
296
- else:
297
- print(f"ПОПЕРЕДЖЕННЯ: Файл {DEFAULT_CLASSES_FILE} не знайдено")
298
- classes_json = {}
299
 
300
- # Якщо є класи, ініціалізуємо signatures
301
- if classes_json:
302
  print("Ініціалізація signatures...")
303
  try:
304
- init_message = initialize_signatures(
305
  signatures_file=DEFAULT_SIGNATURES_FILE
306
  )
307
  print(f"Результат ініціалізації: {init_message}")
@@ -333,9 +51,7 @@ def main():
333
  single_process_btn = gr.Button("Проаналізувати")
334
 
335
  with gr.Column():
336
- result_text = gr.JSON(
337
- label="Результати аналізу"
338
- )
339
 
340
  # Налаштування моделі
341
  with gr.Accordion("Налаштування моделі", open=False):
@@ -356,29 +72,7 @@ def main():
356
  with gr.Row():
357
  build_btn = gr.Button("Оновити signatures")
358
  build_out = gr.Label(label="Статус signatures")
359
-
360
- # Оновлений обробник для перебудови signatures
361
- def update_with_file(file, model_name, force):
362
- if file is None:
363
- return "Виберіть файл з класами"
364
- try:
365
- temp_path = file.name
366
- return reload_classes_and_signatures(temp_path, model_name, force)
367
- except Exception as e:
368
- return f"Помилка при оновленні: {str(e)}"
369
-
370
- single_process_btn.click(
371
- fn=process_single_text,
372
- inputs=[text_input, threshold_slider],
373
- outputs=result_text
374
- )
375
 
376
- build_btn.click(
377
- fn=update_with_file,
378
- inputs=[json_file, model_choice, force_rebuild],
379
- outputs=build_out
380
- )
381
-
382
  # Вкладка 2: Batch Processing
383
  with gr.TabItem("Пакетна обробка"):
384
  gr.Markdown("## 1) Завантаження даних")
@@ -407,32 +101,11 @@ def main():
407
  )
408
 
409
  classify_btn = gr.Button("Класифікувати")
410
- classify_out = gr.Dataframe(
411
- label="Результат (Message / Target / Scores)"
412
- )
413
 
414
  gr.Markdown("## 3) Зберегти результати")
415
  save_btn = gr.Button("Зберегти розмічені дані")
416
  save_out = gr.Label()
417
-
418
- # Підключаємо обробники подій
419
- load_btn.click(
420
- fn=load_data,
421
- inputs=[csv_input, emb_input],
422
- outputs=load_output
423
- )
424
-
425
- classify_btn.click(
426
- fn=classify_rows,
427
- inputs=[filter_in, batch_threshold],
428
- outputs=classify_out
429
- )
430
-
431
- save_btn.click(
432
- fn=lambda: df.to_csv("messages_with_labels.csv", index=False) if df is not None else "Дані відсутні!",
433
- inputs=[],
434
- outputs=save_out
435
- )
436
 
437
  gr.Markdown("""
438
  ### Інструкція:
@@ -445,6 +118,51 @@ def main():
445
  4. На вкладці "Пакетна обробка" можна аналізувати багато повідомлень
446
  5. Результати можна зберегти в CSV файл
447
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
  demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
450
 
 
 
1
  import gradio as gr
2
+ from sdc_classifier import SDCClassifier
 
 
 
 
 
3
  from dotenv import load_dotenv
4
 
5
  # Load environment variables
6
  load_dotenv()
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  def main():
9
+ # Ініціалізуємо класифікатор
10
+ classifier = SDCClassifier()
11
 
12
+ # Спроба завантажити початкові класи та signatures
13
+ DEFAULT_CLASSES_FILE = "classes_short.json"
14
+ DEFAULT_SIGNATURES_FILE = "signatures.npz"
15
+
16
+ print("Завантаження початкових класів...")
17
+ classifier.load_classes(DEFAULT_CLASSES_FILE)
 
 
 
18
 
19
+ if classifier.classes_json:
 
20
  print("Ініціалізація signatures...")
21
  try:
22
+ init_message = classifier.initialize_signatures(
23
  signatures_file=DEFAULT_SIGNATURES_FILE
24
  )
25
  print(f"Результат ініціалізації: {init_message}")
 
51
  single_process_btn = gr.Button("Проаналізувати")
52
 
53
  with gr.Column():
54
+ result_text = gr.JSON(label="Результати аналізу")
 
 
55
 
56
  # Налаштування моделі
57
  with gr.Accordion("Налаштування моделі", open=False):
 
72
  with gr.Row():
73
  build_btn = gr.Button("Оновити signatures")
74
  build_out = gr.Label(label="Статус signatures")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
 
 
 
 
 
 
76
  # Вкладка 2: Batch Processing
77
  with gr.TabItem("Пакетна обробка"):
78
  gr.Markdown("## 1) Завантаження даних")
 
101
  )
102
 
103
  classify_btn = gr.Button("Класифікувати")
104
+ classify_out = gr.Dataframe(label="Результат (Message / Target / Scores)")
 
 
105
 
106
  gr.Markdown("## 3) Зберегти результати")
107
  save_btn = gr.Button("Зберегти розмічені дані")
108
  save_out = gr.Label()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  gr.Markdown("""
111
  ### Інструкція:
 
118
  4. На вкладці "Пакетна обробка" можна аналізувати багато повідомлень
119
  5. Результати можна зберегти в CSV файл
120
  """)
121
+
122
+ # Підключення обробників подій
123
+ def update_with_file(file, model_name, force):
124
+ if file is None:
125
+ return "Виберіть файл з класами"
126
+ try:
127
+ temp_path = file.name
128
+ classifier.load_classes(temp_path)
129
+ return classifier.initialize_signatures(
130
+ model_name=model_name,
131
+ signatures_file=DEFAULT_SIGNATURES_FILE,
132
+ force_rebuild=force
133
+ )
134
+ except Exception as e:
135
+ return f"Помилка при оновленні: {str(e)}"
136
+
137
+ single_process_btn.click(
138
+ fn=lambda text, threshold: classifier.process_single_text(text, threshold),
139
+ inputs=[text_input, threshold_slider],
140
+ outputs=result_text
141
+ )
142
+
143
+ build_btn.click(
144
+ fn=update_with_file,
145
+ inputs=[json_file, model_choice, force_rebuild],
146
+ outputs=build_out
147
+ )
148
+
149
+ load_btn.click(
150
+ fn=lambda csv, emb: classifier.load_data(csv, emb),
151
+ inputs=[csv_input, emb_input],
152
+ outputs=load_output
153
+ )
154
+
155
+ classify_btn.click(
156
+ fn=lambda filter_str, threshold: classifier.classify_rows(filter_str, threshold),
157
+ inputs=[filter_in, batch_threshold],
158
+ outputs=classify_out
159
+ )
160
+
161
+ save_btn.click(
162
+ fn=lambda: classifier.save_results("messages_with_labels.csv"),
163
+ inputs=[],
164
+ outputs=save_out
165
+ )
166
 
167
  demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
168
 
app_old.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import numpy as np
5
+ import json
6
+ from typing import Dict, List
7
+
8
+ from openai import OpenAI
9
+ from dotenv import load_dotenv
10
+
11
+ # Load environment variables
12
+ load_dotenv()
13
+
14
+ # OpenAI setup
15
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
16
+ client = OpenAI(api_key=OPENAI_API_KEY)
17
+
18
+
19
+ DEFAULT_CLASSES_FILE = "classes.json" # Файл за замовчуванням
20
+ DEFAULT_SIGNATURES_FILE = "signatures.npz" # Файл для збереження signatures
21
+
22
+
23
+ ##############################################################################
24
+ # 1. Функції для роботи з класами та signatures
25
+ ##############################################################################
26
+ def load_classes(json_path: str) -> dict:
27
+ """
28
+ Завантаження класів та їх хінтів з JSON файлу
29
+ """
30
+ try:
31
+ with open(json_path, 'r', encoding='utf-8') as f:
32
+ classes = json.load(f)
33
+ return classes
34
+ except FileNotFoundError:
35
+ print(f"Файл {json_path} не знайдено! Використовуємо пустий словник класів.")
36
+ return {}
37
+ except json.JSONDecodeError:
38
+ print(f"Помилка читання JSON з файлу {json_path}! Використовуємо пустий словник класів.")
39
+ return {}
40
+
41
+ def save_signatures(signatures: Dict[str, np.ndarray], filename: str = "signatures.npz") -> None:
42
+ """
43
+ Зберігає signatures у NPZ файл
44
+ """
45
+ if signatures:
46
+ np.savez(filename, **signatures)
47
+
48
+ def load_signatures(filename: str = "signatures.npz") -> Dict[str, np.ndarray]:
49
+ """
50
+ Завантажує signatures з NPZ файлу
51
+ """
52
+ try:
53
+ with np.load(filename) as data:
54
+ return {key: data[key] for key in data.files}
55
+ except (FileNotFoundError, IOError):
56
+ return None
57
+
58
+ def reload_classes_and_signatures(json_path: str, model_name: str, force_rebuild: bool) -> str:
59
+ """
60
+ Перезавантажує класи з нового JSON файлу та оновлює signatures
61
+ """
62
+ global classes_json, class_signatures
63
+
64
+ try:
65
+ new_classes = load_classes(json_path)
66
+ if not new_classes:
67
+ return "Помилка: Файл класів порожній або має неправильний формат"
68
+
69
+ classes_json = new_classes
70
+ print(f"Завантажено {len(classes_json)} класів з {json_path}")
71
+
72
+ # Зберігаємо новий файл класів як файл за замовчуванням
73
+ try:
74
+ with open(DEFAULT_CLASSES_FILE, 'w', encoding='utf-8') as f:
75
+ json.dump(classes_json, f, ensure_ascii=False, indent=2)
76
+ print(f"Збережено як {DEFAULT_CLASSES_FILE}")
77
+ except Exception as e:
78
+ print(f"Помилка при збереженні файлу класів: {str(e)}")
79
+
80
+ result = initialize_signatures(
81
+ model_name=model_name,
82
+ signatures_file=DEFAULT_SIGNATURES_FILE,
83
+ force_rebuild=force_rebuild
84
+ )
85
+ return f"Класи оновлено. {result}"
86
+
87
+ except Exception as e:
88
+ return f"Помилка при оновленні класів: {str(e)}"
89
+
90
+
91
+ def initialize_signatures(model_name: str = "text-embedding-3-large",
92
+ signatures_file: str = "signatures.npz",
93
+ force_rebuild: bool = False) -> str:
94
+ """
95
+ Ініціалізує signatures: завантажує існуючі або створює нові
96
+ """
97
+ global class_signatures, classes_json
98
+
99
+ if not classes_json:
100
+ return "Помилка: Не знайдено жодного класу в classes.json"
101
+
102
+ print(f"Знайдено {len(classes_json)} класів")
103
+
104
+ # Спробуємо завантажити існуючі signatures
105
+ if not force_rebuild and os.path.exists(signatures_file):
106
+ try:
107
+ loaded_signatures = load_signatures(signatures_file)
108
+ # Перевіряємо, чи всі класи з classes_json є в signatures
109
+ if loaded_signatures and all(cls in loaded_signatures for cls in classes_json):
110
+ class_signatures = loaded_signatures
111
+ print("Успішно завантажено збережені signatures")
112
+ return f"Завантажено існуючі signatures для {len(class_signatures)} класів"
113
+ except Exception as e:
114
+ print(f"Помилка при завантаженні signatures: {str(e)}")
115
+
116
+ # Якщо немає файлу або примусова перебудова - створюємо нові
117
+ try:
118
+ class_signatures = {}
119
+ total_classes = len(classes_json)
120
+ print(f"Починаємо створення нових signatures для {total_classes} класів...")
121
+
122
+ for idx, (cls_name, hints) in enumerate(classes_json.items(), 1):
123
+ if not hints:
124
+ print(f"Пропускаємо клас {cls_name} - немає хінтів")
125
+ continue
126
+
127
+ print(f"Обробка класу {cls_name} ({idx}/{total_classes})...")
128
+ try:
129
+ arr = embed_hints(hints, model_name=model_name)
130
+ class_signatures[cls_name] = arr.mean(axis=0)
131
+ print(f"Успішно створено signature для {cls_name}")
132
+ except Exception as e:
133
+ print(f"Помилка при створенні signature для {cls_name}: {str(e)}")
134
+ continue
135
+
136
+ if not class_signatures:
137
+ return "Помилка: Не вдалося створити жодного signature"
138
+
139
+ # Зберігаємо нові signatures
140
+ try:
141
+ save_signatures(class_signatures, signatures_file)
142
+ print("Signatures збережено у файл")
143
+ except Exception as e:
144
+ print(f"Помилка при збереженні signatures: {str(e)}")
145
+
146
+ return f"Створено та збережено нові signatures для {len(class_signatures)} класів"
147
+ except Exception as e:
148
+ return f"Помилка при створенні signatures: {str(e)}"
149
+
150
+ # Ініціалізація глобальних змінних
151
+ classes_json = {}
152
+ df = None
153
+ embeddings = None
154
+ class_signatures = None
155
+ embeddings_mean = None
156
+ embeddings_std = None
157
+
158
+ ##############################################################################
159
+ # 2. Функції для роботи з даними та класифікації
160
+ ##############################################################################
161
+ def load_data(csv_path: str = "messages.csv", emb_path: str = "embeddings.npy"):
162
+ global df, embeddings, embeddings_mean, embeddings_std
163
+ df_local = pd.read_csv(csv_path)
164
+ emb_local = np.load(emb_path)
165
+ assert len(df_local) == len(emb_local), "CSV і embeddings різної довжини!"
166
+
167
+ df_local["Target"] = "Unlabeled"
168
+
169
+ embeddings_mean = emb_local.mean(axis=0)
170
+ embeddings_std = emb_local.std(axis=0)
171
+ emb_local = (emb_local - embeddings_mean) / embeddings_std
172
+
173
+ df = df_local
174
+ embeddings = emb_local
175
+
176
+ return f"Завантажено {len(df)} рядків"
177
+
178
+ def get_openai_embedding(text: str, model_name: str = "text-embedding-3-large") -> list:
179
+ response = client.embeddings.create(
180
+ input=text,
181
+ model=model_name
182
+ )
183
+ return response.data[0].embedding
184
+
185
+ def embed_hints(hint_list: List[str], model_name: str) -> np.ndarray:
186
+ emb_list = []
187
+ total_hints = len(hint_list)
188
+
189
+ for idx, hint in enumerate(hint_list, 1):
190
+ try:
191
+ print(f" Отримання embedding {idx}/{total_hints}: '{hint}'")
192
+ emb = get_openai_embedding(hint, model_name=model_name)
193
+ emb_list.append(emb)
194
+ except Exception as e:
195
+ print(f" Помилка при отриманні embedding для '{hint}': {str(e)}")
196
+ continue
197
+
198
+ if not emb_list:
199
+ raise ValueError("Не вдалося отримати жодного embedding")
200
+
201
+ return np.array(emb_list, dtype=np.float32)
202
+
203
+ def predict_classes(text_embedding: np.ndarray,
204
+ signatures: Dict[str, np.ndarray],
205
+ threshold: float = 0.0) -> Dict[str, float]:
206
+ """
207
+ Повертає словник класів та їх scores для одного тексту.
208
+ Scores - це значення dot product між embedding тексту та signature класу
209
+ """
210
+ results = {}
211
+ for cls, sign in signatures.items():
212
+ score = float(np.dot(text_embedding, sign))
213
+ if score > threshold:
214
+ results[cls] = score
215
+
216
+ # Сортуємо за спаданням score
217
+ results = dict(sorted(results.items(),
218
+ key=lambda x: x[1],
219
+ reverse=True))
220
+
221
+ return results
222
+
223
+ def process_single_text(text: str, threshold: float = 0.3) -> dict:
224
+ if class_signatures is None:
225
+ return {"error": "Спочатку збудуйте signatures!"}
226
+
227
+ emb = get_openai_embedding(text)
228
+
229
+ if embeddings_mean is not None and embeddings_std is not None:
230
+ emb = (emb - embeddings_mean) / embeddings_std
231
+
232
+ predictions = predict_classes(emb, class_signatures, threshold)
233
+
234
+ if not predictions:
235
+ return {"message": text, "result": "Жодного класу не знайдено"}
236
+
237
+ formatted_results = []
238
+ for cls, score in sorted(predictions.items(), key=lambda x: x[1], reverse=True):
239
+ formatted_results.append(f"{cls}: {score:.2%}")
240
+
241
+ return {
242
+ "message": text,
243
+ "result": "\n".join(formatted_results)
244
+ }
245
+
246
+ def classify_rows(filter_substring: str = "", threshold: float = 0.3):
247
+ global df, embeddings, class_signatures
248
+
249
+ if class_signatures is None:
250
+ return "Спочатку збудуйте signatures!"
251
+
252
+ if df is None or embeddings is None:
253
+ return "Дані не завантажені! Спочатку викличте load_data."
254
+
255
+ if filter_substring:
256
+ filtered_idx = df[df["Message"].str.contains(filter_substring,
257
+ case=False,
258
+ na=False)].index
259
+ else:
260
+ filtered_idx = df.index
261
+
262
+ for cls in class_signatures.keys():
263
+ df[f"Score_{cls}"] = 0.0
264
+
265
+ for i in filtered_idx:
266
+ emb_vec = embeddings[i]
267
+ predictions = predict_classes(emb_vec,
268
+ class_signatures,
269
+ threshold=threshold)
270
+
271
+ for cls, score in predictions.items():
272
+ df.at[i, f"Score_{cls}"] = score
273
+
274
+ main_classes = [cls for cls, score in predictions.items()
275
+ if score > threshold]
276
+ df.at[i, "Target"] = "|".join(main_classes) if main_classes else "None"
277
+
278
+ result_columns = ["Message", "Target"] + [f"Score_{cls}"
279
+ for cls in class_signatures.keys()]
280
+ result_df = df.loc[filtered_idx, result_columns].copy()
281
+ return result_df.reset_index(drop=True)
282
+
283
+ ##############################################################################
284
+ # 3. Головний інтерфейс
285
+ ##############################################################################
286
+ def main():
287
+ # Ініціалізуємо класи та signatures при запуску
288
+ print("Завантаження класів...")
289
+
290
+ # Спроба завантажити класи з файлу за замовчуванням
291
+ global classes_json
292
+ if os.path.exists(DEFAULT_CLASSES_FILE):
293
+ classes_json = load_classes(DEFAULT_CLASSES_FILE)
294
+ if not classes_json:
295
+ print(f"ПОПЕРЕДЖЕННЯ: Файл {DEFAULT_CLASSES_FILE} порожній або має неправильний формат")
296
+ else:
297
+ print(f"ПОПЕРЕДЖЕННЯ: Файл {DEFAULT_CLASSES_FILE} не знайдено")
298
+ classes_json = {}
299
+
300
+ # Якщо є класи, ініціалізуємо signatures
301
+ if classes_json:
302
+ print("Ініціалізація signatures...")
303
+ try:
304
+ init_message = initialize_signatures(
305
+ signatures_file=DEFAULT_SIGNATURES_FILE
306
+ )
307
+ print(f"Результат ініціалізації: {init_message}")
308
+ except Exception as e:
309
+ print(f"ПОПЕРЕДЖЕННЯ: Помилка при ініціалізації signatures: {str(e)}")
310
+ else:
311
+ print("Очікую завантаження класів через інтерфейс...")
312
+
313
+ with gr.Blocks() as demo:
314
+ gr.Markdown("# SDC Classifier з Gradio")
315
+
316
+ with gr.Tabs():
317
+ # Вкладка 1: Single Text Testing
318
+ with gr.TabItem("Тестування одного тексту"):
319
+ with gr.Row():
320
+ with gr.Column():
321
+ text_input = gr.Textbox(
322
+ label="Введіть текст для аналізу",
323
+ lines=5,
324
+ placeholder="Введіть текст..."
325
+ )
326
+ threshold_slider = gr.Slider(
327
+ minimum=0.0,
328
+ maximum=1.0,
329
+ value=0.3,
330
+ step=0.05,
331
+ label="Поріг впевненості"
332
+ )
333
+ single_process_btn = gr.Button("Проаналізувати")
334
+
335
+ with gr.Column():
336
+ result_text = gr.JSON(
337
+ label="Результати аналізу"
338
+ )
339
+
340
+ # Налаштування моделі
341
+ with gr.Accordion("Налаштування моделі", open=False):
342
+ with gr.Row():
343
+ model_choice = gr.Dropdown(
344
+ choices=["text-embedding-3-large","text-embedding-3-small"],
345
+ value="text-embedding-3-large",
346
+ label="OpenAI model"
347
+ )
348
+ json_file = gr.File(
349
+ label="Завантажити новий JSON з класами",
350
+ file_types=[".json"]
351
+ )
352
+ force_rebuild = gr.Checkbox(
353
+ label="Примусово перебудувати signatures",
354
+ value=False
355
+ )
356
+ with gr.Row():
357
+ build_btn = gr.Button("Оновити signatures")
358
+ build_out = gr.Label(label="Статус signatures")
359
+
360
+ # Оновлений обробник для перебудови signatures
361
+ def update_with_file(file, model_name, force):
362
+ if file is None:
363
+ return "Виберіть файл з класами"
364
+ try:
365
+ temp_path = file.name
366
+ return reload_classes_and_signatures(temp_path, model_name, force)
367
+ except Exception as e:
368
+ return f"Помилка при оновленні: {str(e)}"
369
+
370
+ single_process_btn.click(
371
+ fn=process_single_text,
372
+ inputs=[text_input, threshold_slider],
373
+ outputs=result_text
374
+ )
375
+
376
+ build_btn.click(
377
+ fn=update_with_file,
378
+ inputs=[json_file, model_choice, force_rebuild],
379
+ outputs=build_out
380
+ )
381
+
382
+ # Вкладка 2: Batch Processing
383
+ with gr.TabItem("Пакетна обробка"):
384
+ gr.Markdown("## 1) Завантаження даних")
385
+ with gr.Row():
386
+ csv_input = gr.Textbox(
387
+ value="messages.csv",
388
+ label="CSV-файл"
389
+ )
390
+ emb_input = gr.Textbox(
391
+ value="embeddings.npy",
392
+ label="Numpy Embeddings"
393
+ )
394
+ load_btn = gr.Button("Завантажити дані")
395
+
396
+ load_output = gr.Label(label="Результат завантаження")
397
+
398
+ gr.Markdown("## 2) Класифікація")
399
+ with gr.Row():
400
+ filter_in = gr.Textbox(label="Фільтр (опціонально)")
401
+ batch_threshold = gr.Slider(
402
+ minimum=0.0,
403
+ maximum=1.0,
404
+ value=0.3,
405
+ step=0.05,
406
+ label="Поріг впевненості"
407
+ )
408
+
409
+ classify_btn = gr.Button("Класифікувати")
410
+ classify_out = gr.Dataframe(
411
+ label="Результат (Message / Target / Scores)"
412
+ )
413
+
414
+ gr.Markdown("## 3) Зберегти результати")
415
+ save_btn = gr.Button("Зберегти розмічені дані")
416
+ save_out = gr.Label()
417
+
418
+ # Підключаємо обробники подій
419
+ load_btn.click(
420
+ fn=load_data,
421
+ inputs=[csv_input, emb_input],
422
+ outputs=load_output
423
+ )
424
+
425
+ classify_btn.click(
426
+ fn=classify_rows,
427
+ inputs=[filter_in, batch_threshold],
428
+ outputs=classify_out
429
+ )
430
+
431
+ save_btn.click(
432
+ fn=lambda: df.to_csv("messages_with_labels.csv", index=False) if df is not None else "Дані відсутні!",
433
+ inputs=[],
434
+ outputs=save_out
435
+ )
436
+
437
+ gr.Markdown("""
438
+ ### Інструкція:
439
+ 1. У вкладці "Налаштування моделі" можна:
440
+ - Завантажити новий JSON файл з класами
441
+ - Вибрати модель для embeddings
442
+ - Примусово перебудувати signatures
443
+ 2. Після зміни класів натисніть "Оновити signatures"
444
+ 3. Використовуйте повзунок "Поріг впевненості" для фільтрації результатів
445
+ 4. На вкладці "Пакетна обробка" можна аналізувати багато повідомлень
446
+ 5. Результати можна зберегти в CSV файл
447
+ """)
448
+
449
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
450
+
451
+ if __name__ == "__main__":
452
+ main()
sdc_classifier.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import json
5
+ from typing import Dict, List
6
+ from openai import OpenAI
7
+ from pathlib import Path
8
+
9
+ class SDCClassifier:
10
+ def __init__(self, openai_api_key: str = None):
11
+ """
12
+ Ініціалізація класифікатора SDC
13
+
14
+ Args:
15
+ openai_api_key: API ключ для OpenAI (опціонально, можна взяти з env)
16
+ """
17
+ self.client = OpenAI(api_key=openai_api_key or os.getenv("OPENAI_API_KEY"))
18
+ self.classes_json = {}
19
+ self.class_signatures = None
20
+ self.df = None
21
+ self.embeddings = None
22
+ self.embeddings_mean = None
23
+ self.embeddings_std = None
24
+
25
+ def load_classes(self, json_path: str) -> dict:
26
+ """Завантаження класів та їх хінтів з JSON файлу"""
27
+ try:
28
+ with open(json_path, 'r', encoding='utf-8') as f:
29
+ self.classes_json = json.load(f)
30
+ return self.classes_json
31
+ except FileNotFoundError:
32
+ print(f"Файл {json_path} не знайдено!")
33
+ return {}
34
+ except json.JSONDecodeError:
35
+ print(f"Помилка читання JSON з файлу {json_path}!")
36
+ return {}
37
+
38
+ def save_signatures(self, filename: str = "signatures.npz") -> None:
39
+ """Зберігає signatures у NPZ файл"""
40
+ if self.class_signatures:
41
+ np.savez(filename, **self.class_signatures)
42
+
43
+ def load_signatures(self, filename: str = "signatures.npz") -> Dict[str, np.ndarray]:
44
+ """Завантажує signatures з NPZ файлу"""
45
+ try:
46
+ with np.load(filename) as data:
47
+ self.class_signatures = {key: data[key] for key in data.files}
48
+ return self.class_signatures
49
+ except (FileNotFoundError, IOError):
50
+ return None
51
+
52
+ def get_openai_embedding(self, text: str, model_name: str = "text-embedding-3-large") -> list:
53
+ """Отримання ембедінгу тексту через OpenAI API"""
54
+ response = self.client.embeddings.create(
55
+ input=text,
56
+ model=model_name
57
+ )
58
+ return response.data[0].embedding
59
+
60
+ def embed_hints(self, hint_list: List[str], model_name: str) -> np.ndarray:
61
+ """Створення ембедінгів для списку хінтів"""
62
+ emb_list = []
63
+ total_hints = len(hint_list)
64
+
65
+ for idx, hint in enumerate(hint_list, 1):
66
+ try:
67
+ print(f" Отримання embedding {idx}/{total_hints}: '{hint}'")
68
+ emb = self.get_openai_embedding(hint, model_name=model_name)
69
+ emb_list.append(emb)
70
+ except Exception as e:
71
+ print(f" Помилка при отриманні embedding для '{hint}': {str(e)}")
72
+ continue
73
+
74
+ if not emb_list:
75
+ raise ValueError("Не вдалося отримати жодного embedding")
76
+
77
+ return np.array(emb_list, dtype=np.float32)
78
+
79
+ def initialize_signatures(self, model_name: str = "text-embedding-3-large",
80
+ signatures_file: str = "signatures.npz",
81
+ force_rebuild: bool = False) -> str:
82
+ """Ініціалізує signatures: завантажує існуючі або створює нові"""
83
+ if not self.classes_json:
84
+ return "Помилка: Не знайдено жодного класу в classes.json"
85
+
86
+ print(f"Знайдено {len(self.classes_json)} класів")
87
+
88
+ if not force_rebuild and os.path.exists(signatures_file):
89
+ try:
90
+ loaded_signatures = self.load_signatures(signatures_file)
91
+ if loaded_signatures and all(cls in loaded_signatures for cls in self.classes_json):
92
+ print("Успішно завантажено збережені signatures")
93
+ return f"Завантажено існуючі signatures для {len(self.class_signatures)} класів"
94
+ except Exception as e:
95
+ print(f"Помилка при завантаженні signatures: {str(e)}")
96
+
97
+ try:
98
+ self.class_signatures = {}
99
+ total_classes = len(self.classes_json)
100
+ print(f"Починаємо створення нових signatures для {total_classes} класів...")
101
+
102
+ for idx, (cls_name, hints) in enumerate(self.classes_json.items(), 1):
103
+ if not hints:
104
+ print(f"Пропускаємо клас {cls_name} - немає хінтів")
105
+ continue
106
+
107
+ print(f"Обробка класу {cls_name} ({idx}/{total_classes})...")
108
+ try:
109
+ arr = self.embed_hints(hints, model_name=model_name)
110
+ self.class_signatures[cls_name] = arr.mean(axis=0)
111
+ print(f"Успішно створено signature для {cls_name}")
112
+ except Exception as e:
113
+ print(f"Помилка при створенні signature для {cls_name}: {str(e)}")
114
+ continue
115
+
116
+ if not self.class_signatures:
117
+ return "Помилка: Не вдалося створити жодного signature"
118
+
119
+ self.save_signatures(signatures_file)
120
+ print("Signatures збережено у файл")
121
+
122
+ return f"Створено та збережено нові signatures для {len(self.class_signatures)} класів"
123
+ except Exception as e:
124
+ return f"Помилка при створенні signatures: {str(e)}"
125
+
126
+ def load_data(self, csv_path: str = "messages.csv", emb_path: str = "embeddings.npy"):
127
+ """Завантаження даних з CSV та NPY файлів"""
128
+ self.df = pd.read_csv(csv_path)
129
+ emb_local = np.load(emb_path)
130
+ assert len(self.df) == len(emb_local), "CSV і embeddings різної довжини!"
131
+
132
+ self.df["Target"] = "Unlabeled"
133
+
134
+ self.embeddings_mean = emb_local.mean(axis=0)
135
+ self.embeddings_std = emb_local.std(axis=0)
136
+ self.embeddings = (emb_local - self.embeddings_mean) / self.embeddings_std
137
+
138
+ return f"Завантажено {len(self.df)} рядків"
139
+
140
+ def predict_classes(self, text_embedding: np.ndarray, threshold: float = 0.0) -> Dict[str, float]:
141
+ """Передбачення класів для одного тексту"""
142
+ results = {}
143
+ for cls, sign in self.class_signatures.items():
144
+ score = float(np.dot(text_embedding, sign))
145
+ if score > threshold:
146
+ results[cls] = score
147
+
148
+ return dict(sorted(results.items(), key=lambda x: x[1], reverse=True))
149
+
150
+ def process_single_text(self, text: str, threshold: float = 0.3) -> dict:
151
+ """Обробка одного тексту"""
152
+ if self.class_signatures is None:
153
+ return {"error": "Спочатку збудуйте signatures!"}
154
+
155
+ emb = self.get_openai_embedding(text)
156
+
157
+ if self.embeddings_mean is not None and self.embeddings_std is not None:
158
+ emb = (emb - self.embeddings_mean) / self.embeddings_std
159
+
160
+ predictions = self.predict_classes(emb, threshold)
161
+
162
+ if not predictions:
163
+ return {"message": text, "result": "Жодного класу не знайдено"}
164
+
165
+ formatted_results = []
166
+ for cls, score in predictions.items():
167
+ formatted_results.append(f"{cls}: {score:.2%}")
168
+
169
+ return {
170
+ "message": text,
171
+ "result": "\n".join(formatted_results)
172
+ }
173
+
174
+ def classify_rows(self, filter_substring: str = "", threshold: float = 0.3):
175
+ """Класифікація всіх або відфільтрованих рядків"""
176
+ if self.class_signatures is None:
177
+ return "Спочатку збудуйте signatures!"
178
+
179
+ if self.df is None or self.embeddings is None:
180
+ return "Дані не завантажені! Спочатку викличте load_data."
181
+
182
+ if filter_substring:
183
+ filtered_idx = self.df[self.df["Message"].str.contains(filter_substring,
184
+ case=False,
185
+ na=False)].index
186
+ else:
187
+ filtered_idx = self.df.index
188
+
189
+ for cls in self.class_signatures.keys():
190
+ self.df[f"Score_{cls}"] = 0.0
191
+
192
+ for i in filtered_idx:
193
+ emb_vec = self.embeddings[i]
194
+ predictions = self.predict_classes(emb_vec, threshold=threshold)
195
+
196
+ for cls, score in predictions.items():
197
+ self.df.at[i, f"Score_{cls}"] = score
198
+
199
+ main_classes = [cls for cls, score in predictions.items()
200
+ if score > threshold]
201
+ self.df.at[i, "Target"] = "|".join(main_classes) if main_classes else "None"
202
+
203
+ result_columns = ["Message", "Target"] + [f"Score_{cls}"
204
+ for cls in self.class_signatures.keys()]
205
+ result_df = self.df.loc[filtered_idx, result_columns].copy()
206
+ return result_df.reset_index(drop=True)
207
+
208
+ def save_results(self, output_path: str = "messages_with_labels.csv") -> str:
209
+ """Зберігання результатів класифікації"""
210
+ if self.df is None:
211
+ return "Дані відсутні!"
212
+
213
+ self.df.to_csv(output_path, index=False)
214
+ return f"Дані збережено у файл {output_path}"