Move SSD OCR application loop to typed Kotlin

ober

6c4ec13a60efa952876979c0fe74494fe1436fd7

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 24acb0f..e193b92 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1450,12 +1450,18 @@
     (typed-kotlin-file "com/sfb/ssdreview/OcrCandidateSelect.kt"
       (kotlin-imports (android content Context))
       (typed-library (com sfb ssdreview)
-        (export bestOcrCandidate)
+        (export bestOcrCandidate applyOcrGuesses)
         (type Context)
         (type Int32)
         (type Float32)
+        (type FloatArray)
         (extern (boxTypesResolve (context : Context) (raw : String)) : (Nullable BoxType)
           (kotlin-call BoxTypes resolve))
+        (extern (nearbyTextForOcr (words : (MutableList OcrWord))
+                                  (bbox : FloatArray)
+                                  (imageWidth : Int32)
+                                  (imageHeight : Int32)) : String
+          (kotlin-call nearbyText))
         (def (ocrPatternMatches (pattern : OcrPattern) (text : String)) : Bool
           (for/fold ((matched #f))
                     ((i (in-range (int32 0) (list-size (OcrPattern-tokens pattern)))))
@@ -1517,7 +1523,57 @@
                     race
                     seen
                     best
-                    (list-ref patterns i))))))))
+                    (list-ref patterns i))))))
+        (def (ocrCandidateCanReplace (group : SsdGroup)
+                                     (candidate : OcrCandidate)) : Bool
+          (if (bankCompatible (SsdGroup-label group) (SsdGroup-boxTypeId group))
+              (bankCompatible
+                (OcrCandidate-label candidate)
+                (OcrCandidate-boxTypeId candidate))
+              #t))
+        (def (applyOcrGuessForGroup (context : Context)
+                                    (session : SsdSession)
+                                    (patterns : (List OcrPattern))
+                                    (race : String)
+                                    (group : SsdGroup)) : Int32
+          (if (shouldReplaceOcrLabel
+                (SsdGroup-label group)
+                (SsdGroup-status group)
+                (SsdGroup-notes group))
+              (let ((text
+                      (nearbyTextForOcr
+                        (SsdSession-ocrWords session)
+                        (SsdGroup-bbox group)
+                        (SsdSession-imageWidth session)
+                        (SsdSession-imageHeight session))))
+                (let ((best (bestOcrCandidate context patterns text race)))
+                  (if (nullable-null? best)
+                      (int32 0)
+                      (if (ocrCandidateCanReplace group (nullable-get best))
+                          (begin
+                            (applyOcrCandidateToGroup group (nullable-get best))
+                            (int32 1))
+                          (int32 0)))))
+              (int32 0)))
+        (def (applyOcrGuesses (context : Context)
+                              (session : SsdSession)
+                              (patterns : (List OcrPattern))) : Int32
+          (if (= (list-size (SsdSession-ocrWords session)) (int32 0))
+              (int32 0)
+              (let ((race
+                      (raceKey
+                        (string-append
+                          (SsdSession-sourceName session)
+                          (string-append " " (SsdSession-sourceUri session))))))
+                (for/fold ((applied (int32 0)))
+                          ((i (in-range (int32 0) (list-size (SsdSession-groups session)))))
+                  (+ applied
+                     (applyOcrGuessForGroup
+                       context
+                       session
+                       patterns
+                       race
+                       (list-ref (SsdSession-groups session) i)))))))))
 
     (typed-kotlin-file "com/sfb/ssdreview/LearnedSuppression.kt"
       (kotlin-imports (org json JSONObject))
@@ -3549,7 +3605,7 @@
        "                    }"
        "                    applyInitialGuesses(this, session)"
        "                    session.ocrWords.addAll(SsdOcr.recognizeWords(bitmap))"
-       "                    ocrGuesses = SsdOcr.applyOcrGuesses(this, session)"
+       "                    ocrGuesses = applyOcrGuesses(this, session, SsdOcr.patterns)"
        "                }"
        "                val learnedGuesses = if (existingTruth == null || existingTruthGroupCount == 0) truthStore.applyLearnedGuesses(session) else 0"
        "                runOnUiThread {"
@@ -4767,7 +4823,7 @@
        "import com.google.mlkit.vision.text.latin.TextRecognizerOptions"
        ""
        "object SsdOcr {"
-       "    private val patterns = listOf("
+       "    val patterns = listOf("
        "        OcrPattern(listOf(\"PH 1\", \"PH1\", \"PHASER 1\", \"PHASER1\"), \"Phaser-1\", 0.94f),"
        "        OcrPattern(listOf(\"PH 2\", \"PH2\", \"PHASER 2\", \"PHASER2\"), \"Phaser-2\", 0.93f),"
        "        OcrPattern(listOf(\"PH 3\", \"PH3\", \"PHASER 3\", \"PHASER3\"), \"Phaser-3\", 0.93f),"
@@ -4830,21 +4886,6 @@
        "        }"
        "    }"
        ""
-       "    fun applyOcrGuesses(context: Context, session: SsdSession): Int {"
-       "        if (session.ocrWords.isEmpty()) return 0"
-       "        val race = raceKey(session.sourceName + \" \" + session.sourceUri)"
-       "        var applied = 0"
-       "        session.groups.forEach { group ->"
-       "            if (!shouldReplaceOcrLabel(group.label, group.status, group.notes)) return@forEach"
-       "            val text = nearbyText(session.ocrWords, group.bbox, session.imageWidth, session.imageHeight)"
-       "            val best = bestOcrCandidate(context, patterns, text, race) ?: return@forEach"
-       "            if (bankCompatible(group.label, group.boxTypeId) && !bankCompatible(best.label, best.boxTypeId)) return@forEach"
-       "            applyOcrCandidateToGroup(group, best)"
-       "            applied += 1"
-       "        }"
-       "        return applied"
-       "    }"
-       ""
        "}"
        ))
     (kotlin-file-lines "com/sfb/ssdreview/SsdReviewView.kt"