# ANKAHI — FULL APPLICATION BUILD INSTRUCTIONS ## Complete Agent Prompt: Backend + Frontend + Integration ## Read every word. Build every file. Leave nothing as a placeholder. --- ## CONTEXT: WHAT YOU HAVE The AI model is already trained and sitting at this exact path: ``` /punjabi_gemma/ankahi/ ├── chat_template.jinja ← Gemma 4 chat format (how to structure prompts) ├── config.json ← Model architecture config ├── generation_config.json ← Default generation params (temperature, top_k etc) ├── model.safetensors ← The trained weights (15GB, merged Stage1+3+4) ├── processor_config.json ← Multimodal processor config ├── tokenizer_config.json ← Tokenizer settings └── tokenizer.json ← Vocabulary ``` The 5 persona LoRA adapters are at: ``` /artifacts/stage2/ ├── ananya/persona-ananya.lora/adapter_model.safetensors ├── arjun/persona-arjun.lora/adapter_model.safetensors ├── priya/persona-priya.lora/adapter_model.safetensors ├── rohan/persona-rohan.lora/adapter_model.safetensors └── zara/persona-zara.lora/adapter_model.safetensors ``` The pictogram symbol library is at: ``` /data/processed/symbols_canonical.parquet ← 16,756 symbols (ARASAAC + Mulberry) /data/raw/arasaac/images/ ← PNG images, named {id}.png ``` --- ## WHAT YOU ARE BUILDING A complete application with three parts: **Part 1 — Python FastAPI Backend** running on the same machine as the model. Serves inference, handles persona switching, manages the training buffer, and exposes REST endpoints. **Part 2 — Flutter Android App** that is the actual AAC interface a child uses daily. Talks to the backend over local WiFi. Full offline capability is the goal; for demo purposes the backend runs on the same LAN. **Part 3 — Web Landing Page** (single HTML file) that demonstrates the product for hackathon judges. Fully self-contained, no backend needed, interactive demo runs in the browser via JavaScript simulation. --- # PART 1 — PYTHON FASTAPI BACKEND ## Directory structure to create ``` ankahi_backend/ ├── main.py ← FastAPI app entry point ├── model_loader.py ← Loads model + switches persona adapters ├── inference.py ← Core predict() function ├── persona_manager.py ← Persona profiles + system prompts ├── training_buffer.py ← SQLite utterance logging ├── tts_service.py ← Text-to-speech (gTTS + voice clone fallback) ├── pictogram_service.py ← Symbol search + image serving ├── schemas.py ← Pydantic request/response models ├── config.py ← All paths and constants ├── requirements.txt ← Exact versions └── run.sh ← One-command startup script ``` --- ## FILE: config.py ```python # All absolute paths and constants. Change MODEL_PATH if your machine differs. import os MODEL_PATH = "/punjabi_gemma/ankahi" ADAPTERS_BASE = "/artifacts/stage2" SYMBOLS_PARQUET = "/data/processed/symbols_canonical.parquet" SYMBOLS_IMAGE_DIR = "/data/raw/arasaac/images" SQLITE_DB_PATH = "/tmp/ankahi_buffer.db" TTS_OUTPUT_DIR = "/tmp/ankahi_tts" VOICE_PROFILES_DIR = "/tmp/ankahi_voices" # Generation settings (read from model's generation_config.json, override here) MAX_NEW_TOKENS = 150 TEMPERATURE = 0.7 TOP_K = 40 TOP_P = 0.95 N_ALTERNATIVES = 3 # Server HOST = "0.0.0.0" PORT = 8000 # Persona IDs PERSONA_IDS = ["ananya", "arjun", "priya", "rohan", "zara"] # Languages LANG_CODES = { "hi": "Hindi", "pa": "Punjabi", "bn": "Bengali", "ta": "Tamil", "te": "Telugu", "mr": "Marathi", "en": "English" } ``` --- ## FILE: schemas.py Define every Pydantic model used across the API. Create these exact models: ```python from pydantic import BaseModel from typing import Optional, List class PredictRequest(BaseModel): pictogram_sequence: List[str] # ["mother", "food", "want"] persona_id: str # "arjun" context: Optional[str] = "daily" # "breakfast" / "school" / "bedtime" etc audio_transcript: Optional[str] = None # partial word from vocalization image_base64: Optional[str] = None # base64 PNG for visual grounding n_alternatives: int = 3 class PredictResponse(BaseModel): alternatives: List[str] # ["Mummy, mujhe paratha khana hai!", ...] primary: str # alternatives[0] persona_id: str inference_time_ms: float model_version: str = "ankahi-gemma4-e4b-v1" class SpeakRequest(BaseModel): text: str persona_id: str language: str = "hi" class SpeakResponse(BaseModel): audio_url: str # /audio/{filename}.wav duration_seconds: float class LogUtteranceRequest(BaseModel): pictogram_sequence: List[str] confirmed_sentence: str persona_id: str context: Optional[str] = None was_first_alternative: bool = True class PersonaProfile(BaseModel): persona_id: str name: str age: int city: str primary_language: str secondary_languages: List[str] cp_type: str severity: str example_output: str class SymbolSearchRequest(BaseModel): query: str language: str = "en" limit: int = 20 category: Optional[str] = None class SymbolResult(BaseModel): symbol_id: str concept_en: str concept_local: str image_url: str category: str class HealthResponse(BaseModel): status: str model_loaded: bool active_persona: Optional[str] gpu_memory_gb: Optional[float] uptime_seconds: float ``` --- ## FILE: persona_manager.py Build a complete PersonaManager class. Store all 5 persona profiles as Python dicts. Each persona must have: persona_id, name, age, city, state, primary_language, secondary_languages, cp_type, severity, additional_conditions, code_switch_pattern, caregiver_name, caregiver_relation, favourite_foods, favourite_activities, and a to_system_prompt() method. The system prompts must be specific and detailed. Here is the exact content for each: **Arjun system prompt:** "You are Ankahi, an AAC assistant for Arjun, a 9-year-old boy in Ludhiana, Punjab with dyskinetic cerebral palsy and mild intellectual disability. His primary caregiver is his Mummy. He lives in a joint family with his Dadi (paternal grandmother). He speaks Punjabi at home, Hindi at school, and uses English words for technology and school subjects. His typical code-switch: Punjabi pronouns + Hindi verbs + English nouns (e.g. 'Mainu paratha khana chahida'). Favourite foods: paratha, lassi, aloo sabzi, kadhi. Favourite activities: watching cricket on TV, toy cars, Sunday Gurudwara visits. He has physiotherapy at 3pm daily which he resists. Predict short (6–12 word) sentences in his natural language mix. Offer 3 alternatives ranked by likelihood." **Ananya system prompt:** "You are Ankahi for Ananya, a 6-year-old girl in Chennai with spastic quadriplegia CP. She uses eye-gaze to select pictograms — so sentences must be VERY SHORT (3–6 words maximum). Her Amma (mother) is primary caregiver. She speaks Tamil at home and basic English from school. She never says 'Mummy' — always 'Amma'. Favourite foods: idli, dosa, mango juice, curd rice. She loves Chuchu TV and being read to. Predict in Tamil-first with minimal English. Never generate sentences longer than 6 words." **Priya system prompt:** "You are Ankahi for Priya, a 4-year-old girl in Kolkata with spastic CP and cortical visual impairment (CVI). She is very young and uses only 1 or 2 pictograms at a time. Her most important person is her 'Didu' (maternal grandmother). She speaks Bengali. Sentences must be 2–4 words maximum, toddler register. Example: 'Didu aasho' (Didu come). She knows Dora the Explorer characters in English. Primary caregiver is her mother. Never generate complex sentences — she is 4 years old." **Rohan system prompt:** "You are Ankahi for Rohan, an 11-year-old boy in Delhi NCR with athetoid CP. He is the oldest and most communicatively capable — he has used an AAC app before. He speaks Hindi and heavy English mixing for school and technology topics. He expresses opinions, not just needs ('Mujhe woh movie boring lagi'). He likes YouTube science videos, chess, Bollywood music. Sentences can be longer (8–15 words). He refers to his mother as 'Mamma'. Generate more complex, opinionated sentences appropriate for an 11-year-old." **Zara system prompt:** "You are Ankahi for Zara, a 7-year-old girl in Pune with mild spastic CP. Her primary caregiver is her father — she calls him 'Baba' (NOT 'Papa'). She has a younger sister named Riya who appears in her utterances constantly. She speaks Marathi at home, Hindi from TV/films, English at school. Favourite foods: puran poli, misal pav, shrikhand. She loves drawing and singing Marathi songs. Sentences 5–10 words. Mix Marathi + Hindi naturally." --- ## FILE: model_loader.py Build a ModelLoader class with these exact methods: **__init__:** Initialize with model_path, set self.model = None, self.processor = None, self.active_persona = None, self.active_adapter = None. Also set self.load_start_time = None. **load_base_model():** Load the model from MODEL_PATH using transformers AutoModelForCausalLM with device_map="auto", torch_dtype=torch.bfloat16. Load the processor with AutoProcessor. Set model to eval() mode. Read the chat_template.jinja file and store it. Log GPU memory after loading. **load_persona_adapter(persona_id):** If persona_id is same as self.active_persona, return immediately (no reload). Otherwise: unload current adapter if any (call merge_adapter then unload), load the new adapter from ADAPTERS_BASE/persona_id/persona-{persona_id}.lora/ using PeftModel.from_pretrained. Set self.active_persona = persona_id. Log the switch. **unload_adapter():** If an adapter is loaded, call model.merge_and_unload() or just reload the base model. Reset self.active_persona = None. **get_gpu_memory_gb():** Return torch.cuda.memory_allocated() / 1e9. **is_ready():** Return True if self.model is not None. Use a global singleton instance. Import this singleton in inference.py, main.py etc. --- ## FILE: inference.py Build a predict() function with this exact signature: ``` predict( pictogram_sequence: list[str], persona_id: str, context: str = "daily", audio_transcript: str = None, image_base64: str = None, n_alternatives: int = 3 ) -> dict ``` **Inside predict():** 1. Get the persona's system prompt from PersonaManager 2. Build the full prompt using the chat_template.jinja format. The prompt structure must be: - System turn: the persona system prompt - Model ack: "Understood. Ready." - User turn: "Pictogram sequence: [{seq}]\nContext: {context}{audio_note}{image_note}\nPredict {n} most likely sentences. Use child's language mix. Short sentences. Numbered list." - Start model turn (leave open for generation) 3. If image_base64 is provided, decode it, open as PIL Image, pass to processor 4. If audio_transcript is provided, append to user turn: "\nChild vocalization heard: '{audio_transcript}' — use this to refine the prediction" 5. Tokenize with processor (or tokenizer if no image) 6. Generate with model.generate() using: max_new_tokens=150, temperature=0.7, top_k=40, do_sample=True 7. Decode only the new tokens (skip the input_ids length) 8. Parse the numbered list from the output — extract lines starting with "1.", "2.", "3." 9. Return dict with alternatives list, primary (first one), inference_time_ms, persona_id **Error handling:** Wrap generation in try/except. On any error, return ["Prediction failed — please try again"] as alternatives. Log the full exception. **Streaming version:** Also implement predict_stream() that yields tokens one by one using model.generate() with a streamer. The endpoint will use Server-Sent Events. --- ## FILE: training_buffer.py Build a SQLite-backed TrainingBuffer class: **Schema:** ```sql CREATE TABLE utterances ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, pictogram_seq TEXT NOT NULL, confirmed_sentence TEXT NOT NULL, persona_id TEXT NOT NULL, context TEXT, was_first_alternative INTEGER DEFAULT 1, session_id TEXT ); ``` **Methods:** - `log(pictogram_seq, confirmed_sentence, persona_id, context, was_first_alternative)` — insert one record - `get_for_persona(persona_id)` — return all records for that persona as list of dicts - `count(persona_id=None)` — count records (all or per persona) - `export_jsonl(persona_id)` — return JSONL string of all records for persona - `clear(persona_id)` — delete all records for persona - `get_stats()` — return dict of {persona_id: count} for all personas Use sqlite3 from stdlib. Create the DB and table on first use. Thread-safe (use a threading.Lock). --- ## FILE: tts_service.py Build a TtsService class that generates audio files: **Method speak(text, language, voice_profile_dir=None) → str:** Priority 1 — If voice_profile_dir exists and has reference.wav: use gTTS as a temporary fallback (real voice cloning requires the clone_parent_voice.py script to have been run already). Check if a pre-synthesised WAV exists for this text hash, return it if so. Priority 2 — gTTS fallback: ```python from gtts import gTTS tts = gTTS(text=text, lang=language_code, slow=False) tts.save(output_path) ``` Language code mapping: hi→hi, ta→ta, bn→bn, te→te, mr→mr, pa→pa (gTTS may not support pa, fallback to hi), en→en. Save all output files to TTS_OUTPUT_DIR with filename = sha256(text+language)[:12] + ".mp3" Return the relative URL path: "/audio/{filename}.mp3" Also expose a `list_available_voices()` method that checks VOICE_PROFILES_DIR for subdirectories. --- ## FILE: pictogram_service.py Build a PictogramService class: **__init__:** Load symbols_canonical.parquet into a pandas DataFrame. Build a search index: for each row, create a combined text field = concept_en + " " + concept_hi + " " + concept_ta (etc). Store as list for fuzzy search. **search(query, language, limit, category) → List[SymbolResult]:** - Lowercase the query - Filter by category if provided - Search concept columns using str.contains() across the query - Also search by English concept as fallback - Return top `limit` results with image_url = "/symbols/{symbol_id}.png" **get_categories() → List[str]:** Return sorted unique list of all categories in the dataframe. **get_by_id(symbol_id) → SymbolResult:** Look up a single symbol by ID. The image files are served statically from SYMBOLS_IMAGE_DIR. The symbol IDs are the numeric ARASAAC IDs (the PNG filename without extension). --- ## FILE: main.py Build the complete FastAPI application. Every endpoint listed below must be implemented fully — no stubs, no "TODO" comments. ```python from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse, FileResponse import time, asyncio app = FastAPI( title="Ankahi API", description="Offline AAC backend for Indian children with CP — Gemma 4 E4B", version="1.0.0" ) # CORS — allow the Flutter app and the landing page to call this app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) # Serve audio files and pictogram images as static files app.mount("/audio", StaticFiles(directory=TTS_OUTPUT_DIR), name="audio") app.mount("/symbols", StaticFiles(directory=SYMBOLS_IMAGE_DIR), name="symbols") startup_time = time.time() ``` **Startup event:** On startup, call model_loader.load_base_model(). If it fails, log the error but don't crash — the health endpoint will report model_loaded: False. Load the persona manager. Initialise the training buffer. Initialise pictogram service. **Endpoints to implement — every single one:** ``` GET /health → HealthResponse → Returns: status "ok", model_loaded bool, active_persona, gpu_memory_gb, uptime_seconds POST /predict → PredictRequest → PredictResponse → Calls inference.predict() → If persona_id changes, calls model_loader.load_persona_adapter() first → Times the inference, returns inference_time_ms → Raises HTTP 503 if model not loaded → Raises HTTP 422 if pictogram_sequence is empty GET /predict/stream?pictogram_sequence=water,want&persona_id=arjun&context=morning → StreamingResponse (text/event-stream, Server-Sent Events) → Each token yielded as: data: {"token": "Muj"}\n\n → Final event: data: {"done": true, "full_text": "Mujhe pani chahiye"}\n\n POST /speak → SpeakRequest → SpeakResponse → Calls tts_service.speak() → Returns audio_url and duration_seconds → Creates TTS_OUTPUT_DIR if not exists GET /audio/{filename} → FileResponse → Serves the audio file from TTS_OUTPUT_DIR → Raises HTTP 404 if not found POST /utterances/log → LogUtteranceRequest → {"success": true, "total_buffered": int} → Logs to training_buffer → Background task: if count > 100 for this persona, log a reminder GET /utterances/stats → {"total": int, "by_persona": {persona_id: count}} GET /utterances/export/{persona_id} → Returns JSONL text file download → Content-Disposition: attachment; filename=utterances_{persona_id}.jsonl DELETE /utterances/{persona_id} → Clears buffer for that persona → Returns {"cleared": int} GET /personas → List[PersonaProfile] → Returns all 5 persona profiles GET /personas/{persona_id} → PersonaProfile → Returns one persona, raises 404 if not found POST /personas/switch/{persona_id} → {"success": true, "persona_id": str, "adapter_loaded": bool} → Calls model_loader.load_persona_adapter() → Takes 5–30 seconds — do NOT do this mid-session GET /symbols/search?query=water&language=hi&limit=20 → List[SymbolResult] → Calls pictogram_service.search() GET /symbols/categories → List[str] GET /symbols/{symbol_id} → SymbolResult → Raises 404 if not found GET /symbols/{symbol_id}/image → FileResponse (PNG) → Serves from SYMBOLS_IMAGE_DIR/{symbol_id}.png GET /demo/quick → {"alternatives": List[str], "persona": str, "sequence": List[str]} → Runs a fixed demo prediction: ["mother", "food", "want"] for persona arjun → Used by the landing page to show the API is live ``` --- ## FILE: requirements.txt ``` fastapi==0.111.0 uvicorn[standard]==0.29.0 pydantic==2.7.0 torch==2.5.1 transformers>=4.54.0 peft>=0.12.0 accelerate>=0.30.0 safetensors>=0.4.3 pandas>=2.2.0 pyarrow>=16.0.0 Pillow>=10.3.0 gTTS>=2.5.0 python-multipart>=0.0.9 aiofiles>=23.2.1 ``` --- ## FILE: run.sh ```bash #!/bin/bash set -e echo "Starting Ankahi API server..." echo "Model path: /punjabi_gemma/ankahi" echo "GPU: $(nvidia-smi --query-gpu=name --format=csv,noheader)" cd "$(dirname "$0")" pip install -r requirements.txt -q uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 --reload ``` --- # PART 2 — FLUTTER ANDROID APP ## Directory structure ``` mobile/ankahi_flutter/ ├── pubspec.yaml ├── lib/ │ ├── main.dart │ ├── config/ │ │ └── app_config.dart ← API base URL, constants │ ├── api/ │ │ └── ankahi_api.dart ← All HTTP calls to the backend │ ├── models/ │ │ ├── persona.dart │ │ ├── pictogram.dart │ │ ├── prediction.dart │ │ └── utterance.dart │ ├── providers/ │ │ ├── persona_provider.dart │ │ ├── prediction_provider.dart │ │ └── session_provider.dart │ ├── screens/ │ │ ├── home_screen.dart ← Main AAC grid interface │ │ ├── setup_screen.dart ← First-run setup │ │ ├── persona_screen.dart ← Select/switch persona │ │ └── settings_screen.dart │ ├── widgets/ │ │ ├── pictogram_grid.dart ← The 4×6 symbol grid │ │ ├── pictogram_tile.dart ← Individual symbol card │ │ ├── sequence_bar.dart ← Selected symbols + predict button │ │ ├── alternatives_panel.dart ← Prediction results │ │ ├── large_text_display.dart ← Confirmed sentence (big font) │ │ ├── speaking_indicator.dart ← Animated waveform when TTS plays │ │ └── category_tabs.dart │ └── services/ │ ├── audio_service.dart ← Plays TTS audio from API │ ├── storage_service.dart ← SharedPreferences wrapper │ └── connectivity_service.dart ``` --- ## FILE: app_config.dart ```dart class AppConfig { // Change this to your H100 machine's local IP address // Find it with: hostname -I | awk '{print $1}' static const String apiBaseUrl = 'http://192.168.1.100:8000'; // Increase this if your machine is slow to load an adapter static const Duration apiTimeout = Duration(seconds: 45); // How many pictograms to show per page static const int gridColumns = 6; static const int gridRows = 4; // Auto-predict when this many symbols are selected static const int autoPredictAt = 4; // Persona colours (used for UI theming when that persona is active) static const Map personaColors = { 'arjun': 0xFF1A1464, // deep indigo 'ananya': 0xFF4A0E0E, // deep maroon 'priya': 0xFF0E3A4A, // deep teal 'rohan': 0xFF1A3A0E, // deep green 'zara': 0xFF4A2E0E, // deep amber }; } ``` --- ## FILE: ankahi_api.dart Build a complete Dart API client class `AnkahiApi`. Use the `http` package. Implement these methods with full error handling: **predict(List sequence, String personaId, {String? context, String? audioHint}) → Future** POST /predict, handle 503 (model not ready), 422 (bad input), network errors. Return PredictionResult with alternatives and inferenceTimeMs. **speak(String text, String personaId, String language) → Future** POST /speak, return the full audio URL (prepend apiBaseUrl to the path). **logUtterance(List seq, String confirmed, String personaId, {bool wasFirst = true}) → Future** POST /utterances/log, fire and forget (don't await in UI). **getPersonas() → Future>** GET /personas **switchPersona(String personaId) → Future** POST /personas/switch/{personaId}, returns success bool. Show a loading dialog because this takes 5–30 seconds. **searchSymbols(String query, {String language = 'en', int limit = 20}) → Future>** GET /symbols/search **getHealth() → Future** GET /health For all methods: set a timeout of AppConfig.apiTimeout. On timeout, throw an ApiTimeoutException with a user-friendly message. On any HTTP error, throw ApiException with the status code and message. --- ## FILE: home_screen.dart — THE MAIN AAC SCREEN This is the most important file. Build it completely. No shortcuts. **State variables:** - `List _selectedSymbols = []` — current tap sequence - `PredictionResult? _prediction` — current prediction - `bool _isPredicting = false` - `String? _confirmedSentence` — what was confirmed - `bool _isSpeaking = false` - `String _activeCategory = 'all'` - `List _displayedSymbols = []` — filtered by category - `String _activePersonaId` — loaded from storage on init **initState:** Load active persona from SharedPreferences. Call api.getHealth() — if model not loaded, show a banner "AI loading, predictions unavailable". Load symbols from API by calling searchSymbols with empty query to get defaults. **Layout (build method):** Use a Column with these children in order: 1. **AppBar** — custom, not Flutter's default AppBar. A Container with the persona's colour from AppConfig.personaColors. Left: "अनकही" text + persona name. Right: connection status dot (green/red based on health), settings icon. Height: 56px. 2. **Sequence Bar** (SequenceBar widget, height 80px) — shows selected symbols as horizontal chips. Each chip has the symbol image + label. Backspace button. Clear button. "PREDICT →" button in saffron. When sequence is empty, shows "Tap symbols to build a message..." in muted italic. 3. **Divider** (1px, grey) 4. **Alternatives Panel** (AlternativesPanel widget) — only visible when _prediction != null. Shows 3 numbered alternatives. Each is a tappable card. When tapped: call _onSentenceConfirmed(). This panel must be animated — slides down from above with a 300ms easeOut when prediction arrives. 5. **Large Text Display** (LargeTextDisplay widget) — only visible when _confirmedSentence != null. Shows the confirmed sentence in very large text (28px minimum). Has a speaking indicator (animated bars) that plays when TTS is active. Must be readable from 1 metre away. 6. **Category Tabs** (CategoryTabBar widget) — horizontal scrollable row. Categories: All, Food/Drink, People, Actions, Feelings, Personal, Places, Objects. Saffron indicator under active tab. 7. **Pictogram Grid** (Expanded, GridView.builder) — shows _displayedSymbols. On tap: call _onSymbolTap(symbol). gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: AppConfig.gridColumns, childAspectRatio: 0.85). **_onSymbolTap(symbol):** - If _selectedSymbols.length >= AppConfig.autoPredictAt → call _predict() first - Add symbol to _selectedSymbols - Rebuild the sequence bar **_predict():** - Set _isPredicting = true - Call api.predict(_selectedSymbols.map((s) => s.conceptEn).toList(), _activePersonaId) - Set _prediction = result - Set _isPredicting = false - Scroll the predictions into view (use a ScrollController or GlobalKey) **_onSentenceConfirmed(String sentence, int index):** - Set _confirmedSentence = sentence - Call api.logUtterance() in background (don't await) - Call audioService.playFromUrl(await api.speak(sentence, _activePersonaId, lang)) - After 4 seconds, call _clearAll() **_clearAll():** Reset _selectedSymbols, _prediction, _confirmedSentence to empty/null. --- ## FILE: pictogram_tile.dart Each tile is a GestureDetector wrapping an AnimatedContainer. The container has: - Border radius: 12px - Border: 1.5px grey when unselected, 2.5px saffron (#E8600A) when selected - Box shadow: subtle when unselected, saffron glow (blurRadius: 12, color: saffron.withOpacity(0.3)) when selected - Background: white when unselected, very light saffron (#FFF3E0) when selected - Animation duration: 120ms Inside the container (Column): - Expanded flex: 3 — the symbol image. Use Image.network(symbol.imageUrl, fit: BoxFit.contain). Use a placeholder grey icon while loading. Use an error icon if image fails. - Expanded flex: 1 — two Text widgets. First: concept in local language (16px, bold, overflow: ellipsis, maxLines: 1). Second: concept in English (12px, muted grey, overflow: ellipsis). On tap: run a scale animation (ScaleTransition or AnimatedScale, scale 0.92 → 1.0 over 100ms) then call onTap. --- ## FILE: alternatives_panel.dart An AnimatedSwitcher wrapping a Column. When prediction arrives, the column slides in from above. Each alternative is a Card widget: - Elevation: 2 (unselected), 6 (on hover/focus) - A Row with: - A CircleAvatar (radius 16) with number 1, 2, or 3. Colours: saffron for 1st, sage green for 2nd, purple for 3rd. - Expanded Text: the sentence in Crimson Pro style (use GoogleFonts.notoSerif for Devanagari compatibility), fontSize 18. - A speaker IconButton (Icons.volume_up_outlined) that calls tts directly on that alternative. - On tap of the card: call onSelected(sentence, index). Include a "Thinking..." loading state (3 animated dots, staggered opacity animation) when isPredicting is true. --- ## FILE: speaking_indicator.dart An animated widget showing 5 vertical bars doing a sine-wave animation. Use AnimationController with duration 1200ms, repeat. Each bar: - Width: 4px - Max height: 24px - Height animated by: sin(time * 2π + i * 0.4) * 0.5 + 0.5 (gives offset sine per bar) - Colour: saffron (#E8600A) Fade in when isSpeaking=true, fade out when false. The whole widget is 40px wide, 32px tall. --- ## FILE: pubspec.yaml ```yaml name: ankahi_flutter description: Ankahi AAC — Gemma 4 E4B offline voice for Indian CP children version: 1.0.0+1 environment: sdk: '>=3.3.0 <4.0.0' dependencies: flutter: sdk: flutter http: ^1.2.1 provider: ^6.1.2 shared_preferences: ^2.2.3 just_audio: ^0.9.38 cached_network_image: ^3.3.1 google_fonts: ^6.1.0 permission_handler: ^11.3.1 path_provider: ^2.1.2 uuid: ^4.4.0 intl: ^0.19.0 audioplayers: ^6.0.0 flutter_animate: ^4.5.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 flutter: uses-material-design: true assets: - assets/pictograms/fallback/ - assets/fonts/ ``` --- # PART 3 — WEB LANDING PAGE (Single HTML file) ## File: landing_page.html Build a complete, self-contained single HTML file. All CSS inline in `