Skip to content

Debanitrkl/bike-analyzer

Repository files navigation

BikeDoc — Garage

Grounded RAG troubleshooting for any bike. Pick a bike, drop in its Owner's Manual / Service Manual / User Guide (or auto-fetch from the OEM), then chat with citations, voice (Sarvam Saaras + Bulbul), and image diagnosis (Gemini Flash). UI inspired by CRED Garage — a rotating 3D model anchors a glassmorphic chat surface.

Disclaimer: Not affiliated with any manufacturer. For informational use only — consult an authorised service centre for repairs. PDFs are downloaded at ingest-time and never redistributed by this project.

What's in v2

  • Multi-bike corpus keyed by bike_id — single Qdrant collection with per-bike payload filtering. Seeded with the top-selling Indian bikes (Himalayan, Classic 350, Pulsar 150, Apache RTR, Activa 6G, Splendor Plus, R15, FZ, Duke 200, Chetak) — extend in backend/app/bikes/registry.py.
  • Two onboarding paths per bike:
    • Drag-and-drop PDF upload, one slot per doc type.
    • Auto-fetch from the OEM URL list, with HEAD-derived size + reachability previews before ingest.
  • CRED-style frontend — dark canvas, animated aurora gradient, frosted- glass panels, rotating 3D bike model via Google <model-viewer> (or a static hero image fallback).
  • PDF.js citation deep-link — every citation chip opens a modal scrolled to the right page with a best-effort bbox highlight.
  • Hinglish code-switch — STT returns language_code, threaded into both the chat call and the next TTS call so the Bulbul speaker auto-matches.
  • Ragas eval harness — 25 hand-authored Q/A pairs, faithfulness gate at 0.85. make eval.

Architecture

flowchart TB
    subgraph CLIENT["Client · Vercel / Next.js"]
        UI[Garage UI<br/>chat · upload · mic · image]
        STORE[(localStorage<br/>activeBike · langPref · messagesByBike)]
        UI <--> STORE
    end

    subgraph API["API · FastAPI on uvicorn"]
        ROUTES["/bikes  /chat  /chat/stream<br/>/voice/* · /pdf/* · /bikes/{id}/upload"]
        DETECT["Lang detect<br/>Devanagari + Roman-Hindi cues"]
        GUARD["Guardrails<br/>verbatim spec check · severity floor<br/>retrieval gate 0.35"]
        ROUTES --> DETECT --> GUARD
    end

    subgraph INGEST["Ingest pipeline (one-time + on upload)"]
        PDF[OEM PDF / upload] --> DOCLING[Docling<br/>page index + markdown tree]
        DOCLING --> CHUNK[Header-aware chunker<br/>800/120 + parent/child]
        CHUNK --> EMB1[fastembed bge-small<br/>384 dim, local ONNX]
        EMB1 --> QD[(Qdrant<br/>single collection<br/>payload-indexed on bike_id)]
    end

    subgraph RETRIEVE["Retrieval"]
        Q[Query] --> BM25[BM25<br/>per-bike cache]
        Q --> EMB2[fastembed embed]
        EMB2 --> DENSE[Qdrant dense search]
        BM25 --> RRF[RRF k=60]
        DENSE --> RRF
        RRF --> RERANK{Cohere<br/>key set?}
        RERANK -- yes --> COHERE[Rerank top-5]
        RERANK -- no --> TOP5[RRF top-5]
        COHERE --> PROMOTE[Promote parent chunks]
        TOP5 --> PROMOTE
    end

    subgraph LLM["LLM"]
        SARVAM["Sarvam-M<br/>response_format=json_object<br/>★ Hinglish primary"]
        OLLAMA[Ollama qwen2.5:3b<br/>format=json · offline fallback]
        SARVAM -. on HTTPError .-> OLLAMA
    end

    subgraph SARVAM_VOICE["Sarvam voice"]
        STT[Saaras v3 STT<br/>★ returns language_code]
        TTS[Bulbul v3 TTS<br/>★ speaker auto-picked by lang]
    end

    GEMINI[Gemini Flash<br/>image diagnosis JSON]

    UI -- HTTPS --> ROUTES
    UI -- mic blob --> STT
    STT -- transcript + lang --> UI
    UI -- image base64 --> ROUTES
    ROUTES -. when image .-> GEMINI
    GUARD --> RETRIEVE
    PROMOTE --> LLM
    LLM --> ROUTES
    ROUTES -- text --> UI
    UI -- text + lang --> TTS
    TTS -- audio --> UI
    INGEST --> RETRIEVE

    classDef sarvam fill:#1a1d33,stroke:#a78bfa,color:#fff
    class SARVAM,STT,TTS,SARVAM_VOICE sarvam
Loading

★ marks the three Sarvam touch-points. See Why Sarvam below for the reasoning.

Layer summary

  • Frontend: Next.js 16, Tailwind v4, Framer Motion, Zustand (persist → localStorage), react-pdf, Google <model-viewer>, react-dropzone.
  • Backend: FastAPI. Endpoints under /bikes/* for registry, status, upload, OEM auto-fetch. /chat, /chat/stream, /voice/* thread bike_id and language_code end-to-end.
  • Ingest: Docling → per-item page index → header-aware markdown chunker (800/120 + breadcrumb + parent/child) → fastembed (bge-small-en-v1.5, 384 dim, local ONNX) → Qdrant with payload indexes on bike_id, doc_type.
  • Retrieval: BM25 (per-bike cached BM25Okapi) + dense → RRF k=60 → top-5, parent chunk promoted to LLM context. Optional Cohere Rerank when COHERE_API_KEY is set.
  • LLM: Sarvam-M primary via response_format=json_object; Ollama qwen2.5:3b as offline fallback (no key needed for local dev).
  • Guardrails: structured JSON schema, retrieval gate at 0.35, verbatim regex validation of part numbers / torque against retrieved chunks, deterministic severity floor (STOP_RIDING keywords can't be downgraded), bike_id-scoped empty-corpus refusal.

Why Sarvam

Sarvam shows up in three places, each chosen because the alternative we tried failed an explicit demo requirement:

1. Sarvam-M as the primary chat LLM

Decision: call api.sarvam.ai/v1/chat/completions with model="sarvam-m", response_format=json_object.

What we tested against it. We ran the same Hinglish prompts through qwen2.5:3b (Ollama, local) and GPT-4o-mini. With a directive "reply in Hinglish if the user wrote Hinglish" — even when restated as the last line of the user message — qwen2.5:3b consistently replied in English. GPT-4o-mini replied in transliterated Hindi but stripped technical English terms ("torque" → "घूर्णन-बल") which a mechanic can't act on. Sarvam-M was the only model that mirrored Romanised Hinglish and kept the English technical vocabulary the manual uses.

Where the boundary is. When the network is down or SARVAM_API_KEY is unset, we fall through to Ollama. The demo still works — it just speaks English. See backend/app/rag/llm.py.

2. Saaras v3 for STT (with language_code return)

Decision: call api.sarvam.ai/v1/speech-to-text with language_code="unknown" so Saaras detects automatically.

What's special. Saaras returns the detected language_code alongside the transcript. Off-the-shelf Whisper returns ISO-639-1 codes that don't distinguish hi-IN (Hindi) from a Hinglish utterance — and Whisper-large is the smallest local variant that even attempts code-mixed Indic. Saaras's language_code is what we thread forward into the LLM and TTS.

3. Bulbul v3 for TTS (speaker auto-picked from language_code)

Decision: call api.sarvam.ai/v1/text-to-speech with target_language_code taken directly from Saaras's detection.

Why this completes the loop. The detected language_code flows: mic → Saaras (returns lang) → /chat (system prompt picks Hinglish style) → /voice/synthesize → Bulbul (picks a matching speaker). The user speaks Hinglish, gets a Hinglish reply, hears it back in a Hinglish voice. No manual language toggle required. This end-to-end code-switch is the design centrepiece — see effectiveLang in frontend/src/components/chat.tsx.

What's novel

The pieces below are either non-obvious or hard to find in public RAG demos:

  1. Language-aware closed loop, automatically. Saaras returns language_code → it threads into the chat request → server prompt picks Hinglish style → Bulbul TTS speaker is auto-matched. No UI lang toggle is required for the demo path. For typed input where Saaras hasn't run, the server sniffs Devanagari script + Roman-Hindi cue words (kya, kaise, hai, chahiye, …) and synthesises the same language_code.
  2. Single Qdrant collection, multi-bike payload-filtered. Instead of N collections, one collection has payload-indexed bike_id/doc_type/manual_id. Adding a bike means appending a row to registry.py — no schema migration, no index creation. Filter cost is O(1) after the payload index is built.
  3. Parent-child chunking with parent promotion. Children (≤400 tok) drive retrieval signal; the parent block (≤800 tok) is what actually lands in the LLM prompt. The model gets H1→H4 breadcrumbs and surrounding context that a child chunk alone would miss.
  4. Verbatim spec guardrail. A regex pass over the LLM output extracts part numbers, torque values, fluid grades. If they don't appear verbatim in the retrieved chunks, confidence is dropped to low and a "specs could not be verified" warning is appended. Stops the model from confidently making up a torque value.
  5. Deterministic severity floor. The model can escalate severity but never downgrade safety-critical issues. A keyword set (brake fluid leak, oil pressure, steering wobble, …) forces STOP_RIDING regardless of model judgment.
  6. Citation deep-link with bbox. Every citation chip opens a react-pdf modal scrolled to the cited page with a best-effort bbox highlight. The user can verify the manual passage in two clicks.
  7. bike_id-scoped refusal, not brand keyword matching. "I don't have this manual" is driven by count_chunks(bike_id, ...) == 0, not by hardcoded brand strings. Add Suzuki Hayabusa to the registry without touching prompts.
  8. Persistent UX state, volatile server state. Active bike + language toggle + per-bike chat history live in localStorage via zustand persist. Bike list + chunk counts are deliberately not persisted — they re-fetch on mount so registry edits and new ingests reflect immediately. See State persistence on /about.
  9. Always-on Documents tab. Bikes that already have manuals can accept more uploads without rebuilding. The handler short-circuits when every requested doc_type is already indexed and force=false, so the second click on "Add all to corpus" returns in <100ms instead of re-running Docling.

Run locally (≈ 7 min, no paid keys required for chat)

The default path is fully OSS: local Qdrant, local fastembed, local Ollama. Sarvam (voice) and Gemini (vision) keys are optional — without them, those endpoints return 503 but chat works end-to-end.

Prerequisites

Need Why
Python 3.11+ backend
Node 20+ frontend
Docker Desktop Qdrant container
Homebrew (macOS) Ollama install
~3 GB free disk qwen2.5:3b model (~2 GB) + fastembed (~150 MB)

Step 1 — clone + env

git clone https://github.com/Debanitrkl/bike-analyzer.git
cd bike-analyzer
cp .env.example .env
cp frontend/.env.local.example frontend/.env.local

Edit .env only if you want voice (SARVAM_API_KEY) or vision (GOOGLE_API_KEY). Everything else has working local defaults.

Step 2 — Ollama + model (one-time)

brew install ollama
brew services start ollama        # daemon on localhost:11434
ollama pull qwen2.5:3b            # ~2 GB

Linux: curl -fsSL https://ollama.com/install.sh | sh then ollama serve &.

Step 3 — Qdrant (Docker)

open -a Docker                    # if not already running
make qdrant                       # docker compose up -d qdrant

Sanity: curl localhost:6333/healthzhealthz check passed.

Step 4 — backend

cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000

In another shell, sanity: curl localhost:8000/health{"status":"ok"}.

Step 5 — seed ingest

# from repo root, .venv still active
make ingest

Pulls OEM PDFs for the bikes that have URLs in registry.py (RE Himalayan + Classic 350), runs Docling → header-aware chunker → fastembed → Qdrant. ~3–5 min the first run; PDFs are cached so re-runs are fast.

Sanity: curl localhost:8000/bikes/royal-enfield-himalayan/statusready: true, total_chunks: ~480.

Step 6 — frontend

cd frontend
npm install
npm run dev

Open http://localhost:3000, pick Royal Enfield Himalayan, ask "What is the recommended front tyre pressure?" — should cite page 7 with 25 PSI / 1.75 kg/cm².

First chat per session waits ~25 s while Ollama loads the model into memory. Subsequent chats are sub-second.

One-liner restart (after first setup)

make qdrant                                          # ensure Qdrant up
brew services restart ollama                         # ensure Ollama up
cd backend && source .venv/bin/activate && uvicorn app.main:app --reload --port 8000 &
cd frontend && npm run dev

Troubleshooting

Symptom Fix
Ollama unreachable in chat brew services restart ollama and curl localhost:11434/api/version
Index required but not found from Qdrant recreate the collection: python -c "from app.rag.store import ensure_collection; ensure_collection(recreate=True)" then re-ingest
Failed to fetch in browser check CORS_ORIGINS in .env matches http://localhost:3000
First chat returns "no relevant context" confirm ingest ran: curl localhost:8000/bikes/<bike_id>/status
Empty bike picker check Network tab — /bikes should return 10 entries; if 0, backend env didn't load

Open http://localhost:3000

7. Eval

make eval        # full Ragas (calls the LLM judge — needs OPENAI_API_KEY)
make eval-fast   # skip Ragas, just retrieval+gen sanity

Adding a bike

  1. Append to BIKES in backend/app/bikes/registry.py. Fill in OEM URLs in sources=(...) if any are publicly hosted.
  2. (Optional) Drop a Draco-compressed GLB at frontend/public/models/<bike_id>.glb and a hero image at frontend/public/images/<bike_id>.webp (see frontend/public/attribution.md for sources).
  3. Restart the backend. New bike shows up in the picker.

API

Endpoint Description
GET /bikes List registry bikes
GET /bikes/{id}/status Per-doc-type chunk counts + readiness
GET /bikes/{id}/sources OEM URL previews (HEAD-checked)
POST /bikes/{id}/ingest-from-urls Download + ingest OEM docs
POST /bikes/{id}/upload Upload PDF (multipart, doc_type form field)
GET /pdf/{bike_id}/{doc_type} Serve the cached PDF (citation modal)
POST /chat / POST /chat/stream Chat (accepts bike_id)
POST /voice/transcribe Saaras v3 STT
POST /voice/synthesize Bulbul v3 TTS, speaker auto-picked from language_code
POST /ingest Legacy: seed-ingest every bike in the registry

Trade-offs (documented for evaluators)

  • Sarvam-M vs GPT-4o-mini: Sarvam-M is primary for Hinglish + brand fit and now uses response_format=json_object so JSON parse failures should be rare. GPT-4o-mini fallback kicks in on parse errors or API failure.
  • Embeddings: Voyage-3-large when keyed; otherwise bge-small-en-v1.5 via fastembed for local dev without Voyage billing.
  • BM25: still Python-side (per-bike BM25Okapi cache). Qdrant native sparse vectors are the right v2 upgrade for production scale.
  • Voice streaming: REST only in v1. WebSocket Saaras + Bulbul is a v2 follow-up for <250 ms partials.
  • 3D models: five Tier-1 CC-BY Sketchfab models, dropped manually in frontend/public/models/. Bikes without a GLB fall back to a static hero image with the same aurora treatment.

Research brief

See sarvam_bike_assistant_research.md for the original opinionated architecture writeup.

Deploy

The default OSS path is local-first — Ollama doesn't run on Render's free tier, so a one-click hosted deploy needs a tweak.

Frontend → Vercel (works as-is):

  1. Import this repo in Vercel, Root Directory = frontend.
  2. Set NEXT_PUBLIC_BACKEND_URL to your tunneled local backend (ngrok / cloudflared) or a hosted backend URL.
  3. Deploy.

Backend — three realistic options:

Option Where Ollama lives Notes
Local + tunnel Your laptop Cheapest for demos. ngrok http 8000 → paste URL into Vercel.
Modal / Fly.io On the same box as FastAPI (GPU optional) Modal has a 1-click Ollama recipe; pay only when running.
Cloud LLM swap None — swap Ollama back for Sarvam-M / OpenAI in app/rag/llm.py Render free tier works. Loses the "free OSS" property.

render.yaml is left in the repo for the cloud-LLM option. For the local-first path it isn't used.

Submission checklist

  • Deploy (Vercel frontend + Render backend)
  • Multi-bike corpus + onboarding flow
  • PDF.js citation deep-link modal
  • Hinglish code-switch end-to-end (STT → LLM → TTS speaker)
  • Ragas eval harness (make eval)
  • 2-min Loom demo
  • 5-slide deck

About

BikeDoc — grounded RAG troubleshooting for any Indian bike. FastAPI + Next.js, Sarvam-M, Qdrant, citations, voice, vision.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors