File size: 56,720 Bytes
dc7e099 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 | # 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 |