
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
$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
$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)
$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
$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
$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
$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
$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
$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
$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)
$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.
Vercel or Railway (Cloud Hosting for Middleware)
$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.
Supabase (Database & Auth)
$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)
$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.
netspot --survey --export heatmap_service_lane.pdfIf 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.
# 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: DropVLAN 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.
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.
-- 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/webmThe 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 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
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
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 CLIThe 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.
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.
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.
# 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 approvalDMS 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.
# 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.comRailway 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 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)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.
Step 12: Install Consent Signage and Update Intake Forms
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.
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).
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).
- 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)
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
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
// 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
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
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
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
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:
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
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.
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?