Spaces:
Running
Running
Upload app_v2_entry.py
Browse files- app_v2_entry.py +193 -9
app_v2_entry.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""VNEWS v2 Entry Point - with fast bongda proxy + rewrite endpoints"""
|
| 2 |
import sys, os
|
| 3 |
from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api, HL_LEAGUES
|
| 4 |
|
|
@@ -506,7 +506,7 @@ def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access
|
|
| 506 |
@app.get('/s')
|
| 507 |
async def _sh(url:str='',title:str='',img:str=''):return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body></body></html>')
|
| 508 |
|
| 509 |
-
from wc2026_scraper import
|
| 510 |
|
| 511 |
_xlb_cache = {}
|
| 512 |
_xlb_lock = threading.Lock()
|
|
@@ -771,12 +771,170 @@ def api_wall_delete(post_id: str):
|
|
| 771 |
return JSONResponse({"ok": True})
|
| 772 |
return JSONResponse({"error": "Post not found"}, status_code=404)
|
| 773 |
|
| 774 |
-
# =====
|
| 775 |
import random as _random2
|
| 776 |
from urllib.parse import quote as _quote2
|
| 777 |
|
| 778 |
_UA_RW = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Language': 'vi-VN,vi;q=0.9'}
|
| 779 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
def _scrape_article_for_rewrite(url):
|
| 781 |
"""Scrape article: extract title, paragraphs, images, OG image."""
|
| 782 |
try:
|
|
@@ -872,6 +1030,11 @@ async def api_rewrite_slide(request: Request):
|
|
| 872 |
img = '/api/proxy/img?url=' + _quote2(img, safe='')
|
| 873 |
slides.append({'text': point, 'image': img, 'index': i + 1})
|
| 874 |
summary_text = '\n\n'.join([f"• {s['text']}" for s in slides])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
post = {
|
| 876 |
"id": str(int(time.time() * 1000)) + str(_random2.randint(100, 999)),
|
| 877 |
"title": data['title'],
|
|
@@ -882,8 +1045,9 @@ async def api_rewrite_slide(request: Request):
|
|
| 882 |
"slides": slides,
|
| 883 |
"images": images[:10],
|
| 884 |
"video": "",
|
| 885 |
-
"voice":
|
| 886 |
-
"emotion":
|
|
|
|
| 887 |
"ts": int(time.time())
|
| 888 |
}
|
| 889 |
posts = _load_wall_posts()
|
|
@@ -894,7 +1058,7 @@ async def api_rewrite_slide(request: Request):
|
|
| 894 |
|
| 895 |
@app.post("/api/rewrite_share")
|
| 896 |
async def api_rewrite_share(request: Request):
|
| 897 |
-
"""Rewrite article and post to Tường AI
|
| 898 |
body = await request.json()
|
| 899 |
url = _clean(body.get("url", ""))
|
| 900 |
ctx = _clean(body.get("context", ""))
|
|
@@ -919,6 +1083,8 @@ async def api_rewrite_share(request: Request):
|
|
| 919 |
domain = urlparse(url).netloc.replace('www.', '')
|
| 920 |
except:
|
| 921 |
pass
|
|
|
|
|
|
|
| 922 |
ai_text = None
|
| 923 |
try:
|
| 924 |
import ai_ext
|
|
@@ -933,7 +1099,21 @@ async def api_rewrite_share(request: Request):
|
|
| 933 |
ai_text = '\n\n'.join([f"• {p}" for p in key_pts])
|
| 934 |
else:
|
| 935 |
ai_text = f"Tóm tắt: {data['title']}\n\n{raw_text[:1200]}\n\nNguồn: {domain}"
|
|
|
|
|
|
|
|
|
|
| 936 |
images = data.get('images', [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
post = {
|
| 938 |
"id": str(int(time.time() * 1000)) + str(_random2.randint(100, 999)),
|
| 939 |
"title": data['title'],
|
|
@@ -941,16 +1121,18 @@ async def api_rewrite_share(request: Request):
|
|
| 941 |
"img": images[0] if images else '',
|
| 942 |
"url": url,
|
| 943 |
"kind": "rewrite",
|
|
|
|
| 944 |
"images": images[:10],
|
| 945 |
"video": "",
|
| 946 |
-
"voice":
|
| 947 |
-
"emotion":
|
|
|
|
| 948 |
"ts": int(time.time())
|
| 949 |
}
|
| 950 |
posts = _load_wall_posts()
|
| 951 |
posts.insert(0, post)
|
| 952 |
_save_wall_posts(posts)
|
| 953 |
-
return JSONResponse({"post": post})
|
| 954 |
|
| 955 |
|
| 956 |
@app.post("/api/url_wall")
|
|
@@ -960,6 +1142,8 @@ async def api_url_wall(request: Request):
|
|
| 960 |
url = _clean(body.get("url", ""))
|
| 961 |
if not url or not url.startswith('http'):
|
| 962 |
return JSONResponse({"error": "URL không hợp lệ"}, status_code=400)
|
|
|
|
|
|
|
| 963 |
return await api_rewrite_share(request)
|
| 964 |
|
| 965 |
|
|
|
|
| 1 |
+
"""VNEWS v2 Entry Point - with fast bongda proxy + rewrite endpoints + multilingual TTS"""
|
| 2 |
import sys, os
|
| 3 |
from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api, HL_LEAGUES
|
| 4 |
|
|
|
|
| 506 |
@app.get('/s')
|
| 507 |
async def _sh(url:str='',title:str='',img:str=''):return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body></body></html>')
|
| 508 |
|
| 509 |
+
from wc2026_scraper import scrape_summary,scrape_fixtures,scrape_standings,scrape_stats,scrape_wc_news,scrape_road_to_wc,get_wc2026_all,scrape_history,scrape_h2h,scrape_lineups,scrape_match_detail
|
| 510 |
|
| 511 |
_xlb_cache = {}
|
| 512 |
_xlb_lock = threading.Lock()
|
|
|
|
| 771 |
return JSONResponse({"ok": True})
|
| 772 |
return JSONResponse({"error": "Post not found"}, status_code=404)
|
| 773 |
|
| 774 |
+
# ===== LANGUAGE & EMOTION DETECTION =====
|
| 775 |
import random as _random2
|
| 776 |
from urllib.parse import quote as _quote2
|
| 777 |
|
| 778 |
_UA_RW = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Language': 'vi-VN,vi;q=0.9'}
|
| 779 |
|
| 780 |
+
# Unique character markers for language detection
|
| 781 |
+
_UNIQUE_CHARS = {
|
| 782 |
+
'vietnamese': set('đăâêôơưàảãạáằẳẵặắầẩẫậấèẻẽẹéềễểệếìỉĩịíòỏõọóồổỗộốờởỡợớùủũụúừửữựứỳỷỹỵý'),
|
| 783 |
+
'spanish': set('ñáéíóúü¿¡'),
|
| 784 |
+
'portuguese': set('ãõçáéíóúâêôà'),
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
_STOPWORDS = {
|
| 788 |
+
'english': {'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but', 'in', 'with', 'to', 'for', 'of', 'not', 'no', 'can', 'had', 'have', 'has', 'was', 'were', 'are', 'be', 'been', 'this', 'that', 'it', 'he', 'she', 'they', 'his', 'her', 'my', 'your', 'our', 'we', 'you', 'i'},
|
| 789 |
+
'vietnamese': {'là', 'của', 'và', 'có', 'được', 'cho', 'không', 'với', 'này', 'đó', 'từ', 'trong', 'đã', 'sẽ', 'một', 'các', 'những', 'về', 'tại', 'người', 'năm', 'đến', 'ra', 'lại', 'như', 'khi', 'để', 'rất', 'cũng', 'mà', 'nếu', 'sau', 'trên', 'theo', 'vì', 'do', 'nên', 'thì', 'mình', 'tôi', 'bạn', 'anh', 'chị', 'em'},
|
| 790 |
+
'portuguese': {'de', 'um', 'que', 'e', 'do', 'da', 'em', 'para', 'com', 'não', 'uma', 'os', 'no', 'se', 'na', 'por', 'mais', 'as', 'dos', 'como', 'mas', 'ao', 'ele', 'das', 'tem', 'seu', 'sua', 'ou', 'quando', 'muito', 'nos', 'já', 'eu', 'também', 'só', 'pelo', 'pela', 'até', 'isso', 'ela', 'entre', 'depois', 'sem', 'mesmo', 'aos', 'são', 'está', 'ter', 'ser', 'foi', 'era', 'há', 'estão', 'você', 'nós', 'eles', 'elas'},
|
| 791 |
+
'spanish': {'de', 'que', 'el', 'en', 'y', 'a', 'los', 'del', 'se', 'las', 'por', 'un', 'para', 'con', 'no', 'una', 'su', 'al', 'es', 'lo', 'como', 'más', 'pero', 'sus', 'le', 'ya', 'o', 'fue', 'este', 'ha', 'si', 'porque', 'esta', 'son', 'entre', 'está', 'cuando', 'muy', 'sin', 'sobre', 'ser', 'también', 'me', 'hasta', 'hay', 'donde', 'han', 'quien', 'están', 'desde', 'todo', 'nos', 'durante', 'todos', 'uno', 'les', 'ni', 'contra', 'otros', 'fueron', 'ese', 'eso', 'ante', 'ellos', 'yo', 'tú', 'él', 'ella', 'nosotros', 'usted', 'ustedes'},
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
def detect_language(text):
|
| 795 |
+
"""Detect language from text content using stopword + character analysis."""
|
| 796 |
+
if not text:
|
| 797 |
+
return 'vietnamese'
|
| 798 |
+
text_lower = text.lower()
|
| 799 |
+
text_chars = set(text_lower)
|
| 800 |
+
|
| 801 |
+
# Strong signal: Vietnamese unique characters
|
| 802 |
+
vn_chars = len(text_chars & _UNIQUE_CHARS['vietnamese'])
|
| 803 |
+
if vn_chars >= 2:
|
| 804 |
+
return 'vietnamese'
|
| 805 |
+
|
| 806 |
+
# Spanish unique chars (ñ, ¿, ¡)
|
| 807 |
+
es_chars = len(text_chars & _UNIQUE_CHARS['spanish'])
|
| 808 |
+
pt_chars = len(text_chars & _UNIQUE_CHARS['portuguese'])
|
| 809 |
+
|
| 810 |
+
# Stopword scoring
|
| 811 |
+
words = set(re.findall(r'\b\w+\b', text_lower))
|
| 812 |
+
scores = {}
|
| 813 |
+
for lang, stops in _STOPWORDS.items():
|
| 814 |
+
scores[lang] = len(words & stops) / max(len(stops), 1)
|
| 815 |
+
|
| 816 |
+
# Disambiguate Portuguese vs Spanish
|
| 817 |
+
pt_markers = {'não', 'pelo', 'pela', 'isso', 'há', 'estão', 'num', 'numa', 'tenho', 'posso', 'você', 'nós', 'eles', 'elas', 'também', 'muito', 'já', 'só', 'até', 'entre', 'depois', 'sem', 'mesmo', 'aos', 'serão'}
|
| 818 |
+
es_markers = {'pero', 'está', 'están', 'porque', 'también', 'hasta', 'donde', 'quien', 'fue', 'son', 'fueron', 'ese', 'eso', 'ante', 'ellos', 'ella', 'nosotros', 'usted', 'ustedes', 'tú', 'él', 'desde', 'todo', 'durante', 'todos', 'uno', 'les', 'ni', 'contra', 'otros', 'fueron'}
|
| 819 |
+
|
| 820 |
+
pt_overlap = len(words & pt_markers)
|
| 821 |
+
es_overlap = len(words & es_markers)
|
| 822 |
+
|
| 823 |
+
if scores.get('portuguese', 0) > 0 and pt_overlap > es_overlap:
|
| 824 |
+
return 'portuguese'
|
| 825 |
+
if scores.get('spanish', 0) > 0 and es_overlap > pt_overlap:
|
| 826 |
+
return 'spanish'
|
| 827 |
+
if scores.get('english', 0) > 0.15:
|
| 828 |
+
return 'english'
|
| 829 |
+
|
| 830 |
+
best = max(scores, key=scores.get)
|
| 831 |
+
return best if scores[best] > 0.05 else 'vietnamese'
|
| 832 |
+
|
| 833 |
+
# Emotion keyword-based detection
|
| 834 |
+
_EMOTION_KEYWORDS = {
|
| 835 |
+
'happy': {
|
| 836 |
+
'en': ['happy', 'joy', 'wonderful', 'great', 'amazing', 'fantastic', 'love', 'excellent', 'beautiful', 'glad', 'delighted', 'pleased', 'cheerful', 'celebrate', 'victory', 'win', 'success'],
|
| 837 |
+
'pt': ['feliz', 'alegria', 'maravilhoso', 'ótimo', 'incrível', 'fantástico', 'amor', 'excelente', 'lindo', 'contente', 'encantado', 'vitória', 'sucesso'],
|
| 838 |
+
'es': ['feliz', 'alegria', 'maravilloso', 'genial', 'increíble', 'fantástico', 'amor', 'excelente', 'hermoso', 'contento', 'encantado', 'victoria', 'éxito'],
|
| 839 |
+
'vi': ['vui', 'hạnh phúc', 'tuyệt vời', 'tuyệt', 'ý nghĩa', 'đẹp', 'thích', 'yêu', 'vui vẻ', 'hân hoan', 'phấn khích', 'chiến thắng', 'thành công'],
|
| 840 |
+
},
|
| 841 |
+
'sad': {
|
| 842 |
+
'en': ['sad', 'unhappy', 'terrible', 'awful', 'horrible', 'miserable', 'depressed', 'grief', 'sorrow', 'tragic', 'unfortunate', 'painful', 'death', 'die', 'kill'],
|
| 843 |
+
'pt': ['triste', 'infeliz', 'terrível', 'horrível', 'miserável', 'deprimido', 'dor', 'trágico', 'infelizmente', 'penoso', 'morte', 'morrer'],
|
| 844 |
+
'es': ['triste', 'infeliz', 'terrible', 'horrible', 'miserable', 'deprimido', 'dolor', 'trágico', 'desafortunado', 'penoso', 'muerte', 'morir'],
|
| 845 |
+
'vi': ['buồn', 'không vui', 'tồi tệ', 'kinh khủng', 'đau khổ', 'đau buồn', 'bi thương', 'khốn nạn', 'đau đớn', 'thảm họa', 'chết', 'mất'],
|
| 846 |
+
},
|
| 847 |
+
'excited': {
|
| 848 |
+
'en': ['excited', 'thrilling', 'amazing', 'wow', 'incredible', 'unbelievable', 'awesome', 'exhilarating', 'electrifying', 'breathtaking', 'breakthrough', 'record'],
|
| 849 |
+
'pt': ['animado', 'emocionante', 'incrível', 'impressionante', 'sensacional', 'eletrizante', 'empolgante', 'recorde'],
|
| 850 |
+
'es': ['emocionante', 'increíble', 'impresionante', 'sensacional', 'electrizante', 'emocionado', 'entusiasmado', 'récord'],
|
| 851 |
+
'vi': ['hào hứng', 'phấn khích', 'thú vị', 'tuyệt cú mèo', 'đỉnh cao', 'ngoạn mục', 'sục sôi', 'kỷ lục', 'đột phá'],
|
| 852 |
+
},
|
| 853 |
+
'humorous': {
|
| 854 |
+
'en': ['funny', 'hilarious', 'joke', 'laugh', 'comedy', 'humor', 'amusing', 'witty', 'sarcastic', 'ironic', 'ridiculous', 'absurd', 'lol', 'haha'],
|
| 855 |
+
'pt': ['engraçado', 'hilário', 'piada', 'rir', 'comédia', 'humor', 'divertido', 'irônico', 'ridículo', 'absurdo', 'kkk'],
|
| 856 |
+
'es': ['gracioso', 'hilarante', 'broma', 'risa', 'comedia', 'humor', 'divertido', 'irónico', 'ridículo', 'absurdo', 'jaja'],
|
| 857 |
+
'vi': ['hài hước', 'buồn cười', 'đùa', 'cười', 'hài', 'vui nhộn', 'hóm hỉnh', 'mỉa mai', 'lố bịch', 'vô lý', 'haha'],
|
| 858 |
+
},
|
| 859 |
+
'serious': {
|
| 860 |
+
'en': ['serious', 'critical', 'important', 'urgent', 'severe', 'grave', 'significant', 'crucial', 'vital', 'essential', 'alarming', 'concerning', 'crisis', 'war', 'conflict'],
|
| 861 |
+
'pt': ['sério', 'crítico', 'importante', 'urgente', 'grave', 'significativo', 'crucial', 'vital', 'essencial', 'preocupante', 'crise', 'guerra', 'conflito'],
|
| 862 |
+
'es': ['serio', 'crítico', 'importante', 'urgente', 'grave', 'significativo', 'crucial', 'vital', 'esencial', 'preocupante', 'crisis', 'guerra', 'conflicto'],
|
| 863 |
+
'vi': ['nghiêm trọng', 'quan trọng', 'khẩn cấp', 'nghiêm túc', 'đáng kể', 'thiết yếu', 'cần thiết', 'báo động', 'lo ngại', 'khủng hoảng', 'chiến tranh', 'xung đột'],
|
| 864 |
+
},
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
def detect_emotion(text, language='vietnamese'):
|
| 868 |
+
"""Detect emotion from text using keyword matching."""
|
| 869 |
+
if not text:
|
| 870 |
+
return 'neutral'
|
| 871 |
+
text_lower = text.lower()
|
| 872 |
+
|
| 873 |
+
scores = {}
|
| 874 |
+
for emotion, lang_keywords in _EMOTION_KEYWORDS.items():
|
| 875 |
+
keywords = lang_keywords.get(language, lang_keywords.get('en', []))
|
| 876 |
+
score = sum(1 for kw in keywords if kw in text_lower)
|
| 877 |
+
scores[emotion] = score
|
| 878 |
+
|
| 879 |
+
if max(scores.values()) == 0:
|
| 880 |
+
return 'neutral'
|
| 881 |
+
|
| 882 |
+
return max(scores, key=scores.get)
|
| 883 |
+
|
| 884 |
+
def detect_language_and_emotion(title, text):
|
| 885 |
+
"""Detect both language and emotion from article content."""
|
| 886 |
+
combined = f"{title} {text}"
|
| 887 |
+
lang = detect_language(combined)
|
| 888 |
+
emotion = detect_emotion(combined, lang)
|
| 889 |
+
return lang, emotion
|
| 890 |
+
|
| 891 |
+
# Voice selection based on language and emotion
|
| 892 |
+
VOICE_BY_LANG_EMOTION = {
|
| 893 |
+
'vietnamese': {
|
| 894 |
+
'happy': ('hoaimy', 'vui'),
|
| 895 |
+
'sad': ('namminh', 'buồn'),
|
| 896 |
+
'excited': ('hoaimy', 'hào hứng'),
|
| 897 |
+
'humorous': ('hoaimy', 'vui'),
|
| 898 |
+
'serious': ('namminh', 'nghiêm túc'),
|
| 899 |
+
'neutral': ('hoaimy', 'trung_tinh'),
|
| 900 |
+
},
|
| 901 |
+
'portuguese': {
|
| 902 |
+
'happy': ('pt_thalita', 'feliz'),
|
| 903 |
+
'sad': ('thalita', 'triste'),
|
| 904 |
+
'excited': ('pt_francisco', 'animado'),
|
| 905 |
+
'humorous': ('pt_thalita', 'engraçado'),
|
| 906 |
+
'serious': ('thalita', 'sério'),
|
| 907 |
+
'neutral': ('pt_thalita', 'neutro'),
|
| 908 |
+
},
|
| 909 |
+
'english': {
|
| 910 |
+
'happy': ('jenny', 'happy'),
|
| 911 |
+
'sad': ('jenny', 'sad'),
|
| 912 |
+
'excited': ('andrew', 'excited'),
|
| 913 |
+
'humorous': ('jenny', 'funny'),
|
| 914 |
+
'serious': ('andrew', 'serious'),
|
| 915 |
+
'neutral': ('jenny', 'neutral'),
|
| 916 |
+
},
|
| 917 |
+
'spanish': {
|
| 918 |
+
'happy': ('ela', 'feliz'),
|
| 919 |
+
'sad': ('es_carlos', 'triste'),
|
| 920 |
+
'excited': ('ela', 'emocionado'),
|
| 921 |
+
'humorous': ('ela', 'gracioso'),
|
| 922 |
+
'serious': ('es_carlos', 'serio'),
|
| 923 |
+
'neutral': ('ela', 'neutro'),
|
| 924 |
+
},
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
def get_voice_for_content(title, text, preferred_voice=None):
|
| 928 |
+
"""Get appropriate voice based on content language and emotion."""
|
| 929 |
+
if preferred_voice and preferred_voice in ('hoaimy', 'namminh', 'andrew', 'jenny', 'thalita', 'pt_thalita', 'pt_francisco', 'ela', 'es_carlos', 'denise', 'katja', 'nanami', 'sunhee', 'xiaochen'):
|
| 930 |
+
return preferred_voice
|
| 931 |
+
|
| 932 |
+
lang, emotion = detect_language_and_emotion(title, text)
|
| 933 |
+
lang_map = VOICE_BY_LANG_EMOTION.get(lang, VOICE_BY_LANG_EMOTION['vietnamese'])
|
| 934 |
+
voice, _ = lang_map.get(emotion, lang_map['neutral'])
|
| 935 |
+
return voice
|
| 936 |
+
|
| 937 |
+
|
| 938 |
def _scrape_article_for_rewrite(url):
|
| 939 |
"""Scrape article: extract title, paragraphs, images, OG image."""
|
| 940 |
try:
|
|
|
|
| 1030 |
img = '/api/proxy/img?url=' + _quote2(img, safe='')
|
| 1031 |
slides.append({'text': point, 'image': img, 'index': i + 1})
|
| 1032 |
summary_text = '\n\n'.join([f"• {s['text']}" for s in slides])
|
| 1033 |
+
|
| 1034 |
+
# Auto-detect language and emotion
|
| 1035 |
+
lang, emotion = detect_language_and_emotion(data['title'], summary_text)
|
| 1036 |
+
voice = get_voice_for_content(data['title'], summary_text)
|
| 1037 |
+
|
| 1038 |
post = {
|
| 1039 |
"id": str(int(time.time() * 1000)) + str(_random2.randint(100, 999)),
|
| 1040 |
"title": data['title'],
|
|
|
|
| 1045 |
"slides": slides,
|
| 1046 |
"images": images[:10],
|
| 1047 |
"video": "",
|
| 1048 |
+
"voice": voice,
|
| 1049 |
+
"emotion": emotion,
|
| 1050 |
+
"language": lang,
|
| 1051 |
"ts": int(time.time())
|
| 1052 |
}
|
| 1053 |
posts = _load_wall_posts()
|
|
|
|
| 1058 |
|
| 1059 |
@app.post("/api/rewrite_share")
|
| 1060 |
async def api_rewrite_share(request: Request):
|
| 1061 |
+
"""Rewrite article and post to Tường AI with SLIDES + AI text."""
|
| 1062 |
body = await request.json()
|
| 1063 |
url = _clean(body.get("url", ""))
|
| 1064 |
ctx = _clean(body.get("context", ""))
|
|
|
|
| 1083 |
domain = urlparse(url).netloc.replace('www.', '')
|
| 1084 |
except:
|
| 1085 |
pass
|
| 1086 |
+
|
| 1087 |
+
# Generate AI summary text
|
| 1088 |
ai_text = None
|
| 1089 |
try:
|
| 1090 |
import ai_ext
|
|
|
|
| 1099 |
ai_text = '\n\n'.join([f"• {p}" for p in key_pts])
|
| 1100 |
else:
|
| 1101 |
ai_text = f"Tóm tắt: {data['title']}\n\n{raw_text[:1200]}\n\nNguồn: {domain}"
|
| 1102 |
+
|
| 1103 |
+
# Build slides from key points (FIX: include slides in rewrite_share too!)
|
| 1104 |
+
points = _extract_key_points_rw(data['paragraphs'], max_points=6)
|
| 1105 |
images = data.get('images', [])
|
| 1106 |
+
slides = []
|
| 1107 |
+
for i, point in enumerate(points):
|
| 1108 |
+
img = images[i] if i < len(images) else (images[-1] if images else '')
|
| 1109 |
+
if img and 'cdnphoto.dantri' in img:
|
| 1110 |
+
img = '/api/proxy/img?url=' + _quote2(img, safe='')
|
| 1111 |
+
slides.append({'text': point, 'image': img, 'index': i + 1})
|
| 1112 |
+
|
| 1113 |
+
# Auto-detect language and emotion
|
| 1114 |
+
lang, emotion = detect_language_and_emotion(data['title'], ai_text)
|
| 1115 |
+
voice = get_voice_for_content(data['title'], ai_text)
|
| 1116 |
+
|
| 1117 |
post = {
|
| 1118 |
"id": str(int(time.time() * 1000)) + str(_random2.randint(100, 999)),
|
| 1119 |
"title": data['title'],
|
|
|
|
| 1121 |
"img": images[0] if images else '',
|
| 1122 |
"url": url,
|
| 1123 |
"kind": "rewrite",
|
| 1124 |
+
"slides": slides,
|
| 1125 |
"images": images[:10],
|
| 1126 |
"video": "",
|
| 1127 |
+
"voice": voice,
|
| 1128 |
+
"emotion": emotion,
|
| 1129 |
+
"language": lang,
|
| 1130 |
"ts": int(time.time())
|
| 1131 |
}
|
| 1132 |
posts = _load_wall_posts()
|
| 1133 |
posts.insert(0, post)
|
| 1134 |
_save_wall_posts(posts)
|
| 1135 |
+
return JSONResponse({"post": post, "slides": slides})
|
| 1136 |
|
| 1137 |
|
| 1138 |
@app.post("/api/url_wall")
|
|
|
|
| 1142 |
url = _clean(body.get("url", ""))
|
| 1143 |
if not url or not url.startswith('http'):
|
| 1144 |
return JSONResponse({"error": "URL không hợp lệ"}, status_code=400)
|
| 1145 |
+
# Reuse rewrite_share logic
|
| 1146 |
+
req._body = json.dumps({"url": url}).encode()
|
| 1147 |
return await api_rewrite_share(request)
|
| 1148 |
|
| 1149 |
|