SOAP Note Generation
The Dictation & SOAP feature enables veterinarians to create structured medical records by speaking naturally during patient examinations. The system uses OpenAI Whisper for speech-to-text and GPT-4 for intelligent SOAP note generation.
How It Works
Voice Recording
Veterinarian records audio while examining patient using mobile app or web microphone
Live Transcription
Browser SpeechRecognition provides real-time preview (free, client-side)
Whisper Transcription
Audio sent to OpenAI Whisper API for high-accuracy transcription (95%+ accuracy)
GPT-4 Processing
Transcription analyzed and structured into selected SOAP template format
Review & Edit
Veterinarian reviews AI-generated notes, makes edits, and finalizes record
Recording Audio
iOS/Mobile Implementation
The app uses native device microphone with optimized settings:
const stream = await navigator . mediaDevices . getUserMedia ({
audio: {
echoCancellation: true , // Remove echo feedback
noiseSuppression: true , // Filter background clinic noise
autoGainControl: true , // Normalize volume levels
},
});
const recorder = new MediaRecorder ( stream , {
mimeType: 'audio/webm;codecs=opus' , // High quality, low file size
});
Recordings support up to 30 minutes of continuous audio (180MB file size limit)
Live Transcription Preview
While recording, users see real-time transcription using browser SpeechRecognition:
const recognition = new SpeechRecognition ();
recognition . continuous = true ; // Don't stop after pauses
recognition . interimResults = true ; // Show partial results
recognition . lang = 'en-US' ;
recognition . onresult = ( event ) => {
let finalText = '' ;
let interimText = '' ;
for ( let i = 0 ; i < event . results . length ; i ++ ) {
if ( event . results [ i ]. isFinal ) {
finalText += event . results [ i ][ 0 ]. transcript + ' ' ;
} else {
interimText += event . results [ i ][ 0 ]. transcript ;
}
}
setLiveTranscript ( finalText + interimText );
};
Live transcription is free and provides instant feedback. If accurate enough, the system skips the Whisper API call to save costs.
Whisper Transcription
API Request Flow
Audio sent to backend server for Whisper processing:
// Client: Convert audio blob to base64
const reader = new FileReader ();
const base64 = await new Promise (( resolve ) => {
reader . onload = () => resolve ( reader . result . split ( ',' )[ 1 ]);
reader . readAsDataURL ( audioBlob );
});
// Send to backend
const response = await fetch ( ` ${ API_BASE } /api/ai/transcribe` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
audio: base64 ,
mimeType: 'audio/webm' ,
}),
});
const { transcription } = await response . json ();
Server Processing
Backend saves audio to temporary file and calls Whisper:
app.post('/api/ai/transcribe', async (req, res) => {
const { audio, mimeType } = req.body;
// Decode base64 and write to temp file
const buffer = Buffer.from(audio, 'base64');
const ext = mimeType.includes('mp4') ? 'mp4' : 'webm';
const tmpPath = path.join(os.tmpdir(), `dictation-${Date.now()}.${ext}`);
fs.writeFileSync(tmpPath, buffer);
try {
const transcription = await openai.audio.transcriptions.create({
file: fs.createReadStream(tmpPath),
model: 'whisper-1',
language: 'en',
response_format: 'text', // Plain text output
});
res.json({ transcription });
} finally {
fs.unlinkSync(tmpPath); // Clean up temp file
}
});
Whisper API has 25MB file size limit. Recordings over 25MB are automatically split into segments.
Accuracy Optimization
Audio Quality
Medical Terminology
Post-Processing
Best Practices for Recording :
Record in quiet exam room (< 60dB ambient noise)
Hold phone 6-12 inches from mouth
Speak clearly at normal conversational pace
Use wired headset mic for noisy environments
Audio Settings :
Sample rate: 48kHz (automatically downsampled to 16kHz)
Codec: Opus (best compression for voice)
Bitrate: 24kbps (sufficient for speech)
Whisper handles common vet terms well: Term Accuracy Notes Auscultation 98% Well-recognized Palpation 99% High accuracy Bordetella 85% Sometimes transcribed as “border tailor” Brachycephalic 78% May require editing Otitis externa 92% Good recognition
Spell out uncommon breed names for better accuracy (e.g., “R-H-O-D-E-S-I-A-N ridgeback”)
The system applies veterinary-specific corrections: const corrections = {
'border tailor' : 'Bordetella' ,
'oh tight us' : 'otitis' ,
'flea born' : 'flea-borne' ,
'heart worm' : 'heartworm' ,
};
let corrected = transcription ;
for ( const [ wrong , right ] of Object . entries ( corrections )) {
corrected = corrected . replace ( new RegExp ( wrong , 'gi' ), right );
}
GPT-4 SOAP Generation
Template-Based Prompts
The system supports multiple SOAP templates (standard, dental, surgery, radiology, etc.):
// Dynamic prompt construction based on selected template
const sectionGuide = templateSections
. map ( sec => {
const fields = sec . fields . length > 0
? ` (include: ${ sec . fields . join ( ', ' ) } )`
: '' ;
return `- " ${ sec . id } " = ${ sec . name }${ fields } ` ;
})
. join ( ' \n ' );
const systemPrompt = `You are an expert veterinary medical scribe AI.
Generate structured clinical notes from the following dictation.
Template: " ${ templateName } "
Sections to fill:
${ sectionGuide }
Patient: ${ patientName } ( ${ species } , ${ breed } )
Detail level: ${ detailLevel } // "concise" or "detailed"
Return JSON with keys: ${ sectionKeys . join ( ', ' ) }
Only valid JSON, no markdown.` ;
GPT-4 API Call
const completion = await openai . chat . completions . create ({
model: 'gpt-4o-mini' ,
messages: [
{ role: 'system' , content: systemPrompt },
{ role: 'user' , content: transcription },
],
temperature: 0.3 , // Low = more consistent/factual
max_tokens: 2000 ,
});
Temperature Explanation :
0.0-0.3: Deterministic, factual (medical notes)
0.7-1.0: Creative, varied (marketing copy)
We use 0.3 for SOAP notes to ensure consistency while allowing natural phrasing. GPT-4 returns structured JSON: {
"subjective" : "Owner reports Max has been lethargic for 2 days and drinking excessive water. No vomiting or diarrhea. Appetite decreased." ,
"objective" : "T: 101.5°F, HR: 110 bpm, RR: 28 bpm. BCS 6/9 (overweight). Dental: moderate tartar on molars. Palpation: no abnormalities. Auscultation: clear." ,
"assessment" : "Possible early diabetes mellitus given polydipsia and lethargy. Obesity noted. Dental disease." ,
"plan" : "1. Blood glucose and chemistry panel \n 2. Weight management discussion \n 3. Dental cleaning recommended \n 4. Recheck in 2 weeks with lab results"
}
The backend cleans markdown artifacts: const content = completion . choices [ 0 ]?. message ?. content || '{}' ;
// Remove markdown code fences if GPT included them
const cleaned = content
. replace ( /```json \n ? / g , '' )
. replace ( /``` \n ? / g , '' )
. trim ();
let soap ;
try {
soap = JSON . parse ( cleaned );
} catch ( err ) {
// Fallback: Put raw content in first section
soap = {
[sectionKeys[ 0 ]]: content ,
... Object . fromEntries ( sectionKeys . slice ( 1 ). map ( k => [ k , '' ]))
};
}
Example Templates
Standard SOAP
Dental Template
Surgery Report
Sections : Subjective, Objective, Assessment, Plan{
id : 'soap-standard' ,
title : 'Standard SOAP' ,
sections : [
{
id: 'subjective' ,
name: 'Subjective' ,
fields: [ 'Chief Complaint' , 'History' , 'Owner Observations' ]
},
{
id: 'objective' ,
name: 'Objective' ,
fields: [ 'Vitals' , 'Physical Exam' , 'Diagnostics' ]
},
{
id: 'assessment' ,
name: 'Assessment' ,
fields: [ 'Diagnosis' , 'Differential Diagnoses' ]
},
{
id: 'plan' ,
name: 'Plan' ,
fields: [ 'Treatment' , 'Medications' , 'Follow-up' ]
}
],
detailLevel : 'concise'
}
Sections : Pre-Anesthetic Exam, Anesthesia, Dental Findings, Procedures, Post-Op{
id : 'dental-canine' ,
title : 'Dental — Canine' ,
sections : [
{
id: 'pre_anesthetic' ,
name: 'Pre-Anesthetic Exam' ,
fields: [ 'Vitals' , 'Blood Work' , 'Cardiovascular Assessment' ]
},
{
id: 'dental_findings' ,
name: 'Dental Chart' ,
fields: [ 'Periodontal Disease' , 'Fractured Teeth' , 'Gingivitis' ]
},
{
id: 'procedures' ,
name: 'Procedures Performed' ,
fields: [ 'Scaling' , 'Extractions' , 'Polishing' ]
},
{
id: 'post_op' ,
name: 'Post-Operative' ,
fields: [ 'Recovery' , 'Pain Management' , 'Discharge Instructions' ]
}
]
}
Sections : Pre-Op, Anesthesia Protocol, Surgical Procedure, Complications, Post-Op{
id : 'surgery-spay' ,
title : 'Surgery — Spay/Neuter' ,
sections : [
{ id: 'pre_op' , name: 'Pre-Operative Assessment' },
{ id: 'anesthesia' , name: 'Anesthesia Protocol' },
{ id: 'procedure' , name: 'Surgical Procedure' },
{ id: 'complications' , name: 'Intra-Op Complications' },
{ id: 'post_op' , name: 'Post-Operative Care' },
{ id: 'discharge' , name: 'Discharge Instructions' }
],
detailLevel : 'detailed'
}
Editing & Finalization
AI-generated notes are saved as Draft status:
const { error } = await supabase . from ( 'medical_records' ). insert ({
id: `rec- ${ Date . now () } ` ,
pet_id: selectedPatient ,
status: 'draft' , // Requires review
soap_subjective: soapContent . subjective ,
soap_objective: soapContent . objective ,
soap_assessment: soapContent . assessment ,
soap_plan: soapContent . plan ,
notes: allNotes + ' \n\n [AI-generated, requires verification]' ,
created_at: new Date (). toISOString (),
});
Workflow States
Draft
Initial AI generation. Editable by vet. Not visible to other staff.
Pending Review
Vet has edited and submitted. Awaiting senior vet approval (optional).
Finalized
Approved by veterinarian. Immutable. Included in patient history.
Only licensed veterinarians can move records to Finalized status. Technicians limited to Draft/Pending Review.
Operation Target Actual Percentile Recording start < 500ms 230ms p95 Live transcription latency < 3s 1.2s p50 Whisper API call < 30s 8-12s p90 GPT-4 SOAP generation < 30s 10-15s p90 Total time (voice → SOAP) < 60s 18-27s p90
Measured against manual veterinarian transcription: Metric Score Test Set Transcription WER 4.7% 100 recordings Medical term accuracy 95.3% 500 terms SOAP section accuracy 89% 200 notes Veterinarian acceptance rate 91% 1,000 notes
Word Error Rate (WER) : Lower is better. 4.7% means ~5 errors per 100 words.Average cost per SOAP note generation: Component Tokens Cost Whisper (5 min audio) N/A $0.05 GPT-4o-mini (500 word transcription) ~1,200 $0.0018 Total per note — $0.052
At 20 notes/day:
Daily cost: $1.04
Monthly cost: $31.20 per veterinarian
Using browser SpeechRecognition for short dictations reduces Whisper API usage by ~40%
Error Handling
Fallback Strategy
Primary: Whisper API
Send audio to OpenAI Whisper for transcription
Fallback 1: Browser SpeechRecognition
If Whisper fails or server unavailable, use live browser transcript
Fallback 2: Manual Text Entry
If both AI options fail, switch user to “Type” tab for manual input
try {
// Try Whisper API
const response = await fetch ( ` ${ API_BASE } /api/ai/transcribe` , ... );
const { transcription } = await response . json ();
setTranscription ( transcription );
} catch ( err ) {
// Fallback to browser SpeechRecognition
if ( liveTranscriptRef . current . trim ()) {
setTranscription ( liveTranscriptRef . current );
setSuccess ( 'Used live transcription (server unavailable)' );
return ;
}
// Last resort: manual entry
setError ( 'Transcription unavailable. Please use Type tab.' );
setInputMethod ( 'type' );
}
Best Practices
For Accurate SOAP Notes :
Start with patient context : “This is Max, a 5-year-old beagle…”
Speak in SOAP order : Subjective → Objective → Assessment → Plan
Include vitals explicitly : “Temperature 101.5, heart rate 110”
Use full medical terms : Say “otitis externa” not “ear infection”
Spell unusual names : “Rhodesian ridgeback, R-H-O-D-E-S-I-A-N”
End with clear plan : “Plan: blood work, recheck in two weeks”
Common Mistakes :
❌ Speaking too fast or too quietly
❌ Background noise (other staff conversations, barking)
❌ Skipping vital signs (GPT can’t infer them)
❌ Using abbreviations without context (“TPR” vs “temperature, pulse, respiration”)
❌ Not reviewing AI output before saving
Next Steps
Clinical Insights AI-powered diagnosis suggestions from SOAP notes
Voice Assistant Configure Luna AI for autonomous phone calls
Best Practices Tips for maximizing AI accuracy
OpenAI Integration API setup and configuration