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