From f7b9a51c47fb04648336d6574c8ce780b8e4d7d5 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Tue, 31 Mar 2026 05:12:56 +0530 Subject: [PATCH] fix: sanitize noisy OCR output for OPD scan AI results - add sanitizeExtractedScanText() to clean OCR text before analysis/storage - strip non-ASCII/control noise that appears as mixed-script junk characters - remove repeated junk tokens (e.g. ffffff, repeated words) - add ECG-specific OCR normalization (KECG->ECG, Uniconfirmed->Unconfirmed, avt->aVL, VT->V1) - apply sanitization to both OCR sources (document intelligence and vision fallback) - keep fallback behavior when sanitization returns empty --- app/Controllers/Opd.php | 49 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/app/Controllers/Opd.php b/app/Controllers/Opd.php index f3aab31..e78f75b 100644 --- a/app/Controllers/Opd.php +++ b/app/Controllers/Opd.php @@ -4584,18 +4584,63 @@ class Opd extends BaseController $docIntelText = $this->extractTextWithAzureDocumentIntelligence($filePath); if ($docIntelText !== '') { - return preg_replace('/\s+/', ' ', trim($docIntelText)) ?: trim($docIntelText); + $clean = $this->sanitizeExtractedScanText($docIntelText, $scanType); + return $clean !== '' ? $clean : (preg_replace('/\s+/', ' ', trim($docIntelText)) ?: trim($docIntelText)); } $azurePrompt = 'Read and extract all visible clinical text from this ' . strtoupper($scanType) . ' medical image. Return plain text only. If unclear, return best readable lines.'; $azureText = $this->callAzureOpenAiVision($filePath, $mimeType, $azurePrompt, 1800, false); if ($azureText !== '') { - return preg_replace('/\s+/', ' ', trim($azureText)) ?: trim($azureText); + $clean = $this->sanitizeExtractedScanText($azureText, $scanType); + return $clean !== '' ? $clean : (preg_replace('/\s+/', ' ', trim($azureText)) ?: trim($azureText)); } return null; } + /** + * Clean OCR output so OPD UI stores readable clinical text only. + */ + private function sanitizeExtractedScanText(string $text, string $scanType): string + { + $text = trim($text); + if ($text === '') { + return ''; + } + + // Remove control and non-ASCII characters that usually come from OCR noise. + $text = preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', ' ', $text) ?? $text; + + // Drop repeated-char junk words like "ffffff". + $text = preg_replace('/\b([A-Za-z])\1{4,}\b/', ' ', $text) ?? $text; + + // Collapse very long repeated words into one token. + $text = preg_replace('/\b([A-Za-z]{2,})\b(?:\s+\1\b){2,}/i', '$1', $text) ?? $text; + + $isEcg = strtolower(trim($scanType)) === 'ecg' || stripos($text, 'ecg') !== false; + if ($isEcg) { + // Common OCR fixes for ECG templates. + $replacements = [ + '/\bKECG\b/i' => 'ECG', + '/\bUniconfirmed\b/i' => 'Unconfirmed', + '/\bavt\b/i' => 'aVL', + '/\baVR\b/i' => 'aVR', + '/\baVF\b/i' => 'aVF', + '/\baVL\b/i' => 'aVL', + '/\bVT\b/' => 'V1', + ]; + + foreach ($replacements as $pattern => $replacement) { + $text = preg_replace($pattern, $replacement, $text) ?? $text; + } + } + + // Normalize whitespace for compact storage/response. + $text = preg_replace('/\s+/', ' ', trim($text)) ?? trim($text); + + return $text; + } + /** * @return array{updated:bool,session_id:int,target_field:string} */