From df609cce2f1cd0af2d3fbf3c5d6f89019ca66eea Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:03:54 +0800 Subject: [PATCH 01/23] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..4885b47 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# ocr-based-qwen +逆向https://chat.qwenlm.ai/ 的OCR From 62054add3b7c28654b7a3b2e481a622188983d27 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:05:59 +0800 Subject: [PATCH 02/23] Create worker.js --- worker.js | 772 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 772 insertions(+) create mode 100644 worker.js diff --git a/worker.js b/worker.js new file mode 100644 index 0000000..54a7e91 --- /dev/null +++ b/worker.js @@ -0,0 +1,772 @@ +addEventListener('fetch', event => { + event.respondWith(handleRequest(event.request)); +}); + +async function handleRequest(request) { + const url = new URL(request.url); + + // 处理 CORS 预检请求 + if (request.method === 'OPTIONS') { + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } + + // 处理 POST 请求 + if (request.method === 'POST' && url.pathname === '/recognize') { + try { + const { token, imageId } = await request.json(); + + if (!token || !imageId) { + return new Response(JSON.stringify({ error: 'Missing token or imageId' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 调用 QwenLM API + const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', { + method: 'POST', + headers: { + 'accept': '*/*', + 'authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + stream: false, + model: 'qwen-vl-max-latest', + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: '请严格只返回图片中的内容,不要添加任何解释、描述或多余的文字' }, + { type: 'image', image: imageId }, // 使用上传后的图片 ID + ], + }, + ], + session_id: '1', + chat_id: '2', + id: '3', + }), + }); + + const data = await response.json(); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (error) { + return new Response(JSON.stringify({ error: 'Internal Server Error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } + } + + // 返回前端界面 + return new Response(getHTML(), { + headers: { 'Content-Type': 'text/html' }, + }); +} + +function getHTML() { + return ` + + + + + + 智能图片识别 + + + + + + +
+

智能图片识别

+
+ 📸 +
+ 拖拽图片到这里,或点击上传
+ 支持复制粘贴图片 +
+ +
+
+
+
+ 识别结果 + +
+
+
+ +
+

识别历史

+
+
+
+ + + + + + + `; +} From 6b0d0520fe8b3a6f4c7f1a89ae4169276620d075 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:08:38 +0800 Subject: [PATCH 03/23] Update README.md --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4885b47..d5d8a15 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ -# ocr-based-qwen -逆向https://chat.qwenlm.ai/ 的OCR +# 🖼️ QwenLM OCR 逆向工程项目 + +本项目是对 [QwenLM](https://chat.qwenlm.ai/) 的 OCR 功能进行逆向工程的实现。通过调用 QwenLM 的 API,你可以从图片中提取文字内容,并且该项目支持一键部署到 **Cloudflare Workers** (CF) 上。 + +## 🚀 功能特性 + +- **图片 OCR**:使用 QwenLM 强大的 OCR 功能从图片中提取文字。 +- **拖拽上传**:直接将图片拖拽到页面即可识别。 +- **复制粘贴**:支持从剪贴板直接粘贴图片进行识别。 +- **Token 管理**:支持多 Token 轮询使用,提升稳定性。 +- **历史记录**:保存每次识别的结果和图片,方便查看。 +- **一键复制**:轻松复制识别结果到剪贴板。 + +## 🛠️ 部署指南 + +### 1. 部署到 Cloudflare Workers + +1. **配置 Cloudflare Workers**: + - 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)。 + - 创建一个新的 Worker。 + - 将 `worker.js` 中的代码复制到 Worker 编辑器中。 + +2. **部署**: + - 保存并部署 Worker。 + - 获取 Worker 的访问地址,即可使用。 + + +## 🧩 使用说明 + +1. **设置 Token**: + - 点击右上角的 **⚙️ Token设置** 按钮。 + - 输入你的 QwenLM API Token(多个 Token 用英文逗号分隔)。 + - 点击 **保存**。 + +2. **上传图片**: + - 拖拽图片到页面,或点击上传区域选择图片。 + - 支持直接粘贴图片。 + +3. **查看结果**: + - 识别结果会显示在页面下方。 + - 点击 **复制结果** 按钮,将识别内容复制到剪贴板。 + +4. **查看历史记录**: + - 点击左侧的 **📋 识别历史** 按钮,查看历史识别记录。 + - 点击历史记录中的图片,可以查看大图。 + + + +## 📜 许可证 + +本项目基于 MIT 许可证开源。详情请查看 [LICENSE](LICENSE) 文件。 + +## 🙏 致谢 + +- 感谢 [QwenLM](https://chat.qwenlm.ai/) 提供的 OCR 功能。 +- 感谢 Cloudflare 提供的 Workers 服务。 + + +--- + +🌟 如果觉得这个项目对你有帮助,欢迎点个 Star 支持一下!🌟 From 74ed7662fb915da67510359496f07c76264aedfd Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:11:40 +0800 Subject: [PATCH 04/23] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d5d8a15..fc68982 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # 🖼️ QwenLM OCR 逆向工程项目 本项目是对 [QwenLM](https://chat.qwenlm.ai/) 的 OCR 功能进行逆向工程的实现。通过调用 QwenLM 的 API,你可以从图片中提取文字内容,并且该项目支持一键部署到 **Cloudflare Workers** (CF) 上。 +## 项目展示 +![image](https://github.com/user-attachments/assets/0805489b-b5b2-454b-a46d-31c005113ca6) ## 🚀 功能特性 From 1afe2cbb00151154b5b6f836b6aa55d9d79bd811 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:45:05 +0800 Subject: [PATCH 05/23] Update README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fc68982..002e4a9 100644 --- a/README.md +++ b/README.md @@ -30,19 +30,22 @@ ## 🧩 使用说明 1. **设置 Token**: + - 获取token: + ![image](https://github.com/user-attachments/assets/125f702e-856b-4a11-b5da-8df654ac8b7e) + - 点击右上角的 **⚙️ Token设置** 按钮。 - 输入你的 QwenLM API Token(多个 Token 用英文逗号分隔)。 - 点击 **保存**。 -2. **上传图片**: +3. **上传图片**: - 拖拽图片到页面,或点击上传区域选择图片。 - 支持直接粘贴图片。 -3. **查看结果**: +4. **查看结果**: - 识别结果会显示在页面下方。 - 点击 **复制结果** 按钮,将识别内容复制到剪贴板。 -4. **查看历史记录**: +5. **查看历史记录**: - 点击左侧的 **📋 识别历史** 按钮,查看历史识别记录。 - 点击历史记录中的图片,可以查看大图。 From f251d38ca0f7eb5e224a05b6480dabbab0b13fde Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:47:17 +0800 Subject: [PATCH 06/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 002e4a9..325b9d2 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ## 🧩 使用说明 1. **设置 Token**: - - 获取token: + - 前往https://chat.qwenlm.ai/ 获取token: ![image](https://github.com/user-attachments/assets/125f702e-856b-4a11-b5da-8df654ac8b7e) - 点击右上角的 **⚙️ Token设置** 按钮。 From c6e39048c71e04d4d6dda6a863d27904989c5574 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:55:45 +0800 Subject: [PATCH 07/23] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 325b9d2..ed8a47c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ - 感谢 [QwenLM](https://chat.qwenlm.ai/) 提供的 OCR 功能。 - 感谢 Cloudflare 提供的 Workers 服务。 +- 感谢 @JIU-W,@lonelykkk的思路支持 --- From 3b1523b6f2f935d44c5357c1a90c5095349905d5 Mon Sep 17 00:00:00 2001 From: JIU-W <142167539+JIU-W@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:37:13 +0800 Subject: [PATCH 08/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed8a47c..560f0ff 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ - 感谢 [QwenLM](https://chat.qwenlm.ai/) 提供的 OCR 功能。 - 感谢 Cloudflare 提供的 Workers 服务。 -- 感谢 @JIU-W,@lonelykkk的思路支持 +- 感谢 @lonelykkk的思路支持 --- From 8e8e09067150168fb07d4425177e3d03e08de48f Mon Sep 17 00:00:00 2001 From: lonelykkk <140876474+lonelykkk@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:12:34 +0800 Subject: [PATCH 09/23] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 560f0ff..325b9d2 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ - 感谢 [QwenLM](https://chat.qwenlm.ai/) 提供的 OCR 功能。 - 感谢 Cloudflare 提供的 Workers 服务。 -- 感谢 @lonelykkk的思路支持 --- From 838adbd6e0df40e3e50f516607456bec07e851f9 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:45:57 +0800 Subject: [PATCH 10/23] Update README.md --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 325b9d2..7d7ad5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # 🖼️ QwenLM OCR 逆向工程项目 本项目是对 [QwenLM](https://chat.qwenlm.ai/) 的 OCR 功能进行逆向工程的实现。通过调用 QwenLM 的 API,你可以从图片中提取文字内容,并且该项目支持一键部署到 **Cloudflare Workers** (CF) 上。 + ## 项目展示 ![image](https://github.com/user-attachments/assets/0805489b-b5b2-454b-a46d-31c005113ca6) @@ -12,6 +13,8 @@ - **Token 管理**:支持多 Token 轮询使用,提升稳定性。 - **历史记录**:保存每次识别的结果和图片,方便查看。 - **一键复制**:轻松复制识别结果到剪贴板。 +- **数学公式识别**:特别优化了对数学公式的提取,支持 LaTeX 格式输出。 +- **API 支持**:提供 `curl` 接口调用,支持 base64 和图片 URL 两种方式。 ## 🛠️ 部署指南 @@ -26,30 +29,45 @@ - 保存并部署 Worker。 - 获取 Worker 的访问地址,即可使用。 - ## 🧩 使用说明 1. **设置 Token**: - - 前往https://chat.qwenlm.ai/ 获取token: - ![image](https://github.com/user-attachments/assets/125f702e-856b-4a11-b5da-8df654ac8b7e) - + - 前往 [QwenLM](https://chat.qwenlm.ai/) 获取 Token。 - 点击右上角的 **⚙️ Token设置** 按钮。 - 输入你的 QwenLM API Token(多个 Token 用英文逗号分隔)。 - 点击 **保存**。 -3. **上传图片**: +2. **上传图片**: - 拖拽图片到页面,或点击上传区域选择图片。 - 支持直接粘贴图片。 -4. **查看结果**: +3. **查看结果**: - 识别结果会显示在页面下方。 - 点击 **复制结果** 按钮,将识别内容复制到剪贴板。 -5. **查看历史记录**: +4. **查看历史记录**: - 点击左侧的 **📋 识别历史** 按钮,查看历史识别记录。 - 点击历史记录中的图片,可以查看大图。 - +5. **API 调用**: + - **支持 base64**: + ```bash + curl --location 'https://ocr.doublefenzhuan.me/api/recognize/base64' \ + --header 'Content-Type: application/json' \ + --data '{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUzZTk0Nzg4LWMwM2QtNDY4Mi05OTNhLWE0ZDNjNGUyZDY0OSIsImV4cCI6MTczOTA3NTE0MX0.FtwG6xDLYd2rngWUhuldg56WXCiLSTL0RI6xJJQ4vHM", + "base64Image": "xxx" + }' + ``` + - **支持图片 URL**: + ```bash + curl --location 'https://ocr.doublefenzhuan.me/api/recognize/url' \ + --header 'Content-Type: application/json' \ + --data '{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUzZTk0Nzg4LWMwM2QtNDY4Mi05OTNhLWE0ZDNjNGUyZDY0OSIsImV4cCI6MTczOTA3NTE0MX0.FtwG6xDLYd2rngWUhuldg56WXCiLSTL0RI6xJJQ4vHM", + "imageUrl": "xxxx" + }' + ``` ## 📜 许可证 @@ -60,7 +78,53 @@ - 感谢 [QwenLM](https://chat.qwenlm.ai/) 提供的 OCR 功能。 - 感谢 Cloudflare 提供的 Workers 服务。 - --- 🌟 如果觉得这个项目对你有帮助,欢迎点个 Star 支持一下!🌟 + +**体验地址**:[智能图片识别 (doublefenzhuan.me)](https://ocr.doublefenzhuan.me/) + +**GitHub 仓库**:[Cunninger/ocr-based-qwen](https://github.com/Cunninger/ocr-based-qwen) + +--- + +#### 后续计划 +- 优化数学公式识别精度; +- 增加更多 API 功能支持; +- 提升识别速度和稳定性。 + +快来体验吧!如果有任何问题或建议,欢迎在 GitHub 上提 Issue 或直接联系我! + +## 更新 +### 2025/01/13 应佬友需求,优化了对数学公式的识别,效果如下图 + - 原图: + +![image|690x401, 75%](upload://jsL7sKDLKKP4xpNNgm9kHXq7qdD.png) + 识别效果图: + ![image|536x500](upload://1mLEmxYZ3MPl0DvMsLSz7noQb4i.png) + +### 2025/01/13 18点34分 支持`curl`接口调用 +- **支持base64**: +``` +curl --location 'https://ocr.doublefenzhuan.me/api/recognize/base64' \ +--header 'Content-Type: application/json' \ +--data '{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUzZTk0Nzg4LWMwM2QtNDY4Mi05OTNhLWE0ZDNjNGUyZDY0OSIsImV4cCI6MTczOTA3NTE0MX0.FtwG6xDLYd2rngWUhuldg56WXCiLSTL0RI6xJJQ4vHM", + "base64Image": "xxx" +}' +``` +- 效果图: +![image](https://github.com/user-attachments/assets/ef160aae-e741-49d3-96f0-a0969b883f1a) + +- **支持图片URL**: +```bash +curl --location 'https://ocr.doublefenzhuan.me/api/recognize/url' \ +--header 'Content-Type: application/json' \ +--data '{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUzZTk0Nzg4LWMwM2QtNDY4Mi05OTNhLWE0ZDNjNGUyZDY0OSIsImV4cCI6MTczOTA3NTE0MX0.FtwG6xDLYd2rngWUhuldg56WXCiLSTL0RI6xJJQ4vHM", + + "imageUrl": "xxxx" +}' +``` +- 效果图: +![image](https://github.com/user-attachments/assets/db0c89f9-96f1-45b1-b1e9-88ac3d01e196) From 1178a56d624027a5724b3216d3b62e790dbd9da7 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:46:31 +0800 Subject: [PATCH 11/23] Update worker.js --- worker.js | 1803 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 1078 insertions(+), 725 deletions(-) diff --git a/worker.js b/worker.js index 54a7e91..b2b525c 100644 --- a/worker.js +++ b/worker.js @@ -16,757 +16,1110 @@ async function handleRequest(request) { }); } - // 处理 POST 请求 - if (request.method === 'POST' && url.pathname === '/recognize') { - try { - const { token, imageId } = await request.json(); - - if (!token || !imageId) { - return new Response(JSON.stringify({ error: 'Missing token or imageId' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); + // API路由处理 + switch (url.pathname) { + // 1. 通过图片URL识别 + case '/api/recognize/url': + if (request.method === 'POST') { + return handleImageUrlRecognition(request); } + break; - // 调用 QwenLM API - const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', { - method: 'POST', - headers: { - 'accept': '*/*', - 'authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - stream: false, - model: 'qwen-vl-max-latest', - messages: [ - { - role: 'user', - content: [ - { type: 'text', text: '请严格只返回图片中的内容,不要添加任何解释、描述或多余的文字' }, - { type: 'image', image: imageId }, // 使用上传后的图片 ID - ], - }, - ], - session_id: '1', - chat_id: '2', - id: '3', - }), - }); + // 2. 通过Base64识别 + case '/api/recognize/base64': + if (request.method === 'POST') { + return handleBase64Recognition(request); + } + break; - const data = await response.json(); - return new Response(JSON.stringify(data), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, + // 3. 通过图片文件识别 (原有的/recognize端点) + case '/recognize': + if (request.method === 'POST') { + return handleFileRecognition(request); + } + break; + + // 返回前端界面 + case '/': + return new Response(getHTML(), { + headers: { 'Content-Type': 'text/html' }, }); - } catch (error) { - return new Response(JSON.stringify({ error: 'Internal Server Error' }), { - status: 500, + } + + return new Response('Not Found', { status: 404 }); +} + +// 处理图片URL识别 +async function handleImageUrlRecognition(request) { + try { + const { token, imageUrl } = await request.json(); + + if (!token || !imageUrl) { + return new Response(JSON.stringify({ + error: 'Missing token or imageUrl' + }), { + status: 400, headers: { 'Content-Type': 'application/json' }, }); } - } - // 返回前端界面 - return new Response(getHTML(), { - headers: { 'Content-Type': 'text/html' }, + // 下载图片 + const imageResponse = await fetch(imageUrl); + const imageBlob = await imageResponse.blob(); + + // 上传到QwenLM + const formData = new FormData(); + formData.append('file', imageBlob); + + const uploadResponse = await fetch('https://chat.qwenlm.ai/api/v1/files/', { + method: 'POST', + headers: { + 'accept': 'application/json', + 'authorization': `Bearer ${token}`, + }, + body: formData, + }); + + const uploadData = await uploadResponse.json(); + if (!uploadData.id) throw new Error('File upload failed'); + + // 调用识别API + return await recognizeImage(token, uploadData.id); + } catch (error) { + return new Response(JSON.stringify({ + error: error.message || 'Internal Server Error' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// 处理Base64识别 +async function handleBase64Recognition(request) { + try { + const { token, base64Image } = await request.json(); + + if (!token || !base64Image) { + return new Response(JSON.stringify({ + error: 'Missing token or base64Image' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 转换Base64为Blob + const imageData = base64Image.startsWith('data:') ? + base64Image : + 'data:image/png;base64,' + base64Image; + + const response = await fetch(imageData); + const blob = await response.blob(); + + // 上传到QwenLM + const formData = new FormData(); + formData.append('file', blob); + + const uploadResponse = await fetch('https://chat.qwenlm.ai/api/v1/files/', { + method: 'POST', + headers: { + 'accept': 'application/json', + 'authorization': `Bearer ${token}`, + }, + body: formData, + }); + + const uploadData = await uploadResponse.json(); + if (!uploadData.id) throw new Error('File upload failed'); + + // 调用识别API + return await recognizeImage(token, uploadData.id); + } catch (error) { + return new Response(JSON.stringify({ + error: error.message || 'Internal Server Error' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// 处理文件识别 (原有功能) +async function handleFileRecognition(request) { + try { + const { token, imageId } = await request.json(); + + if (!token || !imageId) { + return new Response(JSON.stringify({ + error: 'Missing token or imageId' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return await recognizeImage(token, imageId); + } catch (error) { + return new Response(JSON.stringify({ + error: error.message || 'Internal Server Error' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// 通用的识别函数 +async function recognizeImage(token, imageId) { + const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', { + method: 'POST', + headers: { + 'accept': '*/*', + 'authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + stream: false, + model: 'qwen-vl-max-latest', + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: '请识别图片中的内容。对于数学公式和数学符号,请使用标准的LaTeX格式输出。' + + '要求:\n' + + '1. 所有数学公式和单个数学符号都要用LaTeX格式\n' + + '2. 普通文本保持原样\n' + + '3. 对于行内公式使用$单个符号$\n' + + '4. 对于独立公式块使用$$公式$$\n' + + '5. 严格保持原文的段落格式和换行\n' + + '6. 当文本明显换行时,使用\\n进行换行处理' + }, + { type: 'image', image: imageId }, + ], + }, + ], + session_id: '1', + chat_id: '2', + id: '3', + }), + }); + + const data = await response.json(); + + // 处理识别结果 + let result = data.choices[0]?.message?.content || '识别失败'; + result = result + .replace(/\\(/g, '\\(') + .replace(/\\)/g, '\\)') + .replace(/\n{3,}/g, '\n\n') + .replace(/([^\n])\n([^\n])/g, '$1\n$2') + .trim(); + + return new Response(JSON.stringify({ + success: true, + result: result + }), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, }); } function getHTML() { - return ` - - - - - - 智能图片识别 - - - - - - -
-

智能图片识别

-
- 📸 -
- 拖拽图片到这里,或点击上传
- 支持复制粘贴图片 -
- -
-
-
-
- 识别结果 - -
-
-
- -
-

识别历史

-
-
-
- - - - ', + '', + '', + + '', + '', + '', + '', + '', + + '
', + '

智能图片识别

', + '
', + '📸', + '
', + '拖拽图片到这里,点击上传,或粘贴Base64图片内容
', + '支持复制粘贴图片', + '
', + '', + '', + '', + '
', + '
', + '
', + '
', + '识别结果', + '', + '
', + '
', + '
', + '', + '
', + '

识别历史

', + '
', + '
', + '
', + + '', + + '', + '', + '' + ].join('\n'); + + return html; } From 3da4260d990e2bb932647f33f623348f103746a1 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:47:59 +0800 Subject: [PATCH 12/23] Update README.md --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d7ad5f..d496b17 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,13 @@ ## 更新 ### 2025/01/13 应佬友需求,优化了对数学公式的识别,效果如下图 - - 原图: +- 原图: -![image|690x401, 75%](upload://jsL7sKDLKKP4xpNNgm9kHXq7qdD.png) - 识别效果图: - ![image|536x500](upload://1mLEmxYZ3MPl0DvMsLSz7noQb4i.png) +![image](https://github.com/user-attachments/assets/9841509d-be56-4eb9-aafa-4d4ca5555c2e) + +- 识别效果图: +![image](https://github.com/user-attachments/assets/2340dc6d-9156-4866-aa53-cdfd1911a651) + ### 2025/01/13 18点34分 支持`curl`接口调用 - **支持base64**: From a6a5241c269a70a388dc862ea3c08ee8a5a7a219 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:50:30 +0800 Subject: [PATCH 13/23] Update worker.js --- worker.js | 379 +++++++++++------------------------------------------- 1 file changed, 77 insertions(+), 302 deletions(-) diff --git a/worker.js b/worker.js index b2b525c..12269de 100644 --- a/worker.js +++ b/worker.js @@ -16,216 +16,86 @@ async function handleRequest(request) { }); } - // API路由处理 - switch (url.pathname) { - // 1. 通过图片URL识别 - case '/api/recognize/url': - if (request.method === 'POST') { - return handleImageUrlRecognition(request); + // 处理 POST 请求 + if (request.method === 'POST' && url.pathname === '/recognize') { + try { + const { token, imageId } = await request.json(); + + if (!token || !imageId) { + return new Response(JSON.stringify({ error: 'Missing token or imageId' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); } - break; - // 2. 通过Base64识别 - case '/api/recognize/base64': - if (request.method === 'POST') { - return handleBase64Recognition(request); - } - break; - - // 3. 通过图片文件识别 (原有的/recognize端点) - case '/recognize': - if (request.method === 'POST') { - return handleFileRecognition(request); - } - break; - - // 返回前端界面 - case '/': - return new Response(getHTML(), { - headers: { 'Content-Type': 'text/html' }, - }); - } - - return new Response('Not Found', { status: 404 }); -} - -// 处理图片URL识别 -async function handleImageUrlRecognition(request) { - try { - const { token, imageUrl } = await request.json(); - - if (!token || !imageUrl) { - return new Response(JSON.stringify({ - error: 'Missing token or imageUrl' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - - // 下载图片 - const imageResponse = await fetch(imageUrl); - const imageBlob = await imageResponse.blob(); - - // 上传到QwenLM - const formData = new FormData(); - formData.append('file', imageBlob); - - const uploadResponse = await fetch('https://chat.qwenlm.ai/api/v1/files/', { - method: 'POST', - headers: { - 'accept': 'application/json', - 'authorization': `Bearer ${token}`, - }, - body: formData, - }); - - const uploadData = await uploadResponse.json(); - if (!uploadData.id) throw new Error('File upload failed'); - - // 调用识别API - return await recognizeImage(token, uploadData.id); - } catch (error) { - return new Response(JSON.stringify({ - error: error.message || 'Internal Server Error' - }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }); - } -} - -// 处理Base64识别 -async function handleBase64Recognition(request) { - try { - const { token, base64Image } = await request.json(); - - if (!token || !base64Image) { - return new Response(JSON.stringify({ - error: 'Missing token or base64Image' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - - // 转换Base64为Blob - const imageData = base64Image.startsWith('data:') ? - base64Image : - 'data:image/png;base64,' + base64Image; - - const response = await fetch(imageData); - const blob = await response.blob(); - - // 上传到QwenLM - const formData = new FormData(); - formData.append('file', blob); - - const uploadResponse = await fetch('https://chat.qwenlm.ai/api/v1/files/', { - method: 'POST', - headers: { - 'accept': 'application/json', - 'authorization': `Bearer ${token}`, - }, - body: formData, - }); - - const uploadData = await uploadResponse.json(); - if (!uploadData.id) throw new Error('File upload failed'); - - // 调用识别API - return await recognizeImage(token, uploadData.id); - } catch (error) { - return new Response(JSON.stringify({ - error: error.message || 'Internal Server Error' - }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }); - } -} - -// 处理文件识别 (原有功能) -async function handleFileRecognition(request) { - try { - const { token, imageId } = await request.json(); - - if (!token || !imageId) { - return new Response(JSON.stringify({ - error: 'Missing token or imageId' - }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); - } - - return await recognizeImage(token, imageId); - } catch (error) { - return new Response(JSON.stringify({ - error: error.message || 'Internal Server Error' - }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }); - } -} - -// 通用的识别函数 -async function recognizeImage(token, imageId) { - const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', { - method: 'POST', - headers: { - 'accept': '*/*', - 'authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - stream: false, - model: 'qwen-vl-max-latest', - messages: [ - { - role: 'user', - content: [ - { - type: 'text', - text: '请识别图片中的内容。对于数学公式和数学符号,请使用标准的LaTeX格式输出。' + - '要求:\n' + - '1. 所有数学公式和单个数学符号都要用LaTeX格式\n' + - '2. 普通文本保持原样\n' + - '3. 对于行内公式使用$单个符号$\n' + - '4. 对于独立公式块使用$$公式$$\n' + - '5. 严格保持原文的段落格式和换行\n' + - '6. 当文本明显换行时,使用\\n进行换行处理' - }, - { type: 'image', image: imageId }, - ], + // 调用 QwenLM API + const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', { + method: 'POST', + headers: { + 'accept': '*/*', + 'authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', }, - ], - session_id: '1', - chat_id: '2', - id: '3', - }), - }); + body: JSON.stringify({ + stream: false, + model: 'qwen-vl-max-latest', + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: '请识别图片中的内容。对于数学公式和数学符号,请使用标准的LaTeX格式输出。' + + '要求:\n' + + '1. 所有数学公式和单个数学符号都要用LaTeX格式\n' + + '2. 普通文本保持原样\n' + + '3. 对于行内公式使用$单个符号$\n' + + '4. 对于独立公式块使用$$公式$$\n' + + '5. 严格保持原文的段落格式和换行\n' + + '6. 当文本明显换行时,使用\\n进行换行处理\n' + + '请尽可能精确地转换每个数学符号并保持原始排版格式。' + }, + { type: 'image', image: imageId }, // 使用上传后的图片 ID + ], + }, + ], + session_id: '1', + chat_id: '2', + id: '3', + }), + }); - const data = await response.json(); - - // 处理识别结果 - let result = data.choices[0]?.message?.content || '识别失败'; - result = result - .replace(/\\(/g, '\\(') - .replace(/\\)/g, '\\)') - .replace(/\n{3,}/g, '\n\n') - .replace(/([^\n])\n([^\n])/g, '$1\n$2') - .trim(); + const data = await response.json(); + + // 对识别结果进行后处理,确保LaTeX格式正确并保持换行 + let result = data.choices[0]?.message?.content || '识别失败'; + result = result + // 修复可能的LaTeX格式问题 + .replace(/\\(/g, '\\(') + .replace(/\\)/g, '\\)') + // 确保连续的换行符被保留(2个以上的换行符表示段落分隔) + .replace(/\n{3,}/g, '\n\n') + // 保留单个换行符,不合并 + .replace(/([^\n])\n([^\n])/g, '$1\n$2') + .trim(); - return new Response(JSON.stringify({ - success: true, - result: result - }), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, + return new Response(JSON.stringify({ ...data, choices: [{ message: { content: result } }] }), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (error) { + return new Response(JSON.stringify({ error: 'Internal Server Error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } + } + + // 返回前端界面 + return new Response(getHTML(), { + headers: { 'Content-Type': 'text/html' }, }); } @@ -686,30 +556,6 @@ function getHTML() { ' .copy-btn.copied {', ' background: #27ae60;', ' }', - - ' /* Base64输入相关样式 */', - ' #base64Input {', - ' width: 100%;', - ' height: 100px;', - ' padding: 10px;', - ' margin-top: 10px;', - ' border: 1px solid #dcdde1;', - ' border-radius: 8px;', - ' resize: vertical;', - ' }', - ' .toggle-btn {', - ' background: #3498db;', - ' color: white;', - ' border: none;', - ' padding: 8px 15px;', - ' border-radius: 5px;', - ' cursor: pointer;', - ' margin-top: 10px;', - ' transition: background 0.3s ease;', - ' }', - ' .toggle-btn:hover {', - ' background: #2980b9;', - ' }', '', '', '', @@ -732,11 +578,9 @@ function getHTML() { '
', '📸', '
', - '拖拽图片到这里,点击上传,或粘贴Base64图片内容
', + '拖拽图片到这里,或点击上传
', '支持复制粘贴图片', '
', - '', - '', '', '
', '
', @@ -976,15 +820,7 @@ function getHTML() { ' });', ' // 点击上传', - ' uploadArea.addEventListener(\'click\', (e) => {', - ' // 如果点击的是 base64Input 或 toggleBase64 按钮,不触发文件上传', - ' if (e.target.id === \'base64Input\' || ', - ' e.target.id === \'toggleBase64\' || ', - ' e.target.closest(\'#base64Input\') || ', - ' e.target.closest(\'#toggleBase64\')) {', - ' return;', - ' }', - + ' uploadArea.addEventListener(\'click\', () => {', ' const input = document.createElement(\'input\');', ' input.type = \'file\';', ' input.accept = \'image/*\';', @@ -1039,7 +875,6 @@ function getHTML() { ' // 复制结果功能', ' copyBtn.addEventListener(\'click\', async () => {', ' const result = resultDiv.textContent;', - ' toggleBase64.textContent = \'隐藏Base64输入\';', ' try {', ' await navigator.clipboard.writeText(result);', ' copyBtn.textContent = \'已复制\';', @@ -1056,66 +891,6 @@ function getHTML() { ' document.getElementById("closeSidebar").addEventListener("click", () => {', ' sidebar.classList.remove("open");', ' });', - ' // Base64 输入相关功能', - ' const base64Input = document.getElementById(\'base64Input\');', - ' const toggleBase64 = document.getElementById(\'toggleBase64\');', - - ' // 切换 Base64 输入框显示', - ' toggleBase64.addEventListener(\'click\', (e) => {', - ' e.stopPropagation(); // 阻止事件冒泡到 uploadArea', - ' if (base64Input.style.display === \'none\') {', - ' base64Input.style.display = \'block\';', - ' toggleBase64.textContent = \'隐藏Base64输入\';', - ' } else {', - ' base64Input.style.display = \'none\';', - ' toggleBase64.textContent = \'切换Base64输入\';', - ' }', - ' });', - - ' // 为 base64Input 添加阻止事件冒泡', - ' document.getElementById(\'base64Input\').addEventListener(\'click\', (e) => {', - ' e.stopPropagation(); // 阻止事件冒泡到 uploadArea', - ' });', - - ' // base64Input 的 input 事件处理也需要阻止冒泡', - ' base64Input.addEventListener(\'input\', async (e) => {', - ' e.stopPropagation();', - ' const base64Content = base64Input.value.trim();', - ' if (base64Content) {', - ' try {', - ' // 尝试转换Base64为Blob', - ' let imageData;', - ' if (base64Content.startsWith(\'data:image\')) {', - ' imageData = base64Content;', - ' } else {', - ' imageData = \'data:image/png;base64,\' + base64Content;', - ' }', - - ' // 验证Base64是否为有效图片', - ' const img = new Image();', - ' img.src = imageData;', - ' await new Promise((resolve, reject) => {', - ' img.onload = resolve;', - ' img.onerror = reject;', - ' });', - - ' // 转换Base64为Blob', - ' const response = await fetch(imageData);', - ' const blob = await response.blob();', - ' const file = new File([blob], "image.png", { type: "image/png" });', - - ' // 显示预览', - ' previewImage.src = imageData;', - ' previewImage.style.display = \'block\';', - - ' // 处理图片', - ' processImage(file);', - ' } catch (error) {', - ' alert(\'无效的Base64图片内容\');', - ' console.error(\'Base64处理错误:\', error);', - ' }', - ' }', - ' });', '', '', '' From 2a39a2c9693f865be2cf3bf957ad3d8202181439 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:54:12 +0800 Subject: [PATCH 14/23] Update worker.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 新增数学公式识别功能,支持 LaTeX 格式输出 feat: 新增 API 调用支持,提供 base64 和图片 URL 两种方式 --- worker.js | 384 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 307 insertions(+), 77 deletions(-) diff --git a/worker.js b/worker.js index 12269de..58a13d7 100644 --- a/worker.js +++ b/worker.js @@ -16,86 +16,216 @@ async function handleRequest(request) { }); } - // 处理 POST 请求 - if (request.method === 'POST' && url.pathname === '/recognize') { - try { - const { token, imageId } = await request.json(); - - if (!token || !imageId) { - return new Response(JSON.stringify({ error: 'Missing token or imageId' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }); + // API路由处理 + switch (url.pathname) { + // 1. 通过图片URL识别 + case '/api/recognize/url': + if (request.method === 'POST') { + return handleImageUrlRecognition(request); } + break; - // 调用 QwenLM API - const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', { - method: 'POST', - headers: { - 'accept': '*/*', - 'authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - stream: false, - model: 'qwen-vl-max-latest', - messages: [ - { - role: 'user', - content: [ - { - type: 'text', - text: '请识别图片中的内容。对于数学公式和数学符号,请使用标准的LaTeX格式输出。' + - '要求:\n' + - '1. 所有数学公式和单个数学符号都要用LaTeX格式\n' + - '2. 普通文本保持原样\n' + - '3. 对于行内公式使用$单个符号$\n' + - '4. 对于独立公式块使用$$公式$$\n' + - '5. 严格保持原文的段落格式和换行\n' + - '6. 当文本明显换行时,使用\\n进行换行处理\n' + - '请尽可能精确地转换每个数学符号并保持原始排版格式。' - }, - { type: 'image', image: imageId }, // 使用上传后的图片 ID - ], - }, - ], - session_id: '1', - chat_id: '2', - id: '3', - }), + // 2. 通过Base64识别 + case '/api/recognize/base64': + if (request.method === 'POST') { + return handleBase64Recognition(request); + } + break; + + // 3. 通过图片文件识别 (原有的/recognize端点) + case '/recognize': + if (request.method === 'POST') { + return handleFileRecognition(request); + } + break; + + // 返回前端界面 + case '/': + return new Response(getHTML(), { + headers: { 'Content-Type': 'text/html' }, }); + } - const data = await response.json(); - - // 对识别结果进行后处理,确保LaTeX格式正确并保持换行 - let result = data.choices[0]?.message?.content || '识别失败'; - result = result - // 修复可能的LaTeX格式问题 - .replace(/\\(/g, '\\(') - .replace(/\\)/g, '\\)') - // 确保连续的换行符被保留(2个以上的换行符表示段落分隔) - .replace(/\n{3,}/g, '\n\n') - // 保留单个换行符,不合并 - .replace(/([^\n])\n([^\n])/g, '$1\n$2') - .trim(); + return new Response('Not Found', { status: 404 }); +} - return new Response(JSON.stringify({ ...data, choices: [{ message: { content: result } }] }), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }); - } catch (error) { - return new Response(JSON.stringify({ error: 'Internal Server Error' }), { - status: 500, +// 处理图片URL识别 +async function handleImageUrlRecognition(request) { + try { + const { token, imageUrl } = await request.json(); + + if (!token || !imageUrl) { + return new Response(JSON.stringify({ + error: 'Missing token or imageUrl' + }), { + status: 400, headers: { 'Content-Type': 'application/json' }, }); } - } - // 返回前端界面 - return new Response(getHTML(), { - headers: { 'Content-Type': 'text/html' }, + // 下载图片 + const imageResponse = await fetch(imageUrl); + const imageBlob = await imageResponse.blob(); + + // 上传到QwenLM + const formData = new FormData(); + formData.append('file', imageBlob); + + const uploadResponse = await fetch('https://chat.qwenlm.ai/api/v1/files/', { + method: 'POST', + headers: { + 'accept': 'application/json', + 'authorization': `Bearer ${token}`, + }, + body: formData, + }); + + const uploadData = await uploadResponse.json(); + if (!uploadData.id) throw new Error('File upload failed'); + + // 调用识别API + return await recognizeImage(token, uploadData.id); + } catch (error) { + return new Response(JSON.stringify({ + error: error.message || 'Internal Server Error' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// 处理Base64识别 +async function handleBase64Recognition(request) { + try { + const { token, base64Image } = await request.json(); + + if (!token || !base64Image) { + return new Response(JSON.stringify({ + error: 'Missing token or base64Image' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // 转换Base64为Blob + const imageData = base64Image.startsWith('data:') ? + base64Image : + 'data:image/png;base64,' + base64Image; + + const response = await fetch(imageData); + const blob = await response.blob(); + + // 上传到QwenLM + const formData = new FormData(); + formData.append('file', blob); + + const uploadResponse = await fetch('https://chat.qwenlm.ai/api/v1/files/', { + method: 'POST', + headers: { + 'accept': 'application/json', + 'authorization': `Bearer ${token}`, + }, + body: formData, + }); + + const uploadData = await uploadResponse.json(); + if (!uploadData.id) throw new Error('File upload failed'); + + // 调用识别API + return await recognizeImage(token, uploadData.id); + } catch (error) { + return new Response(JSON.stringify({ + error: error.message || 'Internal Server Error' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// 处理文件识别 (原有功能) +async function handleFileRecognition(request) { + try { + const { token, imageId } = await request.json(); + + if (!token || !imageId) { + return new Response(JSON.stringify({ + error: 'Missing token or imageId' + }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return await recognizeImage(token, imageId); + } catch (error) { + return new Response(JSON.stringify({ + error: error.message || 'Internal Server Error' + }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// 通用的识别函数 +async function recognizeImage(token, imageId) { + const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', { + method: 'POST', + headers: { + 'accept': '*/*', + 'authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + stream: false, + model: 'qwen-vl-max-latest', + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: '请识别图片中的内容。对于数学公式和数学符号,请使用标准的LaTeX格式输出。' + + '要求:\n' + + '1. 所有数学公式和单个数学符号都要用LaTeX格式\n' + + '2. 普通文本保持原样\n' + + '3. 对于行内公式使用$单个符号$\n' + + '4. 对于独立公式块使用$$公式$$\n' + + '5. 严格保持原文的段落格式和换行\n' + + '6. 当文本明显换行时,使用\\n进行换行处理' + }, + { type: 'image', image: imageId }, + ], + }, + ], + session_id: '1', + chat_id: '2', + id: '3', + }), + }); + + const data = await response.json(); + + // 处理识别结果 + let result = data.choices[0]?.message?.content || '识别失败'; + result = result + .replace(/\\(/g, '\\(') + .replace(/\\)/g, '\\)') + .replace(/\n{3,}/g, '\n\n') + .replace(/([^\n])\n([^\n])/g, '$1\n$2') + .trim(); + + return new Response(JSON.stringify({ + success: true, + result: result + }), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, }); } @@ -556,6 +686,30 @@ function getHTML() { ' .copy-btn.copied {', ' background: #27ae60;', ' }', + + ' /* Base64输入相关样式 */', + ' #base64Input {', + ' width: 100%;', + ' height: 100px;', + ' padding: 10px;', + ' margin-top: 10px;', + ' border: 1px solid #dcdde1;', + ' border-radius: 8px;', + ' resize: vertical;', + ' }', + ' .toggle-btn {', + ' background: #3498db;', + ' color: white;', + ' border: none;', + ' padding: 8px 15px;', + ' border-radius: 5px;', + ' cursor: pointer;', + ' margin-top: 10px;', + ' transition: background 0.3s ease;', + ' }', + ' .toggle-btn:hover {', + ' background: #2980b9;', + ' }', '', '', '', @@ -578,9 +732,11 @@ function getHTML() { '
', '📸', '
', - '拖拽图片到这里,或点击上传
', + '拖拽图片到这里,点击上传,或粘贴Base64图片内容
', '支持复制粘贴图片', '
', + '', + '', '', '
', '
', @@ -781,10 +937,14 @@ function getHTML() { ' const recognizeData = await recognizeResponse.json();', - ' // 提取并显示识别结果', - ' const result = recognizeData.choices[0]?.message?.content || \'识别失败\';', - ' resultDiv.innerHTML = result;', // 使用innerHTML而不是textContent以支持公式渲染 - ' MathJax.typesetPromise([resultDiv]).then(() => {', // 渲染数学公式 + ' // 修改这里:使用新的响应格式', + ' if (!recognizeData.success) {', + ' throw new Error(recognizeData.error || \'识别失败\');', + ' }', + + ' const result = recognizeData.result || \'识别失败\';', + ' resultDiv.innerHTML = result; // 使用innerHTML而不是textContent以支持公式渲染', + ' MathJax.typesetPromise([resultDiv]).then(() => {', // 渲染数学公式', ' resultContainer.classList.add(\'show\');', ' });', @@ -820,7 +980,15 @@ function getHTML() { ' });', ' // 点击上传', - ' uploadArea.addEventListener(\'click\', () => {', + ' uploadArea.addEventListener(\'click\', (e) => {', + ' // 如果点击的是 base64Input 或 toggleBase64 按钮,不触发文件上传', + ' if (e.target.id === \'base64Input\' || ', + ' e.target.id === \'toggleBase64\' || ', + ' e.target.closest(\'#base64Input\') || ', + ' e.target.closest(\'#toggleBase64\')) {', + ' return;', + ' }', + ' const input = document.createElement(\'input\');', ' input.type = \'file\';', ' input.accept = \'image/*\';', @@ -875,6 +1043,7 @@ function getHTML() { ' // 复制结果功能', ' copyBtn.addEventListener(\'click\', async () => {', ' const result = resultDiv.textContent;', + ' toggleBase64.textContent = \'隐藏Base64输入\';', ' try {', ' await navigator.clipboard.writeText(result);', ' copyBtn.textContent = \'已复制\';', @@ -891,6 +1060,67 @@ function getHTML() { ' document.getElementById("closeSidebar").addEventListener("click", () => {', ' sidebar.classList.remove("open");', ' });', + ' // Base64 输入相关功能', + ' const base64Input = document.getElementById(\'base64Input\');', + ' const toggleBase64 = document.getElementById(\'toggleBase64\');', + + ' // 切换 Base64 输入框显示', + ' toggleBase64.addEventListener(\'click\', (e) => {', + ' e.stopPropagation(); // 阻止事件冒泡到 uploadArea', + ' if (base64Input.style.display === \'none\') {', + ' base64Input.style.display = \'block\';', + ' toggleBase64.textContent = \'隐藏Base64输入\';', + ' } else {', + ' base64Input.style.display = \'none\';', + ' toggleBase64.textContent = \'切换Base64输入\';', + ' }', + ' });', + + ' // 为 base64Input 添加阻止事件冒泡', + ' document.getElementById(\'base64Input\').addEventListener(\'click\', (e) => {', + ' e.stopPropagation(); // 阻止事件冒泡到 uploadArea', + ' });', + + ' // base64Input 的 input 事件处理也需要阻止冒泡', + ' base64Input.addEventListener(\'input\', async (e) => {', + ' e.stopPropagation();', + ' const base64Content = base64Input.value.trim();', + ' if (base64Content) {', + ' try {', + ' // 尝试转换Base64为Blob', + ' let imageData;', + ' if (base64Content.startsWith(\'data:image\')) {', + ' imageData = base64Content;', + ' } else {', + ' imageData = \'data:image/png;base64,\' + base64Content;', + ' }', + + ' // 验证Base64是否为有效图片', + ' const img = new Image();', + ' img.src = imageData;', + ' await new Promise((resolve, reject) => {', + ' img.onload = resolve;', + ' img.onerror = reject;', + ' });', + + ' // 转换Base64为Blob', + ' const response = await fetch(imageData);', + ' const blob = await response.blob();', + ' const file = new File([blob], "image.png", { type: "image/png" });', + + ' // 显示预览', + ' previewImage.src = imageData;', + ' previewImage.style.display = \'block\';', + + ' // 处理图片', + ' await processImage(file);', + ' } catch (error) {', + ' resultDiv.textContent = \'处理失败: \' + error.message;', + ' resultContainer.classList.add(\'show\');', + ' console.error(\'Base64处理错误:\', error);', + ' }', + ' }', + ' });', '', '', '' From ee53bd6ecfc18e8f1ee78d3584bb2a8de4408efc Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:21:00 +0800 Subject: [PATCH 15/23] Update worker.js --- worker.js | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 318 insertions(+), 9 deletions(-) diff --git a/worker.js b/worker.js index 58a13d7..f7f2ff0 100644 --- a/worker.js +++ b/worker.js @@ -236,7 +236,7 @@ function getHTML() { '', '', '', - '智能公式识别', + 'Qwen VL 智能识别系统', // 添加 MathJax 支持 '', @@ -273,6 +273,7 @@ function getHTML() { ' .container {', ' background: rgba(255, 255, 255, 0.95);', ' padding: 2.5rem;', + ' padding-bottom: 4rem;', ' border-radius: 16px;', ' box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);', ' width: 90%;', @@ -282,9 +283,63 @@ function getHTML() { ' h1 {', ' color: #2c3e50;', - ' margin-bottom: 1.5rem;', - ' font-size: 1.8rem;', + ' margin-bottom: 0.5rem;', + ' font-size: 2.2rem;', ' text-align: center;', + ' font-weight: 700;', + ' text-transform: uppercase;', + ' letter-spacing: 2px;', + ' background: linear-gradient(135deg, #1a5fb4 0%, #3498db 100%);', + ' -webkit-background-clip: text;', + ' -webkit-text-fill-color: transparent;', + ' text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);', + ' position: relative;', + ' padding-bottom: 10px;', + ' animation: titleFadeIn 1s ease-out;', + ' }', + + ' @keyframes titleFadeIn {', + ' from {', + ' opacity: 0;', + ' transform: translateY(-20px);', + ' }', + ' to {', + ' opacity: 1;', + ' transform: translateY(0);', + ' }', + ' }', + + ' h1::after {', + ' content: "";', + ' position: absolute;', + ' bottom: 0;', + ' left: 50%;', + ' transform: translateX(-50%);', + ' width: 100px;', + ' height: 3px;', + ' background: linear-gradient(90deg, transparent, #3498db, transparent);', + ' }', + + ' .subtitle {', + ' color: #7f8c8d;', + ' text-align: center;', + ' font-size: 1.1rem;', + ' margin-bottom: 1.5rem;', + ' font-weight: 300;', + ' letter-spacing: 1px;', + ' opacity: 0.8;', + ' animation: subtitleFadeIn 1s ease-out 0.3s both;', + ' }', + + ' @keyframes subtitleFadeIn {', + ' from {', + ' opacity: 0;', + ' transform: translateY(10px);', + ' }', + ' to {', + ' opacity: 0.8;', + ' transform: translateY(0);', + ' }', ' }', ' .upload-area {', @@ -631,11 +686,12 @@ function getHTML() { ' /* 添加左侧边栏样式 */', ' .history-sidebar {', ' position: fixed;', - ' left: -300px;', + ' left: -400px;', ' top: 0;', - ' width: 300px;', + ' width: 400px;', ' height: 100vh;', - ' background: white;', + ' background: rgba(255, 255, 255, 0.98);', + ' backdrop-filter: blur(10px);', ' box-shadow: 5px 0 15px rgba(0, 0, 0, 0.1);', ' transition: left 0.3s ease;', ' padding: 20px;', @@ -710,6 +766,193 @@ function getHTML() { ' .toggle-btn:hover {', ' background: #2980b9;', ' }', + + ' /* 修改历史记录侧边栏样式 */', + ' .history-item {', + ' background: #ffffff;', + ' border-radius: 12px;', + ' margin-bottom: 20px;', + ' box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);', + ' overflow: hidden;', + ' transition: transform 0.2s ease, box-shadow 0.2s ease;', + ' }', + + ' .history-item:hover {', + ' transform: translateY(-2px);', + ' box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);', + ' }', + + ' .history-image-container {', + ' position: relative;', + ' width: 100%;', + ' height: 200px;', + ' overflow: hidden;', + ' }', + + ' .history-image {', + ' width: 100%;', + ' height: 100%;', + ' object-fit: cover;', + ' transition: transform 0.3s ease;', + ' }', + + ' .image-overlay {', + ' position: absolute;', + ' top: 0;', + ' left: 0;', + ' width: 100%;', + ' height: 100%;', + ' background: rgba(0, 0, 0, 0.4);', + ' display: flex;', + ' justify-content: center;', + ' align-items: center;', + ' opacity: 0;', + ' transition: opacity 0.3s ease;', + ' }', + + ' .history-image-container:hover .image-overlay {', + ' opacity: 1;', + ' }', + + ' .history-image-container:hover .history-image {', + ' transform: scale(1.05);', + ' }', + + ' .overlay-btn {', + ' background: rgba(255, 255, 255, 0.9);', + ' color: #2c3e50;', + ' border: none;', + ' padding: 8px 16px;', + ' border-radius: 20px;', + ' cursor: pointer;', + ' font-size: 0.9rem;', + ' transition: all 0.2s ease;', + ' }', + + ' .overlay-btn:hover {', + ' background: #ffffff;', + ' transform: scale(1.05);', + ' }', + + ' .history-content {', + ' padding: 16px;', + ' }', + + ' .history-header {', + ' display: flex;', + ' justify-content: space-between;', + ' align-items: center;', + ' margin-bottom: 12px;', + ' padding-bottom: 12px;', + ' border-bottom: 1px solid #eee;', + ' }', + + ' .history-time {', + ' color: #7f8c8d;', + ' font-size: 0.9rem;', + ' }', + + ' .history-actions {', + ' display: flex;', + ' gap: 8px;', + ' }', + + ' .action-btn {', + ' background: none;', + ' border: 1px solid #e0e0e0;', + ' padding: 4px 8px;', + ' border-radius: 4px;', + ' cursor: pointer;', + ' font-size: 0.8rem;', + ' transition: all 0.2s ease;', + ' }', + + ' .action-btn.copy-btn {', + ' color: #3498db;', + ' }', + + ' .action-btn.delete-btn {', + ' color: #e74c3c;', + ' }', + + ' .action-btn:hover {', + ' background: #f8f9fa;', + ' transform: translateY(-1px);', + ' }', + + ' .history-text {', + ' color: #2c3e50;', + ' font-size: 0.95rem;', + ' line-height: 1.6;', + ' max-height: 200px;', + ' overflow-y: auto;', + ' padding-right: 8px;', + ' }', + + ' .history-text::-webkit-scrollbar {', + ' width: 4px;', + ' }', + + ' .history-text::-webkit-scrollbar-track {', + ' background: #f1f1f1;', + ' }', + + ' .history-text::-webkit-scrollbar-thumb {', + ' background: #c0c0c0;', + ' border-radius: 2px;', + ' }', + + ' .footer {', + ' position: fixed;', + ' bottom: 0;', + ' left: 0;', + ' width: 100%;', + ' padding: 15px;', + ' text-align: center;', + ' background: rgba(255, 255, 255, 0.9);', + ' backdrop-filter: blur(5px);', + ' z-index: 900;', + ' border-top: 1px solid rgba(0, 0, 0, 0.1);', + ' }', + + ' .powered-by {', + ' color: #7f8c8d;', + ' font-size: 0.9rem;', + ' }', + + ' .powered-by a {', + ' color: #3498db;', + ' text-decoration: none;', + ' transition: color 0.3s ease;', + ' font-weight: 500;', + ' }', + + ' .powered-by a:hover {', + ' color: #2980b9;', + ' }', + + ' .footer-content {', + ' margin-top: 2rem;', + ' padding-top: 1rem;', + ' border-top: 1px solid #eee;', + ' text-align: center;', + ' }', + + ' .powered-by {', + ' color: #7f8c8d;', + ' font-size: 0.9rem;', + ' }', + + ' .powered-by a {', + ' color: #3498db;', + ' text-decoration: none;', + ' transition: color 0.3s ease;', + ' font-weight: 500;', + ' }', + + ' .powered-by a:hover {', + ' color: #2980b9;', + ' }', '', '', '', @@ -728,7 +971,8 @@ function getHTML() { '', '
', - '

智能图片识别

', + '

Qwen VL 智能识别系统

', + '
基于通义千问大模型的多模态智能识别引擎
', '
', '📸', '
', @@ -752,6 +996,11 @@ function getHTML() { '

识别历史

', '
', '
', + '', '
', '\';', + ' html += \'
\';', ' }', ' historyList.innerHTML = html;', + + ' // 渲染数学公式', + ' MathJax.typesetPromise([historyList]);', ' };', ' // 初始化变量', @@ -1019,6 +1292,12 @@ function getHTML() { ' function showFullImage(src) {', ' modal.style.display = "block";', ' modalImg.src = src;', + ' modalImg.style.opacity = "0";', + + ' setTimeout(() => {', + ' modalImg.style.transition = "opacity 0.3s ease";', + ' modalImg.style.opacity = "1";', + ' }, 50);', ' }', ' // 点击模态框关闭', @@ -1121,6 +1400,36 @@ function getHTML() { ' }', ' }', ' });', + + ' // 复制历史记录结果', + ' async function copyHistoryResult(index) {', + ' const history = historyManager.loadHistory(currentToken);', + ' const result = history[index].result;', + ' try {', + ' await navigator.clipboard.writeText(result);', + ' const btn = event.target;', + ' btn.textContent = \'已复制\';', + ' btn.style.background = \'#27ae60\';', + ' btn.style.color = \'#fff\';', + ' setTimeout(() => {', + ' btn.textContent = \'复制结果\';', + ' btn.style.background = \'none\';', + ' btn.style.color = \'#3498db\';', + ' }, 2000);', + ' } catch (err) {', + ' console.error(\'复制失败:\', err);', + ' }', + ' }', + + ' // 删除历史记录项', + ' function deleteHistoryItem(index) {', + ' if (confirm(\'确定要删除这条历史记录吗?\')) {', + ' const history = historyManager.loadHistory(currentToken);', + ' history.splice(index, 1);', + ' historyManager.saveHistory(currentToken, history);', + ' historyManager.displayHistory(currentToken);', + ' }', + ' }', '', '', '' From 25e09c58f198abc154f21313fea85c98bc3eb4d9 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:48:03 +0800 Subject: [PATCH 16/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d496b17..49a0763 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 本项目是对 [QwenLM](https://chat.qwenlm.ai/) 的 OCR 功能进行逆向工程的实现。通过调用 QwenLM 的 API,你可以从图片中提取文字内容,并且该项目支持一键部署到 **Cloudflare Workers** (CF) 上。 ## 项目展示 -![image](https://github.com/user-attachments/assets/0805489b-b5b2-454b-a46d-31c005113ca6) +![image](https://github.com/user-attachments/assets/6a292a81-98e4-4f6d-8759-a310c95499b1) ## 🚀 功能特性 From 778ceac26f505611663ab1e24fe94e0982499b39 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:14:05 +0800 Subject: [PATCH 17/23] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 49a0763..8f407e0 100644 --- a/README.md +++ b/README.md @@ -130,3 +130,6 @@ curl --location 'https://ocr.doublefenzhuan.me/api/recognize/url' \ ``` - 效果图: ![image](https://github.com/user-attachments/assets/db0c89f9-96f1-45b1-b1e9-88ac3d01e196) + +## 趋势 +[![Star History Chart](https://api.star-history.com/svg?repos=cunninger/ocr-based-qwen&type=Date)](https://star-history.com/#Cunninger/ocr-based-qwen&Date) From 2f449bb07e32b291f771b7bc2e9345726759443a Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:18:07 +0800 Subject: [PATCH 18/23] Create LICENSE --- LICENSE | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96112c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,32 @@ +重要声明 +由于本项目是对 QwenLM 的逆向工程实现,仅供学习和研究使用。任何商业用途或滥用行为均与作者无关。请遵守相关法律法规和平台的使用条款。 + +开源协议 (LICENSE) +本项目采用 MIT 许可证,明确限制仅用于学习和研究目的。以下是 LICENSE 文件内容: + +``` +MIT License + +Copyright (c) 2025 [cunninger] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +1. The Software is provided for educational and research purposes only. + Commercial use is strictly prohibited. + +2. The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` From 3927019eb8c9f4d637bcfaedfa6f75d1e2dcb622 Mon Sep 17 00:00:00 2001 From: programmerWsy <113076850+Cunninger@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:27:18 +0800 Subject: [PATCH 19/23] Update worker.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: resolve TypeError in HistoryManager by ensuring MathJax.typesetPromise is available - Fixed the uncaught TypeError caused by `MathJax.typesetPromise` not being a function. - Ensured MathJax is properly loaded before calling `typesetPromise`. - Updated the HistoryManager.displayHistory function to handle MathJax initialization correctly. Fixes issue at (索引):750:15 and (索引):923:22. --- worker.js | 182 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 134 insertions(+), 48 deletions(-) diff --git a/worker.js b/worker.js index f7f2ff0..9cd7cc8 100644 --- a/worker.js +++ b/worker.js @@ -188,14 +188,15 @@ async function recognizeImage(token, imageId) { content: [ { type: 'text', - text: '请识别图片中的内容。对于数学公式和数学符号,请使用标准的LaTeX格式输出。' + - '要求:\n' + - '1. 所有数学公式和单个数学符号都要用LaTeX格式\n' + - '2. 普通文本保持原样\n' + - '3. 对于行内公式使用$单个符号$\n' + - '4. 对于独立公式块使用$$公式$$\n' + - '5. 严格保持原文的段落格式和换行\n' + - '6. 当文本明显换行时,使用\\n进行换行处理' + text: '请识别图片中的内容,并按以下要求输出:\n' + + '1. 所有数学公式和数学符号都必须使用标准的LaTeX格式\n' + + '2. 行内公式使用单个$符号包裹,如:$x^2$\n' + + '3. 独立公式块使用两个$$符号包裹,如:$$\\sum_{i=1}^n i^2$$\n' + + '4. 普通文本保持原样,不要使用LaTeX格式\n' + + '5. 保持原文的段落格式和换行\n' + + '6. 明显的换行使用\\n表示\n' + + '7. 不要输出任何额外的解释或说明\n' + + '8. 确保所有数学符号都被正确包裹在$或$$中' }, { type: 'image', image: imageId }, ], @@ -209,13 +210,17 @@ async function recognizeImage(token, imageId) { const data = await response.json(); - // 处理识别结果 + // 处理识别结果,增加额外的格式化步骤 let result = data.choices[0]?.message?.content || '识别失败'; result = result .replace(/\\(/g, '\\(') .replace(/\\)/g, '\\)') .replace(/\n{3,}/g, '\n\n') .replace(/([^\n])\n([^\n])/g, '$1\n$2') + // 确保数学公式被正确包裹 + .replace(/\$\s+/g, '$') + .replace(/\s+\$/g, '$') + .replace(/\$\$/g, '$$') .trim(); return new Response(JSON.stringify({ @@ -240,7 +245,6 @@ function getHTML() { // 添加 MathJax 支持 '', - '', '', + '', + '', '