Yermek68 commited on
Commit
928a3cd
·
verified ·
1 Parent(s): 11a33db

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -167
app.py CHANGED
@@ -1,229 +1,204 @@
1
  import os
 
 
2
  import gradio as gr
3
  from transformers import pipeline
4
  import pdfplumber
5
-
6
- # OCR
7
- try:
8
- import pytesseract
9
- from pdf2image import convert_from_path
10
- OCR_AVAILABLE = True
11
- except ImportError:
12
- OCR_AVAILABLE = False
13
-
14
- # DOCX / PDF экспорт
15
  from docx import Document
16
  from fpdf import FPDF
17
 
 
18
 
19
- # ---------- НАСТРОЙКИ ----------
20
-
21
- # Имя файла шрифта в корне Space
22
  FONT_PATH = "DejaVuSans.ttf"
 
23
 
24
- # Глобальная модель (ленивая загрузка)
25
- summarizer = None
 
26
 
 
 
27
 
28
- # ---------- МОДЕЛЬ ----------
29
 
30
  def get_summarizer():
31
- global summarizer
32
- if summarizer is None:
33
- summarizer = pipeline(
34
  "summarization",
35
- model="sshleifer/distilbart-cnn-12-6"
36
  )
37
- return summarizer
38
 
39
 
40
- # ---------- ЧТЕНИЕ ФАЙЛА ----------
41
-
42
- def extract_pdf_text(path: str):
43
- """Пытаемся вытащить текст из PDF. Если нет текста — пробуем OCR (если доступен)."""
44
- text = ""
45
-
46
- # 1) обычный текстовый PDF
47
- try:
48
- with pdfplumber.open(path) as pdf:
49
- for page in pdf.pages:
50
- chunk = page.extract_text()
51
- if chunk:
52
- text += chunk + "\n"
53
- except Exception as e:
54
- return "", f"Ошибка при чтении PDF: {e}"
55
 
56
- if text.strip():
57
- return text, None
 
 
58
 
59
- # 2) если текста нет и OCR недоступен
60
- if not OCR_AVAILABLE:
61
- return "", "PDF выглядит как скан. Для OCR нужен pytesseract + pdf2image + tesseract-ocr."
62
 
63
- # 3) OCR по картинкам
64
- try:
65
- images = convert_from_path(path, dpi=200)
66
- ocr_text = ""
67
- for img in images:
68
- ocr_text += pytesseract.image_to_string(img) + "\n"
69
- if not ocr_text.strip():
70
- return "", "OCR не смог распознать текст в этом PDF."
71
- return ocr_text, None
72
- except Exception as e:
73
- return "", f"Ошибка OCR при обработке PDF: {e}"
74
 
 
 
 
 
75
 
76
- def read_file(path: str):
77
- """Чтение PDF или текстового файла по пути."""
78
- if not path:
79
- return "", "Файл не передан."
80
 
81
- lower = path.lower()
 
 
 
 
82
 
83
- if lower.endswith(".pdf"):
84
- return extract_pdf_text(path)
 
 
85
 
86
- # TXT / другие текстовые файлы
87
- try:
88
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
89
- return f.read(), None
90
- except Exception as e:
91
- return "", f"Ошибка при чтении TXT: {e}"
 
92
 
 
 
93
 
94
- # ---------- ЧАНКИНГ ТЕКСТА ----------
95
-
96
- def chunk_text(text: str, max_chars: int = 2500):
97
- """Режем длинный текст на куски, стара��сь обрезать по точкам."""
98
- chunks = []
99
- while len(text) > max_chars:
100
- cut = text[:max_chars]
101
- last_dot = cut.rfind(".")
102
- if last_dot != -1:
103
- cut = cut[:last_dot + 1]
104
- chunks.append(cut)
105
- text = text[len(cut):]
106
- if text:
107
- chunks.append(text)
108
- return chunks
109
 
110
 
111
  def summarize_long_text(text: str) -> str:
112
- model = get_summarizer()
113
- parts = []
114
- for chunk in chunk_text(text, max_chars=2500):
115
- if not chunk.strip():
 
 
 
 
116
  continue
117
- summary = model(
118
  chunk,
119
- max_length=180,
120
- min_length=60,
121
  do_sample=False
122
  )
123
- parts.append(summary[0]["summary_text"])
124
- return "\n\n".join(parts)
 
 
125
 
 
126
 
127
- # ---------- ЭКСПОРТ В DOCX / PDF ----------
128
 
129
- def save_docx(summary: str) -> str:
 
 
130
  doc = Document()
131
- doc.add_heading("Eroha Summarizer – Резюме документа", level=1)
132
- for paragraph in summary.split("\n"):
133
  doc.add_paragraph(paragraph)
134
- out_path = "/tmp/summary.docx"
135
- doc.save(out_path)
136
- return out_path
 
 
 
 
 
 
 
 
 
 
137
 
 
138
 
139
- def save_pdf(summary: str) -> str:
140
  pdf = FPDF()
141
  pdf.add_page()
142
 
143
- # Подключаем Unicode-шрифт
144
  try:
145
- pdf.add_font("DejaVu", "", FONT_PATH, uni=True)
146
- pdf.set_font("DejaVu", size=12)
147
- except Exception:
148
- # Фоллбек – латинский Arial (кириллица может не сохраниться, но ошибок не будет)
149
- pdf.set_font("Arial", size=12)
150
-
151
- for line in summary.split("\n"):
152
- try:
153
- pdf.multi_cell(0, 8, line)
154
- except Exception:
155
- # Если шрифт не поддерживает символы – пропускаем проблемную строку
156
- continue
157
 
158
- out_path = "/tmp/summary.pdf"
159
- pdf.output(out_path)
160
- return out_path
161
 
 
 
 
 
162
 
163
- # ---------- ОСНОВНАЯ ФУНКЦИЯ ДЛЯ GRADIO ----------
 
164
 
165
- def summarize_file(file_path: str):
166
- text, err = read_file(file_path)
167
- if err:
168
- return f"⚠️ {err}", None, None
169
 
170
- if not text.strip():
171
- return "⚠️ Не удалось извлечь текст из файла.", None, None
172
 
173
- if len(text.strip()) < 80:
174
- return "⚠️ Слишком мало текста для суммаризации.", None, None
 
 
 
 
 
 
 
 
175
 
176
- # Суммаризация с чанкингом
177
  try:
178
- final_summary = summarize_long_text(text)
179
- except Exception as e:
180
- return f"⚠️ Ошибка суммаризации: {e}", None, None
181
 
182
- # Экспорт в DOCX / PDF
183
- try:
184
- docx_path = save_docx(final_summary)
185
- except Exception as e:
186
- docx_path = None
187
- final_summary += f"\n\n[Предупреждение: ошибка сохранения DOCX: {e}]"
188
-
189
- try:
190
- pdf_path = save_pdf(final_summary)
191
- except Exception as e:
192
- pdf_path = None
193
- final_summary += f"\n\n[Предупреждение: ошибка сохранения PDF: {e}]"
194
 
195
- return final_summary, docx_path, pdf_path
196
 
 
 
197
 
198
- # ---------- ИНТЕРФЕЙС GRADIO ----------
 
199
 
200
- with gr.Blocks() as demo:
201
- gr.Markdown("# Eroha Summarizer 🧠")
202
- gr.Markdown(
203
- "Загрузите документ (**PDF или TXT**), и модель создаст краткое резюме "
204
- "с возможностью скачивания **DOCX** и **PDF**."
205
- )
206
-
207
- with gr.Row():
208
- file_input = gr.File(
209
- type="filepath",
210
- label="Загрузите файл (.pdf или .txt)"
211
- )
212
- with gr.Column():
213
- summary_output = gr.Textbox(
214
- label="Результат суммаризации",
215
- lines=20
216
- )
217
- docx_output = gr.File(label="Скачать DOCX")
218
- pdf_output = gr.File(label="Скачать PDF")
219
-
220
- submit_btn = gr.Button("Запустить суммаризацию")
221
-
222
- submit_btn.click(
223
- fn=summarize_file,
224
- inputs=file_input,
225
- outputs=[summary_output, docx_output, pdf_output]
226
- )
227
 
228
  if __name__ == "__main__":
229
  demo.launch()
 
1
  import os
2
+ from datetime import datetime
3
+
4
  import gradio as gr
5
  from transformers import pipeline
6
  import pdfplumber
 
 
 
 
 
 
 
 
 
 
7
  from docx import Document
8
  from fpdf import FPDF
9
 
10
+ # ===== НАСТРОЙКИ =====
11
 
12
+ # Имя шрифта TTF, который лежит в корне Space (Files → root)
 
 
13
  FONT_PATH = "DejaVuSans.ttf"
14
+ FONT_FAMILY = "DejaVu"
15
 
16
+ # Максимальная длина текста в одном заходе в модель (по символам)
17
+ # Это грубая оценка, чтобы не превышать лимит ~1024 токена у BART
18
+ CHUNK_SIZE = 2000
19
 
20
+ # Ленивая инициализация summarizer, чтобы не грузить модель при импортe
21
+ _summarizer = None
22
 
 
23
 
24
  def get_summarizer():
25
+ global _summarizer
26
+ if _summarizer is None:
27
+ _summarizer = pipeline(
28
  "summarization",
29
+ model="facebook/bart-large-cnn"
30
  )
31
+ return _summarizer
32
 
33
 
34
+ # ===== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ def read_text_from_file(file_path: str) -> str:
37
+ """Читает текст из PDF или TXT."""
38
+ if not file_path:
39
+ return ""
40
 
41
+ path_lower = file_path.lower()
 
 
42
 
43
+ # PDF
44
+ if path_lower.endswith(".pdf"):
45
+ text = []
46
+ with pdfplumber.open(file_path) as pdf:
47
+ for page in pdf.pages:
48
+ page_text = page.extract_text() or ""
49
+ text.append(page_text)
50
+ return "\n".join(text)
 
 
 
51
 
52
+ # TXT (или любой другой текстовый)
53
+ with open(file_path, "rb") as f:
54
+ raw = f.read()
55
+ return raw.decode("utf-8", errors="ignore")
56
 
 
 
 
 
57
 
58
+ def split_into_chunks(text: str, chunk_size: int = CHUNK_SIZE):
59
+ """Режет длинный текст на куски по chunk_size символов."""
60
+ text = text.strip()
61
+ if len(text) <= chunk_size:
62
+ return [text]
63
 
64
+ chunks = []
65
+ start = 0
66
+ while start < len(text):
67
+ end = start + chunk_size
68
 
69
+ # стараемся резать по границе предложения/абзаца
70
+ if end < len(text):
71
+ dot_pos = text.rfind(".", start, end)
72
+ newline_pos = text.rfind("\n", start, end)
73
+ sep_pos = max(dot_pos, newline_pos)
74
+ if sep_pos > start + chunk_size * 0.3:
75
+ end = sep_pos + 1
76
 
77
+ chunks.append(text[start:end].strip())
78
+ start = end
79
 
80
+ return [c for c in chunks if c]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
 
83
  def summarize_long_text(text: str) -> str:
84
+ """Суммаризирует длинный текст по частям и склеивает результат."""
85
+ summarizer = get_summarizer()
86
+ chunks = split_into_chunks(text)
87
+
88
+ summaries = []
89
+ for chunk in chunks:
90
+ # подстрахуемся от совсем коротких кусков
91
+ if len(chunk) < 50:
92
  continue
93
+ result = summarizer(
94
  chunk,
95
+ max_length=200,
96
+ min_length=50,
97
  do_sample=False
98
  )
99
+ summaries.append(result[0]["summary_text"].strip())
100
+
101
+ if not summaries:
102
+ return "⚠️ Не удалось создать осмысленное резюме (слишком мало текста)."
103
 
104
+ return "\n\n".join(summaries)
105
 
 
106
 
107
+ def save_docx(summary_text: str) -> str:
108
+ """Сохраняет резюме в DOCX и возвращает путь к файлу."""
109
+ filename = f"summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
110
  doc = Document()
111
+ doc.add_heading("Резюме документа", level=1)
112
+ for paragraph in summary_text.split("\n\n"):
113
  doc.add_paragraph(paragraph)
114
+ doc.save(filename)
115
+ return filename
116
+
117
+
118
+ def save_pdf(summary_text: str) -> str | None:
119
+ """
120
+ Сохраняет резюме в PDF и возвращает путь к файлу.
121
+ Если шрифт не найден или не подключился — возвращает None,
122
+ чтобы не падать с Unicode ошибкой.
123
+ """
124
+ if not os.path.exists(FONT_PATH):
125
+ # Шрифт не найден — лучше вернуть None, чем падать
126
+ return None
127
 
128
+ filename = f"summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
129
 
 
130
  pdf = FPDF()
131
  pdf.add_page()
132
 
133
+ # Регистрируем Unicode-шрифт
134
  try:
135
+ pdf.add_font(FONT_FAMILY, "", FONT_PATH, uni=True)
136
+ except Exception as e:
137
+ # Если даже тут что-то пошло не так — не ломаем всё приложение
138
+ print(f"Ошибка подключения шрифта для PDF: {e}")
139
+ return None
 
 
 
 
 
 
 
140
 
141
+ pdf.set_font(FONT_FAMILY, size=12)
 
 
142
 
143
+ # Пишем текст резюме
144
+ for line in summary_text.split("\n"):
145
+ pdf.multi_cell(0, 8, line)
146
+ pdf.ln(0.5)
147
 
148
+ pdf.output(filename)
149
+ return filename
150
 
 
 
 
 
151
 
152
+ # ===== ОСНОВНАЯ ФУНКЦИЯ ДЛЯ GRADIO =====
 
153
 
154
+ def summarize_file(file) -> tuple[str, str | None, str | None]:
155
+ """
156
+ Основной обработчик:
157
+ 1) читает файл,
158
+ 2) делает суммаризацию,
159
+ 3) сохраняет DOCX и PDF.
160
+ Возвращает: (текстовое резюме, путь к DOCX, путь к PDF).
161
+ """
162
+ if file is None:
163
+ return "⚠️ Пожалуйста, загрузите файл.", None, None
164
 
 
165
  try:
166
+ text = read_text_from_file(file.name)
 
 
167
 
168
+ if len(text.strip()) < 50:
169
+ return "⚠️ Слишком короткий текст для суммаризации.", None, None
 
 
 
 
 
 
 
 
 
 
170
 
171
+ summary_text = summarize_long_text(text)
172
 
173
+ docx_path = save_docx(summary_text)
174
+ pdf_path = save_pdf(summary_text)
175
 
176
+ # Если PDF не создался (нет шрифта) — просто не отдаём файл
177
+ return summary_text, docx_path, pdf_path
178
 
179
+ except Exception as e:
180
+ # Логируем в консоль Space, а пользователю — аккуратное сообщение
181
+ print(f"Ошибка при суммаризации: {e}")
182
+ return f" Ошибка суммаризации: {e}", None, None
183
+
184
+
185
+ # ===== ИНТЕРФЕЙС GRADIO =====
186
+
187
+ demo = gr.Interface(
188
+ fn=summarize_file,
189
+ inputs=gr.File(label="Загрузите файл (.pdf или .txt)"),
190
+ outputs=[
191
+ gr.Textbox(label="Результат суммаризации"),
192
+ gr.File(label="Скачать DOCX"),
193
+ gr.File(label="Скачать PDF"),
194
+ ],
195
+ title="Eroha Summarizer 🧠",
196
+ description=(
197
+ "Загрузите документ (PDF или TXT), модель создаст краткое резюме. "
198
+ "Результат можно скачать в DOCX и PDF. Для корректного PDF нужен файл шрифта "
199
+ f"{FONT_PATH} в корне Space."
200
+ ),
201
+ )
 
 
 
 
202
 
203
  if __name__ == "__main__":
204
  demo.launch()