Running 100% on-device

Muse — a self-aware AI assistant that lives on your phone.

Muse pairs a local llama.cpp inference engine with memory, actions, LAN AI sharing, and a clean Jetpack Compose UI — so your data never leaves the device unless you choose to host it on your own network.

100%
Offline
12+
Features
5
Languages
GGUF
Model format
Introduction

What is Muse?

Muse is an Android assistant powered by local GGUF language models. It ships with a full-featured chat UI, persistent memory, action parsing, device-stats monitoring, and a peer-to-peer LAN mode that lets one phone serve AI answers to others on the same network — no cloud required.

Product Vision

A proactive, personal AI.

Five pillars guide every design decision.

◉
Self-aware of environment
Context-aware: knows device state, time, and what the user is doing.
contextambient
✦
Selective user memory
Remembers what matters — preferences, facts, goals — not raw logs.
Room DB
⌘
Obsidian “second brain”
Connect to an Obsidian vault to store and recall long-term knowledge.
planned
◈
Message awareness
Read incoming SMS and notifications; reply on the user's behalf when allowed.
planned
✿
Task & reminder creation
Extracts actionable items from conversation and creates alarms, tasks, timers.
live
Architecture

Tech stack

Modern Android, MVVM, fully Kotlin. Native inference via JNI + llama.cpp.

Kotlin
Jetpack Compose
Hilt DI
Room
Coroutines / Flow
C++ / JNI
llama.cpp
CMake
DataStore / Prefs
WorkManager
At a glance

12 features, one app.

Hover a card for a glow preview. Click to jump to its detailed documentation.

Feature · 01

Local LLM Inference

A streaming, model-aware native inference layer bridged to Kotlin via JNI.

1 Engine

The Kotlin LLMEngine interface exposes a single streaming method. NativeEngine implements it by calling into jni_interface.cpp, which drives llama.cpp.

interface LLMEngine {
    fun generateStream(
        prompt: String,
        opts: GenOptions,
        onStatus: (Status) -> Unit = {}
    ): Flow<String>
}

Model families & prompt formats

  • Gemma Instruct → <start_of_turn>system / user / model
  • Qwen (ChatML) → <|im_start|>system / user / assistant
  • LLaMA / Tiny → ### SYSTEM / ### CONVERSATION / ### RESPONSE
  • Universal fallback uses the ### SYSTEM format
  • Detection uses word-boundary regex via isGemmaInstruct()

Sampling options

GenOptions carries maxNewTokens, temperature, topP, topK, repeatPenalty, parseSpecial and addSpecial. The right preset is chosen via chooseGenOptionsForModel(modelName) which reads the user's saved preset from PresetStore.

Load lifecycle

ModelManager (Hilt-scoped singleton) owns loading and exposes loadState: StateFlow<ModelLoadState> with states Idle · Loading · Ready(modelName) · Error.

Feature · 02

Model Download System

A foreground service that survives the system killing apps, with per-model notifications.

  • ModelDownloadService uses PARTIAL_WAKE_LOCK — never sleeps mid-download
  • HTTP Range header support — resumes from .part files
  • One notification per model (unique ID), shows speed, bytes, %
  • Token expiry (401/403) → deletes .part and prompts re-download
  • External GGUF import through Storage Access Framework
  • After two failures, the app shows a copyable URL
  • Checks available RAM and warns before starting oversized downloads
  • Pause + Stop buttons in notification and UI
  • DownloadProgressBus — a SharedFlow bridging Service → ViewModel
⚡
One of the trickiest parts: the download notification id is derived from the model name, so each model gets its own progress notification and they don't overwrite each other.
Feature · 03

Chat System

Streaming token UI, blank-token cutoff, think-tag stripping, ChatML artifact sanitization.

1 ChatViewModel state

  • chatMessages: StateFlow<List<ChatMessage>>
  • currentStreamingReply: StateFlow<String?> — live token stream
  • isGenerating: StateFlow<Boolean>
  • promptEval: StateFlow<Pair<Int,Int>?> — shown in top bar
  • chatHistory: StateFlow<List<ChatSession>>
  • modelLoadState — delegated from ModelManager

2 sendMessage() pipeline

// 1.  Persist user msg to Room + state
// 2.  Route to LAN if receiver mode
// 3.  Run memory extraction
// 4.  Auto-name session (first 40 chars)
// 5.  Assemble context + notification block
// 6.  buildPromptForModel(...)
// 7.  chooseGenOptionsForModel(...)
// 8.  InferenceService.start()          // foreground notif
// 9.  engine.generateStream()  .takeWhile { !threeBlanksInARow() }
// 10. stripThinkTags(tok)               // per-token
// 11. ActionParser + ActionExecutor
// 12. Save bot message to Room
// 13. InferenceService.stop()

3 Post-processing

  • stripThinkTags(text) — removes <think>…</think>; handles streaming unclosed tags
  • chatMlStopIndex(text) — first ChatML / EOS marker location
  • stripChatMlArtifacts(text) — truncates at marker & regex-strips stray <|…|>
ℹ
isModelAvailable() returns true if a local .gguf exists or LAN receiver mode is active — so NewChatScreen never redirects users who are connected to a host.
Feature · 04

Memory System

Selective fact extraction. What matters stays; chat transcripts don't linger.

  • processUserMessage(msg) — extracts and saves facts from user input
  • processAiReply(reply) — reads memory tags from AI responses
  • getMemorySummary() — compact string injected into every prompt's system block
  • Stored as UserMemory Room entities with an active/inactive flag
  • User can inspect & delete memories in MemoryScreen
  • Master toggle: memory_enabled pref (default on)
Feature · 05

Actions System

The AI can do things, not just talk — entirely on-device.

  • ActionParser.parse(reply) — extracts [ACTION:…] tags, returns (cleanReply, List<Action>)
  • ActionExecutor.execute(actions, sessionId, ctx) — runs them locally:
    • Create reminder / alarm
    • Set timer
    • Save task
  • Results are pushed through _actionEvent: SharedFlow<String> → a toast
Feature · 06

LAN AI Sharing

One phone loads a big model, the others query it over Wi-Fi. No cloud.

1 Protocol

Line-delimited TCP on port 8765.

Client
→
AUTH <6-digit-code>
Client
→
MODEL
Client
→
GENERATE <base64-json>
Server
←
TOKEN <base64> · · · DONE

2 Host device

  • LanHostService — foreground service, survives backgrounding
  • Notification: "N connected · X messages served · Code: XXXXXX"
  • onClientChange callback → saves to PREF_CONNECTED_CLIENTS
  • ViewModel registers OnSharedPreferenceChangeListener for zero-latency UI

3 Receiver device

  • connectReceiver() — auth test only, disconnects after OK
  • Connection-per-message on inference (no long-lived socket)
  • sendMessageViaLan() mirrors sendMessage() exactly — same memory, context, session naming, post-processing
  • Uses buildPromptForModel(hostModelName) — format matches host's model family
  • Uses chooseGenOptionsForModel(hostModelName) — same sampling params as local

4 Host discovery

scanForHosts() parallel-connects to all 254 IPs in the subnet with a 300 ms timeout per socket. Found hosts are shown as tappable rows on the Receive tab.

Feature · 07

Device Stats Monitor

Tiny text in the chat top bar that shows you what your silicon is doing.

  • Polls every 2 seconds
  • CPU% from /proc/stat
  • Temperature from /sys/class/thermal
  • GPU% from /sys/class/kgsl
  • Exposed as statsFlow(): Flow<DeviceStats>
  • Rendered in ChatScreen's top bar as tertiary 10 sp text — e.g. CPU 34% · 42°C
Feature · 08

Backup & Restore

Full JSON dump that never needs storage permissions.

  • Serializes ChatSession, ChatMessage, UserMemory, UserProfile via Gson
  • Writes to cacheDir/exports/ (must match file_provider_paths.xml)
  • Shares via ACTION_SEND + FileProvider — no WRITE permission needed
  • importBackup(uri) reads via SAF ContentResolver and upserts records
  • BackupState carries both exportError and importError
⚠
Sharp edge: the path in file_provider_paths.xml must be <cache-path path="exports/">. Writing anywhere else silently crashes FileProvider.getUriForFile().
Feature · 09

Settings System

Everything user-facing lives in PrefsManager.

  • Backed by SharedPreferences (file: app_prefs)
  • Top-level screen groups: Appearance · AI Behaviour · Data Management · LAN Hosting
  • AdvancedSettingsScreen toggles which rows show up in the main settings list
Feature · 11

Database

Room. Four tables. Simple on purpose.

TablePurposeKey DAO methods
ChatSessionConversation envelope — name, timestampsgetAllSessions()
ChatMessageRows: user & bot messages with session FKgetMessagesForSession(id), getMessageCount()
UserMemoryFacts with active flaggetAllActiveMemories(), activeMemoryCount()
UserProfileUser name, assistant name, preferencesgetProfile(), insertProfile()
Feature · 12

Permissions & Notifications

Only what the features actually need.

Manifest permissions

INTERNET WAKE_LOCK FOREGROUND_SERVICE FOREGROUND_SERVICE_DATA_SYNC FOREGROUND_SERVICE_SPECIAL_USE RECORD_AUDIO POST_NOTIFICATIONS SCHEDULE_EXACT_ALARM USE_EXACT_ALARM RECEIVE_BOOT_COMPLETED

Notification channels

  • lan_host — LAN hosting service (IMPORTANCE_LOW)
  • Model download — one per model
  • Inference service — running indicator

FileProvider

Authority: ${applicationId}.fileprovider — paths in res/xml/file_provider_paths.xml.

UI

Theme system

LucyColorPalette (not LucyColorScheme) accessed via LucyColors.current. Both modes toggled through SettingsViewModel.themeMode.

◐
Dark
Deep navy surfaces, soft violet accent, low-glare text.
bg #0b0d13 accent #8b7cff
◑
Light
Warm white surfaces, indigo accent, elevated cards.
bg #f7f8fb accent #6c63ff
Motion

Animation gallery

Everything in the app that moves is here — hover to see it live. All are CSS-driven, GPU-friendly, and respect prefers-reduced-motion.

Typing dots
"AI is thinking…"
M
Pulsing ring
Active session / mic
Streaming bar
Model download progress
Orbit loader
Model loading
Shimmer skeleton
Chat history load
Voice waveform
Voice mode capture
Gradient avatar
Assistant identity
Hosting · 2 connected
LAN badge pulse
Connection status
Reference

Pref keys quick reference

All persistent prefs are stored in the app_prefs SharedPreferences file.

KeyOwnerValues
lan_is_receiverLanHostViewModel"true" / "false"
lan_is_hostingLanHostViewModel"true" / "false"
lan_pairing_codeLanHostViewModel6-digit string
lan_receiver_host_ipLanHostViewModelIP string
lan_receiver_codeLanHostViewModel6-digit string
lan_host_message_countLanHostServiceInt
lan_host_connected_clientsLanHostServiceInt
memory_enabledSettingsViewModel"true" / "false"
context_awareness_enabledSettingsViewModel"true" / "false"
assistant_nameSettingsViewModelString