""" app.py — AIRA: Arid-climate Intelligent Risk Assistant Complete Gradio app for Hugging Face Spaces. Qwen stack: Qwen2-VL-2B (vision) + Qwen3-8B-Instruct (reasoning) + Whisper-Tiny (STT) + gTTS (TTS) """ from __future__ import annotations import math import threading import requests from datetime import datetime, timezone from typing import Optional, Dict, Any, List, Tuple import gradio as gr from PIL import Image, ImageDraw # ---------------- Optional ML deps (lazy load) ---------------- try: import torch from transformers import ( Qwen2VLForConditionalGeneration, AutoProcessor, AutoModelForCausalLM, AutoTokenizer, pipeline, ) _TORCH = True except Exception: _TORCH = False try: from gtts import gTTS _GTTS = True except Exception: _GTTS = False # ---------------- Config ---------------- QATAR_CITIES: Dict[str, Dict[str, float]] = { "Doha": {"lat": 25.2867, "lon": 51.5343}, "Al Wakrah": {"lat": 25.1715, "lon": 51.6047}, "Al Khor": {"lat": 25.6819, "lon": 51.5010}, "Mesaieed": {"lat": 24.7728, "lon": 51.5045}, "Dukhan": {"lat": 25.4219, "lon": 50.7531}, "Al Rayyan": {"lat": 25.2333, "lon": 51.4333}, "Lusail": {"lat": 25.3628, "lon": 51.5010}, } LANGS: Dict[str, Dict[str, Any]] = { "en": {"name": "English", "tts": "en", "rtl": False}, "ar": {"name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "tts": "ar", "rtl": True}, "hi": {"name": "\u0939\u093f\u0928\u094d\u0926\u093f", "tts": "hi", "rtl": False}, "ur": {"name": "\u0627\u0631\u062f\u0648", "tts": "ur", "rtl": False}, } VISION_MODEL_ID = "Qwen/Qwen2-VL-2B-Instruct" REASON_MODEL_ID = "Qwen/Qwen3-4B-Instruct" # Qwen3 reasoning engine ASR_MODEL_ID = "openai/whisper-tiny" VISION_PROMPT = ( "You are AIRA, a workplace heat-safety assistant for Qatar outdoor workers. " "Inspect the image for heat-stress indicators: sweat, flushed/pale skin, dizziness posture, " "lack of shade, direct sun on workers, missing water, heavy PPE, hot surfaces, unsafe conditions. " "Classify into: Safe_Shaded, Direct_Sun_Exposure, High_Physical_Exertion, Visible_Heat_Distress. " "Be concise, practical, safety-first. Reply in the user's language." ) QATAR_SYSTEM_PROMPT = ( "You are AIRA, a heat-safety reasoning engine for Qatar outdoor workers, aligned with Qatar " "Ministry of Public Health (MoPH) Wet Bulb Globe Temperature (WBGT) guidelines. " "Given temperature, humidity, work intensity, and vision findings, output: " "(1) WBGT estimate, (2) Heat risk level (Low/Moderate/High/Extreme), " "(3) Hydration plan (Litres per hour), (4) Work-rest cycle (minutes work / minutes rest in shade), " "(5) Immediate safety actions in Arabic, Hindi, Urdu, or English as requested. " "Be concise, actionable, and safety-first." ) # Model singletons (lazy, thread-safe) _lock = threading.Lock() _vm = None _vp = None _rm = None _rt = None _asr = None def _load_vision(): global _vm, _vp with _lock: if _vm is None and _TORCH: _vm = Qwen2VLForConditionalGeneration.from_pretrained( VISION_MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True, ) _vp = AutoProcessor.from_pretrained(VISION_MODEL_ID, trust_remote_code=True) return _vm, _vp def _load_reason(): global _rm, _rt with _lock: if _rm is None and _TORCH: _rm = AutoModelForCausalLM.from_pretrained( REASON_MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True, ) _rt = AutoTokenizer.from_pretrained(REASON_MODEL_ID, trust_remote_code=True) return _rm, _rt def _load_asr(): global _asr with _lock: if _asr is None and _TORCH: _asr = pipeline( "automatic-speech-recognition", model=ASR_MODEL_ID, device=0 if torch.cuda.is_available() else -1, ) return _asr # ---------------- Weather (Open-Meteo, free, no key) ---------------- def get_qatar_weather(city: str = "Doha") -> Dict[str, Any]: c = QATAR_CITIES.get(city, QATAR_CITIES["Doha"]) try: r = requests.get( "https://api.open-meteo.com/v1/forecast", params={ "latitude": c["lat"], "longitude": c["lon"], "current": "temperature_2m,relative_humidity_2m,apparent_temperature,wind_speed_10m", "timezone": "Asia/Qatar", }, timeout=15, ) r.raise_for_status() d = r.json()["current"] return { "city": city, "temp": d.get("temperature_2m"), "feels_like": d.get("apparent_temperature"), "humidity": d.get("relative_humidity_2m"), "wind": d.get("wind_speed_10m"), "ok": True, } except Exception as e: return {"city": city, "temp": 44, "feels_like": 52, "humidity": 45, "wind": 12, "ok": False, "e": str(e)} # ---------------- WBGT / Safety Engine (Qatar MoPH aligned) ---------------- def _wet_bulb(temp_c: float, rh: float) -> float: """Stull approximation of wet-bulb temperature (°C).""" t = temp_c h = rh tw = ( t * math.atan(0.151977 * math.sqrt(h + 8.313659)) + math.atan(t + h) - math.atan(h - 1.676331) + 0.00391838 * h ** (3 / 2) * math.atan(0.023101 * h) - 4.686035 ) return tw def wbgt_estimate(temp_c: float, rh: float, sun_exposed: bool = True) -> float: """Estimate WBGT (°C). If sun-exposed, add solar radiation load (~+2 to +4°C globe).""" tw = _wet_bulb(temp_c, rh) # Simplified WBGT = 0.7*Tw + 0.2*Tg + 0.1*T ; approximate Tg ≈ T + solar load tg = temp_c + (3.0 if sun_exposed else 1.0) wbgt = 0.7 * tw + 0.2 * tg + 0.1 * temp_c return round(wbgt, 1) def wbgt_guidance(wbgt: float, intensity: str = "moderate") -> Dict[str, Any]: """Qatar MoPH-style work/rest & hydration guidance from WBGT.""" if wbgt < 29: level, label, color = "low", "Low", "#22c55e" work, rest = 60, 0 water = 0.5 elif wbgt < 31: level, label, color = "moderate", "Moderate", "#eab308" work, rest = 45, 15 water = 0.75 elif wbgt < 33: level, label, color = "high", "High", "#f97316" work, rest = 30, 30 water = 1.0 else: level, label, color = "extreme", "Extreme", "#ef4444" work, rest = 0, 60 water = 1.25 # Adjust for intensity mult = {"light": 0.9, "moderate": 1.0, "heavy": 1.2}.get(intensity, 1.0) water = round(water * mult, 2) return { "wbgt": wbgt, "level": level, "label": label, "color": color, "work_min": work, "rest_min": rest, "water_l_hr": water, "intensity": intensity, } def safety_report(temp: float, hum: float, intensity: str, vision_text: str, lang: str) -> str: """Generate localized safety report using Qwen3 reasoning (fallback to template).""" wbgt = wbgt_estimate(temp, hum, sun_exposed="direct sun" in vision_text.lower()) guidance = wbgt_guidance(wbgt, intensity) rm, rt = _load_reason() if rm is not None and rt is not None: try: user_msg = ( f"Temperature: {temp}°C, Humidity: {hum}%, WBGT: {wbgt}°C, " f"Intensity: {intensity}, Vision: {vision_text}. " f"Give a concise heat-safety report in {lang}." ) messages = [ {"role": "system", "content": QATAR_SYSTEM_PROMPT}, {"role": "user", "content": user_msg}, ] text = rt.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = rt(text=[text], return_tensors="pt").to(rm.device) out = rm.generate(**inputs, max_new_tokens=300, do_sample=False) result = rt.batch_decode(out, skip_special_tokens=True)[0] if "assistant" in result: result = result.split("assistant")[-1].strip() return result or _template_report(guidance, vision_text, lang) except Exception as e: return f"(Qwen3 reasoning unavailable: {e})\n\n" + _template_report(guidance, vision_text, lang) return _template_report(guidance, vision_text, lang) def _template_report(guidance: Dict, vision_text: str, lang: str) -> str: templates = { "en": ( f"**WBGT:** {guidance['wbgt']}°C | **Risk:** {guidance['label']} \n" f"**Hydration:** {guidance['water_l_hr']} L/hr | **Work:** {guidance['work_min']} min / " f"**Rest:** {guidance['rest_min']} min in shade \n" f"**Vision findings:** {vision_text} \n" f"**Action:** {guidance['note'] if 'note' in guidance else 'Monitor worker closely.'}" ), "ar": ( f"**WBGT:** {guidance['wbgt']}°C | **المخاطر:** {guidance['label']} \n" f"**الترطيب:** {guidance['water_l_hr']} لتر/ساعة | **العمل:** {guidance['work_min']} دقيقة / " f"**الراحة:** {guidance['rest_min']} دقيقة في الظل \n" f"**ملاحظات الرؤية:** {vision_text}" ), "hi": ( f"**WBGT:** {guidance['wbgt']}°C | **जोखिम:** {guidance['label']} \n" f"**हाइड्रेशन:** {guidance['water_l_hr']} L/घंटा | **काम:** {guidance['work_min']} मिनट / " f"**आराम:** {guidance['rest_min']} मिनट छांव में \n" f"**विज़न निष्कर्ष:** {vision_text}" ), "ur": ( f"**WBGT:** {guidance['wbgt']}°C | **خطرہ:** {guidance['label']} \n" f"**پانی:** {guidance['water_l_hr']} L/گھنٹہ | **کام:** {guidance['work_min']} منٹ / " f"** आरام:** {guidance['rest_min']} منٹ سائے میں \n" f"**بینائی کے نتائج:** {vision_text}" ), } return templates.get(lang, templates["en"]) # ---------------- Vision analysis (Qwen2-VL) ---------------- def analyze_image(image: Image.Image, lang: str = "en") -> str: vm, vp = _load_vision() if vm is None or vp is None: return _vision_fallback(image, lang) try: messages = [ { "role": "user", "content": [ {"type": "image", "image": image}, {"type": "text", "text": VISION_PROMPT}, ], } ] text = vp.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = vp(text=[text], images=[image], padding=True, return_tensors="pt").to(vm.device) out = vm.generate(**inputs, max_new_tokens=256, do_sample=False) result = vp.batch_decode(out, skip_special_tokens=True)[0] if "assistant" in result: result = result.split("assistant")[-1].strip() return result or _vision_fallback(image, lang) except Exception as e: return f"(Vision error: {e})\n\n" + _vision_fallback(image, lang) def _vision_fallback(image: Image.Image, lang: str) -> str: wd, ht = image.size msgs = { "en": f"Vision fallback ({wd}x{ht}). Check sweat, skin, shade, water. If unwell, stop & cool down.", "ar": f"\u0628\u062f\u064a\u0644 \u0627\u0644\u0631\u0624\u064a\u0629 ({wd}\u00d7{ht}). \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0639\u0631\u0642, \u0627\u0644\u0628\u0634\u0627\u0631\u0629, \u0627\u0644\u0638\u0644, \u0627\u0644\u0645\u0627\u0621.", "hi": f"\u0935\u093f\u091c\u094d\u091e\u093e \u092c\u093e\u0902\u0921 ({wd}\u00d7{ht}). \u092a\u0938\u094d\u0925\u093e\u0928, \u0924\u094d\u092f\u093e\u0916, \u091b\u092a\u093e\u0930, \u092a\u093e\u0928\u093e \u091c\u093e\u0924\u094d\u092e\u093e.", "ur": f"\u0628\u06cc\u0646\u0627\u0626\u06cc \u0645\u062a\u0628\u0627\u062f\u0644 ({wd}\u00d7{ht}). \u067e\u0633\u06cc\u0646\u0627, \u062c\u0644\u062f, \u0633\u0627\u064a\u0647, \u067e\u0627\u0646\u06cc \u062a\u0627\u0645 \u06a9\u06cc\u0646.", } return msgs.get(lang, msgs["en"]) # ---------------- Voice: STT + TTS ---------------- def transcribe(audio_path: Optional[str], lang: str) -> str: if not audio_path: return "" asr = _load_asr() if asr is None: return "(Whisper unavailable — type instead.)" try: res = asr(audio_path, generate_kwargs={"language": lang if lang == "en" else None}) return res.get("text", "").strip() except Exception as e: return f"(STT error: {e})" def tts_to_file(text: str, lang: str) -> Optional[str]: if not text or not _GTTS: return None try: path = "/tmp/aira_tts.mp3" gTTS(text=text, lang=LANGS.get(lang, {}).get("tts", "en")).save(path) return path except Exception: return None # ---------------- UI strings ---------------- UI: Dict[str, Dict[str, str]] = { "en": { "t": "AIRA v2", "s": "Voice • Vision • Safety Copilot — Qatar Heat Workers", "tag": "Qwen2-VL • Qwen3 Reasoning • Whisper STT • gTTS • HF Spaces", "w": "Live Qatar Weather", "c": "City", "rf": "Refresh", "v": "Vision Scan", "vo": "Voice Copilot", "sf": "Safety Planner", "vp": "Concern? (optional)", "vb": "Analyze Image", "vb2": "Record & Translate", "tts": "Speak Safety Advice", "sn": "Hydration & Work-Rest Plan", "wt": "Weight (kg)", "wi": "Work intensity", "l": "Light", "m": "Moderate", "h": "Heavy", "lg": "Language", "st": "Temp (°C)", "hu": "Humidity (%)", "wbgt": "WBGT & Safety", "f": "Qatar Vision 2030 • Quality of Life • Open Source", }, "ar": { "t": "أيرا v2", "s": "مساعد صوت وبصر وأمان — عمال حرارة قطر", "tag": "كيوين2-في إل • كيوين3 reasoning • ويسبر • جي تي تي إس • هجنج فيس سبيسز", "w": "\u0627\u0644\u0637\u0642\u0633", "c": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629", "rf": "\u062a\u062d\u062f\u064a\u062f", "v": "\u062a\u062d\u0644\u064a\u0644 \u0628\u0635\u0631\u064a", "vo": "\u0627\u0644\u0635\u0648\u062a", "sf": "\u0627\u0644\u0623\u0645\u0646", "vp": "\u0645\u0627\u0630\u0627 \u064a\u0642\u0644\u0642\u0643? ", "vb": "\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0635\u0648\u0627\u0631", "vb2": "\u062a\u0633\u062c\u064a\u0644 \u0648\u062a\u0631\u062c\u0645\u0629", "tts": "\u0627\u0633\u062a\u0645\u0639 \u0644\u0644\u0646\u0635\u064a\u062d\u0629", "sn": "\u062e\u0637\u0629 \u0627\u0644\u062a\u0631\u0637\u064a\u0628 \u0648\u0627\u0644\u0648\u0642\u0641", "wt": "\u0627\u0644\u0648\u0632\u0646 (\u0643\u063a)", "wi": "\u0634\u062f\u0629 \u0627\u0644\u0639\u0645\u0644", "l": "\u062e\u0641\u064a\u0641", "m": "\u0645\u062a\u0648\u0633\u0637", "h": "\u062b\u0642\u064a\u0644", "lg": "\u0627\u0644\u0644\u063a\u0629", "st": "\u062f\u0631\u062c\u0629 \u0627\u0644\ud83e\udd21\ufe0f", "hu": "\u0646\u0633\u0628\u0629 \u0627\u0644\u0647\u0648\u0627\u0621 (%)", "wbgt": "\u0648\u0628\u062c\u064a\u0634\u064a \u0648\u0627\u0644\u0623\u0645\u0646", "f": "\u0631\u0624\u064a\u0629 \u0642\u0637\u0631 2030 \u2022 \u0645\u062d\u062a\u0648\u0649 \u0627\u0644\u062d\u064a\u0627\u0629 \u2022 \u0645\u0641\u062a\u0648\u062d \u0627\u0644\u0645\u062d\u062f\u0648", }, "hi": { "t": "आईरा v2", "s": "आवाज़ • दृष्टि • सुरक्षा सहायक — कतर के गर्मी श्रमिक", "tag": "क्यूइन2-वीएल • क्यूइन3 तर्क • व्हिस्पर • जीटीटीएस • एचएफ स्पेस", "w": "\u092e\u094c\u092e\u093e", "c": "\u0936\u0939\u0930", "rf": "\u0924\u093e\u091a\u093e\u0921\u093c", "v": "\u0935\u093f\u091c\u094d\u091e\u093e \u0938\u0915\u092c\u0942", "vo": "\u0906\u0935\u094d\u092f\u093e", "sf": "\u092f\u0947\u0928\u094d\u0927\u0915\u093e\u0930\u094d\u091c", "vp": "\u091a\u094d\u092f\u093e\u0924\u093f? ", "vb": "\u091b\u093e\u0935\u093f \u092a\u094d\u0930\u0935\u093e\u0928", "vb2": "\u0930\u093f\u0902\u0915\u094d\u0917 \u640 \u0905\u0928\u0941\u0926\u093e\u0921", "tts": "\u0938\u092e\u093e \u0938\u0942\u0928\u093e", "sn": "\u091c\u0932\u094d\u092f\u094b\u092b\u094c\u0928 \u2022 \u0935\u093e\u0930\u094d\u092f\u093e-\u0935\u094d\u0930\u092e\u093f\u0930\u094d\u092e\u093e", "wt": "\u0935\u0928\u0928 (\u0915\u093f\u0902\u0917)", "wi": "\u0915\u093e\u0930\u094d\u0924 \u0924\u093f\u0935\u093e\u091f", "l": "\u0939\u092e\u0932", "m": "\u092e\u0902\u0921\u0935\u094d\u092e\u094d\u092e", "h": "\u092d\u093e\u0930\u0940", "lg": "\u092d\u093e\u0907\u091c", "st": "\u0926\u094d\u0935\u093e\u091f (\u092e\u094d\u092e\u093e\u0902)", "hu": "\u0928\u094d\u092f\u093e\u0930\u094d\u0925\u093e\u092f (%)", "wbgt": "डब्ल्यूबीजीटी और सुरक्षा", "f": "\u0915\u093e\u0924\u0930 \u0935\u093f\u0936\u094d\u0935\u0935\u093e\u0928 2030 \u2022 \u091c\u0940\u092e\u093e\u0928 \u0917\u0941\u0928\u093f\u092e \u2022 \u0914\u092a\u0923 \u0938\u094c\u092e\u0938", }, "ur": { "t": "ایرا v2", "s": "آواز • بینائی • حفاظتی معاون — گرمی کے مزدور", "tag": "کوئیں2-وی ایل • کوئیں3 استدلال • ويسپر • جی ٹی ٹی ایس • ایچ ایف سلیٹس", "w": "\u0645\u0648\u0633\u0645", "c": "\u0634\u0647\u0631", "rf": "\u062a\u0627\u0632\u0647 \u0643\u06cc\u064a\u0627", "v": "\u0628\u06cc\u0646\u0627\u0626\u06cc", "vo": "\u0622\u0627\u0632", "sf": "\u067e\u0644\u0627\u0646 \u0627\u0645\u0646", "vp": "\u06a9\u06cc\u0627 \u0646\u06cc\u06ba\u0634? ", "vb": "\u062a\u0633\u0648\u06cc\u0631 \u0635\u0648\u06cc\u0631", "vb2": "\u0631\u06cc\u06a9\u0631\u0627\u062f \u0648 \u062a\u0631\u062c\u0645\u0629", "tts": "\u0645\u0634\u0648\u0631\u0647 \u0633\u0646\u06cc\u0646", "sn": "\u067e\u0627\u0646\u06cc \u0648 \u0648\u0642\u0641\u0647", "wt": "\u0648\u0632\u0646 (\u06a9\u06cc\u0644\u0648\u063a\u0631\u0627\u0645)", "wi": "\u06a9\u0627\u0645 \u0643\u06cc \u0634\u062f\u062a", "l": "\u0647\u0644\u0643\u0627", "m": "\u0645\u062a\u0648\u0633\u0648\u062a", "h": "\u0628\u062d\u0627\u0631\u06cc", "lg": "\u0632\u0628\u0627\u0646", "st": "درجہ حرارت (\u0628\u0648)", "hu": "نمی (\u0641\u06ccصد)", "wbgt": "وی بی جی ٹی اور حفاظت", "f": "\u0642\u0637\u0631 \u0648\u06cc\u0632\u06cc\u0646 2030 \u2022 \u0632\u06cc\u0646\u06af\u06cc \u0643\u0627 \u0645\u06cc\u0627\u0631 \u2022 \u0627\u0648\u067e\u0646 \u0633\u0648\u0631\u0633", }, } def tr(lang: str, k: str) -> str: return UI.get(lang, UI["en"]).get(k, UI["en"][k]) # ---------------- CSS ---------------- CSS = """ :root{--deep:#78350f;--desert:#d97706;--maroon:#8d2f3a;} .gradio-container{font-family:system-ui,sans-serif;background:linear-gradient(135deg,#fdf3e3 0%,#e8d4b3 60%,#d97706 100%);min-height:100vh} body[dir=rtl],.gradio-container[dir=rtl]{direction:rtl} .aira-header{text-align:center;padding:2rem 1rem 1rem;border-bottom:3px solid var(--maroon);margin-bottom:1rem} .aira-title{font-size:2.5rem;font-weight:800;color:var(--deep);margin:0} .aira-subtitle{font-size:1.1rem;color:#374151;margin:.25rem 0 .5rem} .aira-tagline{font-size:.9rem;color:var(--desert);font-weight:600;text-transform:uppercase;letter-spacing:1px} .weather-card{background:rgba(255,255,255,.85);border-radius:16px;padding:1.25rem;box-shadow:0 10px 25px -5px rgba(0,0,0,.1);margin-bottom:1rem;border-left:5px solid var(--maroon)} .weather-temp{font-size:3rem;font-weight:900;color:var(--deep)} .risk-badge{display:inline-block;padding:.35rem 1rem;border-radius:999px;font-weight:800;font-size:.95rem;color:#fff} .plan-card{background:rgba(255,255,255,.85);border-radius:16px;padding:1.25rem;box-shadow:0 10px 25px -5px rgba(0,0,0,.1);margin-bottom:1rem} footer{text-align:center;padding:1.5rem;color:#6b7280;font-size:.85rem;border-top:1px solid rgba(255,255,255,.3);margin-top:2rem} @media(max-width:640px){.aira-title{font-size:1.75rem}.weather-temp{font-size:2.25rem}} # ---------------- Gradio App ---------------- def build_app() -> gr.Blocks: with gr.Blocks(title="AIRA v2", css=CSS, theme=gr.themes.Soft(primary_hue="orange")) as demo: # State lang = gr.State("en") city = gr.State("Doha") ws = gr.State({}) guidance = gr.State({}) # Header gr.HTML( f'
' f'
\U0001f321\ufe0f {tr("en","t")}
' f'
{tr("en","s")}
' f'
{tr("en","tag")}
' f'
' ) # Weather row with gr.Row(equal_height=True): cd = gr.Dropdown(choices=list(QATAR_CITIES), value="Doha", label=tr("en","c"), scale=1) rb = gr.Button(tr("en","rf"), variant="primary", scale=0) wd = gr.HTML(value=_weather_html("Doha", get_qatar_weather("Doha"))) # Tabs with gr.Tabs(): # --- Vision Tab --- with gr.TabItem(tr("en","v"), id="vision"): with gr.Row(): with gr.Column(scale=1): vi = gr.Image(type="pil", label="Worker / job-site photo") vp2 = gr.Textbox(label=tr("en","vp"), placeholder="e.g. sweating, no shade...", lines=2) vb = gr.Button(tr("en","vb"), variant="primary") with gr.Column(scale=1): vo = gr.Markdown(label="Aira Vision Assessment") # --- Voice Tab --- with gr.TabItem(tr("en","vo"), id="voice"): with gr.Row(): with gr.Column(scale=1): va = gr.Audio(sources=["microphone"], type="filepath", label="Speak AR/HI/UR/EN") vb2 = gr.Button(tr("en","vb2"), variant="primary") vt = gr.Textbox(label="Transcribed", lines=3, interactive=False) with gr.Column(scale=1): tb = gr.Button(tr("en","tts"), variant="secondary") ta = gr.Audio(label="Safety audio (auto-play)", autoplay=True) # --- Safety Tab --- with gr.TabItem(tr("en","sf"), id="safety"): with gr.Row(): with gr.Column(scale=1): st_in = gr.Number(label=tr("en","st"), value=40, minimum=0, maximum=60, step=1) hu_in = gr.Number(label=tr("en","hu"), value=50, minimum=0, maximum=100, step=1) wi_in = gr.Radio([tr("en","l"), tr("en","m"), tr("en","h")], value=tr("en","m"), label=tr("en","wi")) sl_in = gr.Dropdown([f"{x['name']} ({k})" for k, x in LANGS.items()], value="English (en)", label=tr("en","lg")) with gr.Column(scale=1): rh_out = gr.HTML(label=tr("en","wbgt")) ph_out = gr.HTML(label=tr("en","sn")) gr.HTML(f"") # ---------------- Helpers ---------------- def _weather_html(ct: str, w: Dict) -> str: if not w.get("ok"): return f'
{w.get("temp","—")}\u00b0C
{ct} — weather unavailable
' r = wbgt_guidance(wbgt_estimate(w.get("temp"), w.get("humidity"), True), "moderate") return ( f'
' f'
' f'
{w.get("temp")}\u00b0C
' f'
Feels {w.get("feels_like")}\u00b0C • Hum {w.get("humidity")}% • Wind {w.get("wind")} km/h • {ct}
' f'
{r["label"]} risk
' f'
' ) def _safety_html(temp, hum, intensity, lang_code, vision_text=""): wbgt = wbgt_estimate(temp, hum, sun_exposed="direct sun" in vision_text.lower()) g = wbgt_guidance(wbgt, intensity) rep = safety_report(temp, hum, intensity, vision_text, lang_code) html = ( f'
' f'
' f'WBGT: {g["wbgt"]}°C Risk: {g["label"]}' f'
' f'
{rep}
' f'
' ) return html, g # ---------------- Event handlers ---------------- def oref(c): w = get_qatar_weather(c) ws.value = w return _weather_html(c, w) def olg(ch): lang.value = ch.split("(")[-1].strip(")") if "(" in ch else "en" def ov(img, pr, l_code, demo_flag): if demo_flag or img is None: return _vision_fallback(Image.new("RGB", (400, 300), (200, 200, 200)), l_code) return analyze_image(img, l_code) def ovc(audio, l_code): tx = transcribe(audio, l_code) w = ws.value or get_qatar_weather(city.value or "Doha") g = guidance.value or wbgt_guidance(wbgt_estimate(w.get("temp"), w.get("humidity"), True), "moderate") rep = safety_report(w.get("temp"), w.get("humidity"), g["intensity"], tx or "no voice input", l_code) if tx: rep = f'You said: "{tx}"\n\n{rep}' tts_path = tts_to_file(rep, l_code) return rep, tts_path def osafety(temp, hum, wi_label, lang_choice): l_code = lang_choice.split("(")[-1].strip(")") if "(" in lang_choice else "en" lang.value = l_code intensity = {"light": "light", "moderate": "moderate", "heavy": "heavy"}.get( {tr("en","l"):"light", tr("en","m"):"moderate", tr("en","h"):"heavy"}.get(wi_label, "moderate"), "moderate" ) html, g = _safety_html(temp, hum, intensity, l_code) guidance.value = g return html def ovoice_tts(txt, l_code): return tts_to_file(txt, l_code) # Wiring rb.click(oref, [cd], [wd]) cd.change(oref, [cd], [wd]) sl_in.change(olg, [sl_in], []) vb.click(ov, [vi, vp2, lang, gr.Checkbox(value=False, visible=False)], [vo]) vb2.click(ovc, [va, lang], [vt, ta]) tb.click(ovoice_tts, [vt, lang], [ta]) st_in.change(osafety, [st_in, hu_in, wi_in, sl_in], [rh_out]) hu_in.change(osafety, [st_in, hu_in, wi_in, sl_in], [rh_out]) wi_in.change(osafety, [st_in, hu_in, wi_in, sl_in], [rh_out]) sl_in.change(osafety, [st_in, hu_in, wi_in, sl_in], [rh_out]) demo.load(osafety, [st_in, hu_in, wi_in, sl_in], [rh_out]) return demo if __name__ == "__main__": build_app().launch(server_name="0.0.0.0", server_port=7860)