Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

6.4 KiB

Android live voice pipeline: audioFrame → speaker-attributed turns

This document maps the end-to-end on-device path that turns the Android audioFrame PCM stream (see AUDIO_FRAMES.md) into live, VAD-segmented, speaker-attributed voice turns.

There are two transports, and they target different builds. For the normal Android APK (ai.elizaos.app) the canonical path is the in-process JNI/bionic host — the four fused voice classifiers run inside the Capacitor app process via libelizainference.so, with no separate agent process and no HTTP hop. The legacy musl bun-agent transport remains only for the privileged AOSP build (where the embedded bun agent runs platform-signed); it is not the path the normal APK uses.

The canonical pipeline (normal APK: in-process JNI/bionic host)

 Android native AudioRecord (plugin-native-talkmode, Kotlin)
   │  emits `audioFrame` Capacitor event: base64 LE-s16 16 kHz mono PCM,
   │  20 ms/frame, { sampleRate, channels, samples, rms, timestamp, frameIndex }
   ▼
 Capacitor WebView (JS renderer)
   │  TalkMode.addListener("audioFrame", …)  →  JniVoicePipeline
   │    (packages/ui/src/voice/jni-voice-pipeline.ts)
   │  batches ~1 s of frames → ElizaVoice.pipelineProcess({ handle, pcm16 })
   ▼
 ElizaVoice JNI host  (ai.elizaos.app process, BIONIC — same process, NO agent)
   │  packages/app-core/platforms/android/app/src/main/elizavoice-jni/
   │  libelizavoicejni.so → libelizainference.so (fused, ABI v7, all four runtimes)
   │  1. native VAD hot-loop + turn segmentation (ported VadDetector state machine):
   │       streams the PCM through eliza_inference_vad_process, applies the
   │       onset/offset/pause/end-hangover thresholds, buffers the turn PCM
   │       (+ pre-roll) between speech-start and speech-end — ZERO per-512-window
   │       JS↔native bridge calls.
   │  2. on speech-end: eliza_inference_speaker_embed (256-d WeSpeaker embedding)
   │       + eliza_inference_diariz_segment (293 pyannote frame labels), natively.
   │  3. returns a turn-level result (base64 embedding + int8 labels) to JS.
   ▼
 JS (JniVoicePipeline)
   │  decodes the embedding + labels, runs the injected speaker resolver
   │  (embedding → enrolled entity) and buildVoiceTurnSignal (the ambient gate),
   │  and surfaces a JniAttributedTurn.
   ▼
 voiceTurnSignal  → the `core.voice_turn_signal` server gate decides
                     whether the agent speaks (owner / bystander / wake word).

The native ops the JNI host wraps (all eliza_inference_*, fused into the one libelizainference.so):

Stage JNI surface native runtime
Silero VAD (turn segmentation) vad open/processBatch/reset/close + the streaming pipeline* eliza_inference_vad_*
openWakeWord ("hey eliza") wakeword open/scoreBatch/reset/close eliza_inference_wakeword_*
WeSpeaker encoder (speaker embedding) speaker open/embed/close eliza_inference_speaker_*
pyannote diarizer (segment by speaker) diariz open/segment/close eliza_inference_diariz_*

Each resolves its GGUF from the on-device bundle (<files>/eliza-1/bundle/{vad,wake,speaker,diariz}/…). The split is: the VAD hot-loop + turn segmentation + speaker/diariz forward passes run natively; the speaker-match-against-enrolled-profiles + the ambient gate stay in JS (per-turn, infrequent).

What is verified on-device (Pixel 9a, ai.elizaos.app)

The whole pipeline runs in the bionic app process — proven via CDP + logcat (the in-process verification channel; every line is emitted by the ai.elizaos.app pid, never the agent):

  • ABI + capability: eliza_inference_abi_version() = 7, and all four classifiers report supported in-process: vad=1 wakeword=1 speaker=1 diariz=1.
  • Full pipeline on real speech (freeman.wav + 2 s trailing silence, fed in 1 s batches via pipelineSelfTest):
    pipelineSelfTest: feeding 308224 samples (19.26s), chunk=16000
    TURN jni_0: samples=285184 (17.82s) | speaker: embDim=256 norm=1.0000 |
                diariz: frames=293 distinctClasses=1
    
    The 17.82 s turn (< 19.26 s fed) is a real VAD speech-end firing at the silence boundary — turn segmentation, the 256-d L2-normalized speaker embedding, and the 293-frame diarizer all ran in-process.
  • Wake-word (wakewordSelfTest, "hey eliza" clip vs silence): posMax=1.0000 negMax=0.0000.

The JS-side consumer (JniVoicePipeline) has a host unit test (packages/ui/src/voice/jni-voice-pipeline.test.ts, 5 cases): lifecycle, runtime-unavailable refusal, frame batching (one bridge call per ~1 s), turn embedding/label decode, and the confident-bystander suppression gate.

The platform-agnostic agent-side consumer (plugins/plugin-local-inference/src/services/voice/audio-frame-consumer.ts) also has its host unit test + the real-model smoke (packages/app-core/scripts/voice-attribution-smoke.ts), shared by both transports.

On-device verification surface

window.__jniVoice (installed on Android by main.tsxinstallJniVoiceHarness) drives the in-process pipeline from CDP:

window.__jniVoice.start()   → open native pipeline + start mic + pump
window.__jniVoice.status()  → { running, framesSent, turnsObserved, abi, recentTurns }
window.__jniVoice.stop()    → stop capture, flush the open turn, free handles

Legacy: musl bun-agent transport (AOSP build only)

Before the JNI host, the four classifiers ran in the embedded bun agent (a separate musl process), reached from the WebView by POSTing batched frames to POST /api/voice/audio-frames (AudioFramePumpLiveDiarizationSessionAudioFrameConsumer). That path dlopened standalone musl libs (libsilero_vad.so, libvoice_classifier.so, cross-compiled with zig cc --target=aarch64-linux-musl) via bun:ffi, pointed at by the ELIZA_SILERO_VAD_LIB / ELIZA_VOICE_CLASSIFIER_LIB env vars ElizaAgentService exports when those .so are present in nativeLibraryDir.

This transport is superseded for the normal APK by the JNI/bionic host above. The standalone musl voice .so and their zig cross-build script (packages/native/scripts/build-voice-libs-android-musl.mjs) have been removed. The musl libllama.so (the text agent — a separate concern) is untouched, and the bun-agent voice path remains available on the privileged AOSP build (out of scope here — to be unified with the JNI host later).