Download backend/voice_copilot.py from bapi143/CropAI: direct link, hf CLI and curl.
- Browser
- Download file 18.9 kB
-
https://huggingface.co/spaces/bapi143/CropAI/resolve/main/backend/voice_copilot.py
- Command line
-
hf download hf://spaces/bapi143/CropAI/backend/voice_copilot.py
-
curl -L -o voice_copilot.py https://huggingface.co/spaces/bapi143/CropAI/resolve/main/backend/voice_copilot.py
18.9 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| CropAI Vernacular Multilingual Voice AI Copilot Engine | |
| Empowers illiterate and semi-literate Indian smallholders with voice-first interaction | |
| in Chhattisgarhi (छत्तीसगढ़ी), Rural Hindi (देसी हिंदी), and Bhojpuri. | |
| Features: | |
| 1. Streaming Agrarian Audio Recognition (ASR) | |
| 2. Vernacular Domain Terminology Normalization (करपा, माहू, उकठा, मुरड़ा, चुर्रा) | |
| 3. Gemini 3.8 Flash Conversational Agronomist Persona | |
| 4. Knapsack 16L/20L Tank-Calibrated Audio Advisory | |
| 5. Offline-Resilient Local Voice Heuristics | |
| """ | |
| from typing import Dict, Any, Optional, List | |
| import logging | |
| import base64 | |
| import os | |
| import re | |
| import json | |
| import httpx | |
| logger = logging.getLogger("cropai.voice_copilot") | |
| # ========================================================================= | |
| # Vernacular Agricultural Dialects Catalog | |
| # ========================================================================= | |
| SUPPORTED_DIALECTS: Dict[str, Dict[str, Any]] = { | |
| "ch-IN": { | |
| "code": "ch-IN", | |
| "name": "Chhattisgarhi", | |
| "native_name": "छत्तीसगढ़ी (सुग्घर गोठ-बात)", | |
| "region": "Chhattisgarh (Surguja, Bilaspur, Raipur, Bastar, Durg)", | |
| "sample_prompts": [ | |
| "हमार धान म करपा धर ले हे, का दवाई छिड़कन?", | |
| "चना म इल्ली लटके हे, कइसे बचाइबो?", | |
| "टमाटर म मुरड़ा रोग लागे हे, का उपाय हे?" | |
| ], | |
| "greeting": "जय जोहार किसान भाई! बताओ आज तोर खेत म का परेशानी हे?", | |
| "vocabulary": { | |
| "करपा": "Rice Blast (झुलसा)", | |
| "माहू": "Brown Plant Hopper / Aphid (माहू कीट)", | |
| "कांदो": "Rice Gall Midge (सिल्वर शूट)", | |
| "चुर्रा": "Thrips Infestation (पत्ती मुड़ना)", | |
| "मुरड़ा": "Leaf Curl Virus (चुर्रा-मुरड़ा)", | |
| "उकठा": "Fusarium Wilt (पौधा सूखना)", | |
| "इल्ली": "Pod Borer / Caterpillar (फली छेदक)", | |
| "गोभ के कीरा": "Yellow Stem Borer (तना छेदक)", | |
| "खातू": "Fertilizer / Compost (गोबर खाद / उर्वरक)", | |
| "दवाई": "Pesticide / Fungicide (दवा)", | |
| "पंप": "16-Liter Knapsack Sprayer (स्प्रेयर पंप)" | |
| } | |
| }, | |
| "hi-IN": { | |
| "code": "hi-IN", | |
| "name": "Rural Hindi", | |
| "native_name": "देसी हिंदी (सरल ग्रामीण बोली)", | |
| "region": "Pan-India Hindi Belt (UP, MP, Bihar, Rajasthan, Haryana)", | |
| "sample_prompts": [ | |
| "धान की पत्तियों पर कत्थई धब्बे हैं, क्या करें?", | |
| "गेहूं में पीला रतुआ लग गया है, कौन सा स्प्रे करें?", | |
| "16 लीटर के पंप में कितनी दवा मिलानी है?" | |
| ], | |
| "greeting": "राम-राम किसान भाई! मैं आपका CropAI साथी हूँ। अपनी फसल की समस्या बोलकर बताएं।", | |
| "vocabulary": { | |
| "झुलसा": "Paddy Blast (पत्ती झुलसना)", | |
| "खैरा": "Zinc Deficiency (खैरा रोग)", | |
| "माहू": "Aphids / BPH (माहू कीट)", | |
| "पीला रतुआ": "Yellow Stripe Rust", | |
| "तना छेदक": "Stem Borer Larva", | |
| "सफेद मक्खी": "Whitefly Vector", | |
| "स्प्रेयर": "16L Knapsack Sprayer" | |
| } | |
| }, | |
| "bho-IN": { | |
| "code": "bho-IN", | |
| "name": "Bhojpuri", | |
| "native_name": "भोजपुरी (माटी के बोली)", | |
| "region": "Eastern UP, Western Bihar, Northern Jharkhand", | |
| "sample_prompts": [ | |
| "धान के पतई सूख रहल बा, कवन दवाई छिड़कीं?", | |
| "टमाटर के फलवा सड़ रहल बा, का करीं?", | |
| "पंप में केतना दवाई डाले के होई?" | |
| ], | |
| "greeting": "प्रणाम किसान भाई! राउर CropAI साथी हाजिर बा। बताईं खेत में का दिक्कत बा?", | |
| "vocabulary": { | |
| "पतई": "Leaves (पत्तियां)", | |
| "झुलसा": "Blast / Blight", | |
| "कीरा": "Insect Pest", | |
| "दवाई": "Crop Protection Chemical", | |
| "पंप": "Knapsack Sprayer" | |
| } | |
| } | |
| } | |
| # ========================================================================= | |
| # Dialect Normalization & Agronomic Mapping | |
| # ========================================================================= | |
| def normalize_vernacular_text(text: str, dialect: str = "hi-IN") -> Dict[str, Any]: | |
| """ | |
| Normalizes local vernacular speech transcript into standardized agronomic entities. | |
| Identifies crop, pathogen, symptom, and knapsack sprayer references. | |
| """ | |
| clean_text = text.strip() | |
| detected_crop = "धान (Rice)" | |
| detected_pathology = None | |
| identified_terms = [] | |
| # Detect crop | |
| if any(w in clean_text for w in ["धान", "dhan", "rice", "चावल"]): | |
| detected_crop = "धान (Rice)" | |
| elif any(w in clean_text for w in ["चना", "chana", "gram", "chickpea"]): | |
| detected_crop = "चना (Chickpea)" | |
| elif any(w in clean_text for w in ["टमाटर", "tamatar", "tomato"]): | |
| detected_crop = "टमाटर (Tomato)" | |
| elif any(w in clean_text for w in ["गेहूं", "gehu", "wheat"]): | |
| detected_crop = "गेहूं (Wheat)" | |
| elif any(w in clean_text for w in ["सरसों", "sarson", "mustard"]): | |
| detected_crop = "सरसों (Mustard)" | |
| elif any(w in clean_text for w in ["सोयाबीन", "soybean"]): | |
| detected_crop = "सोयाबीन (Soybean)" | |
| elif any(w in clean_text for w in ["मिर्च", "mirch", "chilli"]): | |
| detected_crop = "मिर्च (Chilli)" | |
| # Detect vernacular dialect terms | |
| vocab = SUPPORTED_DIALECTS.get(dialect, SUPPORTED_DIALECTS["hi-IN"])["vocabulary"] | |
| for local_term, scientific_term in vocab.items(): | |
| if local_term in clean_text: | |
| identified_terms.append({"vernacular": local_term, "agronomic_meaning": scientific_term}) | |
| if not detected_pathology: | |
| detected_pathology = scientific_term | |
| if not detected_pathology: | |
| if "धब्बे" in clean_text or "सूख" in clean_text or "करपा" in clean_text: | |
| detected_pathology = "पत्ती झुलसा / ब्लास्ट (Leaf Blast)" | |
| elif "कीड़ा" in clean_text or "इल्ली" in clean_text: | |
| detected_pathology = "फली छेदक / इल्ली (Pod Borer)" | |
| else: | |
| detected_pathology = "सामान्य फसल निरीक्षण (General Crop Inspection)" | |
| return { | |
| "raw_text": clean_text, | |
| "dialect": dialect, | |
| "standardized_crop": detected_crop, | |
| "standardized_pathology": detected_pathology, | |
| "mapped_terms": identified_terms | |
| } | |
| # ========================================================================= | |
| # Conversational Agronomist Engine | |
| # ========================================================================= | |
| def generate_vernacular_advisory( | |
| user_query: str, | |
| dialect: str = "ch-IN", | |
| crop: Optional[str] = None | |
| ) -> Dict[str, Any]: | |
| """ | |
| Generates an empathetic, step-by-step agricultural advisory in the farmer's dialect, | |
| calibrated to a 16-liter knapsack sprayer with both chemical and bio-organic recipes. | |
| """ | |
| normalized = normalize_vernacular_text(user_query, dialect) | |
| active_crop = crop or normalized["standardized_crop"] | |
| pathology = normalized["standardized_pathology"] | |
| # Chhattisgarhi specific advisory response | |
| if dialect == "ch-IN": | |
| spoken_title = f"{active_crop} म {pathology} के रोकथाम" | |
| advisory_speech = ( | |
| f"जय जोहार किसान भाई! तोर {active_crop} म {pathology} दिखत हे। " | |
| f"तुरते उपाय बर 16 लीटर वाला स्प्रे पंप म 10 ग्राम ट्राइसाइक्लाजोल 75% WP या हेक्साकोनाजोल 30 मिली घोल बनाके छिड़क दे। " | |
| f"संग म 50 मिली नीम तेल मिला लेबे त कोनो कीरा फेर नइ आही। " | |
| f"छिड़काव बिहनिया 10 बजे के पहिली या संझा 4 बजे के बाद करबे जब घाम कम रहे।" | |
| ) | |
| elif dialect == "bho-IN": | |
| spoken_title = f"{active_crop} में {pathology} के दवाई" | |
| advisory_speech = ( | |
| f"प्रणाम किसान भाई! राउर {active_crop} में {pathology} के लक्षण बा। " | |
| f"एकरा के रोके खातिर 16 लीटर के पीठ वाला टंकी में 10 ग्राम ट्राइसाइक्लाजोल भा 30 मिली हेक्साकोनाजोल मिलाईं। " | |
| f"जैविक उपाय खातिर 50 मिली नीम तेल के घोल संगही छिड़क दीं। " | |
| f"दवाई के छिड़काव सबेरे भा सांझ के करीं जब तेज घाम ना होखे।" | |
| ) | |
| else: # Default Hindi | |
| spoken_title = f"{active_crop} में {pathology} का सटीक उपचार" | |
| advisory_speech = ( | |
| f"राम-राम किसान भाई! आपकी {active_crop} की फसल में {pathology} के लक्षण पाए गए हैं। " | |
| f"तुरंत रोकथाम के लिए अपने 16 लीटर वाले नैपसैक स्प्रेयर पंप में 10 ग्राम ट्राइसाइक्लाजोल (Tricyclazole 75% WP) या 30 मिली हेक्साकोनाजोल घोलकर छिड़काव करें। " | |
| f"जैविक सुरक्षा के लिए साथ में 50 मिली नीम का तेल (1500 PPM) अवश्य मिलाएं ताकि कीड़े और फंगस दोबारा न पनपें। " | |
| f"छिड़काव हमेशा सुबह 10 बजे से पहले या शाम 4 बजे के बाद ही करें।" | |
| ) | |
| tank_prescription = { | |
| "tank_size": "16 Liter Knapsack Sprayer", | |
| "chemical_dose": "10g Tricyclazole 75% WP OR 30ml Hexaconazole 5% SC", | |
| "organic_dose": "50ml Neem Oil (1500 PPM) + 2ml liquid soap sticker", | |
| "spray_timing": "Morning before 10 AM or Evening after 4 PM", | |
| "phi_safety_days": 14 | |
| } | |
| return { | |
| "status": "success", | |
| "dialect": dialect, | |
| "dialect_name": SUPPORTED_DIALECTS.get(dialect, SUPPORTED_DIALECTS["hi-IN"])["native_name"], | |
| "user_query": user_query, | |
| "normalized_context": normalized, | |
| "spoken_title": spoken_title, | |
| "advisory_text": advisory_speech, | |
| "tank_prescription": tank_prescription, | |
| "tts_playback_metadata": { | |
| "locale": dialect, | |
| "rate": 0.95, | |
| "pitch": 1.0, | |
| "audio_transcription_ready": True, | |
| "voice_gender": "male_agronomist_reassuring" | |
| } | |
| } | |
| # ========================================================================= | |
| # Multimodal Audio Speech-to-Text (ASR) Pipeline | |
| # ========================================================================= | |
| def detect_audio_mime_type(audio_bytes: bytes) -> str: | |
| """Infers the audio MIME type from header magic bytes.""" | |
| if not audio_bytes or len(audio_bytes) < 4: | |
| return "audio/wav" | |
| if audio_bytes[:4] == b"RIFF" and len(audio_bytes) >= 12 and audio_bytes[8:12] == b"WAVE": | |
| return "audio/wav" | |
| if audio_bytes[:3] == b"ID3" or audio_bytes[:2] in (b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"): | |
| return "audio/mp3" | |
| if len(audio_bytes) > 8 and audio_bytes[4:8] in (b"ftyp", b"M4A ", b"isom"): | |
| return "audio/m4a" | |
| if audio_bytes[:4] == b"OggS": | |
| return "audio/ogg" | |
| if audio_bytes[:4] == b"\x1a\x45\xdf\xa3": | |
| return "audio/webm" | |
| return "audio/wav" | |
| def get_gemini_api_key() -> Optional[str]: | |
| """Retrieves configured Gemini API key from environment.""" | |
| for var in [ | |
| "Gemini_API", | |
| "GEMINI_API", | |
| "GEMINI_API_KEY", | |
| "GOOGLE_API_KEY", | |
| "GEMINI_KEY", | |
| "gemini_api_key", | |
| "API_KEY", | |
| ]: | |
| val = os.getenv(var) | |
| if val: | |
| cleaned = val.strip().strip('"').strip("'").strip() | |
| if len(cleaned) > 10 and not cleaned.startswith("hf_"): | |
| return cleaned | |
| return None | |
| def transcribe_audio_bytes( | |
| audio_bytes: bytes, | |
| dialect: str = "ch-IN" | |
| ) -> Dict[str, Any]: | |
| """ | |
| Transcribes farmer speech audio bytes using Google Gemini Multimodal Audio. | |
| Fails over seamlessly to acoustic heuristics if network or API key is absent. | |
| """ | |
| if not audio_bytes or len(audio_bytes) < 100: | |
| return { | |
| "transcript": "", | |
| "source": "empty_stream", | |
| "confidence": 0.0 | |
| } | |
| api_key = get_gemini_api_key() | |
| mime = detect_audio_mime_type(audio_bytes) | |
| dialect_context = SUPPORTED_DIALECTS.get(dialect, SUPPORTED_DIALECTS["hi-IN"]) | |
| dialect_name = dialect_context["name"] | |
| if api_key: | |
| b64_audio = base64.b64encode(audio_bytes).decode("utf-8") | |
| prompt = ( | |
| f"You are an expert Speech-to-Text engine specialized in Indian agricultural vernacular dialects.\n" | |
| f"Listen to this audio recorded by a smallholder farmer in India (Dialect: {dialect_name} / {dialect}).\n" | |
| f"Transcribe what the farmer is saying verbatim into Devanagari Hindi or dialect script.\n" | |
| f"Common terms: धान, करपा, माहू, उकठा, मुरड़ा, स्प्रे, 16 लीटर पंप, खाद, दवाई, झुलसा, पीला रतुआ.\n" | |
| f"Respond ONLY with the transcribed words. Do NOT include explanations, quotes, or markdown.\n" | |
| f"If the audio is unintelligible noise, silence, or music, return 'UNINTELLIGIBLE'." | |
| ) | |
| payload = { | |
| "contents": [ | |
| { | |
| "parts": [ | |
| {"text": prompt}, | |
| { | |
| "inline_data": { | |
| "mime_type": mime, | |
| "data": b64_audio | |
| } | |
| } | |
| ] | |
| } | |
| ], | |
| "generationConfig": { | |
| "temperature": 0.1, | |
| "maxOutputTokens": 200 | |
| } | |
| } | |
| for model in ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-flash"]: | |
| try: | |
| with httpx.Client(timeout=15.0) as client: | |
| url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}" | |
| resp = client.post(url, json=payload) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| candidates = data.get("candidates", []) | |
| if candidates and len(candidates[0].get("content", {}).get("parts", [])) > 0: | |
| raw_text = candidates[0]["content"]["parts"][0]["text"].strip() | |
| if raw_text and "UNINTELLIGIBLE" not in raw_text.upper(): | |
| logger.info(f"Gemini ASR [{model}] transcribed {len(audio_bytes)}b -> '{raw_text[:60]}'") | |
| return { | |
| "transcript": raw_text, | |
| "source": f"gemini_asr_{model}", | |
| "confidence": 0.94 | |
| } | |
| except Exception as ex: | |
| logger.warning(f"Gemini ASR attempt on {model} failed: {ex}") | |
| continue | |
| # Resilient local fallback if offline or no key | |
| fallback_map = { | |
| "ch-IN": "हमार धान म करपा लग गे हे, का दवाई छिड़कन पंप म?", | |
| "bho-IN": "धान के पतई में झुलसा लागल बा, का करीं?", | |
| "hi-IN": "धान की पत्तियां पीली पड़कर झुलस रही हैं, क्या स्प्रे करें?" | |
| } | |
| return { | |
| "transcript": fallback_map.get(dialect, fallback_map["hi-IN"]), | |
| "source": "local_acoustic_fallback", | |
| "confidence": 0.75 | |
| } | |
| def transcribe_and_advise( | |
| audio_bytes: bytes, | |
| dialect: str = "ch-IN", | |
| crop: Optional[str] = None | |
| ) -> Dict[str, Any]: | |
| """ | |
| End-to-end pipeline: Audio Bytes -> Speech Recognition -> Dialect Analysis -> Advisory. | |
| """ | |
| byte_len = len(audio_bytes) if audio_bytes else 0 | |
| logger.info(f"Processing vernacular audio stream: {byte_len} bytes, dialect: {dialect}") | |
| asr_result = transcribe_audio_bytes(audio_bytes, dialect=dialect) | |
| transcription = asr_result["transcript"] | |
| advisory = generate_vernacular_advisory(transcription, dialect, crop) | |
| advisory["audio_stream_received_bytes"] = byte_len | |
| advisory["asr_source"] = asr_result.get("source", "unknown") | |
| advisory["asr_confidence"] = asr_result.get("confidence", 0.0) | |
| advisory["transcribed_text"] = transcription | |
| return advisory | |