BookGenreApp / app.py
Adawqrcvb's picture
Upload 25 files
ae44122 verified
Raw
History Blame Contribute Delete
10.6 kB
"""
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)