Synaplan
Open-source AI platform for companies: self-hosted AI agents, GDPR-compliant chatbot widget, RAG knowledge base, WhatsApp and Outlook AI, MCP, multi-model routing. Docker, Kubernetes or hosted in Germ
Residents photo-report overflowing trash bins; city staff see where new bins would help most.
Translation pending
BusinessTypeScript · PythonGitHub @Jackal-HQOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Trash Overflow Planner — residents photo-report overflowing trash bins from a
phone; city staff get a web dashboard that ranks where new bins would help most.
Home city is Orlando, FL; NYC and Chicago prove it ports by config alone.
Hackathon project for the OneEthos "Innovation for Good" track (with Hugging Face,
Databricks, and Neon).Read the full README ↗Trash Overflow Planner — residents photo-report overflowing trash bins from a phone; city staff get a web dashboard that ranks where new bins would help most.
Home city is Orlando, FL; NYC and Chicago prove it ports by config alone. Hackathon project for the OneEthos "Innovation for Good" track (with Hugging Face, Databricks, and Neon).
The core insight: raw 311 counts measure who reports, not where trash is. Bin-It predicts expected overflow per area from objective proxies, compares it to reports per 1,000 residents, flags under-reported areas, and recommends bin placements. All projected changes are shown as estimates, never measured outcomes.
Mobile (Expo) --\
>--> API (Next.js route handlers on Vercel) --> Neon Postgres (PostGIS, pgvector, branches)
Web (Next.js) --/ |--> Photo classifier (FastAPI + CLIP, Docker via tunnel)
|--> Hugging Face Inference (small LLM, recommendation rationales)
'--> Neon Object Storage (photos)
Databricks (batch) --> CSV export --> db/load_gold.py --> Neon
Clients only ever talk to the API. They never touch the database, storage, classifier,
or Databricks directly. Databricks outputs reach Neon as exported CSV loaded by
db/load_gold.py.
| Part | Tech |
|---|---|
| Web dashboard + API | Next.js (App Router, v16) + TypeScript — apps/web |
| Map / heatmap | MapLibre (react-map-gl), deck.gl H3HexagonLayer + h3-js |
| Mobile | Expo SDK 57 (React Native) + Expo Router, runs in Expo Go — apps/mobile. Nearby map: MapLibre GL JS + OpenFreeMap tiles in a WebView (no map API key) |
| Database | Neon Postgres + PostGIS + pgvector |
| Photo storage | Neon Object Storage (S3-compatible), bucket report-photos |
| Classifier | FastAPI + zero-shot CLIP (openai/clip-vit-base-patch32, 512-dim) — classifier |
| LLM rationales | huggingface_hub Inference Providers (numbers-only prompt) |
| Batch modeling | Databricks (H3 res 9, MLflow) — data/notebooks (planned) |
Everything is TypeScript except classifier/, db/*.py, and data/*.py (Python).
apps/web/ Next.js dashboard pages/components + app/api route handlers
apps/mobile/ Expo app: Report + Nearby tabs, ticket/report detail
classifier/ FastAPI + CLIP container (Dockerfile, app.py, requirements.txt)
db/ migrations/001_init.sql, seed.py, load_gold.py, reset_demo.sql
data/ cities/*.yaml, test_photos/ (+labels.csv), eval.py, fetch_city.py
scripts/ test_upload.py, smoke.sh (smoke planned)
docs/api.md the authoritative API contract
mocks/ sample JSON, one file per endpoint
requirements.txt Python deps for data/*.py and db/*.py (the classifier has its own)
.claude/CLAUDE.md full project brief (source of truth for intent)
KIRO.md operating guide for the Kiro CLI agent
The contract in docs/api.md and the samples in mocks/ are
authoritative. Endpoints:
| Endpoint | Used by | Purpose | State |
|---|---|---|---|
GET /api/cities | both | cities with center, bbox | live |
POST /api/reports | mobile | multipart photo report → classified, maybe merged ticket | live |
GET /api/reports/nearby | mobile | tickets + bins around a point or inside a bbox | live |
GET /api/reports/{id} | mobile | one report | live |
GET /api/tickets | web | queue; filter by city/status/label | live |
PATCH /api/tickets/{id} | web | change status (staff key) | live |
GET /api/health | both | database + classifier status | live |
GET /api/cells | web | per-cell scores keyed by H3 id | live |
GET /api/recommendations | web | ranked new-bin sites with rationale | live |
POST /api/scenarios, DELETE /api/scenarios/{id} | web | what-if on a Neon branch | live (Neon branch, falls back to a calculated result) |
Conventions: JSON everywhere; errors are {"error": {"code": "...", "message": "..."}};
every call takes city=<slug> (orlando, nyc, chicago). Mobile sends an anonymous
X-Device-Id; staff endpoints need X-Staff-Key (env STAFF_KEY; PATCH returns 401
when it is unset). No accounts, ever. Set USE_MOCKS=1 to make route handlers serve files
from mocks/; set it to 0 for real data.
A new report merges into an existing ticket when it is in the same city, within 50 m, from
the last 7 days, on a ticket that is not resolved, and its photo embedding has cosine
similarity ≥ 0.85. Reports the classifier could not label (unclassified) never merge.
cd apps/web
npm install
npm run dev # http://localhost:3000, serves mocks when USE_MOCKS=1
psql $env:DATABASE_URL -f db/migrations/001_init.sql
The schema (db/migrations/001_init.sql) creates cities, tickets, reports
(with embedding vector(512) and is_synthetic), bins, cell_scores, and
bin_candidates, plus GiST and HNSW indexes.
School / venue wifi blocks outbound TCP 5432.
psql,db/seed.py, anddb/load_gold.pyfail there. Run the migration in the Neon Console SQL Editor and run the Python scripts from a hotspot. The Vercel-hosted API is unaffected.
docker build -t trash-classifier classifier/
docker run -d --name classifier -p 7860:7860 -e CLASSIFIER_KEY=<secret> --restart unless-stopped trash-classifier
cloudflared tunnel --url http://localhost:7860 # or: ngrok http 7860
The classifier (classifier/app.py) uses CLIP zero-shot classification: each label is
an average of several prompt phrasings, label and severity are scored independently, and
clean/not_trash are pinned to severity 0. POST /classify returns label,
label_confidence, severity (0-3), and the 512-dim embedding.
Use Python 3.12 (newer versions often lack prebuilt wheels for psycopg[binary] and torch).
py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txt # data/*.py and db/*.py (requests, boto3, psycopg, ...)
python data/eval.py # classifier accuracy on the labeled photos
db/seed.py and db/load_gold.py read .env.local from the repo root themselves
(python-dotenv), so run them from the repo root. Add pip install -r classifier/requirements.txt
only if you want to run CLIP locally.
python db/seed.py # needs the classifier tunnel up + storage and DATABASE_URL env vars
Loads the three cities from data/cities/*.yaml, then creates demo reports from
data/test_photos/labels.csv, all flagged is_synthetic: Orlando gets every photo in 8
clusters, NYC and Chicago get every third photo in 6 clusters each. Each photo is uploaded
and classified once. It is safe to rerun (it clears its own device_id = 'seed' rows first).
Run db/reset_demo.sql in the Neon SQL Editor. It deletes reports sent from the device ids
test and demo, removes tickets left empty, and recomputes ticket counts, severity and
status, so the seeded baseline comes back. Reports made from a phone use a random device id,
so add that id to the list in the file if you want them removed too.
python db/load_gold.py orlando # then nyc, chicago
Reads data/gold/<slug>/cell_scores.csv, bins.csv and bin_candidates.csv and replaces that
city's rows in one transaction (a bad file changes nothing). Required columns:
| File | Columns |
|---|---|
cell_scores.csv | h3_cell, lat, lng, expected_overflow, reports_per_1k, under_reported, demand |
bins.csv | lat, lng, source (osm or city) |
bin_candidates.csv | rank, lat, lng, gain, nearest_bin_m, nearby_reports |
lat/lng are the cell centroid for cells. Rerunning clears bin_candidates.rationale, so
rerun python data/explain.py <slug> afterwards (see below). Mobile only shows tickets and bins; cell scores and
recommendations are dashboard features.
python data/explain.py orlando # or: all
python data/explain.py orlando --no-llm # template sentences only
Fills bin_candidates.rationale for every site. The top 20 sites per city get a one-sentence
explanation from a small Hugging Face chat model (HF_TOKEN, optional HF_MODEL); the prompt
contains only numbers from the database (nearest bin distance, nearby reports, where the cell's
estimated overflow ranks in the city, under-reported flag). Other sites, and any LLM failure,
get a template sentence from the same facts. Existing rationales are kept unless --force.
cd apps/mobile
npm install
copy .env.local.example .env.local # set EXPO_PUBLIC_API_BASE to the deployed API URL
npx expo start -c # add --tunnel only if wifi blocks phone-to-laptop traffic
The app targets Expo SDK 57, so use the current Expo Go from the store. See
apps/mobile/README.md for screens, how the map works and
troubleshooting.
In PowerShell,
curlis an alias forInvoke-WebRequest; usecurl.exefor the curl examples indocs/api.md.
Secrets live in .env.local and are never committed. Keys: DATABASE_URL,
NEON_API_KEY, NEON_PROJECT_ID, HF_TOKEN, CLASSIFIER_URL, CLASSIFIER_KEY,
STORAGE_ENDPOINT, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY,
STORAGE_BUCKET, AWS_REGION, PHOTO_BASE_URL, STAFF_KEY, USE_MOCKS. Mobile:
EXPO_PUBLIC_API_BASE (public, inlined into the app bundle). Web: NEXT_PUBLIC_MAP_STYLE_URL,
optional NEXT_PUBLIC_USE_CACHE. apps/web/.env.local serves the Next.js app and the Python
scripts read the repo-root .env.local; keep both in sync. Restart the dev server after
changing either.
NEON_API_KEY and other secrets must never appear in web or mobile client code —
only API routes use them.
No accounts and no names. Photos may show passers-by and are served from public URLs,
so test photos must not contain identifiable faces or license plates. EXIF is stripped
(except coordinates) before any photo is published. Seeded/demo data is flagged
is_synthetic and labeled as "demo data" in the UI — synthetic data is never presented
as real.
| Step | State |
|---|---|
| Neon project, schema, bucket | done: schema loaded, photos upload to the bucket |
| Mock API | done: handlers serve mocks/ behind USE_MOCKS, deployed on Vercel |
| Classifier container + tunnel | done: CLIP zero-shot behind a tunnel (84% label accuracy and 59% exact severity on our 119 photos at the last eval; prompts retuned since, re-measure after rebuilding the container) |
| Seed data | done: Orlando, NYC and Chicago demo reports (db/seed.py), flagged synthetic |
| Real API handlers | partly done: cities, health, reports, reports/nearby, reports/{id}, tickets, tickets/{id} are live; all handlers live |
| Databricks pipeline + gold load | loader written (db/load_gold.py); notebooks and CSV exports not in the repo yet |
| Mobile app | done: report flow, nearby map and list, ticket and report detail |
| Web dashboard | in progress (see apps/web) |
| Smoke tests, cache mode, recorded demo | planned (scripts/smoke.sh) |
See .claude/CLAUDE.md for the full brief and build gates.
MIT — see LICENSE.