Knowledge as of Oct 26, 2026
The Inference TimesReference architectures, tutorials, demos and free skills — by Nitin Goswami
Epoch III · Step 16Ask the AI Nitin →

DESIGN DOCUMENT · voice-insight-template

Voice Insight: turn call recordings into grounded, quote-checked insight

An open-source reference pipeline for speech-to-text followed by LLM inference, with every model claim checked against the transcript before it is shown. The worked example is call-centre quality review; each stage maps to the CoALA framework.

Stack
Next.js 16 · AssemblyAI · zod · Tailwind CSS 4 · Vitest
Models
grok-4.5 · AssemblyAI default speech model
Date
October 19, 2026
Status
Approved · built
▶ Try it livevoice-insight.nitingoswami.io

Purpose & scope

The app is a learning template for the voice-to-insight pattern. Speech-to-text acts as a grounding action: it turns audio into words with speakers and timings. Those words, the rubric and the rules are placed in the model's working memory (the prompt). The rubric and rules are the agent's procedural memory. The LLM performs a reasoning action and proposes claims, each with a quote. The verifier is the decision procedure: it evaluates every proposal against the transcript and keeps only the grounded ones. The vocabulary follows CoALA (after Sumers et al., 2023).

POST /api/transcribeAudio → words · speakersGrounding action (perception)
formatTranscript()Timestamped transcript in promptWorking memory
QUALITY_GUIDE · SYSTEM_PROMPTRubric + rulesProcedural memory
structuredCall()Claims with quotesReasoning action
verifyPoints()Keep grounded, drop the restDecision procedure (evaluate → select)

In scope

  • Transcribe an uploaded call in any of the provider's languages, with language detection and two-speaker diarization.
  • Compute talk share, silence, overtalk, monologue length, speaking rate and first-response time from word timings, with no model.
  • Score the call against a nine-category rubric with structured JSON output, in the language it was spoken.
  • Verify every quote for text, time (±5 s) and speaker; drop and count failures; withhold the score when nothing survives.
  • Play the original audio in the browser and seek to any quote.
  • Run as a private demo (access code), a public demo with a capped upload size, or fully open for local development.

Out of scope

Persistence of any kind (no database, history or accounts), files over 4.5 MB, more than two speakers, a human review loop that feeds corrections back into the rubric, streaming output and multi-tenant access control. Section 10 lists these as follow-ups.

Use cases

Visitor Voice Insight · /demo Analyse a call Inspect evidence View sample output Unlock larger uploads AssemblyAI API xAI API transcribe analyse
Use case diagram: a visitor analyses a call, inspects the evidence, views a free sample, or unlocks larger uploads.
IDUse caseBehaviour
UC1Analyse a callChecks the file size against the visitor's limit, relays the audio to AssemblyAI, polls until the transcript is ready, then returns a score, category scores, strengths and coaching points, each with a verified quote, plus measured metrics. About 50 s for a 10-minute call.
UC2Inspect evidenceClicking a quote, a timeline bar or a transcript line seeks the audio to that moment. The Provenance panel lists every dropped claim and its reason.
UC3View sample outputShows a built-in example result, including one discarded claim, under a banner. No API is called.
UC4Unlock larger uploadsIn public mode, a valid access code sets a signed cookie that lifts the upload cap from PUBLIC_MAX_MB to 4.5 MB. In private mode, the code is required to open /demo at all.

Architecture

There is one runtime part: a Next.js 16 app. Its browser side holds the audio file (played from an object URL), the job id and the result. Its server side is three stateless API routes plus an optional access gate in proxy.ts. Two third-party APIs do the heavy work: AssemblyAI for speech-to-text and xAI for the LLM. There is no database, storage bucket, queue or webhook.

POST /api/transcribe · GET /api/status (poll) POST /api/insight Call recording MP3 · M4A · any language AssemblyAI · ASR language · diarization Transcript words · speakers · ms Prompt builder rubric + rules + transcript LLM · grok-4.5 JSON-schema output audio words lines ask Quote verifier found · on time · speaker Verified insight kept claims + drop count Metrics · no model talk share · silence claims + quotes word stream = ground truth timings kept Results page (/demo) claims with quotes · click a quote → audio seeks verified claims measured object URL
Pipeline: audio is transcribed, the LLM judges the transcript, and the verifier checks every quote against the words before anything is shown.
Browser · React Analyzer <audio> · object URL UnlockForm Next.js 16 · :3000 · Vercel Functions proxy.ts · optional access gate /api/transcribe /api/status /api/insight /api/access providers/asr-assemblyai.ts analysis/run.ts access.ts metrics.ts llm.ts verify.ts AssemblyAI API xAI API fetch /api/* POST code gated if code set open upload · submit status fetchResult analyseCall check code metrics prompt verify HTTPS /v2 json_schema
Component diagram: the browser, the API routes behind proxy.ts, the analysis modules, and the LLM path to xAI.
Visitor's machine Browser audio file + job id Vercel project Firewall · WAF per-IP rate limits proxy.ts access gate (optional) Fluid Compute · Node 24 API routes localhost:3000 in dev AssemblyAI api.assemblyai.com/v2 xAI api.x.ai/v1 HTTPS allowed passed REST OpenAI API
Deployment view for the hosted demo on Vercel, with firewall rate limits in front of the access gate.

Key flows

4.1 Transcription: upload → relay → poll

The browser posts the raw bytes. The route checks the size against the visitor's limit, relays the audio to AssemblyAI's upload endpoint, and starts a job with language_detection, speaker_labels and sentiment_analysis. The browser keeps the job id and polls every 3 s, so the server stays stateless.

Visitor Analyzer /api/transcribe /api/status AssemblyAI 1 choose audio file 2 size ≤ maxBytes? 3 POST audio bytes 4 cap check (cookie) 5 POST /v2/upload 6 upload_url 7 POST /v2/transcript 8 job id 9 { id } loop · every 3 s until ready 10 GET ?id= 11 GET /v2/transcript/id 12 status 13 working | ready
Sequence: transcription with polling (CoALA grounding action).

4.2 Analysis: fetch → prompt → infer → verify

Analyzer /api/insight AssemblyAI run.ts xAI · grok-4.5 verify.ts 1 POST { id } 2 fetchResult(id) 3 words · utterances 4 guessAgentSpeaker 5 analyseCall(…) 6 rubric + rules + transcript 7 chat.completions 8 JSON insight 9 zod safeParse 10 verifyPoints 11 kept + dropped 12 computeMetrics 13 insight · metrics · stats 14 200 + transcript
Sequence: one analysis (working memory → reasoning → decision procedure).

4.3 Verification: locate → time → speaker

Each quote is fuzzy-matched against the word stream (normalised Levenshtein similarity ≥ 0.85), then the nearest occurrence must be within TIMESTAMP_TOLERANCE_MS = 5000 of the claimed time, then the diarized speaker must match. A kept quote is snapped to the transcript's own timings. If claims were made and none survived, the overall and category scores are withheld.

Locate quote fuzzy match ≥ 0.85 Check time nearest within ±5 s Check speaker diarized label Kept snapped to real timings Dropped and counted reason shown under Provenance found on time match quote_not_found timestamp_mismatch speaker_mismatch
State diagram: one model quote through the verifier (CoALA decision procedure).

4.4 Analyzer lifecycle

idle uploading transcribing analyzing done Analyse job id ready 200 OK View sample output · no API call error · message shown choose another file
State diagram for the Analyzer panel.

Backend design

«interface» AsrProvider +upload(audio, type) +submit(audioUrl) +status(id) +fetchResult(id) assemblyAi +name -call(path) analysis/run.ts +analyseCall(params) AnalysisResult +model +insight +metrics +verification analysis/prompt.ts +SYSTEM_PROMPT +formatTranscript() +buildUserPrompt() builds analysis/schema.ts +insightSchema +insightJsonSchema() +toInsight(raw) validates providers/llm.ts +structuredCall(params) -getClient() calls verify.ts +normalize(text) +locateQuote() +verifyPoints() +combineStats() checks metrics.ts +computeMetrics() +guessAgentSpeaker() measures implements returns
Class diagram: analysis modules, the ASR interface and the result model.
FileResponsibility
src/app/api/transcribe/route.tsReads the body, enforces uploadLimitFor(cookie), uploads to AssemblyAI and submits the job. maxDuration = 60.
src/app/api/status/route.tsReturns working, ready or failed for a job id.
src/app/api/insight/route.tsFetches the finished transcript, guesses the agent speaker, runs analyseCall. maxDuration = 300.
src/app/api/access/route.tsPOST exchanges the code for an HMAC cookie (7 days); DELETE clears it.
src/proxy.tsRedirects or returns 401 for /demo and the three API routes when the gate is active.
src/lib/access.tsGate state, public mode (PUBLIC_MAX_MB), per-visitor upload limit, HMAC tokens, constant-time compare, open-redirect guard.
src/lib/providers/asr.ts · asr-assemblyai.tsThe AsrProvider interface and its AssemblyAI implementation, normalised to words, utterances, language and sentiment.
src/lib/providers/llm.tsstructuredCall(): OpenAI SDK against XAI_BASE_URL (default https://api.x.ai/v1), temperature: 0, response_format: json_schema with strict: true.
src/lib/quality-guide.tsThe nine rubric categories; drives the prompt, the schema enum and the chart.
src/lib/analysis/prompt.tsSYSTEM_PROMPT, formatTranscript() ([start-end] A [agent]: text) and buildUserPrompt().
src/lib/analysis/schema.tszod schema for the model's answer, converted to JSON Schema; toInsight() maps it to app types.
src/lib/analysis/run.tsOrchestrates prompt → model → parse → verify → metrics, and withholds scores when nothing is verified.
src/lib/verify.tsScript-aware normalisation, quote location, time and speaker checks, drop reasons and stats.
src/lib/metrics.tsDeterministic call metrics and the first-speaker heuristic.

System prompt

The user prompt adds the detected language, the duration, the rubric categories and the transcript in its original language, one [start_ms-end_ms] line per utterance.

API contract

MethodPathRequestResponse
POST/api/transcriberaw audio body, content-type audio/*{"id"}
GET/api/status?id=n/a{"state":"working"} · {"state":"ready"} · {"state":"failed","error"}
POST/api/insight{"id"}{"transcript":{"utterances","words","sentimentSegments","languageCode","languageConfidence","durationMs","agentSpeakerLabel"},"model","insight","metrics","verification"}
POST/api/access{"code"}{"ok":true} + Set-Cookie: voice_insight_access
DELETE/api/accessn/a{"ok":true}, cookie cleared

Errors: 400 for an empty body, a missing id or invalid JSON; 401 for a missing access cookie on a gated route or a wrong code; 409 when posting a code to a deployment with no ACCESS_CODE; 413 for a file over the visitor's limit; 422 when no speech was detected; 500 with a readable error for provider or schema-validation failures.

Data model

Nothing is persisted, so there is no schema to migrate. The shapes live in TypeScript (src/lib/analysis/types.ts, src/lib/providers/asr.ts), and the model's half is enforced by the zod schema in src/lib/analysis/schema.ts. One /api/insight response is the whole record, and it lives only in the browser tab.

Transcript languageCode detected durationMs agentSpeakerLabel a guess utterances[] turns Word text startMs · endMs confidence 0–1 speaker "A" / "B" Insight summary outcome resolved… score null = abstain scoreBasis Point point one sentence category rubric key Evidence quoteOriginal verified quoteEnglish gloss only startMs · endMs snapped speaker asrConfidence mean CategoryScore key rubric key score null = abstain note VerificationStats checked count DroppedQuote point reason 3 kinds quote has scores strengths · coaching cites matched to records
Entity-relationship diagram of the /api/insight response.

Frontend design

layout.tsx fonts · theme boot · limits Masthead stages · status · ThemeToggle demo/page.tsx server · reads cookie DemoFooter design doc · source · licence Analyzer file · stage · result · selected API routes transcribe · status · insight maxBytes fetch × 3 StageBar stage · elapsed CallTimeline seek on click InsightCard quotes → seek(ms) MeasuredPanel metrics ProvenancePanel dropped list TranscriptPanel active utterance <audio> object URL LockButton only when unlocked
Frontend component tree: the shared masthead and footer around the Analyzer, which owns all state.
  • The root layout wraps every page in the shared demo look (newsroom tool, from the demo-doc skill): a masthead strip linking The Inference Times and this post, the pipeline as a stage strip, a status note with the public upload limit, a night-edition toggle, and a footer linking the design doc, source, fork and licence.
  • demo/page.tsx is a server component: it reads the access cookie and passes maxBytes, gated and canUnlock to the client Analyzer, which owns all state.
  • The Analyzer calls the three routes with plain fetch, polling /api/status every 3 s, and shows a stage bar with elapsed time.
  • One seek(ms) function moves the <audio> element. Quotes, the call timeline and transcript lines all call it, which is what makes the evidence checkable by ear. Measured panels are labelled apart from the model's judgement, and Provenance shows how many quotes were checked and dropped.

Project layout

voice-insight-template/
├── README.md                       quick start, pipeline figure, deploy + spending limits
├── CONTRIBUTING.md · LICENSE       MIT
├── .env.example                    ASSEMBLYAI_API_KEY, XAI_API_KEY, ACCESS_CODE, PUBLIC_MAX_MB
├── .github/                        CI (lint · typecheck · test · build), issue + PR templates
├── docs/
│   ├── ARCHITECTURE.md             stage-by-stage walkthrough
│   ├── DESIGN.md                   this document
│   └── figures/                    fig-01 … fig-11 (hand-drawn SVG)
└── src/
    ├── proxy.ts                    optional access gate
    ├── app/                        layout · demo-look.css · page · demo/ · unlock/ · api/{transcribe, status, insight, access}
    ├── components/                 analyzer.tsx · charts/ · landing/ · demo-look/ · unlock-form · lock-button
    └── lib/                        access · limits · metrics · verify · quality-guide · analysis/ · providers/

Run & verify

No install needed to try it: the live demo runs in public mode with a capped upload size, and View sample output works without uploading anything. To run it yourself:

git clone https://github.com/nitingoswami-io/voice-insight-template.git && cd voice-insight-template
npm install
cp .env.example .env.local     # add ASSEMBLYAI_API_KEY and XAI_API_KEY
npm test                       # offline, no keys needed
npm run dev                    # http://localhost:3000
CheckExpected
npm test110 tests pass with no network or keys.
GET / and GET /demo200; with no ACCESS_CODE, no lock button is shown.
/demo → View sample outputA scored call with verified quotes, and one claim under Provenance discarded as quote_not_found ("Engineer Tuesday morning ko definitely aa jayega, I guarantee it.").
Upload a 10-minute support call (mono MP3)About 50 s end to end; every coaching point carries a quote that plays the right moment when clicked.
PUBLIC_MAX_MB=0.001 and a 4 KB upload413 with "The public demo accepts up to …". With a valid access cookie the same upload passes the cap.
ACCESS_CODE set: POST /api/transcribe with no cookie401 {"error":"Access code required."}; /demo redirects to /unlock?next=%2Fdemo.

Next steps

IdeaWhat it teaches
Persist analyses and let a reviewer accept or reject each pointEpisodic memory, and a human-gated learning action that tunes the rubric from feedback
Stereo (multichannel) recordingsRemoves the speaker-role guess and turn-boundary errors
A "swap speakers" controlSmall UI fix with a visible effect on every score
Direct-to-storage uploads with signed URLsLifts the 4.5 MB cap; how serverless body limits shape architecture
A second AsrProvider (self-hosted Whisper) and a vLLM endpointProvider seams; keeping transcripts on your own infrastructure
Aggregate the drop rate across many callsVerification stats as a live hallucination metric

Contribute

Fork the repo, pick an idea from the table, and open a pull request. Bugs and unclear steps go in issues; the workflow is in CONTRIBUTING.md. Fork

Credits & sources

  1. Theodore R. Sumers, Shunyu Yao, Karthik Narasimhan, Thomas L. Griffiths. Cognitive Architectures for Language Agents (2023)The CoALA vocabulary (grounding and reasoning actions, working and procedural memory, the decision procedure) used to map each stage.
  2. AssemblyAI. Speaker DiarizationThe speaker_labels feature that splits agent and customer.
  3. xAI. Structured Outputsresponse_format with json_schema, which makes a quote mandatory on every observation.
  4. Colin McDonnell and contributors. zod · MITSchema definition and validation for the model's answer.
  5. V. I. Levenshtein. Binary codes capable of correcting deletions, insertions, and reversals (1966)The edit distance behind fuzzy quote matching.
  6. Vercel. create-next-appThe starting scaffold.
  7. Original workThe quote-verification layer (text, time and speaker checks with counted drops and withheld scores), the measured-versus-judged split, and the public-demo cost limits are new in this design.

Demo · RAG

RAG Demo: ask real-world documents, see the evidence

A minimal web app that shows semantic Retrieval-Augmented Generation end to end. Pick a knowledge base (HR policies, a lease, a router manual), ask a question, and see the passages, similarity scores and database rows behind every answer. Each stage maps to the CoALA framework.

  • FastAPI
  • LangChain
  • pgvector
  • Next.js
FollowsCoALA →