Spaces:
Running
Running
Test upload size - first half of ai_ext.py from c93b544
Browse files
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
|