The problem
A camera app for blind and low-vision people that says out loud what is in front of the phone: objects, printed text, mail and bills, and a short description of a scene. Everything has to run on the phone. No account, no server, nothing uploaded.
Constraints
- On-device only, iPhone and Android, including budget Android phones.
- Memory. A comment at
ios/CameraPreview.swift:375notes the camera daemon holding about 1.5 GB. On 6 GB iPhones, the vision language model plus the camera got the app killed by jetsam. - Heat and battery.
ThermalManagerinios/CameraViewModel.swiftreacts toProcessInfo.thermalState, and frame rate and input size come from a device tier (10, 15 or 20 fps; 352, 512 or 640 px input). - A Neural Engine bug on iOS 17.3.x.
YOLOv8Processor.inituses.cpuAndNeuralEnginebut falls back to.cpuAndGPUon that release (ios/YOLOv8Processor.swift:104-112). - Accessibility first: VoiceOver and TalkBack handoff, and numbers formatted for speech.
What he built
The models are upstream; the pipeline around them is his.
- Detection. Ultralytics YOLOv8n trained on Open Images V7 (601 classes), converted to Core ML on iOS and TFLite on Android.
YOLOv8Processor.swiftdoes its own decode with a dynamic threshold, context boosts, NMS and conflict resolution.MetalImageResizer.swiftletterboxes to 640x640 on Metal with a reused pixel buffer pool.ObjectTracker.swiftsmooths boxes across frames. - "What's this?" narrator. A LoRA fine-tune of Qwen3.5-0.8B, 4-bit, running through MLX Swift (
MLXVLM) inOnDeviceVisionNarrator.swift, with a frozen prompt and the comment "Do not edit without retuning." On Android the same model runs through llama.cpp over a JNI bridge (cpp/narrator_jni.cpp) that probes each GPU and leaves a crash marker so the next launch falls back to CPU. - Guards around a small model.
FrameQualityGate.swiftscores a 256 px grayscale thumbnail (Laplacian variance, brightness, clipping) before inference, designed so it "can only ever skip a scan, never block one by mistake."MoneyCrossCheck.swiftcompares dollar amounts from the model against Apple Vision OCR and hedges when they disagree, because "A small vision model can read '$128.40' as '$126.40'." - Help Me Aim. Uses prompt-free YOLOE (4,585 classes) to talk a user into framing a shot. The steering logic in
HelpMeAim.swiftis pure box arithmetic so it can be unit tested without a camera. - Model delivery.
VisionModelStore.swiftverifies size and sha256, then swaps the model in atomically.
Upstream credit: Ultralytics YOLOv8 and YOLOE, Qwen3.5-0.8B, MLX Swift and mlx-swift-lm, llama.cpp, Apple Core ML, Vision, AVFoundation and ARKit, Google TFLite and ML Kit.
Hard bugs
- Frames skipped forever (521e1ff).
predictWithThermalLimitsgated on a counter that only advanced insidepredict(). After one skipped frame, every later frame was skipped. - Jetsam during inference (54e90d3, d40ea46). The camera session now pauses while the model runs, and the MLX Metal cache is capped at 20 MB.
- Upstream crash (e432ad8). mlx-swift-lm kept M-RoPE position ids between calls, crashing the first request. Fixed by moving to mlx-swift-lm 3.31.4.
- YOLOE boxes 2 to 4x too tall (0355486). The Core ML output's strides were not a plain 4x8400 layout, so y, w and h came from a neighboring anchor. Reads now go through the real strides and match a Mac reference.
Results (from the repo)
- Android narrator on a Helio P35: 314 s down to 87 s after showing the model a 512 px copy of the photo (commit 36c7510; android/NARRATOR.md).
- YOLOv8 on an MT6765: about 1630 ms CPU fp16, 870 ms GPU fp16, 690 ms GPU fp32 (
YoloDetector.kt:24-26). - Tests: 148 Swift Testing
@Testcases, 36 XCUITests, 100 Android@Test. No CI config. - Shipped on the App Store and Google Play.
- The narrator that ships is Qwen3.5-0.8B: the bundled model in the iOS 1.3.0 build 30 archive has a 1024-wide, 24-layer text config and a model card naming Qwen/Qwen3.5-0.8B as base, and the Android 1.17 GGUF names Qwen3.5 0.8B.
What it shows
He takes models from research checkpoints to shipped phone apps on two platforms, and handles the parts that decide whether that works: Neural Engine quirks, memory limits, thermal tiers, tensor layouts, and guarding a small model's mistakes with cheaper classical checks.