Tungdabiban commited on
Commit
440b8dd
·
verified ·
1 Parent(s): 1810c40

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +412 -0
  2. requirements.txt +9 -0
app.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Text Summarization App for Hugging Face CPU Space
3
+ Model: vinai/bartpho-syllable-base
4
+ """
5
+
6
+ import re
7
+ import io
8
+ from typing import Optional
9
+ from fastapi import FastAPI, File, UploadFile, HTTPException
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from pydantic import BaseModel
12
+ from transformers import pipeline
13
+ import fitz # PyMuPDF
14
+
15
+ # Optional: newspaper3k for URL extraction
16
+ try:
17
+ from newspaper import Article
18
+ NEWSPAPER_AVAILABLE = True
19
+ except ImportError:
20
+ NEWSPAPER_AVAILABLE = False
21
+
22
+ # ============================================================
23
+ # Initialize FastAPI App
24
+ # ============================================================
25
+ app = FastAPI(
26
+ title="Vietnamese Text Summarizer",
27
+ description="Summarize Vietnamese text using BARTpho model",
28
+ version="1.0.0"
29
+ )
30
+
31
+ # CORS middleware
32
+ app.add_middleware(
33
+ CORSMiddleware,
34
+ allow_origins=["*"],
35
+ allow_credentials=True,
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ # ============================================================
41
+ # Load Model
42
+ # ============================================================
43
+ print("Loading BARTpho model...")
44
+ summarizer = pipeline(
45
+ "summarization",
46
+ model="vinai/bartpho-syllable-base",
47
+ tokenizer="vinai/bartpho-syllable-base",
48
+ device=-1 # CPU
49
+ )
50
+ print("Model loaded successfully!")
51
+
52
+
53
+ # ============================================================
54
+ # Request Models
55
+ # ============================================================
56
+ class SummarizeRequest(BaseModel):
57
+ text: Optional[str] = None
58
+ url: Optional[str] = None
59
+
60
+
61
+ # ============================================================
62
+ # Helper Functions
63
+ # ============================================================
64
+
65
+ def chunk_text_by_words(text: str, max_words: int = 700) -> list[str]:
66
+ """
67
+ Chia văn bản thành các đoạn tối đa max_words từ.
68
+ Giữ nguyên câu hoàn chỉnh khi có thể.
69
+ """
70
+ # Clean text
71
+ text = re.sub(r'\s+', ' ', text).strip()
72
+
73
+ # Split into sentences
74
+ sentences = re.split(r'(?<=[.!?])\s+', text)
75
+
76
+ chunks = []
77
+ current_chunk = []
78
+ current_word_count = 0
79
+
80
+ for sentence in sentences:
81
+ sentence_words = sentence.split()
82
+ sentence_word_count = len(sentence_words)
83
+
84
+ # Nếu câu đơn lẻ dài hơn max_words, chia nhỏ câu đó
85
+ if sentence_word_count > max_words:
86
+ # Lưu chunk hiện tại trước
87
+ if current_chunk:
88
+ chunks.append(' '.join(current_chunk))
89
+ current_chunk = []
90
+ current_word_count = 0
91
+
92
+ # Chia câu dài thành các phần
93
+ for i in range(0, sentence_word_count, max_words):
94
+ chunk_words = sentence_words[i:i + max_words]
95
+ chunks.append(' '.join(chunk_words))
96
+
97
+ # Nếu thêm câu này vượt quá giới hạn
98
+ elif current_word_count + sentence_word_count > max_words:
99
+ # Lưu chunk hiện tại
100
+ if current_chunk:
101
+ chunks.append(' '.join(current_chunk))
102
+ # Bắt đầu chunk mới với câu này
103
+ current_chunk = [sentence]
104
+ current_word_count = sentence_word_count
105
+
106
+ else:
107
+ current_chunk.append(sentence)
108
+ current_word_count += sentence_word_count
109
+
110
+ # Lưu chunk cuối cùng
111
+ if current_chunk:
112
+ chunks.append(' '.join(current_chunk))
113
+
114
+ return chunks
115
+
116
+
117
+ def fix_truncated_sentence(text: str) -> str:
118
+ """
119
+ Xử lý câu bị cụt ở cuối.
120
+ - Nếu câu cuối không kết thúc bằng dấu câu, thêm dấu chấm
121
+ - Hoặc xóa câu bị cụt nếu quá ngắn
122
+ """
123
+ text = text.strip()
124
+
125
+ if not text:
126
+ return text
127
+
128
+ # Kiểm tra nếu kết thúc bằng dấu câu
129
+ if text[-1] in '.!?':
130
+ return text
131
+
132
+ # Tìm câu cuối cùng hoàn chỉnh
133
+ last_sentence_end = max(
134
+ text.rfind('.'),
135
+ text.rfind('!'),
136
+ text.rfind('?')
137
+ )
138
+
139
+ if last_sentence_end > 0:
140
+ # Lấy phần sau dấu câu cuối
141
+ incomplete_part = text[last_sentence_end + 1:].strip()
142
+
143
+ # Nếu phần không hoàn chỉnh quá ngắn (ít hơn 5 từ), xóa nó
144
+ if len(incomplete_part.split()) < 5:
145
+ return text[:last_sentence_end + 1]
146
+ else:
147
+ # Thêm dấu chấm để kết thúc
148
+ return text + '.'
149
+
150
+ # Nếu không có dấu câu nào, thêm dấu chấm
151
+ return text + '.'
152
+
153
+
154
+ def format_as_bullet_points(summaries: list[str]) -> list[str]:
155
+ """
156
+ Chuyển đổi các đoạn tóm tắt thành danh sách bullet points.
157
+ """
158
+ bullet_points = []
159
+
160
+ for summary in summaries:
161
+ # Chia thành các câu
162
+ sentences = re.split(r'(?<=[.!?])\s+', summary)
163
+
164
+ for sentence in sentences:
165
+ sentence = sentence.strip()
166
+ if sentence and len(sentence) > 10: # Bỏ qua câu quá ngắn
167
+ # Đảm bảo câu kết thúc đúng
168
+ sentence = fix_truncated_sentence(sentence)
169
+ bullet_points.append(sentence)
170
+
171
+ return bullet_points
172
+
173
+
174
+ def generate_summary(text: str) -> str:
175
+ """
176
+ Sinh tóm tắt với các tham số chống cụt.
177
+ """
178
+ try:
179
+ result = summarizer(
180
+ text,
181
+ max_length=300,
182
+ min_length=100,
183
+ no_repeat_ngram_size=3,
184
+ repetition_penalty=2.5,
185
+ num_beams=4,
186
+ do_sample=False,
187
+ early_stopping=True
188
+ )
189
+ return result[0]['summary_text']
190
+ except Exception as e:
191
+ print(f"Error generating summary: {e}")
192
+ return ""
193
+
194
+
195
+ def summarize_long_text(text: str) -> list[str]:
196
+ """
197
+ Tóm tắt văn bản dài bằng cách chia nhỏ và tóm tắt từng phần.
198
+ """
199
+ # Chia văn bản thành các chunk 700 từ
200
+ chunks = chunk_text_by_words(text, max_words=700)
201
+
202
+ summaries = []
203
+ for i, chunk in enumerate(chunks):
204
+ print(f"Processing chunk {i + 1}/{len(chunks)}...")
205
+ summary = generate_summary(chunk)
206
+ if summary:
207
+ summaries.append(summary)
208
+
209
+ return summaries
210
+
211
+
212
+ def extract_text_from_url(url: str) -> str:
213
+ """
214
+ Trích xuất văn bản từ URL báo chí sử dụng newspaper3k.
215
+ """
216
+ if not NEWSPAPER_AVAILABLE:
217
+ raise HTTPException(
218
+ status_code=400,
219
+ detail="newspaper3k không được cài đặt. Vui lòng gửi text trực tiếp."
220
+ )
221
+
222
+ try:
223
+ article = Article(url, language='vi')
224
+ article.download()
225
+ article.parse()
226
+
227
+ text = article.text
228
+ if not text:
229
+ raise HTTPException(
230
+ status_code=400,
231
+ detail="Không thể trích xuất văn bản từ URL."
232
+ )
233
+
234
+ return text
235
+ except Exception as e:
236
+ raise HTTPException(
237
+ status_code=400,
238
+ detail=f"Lỗi khi trích xuất URL: {str(e)}"
239
+ )
240
+
241
+
242
+ def extract_text_from_pdf_bytes(pdf_bytes: bytes) -> str:
243
+ """
244
+ Đọc PDF từ byte stream sử dụng PyMuPDF.
245
+ Không lưu file ra ổ cứng.
246
+ """
247
+ try:
248
+ # Mở PDF từ byte stream
249
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
250
+
251
+ text_parts = []
252
+ for page_num in range(len(doc)):
253
+ page = doc[page_num]
254
+ text = page.get_text("text")
255
+ if text:
256
+ text_parts.append(text)
257
+
258
+ doc.close()
259
+
260
+ full_text = '\n'.join(text_parts)
261
+
262
+ if not full_text.strip():
263
+ raise HTTPException(
264
+ status_code=400,
265
+ detail="Không thể trích xuất văn bản từ PDF. File có thể là ảnh scan."
266
+ )
267
+
268
+ return full_text
269
+ except Exception as e:
270
+ raise HTTPException(
271
+ status_code=400,
272
+ detail=f"Lỗi khi đọc PDF: {str(e)}"
273
+ )
274
+
275
+
276
+ # ============================================================
277
+ # API Endpoints
278
+ # ============================================================
279
+
280
+ @app.get("/")
281
+ async def root():
282
+ """Health check endpoint."""
283
+ return {
284
+ "status": "running",
285
+ "model": "vinai/bartpho-syllable-base",
286
+ "endpoints": ["/summarize", "/upload-pdf"]
287
+ }
288
+
289
+
290
+ @app.get("/health")
291
+ async def health_check():
292
+ """Health check for Hugging Face Space."""
293
+ return {"status": "healthy"}
294
+
295
+
296
+ @app.post("/summarize")
297
+ async def summarize_text(request: SummarizeRequest):
298
+ """
299
+ Tóm tắt văn bản hoặc URL báo chí.
300
+
301
+ - Gửi `text` để tóm tắt văn bản trực tiếp
302
+ - Gửi `url` để trích xuất và tóm tắt bài báo
303
+ """
304
+ # Validate input
305
+ if not request.text and not request.url:
306
+ raise HTTPException(
307
+ status_code=400,
308
+ detail="Vui lòng cung cấp 'text' hoặc 'url'."
309
+ )
310
+
311
+ # Get text from URL or use provided text
312
+ if request.url:
313
+ text = extract_text_from_url(request.url)
314
+ else:
315
+ text = request.text
316
+
317
+ # Validate text length
318
+ if not text or len(text.strip()) < 50:
319
+ raise HTTPException(
320
+ status_code=400,
321
+ detail="Văn bản quá ngắn để tóm tắt (cần ít nhất 50 ký tự)."
322
+ )
323
+
324
+ # Generate summaries
325
+ summaries = summarize_long_text(text)
326
+
327
+ if not summaries:
328
+ raise HTTPException(
329
+ status_code=500,
330
+ detail="Không thể tạo tóm tắt."
331
+ )
332
+
333
+ # Format as bullet points
334
+ bullet_points = format_as_bullet_points(summaries)
335
+
336
+ return {
337
+ "success": True,
338
+ "original_length": len(text),
339
+ "num_chunks": len(summaries),
340
+ "bullet_points": bullet_points
341
+ }
342
+
343
+
344
+ @app.post("/upload-pdf")
345
+ async def upload_pdf(file: UploadFile = File(...)):
346
+ """
347
+ Upload và tóm tắt file PDF.
348
+
349
+ - Đọc trực tiếp từ byte stream, không lưu file ra ổ cứng
350
+ - Hỗ trợ file PDF có text (không hỗ trợ ảnh scan)
351
+ """
352
+ # Validate file type
353
+ if not file.filename.lower().endswith('.pdf'):
354
+ raise HTTPException(
355
+ status_code=400,
356
+ detail="Chỉ hỗ trợ file PDF."
357
+ )
358
+
359
+ # Read file content directly from byte stream
360
+ pdf_bytes = await file.read()
361
+
362
+ if len(pdf_bytes) == 0:
363
+ raise HTTPException(
364
+ status_code=400,
365
+ detail="File rỗng."
366
+ )
367
+
368
+ # Limit file size (10MB max)
369
+ max_size = 10 * 1024 * 1024 # 10MB
370
+ if len(pdf_bytes) > max_size:
371
+ raise HTTPException(
372
+ status_code=400,
373
+ detail="File quá lớn. Giới hạn 10MB."
374
+ )
375
+
376
+ # Extract text from PDF bytes
377
+ text = extract_text_from_pdf_bytes(pdf_bytes)
378
+
379
+ # Validate extracted text
380
+ if len(text.strip()) < 50:
381
+ raise HTTPException(
382
+ status_code=400,
383
+ detail="Văn bản trích xuất từ PDF quá ngắn."
384
+ )
385
+
386
+ # Generate summaries
387
+ summaries = summarize_long_text(text)
388
+
389
+ if not summaries:
390
+ raise HTTPException(
391
+ status_code=500,
392
+ detail="Không thể tạo tóm tắt."
393
+ )
394
+
395
+ # Format as bullet points
396
+ bullet_points = format_as_bullet_points(summaries)
397
+
398
+ return {
399
+ "success": True,
400
+ "filename": file.filename,
401
+ "original_length": len(text),
402
+ "num_chunks": len(summaries),
403
+ "bullet_points": bullet_points
404
+ }
405
+
406
+
407
+ # ============================================================
408
+ # Run with Uvicorn (for local development)
409
+ # ============================================================
410
+ if __name__ == "__main__":
411
+ import uvicorn
412
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.104.0
2
+ uvicorn>=0.24.0
3
+ transformers>=4.35.0
4
+ torch>=2.0.0
5
+ sentencepiece>=0.1.99
6
+ PyMuPDF>=1.23.0
7
+ python-multipart>=0.0.6
8
+ newspaper3k>=0.2.8
9
+ lxml_html_clean>=0.1.0