初始化项目,由ModelHub XC社区提供模型
Model: FLYFAI/Invoice Source: Original Platform
This commit is contained in:
1
demo/InvoicePrompt
Normal file
1
demo/InvoicePrompt
Normal file
@@ -0,0 +1 @@
|
||||
提取票据里面的所有信息
|
||||
143
demo/LM.py
Normal file
143
demo/LM.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import io, base64, math
|
||||
import uuid
|
||||
|
||||
from PIL import Image, ImageOps, ImageFilter
|
||||
from openai import AsyncOpenAI
|
||||
from pathlib import Path
|
||||
from pdf2image import convert_from_path
|
||||
import asyncio
|
||||
|
||||
# 请求模型地址
|
||||
model_base_url = ""
|
||||
|
||||
async def img_to_base64_qwen3vl(original, path, max_side=2096):
|
||||
img = Image.open(path)
|
||||
img = ImageOps.exif_transpose(img).convert("RGB")
|
||||
|
||||
w, h = img.size
|
||||
s = min(1.0, max_side / max(w, h))
|
||||
if s < 1:
|
||||
img = img.resize((int(w * s), int(h * s)), Image.Resampling.LANCZOS)
|
||||
|
||||
img = img.filter(ImageFilter.UnsharpMask(1, 120, 3))
|
||||
|
||||
w, h = img.size
|
||||
nw, nh = math.ceil(w / 32) * 32, math.ceil(h / 32) * 32
|
||||
img = ImageOps.expand(
|
||||
img,
|
||||
((nw - w) // 2, (nh - h) // 2, nw - w - (nw - w) // 2, nh - h - (nh - h) // 2),
|
||||
fill=(128, 128, 128)
|
||||
)
|
||||
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=90, subsampling=0, optimize=True)
|
||||
b64 = base64.b64encode(buf.getvalue()).decode()
|
||||
img.save(f"./SC/{str(uuid.uuid4())}_{original}", format="JPEG", quality=90, subsampling=0, optimize=True)
|
||||
return f"data:image/jpeg;base64,{b64}"
|
||||
|
||||
|
||||
def pdf_to_images_sync(original, pdf_path, dpi=300, fmt="png"):
|
||||
try:
|
||||
poppler_path = r"C:\Users\FLYF\Downloads\Release-25.12.0-0\poppler-25.12.0\Library\bin"
|
||||
|
||||
pdf_path = Path(pdf_path)
|
||||
id = str(uuid.uuid4())
|
||||
|
||||
output_dir = Path(f"./TEMP/{pdf_path.stem}_{id}"[:200])
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
images = convert_from_path(
|
||||
pdf_path,
|
||||
dpi=dpi,
|
||||
poppler_path=poppler_path
|
||||
)
|
||||
|
||||
img_paths = []
|
||||
ima_names = []
|
||||
|
||||
for i, img in enumerate(images, start=1):
|
||||
out_path = output_dir / f"{pdf_path.stem}_{i}.{fmt}"
|
||||
img.save(out_path)
|
||||
img_paths.append(str(out_path))
|
||||
ima_names.append(f"{original}_{i}.{fmt}")
|
||||
|
||||
# output = {
|
||||
# "pdf_name": original,
|
||||
# "result": img_paths,
|
||||
# "state": "success"
|
||||
# }
|
||||
|
||||
return img_paths, ima_names
|
||||
except Exception as e:
|
||||
return img_paths, ima_names
|
||||
|
||||
|
||||
async def pdf_to_images_async(*args, **kwargs):
|
||||
return await asyncio.to_thread(pdf_to_images_sync, *args, **kwargs)
|
||||
|
||||
|
||||
with open("SystemPrompt", 'r', encoding='utf-8') as f:
|
||||
SystemPrompt = f.read()
|
||||
|
||||
client = AsyncOpenAI(api_key="1", base_url=model_base_url)
|
||||
|
||||
|
||||
async def qwen3vl(original, img_path, prompt):
|
||||
try:
|
||||
img = await img_to_base64_qwen3vl(original, img_path)
|
||||
response = await client.chat.completions.create(
|
||||
model="Qwen3-VL-FLYFAI",
|
||||
messages=[
|
||||
{"role": "system", "content": SystemPrompt},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": img
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
temperature=0.1,
|
||||
top_p=0.3,
|
||||
max_tokens=4096,
|
||||
timeout=90
|
||||
)
|
||||
output = {
|
||||
"image_name": original,
|
||||
"result": response.choices[0].message.content,
|
||||
"state": "success"
|
||||
}
|
||||
return output
|
||||
except Exception as e:
|
||||
return {"image_name": original, "result": str(e), "state": "error"}
|
||||
|
||||
|
||||
async def run_one(semaphore, prompt, original, img_path, retries=3):
|
||||
async with semaphore:
|
||||
for i in range(retries):
|
||||
try:
|
||||
return await asyncio.wait_for(qwen3vl(original, img_path, prompt), timeout=90)
|
||||
except Exception as e:
|
||||
if i == retries - 1:
|
||||
return {"image_name": original, "result": str(e), "state": "error"}
|
||||
await asyncio.sleep(3)
|
||||
|
||||
|
||||
async def run_images(original, img_paths, prompt, max_concurrency=30):
|
||||
sem = asyncio.Semaphore(max_concurrency)
|
||||
tasks = [run_one(sem, prompt, original, p) for original, p in zip(original, img_paths)]
|
||||
return await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
async def run_pdf(original, pdf_path, prompt):
|
||||
img_paths, ima_names = await pdf_to_images_async(original, pdf_path)
|
||||
result = await run_images(ima_names, img_paths, prompt)
|
||||
return result
|
||||
7
demo/OCR_MD
Normal file
7
demo/OCR_MD
Normal file
@@ -0,0 +1,7 @@
|
||||
Convert the provided image into Markdown format. Ensure that all content from the page is included, such as headers, footers, subtexts, images (with alt text if possible), tables, and any other elements.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Output Only Markdown: Return solely the Markdown content without any additional explanations or comments.
|
||||
- No Delimiters: Do not use code fences or delimiters like \`\`\`markdown.
|
||||
- Complete Content: Do not omit any part of the page, including headers, footers, and subtext.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
24
demo/SystemPrompt
Normal file
24
demo/SystemPrompt
Normal file
@@ -0,0 +1,24 @@
|
||||
你是一名专业的【结构化信息提取助手】。
|
||||
|
||||
## 任务说明
|
||||
|
||||
你的任务是:**从用户提供的 OCR 文本中,逐项、完整地提取所有信息**,并输出**严格符合预定格式的结构化结果**。
|
||||
|
||||
## 规则与约束(必须严格遵守)
|
||||
|
||||
1. **禁止任何主观推测、臆断、补全或改写内容**
|
||||
|
||||
* 仅基于 OCR 文本中的**真实信息**进行提取。
|
||||
* **不得**根据常识或推测补充任何缺失的字段,所有输出必须完全反映文本中原始存在的信息。
|
||||
|
||||
2. **允许修复明显的 OCR 错别字**
|
||||
|
||||
* 例如:错别字、字符混淆(如 `O` 与 `0`、`l` 与 `1` 等)可以进行修正。
|
||||
* **不得**改变任何原始的语义或数据内容。
|
||||
|
||||
## 输出要求
|
||||
|
||||
* 所有提取结果必须按照事先约定的**结构化格式**输出。
|
||||
* 对每项信息都需要**逐条明确**列出,确保无一遗漏。
|
||||
|
||||
|
||||
BIN
demo/__pycache__/LM.cpython-312.pyc
Normal file
BIN
demo/__pycache__/LM.cpython-312.pyc
Normal file
Binary file not shown.
105
demo/main2.py
Normal file
105
demo/main2.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from fastapi import FastAPI, Request, UploadFile, File, Form
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
import shutil
|
||||
import os
|
||||
import tempfile
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
|
||||
from LM import run_pdf, run_images
|
||||
|
||||
|
||||
try:
|
||||
with open("InvoicePrompt", 'r', encoding='utf-8') as f:
|
||||
SYS_INVOICE_PROMPT = f.read()
|
||||
except FileNotFoundError:
|
||||
SYS_INVOICE_PROMPT = "请识别发票内容并返回JSON格式数据。"
|
||||
print("Warning: InvoicePrompt file not found.")
|
||||
|
||||
try:
|
||||
with open("OCR_MD", 'r', encoding='utf-8') as f:
|
||||
SYS_OCR_PROMPT = f.read()
|
||||
except FileNotFoundError:
|
||||
SYS_OCR_PROMPT = "请识别图片中的文字并转换为Markdown格式。"
|
||||
print("Warning: OCR_MD file not found.")
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def read_root(request: Request):
|
||||
|
||||
return templates.TemplateResponse("index2.html", {"request": request})
|
||||
|
||||
|
||||
@app.post("/api/recognize")
|
||||
async def recognize_files(
|
||||
files: List[UploadFile] = File(...),
|
||||
mode: str = Form(...),
|
||||
custom_prompt: str = Form(None)
|
||||
):
|
||||
results = []
|
||||
|
||||
final_prompt = ""
|
||||
if mode == 'invoice':
|
||||
final_prompt = SYS_INVOICE_PROMPT
|
||||
print("Using System Invoice Prompt")
|
||||
elif mode == 'ocr':
|
||||
final_prompt = SYS_OCR_PROMPT
|
||||
print("Using System OCR Prompt")
|
||||
elif mode == 'custom':
|
||||
final_prompt = custom_prompt if custom_prompt else ""
|
||||
print(f"Using Custom Prompt (Length: {len(final_prompt)})")
|
||||
else:
|
||||
final_prompt = SYS_INVOICE_PROMPT
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tasks = []
|
||||
|
||||
for file in files:
|
||||
file_path = os.path.join(temp_dir, file.filename)
|
||||
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
original_name = file.filename
|
||||
ext = os.path.splitext(original_name)[1].lower()
|
||||
|
||||
if ext == ".pdf":
|
||||
tasks.append(run_pdf(original_name, file_path, prompt=final_prompt))
|
||||
|
||||
elif ext in [".jpg", ".jpeg", ".png", ".bmp", ".webp"]:
|
||||
tasks.append(run_images([original_name], [file_path], prompt=final_prompt))
|
||||
|
||||
else:
|
||||
results.append({
|
||||
"image_name": original_name,
|
||||
"result": "Unsupported file type",
|
||||
"state": "error"
|
||||
})
|
||||
|
||||
if tasks:
|
||||
processed_results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
for res in processed_results:
|
||||
if isinstance(res, Exception):
|
||||
print(f"Internal Error: {str(res)}")
|
||||
results.append({
|
||||
"state": "error",
|
||||
"result": f"Internal execution error: {str(res)}"
|
||||
})
|
||||
else:
|
||||
results.append(res)
|
||||
|
||||
return JSONResponse(content={"data": results})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=9420)
|
||||
9
demo/static/css/all.min.css
vendored
Normal file
9
demo/static/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
35
demo/static/css/inter-fonts.css
Normal file
35
demo/static/css/inter-fonts.css
Normal file
@@ -0,0 +1,35 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuOKfMZg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf) format('truetype');
|
||||
}
|
||||
BIN
demo/static/favicon.ico
Normal file
BIN
demo/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
3
demo/static/js/FileSaver.min.js
vendored
Normal file
3
demo/static/js/FileSaver.min.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||
|
||||
//# sourceMappingURL=FileSaver.min.js.map
|
||||
13
demo/static/js/jszip.min.js
vendored
Normal file
13
demo/static/js/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
83
demo/static/js/tailwindcss.js
Normal file
83
demo/static/js/tailwindcss.js
Normal file
File diff suppressed because one or more lines are too long
24
demo/static/js/xlsx.full.min.js
vendored
Normal file
24
demo/static/js/xlsx.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
demo/static/logo.png
Normal file
BIN
demo/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
demo/static/webfonts/fa-brands-400.ttf
Normal file
BIN
demo/static/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
demo/static/webfonts/fa-brands-400.woff2
Normal file
BIN
demo/static/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
demo/static/webfonts/fa-regular-400.ttf
Normal file
BIN
demo/static/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
demo/static/webfonts/fa-regular-400.woff2
Normal file
BIN
demo/static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
demo/static/webfonts/fa-solid-900.ttf
Normal file
BIN
demo/static/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
demo/static/webfonts/fa-solid-900.woff2
Normal file
BIN
demo/static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
demo/static/webfonts/fa-v4compatibility.ttf
Normal file
BIN
demo/static/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
demo/static/webfonts/fa-v4compatibility.woff2
Normal file
BIN
demo/static/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
1027
demo/templates/index2.html
Normal file
1027
demo/templates/index2.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user