Automated sales-call intelligence for FitNova. Transcribes, diarises, scores, and flags issues in advisor calls — surfaced via role-based dashboards with a human-in-the-loop feedback mechanism.
Live Demo: fitnova-dashboard.onrender.com — Login: director@fitnova.in / admin123
Video Walkthrough:
# 1. Install dependencies
pip install -r fitnova-call-intel/requirements.txt
# 2. Seed DB & process sample calls
python fitnova-call-intel/scripts/run_demo.py
# 3. Start API (Terminal 1)
uvicorn fitnova.call-intel.fitnova.api.main:app --reload
# 4. Start dashboard (Terminal 2)
streamlit run fitnova-call-intel/fitnova/dashboard/app.pyDemo logins: director@fitnova.in / admin123 — sees org-wide scores and all teams.
flowchart LR
A[Call Source<br/>Folder / CRM / Telephony] --> B[Ingestion Layer<br/>Source-Agnostic Adapters]
B --> C[Transcription +<br/>Diarisation<br/>AssemblyAI]
C --> D[PII Redaction<br/>spaCy NER + Regex]
D --> E[Analysis Engine<br/>Claude Structured Output]
E --> F[Storage<br/>SQLite + SQLAlchemy]
F --> G[FastAPI + Cache]
G --> H[Streamlit Dashboard<br/>3 Role Views]
H --> I[Feedback Loop<br/>Contest -> Review]
I --> F
-
Ingestion (
fitnova/ingestion/) — Source-agnostic viaCallSourceabstract base.FolderSourcereadsdata/incoming/as.mp3/.wav+.jsonmetadata pairs. CRM and Telephony stubs exist with docstrings describing real integration. Switching telephony vendors = write a new class with 2 methods. -
Transcription + Diarisation (
fitnova/pipeline/transcribe.py) — AssemblyAI withspeaker_labels=True, language auto-detection (handles English and Hinglish). Maps labels A/B to "advisor"/"customer" using first-speaker heuristic. Poor diarisation fallback: mono audio -> all segments "unknown", flagged on Call row. -
PII Redaction (
fitnova/analysis/redactor.py) — Two-layer approach: (a) spaCy NER (PERSON, GPE, ORG, EMAIL, PHONE, MONEY, etc.) + (b) regex patterns for Indian identifiers (Aadhaar, PAN, phone, email, pin-code). Also leverages AssemblyAI's built-in server-side PII redaction (person_name, phone, email, location, banking, credit_card). Excluded words list prevents false positives on domain terms. -
Analysis Engine (
fitnova/analysis/tagger.py) — Claude with tool-use structured output. Closed set of 7 allowed tags, 5 scoring dimensions (1-5). Anti-hallucination guardrail: every tag'squoted_linemust appear verbatim in transcript — dropped otherwise. -
Storage (
fitnova/storage/) — SQLite with SQLAlchemy. Org -> Team -> Advisor -> Call -> (Segments, Scores, Tags, Contests). Idempotent viaaudio_hash(SHA-256). -
Surfacing (
fitnova/dashboard/app.py) — Streamlit: Sales Director (org-wide), Team Leader (drill-down + contest review), Advisor (own calls + contest). -
Feedback — Advisors contest flags -> Team Leader upholds/dismisses. Creates audit trail.
| Dimension | 1-5 Scale |
|---|---|
| Needs Discovery | Did advisor ask about goals/budget/constraints before pitching? |
| Product Knowledge | Accurate, specific program/pricing/policy knowledge |
| Objection Handling | Addressed concerns without overpromising |
| Compliance | No false claims, no pressure tactics, proper consent |
| Next-Step Booking | Specific trial session booked with time/logistics |
Overall score = average of all 5 dimensions.
| Tag | Default Severity | Description |
|---|---|---|
no_needs_discovery |
high | Pitched without understanding goals |
over_promising |
high | "Guaranteed results", unrealistic promises |
pressure_tactics |
high | Limited-time offers, false urgency |
price_before_value |
medium | Discount/pricing before value established |
undisclosed_costs |
high | Hidden fees mentioned late |
weak_trial_booking |
medium | No specific trial booked |
talking_over_customer |
low | Interrupting or dominating conversation |
| Case | Handling |
|---|---|
| Poor diarisation | Mono/low-quality audio -> all segments tagged "unknown", diarization_quality="failed" on Call row |
| Non-sales calls | is_sales_call classification -> scoring/tagging skipped, status = non_sales_call |
| Code-switching (Hinglish) | AssemblyAI language auto-detection; prompts mention Hinglish; demo includes Hinglish sample call |
| PII (redaction) | Two-layer NER + regex redaction on transcripts and analysis output; AssemblyAI server-side redaction at ingestion |
| Hallucinated tags | Verbatim-quote verification drops non-matching tags |
| Duplicate processing | SHA-256 audio hash + unique constraint -> idempotent |
| API failures | Retry support in orchestrator (catches exceptions, sets status="failed" with error log, re-raises) |
| Component | Status | Notes |
|---|---|---|
| Folder ingestion | Real | Reads data/incoming/ pairs |
| CRM/Telephony sources | Stub | Interface defined, raises NotImplementedError with docstring |
| Transcription (AssemblyAI) | Real when API key set | Falls back to script-parsing stub |
| Language detection | Real | Auto-detects English and Hinglish via AssemblyAI |
| PII redaction (NER) | Real | spaCy en_core_web_sm + regex patterns |
| PII redaction (AssemblyAI) | Real | Server-side, before transcript returned |
| Analysis (Claude) | Real when API key set | Falls back to rule-based stub |
| Anti-hallucination guardrail | Real | Verbatim-quote verification in both modes |
| Storage / DB | Real | SQLite + SQLAlchemy |
| FastAPI | Real | Full REST surface (12 endpoints) |
| Rate limiting + queue | Real | Sliding-window, 202 queue fallback |
| Response caching | Real | In-memory TTL cache |
| Dashboard | Real | Streamlit, 3 role views, contest workflow |
| Contest workflow | Real | Creates Contest row, updates Tag status |
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/health |
GET | No | Health check |
/auth/login |
POST | No | Login, returns JWT |
/auth/register |
POST | No | Register new user |
/auth/me |
GET | Yes | Current user info |
/orgs/{id}/summary |
GET | Yes* | Org-wide scores + teams |
/teams/{id}/summary |
GET | Yes* | Team scores + advisors |
/advisors/{id}/summary |
GET | Yes* | Advisor scores + calls |
/calls/process |
POST | Yes | Process a call (202 if rate-limited) |
/calls/{id} |
GET | Yes* | Full call detail (cached) |
/tags/{id}/contest |
POST | Yes | Contest a tag |
/incoming/list |
GET | Yes | List unprocessed files |
/tasks/{id} |
GET | Yes | Poll async task status |
*Role-scoped: SD sees all, TL sees own team, Advisor sees own calls.
| Decision | Why | When to Revisit |
|---|---|---|
| SQLite for MVP | Zero config, fast for demo | When concurrent writes exceed 10/min -> PostgreSQL |
| In-process async queue | No infra needed for demo | When queue depth exceeds 50 -> Celery + Redis |
| First-speaker heuristic | Simple, no voice data needed | When inbound calls are common -> voice-print or CRM caller-field |
| Two-layer PII redaction | spaCy NER catches names/locations; regex catches Indian IDs; AssemblyAI acts as guard at ingestion | If false-positive rate is high -> fine-tune excluded words or use Presidio |
| Language auto-detection | AssemblyAI handles 99+ languages; no hardcoded language | If accuracy for a specific language is poor -> explicit language_code |
-
High-volume concurrent processing — SQLite + in-process queue would collapse above ~50 calls/hour. Queued tasks lost on restart. Fix: Celery + PostgreSQL.
-
Inbound calls — First-speaker heuristic mislabels the advisor if customer speaks first. Fix: voice-print or CRM caller-field.
-
Extreme code-switching — While AssemblyAI auto-detects mixed language, scoring prompts may miss subtle compliance issues in Hindi-dominant segments. Fix: bilingual prompts with Hinglish few-shot examples.
-
Stereo vs mono — AssemblyAI handles both, but stub parser assumes text format. Non-text audio files with no API key would fall through to hardcoded demo transcript.
-
Memory pressure on cache — In-memory TTL cache grows unbounded. Fix: Redis or capped-LRU cache at scale.
-
Token expiry with no refresh — JWT expires but no refresh endpoint. Fine for demo, annoying in production. Fix: add
/auth/refresh.
