Spaces:
Runtime error
Runtime error
| """ | |
| Flask-приложение для определения жанра книги по аннотации. | |
| Приложение разработано как демонстрационная часть выпускной квалификационной | |
| работы по теме многоклассовой классификации текстов аннотаций книг по жанрам. | |
| Пользователь вводит аннотацию на английском языке, после чего приложение строит | |
| признаки текста и возвращает один из пяти жанров. | |
| """ | |
| from __future__ import annotations | |
| # Библиотека transformers может попытаться загрузить TensorFlow, если он установлен в системе. | |
| # Для приложения используется PyTorch, поэтому TensorFlow отключается до импорта модулей модели. | |
| # Эти переменные должны быть заданы в самом начале файла, иначе настройка может не сработать. | |
| import os | |
| os.environ.setdefault("USE_TF", "0") | |
| os.environ.setdefault("USE_TORCH", "1") | |
| os.environ.setdefault("TRANSFORMERS_NO_TF", "1") | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from typing import Dict, List | |
| import pandas as pd | |
| from flask import Flask, jsonify, render_template, request | |
| from services.prediction_service import BookGenrePredictionService, ModelFilesError | |
| # Создаем экземпляр приложения Flask. | |
| # Папки templates и static будут найдены автоматически, так как имеют стандартные названия. | |
| app = Flask(__name__) | |
| # Ограничиваем размер пользовательского запроса. | |
| # Для текстовой аннотации 64 килобайт достаточно, а лишняя защита снижает риск случайной перегрузки. | |
| app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 | |
| def get_prediction_service() -> BookGenrePredictionService: | |
| """ | |
| Загружает сервис классификации один раз и переиспользует его между запросами. | |
| Загрузка модели смысловых векторов занимает время и память, поэтому сервис | |
| кэшируется. При первом обращении создается объект BookGenrePredictionService, | |
| при последующих обращениях возвращается уже загруженный экземпляр. | |
| """ | |
| return BookGenrePredictionService(model_dir="model") | |
| def is_model_package_available() -> bool: | |
| """ | |
| Проверяет наличие главного файла модели. | |
| Папка model в переданном архиве намеренно оставлена пустой. Пользователь | |
| должен самостоятельно скопировать в нее файлы из архива с результатами | |
| эксперимента. Проверка позволяет вывести понятное сообщение на главной | |
| странице вместо технической ошибки. | |
| """ | |
| return Path("model/book_genre_model_package.joblib").exists() | |
| def load_metrics_summary() -> List[Dict[str, str]]: | |
| """ | |
| Загружает краткую справку о качестве модели, если файл с метриками существует. | |
| Приложение может работать без папки results. Если файлы результатов еще не | |
| скопированы, функция возвращает пустой список, а интерфейс просто не показывает | |
| блок с метриками. | |
| """ | |
| metrics_path = Path("results/final_metrics.csv") | |
| if not metrics_path.exists(): | |
| return [] | |
| try: | |
| metrics_table = pd.read_csv(metrics_path) | |
| except Exception: | |
| return [] | |
| if metrics_table.empty: | |
| return [] | |
| row = metrics_table.iloc[0] | |
| return [ | |
| { | |
| "name": "Доля верных ответов", | |
| "value": f"{float(row.get('Доля верных ответов', 0)):.4f}", | |
| }, | |
| { | |
| "name": "F1-мера с усреднением по классам", | |
| "value": f"{float(row.get('F1-мера с усреднением по классам', 0)):.4f}", | |
| }, | |
| { | |
| "name": "F1-мера с учетом размера классов", | |
| "value": f"{float(row.get('F1-мера с учетом размера классов', 0)):.4f}", | |
| }, | |
| ] | |
| def get_default_genres() -> List[str]: | |
| """ | |
| Возвращает список жанров для главной страницы до загрузки модели. | |
| Даже если файлы модели еще не скопированы, пользователь видит, какие классы | |
| поддерживает приложение. | |
| """ | |
| return [ | |
| "детектив", | |
| "историческая проза", | |
| "научная литература", | |
| "романтика", | |
| "фантастика", | |
| ] | |
| def health(): | |
| """ | |
| Возвращает простой ответ для проверки работоспособности приложения. | |
| Такой маршрут удобен при размещении на сервере: по нему можно быстро понять, | |
| что Flask-приложение запущено и принимает запросы. Проверка не загружает | |
| модель классификации, поэтому выполняется быстро и не расходует лишнюю память. | |
| """ | |
| return jsonify({"status": "ok"}) | |
| def index(): | |
| """ | |
| Отображает главную страницу приложения. | |
| На странице размещено поле для ввода аннотации, список поддерживаемых жанров, | |
| справка об ограничении языка и, при наличии файлов results, метрики лучшей модели. | |
| """ | |
| genres = get_default_genres() | |
| if is_model_package_available(): | |
| try: | |
| genres = get_prediction_service().get_available_genres() | |
| except ModelFilesError: | |
| # Если пакет модели есть, но поврежден, список жанров оставляется базовым. | |
| # Подробная ошибка будет показана при попытке выполнить классификацию. | |
| genres = get_default_genres() | |
| return render_template( | |
| "index.html", | |
| genres=genres, | |
| metrics=load_metrics_summary(), | |
| model_available=is_model_package_available(), | |
| ) | |
| def predict(): | |
| """ | |
| Принимает аннотацию из формы и возвращает страницу с результатом. | |
| Если модель отсутствует или текст слишком короткий, пользователь получает | |
| понятное сообщение с рекомендацией, что именно нужно исправить. | |
| """ | |
| annotation = request.form.get("annotation", "") | |
| try: | |
| service = get_prediction_service() | |
| prediction_result = service.predict(annotation) | |
| except (ModelFilesError, ValueError) as error: | |
| return render_template( | |
| "error.html", | |
| error_message=str(error), | |
| annotation=annotation, | |
| genres=get_default_genres(), | |
| ), 400 | |
| except Exception as error: | |
| return render_template( | |
| "error.html", | |
| error_message=( | |
| "При выполнении классификации возникла непредвиденная ошибка. " | |
| "Проверьте, что все файлы модели скопированы в папку model, " | |
| "а зависимости установлены из файла requirements.txt." | |
| ), | |
| annotation=annotation, | |
| genres=get_default_genres(), | |
| ), 500 | |
| return render_template( | |
| "result.html", | |
| result=prediction_result, | |
| genres=service.get_available_genres(), | |
| metrics=load_metrics_summary(), | |
| ) | |
| def request_entity_too_large(error): | |
| """ | |
| Обрабатывает ситуацию, когда пользователь отправил слишком большой текст. | |
| Ограничение нужно для защиты приложения от случайной передачи больших файлов | |
| вместо обычной аннотации. | |
| """ | |
| return render_template( | |
| "error.html", | |
| error_message="Текст слишком большой. Введите обычную аннотацию книги объемом в несколько предложений.", | |
| annotation="", | |
| genres=get_default_genres(), | |
| ), 413 | |
| if __name__ == "__main__": | |
| # При локальном запуске приложение использует значения из переменных окружения. | |
| # На Hugging Face Spaces порт должен совпадать с параметром app_port в README.md. | |
| # По умолчанию выбран порт 7860, так как именно его ожидает Docker Space. | |
| host = os.environ.get("HOST", "0.0.0.0") | |
| port = int(os.environ.get("PORT", "7860")) | |
| # Режим отладки включается только явно через переменную FLASK_DEBUG=1. | |
| # На сервере отладку оставляют отключенной, чтобы не показывать технические ошибки пользователю. | |
| debug_mode = os.environ.get("FLASK_DEBUG", "0") == "1" | |
| app.run(host=host, port=port, debug=debug_mode) | |