Spaces:
Runtime error
Runtime error
File size: 10,616 Bytes
ae44122 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | """
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
@lru_cache(maxsize=1)
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 [
"детектив",
"историческая проза",
"научная литература",
"романтика",
"фантастика",
]
@app.route("/health", methods=["GET"])
def health():
"""
Возвращает простой ответ для проверки работоспособности приложения.
Такой маршрут удобен при размещении на сервере: по нему можно быстро понять,
что Flask-приложение запущено и принимает запросы. Проверка не загружает
модель классификации, поэтому выполняется быстро и не расходует лишнюю память.
"""
return jsonify({"status": "ok"})
@app.route("/", methods=["GET"])
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(),
)
@app.route("/predict", methods=["POST"])
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(),
)
@app.errorhandler(413)
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)
|