Capture grocery shelf prices with your phone camera. Roboflow AI extracts the data → review & save → sync to Price Scout.
Two interfaces: a mobile app (Expo React Native, iOS App Store ready) and Python CLI scripts for bulk processing and dataset management.
┌─ Mobile App (Expo) ─────────────────────┐
│ │
iPhone Camera ───────▶ │ Camera → Roboflow API → Review → Save │
│ │ │
│ AsyncStorage │
│ │ │
│ Sync to Price Scout │
└─────────────────────────────────────────┘
┌─ Python CLI ────────────────────────────┐
│ │
iPhone Photos ───────▶ │ Convert → Resize → Upload → Roboflow │
(HEIC / MOV) │ │ │
│ Dataset (Universe) │
│ │ │
│ Train → Custom Model │
└─────────────────────────────────────────┘
Image ──▶ Object Detection ──▶ Crop tags ──▶ GPT-4o (structured) ──▶ JSON
(find price tags) (isolate) (extract fields) (output)
| Step | What happens |
|---|---|
| Capture | Take a photo or pick from library |
| Detect | Roboflow object detection finds every price tag |
| Extract | GPT-4o reads each cropped tag → product name, brand, price, sale price, unit, UPC |
| Review | Edit/confirm extracted records before saving |
| Store | Saved on-device (AsyncStorage), organized by store and date |
| Sync | One-tap sync to Price Scout GraphQL API, or export as JSON |
npm install
npx expo startOpen Settings tab → enter API keys → select store → start capturing.
cd scripts
pip install -r requirements.txt
cp ../.env.example ../.env # fill in API keysiPhones shoot HEIC at 5-7MB per image. Uploading these directly to Roboflow is painfully slow and wasteful:
| Issue | Impact |
|---|---|
| HEIC format | Roboflow must decode on server side → slower ingestion, occasional failures |
| 6MB per image | Upload time scales linearly with size → 452 images = 2.5GB = 30+ min |
| Serial uploads | One-by-one API calls → most time spent waiting for network round-trips |
| No deduplication | Re-running uploads creates duplicates |
Our pipeline fixes all of this:
iPhone HEIC/MOV
↓
Convert (ImageMagick / ffmpeg)
↓
Resize to 1280px max, JPEG quality 80
↓
Parallel upload (8 workers)
↓
Roboflow dataset ✓
| Metric | Before (raw) | After (optimized) | Improvement |
|---|---|---|---|
| Image size | 5-7MB each | ~270KB each | 95% smaller |
| Total payload | 2,563 MB | 122 MB | 21x smaller |
| Upload speed | ~6 imgs/min (serial) | ~320 imgs/min (8 workers) | 53x faster |
| 452 images | 30+ minutes | 84 seconds | 21x faster |
| Failures | Occasional HEIC decode errors | 0 failures | 100% clean |
- Roboflow resizes internally for training anyway (typically 640x640)
- 1280px preserves enough detail for price tag text
- Smaller images = faster training, less compute cost
- Better model generalization (less noise from ultra-high-res)
- JPEG quality 80 is visually lossless for training purposes
Auto-resizes and parallel-uploads images to Roboflow. Handles JPG, PNG, HEIC, and optionally extracts video frames.
# Upload a folder of images (auto-resize + 8 parallel workers)
python upload_to_roboflow.py ~/Downloads/roboflow_ready_images/
# Include video frame extraction
python upload_to_roboflow.py ~/Downloads/ --include-videos --recursive
# Custom batch name and more workers
python upload_to_roboflow.py ~/photos/ --batch-name "safeway-apr-4" --workers 10
# Skip resize (upload originals)
python upload_to_roboflow.py ~/photos/ --no-resizeWhat it does:
- Finds all JPG/PNG/HEIC images (and MOV/MP4 with
--include-videos) - Converts HEIC → JPG via ImageMagick
- Resizes to 1280px max dimension, JPEG quality 80
- Uploads with 8 parallel workers (configurable)
- Tracks progress every 25 images
- Skips duplicates (Roboflow rejects them automatically)
- Logs failures cleanly (up to 5 shown)
- Cleans up temp files on exit
Scans ~/Downloads for iPhone HEIC/MOV files, converts, resizes, and uploads in one command.
# Process everything in ~/Downloads
python convert_and_upload_to_roboflow.py
# Custom source directory
python convert_and_upload_to_roboflow.py --source ~/Photos/store-trip
# Process ALL files, not just "* 2.*" pattern
python convert_and_upload_to_roboflow.py --all-filesRun the price extraction workflow on one image.
python run_workflow.py ~/photos/shelf.jpg --store store-safeway
python run_workflow.py https://example.com/shelf.jpg --store store-raleysProcess an entire video through the Roboflow workflow using WebRTC streaming.
python video_inference.py ~/videos/store_walk.mp4 --store store-safeway
python video_inference.py ~/videos/walk.mp4 --store store-raleys --save-framespython grab_prices.py photo.jpg --store store-safeway
python grab_prices.py ./photos/ --store store-grocery-outletpython transform_to_price_scout.py output/capture.json --price-scout-path ../../price-scoutMatches extracted products to existing Price Scout items via UPC (exact) or name+brand (Levenshtein).
If uploads are slow, it's almost certainly because your images are too large. iPhone photos at full resolution are 5-7MB each. Always resize before uploading.
# Quick resize with ImageMagick (if you want to do it manually)
magick mogrify -resize 1280x1280\> -quality 80 *.jpgRoboflow can accept HEIC but has to decode it server-side, which is slower and occasionally fails. Always convert to JPEG first. Our scripts handle this automatically.
The Roboflow API accepts one image per request. Uploading 500 images serially means 500 sequential HTTP requests. With 8 parallel workers, you're making 8 requests simultaneously → 8x throughput.
Don't go above ~10 workers or you'll hit rate limits.
Roboflow rejects images that are already in the dataset. Our scripts detect this and count them as "skipped" rather than "failed." Safe to re-run.
When extracting frames from store walk videos, many consecutive frames are nearly identical. We extract at 1 FPS (not 30 FPS) to avoid flooding the dataset with duplicates. You can adjust VIDEO_FRAME_FPS in the scripts.
All scripts read from .env in the project root. The .gitignore excludes .env and all variants (.env.*, .env copy*). API keys configured in the mobile app are stored in iOS Keychain via expo-secure-store.
price-grabber-roboflow/
├── App.js ← Root: tab navigator + stack nav
├── app.json ← Expo config, iOS permissions
├── eas.json ← EAS Build profiles
├── package.json ← JS dependencies
├── index.js ← Entry point
├── src/
│ ├── constants/
│ │ ├── theme.js ← Colors, spacing, typography
│ │ └── stores.js ← Ukiah store list
│ ├── screens/
│ │ ├── CameraScreen.js ← Camera capture + photo library
│ │ ├── ProcessingScreen.js ← Roboflow API call + progress
│ │ ├── ReviewScreen.js ← Edit/confirm prices before save
│ │ ├── HistoryScreen.js ← Past captures, sync, export
│ │ └── SettingsScreen.js ← API keys, store, sync config
│ └── services/
│ ├── roboflowService.js ← Roboflow Workflow API client
│ ├── captureStorage.js ← AsyncStorage CRUD
│ ├── secureStorage.js ← SecureStore for keys
│ └── syncService.js ← Price Scout GraphQL sync
├── scripts/
│ ├── upload_to_roboflow.py ← Resize + parallel upload
│ ├── convert_and_upload_to_roboflow.py ← HEIC/MOV → resize → upload
│ ├── run_workflow.py ← Single image inference
│ ├── video_inference.py ← Video inference (WebRTC)
│ ├── grab_prices.py ← Original CLI price grabber
│ ├── transform_to_price_scout.py ← Convert to Price Scout format
│ ├── matcher.py ← Fuzzy match to item catalog
│ ├── resize_and_zip.sh ← Manual resize + zip for UI upload
│ ├── convert_selected.sh ← HEIC/MOV shell converter
│ ├── workflow_definition.json ← Importable Roboflow workflow
│ └── requirements.txt ← Python dependencies
├── assets/ ← App icon, splash screen
└── .skills/ ← Apollo GraphQL agent skill
| Data | Storage | Encrypted |
|---|---|---|
| API keys (Roboflow, OpenAI) | expo-secure-store |
✅ iOS Keychain |
| Settings (store, URLs) | expo-secure-store |
✅ iOS Keychain |
| Captures (prices, photos) | AsyncStorage |
❌ (on-device only) |
{
"id": "uuid",
"storeId": "store-safeway",
"storeName": "Safeway",
"imageUri": "file:///path/to/photo.jpg",
"records": [
{
"productName": "Whole Milk, 1 Gallon",
"brand": "Store Brand",
"price": 4.29,
"salePrice": null,
"unit": "gallon",
"upc": null
}
],
"createdAt": "2026-04-03T19:00:00.000Z",
"synced": false,
"syncedAt": null
}Set Price Scout GraphQL URL in Settings → tap Sync on History screen.
Tap Export on History → share via AirDrop/Messages → process with Python:
cd scripts
python transform_to_price_scout.py exported_data.json --price-scout-path ../../price-scoutnpm install -g eas-cli
eas login
eas build --platform ios --profile production
eas submit --platform ios| Field | Value |
|---|---|
| Bundle ID | com.pricescout.grabber |
| App Name | Price Grabber |
| Category | Utilities / Shopping |
| Privacy | Camera, Photo Library |
- 582 images uploaded to Roboflow Universe
- 452 store shelf photos (3 Ukiah stores: Safeway, Raley's, Grocery Outlet)
- 130 video frames from store walk-throughs
- All resized to 1280px max, JPEG quality 80
- Annotate at app.roboflow.com — draw bounding boxes around price tags, class:
price-tag - Generate a dataset version (auto train/valid/test split)
- Train on Roboflow (free on Public plan, ~15-30 min)
- Update workflow to use your model:
your-workspace/find-3stores-shelf-prices/N
- Draw tight boxes around the full price tag (product name + price + any sale info)
- Include all visible tags, even partially obscured ones
- Be consistent across stores — same class for all tag styles
- Aim for 50+ annotated images minimum before first training
- Node.js 18+ (for Expo app)
- Python 3.10+ (for scripts)
- ImageMagick (
brew install imagemagick) — for HEIC conversion - ffmpeg (
brew install ffmpeg) — for video frame extraction - Roboflow account (free Public plan) — app.roboflow.com
- OpenAI API key — platform.openai.com
- Annotate 50+ images → train first custom model
- Add barcode scanner (expo-barcode-scanner) for UPC auto-fill
- Offline queue — retry sync when connection is restored
- Price history charts per item
- Multi-photo batch capture mode
- Auto-upload from mobile app directly to Roboflow dataset