Spaces:
Running
on
Zero
Running
on
Zero
__all__ = ["binary_app"] | |
import gradio as gr | |
import torch | |
import os | |
import spaces | |
import gc | |
from model_utils import load_model, load_ternary_model, classify_text | |
from binoculars_utils import compute_scores, cleanup_model, cleanup_models | |
MINIMUM_TOKENS = 1000 | |
SAMPLE_TEXT = """Привет! Я хотел бы поделиться с вами своим опытом путешествия по Санкт-Петербургу — одному из самых красивых и удивительных городов России. Это было по-настоящему незабываемое приключение, которое оставило множество ярких впечатлений. Санкт-Петербург сразу поразил меня своей атмосферой. Город буквально дышит историей: старинные здания, величественные соборы, широкие проспекты и, конечно же, каналы — всё это создает уникальный облик. Прогуливаясь по улицам, я чувствовал себя так, будто перенёсся в другое время, в эпоху империи, великих художников, писателей и архитекторов. Особенно яркое впечатление на меня произвёл Эрмитаж. Это не просто музей — это настоящий дворец искусства. Огромное здание Зимнего дворца снаружи выглядит торжественно и величественно, а внутри скрывает одну из крупнейших коллекций произведений искусства в мире. Я провёл там несколько часов, но мне показалось, что этого времени всё равно недостаточно, чтобы увидеть и осмыслить всё, что там представлено. Картины, скульптуры, антикварная мебель, уникальные экспозиции — всё это завораживало и вдохновляло. Не менее захватывающим оказалось путешествие по каналам города. Санкт-Петербург не зря называют «Северной Венецией»: реки и каналы буквально пронизывают его карту, а прогулка на теплоходе позволила взглянуть на город с нового, водного ракурса. Я любовался старинными мостами — каждый со своей историей и архитектурным стилем — и фасадами домов, отражающимися в воде. Это было невероятно романтично и красиво, особенно когда солнце начинало садиться, заливая город золотистым светом. В целом, поездка в Петербург стала для меня настоящим открытием. Этот город невозможно охватить за одно путешествие — он требует времени, внимания и желания узнавать всё больше. Я бы с радостью вернулся туда снова, чтобы ещё глубже погрузиться в его атмосферу, посетить новые музеи, открыть для себя скрытые уголки и просто снова почувствовать эту магию, которую излучает Петербург.""" | |
css = """ | |
.human-text { | |
color: black !important; | |
line-height: 1.9em; | |
padding: 0.5em; | |
background: #ccffcc; | |
border-radius: 0.5rem; | |
font-weight: bold; | |
} | |
.ai-text { | |
color: black !important; | |
line-height: 1.9em; | |
padding: 0.5em; | |
background: #ffad99; | |
border-radius: 0.5rem; | |
font-weight: bold; | |
} | |
.rephrased-text { | |
color: black !important; | |
line-height: 1.9em; | |
padding: 0.5em; | |
background: #ffcc99; | |
border-radius: 0.5rem; | |
font-weight: bold; | |
} | |
.analysis-block { | |
background: #f5f5f5; | |
padding: 15px; | |
border-radius: 8px; | |
margin-top: 10px; | |
} | |
.scores { | |
font-size: 1.1em; | |
padding: 10px; | |
background: #e6f7ff; | |
border-radius: 5px; | |
margin: 10px 0; | |
} | |
""" | |
def run_classifier(text, mode="binary", show_analysis=False): | |
# Check GPU status at the beginning | |
if torch.cuda.is_available(): | |
print(f"Starting classification with GPU: {torch.cuda.get_device_name(0)}") | |
print(f"Initial GPU memory: {torch.cuda.memory_allocated(0) / 1e9:.2f} GB allocated") | |
torch.cuda.empty_cache() | |
else: | |
print("No GPU available, running on CPU") | |
if len(text.strip()) < MINIMUM_TOKENS: | |
return gr.Markdown(f"Текст слишком короткий. Требуется минимум {MINIMUM_TOKENS} символов."), None, None | |
try: | |
# Load appropriate classifier model based on mode | |
if mode == "binary": | |
model, scaler, label_encoder, imputer = load_model() | |
else: # ternary | |
model, scaler, label_encoder, imputer = load_ternary_model() | |
# Compute scores | |
scores = compute_scores(text, use_chat=True, use_coder=True) | |
# Run classification | |
result = classify_text(text, model, scaler, label_encoder, imputer=imputer, scores=scores) | |
# Format results | |
predicted_class = result['predicted_class'] | |
probabilities = result['probabilities'] | |
# Format probabilities | |
prob_str = "" | |
for cls, prob in probabilities.items(): | |
prob_str += f"- {cls}: {prob:.4f}\n" | |
# Format scores | |
scores_str = "" | |
if scores: | |
scores_str = "### Binoculars Scores\n" | |
if 'score_chat' in scores: | |
scores_str += f"- Score Chat: {scores['score_chat']:.4f}\n" | |
if 'score_coder' in scores: | |
scores_str += f"- Score Coder: {scores['score_coder']:.4f}\n" | |
# Result markdown | |
class_style = "human-text" if predicted_class == "Human" else "ai-text" if predicted_class in ["AI", "Raw AI"] else "rephrased-text" | |
result_md = f""" | |
## Результат классификации | |
Предсказанный класс: <span class="{class_style}">{predicted_class}</span> | |
### Вероятности классов: | |
{prob_str} | |
""" | |
# Analysis markdown | |
analysis_md = None | |
if show_analysis: | |
features = result['features'] | |
text_analysis = result['text_analysis'] | |
basic_stats_dict = { | |
'total_tokens': 'Количество токенов', | |
'total_words': 'Количество слов', | |
'unique_words': 'Количество уникальных слов', | |
'stop_words': 'Количество стоп-слов', | |
'avg_word_length': 'Средняя длина слова (символов)' | |
} | |
morph_dict = { | |
'pos_distribution': 'Распределение частей речи', | |
'unique_lemmas': 'Количество уникальных лемм', | |
'lemma_word_ratio': 'Отношение лемм к словам' | |
} | |
synt_dict = { | |
'dependencies': 'Зависимости между словами', | |
'noun_chunks': 'Количество именных групп' | |
} | |
entities_dict = { | |
'total_entities': 'Общее количество именованных сущностей', | |
'entity_types': 'Типы именованных сущностей' | |
} | |
diversity_dict = { | |
'ttr': 'TTR (отношение типов к токенам)', | |
'mtld': 'MTLD (мера лексического разнообразия)' | |
} | |
structure_dict = { | |
'sentence_count': 'Количество предложений', | |
'avg_sentence_length': 'Средняя длина предложения (токенов)', | |
'question_sentences': 'Количество вопросительных предложений', | |
'exclamation_sentences': 'Количество восклицательных предложений' | |
} | |
readability_dict = { | |
'words_per_sentence': 'Слов на предложение', | |
'syllables_per_word': 'Слогов на слово', | |
'flesh_kincaid_score': 'Индекс читабельности Флеша-Кинкейда', | |
'long_words_percent': 'Процент длинных слов' | |
} | |
semantic_dict = { | |
'avg_coherence_score': 'Средняя связность между предложениями' | |
} | |
analysis_md = "## Анализ текста\n\n" | |
# Add Binoculars Scores to analysis section | |
if scores: | |
analysis_md += scores_str + "\n" | |
# Basic statistics | |
analysis_md += "### Основная статистика\n" | |
for key, value in text_analysis.get('basic_stats', {}).items(): | |
label = basic_stats_dict.get(key, key) | |
if isinstance(value, float): | |
analysis_md += f"- {label}: {value:.2f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
analysis_md += "\n" | |
# Morphological analysis | |
analysis_md += "### Морфологический анализ\n" | |
morph_analysis = text_analysis.get('morphological_analysis', {}) | |
for key, value in morph_analysis.items(): | |
label = morph_dict.get(key, key) | |
if key == 'pos_distribution': | |
analysis_md += f"- {label}:\n" | |
for pos, count in value.items(): | |
pos_name = pos | |
if pos == 'NOUN': pos_name = 'Существительные' | |
elif pos == 'VERB': pos_name = 'Глаголы' | |
elif pos == 'ADJ': pos_name = 'Прилагательные' | |
elif pos == 'ADV': pos_name = 'Наречия' | |
elif pos == 'PROPN': pos_name = 'Имена собственные' | |
elif pos == 'DET': pos_name = 'Определители' | |
elif pos == 'ADP': pos_name = 'Предлоги' | |
elif pos == 'PRON': pos_name = 'Местоимения' | |
elif pos == 'CCONJ': pos_name = 'Сочинительные союзы' | |
elif pos == 'SCONJ': pos_name = 'Подчинительные союзы' | |
elif pos == 'NUM': pos_name = 'Числительные' | |
elif pos == 'PART': pos_name = 'Частицы' | |
elif pos == 'PUNCT': pos_name = 'Знаки препинания' | |
elif pos == 'AUX': pos_name = 'Вспомогательные глаголы' | |
elif pos == 'SYM': pos_name = 'Символы' | |
elif pos == 'INTJ': pos_name = 'Междометия' | |
elif pos == 'X': pos_name = 'Другое (X)' | |
analysis_md += f" - {pos_name}: {count}\n" | |
elif isinstance(value, float): | |
analysis_md += f"- {label}: {value:.3f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
analysis_md += "\n" | |
# Syntactic analysis | |
analysis_md += "### Синтаксический анализ\n" | |
synt_analysis = text_analysis.get('syntactic_analysis', {}) | |
for key, value in synt_analysis.items(): | |
label = synt_dict.get(key, key) | |
if key == 'dependencies': | |
analysis_md += f"- {label}:\n" | |
for dep, count in value.items(): | |
dep_name = dep | |
if dep == 'nsubj': dep_name = 'Подлежащие' | |
elif dep == 'obj': dep_name = 'Дополнения' | |
elif dep == 'amod': dep_name = 'Определения' | |
elif dep == 'nmod': dep_name = 'Именные модификаторы' | |
elif dep == 'ROOT': dep_name = 'Корневые узлы' | |
elif dep == 'punct': dep_name = 'Пунктуация' | |
elif dep == 'case': dep_name = 'Падежные маркеры' | |
elif dep == 'dep': dep_name = 'Общие зависимости' | |
elif dep == 'appos': dep_name = 'Приложения' | |
elif dep == 'flat:foreign': dep_name = 'Иностранные выражения' | |
elif dep == 'conj': dep_name = 'Сочинительные конструкции' | |
elif dep == 'obl': dep_name = 'Косвенные дополнения' | |
analysis_md += f" - {dep_name}: {count}\n" | |
elif key == 'noun_chunks': | |
if isinstance(value, bool): | |
analysis_md += f"- {label}: {0 if value is False else value}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
elif isinstance(value, float): | |
analysis_md += f"- {label}: {value:.3f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
analysis_md += "\n" | |
# Named entities | |
analysis_md += "### Именованные сущности\n" | |
entities = text_analysis.get('named_entities', {}) | |
for key, value in entities.items(): | |
label = entities_dict.get(key, key) | |
if key == 'entity_types': | |
analysis_md += f"- {label}:\n" | |
for ent, count in value.items(): | |
ent_name = ent | |
if ent == 'PER': ent_name = 'Люди' | |
elif ent == 'LOC': ent_name = 'Локации' | |
elif ent == 'ORG': ent_name = 'Организации' | |
analysis_md += f" - {ent_name}: {count}\n" | |
elif isinstance(value, float): | |
analysis_md += f"- {label}: {value:.3f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
analysis_md += "\n" | |
# Lexical diversity | |
analysis_md += "### Лексическое разнообразие\n" | |
for key, value in text_analysis.get('lexical_diversity', {}).items(): | |
label = diversity_dict.get(key, key) | |
if isinstance(value, float): | |
analysis_md += f"- {label}: {value:.3f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
analysis_md += "\n" | |
# Text structure | |
analysis_md += "### Структура текста\n" | |
for key, value in text_analysis.get('text_structure', {}).items(): | |
label = structure_dict.get(key, key) | |
if isinstance(value, float): | |
analysis_md += f"- {label}: {value:.2f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
analysis_md += "\n" | |
# Readability | |
analysis_md += "### Читабельность\n" | |
for key, value in text_analysis.get('readability', {}).items(): | |
label = readability_dict.get(key, key) | |
if isinstance(value, float): | |
analysis_md += f"- {label}: {value:.2f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
analysis_md += "\n" | |
# Semantic coherence | |
analysis_md += "### Семантическая связность\n" | |
for key, value in text_analysis.get('semantic_coherence', {}).items(): | |
label = semantic_dict.get(key, key) | |
if isinstance(value, float): | |
analysis_md += f"- {label}: {value:.3f}\n" | |
else: | |
analysis_md += f"- {label}: {value}\n" | |
# Return results | |
result_output = gr.Markdown(result_md) | |
analysis_output = gr.Markdown(analysis_md) if analysis_md else None | |
# Report final GPU memory status | |
if torch.cuda.is_available(): | |
print(f"Final GPU memory: {torch.cuda.memory_allocated(0) / 1e9:.2f} GB allocated") | |
return result_output, analysis_output, text | |
except Exception as e: | |
# Выводим ошибку в случае проблем | |
error_msg = f"Ошибка при классификации: {str(e)}" | |
print(error_msg) | |
return gr.Markdown(error_msg), None, text | |
def reset_outputs(): | |
# Force memory cleanup when resetting | |
if torch.cuda.is_available(): | |
torch.cuda.empty_cache() | |
return None, None, "" | |
with gr.Blocks(css=css, theme=gr.themes.Base()) as binary_app: | |
with gr.Row(): | |
with gr.Column(scale=3): | |
gr.HTML("<h1>Детектор AI-текста на русском языке</h1>") | |
with gr.Row(): | |
with gr.Column(): | |
input_text = gr.Textbox(value=SAMPLE_TEXT, placeholder="Введите текст для анализа", | |
lines=10, label="Текст для анализа от 1000 токенов") | |
with gr.Row(): | |
model_mode = gr.Radio( | |
["binary", "ternary"], | |
label="Режим классификации", | |
value="binary", | |
info="Выберите тип классификации: бинарная (человек/ИИ) или тернарная (человек/ИИ/перефразированный ИИ)" | |
) | |
analysis_checkbox = gr.Checkbox(label="Показать детальный анализ текста", value=False) | |
with gr.Row(): | |
submit_button = gr.Button("Классифицировать", variant="primary") | |
clear_button = gr.Button("Очистить") | |
with gr.Row(): | |
with gr.Column(): | |
result_output = gr.Markdown(label="Результат") | |
with gr.Row(): | |
with gr.Column(): | |
analysis_output = gr.Markdown(label="Анализ") | |
with gr.Accordion("О методе", open=False): | |
gr.Markdown(""" | |
Эта демонстрация использует нейронные сети для классификации текста в двух режимах: | |
#### Бинарная классификация: | |
- Human (Человек) - текст написан человеком | |
- AI (ИИ) - текст сгенерирован искусственным интеллектом | |
#### Тернарная классификация: | |
- Human (Человек) - текст написан человеком | |
- Raw AI (Чистый ИИ) - текст сгенерирован искусственным интеллектом без редактирования | |
- Rephrased AI (Перефразированный ИИ) - текст отредактированный при помощи ИИ | |
#### Демонстрация основана на комплексном анализе текста, который включает:: | |
- Вычисление показателей перплексии и кросс-перплексии с использованием подхода Binoculars | |
- Анализ морфологических, синтаксических, семантических и других особенностей текста | |
#### Рекомендации: | |
- Для более точной классификации рекомендуется использовать длинные тексты | |
- Модели подготовлены и обучены для русскоязычных текстов | |
""") | |
# Set up event handlers | |
submit_button.click( | |
fn=run_classifier, | |
inputs=[input_text, model_mode, analysis_checkbox], | |
outputs=[result_output, analysis_output, input_text] | |
) | |
clear_button.click( | |
fn=reset_outputs, | |
inputs=[], | |
outputs=[result_output, analysis_output, input_text] | |
) |