← All projects MUDRA+
iQOO City Battles, Chennai · on-device

MUDRA+

Sign two or three glosses, pick a candidate sentence, and the phone speaks it into a call. Hand geometry for recognition, a small local LLM only for unseen sequences — no server, no API key, no account.

CAPTURERECOGNISECOMPOSECONFIRM & SPEAKRECEIVE SIDEVisionCamera — frame processor on the camera threadVisionCameraframe processor on thecamera threadMediaPipe plugin — native Kotlin · hand_landmarker.task → 21 × {x,y,z}MediaPipe pluginnative Kotlin ·hand_landmarker.task → 21 ×{x,y,z}assessCapture — hand present · close enough · steady (rolling jitter)assessCapturehand present · close enough· steady (rolling jitter)gestureRecognizer — wrist-normalise → 72 features → RMS distance → nearest | UNKNOWN (0.42)gestureRecognizerwrist-normalise → 72features → RMS distance →nearest | UNKNOWN (0.42)Template store — bundled + user-recorded, atomic JSON writesTemplate storebundled + user-recorded,atomic JSON writesSentence memory — seen gloss sequence → remembered picks (~1 µs)Sentence memoryseen gloss sequence →remembered picks (~1 µs)llama.rn · Qwen2.5-3B — Q4_K_M GGUF, only for unseen sequencesllama.rn · Qwen2.5-3BQ4_K_M GGUF, only for unseensequencesYou pick one — pick · edit · discard — nothing is spoken before thisYou pick onepick · edit · discard —nothing is spoken beforethisSystem TTS — speakerphone into the call (acoustic coupling)System TTSspeakerphone into the call(acoustic coupling)LAN relay — mDNS “aslrelay” + TCP :12345 to a second phoneLAN relaymDNS “aslrelay” + TCP :12345to a second phoneMic PCM stream — pauses while the phone is speakingMic PCM streampauses while the phone isspeakingwhisper.rn · tiny — ggml-tiny, on-device transcriptionwhisper.rn · tinyggml-tiny, on-devicetranscriptionTranscript — other party’s speech on screenTranscriptother party’s speech onscreenframes21 ptsacceptedtemplatesseennew2–3 candidatesremember pick
Device / nativeCore logicGate / helperStorageModel
Everything in the live path runs on the phone. Hover a box to trace it; the dashed loop is the memory write-back after you confirm a sentence.
Design rationale

Fewer signs per sentence

Recognising an n-sign sentence succeeds with probability ≈ pⁿ. The architecture attacks both terms: shorten the signed key (autocomplete does the rest) and keep p high with a small, well-separated vocabulary plus a rejection threshold that answers UNKNOWN instead of guessing.

8 signsfull sentence
0.66 · 0.43
5 signs
0.77 · 0.59
3 signsgloss key + autocomplete
0.86 · 0.73

Survival probability at per-sign accuracy p = 0.95 · p = 0.90. A model of the failure mode, not a measurement.

Why no trained classifier in the live path

A classifier’s output layer is fixed to its training vocabulary. Adding one sign means data collection, retraining, re-export and shipping a new model. Template geometry lets a user record a new sign in about a second and use it on the next frame.

System architecture

On-device, end to end

See the diagram above. Walk through the signing flow:

Hold-to-confirm

A guide circle fills over the hold duration; a cooldown forces the hand to visibly re-form before the same gloss can fire again. On the call screen a detection zone restricts matching to hands held at sign height.

assessCapture → ok match → REACH (rms 0.14) ring: ████████░░ 80%

Glosses accumulate

Consecutive repeats of the same held sign are de-duplicated. A short key like ME NEED METFORMIN is enough to retrieve or generate a whole sentence.

["ME", "NEED", "METFORMIN"]

Seen? Remember. New? Compose.

An exact repeat returns from memory; a sequence whose candidates you chose before returns from sentence memory. Only unseen sequences wake the 3B model — which also keeps the phone cool under camera load.

sentenceMemory.get() → miss llm.complete() 1. I need one strip of Metformin, please. 2. I need Metformin. 3. Do you have Metformin?

The confirmation gate

Prompts ask for multiple candidates because a gloss sequence is genuinely ambiguous. Output is a candidate, never an action — also the guard against a hallucinated sentence.

▶ 1. I need one strip of Metformin, please. 2. I need Metformin. [ pick ] [ edit ] [ discard ]

Speakerphone or LAN relay

Android has no public API to inject audio into a cellular call, so output is acoustic coupling or a LAN relay to a second phone that speaks and streams a transcript back.

route: tts → speakerphone or relay → peer :12345 mic paused while speaking
Recognition

Geometry, not inference

The only vision model is MediaPipe Hand Landmarker. The sign matcher itself is plain TypeScript — not a model.

21 landmarks per hand. Wrist (red) is the origin.

① Normalise

Translate to the wrist, scale by hand size → translation- and scale-invariant.

② 72-d feature vector

Same transform the snapshot trainer used, so bundled and user-recorded templates compare directly.

③ RMS distance

Rank templates; above 0.42 → UNKNOWN.

Capture-quality gate

CheckRejection
Landmarks validno-hand
handSpreadtoo-far
maxJitterunstable

Steadiness is judged across the whole hold window, not just the final frame.

REACH
0.14
HOME
0.51
GO
0.63
WHERE
0.58
Custom: METFORMIN
0.47

Red line = UNKNOWN threshold 0.42. Here the best template (REACH, 0.14) clears it → gloss emitted. If every bar sat right of the line the answer would be UNKNOWN. Distances are illustrative.

Autocomplete

Two stores, different jobs

Sentence memory (shipped) records which candidate you picked per exact gloss sequence; a different pick later adds an option. The standalone translation-memory engine handles corpora bigger than a JSON file.

QUERYEXACT PATH · O(1)FUZZY PATH · 2 STAGESRESULTGloss sequence — ["ME","NEED","METFORMIM"]Gloss sequence["ME","NEED","METFORMIM"]FNV-1a hash table — open addressing, verify tokens on collision · ~1 µs, flat 1k→50kFNV-1a hash tableopen addressing, verify tokenson collision · ~1 µs, flat1k→50kInverted index walk — IDF-weighted overlap; skip tokens in >60% of memories; single-edit repairInverted index walkIDF-weighted overlap; skiptokens in >60% of memories;single-edit repairTop 48 → rescoring — 0.45·Jaccard + 0.25·coverage + 0.30·LCS (order-aware)Top 48 → rescoring0.45·Jaccard + 0.25·coverage +0.30·LCS (order-aware)Candidates — ties: use count → recency → idCandidatesties: use count → recency → idexact?misshit
LookupFuzzy retrievalI/O
Implemented twice — Kotlin (Room/SQLite, ADPF hints) and TypeScript (zero runtime deps). Measured on a CI container, not the phone.

Fuzzy score composition

452530
weighted Jaccard 0.45query coverage 0.25LCS ratio 0.30
0
exact lookup, flat 1k→50k
0
tests across both ports
0
candidates rescored
0
token frequency skipped
Failure behaviour: ids are assigned in RAM so a confirmed phrase is queryable before SQLite has heard of it (write-behind). A corrupt or full DB flips to memory-only and notifies the app; warm-up skips corrupt rows; malformed recogniser output is a returned value, never an exception; capacity is bounded with least-used eviction and pinned entries are never evicted.
Audio & models

Android gives no way to inject call audio

RouteHowTrade-off
Acoustic couplingSpeakerphone; the call’s open mic picks up TTSWorks on any carrier call; lower quality, echo cancellation can suppress it
LAN relayzeroconf aslrelay + TCP :12345; sender transmits text, receiver speaks and streams transcript backClean audio, no cloud telephony; both phones on one network (hotspot as fallback)

MediaPipe Hand Landmarker

Committed in the repo. The only vision model.

Qwen2.5-3B-Instruct Q4_K_M

~2 GB GGUF, sideloaded (debug) or bundled (release). 1.5B / 3B / 7B suggested per RAM tier via the llm-testbed harness.

Whisper tiny (ggml)

~75 MB, committed. Inbound transcription via whisper.rn.

Scope, honestly

Autocomplete, not translation

MUDRA+ is an autocomplete for a signer’s own recurring phrases. Interpretation is a human skill and this does not attempt it — the person confirms every sentence. The demo vocabulary is a handful of bundled single-hand static poses (GO, HOME, I, LEFT, PARK, REACH, RIGHT, WHERE) plus whatever the user records; two-handed and motion signs are not implemented. The next step is evaluating ergonomics with signers.
React Native 0.76KotlinTypeScriptVisionCameraMediaPipellama.rnwhisper.rnRoom / SQLiteJestGradle
← PreviousPROBE Next →PathShala