# ivr.php latency patch — deploy notes

## Before you upload

1. **Back up the live file.** `cp ivr.php ivr.php.pre-latency`
2. **Check which flush function your PHP has.** From SSH or a cPanel terminal:

   ```
   php -r 'var_dump(function_exists("fastcgi_finish_request"), function_exists("litespeed_finish_request"));'
   ```

   If both are `false`, `FAST_BACKGROUND_STT` still works via the
   `ignore_user_abort` + `flush()` fallback, but it is less reliable
   under some handlers. Test one call and check the log for the
   `bg STT (flush)` line and its timing.

3. **Rename two prompt files, or leave as patched.** The patch points
   at the filenames that actually exist:
   - `02_zip_retry.mp3` -> `02_ask_zip_retry.mp3`
   - `20_fleet_thanks.mp3` -> `20_fleet_thanks1.mp3`

   If you would rather fix the filenames than the code, rename the MP3s
   on the server and revert those two references.

## Toggles

Top of the file, around line 50:

```php
define('FAST_BACKGROUND_STT', true);  // transcribe while filler plays
define('FAST_GATHER_FIELDS',  true);  // zip/glass/date via Deepgram, no Whisper
define('FAST_SHORT_FILLER',   true);  // 2.0s "one moment" instead of 9.5s
define('FAST_TRIM_PAUSES',    true);  // Record silence timeout 3s -> 2s
```

**Set all four to `false` and you get the original behavior back.** That is
the fastest rollback if something sounds wrong on a live call.

Suggested test order — one toggle at a time, one test call after each:

| Step | Set true | Listen for |
|---|---|---|
| 1 | `FAST_BACKGROUND_STT` only | Same 9.5s filler, but the next prompt fires the instant it ends instead of 3-5s later |
| 2 | `+ FAST_SHORT_FILLER` | Filler drops to 2s. If you hear silence after it, STT isn't finishing in time — check the `bg STT` timings in the log |
| 3 | `+ FAST_TRIM_PAUSES` | Recording cuts 1s sooner. Watch that it isn't clipping people mid-address |
| 4 | `+ FAST_GATHER_FIELDS` | Zip, glass, other-glass and install date answer with no filler at all. Check Deepgram accuracy on zips |

## What changed

**Structural**
- `capture_and_transcribe()` / `take_transcript()` — emit the filler TwiML,
  flush it to Twilio, then run Whisper in the same PHP process while the
  audio plays. Was: play filler to completion, redirect, *then* transcribe.
- `ivr_flush_response()` — fastcgi / litespeed / plain-flush fallback.
- Applied to: zip, glass, other-glass, year-make-model, name, street, contact.
- Street: the Nominatim geocode now runs inside the flushed window too.
- Contact: the Claude extraction call also runs inside the flushed window,
  so Whisper + Claude overlap the filler instead of stacking after it.

**Gather instead of Record+Whisper** (behind `FAST_GATHER_FIELDS`)
- zip, glass, other-glass, install date. Deepgram returns the answer in the
  same webhook, so there is no download, no OpenAI hop, no filler, no redirect.
- Zip accepts keypad entry as well now.

**Removed / fixed**
- Dead `get_truck_nags_style()` call in `process_yearmake` whose result
  (`$style_check`) was never read — one wasted proxy round-trip per call.
- ElevenLabs cache no longer expires after an hour. The filename is an md5
  of the text, so identical text means identical audio; the TTL just forced
  needless regeneration. Delete a file under `prompts/joined/` to force a regen.
- Whisper's Twilio download now retries once after 500ms. A recording that
  isn't readable yet used to return empty and get treated as "caller said
  nothing", triggering a spurious retry prompt.
- Retry paths now clear `rec_*` and `*_transcript` from the session. The zip
  retry in particular used to re-transcribe the *same* stored recording, so a
  misheard zip could loop on the same bad audio.
- Redirect hops collapsed: `got_mode` -> `start_fleet`, and
  `got_name` -> `got_street`. Each was a full HTTP round-trip to reach a
  state that only played the next prompt.
- Two `<Play>` references pointed at files that don't exist on disk
  (`02_zip_retry.mp3`, `20_fleet_thanks.mp3`). The fleet one is on the happy
  path, right before `process_joined`.

## Still worth doing, not in this patch

- `zip_to_city_state()` calls zippopotam.us live on every call. A ZIP table in
  MySQL removes a third-party dependency from the critical path.
- `<Pause length="1"/>` before every `<Record>` is there to stop the mic
  catching the prompt tail. TwiML `<Pause>` takes whole seconds only, so the
  way to shave it is trimming trailing silence off the prompt MP3s and then
  dropping the pause.
- `whisper-1` is the slowest current OpenAI transcription model. Since the
  work is now hidden behind the filler it matters less, but a faster model
  would let you shorten the filler further.
- `ask_style_choice` and `retail_price_quote` are still reached by redirect.
  Each costs ~300ms and only fires on some calls, so they were left alone.
