bep40 commited on
Commit
ef4431e
·
verified ·
1 Parent(s): 9ce3777

Upload app_v2_entry.py

Browse files
Files changed (1) hide show
  1. 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(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,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
- # ===== REWRITE / ARTICLE-TO-WALL ENDPOINTS =====
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": "hoaimy",
886
- "emotion": "trung_tinh",
 
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 (with AI fallback to extractive)."""
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": "hoaimy",
947
- "emotion": "trung_tinh",
 
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