{"version":3,"file":"service/utils.mjs","sources":["../../../src/service/utils.ts"],"sourcesContent":["import {\n  AIResponseParseError,\n  extractJSONFromCodeBlock,\n} from '@/ai-model/service-caller';\nimport { expandSearchArea } from '@/common';\nimport type {\n  AIDescribeElementResponse,\n  DumpMeta,\n  PartialServiceDumpFromSDK,\n  Rect,\n  ServiceDump,\n  Size,\n} from '@/types';\nimport { uuid } from '@midscene/shared/utils';\n\nexport const DESCRIBE_POINT_MARKER_MAX_SIZE = 40;\nexport const DESCRIBE_RECT_MARKER_BORDER_THICKNESS = 1;\nexport const DESCRIBE_LARGE_RECT_MARKER_BORDER_THICKNESS = 2;\nexport const DESCRIBE_WIDE_MARKER_INSET_MIN_WIDTH = 100;\nconst DESCRIBE_WIDE_MARKER_HORIZONTAL_INSET_RATIO = 0.15;\nconst DESCRIBE_WIDE_MARKER_VERTICAL_INSET_RATIO = 0.1;\n\n// Deep describe uses two images: an overview for page-level ownership and a\n// focused crop for local detail. The overview keeps the original screenshot\n// size; only focused crops may be upscaled for small local details.\nexport const DESCRIBE_DEEP_CONTEXT_CONFIG = {\n  resize: {\n    cropMaxLongEdge: 1000,\n    cropUpscaleMaxRatio: 2,\n  },\n} as const;\n\nexport type DescribeDeepContextArea = {\n  kind: 'focused';\n  rect: Rect;\n};\n\nexport function clampRect(rect: Rect, size: Size): Rect {\n  const width = Math.min(rect.width, size.width);\n  const height = Math.min(rect.height, size.height);\n  return {\n    left: Math.max(0, Math.min(rect.left, size.width - width)),\n    top: Math.max(0, Math.min(rect.top, size.height - height)),\n    width,\n    height,\n  };\n}\n\nexport function getDescribeDeepContextAreas(\n  rect: Rect,\n  screenSize: Size,\n): DescribeDeepContextArea[] {\n  return [{ kind: 'focused', rect: expandSearchArea(rect, screenSize) }];\n}\n\nexport function getRectInCrop(\n  rect: Rect,\n  cropRect: Rect,\n  cropSize: Size,\n): Rect {\n  return clampRect(\n    {\n      left: rect.left - cropRect.left,\n      top: rect.top - cropRect.top,\n      width: rect.width,\n      height: rect.height,\n    },\n    cropSize,\n  );\n}\n\nexport function getDescribeMarkerRect(rect: Rect): Rect {\n  if (rect.width < DESCRIBE_WIDE_MARKER_INSET_MIN_WIDTH) {\n    return rect;\n  }\n\n  const horizontalInset = Math.round(\n    rect.width * DESCRIBE_WIDE_MARKER_HORIZONTAL_INSET_RATIO,\n  );\n  const verticalInset = Math.round(\n    rect.height * DESCRIBE_WIDE_MARKER_VERTICAL_INSET_RATIO,\n  );\n\n  return {\n    left: rect.left + horizontalInset,\n    top: rect.top + verticalInset,\n    width: Math.max(rect.width - horizontalInset * 2, 1),\n    height: Math.max(rect.height - verticalInset * 2, 1),\n  };\n}\n\nexport function getDescribeMarkerBorderThickness(rect: Rect): number {\n  return rect.width <= DESCRIBE_POINT_MARKER_MAX_SIZE &&\n    rect.height <= DESCRIBE_POINT_MARKER_MAX_SIZE\n    ? DESCRIBE_RECT_MARKER_BORDER_THICKNESS\n    : DESCRIBE_LARGE_RECT_MARKER_BORDER_THICKNESS;\n}\n\nexport function getDescribeDeepLocateResizeSize(size: Size): Size | undefined {\n  const maxEdge = Math.max(size.width, size.height);\n  if (!maxEdge) {\n    return undefined;\n  }\n  const { resize } = DESCRIBE_DEEP_CONTEXT_CONFIG;\n  const scale = Math.min(\n    resize.cropUpscaleMaxRatio,\n    resize.cropMaxLongEdge / maxEdge,\n  );\n  if (scale <= 1.05) {\n    return undefined;\n  }\n  return {\n    width: Math.round(size.width * scale),\n    height: Math.round(size.height * scale),\n  };\n}\n\nexport function createServiceDump(\n  data: PartialServiceDumpFromSDK,\n): ServiceDump {\n  const baseData: DumpMeta = {\n    logTime: Date.now(),\n  };\n  const finalData: ServiceDump = {\n    logId: uuid(),\n    ...baseData,\n    ...data,\n  };\n\n  return finalData;\n}\n\nfunction readNextSignificantChar(input: string, startIndex: number) {\n  let index = startIndex;\n  while (index < input.length && /\\s/.test(input[index])) {\n    index += 1;\n  }\n  return input[index];\n}\n\nfunction extractPossiblyMalformedStringField(input: string, fieldName: string) {\n  const escapedFieldName = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n  const fieldStart = new RegExp(`\"${escapedFieldName}\"\\\\s*:\\\\s*\"`).exec(input);\n  if (!fieldStart) {\n    return undefined;\n  }\n\n  let index = fieldStart.index + fieldStart[0].length;\n  let escaped = false;\n  let valueForJsonParse = '';\n\n  for (; index < input.length; index += 1) {\n    const char = input[index];\n\n    if (escaped) {\n      valueForJsonParse += char;\n      escaped = false;\n      continue;\n    }\n\n    if (char === '\\\\') {\n      valueForJsonParse += char;\n      escaped = true;\n      continue;\n    }\n\n    if (char !== '\"') {\n      valueForJsonParse += char;\n      continue;\n    }\n\n    const nextSignificantChar = readNextSignificantChar(input, index + 1);\n    if (\n      nextSignificantChar === ',' ||\n      nextSignificantChar === '}' ||\n      nextSignificantChar === ']' ||\n      nextSignificantChar === undefined\n    ) {\n      try {\n        return JSON.parse(`\"${valueForJsonParse}\"`);\n      } catch {\n        return valueForJsonParse;\n      }\n    }\n\n    valueForJsonParse += '\\\\\"';\n  }\n\n  return undefined;\n}\n\nexport function recoverDescribeResponseFromParseError(\n  error: unknown,\n): Pick<AIDescribeElementResponse, 'description'> | undefined {\n  const message = error instanceof Error ? error.message : String(error);\n  const rawResponse =\n    error instanceof AIResponseParseError\n      ? error.rawResponse\n      : message.match(/Response -\\s*\\n\\s*([\\s\\S]*)$/)?.[1];\n\n  if (\n    !rawResponse ||\n    (!message.includes('failed to parse LLM response into JSON') &&\n      !(error instanceof AIResponseParseError))\n  ) {\n    return undefined;\n  }\n\n  const jsonLikeResponse = extractJSONFromCodeBlock(rawResponse);\n  const description = extractPossiblyMalformedStringField(\n    jsonLikeResponse,\n    'description',\n  )?.trim();\n\n  if (!description) {\n    return undefined;\n  }\n\n  return { description };\n}\n"],"names":["DESCRIBE_POINT_MARKER_MAX_SIZE","DESCRIBE_RECT_MARKER_BORDER_THICKNESS","DESCRIBE_LARGE_RECT_MARKER_BORDER_THICKNESS","DESCRIBE_WIDE_MARKER_INSET_MIN_WIDTH","DESCRIBE_WIDE_MARKER_HORIZONTAL_INSET_RATIO","DESCRIBE_WIDE_MARKER_VERTICAL_INSET_RATIO","DESCRIBE_DEEP_CONTEXT_CONFIG","clampRect","rect","size","width","Math","height","getDescribeDeepContextAreas","screenSize","expandSearchArea","getRectInCrop","cropRect","cropSize","getDescribeMarkerRect","horizontalInset","verticalInset","getDescribeMarkerBorderThickness","getDescribeDeepLocateResizeSize","maxEdge","resize","scale","createServiceDump","data","baseData","Date","finalData","uuid","readNextSignificantChar","input","startIndex","index","extractPossiblyMalformedStringField","fieldName","escapedFieldName","fieldStart","RegExp","escaped","valueForJsonParse","char","nextSignificantChar","undefined","JSON","recoverDescribeResponseFromParseError","error","message","Error","String","rawResponse","AIResponseParseError","jsonLikeResponse","extractJSONFromCodeBlock","description"],"mappings":"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"}