|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Build & Development Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +# Build debug APK |
| 9 | +./gradlew :app:assembleDebug |
| 10 | + |
| 11 | +# Build release APK |
| 12 | +./gradlew :app:assembleRelease |
| 13 | + |
| 14 | +# Install on connected device/emulator |
| 15 | +./gradlew :app:installDebug |
| 16 | + |
| 17 | +# Run all tests |
| 18 | +./gradlew :app:test |
| 19 | + |
| 20 | +# Lint |
| 21 | +./gradlew :app:lint |
| 22 | + |
| 23 | +# Detekt static analysis |
| 24 | +./gradlew detekt |
| 25 | + |
| 26 | +# ktlint formatting check |
| 27 | +./gradlew ktlintCheck |
| 28 | + |
| 29 | +# ktlint auto-format |
| 30 | +./gradlew ktlintFormat |
| 31 | + |
| 32 | +# Full build (includes SDK modules) |
| 33 | +./gradlew build |
| 34 | + |
| 35 | +# Verify build (checks ANDROID_HOME, optionally rebuilds natives) |
| 36 | +./scripts/verify.sh |
| 37 | + |
| 38 | +# Check SDK API call coverage |
| 39 | +./scripts/smoke.sh |
| 40 | +``` |
| 41 | + |
| 42 | +**Build targets**: compileSdk 36, minSdk 24, targetSdk 36, JVM target 17. ABI splits: `arm64-v8a`, `x86_64`. |
| 43 | + |
| 44 | +## SDK Module Wiring |
| 45 | + |
| 46 | +This app does **not** consume published artifacts. It depends on local SDK source via `settings.gradle.kts`: |
| 47 | + |
| 48 | +- `project(":runanywhere-kotlin")` → `../../../sdk/runanywhere-kotlin` (core SDK) |
| 49 | +- `project(":runanywhere-core-llamacpp")` → `../../../sdk/runanywhere-core-llamacpp` (LLM/GGUF inference) |
| 50 | +- `project(":runanywhere-core-onnx")` → `../../../sdk/runanywhere-core-onnx` (STT/TTS/VAD via ONNX) |
| 51 | +- Genie NPU AAR from Maven Central (`com.qualcomm.qti:QNNSdk`) — Qualcomm Snapdragon on-device inference |
| 52 | + |
| 53 | +Key gradle properties in `gradle.properties`: |
| 54 | +- `runanywhere.useLocalNatives=true` — use pre-built JNI .so files from local paths |
| 55 | +- `runanywhere.testLocal=true` — local test mode |
| 56 | +- `runanywhere.rebuildCommons=false` — skip native rebuild |
| 57 | + |
| 58 | +Native libraries require `useLegacyPackaging=true` and `pickFirsts` for duplicate .so resolution in `app/build.gradle.kts`. |
| 59 | + |
| 60 | +## Architecture |
| 61 | + |
| 62 | +### Pattern: MVVM + Jetpack Compose + Single Activity |
| 63 | + |
| 64 | +- **Single Activity**: `MainActivity.kt` — initializes PDF resources, observes `SDKInitializationState`, renders Compose root |
| 65 | +- **Navigation**: `AppNavigation.kt` with `NavigationRoute` object constants. Bottom tabs: Chat, Vision, Voice, More, Settings. "More" is a hub linking to STT, TTS, RAG, Benchmarks, LoRA Manager, Solutions |
| 66 | +- **State Management**: `MutableStateFlow` / `StateFlow` in every ViewModel, consumed via `collectAsState()` in Compose |
| 67 | +- **SDK Communication**: All SDK calls go through `RunAnywhere.*` extension functions. Events propagate via `EventBus.events` (SharedFlow) filtered by type (`LLMEvent`, `ModelEvent`, `STTEvent`, `TTSEvent`) |
| 68 | + |
| 69 | +### SDK Initialization Flow |
| 70 | + |
| 71 | +`RunAnywhereApplication.onCreate()` → delayed post to main looper → IO coroutine: |
| 72 | +1. `AndroidPlatformContext.initialize(context)` |
| 73 | +2. `RunAnywhere.initialize(apiKey, baseURL, environment)` — environment from `BuildConfig.DEBUG_MODE`, not `BuildConfig.DEBUG` |
| 74 | +3. `RunAnywhere.completeServicesInitialization()` — device registration |
| 75 | +4. `ModelList.setupModels()` — registers all models and backends |
| 76 | + |
| 77 | +Falls back to development/offline mode on failure. Custom API config read from `EncryptedSharedPreferences`. |
| 78 | + |
| 79 | +### Backend Registration Order |
| 80 | + |
| 81 | +In `ModelList.setupModels()`: |
| 82 | +``` |
| 83 | +LlamaCPP.register(priority = 100) — LLM, VLM inference via GGUF |
| 84 | +ONNX.register(priority = 100) — STT, TTS, VAD, embeddings |
| 85 | +Genie.register(priority = 200) — NPU inference (Snapdragon only, higher priority) |
| 86 | +``` |
| 87 | + |
| 88 | +## Feature Modules |
| 89 | + |
| 90 | +### Chat / LLM (`presentation/chat/`) |
| 91 | +- **ChatViewModel**: Streaming via `RunAnywhere.generateStream()`, non-streaming via `RunAnywhere.generate()`, tool calling via `RunAnywhereToolCalling.generateWithTools()` |
| 92 | +- Parses `<think>...</think>` tags for thinking/reasoning mode display |
| 93 | +- Tracks analytics: tokens/sec, TTFT (time to first token) |
| 94 | +- Conversation persistence via `ConversationStore` (JSON files in `filesDir/Conversations/`) |
| 95 | +- Subscribes to `EventBus.events.filterIsInstance<LLMEvent>()` |
| 96 | +- Proto-backed generation options: `LLMGenerationOptions` |
| 97 | + |
| 98 | +### Speech-to-Text (`presentation/stt/`) |
| 99 | +- **SpeechToTextViewModel**: Two modes — BATCH (`RunAnywhere.transcribe()`) and LIVE (`RunAnywhere.transcribeStream()`) |
| 100 | +- Audio capture via `AudioCaptureService` (16kHz, mono, PCM 16-bit, 100ms chunks) |
| 101 | +- Model: Sherpa Whisper Tiny (SHERPA framework) |
| 102 | + |
| 103 | +### Text-to-Speech (`presentation/tts/`) |
| 104 | +- **TextToSpeechViewModel**: SDK TTS via `RunAnywhere.synthesize(TTSOptions)`, plus Android system TTS fallback |
| 105 | +- AudioTrack playback with WAV header parsing (scans for "data" chunk marker) |
| 106 | +- Models: Piper US/British English voices (SHERPA framework) |
| 107 | + |
| 108 | +### Voice Assistant (`presentation/voice/`) |
| 109 | +- **VoiceAssistantViewModel**: Full STT → LLM → TTS pipeline |
| 110 | +- Streaming: `RunAnywhere.streamVoiceAgent()` for proto-backed voice events |
| 111 | +- One-shot: `RunAnywhere.processVoiceTurn()` |
| 112 | +- Speech detection: audio level threshold (0.1f), silence timeout (1500ms) |
| 113 | +- Continuous conversation mode with auto-resume after TTS playback |
| 114 | + |
| 115 | +### Vision / VLM (`presentation/vlm/`) |
| 116 | +- **VLMViewModel**: CameraX `LifecycleCameraController` for frame capture |
| 117 | +- Frame format: RGBA_8888 bitmap → RGB byte array conversion |
| 118 | +- Auto-streaming mode captures frames every 2.5 seconds |
| 119 | +- Inference via `RunAnywhere.processImageStream()` for streaming token output |
| 120 | +- Proto-backed: `VLMGenerationOptions` |
| 121 | +- Models: SmolVLM, LFM2-VL, Qwen2-VL (LLAMA_CPP framework) |
| 122 | + |
| 123 | +### RAG (`presentation/rag/`) |
| 124 | +- **RAGViewModel**: Document-based retrieval-augmented generation |
| 125 | +- Pipeline: extract text (`DocumentService.extractText`) → `RunAnywhere.ragCreatePipeline(RAGConfiguration)` → `RunAnywhere.ragIngest()` → `RunAnywhere.ragQuery()` |
| 126 | +- Teardown: `RunAnywhere.ragDestroyPipeline()` |
| 127 | +- Proto-backed: `RAGConfiguration` |
| 128 | +- Embedding model: All MiniLM L6 v2 (ONNX framework) |
| 129 | + |
| 130 | +### Benchmarks (`presentation/benchmarks/`) |
| 131 | +- **BenchmarkViewModel**: Orchestrates via `BenchmarkRunner` |
| 132 | +- Supports LLM, STT, TTS, VLM benchmark types |
| 133 | +- Results persisted via `BenchmarkStore` |
| 134 | +- Export formats: clipboard, CSV, JSON |
| 135 | + |
| 136 | +### LoRA Adapters (`presentation/lora/`) |
| 137 | +- **LoraViewModel**: Full lifecycle — download, load, unload, delete, compatibility check |
| 138 | +- SDK extensions: `loadLoraAdapter`, `removeLoraAdapter`, `clearLoraAdapters`, `checkLoraCompatibility`, `downloadLoraAdapter` |
| 139 | +- Proto-backed: `LoRAAdapterConfig`, `LoRAAdapterInfo`, `LoraCompatibilityResult` |
| 140 | + |
| 141 | +### Model Selection (`presentation/models/`) |
| 142 | +- **ModelSelectionViewModel**: Context-aware filtering via `ModelSelectionContext` enum (LLM, STT, TTS, VOICE, RAG_EMBEDDING, RAG_LLM, VLM) |
| 143 | +- Download: `RunAnywhere.downloadModel(id)` returns `Flow<DownloadProgress>` |
| 144 | +- Context-aware loading dispatches to: `loadLLMModel`, `loadSTTModel`, `loadTTSVoice`, `loadVLMModel` |
| 145 | +- RAG contexts select by reference only (no memory load) |
| 146 | + |
| 147 | +### Settings (`presentation/settings/`) |
| 148 | +- **SettingsViewModel**: Storage management via `RunAnywhere.storageInfo()`, `RunAnywhere.deleteModel()`, `RunAnywhere.clearCache()` |
| 149 | +- API config in `EncryptedSharedPreferences` (key: `runanywhere_encrypted_prefs`) |
| 150 | +- Generation settings (temperature, maxTokens, systemPrompt) in standard `SharedPreferences` |
| 151 | + |
| 152 | +## Data Layer |
| 153 | + |
| 154 | +### Models & Persistence |
| 155 | +- `ChatMessage.kt` — Serializable domain models: `ChatMessage`, `Conversation`, `MessageAnalytics`, `MessageRole`, `ToolCallInfo`, `PerformanceSummary` |
| 156 | +- `ConversationStore.kt` — Singleton, persists conversations as JSON in `filesDir/Conversations/` using kotlinx.serialization |
| 157 | +- `ModelList.kt` — Central model registry. All available models (LLM, STT, TTS, embedding, VLM, LoRA, Genie NPU) registered at startup |
| 158 | + |
| 159 | +### Audio |
| 160 | +- `AudioCaptureService.kt` — Wraps `AudioRecord`, emits PCM chunks via `callbackFlow`. 16kHz, mono, 16-bit. Calculates RMS for level visualization. |
| 161 | + |
| 162 | +## Conventions |
| 163 | + |
| 164 | +- **iOS is source of truth** — many ViewModels and components reference iOS equivalents in comments (e.g., "Reference: iOS ModelSelectionSheet.swift"). When behavior is unclear, check the iOS example app. |
| 165 | +- **Proto-backed types** — SDK types use protobuf extensively. Always use the proto-generated types (`LLMGenerationOptions`, `TTSOptions`, `VLMGenerationOptions`, `RAGConfiguration`, `LoRAAdapterConfig`) rather than raw strings/maps. |
| 166 | +- **Timber logging** — all logging uses Timber with emoji prefixes for log categories |
| 167 | +- **Structured types over strings** — enums and sealed classes for state, errors, and categories. Never use raw strings for identifiers. |
| 168 | +- **`BuildConfig.DEBUG_MODE`** reflects actual build type (debug vs release). `BuildConfig.DEBUG` is always true (isDebuggable enabled for release logging). |
| 169 | + |
| 170 | +## Manifest & Permissions |
| 171 | + |
| 172 | +- `RECORD_AUDIO` — STT, Voice features |
| 173 | +- `CAMERA` — VLM camera capture |
| 174 | +- `INTERNET` — model downloads, backend communication |
| 175 | +- `largeHeap=true` — required for on-device model inference |
| 176 | +- `extractNativeLibs=true` — JNI .so extraction |
| 177 | +- Qualcomm FastRPC library declared for NPU access |
| 178 | +- 16KB page size support property enabled |
| 179 | + |
| 180 | +## Code Quality |
| 181 | + |
| 182 | +- **Detekt** (`detekt.yml`): Focused on unused code — UnusedImports, UnusedPrivateClass, UnusedPrivateMember, UnusedPrivateProperty, GlobalCoroutineUsage, EmptyCatchBlock, NotImplementedDeclaration, VarCouldBeVal |
| 183 | +- **ktlint** v1.5.0 with android mode enabled |
| 184 | +- Run `./gradlew detekt` and `./gradlew ktlintCheck` before committing |
0 commit comments