#!/usr/bin/env python3 """ Sidewalk FM — Small Trader Ledger & Advisor HuggingFace Build Small Hackathon 2026 — Backyard AI track Architecture: 1. Extraction: regex/rules first → NVIDIA NIM backup (nemotron-mini-4b-instruct) 2. Math: pure Python — never the LLM (deterministic) 3. Advisory: Modal vLLM (nemotron-mini-4b-instruct) → deterministic fallback Fully NVIDIA stack. All models ≤4B → Tiny Titan eligible. Advisory served on Modal for runtime integration → Modal prize eligible. Models: - Extraction backup: nvidia/nemotron-mini-4b-instruct (4B, NIM) - Advisory: nvidia/nemotron-mini-4b-instruct (4B, Modal vLLM) """ import os import re import json import math import logging import base64 from datetime import datetime from typing import Dict, List, Optional # --- Config --------------------------------------------------------------- NIM_API_KEY = os.environ.get("NVIDIA_NIM_API_KEY", "") NIM_BASE = "https://integrate.api.nvidia.com/v1" # Modal advisory endpoint (rebuild per P1) MODAL_BASE = os.environ.get("MODAL_BASE", "") # e.g. https://joshua-abok--sidewalk-fm-advisor-serve.modal.run/v1 EXTRACTION_MODEL = "nvidia/nemotron-mini-4b-instruct" ADVISORY_MODEL = "nvidia/nemotron-mini-4b-instruct" # Ledger state ledger: List[Dict] = [] # Structured agent traces (📡 badge) agent_traces: List[Dict] = [] def add_trace(step: str, **kwargs): """Log a structured agent trace.""" trace = {"step": step, "ts": datetime.now().isoformat(), **kwargs} agent_traces.append(trace) # --- Extraction: Deterministic First, LLM Backup -------------------------- TRANSACTION_PATTERNS = [ re.compile(r'(?i)(?:bought|purchased|got)\s+(\d+(?:\.\d+)?)\s+(\w[\w\s]*?)\s+(?:at|for)\s+(\d+(?:\.\d+)?)\s*(\w*)?'), re.compile(r'(?i)(?:sold|gave|exchanged)\s+(\d+(?:\.\d+)?)\s+(\w[\w\s]*?)\s+(?:at|for)\s+(\d+(?:\.\d+)?)\s*(\w*)?'), re.compile(r'(?i)(?:return|returned|brought back)\s+(\d+(?:\.\d+)?)\s+(\w[\w\s]*?)'), re.compile(r'(?i)(?:bought|purchased)\s+(\d+(?:\.\d+)?)\s+(\w[\w\s]*?)\s+(?:at|for)\s+(\d+(?:\.\d+)?)'), re.compile(r'(\d+(?:\.\d+)?)\s+(\w[\w\s]*?)\s+at\s+(\d+(?:\.\d+)?)'), ] BUY_PATTERN = re.compile(r'(?i)(?:bought|purchased|got|purchase|buy)\b') SELL_PATTERN = re.compile(r'(?i)(?:sold|gave|exchanged|sell)\b') RETURN_PATTERN = re.compile(r'(?i)(?:return|returned|brought back)\b') REFUND_PATTERN = re.compile(r'(?i)(?:refund|got money back)\b') def extract_deterministic(text: str) -> Optional[Dict]: """Extract structured transaction using regex only.""" text_stripped = text.strip() if RETURN_PATTERN.search(text_stripped) or REFUND_PATTERN.search(text_stripped): action = "return" elif BUY_PATTERN.search(text_stripped): action = "buy" elif SELL_PATTERN.search(text_stripped): action = "sell" else: action = "buy" for pattern in TRANSACTION_PATTERNS: m = pattern.search(text_stripped) if m: groups = m.groups() if len(groups) >= 3: try: qty = float(groups[0]) except (ValueError, IndexError): continue item = groups[1].strip() item = re.sub(r'\s+(and|or|but|for|with|from)\s*$', '', item, flags=re.I).strip() try: price = float(groups[2]) except (ValueError, IndexError): continue currency = "" vendor = "" if len(groups) > 3 and groups[3]: cc = groups[3].strip() currency_map = { "shilling": "KES", "shillings": "KES", "sh": "KES", "kes": "KES", "kenya": "KES", "dollar": "USD", "dollars": "USD", "$": "USD", "usd": "USD", "us dollars": "USD", "naira": "NGN", "ngn": "NGN", "nigerian": "NGN", "peso": "MXN", "pesos": "MXN", "mxn": "MXN", "rupee": "INR", "rupees": "INR", "inr": "INR", "euro": "EUR", "eur": "EUR", "pound": "GBP", "gbp": "GBP", "sterling": "GBP", "cfa": "XOF", "yen": "JPY", "jpy": "JPY", "rand": "ZAR", "zar": "ZAR", } currency = currency_map.get(cc.lower(), cc.upper()) if cc.lower() in currency_map else ("KES" if cc.lower() in currency_map else "") from_match = re.search(r'from\s+(\w[\w\s]{0,30}?)(?:\s+(?:at|for|and|but|with|$))', text_stripped) if from_match: vendor = from_match.group(1).strip() return { "action": action, "item": item, "quantity": round(qty, 2), "unit_price": round(price, 2), "currency": currency if currency else "KES", "vendor": vendor if vendor else None, } return None # --- Swahili (Kiswahili) deterministic extraction ------------------------ # Spoken Swahili order is action → item → qty → "kwa" → price, with number WORDS # (hamsini=50, "mia mbili"=200). Lets a Kiswahili voice note log end-to-end. _SW_UNITS = {"sifuri": 0, "moja": 1, "mbili": 2, "tatu": 3, "nne": 4, "tano": 5, "sita": 6, "saba": 7, "nane": 8, "tisa": 9, "kumi": 10} _SW_TENS = {"ishirini": 20, "thelathini": 30, "arobaini": 40, "hamsini": 50, "sitini": 60, "sabini": 70, "themanini": 80, "tisini": 90} _SW_SCALE = {"mia": 100, "elfu": 1000, "laki": 100000, "milioni": 1000000} _SW_NUMWORDS = set(_SW_UNITS) | set(_SW_TENS) | set(_SW_SCALE) | {"na"} _SW_STOP = {"na", "kwa", "kila", "ya", "za", "wa", "la", "ni", "kwenye"} _SW_VERB_STEMS = ("nunua", "uza", "rudish") def _sw_words_to_int(tokens): """Compose Swahili number words into an int (e.g. ['mia','mbili']→200).""" total = 0 i, n = 0, len(tokens) seen = False while i < n: t = tokens[i] if t == "na": i += 1 continue if t in _SW_SCALE: mult = 1 if i + 1 < n and tokens[i + 1] in _SW_UNITS and _SW_UNITS[tokens[i + 1]] <= 9: mult = _SW_UNITS[tokens[i + 1]] i += 1 total += _SW_SCALE[t] * mult seen = True elif t in _SW_TENS: total += _SW_TENS[t] seen = True elif t in _SW_UNITS: total += _SW_UNITS[t] seen = True else: break i += 1 return total if seen else None def _grab_number(tokens): """Digits win; else the first contiguous run of Swahili number words.""" for tk in tokens: if re.fullmatch(r"\d+(?:\.\d+)?", tk): return float(tk) run, started = [], False for w in tokens: if w in _SW_NUMWORDS: run.append(w) started = True elif started: break v = _sw_words_to_int(run) return float(v) if v is not None else None def extract_swahili(text: str) -> Optional[Dict]: """Deterministic Kiswahili extractor. Returns None if not Swahili / not parseable.""" t = text.lower().strip() if "rudish" in t: action = "return" elif "uza" in t: action = "sell" elif "nunua" in t: action = "buy" else: return None tokens = re.findall(r"[a-z]+|\d+(?:\.\d+)?", t) if "kwa" in tokens: k = tokens.index("kwa") left, right = tokens[:k], tokens[k + 1:] else: left, right = tokens, [] qty = _grab_number(left) price = _grab_number(right) if right else None item_words = [ w for w in left if w.isalpha() and w not in _SW_NUMWORDS and w not in _SW_STOP and not any(stem in w for stem in _SW_VERB_STEMS) ] item = " ".join(item_words[:3]).strip() if qty is None or not item: return None return { "action": action, "item": item, "quantity": round(qty, 2), "unit_price": round(price if price else 0.0, 2), "currency": "KES", "vendor": None, } def extract_via_llm(text: str) -> Optional[Dict]: """Fallback: use NIM nemotron-mini-4b-instruct for messy input.""" if not NIM_API_KEY: return None try: import httpx headers = {"Authorization": f"Bearer {NIM_API_KEY}", "Content-Type": "application/json"} payload = { "model": EXTRACTION_MODEL, "messages": [ {"role": "system", "content": "You are a transaction parser for a small street trader. Extract action (buy/sell/return/refund), item (string), quantity (number), unit_price (number), currency (string), vendor (string, optional). Return ONLY valid JSON. No markdown fences."}, {"role": "user", "content": text}, ], "max_tokens": 256, "temperature": 0.0, } with httpx.Client(timeout=30) as client: resp = client.post(f"{NIM_BASE}/chat/completions", headers=headers, json=payload) resp.raise_for_status() raw = resp.json()["choices"][0]["message"]["content"] if raw.startswith("```"): raw = raw.split("\n", 1)[1].rsplit("\n", 1)[0] return json.loads(raw) except Exception: return None def parse_transaction(text: str) -> Dict: """Strategy: regex first (fast, reliable) → LLM backup (handles messy input).""" result = extract_deterministic(text) if result: add_trace("extraction", method="regex-en", success=True, fields=list(result.keys())) return result add_trace("extraction", method="regex-en", success=False) # Kiswahili deterministic extractor (action→item→qty→kwa→price, number words). result = extract_swahili(text) if result: add_trace("extraction", method="regex-sw", success=True, fields=list(result.keys())) return result add_trace("extraction", method="regex-sw", success=False) result = extract_via_llm(text) if result and "error" not in result: add_trace("extraction", method="llm", success=True, fields=list(result.keys())) return result add_trace("extraction", method="llm", success=False) return {"error": "Could not parse transaction. Try: 'bought 10 mangoes at 200 shillings'"} # --- Ledger Operations ---------------------------------------------------- def validate_transaction(tx: Dict) -> Optional[str]: """Validate transaction against hard rules.""" if "error" in tx: return tx["error"] action = tx.get("action", "").lower() if action not in ("buy", "sell", "return", "refund"): return f"Unknown action: '{action}'. Must be buy/sell/return/refund." qty = tx.get("quantity", 0) if not isinstance(qty, (int, float)) or qty <= 0: return f"Quantity must be a positive number, got: {qty}" price = tx.get("unit_price", 0) if not isinstance(price, (int, float)) or price < 0: return f"Price must be zero or positive, got: {price}" if not tx.get("item", "").strip(): return "Item name cannot be empty." return None def add_entry(entry: Dict) -> Dict: """Add a validated entry to the ledger.""" entry["timestamp"] = datetime.now().isoformat() entry["id"] = len(ledger) + 1 ledger.append(entry) return entry def calculate_margins() -> Dict: """Calculate current P&L from ledger. Pure Python — never the LLM.""" revenue = 0.0 costs = 0.0 inventory: Dict[str, float] = {} daily_stats: Dict[str, Dict] = {} for entry in ledger: action = entry.get("action", "").lower() item = str(entry.get("item", "unknown")) qty = float(entry.get("quantity", 0)) price = float(entry.get("unit_price", 0)) ts = entry.get("timestamp", "") day = ts[:10] if ts else "unknown" if day not in daily_stats: daily_stats[day] = {"revenue": 0, "costs": 0, "transactions": 0} daily_stats[day]["transactions"] += 1 if action in ("buy", "purchase"): costs += qty * price inventory[item] = inventory.get(item, 0) + qty elif action in ("sell", "sale"): revenue += qty * price inventory[item] = inventory.get(item, 0) - qty elif action in ("return", "refund"): if entry.get("action", "").lower() == "return": costs -= qty * price inventory[item] = inventory.get(item, 0) + qty else: revenue -= qty * price inventory[item] = inventory.get(item, 0) + qty profit = revenue - costs margin_pct = (profit / revenue * 100) if revenue > 0 else 0.0 item_revenue: Dict[str, float] = {} for entry in ledger: item = str(entry.get("item", "unknown")) total = float(entry.get("quantity", 0)) * float(entry.get("unit_price", 0)) if entry.get("action", "").lower() in ("sell", "sale"): item_revenue[item] = item_revenue.get(item, 0) + total top_items = sorted(item_revenue.items(), key=lambda x: x[1], reverse=True)[:5] return { "revenue": round(revenue, 2), "costs": round(costs, 2), "profit": round(profit, 2), "margin_pct": round(margin_pct, 1), "transaction_count": len(ledger), "inventory": {k: round(v, 1) for k, v in sorted(inventory.items())}, "top_items": top_items, "days_active": len(daily_stats), "daily_stats": daily_stats, } # --- Voice Input: Nemotron ASR 0.6B (gated — never breaks demo) ---------- # --- Zero GPU ASR: run our fine-tuned Swahili wav2vec2 + Whisper in-Space -------- # HF Inference (serverless) no longer serves these ASR models, so we run them on the # Space's Zero GPU. `spaces` is only present on HF; import is guarded so local runs # (and non-GPU hardware) still import the module without error. try: import spaces _HAS_SPACES = True except Exception: _HAS_SPACES = False _ASR_PIPES: Dict[str, object] = {} def _get_asr_pipe(model_id: str): """Lazily build + cache a transformers ASR pipeline (loads on CPU; moved to GPU inside the Zero GPU call).""" if model_id not in _ASR_PIPES: from transformers import pipeline _ASR_PIPES[model_id] = pipeline("automatic-speech-recognition", model=model_id) return _ASR_PIPES[model_id] def _run_asr(audio_path: str, model_id: str, language: Optional[str] = None) -> str: pipe = _get_asr_pipe(model_id) try: pipe.model.to("cuda") except Exception: pass kwargs = {} # Pin Whisper to the chosen language so it can't mis-detect Swahili as French. if "whisper" in model_id.lower() and language in ("sw", "en"): kwargs["generate_kwargs"] = { "language": {"sw": "swahili", "en": "english"}[language], "task": "transcribe", } out = pipe(audio_path, **kwargs) return (out.get("text") if isinstance(out, dict) else str(out)).strip() # Decorate for Zero GPU when running on HF (no-op otherwise). if _HAS_SPACES: _run_asr = spaces.GPU(duration=300)(_run_asr) def transcribe_audio(audio_path: str, language: str = "auto") -> Optional[str]: """Transcribe in-Space on Zero GPU, routed by language. Gated — never breaks the demo. Swahili uses our own fine-tuned, published model (Well-Tuned); English/auto uses Whisper. Both run on the Space's Zero GPU. Any failure degrades to 'type instead'. """ if not audio_path: return None SW_MODEL = os.environ.get( "ASR_SW_MODEL", "Joshua-Abok/finetuning-wav2vec-large-swahili-asr-model_v12") EN_MODEL = os.environ.get("ASR_EN_MODEL", "openai/whisper-small") # fast on a GPU slice if language == "sw": order = [SW_MODEL, EN_MODEL] elif language == "en": order = [EN_MODEL] else: # auto order = [EN_MODEL, SW_MODEL] hint = language if language in ("sw", "en") else None for model in order: try: text = _run_asr(audio_path, model, hint) if text: add_trace("asr", method=model, success=True) return text except Exception as e: add_trace("asr", method=model, success=False, error=str(e)[:80]) continue return None def generate_speech(text: str) -> Optional[bytes]: """Generate speech via NVIDIA Nemotron TTS. Fails silently.""" if not text or not NIM_API_KEY: return None try: import httpx headers = {"Authorization": f"Bearer {NIM_API_KEY}", "Content-Type": "application/json"} payload = {"model": "nvidia/nemotron-tts-1b", "text": text} with httpx.Client(timeout=30) as client: resp = client.post(f"{NIM_BASE}/tts/generate", headers=headers, json=payload) resp.raise_for_status() add_trace("tts", method="nemotron-tts-1b", success=True) return resp.content except Exception as e: add_trace("tts", method="nemotron-tts-1b", success=False, error=str(e)[:80]) return None # --- Advisory ------------------------------------------------------------- def get_deterministic_advice() -> str: """Rule-based advisory from ledger numbers.""" m = calculate_margins() parts = [] if m["transaction_count"] == 0: return "📊 Add your first transaction to get started! Example: 'bought 10 mangoes at 200 shillings'" if m["transaction_count"] < 3: parts.append(f"📊 {m['transaction_count']} transaction(s) logged. Keep going!") if m["top_items"]: parts.append(f"📦 Top item: {m['top_items'][0][0]}") return " ".join(parts) parts.append(f"📊 Revenue: {m['revenue']:.0f} | Costs: {m['costs']:.0f} | Profit: {m['profit']:.0f}") if m["profit"] > 0: parts.append(f"✅ You're making a {m['margin_pct']:.1f}% margin. Good work!") elif m["profit"] < 0: parts.append(f"⚠️ You're losing {abs(m['profit']):.0f}. Check your costs!") else: parts.append("⚖️ Breaking even — look for ways to increase prices or reduce costs.") if m["top_items"]: parts.append(f"💰 Your best seller: {m['top_items'][0][0]} ({m['top_items'][0][1]:.0f} total)") low_stock = [item for item, qty in m["inventory"].items() if qty < 0] if low_stock: parts.append(f"🚨 {', '.join(low_stock)} are oversold/out of stock!") if m["days_active"] >= 2: parts.append(f"📅 You've been tracking {m['days_active']} days. Consistency builds business!") return "\n".join(parts) async def get_modal_advisory(): """Get AI advisory from Modal vLLM. Falls back to deterministic if unavailable.""" if not MODAL_BASE: return get_deterministic_advice() try: m = calculate_margins() prompt = f"""You are a helpful bookkeeping assistant for a small street trader named Mama Aisha. Current financial summary: - Revenue: {m['revenue']:.0f} | Costs: {m['costs']:.0f} | Profit: {m['profit']:.0f} - Margin: {m['margin_pct']:.1f}% | Transactions: {m['transaction_count']} - Inventory: {m['inventory']} Provide concise, actionable advice. 2-3 sentences max. Street-smart.""" payload = { "model": ADVISORY_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 512, "temperature": 0.7, } import httpx async with httpx.AsyncClient(timeout=30) as client: resp = await client.post(f"{MODAL_BASE}/chat/completions", json=payload) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] except Exception as e: logging.warning(f"Modal advisory failed: {e}") add_trace("advisory", method="modal", success=False, error=str(e)) return get_deterministic_advice() # --- Custom Ledger UI (HTML for Off Brand badge) -------------------------- def format_ledger() -> str: """Format ledger as styled HTML table.""" if not ledger: return '

No transactions yet. Try: "bought 50 mangoes at 200 shillings"

' html = '' html += '' html += '' for e in ledger: action = e.get("action", "") item = e.get("item", "") qty = e.get("quantity", 0) price = e.get("unit_price", 0) total = qty * price currency = e.get("currency", "") ts = e.get("timestamp", "")[:16] html += ''.format(e["id"], ts, action, item, qty, price, total, currency) html += '
#TimeActionItemQtyPriceTotalCurrency
{}{}{}{}{:.1f}{:.2f}{:.2f}{}
' return html def format_margins() -> str: """Format margins as styled HTML block with color coding.""" m = calculate_margins() if m["transaction_count"] == 0: return '

Add transactions to see your margins.

' if m["profit"] > 0: color, icon, bg = "#2E7D32", "✅", "rgba(46,125,50,0.04)" elif m["profit"] < 0: color, icon, bg = "#C62828", "⚠️", "rgba(198,40,40,0.04)" else: color, icon, bg = "#F57C00", "⚖️", "rgba(245,124,0,0.04)" inv_items = list(m["inventory"].items()) inv_str = ", ".join("{}={}".format(k, v) for k, v in inv_items[:6]) if len(inv_items) > 6: inv_str += " (+{} more)".format(len(inv_items) - 6) return '
'.format(color, color + "22", bg) + \ '

{} Revenue: {:.0f}

'.format(icon, m["revenue"]) + \ '

Costs: {:.0f}

'.format(m["costs"]) + \ '

Profit: {:.0f}

'.format(m["profit"]) + \ '

Margin: {:.1f}%

'.format(m["margin_pct"]) + \ '

Transactions: {} | Days: {}

'.format(m["transaction_count"], m["days_active"]) + \ '

Inventory: {}

'.format(inv_str) + \ '
' CSS = """ .ledger table { width: 100%; border-collapse: collapse; } .ledger th, .ledger td { padding: 8px 10px; border: 1px solid #ddd; text-align: left; font-size: 13px; } .ledger th { background: #2E7D32; color: white; font-weight: 600; } .ledger tr:nth-child(even) { background: #fafafa; } .ledger tr:hover { background: #f0f7f0; } .empty { color: #888; font-style: italic; font-size: 14px; } .voice-status { font-size: 12px; color: #666; margin-top: 4px; } """ def process_transaction(text: str, state: Dict): """Process a natural language transaction.""" if not text.strip(): return state, "Please enter a transaction.", format_ledger(), format_margins() parsed = parse_transaction(text) error = validate_transaction(parsed) if error: add_trace("validation", success=False, error=error) return state, "❌ {}".format(error), format_ledger(), format_margins() entry = add_entry(parsed) state["ledger"] = ledger status = "✅ {} {}x {} @ {} {}".format( entry.get('action', '?'), entry.get('quantity', '?'), entry.get('item', '?'), entry.get('unit_price', '?'), entry.get('currency', '')) if entry.get("vendor"): status += " from {}".format(entry["vendor"]) return state, status, format_ledger(), format_margins() def process_voice(audio_path: str, language: str, state: Dict): """Process voice input (English/Swahili) — gated so it never crashes the demo.""" if not audio_path: return state, "Record and upload audio to use voice input.", format_ledger(), format_margins() lang = {"English": "en", "Swahili": "sw", "Auto": "auto"}.get(language, "auto") try: text = transcribe_audio(audio_path, language=lang) if not text: notice = "🎤 Voice unavailable — please type your transaction instead." add_trace("voice", success=False, reason="no_transcription") return state, notice, format_ledger(), format_margins() parsed = parse_transaction(text) error = validate_transaction(parsed) if error: add_trace("voice", success=False, reason="validation", error=error) return state, "🎤 Heard: \"{}\"\n\n❌ {}".format(text, error), format_ledger(), format_margins() entry = add_entry(parsed) state["ledger"] = ledger status = "🎤 Heard ({}): \"{}\"\n\n✅ {} {}x {} @ {} {}".format( lang, text, entry.get('action', '?'), entry.get('quantity', '?'), entry.get('item', '?'), entry.get('unit_price', '?'), entry.get('currency', '')) if entry.get("vendor"): status += " from {}".format(entry["vendor"]) return state, status, format_ledger(), format_margins() except Exception as e: add_trace("voice", success=False, reason="exception", error=str(e)[:80]) return state, "🎤 Voice unavailable — please type your transaction instead.", format_ledger(), format_margins() def get_ai_advice(state: Dict): """Get AI advisory (Modal → deterministic fallback).""" if not ledger: return "📊 Add some transactions first! Try: 'bought 10 onions at 50 shillings'" try: import asyncio loop = asyncio.get_event_loop() return loop.run_until_complete(get_modal_advisory()) except Exception: return get_deterministic_advice() def get_traces(): """Return latest 20 agent traces.""" if not agent_traces: return "No traces yet. Start logging transactions!" recent = agent_traces[-20:] lines = [] for t in recent: lines.append("{} | {} | {} | {}".format( t.get('ts', '')[:19], t['step'], t.get('method', '?'), t.get('success', '?'))) return "\n".join(lines) # --- Build Gradio interface ----------------------------------------------- import gradio as gr with gr.Blocks(title="Sidewalk FM — Mama Aisha's Ledger") as demo: gr.Markdown(""" # 📊 Sidewalk FM — Leja ya Mama Aisha / Mama Aisha's Ledger *Msaidizi wa hesabu kwa Mama Aisha, mfanyabiashara wa sokoni.* *A bookkeeping assistant for Mama Aisha, a morning-market produce trader.* **Andika au sema** muamala wako — **kwa Kiswahili au Kiingereza.** Hesabu ni sahihi kila wakati. Track buys, sells & margins by **voice or text, in Swahili or English.** Powered by **NVIDIA Nemotron Mini 4B** (advice) + our **fine-tuned Kiswahili ASR** — Tiny Titan + Well-Tuned. **Mifano / Examples:** - 🇰🇪 *"nimenunua maembe hamsini kwa mia mbili"* (bought 50 mangoes at 200) - 🇬🇧 *"sold 30 maize for 1500"* - *"return 5 broken tomatoes" / "rudisha nyanya tano"* """) with gr.Row(): with gr.Column(scale=2): tx_input = gr.Textbox( label="Muamala / Transaction (sema kwa lugha yako)", placeholder="nimenunua maembe hamsini kwa mia mbili / bought 50 mangoes at 200", lines=2, ) tx_btn = gr.Button("📝 Ongeza / Add Transaction", variant="primary") tx_status = gr.Markdown("Add a transaction above...") gr.HTML("
") gr.Markdown("### 🎤 Voice Input — English & Kiswahili") gr.Markdown("*Pick a language, click the mic, speak your transaction, then press Record & Add.*") lang_dropdown = gr.Dropdown( choices=["Auto", "English", "Swahili"], value="Auto", label="Language (Lugha) — Swahili uses our fine-tuned model 🏅", ) audio_input = gr.Audio( label="Record your transaction", type="filepath", sources=["microphone"], ) voice_btn = gr.Button("🎙️ Rekodi / Record & Add", variant="secondary") voice_notice = gr.Markdown("Voice input ready", elem_classes=["voice-status"]) with gr.Column(scale=1): advice_btn = gr.Button("💡 Ushauri / Get Advice", variant="secondary") advice_out = gr.Markdown("Add transactions, then click for advice!") gr.HTML(label="Margins") margins_display = gr.HTML(label="Margins") with gr.Tabs(): with gr.TabItem("Leja / Ledger"): ledger_display = gr.HTML(label="Transaction Ledger") with gr.TabItem("Agent Traces"): trace_display = gr.Textbox(label="Structured Agent Traces (📡)", lines=8, interactive=False) state = gr.State({"ledger": []}) tx_btn.click(process_transaction, inputs=[tx_input, state], outputs=[state, tx_status, ledger_display, margins_display]) voice_btn.click(process_voice, inputs=[audio_input, lang_dropdown, state], outputs=[state, tx_status, ledger_display, margins_display]) advice_btn.click(get_ai_advice, inputs=[state], outputs=[advice_out]) trace_display.change(get_traces, outputs=[trace_display]) demo.load(lambda: {"ledger": []}, outputs=[state]) if __name__ == "__main__": theme = gr.themes.Soft(primary_hue="green") demo.launch(server_name="0.0.0.0", server_port=7860, share=False, theme=theme, css=CSS)