Lose it once. Not twice.
Drop in a recording of a sales appointment you lost. Rerun finds the exchange where the deal died, then puts you back in that moment against a buyer built from that transcript — same objection, same wording, same stubbornness — until you can get past it.
Built for field reps: solar, HVAC, roofing, windows.
- Drop in a conversation. Audio or a pasted transcript.
- Find the cut. The pivotal exchange, with both sides quoted verbatim.
- Take the drill. Voice roleplay against a buyer seeded from that transcript.
- See the delta. Your attempt scored against your original handling.
- Run it again. Attempts stack under the cut.
Node 20 or newer.
npm install
cp .env.example .env # add a Groq API key
npm run seed
npm run devThe database is one SQLite file at ./data/rerun.db. No server, no container,
no account.
One key in GROQ_API_KEY is enough. The free tier caps at 8,000 tokens per
minute per key, which is the limit you feel during corpus generation, so extra
keys can be listed as GROQ_API_KEY_2, GROQ_API_KEY_3 and so on. Calls are
spread across whichever keys are present, and a key that hits a limit is parked
until it recovers.
| Command | What it does |
|---|---|
npm run generate-corpus |
Builds the 24-conversation training corpus with ground-truth labels |
npm run inspect-corpus |
Checks the corpus against the distribution and realism it is meant to have |
npm run relabel-corpus |
Re-locates pivotal turns whose label looks wrong |
npm run diversify-labels |
Spreads objection types across the corpus |
npm run voice-corpus |
Voices four transcripts through Piper into stitched WAVs |
npm run ingest-corpus |
Runs voiced audio back through the full ingest path and scores it |
npm run check-copy |
Checks UI copy and the README against the banned-words list |
npm run eval |
Scores the detector against the ground truth and prints the number |
npm run cache-stats |
Tokens banked on disk, which a rerun does not spend again |
npm run seed |
Loads finished conversations, diagnoses and one scored drill |
Every model response is cached to .cache/llm by prompt hash, so re-running any
script costs nothing. Deleting that directory means paying for it all again.
lib/models.ts every model call, the key pool, the cache, JSON validation
lib/types.ts shared types and the objection taxonomy
lib/db.ts SQLite schema
lib/tts.ts Piper speech synthesis and WAV handling
lib/stt.ts speech to text, with chunking and its own cache
lib/pitch.ts acoustic speaker separation
lib/ingest.ts audio or text in, normalised turns out
scripts/ corpus generation, voicing, ingest, evaluation, seeding
corpus/ generated transcripts and their ground-truth plans
specs/ the build spec and a numbered note per milestone
Speech synthesis runs locally through Piper. It is not part of npm install,
because the voices are about 260MB and only the four demo conversations need
them. Python 3.9 or newer:
python -m venv piper/venv
piper/venv/Scripts/python -m pip install piper-tts # bin/python on macOS and Linux
piper/venv/Scripts/python -m piper.download_voices --download-dir piper/voices \
en_US-ryan-medium en_US-amy-medium fr_FR-siwis-medium fr_FR-upmc-medium
npm run voice-corpusSpeech to text runs on Groq's free Whisper tier and needs no extra setup.
Synthesised turns are cached in .cache/tts and transcriptions in .cache/stt,
so re-running either script is fast and costs nothing.
Every accuracy figure in this repo is measured on synthesized speech: two distinct Piper voices, no room noise, no overlapping microphones. Real recordings are harder, and speaker separation is the part that will suffer first — one phone on a kitchen table gives both people the same channel and similar volume. Treat the diarisation numbers as an upper bound, not a promise.
Two known limits, with what would fix them:
- Confidence on a diagnosis is not yet meaningful. It scores 0.81 on correct answers and 0.79 on wrong ones, so it cannot be used to decide when to ask the rep for help. The fix is self-consistency — run the diagnosis three times at a higher temperature and use cross-run agreement as the score, which turns "confidently wrong" into "disagrees with itself". It is not built because it triples the cost of the run.
- Speaker labels come from a text pass plus a pitch check, and the pitch refinement stands down when two voices sit close together. Turns it is unsure about are reported so they can be corrected by hand.
There is no public corpus of in-home sales conversations, so this repo generates one. Each conversation is planned before it is written: vertical, outcome, buyer, and for the lost ones a designated pivotal turn with the objection type and a note on how the rep mishandled it. The dialogue is then written to realise that plan.
The plan is never shown to the detector. It exists so npm run eval can report
how often the detector picks the same pivotal turn as the plan, within a
tolerance of two turns.