Skip to content

feat(kids-mode): guided Japji practice with pronunciation feedback - #401

Open
avioberoi wants to merge 23 commits into
KhalisFoundation:devfrom
avioberoi:kids-mode-design
Open

avioberoi wants to merge 23 commits into
KhalisFoundation:devfrom
avioberoi:kids-mode-design

Conversation

@avioberoi

@avioberoi avioberoi commented Oct 17, 2026 •

Copy link
Copy Markdown

What this adds

Kids Mode: a practice layer for children in grades 6 to 8 that sits on top of the banis Sundar Gutka already carries. A bani becomes a journey of sections; each section has listen, practise and a recited challenge with word-level pronunciation feedback against the reciter the child chose. Japji Sahib ships first with a full designed journey. The child listens to a reciter, recites a line, and sees which words sounded like the reciter and which need another try. It is off by default. A parent turns it on in Settings, and a second switch has to be on before any audio leaves the phone.

We built this for the Sarbat hackathon as an extension of Sundar Gutka, so the aim was to touch as little of the existing app as we could and keep the new code in one folder.

How it is wired

  • Settings: a "Kids Mode" section on the screen's own SettingsSection and SettingsToggleRow, with the Kids Mode switch, the "Check pronunciation online" switch, and a Reciter chooser (Giani Gurdev Singh, Bhai Jarnail Singh, Bibi Indermohan Kaur). Audio and scoring follow the choice.
  • Home header: a Kids icon in the left slot (the mirror of the settings slot), built like the settings control so the two match in size and hit area, shown only when Kids Mode is on, opens the KidsHome route with the native header hidden.
  • src/KidsMode/design/: the screens as our designer built and tested them in an Expo app, ported to JavaScript with the types stripped. They stay verbatim (/* eslint-disable */ at the top) so they can be diffed against the source. The edits we did make are listed below.
  • src/KidsMode/host/: what the screens need from the app, written for this stack: reference audio through react-native-track-player, icons through react-native-vector-icons, bundled art, and line recording through react-native-audio-recorder-player.
  • src/KidsMode/scoring/: the client for the scoring service. KIDS_SCORING_URL and KIDS_SCORING_API_KEY in src/common/constant.js are empty here. With no URL the online check stays off, and with the opt-in off the client returns without a network call.
  • Progress and settings live in the kidsMode redux slice (experience) and persist with the rest of the store.
  • Bani data comes from the app itself: sections, text, transliteration and translation through a query in src/database/db.js (getBaniLinesByToken), line timings from the reader's per-reciter lyrics files, the track from the audio manifest. Japji built this way is identical to the design's own data on all 374 lines. The screens open Japji, whose journey the design draws; the other banis in the catalog stay "coming soon" until they have one.
  • Native changes: RECORD_AUDIO on Android; NSMicrophoneUsageDescription and the Feather and FontAwesome5 icon fonts in UIAppFonts on iOS (the Baloo Paaji 2 fonts were already registered); one new dependency (react-native-audio-recorder-player 3.6.14).

How the scoring works

The app knows the text, so the service does not transcribe. It aligns the recording to the expected text (CTC forced alignment), then scores each word on how strongly the model's frame probabilities favour the expected sounds, against the one reciter the child heard. The text it aligns to is the recited form, as the terminal sihari and aunkar are written but not spoken, and all three reciters in the app drop them (33 of 36 words in Pauri 1). Each word comes back as like, check or unclear. There is no 0 to 100 score per word, as the acoustic distances mean nothing at that resolution. Silence, a clip under a second, or more than half the words unaligned returns unclear with no score, as we would rather say nothing than invent a number.

We tried five models on the three reciter recordings that ship with the app, with planted errors (matra swaps such as ਸੋਚੈ to ਸੋਚਿ, consonant swaps such as ਲਖ to ਰਖ, a deleted word) and timed them on CPU:

Model Matra swaps caught Terminal-vowel rule Consonant swaps Time per 27 s pauri (CPU) Licence
w2v-BERT Punjabi (kdcyberdude/w2v-bert-punjabi) 83% 100% 100% 44 s CC-BY-4.0
IndicConformer trained on Gurbani (surindersinghssj/indicconformer-pa-v3-kirtan) 88% 0% 100% 3.2 s Apache-2.0
MMS 1B + Punjabi adapter 88% 89% 100% 19 s non-commercial
MMS forced aligner (Roman letters) 25% 11% 20% 7 s non-commercial
IPA phone model (xlsr-53-espeak) 17% 0% 73% 15 s Apache-2.0

The Gurbani-trained IndicConformer runs the demo service, as it is fast and Apache licensed. It cannot hear the silent-sihari rule, so w2v-BERT Punjabi is the candidate for production once it has an int8 export or a GPU. No model heard a dropped word-final kanna (ਸਿਆਣਪਾ to ਸਿਆਣਪ) or extra speech the text does not contain. Everything was measured on three adult professional reciters in studio conditions. No child's voice has been scored yet, so the false-flag rate on children is unknown. The live service scores Japji, Jaap, Savaiye, Chaupai, Anand, Rehras and Sohila against the reciters the CDN has for each, and serves the word clips the review screen plays. Method, scripts and numbers: https://github.com/Gurjaapk1/Ucharaan-2026 (docs/scoring-writeup.md, demo-server/).

Edits to the designed screens

  • The privacy line said the voice is not sent anywhere, which was true of the earlier on-device version and not of the service. It now says the recording goes to the scoring service only when a parent turns on "Check pronunciation online".
  • KidsApp takes host.initialScreen, so the Kids icon opens the journey (or the explorer choice on first run) instead of the design's own copy of the Sundar Gutka home.
  • Android's hardware back steps through the journey like the header chevron (one BackHandler listener).
  • A closing ॥ is kept on the line of its last word at render time (components.jsx); the stored text and the word tokenizer are untouched.
  • The Illustrations switch gets the same track colour as its neighbours.
  • A conditional in the word review with two identical branches is reduced to its text (DeepScan).
  • Inside the app every route to the design's own home screen leaves the journey instead, and its Kids Mode switch is hidden (the parent's lives in Settings).
  • The finish guard is a ref, and navigation cancels the recorder even before it reports recording.
  • A line the service could not check reads "Not checked" in the review.
  • Gurjaap's region environments replace the six earlier map scenes (new nodes placed on the platforms).
  • The practice label, the privacy line and the word review show the reciter the parent chose (host.reciterName) instead of a fixed name.
  • Back from the journey returns to the Sundar Gutka home through host.exit instead of the design's own copy of it.
  • The "Preview controls" section and the prototype's About text in the Kids settings only show in dev builds (__DEV__).
  • Artwork is re-encoded from the originals: 4.9 MB in total (56 files), story art at 1600 px wide.

Where this goes

The pieces are per bani, not per Japji. Lines and sections come from the app's own database, line timings from the reader's per-reciter timing files, reference audio from the CDN, and the scoring service takes a bani id. Adding a bani to Kids Mode is content work: a map, section art and a story for it. Pronunciation feedback is the first thing a child gets from it; the longer aim is that practising a bani daily is something a child wants to keep doing, which is what the explorer, coins and rewards are for.

Files outside src/KidsMode

README.md
android/app/src/main/AndroidManifest.xml
ios/SundarGutka/Info.plist
package.json, yarn.lock
src/HomeScreen/components/BaniHeader.jsx, src/HomeScreen/styles.js
src/common/actions/actionTypes.js, src/common/actions/index.js, src/common/constant.js, src/common/localization.js
src/navigation/index.jsx
assets/kids/

Testing

  • yarn lint: 0 errors, 1 warning (the existing no-console in index.js).
  • yarn test: 66 suites, 984 tests pass on the merge with dev at 806bdab.
  • Service, live: clips cut from the Bhai Jarnail Singh track were sent through the shipped client and recorder hook to the scoring service. The whole pauri comes back with all 47 words like; with line 3 cut out, every word of line 3 is check and the line scores 0; half a second of silence returns unclear with no score. Requests take 1 to 3 s.
  • Android, emulator (GitHub Actions, API 34, debug APK with the JS bundle embedded, on this branch merged with dev at 806bdab): the app launches, Kids Mode and online checking are switched on in Settings, the Reciter chooser opens and closes, and a Maestro script walks explorer → worlds → map → Pauri 1 → practice (reference audio plays) → challenge → Pauri 2 story → night mode and back out, with a screenshot at each step. Fonts, icons and art all render in day and night mode. On this build the challenge screen says "Pronunciation checking is not set up in this version", since no scoring URL is committed. This ran on a copy of this branch with test IDs added for the script.
  • iOS: the Debug build succeeds on Xcode 16.4 (simulator SDK 18.5); no simulator run yet.
  • Not yet run on a physical phone, so microphone recording and playback after recording on iOS are untested.

Review round (manjotkaur27, Oct 18)

All five recorder and scoring points and the three UX points are addressed, each with a test that fails on the old code; the reply on the PR lists them one by one. Child-facing text is in src/KidsMode/scoring/messages.js; technical detail goes to logError.

Known limits

  • src/KidsMode/host/bani.js keeps its section splitting and seq ranges keyed by bani token on purpose: the data layer is per bani even though only Japji has a journey.

  • Kids Mode copy is English only, like the ported screens; the child-facing strings sit in src/KidsMode/scoring/messages.js, and localising them is a follow-up.

  • The line timings in bani.json end a little early on four of the six Pauri 1 lines, so "Listen" clips the last word; a data fix for the designer.

  • Reference audio and word clips go through the app's single track player, so opening a line in Kids Mode replaces whatever the reader had queued.

  • Playback after recording on iOS depends on the audio session being reset, which needs a device check. A phone call that never changes AppState is not detected on Android (the recorder library exposes no interruption event).

  • Scores are uncalibrated heuristics from a one-day spike; the 10% false-flag target is a target.

  • The bani loader reads the audio manifest directly, so the reader's offline downloads and its length-variant tracks for Chaupai and Rehras are not used yet; Japji is unaffected.

Licence

Code under the repo licence. Baloo Paaji 2 is under the SIL Open Font License; GurbaniAkhar was already in the repo. The artwork was made by our team for this feature.

Ports the tested Kids Mode screens (src/KidsMode/design, verbatim, types
stripped) onto this app's stack: track-player audio, vector icons, bundled
art, react-native-audio-recorder-player recording and a scoring client that
uploads nothing unless the parent opts in. Kids entry in the home header,
progress persisted in the kidsMode slice, scorer URL and key as config in
constant.js.
@avioberoi avioberoi changed the title feat: Kids Mode (Uchaaran) — guided Japji practice with pronunciation feedback feat(kids-mode): guided Japji practice with pronunciation feedback Oct 17, 2026
Register Feather and FontAwesome5 fonts on iOS so the Kids icons render;
drop the duplicate Baloo entries. Record AAC mono on both platforms. Say
plainly when no scorer is configured; retry only dropped connections.
Ignore stale audio loads and skip reloading an already loaded track.
Leave the journey through the navigator instead of the design's copy of
the home screen; keep the design's preview controls to dev builds; state
the real privacy terms. Remove unused images, styles and strings.
A new storage object on every render made the design's save effect
re-run, and each save re-rendered the screen through the store, so Kids
Mode looped on mount. Story art is re-encoded at 1600 px from the
originals instead of 1024, as the story scene pans across it at four
times the screen width.
Android's back button now steps through the journey like the header
chevron. The status bar follows Kids night mode instead of staying white.
A closing ॥ stays on the line of its last word. Switches share one
track colour.
…reciter

Sections, text, transliteration and translation come from the bundled
database and line timings from the reader's per-reciter lyrics, so any
bani with a recording can load; Japji is checked identical to the
design's data on all 374 lines. Settings gains a Reciter row; audio and
scoring follow the choice. The scoring client takes the bani id from the
loaded bani instead of a constant.
The practice label, the privacy line and the word review name the reciter
the parent chose instead of Bhai Jarnail Singh. The screen waits for the
bani before mounting the design and shows a database error with a way
back. A manifest track by a reciter the scorer has no reference for is
skipped rather than mislabelled. The Japji copy of the design's data is
gone now that the database supplies it.
@manjotkaur27

manjotkaur27 commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

The feature looks amazing, especially UI! The journey, practice flow and scoring client are well put together, and the opt-in and "nothing uploads unless the parent turns it on" handling is careful. I'm requesting changes for the recorder lifecycle and two scoring correctness issues.

Points to review

1. The microphone can stay on after Kids Mode is closed. src/KidsMode/host/recitation.js:262
After a line is scored, stop() calls beginRecording() without checking that the hook is still mounted. The back button is blocked while a line is checked, but iOS swipe-back isn't (headerShown: false leaves the gesture on). If the screen closes mid-check, the unmount cleanup stops the recorder, then the score comes back and starts a new recorder and metering listener that nothing stops. The microphone stays on in the background and the iOS session stays in play-and-record, which also sends later playback to the earpiece. Please guard with a mounted ref, or with a cancel token that cancel() and unmount both set.

2. The challenge's finish logic can run twice. src/KidsMode/screens/KidsHomeScreen.jsx:62
useRecordedRecitation, useReferenceAudio and useClipPlayer return a new object on every render, so the host memo is rebuilt every render, about 10 times a second from the level meter while recording. The KidsApp effect keyed on recitation that calls finishRecitation reruns each time, and it can fire twice before checking is set. Two stop() calls then race over the same LINE_PATH. Memoizing the three hook return values fixes both the double finish and the wasted renders.

3. A line the service doesn't cover can never be passed. src/KidsMode/host/recitation.js:223
If the manifest lists coverage for the reciter but not the current line's seq, stop() returns { retry: 'This line cannot be checked yet.' }. finishRecitation then calls recitation.retry(), which records the same line again, forever. The child can leave, but can never finish the pauri. I'd skip the line, either unscored or marked "not checked", and leave it out of the average.

4. The score can be against a different reciter than the one shown. src/KidsMode/host/recitation.js:145
useBani already settles on a reciter whose audio exists, and the UI names it. If the scoring manifest doesn't list that reciter, useRecordedRecitation silently falls back to manifest.reciters[0]. The child imitates one voice, the screen names that voice, and the score and word clips come from another. If the reciter the child heard isn't scorable, it's better to say so, or to choose the scorable reciter up front so audio and scoring match.

5. No handling for backgrounding or audio interruptions. src/KidsMode/host/recitation.js
Nothing listens to AppState or to audio-session interruptions (calls, Siri). After an interruption the recorder may have stopped while the UI still says recording, and the next stop() uploads a truncated or missing file. Please stop or cancel the recording when the app goes to the background and return the child to "ready". Relatedly, stop() should check that LINE_PATH exists and isn't empty before posting, so a failed start gives a clear message on the phone rather than a service error.

UX Fixes

  1. Prototype screens and text can be reached in the app
  • The bottom bar on the Kids settings screen has a Home button. It opens the prototype's copy of the Sundar Gutka home screen and its reader (design/KidsApp.jsx:154-158, :168, reached from :261). That screen has no back button.
  • The second "Kids Mode" switch inside the Kids settings screen (KidsApp.jsx:160) is separate from the parent's switch. Turning it off sends the child to the screen above.
    Navigation to and from kids settings screen needs to be fixed.

2. Children see raw technical errors

  • the HTTP status code and up to 120 characters of the server's reply (scoring/client.js:139-145)
  • the setting name KIDS_SCORING_URL (client.js:39)
  • the recorder's own error messages (recitation.js:193)
  • the database error on the loading screen (KidsHomeScreen.jsx:83)
  • Show kid-friendly text and log the details.

3. Night mode is incomplete

  • The pop-up panel is hard-coded white (host/layout.jsx:13, :33-34).
  • The loading and error screens are always light (KidsHomeScreen.jsx:113-117

Gurjaapk1 and others added 12 commits October 22, 2026 16:57
…in load error

Inside the app, every path to the design's own home screen (bottom bar
Home, back from settings, its Kids Mode switch) leaves the journey instead,
and that switch is hidden since the parent's lives in Settings. The sheet
and the loading and error screens follow night mode. A database failure
shows a plain message and logs the detail.
…as such

The new region environments replace the six earlier images, which nothing
references now. A line the service could not check reads "Not checked"
instead of a percentage.
Work begun before a cancel, an unmount or a move to the background stops
at its next await instead of reopening the microphone. stop() is
single-flight and the design's finish guard is a ref, so one line is
checked once. A line the service cannot check is skipped when it begins,
left out of the average and shown as not checked, instead of being
recorded forever. The score is always against the reciter the screen
names; a reciter the service lacks blocks checking with a plain message.
Backgrounding cancels the recording and sends the child back to ready;
an empty or missing file is never posted. The hooks return the same
object until a field changes. Technical detail goes to the log; the child
reads plain words.
# Conflicts:
#	src/HomeScreen/components/BaniHeader.jsx
#	src/HomeScreen/styles.js
#	src/Settings/index.js
#	src/common/actions/index.js
#	src/common/localization.js
#	src/navigation/index.jsx
The bani query lives in the database module with the other queries and
logs its failures. The header's Kids glyph is an IconAction like the
settings glyph opposite it. The error screen's way back is the app's
Button with its Go back string. A recording under a kilobyte is treated
as no recording. The review test asserts the fields it cares about
instead of a snapshot. The six region scenes are re-encoded to the size
the other art uses.
@avioberoi

Copy link
Copy Markdown
Author

Thanks, Manjot! All eight are in now, each with a test that fails on the old code, and the branch also takes in dev at 806bdab and Gurjaap's new region art.

  1. A run counter in the recorder hook is bumped by cancel, unmount and backgrounding, and every continuation after an await checks it before touching the microphone, so a swipe-back mid-check deletes the recording and starts nothing.
  2. The three hooks return memoized objects, stop() is single-flight and the design's finish has a ref guard, so one line is checked once. The level meter still changes the object about 10 times a second while recording, so the re-renders are fewer but not gone.
  3. Coverage is decided when a line begins and the hook skips to the next covered line, so a skipped line shows "Not checked", carries no score and stays out of the average.
  4. The fallback to reciters[0] is gone. If the manifest doesn't list the reciter the screen names, start() returns false with "Checking isn't available for this reciter yet."
  5. AppState is watched only while recording. Background or inactive cancels the recording and the next retry() fails once, which lands the child on "ready" with the reason. stop() checks the file exists and is over a kilobyte before posting. The start check looks at background only, not inactive, so the first-run microphone prompt can't cancel it.
  6. Every route to the prototype home now leaves the journey, and its Kids Mode switch is hidden inside the app.
  7. Child-facing text is in one file (scoring/messages.js). Status codes, server text, the setting name and the database error go to logError.
  8. The sheet, loading and error screens follow night mode.

Testing: lint and the full suite (66 suites, 984 tests) pass on the merged tree, the Android build runs the whole Kids Mode flow on an emulator in CI with screenshots, and Gurjaap is doing the iPhone pass for the recorder and playback paths.

The branch carries two merge commits and one of Gurjaap's, so a squash merge would keep dev's history clean if that's your preference. Let me know if anything else needs a change.

-Avi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants