diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 2846f22..3dada1a 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1,13 +1,13 @@ export namespace application { - + export class Point { x: number; y: number; - + static createFrom(source: any = {}) { return new Point(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.x = source["x"]; @@ -25,7 +25,7 @@ export namespace application { static createFrom(source: any = {}) { return new Annotation(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.tool = source["tool"]; @@ -35,7 +35,7 @@ export namespace application { this.strokeWidth = source["strokeWidth"]; this.fontSize = source["fontSize"]; } - + convertValues(a: any, classs: any, asMap: boolean = false): any { if (!a) { return a; @@ -58,7 +58,7 @@ export namespace application { } export namespace domain { - + export class OCRProviderConfig { label: string; endpoint: string; @@ -69,11 +69,11 @@ export namespace domain { action: string; version: string; timeoutSecs: number; - + static createFrom(source: any = {}) { return new OCRProviderConfig(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.label = source["label"]; @@ -90,17 +90,17 @@ export namespace domain { export class OCRConfig { activeProvider: string; providers: Record; - + static createFrom(source: any = {}) { return new OCRConfig(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.activeProvider = source["activeProvider"]; this.providers = this.convertValues(source["providers"], OCRProviderConfig, true); } - + convertValues(a: any, classs: any, asMap: boolean = false): any { if (!a) { return a; @@ -128,11 +128,11 @@ export namespace domain { temperature: number; timeoutSecs: number; maxInlineBytes: number; - + static createFrom(source: any = {}) { return new LLMProviderConfig(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.label = source["label"]; @@ -149,18 +149,18 @@ export namespace domain { activeProvider: string; prompt: string; providers: Record; - + static createFrom(source: any = {}) { return new LLMConfig(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.activeProvider = source["activeProvider"]; this.prompt = source["prompt"]; this.providers = this.convertValues(source["providers"], LLMProviderConfig, true); } - + convertValues(a: any, classs: any, asMap: boolean = false): any { if (!a) { return a; @@ -189,11 +189,11 @@ export namespace domain { strictHostKey: boolean; knownHostsPath: string; connectTimeoutSecs: number; - + static createFrom(source: any = {}) { return new SSHConfig(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.host = source["host"]; @@ -216,11 +216,11 @@ export namespace domain { pathPrefix: string; publicUrlBase: string; usePathStyle: boolean; - + static createFrom(source: any = {}) { return new S3Config(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.endpoint = source["endpoint"]; @@ -240,11 +240,11 @@ export namespace domain { ssh: SSHConfig; llm: LLMConfig; ocr: OCRConfig; - + static createFrom(source: any = {}) { return new AppConfig(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.hotkey = source["hotkey"]; @@ -254,7 +254,7 @@ export namespace domain { this.llm = this.convertValues(source["llm"], LLMConfig); this.ocr = this.convertValues(source["ocr"], OCRConfig); } - + convertValues(a: any, classs: any, asMap: boolean = false): any { if (!a) { return a; @@ -273,24 +273,24 @@ export namespace domain { return a; } } - - - - - + + + + + } export namespace main { - + export class CaptureActionResult { completed: boolean; path?: string; - + static createFrom(source: any = {}) { return new CaptureActionResult(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.completed = source["completed"]; @@ -302,11 +302,11 @@ export namespace main { y: number; w: number; h: number; - + static createFrom(source: any = {}) { return new RegionRect(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.x = source["x"]; @@ -318,17 +318,17 @@ export namespace main { export class CaptureResult { rect: RegionRect; annotations: application.Annotation[]; - + static createFrom(source: any = {}) { return new CaptureResult(source); } - + constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.rect = this.convertValues(source["rect"], RegionRect); this.annotations = this.convertValues(source["annotations"], application.Annotation); } - + convertValues(a: any, classs: any, asMap: boolean = false): any { if (!a) { return a; @@ -349,3 +349,4 @@ export namespace main { } } + diff --git a/internal/infrastructure/ocr/client.go b/internal/infrastructure/ocr/client.go index 61248fe..93a93dc 100644 --- a/internal/infrastructure/ocr/client.go +++ b/internal/infrastructure/ocr/client.go @@ -264,6 +264,9 @@ func parseOCRText(data []byte) (string, error) { if msg := responseError(root); msg != "" { return "", errors.New(msg) } + if text := normalizeOCRText(extractOverallOCRText(root, "")); text != "" { + return text, nil + } parts := collectOCRParts(root, "") if len(parts) == 0 { return "", fmt.Errorf("no text found") @@ -312,6 +315,62 @@ func responseError(v any) string { 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 { switch value := v.(type) { 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 { return []string{ "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 { for _, item := range priorityOCRKeys() { if item == key { @@ -459,6 +580,18 @@ func dedupeNonEmpty(parts []string) []string { 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 { return strings.HasPrefix(text, "{") || strings.HasPrefix(text, "[") } diff --git a/internal/infrastructure/ocr/client_test.go b/internal/infrastructure/ocr/client_test.go index 14c7307..163aa75 100644 --- a/internal/infrastructure/ocr/client_test.go +++ b/internal/infrastructure/ocr/client_test.go @@ -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) { _, err := parseOCRText([]byte(`{"ResponseMetadata":{"Error":{"Code":"BadRequest","Message":"bad image"}}}`)) if err == nil || !strings.Contains(err.Error(), "bad image") {