Skip to content

d4551/Bao-Translate

Repository files navigation

Bao Translate

Bao Translate

Private, on-device live speech translation with local voice cloning.

License: Apache 2.0 Platform: Android 12+ Version: 1.0.15 Languages: 12 targets Runtime: LiteRT-LM, sherpa-onnx, ONNX Runtime Unit tests: 563 passing Strict gate: 563 of 563 Android Lint: 0 errors P2P: LibreDrop Quick Share

ELI5

Bao Translate is a tiny interpreter that runs on your Android phone. You speak, it captions and translates the words, then it speaks the translation out loud. If you enroll a short local voice profile, translated speech can be converted toward your timbre. After the model stack is downloaded, conversation audio and voice profiles stay on the device.

It also carries LibreDrop: two phones on the same Wi-Fi can send each other files directly. One phone announces itself on the network, the other finds it, they do a secret handshake, both screens show the same 4-digit number so you can check you're talking to the right phone, and then the file goes across. Nothing touches a server.

What It Does

  • Live speech translation: microphone audio flows through local VAD, STT, translation, TTS, and playback.
  • Streaming captions: English uses a sherpa-onnx Zipformer transducer; 10 other languages use lazy Vosk caption models; Arabic falls back to chunked Whisper captions.
  • On-device translation: Qwen2.5 1.5B is the required LiteRT-LM translation model; Gemma 4 E2B is an optional upgrade.
  • On-device speech: Kokoro handles en/es/fr/hi/it/pt/zh; optional Supertonic handles de/ja/ko/ru/ar; Android platform TTS is the fallback when a supplemental voice is unavailable.
  • Voice cloning: OpenVoice ONNX tone conversion is provisioned with the required stack and is applied when a local or peer voice profile is available.
  • Conversation modes: face-to-face use, continuous translation, per-speaker language selection, Bluetooth audio routing, and Nearby Connections relay between Android devices.
  • Model management: downloads are curated, status-tracked, resumable, integrity-checked, and visible in the app.
  • LibreDrop peer-to-peer transfer: a from-scratch implementation of the Quick Share wire protocol (UKEY2 handshake, D2D key derivation, SecureMessage channel, chunked payloads) with send and receive over Wi-Fi LAN. See docs/LIBREDROP.md for the protocol, security properties, and the parts that are not wired yet.

How It Works

flowchart LR
  mic["Microphone"] --> vad["Silero VAD"]
  vad --> stt["Whisper Base STT"]
  vad -. live audio .-> caption{"Caption route"}
  caption -->|en| sherpa["sherpa-onnx Zipformer"]
  caption -->|"es/fr/de/zh/ja/ko/pt/it/ru/hi"| vosk["Vosk small model"]
  caption -->|"ar or missing model"| chunked["chunked Whisper fallback"]
  stt --> translate["LiteRT-LM translation"]
  translate --> tts{"TTS router"}
  tts -->|"en/es/fr/hi/it/pt/zh"| kokoro["Kokoro"]
  tts -->|"de/ja/ko/ru/ar if installed"| supertonic["Supertonic"]
  tts -->|fallback| platform["Android platform TTS"]
  kokoro --> clone{"Voice profile?"}
  supertonic --> clone
  platform --> clone
  clone -->|yes| openvoice["OpenVoice tone conversion"]
  clone -->|"no or conversion fails"| playback["Audio playback"]
  openvoice --> playback
  playback --> speaker["Speaker or selected output"]
  playback -. Nearby relay .-> peer["Paired Android device"]
Loading

The live caption path is optimized for responsiveness while Whisper provides the final transcript used for translation. The TTS router chooses the best local speech engine for the target language, then OpenVoice can convert the synthesized audio toward the enrolled speaker timbre.

Model Provisioning

flowchart TD
  firstRun["First app run"] --> required["Required readiness gate"]
  required --> whisper["Whisper Base STT - 148 MB"]
  required --> qwen["Qwen2.5 1.5B translation - 1.5 GB"]
  required --> silero["Silero VAD - 2 MB"]
  required --> kokoro["Kokoro TTS - 142 MB app estimate"]
  required --> openvoice["OpenVoice tone conversion - 125 MB app estimate"]

  firstRun --> auto["Auto-provisioned upgrade"]
  auto --> streaming["English streaming ASR - 44 MB"]

  settings["Optional settings download"] --> gemma["Gemma 4 E2B translation - 2.5 GB"]
  settings --> supertonic["Supertonic TTS - 80 MB"]

  languageUse["First use of non-English caption language"] --> vosk["Lazy Vosk caption model - 32 to 86 MB each"]
Loading

Required models must be ready before the core translation loop is considered ready. The English streaming caption model is auto-provisioned but does not block the required readiness gate. Vosk caption models are downloaded only when a language needs them.

Conversation Relay

sequenceDiagram
  participant A as Device A
  participant B as Device B
  A->>A: Capture, caption, transcribe
  A->>A: Translate into listener language
  A->>B: Send Nearby payload and speaker metadata
  B->>B: Synthesize translated speech
  B->>B: Apply peer timbre when available
  B->>B: Play through selected output route
Loading

Nearby Connections is used for the two-device mesh. Each side can keep its own source and target language, and peer timbre metadata lets the receiving device speak the translation in the other speaker's voice profile when available.

LibreDrop File Transfer

LibreDrop speaks the Quick Share wire protocol directly — the transfer itself does not go through Google Play Services. Receiving runs in a connectedDevice foreground service so the device stays reachable while the app is backgrounded; it is opt-in and off by default.

sequenceDiagram
  participant S as Sender
  participant R as Receiver
  Note over R: "Receive files" ON → foreground service → TCP listener + mDNS advertise
  S->>R: mDNS resolve, then TCP connect
  S->>R: ConnectionRequest
  S->>R: UKEY2 ClientInit
  R->>S: UKEY2 ServerInit
  S->>R: UKEY2 ClientFinished
  Note over S,R: D2D keys derived (HKDF). Both compute the same 4-digit PIN.
  S->>R: IntroductionFrame (names, sizes, payload ids)
  Note over R: Consent notification — user taps Accept or Reject
  R->>S: ConnectionResponseFrame{ACCEPT}
  loop per 512 KiB chunk
    S->>R: PayloadTransferFrame{DATA}
  end
  S->>R: LAST_CHUNK, then Disconnection
Loading
Capability State
Receive over Wi-Fi LAN Working
Send over Wi-Fi LAN Working
Wi-Fi Direct / hotspot / Bluetooth upgrade Medium providers exist; the sender does not negotiate a bandwidth upgrade
Consent surface In-app modal (LibreDropConsentActivity) with heads-up notification actions as fallback; both drive the same decision sink
QR pairing, AI file descriptions, voice share Code present, no UI entry point

Peer-supplied filenames are sanitized before they reach MediaStore or a File — separators, control characters, NUL, leading dots and .. segments are all neutralised. Full detail, including known gaps, is in docs/LIBREDROP.md.

Privacy And Trust Boundaries

Everything after provisioning runs locally: speech, transcript, translation, synthesis and voice conversion never leave the device, and LibreDrop transfers go peer-to-peer without a relay server. The boundaries that data does cross are handled explicitly.

flowchart LR
  subgraph onDevice["Stays on device"]
    audio["Microphone audio, transcripts, voice profiles"]
    store["App storage - models, skills, downloads"]
  end
  logcat["logcat"]
  peer["Paired or nearby device"]
  web["Skill WebView content"]
  audio -->|"release builds drop v and d"| logcat
  audio -->|"Nearby relay, conversation modes only"| peer
  peer -->|"LibreDrop - consent required, filenames sanitized"| store
  web -->|"origin checked, storage quotas enforced"| store
Loading
Boundary Control Proven by
Diagnostic logging BaoLog.shouldEmit drops v and d in release builds — those are the levels where call sites interpolated typed input and model responses. i/w/e survive for field bug reports and are expected to carry shapes, not content. Release does not minify, so nothing else would have stripped them. BaoLogTest
Inbound files Every peer-supplied name goes through FilenameSanitizer before MediaStore or a File; a transfer only starts after an explicit Accept in LibreDropConsentActivity or its notification fallback. Receiving is opt-in and off on a fresh install. FilenameSanitizerTest, ConsentIntentsTest, LibreDropConsentActivityTest, ReceiveModePreferencesTest
Skill WebView isTrustedLocalWebViewUrl compares full origin (scheme, host, port) rather than prefix, so userinfo spoofing and look-alike hosts are rejected; the WebView runs JavaScript, so this is the gate. WebViewTrustAndFileNameTest
Skill storage bridge SkillStorageBridge caps what page JavaScript can persist before it reaches SharedPreferences: keys ≤ 256 chars, values ≤ 64 KiB, 512 entries total, with a rejected write returning false rather than throwing. SkillStorageBridgeTest
Imported model names ensureValidFileName replaces path separators and everything outside [A-Za-z0-9._-]. Its one documented gap — leading dots are not stripped — is pinned by a test rather than left implicit. WebViewTrustAndFileNameTest

Supported Languages

English, Spanish, French, German, Chinese, Japanese, Korean, Portuguese, Italian, Russian, Arabic, and Hindi.

Capability Coverage
Translation targets All 12 selectable languages
Source language Manual selection or Auto detect
Live captions English via sherpa-onnx Zipformer; es/fr/de/zh/ja/ko/pt/it/ru/hi via Vosk; Arabic via chunked Whisper fallback
Local speech Kokoro for en/es/fr/hi/it/pt/zh; Supertonic for de/ja/ko/ru/ar when installed; Android platform TTS fallback
Voice conversion OpenVoice tone conversion for enrolled local and peer voice profiles

Model Stack

Models are downloaded in-app from curated sources on first use or from settings.

Role Model Provisioning
Voice activity detection Silero VAD Required
Speech-to-text Whisper Base through sherpa-onnx Required
Streaming captions, English sherpa-onnx Zipformer transducer Auto-provisioned
Streaming captions, 10 languages Vosk small models Lazy per language
Translation Qwen2.5 1.5B through LiteRT-LM Required
Translation upgrade Gemma 4 E2B through LiteRT-LM Optional
Text-to-speech Kokoro Multi-Lang Required
Supplemental TTS Supertonic TTS through sherpa-onnx Optional
Fallback TTS Android TextToSpeech engine Device-provided
Voice conversion OpenVoice tone converter and reference encoder through ONNX Runtime Required

Getting Started

Requirements:

  • Android 12 / API 31 or newer.
  • Enough local storage for the selected model stack.
  • Network access for initial model downloads.
  • Optional Bluetooth headset or second Android device for advanced conversation testing.

Steps:

  1. Install an APK you have access to, or build from source.
  2. Launch the app and download the required models.
  3. Optional: enroll your voice in settings.
  4. Choose source and target languages, pick audio devices if needed, and start translating.

Source builds that need gated in-app model downloads also need the Hugging Face OAuth Gradle properties or environment variables described in DEVELOPMENT.md.

Build From Source

The Android Gradle project is rooted at Android/src.

cd Android/src

./gradlew :app:assembleDebug
./gradlew :app:testDebugUnitTest
./gradlew :app:connectedDebugAndroidTest
./gradlew :app:smokeE2e

adb install -r app/build/outputs/apk/debug/app-debug.apk

Build notes:

  • applicationId = com.google.ai.edge.gallery.
  • versionName = 1.0.15; versionCode = 33.
  • compileSdk = 37; minSdk = 31; targetSdk = 37.
  • The wrapper uses Gradle 9.6.1 with AGP 9.4.0-alpha02 and Kotlin 2.4.20-Beta1.
  • Gradle toolchains use JDK 26 for compilation and emit Java 17 bytecode.
  • The vendored sherpa-onnx AAR lives under Android/src/app/libs/.
  • Verification task definitions are applied from Android/src/gradle/verification.gradle.kts; they are kept outside the module directory because Android Lint's build-script pass crashes when it tries to resolve an applied .gradle.kts that has no compilation classpath of its own.
  • kotlinx-coroutines-test is a test-only dependency pinned to the same version as kotlinx-coroutines-core; the two must match or the internal TestCoroutineScheduler wiring breaks at runtime.
  • sherpa-onnx and onnxruntime-android both ship libonnxruntime.so; packaging keeps one shared object with jniLibs.pickFirsts.

See DEVELOPMENT.md for local setup, Hugging Face OAuth configuration, and verification notes.

Repository Map

flowchart TB
  root["Repository root"] --> android["Android/src - Android Gradle app"]
  root --> brand["bao-translate - brand and store assets"]
  root --> docs["docs/adr - architecture decisions"]
  root --> mcp["mcp - Model Context Protocol guide"]
  root --> skills["skills - bundled and featured agent skills"]
  root --> allowlists["model_allowlists - curated model lists"]
  android --> feature["Bao Translate custom task"]
  android --> gallery["AI Edge Gallery foundation"]
  feature --> audio["audio routing and playback"]
  feature --> stt["VAD, STT, streaming captions"]
  feature --> translate["LiteRT-LM translation"]
  feature --> tts["Kokoro, Supertonic, platform TTS, OpenVoice"]
  feature --> nearby["Nearby conversation mesh"]
  android --> libredrop["LibreDrop custom task"]
  libredrop --> ldproto["protocol - UKEY2, SecureMessage, payloads"]
  libredrop --> lddisc["discovery - mDNS, BLE, medium providers"]
  libredrop --> ldsvc["service - receiver FGS, consent, downloads"]
Loading
Path Purpose
Android/ Android application docs and Gradle project
Android/src App source, Gradle wrapper, tests, and resources
bao-translate/ Brand assets and app icon resources
docs/ Architecture decisions and supporting documentation
docs/LIBREDROP.md LibreDrop protocol, security properties, coverage, and known gaps
mcp/ Model Context Protocol integration guide
skills/ Agent skill documentation and examples
model_allowlists/ and model_allowlist.json Curated model allowlists
PLAN.md Engineering plan: verified work, remaining follow-ups, reproduction commands
Function_Calling_Guide.md Guide for adding custom mobile actions
Bug_Reporting_Guide.md Android bug report capture guide

Documentation

Quality And Verification

Current state of the JVM unit suite:

Metric Value
Unit tests 563 passing, 0 failing, 0 skipped, 46 classes
Strict gate (@Category(Strict)) 563 of 563 — every class carries the marker, so the whole suite is gating
LibreDrop tests 154
Instrumentation tests 105 @Test methods, device or emulator required
Android Lint (debug) 0 errors, 26 warnings

The single gate that runs everything:

cd Android/src
./gradlew :app:verifyReleaseReady
flowchart LR
  gate["verifyReleaseReady"] --> compile["compileDebugKotlin"]
  gate --> compileAt["compileDebugAndroidTestKotlin"]
  gate --> unit["testDebugUnitTest - 563"]
  gate --> strict["testDebugUnitTestStrict - 563"]
  gate --> lint["lintDebug"]
  gate --> apk["assembleDebug"]
  gate --> apkAt["assembleDebugAndroidTest"]
  device["Device gates - not in verifyReleaseReady"] --> smoke["smokeE2e"]
  device --> connected["connectedDebugAndroidTest - 105"]
Loading

The strict task reuses the exact compiled classes and runtime classpath of testDebugUnitTest and filters them through JUnit 4 categories, so it runs the same bytecode rather than a parallel build. The task definitions live in gradle/verification.gradle.kts, outside the module directory for the Lint reason noted in the build notes above.

Device gates, which need hardware or an emulator:

./gradlew :app:smokeE2e
./gradlew :app:connectedDebugAndroidTest

Known false-green surfaces in the instrumentation suite

The 105 @Test methods under androidTest are not as strict as the JVM suite. An audit of the real source found five places where a green run does not mean the behaviour was proven:

Test Why a pass can be empty
BaoTranslateLiveMicTranslationE2eTest.twoDevice_sendsTranscriptToLivePeerOverNearby assumeTrue(peer != null) — with one device attached the "two-device proof" reports success having proven nothing.
BaoTranslateLiveMicTranslationE2eTest.receivedPeerMessageUsesPeerTimbreWhenEmbeddingPresent Routed through ensureOpenVoiceReady, which assumeTrues that the OpenVoice ONNX files are provisioned. On a device without them the timbre assertions never execute.
BaoTranslateLiveMicTranslationE2eTest.receivedPeerMessageWithoutEmbeddingDoesNotUseLocalVoice Same ensureOpenVoiceReady skip.
BaoTranslateBluetoothAudioRoutingTest.audioRouter_cleanup_idempotent No assertion — passes if cleanup() became a no-op.
BaoTranslateDeviceAudioRouteTest.audioRouter_play_thenReset_isSafe No assertion — same.

Read a green connectedDebugAndroidTest accordingly: the two-device Nearby path is only actually exercised when a second device is present and in Conversation Mode, and the voice-conversion path only when the OpenVoice models are already on the device.

The JVM suite is stricter but not perfect. None of its 563 methods are @Ignored and skipped=0 in every run; its three assume* calls all sit in ModelIntegrityTest (two symlink-capability guards and one POSIX-only guard) and none of them fired on this machine. Five methods are crash-only contracts that assert nothing and fail only if the code under test throws — VadProcessorTest.reset_idempotent, cleanup_calledTwice_safe, concurrentCalls_serializedAndSafe (deliberately, since thread interleaving is not deterministic), VoiceProfileManagerTest.deleteProfile_idempotent_forMissingProfile, and StrictHarnessTest.whitespaceTorture_doesNotPanic_onAnyEntry. Everything else asserts, including the kotest forAll property tests, which fail on falsification.

Three hardening rules the suite enforces on itself, not just on production code:

  • CrossCuttingHardeningTest.runBlockingWithTimeout_isRequired statically scans the test tree and fails if any runBlocking is not paired with a withTimeout, so a hung coroutine cannot masquerade as a slow test.
  • smokeE2e parses the instrumentation output rather than trusting the exit code, because adb shell am instrument returns 0 even when tests fail or the process crashes.
  • DeadSurfaceGuardTest re-derives the set of production types with no caller in any source set and fails on anything newly unreferenced. The compiler happily builds code nobody calls and Lint's unused-symbol checks do not span source sets, which is how 2,941 lines of shipped-but-unwired Kotlin accumulated unnoticed. Its 17 known-dead entries are listed with a reason each — mostly the non-Wi-Fi-LAN bootstrap clients the sender never negotiates, plus the QR-pairing and "AI assistance" surfaces with no UI entry point. The list is a ratchet: shrinking it is progress, growing it has to be deliberate.

Thinnest coverage

Measured Kotlin LOC per package, main against its own unit tests:

Package main unit test
customtasks/libredrop 45,105 2,611
ui/common 15,089 363
customtasks/baotranslate 13,774 3,792
customtasks/agentchat 8,233 0
ui/modelmanager 3,255 0

The largest remaining gaps are not in LibreDrop — that went from the worst-covered area to among the best. customtasks/agentchat is the biggest package with no unit tests at all, and ui/common has only the first two (WebViewTrustAndFileNameTest for WebView origin trust and imported-model file naming, SkillStorageBridgeTest for the JavaScript storage bridge's quotas) against 15k lines. Those two packages are where the next coverage work belongs.

What is not covered

Unit tests do not reach the LibreDrop foreground-service lifecycle (ReceiverForegroundService), BLE discovery, the Wi-Fi Direct / hotspot medium providers, or the notification builders. Those need instrumentation on a device, or a service refactor small enough to sit behind a Robolectric harness.

Robolectric itself is available and now carries four classes — ReceiveModePreferencesTest against a real SharedPreferences, LibreDropConsentActivityTest against the consent trampoline's defensive paths, WebViewTrustAndFileNameTest against Android's own Uri parser, and SkillStorageBridgeTest against the storage bridge — with two toolchain caveats recorded in the build: its bundled ASM cannot read Java 26 class files, so tasks.withType<Test> pins a JDK 21 launcher; and it ships emulated frameworks only up to SDK 36, so Robolectric tests carry @Config(sdk = [36]). The pin is 4.16.1 (latest stable); 4.17-beta-2 accepts SDK 37 but fails on both JDK 21 and 26, so neither workaround can be dropped yet.

Use a physical device for full translation, microphone capture, Bluetooth audio routing, voice cloning, and Nearby conversation validation. Emulator coverage is useful for compile, unit, and focused UI checks, but it does not replace hardware verification for the audio pipeline. A LibreDrop transfer between two real devices likewise cannot be proven by the loopback test alone, which exercises the protocol but not the radio.

Built On

Bao Translate builds on these open-source projects:

Support And Contributing

License

Bao Translate is licensed under the Apache License, Version 2.0. See LICENSE. This project is derived from Google AI Edge Gallery; upstream copyright notices and Apache-2.0 licensing are retained.

Releases

Packages

Used by

Contributors

Languages