bep40 commited on
Commit
f28ba6b
·
verified ·
1 Parent(s): 5c1b2f7

Upgrade personal opinion post v2: AI tổng hợp bài viết từ nguồn tin + hình ảnh

Browse files
Files changed (1) hide show
  1. 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: viết bài dựa trên quan điểm cá nhân + tin HOT =====
1467
- @app.post("/api/personal_post")
1468
- async def api_personal_post(request: Request):
1469
- """Create a post based on personal opinion with HOT news references."""
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) >= 8:
1493
  break
1494
- if len(all_sources) >= 8:
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[:6]:
1503
  url = src.get("url", "")
1504
- if url:
1505
- try:
1506
- art = _scrape_article_for_rewrite(url)
1507
- if art and art.get("og_image"):
1508
- img = art.get("og_image")
1509
- if any(x in img for x in ["cdnphoto.dantri", "refooty", "vnexpress", "vcdn"]):
 
 
 
 
 
 
 
 
 
 
 
1510
  img = "/api/proxy/img?url=" + _quote2(img, safe="")
1511
  source_images.append(img)
1512
- except:
1513
- pass
 
 
1514
 
1515
- if custom_title:
1516
- title = custom_title[:80]
1517
- else:
1518
- import re
1519
- opinion_words = re.findall(r"[A-Za-zÀ-ỹ0-9]+", opinion)
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
- sources_str = "\n".join([f"- {s.get('title', '')} ({s.get('via', '')})" for s in all_sources[:5]])
1532
- prompt = "Viết bài blog dựa trên quan điểm cá nhân:\n\nQuan điểm: " + opinion[:2000] + "\n\nNguồn tin tham khảo:\n" + sources_str + "\n\nYêu cầu:\n1. Mở đầu bằng ý kiến cá nhân\n2. Phân tích dựa trên các nguồn tin\n3. Kết luận rõ ràng\n4. Ghi nguồn tin cuối bài"
1533
- ai_text = await ai_ext.qwen_generate(prompt, max_tokens=1500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1534
  except:
1535
  pass
1536
 
1537
  if not ai_text or len(ai_text) < 100:
1538
- ai_text = "## Quan điểm nhân\n\n" + opinion + "\n\n"
1539
- if all_sources:
1540
- ai_text += "## Nguồn tham khảo\n\n"
1541
- for i, s in enumerate(all_sources[:5]):
1542
- ai_text += str(i+1) + ". " + s.get('title', '') + " (" + s.get('via', '') + ")\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1543
 
1544
- if custom_slides and len(custom_slides) > 0:
1545
- slides = []
1546
- for i, slide in enumerate(custom_slides):
1547
- slides.append({
1548
- "text": slide.get("text", ""),
1549
- "image": slide.get("image", ""),
1550
- "index": i + 1
1551
- })
1552
- else:
1553
- slides = []
1554
  slides.append({
1555
- "text": opinion[:250] if len(opinion) > 250 else opinion,
1556
- "image": source_images[0] if source_images else "",
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
- post = {
1573
- "id": str(int(time.time() * 1000)) + str(_random2.randint(100, 999)),
1574
  "title": title,
1575
  "text": ai_text,
1576
- "img": source_images[0] if source_images else "",
1577
- "url": "",
1578
- "kind": "personal_opinion",
1579
- "slides": slides,
1580
  "images": source_images[:10],
1581
- "video": "",
1582
- "voice": voice,
1583
- "emotion": emotion,
1584
- "language": lang,
1585
- "ts": int(time.time()),
1586
- "sources": all_sources[:5]
1587
  }
1588
 
1589
- posts = _load_wall_posts()
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/preview")
1597
- async def api_personal_post_preview(request: Request):
1598
- """Preview personal post before publishing."""
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) >= 8:
1620
  break
1621
- if len(all_sources) >= 8:
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[:6]:
1630
  url = src.get("url", "")
1631
- if url:
1632
- try:
1633
- art = _scrape_article_for_rewrite(url)
1634
- if art and art.get("og_image"):
1635
- img = art.get("og_image")
1636
- if any(x in img for x in ["cdnphoto.dantri", "refooty", "vnexpress", "vcdn"]):
 
 
 
 
 
 
 
 
 
 
1637
  img = "/api/proxy/img?url=" + _quote2(img, safe="")
1638
  source_images.append(img)
1639
- except:
1640
- pass
1641
 
1642
- import re
1643
- opinion_words = re.findall(r"[A-Za-zÀ-ỹ0-9]+", opinion)
1644
- title_words = opinion_words[:8] if len(opinion_words) >= 8 else opinion_words[:4]
1645
- title = " ".join([w[0].upper() + w[1:] for w in title_words]) if title_words else "Quan điểm cá nhân"
1646
- title = title[:80]
1647
-
1648
- sources_str = "\n".join([f"- {s.get('title', '')}" for s in all_sources[:5]])
1649
- prompt = "Tóm tắt quan điểm + tham khảo nguồn tin:\n\nQuan điểm: " + opinion[:2000] + "\n\nNguồn: " + sources_str + "\n\nTạo 4-6 ý slide ngắn gọn."
1650
 
 
1651
  ai_text = None
1652
  try:
1653
  import ai_ext
1654
- if hasattr(ai_ext, "qwen_generate"):
1655
- ai_text = await ai_ext.qwen_generate(prompt, max_tokens=800)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1656
  except:
1657
  pass
1658
 
1659
- if not ai_text or len(ai_text) < 50:
1660
- ai_text = opinion[:300]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1661
 
1662
- preview = {
 
1663
  "title": title,
1664
  "text": ai_text,
1665
- "opinion": opinion,
1666
- "images": source_images,
1667
- "sources": all_sources[:5],
1668
- "slides": [
1669
- {"text": opinion[:250], "image": source_images[0] if source_images else ""},
1670
- *[
1671
- {"text": s.get("title", "")[:200], "image": source_images[i+1] if i+1 < len(source_images) else ""}
1672
- for i, s in enumerate(all_sources[:4])
1673
- ]
1674
- ]
 
1675
  }
1676
 
1677
- return JSONResponse({"preview": preview})
 
 
 
 
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)