UNPKG

koishi-plugin-ocr

Version:

本地 OCR 服务,调用 PaddleOCR 或 RapidOCR 进行识别(开坑中,一堆bug,勿用)

277 lines (275 loc) 10.9 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, name: () => name }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var import_paddleocrjson = __toESM(require("paddleocrjson")); var import_os = require("os"); var import_path = __toESM(require("path")); var import_fs = __toESM(require("fs")); var import_http = __toESM(require("http")); var import_https = __toESM(require("https")); var name = "ocr"; var defaultExePath = /* @__PURE__ */ __name(() => { const osType = (0, import_os.platform)(); if (osType === "win32") return "PaddleOCR-json.exe"; else return "./PaddleOCR-json"; }, "defaultExePath"); var Config = import_koishi.Schema.object({ timeout: import_koishi.Schema.number().default(3e4).description("OCR识别超时时间(毫秒)"), exePath: import_koishi.Schema.string().default(defaultExePath()).description("PaddleOCR-json可执行文件路径"), cwd: import_koishi.Schema.string().description("PaddleOCR-json工作目录,默认为basedir下的data/PaddleOCR-json目录"), args: import_koishi.Schema.array(String).default([]).description('PaddleOCR-json启动参数,例如 ["-port=9985", "-addr=loopback"]'), debug: import_koishi.Schema.boolean().default(false).description("是否开启调试模式") }); var Ocr = class extends import_koishi.Service { constructor(ctx, config) { super(ctx, "ocr"); this.config = config; } static { __name(this, "Ocr"); } static [import_koishi.Service.provide] = "ocr"; static inject = ["ocr"]; ocr; pid = null; initialized = false; ocrInitComplete = false; async start() { if (!this.config.cwd) { this.config.cwd = import_path.default.join(this.ctx.baseDir, "data/PaddleOCR-json"); } try { if (!import_fs.default.existsSync(this.config.cwd)) { import_fs.default.mkdirSync(this.config.cwd, { recursive: true }); this.logger.info(`创建工作目录: ${this.config.cwd}`); } } catch (error) { this.logger.error(`创建工作目录失败: ${error.message}`); } const exeFullPath = import_path.default.isAbsolute(this.config.exePath) ? this.config.exePath : import_path.default.join(this.config.cwd, this.config.exePath); if (!import_fs.default.existsSync(exeFullPath)) { this.logger.warn(`可执行文件不存在: ${exeFullPath}`); } try { this.ocr = new import_paddleocrjson.default(this.config.exePath, this.config.args, { cwd: this.config.cwd }, this.config.debug); if (this.config.debug) { this.ocr.stdout.on("data", (chunk) => this.logger.info(chunk.toString())); this.ocr.stderr.on("data", (data) => this.logger.warn(data.toString())); } this.ocr.on("error", (error) => { this.logger.error(`OCR进程错误: ${error.message}`); this.initialized = this.ocrInitComplete = false; }); this.ocr.on("exit", (code) => { this.logger.info(`OCR进程退出,退出码: ${code}`); this.initialized = this.ocrInitComplete = false; }); this.ocr.on("init", (pid, addr, port) => { this.logger.info(`OCR初始化完成!PID: ${pid}, 地址: ${addr}, 端口: ${port}`); this.pid = pid; this.ocrInitComplete = true; }); this.initialized = true; return new Promise((resolve) => { if (this.ocrInitComplete) { resolve(); return; } this.ocr.once("init", () => resolve()); setTimeout(() => { if (!this.ocrInitComplete) { this.logger.warn("OCR服务初始化超时,但继续启动"); } resolve(); }, 1e4); }); } catch (error) { this.logger.error(`OCR初始化失败: ${error.message}`); this.initialized = false; throw error; } } async stop() { if (this.ocr) { try { this.logger.info("正在停止OCR服务..."); this.ocr.terminate(); if (this.pid) { await new Promise((resolve) => setTimeout(resolve, 1e3)); try { this.logger.info(`OCR进程(PID:${this.pid})应该已终止`); } catch (e) { this.logger.warn(`尝试检查进程状态时出错: ${e.message}`); } } this.ocr.removeAllListeners(); this.ocr = null; this.initialized = false; this.ocrInitComplete = false; this.pid = null; this.logger.info("OCR服务已停止并清理完成"); } catch (error) { this.logger.warn(`终止OCR进程时发生错误: ${error.message}`); } } } isReady() { return this.initialized && this.ocr && this.ocrInitComplete; } /** * 将Element元素转换为统一的base64格式 */ async getImageBase64(imageElement) { const url = "url" in imageElement && typeof imageElement.url === "string" ? imageElement.url : imageElement?.type === "image" && imageElement?.attrs?.url ? imageElement.attrs.url : null; if (!url) throw new Error("无效的图像元素"); return this.fetchImageFromUrl(url); } // 辅助方法:从URL获取图像并转为base64 async fetchImageFromUrl(imageUrl) { if (imageUrl.startsWith("data:image") && imageUrl.includes("base64,")) { return imageUrl; } if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) { return new Promise((resolve, reject) => { const isHttps = imageUrl.startsWith("https://"); const requestModule = isHttps ? import_https.default : import_http.default; const options = { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }, timeout: 1e4 }; const req = requestModule.get(imageUrl, options, (res) => { if (res.statusCode !== 200) { reject(new Error(`请求失败,状态码: ${res.statusCode}`)); return; } const chunks = []; res.on("data", (chunk) => chunks.push(Buffer.from(chunk))); res.on("end", () => { const buffer = Buffer.concat(chunks); const base64 = buffer.toString("base64"); const mimeType = res.headers["content-type"] || "image/png"; resolve(`data:${mimeType};base64,${base64}`); }); }); req.on("error", (err) => { reject(new Error(`获取图像失败: ${err.message}`)); }); req.on("timeout", () => { req.destroy(); reject(new Error("请求超时")); }); req.end(); }); } try { const imagePath = import_path.default.isAbsolute(imageUrl) ? imageUrl : import_path.default.join(this.config.cwd, imageUrl); if (!import_fs.default.existsSync(imagePath)) { throw new Error(`文件不存在: ${imagePath}`); } const imageBuffer = import_fs.default.readFileSync(imagePath); const base64 = imageBuffer.toString("base64"); const ext = import_path.default.extname(imagePath).toLowerCase(); const mimeType = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".gif" ? "image/gif" : "image/png"; return `data:${mimeType};base64,${base64}`; } catch (error) { throw new Error(`图像处理失败: ${error.message}`); } } async recognizeText(imageElement) { if (!this.isReady()) { if (this.initialized && this.ocr && !this.ocrInitComplete) { await new Promise((resolve) => setTimeout(resolve, 5e3)); if (!this.isReady()) { throw new Error("OCR服务未就绪"); } } else { throw new Error("OCR服务未就绪"); } } try { const base64Image = await this.getImageBase64(imageElement); const result = await this.ocr.flush({ image_base64: base64Image, timeout: this.config.timeout }); if (Array.isArray(result)) { return result.map((item) => item.text).join("\n"); } return JSON.stringify(result); } catch (error) { this.logger.error(`OCR识别失败: ${error.message}`); throw new Error("OCR识别失败: " + error.message); } } }; function apply(ctx, config) { ctx.plugin(Ocr, config); ctx.command("ocr [image:image]", "图像文字识别").option("image", "-i <url:string> 指定图片URL", { authority: 1 }).action(async ({ options, session }) => { if (!ctx.ocr || !ctx.ocr.isReady()) { return "OCR服务未就绪,请联系管理员检查配置"; } let imageElement = null; if (options.image) { imageElement = (0, import_koishi.h)("image", { url: options.image }); } else if (session.content) { const images = import_koishi.h.parse(session.content).filter((node) => node.type === "image"); if (images.length > 0) { imageElement = images[0]; } } if (!imageElement) { return "请提供图片"; } try { const result = await ctx.ocr.recognizeText(imageElement); return result || "未识别到任何文字"; } catch (error) { return `识别失败:${error.message}`; } }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, name });