fix: prefer aggregate OCR text
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
export namespace application {
|
export namespace application {
|
||||||
|
|
||||||
export class Point {
|
export class Point {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Point(source);
|
return new Point(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.x = source["x"];
|
this.x = source["x"];
|
||||||
@@ -25,7 +25,7 @@ export namespace application {
|
|||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new Annotation(source);
|
return new Annotation(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.tool = source["tool"];
|
this.tool = source["tool"];
|
||||||
@@ -35,7 +35,7 @@ export namespace application {
|
|||||||
this.strokeWidth = source["strokeWidth"];
|
this.strokeWidth = source["strokeWidth"];
|
||||||
this.fontSize = source["fontSize"];
|
this.fontSize = source["fontSize"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
@@ -58,7 +58,7 @@ export namespace application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace domain {
|
export namespace domain {
|
||||||
|
|
||||||
export class OCRProviderConfig {
|
export class OCRProviderConfig {
|
||||||
label: string;
|
label: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
@@ -69,11 +69,11 @@ export namespace domain {
|
|||||||
action: string;
|
action: string;
|
||||||
version: string;
|
version: string;
|
||||||
timeoutSecs: number;
|
timeoutSecs: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new OCRProviderConfig(source);
|
return new OCRProviderConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.label = source["label"];
|
this.label = source["label"];
|
||||||
@@ -90,17 +90,17 @@ export namespace domain {
|
|||||||
export class OCRConfig {
|
export class OCRConfig {
|
||||||
activeProvider: string;
|
activeProvider: string;
|
||||||
providers: Record<string, OCRProviderConfig>;
|
providers: Record<string, OCRProviderConfig>;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new OCRConfig(source);
|
return new OCRConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.activeProvider = source["activeProvider"];
|
this.activeProvider = source["activeProvider"];
|
||||||
this.providers = this.convertValues(source["providers"], OCRProviderConfig, true);
|
this.providers = this.convertValues(source["providers"], OCRProviderConfig, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
@@ -128,11 +128,11 @@ export namespace domain {
|
|||||||
temperature: number;
|
temperature: number;
|
||||||
timeoutSecs: number;
|
timeoutSecs: number;
|
||||||
maxInlineBytes: number;
|
maxInlineBytes: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new LLMProviderConfig(source);
|
return new LLMProviderConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.label = source["label"];
|
this.label = source["label"];
|
||||||
@@ -149,18 +149,18 @@ export namespace domain {
|
|||||||
activeProvider: string;
|
activeProvider: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
providers: Record<string, LLMProviderConfig>;
|
providers: Record<string, LLMProviderConfig>;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new LLMConfig(source);
|
return new LLMConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.activeProvider = source["activeProvider"];
|
this.activeProvider = source["activeProvider"];
|
||||||
this.prompt = source["prompt"];
|
this.prompt = source["prompt"];
|
||||||
this.providers = this.convertValues(source["providers"], LLMProviderConfig, true);
|
this.providers = this.convertValues(source["providers"], LLMProviderConfig, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
@@ -189,11 +189,11 @@ export namespace domain {
|
|||||||
strictHostKey: boolean;
|
strictHostKey: boolean;
|
||||||
knownHostsPath: string;
|
knownHostsPath: string;
|
||||||
connectTimeoutSecs: number;
|
connectTimeoutSecs: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new SSHConfig(source);
|
return new SSHConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.host = source["host"];
|
this.host = source["host"];
|
||||||
@@ -216,11 +216,11 @@ export namespace domain {
|
|||||||
pathPrefix: string;
|
pathPrefix: string;
|
||||||
publicUrlBase: string;
|
publicUrlBase: string;
|
||||||
usePathStyle: boolean;
|
usePathStyle: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new S3Config(source);
|
return new S3Config(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.endpoint = source["endpoint"];
|
this.endpoint = source["endpoint"];
|
||||||
@@ -240,11 +240,11 @@ export namespace domain {
|
|||||||
ssh: SSHConfig;
|
ssh: SSHConfig;
|
||||||
llm: LLMConfig;
|
llm: LLMConfig;
|
||||||
ocr: OCRConfig;
|
ocr: OCRConfig;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new AppConfig(source);
|
return new AppConfig(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.hotkey = source["hotkey"];
|
this.hotkey = source["hotkey"];
|
||||||
@@ -254,7 +254,7 @@ export namespace domain {
|
|||||||
this.llm = this.convertValues(source["llm"], LLMConfig);
|
this.llm = this.convertValues(source["llm"], LLMConfig);
|
||||||
this.ocr = this.convertValues(source["ocr"], OCRConfig);
|
this.ocr = this.convertValues(source["ocr"], OCRConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
@@ -273,24 +273,24 @@ export namespace domain {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace main {
|
export namespace main {
|
||||||
|
|
||||||
export class CaptureActionResult {
|
export class CaptureActionResult {
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new CaptureActionResult(source);
|
return new CaptureActionResult(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.completed = source["completed"];
|
this.completed = source["completed"];
|
||||||
@@ -302,11 +302,11 @@ export namespace main {
|
|||||||
y: number;
|
y: number;
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new RegionRect(source);
|
return new RegionRect(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.x = source["x"];
|
this.x = source["x"];
|
||||||
@@ -318,17 +318,17 @@ export namespace main {
|
|||||||
export class CaptureResult {
|
export class CaptureResult {
|
||||||
rect: RegionRect;
|
rect: RegionRect;
|
||||||
annotations: application.Annotation[];
|
annotations: application.Annotation[];
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new CaptureResult(source);
|
return new CaptureResult(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.rect = this.convertValues(source["rect"], RegionRect);
|
this.rect = this.convertValues(source["rect"], RegionRect);
|
||||||
this.annotations = this.convertValues(source["annotations"], application.Annotation);
|
this.annotations = this.convertValues(source["annotations"], application.Annotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
if (!a) {
|
if (!a) {
|
||||||
return a;
|
return a;
|
||||||
@@ -349,3 +349,4 @@ export namespace main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,6 +264,9 @@ func parseOCRText(data []byte) (string, error) {
|
|||||||
if msg := responseError(root); msg != "" {
|
if msg := responseError(root); msg != "" {
|
||||||
return "", errors.New(msg)
|
return "", errors.New(msg)
|
||||||
}
|
}
|
||||||
|
if text := normalizeOCRText(extractOverallOCRText(root, "")); text != "" {
|
||||||
|
return text, nil
|
||||||
|
}
|
||||||
parts := collectOCRParts(root, "")
|
parts := collectOCRParts(root, "")
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return "", fmt.Errorf("no text found")
|
return "", fmt.Errorf("no text found")
|
||||||
@@ -312,6 +315,62 @@ func responseError(v any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractOverallOCRText(v any, parentKey string) string {
|
||||||
|
switch value := v.(type) {
|
||||||
|
case string:
|
||||||
|
text := strings.TrimSpace(value)
|
||||||
|
if text == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if looksLikeJSON(text) {
|
||||||
|
var nested any
|
||||||
|
if err := json.Unmarshal([]byte(text), &nested); err == nil {
|
||||||
|
if nestedText := extractOverallOCRText(nested, parentKey); nestedText != "" {
|
||||||
|
return nestedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isAggregateContainerKey(parentKey) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
case map[string]any:
|
||||||
|
for _, key := range aggregateTextKeys() {
|
||||||
|
if child, ok := value[key]; ok {
|
||||||
|
if text := extractOverallOCRText(child, key); text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range responseContainerKeys() {
|
||||||
|
if child, ok := value[key]; ok {
|
||||||
|
if text := extractOverallOCRText(child, key); text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys := make([]string, 0, len(value))
|
||||||
|
for key := range value {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, key := range keys {
|
||||||
|
if isAggregateTextKey(key) ||
|
||||||
|
isResponseContainerKey(key) ||
|
||||||
|
isBlockContainerKey(key) ||
|
||||||
|
isMetadataKey(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if text := extractOverallOCRText(value[key], key); text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func collectOCRParts(v any, parentKey string) []string {
|
func collectOCRParts(v any, parentKey string) []string {
|
||||||
switch value := v.(type) {
|
switch value := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
@@ -359,6 +418,36 @@ func collectOCRParts(v any, parentKey string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func aggregateTextKeys() []string {
|
||||||
|
return []string{
|
||||||
|
"content",
|
||||||
|
"Content",
|
||||||
|
"text",
|
||||||
|
"Text",
|
||||||
|
"full_text",
|
||||||
|
"FullText",
|
||||||
|
"fullText",
|
||||||
|
"plain_text",
|
||||||
|
"PlainText",
|
||||||
|
"plainText",
|
||||||
|
"recognized_text",
|
||||||
|
"RecognizedText",
|
||||||
|
"recognizedText",
|
||||||
|
"ocr_text",
|
||||||
|
"OCRText",
|
||||||
|
"ocrText",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseContainerKeys() []string {
|
||||||
|
return []string{
|
||||||
|
"Data",
|
||||||
|
"data",
|
||||||
|
"Result",
|
||||||
|
"result",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func priorityOCRKeys() []string {
|
func priorityOCRKeys() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"Data",
|
"Data",
|
||||||
@@ -394,6 +483,38 @@ func priorityOCRKeys() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAggregateTextKey(key string) bool {
|
||||||
|
for _, item := range aggregateTextKeys() {
|
||||||
|
if item == key {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAggregateContainerKey(key string) bool {
|
||||||
|
return isAggregateTextKey(key) || isResponseContainerKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isResponseContainerKey(key string) bool {
|
||||||
|
for _, item := range responseContainerKeys() {
|
||||||
|
if item == key {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBlockContainerKey(key string) bool {
|
||||||
|
switch normalizeKey(key) {
|
||||||
|
case "wordsresult", "prismwordsinfo", "prismwords", "ocrinfos", "items",
|
||||||
|
"blocks", "regions", "words", "linetexts", "lines", "cells":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isPriorityOCRKey(key string) bool {
|
func isPriorityOCRKey(key string) bool {
|
||||||
for _, item := range priorityOCRKeys() {
|
for _, item := range priorityOCRKeys() {
|
||||||
if item == key {
|
if item == key {
|
||||||
@@ -459,6 +580,18 @@ func dedupeNonEmpty(parts []string) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeOCRText(text string) string {
|
||||||
|
lines := strings.Split(strings.TrimSpace(text), "\n")
|
||||||
|
out := make([]string, 0, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
out = append(out, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(out, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
func looksLikeJSON(text string) bool {
|
func looksLikeJSON(text string) bool {
|
||||||
return strings.HasPrefix(text, "{") || strings.HasPrefix(text, "[")
|
return strings.HasPrefix(text, "{") || strings.HasPrefix(text, "[")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,33 @@ func TestParseOCRTextSupportsWordsResult(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseOCRTextPrefersOverallContentOverBlocks(t *testing.T) {
|
||||||
|
text, err := parseOCRText([]byte(`{
|
||||||
|
"Data": "{\"content\":\"整体识别文本\",\"prism_wordsInfo\":[{\"word\":\"整体\"},{\"word\":\"识别\"},{\"word\":\"文本\"}]}"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse ocr text: %v", err)
|
||||||
|
}
|
||||||
|
if text != "整体识别文本" {
|
||||||
|
t.Fatalf("expected overall content only, got %q", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOCRTextPrefersResultTextOverWordsResult(t *testing.T) {
|
||||||
|
text, err := parseOCRText([]byte(`{
|
||||||
|
"Result": {
|
||||||
|
"text": "hello world",
|
||||||
|
"words_result": [{"words":"hello"},{"words":"world"}]
|
||||||
|
}
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse ocr text: %v", err)
|
||||||
|
}
|
||||||
|
if text != "hello world" {
|
||||||
|
t.Fatalf("expected result text only, got %q", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseOCRTextReturnsProviderError(t *testing.T) {
|
func TestParseOCRTextReturnsProviderError(t *testing.T) {
|
||||||
_, err := parseOCRText([]byte(`{"ResponseMetadata":{"Error":{"Code":"BadRequest","Message":"bad image"}}}`))
|
_, err := parseOCRText([]byte(`{"ResponseMetadata":{"Error":{"Code":"BadRequest","Message":"bad image"}}}`))
|
||||||
if err == nil || !strings.Contains(err.Error(), "bad image") {
|
if err == nil || !strings.Contains(err.Error(), "bad image") {
|
||||||
|
|||||||
Reference in New Issue
Block a user