Spaces:
Running
Running
Upgrade personal opinion post v2: AI tổng hợp bài viết từ nguồn tin + hình ảnh
Browse files- app_v2_entry.py +235 -118
app_v2_entry.py
CHANGED
|
@@ -1463,24 +1463,24 @@ async def api_url_wall(request: Request):
|
|
| 1463 |
return await api_rewrite_share(request)
|
| 1464 |
|
| 1465 |
|
| 1466 |
-
# ===== PERSONAL OPINION POST:
|
| 1467 |
-
@app.post("/api/personal_post")
|
| 1468 |
-
async def
|
| 1469 |
-
"""
|
| 1470 |
body = await request.json()
|
| 1471 |
opinion = _clean(body.get("opinion", ""))
|
| 1472 |
selected_topics = body.get("selected_topics", []) or []
|
| 1473 |
selected_sources = body.get("selected_sources", []) or []
|
| 1474 |
-
custom_title = body.get("custom_title", "")
|
| 1475 |
-
custom_slides = body.get("custom_slides", [])
|
| 1476 |
|
| 1477 |
if not opinion or len(opinion) < 10:
|
| 1478 |
return JSONResponse({"error": "Quan điểm cá nhân quá ngắn (cần ít nhất 10 ký tự)"}, status_code=400)
|
| 1479 |
|
|
|
|
| 1480 |
if not selected_topics:
|
| 1481 |
hot = _get_hot_topics()
|
| 1482 |
selected_topics = [t.get("topic", "") for t in hot[:3] if t.get("topic")]
|
| 1483 |
|
|
|
|
| 1484 |
all_sources = []
|
| 1485 |
seen_urls = set()
|
| 1486 |
for topic in selected_topics[:3]:
|
|
@@ -1489,120 +1489,153 @@ async def api_personal_post(request: Request):
|
|
| 1489 |
if s.get("url") and s["url"] not in seen_urls:
|
| 1490 |
seen_urls.add(s["url"])
|
| 1491 |
all_sources.append(s)
|
| 1492 |
-
if len(all_sources) >=
|
| 1493 |
break
|
| 1494 |
-
if len(all_sources) >=
|
| 1495 |
break
|
| 1496 |
|
| 1497 |
for src in selected_sources:
|
| 1498 |
if src.get("url") and src["url"] not in seen_urls:
|
| 1499 |
all_sources.insert(0, src)
|
| 1500 |
|
|
|
|
|
|
|
| 1501 |
source_images = []
|
| 1502 |
-
for src in all_sources[:
|
| 1503 |
url = src.get("url", "")
|
| 1504 |
-
if url:
|
| 1505 |
-
|
| 1506 |
-
|
| 1507 |
-
|
| 1508 |
-
|
| 1509 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1510 |
img = "/api/proxy/img?url=" + _quote2(img, safe="")
|
| 1511 |
source_images.append(img)
|
| 1512 |
-
|
| 1513 |
-
|
|
|
|
|
|
|
| 1514 |
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
else:
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
title_words = opinion_words[:8] if len(opinion_words) >= 8 else opinion_words[:4]
|
| 1521 |
-
if title_words:
|
| 1522 |
-
title = " ".join([w[0].upper() + w[1:] for w in title_words])
|
| 1523 |
-
else:
|
| 1524 |
-
title = "Quan điểm cá nhân"
|
| 1525 |
-
title = title[:80]
|
| 1526 |
|
|
|
|
| 1527 |
ai_text = None
|
| 1528 |
try:
|
| 1529 |
import ai_ext
|
| 1530 |
if hasattr(ai_ext, 'qwen_generate'):
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1534 |
except:
|
| 1535 |
pass
|
| 1536 |
|
| 1537 |
if not ai_text or len(ai_text) < 100:
|
| 1538 |
-
|
| 1539 |
-
|
| 1540 |
-
|
| 1541 |
-
|
| 1542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1543 |
|
| 1544 |
-
|
| 1545 |
-
|
| 1546 |
-
|
| 1547 |
-
|
| 1548 |
-
"text": slide.get("text", ""),
|
| 1549 |
-
"image": slide.get("image", ""),
|
| 1550 |
-
"index": i + 1
|
| 1551 |
-
})
|
| 1552 |
-
else:
|
| 1553 |
-
slides = []
|
| 1554 |
slides.append({
|
| 1555 |
-
"text":
|
| 1556 |
-
"image":
|
| 1557 |
-
"index": 1
|
| 1558 |
})
|
| 1559 |
-
for i, src in enumerate(all_sources[:5]):
|
| 1560 |
-
slide_text = src.get("title", "")[:200]
|
| 1561 |
-
if src.get("via"):
|
| 1562 |
-
slide_text += "\n\nNguồn: " + src.get('via')
|
| 1563 |
-
slides.append({
|
| 1564 |
-
"text": slide_text,
|
| 1565 |
-
"image": source_images[i+1] if i+1 < len(source_images) else "",
|
| 1566 |
-
"index": i + 2
|
| 1567 |
-
})
|
| 1568 |
-
|
| 1569 |
-
lang, emotion = detect_language_and_emotion(title, ai_text)
|
| 1570 |
-
voice = get_voice_for_content(title, ai_text)
|
| 1571 |
|
| 1572 |
-
|
| 1573 |
-
"id": str(int(time.time() * 1000)) + str(_random2.randint(100, 999)),
|
| 1574 |
"title": title,
|
| 1575 |
"text": ai_text,
|
| 1576 |
-
"
|
| 1577 |
-
"url": "",
|
| 1578 |
-
"kind": "personal_opinion",
|
| 1579 |
-
"slides": slides,
|
| 1580 |
"images": source_images[:10],
|
| 1581 |
-
"
|
| 1582 |
-
"
|
| 1583 |
-
"emotion": emotion,
|
| 1584 |
-
"language": lang,
|
| 1585 |
-
"ts": int(time.time()),
|
| 1586 |
-
"sources": all_sources[:5]
|
| 1587 |
}
|
| 1588 |
|
| 1589 |
-
|
| 1590 |
-
posts.insert(0, post)
|
| 1591 |
-
_save_wall_posts(posts)
|
| 1592 |
-
|
| 1593 |
-
return JSONResponse({"post": post, "slides": slides})
|
| 1594 |
|
| 1595 |
|
| 1596 |
-
@app.post("/api/personal_post
|
| 1597 |
-
async def
|
| 1598 |
-
"""
|
| 1599 |
body = await request.json()
|
| 1600 |
opinion = _clean(body.get("opinion", ""))
|
| 1601 |
selected_topics = body.get("selected_topics", []) or []
|
| 1602 |
selected_sources = body.get("selected_sources", []) or []
|
|
|
|
|
|
|
| 1603 |
|
| 1604 |
if not opinion or len(opinion) < 10:
|
| 1605 |
-
return JSONResponse({"error": "Quan điểm cá nhân quá ngắn"}, status_code=400)
|
| 1606 |
|
| 1607 |
if not selected_topics:
|
| 1608 |
hot = _get_hot_topics()
|
|
@@ -1616,68 +1649,152 @@ async def api_personal_post_preview(request: Request):
|
|
| 1616 |
if s.get("url") and s["url"] not in seen_urls:
|
| 1617 |
seen_urls.add(s["url"])
|
| 1618 |
all_sources.append(s)
|
| 1619 |
-
if len(all_sources) >=
|
| 1620 |
break
|
| 1621 |
-
if len(all_sources) >=
|
| 1622 |
break
|
| 1623 |
|
| 1624 |
for src in selected_sources:
|
| 1625 |
if src.get("url") and src["url"] not in seen_urls:
|
| 1626 |
all_sources.insert(0, src)
|
| 1627 |
|
|
|
|
| 1628 |
source_images = []
|
| 1629 |
-
for src in all_sources[:
|
| 1630 |
url = src.get("url", "")
|
| 1631 |
-
if url:
|
| 1632 |
-
|
| 1633 |
-
|
| 1634 |
-
|
| 1635 |
-
|
| 1636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1637 |
img = "/api/proxy/img?url=" + _quote2(img, safe="")
|
| 1638 |
source_images.append(img)
|
| 1639 |
-
|
| 1640 |
-
|
| 1641 |
|
| 1642 |
-
|
| 1643 |
-
|
| 1644 |
-
|
| 1645 |
-
|
| 1646 |
-
|
| 1647 |
-
|
| 1648 |
-
|
| 1649 |
-
|
| 1650 |
|
|
|
|
| 1651 |
ai_text = None
|
| 1652 |
try:
|
| 1653 |
import ai_ext
|
| 1654 |
-
if hasattr(ai_ext,
|
| 1655 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1656 |
except:
|
| 1657 |
pass
|
| 1658 |
|
| 1659 |
-
if not ai_text or len(ai_text) <
|
| 1660 |
-
ai_text = opinion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1661 |
|
| 1662 |
-
|
|
|
|
| 1663 |
"title": title,
|
| 1664 |
"text": ai_text,
|
| 1665 |
-
"
|
| 1666 |
-
"
|
| 1667 |
-
"
|
| 1668 |
-
"slides":
|
| 1669 |
-
|
| 1670 |
-
|
| 1671 |
-
|
| 1672 |
-
|
| 1673 |
-
|
| 1674 |
-
|
|
|
|
| 1675 |
}
|
| 1676 |
|
| 1677 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1678 |
|
| 1679 |
|
| 1680 |
-
# ===== END PERSONAL OPINION POST =====
|
| 1681 |
|
| 1682 |
def _bg():
|
| 1683 |
time.sleep(15)
|
|
|
|
| 1463 |
return await api_rewrite_share(request)
|
| 1464 |
|
| 1465 |
|
| 1466 |
+
# ===== PERSONAL OPINION POST v2: AI tổng hợp bài viết từ quan điểm + nguồn tin HOT =====
|
| 1467 |
+
@app.post("/api/personal_post/preview")
|
| 1468 |
+
async def api_personal_post_preview(request: Request):
|
| 1469 |
+
"""Preview personal post: fetch full articles, let AI compose logical article with images."""
|
| 1470 |
body = await request.json()
|
| 1471 |
opinion = _clean(body.get("opinion", ""))
|
| 1472 |
selected_topics = body.get("selected_topics", []) or []
|
| 1473 |
selected_sources = body.get("selected_sources", []) or []
|
|
|
|
|
|
|
| 1474 |
|
| 1475 |
if not opinion or len(opinion) < 10:
|
| 1476 |
return JSONResponse({"error": "Quan điểm cá nhân quá ngắn (cần ít nhất 10 ký tự)"}, status_code=400)
|
| 1477 |
|
| 1478 |
+
# Lấy HOT topics nếu ko có selected
|
| 1479 |
if not selected_topics:
|
| 1480 |
hot = _get_hot_topics()
|
| 1481 |
selected_topics = [t.get("topic", "") for t in hot[:3] if t.get("topic")]
|
| 1482 |
|
| 1483 |
+
# Tìm nguồn tin
|
| 1484 |
all_sources = []
|
| 1485 |
seen_urls = set()
|
| 1486 |
for topic in selected_topics[:3]:
|
|
|
|
| 1489 |
if s.get("url") and s["url"] not in seen_urls:
|
| 1490 |
seen_urls.add(s["url"])
|
| 1491 |
all_sources.append(s)
|
| 1492 |
+
if len(all_sources) >= 6:
|
| 1493 |
break
|
| 1494 |
+
if len(all_sources) >= 6:
|
| 1495 |
break
|
| 1496 |
|
| 1497 |
for src in selected_sources:
|
| 1498 |
if src.get("url") and src["url"] not in seen_urls:
|
| 1499 |
all_sources.insert(0, src)
|
| 1500 |
|
| 1501 |
+
# Scrape nội dung đầy đủ từng nguồn (paragraphs + images)
|
| 1502 |
+
source_details = []
|
| 1503 |
source_images = []
|
| 1504 |
+
for src in all_sources[:5]:
|
| 1505 |
url = src.get("url", "")
|
| 1506 |
+
if not url:
|
| 1507 |
+
continue
|
| 1508 |
+
try:
|
| 1509 |
+
art = _scrape_article_for_rewrite(url)
|
| 1510 |
+
if art:
|
| 1511 |
+
src_detail = {
|
| 1512 |
+
"title": art.get("title", src.get("title", "")),
|
| 1513 |
+
"url": url,
|
| 1514 |
+
"via": src.get("via", ""),
|
| 1515 |
+
"paragraphs": art.get("paragraphs", [])[:8],
|
| 1516 |
+
"images": art.get("images", [])[:3],
|
| 1517 |
+
"og_image": art.get("og_img", "")
|
| 1518 |
+
}
|
| 1519 |
+
source_details.append(src_detail)
|
| 1520 |
+
# Collect images for proxy
|
| 1521 |
+
for img in art.get("images", [])[:2]:
|
| 1522 |
+
if any(x in img for x in ["cdnphoto.dantri", "vnexpress", "vcdn", "refooty"]):
|
| 1523 |
img = "/api/proxy/img?url=" + _quote2(img, safe="")
|
| 1524 |
source_images.append(img)
|
| 1525 |
+
except:
|
| 1526 |
+
pass
|
| 1527 |
+
if len(source_details) >= 5:
|
| 1528 |
+
break
|
| 1529 |
|
| 1530 |
+
# Tạo title từ opinion
|
| 1531 |
+
opinion_words = re.findall(r"[A-Za-zÀ-ỹ0-9]+", opinion)
|
| 1532 |
+
title_words = opinion_words[:8] if len(opinion_words) >= 8 else opinion_words[:4]
|
| 1533 |
+
title = " ".join([w[0].upper() + w[1:] for w in title_words]) if title_words else "Quan điểm cá nhân"
|
| 1534 |
+
title = title[:80]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1535 |
|
| 1536 |
+
# AI sinh bài viết hoàn chỉnh
|
| 1537 |
ai_text = None
|
| 1538 |
try:
|
| 1539 |
import ai_ext
|
| 1540 |
if hasattr(ai_ext, 'qwen_generate'):
|
| 1541 |
+
# Build detailed context from source articles
|
| 1542 |
+
source_context = ""
|
| 1543 |
+
for i, sd in enumerate(source_details[:5]):
|
| 1544 |
+
src_title = sd.get("title", "")
|
| 1545 |
+
src_via = sd.get("via", "")
|
| 1546 |
+
src_paras = sd.get("paragraphs", [])
|
| 1547 |
+
source_context += f"\n=== Nguồn {i+1}: {src_title} ({src_via}) ===\n"
|
| 1548 |
+
for j, p in enumerate(src_paras[:4]):
|
| 1549 |
+
source_context += f" - {p[:300]}\n"
|
| 1550 |
+
|
| 1551 |
+
prompt = (
|
| 1552 |
+
"Bạn là nhà báo chuyên nghiệp tiếng Việt. Hãy viết một bài phân tích dựa trên quan điểm cá nhân và các nguồn tin sau đây.\n\n"
|
| 1553 |
+
"=== QUAN ĐIỂM CÁ NHÂN ===\n" + opinion[:2000] + "\n\n"
|
| 1554 |
+
"=== NGUỒN TIN THAM KHẢO (kèm nội dung chi tiết) ===\n" + source_context + "\n\n"
|
| 1555 |
+
"=== YÊU CẦU BÀI VIẾT ===\n"
|
| 1556 |
+
"1. Mở đầu: Giới thiệu chủ đề và nêu quan điểm cá nhân (1-2 câu)\n"
|
| 1557 |
+
"2. Thân bài: Phân tích luận điểm, dùng dẫn chứng CỤ THỂ từ nguồn tin (trích dẫn nguồn kèm tên báo)\n"
|
| 1558 |
+
"3. Mỗi luận điểm là 1 đoạn ngắn 2-4 câu, có ghi nguồn rõ ràng (VD: Theo VnExpress, ...)\n"
|
| 1559 |
+
"4. Kết luận: Tổng kết quan điểm, gợi mở suy nghĩ\n"
|
| 1560 |
+
"5. Cuối bài: Ghi danh sách nguồn tham khảo\n\n"
|
| 1561 |
+
"Viết tự nhiên, mạch lạc, giọng văn báo chí - phân tích. Độ dài: 300-600 từ."
|
| 1562 |
+
)
|
| 1563 |
+
ai_text = await ai_ext.qwen_generate(prompt, max_tokens=2000)
|
| 1564 |
except:
|
| 1565 |
pass
|
| 1566 |
|
| 1567 |
if not ai_text or len(ai_text) < 100:
|
| 1568 |
+
# Fallback: build article manually
|
| 1569 |
+
ai_text = "## " + title + "\n\n"
|
| 1570 |
+
ai_text += opinion + "\n\n"
|
| 1571 |
+
for i, sd in enumerate(source_details[:5]):
|
| 1572 |
+
ai_text += "### " + sd.get("title", f"Nguồn {i+1}") + "\n"
|
| 1573 |
+
for p in sd.get("paragraphs", [])[:3]:
|
| 1574 |
+
ai_text += p[:250] + "\n"
|
| 1575 |
+
ai_text += "*Nguồn: " + sd.get("via", "") + "*\n\n"
|
| 1576 |
+
ai_text += "\n---\n*Bài viết tổng hợp từ quan điểm cá nhân và các nguồn tin liên quan*"
|
| 1577 |
+
|
| 1578 |
+
# Tạo slides từ bài viết
|
| 1579 |
+
slide_parts = []
|
| 1580 |
+
if ai_text:
|
| 1581 |
+
paragraphs = [p.strip() for p in ai_text.split("\n") if p.strip() and len(p.strip()) > 40]
|
| 1582 |
+
current_para = ""
|
| 1583 |
+
para_count = 0
|
| 1584 |
+
for p in paragraphs:
|
| 1585 |
+
if p.startswith("## ") or p.startswith("### ") or p.startswith("---"):
|
| 1586 |
+
if current_para and para_count < 6:
|
| 1587 |
+
slide_parts.append(current_para)
|
| 1588 |
+
para_count += 1
|
| 1589 |
+
current_para = ""
|
| 1590 |
+
elif not p.startswith("*") and not p.startswith("- "):
|
| 1591 |
+
if len(p) > 80:
|
| 1592 |
+
if current_para:
|
| 1593 |
+
current_para += "\n\n" + p
|
| 1594 |
+
else:
|
| 1595 |
+
current_para = p
|
| 1596 |
+
if current_para and para_count < 6:
|
| 1597 |
+
slide_parts.append(current_para)
|
| 1598 |
+
|
| 1599 |
+
if len(slide_parts) < 2:
|
| 1600 |
+
# Create slides from opinion + sources
|
| 1601 |
+
slide_parts = [opinion[:300]]
|
| 1602 |
+
for sd in source_details[:4]:
|
| 1603 |
+
slide_parts.append(sd.get("title", "")[:200] + "\n\n" + (sd.get("paragraphs", [""])[0][:200] if sd.get("paragraphs") else ""))
|
| 1604 |
|
| 1605 |
+
slides = []
|
| 1606 |
+
total = min(len(slide_parts), 6)
|
| 1607 |
+
for i in range(total):
|
| 1608 |
+
img = source_images[i] if i < len(source_images) else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1609 |
slides.append({
|
| 1610 |
+
"text": slide_parts[i],
|
| 1611 |
+
"image": img,
|
| 1612 |
+
"index": i + 1
|
| 1613 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1614 |
|
| 1615 |
+
preview = {
|
|
|
|
| 1616 |
"title": title,
|
| 1617 |
"text": ai_text,
|
| 1618 |
+
"opinion": opinion,
|
|
|
|
|
|
|
|
|
|
| 1619 |
"images": source_images[:10],
|
| 1620 |
+
"sources": source_details[:5],
|
| 1621 |
+
"slides": slides
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1622 |
}
|
| 1623 |
|
| 1624 |
+
return JSONResponse({"preview": preview})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1625 |
|
| 1626 |
|
| 1627 |
+
@app.post("/api/personal_post")
|
| 1628 |
+
async def api_personal_post(request: Request):
|
| 1629 |
+
"""Create and save personal opinion post."""
|
| 1630 |
body = await request.json()
|
| 1631 |
opinion = _clean(body.get("opinion", ""))
|
| 1632 |
selected_topics = body.get("selected_topics", []) or []
|
| 1633 |
selected_sources = body.get("selected_sources", []) or []
|
| 1634 |
+
custom_title = body.get("custom_title", "")
|
| 1635 |
+
custom_slides = body.get("custom_slides", [])
|
| 1636 |
|
| 1637 |
if not opinion or len(opinion) < 10:
|
| 1638 |
+
return JSONResponse({"error": "Quan điểm cá nhân quá ngắn (cần ít nhất 10 ký tự)"}, status_code=400)
|
| 1639 |
|
| 1640 |
if not selected_topics:
|
| 1641 |
hot = _get_hot_topics()
|
|
|
|
| 1649 |
if s.get("url") and s["url"] not in seen_urls:
|
| 1650 |
seen_urls.add(s["url"])
|
| 1651 |
all_sources.append(s)
|
| 1652 |
+
if len(all_sources) >= 6:
|
| 1653 |
break
|
| 1654 |
+
if len(all_sources) >= 6:
|
| 1655 |
break
|
| 1656 |
|
| 1657 |
for src in selected_sources:
|
| 1658 |
if src.get("url") and src["url"] not in seen_urls:
|
| 1659 |
all_sources.insert(0, src)
|
| 1660 |
|
| 1661 |
+
source_details = []
|
| 1662 |
source_images = []
|
| 1663 |
+
for src in all_sources[:5]:
|
| 1664 |
url = src.get("url", "")
|
| 1665 |
+
if not url:
|
| 1666 |
+
continue
|
| 1667 |
+
try:
|
| 1668 |
+
art = _scrape_article_for_rewrite(url)
|
| 1669 |
+
if art:
|
| 1670 |
+
src_detail = {
|
| 1671 |
+
"title": art.get("title", src.get("title", "")),
|
| 1672 |
+
"url": url,
|
| 1673 |
+
"via": src.get("via", ""),
|
| 1674 |
+
"paragraphs": art.get("paragraphs", [])[:6],
|
| 1675 |
+
"images": art.get("images", [])[:2],
|
| 1676 |
+
"og_image": art.get("og_img", "")
|
| 1677 |
+
}
|
| 1678 |
+
source_details.append(src_detail)
|
| 1679 |
+
for img in art.get("images", [])[:2]:
|
| 1680 |
+
if any(x in img for x in ["cdnphoto.dantri", "vnexpress", "vcdn", "refooty"]):
|
| 1681 |
img = "/api/proxy/img?url=" + _quote2(img, safe="")
|
| 1682 |
source_images.append(img)
|
| 1683 |
+
except:
|
| 1684 |
+
pass
|
| 1685 |
|
| 1686 |
+
# Title
|
| 1687 |
+
if custom_title:
|
| 1688 |
+
title = custom_title[:80]
|
| 1689 |
+
else:
|
| 1690 |
+
opinion_words = re.findall(r"[A-Za-zÀ-ỹ0-9]+", opinion)
|
| 1691 |
+
title_words = opinion_words[:8] if len(opinion_words) >= 8 else opinion_words[:4]
|
| 1692 |
+
title = " ".join([w[0].upper() + w[1:] for w in title_words]) if title_words else "Quan điểm cá nhân"
|
| 1693 |
+
title = title[:80]
|
| 1694 |
|
| 1695 |
+
# AI sinh bài
|
| 1696 |
ai_text = None
|
| 1697 |
try:
|
| 1698 |
import ai_ext
|
| 1699 |
+
if hasattr(ai_ext, 'qwen_generate'):
|
| 1700 |
+
source_context = ""
|
| 1701 |
+
for i, sd in enumerate(source_details[:5]):
|
| 1702 |
+
src_title = sd.get("title", "")
|
| 1703 |
+
src_via = sd.get("via", "")
|
| 1704 |
+
src_paras = sd.get("paragraphs", [])
|
| 1705 |
+
source_context += f"\nNguồn {i+1}: {src_title} ({src_via})\n"
|
| 1706 |
+
for j, p in enumerate(src_paras[:3]):
|
| 1707 |
+
source_context += f" - {p[:300]}\n"
|
| 1708 |
+
prompt = (
|
| 1709 |
+
"Bạn là nhà báo tiếng Việt. 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"
|
| 1710 |
+
"=== QUAN ĐIỂM ===\n" + opinion[:2000] + "\n\n"
|
| 1711 |
+
"=== NGUỒN TIN ===\n" + source_context + "\n\n"
|
| 1712 |
+
"Yêu cầu:\n- Mở đầu: giới thiệu chủ đề + nêu quan điểm\n- Thân bài: phân tích có dẫn chứng từ nguồn (ghi rõ nguồn)\n- Mỗi đoạn 2-4 câu\n- Kết luận: tổng kết\n- Cuối: danh sách nguồn\n\nViết tự nhiên, 300-600 từ."
|
| 1713 |
+
)
|
| 1714 |
+
ai_text = await ai_ext.qwen_generate(prompt, max_tokens=2000)
|
| 1715 |
except:
|
| 1716 |
pass
|
| 1717 |
|
| 1718 |
+
if not ai_text or len(ai_text) < 100:
|
| 1719 |
+
ai_text = "## " + title + "\n\n" + opinion + "\n\n"
|
| 1720 |
+
for i, sd in enumerate(source_details[:5]):
|
| 1721 |
+
ai_text += "### " + sd.get("title", "") + "\n"
|
| 1722 |
+
for p in sd.get("paragraphs", [])[:2]:
|
| 1723 |
+
ai_text += p[:250] + "\n"
|
| 1724 |
+
ai_text += "\n---\n*Nguồn: " + sd.get("via", "") + "*\n\n"
|
| 1725 |
+
|
| 1726 |
+
# Tạo slides
|
| 1727 |
+
if custom_slides and len(custom_slides) > 0:
|
| 1728 |
+
slides = []
|
| 1729 |
+
for i, slide in enumerate(custom_slides):
|
| 1730 |
+
slides.append({
|
| 1731 |
+
"text": slide.get("text", ""),
|
| 1732 |
+
"image": slide.get("image", ""),
|
| 1733 |
+
"index": i + 1
|
| 1734 |
+
})
|
| 1735 |
+
else:
|
| 1736 |
+
slide_parts = []
|
| 1737 |
+
if ai_text:
|
| 1738 |
+
paragraphs = [p.strip() for p in ai_text.split("\n") if p.strip() and len(p.strip()) > 40]
|
| 1739 |
+
current = ""
|
| 1740 |
+
count = 0
|
| 1741 |
+
for p in paragraphs:
|
| 1742 |
+
if p.startswith("## ") or p.startswith("### ") or p.startswith("---"):
|
| 1743 |
+
if current and count < 6:
|
| 1744 |
+
slide_parts.append(current)
|
| 1745 |
+
count += 1
|
| 1746 |
+
current = ""
|
| 1747 |
+
elif len(p) > 80:
|
| 1748 |
+
if current:
|
| 1749 |
+
current += "\n\n" + p
|
| 1750 |
+
else:
|
| 1751 |
+
current = p
|
| 1752 |
+
if current and count < 6:
|
| 1753 |
+
slide_parts.append(current)
|
| 1754 |
+
|
| 1755 |
+
if len(slide_parts) < 2:
|
| 1756 |
+
slide_parts = [opinion[:300]]
|
| 1757 |
+
for sd in source_details[:5]:
|
| 1758 |
+
slide_parts.append(sd.get("title", "")[:200] + "\n" + (sd.get("paragraphs", [""])[0][:200] if sd.get("paragraphs") else ""))
|
| 1759 |
+
|
| 1760 |
+
slides = []
|
| 1761 |
+
total = min(len(slide_parts), 6)
|
| 1762 |
+
for i in range(total):
|
| 1763 |
+
img = source_images[i] if i < len(source_images) else ""
|
| 1764 |
+
slides.append({
|
| 1765 |
+
"text": slide_parts[i],
|
| 1766 |
+
"image": img,
|
| 1767 |
+
"index": i + 1
|
| 1768 |
+
})
|
| 1769 |
+
|
| 1770 |
+
lang, emotion = detect_language_and_emotion(title, ai_text)
|
| 1771 |
+
voice = get_voice_for_content(title, ai_text)
|
| 1772 |
|
| 1773 |
+
post = {
|
| 1774 |
+
"id": str(int(time.time() * 1000)) + str(_random2.randint(100, 999)),
|
| 1775 |
"title": title,
|
| 1776 |
"text": ai_text,
|
| 1777 |
+
"img": source_images[0] if source_images else "",
|
| 1778 |
+
"url": "",
|
| 1779 |
+
"kind": "personal_opinion",
|
| 1780 |
+
"slides": slides,
|
| 1781 |
+
"images": source_images[:10],
|
| 1782 |
+
"video": "",
|
| 1783 |
+
"voice": voice,
|
| 1784 |
+
"emotion": emotion,
|
| 1785 |
+
"language": lang,
|
| 1786 |
+
"ts": int(time.time()),
|
| 1787 |
+
"sources": source_details[:5]
|
| 1788 |
}
|
| 1789 |
|
| 1790 |
+
posts = _load_wall_posts()
|
| 1791 |
+
posts.insert(0, post)
|
| 1792 |
+
_save_wall_posts(posts)
|
| 1793 |
+
|
| 1794 |
+
return JSONResponse({"post": post, "slides": slides})
|
| 1795 |
|
| 1796 |
|
| 1797 |
+
# ===== END PERSONAL OPINION POST v2 =====
|
| 1798 |
|
| 1799 |
def _bg():
|
| 1800 |
time.sleep(15)
|