Skip to content

Commit ff1f1e2

Browse files
updates
1 parent 10cdf3c commit ff1f1e2

12 files changed

Lines changed: 3291 additions & 1157 deletions

File tree

CLAUDE.md

Lines changed: 356 additions & 671 deletions
Large diffs are not rendered by default.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## What This App Is
6+
7+
A Flutter reference app demonstrating the RunAnywhere on-device AI SDK. It mirrors the native iOS app's feature set: LLM chat (streaming + non-streaming), speech-to-text, text-to-speech, voice assistant pipeline (STT→LLM→TTS), vision/VLM with live camera, tool calling, RAG with PDF ingestion, structured JSON output, and a solutions YAML runner. Eight tabs: Chat, Vision, STT, Speak, Voice, Tools, Solutions, Settings.
8+
9+
## Common Commands
10+
11+
```bash
12+
# From this directory (examples/flutter/RunAnywhereAI/)
13+
14+
# Resolve packages (must run first)
15+
flutter pub get
16+
17+
# Static analysis (strict mode — dead_code/unused_import are errors)
18+
flutter analyze
19+
20+
# Run on connected device or emulator
21+
flutter run
22+
23+
# Run on specific iOS simulator
24+
flutter run -d "iPhone 16 Pro"
25+
26+
# Build debug APK (no device needed)
27+
flutter build apk --debug
28+
29+
# Build iOS simulator app
30+
flutter build ios --simulator --debug
31+
32+
# Format
33+
dart format lib/ test/
34+
35+
# Run tests (only one smoke test exists)
36+
flutter test
37+
38+
# Quick smoke check (greps for SDK API coverage + runs flutter analyze)
39+
./scripts/smoke.sh
40+
41+
# Full verification (pub get + analyze + APK build)
42+
./scripts/verify.sh
43+
44+
# Full verification including iOS
45+
RUN_IOS=1 ./scripts/verify.sh
46+
47+
# Rebuild native binaries if C++ layer changed (run from repo root)
48+
# Android:
49+
../../../scripts/build-core-android.sh arm64-v8a
50+
# iOS:
51+
../../../scripts/build-core-xcframework.sh
52+
```
53+
54+
For iOS, after `flutter pub get`, you may need `cd ios && pod install && cd ..` if Pods are stale.
55+
56+
## SDK Dependency Chain
57+
58+
The app depends on four local Flutter SDK packages via `path:` dependencies in `pubspec.yaml`:
59+
60+
```
61+
runanywhere → ../../../sdk/runanywhere-flutter/packages/runanywhere
62+
runanywhere_llamacpp → ../../../sdk/runanywhere-flutter/packages/runanywhere_llamacpp
63+
runanywhere_genie → ../../../sdk/runanywhere-flutter/packages/runanywhere_genie
64+
runanywhere_onnx → ../../../sdk/runanywhere-flutter/packages/runanywhere_onnx
65+
```
66+
67+
These packages wrap pre-built native C++ libraries via Dart FFI (`dart:ffi`), not method channels. AI inference calls go directly from Dart → native `.so`/xcframework without any platform channel hop.
68+
69+
- **Android**: `.so` files live in each SDK package's `android/src/main/jniLibs/` dirs. The Gradle property `runanywhere.useLocalNatives=true` (in `android/gradle.properties`) tells the build to use these local files instead of downloading from GitHub releases.
70+
- **iOS**: xcframeworks (`RACommons`, `RABackendLLAMACPP`, `RABackendONNX`, `RABackendSherpa`) are vendored in each SDK package's `ios/Frameworks/` dirs. Static linkage (`use_frameworks! :linkage => :static` in Podfile) is required so `DynamicLibrary.executable()` can find the symbols at runtime.
71+
72+
If native binaries are missing (fresh clone), they must be staged first — see README's "Clean-Clone Bring-Up" or use `scripts/verify.sh` with `REFRESH_ANDROID_NATIVE=1` / `REFRESH_IOS_NATIVE=1`.
73+
74+
## Architecture
75+
76+
### Initialization (runanywhere_ai_app.dart)
77+
78+
App startup runs a multi-phase sequence in `initState` via `addPostFrameCallback`:
79+
80+
1. **Eager .so loading** (Android only) — `DynamicLibrary.open()` on 6 `.so` files to preload before any SDK call
81+
2. **SDK init** — reads API key / base URL from secure storage (`KeychainHelper`); calls `RunAnywhereSDK.instance.initialize(...)` with or without credentials
82+
3. **Module registration** — guarded by a static `_modulesRegistered` flag to survive hot-reload. Registers: LlamaCpp (9 GGUF models), Genie NPU (Android/Snapdragon only, chip-conditional models), VLM (SmolVLM 500M), Sherpa STT/TTS (Whisper + Piper models), RAG embeddings (MiniLM), ONNX backend, RAG backend
83+
4. **Model refresh**`ModelManager.shared.refresh()` notifies all listeners
84+
85+
### State Management
86+
87+
Three patterns coexist:
88+
89+
1. **Provider** (app-wide) — only `ModelManager.shared` is in the `MultiProvider` tree at the root
90+
2. **Singleton ChangeNotifier + ListenableBuilder** (feature-level) — `ModelListViewModel.shared`, `ToolSettingsViewModel.shared`, `ConversationStore.shared`, `DeviceInfoService.shared` are accessed directly, not through Provider
91+
3. **Local setState** (per-screen ephemeral state) — recording flags, streaming text buffers, error messages
92+
93+
### Navigation
94+
95+
`ContentView` uses `Scaffold` + `NavigationBar` + `IndexedStack` (all 8 tabs stay mounted). No named routes or GoRouter. Secondary screens (`RagDemoView`, `StructuredOutputView`, `VLMCameraView`) use `Navigator.push(MaterialPageRoute(...))`. Model pickers use `showModalBottomSheet`.
96+
97+
### Core Services (singletons in core/services/)
98+
99+
- **AudioRecordingService** — wraps `record` package; 16kHz mono WAV; emits normalized dB levels on a broadcast stream
100+
- **AudioPlayerService** — wraps `audioplayers`; constructs WAV headers from raw PCM16 bytes; writes temp files for playback
101+
- **ConversationStore** — file-based JSON persistence under `<documents>/Conversations/<id>.json`; messages carry optional `thinkingContent` and `MessageAnalytics`
102+
- **KeychainService / KeychainHelper** — wraps `flutter_secure_storage`; iOS Keychain with `first_unlock_this_device`, Android `EncryptedSharedPreferences`; keys prefixed with `com.runanywhere.RunAnywhereAI_`
103+
- **PermissionService** — wraps `permission_handler`; requests microphone + speech (iOS only) for STT, camera for VLM
104+
105+
### Platform Channel (com.runanywhere.sdk/native)
106+
107+
Both platforms implement the same 6 methods for audio session and device info — this is separate from the AI SDK's FFI path:
108+
109+
- `configureAudioSession(mode)` / `activateAudioSession` / `deactivateAudioSession`
110+
- `requestMicrophonePermission` / `hasMicrophonePermission`
111+
- `getDeviceCapabilities` (memory, processors)
112+
113+
iOS: `AppDelegate.swift`. Android: `PlatformChannelHandler.kt`.
114+
115+
### SDK API Surface Used
116+
117+
All AI calls go through `RunAnywhereSDK.instance`:
118+
- `.llm.generate()` / `.llm.generateStream()` / `.llm.load()` / `.llm.unload()`
119+
- `.stt.transcribe()` / `.stt.load()`
120+
- `.tts.synthesize()` / `.tts.loadVoice()`
121+
- `.vlm.processImageStream()` / `.vlm.load()`
122+
- `.voice.eventStream()` / `.voice.initializeWithLoadedModels()`
123+
- `.tools.register()` / `.tools.generateWithTools()`
124+
- `.rag.createPipeline()` / `.rag.ingest()` / `.rag.query()` / `.rag.destroyPipeline()`
125+
- `.solutions.run(yaml:)`
126+
- `.downloads.start()` / `.downloads.delete()` / `.downloads.list()` / `.downloads.getStorageInfo()`
127+
- `.models.register()` / `.models.registerMultiFile()` / `.models.available()`
128+
- `.hardware.getChipEnum()`
129+
130+
### Feature-Specific Notes
131+
132+
- **Chat**: streaming generation appends tokens to a placeholder message at a fixed list index via `setState`. Tool calling detects `lfm2`+`tool` in the model name to select `ToolCallFormatNames.lfm2`. Thinking content parsed from `<think>...</think>` blocks.
133+
- **VLM**: `VLMViewModel` (non-singleton, created per view) uses a `Timer.periodic` at 2.5s for auto-streaming mode. Camera frames are BGRA→file→`VLMImage(filePath:)`.
134+
- **Voice Assistant**: subscribes to `voice.eventStream()` which emits protobuf `VoiceEvent` messages. Event payload types: `state`, `vad`, `userSaid`, `assistantToken`, `audio`, `error`.
135+
- **RAG**: `DocumentService` uses `syncfusion_flutter_pdf` for PDF text extraction. The RAG model selection flow does NOT pre-load models into memory — it only passes paths to `RAGConfiguration`.
136+
- **Tools**: three demo tools registered (`get_weather`, `calculate`, `get_current_time`). Weather tool uses Open-Meteo free API via `package:http`.
137+
- **Structured Output**: uses `LLMGenerationOptions(jsonSchema:)` with predefined schemas.
138+
139+
## Build Configuration Gotchas
140+
141+
- **Android `packagingOptions`** (`android/app/build.gradle`): `pickFirst '**/libc++_shared.so'` and `pickFirst '**/libomp.so'` — required because multiple SDK plugin packages each bundle these shared libs
142+
- **Android `extractNativeLibs="true"`** and `<uses-native-library android:name="libcdsprpc.so" android:required="false"/>` in `AndroidManifest.xml` — required for Genie NPU (Qualcomm FastRPC)
143+
- **iOS Podfile post_install**: forces `EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64` on all pods and Runner — locally built xcframeworks only contain arm64 simulator slices
144+
- **iOS Podfile permission flags**: `PERMISSION_MICROPHONE=1`, `PERMISSION_SPEECH_RECOGNIZER=1`, `PERMISSION_CAMERA=1` must be set for `permission_handler` to compile those capabilities
145+
- **Gradle heap**: `-Xmx6g` in `gradle.properties` — native compilation is memory-intensive
146+
- **Kotlin 2.1.21 / AGP 8.9.1 / Gradle 8.11.1** — these versions must stay in sync; mismatches cause build failures
147+
148+
## Analysis Options
149+
150+
`analysis_options.yaml` enables strict Dart analysis:
151+
- `strict-casts`, `strict-inference`, `strict-raw-types` all enabled
152+
- `dead_code`, `unused_import`, `unused_local_variable`, `unused_element`, `unused_field` are **errors** (not warnings)
153+
- Generated files (`*.g.dart`, `*.freezed.dart`, `lib/generated/**`) are excluded
154+
155+
## Platform Requirements
156+
157+
- Flutter `>=3.10.0`, Dart `>=3.0.0 <4.0.0`
158+
- Android: compileSdk 36, targetSdk 34, minSdk from Flutter default, JVM 17
159+
- iOS: deployment target 15.1 (enforced by Podfile), Xcode 15+
160+
- Physical ARM64 device recommended — native libs are optimized for arm64

0 commit comments

Comments
 (0)