Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ object MarginAnnotationParser {

val distribution = distributeDetections(sanitizedDetections, imageWidth, leftGuidePct, rightGuidePct)
val canvasTags = extractCanvasTags(distribution.canvas)
val (leftCanvasTags, rightCanvasTags) = splitCanvasTags(canvasTags, imageWidth, leftGuidePct, rightGuidePct)

val annotationMap = mutableMapOf<String, String>()
annotationMap.putAll(parseMarginGroup(distribution.leftMargin, leftCanvasTags))
annotationMap.putAll(parseMarginGroup(distribution.rightMargin, rightCanvasTags))
val annotationMap = parseMarginsGlobally(
leftMargin = distribution.leftMargin,
rightMargin = distribution.rightMargin,
canvasTags = canvasTags
)

return Pair(distribution.canvas, annotationMap)
}
Expand Down Expand Up @@ -76,41 +77,43 @@ object MarginAnnotationParser {
}

/**
* Splits the extracted canvas tags into left and right groups based on the canvas midpoint.
* Processes both margins simultaneously to prevent cross-margin collisions.
* Gathers all explicit annotations first, then resolves all implicit blocks
* against a shared pool of remaining tags.
*/
private fun splitCanvasTags(
canvasTags: List<Pair<String, DetectionResult>>,
imageWidth: Int,
leftGuidePct: Float,
rightGuidePct: Float
): Pair<List<Pair<String, DetectionResult>>, List<Pair<String, DetectionResult>>> {
val canvasMidX = imageWidth * (leftGuidePct + rightGuidePct) / 2f
val leftTags = canvasTags.filter { (_, det) -> centerX(det) < canvasMidX }
val rightTags = canvasTags.filter { (_, det) -> centerX(det) >= canvasMidX }
return Pair(leftTags, rightTags)
}

/**
* Processes a specific margin (left or right), extracting explicit annotations and
* resolving implicit ones against the available canvas tags.
*/
private fun parseMarginGroup(
detections: List<DetectionResult>,
private fun parseMarginsGlobally(
leftMargin: List<DetectionResult>,
rightMargin: List<DetectionResult>,
canvasTags: List<Pair<String, DetectionResult>>
): Map<String, String> {
if (detections.isEmpty()) return emptyMap()
val leftBlocks = extractBlocks(leftMargin.sortedBy { it.boundingBox.top })
val rightBlocks = extractBlocks(rightMargin.sortedBy { it.boundingBox.top })

val sortedDetections = detections.sortedBy { it.boundingBox.top }
val globalExplicitAnnotations = mergeAnnotations(
leftBlocks.explicitAnnotations,
rightBlocks.explicitAnnotations
)

val groupedBlocks = extractBlocks(sortedDetections)
val allImplicitBlocks = leftBlocks.implicitBlocks + rightBlocks.implicitBlocks

val resolvedImplicitAnnotations = resolveImplicitBlocks(
implicitBlocks = groupedBlocks.implicitBlocks,
implicitBlocks = allImplicitBlocks,
canvasTags = canvasTags,
existingAnnotations = groupedBlocks.explicitAnnotations
existingAnnotations = globalExplicitAnnotations
)

return groupedBlocks.explicitAnnotations + resolvedImplicitAnnotations
return mergeAnnotations(globalExplicitAnnotations, resolvedImplicitAnnotations)
}

/**
* Merges multiple annotation maps. If a tag exists in multiple maps,
* their values are combined separated by " | ".
*/
private fun mergeAnnotations(vararg maps: Map<String, String>): MutableMap<String, String> {
return maps.flatMap { it.toList() }
.groupBy({ it.first }, { it.second })
.mapValues { (_, values) -> values.joinToString(" | ") }
.toMutableMap()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ package org.appdevforall.codeonthego.computervision.domain
*/
internal object WidgetTagParser {
private val tagRegex = Regex("^(?i)(B|P|D|T|C|R|SW|S)-[A-Z0-9_]+$")
private val tagExtractRegex = Regex("^(?i)(SW|S\\s*8|8\\s*W|[BPDTCRS8]\\s*W?)([\\s\\-_]*)([A-Z0-9_\\-]+)")
private val tagExtractRegex = Regex("^(?i)([A-Z0-9\\s]+)([\\s\\-_.]+)([A-Z0-9_\\-]+)")
private val VALID_PREFIXES = setOf("B", "P", "D", "T", "C", "R", "SW", "S")

fun isTag(text: String): Boolean {
val cleaned = text.trim().trimEnd('.', ',', ';', ':', '_', '|')
Expand All @@ -20,8 +21,12 @@ internal object WidgetTagParser {
return normalizeTagText(cleaned).matches(tagRegex)
}

private fun parseTagParts(match: MatchResult): Pair<String, String> {
val prefix = normalizePrefix(match.groupValues[1])
private fun parseTagParts(match: MatchResult): Pair<String, String>? {
val rawPrefix = match.groupValues[1]
val prefix = normalizePrefix(rawPrefix)

if (prefix !in VALID_PREFIXES) return null

var tokenRaw = match.groupValues[3].trim('-')

val upperToken = tokenRaw.uppercase()
Expand All @@ -46,8 +51,9 @@ internal object WidgetTagParser {

if (!isValidTagMatch(match)) return cleaned.uppercase()

val (prefix, token) = parseTagParts(match)
return "$prefix-$token"
val parts = parseTagParts(match) ?: return cleaned.uppercase()

return "${parts.first}-${parts.second}"
}

fun extractTag(text: String): Pair<String, String?>? {
Expand All @@ -56,8 +62,9 @@ internal object WidgetTagParser {

if (!isValidTagMatch(match)) return null

val (prefix, token) = parseTagParts(match)
val finalTag = "$prefix-$token"
val parts = parseTagParts(match) ?: return null

val finalTag = "${parts.first}-${parts.second}"

if (!finalTag.matches(tagRegex)) return null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package org.appdevforall.codeonthego.computervision.domain.parser

import com.itsaky.androidide.fuzzysearch.FuzzySearch
import org.appdevforall.codeonthego.computervision.domain.grammar.UiGrammarValidator
import org.appdevforall.codeonthego.computervision.domain.parser.sanitizer.OcrSanitizerFactory
import java.lang.StringBuilder

object FuzzyAttributeParser {
private val grammarValidator = UiGrammarValidator()
private const val PIPE_DELIMITER = "|"
private val multipleUnderscoresRegex = Regex("_+")
private val inputTypeValues = InputTypeValueSet.values.map { it.lowercase() }.toSet()
private val sanitizer = OcrSanitizerFactory.createDefaultSanitizer()

private val cleaners = mapOf(
ValueType.TEXT_CONTENT to TextContentCleaner,
Expand Down Expand Up @@ -36,10 +38,7 @@ object FuzzyAttributeParser {
}

private fun tokenizeAnnotation(annotation: String): List<String> {
val sanitized = annotation
.replace(Regex("(?i)backgroundired"), "background red")
.replace(Regex("(?i)backgroundred"), "background red")
.replace(Regex("(?i)horizontal\\s+gravity\\s*:\\s*center\\s+layout"), "layout_gravity: center_horizontal")
val sanitized = sanitizer.sanitize(annotation)

return if (sanitized.contains(PIPE_DELIMITER)) {
sanitized.split(PIPE_DELIMITER).map { it.trim() }.filter { it.isNotEmpty() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer


class CompositeOcrSanitizer(
private val sanitizers: List<OcrSanitizer>
) : OcrSanitizer {
override fun sanitize(input: String): String {
return sanitizers.fold(input) { acc, sanitizer ->
sanitizer.sanitize(acc)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer


interface OcrSanitizer {
fun sanitize(input: String): String
}

abstract class DictionaryRegexSanitizer : OcrSanitizer {
protected abstract val rawRules: Map<String, String>

private val compiledRules: List<Pair<Regex, String>> by lazy {
rawRules.map { (pattern, replacement) ->
Regex(pattern, RegexOption.IGNORE_CASE) to replacement
}
}

override fun sanitize(input: String): String {
return compiledRules.fold(input) { acc, (regex, replacement) ->
acc.replace(regex, replacement)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer


object OcrSanitizerFactory {
fun createDefaultSanitizer(): OcrSanitizer {
return CompositeOcrSanitizer(
listOf(
ColorSanitizer(),
TextAttributeSanitizer(),
DimensionSanitizer(),
MarginPaddingSanitizer(),
StructureSanitizer()
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer


class ColorSanitizer : DictionaryRegexSanitizer() {
override val rawRules = mapOf(
"backgroundired" to "background red",
"backgroundred" to "background red"
)
}

class TextAttributeSanitizer : DictionaryRegexSanitizer() {
override val rawRules = mapOf(
"text\\s*st[yj]l?e?" to "text_style"
)
}

class DimensionSanitizer : DictionaryRegexSanitizer() {
override val rawRules = mapOf(
"[il]ayout\\.?\\s*w[io]l?[td]h\\.?" to "layout_width:",
"layout\\s*hei[sck]+t\\.?" to "layout_height:",
"m?w?at[ce]h[-_\\s]?p[ar]+ent" to "match_parent"
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

class MarginPaddingSanitizer : DictionaryRegexSanitizer() {
override val rawRules = mapOf(
"layout_margin\\s+(top|bottom|start|end|left|right)" to "layout_margin_$1",
"padding\\s+(top|bottom|start|end|left|right)" to "padding_$1"
)
}

class StructureSanitizer : DictionaryRegexSanitizer() {
override val rawRules = mapOf(
"horizontal\\s+gravity\\s*:\\s*center\\s+layout" to "layout_gravity: center_horizontal"
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.appdevforall.codeonthego.computervision.domain.parser.sanitizer

import org.junit.Assert.assertEquals
import org.junit.Test

class OcrSanitizerRulesTest {

@Test
fun `color sanitizer fixes merged background color tokens`() {
val sanitizer = ColorSanitizer()

assertEquals("background red", sanitizer.sanitize("backgroundired"))
assertEquals("background red", sanitizer.sanitize("backgroundred"))
}

@Test
Comment thread
jatezzz marked this conversation as resolved.
fun `text attribute sanitizer normalizes OCR variants of text style`() {
val sanitizer = TextAttributeSanitizer()

assertEquals("text_style", sanitizer.sanitize("text style"))
assertEquals("text_style", sanitizer.sanitize("text stjle"))
}

@Test
fun `dimension sanitizer fixes width and height OCR mistakes`() {
val sanitizer = DimensionSanitizer()

assertEquals("layout_width: 120dp", sanitizer.sanitize("iayout widh. 120dp"))
assertEquals("layout_height: 48dp", sanitizer.sanitize("layout heist. 48dp"))
}

@Test
fun `dimension sanitizer normalizes match parent OCR variants`() {
val sanitizer = DimensionSanitizer()

assertEquals("layout_width: match_parent", sanitizer.sanitize("layout_width: match parent"))
assertEquals("layout_width: match_parent", sanitizer.sanitize("layout_width: match-parrent"))
}

@Test
fun `margin and padding sanitizer preserves edge names`() {
val sanitizer = MarginPaddingSanitizer()

assertEquals("layout_margin_top: 16dp", sanitizer.sanitize("layout_margin top: 16dp"))
assertEquals("padding_end: 8dp", sanitizer.sanitize("padding end: 8dp"))
}

@Test
fun `structure sanitizer rewrites horizontal center layout phrase`() {
val sanitizer = StructureSanitizer()

assertEquals(
"layout_gravity: center_horizontal",
sanitizer.sanitize("horizontal gravity: center layout")
)
}

@Test
fun `default sanitizer applies multiple OCR cleanup rules in sequence`() {
val sanitizer = OcrSanitizerFactory.createDefaultSanitizer()
val input = "backgroundired | text stjle: bold | iayout widh. match parrent | padding end: 8dp"

assertEquals(
"background red | text_style: bold | layout_width: match_parent | padding_end: 8dp",
sanitizer.sanitize(input)
)
}
}
Loading