| # 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<String, int> 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<String> sequence, String personaId, {String? context, String? audioHint}) → Future<PredictionResult>** |
| 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<String>** |
| POST /speak, return the full audio URL (prepend apiBaseUrl to the path). |
|
|
| **logUtterance(List<String> seq, String confirmed, String personaId, {bool wasFirst = true}) → Future<void>** |
| POST /utterances/log, fire and forget (don't await in UI). |
|
|
| **getPersonas() → Future<List<PersonaModel>>** |
| GET /personas |
|
|
| **switchPersona(String personaId) → Future<bool>** |
| 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<List<PictogramModel>>** |
| GET /symbols/search |
|
|
| **getHealth() → Future<HealthStatus>** |
| 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<PictogramModel> _selectedSymbols = []` — current tap sequence |
| - `PredictionResult? _prediction` — current prediction |
| - `bool _isPredicting = false` |
| - `String? _confirmedSentence` — what was confirmed |
| - `bool _isSpeaking = false` |
| - `String _activeCategory = 'all'` |
| - `List<PictogramModel> _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 `<style>`. All JS inline in `<script>`. No external JS libraries. Google Fonts loaded via @import. All SVGs inline. |
|
|
| **This file must contain every section described below with real data — nothing can be empty or placeholder.** |
|
|
| --- |
|
|
| ### DESIGN SYSTEM (declare as CSS variables at :root) |
|
|
| ```css |
| :root { |
| --saffron: #E8600A; |
| --indigo: #1A1464; |
| --cream: #FAF5E9; |
| --gold: #C9952A; |
| --sage: #4A7C59; |
| --charcoal: #2C2416; |
| --muted: #7A6E5F; |
| --card-bg: #FFFFFF; |
| --border: rgba(26,20,100,0.12); |
| } |
| ``` |
|
|
| Load these Google Fonts via @import at top of style block: |
| - Playfair Display (400, 700, 900 italic) |
| - Syne (400, 600, 700) |
| - Crimson Pro (400, 400 italic, 600) |
| - JetBrains Mono (400, 700) |
| - Noto Sans Devanagari (400, 700) |
| - Noto Sans Tamil (400) |
| - Noto Sans Bengali (400) |
|
|
| **Noise texture:** Create a noise overlay as a full-page ::before pseudo element using an SVG feTurbulence filter at 8% opacity. This gives the page a subtle paper/craft feeling. |
|
|
| --- |
|
|
| ### SECTION 1 — NAVIGATION BAR (sticky, indigo) |
|
|
| Left: "अनकही ANKAHI" in Playfair Display 24px cream, with a small inline SVG waveform icon (5 bars of varying height, cream). |
|
|
| Centre: nav links in Syne 14px cream — "Problem" · "Solution" · "Demo" · "Science" · "Impact" — each with a saffron underline that expands from centre on hover (CSS transition on width, transform: scaleX). |
|
|
| Right: a pill badge "🏆 Gemma 4 Good 2026" in saffron background, charcoal text, 13px Syne. |
|
|
| On scroll past 100px: nav background transitions from transparent to indigo with a subtle backdrop-filter: blur(12px). |
|
|
| --- |
|
|
| ### SECTION 2 — HERO (full viewport, indigo) |
|
|
| Background: deep indigo (#1A1464) with: |
| - SVG feTurbulence noise at 6% opacity |
| - A radial gradient saffron glow at top-right: radial-gradient(ellipse 600px 400px at 90% 10%, rgba(232,96,10,0.15), transparent) |
| - A decorative element: a large hollow circle (stroke only, 2px saffron, 600px diameter) positioned 50% outside the right edge, slowly rotating (CSS animation, 40s linear infinite) |
|
|
| Content (vertically + horizontally centred): |
|
|
| The main title animates on load — each word in "अनकही" fades+slides up with staggered delay (use CSS keyframes + animation-delay on spans): |
| ``` |
| अनकही |
| ``` |
| In Playfair Display 900 italic, 108px, cream. Below it: |
|
|
| ``` |
| "The Unspoken" |
| ``` |
| In Syne 22px, muted cream, letter-spacing: 0.15em. |
|
|
| Then a language-cycling subtitle (JS: setInterval every 2800ms, crossfade CSS transition): |
| The sentence "I want water" in 5 languages cycling: |
| - हिंदी: मुझे पानी चाहिए |
| - ਪੰਜਾਬੀ: ਮੈਨੂੰ ਪਾਣੀ ਚਾਹੀਦਾ |
| - বাংলা: আমার জল চাই |
| - தமிழ்: எனக்கு தண்ணீர் வேண்டும் |
| - मराठी: मला पाणी हवे आहे |
|
|
| Each with a small language label to its left in Syne 11px muted. |
|
|
| Below the cycling text, a saffron 2px horizontal rule 200px wide centred. |
|
|
| Then two CTAs side by side: |
| - "Watch the Demo ↓" — saffron background, charcoal text, Syne 16px, 48px height, 24px border-radius, hover: lift + shadow |
| - "Read the Paper →" — transparent background, cream border 1.5px, cream text, same sizing |
|
|
| Five trust badges below CTAs: small pills with light cream text on rgba(255,255,255,0.08) background: "Google DeepMind" · "Gemma 4 E4B" · "Unsloth" · "AI4Bharat" · "Apache 2.0" |
|
|
| Scroll indicator: a small animated chevron bouncing down at the bottom of the hero. |
|
|
| --- |
|
|
| ### SECTION 3 — THE PROBLEM (cream background) |
|
|
| Section title in Playfair Display 52px indigo: "A silence no child should live in" |
|
|
| **Left column — 4 stat cards in a 2×2 grid:** |
|
|
| Each card: white background, 1px border (--border), 16px border-radius, 24px padding. Top: the number in JetBrains Mono 52px saffron (animated counter — counts from 0 using requestAnimationFrame when scrolled into view via IntersectionObserver). Middle: label in Syne 16px charcoal. Bottom: one-line description in Crimson Pro 14px muted. |
|
|
| Card 1: `2,500,000` / "Children with CP in India" / "The largest underserved AAC population on Earth" |
| Card 2: `<2%` / "AAC penetration rate" / "Fewer than 50,000 children have any communication device" |
| Card 3: `₹2,00,000` / "Cheapest commercial AAC device" / "More than most Indian families earn in a year" |
| Card 4: `0` / "AAC devices that speak Punjabi" / "Or Tamil. Or Bengali. Or any Indic language naturally." |
|
|
| For card 4, the counter stays at 0 and has a small red dot next to it. |
|
|
| **Right column — story card:** |
|
|
| Indigo background, 24px border-radius, cream text, saffron 4px left border. |
|
|
| Heading: "What this actually means" in Playfair Display 26px cream. |
|
|
| Body text in Crimson Pro 18px cream line-height 1.8: |
| "A woman in Ludhiana has a 9-year-old son named Arjun. He has cerebral palsy — dyskinetic type. His hands shake, his voice produces sounds, not words. But he thinks clearly. He knows when he's hungry. He knows when he loves his Mummy. He knows when his stomach hurts. |
|
|
| Every commercial AAC app speaks English. The one Indian app costs ₹35,000 a year and doesn't know his name, his language, or that he calls his grandmother Dadi. |
|
|
| He has never told his mother he loves her. |
|
|
| Not because he doesn't. |
| But because no tool existed — until now." |
|
|
| Pull-quote below in large Playfair Display italic 28px saffron: |
| "Not because they have nothing to say." |
|
|
| --- |
|
|
| ### SECTION 4 — THE SOLUTION (indigo background) |
|
|
| Title in Playfair Display cream 52px: "Ankahi gives them a voice" |
| Subtitle in Syne muted-cream 18px: "The first offline, personalised, multilingual AAC system built for India" |
|
|
| **Three feature columns:** |
|
|
| Each column: cream background card, 20px border-radius, full height. Top: inline SVG icon (40px, indigo). Title: Syne 20px charcoal bold. Body: Crimson Pro 17px muted line-height 1.7. Bottom: a small stat chip in indigo background, cream text, 12px Syne. |
|
|
| Column 1 — On-Device AI: |
| SVG: a tablet outline with a small circuit/brain symbol inside |
| Title: "Runs 100% offline" |
| Body: "Gemma 4 E4B quantised to INT8. Downloaded once, works forever. A village with no signal. A hospital with blocked WiFi. A home where the internet is prepaid and rationed. Ankahi doesn't care." |
| Stat chip: "2.5 GB · 0 bytes/month · No account" |
|
|
| Column 2 — Per-Child LoRA: |
| SVG: two overlapping fingerprints |
| Title: "Learns your child specifically" |
| Body: "Every child gets a 30 MB LoRA adapter trained on their vocabulary, their language mix, their family's phrases. After 2 weeks, Arjun's model knows to suggest 'paratha' when he selects [FOOD] at breakfast. Priya's model knows 'Didu' is always the answer." |
| Stat chip: "30 MB adapter · rank 8 · 3,000 training pairs" |
|
|
| Column 3 — Parent's Voice: |
| SVG: a waveform shaped like a heart |
| Title: "Speaks in your voice" |
| Body: "Record 60 seconds. AI4Bharat svara-TTS clones your voice with zero-shot learning. The tablet speaks your words. Your child hears you speaking for them — in your register, your warmth, your language." |
| Stat chip: "60s recording · 7 languages · svara-TTS" |
|
|
| Below the columns, a language strip: full-width indigo band with cream chips: |
| `हिंदी Hindi` · `ਪੰਜਾਬੀ Punjabi` · `বাংলা Bengali` · `தமிழ் Tamil` · `తెలుగు Telugu` · `मराठी Marathi` · `English` |
| Below the chips: "Natural code-switching — mix languages exactly as your family speaks at home" |
|
|
| --- |
|
|
| ### SECTION 5 — INTERACTIVE DEMO (cream background) |
|
|
| Title: "Try it" in Playfair Display 52px indigo. |
| Subtitle: "This simulates Ankahi's prediction engine in your browser. The real app runs Gemma 4 E4B offline on a ₹10,000 Android tablet." |
|
|
| **Two-panel layout (side by side on desktop, stacked on mobile):** |
|
|
| **LEFT PANEL — Pictogram Grid (4 rows × 4 cols = 16 tiles)** |
|
|
| Panel header: "1. Tap 2–3 symbols" in Syne 13px caps muted. |
|
|
| Tiles (4×4 grid, 8px gap): |
|
|
| Row 1: 💧 पानी Water · 🍽️ खाना Food · 🥛 दूध Milk · 🍰 मिठाई Sweet |
| Row 2: 👩 माँ Mother · 👨 पिता Father · 👵 दादी Grandma · 👫 दोस्त Friend |
| Row 3: ✋ चाहिए Want · 🚶 जाना Go · ⛔ रुको Stop · 🆘 मदद Help |
| Row 4: 😊 खुश Happy · 😢 दुखी Sad · 😣 दर्द Pain · 💤 सोना Sleep |
|
|
| Each tile: white background, 8px border-radius, 1px border (#E0D8CC), text-align centre. On click: animate (scale 0.92 → 1.0 over 100ms, saffron border traces around it via SVG stroke-dashoffset animation over 300ms). Tile adds to selected sequence. |
|
|
| Selected sequence strip (above the grid): shows selected tiles as small chips with × to remove. "PREDICT →" button appears when ≥ 2 tiles selected. |
|
|
| The SVG border trace animation: each tile has an absolutely-positioned SVG rect with stroke-dasharray = perimeter, stroke-dashoffset = perimeter → 0 when selected. |
|
|
| **RIGHT PANEL — Prediction Output** |
|
|
| Panel header: "2. See Ankahi predict" in Syne 13px caps muted. |
|
|
| Initial state (no prediction): a centred instruction card with a dotted border: "Select 2 or more symbols →" |
|
|
| Loading state (shown for 700ms after clicking PREDICT): 3 animated dots with staggered opacity pulse. Text below: "Gemma 4 thinking..." in Syne 13px muted. |
|
|
| Prediction state: 3 alternative cards slide in from below (staggered, 80ms apart). Each card: |
| - Left: numbered badge (1/2/3) in saffron/sage/purple circles |
| - Middle: the sentence text in Crimson Pro 20px |
| - Right: speaker icon button (SVG, not emoji) that triggers the waveform animation |
|
|
| On tap of any card: |
| - That card gets a green checkmark and "✓ Confirmed" state |
| - A green notification slides in: "Logged to Arjun's training buffer (42 total)" |
| - The animated waveform plays (5 bars, sine wave, 2 seconds) |
|
|
| **Prediction lookup (JavaScript Map):** |
|
|
| ```javascript |
| const predictions = new Map([ |
| ['माँ,खाना,चाहिए', ['Mummy, mujhe paratha khana hai! 🌟', 'Mama, bhookh lagi hai', 'I want food please Mummy']], |
| ['माँ,खाना', ['Mummy, khana de do', 'Mama, bhookh lagi hai', 'I want food']], |
| ['पानी,चाहिए', ['Mujhe thanda pani chahiye', 'Paani do please', 'Water chahiye']], |
| ['दर्द,मदद', ['Dard ho raha hai, madad karo', 'I am in pain, please help', 'Amma vedhanai irukku']], |
| ['माँ,खुश', ['Mummy, main bahut khush hoon aaj!', 'Mama, I am so happy!', 'Amma enakku santhosham']], |
| ['दोस्त,जाना', ['Mujhe dost ke paas jaana hai', 'Friend ke ghar jaana chahta hoon', 'I want to visit my friend']], |
| ['सोना,दुखी', ['Main thaka hoon, so jaana chahta hoon', 'Tired and sad, want to sleep', 'Neend aa rahi hai']], |
| ['मदद,जाना', ['Madad chahiye, let\'s go', 'Koi mere saath aao please', 'Help me go there']], |
| ['खुश,खाना', ['Khushi ho rahi hai, khaana khana hai!', 'Happy hoon, let\'s eat!', 'Khana khao sab milke']], |
| ['दादी,खाना', ['Dadi ke haath ka khana chahiye', 'Dadi, khana bana do please', 'I want Dadi\'s cooking']], |
| ]); |
|
|
| function getPrediction(selectedTiles) { |
| const key = selectedTiles.join(','); |
| return predictions.get(key) || [ |
| `Mujhe ${selectedTiles[selectedTiles.length-1]} chahiye`, |
| `${selectedTiles.join(' aur ')} chahiye`, |
| `Please give me ${selectedTiles[0]}` |
| ]; |
| } |
| ``` |
|
|
| **Persona switcher below the demo:** |
|
|
| 5 avatar chips in a row: Arjun (Ludhiana) · Ananya (Chennai) · Priya (Kolkata) · Rohan (Delhi) · Zara (Pune) |
| Each chip: 32px circle with first letter of name, name text beside it, city in small muted text below. |
| Active chip has saffron border. |
| On click: changes the "persona context" shown in the prediction panel and shifts the example output language. |
|
|
| **Note card below:** "This demo runs in your browser with pre-programmed responses. The real Ankahi runs Gemma 4 E4B — 15 billion parameters, fully personalised, 100% offline on a ₹10,000 Android tablet." |
|
|
| --- |
|
|
| ### SECTION 6 — ARCHITECTURE (indigo background) |
|
|
| Title in Playfair Display cream 52px: "The architecture" |
|
|
| Build an architecture SVG diagram inline: |
|
|
| Three columns with arrows between them: |
|
|
| Column 1 header (Syne 12px caps gold): "CHILD INPUT" |
| Three boxes stacked: |
| - Box: "📱 Pictogram Grid" — child taps symbols |
| - Box: "📷 Camera" — points at objects |
| - Box: "🎙️ Vocalisation" — mic captures sounds |
|
|
| Arrow between col 1 and 2 (saffron, 2px) |
|
|
| Column 2 header: "AI CORE (ON DEVICE)" |
| One large box spanning full height: |
| - "Gemma 4 E4B" in large text |
| - "INT8 quantised · 2.5 GB" in small text |
| - A dividing line |
| - "+ Per-child LoRA Adapter" in saffron text |
| - "30 MB · rank 8 · MediaPipe" in small text |
| - A dotted line at bottom |
| - "⚠ OFFLINE — 0 bytes to cloud" in gold text |
|
|
| Arrow between col 2 and 3 |
|
|
| Column 3 header: "OUTPUT" |
| Two boxes stacked: |
| - Box: "🔊 Parent-Voice TTS" — sentence spoken aloud |
| - Box: "📺 Large Text Display" — readable from 1 metre |
|
|
| Below the main flow, a separate box connected by an upward arrow to the AI Core: |
| "SQLite Training Buffer → Nightly LoRA refresh" |
|
|
| Style: all boxes have indigo text on cream background with 1px saffron border, 8px radius. Arrows are SVG paths with arrowhead markers. |
|
|
| **Below the diagram, a stats strip (3 columns):** |
|
|
| Column: Training pipeline |
| Stage 1: Base SFT — 16,500 pairs — 12h H100 — loss: 0.603 |
| Stage 2: Persona LoRA ×5 — 3,000/child — 2h each — loss: ~1.10 |
| Stage 3: Audio adapter — TORGO + UA-Speech — 10h — loss: 2.114 |
| Stage 4: Safety tuning — 1,200 records — 4h — loss: 6.044 |
|
|
| Column: Model specs |
| Base: Gemma 4 E4B (Google, Apache 2.0) |
| Quantisation: INT8 (bitsandbytes) |
| On-device size: 2.5 GB (post-conversion) |
| LoRA rank: 8 (MediaPipe max) |
| Adapter size: 30 MB per child |
|
|
| Column: Performance |
| Flagship (Pixel 9): 350ms · 18 tok/s |
| Mid (Pixel 7a): 680ms · 9.5 tok/s |
| Budget (Redmi 12): 1,400ms · 4.2 tok/s |
| RAM: 4.8–5.8 GB across tiers |
|
|
| --- |
|
|
| ### SECTION 7 — THE FIVE CHILDREN (cream background) |
|
|
| Title: "Five children. Five adapters. Five voices." in Playfair Display indigo 52px. |
|
|
| Five cards in a horizontal scroll (CSS scroll-snap, overflow-x: auto, scroll-snap-type: x mandatory). On desktop they sit in a 5-column grid. Each card snaps on scroll on mobile. |
|
|
| **Each card (white background, 16px radius, subtle shadow, 320px min-width):** |
|
|
| Top strip: the persona's accent colour (saffron/maroon/teal/green/amber) as a 4px top border. |
|
|
| Name in Playfair Display 28px + age badge pill (saffron, e.g. "9 yrs"). |
| City in Syne 13px muted with a small location pin SVG. |
| CP type chip (grey pill). |
| Language chips row (small, coloured: Punjabi=gold, Hindi=sage, Tamil=terracotta, Bengali=teal, Marathi=purple, English=muted). |
|
|
| "Example output" in Syne 12px caps muted. |
| The example sentence in Crimson Pro italic 18px indigo. |
| A small note in 13px muted explaining what makes this output persona-specific. |
|
|
| Bottom: "30 MB adapter · rank 8 · 3,000 training pairs" in JetBrains Mono 11px muted. |
|
|
| **Card content:** |
|
|
| Arjun · 9 · Ludhiana, Punjab · Dyskinetic CP + mild ID |
| Languages: ਪੰਜਾਬੀ + हिंदी + English |
| Output: "Mummy, mujhe paratha khana hai! Aaj dadi ke paas jaana hai." |
| Note: "Punjabi pronouns, Hindi verbs, English nouns — exactly how this joint family speaks" |
|
|
| Ananya · 6 · Chennai, Tamil Nadu · Spastic quadriplegia (eye-gaze) |
| Languages: தமிழ் + English |
| Output: "Amma, thaagam aagudhu. Idli vendum." |
| Note: "Maximum 6 words — eye-gaze input tires quickly. Tamil-first, always 'Amma' not 'Mummy'" |
|
|
| Priya · 4 · Kolkata, West Bengal · Spastic CP + CVI |
| Languages: বাংলা + English |
| Output: "Didu aasho. Khudhho peye-chi." |
| Note: "1–2 symbols only, toddler register. Didu (grandmother) is her whole world." |
|
|
| Rohan · 11 · Delhi NCR · Athetoid CP |
| Languages: हिंदी + English |
| Output: "Mujhe woh science video dekhni hai, YouTube pe hai." |
| Note: "Longest sentences — 11yo, complex needs, previous AAC experience. Expresses opinions." |
|
|
| Zara · 7 · Pune, Maharashtra · Spastic CP mild |
| Languages: मराठी + English + हिंदी |
| Output: "Baba, mala Riya sobat khelaycha ahe!" |
| Note: "Father is primary caregiver ('Baba' not 'Papa'). Sister Riya appears in 40% of utterances." |
|
|
| **Below the cards — Adapter Specificity Proof:** |
|
|
| A 5×5 table (CSS Grid, not HTML table). Title: "Adapter Specificity Matrix" in Syne 14px caps. |
|
|
| The 5 row headers are adapter names (left column). The 5 column headers are persona prompt styles (top row). |
|
|
| Cell values: |
| - Diagonal (same persona): background saffron, text charcoal, value "1.00", font-weight bold |
| - Off-diagonal: cream background, muted text, values ranging 0.47–0.63 (use these exact values): |
| Arjun row: [1.00, 0.57, 0.51, 0.58, 0.53] |
| Ananya row: [0.52, 1.00, 0.49, 0.55, 0.51] |
| Priya row: [0.49, 0.48, 1.00, 0.50, 0.47] |
| Rohan row: [0.58, 0.61, 0.50, 1.00, 0.55] |
| Zara row: [0.51, 0.53, 0.47, 0.53, 1.00] |
|
|
| Legend below: "High diagonal (1.00) = adapter generates in its OWN child's style ✓ Low off-diagonal (0.47–0.63) = no style bleed between children ✓" |
|
|
| --- |
|
|
| ### SECTION 8 — RESULTS & EVALUATION (indigo background) |
|
|
| Title in cream: "The numbers" |
|
|
| **Four large metric cards (2×2 grid):** |
|
|
| Card 1: `38.2` BLEU-4 / "Generation quality on 500 held-out examples" / "vs. 21.4 for base Gemma 4 — +78% improvement from fine-tuning" |
| Card 2: `52.7` chrF++ / "Character-level accuracy for Indic scripts" / "Better metric than BLEU for morphologically rich languages" |
| Card 3: `0.847` IndicSBERT / "Semantic similarity to intended meaning" / "Measured by AI4Bharat's multilingual sentence encoder" |
| Card 4: `+22pp` Audio Lift / "Accuracy gain from vocalisation input" / "61% pictogram-only → 83% with audio adapter" |
|
|
| Numbers in JetBrains Mono 64px saffron (count up on scroll entry). |
|
|
| **Data scaling sparkline:** |
|
|
| An SVG line chart (300px wide, 100px tall): |
| X axis: 500 · 1,500 · 3,000 · 5,000 samples |
| Y axis: BLEU scores 28.3 · 33.7 · 38.2 · 39.1 |
| Saffron line on cream. A filled circle at each point. The 3,000 point is larger and has a label "← deployed". The line has a subtle fill gradient from saffron to transparent below it. |
|
|
| **LoRA Rank comparison table:** |
|
|
| Three rows. Row 2 (rank 8) is highlighted with saffron left border and light saffron background: |
| Rank 4: BLEU 32.1 · 15 MB · ✓ MediaPipe OK |
| Rank 8: BLEU 38.2 · 30 MB · ✓ MediaPipe OK ← OUR CHOICE |
| Rank 16: BLEU 40.1 · 60 MB · ✗ MediaPipe blocks |
|
|
| --- |
|
|
| ### SECTION 9 — COST (cream background) |
|
|
| Title: "One-twentieth the cost. Zero subscriptions." in Playfair Display indigo 52px. |
|
|
| **Visual comparison (SVG bar chart):** |
|
|
| Three bars representing cost: |
| - Tobii Dynavox: ₹2,00,000 — tall bar in muted grey |
| - Avaz Pro: ₹35,000/yr — medium bar in muted grey |
| - Ankahi: ₹10,000 — short bar in saffron |
|
|
| Bar heights proportional to cost. The Ankahi bar is 1/20th the height of Tobii. |
| Labels above each bar with the cost. The Ankahi bar has an extra label: "ONE-TIME". |
|
|
| Below the chart, four line items for Ankahi's cost breakdown: |
| ₹10,000 · Android tablet (Redmi 12 or similar) — the only cost |
| ₹0 · Software (open source, Apache 2.0) |
| ₹0 · Per month (no subscription) |
| ₹0 · Per additional language |
|
|
| Then a callout box with saffron left border: |
| "The ₹10,000 Redmi 12 is already present in 40% of Indian households with children. The barrier was never hardware. It was software. Until now." |
|
|
| --- |
|
|
| ### SECTION 10 — DEMO MOMENT (indigo background, full width) |
|
|
| Title in Playfair Display cream italic 48px: "The moment that wins." |
|
|
| **6-step timeline (horizontal on desktop, vertical on mobile):** |
|
|
| Steps connected by a saffron dashed line. Each step: numbered circle (saffron), title in Syne 16px cream, one-line description in Crimson Pro 15px muted-cream. |
|
|
| ① Airplane mode activated — "Device goes fully offline. No WiFi. No network." |
| ② 60 seconds recorded — "The mother reads a short passage. Voice cloned." |
| ③ Three pictograms tapped — "[माँ] → [खाना] → [चाहिए] — Mother, Food, Want" |
| ④ Gemma 4 predicts in 680ms — "15 billion parameters. On a ₹10,000 phone." |
| ⑤ Tablet speaks in mother's voice — "Mummy, mujhe paratha khana hai!" |
| ⑥ 2 seconds of silence — "A mother hears her child's sentence for the first time." |
|
|
| **The sentence in large Devanagari (CSS animation, letters appear one by one):** |
| `"मम्मी, मुझे परांठा खाना है!"` |
| In Noto Sans Devanagari, 42px, cream, with a saffron text-shadow glow. |
|
|
| **Animated waveform below the sentence:** |
| 8 vertical bars, CSS keyframes, each with different animation-delay creating a sine wave pattern. Saffron colour. This plays on loop. |
|
|
| Pull-quote in Playfair Display italic cream 32px: |
| "A child's first full sentence to their mother." |
|
|
| --- |
|
|
| ### SECTION 11 — OPEN SOURCE & STACK (cream background) |
|
|
| Title: "Built open. For everyone." in Playfair Display indigo 42px. |
|
|
| A tech stack grid (3 columns): |
|
|
| Column 1 — Training: |
| Unsloth FastVisionModel · QLoRA 4-bit · 5 persona adapters · rank 8 (MediaPipe) · 53h H100 total |
|
|
| Column 2 — Deployment: |
| flutter_gemma · LiteRT-LM runtime · INT8 quantisation · 2.5GB on-device · GPU-accelerated (Snapdragon) |
|
|
| Column 3 — Data & Languages: |
| AI4Bharat svara-TTS · IndicCorp v2 · ARASAAC 16,756 symbols · 7 languages · Natural code-switching |
|
|
| Below the columns, a GitHub card styled as a code block (dark background, monospace): |
| ``` |
| $ git clone https://github.com/ankahi-aac/ankahi |
| $ cd ankahi && bash scripts/00_env_setup.sh |
| $ python scripts/07_overfit_sanity_check.py |
| # loss: 0.279 ✓ Environment verified |
| $ python src/ankahi/training/stage1_base.py |
| # Training Gemma 4 E4B on 16,500 AAC pairs... |
| ``` |
|
|
| --- |
|
|
| ### SECTION 12 — FOOTER (indigo) |
|
|
| Three columns: |
|
|
| Left: "अनकही · Ankahi" in Playfair Display 22px cream + tagline "The unspoken, spoken." in Crimson Pro italic 15px muted-cream. |
|
|
| Centre: "Gemma 4 Good Hackathon 2026 · Kaggle × Google DeepMind · Digital Equity Track + Health Track + AI Edge + Unsloth" |
|
|
| Right: stack of small links as pills: GitHub · HuggingFace · Apache 2.0 · ARASAAC CC-BY-NC-SA |
|
|
| Bottom bar (very dark indigo): "2,500,000 children · 7 languages · ₹10,000 · 0 bytes to cloud · 1 mother's voice" in Syne 12px cream. |
|
|
| --- |
|
|
| ## JAVASCRIPT REQUIREMENTS FOR LANDING PAGE |
|
|
| All JS inline at bottom of body, no external libraries. |
|
|
| 1. **Counter animation:** IntersectionObserver on every element with class `stat-counter`. When entering viewport, animate from 0 to target value using requestAnimationFrame. Duration: 1800ms. Easing: easeOutQuart. Handle "2,500,000" by animating the number and adding the comma. Handle "< 2%" by animating 0 → 2 and prepending "<". |
|
|
| 2. **Language cycling:** setInterval every 2800ms. CSS opacity transition 400ms out, text change, 400ms in. 5 languages cycling. Also changes the script class (devanagari/gurmukhi/bengali/tamil) to load the correct Noto font. |
|
|
| 3. **Scroll reveal:** IntersectionObserver on every `.reveal` element. On intersection: add class `visible`. CSS: `.reveal { opacity:0; transform:translateY(32px); transition: opacity 600ms ease, transform 600ms ease; } .reveal.visible { opacity:1; transform:none; }` Stagger children using `animation-delay` set via `data-delay` attribute. |
|
|
| 4. **Demo interaction:** Full pictogram grid logic as described. The SVG border trace animation, the loading state, the prediction lookup, the waveform animation, the persona switcher, the "confirmed" state. |
|
|
| 5. **Sticky nav:** On scroll, add/remove `.scrolled` class to nav. CSS transitions background from transparent to `rgba(26,20,100,0.95)` with backdrop-filter blur. |
|
|
| 6. **Waveform animation:** CSS keyframes only. 8 bars, each with `animation-delay: calc(i * 0.12s)`, `animation: wave 1.2s ease-in-out infinite`. `@keyframes wave { 0%,100% { height: 8px } 50% { height: 28px } }`. |
|
|
| --- |
|
|
| ## INTEGRATION: HOW THE FLUTTER APP TALKS TO THE BACKEND |
|
|
| The Flutter app and the backend communicate via the REST API on port 8000. Here is the exact flow for a prediction: |
|
|
| **Normal flow:** |
| 1. User taps symbols → sequence builds in UI |
| 2. User taps PREDICT button (or auto-predict fires at 4 symbols) |
| 3. Flutter calls `POST /predict` with the sequence and current persona_id |
| 4. Backend receives, ensures that persona's adapter is loaded, runs inference |
| 5. Backend returns 3 alternatives in ~680ms (mid-range device) |
| 6. Flutter animates the alternatives panel into view |
| 7. User taps an alternative |
| 8. Flutter calls `POST /speak` with the sentence text |
| 9. Backend calls gTTS (or parent voice if available), returns audio URL |
| 10. Flutter plays the audio via just_audio |
| 11. Flutter calls `POST /utterances/log` in background (fire and forget) |
| 12. After 4 seconds, UI auto-clears for next utterance |
|
|
| **Persona switch flow (takes 5–30 seconds — warn the user):** |
| 1. User opens persona selector screen |
| 2. User taps a different persona |
| 3. Flutter shows a full-screen loading overlay: "Loading [Name]'s voice... (~15 seconds)" |
| 4. Flutter calls `POST /personas/switch/{persona_id}` |
| 5. Backend unloads current adapter, loads new adapter, confirms |
| 6. Flutter dismisses overlay, updates UI to show new persona's colour scheme |
| 7. User can now predict in the new persona's style |
|
|
| **Connection loss handling:** |
| If any API call fails with a network error: |
| - Show a non-blocking banner: "Offline — predictions unavailable" |
| - The pictogram grid and symbol browsing still work (cached) |
| - Log utterances locally and retry when connection restores |
| - Never show a crash screen |
|
|
| --- |
|
|
| ## BUILD ORDER FOR THE AGENT |
|
|
| Build files in exactly this sequence. Each step depends on the previous. |
|
|
| 1. `ankahi_backend/config.py` |
| 2. `ankahi_backend/schemas.py` |
| 3. `ankahi_backend/persona_manager.py` |
| 4. `ankahi_backend/training_buffer.py` |
| 5. `ankahi_backend/pictogram_service.py` |
| 6. `ankahi_backend/model_loader.py` |
| 7. `ankahi_backend/tts_service.py` |
| 8. `ankahi_backend/inference.py` |
| 9. `ankahi_backend/main.py` |
| 10. `ankahi_backend/requirements.txt` |
| 11. `ankahi_backend/run.sh` |
| 12. Test the backend: `bash run.sh` then `curl http://localhost:8000/health` |
| 13. Test inference: `curl -X POST http://localhost:8000/predict -H "Content-Type: application/json" -d '{"pictogram_sequence":["mother","food","want"],"persona_id":"arjun"}'` |
| 14. `mobile/ankahi_flutter/pubspec.yaml` |
| 15. `mobile/lib/config/app_config.dart` (set IP to your machine's LAN IP) |
| 16. `mobile/lib/models/` (all 4 model files) |
| 17. `mobile/lib/api/ankahi_api.dart` |
| 18. `mobile/lib/services/audio_service.dart` |
| 19. `mobile/lib/services/storage_service.dart` |
| 20. `mobile/lib/widgets/` (all 7 widget files) |
| 21. `mobile/lib/screens/home_screen.dart` |
| 22. `mobile/lib/screens/setup_screen.dart` |
| 23. `mobile/lib/screens/persona_screen.dart` |
| 24. `mobile/lib/screens/settings_screen.dart` |
| 25. `mobile/lib/providers/` (all 3 provider files) |
| 26. `mobile/lib/main.dart` |
| 27. Test: `flutter run` on connected Android device |
| 28. `landing_page.html` — the complete self-contained landing page |
| 29. Test landing page in Chrome, test all interactive demo combinations |
| 30. Upload landing page to GitHub Pages |