Move SSD truth shape reads to typed Kotlin

ober

efc152953f2c2cf548d0d5df76952b0c40c786ef

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 0e7f32f..d46a40e 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1594,9 +1594,11 @@
       (kotlin-imports (org json JSONArray) (org json JSONObject))
       (typed-library (com sfb ssdreview)
         (export truthJsonArrayOrEmpty truthJsonObjectOrEmpty
-                truthGroupCount imageWidth imageHeight)
+                truthGroupCount nullableTruthGroupCount truthSsdAreaRect
+                imageWidth imageHeight)
         (type JSONArray)
         (type JSONObject)
+        (type FloatArray)
         (type Int32)
         (def (truthJsonArrayOrEmpty (raw : (Nullable JSONArray))) : JSONArray
           (if (nullable-null? raw)
@@ -1614,6 +1616,12 @@
           (json-array-length
             (truthJsonArrayOrEmpty
               (json-object-opt-json-array truth "groups"))))
+        (def (nullableTruthGroupCount (truth : (Nullable JSONObject))) : Int32
+          (if (nullable-null? truth)
+              (int32 0)
+              (truthGroupCount (nullable-get truth))))
+        (def (truthSsdAreaRect (truth : JSONObject)) : (Nullable FloatArray)
+          (jsonRect (json-object-opt-json-array truth "ssd_area")))
         (def (imageWidth (truth : JSONObject)) : Int32
           (positiveOrOne
             (json-object-opt-int32-default
@@ -4738,7 +4746,7 @@
        "                )"
        ""
        "                val localTruth = if (forceDetect) null else truthStore.loadTruth(session)"
-	       "                val localGroupCount = localTruth?.optJSONArray(\"groups\")?.length() ?: 0"
+	       "                val localGroupCount = nullableTruthGroupCount(localTruth)"
 	       "                if (localGroupCount >= FAST_TRUTH_GROUP_THRESHOLD) {"
 	       "                    truthStore.applyTruth(session)"
 	      "                    ssdSessionPruneGroupsOutsideSsdArea(session)"
@@ -4758,19 +4766,19 @@
        ""
        "                val remoteMatches = 0"
        "                val existingTruth = localTruth ?: truthStore.loadTruth(session)"
-       "                val existingTruthGroupCount = existingTruth?.optJSONArray(\"groups\")?.length() ?: 0"
+       "                val existingTruthGroupCount = nullableTruthGroupCount(existingTruth)"
        "                var ocrGuesses = 0"
        "                var detectorNote = \"\""
        "                var detectorSource = \"unknown\""
        ""
 	       "                if (existingTruth != null && existingTruthGroupCount > 0 && !forceDetect) {"
 	       "                    detectorSource = \"local-truth\""
-	       "                    session.ssdArea = jsonRect(existingTruth.optJSONArray(\"ssd_area\"))"
+	       "                    session.ssdArea = truthSsdAreaRect(existingTruth)"
 	       "                    truthStore.applyTruth(session)"
 	      "                    ssdSessionPruneGroupsOutsideSsdArea(session)"
        "                } else {"
        "                    if (existingTruth != null) {"
-       "                        session.ssdArea = jsonRect(existingTruth.optJSONArray(\"ssd_area\"))"
+       "                        session.ssdArea = truthSsdAreaRect(existingTruth)"
        "                    }"
        "                    val nativeDetection = SsdVisionBridge.detect(bitmap)"
        "                    val detectedCells: List<SsdCell>"
@@ -4838,7 +4846,7 @@
        "                suppressedGroups = mutableListOf(),"
        "                ocrWords = session.ocrWords.toMutableList()"
        "            )"
-       "            merged.ssdArea = jsonRect(truth.optJSONArray(\"ssd_area\"))"
+       "            merged.ssdArea = truthSsdAreaRect(truth)"
        "            truthStore.applyTruth(merged)"
        "            ssdSessionPruneGroupsOutsideSsdArea(merged)"
        "            runOnUiThread {"