dsh-mobilecode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +164 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +828 -0
- package/lib/device-build.js +964 -0
- package/lib/device-preview.js +870 -0
- package/lib/index.js +976 -0
- package/lib/setup.js +243 -0
- package/package.json +68 -0
- package/scripts/ocr.py +111 -0
package/lib/setup.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-mobilecode — setup services (settings store, plugin doctor, PaddleOCR installer).
|
|
3
|
+
*
|
|
4
|
+
* Shared user-level state lives under ~/.dsh/mobilecode/ (same place as the OCR
|
|
5
|
+
* venv), so it survives GUI restarts and is independent of the mounted copy.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process"
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs"
|
|
10
|
+
import os from "node:os"
|
|
11
|
+
import path from "node:path"
|
|
12
|
+
import { fileURLToPath } from "node:url"
|
|
13
|
+
import * as DeviceBuild from "./device-build.js"
|
|
14
|
+
|
|
15
|
+
export const HOME = path.join(os.homedir(), ".dsh", "mobilecode")
|
|
16
|
+
const SETTINGS_FILE = path.join(HOME, "settings.json")
|
|
17
|
+
const OCR_LOG_FILE = path.join(HOME, "ocr-install.log")
|
|
18
|
+
export const OCR_README = path.join(HOME, "OCR-INSTALL.md")
|
|
19
|
+
|
|
20
|
+
export function ensureHome() {
|
|
21
|
+
if (!existsSync(HOME)) mkdirSync(HOME, { recursive: true })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── settings ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export function readSettings() {
|
|
27
|
+
ensureHome()
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(SETTINGS_FILE, "utf8"))
|
|
30
|
+
} catch {
|
|
31
|
+
return {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeSettings(patch) {
|
|
36
|
+
ensureHome()
|
|
37
|
+
const next = { ...readSettings(), ...patch }
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2) + "\n", "utf8")
|
|
40
|
+
} catch {
|
|
41
|
+
/* read-only home — settings degrade to in-memory */
|
|
42
|
+
}
|
|
43
|
+
return next
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── OCR installer ──────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const OCR_PACKAGES = ["setuptools", "wheel", "numpy<2", "paddleocr==3.7.0", "paddlepaddle==3.3.1"]
|
|
49
|
+
const OCR_INSTALL_STATE = { state: "idle", log: [], done: false, failed: false } // in-memory + log file
|
|
50
|
+
|
|
51
|
+
function ocrLog(line) {
|
|
52
|
+
OCR_INSTALL_STATE.log.push(line)
|
|
53
|
+
if (OCR_INSTALL_STATE.log.length > 200) OCR_INSTALL_STATE.log.splice(0, OCR_INSTALL_STATE.log.length - 200)
|
|
54
|
+
try { appendFileSync(OCR_LOG_FILE, line + "\n", "utf8") } catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when the venv python exists AND imports paddleocr — the real "installed" test. */
|
|
58
|
+
export async function ocrWorking() {
|
|
59
|
+
const python = DeviceBuild.ocrPython()
|
|
60
|
+
if (!python) return false
|
|
61
|
+
const probe = await DeviceBuild.capture(python, ["-c", "import paddleocr, paddle; print('ok')"]).catch(() => "")
|
|
62
|
+
return probe.includes("ok")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Current OCR state for the UI: installed / working / install progress / failure. */
|
|
66
|
+
export async function ocrStatus() {
|
|
67
|
+
const python = DeviceBuild.ocrPython()
|
|
68
|
+
const working = await ocrWorking()
|
|
69
|
+
const state = OCR_INSTALL_STATE.state
|
|
70
|
+
const out = { installed: !!python, working, state: "idle", log: [] }
|
|
71
|
+
if (OCR_INSTALL_STATE.failed) out.state = "failed"
|
|
72
|
+
else if (state === "installing") out.state = "installing"
|
|
73
|
+
else if (working) out.state = "done"
|
|
74
|
+
out.log = OCR_INSTALL_STATE.log.slice(-60)
|
|
75
|
+
if (out.state === "failed" && out.log.length === 0) {
|
|
76
|
+
try {
|
|
77
|
+
const tail = readFileSync(OCR_LOG_FILE, "utf8").split("\n").slice(-60)
|
|
78
|
+
out.log = tail
|
|
79
|
+
} catch { /* no log file yet */ }
|
|
80
|
+
}
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Start a detached PaddleOCR install (venv + pip) that survives GUI restarts.
|
|
86
|
+
* Writes an install script to disk and spawns it via cmd.exe, so the chain
|
|
87
|
+
* keeps running even if dsh exits. The script appends to OCR_LOG_FILE so
|
|
88
|
+
* status can always be recovered by a later GUI.
|
|
89
|
+
*/
|
|
90
|
+
export function startOcrInstall() {
|
|
91
|
+
ensureHome()
|
|
92
|
+
if (OCR_INSTALL_STATE.state === "installing") return { started: false, message: "install already in progress" }
|
|
93
|
+
OCR_INSTALL_STATE.state = "installing"
|
|
94
|
+
OCR_INSTALL_STATE.done = false
|
|
95
|
+
OCR_INSTALL_STATE.failed = false
|
|
96
|
+
OCR_INSTALL_STATE.log = []
|
|
97
|
+
|
|
98
|
+
const venv = path.join(HOME, "ocr-venv")
|
|
99
|
+
const python = path.join(venv, "Scripts", "python.exe")
|
|
100
|
+
const scriptFile = path.join(HOME, "ocr-install.cmd")
|
|
101
|
+
const lines = [
|
|
102
|
+
"@echo off",
|
|
103
|
+
"setlocal",
|
|
104
|
+
`echo [dsh-mobilecode] OCR install started %date% %time% >> "${OCR_LOG_FILE}"`,
|
|
105
|
+
`echo venv: ${venv} >> "${OCR_LOG_FILE}"`,
|
|
106
|
+
`py -3.12 -m venv "${venv}" >> "${OCR_LOG_FILE}" 2>&1 || py -3 -m venv "${venv}" >> "${OCR_LOG_FILE}" 2>&1 || python -m venv "${venv}" >> "${OCR_LOG_FILE}" 2>&1`,
|
|
107
|
+
`"${python}" -m pip install --disable-pip-version-check ${OCR_PACKAGES.map((p) => `"${p}"`).join(" ")} >> "${OCR_LOG_FILE}" 2>&1`,
|
|
108
|
+
`echo INSTALL_EXIT=%ERRORLEVEL% >> "${OCR_LOG_FILE}"`,
|
|
109
|
+
]
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(scriptFile, lines.join("\r\n"), "utf8")
|
|
112
|
+
} catch (error) {
|
|
113
|
+
OCR_INSTALL_STATE.state = "failed"
|
|
114
|
+
OCR_INSTALL_STATE.failed = true
|
|
115
|
+
return { started: false, message: `cannot write install script: ${error.message}` }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const child = spawn("cmd.exe", ["/c", scriptFile], {
|
|
119
|
+
detached: true,
|
|
120
|
+
stdio: "ignore",
|
|
121
|
+
windowsHide: true,
|
|
122
|
+
})
|
|
123
|
+
child.unref()
|
|
124
|
+
|
|
125
|
+
ocrLog(`install started (script ${scriptFile})`)
|
|
126
|
+
OCR_INSTALL_STATE.state = "installing"
|
|
127
|
+
|
|
128
|
+
// The detached child writes INSTALL_EXIT=<code> to the log; poll for it.
|
|
129
|
+
const poll = setInterval(async () => {
|
|
130
|
+
try {
|
|
131
|
+
const log = readFileSync(OCR_LOG_FILE, "utf8")
|
|
132
|
+
if (/INSTALL_EXIT=0/.test(log)) {
|
|
133
|
+
clearInterval(poll)
|
|
134
|
+
OCR_INSTALL_STATE.state = "done"
|
|
135
|
+
OCR_INSTALL_STATE.done = true
|
|
136
|
+
ocrLog("install completed")
|
|
137
|
+
} else if (/INSTALL_EXIT=[1-9]/.test(log)) {
|
|
138
|
+
clearInterval(poll)
|
|
139
|
+
OCR_INSTALL_STATE.state = "failed"
|
|
140
|
+
OCR_INSTALL_STATE.failed = true
|
|
141
|
+
ocrLog("install failed (see log above)")
|
|
142
|
+
}
|
|
143
|
+
} catch { /* log not written yet */ }
|
|
144
|
+
}, 2000)
|
|
145
|
+
poll.unref?.()
|
|
146
|
+
|
|
147
|
+
return { started: true, log: OCR_INSTALL_STATE.log.slice(-10) }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── plugin doctor ──────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const SYSTEM_CHECK = (name, ok, detail, fix) => ({ name, ok, detail, ...(fix ? { fix } : {}) })
|
|
153
|
+
|
|
154
|
+
/** Run every health check. Each entry: {id, name, ok, detail, fix?}. */
|
|
155
|
+
export async function runDoctor() {
|
|
156
|
+
const checks = []
|
|
157
|
+
|
|
158
|
+
// 1. Runtime: node + npm/npx reachable.
|
|
159
|
+
const nodeOk = typeof process.version === "string"
|
|
160
|
+
checks.push(SYSTEM_CHECK("runtime", nodeOk, `node ${process.version ?? "?"} (dsh host)`))
|
|
161
|
+
|
|
162
|
+
// 2. Android SDK: adb + emulator binaries.
|
|
163
|
+
const adbPath = DeviceBuild.adb()
|
|
164
|
+
const sdkOk = !!adbPath && existsSync(adbPath)
|
|
165
|
+
checks.push(SYSTEM_CHECK("android-sdk", sdkOk, sdkOk ? `adb at ${adbPath}` : "Android SDK not found (adb missing). Install Android Studio or set ANDROID_HOME."))
|
|
166
|
+
|
|
167
|
+
// 3. Emulator binary.
|
|
168
|
+
const emulatorPath = DeviceBuild.emulatorBinary()
|
|
169
|
+
checks.push(SYSTEM_CHECK("emulator", !!emulatorPath, emulatorPath ? `emulator at ${emulatorPath}` : "Emulator binary not found under the Android SDK."))
|
|
170
|
+
|
|
171
|
+
// 4. AVDs configured.
|
|
172
|
+
const avds = await DeviceBuild.androidAvds().catch(() => [])
|
|
173
|
+
checks.push(SYSTEM_CHECK("avds", avds.length > 0, avds.length > 0 ? `AVDs: ${avds.join(", ")}` : "No AVDs configured. Create one in Android Studio (Device Manager)."))
|
|
174
|
+
|
|
175
|
+
// 5. Attached device (live runs need one).
|
|
176
|
+
const devices = await DeviceBuild.devices().catch(() => [])
|
|
177
|
+
const attached = devices.filter((d) => d.state === "device")
|
|
178
|
+
checks.push(SYSTEM_CHECK("device", attached.length > 0, attached.length > 0 ? `attached: ${attached.map((d) => d.serial).join(", ")}` : "No Android device attached. device_run auto-boots an AVD when one is configured."))
|
|
179
|
+
|
|
180
|
+
// 6. PaddleOCR (device_screen OCR) — the only auto-fixable check.
|
|
181
|
+
const python = DeviceBuild.ocrPython()
|
|
182
|
+
const working = await ocrWorking()
|
|
183
|
+
checks.push(SYSTEM_CHECK("paddleocr", working,
|
|
184
|
+
working ? `PaddleOCR ready at ${python}` : python ? "PaddleOCR venv exists but imports failed (broken install)." : "PaddleOCR not installed — device_screen OCR is disabled. Fix installs it automatically.",
|
|
185
|
+
"install"))
|
|
186
|
+
|
|
187
|
+
// 7. OCR script reachable (part of the package).
|
|
188
|
+
const scriptOk = existsSync(fileURLToPath(new URL("../scripts/ocr.py", import.meta.url)))
|
|
189
|
+
checks.push(SYSTEM_CHECK("ocr-script", scriptOk, scriptOk ? "ocr.py present in the plugin package" : "ocr.py missing — reinstall the plugin."))
|
|
190
|
+
|
|
191
|
+
return checks
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Try to auto-fix one check by id. Returns {ok, message}. */
|
|
195
|
+
export async function runFix(id) {
|
|
196
|
+
if (id === "paddleocr") {
|
|
197
|
+
const started = startOcrInstall()
|
|
198
|
+
if (!started.started) return { ok: false, message: started.message ?? "unknown" }
|
|
199
|
+
return { ok: true, message: "PaddleOCR install started in the background. Check OCR status in a minute." }
|
|
200
|
+
}
|
|
201
|
+
return { ok: false, message: `no automatic fix for "${id}"` }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── welcome / ai prompt ────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/** The straight-to-the-point prompt shown on first run; copy-paste into any AI. */
|
|
207
|
+
export const WELCOME_PROMPT = [
|
|
208
|
+
"You have the dsh-mobilecode plugin, which controls the iOS Simulator / Android Emulator on this machine — the same as the device pane's Play button.",
|
|
209
|
+
"",
|
|
210
|
+
"Tools:",
|
|
211
|
+
"- device_detect <directory>: which platforms (ios/android) a project supports and what is attached. Use first when a project may be mobile.",
|
|
212
|
+
"- device_run {action: run|stop|status, platform: ios|android|all, directory}: build, install and launch the app (auto-boots the emulator; minutes).",
|
|
213
|
+
"- device_screen: screenshot PNG + UI hierarchy + local PaddleOCR text, each with ABSOLUTE pixel boxes (e.g. 1080x2400). See what is on screen.",
|
|
214
|
+
"- device_input {action: tap|swipe|text|key, x, y, ...}: act at absolute pixel coordinates — take the center of a device_screen box: x=(x1+x2)/2, y=(y1+y2)/2.",
|
|
215
|
+
"- device_log {buffer: main|crash|events|kernel, filter}: logcat + kernel dmesg. Use when a run fails or the app misbehaves.",
|
|
216
|
+
"- device_status: one snapshot of attached devices, AVDs, running builds, Metro and preview servers.",
|
|
217
|
+
"",
|
|
218
|
+
"Recommended loop: device_detect → device_run → device_screen → device_input → device_screen (verify the change).",
|
|
219
|
+
"When something breaks, device_log (crash buffer, or filter by the app package) tells you why.",
|
|
220
|
+
"Coordinates are deterministic physical pixels; always re-read the screen right before acting.",
|
|
221
|
+
].join("\n")
|
|
222
|
+
|
|
223
|
+
export const OCR_SETUP_README = [
|
|
224
|
+
"# PaddleOCR — dsh-mobilecode",
|
|
225
|
+
"",
|
|
226
|
+
"`device_screen` OCR is powered by a fully local PaddleOCR venv at:",
|
|
227
|
+
"",
|
|
228
|
+
" " + path.join(HOME, "ocr-venv"),
|
|
229
|
+
"",
|
|
230
|
+
"Installed automatically via the Settings → Doctor / PaddleOCR page (one click), or manually:",
|
|
231
|
+
"",
|
|
232
|
+
" py -3.12 -m venv " + path.join(HOME, "ocr-venv"),
|
|
233
|
+
" " + path.join(HOME, "ocr-venv", "Scripts", "python.exe") + " -m pip install setuptools wheel \"numpy<2\" \"paddleocr==3.7.0\" \"paddlepaddle==3.3.1\"",
|
|
234
|
+
"",
|
|
235
|
+
"Paddle 3.x on Windows needs oneDNN disabled (ocr.py passes enable_mkldnn=False);",
|
|
236
|
+
"with that, 3.7.0 works and reads text better than the old 2.7.3 pin (PP-OCRv6 models).",
|
|
237
|
+
"Set DSH_MOBILECODE_OCR_PY to override the venv location.",
|
|
238
|
+
].join("\n")
|
|
239
|
+
|
|
240
|
+
export function writeOcrReadme() {
|
|
241
|
+
ensureHome()
|
|
242
|
+
try { writeFileSync(OCR_README, OCR_SETUP_README, "utf8") } catch { /* ignore */ }
|
|
243
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-mobilecode",
|
|
3
|
+
"description": "MobileCode for the dsh web GUI: detect iOS/Android projects, run serve-sim / serve-avd preview servers, and build-install-launch the app on the simulator or emulator from the session — plus agent tools (device_run, device_detect). Hot-pluggable — mounted via the profile bundle list + cordis.patch.yml, no dsh source changes.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@11.22.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"default": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./client": {
|
|
16
|
+
"default": "./lib/client.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"dsh": {
|
|
21
|
+
"bundle": {
|
|
22
|
+
"patch": "./cordis.patch.yml"
|
|
23
|
+
},
|
|
24
|
+
"client": {
|
|
25
|
+
"inject": [
|
|
26
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
27
|
+
],
|
|
28
|
+
"platform": "web"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^18.2.0",
|
|
33
|
+
"react-dom": "^18.2.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib/**/*.js",
|
|
37
|
+
"scripts/**/*.py",
|
|
38
|
+
"cordis.patch.yml",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "spix18 <spix18@users.noreply.github.com> (https://github.com/spix18)",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"dsh",
|
|
45
|
+
"deepseek-harness",
|
|
46
|
+
"plugin",
|
|
47
|
+
"mobilecode",
|
|
48
|
+
"mobile",
|
|
49
|
+
"ios",
|
|
50
|
+
"android",
|
|
51
|
+
"emulator",
|
|
52
|
+
"simulator",
|
|
53
|
+
"device",
|
|
54
|
+
"preview",
|
|
55
|
+
"expo",
|
|
56
|
+
"react-native",
|
|
57
|
+
"ocr",
|
|
58
|
+
"paddleocr"
|
|
59
|
+
],
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/spix18/dsh-mobilecode.git"
|
|
63
|
+
},
|
|
64
|
+
"homepage": "https://github.com/spix18/dsh-mobilecode#readme",
|
|
65
|
+
"bugs": {
|
|
66
|
+
"url": "https://github.com/spix18/dsh-mobilecode/issues"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/scripts/ocr.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""PaddleOCR one-shot over an image; prints JSON to stdout.
|
|
3
|
+
|
|
4
|
+
Usage: python ocr.py <image.png> [lang]
|
|
5
|
+
Output: {"items":[{"text":str,"confidence":float,"box":[x1,y1,x2,y2]}], "error":str?}
|
|
6
|
+
|
|
7
|
+
Works with both PaddleOCR 2.x (ocr.ocr) and 3.x (ocr.predict) APIs.
|
|
8
|
+
The process exits after one image so the host never keeps a model resident;
|
|
9
|
+
PaddleOCR caches the model in ~/.paddleocr between runs.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# Paddle 3.x PIR executor hits a oneDNN conversion bug on Windows; the CPU backend
|
|
16
|
+
# without mkldnn avoids it. Must be set before `import paddle`.
|
|
17
|
+
os.environ.setdefault("FLAGS_use_mkldnn", "0")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
if len(sys.argv) < 2:
|
|
22
|
+
print(json.dumps({"error": "missing image path"}))
|
|
23
|
+
return 1
|
|
24
|
+
image = sys.argv[1]
|
|
25
|
+
lang = sys.argv[2] if len(sys.argv) > 2 else "ch"
|
|
26
|
+
try:
|
|
27
|
+
from paddleocr import PaddleOCR
|
|
28
|
+
except Exception as exc: # noqa: BLE001
|
|
29
|
+
print(json.dumps({"error": f"paddleocr import failed: {exc}"}))
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
items = []
|
|
33
|
+
# enable_mkldnn=False: paddle 3.x on Windows crashes in the oneDNN PIR
|
|
34
|
+
# executor (ConvertPirAttribute2RuntimeAttribute); the flag is a no-op-safe
|
|
35
|
+
# on 2.x, so this single constructor keeps both stacks working (CPU only).
|
|
36
|
+
# use_doc_unwarping=False (3.x only): the UVDoc unwarping model runs by
|
|
37
|
+
# default and shifts coordinates on non-document images (phone screenshots
|
|
38
|
+
# came out ~80px off in Y); disabling it returns pixel-accurate boxes.
|
|
39
|
+
version3 = str(getattr(__import__("paddleocr"), "__version__", "")).startswith("3")
|
|
40
|
+
if version3:
|
|
41
|
+
ocr = PaddleOCR(lang=lang, enable_mkldnn=False, use_doc_unwarping=False)
|
|
42
|
+
else:
|
|
43
|
+
ocr = PaddleOCR(lang=lang, enable_mkldnn=False)
|
|
44
|
+
if hasattr(ocr, "predict"):
|
|
45
|
+
# PaddleOCR 3.x API.
|
|
46
|
+
try:
|
|
47
|
+
for result in ocr.predict(image):
|
|
48
|
+
# OCRResult is a dict subclass whose dict view carries the
|
|
49
|
+
# fields we need; its `.json` property has a different shape
|
|
50
|
+
# ({'res': ...}), so always read the dict view first.
|
|
51
|
+
data = result if isinstance(result, dict) else getattr(result, "json", result)
|
|
52
|
+
if not isinstance(data, dict):
|
|
53
|
+
continue
|
|
54
|
+
texts = data.get("rec_texts") or []
|
|
55
|
+
scores = data.get("rec_scores") or []
|
|
56
|
+
# dt_polys = list of (4,2) corner-point arrays; rec_boxes is a
|
|
57
|
+
# flat [x1,y1,x2,y2] ndarray — never use `or` on an ndarray.
|
|
58
|
+
boxes = data.get("dt_polys")
|
|
59
|
+
if boxes is None or len(boxes) == 0:
|
|
60
|
+
boxes = data.get("rec_boxes") or []
|
|
61
|
+
for text, score, box in zip(texts, scores, boxes):
|
|
62
|
+
if not text:
|
|
63
|
+
continue
|
|
64
|
+
if box is not None and len(box) == 4 and not hasattr(box[0], "__len__"):
|
|
65
|
+
flat = [float(v) for v in box] # flat [x1,y1,x2,y2]
|
|
66
|
+
else:
|
|
67
|
+
flat = []
|
|
68
|
+
for point in box:
|
|
69
|
+
# points may be list/tuple/ndarray
|
|
70
|
+
if hasattr(point, "__len__") and len(point) >= 2:
|
|
71
|
+
flat.append(float(point[0]))
|
|
72
|
+
flat.append(float(point[1]))
|
|
73
|
+
if len(flat) < 4:
|
|
74
|
+
continue
|
|
75
|
+
xs = flat[0::2]
|
|
76
|
+
ys = flat[1::2]
|
|
77
|
+
items.append({
|
|
78
|
+
"text": str(text),
|
|
79
|
+
"confidence": round(float(score), 4),
|
|
80
|
+
"box": [int(min(xs)), int(min(ys)), int(max(xs)), int(max(ys))],
|
|
81
|
+
})
|
|
82
|
+
except Exception as exc: # noqa: BLE001
|
|
83
|
+
print(json.dumps({"error": f"ocr failed (3.x): {exc}"}))
|
|
84
|
+
return 1
|
|
85
|
+
else:
|
|
86
|
+
# PaddleOCR 2.x API.
|
|
87
|
+
try:
|
|
88
|
+
result = ocr.ocr(image, cls=True)
|
|
89
|
+
for line in result or []:
|
|
90
|
+
for entry in line or []:
|
|
91
|
+
box, text_conf = entry[0], entry[1]
|
|
92
|
+
text, score = text_conf
|
|
93
|
+
if not text:
|
|
94
|
+
continue
|
|
95
|
+
xs = [float(p[0]) for p in box]
|
|
96
|
+
ys = [float(p[1]) for p in box]
|
|
97
|
+
items.append({
|
|
98
|
+
"text": str(text),
|
|
99
|
+
"confidence": round(float(score), 4),
|
|
100
|
+
"box": [int(min(xs)), int(min(ys)), int(max(xs)), int(max(ys))],
|
|
101
|
+
})
|
|
102
|
+
except Exception as exc: # noqa: BLE001
|
|
103
|
+
print(json.dumps({"error": f"ocr failed (2.x): {exc}"}))
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
print(json.dumps({"items": items}))
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
sys.exit(main())
|