fix: prefer aggregate OCR text
This commit is contained in:
@@ -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<string, OCRProviderConfig>;
|
||||
|
||||
|
||||
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<string, LLMProviderConfig>;
|
||||
|
||||
|
||||
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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "[")
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user