SiloSense
Track: Mobile AI. Repo: https://github.com/rapha18th/SiloSense
Inspiration
According to the FAO and World Bank, up to 37% of food crops grown in Sub-Saharan Africa never reach the dinner table.
In cereals, maize, sorghum, and millet losses amount to 20.5%. An estimated $ 4Billion of economic loss per year, equaling nutritional requirements for 48 million people.
The problem disproportionately affects smallholder farmers, who have little capital to invest in modern food storage facilities. Their maize, sorghum, or millet is stored in open mud brick structures, or simple woven baskets, far too vulnerable to pest attack. The crop is often attacked by weevils and grain borers, invisible pests, which damage grain from the inside without being easily detected.
Farmers, unable to detect these hidden infestations, often panic and sell their harvest at a fraction of its market value to avoid post-harvest losses. This results in significant food waste and economic losses for both the farm and the wider community. In addition, when the harvest is so abundant that storage infrastructure is inadequate, the increased density also raises the potential for infestation across the entire stockpile.
The 2024/25 maize season in Zimbabwe delivered 1.82 million tonnes, up significantly from 635,000 tonnes in the last dry season. This volume overwhelmed traditional infrastructure, meaning grain stayed longer in storage, making it a big target for infestation.
SiloSense uses AI and sensors on a consumer-grade smartphone to help smallholder farmers and governments predict infestations weeks early, allowing for proactive interventions such as fumigation or rotation to minimise losses.
What it does
SiloSense turns any low-cost Android smartphone into an off-grid acoustic grain monitor. Hold the phone to a sack, and in 1.5 seconds it tells you, on-device, whether insects are already inside.
- 1.5-second acoustic check: captures 16kHz mono audio directly through the phone's microphone.
- Three-tier diagnostic reporting: Likely Clean, Uncertain, or Likely Infested. The Uncertain band, P(infested) between 0.45 and 0.65, reflects where the model's own validation data stops separating cleanly, instead of forcing a hard cutoff.
- Live on-device telemetry: the app shows the actual ONNX Runtime execution provider that ran and the real millisecond-level latency, on screen, every time.
- Pre-verified sample path: a bundled real infested-grain clip (Tenebrio molitor, in flour) runs the Infested path through the exact same pipeline without needing live infested grain on hand.
Both raw probabilities are always shown underneath the result, so the number behind the colour is never hidden.
The whole pipeline runs offline, with no server, no upload, and no added hardware beyond the phone itself. Two more screens go past the demo and into the evidence: Full model results show the on-device FP32-vs-INT8 benchmark, the execution-provider trace, memory, thermal, and a desktop cross-check, all measured live. Battery test runs a real idle-vs-loaded current-draw measurement, about 90 seconds, and reports the actual number.
| FP32 | INT8 (static QDQ) | Change | |
|---|---|---|---|
| Model size | 0.393 MB | 0.111 MB | 3.54x smaller |
| Validation accuracy | 98.99% | 100.00% | no loss |
| Validation F1 | 0.985 | 1.000 | no loss |
| On-device inference (Galaxy M16, Arm CPU) | 3.71 ms avg | 1.07 ms avg | 3.47x faster |
![]() Live recording, clean |
![]() Bundled infested sample |
![]() Full model results |
![]() Battery test |
![]() See source |
How we built it
- Data indexing and subset extraction: filtered the 106GB SPID/A-SPIDS dataset down to a label-verified 246MB subset by parsing inner recording timestamps against
aspids_log.csv. Augmented the clean class with ESC-50 environmental audio clips. - Audio feature pipeline, pure NumPy to Kotlin: converted 1.5s audio clips into (40, 151) log-mel spectrograms. We avoided
librosa, which pulls innumba/llvmlitecompilation friction on Arm, by building a pure NumPy pipeline in Python, then porting it to native Kotlin (AudioFeatures.kt), including a hand-rolled radix-2 FFT so the app carries no external digital signal processing dependency. We verified numerical parity against the Python reference on the same real clip down to 4.2x10⁻⁷ max error. - Model architecture, SiloSenseNet: a 4-block CNN (Conv2d, BatchNorm, ReLU, MaxPool) with about 98,000 parameters (0.393 MB in FP32). Deliberately sized up from an initial 6K-parameter prototype to give Arm's INT8 vector dot-product engine enough compute payload to show a measurable win.
- Quantization and Arm deployment: static QDQ INT8 quantization, MinMax calibration over 100 real audio clips, cutting model size 3.54x to 0.111 MB with zero validation accuracy loss (98.99% to 100.00%). Deployed on a Samsung Galaxy M16 via ONNX Runtime, configured for NNAPI, then XNNPACK, falling back to plain CPU.

SiloSenseClassifier.kt also enables ONNX Runtime session profiling to see which execution provider actually executes each node, not just which one registers.
| Execution provider | FP32 nodes | INT8 nodes |
|---|---|---|
| XnnpackExecutionProvider | 80 | 70 |
| CPUExecutionProvider | 50 | 100 |
| NNAPI | registered, 0 executed | registered, 0 executed |
DeviceDiagnostics.kt adds real process memory, thermal status, and battery current-draw readings; the last one is read through BatteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) from inside the app process.

Challenges we ran into
The SPID dataset directory structure uses batch export dates rather than true recording dates, causing initial label joins to mislabel files. Parsing session inner logs against aspids_log.csv using true recording timestamps solved this.
We tried using Termux for deployment but we hit WebM decoding issues and a padding bug. We then decided on a native Android build to access low-level microphone APIs directly.
Default dynamic quantization in ONNX Runtime targets MatMul and Gemm nodes, leaving the Conv2d blocks in FP32. We got over this hurdle by applying static QDQ quantization calibrated with real audio samples.
Profiling traces revealed that NNAPI ran zero nodes despite registering successfully, passing all execution work to XNNPACK and CPU. Additionally, static INT8 models loaded 17x slower than FP32 because ONNX Runtime compiles QDQ graphs on cold start.
Reading battery current from /sys/class/power_supply/battery/current_now via adb shell failed with permission errors due to One UI restrictions. The solution was querying Android's BatteryManager API natively inside the application over wireless ADB.
Accomplishments that we're proud of
- 3.47x on-device speedup: static QDQ INT8 quantization dropped average inference latency from 3.71ms (FP32) to 1.07ms (INT8), on a Samsung Galaxy M16, across 50 timed benchmark runs.
- Verified Arm acceleration, not assumed: ONNX Runtime's own profiling trace confirms the convolution nodes ran on XNNPACK's Arm NEON SIMD kernels, not the NPU NNAPI registered for.
- A genuine control test: the same FP32-vs-INT8 comparison on an x86 desktop CPU, no XNNPACK available, yields only a 1.28x speedup (0.226ms to 0.176ms). That gap is what shows the mobile speedup is driven by Arm-specific hardware acceleration and not just quantization alone.
- Real thermal and battery evidence: Android's PowerManager reports NONE for thermal throttling before and after the benchmark. Continuous inference stress-tests the battery down to a measured marginal draw of about 226mA, read through BatteryManager from inside the app process.
- Feature parity to seven decimal places between the Python training pipeline and the on-device Kotlin port.
- The full record-to-result pipeline runs offline in under two seconds, and reports its own uncertainty instead of forcing every borderline reading into a hard clean-or-infested color.
What we learned
- Registration is not execution. An ONNX execution provider like NNAPI registering on a session doesn't mean it ran anything. Profiling the Chrome-trace output was the only way to see that XNNPACK's NEON kernels, not the NPU, did the real work on this device.
- Cold start and steady state pull in opposite directions. INT8 runs 3.47x faster once warm, but its session load time is about 17x slower than FP32 (105.1ms vs 6.2ms), a cost of compiling the QDQ graph on first use.
- One benchmark run isn't a constant. Real passes during development ranged from 1.68x to 4.92x speedup on the same phone, which is why this submission reports a full 50-run benchmark as authoritative rather than a single favorable number.
- A blocked shell command doesn't mean a blocked measurement.
/sys/class/power_supply/battery/current_nowreturns Permission denied over adb shell, but the same read succeeds through Android's BatteryManager API from inside the app process.
What's next for SiloSense
- A multi-device Arm benchmark.
- Calibration against an official grading standard, insects per kilogram or per cent insect-damaged kernels, rather than the heuristic three-tier threshold this build uses.
- Field recordings from real Zimbabwean storage, mud granaries, hermetic bags, brick stores, across maize, sorghum, and groundnuts, to check whether the model generalizes past the SPID/A-SPIDS study conditions it was trained on.
- A warehouse batch mode: scan every sack in a session and get a risk heatmap instead of one result at a time, still entirely on-device.





Log in or sign up for Devpost to join the conversation.