Skip to content

Commit 90a1639

Browse files
Merge pull request #29 from smartscanapp/dev
Merge v2.0.0 updates
2 parents e674871 + 6f2fd42 commit 90a1639

75 files changed

Lines changed: 5862 additions & 130281 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1+
## v2.0.0 - 19/04/2026
2+
3+
### Added
4+
* Added model manager with tests
5+
* Added clustering package, with incremental clustering
6+
* Added `HNSWIndex` to embeddings/ package
7+
* Added classification package
8+
* Added token max length to TextEmbeddingProvider
9+
* Made tokenizers internal
10+
* Added SmartScanException
11+
* Added update method to `IEmbeddingStore`
12+
* Added save method to `IEmbeddingStore`
13+
14+
### Changed
15+
* Renamed ModelSource with ModelAssetSource and added core/models package
16+
* Renamed updatePrototype to updatePrototypeEmbedding
17+
* Renamed filterIds to ids in `IEmbeddingStore` query method and use Set instead of Long
18+
* Return number of items removed in `IEmbeddingStore` remove method
19+
* Return number of items added in `IEmbeddingStore` add method
20+
* Refactor `FileEmbeddingStore` to use a persistent file offset index with in-place updates
21+
* `FileEmbeddingStore` remove method now only removes items from cache and removal are not automatically persisted, call save method to persist removals. Add and update methods both remain write-though.
22+
* MinSDK changed to 28 (previously 30).
23+
* Removed embedBatch method from `IEmbeddingProvider` and added as util method instead in embeddings/ package
24+
* Remove detector/face package and added contents to detectors/
25+
26+
### Fixed
27+
* Filter ids before running query in `FileEmbeddingStore`
28+
* Made `FileEmbeddingStore` thread-safe
29+
30+
### Removed
31+
* Removed `FileEmbeddingStore` pagination query overload
32+
33+
134
## v1.3.0 - 21/12/2025
235

336
### Added

README.md

Lines changed: 90 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,79 @@
33
## Table of Contents
44

55
* [Overview](#overview)
6-
* [Quick Start](#quick-start)
76
* [Documentation](docs/README.md)
8-
* [Key Structure](#key-structure)
97
* [Installation](#installation)
8+
* [Quick Start](#quick-start)
109

11-
+ [1. Install Core Module](#1-install-core-module)
12-
+ [2. Install ML Module (Optional)](#2-install-ml-module-optional)
10+
- [1. Install Core Module](#1-install-core-module)
11+
- [2. Install ML Module (Optional)](#2-install-ml-module-optional)
1312
* [Design Choices](#design-choices)
14-
15-
+ [Core and ML](#core-and-ml)
16-
+ [Constraints](#constraints)
17-
+ [Model](#model)
18-
+ [Embedding Storage](#embedding-storage)
13+
- [Core and ML](#core-and-ml)
14+
- [Model](#model)
15+
- [Embedding Storage](#embedding-storage)
1916
- [Benchmark Summary](#benchmark-summary)
2017

2118
* [Gradle / Kotlin Setup Notes](#gradle--kotlin-setup-notes)
2219

2320

2421
## **Overview**
2522

26-
SmartScanSdk is a modular Android SDK that powers the **SmartScan app**. It provides tools for:
23+
SmartScanSdk is an Android library that powers the **SmartScan app**, providing tools for:
2724

28-
* Image & video processing
2925
* On-device ML inference
30-
* Semantic media indexing and search
26+
* Semantic search
27+
* Indexing
28+
* Embedding storage
29+
* Incremental clustering
30+
* ANN Search / HNSW Index
3131
* Few shot classification
32+
* Image & video processing
3233
* Efficient batch processing
34+
* Model management
3335

3436

3537
> **Note:** The SDK is designed to be flexible, but its primary use is for the SmartScan app and other apps I am developing. It is also subject to rapid experimental changes.
3638
3739
---
3840

41+
## Installation
42+
43+
Add the JitPack repository to your build file (settings.gradle)
44+
45+
```gradle
46+
dependencyResolutionManagement {
47+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
48+
repositories {
49+
mavenCentral()
50+
maven { url = uri("https://jitpack.io") }
51+
}
52+
}
53+
```
54+
55+
### **1. Install Core Module**
56+
57+
```gradle
58+
implementation("com.github.dev-diaries41.smartscan-sdk:smartscan-core:1.1.0")
59+
```
60+
61+
### **2. Install ML Module (Optional)**
62+
63+
```gradle
64+
implementation("com.github.dev-diaries41.smartscan-sdk:smartscan-ml:1.1.0")
65+
```
66+
67+
> `ml` depends on `core`, so including it is enough if you need both.
68+
69+
---
70+
3971
## Quick Start
4072

41-
Below is information on how to get started with embedding, indexing, and searching.
73+
Below is information on how to get started with embedding, clustering, indexing, and searching.
4274

4375
### Embeddings
4476

77+
You can use bundled or downloaded models, see [docs](docs/ml/providers.md) for more details.
78+
4579
#### Text Embeddings
4680

4781
Generate vector embeddings from text strings or batches of text for tasks such as semantic search or similarity comparison.
@@ -51,18 +85,23 @@ Generate vector embeddings from text strings or batches of text for tasks such a
5185
```kotlin
5286
//import com.fpf.smartscansdk.ml.models.providers.embeddings.clip.ClipTextEmbedder
5387

54-
// Requires model to be in raw resources at e.g res/raw/text_encoder_quant_int8.onnx
55-
val textEmbedder = ClipTextEmbedder(context, ResourceId(R.raw.text_encoder_quant_int8))
88+
// downloaded model
89+
val textEmbedder = ModelManager.getTextEmbedder(application, ModelName.ALL_MINILM_L6_V2)
90+
91+
// bundled model
92+
val textEmbedder = ClipTextEmbedder(application, ModelAssetSource.Resource(R.raw.clip_text_encoder_quant), vocabSource = ModelAssetSource.Resource(R.raw.vocab), mergesSource = ModelAssetSource.Resource(R.raw.merges))
5693
val text = "Hello smartscan"
5794
val embedding = textEmbedder.embed(text)
5895

5996
```
6097

6198
**Batch Example:**
6299

100+
Specifically designed for large batches
101+
63102
```kotlin
64103
val texts = listOf("first sentence", "second sentence")
65-
val embeddings = textEmbedder.embedBatch(texts)
104+
val embeddings = embedBatch(context, textEmbedder, texts)
66105
```
67106

68107
---
@@ -76,8 +115,11 @@ Generate vector embeddings from images (as `Bitmap`) for visual search or simila
76115
```kotlin
77116
//import com.fpf.smartscansdk.ml.models.providers.embeddings.clip.ClipImageEmbedder
78117

79-
// Requires model to be in raw resources at e.g res/raw/image_encoder_quant_int8.onnx
80-
val imageEmbedder = ClipImageEmbedder(context, ResourceId(R.raw.image_encoder_quant_int8))
118+
// downloaded model
119+
val imageEmbedder = ModelManager.getImageEmbedder(application, ModelName.DINOV2_SMALL)
120+
121+
// bundled model
122+
val imageEmbedder = ClipImageEmbedder(application, ModelAssetSource.Resource(R.raw.clip_image_encoder_quant))
81123

82124
val embedding = imageEmbedder.embed(bitmap)
83125

@@ -87,23 +129,23 @@ val embedding = imageEmbedder.embed(bitmap)
87129
**Batch Example:**
88130

89131
```kotlin
90-
val images: List<Bitmap> = ...
91-
val embeddings = imageEmbedder.embedBatch(images)
132+
val images = listOf<Bitmap>()
133+
val embeddings = embedBatch(context, imageEmbedder, images)
92134
```
93135

94136
### Indexing
95137

96-
To get started with indexing media quickly, you can use the provided `ImageIndex` and `VideoIndexer` classes as shown below. You can optionally create your own indexers (including for text related data) by implementing the `BatchProcessor` interface. See docs for more details.
138+
To get started with indexing media quickly, you can use the provided `ImageIndex` and `VideoIndexer` classes as shown below. You can optionally create your own indexers (including for text related data) by extending the `BatchProcessor`. See [docs](docs/core/processors.md) for more details.
97139

98140
#### Image Indexing
99141

100142
Index images to enable similarity search. The index is saved as a binary file and managed with a FileEmbeddingStore.
101-
> **Important**: During indexing the MediaStore Id is used to as the id in the `Embedding` which is stored. This can later be used for retrieval.
143+
> **Important**: During indexing the MediaStore Id is used to as the id in the `StoredEmbedding` which is stored. This can later be used for retrieval.
102144
103145

104146
```kotlin
105147
val imageEmbedder = ClipImageEmbedder(context, ResourceId(R.raw.image_encoder_quant_int8))
106-
val imageStore = FileEmbeddingStore(File(context.filesDir, "image_index.bin"), imageEmbedder.embeddingDim, useCache = false) // cache not needed for indexing
148+
val imageStore = FileEmbeddingStore(File(context.filesDir, "image_index.bin"), imageEmbedder.embeddingDim)
107149
val imageIndexer = ImageIndexer(imageEmbedder, context=context, listener = null, store = imageStore) //optionally pass a listener to handle events
108150
val ids = getImageIds() // placeholder function to get MediaStore image ids
109151
imageIndexer.run(ids)
@@ -112,10 +154,11 @@ imageIndexer.run(ids)
112154
#### Video Indexing
113155

114156
Index videos to enable similarity search. The index is saved as a binary file and managed with a FileEmbeddingStore.
157+
> **Important**: During indexing the MediaStore Id is used to as the id in the `StoredEmbedding` which is stored. This can later be used for retrieval.
115158
116159
```kotlin
117160
val imageEmbedder = ClipImageEmbedder(context, ResourceId(R.raw.image_encoder_quant_int8))
118-
val videoStore = FileEmbeddingStore(File(context.filesDir, "video_index.bin"), imageEmbedder.embeddingDim, useCache = false )
161+
val videoStore = FileEmbeddingStore(File(context.filesDir, "video_index.bin"), imageEmbedder.embeddingDim )
119162
val videoIndexer = VideoIndexer(imageEmbedder, context=context, listener = null, store = videoStore, width = ClipConfig.IMAGE_SIZE_X, height = ClipConfig.IMAGE_SIZE_Y)
120163
val ids = getVideoIds() // placeholder function to get MediaStore video ids
121164
videoIndexer.run(ids)
@@ -128,91 +171,46 @@ Below shows how to search using both text queries and an image. The returns resu
128171
#### Text-to-Image Search
129172

130173
```kotlin
131-
val imageStore = FileEmbeddingStore(File(context.filesDir, "image_index.bin"), imageEmbedder.embeddingDim, useCache = false) // cache not needed for indexing
132-
val imageRetriever = FileEmbeddingRetriever(imageStore)
133-
val textEmbedder = ClipTextEmbedder(context, ResourceId(R.raw.text_encoder_quant_int8))
174+
val imageStore = FileEmbeddingStore(File(context.filesDir, "image_index.bin"), imageEmbedder.embeddingDim)
134175
val query = "my search query"
135176
val embedding = textEmbedder.embed(query)
136177
val topK = 20
137178
val similarityThreshold = 0.2f
138-
val results = retriever.query(embedding, topK, similarityThreshold)
179+
val results = imageStore.query(embedding, topK, similarityThreshold) // returns image ids, optionally pass filter ids
139180

140181
```
141182

142183
#### Reverse Image Search
143184

144185
```kotlin
145-
val imageStore = FileEmbeddingStore(File(context.filesDir, "image_index.bin"), imageEmbedder.embeddingDim, useCache = false) // cache not needed for indexing
146-
val imageRetriever = FileEmbeddingRetriever(imageStore)
147-
val imageEmbedder = ClipImageEmbedder(context, ResourceId(R.raw.image_encoder_quant_int8))
186+
val imageStore = FileEmbeddingStore(File(context.filesDir, "image_index.bin"), imageEmbedder.embeddingDim)
148187
val embedding = imageEmbedder.embed(bitmap)
149188
val topK = 20
150189
val similarityThreshold = 0.2f
151-
val results = retriever.query(embedding, topK, similarityThreshold)
152-
153-
```
154-
155-
---
156-
157-
## Key Structure
158-
190+
val results = imageStore.query(embedding, topK, similarityThreshold)
159191
```
160-
SmartScanSdk/
161-
├─ core/ # Essential functionality
162-
│ ├─ data/ # Data classes and processor interfaces
163-
│ ├─ embeddings/ # Embedding utilities and file-based stores
164-
│ ├─ indexers/ # Image and video indexers
165-
│ ├─ media/ # Media helpers (image/video utils)
166-
│ └─ processors/ # Batch processing and memory helpers
167-
168-
└─ ml/ # On-device ML infrastructure + models
169-
├─ data/ # Model loaders and data classes
170-
└─ models/ # Base ML models and providers
171-
└─ providers/
172-
└─ embeddings/ # Embedding providers
173-
├─ clip/ # CLIP image & text embedder
174-
└─ FewShotClassifier.kt # Few-shot classifier
175-
176-
├─ build.gradle
177-
└─ settings.gradle
178-
```
179-
180-
**Notes:**
181-
182-
* `core` and `ml` are standalone Gradle modules.
183-
* Both are set up for **Maven publishing**.
184-
* The structure replaces the old `core` and `extensions` module in versions ≤1.0.4
185192

186-
---
193+
#### ANN Search (HNSW Index)
187194

188-
## Installation
189-
190-
Add the JitPack repository to your build file (settings.gradle)
191-
192-
```gradle
193-
dependencyResolutionManagement {
194-
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
195-
repositories {
196-
mavenCentral()
197-
maven { url = uri("https://jitpack.io") }
198-
}
199-
}
195+
```kotlin
196+
val annIndex = HNSWIndex(dim=512)
197+
val query = "my search query"
198+
val embedding = textEmbedder.embed(query)
199+
val topK = 5
200+
val results = annIndex.query(embedding, topK) // returns nearest neighbour indices must map to item id
200201
```
201202

202-
### **1. Install Core Module**
203-
204-
```gradle
205-
implementation("com.github.dev-diaries41.smartscan-sdk:smartscan-core:1.1.0")
206-
```
203+
### Clustering
207204

208-
### **2. Install ML Module (Optional)**
205+
Incremental clustering groups embeddings as they are added see [docs](docs/core/clustering.md) for more details.
209206

210-
```gradle
211-
implementation("com.github.dev-diaries41.smartscan-sdk:smartscan-ml:1.1.0")
207+
```kotlin
208+
val imageStore = FileEmbeddingStore(File(context.filesDir, "image_index.bin"), imageEmbedder.embeddingDim)
209+
val itemEmbeds = store.get()
210+
val existingClusters: Map<Long, Cluster> = emptyMap() // optionally pass existing clusters
211+
val clusterer = IncrementalClusterer(existingClusters = existingClusters, defaultThreshold = 0.4f)
212+
val result = clusterer.cluster(itemEmbeds)
212213
```
213-
214-
> `ml` depends on `core`, so including it is enough if you need both.
215-
216214
---
217215

218216
## Design Choices
@@ -222,31 +220,16 @@ implementation("com.github.dev-diaries41.smartscan-sdk:smartscan-ml:1.1.0")
222220
* **core** → minimal runtime: shared interfaces, data classes, embeddings, media helpers, processor execution, and efficient batch/concurrent processing.
223221
* **ml** → ML infrastructure and models: model loaders, base models, embedding providers (e.g., CLIP), and few-shot classifiers. Optional or experimental ML-related features can be added under `ml/providers`.
224222

225-
This structure replaces the old `core` and `extensions` modules from versions 1.0.4 and below. It provides more clarity and allows consumers to use core non-ML functionality independently. For the most part, the code itself remains unchanged; only the file organization has been updated. Documentation will be updated shortly.
226-
227-
---
228-
229-
### Constraints
230-
231-
* Full index must be loaded in-memory on Android (no native vector DB support).
232-
* Some users have 40K+ images, so fast processing and loading are critical.
233-
* Balance speed and memory/CPU use across devices (1GB–8GB memory range).
234-
* Concurrency improves speed but increases CPU usage, heat, and battery drain.
235-
236-
To mitigate the constraints described above, all bulk ml relate processing is done using dynamic, concurrent batch processing via the use of `BatchProcessor`, which uses available memory to self-adjust concurrency between batches
237-
238-
### Model
239-
240-
Supports models stored locally or bundled in the app.
241-
242223
---
243224

244225
### Embedding Storage
245226

246-
The SDK only provides a file based implementation of `IEmbeddingStore`, `FileEmbeddingStore` (in core) because the following benchmarks below show much better performance for the loading of embeddings
227+
The SDK only provides a file based implementation of `EmbeddingStore`, `FileEmbeddingStore` (in core) because the following benchmarks below show much better performance for loading embeddings in comparison to Room.
247228

248229
#### **Benchmark Summary**
249230

231+
File-based memory-mapped loading is significantly faster and scales better.
232+
250233
**Real-Life Test Results**
251234

252235
| Embeddings | Room Time (ms) | File Time (ms) |
@@ -267,14 +250,13 @@ The SDK only provides a file based implementation of `IEmbeddingStore`, `FileEmb
267250

268251
![SmartScan Load Benchmark](./benchmarks/smartscan-load-benchmark.png)
269252

270-
File-based memory-mapped loading is significantly faster and scales better.
271253

272254
___
273255

274256
## Gradle / Kotlin Setup Notes
275257

276258
* Java 17 / Kotlin JVM 17
277-
* `compileSdk = 36`, `targetSdk = 34`, `minSdk = 30`
259+
* `compileSdk = 36`, `targetSdk = 34`, `minSdk = 28`
278260
* `core` exposes `androidx.core:core-ktx`
279261
* `ml` depends on `core` and ONNX Runtime
280262

core/build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ android {
1111
compileSdk = 36
1212

1313
defaultConfig {
14-
minSdk = 30
14+
minSdk = 28
1515
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1616
}
1717

@@ -52,6 +52,14 @@ android {
5252
}
5353
}
5454

55+
externalNativeBuild {
56+
cmake {
57+
path ("src/main/cpp/CMakeLists.txt")
58+
}
59+
}
60+
61+
ndkVersion = "27.0.12077973"
62+
5563
}
5664

5765
java {

0 commit comments

Comments
 (0)