Spaces:
Running
Running
Download ural_mega_module.py from BachDaThan/Ural-AI: direct link, hf CLI and curl.
- Browser
- Download file 50.1 kB
-
https://huggingface.co/spaces/BachDaThan/Ural-AI/resolve/main/ural_mega_module.py
- Command line
-
hf download hf://spaces/BachDaThan/Ural-AI/ural_mega_module.py
-
curl -L -o ural_mega_module.py https://huggingface.co/spaces/BachDaThan/Ural-AI/resolve/main/ural_mega_module.py
50.1 kB
| # ================================================================ | |
| # URAL AI - MEGA MODULE v1.0 | |
| # File: ural_mega_module.py | |
| # | |
| # GỒM TẤT CẢ TÍNH NĂNG CÒN THIẾU — 1 FILE DUY NHẤT | |
| # HOÀN TOÀN FREE, KHÔNG KEY, KHÔNG PHÍ ẨN | |
| # NẰM TRONG GIỚI HẠN HF FREE TIER | |
| # | |
| # CÁCH DÙNG: Thêm vào app.py (sau các import ural khác): | |
| # import ural_mega_module | |
| # | |
| # CÁC MODULE TRONG FILE NÀY: | |
| # [1] Tin tức realtime — RSS feeds, không cần key | |
| # [2] Giá crypto — CoinGecko API, free không cần key | |
| # [3] Giá vàng/USD/VND — Tự scrape từ web | |
| # [4] Thời tiết chi tiết — wttr.in, free không cần key | |
| # [5] QR Code tạo — qrcode library (nhẹ, offline) | |
| # [6] Nhật ký cảm xúc — SQLite đã có trong app | |
| # [7] Đổi đơn vị — Logic thuần, không cần mạng | |
| # [8] Tra từ điển VN — Free dictionary API | |
| # [9] Tóm tắt URL tốt hơn — Jina.ai đã có, hook tốt hơn | |
| # [10] Câu đố / Trò chơi — Logic thuần, nâng cấp quiz_game cũ | |
| # ================================================================ | |
| import os, sys, re, json, random, time, datetime, requests | |
| from typing import Optional | |
| # ── [FIX] Đường dẫn DB persistent — TRƯỚC ĐÂY dùng "ural_memory.db" (path | |
| # tương đối) → mất sạch dữ liệu mỗi khi HF Space restart/redeploy vì thư mục | |
| # làm việc không persistent. GIỜ ưu tiên /data (persistent storage của HF), | |
| # fallback về thư mục hiện tại hoặc /tmp nếu /data không truy cập được | |
| # (vd: chạy local dev không có /data). | |
| def _journal_db_path() -> str: | |
| if os.path.exists("/data") and os.path.isdir("/data") and os.access("/data", os.W_OK): | |
| return "/data/ural_memory.db" | |
| return "./ural_memory.db" if os.access(".", os.W_OK) else "/tmp/ural_memory.db" | |
| _URAL_DB_PATH = _journal_db_path() | |
| # ================================================================ | |
| # MODULE 1: TIN TỨC REALTIME — RSS Feeds | |
| # Không cần key, cực nhẹ, ổn định | |
| # ================================================================ | |
| RSS_SOURCES = { | |
| "vnexpress": "https://vnexpress.net/rss/tin-moi-nhat.rss", | |
| "tuoitre": "https://tuoitre.vn/rss/tin-moi-nhat.rss", | |
| "thanhnien": "https://thanhnien.vn/rss/home.rss", | |
| "dantri": "https://dantri.com.vn/rss/home.rss", | |
| "the_gioi": "https://vnexpress.net/rss/the-gioi.rss", | |
| "cong_nghe": "https://vnexpress.net/rss/khoa-hoc-cong-nghe.rss", | |
| "kinh_doanh": "https://vnexpress.net/rss/kinh-doanh.rss", | |
| "giai_tri": "https://vnexpress.net/rss/giai-tri.rss", | |
| "the_thao": "https://vnexpress.net/rss/the-thao.rss", | |
| } | |
| NEWS_KEYWORDS = { | |
| "thế giới": "the_gioi", | |
| "quốc tế": "the_gioi", | |
| "công nghệ": "cong_nghe", | |
| "tech": "cong_nghe", | |
| "kinh doanh": "kinh_doanh", | |
| "kinh tế": "kinh_doanh", | |
| "giải trí": "giai_tri", | |
| "showbiz": "giai_tri", | |
| "thể thao": "the_thao", | |
| "bóng đá": "the_thao", | |
| } | |
| _news_cache = {} # {source: (timestamp, articles)} | |
| NEWS_CACHE_TTL = 300 # 5 phút | |
| def get_news(category: str = "", num: int = 5) -> str: | |
| """Lấy tin tức realtime — ưu tiên brain_memory cache từ RSS-Watcher.""" | |
| import datetime as _dt_gn | |
| # ── BƯỚC 1: Dùng brain_memory cache từ RSS-Watcher (đã có sẵn!) ────── | |
| # RSS-Watcher chạy mỗi 15 phút, lưu vào brain_memory[_rss_hot_YYYY-MM-DD] | |
| # Đây là nguồn dữ liệu đúng, không cần fetch lại | |
| try: | |
| today_key = f"_rss_hot_{_dt_gn.date.today()}" | |
| rss_cached = brain_memory.get(today_key, "") | |
| if rss_cached and len(rss_cached) > 50: | |
| lines = [l.strip() for l in rss_cached.strip().split("\n") if l.strip()] | |
| # Lấy num tin đầu (mỗi tin = 1 dòng "• [SOURCE] title") | |
| news_lines = [l for l in lines if l.startswith("•")][:num] | |
| if news_lines: | |
| cat_label = category if category else "mới nhất" | |
| ts = _dt_gn.datetime.now().strftime("%H:%M %d/%m") | |
| result = [f"📰 **Tin tức {cat_label}** ({ts}):\n"] | |
| for i, line in enumerate(news_lines, 1): | |
| # Format: • [VNEXPRESS] Tiêu đề tin → **1. Tiêu đề tin** | |
| clean = re.sub(r"^•\s*\[\w+\]\s*", "", line).strip() | |
| result.append(f"**{i}. {clean}**") | |
| result.append("") | |
| return "\n".join(result) | |
| except Exception as _e_cache: | |
| pass # Fallback xuống fetch trực tiếp | |
| # ── BƯỚC 2: Fallback - fetch RSS trực tiếp ─────────────────────────── | |
| source_key = "vnexpress" | |
| cat_lower = category.lower() | |
| for kw, src in NEWS_KEYWORDS.items(): | |
| if kw in cat_lower: | |
| source_key = src | |
| break | |
| url = RSS_SOURCES.get(source_key, RSS_SOURCES["vnexpress"]) | |
| cached = _news_cache.get(source_key) | |
| if cached and (time.time() - cached[0]) < NEWS_CACHE_TTL: | |
| articles = cached[1] | |
| else: | |
| try: | |
| resp = requests.get(url, timeout=10, headers={ | |
| "User-Agent": "Mozilla/5.0 (compatible; URAL-AI/1.0)" | |
| }) | |
| resp.encoding = "utf-8" | |
| raw = resp.text | |
| # Parse nhiều format RSS khác nhau | |
| titles = (re.findall(r'<title><!\[CDATA\[(.*?)\]\]></title>', raw, re.DOTALL) | |
| or re.findall(r'<title>(.*?)</title>', raw, re.DOTALL)) | |
| descs = (re.findall(r'<description><!\[CDATA\[(.*?)\]\]></description>', raw, re.DOTALL) | |
| or re.findall(r'<description>(.*?)</description>', raw, re.DOTALL)) | |
| links = re.findall(r'<link>(https?://[^<]+)</link>', raw) | |
| if len(titles) > 1: | |
| titles = titles[1:] # Bỏ title của feed | |
| articles = [] | |
| for i in range(min(num + 2, len(titles))): | |
| title = re.sub(r'<[^>]+>', '', titles[i]).strip() if i < len(titles) else "" | |
| desc = re.sub(r'<[^>]+>', '', descs[i+1] if i+1 < len(descs) else "").strip()[:200] | |
| link = links[i] if i < len(links) else "" | |
| if title and len(title) > 5: | |
| articles.append({"title": title, "desc": desc, "link": link}) | |
| _news_cache[source_key] = (time.time(), articles) | |
| except Exception as e: | |
| return f"⚠️ Không lấy được tin tức: {e}" | |
| if not articles: | |
| return "⚠️ Chưa có tin tức trong cache. Thử lại sau 1 phút." | |
| cat_label = category if category else "mới nhất" | |
| ts = _dt_gn.datetime.now().strftime("%H:%M %d/%m") | |
| result = [f"📰 **Tin tức {cat_label}** ({ts}):\n"] | |
| for i, art in enumerate(articles[:num]): | |
| result.append(f"**{i+1}. {art['title']}**") | |
| if art['desc']: | |
| result.append(f" {art['desc']}...") | |
| if art['link']: | |
| result.append(f" 🔗 {art['link']}") | |
| result.append("") | |
| return "\n".join(result) | |
| # ================================================================ | |
| # MODULE 2: GIÁ CRYPTO — CoinGecko (free, không cần key) | |
| # ================================================================ | |
| CRYPTO_IDS = { | |
| "bitcoin": "bitcoin", "btc": "bitcoin", | |
| "ethereum": "ethereum", "eth": "ethereum", | |
| "bnb": "binancecoin", "binance": "binancecoin", | |
| "xrp": "ripple", "ripple": "ripple", | |
| "solana": "solana", "sol": "solana", | |
| "doge": "dogecoin", "dogecoin": "dogecoin", | |
| "usdt": "tether", "tether": "tether", | |
| "ada": "cardano", "cardano": "cardano", | |
| "ton": "the-open-network", | |
| "not": "notcoin", | |
| "pepe": "pepe", | |
| } | |
| _crypto_cache = {} # {coin_id: (ts, data)} | |
| CRYPTO_CACHE_TTL = 60 # 1 phút | |
| def get_crypto_price(coins: list = None) -> str: | |
| """Giá crypto realtime từ CoinGecko API — miễn phí, không cần key.""" | |
| if not coins: | |
| coins = ["bitcoin", "ethereum", "binancecoin", "solana", "ripple"] | |
| coin_ids = [] | |
| coin_names = {} | |
| for c in coins: | |
| cid = CRYPTO_IDS.get(c.lower(), c.lower()) | |
| coin_ids.append(cid) | |
| coin_names[cid] = c.upper() | |
| ids_str = ",".join(set(coin_ids)) | |
| url = (f"https://api.coingecko.com/api/v3/simple/price" | |
| f"?ids={ids_str}&vs_currencies=usd,vnd" | |
| f"&include_24hr_change=true&include_market_cap=false") | |
| try: | |
| resp = requests.get(url, timeout=8) | |
| data = resp.json() | |
| now = datetime.datetime.now().strftime("%H:%M %d/%m") | |
| lines = [f"💰 **Giá Crypto** (cập nhật {now}):\n"] | |
| for cid, info in data.items(): | |
| name = coin_names.get(cid, cid.upper()) | |
| usd = info.get("usd", 0) | |
| vnd = info.get("vnd", 0) | |
| chg = info.get("usd_24h_change", 0) or 0 | |
| arrow = "📈" if chg >= 0 else "📉" | |
| sign = "+" if chg >= 0 else "" | |
| usd_str = f"${usd:,.2f}" if usd < 1 else f"${usd:,.0f}" | |
| vnd_str = f"{vnd/1_000_000:.1f}M VND" if vnd > 1_000_000 else f"{vnd:,.0f} VND" | |
| lines.append(f"{arrow} **{name}**: {usd_str} ({vnd_str}) | 24h: {sign}{chg:.1f}%") | |
| return "\n".join(lines) | |
| except Exception as e: | |
| return f"⚠️ Không lấy được giá crypto: {e}" | |
| def detect_crypto_request(text: str) -> Optional[list]: | |
| """Trả về danh sách coin được hỏi, hoặc None nếu không phải câu hỏi crypto.""" | |
| t = text.lower() | |
| crypto_triggers = [ | |
| "giá bitcoin", "giá btc", "giá eth", "giá ethereum", | |
| "giá coin", "giá crypto", "giá bnb", "giá sol", | |
| "crypto hôm nay", "coin hôm nay", "thị trường crypto", | |
| "bitcoin bao nhiêu", "eth bao nhiêu", "btc bao nhiêu", | |
| "giá doge", "giá xrp", "giá ada", "giá ton", | |
| ] | |
| if not any(kw in t for kw in crypto_triggers): | |
| return None | |
| # Tìm coin cụ thể | |
| found = [] | |
| for alias, cid in CRYPTO_IDS.items(): | |
| if alias in t and cid not in found: | |
| found.append(cid) | |
| return found if found else ["bitcoin", "ethereum", "binancecoin"] | |
| # ================================================================ | |
| # MODULE 3: GIÁ VÀNG & NGOẠI TỆ | |
| # ================================================================ | |
| _gold_cache = {} | |
| GOLD_CACHE_TTL = 300 # 5 phút | |
| def get_gold_price() -> str: | |
| """Giá vàng và tỷ giá USD/VND từ wttr.in và exchangerate.host.""" | |
| results = [] | |
| # Tỷ giá USD/VND | |
| try: | |
| resp = requests.get( | |
| "https://api.exchangerate-api.com/v4/latest/USD", | |
| timeout=5 | |
| ) | |
| data = resp.json() | |
| vnd_rate = data["rates"].get("VND", 0) | |
| if vnd_rate: | |
| results.append(f"💵 **USD/VND**: {vnd_rate:,.0f} VND") | |
| except Exception: | |
| pass | |
| # Giá vàng XAU/USD qua exchangerate | |
| try: | |
| resp = requests.get( | |
| "https://api.metals.live/v1/spot/gold", | |
| timeout=5 | |
| ) | |
| data = resp.json() | |
| gold_usd = data[0].get("price", 0) if isinstance(data, list) else 0 | |
| if gold_usd: | |
| gold_vnd = gold_usd * 24000 / 31.1035 # oz → chỉ (3.75g) | |
| results.append( | |
| f"🥇 **Vàng thế giới**: ${gold_usd:,.2f}/oz" | |
| f" (~{gold_vnd/1_000_000:.1f}M VND/chỉ)" | |
| ) | |
| except Exception: | |
| pass | |
| now = datetime.datetime.now().strftime("%H:%M %d/%m") | |
| if results: | |
| return f"💹 **Tỷ giá & Vàng** (cập nhật {now}):\n\n" + "\n".join(results) | |
| return "⚠️ Không lấy được tỷ giá lúc này." | |
| GOLD_KEYWORDS = [ | |
| "giá vàng", "vàng hôm nay", "vàng bao nhiêu", | |
| "tỷ giá", "đô la", "usd hôm nay", "usd bao nhiêu", | |
| "dollar", "usd/vnd", "vnd/usd", | |
| ] | |
| def detect_gold_request(text: str) -> bool: | |
| return any(kw in text.lower() for kw in GOLD_KEYWORDS) | |
| # ================================================================ | |
| # MODULE 4: THỜI TIẾT CHI TIẾT — wttr.in (free, không cần key) | |
| # ================================================================ | |
| # ================================================================ | |
| # 🆕 WEATHER v2 — Open-Meteo (100% free, không cần API key, không | |
| # giới hạn ẩn, không thẻ tín dụng). Thay thế wttr.in cũ vì: | |
| # - wttr.in hay timeout/instability, dữ liệu đôi khi cũ | |
| # - Open-Meteo tổng hợp từ nhiều mô hình khí tượng (ECMWF, GFS, | |
| # ICON...) → chính xác hơn, đúng như Gemini đã chỉ ra khi so | |
| # sánh với nguồn NCHMF chính thống của VN. | |
| # - Có UV index, mưa mm, gió, độ ẩm — đủ field như bản cũ. | |
| # Docs: https://open-meteo.com/en/docs (free, unlimited cho non- | |
| # commercial, không cần đăng ký/API key). | |
| # ================================================================ | |
| _OM_GEOCODE_CACHE: dict = {} | |
| def _om_geocode(location: str) -> Optional[dict]: | |
| """Tra cứu toạ độ (lat/lon) từ tên địa điểm — dùng Open-Meteo | |
| Geocoding API (free, không key). Cache trong RAM để đỡ gọi lại.""" | |
| key = location.strip().lower() | |
| if key in _OM_GEOCODE_CACHE: | |
| return _OM_GEOCODE_CACHE[key] | |
| try: | |
| resp = requests.get( | |
| "https://geocoding-api.open-meteo.com/v1/search", | |
| params={"name": location, "count": 1, "language": "vi", "format": "json"}, | |
| timeout=8, | |
| ) | |
| data = resp.json() | |
| results = data.get("results") or [] | |
| if not results: | |
| return None | |
| r = results[0] | |
| info = { | |
| "lat": r.get("latitude"), | |
| "lon": r.get("longitude"), | |
| "name": r.get("name", location), | |
| "admin1": r.get("admin1", ""), | |
| "country": r.get("country", ""), | |
| } | |
| _OM_GEOCODE_CACHE[key] = info | |
| return info | |
| except Exception: | |
| return None | |
| # Mã thời tiết WMO (chuẩn Open-Meteo dùng) → mô tả tiếng Việt + emoji | |
| _WMO_CODES = { | |
| 0: ("Trời quang", "☀️"), 1: ("Ít mây", "🌤️"), 2: ("Có mây", "⛅"), | |
| 3: ("Nhiều mây/âm u", "☁️"), 45: ("Sương mù", "🌫️"), 48: ("Sương mù đóng băng", "🌫️"), | |
| 51: ("Mưa phùn nhẹ", "🌦️"), 53: ("Mưa phùn", "🌦️"), 55: ("Mưa phùn dày", "🌧️"), | |
| 61: ("Mưa nhẹ", "🌦️"), 63: ("Mưa vừa", "🌧️"), 65: ("Mưa to", "🌧️"), | |
| 66: ("Mưa đá nhẹ", "🌨️"), 67: ("Mưa đá to", "🌨️"), | |
| 71: ("Tuyết nhẹ", "🌨️"), 73: ("Tuyết vừa", "🌨️"), 75: ("Tuyết to", "❄️"), | |
| 80: ("Mưa rào nhẹ", "🌦️"), 81: ("Mưa rào vừa", "🌧️"), 82: ("Mưa rào dữ dội", "⛈️"), | |
| 95: ("Dông", "⛈️"), 96: ("Dông kèm mưa đá nhẹ", "⛈️"), 99: ("Dông kèm mưa đá to", "⛈️"), | |
| } | |
| def get_weather_detail(location: str = "Ho Chi Minh City") -> str: | |
| """ | |
| Thời tiết chi tiết từ Open-Meteo — miễn phí 100%, không cần key, | |
| không thẻ, không giới hạn ẩn. Chính xác hơn wttr.in (bản cũ), | |
| dùng dữ liệu tổng hợp từ nhiều mô hình khí tượng (ECMWF/GFS/ICON). | |
| """ | |
| geo = _om_geocode(location) | |
| if not geo: | |
| return f"⚠️ Không tìm thấy địa điểm '{location}' — Boss thử ghi rõ hơn (VD: 'Đà Nẵng, Việt Nam') nhé." | |
| try: | |
| resp = requests.get( | |
| "https://api.open-meteo.com/v1/forecast", | |
| params={ | |
| "latitude": geo["lat"], "longitude": geo["lon"], | |
| "current": "temperature_2m,relative_humidity_2m,apparent_temperature," | |
| "precipitation,weather_code,wind_speed_10m,wind_gusts_10m,uv_index", | |
| # 🆕 minutely_15: dữ liệu cập nhật mỗi 15 phút — trễ pha THẤP | |
| # HƠN NHIỀU so với chỉ dùng "current" (nội suy theo giờ chẵn | |
| # của mô hình, có thể lệch 30-60+ phút khi thời tiết biến | |
| # động nhanh — VD mưa dông/bão đến đột ngột, đúng tình huống | |
| # Gemini phát hiện: Yui báo "nhiều mây" trong khi thực tế | |
| # đang mưa to do hoàn lưu bão). Lấy điểm dữ liệu 15-phút GẦN | |
| # NHẤT với hiện tại làm nguồn chính, "current" chỉ dùng dự | |
| # phòng nếu minutely_15 không trả về được. | |
| "minutely_15": "temperature_2m,relative_humidity_2m,apparent_temperature," | |
| "precipitation,weather_code,wind_speed_10m,wind_gusts_10m", | |
| "daily": "temperature_2m_max,temperature_2m_min,weather_code,uv_index_max,precipitation_sum", | |
| "timezone": "auto", | |
| "forecast_days": 2, | |
| "forecast_minutely_15": 4, # chỉ cần vài điểm gần nhất, đỡ payload | |
| }, | |
| timeout=8, | |
| ) | |
| data = resp.json() | |
| daily = data["daily"] | |
| # 🆕 Ưu tiên điểm minutely_15 GẦN THỜI ĐIỂM HIỆN TẠI NHẤT (index 0 | |
| # của mảng minutely_15 luôn là mốc 15-phút gần nhất đã qua/đang tới, | |
| # trễ tối đa ~15 phút thay vì tối đa ~60 phút như "current" mặc định) | |
| cur_source = "minutely_15" | |
| m15 = data.get("minutely_15") | |
| if m15 and m15.get("temperature_2m") and m15["temperature_2m"][0] is not None: | |
| cur = { | |
| "temperature_2m": m15["temperature_2m"][0], | |
| "relative_humidity_2m": m15["relative_humidity_2m"][0], | |
| "apparent_temperature": m15["apparent_temperature"][0], | |
| "precipitation": m15.get("precipitation", [0])[0] or 0, | |
| "weather_code": m15["weather_code"][0], | |
| "wind_speed_10m": m15["wind_speed_10m"][0], | |
| "wind_gusts_10m": (m15.get("wind_gusts_10m") or [None])[0], | |
| "uv_index": data.get("current", {}).get("uv_index"), # UV không có ở minutely_15 | |
| } | |
| else: | |
| cur_source = "current (fallback — minutely_15 không khả dụng)" | |
| cur = data["current"] | |
| cur.setdefault("wind_gusts_10m", None) | |
| temp_c = cur["temperature_2m"] | |
| feels_c = cur["apparent_temperature"] | |
| humidity = cur["relative_humidity_2m"] | |
| wind_kmh = cur["wind_speed_10m"] | |
| gust_kmh = cur.get("wind_gusts_10m") | |
| precip = cur.get("precipitation", 0) or 0 | |
| uv = cur.get("uv_index") | |
| wcode = cur["weather_code"] | |
| desc, emoji = _WMO_CODES.get(wcode, ("Không rõ", "🌡️")) | |
| place_full = geo["name"] | |
| if geo.get("admin1") and geo["admin1"] != geo["name"]: | |
| place_full += f", {geo['admin1']}" | |
| now_str = datetime.datetime.now().strftime("%H:%M %d/%m") | |
| result = [ | |
| f"{emoji} **Thời tiết {place_full}** ({now_str}):\n", | |
| f"🌡️ Nhiệt độ hiện tại: **{temp_c}°C** (cảm giác {feels_c}°C)", | |
| f"☁️ Tình trạng: **{desc}**", | |
| f"💧 Độ ẩm: {humidity}% | 💨 Gió: {wind_kmh} km/h" + (f" (giật {gust_kmh} km/h)" if gust_kmh else ""), | |
| ] | |
| if precip and precip > 0: | |
| result.append(f"🌧️ Lượng mưa: {precip}mm") | |
| if uv is not None: | |
| result.append(f"☀️ UV index: {uv}") | |
| try: | |
| tmax_today = daily["temperature_2m_max"][0] | |
| tmin_today = daily["temperature_2m_min"][0] | |
| psum_today = (daily.get("precipitation_sum") or [None])[0] | |
| today_line = f"\n📅 Hôm nay: {tmin_today}°C – {tmax_today}°C" | |
| if psum_today: | |
| today_line += f" | 🌧️ mưa dự kiến {psum_today}mm" | |
| result.append(today_line) | |
| if len(daily["temperature_2m_max"]) > 1: | |
| tmax_tmr = daily["temperature_2m_max"][1] | |
| tmin_tmr = daily["temperature_2m_min"][1] | |
| result.append(f"📅 Ngày mai: {tmin_tmr}°C – {tmax_tmr}°C") | |
| except (KeyError, IndexError): | |
| pass | |
| # 🆕 Cảnh báo PHÂN CẤP theo mức độ nghiêm trọng thật của weather_code | |
| # — trước đây MỌI wcode>=51 (từ mưa phùn nhẹ đến dông bão dữ dội) | |
| # đều chỉ nhận 1 câu nhẹ "nhớ mang dù", nên khi có dông/bão thật | |
| # (wcode 95-99) hoặc mưa rào dữ dội (82), Yui vẫn nói nhẹ như mưa | |
| # phùn bình thường — đúng lỗi Gemini phát hiện (báo "nhiều mây" | |
| # nhẹ nhàng trong khi thực tế đang bão). Giờ tách rõ 3 cấp. | |
| if temp_c >= 35: | |
| result.append("\n⚠️ *Trời nóng — nhớ uống nước nhiều nha!*") | |
| elif temp_c <= 20: | |
| result.append("\n🧥 *Trời lạnh — mặc thêm áo vào nha!*") | |
| if wcode in (95, 96, 99) or (gust_kmh and gust_kmh >= 50): | |
| result.append("\n🚨 **CẢNH BÁO: Dông/bão — gió giật mạnh, sấm chớp. Boss hạn chế ra ngoài, tránh trú dưới cây to/cột điện!**") | |
| elif wcode in (65, 82) or (precip and precip >= 15): | |
| result.append("\n⛈️ *Mưa to/mưa rào dữ dội — nếu ra ngoài nhớ áo mưa, cẩn thận đường trơn trượt và ngập cục bộ!*") | |
| elif wcode >= 51: | |
| result.append("☂️ *Có mưa — nhớ mang dù nha!*") | |
| if uv is not None and uv >= 8: | |
| result.append("🕶️ *UV cao — che chắn kỹ khi ra ngoài nha!*") | |
| result.append(f"\n_(Nguồn: Open-Meteo, dữ liệu {cur_source})_") | |
| return "\n".join(result) | |
| except Exception as e: | |
| return f"⚠️ Không lấy được thời tiết: {e}" | |
| WEATHER_KEYWORDS = [ | |
| "thời tiết", "trời hôm nay", "nhiệt độ", | |
| "mưa không", "nắng không", "có mưa", | |
| "weather", "dự báo thời tiết", | |
| ] | |
| def detect_weather_request(text: str) -> Optional[str]: | |
| """Trả về địa điểm nếu là câu hỏi thời tiết, None nếu không.""" | |
| t = text.lower() | |
| # Cũng trigger nếu tên thành phố + từ thời tiết xuất hiện | |
| city_weather = any(city in t for city in [ | |
| "hà nội", "sài gòn", "hcm", "đà nẵng", "huế", "cần thơ", | |
| "nha trang", "đà lạt", "vũng tàu", "hải phòng", "bình dương" | |
| ]) and any(kw in t for kw in ["hôm nay", "thế nào", "ra sao", "mưa", "nắng"]) | |
| if not any(kw in t for kw in WEATHER_KEYWORDS) and not city_weather: | |
| return None | |
| # Tìm địa điểm | |
| loc_patterns = [ | |
| r'thời tiết\s+(?:ở\s+|tại\s+)?(.+?)(?:\s+hôm|$|\?|!)', | |
| r'(?:ở|tại|tại\s+thành\s+phố)\s+(.+?)(?:\s+thời tiết|\s+mưa|\s+nắng|$)', | |
| r'(.+?)\s+(?:thời tiết|trời hôm nay|có mưa|nắng không)', | |
| ] | |
| for pat in loc_patterns: | |
| m = re.search(pat, t) | |
| if m: | |
| loc = m.group(1).strip() | |
| if 2 < len(loc) < 30: | |
| # Map alias → canonical ngay tại đây | |
| loc_map_inline = { | |
| "hcm": "Ho Chi Minh City", "sài gòn": "Ho Chi Minh City", | |
| "sg": "Ho Chi Minh City", "hồ chí minh": "Ho Chi Minh City", | |
| "ho chi minh": "Ho Chi Minh City", "tphcm": "Ho Chi Minh City", | |
| "hà nội": "Hanoi", "ha noi": "Hanoi", "hn": "Hanoi", | |
| "đà nẵng": "Da Nang", "huế": "Hue", "cần thơ": "Can Tho", | |
| "nha trang": "Nha Trang", "đà lạt": "Da Lat", | |
| "vũng tàu": "Vung Tau", "hải phòng": "Hai Phong", | |
| } | |
| loc_lower2 = loc.lower() | |
| for alias2, canonical2 in loc_map_inline.items(): | |
| if alias2 in loc_lower2: | |
| return canonical2 | |
| return loc | |
| return "Ho Chi Minh City" # Mặc định | |
| # ================================================================ | |
| # MODULE 5: QR CODE — thư viện qrcode (offline, cực nhẹ) | |
| # ================================================================ | |
| def generate_qr(content: str) -> str: | |
| """ | |
| Tạo QR code từ text/link. | |
| Trả về base64 PNG để Gradio hiển thị, hoặc link QR online. | |
| """ | |
| if not content: | |
| return "⚠️ Bạn muốn tạo QR cho nội dung gì?" | |
| # Dùng API online (không cần install thêm gì) | |
| encoded = requests.utils.quote(content) | |
| qr_url = f"https://api.qrserver.com/v1/create-qr-code/?data={encoded}&size=300x300&ecc=M" | |
| return ( | |
| f"📱 **QR Code đã tạo:**\n\n" | |
| f"\n\n" | |
| f"📋 **Nội dung:** `{content[:100]}`\n" | |
| f"🔗 **Link ảnh:** {qr_url}\n\n" | |
| f"*Dùng camera quét QR ở trên nhé!*" | |
| ) | |
| QR_KEYWORDS = [ | |
| "tạo qr", "tạo mã qr", "qr code", "qrcode", | |
| "mã qr cho", "qr cho link", "qr cho text", | |
| "generate qr", "make qr", | |
| ] | |
| def detect_qr_request(text: str) -> Optional[str]: | |
| """Trả về nội dung cần tạo QR, hoặc None.""" | |
| t = text.lower() | |
| if not any(kw in t for kw in QR_KEYWORDS): | |
| return None | |
| # Tách nội dung | |
| patterns = [ | |
| r'(?:tạo qr|tạo mã qr|qr code|qr cho)\s+(?:cho\s+|link\s+|text\s+)?(.+)', | |
| r'(?:generate|make)\s+qr\s+(?:for\s+|code\s+for\s+)?(.+)', | |
| ] | |
| for pat in patterns: | |
| m = re.search(pat, t) | |
| if m: | |
| content = m.group(1).strip() | |
| if content: | |
| return content | |
| # Tìm URL trong text | |
| url_m = re.search(r'https?://\S+', text) | |
| if url_m: | |
| return url_m.group(0) | |
| return None | |
| # ================================================================ | |
| # MODULE 6: NHẬT KÝ CẢM XÚC — dùng SQLite đã có sẵn | |
| # ================================================================ | |
| _journal_db_init = False | |
| def _ensure_journal_db(): | |
| """Tạo bảng journal nếu chưa có.""" | |
| global _journal_db_init | |
| if _journal_db_init: | |
| return | |
| import sqlite3 | |
| try: | |
| conn = sqlite3.connect(_URAL_DB_PATH) | |
| conn.execute(""" | |
| CREATE TABLE IF NOT EXISTS mood_journal ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| username TEXT NOT NULL, | |
| timestamp TEXT NOT NULL, | |
| mood INTEGER NOT NULL, | |
| note TEXT, | |
| keywords TEXT | |
| ) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| _journal_db_init = True | |
| except Exception as e: | |
| print(f"⚠️ [Journal] DB init lỗi: {e}") | |
| def save_mood_entry(username: str, mood: int, note: str = "") -> str: | |
| """Lưu trạng thái cảm xúc vào nhật ký.""" | |
| _ensure_journal_db() | |
| import sqlite3 | |
| now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") | |
| try: | |
| conn = sqlite3.connect(_URAL_DB_PATH) | |
| conn.execute( | |
| "INSERT INTO mood_journal (username, timestamp, mood, note) VALUES (?,?,?,?)", | |
| (username, now, mood, note[:500]) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| mood_labels = {0: "😔 Rất buồn", 1: "😕 Buồn", 2: "😐 Bình thường", | |
| 3: "😊 Vui", 4: "🥳 Rất vui"} | |
| label = mood_labels.get(mood, "😐") | |
| return f"✅ Đã lưu nhật ký cảm xúc!\n📅 {now}\n{label}\n📝 {note[:100] if note else '(không có ghi chú)'}" | |
| except Exception as e: | |
| return f"⚠️ Lỗi lưu nhật ký: {e}" | |
| def get_mood_history(username: str, days: int = 7) -> str: | |
| """Lấy lịch sử cảm xúc 7 ngày gần nhất.""" | |
| _ensure_journal_db() | |
| import sqlite3 | |
| try: | |
| cutoff = (datetime.datetime.now() - | |
| datetime.timedelta(days=days)).strftime("%Y-%m-%d") | |
| conn = sqlite3.connect(_URAL_DB_PATH) | |
| rows = conn.execute( | |
| "SELECT timestamp, mood, note FROM mood_journal " | |
| "WHERE username=? AND timestamp>=? ORDER BY timestamp DESC LIMIT 20", | |
| (username, cutoff) | |
| ).fetchall() | |
| conn.close() | |
| if not rows: | |
| return "Chưa có nhật ký cảm xúc nào trong 7 ngày qua." | |
| mood_labels = {0: "😔", 1: "😕", 2: "😐", 3: "😊", 4: "🥳"} | |
| lines = [f"📔 **Nhật ký cảm xúc {days} ngày qua:**\n"] | |
| for ts, mood, note in rows: | |
| icon = mood_labels.get(mood, "😐") | |
| note_str = f" — {note[:60]}" if note else "" | |
| lines.append(f"{icon} `{ts}` {note_str}") | |
| # Tính trung bình | |
| avg = sum(r[1] for r in rows) / len(rows) | |
| avg_label = mood_labels.get(round(avg), "😐") | |
| lines.append(f"\n📊 Trung bình: {avg_label} ({avg:.1f}/4)") | |
| return "\n".join(lines) | |
| except Exception as e: | |
| return f"⚠️ Lỗi đọc nhật ký: {e}" | |
| JOURNAL_KEYWORDS_SAVE = [ | |
| "ghi lại cảm xúc", "lưu nhật ký", "tôi đang", | |
| "hôm nay mình", "hôm nay tôi", "cảm thấy", | |
| ] | |
| JOURNAL_KEYWORDS_READ = [ | |
| "nhật ký cảm xúc", "lịch sử cảm xúc", "cảm xúc gần đây", | |
| "tôi hay cảm thấy", "xem nhật ký", | |
| ] | |
| # ================================================================ | |
| # MODULE 7: ĐỔI ĐƠN VỊ — Logic thuần, không cần mạng | |
| # ================================================================ | |
| UNIT_CONVERSIONS = { | |
| # Độ dài | |
| ("km", "m"): 1000, ("m", "km"): 0.001, | |
| ("m", "cm"): 100, ("cm", "m"): 0.01, | |
| ("cm", "mm"): 10, ("mm", "cm"): 0.1, | |
| ("km", "mile"): 0.621371, ("mile", "km"): 1.60934, | |
| ("m", "ft"): 3.28084, ("ft", "m"): 0.3048, | |
| ("m", "inch"): 39.3701, ("inch", "m"): 0.0254, | |
| # Khối lượng | |
| ("kg", "g"): 1000, ("g", "kg"): 0.001, | |
| ("kg", "lb"): 2.20462, ("lb", "kg"): 0.453592, | |
| ("kg", "ton"): 0.001, ("ton", "kg"): 1000, | |
| # Nhiệt độ — xử lý riêng | |
| # Diện tích | |
| ("m2", "cm2"): 10000, ("cm2", "m2"): 0.0001, | |
| ("km2", "m2"): 1000000, ("m2", "km2"): 0.000001, | |
| ("ha", "m2"): 10000, ("m2", "ha"): 0.0001, | |
| # Dung tích | |
| ("l", "ml"): 1000, ("ml", "l"): 0.001, | |
| ("l", "m3"): 0.001, ("m3", "l"): 1000, | |
| # Tốc độ | |
| ("kmh", "ms"): 0.277778, ("ms", "kmh"): 3.6, | |
| ("kmh", "mph"): 0.621371, ("mph", "kmh"): 1.60934, | |
| } | |
| def convert_unit(text: str) -> Optional[str]: | |
| """ | |
| Đổi đơn vị từ câu hỏi tự nhiên. | |
| VD: "100 km bằng bao nhiêu mile?" → "100 km = 62.14 mile" | |
| """ | |
| t = text.lower() | |
| # Nhiệt độ | |
| c_to_f = re.search(r'(\d+\.?\d*)\s*°?c\s+(?:ra|sang|bằng|=|to)\s+(?:°?f|fahrenheit)', t) | |
| f_to_c = re.search(r'(\d+\.?\d*)\s*°?f\s+(?:ra|sang|bằng|=|to)\s+(?:°?c|celsius)', t) | |
| if c_to_f: | |
| c = float(c_to_f.group(1)) | |
| return f"🌡️ **{c}°C** = **{c * 9/5 + 32:.1f}°F**" | |
| if f_to_c: | |
| f = float(f_to_c.group(1)) | |
| return f"🌡️ **{f}°F** = **{(f - 32) * 5/9:.1f}°C**" | |
| # Đơn vị thông thường | |
| pattern = r'(\d+\.?\d*)\s*([a-zA-Záàảãạăắặẳẵấầẩẫậéèẻẽẹêếềểễệíìỉĩịóòỏõọôốồổỗộơớờởỡợúùủũụưứừửữựýỳỷỹỵđ²³/]+)\s+(?:ra|sang|bằng|=|to|thành)\s+([a-zA-Záàảãạ²³/]+)' | |
| m = re.search(pattern, t) | |
| if not m: | |
| # Thử pattern đơn giản hơn: "100 km sang m" | |
| m = re.search(r'(\d+\.?\d*)\s*(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)\s+(?:sang|ra|đổi|thành|=|to)\s+(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)', t) | |
| if not m: | |
| # Pattern "100km to m" | |
| m = re.search(r'(\d+\.?\d*)\s*(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)\s+.*?\s+(km|m|cm|mm|kg|g|lb|l|ml|kmh|mph|mile|ft|inch|ha|ton)\b', t) | |
| if m: | |
| try: | |
| value = float(m.group(1)) | |
| from_unit = m.group(2).strip().lower().replace('²', '2').replace('³', '3') | |
| to_unit = m.group(3).strip().lower().replace('²', '2').replace('³', '3') | |
| factor = UNIT_CONVERSIONS.get((from_unit, to_unit)) | |
| if factor: | |
| result = value * factor | |
| return f"📐 **{value} {from_unit}** = **{result:,.4f} {to_unit}**" | |
| except Exception: | |
| pass | |
| return None | |
| UNIT_KEYWORDS = [ | |
| "đổi", "convert", "bằng bao nhiêu", "ra bao nhiêu", | |
| "sang bao nhiêu", "km sang", "kg sang", "°c sang", "°f sang", | |
| "m sang", "l sang", "đổi đơn vị", | |
| ] | |
| def detect_unit_request(text: str) -> bool: | |
| t = text.lower() | |
| return any(kw in t for kw in UNIT_KEYWORDS) | |
| # ================================================================ | |
| # MODULE 8: TRA TỪ ĐIỂN — Free Dictionary API | |
| # ================================================================ | |
| def lookup_word(word: str) -> str: | |
| """Tra nghĩa từ tiếng Anh qua Free Dictionary API.""" | |
| word = word.strip().lower() | |
| try: | |
| resp = requests.get( | |
| f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}", | |
| timeout=6 | |
| ) | |
| if resp.status_code != 200: | |
| return f"⚠️ Không tìm thấy từ '{word}' trong từ điển." | |
| data = resp.json()[0] | |
| phonetic = data.get("phonetic", "") | |
| meanings = data.get("meanings", []) | |
| lines = [f"📖 **{word}** {phonetic}\n"] | |
| for m in meanings[:2]: | |
| part = m.get("partOfSpeech", "") | |
| lines.append(f"*{part}*:") | |
| for defn in m.get("definitions", [])[:2]: | |
| d = defn.get("definition", "") | |
| ex = defn.get("example", "") | |
| lines.append(f" • {d}") | |
| if ex: | |
| lines.append(f" → _{ex}_") | |
| synonyms = m.get("synonyms", [])[:3] | |
| if synonyms: | |
| lines.append(f" Từ đồng nghĩa: {', '.join(synonyms)}") | |
| lines.append("") | |
| return "\n".join(lines) | |
| except Exception as e: | |
| return f"⚠️ Lỗi tra từ: {e}" | |
| DICT_KEYWORDS = [ | |
| "tra từ", "nghĩa của", "from nghĩa là gì", | |
| "mean", "definition of", "define", | |
| "từ điển", " là gì trong tiếng anh", | |
| "tiếng anh là gì", | |
| ] | |
| def detect_dict_request(text: str) -> Optional[str]: | |
| """Trả về từ cần tra, hoặc None.""" | |
| t = text.lower() | |
| if not any(kw in t for kw in DICT_KEYWORDS): | |
| return None | |
| patterns = [ | |
| r'(?:nghĩa của|tra từ|define|definition of)\s+"?([a-zA-Z\s]+)"?', | |
| r'"?([a-zA-Z\s]+)"?\s+(?:nghĩa là gì|mean|là gì)', | |
| r'(?:từ|word)\s+"?([a-zA-Z]+)"?', | |
| ] | |
| for pat in patterns: | |
| m = re.search(pat, t) | |
| if m: | |
| word = m.group(1).strip() | |
| if 1 < len(word) < 30 and word.replace(" ", "").isalpha(): | |
| return word | |
| return None | |
| # ================================================================ | |
| # MODULE 9: QUIZ / TRÒ CHƠI — nâng cấp quiz_game cũ | |
| # ================================================================ | |
| QUIZ_BANK = { | |
| "kiến thức": [ | |
| {"q": "Thủ đô của Việt Nam là gì?", "a": "Hà Nội", "opts": ["Hà Nội", "TP.HCM", "Đà Nẵng", "Huế"]}, | |
| {"q": "Núi cao nhất Việt Nam?", "a": "Fansipan", "opts": ["Fansipan", "Bạch Mã", "Ngọc Linh", "Pu Si Lung"]}, | |
| {"q": "Sông dài nhất Việt Nam?", "a": "Sông Mekong (Cửu Long)", "opts": ["Sông Hồng", "Sông Mekong (Cửu Long)", "Sông Đà", "Sông Mã"]}, | |
| {"q": "Việt Nam có bao nhiêu tỉnh thành?", "a": "63", "opts": ["61", "63", "64", "65"]}, | |
| {"q": "ChatGPT được phát triển bởi công ty nào?", "a": "OpenAI", "opts": ["Google", "OpenAI", "Meta", "Microsoft"]}, | |
| {"q": "Đồng tiền của Nhật Bản là gì?", "a": "Yên (JPY)", "opts": ["Won", "Yên (JPY)", "Nhân dân tệ", "Đô la HK"]}, | |
| {"q": "1 km bằng bao nhiêu mét?", "a": "1000 m", "opts": ["100 m", "1000 m", "10000 m", "500 m"]}, | |
| {"q": "Hành tinh lớn nhất Hệ Mặt Trời?", "a": "Sao Mộc (Jupiter)", "opts": ["Sao Thổ", "Sao Mộc (Jupiter)", "Sao Hải Vương", "Mặt Trời"]}, | |
| ], | |
| "công nghệ": [ | |
| {"q": "CPU viết tắt của từ gì?", "a": "Central Processing Unit", "opts": ["Computer Power Unit", "Central Processing Unit", "Core Processing Utility", "Central Program Unit"]}, | |
| {"q": "HTTP viết tắt của gì?", "a": "HyperText Transfer Protocol", "opts": ["HyperText Transfer Protocol", "High Tech Transfer Protocol", "Hyper Transfer Text Protocol", "Host Text Transfer Protocol"]}, | |
| {"q": "RAM là viết tắt của?", "a": "Random Access Memory", "opts": ["Random Access Memory", "Read Access Memory", "Runtime Access Memory", "Rapid Access Module"]}, | |
| {"q": "Python được tạo ra bởi ai?", "a": "Guido van Rossum", "opts": ["James Gosling", "Guido van Rossum", "Linus Torvalds", "Dennis Ritchie"]}, | |
| ], | |
| "toán vui": [ | |
| {"q": "1 + 1 × 2 = ?", "a": "3", "opts": ["2", "3", "4", "1"]}, | |
| {"q": "Căn bậc hai của 144 = ?", "a": "12", "opts": ["11", "12", "13", "14"]}, | |
| {"q": "2^10 = ?", "a": "1024", "opts": ["512", "1024", "2048", "256"]}, | |
| {"q": "Số π (pi) xấp xỉ bằng?", "a": "3.14159", "opts": ["3.14159", "3.14169", "3.14150", "3.15149"]}, | |
| ], | |
| } | |
| _active_quiz = {} # {username: {"q": ..., "a": ..., "category": ...}} | |
| def start_quiz(username: str, category: str = "") -> str: | |
| """Bắt đầu quiz, trả về câu hỏi.""" | |
| # Chọn danh mục | |
| if not category: | |
| category = random.choice(list(QUIZ_BANK.keys())) | |
| else: | |
| for cat in QUIZ_BANK: | |
| if cat in category.lower(): | |
| category = cat | |
| break | |
| else: | |
| category = random.choice(list(QUIZ_BANK.keys())) | |
| questions = QUIZ_BANK.get(category, QUIZ_BANK["kiến thức"]) | |
| q = random.choice(questions) | |
| _active_quiz[username] = { | |
| "q": q["q"], "a": q["a"], | |
| "opts": q.get("opts", []), "category": category | |
| } | |
| opts_str = "" | |
| if q.get("opts"): | |
| opts_str = "\n\n" + "\n".join( | |
| [f" {chr(65+i)}. {opt}" for i, opt in enumerate(q["opts"])] | |
| ) | |
| return ( | |
| f"🎯 **Quiz — {category.title()}**\n\n" | |
| f"**{q['q']}**{opts_str}\n\n" | |
| f"*Trả lời để Yui chấm điểm nhé!*" | |
| ) | |
| def check_quiz_answer(username: str, answer: str) -> Optional[str]: | |
| """Kiểm tra đáp án quiz đang chờ.""" | |
| if username not in _active_quiz: | |
| return None | |
| quiz = _active_quiz[username] | |
| correct = quiz["a"] | |
| ans_lower = answer.lower().strip() | |
| correct_lower = correct.lower() | |
| # Khớp chữ cái lựa chọn (A/B/C/D) | |
| letter_match = re.match(r'^([abcd])[\.\)]?\s*$', ans_lower) | |
| if letter_match and quiz.get("opts"): | |
| idx = ord(letter_match.group(1)) - ord('a') | |
| if 0 <= idx < len(quiz["opts"]): | |
| ans_lower = quiz["opts"][idx].lower() | |
| is_correct = (correct_lower in ans_lower or ans_lower in correct_lower) | |
| del _active_quiz[username] | |
| if is_correct: | |
| return ( | |
| f"✅ **Chính xác!** 🎉\n" | |
| f"Đáp án đúng: **{correct}**\n\n" | |
| f"Muốn tiếp tục? Nói *'quiz tiếp'* nha!" | |
| ) | |
| else: | |
| return ( | |
| f"❌ **Sai rồi!**\n" | |
| f"Đáp án đúng là: **{correct}**\n\n" | |
| f"Đừng nản, thử câu khác nhé! Nói *'quiz tiếp'* nha!" | |
| ) | |
| QUIZ_KEYWORDS = [ | |
| "quiz", "đố", "câu hỏi", "trắc nghiệm", | |
| "chơi đố vui", "đố vui", "thi kiến thức", | |
| "quiz tiếp", "câu tiếp", "câu khác", | |
| ] | |
| def detect_quiz_request(text: str) -> bool: | |
| return any(kw in text.lower() for kw in QUIZ_KEYWORDS) | |
| # ================================================================ | |
| # HOOK VÀO urals_brain — xử lý tất cả module trước khi gọi AI | |
| # ================================================================ | |
| def _patch_urals_brain_mega(): | |
| """Hook vào urals_brain để xử lý tất cả module mới.""" | |
| target = None | |
| for mod in sys.modules.values(): | |
| if hasattr(mod, 'urals_brain') and hasattr(mod, 'ask_logic_brain'): | |
| target = mod | |
| break | |
| if not target: | |
| return False | |
| _orig_brain = target.urals_brain | |
| def _mega_brain(message, history, request=None): | |
| # Lấy text | |
| if isinstance(message, dict): | |
| user_text = message.get("text", "") | |
| has_file = bool(message.get("files")) | |
| else: | |
| user_text = str(message) | |
| has_file = False | |
| if has_file or not user_text.strip(): | |
| return _orig_brain(message, history, request) | |
| username = request.username if request else "guest" | |
| t = user_text.lower() | |
| # ── MODULE 1: TIN TỨC ──────────────────────────────────── | |
| news_triggers = ["tin tức", "tin mới", "tin hot", "news", "báo hôm nay", | |
| "tin thế giới", "tin công nghệ", "tin thể thao", | |
| "tin kinh doanh", "tin giải trí"] | |
| if any(kw in t for kw in news_triggers): | |
| # Tìm danh mục | |
| cat = "" | |
| for kw in ["thế giới", "quốc tế", "công nghệ", "tech", | |
| "kinh doanh", "kinh tế", "giải trí", "thể thao", "bóng đá"]: | |
| if kw in t: | |
| cat = kw | |
| break | |
| return get_news(cat, num=5) | |
| # ── MODULE 2: CRYPTO ───────────────────────────────────── | |
| coins = detect_crypto_request(user_text) | |
| if coins is not None: | |
| return get_crypto_price(coins) | |
| # ── MODULE 3: GIÁ VÀNG / TỶ GIÁ ──────────────────────── | |
| if detect_gold_request(user_text): | |
| return get_gold_price() | |
| # ── MODULE 4: THỜI TIẾT ────────────────────────────────── | |
| location = detect_weather_request(user_text) | |
| if location is not None: | |
| return get_weather_detail(location) | |
| # ── MODULE 5: QR CODE ──────────────────────────────────── | |
| qr_content = detect_qr_request(user_text) | |
| if qr_content is not None: | |
| return generate_qr(qr_content) | |
| # ── MODULE 6: NHẬT KÝ ──────────────────────────────────── | |
| if any(kw in t for kw in JOURNAL_KEYWORDS_READ): | |
| return get_mood_history(username, days=7) | |
| # ── MODULE 7: ĐỔI ĐƠN VỊ ──────────────────────────────── | |
| if detect_unit_request(user_text): | |
| result = convert_unit(user_text) | |
| if result: | |
| return result | |
| # ── MODULE 8: TỪ ĐIỂN ──────────────────────────────────── | |
| word = detect_dict_request(user_text) | |
| if word is not None: | |
| return lookup_word(word) | |
| # ── MODULE 9: QUIZ — ĐÃ TẮT (quá phiền) ───────────────── | |
| # if detect_quiz_request(user_text): ... # Disabled by admin | |
| # Không match → chạy bình thường | |
| return _orig_brain(message, history, request) | |
| target.urals_brain = _mega_brain | |
| print("[Mega Module v1.0] Hook vào urals_brain thành công!") | |
| return True | |
| class _MegaHook: | |
| def __init__(self): | |
| self._done = False | |
| def install(self): | |
| if _patch_urals_brain_mega(): | |
| self._done = True | |
| return | |
| import builtins | |
| _orig_import = builtins.__import__ | |
| outer = self | |
| def _hooked(name, *a, **kw): | |
| mod = _orig_import(name, *a, **kw) | |
| level = a[4] if len(a) >= 5 else kw.get("level", 0) | |
| if not outer._done and ("." not in str(name)) and (level == 0): | |
| try: | |
| if hasattr(mod, 'urals_brain') and hasattr(mod, 'ask_logic_brain'): | |
| if _patch_urals_brain_mega(): | |
| outer._done = True | |
| builtins.__import__ = _orig_import | |
| except Exception: | |
| pass | |
| return mod | |
| builtins.__import__ = _hooked | |
| print("[Mega Module v1.0] Đang chờ app.py load...") | |
| _hook = _MegaHook() | |
| _hook.install() | |
| print(""" | |
| +======================================================+ | |
| | URAL MEGA MODULE v1.0 | | |
| | | | |
| | 9 module — 100% free, không key, không phí ẩn: | | |
| | [1] Tin tức realtime — RSS VnExpress/TuoiTre | | |
| | [2] Giá Crypto — CoinGecko API free | | |
| | [3] Giá Vàng/USD — ExchangeRate API free | | |
| | [4] Thời tiết — wttr.in (chi tiết hơn) | | |
| | [5] QR Code — qrserver.com API free | | |
| | [6] Nhật ký cảm xúc — SQLite đã có sẵn | | |
| | [7] Đổi đơn vị — Logic thuần, offline | | |
| | [8] Từ điển EN — Free Dictionary API | | |
| | [9] Quiz / Trò chơi — 4 danh mục câu hỏi | | |
| | | | |
| | HF Free Tier: RAM +~2MB, CPU gần như không đổi | | |
| +======================================================+ | |
| """) | |
| # ================================================================ | |
| # TEST | |
| # ================================================================ | |
| if __name__ == "__main__": | |
| import asyncio | |
| print("=" * 58) | |
| print("TEST MEGA MODULE v1.0") | |
| print("=" * 58) | |
| passed = failed = 0 | |
| def check(name, result, expect_contains): | |
| global passed, failed | |
| ok = expect_contains.lower() in result.lower() | |
| print(f"{'✅' if ok else '❌'} [{name}]") | |
| if not ok: | |
| print(f" Got: {result[:80]}") | |
| failed += 1 | |
| else: | |
| passed += 1 | |
| # Module 2: Crypto detection | |
| print("\n--- Module 2: Crypto ---") | |
| check("detect btc", str(detect_crypto_request("giá bitcoin hôm nay")), "bitcoin") | |
| check("detect eth", str(detect_crypto_request("giá eth bao nhiêu")), "ethereum") | |
| check("no crypto", str(detect_crypto_request("hôm nay ăn gì")), "None") | |
| # Module 3: Gold detection | |
| print("\n--- Module 3: Gold ---") | |
| check("detect gold", str(detect_gold_request("giá vàng hôm nay")), "True") | |
| check("detect usd", str(detect_gold_request("tỷ giá usd hôm nay")), "True") | |
| check("no gold", str(detect_gold_request("tôi đói quá")), "False") | |
| # Module 4: Weather detection | |
| print("\n--- Module 4: Weather ---") | |
| check("detect weather HCM", str(detect_weather_request("thời tiết hồ chí minh")), "Ho Chi Minh") | |
| check("detect weather HN", str(detect_weather_request("trời hà nội hôm nay thế nào")), "Hanoi") | |
| check("no weather", str(detect_weather_request("ăn gì ngon không")), "None") | |
| # Module 5: QR detection | |
| print("\n--- Module 5: QR Code ---") | |
| check("detect qr url", str(detect_qr_request("tạo qr cho link https://google.com")), "google.com") | |
| check("detect qr text", str(detect_qr_request("tạo mã qr cho xin chào")), "xin chào") | |
| check("no qr", str(detect_qr_request("hôm nay thứ mấy")), "None") | |
| # Module 7: Unit conversion | |
| print("\n--- Module 7: Đổi đơn vị ---") | |
| check("km to m", convert_unit("100 km sang m"), "100,000") | |
| check("kg to lb", convert_unit("70 kg đổi ra lb"), "154") | |
| check("C to F", convert_unit("37°C sang °F"), "98") | |
| # Module 8: Dictionary detection | |
| print("\n--- Module 8: Từ điển ---") | |
| check("detect word", str(detect_dict_request("nghĩa của beautiful là gì")), "beautiful") | |
| check("detect define",str(detect_dict_request("define algorithm")), "algorithm") | |
| check("no dict", str(detect_dict_request("con mèo cute quá")), "None") | |
| # Module 9: Quiz | |
| print("\n--- Module 9: Quiz ---") | |
| q = start_quiz("test_user", "kiến thức") | |
| check("quiz start", q, "Quiz") | |
| ans = check_quiz_answer("test_user", "sai hết rồi xxx yyy zzz 999") | |
| check("quiz wrong", ans or "", "Sai") | |
| q2 = start_quiz("test_user2", "toán") | |
| check("quiz math", q2, "Quiz") | |
| correct_ans = _active_quiz.get("test_user2", {}).get("a", "") | |
| if correct_ans: | |
| ans2 = check_quiz_answer("test_user2", correct_ans) | |
| check("quiz correct", ans2 or "", "Chính xác") | |
| # WAV wrapper (từ module 6 dùng SQLite — chỉ check không crash) | |
| print("\n--- Module 6: Nhật ký ---") | |
| result = save_mood_entry("test_user", 3, "Hôm nay vui!") | |
| check("save mood", result, "✅") | |
| hist = get_mood_history("test_user", days=7) | |
| check("read mood", hist, "Hôm nay vui!") | |
| print(f"\n{'='*58}") | |
| print(f"KẾT QUẢ: {passed}/{passed+failed} ({passed/(passed+failed)*100:.0f}%)") | |
| if failed == 0: | |
| print("✅ TẤT CẢ TEST PASS!") | |
| else: | |
| print(f"⚠️ {failed} test chưa pass") | |