Demo · iQOO City Battles, Chennai

Fewer signs per sentence.

MUDRA+ is a prototype on-device ASL input accelerator: a small, high-accuracy sign vocabulary plus autocomplete, so a short spoken sentence costs a few signs instead of a whole sentence of them. Hand tracking, sentence composition, transcription and memory all run on the phone — no server, no API key, no account.

2–3 signs→memory · ~1 µsorlocal 3B LLM→ candidate sentences→you pick one→spoken
Runs offline iQOO 15 · Snapdragon 8 Elite Gen 5 MediaPipe landmarks, no trained classifier Completed at the hackathon

What this is, and what it isn't

The scope is deliberately narrow, and worth stating before any of the engineering.

Autocomplete, not translation

The system has no opinion about grammar it hasn't been shown. It proposes candidate sentences for a signer's own recurring phrases; the person chooses one, every time, before anything is spoken.

Interpretation is a human skill

This doesn't attempt it. The design assumes a person driving a tool for their own words, with a confirmation step they control — not a machine speaking on anyone's behalf.

A small vocabulary, on purpose

Five bundled single-hand static poses, plus whatever the user records. The interesting question isn't vocabulary size — it's how far a reliable vocabulary gets you when autocomplete carries the rest.

A prototype, measured as one

Every figure on this page is an engineering benchmark. The ergonomics — hold duration, the capture gate, whether the candidate sentences are worth choosing between — are the next thing to evaluate, with signers.

Why autocomplete, and why a small model

Signing a full English sentence is expensive twice over: physically, and statistically.

If per-sign recognition accuracy is p, the chance an n-sign sentence survives end to end is roughly pn. The exponent is the problem:

Signs neededp = 0.95p = 0.90
8 — full sentence0.660.43
50.770.59
3 — gloss key + autocomplete0.860.73

A model of the failure mode, not a measurement — but it is why the architecture is shaped this way.

LEVER 1

Reduce n

Don't sign the sentence — sign the key. ME NEED METFORMIN is enough to retrieve or generate the full phrasing, so the recognised sequence stays short.

LEVER 2

Raise p

A small set of distinct, well-separated poses is far easier to get right than a large one full of near-collisions — and a rejection threshold means the system answers UNKNOWN rather than guessing.

Why there's no trained classifier in the live path

A classifier's output layer is fixed to the vocabulary it was trained on. Adding one sign means collecting data, retraining, re-exporting and shipping a new model — incompatible with the thing this demo most wants to test: a user adding their own sign in a second and using it on the next frame. So detection for the demo is geometric:

21 MediaPipe landmarks
   → normalise around the wrist        (translation- and scale-invariant)
   → 72-dimension feature vector
   → RMS Euclidean distance to every template
   → nearest match, or UNKNOWN above τ = 0.42

sign-embeddings/ implements the fallback — reuse an internal layer of a trained TFLite model as an embedding extractor, match by cosine similarity — for when template matching runs out of headroom. Built and tested, not wired in, because the demo doesn't need it.

Why the LLM is small and rarely called

Glosses aren't English: no articles, no tense, no register. That's a language task, so a language model does it rather than the vision path. But the LLM is the slowest and hottest component, so the architecture minimises how often it runs — an exact sequence already confirmed returns from memory in about a microsecond, a sequence whose candidates the user has chosen from before returns from sentence memory, and only a genuinely new sequence reaches the 3B. That ordering is also what keeps the demo interactive under sustained camera load.

The pipeline

Every box runs on the phone. The only network traffic possible is a LAN socket to a second phone you pair yourself.

Camera react-native-vision-camera MediaPipe Hand Landmarker 21 × {x,y,z} per frame · GPU Capture gate present? close? steady? Template matcher 72 features · RMS · τ = 0.42 Gloss sequence ME NEED METFORMIN Memory exact hit · O(1) · ~1 µs no model runs Local LLM · llama.rn Qwen2.5-3B-Instruct Q4_K_M 2–3 candidate sentences You pick one pick · edit · discard nothing is spoken before this hit new TTS → speaker or LAN relay to a peer phone whisper.rn · ggml-tiny their speech → transcript mic pauses while speaking Remembered gloss sequence → sentence next time: no model
A repeated phrase takes the teal route and never reaches the LLM.

Recognition details

Getting audio into a call

Android exposes no public API for injecting audio into a cellular call, which constrains the whole output stage. Two routes are implemented:

RouteHowTrade-off
Acoustic couplingSpeakerphone; the call's own open mic picks up the TTS outputWorks on any carrier call with no infrastructure; lower quality, and handset echo cancellation can suppress it
LAN relayzeroconf/mDNS discovery + TCP socket; sender transmits text, receiver speaks it and streams a transcript backClean audio, no cloud telephony; both phones must share a network

The autocomplete stores

Two stores, different jobs — one keyed on the gloss sequence, one on what the user actually picked.

Sentence memory — what the app ships

Records which candidate the user picked per exact gloss sequence and resurfaces it ahead of fresh generation, as app-private JSON with atomic writes. The Memory screen reviews and forgets entries. A sequence can legitimately mean different things on different occasions, so picking a different sentence later adds an option rather than replacing the earlier one — and once a sequence has enough remembered, the LLM isn't called for it again.

Translation memory — the engine behind it

A standalone retrieval engine implemented twice, Kotlin (Room/SQLite) and TypeScript, for corpora larger than a JSON file should hold. Dependency-free and independently tested.

~1 µsexact lookup, Kotlin
~2.1 µsexact lookup, TypeScript
flat1 k → 50 k memories
46tests across both ports

The practical effect: the slow path is a one-time cost per phrase. Repeat use of the same phrase is a hash lookup, which is also what keeps the phone cool under sustained camera load.

Models

One vision model, one language model, one speech model — and the component doing sign matching isn't a model at all.

ModelArtifactRuntimeRole
MediaPipe Hand Landmarker hand_landmarker.task MediaPipe Tasks, native, GPU-delegated 21 3-D landmarks per frame — the only vision model in the live path
Qwen2.5-3B-Instruct qwen2.5-3b-instruct-q4_k_m.gguf · ~2.0 GB llama.rn (llama.cpp), CPU/GPU Gloss sequence → candidate sentences; contextual replies
Whisper tiny ggml-tiny.bin · ~75 MB whisper.rn (whisper.cpp) On-device transcription of the other party
Android TTS system voices react-native-tts Speaks the confirmed sentence
Gesture templates custom_gestures.json + user file pure TypeScript — no model 21 landmarks + 72 features per sign; 5 bundled, user-extensible at runtime
Embedding extractor not wired in sign_embed.tflite TFLite Cosine-similarity custom-sign path in sign-embeddings/

No cloud inference: there is no OpenAI, Anthropic or Gemini key in the live app. GGUF choice per RAM tier (1.5B at 6 GB, 3B at 8 GB, 7B at 12 GB+) is documented in llm-testbed/, the harness used to benchmark candidates on the actual device.

On device — Snapdragon 8 Elite Gen 5

Model size

Q4_K_M at ~2 GB fits comfortably in the iQOO 15's RAM, so the demo uses a 3B rather than the 1.5B originally planned, offloaded with n_gpu_layers: 99.

ADPF hints

The memory layer opens a PerformanceHintManager session per lookup thread, declaring a ~2 ms target. On a big.LITTLE part a microsecond-scale burst otherwise finishes on an efficiency core before a reactive governor reacts.

Thread placement

The write-behind thread runs at THREAD_PRIORITY_BACKGROUND so SQLite never contends with the camera pipeline for prime cores. Room opens WAL with synchronous = NORMAL on its own executor.

Thermals set the budget

Continuous camera plus continuous LLM is what heats the phone, so the design optimises for fewer LLM invocations rather than faster ones — which is what both memory stores are for.

Stated plainly, because it is easy to overclaim: inference is configured with n_gpu_layers: 99 and the Android build ships llama.cpp's Hexagon HTP libraries, but whether the HTP backend actually engages depends on the llama.rn build and the device — so treat NPU offload as available-but-unverified, not a measured win. OriginOS performance-mode toggles aren't app-accessible APIs either; ADPF is the supported route to the same scheduler.

Status and limitations

Completed at the end of the hackathon. Demo-quality, and specific about which parts are which.

AreaStateDetail
Hand tracking, template matching, runtime sign captureworkingReal camera, native MediaPipe plugin, on-device matching
Candidate generation (Qwen2.5-3B)workingBundled for the release build
Translation memory, both portsworking46 tests; flat-scaling lookups measured
Sentence memoryworkingRemembers which candidate was picked, per sequence
LAN relay, transcription, TTSworkingReal connection state; mic pauses while speaking
Acoustic coupling into a carrier callcaveatSubject to handset echo cancellation
Vocabularyby design5 bundled static single-hand poses + user recordings
Two-handed / motion signsnot implementedNext on the list
Embedding-based custom signsbuilt, unusedModule tested; template matching suffices today
Cloud telephonynon-goalDeliberately not built

Known limitations

Next, in order

  1. Ergonomic evaluation of the hold-and-confirm loop with signers.
  2. Motion and two-handed signs.
  3. Measured per-sign accuracy on the bundled set.
  4. Wiring sign-embeddings if template matching saturates.

Run it yourself

A physical Android device is required — the camera path can't be demoed on an emulator.

NEED

Toolchain

Node 18+, JDK 17, Android SDK 35, NDK 26.1.10909125, a device on Android 7.0+ with USB debugging.

SHIPPED

Two of three models

The hand landmarker and Whisper tiny are committed to the repo. Only the ~2 GB .gguf has to be fetched.

GRANT

Camera + mic

Signing needs the camera, the receive side needs the mic. Nothing is recorded or uploaded either way.

git clone https://github.com/xreedev/mudra.git
cd mudra/app
npm install

# one-time: the LLM weights (Qwen2.5-3B-Instruct Q4_K_M, from Hugging Face)
adb shell mkdir -p /sdcard/Android/data/com.mudraapp/files/models
adb push qwen2.5-3b-instruct-q4_k_m.gguf /sdcard/Android/data/com.mudraapp/files/models/

npm start          # Metro
npm run android    # build + install

For a release APK the model is bundled instead of pushed — drop it into android/app/src/main/assets/models/ and the app extracts it on first launch. On Windows, scripts/build-release-with-llm.ps1 automates that, plus the MAX_PATH and asset-compression workarounds a multi-GB asset needs.

Then try the loop

  1. Add custom sign — hold a sign inside the guide circle until the ring completes, name it, save. Usable on the next frame.
  2. Call — hold signs to build a gloss sequence, pick one of the proposed sentences, it's spoken.
  3. Memory — the sentence you picked comes back next time those signs do.

Two-phone relay, optional

Both phones on the same Wi-Fi: one opens Call, the other Receive. Discovery is zeroconf/mDNS (aslrelay), the session runs over TCP on port 12345. Guest networks with client isolation block it — a phone hotspot is the reliable fallback. Without a relay it still works on one phone, over speakerphone.

The standalone modules

Each builds and tests on a laptop — no emulator, no device, no network:

cd memory-layer     && ./gradlew :core:test   # Kotlin engine, no Android SDK needed
cd memory-layer-rn  && npm install && npm test
cd sign-embeddings  && npm install && npm test
PathContents
app/The React Native app — camera, recognition, LLM, speech, relay, five screens
app/src/recognition/Matcher, capture gate, duplicate detection, user gesture store, live-hand hooks
app/src/llm/Provider interface, llama.rn provider, prompts, sentence memory
app/src/speech/TTS, voice recorder, Whisper transcriber
app/src/relay/zeroconf discovery, TCP sender/receiver
app/android/.../recognition/Native MediaPipe frame-processor plugin
memory-layer/, memory-layer-rn/The translation-memory engine, Kotlin and TypeScript
sign-embeddings/TFLite-embedding custom-sign path (built, not wired in)
llm-testbed/Standalone app for benchmarking GGUFs on the device

Full setup, troubleshooting and the technical detail behind every claim on this page is in the README.