Spaces:
Running
Running
Fix: Xóa prompt nhà báo, cải thiện slide kết thúc câu hoàn chỉnh
Browse files- app_v2_entry.py +78 -37
app_v2_entry.py
CHANGED
|
@@ -33,6 +33,23 @@ app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
|
|
| 33 |
|
| 34 |
def _clean(s): return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
# Cache for match details (5 min TTL)
|
| 37 |
_match_cache = {}
|
| 38 |
|
|
@@ -1599,7 +1616,6 @@ async def api_personal_post_preview(request: Request):
|
|
| 1599 |
source_context += f" - {p[:300]}\n"
|
| 1600 |
|
| 1601 |
prompt = (
|
| 1602 |
-
"Bạn là nhà báo chuyên nghiệp tiếng Việt. Hãy viết bài phân tích dựa trên quan điểm cá nhân và các nguồn tin sau.\n\n"
|
| 1603 |
"=== QUAN ĐIỂM CÁ NHÂN ===\n" + opinion[:2000] + "\n\n"
|
| 1604 |
"=== NGUỒN TIN THAM KHẢO ===\n" + source_context + "\n\n"
|
| 1605 |
"=== YÊU CẦU VIẾT BÀI THEO SLIDE ===\n"
|
|
@@ -1607,26 +1623,27 @@ async def api_personal_post_preview(request: Request):
|
|
| 1607 |
"\n"
|
| 1608 |
"SLIDE 1 - MỞ ĐẦU:\n"
|
| 1609 |
"- Giới thiệu chủ đề, nêu quan điểm cá nhân (dựa vào QUAN ĐIỂM CÁ NHÂN)\n"
|
| 1610 |
-
"- 2-4 câu, thể hiện rõ góc nhìn của bạn\n"
|
| 1611 |
"\n"
|
| 1612 |
"SLIDE 2-3-4-5 - PHÂN TÍCH:\n"
|
| 1613 |
"- Mỗi slide dùng 1 nguồn tin CỤ THỂ, kết hợp với quan điểm cá nhân\n"
|
| 1614 |
"- Dẫn chứng từ nguồn (ghi rõ tên báo: Theo VnExpress, ...)\n"
|
| 1615 |
-
"- 2-4 câu mỗi slide, trình bày như 1 đoạn văn hoàn chỉnh\n"
|
| 1616 |
"\n"
|
| 1617 |
"SLIDE 6 - KẾT LUẬN:\n"
|
| 1618 |
"- Tổng kết quan điểm, đưa ra nhận định cuối cùng\n"
|
| 1619 |
-
"- 2-3 câu\n"
|
| 1620 |
"\n"
|
| 1621 |
"Định dạng đầu ra:\n"
|
| 1622 |
"---SLIDE 1---\n"
|
| 1623 |
-
"[nội dung đoạn văn slide 1]\n"
|
| 1624 |
"---SLIDE 2---\n"
|
| 1625 |
-
"[nội dung đoạn văn slide 2]\n"
|
| 1626 |
"...v.v...\n"
|
| 1627 |
"\n"
|
| 1628 |
"QUAN TRỌNG:\n"
|
| 1629 |
"- Mỗi slide là 1 đoạn văn HOÀN CHỈNH, 2-4 câu, đọc liền mạch\n"
|
|
|
|
| 1630 |
"- Kết hợp QUAN ĐIỂM CÁ NHÂN với NỘI DUNG NGUỒN TIN\n"
|
| 1631 |
"- Không gạch đầu dòng, không bullet points\n"
|
| 1632 |
"- Viết liền mạch tự nhiên, giọng văn báo chí\n"
|
|
@@ -1656,7 +1673,8 @@ async def api_personal_post_preview(request: Request):
|
|
| 1656 |
matches = re.findall(pattern, ai_text, re.DOTALL)
|
| 1657 |
if matches:
|
| 1658 |
for idx, (num, content) in enumerate(matches):
|
| 1659 |
-
|
|
|
|
| 1660 |
if len(text) > 30:
|
| 1661 |
img = source_images[idx] if idx < len(source_images) else ""
|
| 1662 |
slides.append({"text": text, "image": img, "index": idx + 1})
|
|
@@ -1670,9 +1688,11 @@ async def api_personal_post_preview(request: Request):
|
|
| 1670 |
p = p.strip()
|
| 1671 |
if p.startswith('#') or p.startswith('---') or p.startswith('*Nguồn'):
|
| 1672 |
continue
|
| 1673 |
-
|
|
|
|
|
|
|
| 1674 |
img = source_images[para_count] if para_count < len(source_images) else ""
|
| 1675 |
-
slides.append({"text":
|
| 1676 |
para_count += 1
|
| 1677 |
if para_count >= 6:
|
| 1678 |
break
|
|
@@ -1681,11 +1701,24 @@ async def api_personal_post_preview(request: Request):
|
|
| 1681 |
# Fallback: create from opinion
|
| 1682 |
slides = [{"text": f"Quan điểm: {opinion[:300]}", "image": source_images[0] if source_images else "", "index": 1}]
|
| 1683 |
if ai_text:
|
| 1684 |
-
|
| 1685 |
-
|
| 1686 |
-
|
| 1687 |
-
|
| 1688 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1689 |
|
| 1690 |
preview = {
|
| 1691 |
"title": title,
|
|
@@ -1786,20 +1819,19 @@ async def api_personal_post(request: Request):
|
|
| 1786 |
for j, p in enumerate(src_paras[:3]):
|
| 1787 |
source_context += f" - {p[:300]}\n"
|
| 1788 |
prompt = (
|
| 1789 |
-
"
|
| 1790 |
-
"=== QUAN ĐIỂM ===\n" + opinion[:2000] + "\n\n"
|
| 1791 |
"=== NGUỒN TIN ===\n" + source_context + "\n\n"
|
| 1792 |
"=== YÊU CẦU VIẾT BÀI THEO SLIDE ===\n"
|
| 1793 |
"Viết bài thành 5-6 ĐOẠN VĂN NGẮN, mỗi đoạn là 1 SLIDE.\n"
|
| 1794 |
"\n"
|
| 1795 |
-
"SLIDE 1 - MỞ ĐẦU: Giới thiệu chủ đề, nêu quan điểm cá nhân (2-4 câu)\n"
|
| 1796 |
-
"SLIDE 2-3-4-5 - PHÂN TÍCH: Mỗi slide dùng 1 nguồn tin cụ thể, kết hợp quan điểm cá nhân, ghi rõ nguồn (Theo VnExpress...), 2-4 câu, thành 1 đoạn văn hoàn chỉnh\n"
|
| 1797 |
-
"SLIDE 6 - KẾT LUẬN: Tổng kết quan điểm, nhận định cuối cùng (2-3 câu)\n"
|
| 1798 |
"\n"
|
| 1799 |
"Định dạng:\n"
|
| 1800 |
-
"---SLIDE 1---\n[đoạn văn]\n---SLIDE 2---\n[đoạn văn]\n...\n"
|
| 1801 |
"\n"
|
| 1802 |
-
"QUAN TRỌNG: Mỗi slide là 1 đoạn văn HOÀN CHỈNH, 2-4 câu. Kết hợp QUAN ĐIỂM + NGUỒN TIN. Không gạch đầu dòng. Viết liền mạch. 300-600 từ."
|
| 1803 |
)
|
| 1804 |
ai_text = await ai_ext.qwen_generate(prompt, max_tokens=2000)
|
| 1805 |
except:
|
|
@@ -1830,7 +1862,8 @@ async def api_personal_post(request: Request):
|
|
| 1830 |
matches = re.findall(pattern, ai_text, re.DOTALL)
|
| 1831 |
if matches:
|
| 1832 |
for idx, (num, content) in enumerate(matches):
|
| 1833 |
-
|
|
|
|
| 1834 |
if len(text) > 30:
|
| 1835 |
img = source_images[idx] if idx < len(source_images) else ""
|
| 1836 |
slides.append({"text": text, "image": img, "index": idx + 1})
|
|
@@ -1844,9 +1877,11 @@ async def api_personal_post(request: Request):
|
|
| 1844 |
p = p.strip()
|
| 1845 |
if p.startswith('#') or p.startswith('---') or p.startswith('*Nguồn'):
|
| 1846 |
continue
|
| 1847 |
-
|
|
|
|
|
|
|
| 1848 |
img = source_images[para_count] if para_count < len(source_images) else ""
|
| 1849 |
-
slides.append({"text":
|
| 1850 |
para_count += 1
|
| 1851 |
if para_count >= 6:
|
| 1852 |
break
|
|
@@ -1854,19 +1889,25 @@ async def api_personal_post(request: Request):
|
|
| 1854 |
if len(slides) < 2:
|
| 1855 |
slides = [{"text": f"Quan điểm: {opinion[:300]}", "image": source_images[0] if source_images else "", "index": 1}]
|
| 1856 |
if ai_text:
|
| 1857 |
-
|
| 1858 |
-
|
| 1859 |
-
|
| 1860 |
-
|
| 1861 |
-
|
| 1862 |
-
|
| 1863 |
-
|
| 1864 |
-
|
| 1865 |
-
|
| 1866 |
-
|
| 1867 |
-
|
| 1868 |
-
|
| 1869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1870 |
|
| 1871 |
lang, emotion = detect_language_and_emotion(title, ai_text)
|
| 1872 |
voice = get_voice_for_content(title, ai_text)
|
|
|
|
| 33 |
|
| 34 |
def _clean(s): return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
|
| 35 |
|
| 36 |
+
def _ensure_sentence_complete(text):
|
| 37 |
+
"""Ensure text ends with complete sentence (ends with . ! or ?). Trim if cut mid-sentence."""
|
| 38 |
+
text = _clean(text)
|
| 39 |
+
if not text:
|
| 40 |
+
return text
|
| 41 |
+
# Find last sentence ending
|
| 42 |
+
for end_char in ['.', '!', '?']:
|
| 43 |
+
last_pos = text.rfind(end_char)
|
| 44 |
+
if last_pos > len(text) * 0.5: # Keep if ending is in latter half
|
| 45 |
+
return text[:last_pos + 1].strip()
|
| 46 |
+
# If no ending found, try to find last complete sentence
|
| 47 |
+
sentences = re.split(r'(?<=[.!?])\s+', text)
|
| 48 |
+
complete = [s.strip() for s in sentences if s.strip() and len(s.strip()) > 20]
|
| 49 |
+
if complete[:-1]: # Return all but last incomplete
|
| 50 |
+
return ' '.join(complete[:-1])
|
| 51 |
+
return text[:150] + '.' if len(text) > 150 else text
|
| 52 |
+
|
| 53 |
# Cache for match details (5 min TTL)
|
| 54 |
_match_cache = {}
|
| 55 |
|
|
|
|
| 1616 |
source_context += f" - {p[:300]}\n"
|
| 1617 |
|
| 1618 |
prompt = (
|
|
|
|
| 1619 |
"=== QUAN ĐIỂM CÁ NHÂN ===\n" + opinion[:2000] + "\n\n"
|
| 1620 |
"=== NGUỒN TIN THAM KHẢO ===\n" + source_context + "\n\n"
|
| 1621 |
"=== YÊU CẦU VIẾT BÀI THEO SLIDE ===\n"
|
|
|
|
| 1623 |
"\n"
|
| 1624 |
"SLIDE 1 - MỞ ĐẦU:\n"
|
| 1625 |
"- Giới thiệu chủ đề, nêu quan điểm cá nhân (dựa vào QUAN ĐIỂM CÁ NHÂN)\n"
|
| 1626 |
+
"- 2-4 câu hoàn chỉnh, thể hiện rõ góc nhìn của bạn\n"
|
| 1627 |
"\n"
|
| 1628 |
"SLIDE 2-3-4-5 - PHÂN TÍCH:\n"
|
| 1629 |
"- Mỗi slide dùng 1 nguồn tin CỤ THỂ, kết hợp với quan điểm cá nhân\n"
|
| 1630 |
"- Dẫn chứng từ nguồn (ghi rõ tên báo: Theo VnExpress, ...)\n"
|
| 1631 |
+
"- 2-4 câu hoàn chỉnh mỗi slide, trình bày như 1 đoạn văn hoàn chỉnh\n"
|
| 1632 |
"\n"
|
| 1633 |
"SLIDE 6 - KẾT LUẬN:\n"
|
| 1634 |
"- Tổng kết quan điểm, đưa ra nhận định cuối cùng\n"
|
| 1635 |
+
"- 2-3 câu hoàn chỉnh\n"
|
| 1636 |
"\n"
|
| 1637 |
"Định dạng đầu ra:\n"
|
| 1638 |
"---SLIDE 1---\n"
|
| 1639 |
+
"[nội dung đoạn văn slide 1 - PHẢI KẾT THÚC BẰNG DẤU CHẤM, CHỤM HỎI hoặc CHẤM CÂU]\n"
|
| 1640 |
"---SLIDE 2---\n"
|
| 1641 |
+
"[nội dung đoạn văn slide 2 - PHẢI KẾT THÚC BẰNG DẤU CHẤM, CHỤM HỎI hoặc CHẤM CÂU]\n"
|
| 1642 |
"...v.v...\n"
|
| 1643 |
"\n"
|
| 1644 |
"QUAN TRỌNG:\n"
|
| 1645 |
"- Mỗi slide là 1 đoạn văn HOÀN CHỈNH, 2-4 câu, đọc liền mạch\n"
|
| 1646 |
+
"- PHẢI KẾT THÚC BẰNG DẤU CHẤM (.) HOẶN TOÀN\n"
|
| 1647 |
"- Kết hợp QUAN ĐIỂM CÁ NHÂN với NỘI DUNG NGUỒN TIN\n"
|
| 1648 |
"- Không gạch đầu dòng, không bullet points\n"
|
| 1649 |
"- Viết liền mạch tự nhiên, giọng văn báo chí\n"
|
|
|
|
| 1673 |
matches = re.findall(pattern, ai_text, re.DOTALL)
|
| 1674 |
if matches:
|
| 1675 |
for idx, (num, content) in enumerate(matches):
|
| 1676 |
+
# Normalize: ensure complete sentences
|
| 1677 |
+
text = _ensure_sentence_complete(content)
|
| 1678 |
if len(text) > 30:
|
| 1679 |
img = source_images[idx] if idx < len(source_images) else ""
|
| 1680 |
slides.append({"text": text, "image": img, "index": idx + 1})
|
|
|
|
| 1688 |
p = p.strip()
|
| 1689 |
if p.startswith('#') or p.startswith('---') or p.startswith('*Nguồn'):
|
| 1690 |
continue
|
| 1691 |
+
# Combine multiple short paras into one slide, normalize
|
| 1692 |
+
p_normalized = _ensure_sentence_complete(p)
|
| 1693 |
+
if len(p_normalized) > 50:
|
| 1694 |
img = source_images[para_count] if para_count < len(source_images) else ""
|
| 1695 |
+
slides.append({"text": p_normalized, "image": img, "index": para_count + 1})
|
| 1696 |
para_count += 1
|
| 1697 |
if para_count >= 6:
|
| 1698 |
break
|
|
|
|
| 1701 |
# Fallback: create from opinion
|
| 1702 |
slides = [{"text": f"Quan điểm: {opinion[:300]}", "image": source_images[0] if source_images else "", "index": 1}]
|
| 1703 |
if ai_text:
|
| 1704 |
+
# Split by sentences for better chunks
|
| 1705 |
+
sentences = re.split(r'(?<=[.!?])\s+', ai_text)
|
| 1706 |
+
current_chunk = ""
|
| 1707 |
+
for sent in sentences:
|
| 1708 |
+
sent = _ensure_sentence_complete(sent)
|
| 1709 |
+
if len(sent) > 10 and current_chunk:
|
| 1710 |
+
if len(current_chunk + sent) < 400:
|
| 1711 |
+
current_chunk += " " + sent
|
| 1712 |
+
else:
|
| 1713 |
+
if len(current_chunk) > 50:
|
| 1714 |
+
idx = len(slides)
|
| 1715 |
+
img = source_images[idx] if idx < len(source_images) else ""
|
| 1716 |
+
slides.append({"text": current_chunk, "image": img, "index": idx + 1})
|
| 1717 |
+
current_chunk = sent
|
| 1718 |
+
if len(current_chunk) > 50 and len(slides) < 6:
|
| 1719 |
+
idx = len(slides)
|
| 1720 |
+
img = source_images[idx] if idx < len(source_images) else ""
|
| 1721 |
+
slides.append({"text": current_chunk, "image": img, "index": idx + 1})
|
| 1722 |
|
| 1723 |
preview = {
|
| 1724 |
"title": title,
|
|
|
|
| 1819 |
for j, p in enumerate(src_paras[:3]):
|
| 1820 |
source_context += f" - {p[:300]}\n"
|
| 1821 |
prompt = (
|
| 1822 |
+
"=== QUAN ĐIỂM CÁ NHÂN ===\n" + opinion[:2000] + "\n\n"
|
|
|
|
| 1823 |
"=== NGUỒN TIN ===\n" + source_context + "\n\n"
|
| 1824 |
"=== YÊU CẦU VIẾT BÀI THEO SLIDE ===\n"
|
| 1825 |
"Viết bài thành 5-6 ĐOẠN VĂN NGẮN, mỗi đoạn là 1 SLIDE.\n"
|
| 1826 |
"\n"
|
| 1827 |
+
"SLIDE 1 - MỞ ĐẦU: Giới thiệu chủ đề, nêu quan điểm cá nhân (2-4 câu hoàn chỉnh)\n"
|
| 1828 |
+
"SLIDE 2-3-4-5 - PHÂN TÍCH: Mỗi slide dùng 1 nguồn tin cụ thể, kết hợp quan điểm cá nhân, ghi rõ nguồn (Theo VnExpress...), 2-4 câu hoàn chỉnh, thành 1 đoạn văn hoàn chỉnh\n"
|
| 1829 |
+
"SLIDE 6 - KẾT LUẬN: Tổng kết quan điểm, nhận định cuối cùng (2-3 câu hoàn chỉnh)\n"
|
| 1830 |
"\n"
|
| 1831 |
"Định dạng:\n"
|
| 1832 |
+
"---SLIDE 1---\n[đoạn văn hoàn chỉnh kết thúc bằng dấu chấm]\n---SLIDE 2---\n[đoạn văn hoàn chỉnh kết thúc bằng dấu chấm]\n...\n"
|
| 1833 |
"\n"
|
| 1834 |
+
"QUAN TRỌNG: Mỗi slide là 1 đoạn văn HOÀN CHỈNH, 2-4 câu, PHẢI KẾT THÚC BẰNG DẤU CHẤM (.). Kết hợp QUAN ĐIỂM + NGUỒN TIN. Không gạch đầu dòng. Viết liền mạch. 300-600 từ."
|
| 1835 |
)
|
| 1836 |
ai_text = await ai_ext.qwen_generate(prompt, max_tokens=2000)
|
| 1837 |
except:
|
|
|
|
| 1862 |
matches = re.findall(pattern, ai_text, re.DOTALL)
|
| 1863 |
if matches:
|
| 1864 |
for idx, (num, content) in enumerate(matches):
|
| 1865 |
+
# Normalize: ensure complete sentences
|
| 1866 |
+
text = _ensure_sentence_complete(content)
|
| 1867 |
if len(text) > 30:
|
| 1868 |
img = source_images[idx] if idx < len(source_images) else ""
|
| 1869 |
slides.append({"text": text, "image": img, "index": idx + 1})
|
|
|
|
| 1877 |
p = p.strip()
|
| 1878 |
if p.startswith('#') or p.startswith('---') or p.startswith('*Nguồn'):
|
| 1879 |
continue
|
| 1880 |
+
# Normalize: ensure complete sentences
|
| 1881 |
+
p_normalized = _ensure_sentence_complete(p)
|
| 1882 |
+
if len(p_normalized) > 50:
|
| 1883 |
img = source_images[para_count] if para_count < len(source_images) else ""
|
| 1884 |
+
slides.append({"text": p_normalized, "image": img, "index": para_count + 1})
|
| 1885 |
para_count += 1
|
| 1886 |
if para_count >= 6:
|
| 1887 |
break
|
|
|
|
| 1889 |
if len(slides) < 2:
|
| 1890 |
slides = [{"text": f"Quan điểm: {opinion[:300]}", "image": source_images[0] if source_images else "", "index": 1}]
|
| 1891 |
if ai_text:
|
| 1892 |
+
# Split by sentences for better chunks
|
| 1893 |
+
sentences = re.split(r'(?<=[.!?])\s+', ai_text)
|
| 1894 |
+
current_chunk = ""
|
| 1895 |
+
for sent in sentences:
|
| 1896 |
+
sent = _ensure_sentence_complete(sent)
|
| 1897 |
+
if len(sent) > 10:
|
| 1898 |
+
if current_chunk and len(current_chunk + sent) < 400:
|
| 1899 |
+
current_chunk += " " + sent
|
| 1900 |
+
elif len(current_chunk) > 50:
|
| 1901 |
+
idx = len(slides)
|
| 1902 |
+
img = source_images[idx] if idx < len(source_images) else ""
|
| 1903 |
+
slides.append({"text": current_chunk, "image": img, "index": idx + 1})
|
| 1904 |
+
current_chunk = sent
|
| 1905 |
+
else:
|
| 1906 |
+
current_chunk = sent
|
| 1907 |
+
if len(current_chunk) > 50 and len(slides) < 6:
|
| 1908 |
+
idx = len(slides)
|
| 1909 |
+
img = source_images[idx] if idx < len(source_images) else ""
|
| 1910 |
+
slides.append({"text": current_chunk, "image": img, "index": idx + 1})
|
| 1911 |
|
| 1912 |
lang, emotion = detect_language_and_emotion(title, ai_text)
|
| 1913 |
voice = get_voice_for_content(title, ai_text)
|