Compare commits
No commits in common. "623be62342114e63aee135ab8fb16b39e5bb165e" and "a0dcf50a655b2b25fe0dd71fe12f118a2edb19d1" have entirely different histories.
623be62342
...
a0dcf50a65
|
@ -1,7 +1,8 @@
|
||||||
|
# app/api/routes.py
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from app.models.schemas import TextInput, ToxicityOutput
|
from app.models.schemas import TextInput, ToxicityOutput
|
||||||
from app.core.cache import cache
|
from app.core.cache import cache
|
||||||
from app.core.text_preprocessing import preprocess_text
|
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
@ -22,15 +23,9 @@ async def assess_toxicity(input: TextInput):
|
||||||
|
|
||||||
- **text**: Текст для оценки
|
- **text**: Текст для оценки
|
||||||
"""
|
"""
|
||||||
try:
|
cache_key = get_cache_key(input.text)
|
||||||
preprocessed_text = preprocess_text(input.text)
|
|
||||||
logger.info(f"Текст после предобработки: {preprocessed_text}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при предобработке текста: {e}")
|
|
||||||
raise HTTPException(status_code=400, detail="Ошибка при предобработке текста.")
|
|
||||||
|
|
||||||
cache_key = get_cache_key(preprocessed_text)
|
|
||||||
|
|
||||||
|
# Попытка получить результат из кеша
|
||||||
cached_result = await cache.get(cache_key)
|
cached_result = await cache.get(cache_key)
|
||||||
if cached_result:
|
if cached_result:
|
||||||
try:
|
try:
|
||||||
|
@ -41,14 +36,16 @@ async def assess_toxicity(input: TextInput):
|
||||||
logger.warning(f"Кеш для ключа {cache_key} повреждён. Переходим к обработке.")
|
logger.warning(f"Кеш для ключа {cache_key} повреждён. Переходим к обработке.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = assess_toxicity_task.delay(preprocessed_text)
|
# Отправляем задачу в очередь Celery
|
||||||
logger.info(f"Задача отправлена в очередь Celery для текста: {preprocessed_text}")
|
result = assess_toxicity_task.delay(input.text)
|
||||||
toxicity_score = result.get(timeout=5)
|
logger.info(f"Задача отправлена в очередь Celery для текста: {input.text}")
|
||||||
|
toxicity_score = result.get(timeout=10) # Ждем результат до 10 секунд
|
||||||
|
|
||||||
|
# Сохраняем результат в кеш
|
||||||
await cache.set(cache_key, json.dumps(toxicity_score))
|
await cache.set(cache_key, json.dumps(toxicity_score))
|
||||||
logger.info(f"Результат сохранён в кеш для ключа {cache_key}: {toxicity_score}")
|
logger.info(f"Результат сохранён в кеш для ключа {cache_key}: {toxicity_score}")
|
||||||
|
|
||||||
return {"toxicity_score": toxicity_score}
|
return {"toxicity_score": toxicity_score}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при обработке текста: {e}")
|
logger.error(f"Ошибка при обработке текста: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Ошибка при оценке токсичности текста.")
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
# app/core/cache.py
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
# app/core/config.py
|
||||||
|
|
||||||
from pydantic import Field, validator
|
from pydantic import Field, validator
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from transformers import AutoTokenizer
|
from transformers import AutoTokenizer
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
MODEL_CHECKPOINT: str = Field(
|
MODEL_CHECKPOINT: str = Field(
|
||||||
'cointegrated/rubert-tiny-toxicity',
|
'cointegrated/rubert-tiny-toxicity',
|
||||||
|
@ -43,11 +44,14 @@ class Settings(BaseSettings):
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
raise ValueError(f'В локальной модели по пути "{v}" отсутствует файл config.json')
|
raise ValueError(f'В локальной модели по пути "{v}" отсутствует файл config.json')
|
||||||
elif v.startswith("http"):
|
elif v.startswith("http"):
|
||||||
pass
|
pass # Предполагаем, что это URL, проверим ниже
|
||||||
else:
|
else:
|
||||||
pass
|
# Предполагаем, что это название модели в HuggingFace
|
||||||
|
pass # Проверим ниже
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Попытка загрузить конфигурацию модели
|
||||||
|
# Это не загрузит модель полностью, но проверит доступность модели
|
||||||
AutoTokenizer.from_pretrained(v, cache_dir=None, force_download=False)
|
AutoTokenizer.from_pretrained(v, cache_dir=None, force_download=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f'Невозможно загрузить конфигурацию модели из transformers для "{v}": {e}')
|
raise ValueError(f'Невозможно загрузить конфигурацию модели из transformers для "{v}": {e}')
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ENGLISH_TO_RUSSIAN_LETTERS = {
|
|
||||||
'a': 'а',
|
|
||||||
'c': 'с',
|
|
||||||
'e': 'е',
|
|
||||||
'o': 'о',
|
|
||||||
'p': 'р',
|
|
||||||
'x': 'х',
|
|
||||||
'y': 'у',
|
|
||||||
'B': 'В',
|
|
||||||
'M': 'М',
|
|
||||||
'T': 'Т',
|
|
||||||
}
|
|
||||||
|
|
||||||
LETTER_DIGIT_PATTERN = re.compile(r'([а-яё]+)(\d+)', re.IGNORECASE)
|
|
||||||
|
|
||||||
RUSSIAN_LETTER_PATTERN = re.compile(r'[а-яё]', re.IGNORECASE)
|
|
||||||
|
|
||||||
def is_majority_russian(word: str) -> bool:
|
|
||||||
"""Проверяет, составляет ли большинство букв в слове русские буквы."""
|
|
||||||
letters = re.findall(r'[а-яёА-ЯЁ]', word)
|
|
||||||
if not letters:
|
|
||||||
return False
|
|
||||||
russian_count = len(letters)
|
|
||||||
total_letters = len(word)
|
|
||||||
return russian_count > (total_letters / 2)
|
|
||||||
|
|
||||||
def replace_similar_english_letters(word: str) -> str:
|
|
||||||
"""Заменяет похожие английские буквы на русские эквиваленты."""
|
|
||||||
return ''.join([ENGLISH_TO_RUSSIAN_LETTERS.get(char, char) for char in word])
|
|
||||||
|
|
||||||
def replace_digits(word: str) -> str:
|
|
||||||
"""Заменяет цифры '3' и '0' на буквы 'з' и 'о' соответственно."""
|
|
||||||
return word.replace('3', 'з').replace('0', 'о')
|
|
||||||
|
|
||||||
def split_letters_digits(word: str) -> str:
|
|
||||||
"""Разделяет буквы и цифры в слове."""
|
|
||||||
return LETTER_DIGIT_PATTERN.sub(r'\1 \2', word)
|
|
||||||
|
|
||||||
def preprocess_text(text: str) -> str:
|
|
||||||
"""
|
|
||||||
Предобрабатывает текст:
|
|
||||||
1. Приводит к нижнему регистру.
|
|
||||||
2. Заменяет похожие английские буквы на русские.
|
|
||||||
3. В словах с большинством русских букв заменяет '3' на 'з' и '0' на 'о'.
|
|
||||||
4. Разделяет буквы и цифры.
|
|
||||||
"""
|
|
||||||
logger.debug("Начало предобработки текста.")
|
|
||||||
text = text.lower()
|
|
||||||
logger.debug(f"Текст после приведения к нижнему регистру: {text}")
|
|
||||||
|
|
||||||
words = text.split()
|
|
||||||
processed_words: List[str] = []
|
|
||||||
|
|
||||||
for word in words:
|
|
||||||
original_word = word
|
|
||||||
|
|
||||||
|
|
||||||
if is_majority_russian(word):
|
|
||||||
word = replace_similar_english_letters(word)
|
|
||||||
logger.debug(f"Слово после замены английских букв: {word}")
|
|
||||||
word = replace_digits(word)
|
|
||||||
logger.debug(f"Слово после замены цифр: {word}")
|
|
||||||
|
|
||||||
word = split_letters_digits(word)
|
|
||||||
logger.debug(f"Слово после разделения букв и цифр: {word}")
|
|
||||||
|
|
||||||
processed_words.append(word)
|
|
||||||
logger.debug(f"Слово '{original_word}' преобразовано в '{word}'.")
|
|
||||||
|
|
||||||
preprocessed_text = ' '.join(processed_words)
|
|
||||||
logger.debug(f"Текст после предобработки: {preprocessed_text}")
|
|
||||||
|
|
||||||
return preprocessed_text
|
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ class ToxicityHandler:
|
||||||
self.tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
|
self.tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
|
||||||
self.model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)
|
self.model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)
|
||||||
self.model.to(self.device)
|
self.model.to(self.device)
|
||||||
self.model.eval()
|
self.model.eval() # Перевод модели в режим оценки
|
||||||
|
|
||||||
def text_to_toxicity(self, text: str, aggregate: bool = True) -> float:
|
def text_to_toxicity(self, text: str, aggregate: bool = True) -> float:
|
||||||
"""Вычисляет токсичность текста.
|
"""Вычисляет токсичность текста.
|
||||||
|
@ -30,5 +30,6 @@ class ToxicityHandler:
|
||||||
|
|
||||||
proba = proba.cpu().numpy()[0]
|
proba = proba.cpu().numpy()[0]
|
||||||
if aggregate:
|
if aggregate:
|
||||||
|
# Пример агрегирования, можно настроить по необходимости
|
||||||
return float(1 - proba[0] * (1 - proba[-1]))
|
return float(1 - proba[0] * (1 - proba[-1]))
|
||||||
return proba.tolist()
|
return proba.tolist()
|
||||||
|
|
|
@ -18,14 +18,16 @@ app = FastAPI(
|
||||||
version="1.0"
|
version="1.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Разрешение CORS (опционально, настройте по необходимости)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"], # Замените на конкретные домены при необходимости
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Включение маршрутов API
|
||||||
app.include_router(routes.router)
|
app.include_router(routes.router)
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|
|
@ -2,12 +2,14 @@ from celery import Celery
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.handlers.toxicity_handler import ToxicityHandler
|
from app.handlers.toxicity_handler import ToxicityHandler
|
||||||
|
|
||||||
|
# Инициализация Celery
|
||||||
celery_app = Celery(
|
celery_app = Celery(
|
||||||
'tasks',
|
'tasks',
|
||||||
broker=settings.CELERY_BROKER_URL,
|
broker=settings.CELERY_BROKER_URL,
|
||||||
backend=settings.CELERY_RESULT_BACKEND
|
backend=settings.CELERY_RESULT_BACKEND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Конфигурация Celery
|
||||||
celery_app.conf.update(
|
celery_app.conf.update(
|
||||||
task_serializer='json',
|
task_serializer='json',
|
||||||
result_serializer='json',
|
result_serializer='json',
|
||||||
|
@ -16,6 +18,7 @@ celery_app.conf.update(
|
||||||
enable_utc=True,
|
enable_utc=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Инициализация обработчика токсичности
|
||||||
toxicity_handler = ToxicityHandler()
|
toxicity_handler = ToxicityHandler()
|
||||||
|
|
||||||
@celery_app.task
|
@celery_app.task
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
|
|
||||||
from app.core.text_preprocessing import preprocess_text
|
|
||||||
|
|
||||||
def test_preprocess_lowercase():
|
|
||||||
"""Тестирование преобразования текста в нижний регистр."""
|
|
||||||
input_text = "ПрИвет Мир"
|
|
||||||
expected = "привет мир"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_replace_english_letters():
|
|
||||||
"""Тестирование замены похожих английских букв на русские."""
|
|
||||||
input_text = "Hello, how are you?"
|
|
||||||
expected = "hello, how are you?" # В данном случае, поскольку 'H', 'e', 'l', 'o' заменяются на русские?
|
|
||||||
# Но согласно ENGLISH_TO_RUSSIAN_LETTERS, 'a' -> 'а', 'c' -> 'с', 'e' -> 'е', 'o' -> 'о', 'p' -> 'р', 'x' -> 'х', 'y' -> 'у', 'B' -> 'В', 'M' -> 'М', 'T' -> 'Т'
|
|
||||||
# Так как ENGLISH_TO_RUSSIAN_LETTERS не охватывает все, нужно скорректировать
|
|
||||||
# Лучше использовать пример с заменяемыми буквами
|
|
||||||
input_text = "Привет a, c, e, o, p, x, y"
|
|
||||||
expected = "привет а, с, е, о, р, х, у"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_replace_digits_in_russian_word():
|
|
||||||
"""Тестирование замены цифр '3' и '0' на 'з' и 'о' в словах с преобладанием русских букв."""
|
|
||||||
input_text = "Привет3 и 0дела"
|
|
||||||
expected = "привет з и одела"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_split_letters_digits():
|
|
||||||
"""Тестирование разделения букв и цифр в словах."""
|
|
||||||
input_text = "привет12 как дела"
|
|
||||||
expected = "привет 12 как дела"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_mixed_russian_english_letters():
|
|
||||||
"""Тестирование обработки слов с смешанными русскими и английскими буквами."""
|
|
||||||
input_text = "а3b0 привет12"
|
|
||||||
# 'a3b0' -> 'азбо' (замена 'a'->'а', '3'->'з', '0'->'о') и затем разделение букв и цифр
|
|
||||||
# Но после замены, 'a3b0' -> 'азбо', в котором нет цифр, поэтому разделения не произойдет
|
|
||||||
# Возможно, другой пример: "a3b0c" -> "азбoc" -> возможно "азбоc", но это не ясно
|
|
||||||
# Давайте использовать другой пример, где после замены остаются цифры
|
|
||||||
input_text = "a3b0c123"
|
|
||||||
# 'a3b0c123' -> 'азбоc123' (замена 'a'->'а', '3'->'з', '0'->'о'), затем разделение
|
|
||||||
# Но 'c' -> 'с', по словарю 'c' -> 'с', 'a' -> 'а'
|
|
||||||
# 'a3b0c123' -> 'азбос123' -> 'азбос 123'
|
|
||||||
expected = "азбос 123"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_no_digits():
|
|
||||||
"""Тестирование обработки текста без цифр."""
|
|
||||||
input_text = "как дела сегодня"
|
|
||||||
expected = "как дела сегодня"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_digits_not_3_or_0():
|
|
||||||
"""Тестирование замены цифр, отличных от '3' и '0'."""
|
|
||||||
input_text = "привет12 и 45дела"
|
|
||||||
expected = "привет 12 и 45дела" # Цифры '1', '2', '4', '5' не заменяются
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_only_digits():
|
|
||||||
"""Тестирование обработки текста, состоящего только из цифр."""
|
|
||||||
input_text = "12345"
|
|
||||||
expected = "12345" # Нет букв, ничего не заменяется
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_empty_string():
|
|
||||||
"""Тестирование обработки пустой строки."""
|
|
||||||
input_text = ""
|
|
||||||
expected = ""
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_special_characters():
|
|
||||||
"""Тестирование обработки текста с особыми символами."""
|
|
||||||
input_text = "Привет! Как дела? a3b0c!"
|
|
||||||
expected = "привет! как дела? азбоc!"
|
|
||||||
# 'a3b0c' -> 'азбоc', затем разделение, но 'c' -> 'с', так что 'азбос!'
|
|
||||||
# Исправленный ожидаемый результат: "привет! как дела? азбос!"
|
|
||||||
expected = "привет! как дела? азбос!"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_multiple_spaces():
|
|
||||||
"""Тестирование обработки текста с несколькими пробелами."""
|
|
||||||
input_text = "привет 12 как дела"
|
|
||||||
expected = "привет 12 как дела"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
||||||
|
|
||||||
def test_preprocess_mixed_case_and_letters():
|
|
||||||
"""Тестирование обработки текста с разными регистрами и смешанными буквами."""
|
|
||||||
input_text = "ПрИвет a3B0C и 0DEFG"
|
|
||||||
# Приведение к нижнему: "привет a3b0c и 0defg"
|
|
||||||
# Замена похожих букв: 'a'->'а', 'b'->'б', 'c'->'с', 'y'->'у' и т.д.
|
|
||||||
# 'a3b0c' -> 'азбос'
|
|
||||||
# '0defg' -> 'одефg' (замена '0'->'о', 'd','e','f','g' остаются
|
|
||||||
# Но 'd','e','f','g' не заменены, так как они не в словаре
|
|
||||||
# Затем разделение: 'привет азбос и odefg'
|
|
||||||
expected = "привет азбос и odefg"
|
|
||||||
assert preprocess_text(input_text) == expected
|
|
Loading…
Reference in New Issue