| import os |
| import asyncio |
| import pandas as pd |
| import json |
| import datetime |
| import gradio as gr |
| from fastapi.responses import HTMLResponse, StreamingResponse |
| from fastapi.staticfiles import StaticFiles |
| from fastapi import Form, UploadFile, File, FastAPI |
| from pydantic import BaseModel |
|
|
| |
| try: |
| import llama_cpp |
| default_backend = "llama_cpp" |
| except ImportError: |
| default_backend = "mock" |
| os.environ["BACKEND"] = os.environ.get("BACKEND", default_backend) |
|
|
| import src.llm as llm |
| import src.rag as rag |
| import src.contacts as contacts |
| import src.ledger as ledger |
| import src.db as db |
|
|
| |
| db.init_db() |
|
|
| |
| PROFILE_PATH = os.path.dirname(os.path.abspath(__file__)) + "/src/data/user_profile.json" |
|
|
| |
| STRINGS = { |
| "en": { |
| "title": "🚜 Kisan-Sathi (Farmer Friend)", |
| "subtitle": "Your personal offline agricultural assistant", |
| "save_success": "Entry saved successfully!", |
| "save_fail": "Error saving entry." |
| }, |
| "hi": { |
| "title": "🚜 किसान-साथी (Kisan-Sathi)", |
| "subtitle": "आपका अपना ऑफलाइन कृषि मित्र और बहीखाता सहायक", |
| "save_success": "लेनदेन सफलतापूर्वक सहेजा गया!", |
| "save_fail": "बचत करने में त्रुटि।" |
| } |
| } |
|
|
| |
| def load_user_profile(): |
| return db.get_profile() |
|
|
| def save_user_profile(name, state, district): |
| return db.save_profile(name, state, district) |
|
|
| |
| def get_nearest_pending_task(crop_name): |
| conn = db.get_db() |
| cursor = conn.cursor() |
| cursor.execute( |
| "SELECT id, sow_date, crop, variety FROM calendars WHERE lower(crop) = ? ORDER BY id DESC", |
| (crop_name.lower(),) |
| ) |
| cals = cursor.fetchall() |
| if not cals: |
| conn.close() |
| return None |
| |
| nearest_task = None |
| |
| for cal in cals: |
| cursor.execute( |
| """ |
| SELECT id, stage, action, intervention, target_date, status |
| FROM tasks WHERE calendar_id = ? AND status = 'pending' ORDER BY target_date ASC |
| """, |
| (cal["id"],) |
| ) |
| tasks = cursor.fetchall() |
| for t in tasks: |
| t_date = datetime.datetime.strptime(t["target_date"], "%Y-%m-%d").date() |
| today = datetime.date.today() |
| diff_days = (t_date - today).days |
| if nearest_task is None or t_date < datetime.datetime.strptime(nearest_task["task"]["target_date"], "%Y-%m-%d").date(): |
| nearest_task = { |
| "task": dict(t), |
| "calendar": dict(cal), |
| "diff_days": diff_days |
| } |
| |
| conn.close() |
| return nearest_task |
|
|
| |
| def get_fallback_nudge(crop, lang): |
| today = datetime.date.today() |
| month = today.month |
| day = today.day |
| |
| if lang == "hi": |
| crop_display = "गेहूं" if crop.lower() == "wheat" else "आलू" |
| elif lang == "hinglish": |
| crop_display = "Wheat" if crop.lower() == "wheat" else "Aloo" |
| else: |
| crop_display = "Wheat" if crop.lower() == "wheat" else "Potato" |
| |
| if crop.lower() == "wheat": |
| if month == 11: |
| stage_en = "Sowing (बुवाई) - Nov 15 to Dec 15" |
| stage_hi = "बुवाई (Sowing) - १५ नवंबर से १५ दिसंबर" |
| advice_en = "Sow at 4-5 cm depth. Seed rate: 40-50 kg/acre. Apply DAP." |
| advice_hi = "४-५ सेमी गहराई पर बोएं। यूरिया और डीएपी का प्रयोग करें।" |
| elif month == 12: |
| stage_en = "CRI Irrigation (सिंचाई - CRI) - Dec 05 to Dec 15" |
| stage_hi = "ताज जड़ विकास सिंचाई (CRI Stage) - ०५ दिसंबर से १५ दिसंबर" |
| advice_en = "21-25 days after sowing. Critical root development stage." |
| advice_hi = "बुवाई के २१-२५ दिन बाद। जड़ विकास के लिए सबसे महत्वपूर्ण सिंचाई।" |
| elif month == 1: |
| stage_en = "Tillering (कल्ले फूटना) - Dec 25 to Jan 10" |
| stage_hi = "कल्ले फूटना (Tillering Stage) - २५ दिसंबर से १० जनवरी" |
| advice_en = "Apply first top dressing of Urea (40 kg/acre) and perform weeding." |
| advice_hi = "यूरिया की पहली टॉप ड्रेसिंग (४० किग्रा/एकड़) करें और निराई करें।" |
| elif month == 2: |
| stage_en = "Flowering (फूल आना) - Jan 25 to Feb 15" |
| stage_hi = "फूल आना (Flowering Stage) - २५ जनवरी से १५ फरवरी" |
| advice_en = "Maintain light moisture. Crucial for grain yield." |
| advice_hi = "हल्की नमी बनाए रखें। दाना बनने की प्रक्रिया के लिए महत्वपूर्ण।" |
| elif month in [4, 5]: |
| stage_en = "Harvesting (कटाई) - Apr 01 to Apr 30" |
| stage_hi = "कटाई (Harvesting) - ०१ अप्रैल से ३० अप्रैल" |
| advice_en = "Harvest when grains are dry and golden (moisture < 14%)." |
| advice_hi = "फसल सुनहरी होने और दाने में १४% से कम नमी होने पर कटाई करें।" |
| else: |
| stage_en = "Upcoming: Sowing (बुवाई) starts Nov 15" |
| stage_hi = "आगामी चरण: बुवाई (Sowing) १५ नवंबर से शुरू" |
| advice_en = "Prepare field by deep ploughing and testing soil during dry months." |
| advice_hi = "गर्मियों में गहरी जुताई करें और मिट्टी का परीक्षण करवाएं।" |
| else: |
| if month in [10, 11] and day <= 10: |
| stage_en = "Sowing (बुवाई) - Oct 15 to Nov 10" |
| stage_hi = "बुवाई (Sowing) - १५ अक्टूबर से १० नवंबर" |
| advice_en = "Use certified seed tubers. Spacing 60x20 cm. Apply NPK." |
| advice_hi = "प्रमाणित बीज कंदों का प्रयोग करें। ६०x२० सेमी दूरी रखें।" |
| elif month == 11: |
| stage_en = "Earthing Up (मिट्टी चढ़ाना) - Nov 15 to Nov 30" |
| stage_hi = "मिट्टी चढ़ाना (Earthing Up) - १५ नवंबर से ३० नवंबर" |
| advice_en = "Apply Urea and mound soil around stems 25 days post-sowing." |
| advice_hi = "बुवाई के २५ दिन बाद यूरिया डालें और पौधों के तने के चारों ओर मिट्टी चढ़ाएं।" |
| elif month in [12, 1] and (month == 12 or day <= 15): |
| stage_en = "Blight Monitoring (झुलसा निगरानी) - Dec 01 to Jan 15" |
| stage_hi = "झुलसा रोग निगरानी (Blight Monitoring) - ०१ दिसंबर से १५ जनवरी" |
| advice_en = "Watch for dark spots. Spray Mancozeb (2g/L) on cloudy/foggy days." |
| advice_hi = "पत्तियों पर काले धब्बों की निगरानी करें। फफूंदनाशक का छिड़काव करें।" |
| elif month in [2, 3]: |
| stage_en = "Harvesting (कटाई) - Feb 15 to Mar 15" |
| stage_hi = "कटाई (Harvesting) - १५ फरवरी से १५ मार्च" |
| advice_en = "Cut foliage 10 days before harvest to thicken potato skin." |
| advice_hi = "खुदाई से १० दिन पहले सिंचाई रोकें और डंठल काट लें ताकि छिलका सख्त हो।" |
| else: |
| stage_en = "Upcoming: Sowing (बुवाई) starts Oct 15" |
| stage_hi = "आगामी चरण: बुवाई (Sowing) १५ अक्टूबर से शुरू" |
| advice_en = "Procure certified seeds from cold storage and keep them in shade." |
| advice_hi = "कोल्ड स्टोरेज से प्रमाणित बीज खरीदें और उन्हें छाया में रखें।" |
|
|
| if lang == "hi": |
| return f"💡 **इस सप्ताह आपके खेत पर ({crop_display}):** {stage_hi}\n📌 **सलाह:** {advice_hi}" |
| elif lang == "hinglish": |
| return f"💡 **Is week aapke khet par ({crop_display}):** {stage_hi}\n📌 **Salah:** {advice_hi}" |
| else: |
| return f"💡 **This week on your farm ({crop_display}):** {stage_en}\n📌 **Advice:** {advice_en}" |
|
|
| |
| def get_proactive_nudge(crop, lang): |
| calendars = db.get_calendars() |
| if not calendars: |
| return get_fallback_nudge(crop, lang) |
| |
| nudges = [] |
| for cal in calendars: |
| |
| pending_tasks = [t for t in cal["tasks"] if t["status"] == "pending"] |
| if not pending_tasks: |
| continue |
| |
| |
| pending_tasks.sort(key=lambda t: t["target_date"]) |
| t = pending_tasks[0] |
| |
| t_date = datetime.datetime.strptime(t["target_date"], "%Y-%m-%d").date() |
| today = datetime.date.today() |
| diff_days = (t_date - today).days |
| |
| |
| action_dict = t["action"] |
| if isinstance(action_dict, str): |
| try: |
| action_dict = json.loads(action_dict) |
| except: |
| action_dict = {"en": t["action"], "hi": t["action"], "hinglish": t["action"]} |
| |
| action_text = action_dict.get(lang, action_dict.get("en", "")) |
| |
| sow_dt = datetime.datetime.strptime(cal["sow_date"], "%Y-%m-%d") |
| sow_display = sow_dt.strftime("%d %b %Y") |
| |
| |
| crop_display = cal["crop"] |
| if cal["crop"].lower() in db.CROP_TEMPLATES: |
| crop_display = db.CROP_TEMPLATES[cal["crop"].lower()]["display"].get(lang, cal["crop"]) |
| |
| variety_suffix = f" ({cal['variety']})" if cal["variety"] else "" |
| |
| if diff_days == 0: |
| if lang == "hi": |
| nudge = f"💡 **आज का काम ({crop_display}{variety_suffix}):** {action_text} (बुवाई: {sow_display})" |
| elif lang == "hinglish": |
| nudge = f"💡 **Aaj ka kaam ({crop_display}{variety_suffix}):** {action_text} (Sowing: {sow_display})" |
| else: |
| nudge = f"💡 **Today's Action ({crop_display}{variety_suffix}):** {action_text} (Sown: {sow_display})" |
| elif diff_days > 0: |
| if lang == "hi": |
| nudge = f"💡 **अगला काम ({crop_display}{variety_suffix}):** {diff_days} दिन में {action_text} ({sow_display} को बोया)" |
| elif lang == "hinglish": |
| nudge = f"💡 **Agla kaam ({crop_display}{variety_suffix}):** {diff_days} din me {action_text} ({sow_display} ko boya)" |
| else: |
| nudge = f"💡 **Next Action ({crop_display}{variety_suffix}):** {action_text} in {diff_days} days (Sown on {sow_display})" |
| else: |
| overdue_days = abs(diff_days) |
| if lang == "hi": |
| nudge = f"⚠️ **लंबित काम ({crop_display}{variety_suffix}):** {overdue_days} दिन पहले होना था - {action_text} ({sow_display} को बोया)" |
| elif lang == "hinglish": |
| nudge = f"⚠️ **Overdue kaam ({crop_display}{variety_suffix}):** {overdue_days} din pehle hona tha - {action_text} ({sow_display} ko boya)" |
| else: |
| nudge = f"⚠️ **Overdue Action ({crop_display}{variety_suffix}):** {action_text} was due {overdue_days} days ago (Sown on {sow_display})" |
| nudges.append(nudge) |
| |
| if nudges: |
| return "\n\n".join(nudges) |
| return get_fallback_nudge(crop, lang) |
|
|
|
|
| |
| def load_calendar(crop): |
| crop_key = crop.lower() |
| if crop_key in db.CROP_TEMPLATES: |
| t = db.CROP_TEMPLATES[crop_key] |
| records = [] |
| for stage in t["stages"]: |
| records.append({ |
| "Crop": crop, |
| "Stage (चरण)": stage["action"]["hi"], |
| "Timing (समय सीमा)": f"Day {stage['day_offset']}", |
| "Action & Advice (कार्य और सलाह)": stage["intervention"]["hi"] |
| }) |
| return pd.DataFrame(records) |
| return pd.DataFrame() |
|
|
| def parse_ledger_text(text, lang): |
| """Parses natural language transaction using LLM helper.""" |
| if not text.strip(): |
| return "today", "", "", 0, "sale" |
| |
| extracted = ledger.parse_transaction(text, lambda p, system, stream: llm.generate(p, system, stream=stream)) |
| |
| return ( |
| extracted.get("date", "today"), |
| extracted.get("item", ""), |
| extracted.get("qty", ""), |
| extracted.get("price", 0), |
| extracted.get("type", "sale") |
| ) |
|
|
| |
| class ProfileSaveRequest(BaseModel): |
| name: str |
| state: str |
| district: str |
|
|
| class ParseLedgerRequest(BaseModel): |
| text: str |
| lang: str |
|
|
| class SaveLedgerRequest(BaseModel): |
| date: str |
| item: str |
| qty: str |
| price: float |
| type: str |
| lang: str |
|
|
| class CalendarAddRequest(BaseModel): |
| crop: str |
| variety: str = "" |
| sow_date: str |
| location: str = "" |
|
|
| class CalendarDeleteRequest(BaseModel): |
| calendar_id: int |
|
|
| class CalendarUpdateRequest(BaseModel): |
| calendar_id: int |
| sow_date: str |
|
|
| class TaskToggleRequest(BaseModel): |
| task_id: int |
| status: str |
|
|
| class LedgerDeleteRequest(BaseModel): |
| entry_id: int |
|
|
| |
| app = FastAPI() |
|
|
| |
| app.mount("/assets", StaticFiles(directory="assets"), name="assets") |
|
|
| |
| @app.get("/", response_class=HTMLResponse) |
| def get_homepage(): |
| index_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "index.html") |
| with open(index_path, "r", encoding="utf-8") as f: |
| return f.read() |
|
|
| |
| @app.get("/api/profile") |
| def get_profile(): |
| profile = load_user_profile() |
| return profile if profile else {} |
|
|
| @app.post("/api/profile") |
| def post_profile(req: ProfileSaveRequest): |
| success = save_user_profile(req.name, req.state, req.district) |
| return {"success": success} |
|
|
| |
| @app.get("/api/nudge/{crop}/{lang}") |
| def get_nudge(crop: str, lang: str): |
| nudge = get_proactive_nudge(crop, lang) |
| return {"nudge": nudge} |
|
|
| |
| @app.get("/api/calendar/{crop}") |
| def get_calendar(crop: str): |
| calendar_df = load_calendar(crop) |
| if calendar_df.empty: |
| return [] |
| return calendar_df.to_dict(orient="records") |
|
|
| |
| @app.get("/api/contacts") |
| def get_contacts(): |
| return contacts.load_contacts() |
|
|
| |
| @app.get("/api/calendars") |
| def get_calendars(): |
| return db.get_calendars() |
|
|
| @app.post("/api/calendar/add") |
| def post_calendar_add(req: CalendarAddRequest): |
| try: |
| cal_id = db.add_calendar(req.crop, req.variety, req.sow_date, req.location) |
| return {"success": True, "calendar_id": cal_id} |
| except Exception as e: |
| return {"success": False, "error": str(e)} |
|
|
| @app.post("/api/calendar/delete") |
| def post_calendar_delete(req: CalendarDeleteRequest): |
| success = db.delete_calendar(req.calendar_id) |
| return {"success": success} |
|
|
| @app.post("/api/calendar/update") |
| def post_calendar_update(req: CalendarUpdateRequest): |
| try: |
| success = db.update_calendar_sow_date(req.calendar_id, req.sow_date) |
| return {"success": success} |
| except Exception as e: |
| return {"success": False, "error": str(e)} |
|
|
| @app.post("/api/task/toggle") |
| def post_task_toggle(req: TaskToggleRequest): |
| success = db.update_task_status(req.task_id, req.status) |
| return {"success": success} |
|
|
| |
| @app.post("/api/ledger/parse") |
| def post_ledger_parse(req: ParseLedgerRequest): |
| parsed = parse_ledger_text(req.text, req.lang) |
| return { |
| "date": parsed[0], |
| "item": parsed[1], |
| "qty": parsed[2], |
| "price": parsed[3], |
| "type": parsed[4] |
| } |
|
|
| @app.post("/api/ledger/save") |
| def post_ledger_save(req: SaveLedgerRequest): |
| success = ledger.add_entry(req.date, req.item, req.qty, req.price, req.type) |
| msg = STRINGS[req.lang]["save_success"] if success else STRINGS[req.lang]["save_fail"] |
| return {"success": success, "message": msg} |
|
|
| @app.post("/api/ledger/delete") |
| def post_ledger_delete(req: LedgerDeleteRequest): |
| success = db.delete_ledger_entry(req.entry_id) |
| return {"success": success} |
|
|
| @app.post("/api/ledger/clear") |
| def post_ledger_clear(): |
| success = db.clear_ledger() |
| return {"success": success} |
|
|
| @app.post("/api/reset") |
| def post_reset(): |
| success = db.clear_all_data() |
| return {"success": success} |
|
|
| @app.get("/api/ledger/export") |
| def get_ledger_export(): |
| import io |
| import csv |
| entries, _ = db.get_ledger_entries() |
| |
| output = io.StringIO() |
| writer = csv.writer(output) |
| writer.writerow(["Date", "Type", "Item", "Quantity", "Price", "Timestamp"]) |
| |
| for r in entries: |
| writer.writerow([r["date"], r["type"], r["item"], r["qty"], r["price"], r["created_at"]]) |
| |
| output.seek(0) |
| return StreamingResponse( |
| io.BytesIO(output.getvalue().encode("utf-8")), |
| media_type="text/csv", |
| headers={"Content-Disposition": "attachment; filename=ledger_export.csv"} |
| ) |
|
|
| @app.get("/api/ledger/stats") |
| def get_ledger_stats(): |
| df, summary = ledger.get_ledger_data() |
| transactions = [] |
| if not df.empty: |
| for r in df.to_dict(orient="records"): |
| |
| date_val = r.get("Date", "") |
| item_val = r.get("Item", "") |
| qty_val = r.get("Quantity", "") |
| price_val = r.get("Price", 0) |
| raw_type = str(r.get("Type", "")).lower() |
| tx_id = r.get("Id") |
| |
| type_val = "Sale" if "sale" in raw_type or "बिक्री" in raw_type else "Purchase" |
| transactions.append({ |
| "Id": tx_id, |
| "Date": date_val, |
| "Item": item_val, |
| "Quantity": qty_val, |
| "Price": price_val, |
| "Type": type_val |
| }) |
| return { |
| "summary": summary, |
| "transactions": transactions |
| } |
|
|
| |
| @app.post("/api/ask") |
| async def post_ask( |
| message: str = Form(""), |
| lang: str = Form("hi"), |
| crop: str = Form("Wheat"), |
| history: str = Form("[]"), |
| image: UploadFile = File(None), |
| audio: UploadFile = File(None) |
| ): |
| |
| image_path = None |
| audio_path = None |
| |
| |
| profile = load_user_profile() or {} |
| user_name = profile.get("name", "Ramesh Kumar") |
| user_state = profile.get("state", "Uttar Pradesh") |
| user_district = profile.get("district", "Kanpur Dehat") |
| |
| history_list = [] |
| if history: |
| try: |
| history_list = json.loads(history) |
| except Exception as e: |
| print(f"[app.py] Error parsing history: {e}") |
|
|
| temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp") |
| os.makedirs(temp_dir, exist_ok=True) |
| |
| if image and image.filename: |
| image_path = os.path.join(temp_dir, image.filename) |
| with open(image_path, "wb") as f: |
| f.write(await image.read()) |
| |
| if audio and audio.filename: |
| audio_path = os.path.join(temp_dir, audio.filename) |
| with open(audio_path, "wb") as f: |
| f.write(await audio.read()) |
|
|
| async def event_generator(): |
| |
| check_text = message |
| if audio_path: |
| check_text = llm.transcribe_audio(audio_path, message) |
| |
| |
| is_emergency, warning_msg = contacts.check_emergency_query(check_text) |
| if is_emergency: |
| yield warning_msg |
| |
| if image_path and os.path.exists(image_path): |
| try: os.remove(image_path) |
| except: pass |
| if audio_path and os.path.exists(audio_path): |
| try: os.remove(audio_path) |
| except: pass |
| return |
|
|
| |
| if lang == "hi": |
| lang_instruction = "You MUST write your entire response in Hindi language using Devanagari script (हिन्दी)." |
| elif lang == "hinglish": |
| lang_instruction = "You MUST write your entire response in Hinglish (Hindi language written in Roman/Latin script using English alphabetic characters, e.g., 'Aloo ki kheti ke liye dhyan dein...')." |
| else: |
| lang_instruction = "You MUST write your entire response in English." |
|
|
| |
| grounding_text, sources = rag.retrieve_guides(check_text, crop, lang) |
| if grounding_text: |
| source_cite = "Sources used: " + ", ".join(sources) |
| system_prompt = ( |
| f"You are Kisan-Sathi, a friendly agricultural expert helping {user_name} in {user_district}, {user_state}.\n" |
| f"{lang_instruction}\n" |
| f"Here is verified agricultural guide context regarding the question:\n{grounding_text}\n" |
| f"Provide a short, direct answer in a friendly, conversational tone. Ground your answer in this context.\n" |
| f"CRITICAL: Keep your response extremely short, crisp, and precise (maximum 120-150 words, or 3-4 bullet points max). Do not write a long essay or generic introduction. Focus only on direct, actionable advice.\n" |
| f"Cite the source at the very end as: '{source_cite}'." |
| ) |
| else: |
| system_prompt = ( |
| f"You are Kisan-Sathi, a friendly agricultural expert helping {user_name} in {user_district}, {user_state}.\n" |
| f"{lang_instruction}\n" |
| f"Advise them on {crop} crop management using best practices for {user_district}, {user_state}.\n" |
| f"CRITICAL: Keep your response extremely short, crisp, and precise (maximum 120-150 words, or 3-4 bullet points max). Do not write a long essay or generic introduction. Focus only on direct, actionable advice.\n" |
| f"Suggest contacting a local agricultural officer for specific localized chemical treatments if unsure." |
| ) |
|
|
| |
| response_stream = llm.generate( |
| check_text, |
| system=system_prompt, |
| image_path=image_path, |
| audio_path=audio_path, |
| history=history_list, |
| stream=True |
| ) |
| import re |
| for chunk in response_stream: |
| if chunk.startswith("JSON_OUTPUT:"): |
| continue |
| cleaned_chunk = re.sub(r'\|?<\|im_(?:end|start)\|>', '', chunk) |
| yield cleaned_chunk |
| await asyncio.sleep(0.01) |
| |
| |
| if image_path and os.path.exists(image_path): |
| try: os.remove(image_path) |
| except: pass |
| if audio_path and os.path.exists(audio_path): |
| try: os.remove(audio_path) |
| except: pass |
|
|
| return StreamingResponse(event_generator(), media_type="text/plain") |
|
|
| |
| def gradio_chat_respond(message, history): |
| formatted_history = [] |
| if history: |
| for turn in history: |
| if isinstance(turn, dict): |
| formatted_history.append(turn) |
| elif isinstance(turn, (list, tuple)) and len(turn) == 2: |
| formatted_history.append({"role": "user", "content": turn[0]}) |
| formatted_history.append({"role": "assistant", "content": turn[1]}) |
| |
| response_stream = llm.generate( |
| message, |
| system="You are Kisan-Sathi, a friendly agricultural expert.", |
| history=formatted_history, |
| stream=True |
| ) |
| response_accumulator = "" |
| for chunk in response_stream: |
| if chunk.startswith("JSON_OUTPUT:"): |
| continue |
| response_accumulator += chunk |
| yield response_accumulator |
|
|
| demo = gr.ChatInterface( |
| gradio_chat_respond, |
| title="किसान-साथी (Kisan-Sathi) Fallback", |
| description="Ask Sathi anything about farming, or use the custom home page at http://localhost:7860/" |
| ) |
|
|
| app = gr.mount_gradio_app(app, demo, path="/gradio") |
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run("app:app", host="0.0.0.0", port=7860) |
|
|