初始化项目,由ModelHub XC社区提供模型

Model: FLYFAI/Invoice
Source: Original Platform
This commit is contained in:
ModelHub XC
2026-08-30 02:16:13 +08:00
commit 529c996d3e
47 changed files with 3423 additions and 0 deletions

1
demo/InvoicePrompt Normal file
View File

@@ -0,0 +1 @@
提取票据里面的所有信息

143
demo/LM.py Normal file
View 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
View 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
View File

@@ -0,0 +1,24 @@
你是一名专业的【结构化信息提取助手】。
## 任务说明
你的任务是:**从用户提供的 OCR 文本中,逐项、完整地提取所有信息**,并输出**严格符合预定格式的结构化结果**。
## 规则与约束(必须严格遵守)
1. **禁止任何主观推测、臆断、补全或改写内容**
* 仅基于 OCR 文本中的**真实信息**进行提取。
* **不得**根据常识或推测补充任何缺失的字段,所有输出必须完全反映文本中原始存在的信息。
2. **允许修复明显的 OCR 错别字**
* 例如:错别字、字符混淆(如 `O` 与 `0`、`l` 与 `1` 等)可以进行修正。
* **不得**改变任何原始的语义或数据内容。
## 输出要求
* 所有提取结果必须按照事先约定的**结构化格式**输出。
* 对每项信息都需要**逐条明确**列出,确保无一遗漏。

Binary file not shown.

105
demo/main2.py Normal file
View 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

File diff suppressed because one or more lines are too long

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

3
demo/static/js/FileSaver.min.js vendored Normal file
View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1027
demo/templates/index2.html Normal file

File diff suppressed because it is too large Load Diff