Spaces:
Sleeping
Sleeping
| import os | |
| from datetime import datetime | |
| import gradio as gr | |
| from transformers import pipeline | |
| import pdfplumber | |
| from docx import Document | |
| from fpdf import FPDF | |
| # ===== НАСТРОЙКИ ===== | |
| # Имя шрифта TTF, который лежит в корне Space (Files → root) | |
| FONT_PATH = "DejaVuSans.ttf" | |
| FONT_FAMILY = "DejaVu" | |
| # Максимальная длина текста в одном заходе в модель (по символам) | |
| # Это грубая оценка, чтобы не превышать лимит ~1024 токена у BART | |
| CHUNK_SIZE = 2000 | |
| # Ленивая инициализация summarizer, чтобы не грузить модель при импортe | |
| _summarizer = None | |
| def get_summarizer(): | |
| global _summarizer | |
| if _summarizer is None: | |
| _summarizer = pipeline( | |
| "summarization", | |
| model="facebook/bart-large-cnn" | |
| ) | |
| return _summarizer | |
| # ===== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ===== | |
| def read_text_from_file(file_path: str) -> str: | |
| """Читает текст из PDF или TXT.""" | |
| if not file_path: | |
| return "" | |
| path_lower = file_path.lower() | |
| if path_lower.endswith(".pdf"): | |
| text = [] | |
| with pdfplumber.open(file_path) as pdf: | |
| for page in pdf.pages: | |
| page_text = page.extract_text() or "" | |
| text.append(page_text) | |
| return "\n".join(text) | |
| # TXT (или любой другой текстовый) | |
| with open(file_path, "rb") as f: | |
| raw = f.read() | |
| return raw.decode("utf-8", errors="ignore") | |
| def split_into_chunks(text: str, chunk_size: int = CHUNK_SIZE): | |
| """Режет длинный текст на куски по chunk_size символов.""" | |
| text = text.strip() | |
| if len(text) <= chunk_size: | |
| return [text] | |
| chunks = [] | |
| start = 0 | |
| while start < len(text): | |
| end = start + chunk_size | |
| # стараемся резать по границе предложения/абзаца | |
| if end < len(text): | |
| dot_pos = text.rfind(".", start, end) | |
| newline_pos = text.rfind("\n", start, end) | |
| sep_pos = max(dot_pos, newline_pos) | |
| if sep_pos > start + chunk_size * 0.3: | |
| end = sep_pos + 1 | |
| chunks.append(text[start:end].strip()) | |
| start = end | |
| return [c for c in chunks if c] | |
| def summarize_long_text(text: str) -> str: | |
| """Суммаризирует длинный текст по частям и склеивает результат.""" | |
| summarizer = get_summarizer() | |
| chunks = split_into_chunks(text) | |
| summaries = [] | |
| for chunk in chunks: | |
| # подстрахуемся от совсем коротких кусков | |
| if len(chunk) < 50: | |
| continue | |
| result = summarizer( | |
| chunk, | |
| max_length=200, | |
| min_length=50, | |
| do_sample=False | |
| ) | |
| summaries.append(result[0]["summary_text"].strip()) | |
| if not summaries: | |
| return "⚠️ Не удалось создать осмысленное резюме (слишком мало текста)." | |
| return "\n\n".join(summaries) | |
| def save_docx(summary_text: str) -> str: | |
| """Сохраняет резюме в DOCX и возвращает путь к файлу.""" | |
| filename = f"summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx" | |
| doc = Document() | |
| doc.add_heading("Резюме документа", level=1) | |
| for paragraph in summary_text.split("\n\n"): | |
| doc.add_paragraph(paragraph) | |
| doc.save(filename) | |
| return filename | |
| def save_pdf(summary_text: str) -> str | None: | |
| """ | |
| Сохраняет резюме в PDF и возвращает путь к файлу. | |
| Если шрифт не найден или не подключился — возвращает None, | |
| чтобы не падать с Unicode ошибкой. | |
| """ | |
| if not os.path.exists(FONT_PATH): | |
| # Шрифт не найден — лучше вернуть None, чем падать | |
| return None | |
| filename = f"summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" | |
| pdf = FPDF() | |
| pdf.add_page() | |
| # Регистрируем Unicode-шрифт | |
| try: | |
| pdf.add_font(FONT_FAMILY, "", FONT_PATH, uni=True) | |
| except Exception as e: | |
| # Если даже тут что-то пошло не так — не ломаем всё приложение | |
| print(f"Ошибка подключения шрифта для PDF: {e}") | |
| return None | |
| pdf.set_font(FONT_FAMILY, size=12) | |
| # Пишем текст резюме | |
| for line in summary_text.split("\n"): | |
| pdf.multi_cell(0, 8, line) | |
| pdf.ln(0.5) | |
| pdf.output(filename) | |
| return filename | |
| # ===== ОСНОВНАЯ ФУНКЦИЯ ДЛЯ GRADIO ===== | |
| def summarize_file(file) -> tuple[str, str | None, str | None]: | |
| """ | |
| Основной обработчик: | |
| 1) читает файл, | |
| 2) делает суммаризацию, | |
| 3) сохраняет DOCX и PDF. | |
| Возвращает: (текстовое резюме, путь к DOCX, путь к PDF). | |
| """ | |
| if file is None: | |
| return "⚠️ Пожалуйста, загрузите файл.", None, None | |
| try: | |
| text = read_text_from_file(file.name) | |
| if len(text.strip()) < 50: | |
| return "⚠️ Слишком короткий текст для суммаризации.", None, None | |
| summary_text = summarize_long_text(text) | |
| docx_path = save_docx(summary_text) | |
| pdf_path = save_pdf(summary_text) | |
| # Если PDF не создался (нет шрифта) — просто не отдаём файл | |
| return summary_text, docx_path, pdf_path | |
| except Exception as e: | |
| # Логируем в консоль Space, а пользователю — аккуратное сообщение | |
| print(f"Ошибка при суммаризации: {e}") | |
| return f"❌ Ошибка суммаризации: {e}", None, None | |
| # ===== ИНТЕРФЕЙС GRADIO ===== | |
| demo = gr.Interface( | |
| fn=summarize_file, | |
| inputs=gr.File(label="Загрузите файл (.pdf или .txt)"), | |
| outputs=[ | |
| gr.Textbox(label="Результат суммаризации"), | |
| gr.File(label="Скачать DOCX"), | |
| gr.File(label="Скачать PDF"), | |
| ], | |
| title="Eroha Summarizer 🧠", | |
| description=( | |
| "Загрузите документ (PDF или TXT), модель создаст краткое резюме. " | |
| "Результат можно скачать в DOCX и PDF. Для корректного PDF нужен файл шрифта " | |
| f"{FONT_PATH} в корне Space." | |
| ), | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |