import os import gradio as gr import pandas as pd import numpy as np import json from typing import Dict, List from openai import OpenAI from dotenv import load_dotenv # Load environment variables load_dotenv() # OpenAI setup OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") client = OpenAI(api_key=OPENAI_API_KEY) ############################################################################## # 1. Функції для роботи з класами та signatures ############################################################################## classes_json_load = "classes_short.json" def load_classes(json_path: str = classes_json_load) -> dict: """ Завантаження класів та їх хінтів з JSON файлу """ try: with open(json_path, 'r', encoding='utf-8') as f: classes = json.load(f) return classes except FileNotFoundError: print(f"Файл {json_path} не знайдено! Використовуємо пустий словник класів.") return {} except json.JSONDecodeError: print(f"Помилка читання JSON з файлу {json_path}! Використовуємо пустий словник класів.") return {} def save_signatures(signatures: Dict[str, np.ndarray], filename: str = "signatures.npz") -> None: """ Зберігає signatures у NPZ файл """ if signatures: np.savez(filename, **signatures) def load_signatures(filename: str = "signatures.npz") -> Dict[str, np.ndarray]: """ Завантажує signatures з NPZ файлу """ try: with np.load(filename) as data: return {key: data[key] for key in data.files} except (FileNotFoundError, IOError): return None def initialize_signatures(model_name: str = "text-embedding-3-small", signatures_file: str = "signatures.npz", force_rebuild: bool = False) -> str: """ Ініціалізує signatures: завантажує існуючі або створює нові """ global class_signatures, classes_json if not classes_json: return "Помилка: Не знайдено жодного класу в classes.json" print(f"Знайдено {len(classes_json)} класів") # Спробуємо завантажити існуючі signatures if not force_rebuild and os.path.exists(signatures_file): try: loaded_signatures = load_signatures(signatures_file) # Перевіряємо, чи всі класи з classes_json є в signatures if loaded_signatures and all(cls in loaded_signatures for cls in classes_json): class_signatures = loaded_signatures print("Успішно завантажено збережені signatures") return f"Завантажено існуючі signatures для {len(class_signatures)} класів" except Exception as e: print(f"Помилка при завантаженні signatures: {str(e)}") # Якщо немає файлу або примусова перебудова - створюємо нові try: class_signatures = {} total_classes = len(classes_json) print(f"Починаємо створення нових signatures для {total_classes} класів...") for idx, (cls_name, hints) in enumerate(classes_json.items(), 1): if not hints: print(f"Пропускаємо клас {cls_name} - немає хінтів") continue print(f"Обробка класу {cls_name} ({idx}/{total_classes})...") try: arr = embed_hints(hints, model_name=model_name) class_signatures[cls_name] = arr.mean(axis=0) print(f"Успішно створено signature для {cls_name}") except Exception as e: print(f"Помилка при створенні signature для {cls_name}: {str(e)}") continue if not class_signatures: return "Помилка: Не вдалося створити жодного signature" # Зберігаємо нові signatures try: save_signatures(class_signatures, signatures_file) print("Signatures збережено у файл") except Exception as e: print(f"Помилка при збереженні signatures: {str(e)}") return f"Створено та збережено нові signatures для {len(class_signatures)} класів" except Exception as e: return f"Помилка при створенні signatures: {str(e)}" # Замість хардкоду classes_json тепер використовуємо: classes_json = load_classes() ############################################################################## # 2. Глобальні змінні ############################################################################## df = None embeddings = None class_signatures = None embeddings_mean = None # Для нормалізації single text embeddings_std = None # Для нормалізації single text ############################################################################## # 3. Функції для роботи з даними та класифікації ############################################################################## def load_data(csv_path: str = "messages.csv", emb_path: str = "embeddings.npy"): global df, embeddings, embeddings_mean, embeddings_std df_local = pd.read_csv(csv_path) emb_local = np.load(emb_path) assert len(df_local) == len(emb_local), "CSV і embeddings різної довжини!" df_local["Target"] = "Unlabeled" # Зберігаємо параметри нормалізації embeddings_mean = emb_local.mean(axis=0) embeddings_std = emb_local.std(axis=0) # Нормалізація embeddings emb_local = (emb_local - embeddings_mean) / embeddings_std df = df_local embeddings = emb_local return f"Завантажено {len(df)} рядків" def get_openai_embedding(text: str, model_name: str = "text-embedding-3-small") -> list: response = client.embeddings.create( input=text, model=model_name ) return response.data[0].embedding def embed_hints(hint_list: List[str], model_name: str) -> np.ndarray: """ Отримує embeddings для списку хінтів з виводом прогресу """ emb_list = [] total_hints = len(hint_list) for idx, hint in enumerate(hint_list, 1): try: print(f" Отримання embedding {idx}/{total_hints}: '{hint}'") emb = get_openai_embedding(hint, model_name=model_name) emb_list.append(emb) except Exception as e: print(f" Помилка при отриманні embedding для '{hint}': {str(e)}") continue if not emb_list: raise ValueError("Не вдалося отримати жодного embedding") return np.array(emb_list, dtype=np.float32) def build_class_signatures(model_name: str): global class_signatures signatures = {} for cls_name, hints in classes_json.items(): if not hints: continue arr = embed_hints(hints, model_name=model_name) signatures[cls_name] = arr.mean(axis=0) class_signatures = signatures return "Signatures побудовано!" def predict_classes(text_embedding: np.ndarray, signatures: Dict[str, np.ndarray], threshold: float = 0.0) -> Dict[str, float]: """ Повертає словник класів та їх scores для одного тексту. Scores - це значення dot product між embedding тексту та signature класу """ results = {} for cls, sign in signatures.items(): score = float(np.dot(text_embedding, sign)) if score > threshold: results[cls] = score # Сортуємо за спаданням score results = dict(sorted(results.items(), key=lambda x: x[1], reverse=True)) return results def process_single_text(text: str, threshold: float = 0.3) -> dict: """ Обробка одного тексту """ if class_signatures is None: return {"error": "Спочатку збудуйте signatures!"} # Отримуємо embedding для тексту emb = get_openai_embedding(text) # Нормалізуємо embedding використовуючи збережені параметри if embeddings_mean is not None and embeddings_std is not None: emb = (emb - embeddings_mean) / embeddings_std # Отримуємо передбачення predictions = predict_classes(emb, class_signatures, threshold) # Форматуємо результат if not predictions: return {"message": text, "result": "Жодного класу не знайдено"} formatted_results = [] for cls, score in sorted(predictions.items(), key=lambda x: x[1], reverse=True): formatted_results.append(f"{cls}: {score:.2%}") return { "message": text, "result": "\n".join(formatted_results) } def classify_rows(filter_substring: str = "", threshold: float = 0.3): """ Класифікація з множинними мітками """ global df, embeddings, class_signatures if class_signatures is None: return "Спочатку збудуйте signatures!" if df is None or embeddings is None: return "Дані не завантажені! Спочатку викличте load_data." if filter_substring: filtered_idx = df[df["Message"].str.contains(filter_substring, case=False, na=False)].index else: filtered_idx = df.index # Додаємо колонки для кожного класу зі scores for cls in class_signatures.keys(): df[f"Score_{cls}"] = 0.0 for i in filtered_idx: emb_vec = embeddings[i] predictions = predict_classes(emb_vec, class_signatures, threshold=threshold) # Записуємо scores для кожного класу for cls, score in predictions.items(): df.at[i, f"Score_{cls}"] = score # Визначаємо основні класи (можна встановити поріг) main_classes = [cls for cls, score in predictions.items() if score > threshold] df.at[i, "Target"] = "|".join(main_classes) if main_classes else "None" result_columns = ["Message", "Target"] + [f"Score_{cls}" for cls in class_signatures.keys()] result_df = df.loc[filtered_idx, result_columns].copy() return result_df.reset_index(drop=True) ############################################################################## # 4. Функції для UI ############################################################################## def ui_load_data(csv_path, emb_path): msg = load_data(csv_path, emb_path) return f"{msg}" def ui_build_signatures(model_name): msg = build_class_signatures(model_name) return msg def ui_save_data(): global df if df is None: return "Дані відсутні!" df.to_csv("messages_with_labels.csv", index=False) return "Файл 'messages_with_labels.csv' збережено!" ############################################################################## # 5. Головний інтерфейс ############################################################################## def main(): # Ініціалізуємо класи та signatures при запуску print("Завантаження класів...") if not classes_json: print("КРИТИЧНА ПОМИЛКА: Не вдалося завантажити класи!") return print("Ініціалізація signatures...") try: init_message = initialize_signatures() print(f"Результат ініціалізації: {init_message}") if "Помилка" in init_message: print("ПОПЕРЕДЖЕННЯ: Проблеми з ініціалізацією signatures") except Exception as e: print(f"КРИТИЧНА ПОМИЛКА при ініціалізації signatures: {str(e)}") return with gr.Blocks() as demo: gr.Markdown("# SDC Classifier з Gradio") with gr.Tabs(): # Вкладка 1: Single Text Testing with gr.TabItem("Тестування одного тексту"): with gr.Row(): with gr.Column(): text_input = gr.Textbox( label="Введіть текст для аналізу", lines=5, placeholder="Введіть текст..." ) threshold_slider = gr.Slider( minimum=0.0, maximum=1.0, value=0.3, step=0.05, label="Поріг впевненості" ) single_process_btn = gr.Button("Проаналізувати") with gr.Column(): result_text = gr.JSON( label="Результати аналізу" ) # Модифікована панель налаштувань with gr.Accordion("Налаштування моделі", open=False): with gr.Row(): model_choice = gr.Dropdown( choices=["text-embedding-3-large","text-embedding-3-small"], value="text-embedding-3-small", label="OpenAI model" ) force_rebuild = gr.Checkbox( label="Примусово перебудувати signatures", value=False ) build_btn = gr.Button("Оновити signatures") build_out = gr.Label(label="Статус signatures") # Оновлений обробник для перебудови signatures def rebuild_signatures(model_name, force): return initialize_signatures(model_name, force_rebuild=force) single_process_btn.click( fn=process_single_text, inputs=[text_input, threshold_slider], outputs=result_text ) build_btn.click( fn=rebuild_signatures, inputs=[model_choice, force_rebuild], outputs=build_out ) # Вкладка 2: Batch Processing [залишається без змін] demo.launch(server_name="0.0.0.0", server_port=7860, share=True) if __name__ == "__main__": main()