Spaces:
Configuration error
Configuration error
Download app.py from jonathan4e/aira-v2: direct link, hf CLI and curl.
- Browser
- Download file 28.4 kB
-
https://huggingface.co/spaces/jonathan4e/aira-v2/resolve/main/app.py
- Command line
-
hf download hf://spaces/jonathan4e/aira-v2/app.py
-
curl -L -o app.py https://huggingface.co/spaces/jonathan4e/aira-v2/resolve/main/app.py
28.4 kB
| """ | |
| 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'<div class="aira-header">' | |
| f'<div class="aira-title">\U0001f321\ufe0f {tr("en","t")}</div>' | |
| f'<div class="aira-subtitle">{tr("en","s")}</div>' | |
| f'<div class="aira-tagline">{tr("en","tag")}</div>' | |
| f'</div>' | |
| ) | |
| # 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"<footer>{tr('en','f')}</footer>") | |
| # ---------------- Helpers ---------------- | |
| def _weather_html(ct: str, w: Dict) -> str: | |
| if not w.get("ok"): | |
| return f'<div class="weather-card"><div class="weather-temp">{w.get("temp","—")}\u00b0C</div><div style="color:#6b7280">{ct} — weather unavailable</div></div>' | |
| r = wbgt_guidance(wbgt_estimate(w.get("temp"), w.get("humidity"), True), "moderate") | |
| return ( | |
| f'<div class="weather-card">' | |
| f'<div style="display:flex;justify-content:space-between;align-items:start;">' | |
| f'<div><div class="weather-temp">{w.get("temp")}\u00b0C</div>' | |
| f'<div style="font-size:.9rem;color:#4b5563">Feels {w.get("feels_like")}\u00b0C • Hum {w.get("humidity")}% • Wind {w.get("wind")} km/h • {ct}</div></div>' | |
| f'<div class="risk-badge" style="background:{r["color"]}">{r["label"]} risk</div>' | |
| f'</div></div>' | |
| ) | |
| 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'<div class="plan-card" style="border-left:5px solid {g["color"]}">' | |
| f'<div style="display:flex;justify-content:space-between;margin-bottom:.5rem;">' | |
| f'<strong>WBGT:</strong> {g["wbgt"]}°C <strong>Risk:</strong> <span class="risk-badge" style="background:{g["color"]}">{g["label"]}</span>' | |
| f'</div>' | |
| f'<div style="margin-top:.5rem;line-height:1.6;">{rep}</div>' | |
| f'</div>' | |
| ) | |
| 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) | |