bep40 commited on
Commit
0edf211
·
verified ·
1 Parent(s): eb55d03

Test upload size - first half of ai_ext.py from c93b544

Browse files
Files changed (1) hide show
  1. ai_ext.py +196 -0
ai_ext.py CHANGED
@@ -15,6 +15,7 @@ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
15
 
16
  from main import app
17
 
 
18
  try:
19
  from main import _load_wall, _save_wall, _web_context # noqa: F401
20
  except ImportError:
@@ -74,3 +75,198 @@ HEADERS = {
74
  "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8"
75
  }
76
  LAST_QWEN_ERROR = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  from main import app
17
 
18
+ # Import wall store from main.py so we read/write the SAME file
19
  try:
20
  from main import _load_wall, _save_wall, _web_context # noqa: F401
21
  except ImportError:
 
75
  "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8"
76
  }
77
  LAST_QWEN_ERROR = ""
78
+
79
+ # ===== TTS VOICE CONFIG =====
80
+ TTS_VOICES = {
81
+ "hoaimy": {"id": "vi-VN-HoaiMyNeural", "gender": "female", "name": "Hoài My", "country": "Việt Nam", "lang": "vi", "flag": "🇻🇳"},
82
+ "namminh": {"id": "vi-VN-NamMinhNeural", "gender": "male", "name": "Nam Minh", "country": "Việt Nam", "lang": "vi", "flag": "🇻🇳"},
83
+ "en_us_f": {"id": "en-US-AvaNeural", "gender": "female", "name": "Ava", "country": "Mỹ", "lang": "en", "flag": "🇺🇸"},
84
+ "en_us_m": {"id": "en-US-BrianNeural", "gender": "male", "name": "Brian", "country": "Mỹ", "lang": "en", "flag": "🇺🇸"},
85
+ "en_gb_f": {"id": "en-GB-SoniaNeural", "gender": "female", "name": "Sonia", "country": "Anh", "lang": "en", "flag": "🇬🇧"},
86
+ "en_gb_m": {"id": "en-GB-RyanNeural", "gender": "male", "name": "Ryan", "country": "Anh", "lang": "en", "flag": "🇬🇧"},
87
+ "zh_cn_f": {"id": "zh-CN-XiaoxiaoNeural", "gender": "female", "name": "Xiaoxiao", "country": "Trung Quốc", "lang": "zh", "flag": "🇨🇳"},
88
+ "zh_cn_m": {"id": "zh-CN-YunyangNeural", "gender": "male", "name": "Yunyang", "country": "Trung Quốc", "lang": "zh", "flag": "🇨🇳"},
89
+ "zh_hk_f": {"id": "zh-HK-HiuGaaiNeural", "gender": "female", "name": "HiuGaai", "country": "Hồng Kông", "lang": "zh", "flag": "🇭🇰"},
90
+ "ja_jp_f": {"id": "ja-JP-NanamiNeural", "gender": "female", "name": "Nanami", "country": "Nhật Bản", "lang": "ja", "flag": "🇯🇵"},
91
+ "ja_jp_m": {"id": "ja-JP-KeitaNeural", "gender": "male", "name": "Keita", "country": "Nhật Bản", "lang": "ja", "flag": "🇯🇵"},
92
+ "ko_kr_f": {"id": "ko-KR-SunHiNeural", "gender": "female", "name": "Sun-Hi", "country": "Hàn Quốc", "lang": "ko", "flag": "🇰🇷"},
93
+ "ko_kr_m": {"id": "ko-KR-InJoonNeural", "gender": "male", "name": "InJoon", "country": "Hàn Quốc", "lang": "ko", "flag": "🇰🇷"},
94
+ "fr_fr_f": {"id": "fr-FR-DeniseNeural", "gender": "female", "name": "Denise", "country": "Pháp", "lang": "fr", "flag": "🇫🇷"},
95
+ "fr_fr_m": {"id": "fr-FR-HenriNeural", "gender": "male", "name": "Henri", "country": "Pháp", "lang": "fr", "flag": "🇫🇷"},
96
+ "de_de_f": {"id": "de-DE-KatjaNeural", "gender": "female", "name": "Katja", "country": "Đức", "lang": "de", "flag": "🇩🇪"},
97
+ "de_de_m": {"id": "de-DE-KillianNeural", "gender": "male", "name": "Killian", "country": "Đức", "lang": "de", "flag": "🇩🇪"},
98
+ "es_es_f": {"id": "es-ES-XimenaNeural", "gender": "female", "name": "Ximena", "country": "Tây Ban Nha", "lang": "es", "flag": "🇪🇸"},
99
+ "es_mx_m": {"id": "es-MX-JorgeNeural", "gender": "male", "name": "Jorge", "country": "Mexico", "lang": "es", "flag": "🇲🇽"},
100
+ "th_th_f": {"id": "th-TH-PremwadeeNeural", "gender": "female", "name": "Premwadee", "country": "Thái Lan", "lang": "th", "flag": "🇹🇭"},
101
+ "th_th_m": {"id": "th-TH-NiwatNeural", "gender": "male", "name": "Niwat", "country": "Thái Lan", "lang": "th", "flag": "🇹🇭"},
102
+ "hi_in_f": {"id": "hi-IN-SwaraNeural", "gender": "female", "name": "Swara", "country": "Ấn Độ", "lang": "hi", "flag": "🇮🇳"},
103
+ "hi_in_m": {"id": "hi-IN-MadhurNeural", "gender": "male", "name": "Madhur", "country": "Ấn Độ", "lang": "hi", "flag": "🇮🇳"},
104
+ }
105
+ TTS_DEFAULT_VOICE = "hoaimy"
106
+ TTS_DEFAULT_SPEED = 1.2
107
+
108
+ TOPIC_VOICE_MAP = {
109
+ "bóng đá": "namminh", "thể thao": "namminh", "world cup": "namminh",
110
+ "premier league": "namminh", "champions league": "namminh", "la liga": "namminh",
111
+ "serie a": "namminh", "bundesliga": "namminh", "v-league": "namminh",
112
+ "tennis": "namminh", "olympic": "namminh", "f1": "namminh", "moto": "namminh",
113
+ "sức khỏe": "hoaimy", "làm đẹp": "hoaimy", "giải trí": "hoaimy",
114
+ "âm nhạc": "hoaimy", "phim": "hoaimy", "thời trang": "hoaimy",
115
+ "ẩm thực": "hoaimy", "du lịch": "hoaimy", "gia đình": "hoaimy",
116
+ "tình yêu": "hoaimy", "hôn nhân": "hoaimy", "mẹ và bé": "hoaimy",
117
+ "công nghệ": "namminh", "ai": "namminh", "robot": "namminh",
118
+ "khoa học": "namminh", "vũ trụ": "namminh", "điện thoại": "namminh",
119
+ "laptop": "namminh", "game": "namminh",
120
+ "chính trị": "namminh", "kinh tế": "namminh", "tài chính": "namminh",
121
+ "chứng khoán": "namminh", "ngân hàng": "namminh", "thị trường": "namminh",
122
+ "xã hội": "namminh", "pháp luật": "namminh", "giáo dục": "namminh",
123
+ }
124
+
125
+ def _detect_voice_for_topic(title: str, text: str) -> str:
126
+ combined = (title + " " + text[:500]).lower()
127
+ for keyword, voice_id in TOPIC_VOICE_MAP.items():
128
+ if keyword in combined:
129
+ return voice_id
130
+ return TTS_DEFAULT_VOICE
131
+
132
+ def _clean_text(s: str) -> str:
133
+ s = html_lib.unescape(s or "")
134
+ return re.sub(r"\s+", " ", s).strip()
135
+
136
+ def _domain(u):
137
+ try:
138
+ return urlparse(u).netloc.replace("www.", "")
139
+ except Exception:
140
+ return ""
141
+
142
+ def _safe_name(s):
143
+ return re.sub(r"[^a-zA-Z0-9_-]+", "_", str(s))[:80]
144
+
145
+ def _clean_ai_output(text: str) -> str:
146
+ if not text:
147
+ return ""
148
+ text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
149
+ text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text)
150
+ text = re.sub(r'\*([^*]+)\*', r'\1', text)
151
+ text = re.sub(r'^---+\s*$', '', text, flags=re.MULTILINE)
152
+ text = re.sub(r'^[-*_]{3,}\s*$', '', text, flags=re.MULTILINE)
153
+ leakage = [
154
+ r'Dưới đây là', r'Theo yêu cầu', r'Tôi sẽ viết', r'Tôi sẽ tóm tắt',
155
+ r'Đây là bài', r'Đây là nội dung', r'Bài viết sau đây',
156
+ r'Nội dung (tóm tắt|chính)', r'Nhiệm vụ', r'Vai trò', r'Tôi là',
157
+ r'Dựa trên.*tôi sẽ', r'Hãy', r'Bạn cần', r'Đọc bài viết',
158
+ r'Tôi xin', r'Xin chào', r'Trân trọng', r'Kính thưa',
159
+ r'Dựa trên.*dưới đây', r'Sau đây là', r'Dưới đây là bài',
160
+ ]
161
+ for phrase in leakage:
162
+ text = re.sub(r'^' + phrase + r'[^\n]*\n?', '', text, flags=re.MULTILINE | re.IGNORECASE)
163
+ text = re.sub(r'\n{3,}', '\n\n', text)
164
+ def _norm(s):
165
+ return re.sub(r'\s+', ' ', s.strip().lower())
166
+ raw_parts = re.split(r'(?<=[.!?])\s+', text.strip())
167
+ seen_sentences = set()
168
+ unique_parts = []
169
+ for part in raw_parts:
170
+ n = _norm(part)
171
+ is_dup = False
172
+ if n:
173
+ if n in seen_sentences:
174
+ is_dup = True
175
+ else:
176
+ partial = re.sub(r'\W+', '', n)
177
+ for seen in seen_sentences:
178
+ seen_clean = re.sub(r'\W+', '', seen)
179
+ if partial and seen_clean and (partial in seen_clean or seen_clean in partial):
180
+ shorter = min(len(partial), len(seen_clean))
181
+ longer = max(len(partial), len(seen_clean))
182
+ if shorter > 20 and shorter / longer > 0.75:
183
+ is_dup = True
184
+ break
185
+ if is_dup:
186
+ continue
187
+ if n:
188
+ seen_sentences.add(n)
189
+ unique_parts.append(part)
190
+ result = ' '.join(unique_parts).strip()
191
+ lines = result.split('\n')
192
+ final_lines = []
193
+ prev_line = ""
194
+ for line in lines:
195
+ stripped = line.strip()
196
+ if stripped and stripped == prev_line:
197
+ continue
198
+ final_lines.append(line)
199
+ prev_line = stripped
200
+ result = '\n'.join(final_lines).strip()
201
+ return result
202
+
203
+
204
+ # ===== EXTRACT ALL IMAGES FROM ARTICLE =====
205
+ def _extract_all_images(soup, base_url: str) -> List[Dict]:
206
+ images = []
207
+ seen_urls = set()
208
+ skip_patterns = [
209
+ "avatar", "icon", "logo", "button", "banner-ad", "tracking",
210
+ "beacon", "pixel", "1x1", "spacer", "emoji", "sprite", "placeholder",
211
+ "advertisement", "ads", "widget", "sidebar", "footer-logo",
212
+ ]
213
+ def _add_image(src: str, alt: str = "", source_tag: str = "img"):
214
+ if not src or src.startswith("data:"):
215
+ return
216
+ abs_url = urljoin(base_url, src.strip())
217
+ if abs_url in seen_urls:
218
+ return
219
+ if any(p in abs_url.lower() for p in skip_patterns):
220
+ return
221
+ try:
222
+ parsed = urlparse(abs_url)
223
+ path = parsed.path.lower()
224
+ if any(path.endswith(ext) for ext in ['.svg', '.ico', '.gif']):
225
+ return
226
+ except Exception:
227
+ pass
228
+ seen_urls.add(abs_url)
229
+ images.append({"url": abs_url, "alt": alt, "source": source_tag})
230
+ for img in soup.find_all("img"):
231
+ src = (img.get("src") or img.get("data-src") or img.get("data-lazy-src") or
232
+ img.get("data-original") or img.get("data-srcset", "").split(",")[0].strip().split(" ")[0])
233
+ _add_image(src, alt=img.get("alt", ""), source_tag="img")
234
+ for img in soup.find_all("img", srcset=True):
235
+ for part in img["srcset"].split(","):
236
+ part = part.strip()
237
+ if part:
238
+ _add_image(part.split(" ")[0], alt=img.get("alt", ""), source_tag="srcset")
239
+ for picture in soup.find_all("picture"):
240
+ for source in picture.find_all("source"):
241
+ srcset = source.get("srcset", "")
242
+ for part in srcset.split(","):
243
+ part = part.strip()
244
+ if part:
245
+ _add_image(part.split(" ")[0], source_tag="picture/srcset")
246
+ fallback_img = picture.find("img")
247
+ if fallback_img:
248
+ _add_image(fallback_img.get("src") or fallback_img.get("data-src"),
249
+ alt=fallback_img.get("alt", ""), source_tag="picture/img")
250
+ for img in soup.find_all("img", class_=re.compile(r"wp-image|size-large|size-full|aligncenter")):
251
+ _add_image(img.get("data-src") or img.get("src"), alt=img.get("alt", ""), source_tag="wp-image")
252
+ for tag in soup.find_all(style=re.compile(r"background-image")):
253
+ for m in re.findall(r'url\(["\']?(.*?)["\']?\)', tag.get("style", "")):
254
+ _add_image(m, source_tag="background-style")
255
+ og_image = soup.find("meta", property="og:image")
256
+ if og_image and og_image.get("content"):
257
+ _add_image(og_image["content"], source_tag="og:image")
258
+ tw_image = soup.find("meta", attrs={"name": "twitter:image"})
259
+ if tw_image and tw_image.get("content"):
260
+ _add_image(tw_image["content"], source_tag="twitter:image")
261
+ for figure in soup.find_all("figure"):
262
+ img = figure.find("img")
263
+ if img:
264
+ src = img.get("data-src") or img.get("src")
265
+ figcaption = figure.find("figcaption")
266
+ alt = figcaption.get_text(strip=True) if figcaption else img.get("alt", "")
267
+ _add_image(src, alt=alt, source_tag="figure")
268
+ for a in soup.find_all("a", href=True):
269
+ href = a["href"]
270
+ if any(href.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".webp", ".gif"]):
271
+ _add_image(href, alt=a.get_text(strip=True)[:80], source_tag="link")
272
+ return images