62 min readAmbient capture

Implementation Guide: Transcribe service advisor walkarounds and generate repair order drafts for technician review

Step-by-step implementation guide for deploying AI to transcribe service advisor walkarounds and generate repair order drafts for technician review for Automotive clients.

Hardware Procurement

PLAUD NotePin Wearable AI Recorder

PLAUDPLAUD NotePin (64GB)Qty: 3

$169/unit MSP cost / $249/unit suggested resale to client

Wearable ambient audio capture device worn by each service advisor during vehicle walkarounds. Features 20 hours continuous recording, 64GB local storage, magnetic clip for shirt/lanyard attachment, Bluetooth sync to paired tablet. Records the full advisor-customer conversation for transcription.

Samsung Galaxy Tab Active5 Enterprise Edition

SamsungSM-X306B (128GB, Wi-Fi + LTE)Qty: 3

$450/unit MSP cost via Samsung distribution / $649/unit suggested resale

Rugged tablet carried or stationed by each service advisor. Receives audio from PLAUD NotePin via Bluetooth, runs the custom walkaround capture app, displays the AI-generated repair order draft for review and approval. IP68-rated for shop floor durability, S-Pen for annotations, Samsung Knox MDM built-in. Enterprise Edition includes 5 years of security updates and Knox Suite license.

PLAUD NotePin Charging Dock (3-pack)

PLAUDPLAUD NotePin Charging DockQty: 3

$25/unit MSP cost (included with NotePin kit) / $0 additional

USB-C charging docks stationed at advisor desks for overnight charging of NotePin devices. Each NotePin ships with one dock.

Ubiquiti U6-Pro Wi-Fi 6 Access Point

UbiquitiU6-ProQty: 2

$150/unit MSP cost / $275/unit suggested resale

Wi-Fi 6 (802.11ax) access points to ensure complete coverage across the service drive, write-up area, and first row of shop bays. Required for reliable real-time audio streaming from tablets to cloud STT API. Mount one AP in service drive/reception area, one in shop bay area.

Ubiquiti USW-Lite-8-PoE Switch

UbiquitiUSW-Lite-8-PoEQty: 1

$110 MSP cost / $175 suggested resale

PoE switch to power the two U6-Pro access points and provide wired uplink. Supports 802.3at PoE+ with 52W total PoE budget.

Samsung Galaxy Tab Active5 Protective Case with Hand Strap

Samsung / OtterBoxOtterBox uniVERSE for Galaxy Tab Active5Qty: 3

$50/unit MSP cost / $80/unit suggested resale

Additional drop protection and hand strap for advisors carrying tablets during walkarounds. Prevents drops on concrete shop floor.

RAM Mounts Tablet Wall Dock

RAM MountsRAM-HOL-TAB-LGUQty: 3

$45/unit MSP cost / $75/unit suggested resale

Wall-mounted tablet docking stations at each advisor's desk for charging and desk-mode use of the review/approval UI.

Software Procurement

Deepgram Nova-3 Speech-to-Text API

DeepgramNova-3Qty: ~3,000 min/month (3-advisor shop)

$0.0077/min streaming, $0.0043/min batch — estimated $18–25/month. $200 free credit on signup (~45,000 free minutes).

Core transcription engine. Converts walkaround audio to text with speaker diarization (separating advisor voice from customer voice), punctuation, and paragraph formatting. Nova-3 is optimized for noisy environments, critical for shop floor audio with compressors and impact tools.

OpenAI GPT-5.4 API

OpenAIGPT-5.4Qty: ~600 RO drafts/month

$2.50/M input tokens + $10.00/M output tokens — estimated $8–15/month for ~600 RO drafts/month (~$0.015 per RO draft)

LLM engine for extracting structured repair order data from transcripts. Receives diarized transcript, applies automotive-specific prompt to extract customer complaints, vehicle info, symptoms, requested services, and recommended labor ops. Returns structured JSON matching DMS repair order schema.

Samsung Knox Suite (MDM)

SamsungKnox Suite

$0 Year 1 (included with Enterprise Edition) / $5/device/month Year 2+ — $15/month for 3 tablets

Mobile Device Management for locking down tablets to kiosk mode (walkaround app only), enforcing encryption, pushing app updates, remote wipe capability, and compliance reporting for FTC Safeguards Rule. License type: per-device, included with Galaxy Tab Active5 Enterprise Edition for first year, then ~$5/device/month.

$199–$439/month depending on plan tier (client's existing cost, not incremental)

Target DMS for repair order write-back integration. Tekmetric's open REST API allows creating and updating repair orders, attaching customer information, and linking inspection results. The AI system pushes draft ROs into Tekmetric for technician assignment and dispatch. If client uses Shop-Ware or CDK Drive, substitute the appropriate integration.

$20–50/month for middleware API hosting; $0 during development on free tier

Hosts the custom Node.js/Python middleware that orchestrates the pipeline: receives audio from tablets, calls Deepgram for transcription, calls OpenAI for RO extraction, serves the review UI, and pushes approved ROs to the DMS API.

$25/month Pro plan — includes 8GB database, 250GB bandwidth, auth, and storage

PostgreSQL database for storing transcripts, draft ROs, approval audit logs, and advisor profiles. Supabase Auth handles advisor login on tablets. Supabase Storage stores audio file backups with AES-256 encryption at rest.

PLAUD NotePin Starter Plan (or Pro Plan)

PLAUDNotePin App SubscriptionQty: 1–3 devices

$0/month Starter (300 min/month transcription) or $7.99/month Pro (unlimited) — $0–$24/month for 3 devices

PLAUD's companion app subscription for syncing audio from NotePin to phone/tablet. The Starter plan's 300 min/month may suffice if using only for audio transfer (not PLAUD's built-in transcription, since we use Deepgram). Pro plan needed if using PLAUD's own transcription as fallback.

$0 (Developer plan) or $26/month (Team plan) — recommended Team plan for production

Error monitoring and crash reporting for the custom middleware and tablet app. Alerts the MSP when transcription failures, API errors, or DMS integration issues occur.

Prerequisites

  • Client must have an active DMS with API access — Tekmetric (Grow or Scale plan), Shop-Ware (Pro plan or above), or CDK Drive with Fortellis subscription. Confirm API credentials and write permissions for repair order creation before project kickoff.
  • Service drive and write-up area must have reliable internet connectivity — minimum 25 Mbps upload speed for real-time audio streaming. Run a speed test from the service lane during peak hours (7:30–9:00 AM). If below threshold, upgrade ISP plan or add a dedicated circuit before proceeding.
  • Existing WiFi must cover the entire service drive, write-up area, and at least the first bay row — or budget for the Ubiquiti AP deployment specified in hardware procurement. Conduct a WiFi heat map survey using NetSpot or Ekahau to identify dead zones.
  • Client must designate a Qualified Individual per the FTC Safeguards Rule who will oversee the addition of this system to the shop's Written Information Security Program (WISP). This is typically the shop owner or office manager.
  • Client must have recording consent signage printed and posted in the service drive before go-live. For two-party consent states (CA, CT, FL, IL, MA, MD, MT, NH, PA, WA), client must also update their service intake form to include explicit recording consent language signed by the customer.
  • Each service advisor who will use the system needs a Google account or email address for Supabase authentication, and must be willing to wear the PLAUD NotePin during walkarounds.
  • Client must provide 10–15 sample completed repair orders (anonymized if necessary) representing the full range of service types they handle — maintenance, diagnostics, warranty, body/paint, customer-pay. These are used to train and validate the LLM extraction prompts.
  • Electrical outlets or USB power must be available at each advisor desk for NotePin charging docks and tablet wall mounts.
  • Confirm the client's DMS labor operation codes list (op codes) and parts catalog source (Epicor/WHI, Nexpart, or WorldPac) — the LLM prompt must map natural language descriptions to the correct op codes.

Installation Steps

...

Step 1: Site Survey & WiFi Assessment

Visit the client site to conduct a physical survey of the service drive, advisor write-up area, and shop floor. Map WiFi coverage using a laptop running NetSpot (free tier) or Ekahau. Identify AP mounting locations that provide coverage across the service drive (where walkarounds occur), the advisor desks, and the first row of bays. Document existing network topology — ISP router, switches, existing APs, VLAN configuration, and DMS server/cloud endpoint. Take photos of proposed AP mount points and cable run paths.

On a MacBook or Windows laptop with NetSpot installed: Walk the service drive and shop floor with NetSpot in Survey mode. Export heat map as PDF for the project file.
shell
netspot --survey --export heatmap_service_lane.pdf
Note

If the shop has no existing managed network (just an ISP consumer router), budget additional time and hardware for a proper network stack. The Ubiquiti USW-Lite-8-PoE switch and UniFi Cloud Gateway Ultra ($129) may be needed as a foundation. Do NOT proceed with deployment if WiFi coverage in the service drive is below -65 dBm signal strength.

Step 2: Network Infrastructure Deployment

Install the Ubiquiti U6-Pro access points and PoE switch. Mount APs on ceiling or high wall positions — one in the service drive/reception area, one in the shop bay area near the advisor walkway. Run Cat6 Ethernet from the PoE switch to each AP location. Configure the UniFi controller (cloud-hosted or local) with two SSIDs: one for the shop's existing devices (DMS terminals, POS, etc.) on VLAN 10, and a dedicated 'AI-Capture' SSID on VLAN 20 for the Samsung tablets and NotePin devices. Apply WPA3-Personal or WPA2-Enterprise to the AI-Capture SSID. Set minimum RSSI threshold to prevent tablets from clinging to distant APs.

UniFi VLAN and SSID configuration for AI-Capture network segmentation
bash
# SSH into UniFi Cloud Gateway or access via https://unifi.ui.com
# Create VLAN 20 for IoT/Capture devices
# Network > Settings > Networks > Create New:
#   Name: AI-Capture
#   VLAN ID: 20
#   Gateway/Subnet: 192.168.20.1/24
#   DHCP Range: 192.168.20.10 - 192.168.20.50

# Create SSID:
# WiFi > Create New:
#   Name: ShopName-AI-Capture
#   Security: WPA3-Personal
#   Password: [generate 20+ char password]
#   Network: AI-Capture (VLAN 20)
#   Band: 5 GHz preferred (force 5GHz if all tablets support it)
#   Minimum RSSI: -75 dBm

# Firewall rule — allow VLAN 20 outbound to internet (for API calls)
# Block VLAN 20 to VLAN 10 (isolate from DMS network)
# Settings > Firewall > Rules > Create:
#   Type: LAN In
#   Source: VLAN 20 (192.168.20.0/24)
#   Destination: VLAN 10 (192.168.10.0/24)
#   Action: Drop
Note

VLAN segmentation is critical for FTC Safeguards Rule compliance — the capture devices must not have network-level access to the DMS database or POS systems. All DMS integration happens via cloud APIs, not local network access. Test WiFi throughput from the service drive after installation — should achieve 50+ Mbps on the 5 GHz band.

Step 3: Cloud Infrastructure Provisioning

Set up all cloud accounts and API keys needed for the transcription and AI pipeline. Create accounts for Deepgram, OpenAI, Supabase, and Railway (or Vercel). Configure billing, set usage alerts to prevent runaway costs, and generate API keys. Store all credentials in a password manager (Bitwarden or 1Password) shared with the MSP's project team only — never store in code repositories.

1
Deepgram — Visit https://console.deepgram.com/signup. Create project: 'ClientName-Walkaround'. Generate API key with scope: 'Usage: Listen' (transcription only). Note: $200 free credit applied automatically. Set usage alert at $50/month.
2
OpenAI — Visit https://platform.openai.com/signup. Create organization: 'MSPName-ClientName'. Generate API key with Project-level scope. Set monthly spending limit: $50/month. Enable GPT-5.4 model access.
3
Supabase — Visit https://supabase.com/dashboard. Create new project: 'clientname-walkaround'. Region: us-east-1 (or nearest to client). Generate service_role key for backend. Generate anon key for tablet app. Upgrade to Pro plan ($25/mo).
4
Railway — Visit https://railway.app. Link GitHub repository (created in Step 5). Create new project: 'clientname-walkaround-api'. Set environment variables (all API keys from above).
Note

Set up billing alerts on ALL cloud services to catch unexpected cost spikes. A misconfigured loop could rack up hundreds in API charges overnight. OpenAI's spending limit is hard-capped (API calls return 429 errors when hit), while Deepgram's alerts are notifications only — monitor closely during pilot phase.

Step 4: Supabase Database Schema Setup

Create the database tables that store walkaround recordings, transcripts, draft repair orders, and audit logs. This schema supports the full lifecycle from audio capture through RO approval and DMS push. Apply Row Level Security (RLS) policies so advisors can only see their own walkarounds.

Supabase SQL Editor — full schema with RLS policies
sql
-- Connect to Supabase SQL Editor and run:

-- Advisors table
CREATE TABLE advisors (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  pin TEXT, -- optional 4-digit PIN for tablet login
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now()
};

-- Walkarounds table (one per customer interaction)
CREATE TABLE walkarounds (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  advisor_id UUID REFERENCES advisors(id) NOT NULL,
  customer_name TEXT,
  vehicle_vin TEXT,
  vehicle_year INTEGER,
  vehicle_make TEXT,
  vehicle_model TEXT,
  vehicle_mileage INTEGER,
  audio_storage_path TEXT, -- Supabase Storage path
  audio_duration_seconds INTEGER,
  recording_started_at TIMESTAMPTZ,
  recording_ended_at TIMESTAMPTZ,
  consent_obtained BOOLEAN DEFAULT false,
  status TEXT DEFAULT 'recording' CHECK (status IN ('recording','transcribing','drafting','review','approved','rejected','pushed_to_dms')),
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
};

-- Transcripts table
CREATE TABLE transcripts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  walkaround_id UUID REFERENCES walkarounds(id) NOT NULL,
  raw_transcript JSONB NOT NULL, -- full Deepgram response with diarization
  formatted_text TEXT NOT NULL, -- human-readable transcript
  speaker_labels JSONB, -- mapping of speaker IDs to roles (advisor/customer)
  confidence_score NUMERIC(4,3),
  created_at TIMESTAMPTZ DEFAULT now()
};

-- Draft repair orders table
CREATE TABLE draft_repair_orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  walkaround_id UUID REFERENCES walkarounds(id) NOT NULL,
  customer_concern TEXT NOT NULL,
  technician_notes TEXT,
  vehicle_symptoms TEXT[],
  requested_services JSONB NOT NULL, -- array of {description, op_code, estimated_hours, parts_needed}
  recommended_services JSONB, -- advisor upsell suggestions from AI
  estimated_total_hours NUMERIC(5,2),
  priority TEXT DEFAULT 'normal' CHECK (priority IN ('low','normal','high','urgent')),
  advisor_edits JSONB, -- track what the advisor changed from AI draft
  dms_repair_order_id TEXT, -- ID returned from DMS after push
  approved_by UUID REFERENCES advisors(id),
  approved_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
};

-- Audit log for compliance
CREATE TABLE audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  walkaround_id UUID REFERENCES walkarounds(id),
  action TEXT NOT NULL, -- 'recording_started','consent_confirmed','transcript_created','ro_draft_created','ro_approved','ro_pushed_to_dms','audio_deleted'
  actor_id UUID REFERENCES advisors(id),
  metadata JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
};

-- Enable RLS
ALTER TABLE walkarounds ENABLE ROW LEVEL SECURITY;
ALTER TABLE transcripts ENABLE ROW LEVEL SECURITY;
ALTER TABLE draft_repair_orders ENABLE ROW LEVEL SECURITY;

-- RLS Policies: advisors see only their own data
CREATE POLICY advisor_walkarounds ON walkarounds FOR ALL USING (advisor_id = auth.uid());
CREATE POLICY advisor_transcripts ON transcripts FOR ALL USING (
  walkaround_id IN (SELECT id FROM walkarounds WHERE advisor_id = auth.uid())
};
CREATE POLICY advisor_draft_ros ON draft_repair_orders FOR ALL USING (
  walkaround_id IN (SELECT id FROM walkarounds WHERE advisor_id = auth.uid())
};

-- Storage bucket for audio files
-- Create via Supabase Dashboard > Storage > New Bucket:
--   Name: walkaround-audio
--   Public: false
--   File size limit: 200MB
--   Allowed MIME types: audio/wav, audio/mp3, audio/m4a, audio/webm
Note

The RLS policies use auth.uid() which maps to Supabase Auth. For the backend service (middleware), use the service_role key which bypasses RLS. Ensure the service_role key is NEVER exposed in the tablet app — it should only exist on the server side. Audio files in Supabase Storage are encrypted at rest (AES-256) which satisfies FTC Safeguards Rule requirements.

Step 5: Build the Middleware API Server

Create the Node.js backend application that orchestrates the entire pipeline: receives audio uploads from tablets, sends audio to Deepgram for transcription, sends transcripts to OpenAI for RO extraction, stores results in Supabase, and exposes REST endpoints for the tablet review app. This is the core IP of the solution.

Initialize project, install dependencies, configure environment, and start dev server
bash
# Initialize the project
mkdir walkaround-api && cd walkaround-api
npm init -y
npm install express multer @deepgram/sdk openai @supabase/supabase-js dotenv cors helmet express-rate-limit
npm install -D typescript @types/express @types/multer ts-node nodemon
npx tsc --init

# Create .env file
cat > .env << 'EOF'
DEEPGRAM_API_KEY=your_deepgram_key_here
OPENAI_API_KEY=your_openai_key_here
SUPABASE_URL=https://yourproject.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
PORT=3000
NODE_ENV=development
DMS_TYPE=tekmetric
DMS_API_BASE_URL=https://sandbox.tekmetric.com/api/v1
DMS_API_KEY=your_tekmetric_api_key_here
DMS_SHOP_ID=your_shop_id_here
EOF

# Start development server
npx nodemon src/index.ts
  • Create the main server file — see custom_ai_components for full implementation
  • Create the transcription pipeline — see custom_ai_components
  • Create the RO extraction prompt — see custom_ai_components
  • Create the DMS integration layer — see custom_ai_components
Note

The middleware is intentionally server-side (not running on the tablet) to protect API keys, enable centralized logging, and allow the MSP to update the pipeline without pushing tablet app updates. The tablet app is a thin client that uploads audio and displays results.

Step 6: Build the Tablet Review App (React PWA)

Create a Progressive Web App (PWA) that runs in the Samsung tablet's browser (Chrome). The app provides four screens: (1) Login with advisor PIN/email, (2) Record walkaround — shows recording status, consent confirmation button, and audio level meter, (3) Review draft RO — displays AI-generated repair order with editable fields, (4) History — list of past walkarounds and their status. Using a PWA instead of a native app simplifies deployment and updates — no app store approval needed.

Initialize React PWA, configure manifest, and build for deployment
bash
# Initialize React PWA
npx create-react-app walkaround-tablet --template typescript
cd walkaround-tablet
npm install @supabase/supabase-js axios react-router-dom @mui/material @mui/icons-material @emotion/react @emotion/styled

# Configure PWA manifest for kiosk-style tablet use
# Edit public/manifest.json:
cat > public/manifest.json << 'EOF'
{
  "short_name": "Walkaround",
  "name": "Service Walkaround AI",
  "icons": [{"src": "icon-192.png", "sizes": "192x192", "type": "image/png"}],
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "#1565C0",
  "background_color": "#FFFFFF"
}
EOF

# Build and deploy to Railway (same project as API, different service)
# Or deploy to Vercel for static hosting
npm run build
# Deploy via Railway CLI or Vercel CLI
Note

The PWA approach means the 'app' is actually a website pinned to the tablet home screen. Samsung Knox can lock the tablet to only this Chrome URL in kiosk mode. This eliminates the need for Google Play Store deployment. The app records audio using the Web Audio API / MediaRecorder API, which requires HTTPS — Railway and Vercel provide free SSL.

Step 7: Configure Samsung Tablets with Knox MDM

Enroll each Samsung Galaxy Tab Active5 in Samsung Knox for device management. Configure kiosk mode to restrict tablets to the walkaround PWA, Chrome browser, and Samsung camera app (for photo documentation). Apply security policies for encryption, PIN lock, and remote wipe.

1
Register for Samsung Knox Suite at https://www.samsungknox.com
2
Create Knox Mobile Enrollment (KME) profile
3
Add device IMEIs to KME profile (scan box barcodes)
4
Create Knox Manage enrollment profile: Knox Manage Console > Device Profile > Create: - Profile Name: Walkaround-Advisor - Kiosk Mode: Multi-App Kiosk - Allowed Apps: Chrome (pinned to https://walkaround.clientname.railway.app), Samsung Camera, PLAUD app (com.plaud.notepin), Settings (restricted to WiFi only) - Device Policies: Encryption: Required (enforced), Screen Lock: PIN (minimum 6 digits), USB Debugging: Disabled, Developer Options: Disabled, Factory Reset: Admin only, WiFi Config: Locked to ShopName-AI-Capture SSID, Location: Enabled (for asset tracking), Camera: Enabled, Microphone: Enabled, Bluetooth: Enabled (required for NotePin)
5
Factory reset each tablet, connect to WiFi
6
Tablets auto-enroll into Knox during OOBE setup
7
Profile pushes automatically
Note

Knox Mobile Enrollment (KME) means that if a tablet is lost/stolen or factory reset, it will automatically re-enroll into MDM on next boot. This is critical for FTC Safeguards compliance. Test the kiosk profile on one tablet before imaging all three — ensure Bluetooth pairing with NotePin works within kiosk restrictions.

Step 8: Pair PLAUD NotePins with Tablets

Set up each PLAUD NotePin device, pair it via Bluetooth to its assigned Samsung tablet, and test audio capture quality in the actual service drive environment. Each NotePin should be assigned to a specific advisor and labeled.

1
Install PLAUD app from Galaxy Store / Play Store (whitelisted in Knox)
2
Open PLAUD app, create account or sign in with advisor's email
3
Put NotePin in pairing mode: press and hold button for 3 seconds until LED blinks blue
4
In PLAUD app, tap 'Add Device' > select NotePin from Bluetooth scan
5
Complete pairing, verify firmware is up to date
6
Configure PLAUD app settings: Recording format: WAV (highest quality) or M4A (smaller files) | Auto-sync: ON (sync recordings to tablet when in Bluetooth range) | Transcription: OFF (we use Deepgram instead of PLAUD's built-in)
7
Label each NotePin with advisor name using a label maker
8
Attach NotePin to advisor's preferred wear location: Magnetic clip on shirt collar (best for close-proximity conversations) | Lanyard clip (best for advisors who already wear ID lanyards) | Belt clip (backup option, may be too far from customer)
Critical

Test audio quality before proceeding. Have each advisor do a practice walkaround in the actual service drive while a car is running and the shop is at normal noise levels. Play back the recording — you should be able to clearly distinguish advisor and customer voices. If audio quality is poor, switch to shirt collar clip position or consider the Philips PSM5000 as an upgrade. The NotePin's internal mic is good but not industrial-grade.

Step 9: Configure DMS API Integration

Set up the integration with the client's DMS system. For Tekmetric, register as an API partner, obtain sandbox credentials, and build/test the repair order creation flow. For Shop-Ware, use their open API. For CDK Drive, register on Fortellis and subscribe to the Repair Order API. This step varies significantly by DMS vendor.

bash
# TEKMETRIC INTEGRATION (most common for independent shops):

# 1. Request API access from Tekmetric
# Visit https://www.tekmetric.com/partners or contact partner@tekmetric.com
# Provide: MSP company info, client shop name, use case description
# Receive: API key, shop_id, sandbox endpoint

# 2. Test API connection
curl -X GET 'https://sandbox.tekmetric.com/api/v1/shops/{shop_id}/repair-orders' \
  -H 'Authorization: Bearer {api_key}' \
  -H 'Content-Type: application/json'

# 3. Create a test repair order
curl -X POST 'https://sandbox.tekmetric.com/api/v1/shops/{shop_id}/repair-orders' \
  -H 'Authorization: Bearer {api_key}' \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": 12345,
    "vehicleId": 67890,
    "customerConcern": "Customer reports squealing noise from front brakes when stopping",
    "jobs": [
      {
        "name": "Front Brake Inspection",
        "laborHours": 0.5,
        "note": "Inspect front brake pads, rotors, and calipers. Measure pad thickness and rotor runout."
      }
    ]
  }'

# 4. Verify RO appears in Tekmetric sandbox dashboard
# 5. Switch to production endpoint when ready: https://shop.tekmetric.com/api/v1/

# SHOP-WARE INTEGRATION:
# Similar REST API — see https://developers.shop-ware.com/
# Register at developer portal, obtain OAuth2 credentials

# CDK FORTELLIS INTEGRATION:
# 1. Register at https://developer.fortellis.io/
# 2. Subscribe to 'Service Repair Order' API
# 3. Dealers must authorize your app in their Fortellis Marketplace
# This process can take 2-4 weeks for approval
Note

DMS integration is the highest-risk step in the project. Tekmetric and Shop-Ware have open, well-documented APIs with fast partner approval (days to weeks). CDK Fortellis takes 2-4 weeks. Reynolds & Reynolds requires formal RCI certification which can take 3-6 months — if the client uses Reynolds, plan for a manual CSV export workflow as a temporary bridge. Always test in sandbox first.

Step 10: Deploy Middleware to Production

Push the middleware API server to Railway for production hosting. Configure environment variables, set up auto-scaling, enable logging, and connect the Sentry error monitoring. Verify all API connections work end-to-end from the production environment.

bash
# Install Railway CLI
npm install -g @railway/cli
railway login

# Link to project created in Step 3
cd walkaround-api
railway link

# Set environment variables in Railway
railway variables set DEEPGRAM_API_KEY=dg_live_xxxxx
railway variables set OPENAI_API_KEY=sk-proj-xxxxx
railway variables set SUPABASE_URL=https://abcdefg.supabase.co
railway variables set SUPABASE_SERVICE_ROLE_KEY=eyJxxxxx
railway variables set DMS_TYPE=tekmetric
railway variables set DMS_API_BASE_URL=https://shop.tekmetric.com/api/v1
railway variables set DMS_API_KEY=xxxxx
railway variables set DMS_SHOP_ID=xxxxx
railway variables set NODE_ENV=production
railway variables set SENTRY_DSN=https://xxxxx@sentry.io/xxxxx

# Deploy
railway up

# Verify deployment
curl https://walkaround-api-production.up.railway.app/health
# Expected response: {"status":"ok","deepgram":"connected","openai":"connected","supabase":"connected","dms":"connected"}

# Configure custom domain (optional)
railway domain add api.walkaround.clientdomain.com
Note

Railway provides automatic HTTPS, auto-restart on crash, and log streaming. Set up a Railway Pro plan ($20/month) for production to get SLA guarantees and priority support. Alternatively, deploy to a $5/month DigitalOcean droplet if you want more control, but you'll need to manage SSL, process management (PM2), and restarts yourself.

Step 11: Deploy Tablet App to Production

Build the production version of the React PWA, deploy it to static hosting (Vercel or Railway static), and configure the tablets to use the production URL. Add the PWA to each tablet's home screen and update Knox kiosk profile to point to the production URL.

Build and deploy production PWA
bash
# Build production PWA
cd walkaround-tablet
echo 'REACT_APP_API_URL=https://walkaround-api-production.up.railway.app' > .env.production
echo 'REACT_APP_SUPABASE_URL=https://abcdefg.supabase.co' >> .env.production
echo 'REACT_APP_SUPABASE_ANON_KEY=eyJxxxxx' >> .env.production
npm run build

# Deploy to Vercel
npm install -g vercel
vercel --prod

# Or deploy to Railway as a static site
# railway up (from the walkaround-tablet directory)
1
Open Chrome, navigate to https://walkaround.vercel.app
2
Tap Chrome menu (three dots) > 'Add to Home Screen'
3
Name it 'Walkaround'
4
Verify PWA opens in standalone mode (no browser chrome)
5
Update Knox Manage profile to pin Chrome to this URL
Note

After deploying the PWA, test on each tablet: (1) Login works, (2) Audio recording works, (3) Recording uploads to API, (4) Transcript appears within 30-60 seconds, (5) Draft RO appears within 10-15 seconds after transcript, (6) Editing and approving the RO works, (7) Approved RO appears in the DMS.

Before any recording goes live, install physical signage in the service drive and update the shop's service intake forms to include recording consent. This is a legal requirement in all-party consent states and a best practice everywhere. The MSP should provide the signage templates and form language.

  • Print and install the following signage (minimum 11x17 inches, visible font)
  • SIGN TEXT — NOTICE: AUDIO RECORDING IN USE: For your convenience and to ensure accuracy of service records, conversations in this service area may be recorded and transcribed. Recordings are used solely for creating your vehicle's repair order and are retained for [90 days / as required by law]. If you prefer not to be recorded, please inform your service advisor.
  • Install signs at: (1) Service drive entrance (customer-facing), (2) Each advisor write-up desk, (3) Service waiting area
  • Update service intake form to include — RECORDING CONSENT: I understand that my conversation with the service advisor may be audio recorded for the purpose of accurately documenting my vehicle's service needs. I consent to this recording. [ ] I consent to audio recording [ ] I do NOT consent to audio recording Signature: _____________ Date: _____________
  • For two-party consent states (CA, CT, FL, IL, MA, MD, MT, NH, PA, WA): The consent checkbox is MANDATORY — do not record if customer declines. Configure the tablet app to require consent confirmation before recording starts.
Note

This step is NON-NEGOTIABLE and must be completed before any live recording. In two-party consent states, recording without consent is a criminal offense (felony in some states). The tablet app includes a consent confirmation screen — the advisor must tap 'Customer consented' before the recording begins. If customer declines, the advisor uses traditional manual note-taking for that walkaround.

Step 13: Pilot Testing with Lead Service Advisor

Select the shop's most experienced and tech-comfortable service advisor for a 2-week pilot. They will use the system for all walkarounds during this period while continuing to also enter ROs manually (dual-entry). Compare AI-generated RO drafts against manually entered ROs for accuracy, completeness, and time savings. Collect detailed feedback daily.

  • Pilot success metrics to track:
  • Transcription accuracy (% of words correct — spot-check 5 transcripts/day)
  • RO field extraction accuracy (% of fields correctly populated in draft)
  • Time saved per RO (measure with stopwatch: manual entry vs. review-only)
  • Advisor satisfaction (daily 1-5 rating + written feedback)
  • Customer reaction (any complaints or concerns about recording)
  • System reliability (% of walkarounds that complete the full pipeline without error)
  • Create a pilot tracking spreadsheet — Columns: Date, Walkaround_ID, Manual_RO_Time, AI_Review_Time, Fields_Correct, Fields_Edited, Advisor_Rating, Notes
  • Daily check-in with advisor (5 min at end of day): What worked well today? / What did the AI get wrong? / Any customer questions or concerns? / Suggestions for improvement?
  • LLM prompt tuning during pilot: After each day, review the advisor's edits to AI drafts. Identify systematic errors (e.g., always maps 'brake flush' to wrong op code). Update the system prompt with corrections (see custom_ai_components).
Note

The pilot phase is where the LLM prompt gets tuned to the specific shop's vocabulary, common services, and op code conventions. Expect the first week to require daily prompt adjustments. By week 2, accuracy should stabilize at 85-95% for routine services. Complex diagnostics may remain at 70-80% accuracy and always require advisor review. Do NOT skip the dual-entry requirement during pilot — the manual RO is the safety net.

Step 14: Full Rollout to All Advisors

After successful pilot (meeting defined success criteria), deploy to all remaining service advisors. Conduct a 90-minute group training session covering device usage, the review workflow, consent procedures, and troubleshooting. Switch from dual-entry to AI-primary workflow (advisors review and approve AI drafts only, no manual entry unless system is down).

1
(15 min) Why we're doing this — time savings, accuracy, customer experience
2
(15 min) How the NotePin works — wearing, starting/stopping, charging
3
(15 min) Tablet app walkthrough — login, consent screen, recording, review
4
(15 min) Editing AI drafts — what to look for, common corrections
5
(15 min) Troubleshooting — no Bluetooth, no WiFi, wrong transcript, restart
6
(15 min) Q&A + practice walkarounds
  • Post-training: Pair each advisor's NotePin with their assigned tablet
  • Post-training: Have each advisor do one practice walkaround (with another advisor as 'customer')
  • Post-training: Verify the full pipeline works for each advisor
  • Post-training: Set up a Slack/Teams channel or group text for real-time support during first week
  • Escalation Level 1: Advisor restarts tablet / re-pairs NotePin (self-service)
  • Escalation Level 2: Advisor texts MSP support channel (15 min response during business hours)
  • Escalation Level 3: MSP tech remotes into tablet or server to diagnose (same day)
  • Escalation Level 4: MSP tech on-site visit (next business day)
Note

The first week after full rollout is critical. Have an MSP tech on-call during the shop's business hours (typically 7 AM - 6 PM M-F, 8 AM - 2 PM Sat). Most issues will be Bluetooth pairing failures (fix: forget device + re-pair) or WiFi drops (fix: verify AP coverage, check for interference from shop equipment). Budget 8-16 hours of support time for the first two weeks.

Custom AI Components

Walkaround Transcription Pipeline

Type: workflow Orchestrates the end-to-end flow from audio upload to structured transcript. Receives audio files from the tablet app via HTTP POST, uploads to Supabase Storage for archival, streams or sends audio to Deepgram Nova-3 for transcription with speaker diarization, stores the transcript in the database, and triggers the RO extraction workflow. Handles retry logic, error states, and partial transcription failures gracefully.

Implementation:

src/pipelines/transcription.ts
typescript
// src/pipelines/transcription.ts
import { createClient as createDeepgramClient } from '@deepgram/sdk';
import { createClient } from '@supabase/supabase-js';
import { Readable } from 'stream';
import fs from 'fs';

const deepgram = createDeepgramClient(process.env.DEEPGRAM_API_KEY!);
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export interface TranscriptionResult {
  walkaroundId: string;
  formattedText: string;
  rawResponse: any;
  speakerLabels: Record<number, string>;
  confidenceScore: number;
  durationSeconds: number;
}

export async function processWalkaroundAudio(
  walkaroundId: string,
  audioFilePath: string,
  advisorName: string
): Promise<TranscriptionResult> {
  // 1. Upload audio to Supabase Storage for archival
  const audioBuffer = fs.readFileSync(audioFilePath);
  const storagePath = `walkarounds/${walkaroundId}/${Date.now()}.wav`;
  
  const { error: uploadError } = await supabase.storage
    .from('walkaround-audio')
    .upload(storagePath, audioBuffer, {
      contentType: 'audio/wav',
      upsert: false
    });
  
  if (uploadError) {
    console.error('Audio upload failed:', uploadError);
    // Continue processing even if archival fails — transcription is higher priority
  }

  // Update walkaround record with storage path
  await supabase
    .from('walkarounds')
    .update({ audio_storage_path: storagePath, status: 'transcribing' })
    .eq('id', walkaroundId);

  // 2. Send audio to Deepgram Nova-3 for transcription
  const { result, error: dgError } = await deepgram.listen.prerecorded.transcribeFile(
    audioBuffer,
    {
      model: 'nova-3',
      smart_format: true,
      punctuate: true,
      diarize: true,
      diarize_version: '2024-02-01',
      paragraphs: true,
      utterances: true,
      language: 'en-US',
      // Automotive vocabulary hints improve accuracy
      keywords: [
        'brake:2', 'rotor:2', 'caliper:2', 'pad:2',
        'transmission:2', 'differential:2', 'CV joint:2',
        'catalytic converter:2', 'O2 sensor:2', 'MAF sensor:2',
        'check engine:2', 'ABS:2', 'TPMS:2', 'OBD:2',
        'VIN:3', 'mileage:2', 'odometer:2',
        'oil change:2', 'tire rotation:2', 'alignment:2',
        'coolant:2', 'antifreeze:2', 'serpentine belt:2',
        'timing belt:2', 'spark plug:2', 'ignition coil:2',
        'strut:2', 'shock:2', 'control arm:2', 'ball joint:2',
        'power steering:2', 'rack and pinion:2',
        'alternator:2', 'starter:2', 'battery:2'
      ]
    }
  );

  if (dgError) {
    await supabase
      .from('walkarounds')
      .update({ status: 'error' })
      .eq('id', walkaroundId);
    throw new Error(`Deepgram transcription failed: ${dgError.message}`);
  }

  // 3. Process diarized results into formatted text
  const utterances = result.results?.utterances || [];
  const channels = result.results?.channels || [];
  const duration = result.metadata?.duration || 0;
  
  // Build speaker-labeled transcript
  // Speaker 0 is usually the person closest to the mic (advisor)
  const speakerLabels: Record<number, string> = { 0: advisorName, 1: 'Customer' };
  
  let formattedText = '';
  for (const utterance of utterances) {
    const speaker = speakerLabels[utterance.speaker] || `Speaker ${utterance.speaker}`;
    formattedText += `${speaker}: ${utterance.transcript}\n\n`;
  }

  // Calculate average confidence
  const words = channels[0]?.alternatives[0]?.words || [];
  const avgConfidence = words.length > 0
    ? words.reduce((sum: number, w: any) => sum + w.confidence, 0) / words.length
    : 0;

  // 4. Store transcript in database
  const { error: insertError } = await supabase
    .from('transcripts')
    .insert({
      walkaround_id: walkaroundId,
      raw_transcript: result,
      formatted_text: formattedText,
      speaker_labels: speakerLabels,
      confidence_score: Math.round(avgConfidence * 1000) / 1000
    });

  if (insertError) {
    throw new Error(`Failed to store transcript: ${insertError.message}`);
  }

  // 5. Update walkaround status and log
  await supabase
    .from('walkarounds')
    .update({ status: 'drafting', audio_duration_seconds: Math.round(duration) })
    .eq('id', walkaroundId);

  await supabase.from('audit_log').insert({
    walkaround_id: walkaroundId,
    action: 'transcript_created',
    metadata: { confidence: avgConfidence, duration_seconds: duration, word_count: words.length }
  });

  return {
    walkaroundId,
    formattedText,
    rawResponse: result,
    speakerLabels,
    confidenceScore: avgConfidence,
    durationSeconds: Math.round(duration)
  };
}

Repair Order Extraction Prompt

Type: prompt

The core GPT-5.4 system prompt and structured output schema that transforms a diarized walkaround transcript into a structured repair order draft. This prompt is the most critical piece of the solution and requires ongoing tuning based on the specific shop's terminology, op codes, and service menu. It includes few-shot examples of common automotive scenarios and maps customer descriptions to standard repair terminology.

Implementation:

RO_EXTRACTION_SYSTEM_PROMPT

You are an expert automotive service writer AI assistant. Your job is to read a transcript of a service advisor's vehicle walkaround conversation with a customer and extract structured repair order information. You work at an auto repair shop. The transcript contains a conversation between a service advisor and a customer, captured during a vehicle walkaround inspection in the service drive. Your task: 1. Extract the customer's stated concerns and complaints verbatim or near-verbatim 2. Identify the vehicle information (VIN, year, make, model, mileage, color) 3. Parse the advisor's observations and recommendations 4. Map each service need to a repair order line item with: - A clear job description - The relevant labor operation category - Estimated labor hours (use standard flat-rate times) - Any parts likely needed - Priority level (urgent = safety issue, high = affecting drivability, normal = maintenance, low = cosmetic) Rules: - NEVER fabricate information not present in the transcript - If the VIN is spoken, transcribe it exactly (17 characters). If unclear, mark as "VIN_UNCONFIRMED" - Distinguish between what the CUSTOMER reported vs what the ADVISOR observed - Use standard automotive terminology in job descriptions - When the customer uses informal language (e.g., "it makes a grinding noise"), translate to proper complaint format (e.g., "Customer reports grinding noise from front brakes during braking") - If mileage is mentioned, capture it. If not, leave null. - Separate distinct service needs into individual line items (don't combine unrelated work) - Flag any safety concerns as "urgent" priority - Include the advisor's upsell recommendations as separate "recommended" items (not customer-requested) Common automotive labor operation categories: - ENGINE_MECHANICAL: Internal engine work, timing, head gasket - ENGINE_PERFORMANCE: Tune-up, sensors, fuel system, ignition - COOLING_SYSTEM: Radiator, water pump, thermostat, coolant - ELECTRICAL: Battery, alternator, starter, wiring, modules - TRANSMISSION: Trans service, rebuild, clutch, differential - BRAKES: Pads, rotors, calipers, brake fluid, ABS - STEERING_SUSPENSION: Alignment, struts, shocks, tie rods, ball joints - EXHAUST: Catalytic converter, muffler, O2 sensors, exhaust manifold - HVAC: A/C, heater core, blower motor, compressor - MAINTENANCE: Oil change, filters, fluid services, tire rotation - TIRES: Mount/balance, TPMS, tire replacement - BODY_INTERIOR: Trim, glass, weatherstrip, seats, cosmetic - DIAGNOSIS: General diagnostic, check engine light, electrical diag Few-shot examples: Example 1 - Brake complaint: Transcript excerpt: "Customer: Yeah so when I hit the brakes it's making like a squealing sound, really loud, especially in the morning." Extracted line item: { "description": "Front brake inspection — Customer reports loud squealing noise during braking, especially when cold", "category": "BRAKES", "estimated_hours": 0.5, "parts_needed": ["Front brake pads (pending inspection)", "Front rotors (pending inspection)"], "priority": "high", "source": "customer_reported" } Example 2 - Maintenance: Transcript excerpt: "Advisor: Alright and I can see here you're at 47,000 miles, so you're due for your 50,000 mile service. That includes the oil change, tire rotation, cabin filter, and we'll inspect the brakes and suspension." Extracted line item: { "description": "50,000-mile maintenance service — Oil and filter change, tire rotation, cabin air filter replacement, brake and suspension inspection", "category": "MAINTENANCE", "estimated_hours": 1.5, "parts_needed": ["Engine oil (5W-30, 6 qt)", "Oil filter", "Cabin air filter"], "priority": "normal", "source": "advisor_recommended" } Example 3 - Check engine light: Transcript excerpt: "Customer: The check engine light came on about two days ago and it's been flashing on and off. Advisor: Is it solid or flashing? Customer: It was flashing yesterday but today it's just solid." Extracted line item: { "description": "Check engine light diagnosis — Customer reports CEL illuminated, was flashing intermittently (possible misfire). Solid currently.", "category": "DIAGNOSIS", "estimated_hours": 1.0, "parts_needed": [], "priority": "urgent", "source": "customer_reported" }
Sonnet 4.6
RO_EXTRACTION_RESPONSE_SCHEMA and OP_CODE_MAP — src/prompts/ro-extraction.ts
typescript
// src/prompts/ro-extraction.ts

export const RO_EXTRACTION_RESPONSE_SCHEMA = {
  type: 'object' as const,
  properties: {
    vehicle: {
      type: 'object' as const,
      properties: {
        vin: { type: ['string', 'null'] as const, description: 'Vehicle VIN if spoken in transcript. null if not mentioned.' },
        year: { type: ['integer', 'null'] as const },
        make: { type: ['string', 'null'] as const },
        model: { type: ['string', 'null'] as const },
        trim: { type: ['string', 'null'] as const },
        mileage: { type: ['integer', 'null'] as const },
        color: { type: ['string', 'null'] as const },
        license_plate: { type: ['string', 'null'] as const }
      },
      required: ['vin', 'year', 'make', 'model', 'mileage']
    },
    customer: {
      type: 'object' as const,
      properties: {
        name: { type: ['string', 'null'] as const },
        phone: { type: ['string', 'null'] as const },
        email: { type: ['string', 'null'] as const },
        is_waiter: { type: ['boolean', 'null'] as const, description: 'Whether customer is waiting for the vehicle' },
        promised_time: { type: ['string', 'null'] as const, description: 'Any promised completion time mentioned' }
      },
      required: ['name']
    },
    customer_concern_verbatim: {
      type: 'string' as const,
      description: 'The customer primary concern in their own words, as close to verbatim as possible'
    },
    line_items: {
      type: 'array' as const,
      items: {
        type: 'object' as const,
        properties: {
          description: { type: 'string' as const },
          category: { type: 'string' as const, enum: ['ENGINE_MECHANICAL','ENGINE_PERFORMANCE','COOLING_SYSTEM','ELECTRICAL','TRANSMISSION','BRAKES','STEERING_SUSPENSION','EXHAUST','HVAC','MAINTENANCE','TIRES','BODY_INTERIOR','DIAGNOSIS'] },
          estimated_hours: { type: 'number' as const },
          parts_needed: { type: 'array' as const, items: { type: 'string' as const } },
          priority: { type: 'string' as const, enum: ['urgent','high','normal','low'] },
          source: { type: 'string' as const, enum: ['customer_reported','advisor_recommended','advisor_observed'] }
        },
        required: ['description','category','estimated_hours','parts_needed','priority','source']
      }
    },
    advisor_notes: {
      type: 'string' as const,
      description: 'Any additional observations, context, or instructions the advisor mentioned that dont fit into line items'
    },
    safety_concerns: {
      type: 'array' as const,
      items: { type: 'string' as const },
      description: 'Any safety-related issues identified (worn tires, brake issues, fluid leaks, etc.)'
    },
    confidence_notes: {
      type: 'string' as const,
      description: 'Your assessment of transcript clarity and any items you are uncertain about. Flag anything that needs human verification.'
    }
  },
  required: ['vehicle','customer','customer_concern_verbatim','line_items','advisor_notes','safety_concerns','confidence_notes']
};

// Shop-specific op code mapping (customize per client during pilot phase)
export const OP_CODE_MAP: Record<string, Record<string, string>> = {
  // Format: category -> { description_keyword: op_code }
  // These are examples — replace with the actual client's op codes during setup
  MAINTENANCE: {
    'oil change': 'MAINT-OIL',
    'tire rotation': 'MAINT-ROTATE',
    'cabin filter': 'MAINT-CABIN',
    'air filter': 'MAINT-AIR',
    'spark plug': 'MAINT-SPARK',
    'transmission fluid': 'MAINT-TRANS',
    'coolant flush': 'MAINT-COOL',
    'brake fluid flush': 'MAINT-BRK-FL',
    'power steering flush': 'MAINT-PS-FL',
    'serpentine belt': 'MAINT-BELT',
  },
  BRAKES: {
    'brake inspection': 'BRK-INSP',
    'front brake': 'BRK-FRONT',
    'rear brake': 'BRK-REAR',
    'brake pad': 'BRK-PAD',
    'rotor': 'BRK-ROTOR',
    'caliper': 'BRK-CAL',
  },
  DIAGNOSIS: {
    'check engine': 'DIAG-CEL',
    'electrical diag': 'DIAG-ELEC',
    'noise diag': 'DIAG-NOISE',
    'vibration': 'DIAG-VIB',
    'leak': 'DIAG-LEAK',
  },
  // Add remaining categories during pilot tuning phase
};

RO Draft Generation Workflow

Type: workflow Takes a completed transcript and runs it through the GPT-5.4 API with the structured output schema to produce a repair order draft. Includes retry logic, token counting for cost tracking, and fallback to GPT-5.4 mini if GPT-5.4 is rate-limited or unavailable. Stores the draft in the database and triggers a push notification to the advisor's tablet.

Implementation:

src/pipelines/ro-generation.ts
typescript
// src/pipelines/ro-generation.ts
import OpenAI from 'openai';
import { createClient } from '@supabase/supabase-js';
import { RO_EXTRACTION_SYSTEM_PROMPT, RO_EXTRACTION_RESPONSE_SCHEMA, OP_CODE_MAP } from '../prompts/ro-extraction';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

interface RODraft {
  vehicle: {
    vin: string | null;
    year: number | null;
    make: string | null;
    model: string | null;
    trim: string | null;
    mileage: number | null;
    color: string | null;
    license_plate: string | null;
  };
  customer: {
    name: string | null;
    phone: string | null;
    email: string | null;
    is_waiter: boolean | null;
    promised_time: string | null;
  };
  customer_concern_verbatim: string;
  line_items: Array<{
    description: string;
    category: string;
    estimated_hours: number;
    parts_needed: string[];
    priority: string;
    source: string;
  }>;
  advisor_notes: string;
  safety_concerns: string[];
  confidence_notes: string;
}

export async function generateRODraft(
  walkaroundId: string,
  formattedTranscript: string,
  shopName?: string
): Promise<RODraft> {
  // 1. Build the user prompt with the transcript
  const userPrompt = `Here is the walkaround transcript from ${shopName || 'the shop'}. Please extract the repair order information.

--- TRANSCRIPT START ---
${formattedTranscript}
--- TRANSCRIPT END ---

Extract all repair order information following the schema. Be thorough — capture every service need mentioned, whether explicitly requested by the customer or recommended by the advisor.`;

  // 2. Call GPT-5.4 with structured output (JSON mode)
  let completion;
  let model = 'gpt-5.4';
  
  try {
    completion = await openai.chat.completions.create({
      model: 'gpt-5.4',
      messages: [
        { role: 'system', content: RO_EXTRACTION_SYSTEM_PROMPT },
        { role: 'user', content: userPrompt }
      ],
      response_format: {
        type: 'json_schema',
        json_schema: {
          name: 'repair_order_draft',
          strict: true,
          schema: RO_EXTRACTION_RESPONSE_SCHEMA
        }
      },
      temperature: 0.1, // Low temperature for consistent, factual extraction
      max_tokens: 4000
    });
  } catch (error: any) {
    // Fallback to GPT-5.4 mini if GPT-5.4 fails
    console.warn(`GPT-5.4 failed (${error.message}), falling back to GPT-5.4 mini`);
    model = 'gpt-5.4-mini';
    completion = await openai.chat.completions.create({
      model: 'gpt-5.4-mini',
      messages: [
        { role: 'system', content: RO_EXTRACTION_SYSTEM_PROMPT },
        { role: 'user', content: userPrompt }
      ],
      response_format: { type: 'json_object' },
      temperature: 0.1,
      max_tokens: 4000
    });
  }

  const responseText = completion.choices[0]?.message?.content;
  if (!responseText) {
    throw new Error('Empty response from OpenAI');
  }

  const roDraft: RODraft = JSON.parse(responseText);

  // 3. Post-process: map descriptions to shop-specific op codes
  const enrichedLineItems = roDraft.line_items.map(item => {
    const categoryOps = OP_CODE_MAP[item.category] || {};
    let matchedOpCode = null;
    const descLower = item.description.toLowerCase();
    
    for (const [keyword, opCode] of Object.entries(categoryOps)) {
      if (descLower.includes(keyword)) {
        matchedOpCode = opCode;
        break;
      }
    }
    
    return { ...item, op_code: matchedOpCode };
  });

  // 4. Calculate totals
  const totalEstimatedHours = enrichedLineItems.reduce(
    (sum, item) => sum + item.estimated_hours, 0
  );

  // 5. Store draft in database
  const customerConcern = roDraft.customer_concern_verbatim;
  const requestedServices = enrichedLineItems.filter(
    item => item.source === 'customer_reported'
  );
  const recommendedServices = enrichedLineItems.filter(
    item => item.source !== 'customer_reported'
  );

  const { error: insertError } = await supabase
    .from('draft_repair_orders')
    .insert({
      walkaround_id: walkaroundId,
      customer_concern: customerConcern,
      vehicle_symptoms: roDraft.safety_concerns,
      requested_services: requestedServices,
      recommended_services: recommendedServices,
      estimated_total_hours: totalEstimatedHours,
      technician_notes: roDraft.advisor_notes,
      priority: roDraft.safety_concerns.length > 0 ? 'urgent' : 'normal'
    });

  if (insertError) {
    throw new Error(`Failed to store RO draft: ${insertError.message}`);
  }

  // 6. Update walkaround status
  await supabase
    .from('walkarounds')
    .update({
      status: 'review',
      customer_name: roDraft.customer.name,
      vehicle_vin: roDraft.vehicle.vin,
      vehicle_year: roDraft.vehicle.year,
      vehicle_make: roDraft.vehicle.make,
      vehicle_model: roDraft.vehicle.model,
      vehicle_mileage: roDraft.vehicle.mileage
    })
    .eq('id', walkaroundId);

  // 7. Log for audit and cost tracking
  const usage = completion.usage;
  await supabase.from('audit_log').insert({
    walkaround_id: walkaroundId,
    action: 'ro_draft_created',
    metadata: {
      model,
      input_tokens: usage?.prompt_tokens,
      output_tokens: usage?.completion_tokens,
      total_tokens: usage?.total_tokens,
      line_items_count: enrichedLineItems.length,
      has_safety_concerns: roDraft.safety_concerns.length > 0
    }
  });

  return roDraft;
}

DMS Integration Layer

Type: integration Abstraction layer that pushes approved repair order drafts to the client's DMS. Supports Tekmetric, Shop-Ware, and CDK Drive (via Fortellis) through a common interface. The advisor approves the draft on the tablet, the middleware calls this integration to create the RO in the DMS, and returns the DMS-assigned RO number for confirmation.

Implementation:

src/integrations/dms.ts
typescript
// src/integrations/dms.ts

interface DMSRepairOrder {
  customerId?: string;
  customerName: string;
  customerPhone?: string;
  vehicleVin?: string;
  vehicleYear?: number;
  vehicleMake?: string;
  vehicleModel?: string;
  vehicleMileage?: number;
  customerConcern: string;
  jobs: Array<{
    name: string;
    description: string;
    laborHours: number;
    opCode?: string;
    partsNeeded?: string[];
  }>;
  advisorNotes?: string;
  priority?: string;
}

interface DMSResponse {
  success: boolean;
  repairOrderId: string;
  repairOrderNumber: string;
  error?: string;
}

// Base interface for DMS adapters
interface DMSAdapter {
  createRepairOrder(ro: DMSRepairOrder): Promise<DMSResponse>;
  lookupCustomer(name: string, phone?: string): Promise<{ id: string; name: string } | null>;
  lookupVehicle(vin: string): Promise<{ id: string; year: number; make: string; model: string } | null>;
}

// TEKMETRIC ADAPTER
class TekmetricAdapter implements DMSAdapter {
  private baseUrl: string;
  private apiKey: string;
  private shopId: string;

  constructor() {
    this.baseUrl = process.env.DMS_API_BASE_URL || 'https://shop.tekmetric.com/api/v1';
    this.apiKey = process.env.DMS_API_KEY!;
    this.shopId = process.env.DMS_SHOP_ID!;
  }

  private async fetch(path: string, options: RequestInit = {}): Promise<any> {
    const url = `${this.baseUrl}/shops/${this.shopId}${path}`;
    const response = await fetch(url, {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
    if (!response.ok) {
      const errorBody = await response.text();
      throw new Error(`Tekmetric API error ${response.status}: ${errorBody}`);
    }
    return response.json();
  }

  async lookupCustomer(name: string, phone?: string) {
    try {
      const params = new URLSearchParams();
      if (phone) params.set('phone', phone);
      else params.set('search', name);
      const result = await this.fetch(`/customers?${params.toString()}`);
      if (result.content && result.content.length > 0) {
        return { id: result.content[0].id.toString(), name: `${result.content[0].firstName} ${result.content[0].lastName}` };
      }
      return null;
    } catch {
      return null;
    }
  }

  async lookupVehicle(vin: string) {
    try {
      const result = await this.fetch(`/vehicles?vin=${vin}`);
      if (result.content && result.content.length > 0) {
        const v = result.content[0];
        return { id: v.id.toString(), year: v.year, make: v.make, model: v.model };
      }
      return null;
    } catch {
      return null;
    }
  }

  async createRepairOrder(ro: DMSRepairOrder): Promise<DMSResponse> {
    try {
      // Try to find existing customer and vehicle
      let customerId = ro.customerId;
      let vehicleId: string | undefined;

      if (!customerId && ro.customerName) {
        const customer = await this.lookupCustomer(ro.customerName, ro.customerPhone);
        if (customer) customerId = customer.id;
      }

      if (ro.vehicleVin) {
        const vehicle = await this.lookupVehicle(ro.vehicleVin);
        if (vehicle) vehicleId = vehicle.id;
      }

      const body: any = {
        customerConcern: ro.customerConcern,
        jobs: ro.jobs.map(job => ({
          name: job.name,
          note: job.description + (job.partsNeeded?.length ? `\nParts needed: ${job.partsNeeded.join(', ')}` : ''),
          laborHours: job.laborHours,
          authorized: false // Draft status — technician must authorize
        }))
      };

      if (customerId) body.customerId = parseInt(customerId);
      if (vehicleId) body.vehicleId = parseInt(vehicleId);

      const result = await this.fetch('/repair-orders', {
        method: 'POST',
        body: JSON.stringify(body)
      });

      return {
        success: true,
        repairOrderId: result.id.toString(),
        repairOrderNumber: result.repairOrderNumber || result.id.toString()
      };
    } catch (error: any) {
      return {
        success: false,
        repairOrderId: '',
        repairOrderNumber: '',
        error: error.message
      };
    }
  }
}

// SHOP-WARE ADAPTER
class ShopWareAdapter implements DMSAdapter {
  private baseUrl: string;
  private apiKey: string;

  constructor() {
    this.baseUrl = process.env.DMS_API_BASE_URL || 'https://api.shop-ware.com/api/v1';
    this.apiKey = process.env.DMS_API_KEY!;
  }

  // Similar structure — implement based on Shop-Ware API docs
  // https://developers.shop-ware.com/
  async lookupCustomer(name: string, phone?: string) { return null; /* implement */ }
  async lookupVehicle(vin: string) { return null; /* implement */ }
  async createRepairOrder(ro: DMSRepairOrder): Promise<DMSResponse> {
    // Implement per Shop-Ware API docs
    return { success: false, repairOrderId: '', repairOrderNumber: '', error: 'Shop-Ware adapter not yet implemented' };
  }
}

// Factory function
export function createDMSAdapter(): DMSAdapter {
  const dmsType = process.env.DMS_TYPE || 'tekmetric';
  switch (dmsType) {
    case 'tekmetric': return new TekmetricAdapter();
    case 'shopware': return new ShopWareAdapter();
    default: throw new Error(`Unsupported DMS type: ${dmsType}`);
  }
}

// Main function called when advisor approves an RO draft
export async function pushApprovedROToDMS(
  walkaroundId: string,
  draftRO: any, // draft_repair_orders row from Supabase
  walkaround: any // walkarounds row from Supabase
): Promise<DMSResponse> {
  const adapter = createDMSAdapter();

  const dmsRO: DMSRepairOrder = {
    customerName: walkaround.customer_name || 'Walk-in Customer',
    vehicleVin: walkaround.vehicle_vin,
    vehicleYear: walkaround.vehicle_year,
    vehicleMake: walkaround.vehicle_make,
    vehicleModel: walkaround.vehicle_model,
    vehicleMileage: walkaround.vehicle_mileage,
    customerConcern: draftRO.customer_concern,
    jobs: [
      ...(draftRO.requested_services || []).map((s: any) => ({
        name: s.description.substring(0, 100),
        description: s.description,
        laborHours: s.estimated_hours,
        opCode: s.op_code,
        partsNeeded: s.parts_needed
      })),
      ...(draftRO.recommended_services || []).map((s: any) => ({
        name: `[RECOMMENDED] ${s.description.substring(0, 80)}`,
        description: s.description,
        laborHours: s.estimated_hours,
        opCode: s.op_code,
        partsNeeded: s.parts_needed
      }))
    ],
    advisorNotes: draftRO.technician_notes,
    priority: draftRO.priority
  };

  return adapter.createRepairOrder(dmsRO);
}

Express API Server

Type: integration The main Express.js server that exposes REST endpoints for the tablet app. Handles audio upload, triggers the transcription and RO generation pipelines, serves draft RO data for review, processes approvals, and pushes to the DMS. Includes rate limiting, CORS, helmet security headers, and health check endpoint.

Implementation:

src/index.ts
typescript
// src/index.ts
import express from 'express';
import multer from 'multer';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import { createClient } from '@supabase/supabase-js';
import { processWalkaroundAudio } from './pipelines/transcription';
import { generateRODraft } from './pipelines/ro-generation';
import { pushApprovedROToDMS } from './integrations/dms';
import * as Sentry from '@sentry/node';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
const port = process.env.PORT || 3000;

// Initialize Sentry
if (process.env.SENTRY_DSN) {
  Sentry.init({ dsn: process.env.SENTRY_DSN });
  app.use(Sentry.Handlers.requestHandler());
}

// Middleware
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || '*' }));
app.use(express.json());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 })); // 100 req/15min

// File upload config (max 200MB for long walkaround recordings)
const upload = multer({ dest: '/tmp/uploads/', limits: { fileSize: 200 * 1024 * 1024 } });

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

// Health check
app.get('/health', async (req, res) => {
  try {
    const { error: dbErr } = await supabase.from('advisors').select('id').limit(1);
    res.json({
      status: 'ok',
      deepgram: process.env.DEEPGRAM_API_KEY ? 'configured' : 'missing',
      openai: process.env.OPENAI_API_KEY ? 'configured' : 'missing',
      supabase: dbErr ? 'error' : 'connected',
      dms: process.env.DMS_API_KEY ? 'configured' : 'missing'
    });
  } catch (err) {
    res.status(500).json({ status: 'error', error: String(err) });
  }
});

// POST /walkarounds - Create a new walkaround session
app.post('/walkarounds', async (req, res) => {
  const { advisor_id, consent_obtained } = req.body;
  if (!advisor_id) return res.status(400).json({ error: 'advisor_id required' });
  if (!consent_obtained) return res.status(400).json({ error: 'consent_obtained must be true' });

  const { data, error } = await supabase
    .from('walkarounds')
    .insert({
      advisor_id,
      consent_obtained,
      status: 'recording',
      recording_started_at: new Date().toISOString()
    })
    .select()
    .single();

  if (error) return res.status(500).json({ error: error.message });

  await supabase.from('audit_log').insert({
    walkaround_id: data.id,
    action: 'recording_started',
    actor_id: advisor_id,
    metadata: { consent_obtained }
  });

  res.json({ walkaround_id: data.id, status: 'recording' });
});

// POST /walkarounds/:id/audio - Upload audio and trigger pipeline
app.post('/walkarounds/:id/audio', upload.single('audio'), async (req, res) => {
  const walkaroundId = req.params.id;
  const audioFile = req.file;
  if (!audioFile) return res.status(400).json({ error: 'audio file required' });

  try {
    // Update walkaround with recording end time
    await supabase
      .from('walkarounds')
      .update({ recording_ended_at: new Date().toISOString() })
      .eq('id', walkaroundId);

    // Get advisor info for speaker labeling
    const { data: walkaround } = await supabase
      .from('walkarounds')
      .select('*, advisors(name)')
      .eq('id', walkaroundId)
      .single();

    const advisorName = (walkaround as any)?.advisors?.name || 'Advisor';

    // Step 1: Transcribe
    const transcription = await processWalkaroundAudio(
      walkaroundId,
      audioFile.path,
      advisorName
    );

    // Step 2: Generate RO draft
    const roDraft = await generateRODraft(
      walkaroundId,
      transcription.formattedText
    );

    // Clean up temp file
    const fs = require('fs');
    fs.unlinkSync(audioFile.path);

    res.json({
      walkaround_id: walkaroundId,
      status: 'review',
      transcript_preview: transcription.formattedText.substring(0, 500),
      draft_ro: roDraft,
      confidence: transcription.confidenceScore
    });
  } catch (error: any) {
    Sentry.captureException(error);
    console.error('Pipeline error:', error);
    res.status(500).json({ error: error.message, walkaround_id: walkaroundId });
  }
});

// GET /walkarounds/:id - Get walkaround details including transcript and draft RO
app.get('/walkarounds/:id', async (req, res) => {
  const { data: walkaround, error } = await supabase
    .from('walkarounds')
    .select('*, transcripts(*), draft_repair_orders(*), advisors(name)')
    .eq('id', req.params.id)
    .single();

  if (error || !walkaround) return res.status(404).json({ error: 'Not found' });
  res.json(walkaround);
});

// GET /walkarounds - List walkarounds for an advisor
app.get('/walkarounds', async (req, res) => {
  const advisorId = req.query.advisor_id as string;
  const status = req.query.status as string;
  let query = supabase
    .from('walkarounds')
    .select('id, customer_name, vehicle_make, vehicle_model, status, created_at')
    .order('created_at', { ascending: false })
    .limit(50);

  if (advisorId) query = query.eq('advisor_id', advisorId);
  if (status) query = query.eq('status', status);

  const { data, error } = await query;
  if (error) return res.status(500).json({ error: error.message });
  res.json(data);
});

// POST /walkarounds/:id/approve - Approve draft RO and push to DMS
app.post('/walkarounds/:id/approve', async (req, res) => {
  const walkaroundId = req.params.id;
  const { advisor_id, edits } = req.body; // edits = any changes the advisor made

  try {
    // Get the draft RO
    const { data: draftRO } = await supabase
      .from('draft_repair_orders')
      .select('*')
      .eq('walkaround_id', walkaroundId)
      .single();

    const { data: walkaround } = await supabase
      .from('walkarounds')
      .select('*')
      .eq('id', walkaroundId)
      .single();

    if (!draftRO || !walkaround) {
      return res.status(404).json({ error: 'Walkaround or draft RO not found' });
    }

    // Apply advisor edits if any
    const finalRO = edits ? { ...draftRO, ...edits } : draftRO;

    // Push to DMS
    const dmsResult = await pushApprovedROToDMS(walkaroundId, finalRO, walkaround);

    if (dmsResult.success) {
      // Update records
      await supabase.from('draft_repair_orders').update({
        approved_by: advisor_id,
        approved_at: new Date().toISOString(),
        advisor_edits: edits || null,
        dms_repair_order_id: dmsResult.repairOrderNumber
      }).eq('walkaround_id', walkaroundId);

      await supabase.from('walkarounds').update({
        status: 'pushed_to_dms'
      }).eq('id', walkaroundId);

      await supabase.from('audit_log').insert({
        walkaround_id: walkaroundId,
        action: 'ro_pushed_to_dms',
        actor_id: advisor_id,
        metadata: { dms_ro_id: dmsResult.repairOrderNumber, had_edits: !!edits }
      });

      res.json({
        success: true,
        dms_repair_order_number: dmsResult.repairOrderNumber,
        message: `Repair order ${dmsResult.repairOrderNumber} created in DMS`
      });
    } else {
      res.status(502).json({
        success: false,
        error: dmsResult.error,
        message: 'Failed to push to DMS — draft saved for manual entry'
      });
    }
  } catch (error: any) {
    Sentry.captureException(error);
    res.status(500).json({ error: error.message });
  }
});

// Sentry error handler
if (process.env.SENTRY_DSN) {
  app.use(Sentry.Handlers.errorHandler());
}

app.listen(port, () => {
  console.log(`Walkaround API server running on port ${port}`);
});

Audio Retention & Compliance Manager

Type: workflow Automated background job that enforces the audio retention policy (default 90 days) and generates compliance reports. Runs daily via a cron job. Deletes expired audio files from Supabase Storage, logs deletions to the audit trail, and produces a weekly compliance summary showing recording volumes, consent rates, and retention status — required documentation for the FTC Safeguards Rule Written Information Security Program.

Implementation:

src/jobs/compliance-manager.ts
typescript
// src/jobs/compliance-manager.ts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

const RETENTION_DAYS = parseInt(process.env.AUDIO_RETENTION_DAYS || '90');

export async function runRetentionCleanup(): Promise<{
  filesDeleted: number;
  errors: string[];
}> {
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - RETENTION_DAYS);

  // Find walkarounds older than retention period with audio files
  const { data: expiredWalkarounds, error } = await supabase
    .from('walkarounds')
    .select('id, audio_storage_path')
    .lt('created_at', cutoffDate.toISOString())
    .not('audio_storage_path', 'is', null);

  if (error || !expiredWalkarounds) {
    return { filesDeleted: 0, errors: [error?.message || 'Query failed'] };
  }

  let filesDeleted = 0;
  const errors: string[] = [];

  for (const w of expiredWalkarounds) {
    try {
      // Delete audio file from storage
      const { error: deleteError } = await supabase.storage
        .from('walkaround-audio')
        .remove([w.audio_storage_path]);

      if (deleteError) {
        errors.push(`Failed to delete ${w.audio_storage_path}: ${deleteError.message}`);
        continue;
      }

      // Clear the storage path reference
      await supabase
        .from('walkarounds')
        .update({ audio_storage_path: null })
        .eq('id', w.id);

      // Audit log
      await supabase.from('audit_log').insert({
        walkaround_id: w.id,
        action: 'audio_deleted',
        metadata: {
          reason: 'retention_policy',
          retention_days: RETENTION_DAYS,
          original_path: w.audio_storage_path
        }
      });

      filesDeleted++;
    } catch (err: any) {
      errors.push(`Error processing ${w.id}: ${err.message}`);
    }
  }

  return { filesDeleted, errors };
}

export async function generateComplianceReport(startDate: Date, endDate: Date) {
  // Total walkarounds in period
  const { count: totalWalkarounds } = await supabase
    .from('walkarounds')
    .select('*', { count: 'exact', head: true })
    .gte('created_at', startDate.toISOString())
    .lte('created_at', endDate.toISOString());

  // Consent rate
  const { count: consentedWalkarounds } = await supabase
    .from('walkarounds')
    .select('*', { count: 'exact', head: true })
    .gte('created_at', startDate.toISOString())
    .lte('created_at', endDate.toISOString())
    .eq('consent_obtained', true);

  // ROs successfully pushed to DMS
  const { count: pushedToDMS } = await supabase
    .from('walkarounds')
    .select('*', { count: 'exact', head: true })
    .gte('created_at', startDate.toISOString())
    .lte('created_at', endDate.toISOString())
    .eq('status', 'pushed_to_dms');

  // Audio files currently stored
  const { count: audioFilesStored } = await supabase
    .from('walkarounds')
    .select('*', { count: 'exact', head: true })
    .not('audio_storage_path', 'is', null);

  // Audio files deleted this period
  const { count: audioFilesDeleted } = await supabase
    .from('audit_log')
    .select('*', { count: 'exact', head: true })
    .eq('action', 'audio_deleted')
    .gte('created_at', startDate.toISOString())
    .lte('created_at', endDate.toISOString());

  return {
    report_period: { start: startDate.toISOString(), end: endDate.toISOString() },
    total_walkarounds: totalWalkarounds || 0,
    consent_rate: totalWalkarounds ? ((consentedWalkarounds || 0) / totalWalkarounds * 100).toFixed(1) + '%' : 'N/A',
    ros_pushed_to_dms: pushedToDMS || 0,
    audio_files_currently_stored: audioFilesStored || 0,
    audio_files_deleted_this_period: audioFilesDeleted || 0,
    retention_policy_days: RETENTION_DAYS,
    generated_at: new Date().toISOString()
  };
}

// Schedule this to run daily at 2 AM via Railway cron or a simple setInterval
// In production, add this to your Express server:
// import cron from 'node-cron';
// cron.schedule('0 2 * * *', () => runRetentionCleanup());
// cron.schedule('0 3 * * 1', () => generateComplianceReport(lastWeekStart, lastWeekEnd));

Testing & Validation

  • AUDIO CAPTURE TEST: Have a service advisor wear the PLAUD NotePin and conduct a simulated walkaround in the actual service drive with a running vehicle and normal shop noise. Play back the recorded audio — both advisor and simulated customer voices must be clearly intelligible. If either voice is unclear, adjust NotePin position (move to collar) or switch to Philips PSM5000.
  • BLUETOOTH CONNECTIVITY TEST: Walk the entire service drive and first row of bays while the NotePin is paired with the tablet. Monitor Bluetooth connection status. The connection should not drop within 30 feet of the tablet. If drops occur, ensure the tablet is on the advisor's person (not left at a desk) during walkarounds.
  • WIFI COVERAGE TEST: Using the WiFi Analyzer app on a Samsung tablet, walk the service drive, write-up area, and shop bays. Signal strength must be -65 dBm or better in all areas where walkarounds occur. Upload speed must be 25+ Mbps. Document results with screenshots for the project file.
  • TRANSCRIPTION ACCURACY TEST: Record 5 sample walkarounds covering different service scenarios (oil change, brake complaint, check engine light, tire replacement, major repair estimate). Transcribe each with Deepgram Nova-3. Manually compare transcript to actual spoken words. Accuracy target: 90%+ word accuracy in the service drive environment. Pay special attention to VIN numbers, part names, and vehicle make/model.
  • SPEAKER DIARIZATION TEST: Review the 5 sample transcripts for correct speaker separation. The advisor's words should be labeled 'Advisor' and the customer's words should be labeled 'Customer'. Acceptable accuracy: 85%+ of utterances attributed to the correct speaker. If diarization is consistently wrong, add speaker identification training data.
  • RO EXTRACTION ACCURACY TEST: For each of the 5 sample transcripts, compare the GPT-5.4-extracted repair order fields against what a human service advisor would have entered. Check: (1) Customer concern captured accurately, (2) All mentioned services represented as line items, (3) Vehicle info (year/make/model/mileage) correct, (4) No hallucinated services that weren't discussed, (5) Priority flags are appropriate. Target: 85%+ field accuracy for routine services.
  • OP CODE MAPPING TEST: For the 5 sample RO drafts, verify that the system-suggested op codes match the shop's actual op code list. If the shop uses Mitchell labor guide codes, verify the mapping table includes the top 20 most common services performed at this specific shop.
  • DMS INTEGRATION TEST: Create a test repair order through the API integration to the DMS sandbox (Tekmetric sandbox or Shop-Ware staging). Verify: (1) RO appears in the DMS, (2) Customer name is correct, (3) Vehicle info is correct, (4) All line items are present with descriptions, (5) Labor hours are populated, (6) The RO is in draft/unauthorized status (not accidentally authorized). Then delete the test RO.
  • END-TO-END PIPELINE TEST: Conduct a complete walkaround from recording through DMS push using the production system. Time each stage: (1) Recording completes and uploads (should be < 30 seconds after stopping), (2) Transcript returns (should be < 60 seconds for a 10-minute recording), (3) RO draft appears on tablet (should be < 15 seconds after transcript), (4) After advisor approval, RO appears in DMS (should be < 10 seconds). Total pipeline: under 2 minutes from stop-recording to RO in DMS.
  • CONSENT WORKFLOW TEST: On the tablet app, verify that: (1) Recording cannot start without tapping 'Customer consented' button, (2) The consent confirmation is logged in the audit_log table, (3) The 'pause' or 'stop' button is prominently visible during recording, (4) If advisor selects 'Customer declined recording', the app gracefully exits to a manual notes screen.
  • FAILOVER TEST: Simulate common failure scenarios: (1) Disconnect WiFi during a recording — verify audio is saved locally on NotePin and can be uploaded when WiFi returns, (2) Kill the middleware server — verify the tablet app shows a clear error message and the advisor can save notes manually, (3) Make the DMS API return errors — verify the draft RO is saved locally and can be pushed later via a 'Retry' button.
  • SECURITY TEST: Verify (1) All API calls use HTTPS (no HTTP fallback), (2) Supabase anon key in the tablet app cannot access other advisors' data (test RLS policies), (3) Tablets are encrypted (Settings > Security > Encryption), (4) Knox MDM profile prevents USB debugging and developer options, (5) Audio files in Supabase Storage are not publicly accessible.
  • LOAD TEST: Simulate 5 advisors uploading 10-minute audio files simultaneously (representing morning rush). Verify the middleware handles concurrent requests without timeout or error. Deepgram supports concurrent transcription; verify OpenAI rate limits are not hit (default is 500 RPM for GPT-5.4 which is more than sufficient).

Client Handoff

Conduct a 2-hour client handoff session with the shop owner/manager and all service advisors. Cover the following topics:

1
SYSTEM OVERVIEW (15 min): Walk through the architecture at a high level — NotePin captures audio, tablet sends to cloud, AI transcribes and drafts the RO, advisor reviews and approves, RO appears in DMS. Emphasize that the AI creates DRAFTS — a human always reviews before anything goes to the DMS.
2
DAILY WORKFLOW (20 min): Morning routine — advisors grab their NotePin from the charging dock, clip it on, log into the tablet. For each customer: confirm consent, start recording, do the walkaround, stop recording, wait ~90 seconds, review the draft RO, make edits, approve. End of day: dock NotePin for charging, review any pending drafts.
3
EDITING & APPROVAL (20 min): Live demonstration of reviewing a draft RO. Show how to edit customer concern text, add/remove line items, change labor hours, adjust priority, add advisor notes. Emphasize that 'Approve' pushes to DMS immediately — there is no undo. Show the 'Reject' workflow for bad drafts.
4
CONSENT PROCEDURES (15 min): Review the consent signage locations, intake form language, and tablet consent screen. For two-party consent states: emphasize this is legally mandatory. Role-play the scenario where a customer declines recording.
5
TROUBLESHOOTING GUIDE (15 min): Leave a laminated quick-reference card at each advisor desk covering: NotePin won't connect (forget + re-pair), tablet shows 'offline' (check WiFi, restart tablet), transcript is garbled (re-record, speak closer to mic), RO draft is wrong (edit manually, report to MSP for prompt tuning), DMS push fails (tap Retry, or enter RO manually and notify MSP).
6
COMPLIANCE (10 min): Review with the shop owner: audio retention policy (90 days), FTC Safeguards Rule documentation requirements, weekly compliance report (auto-generated, emailed to owner), procedure for customer data deletion requests (CCPA).
7
SUPPORT & ESCALATION (10 min): Provide MSP support contact info (phone, email, Slack/Teams channel). Explain SLA: 15-minute response during business hours for system-down issues, next-business-day for non-urgent requests. Provide the Sentry dashboard login for the shop owner to view system status.
8
SUCCESS CRITERIA REVIEW (15 min): Review pilot metrics together — time saved per RO, accuracy rates, advisor satisfaction scores. Agree on ongoing success metrics: target 85%+ RO draft accuracy, < 2 min pipeline time, > 90% advisor adoption rate.

Documentation to leave behind:

  • Laminated quick-reference card (one per advisor desk)
  • System architecture diagram (one page)
  • Consent signage templates (digital files)
  • Compliance documentation packet (for FTC Safeguards WISP)
  • MSP support contact card
  • Login credentials document (sealed envelope for shop owner)

Maintenance

ONGOING MAINTENANCE RESPONSIBILITIES:

1. MONITORING (Daily, automated)

  • Sentry alerts for API errors, pipeline failures, and DMS push failures — MSP receives email/Slack notification within 5 minutes of any error
  • Deepgram and OpenAI usage dashboards — check weekly for cost anomalies
  • Supabase database size — monitor storage growth, currently ~1 GB/day for a 3-advisor shop
  • Railway deployment health — auto-restart on crash, monitor uptime via Railway dashboard

2. WEEKLY TASKS (15 min/week)

  • Review the auto-generated compliance report (emailed every Monday)
  • Check Sentry for recurring errors or new error patterns
  • Review API usage costs against budget
  • Check Knox MDM dashboard for tablet health (battery health, OS updates pending)

3. MONTHLY TASKS (1-2 hours/month)

  • Review advisor feedback and RO edit patterns — if advisors consistently edit the same field, the LLM prompt needs tuning
  • Apply prompt improvements based on edit analysis (update the RO_EXTRACTION_SYSTEM_PROMPT with new examples or corrections)
  • Update the OP_CODE_MAP if the shop adds new services or changes op codes
  • Apply Samsung tablet OS security patches via Knox MDM push
  • Update NotePin firmware if PLAUD releases updates
  • Review and rotate API keys (quarterly minimum)

4. QUARTERLY TASKS (2-4 hours/quarter)

  • Full system health review: transcription accuracy spot-check (10 random walkarounds), DMS sync verification, audio retention compliance audit
  • Update Deepgram SDK and OpenAI SDK to latest versions — test in staging before production
  • Review pricing changes from API providers (Deepgram, OpenAI) and adjust client billing if needed
  • Conduct penetration test or vulnerability scan per FTC Safeguards Rule requirements
  • Generate quarterly compliance report for shop owner's WISP documentation

5. MODEL/PROMPT RETRAINING TRIGGERS

  • Advisor accuracy satisfaction drops below 80% (measured by edit rate on draft ROs)
  • Shop adds a new service category (e.g., starts doing EV work, adds ADAS calibration)
  • DMS vendor releases API changes (monitor Tekmetric/Shop-Ware changelogs)
  • OpenAI releases a new model version — test new model against 20 sample transcripts before switching
  • Deepgram releases a new Nova version — test in parallel before cutover

6. SLA CONSIDERATIONS

  • System-down (no recordings possible): 1-hour response, 4-hour resolution target
  • Degraded service (slow pipeline, DMS push failing): 4-hour response, next-business-day resolution
  • Feature request or prompt tuning: Included in monthly managed service, implemented within 5 business days
  • Hardware replacement (broken NotePin or tablet): Ship replacement next business day, MSP keeps 1 spare NotePin and 1 spare tablet in inventory per 3 client shops

7. ESCALATION PATHS

1
Level 1: Advisor self-service (restart device, re-pair Bluetooth) — documented on quick-reference card
2
Level 2: MSP remote support (SSH/remote desktop into middleware, check logs, restart services)
3
Level 3: MSP on-site (hardware replacement, network troubleshooting, WiFi adjustment)
4
Level 4: Vendor escalation (Deepgram support for transcription issues, OpenAI for API issues, Tekmetric for DMS API issues, Samsung Knox support for MDM issues)

8. ANNUAL REVIEW

  • Full ROI analysis: time saved × advisor hourly cost × work days = annual savings vs. system cost
  • Technology refresh assessment: evaluate new STT models, LLM improvements, hardware upgrades
  • Contract renewal and pricing adjustment based on actual usage data
  • FTC Safeguards Rule annual compliance review with shop's Qualified Individual

Alternatives

Philips PSM5000 Premium Audio Capture

Replace the PLAUD NotePin ($179) with the Philips SpeechMike Ambient PSM5000 ($457). The PSM5000 features four beam-forming microphones, ambient sound intelligence that automatically creates separate audio streams for each speaker, and healthcare-grade noise cancellation. Designed for professional ambient recording in noisy environments.

Note

Tradeoffs: Cost is 2.5× higher per advisor ($457 vs $179), but audio quality in extremely noisy shop environments is significantly better. The PSM5000's built-in speaker separation may improve Deepgram's diarization accuracy. Best for high-volume dealerships with 6+ bays and constant tool noise. Overkill for small independent shops with 2-3 bays. Also requires Philips SpeechLive subscription for firmware management.

On-Premises Whisper Transcription (Edge Compute)

Instead of sending audio to Deepgram's cloud API, run OpenAI's Whisper Large v3 model locally on an NVIDIA Jetson Orin Nano Super ($249) installed at the shop. Audio stays on-premises, eliminating cloud STT costs and addressing data privacy concerns for shops that do not want customer audio leaving their network.

AssemblyAI Universal-2 as Primary STT

Replace Deepgram Nova-3 with AssemblyAI's Universal-2 model as the transcription engine. AssemblyAI offers lower base pricing ($0.0025/min vs $0.0077/min) and includes built-in features like topic detection and entity extraction that could supplement the LLM's RO extraction.

Full SaaS Platform: Impel Service AI or AutoVitals

Instead of building a custom solution, deploy an existing automotive-specific SaaS platform like Impel Service AI or AutoVitals' Digital Shop. These platforms provide digital vehicle inspections, workflow management, and some AI-assisted features out of the box, with pre-built DMS integrations.

CDK Drive Fortellis Integration (Franchise Dealerships)

For franchise dealerships running CDK Drive DMS, integrate via the Fortellis developer platform using the Service Repair Order Async API instead of the Tekmetric/Shop-Ware APIs. CDK covers approximately 50% of the US dealer market.

Mobile App (React Native) Instead of PWA

Build a native mobile app using React Native instead of a Progressive Web App. Distribute via Samsung Galaxy Store or direct APK sideloading through Knox MDM.

Want early access to the full toolkit?