feat: add OCR recognition feature with page and API route
This commit is contained in:
+6
-6
@@ -1,11 +1,11 @@
|
||||
FROM node:20-alpine AS base
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
# 阶段1: 安装所有依赖(包括 devDependencies,因为构建需要)
|
||||
FROM base AS deps
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
|
||||
# 阶段2: 构建应用
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
@@ -14,15 +14,15 @@ RUN npx prisma generate
|
||||
ENV DATABASE_URL="file:./dev.db"
|
||||
RUN npx prisma db push
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# 阶段3: 生产运行环境
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
|
||||
# 复制 standalone 构建输出
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
# 复制静态文件
|
||||
@@ -31,7 +31,7 @@ COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
# 复制 public 静态文件
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
|
||||
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
const OCR_SERVICE_URL = "http://192.168.10.236:8000/ocr/predict-pdf-file"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get("file")
|
||||
|
||||
if (!file || !(file instanceof Blob)) {
|
||||
return NextResponse.json({ error: "请上传文件" }, { status: 400 })
|
||||
}
|
||||
|
||||
const ocrFormData = new FormData()
|
||||
const fileName = file instanceof File ? file.name : "file"
|
||||
ocrFormData.append("file", file, fileName)
|
||||
|
||||
const response = await fetch(OCR_SERVICE_URL, {
|
||||
method: "POST",
|
||||
body: ocrFormData,
|
||||
signal: AbortSignal.timeout(60000),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `OCR 服务请求失败 (${response.status})` },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error("OCR API error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "OCR 识别失败,请稍后重试" },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
interface OcrBlock {
|
||||
page: number
|
||||
ocr_result: Array<
|
||||
[
|
||||
[[number, number], [number, number], [number, number], [number, number]],
|
||||
[string, number],
|
||||
]
|
||||
>
|
||||
}
|
||||
|
||||
interface OcrResponse {
|
||||
resultcode: number
|
||||
message: string
|
||||
data: OcrBlock[]
|
||||
}
|
||||
|
||||
interface BoundingBox {
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface TextBlock {
|
||||
text: string
|
||||
confidence: number
|
||||
box: BoundingBox
|
||||
}
|
||||
|
||||
export default function OcrPage() {
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
||||
const [isPdf, setIsPdf] = useState(false)
|
||||
const [ocrData, setOcrData] = useState<OcrResponse | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [dragOver, setDragOver] = useState(false)
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const imageRef = useRef<HTMLImageElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [scale, setScale] = useState({ x: 1, y: 1 })
|
||||
const [imgLoaded, setImgLoaded] = useState(false)
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setOcrData(null)
|
||||
setError("")
|
||||
setCopied(false)
|
||||
setHoveredIndex(null)
|
||||
}, [])
|
||||
|
||||
const handleFile = useCallback((f: File) => {
|
||||
resetState()
|
||||
setFile(f)
|
||||
setLoading(true)
|
||||
|
||||
const isImage = f.type.startsWith("image/")
|
||||
const isPdfFile = f.type === "application/pdf"
|
||||
|
||||
setIsPdf(isPdfFile)
|
||||
|
||||
if (isImage) {
|
||||
if (imageUrl) URL.revokeObjectURL(imageUrl)
|
||||
const url = URL.createObjectURL(f)
|
||||
setImageUrl(url)
|
||||
setImgLoaded(false)
|
||||
} else {
|
||||
setImageUrl(null)
|
||||
setImgLoaded(false)
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("file", f)
|
||||
|
||||
fetch("/api/ocr", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => res.json().catch(() => null))
|
||||
.then((data: OcrResponse | null) => {
|
||||
if (!data || data.resultcode !== 200) {
|
||||
throw new Error(data?.message || "OCR 识别失败")
|
||||
}
|
||||
setOcrData(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err.message || "OCR 识别失败,请稍后重试")
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [resetState, imageUrl])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (imageUrl) URL.revokeObjectURL(imageUrl)
|
||||
}
|
||||
}, [imageUrl])
|
||||
|
||||
useEffect(() => {
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
const items = e.clipboardData?.items
|
||||
if (!items) return
|
||||
|
||||
for (const item of items) {
|
||||
if (item.type.startsWith("image/")) {
|
||||
e.preventDefault()
|
||||
const blob = item.getAsFile()
|
||||
if (blob) {
|
||||
handleFile(blob)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("paste", handlePaste)
|
||||
return () => document.removeEventListener("paste", handlePaste)
|
||||
}, [handleFile])
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
const f = e.dataTransfer.files[0]
|
||||
if (f) handleFile(f)
|
||||
},
|
||||
[handleFile],
|
||||
)
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}, [])
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
}, [])
|
||||
|
||||
const handleFileSelect = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const f = e.target.files?.[0]
|
||||
if (f) handleFile(f)
|
||||
},
|
||||
[handleFile],
|
||||
)
|
||||
|
||||
const handleImageLoad = useCallback(() => {
|
||||
const img = imageRef.current
|
||||
if (!img) return
|
||||
setScale({
|
||||
x: img.clientWidth / img.naturalWidth,
|
||||
y: img.clientHeight / img.naturalHeight,
|
||||
})
|
||||
setImgLoaded(true)
|
||||
}, [])
|
||||
|
||||
const allTextBlocks: TextBlock[] =
|
||||
ocrData?.data?.flatMap((page) =>
|
||||
page.ocr_result.map(([coords, [text, confidence]]) => {
|
||||
const xs = coords.map((c) => c[0])
|
||||
const ys = coords.map((c) => c[1])
|
||||
const left = Math.min(...xs)
|
||||
const top = Math.min(...ys)
|
||||
const right = Math.max(...xs)
|
||||
const bottom = Math.max(...ys)
|
||||
return {
|
||||
text,
|
||||
confidence,
|
||||
box: {
|
||||
left,
|
||||
top,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
},
|
||||
}
|
||||
}),
|
||||
) ?? []
|
||||
|
||||
const handleCopyAll = useCallback(async () => {
|
||||
const text = allTextBlocks.map((b) => b.text).join("\n")
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}, [allTextBlocks])
|
||||
|
||||
const handleCopyBlock = useCallback(async (text: string) => {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<nav className="bg-white border-b border-gray-200 fixed top-0 left-0 right-0 z-50 shadow-sm">
|
||||
<div className="max-w-6xl mx-auto px-6 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">AI</span>
|
||||
</div>
|
||||
<span className="font-bold text-gray-900">冲浪智能体广场</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-6">
|
||||
<Link href="/" className="text-gray-600 hover:text-blue-600 transition">首页</Link>
|
||||
<Link href="/agents" className="text-gray-600 hover:text-blue-600 transition">智能体广场</Link>
|
||||
<Link href="/news" className="text-gray-600 hover:text-blue-600 transition">新闻动态</Link>
|
||||
<Link href="/ocr" className="text-blue-600 font-medium transition">OCR识别</Link>
|
||||
<Link
|
||||
href="/admin/login"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition"
|
||||
>
|
||||
登录
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="pt-20 pb-12">
|
||||
<div className="max-w-6xl mx-auto px-6">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">OCR 文字识别</h1>
|
||||
<p className="text-gray-500">上传图片或截图粘贴,自动识别图片中的文字内容</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`relative border-2 border-dashed rounded-2xl p-10 text-center cursor-pointer transition-all duration-300 mb-6 ${
|
||||
dragOver
|
||||
? "border-blue-400 bg-blue-50"
|
||||
: "border-gray-300 bg-white hover:border-blue-300 hover:bg-gray-50"
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*,.pdf"
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-10 h-10 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin" />
|
||||
<p className="text-gray-500">正在识别中...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-14 h-14 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i className="fa-solid fa-file-image text-2xl text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-700 font-medium">
|
||||
点击上传或拖拽文件到此区域
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
支持图片格式 (PNG, JPG) 或 Ctrl+V 粘贴截图
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl text-sm mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(imageUrl || isPdf) && ocrData && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{imageUrl && (
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center gap-2">
|
||||
<i className="fa-solid fa-image text-blue-600" />
|
||||
<span className="font-medium text-gray-700 text-sm">图片预览</span>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div ref={containerRef} className="relative inline-block max-w-full">
|
||||
<img
|
||||
ref={imageRef}
|
||||
src={imageUrl}
|
||||
alt="上传图片"
|
||||
className="max-w-full h-auto rounded-lg"
|
||||
onLoad={handleImageLoad}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
/>
|
||||
{imgLoaded &&
|
||||
allTextBlocks.map((block, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="absolute border-2 pointer-events-none transition-all duration-200"
|
||||
style={{
|
||||
left: block.box.left * scale.x,
|
||||
top: block.box.top * scale.y,
|
||||
width: block.box.width * scale.x,
|
||||
height: block.box.height * scale.y,
|
||||
borderColor:
|
||||
hoveredIndex === idx
|
||||
? "rgba(59, 130, 246, 0.9)"
|
||||
: "rgba(59, 130, 246, 0.4)",
|
||||
backgroundColor:
|
||||
hoveredIndex === idx
|
||||
? "rgba(59, 130, 246, 0.15)"
|
||||
: "rgba(59, 130, 246, 0.08)",
|
||||
zIndex: hoveredIndex === idx ? 10 : 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPdf && !imageUrl && (
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center gap-2">
|
||||
<i className="fa-solid fa-file-pdf text-red-500" />
|
||||
<span className="font-medium text-gray-700 text-sm">PDF 文件</span>
|
||||
</div>
|
||||
<div className="p-12 flex flex-col items-center justify-center text-gray-400">
|
||||
<i className="fa-solid fa-file-pdf text-5xl mb-4 text-red-300" />
|
||||
<p className="text-sm">{file?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden flex flex-col">
|
||||
<div className="px-5 py-3 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="fa-solid fa-list text-blue-600" />
|
||||
<span className="font-medium text-gray-700 text-sm">识别结果</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({ocrData?.message || ""})
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCopyAll}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition ${
|
||||
copied
|
||||
? "bg-green-50 text-green-600"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-blue-50 hover:text-blue-600"
|
||||
}`}
|
||||
>
|
||||
<i className={`fa-solid ${copied ? "fa-check" : "fa-copy"}`} />
|
||||
{copied ? "已复制" : "复制全部"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto max-h-[500px] p-4 space-y-2">
|
||||
{allTextBlocks.length === 0 ? (
|
||||
<p className="text-gray-400 text-sm text-center py-8">未识别到文字</p>
|
||||
) : (
|
||||
allTextBlocks.map((block, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`group flex items-start gap-3 p-3 rounded-xl cursor-pointer transition-colors ${
|
||||
hoveredIndex === idx
|
||||
? "bg-blue-50 ring-1 ring-blue-200"
|
||||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
onMouseEnter={() => setHoveredIndex(idx)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
onClick={() => handleCopyBlock(block.text)}
|
||||
>
|
||||
<span className="flex-shrink-0 w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center text-xs font-medium text-gray-500 group-hover:bg-blue-100 group-hover:text-blue-600 transition-colors">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-gray-800 leading-relaxed break-all">
|
||||
{block.text}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
置信度: {(block.confidence * 100).toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopyBlock(block.text)
|
||||
}}
|
||||
className="flex-shrink-0 w-7 h-7 rounded-lg flex items-center justify-center text-gray-400 opacity-0 group-hover:opacity-100 hover:bg-blue-100 hover:text-blue-600 transition-all"
|
||||
>
|
||||
<i className="fa-solid fa-copy text-xs" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!file && !loading && (
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center p-4">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<i className="fa-solid fa-upload text-blue-600 text-xl" />
|
||||
</div>
|
||||
<h3 className="font-medium text-gray-900 mb-1">图片上传</h3>
|
||||
<p className="text-xs text-gray-400">支持 PNG、JPG 等常见图片格式</p>
|
||||
</div>
|
||||
<div className="text-center p-4">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<i className="fa-solid fa-paste text-green-600 text-xl" />
|
||||
</div>
|
||||
<h3 className="font-medium text-gray-900 mb-1">截图粘贴</h3>
|
||||
<p className="text-xs text-gray-400">使用 Ctrl+V 快速粘贴截图识别</p>
|
||||
</div>
|
||||
<div className="text-center p-4">
|
||||
<div className="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||
<i className="fa-solid fa-arrows-alt text-purple-600 text-xl" />
|
||||
</div>
|
||||
<h3 className="font-medium text-gray-900 mb-1">拖拽上传</h3>
|
||||
<p className="text-xs text-gray-400">拖拽文件到上传区域即可识别</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div className="border-t border-gray-200 py-8 mt-auto">
|
||||
<div className="max-w-6xl mx-auto px-6 flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-7 h-7 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-xs">AI</span>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-700 text-sm">江苏冲浪软件科技有限公司</span>
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm text-center">
|
||||
© 2026 江苏冲浪软件科技有限公司 · AI 智能体广场
|
||||
</div>
|
||||
<div className="flex gap-4 text-xs text-gray-400">
|
||||
<a className="hover:text-gray-600 cursor-pointer transition">关于我们</a>
|
||||
<a className="hover:text-gray-600 cursor-pointer transition">隐私政策</a>
|
||||
<a className="hover:text-gray-600 cursor-pointer transition">联系我们</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export default async function HomePage() {
|
||||
<Link href="/" className="text-gray-600 hover:text-blue-600 transition">首页</Link>
|
||||
<Link href="/agents" className="text-gray-600 hover:text-blue-600 transition">智能体广场</Link>
|
||||
<Link href="/news" className="text-gray-600 hover:text-blue-600 transition">新闻动态</Link>
|
||||
<Link href="/ocr" className="text-blue-600 font-medium transition">OCR识别</Link>
|
||||
<Link
|
||||
href="/admin/login"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
⨯ The requested resource isn't a valid image for /human/166c0f9202afc463e9264529764ddb7f.jpg received text/html; charset=utf-8 [Error: Failed to find Server Action "x". This request might be from an older or newer deployment. Original error: Cannot read properties of undefined (reading 'workers') Read more: https://nextjs.org/docs/messages/failed-to-find-server-action] ⨯ The requested resource isn't a valid image for /human/0ac179c47f1957d03ccc6858e6351f1d.jpeg received text/html; charset=utf-8 ⨯ The requested resource isn't a valid image for /human/166c0f9202afc463e9264529764ddb7f.jpg received text/html; charset=utf-8 ⨯ The requested resource isn't a valid image for /human/a4c1285f4d131a65821a541c1d58181a.jpg received text/html; charset=utf-8 ⨯ The requested resource isn't a valid image for /human/541ab483fbafcc7b5476a63dbd350fb5.jpg received text/html; charset=utf-8 ⨯ The requested resource isn't a valid image for /human/8365454662deb53637146f8b81476124.png received text/html; charset=utf-8 ⨯ The requested resource isn't a valid image for /human/dd709ef07f783ef0b912127191004580.jpg received text/html; charset=utf-8 ⨯ The requested resource isn't a valid image for /human/decbb93c49351eae57b4f3251cab9f26.jpg received text/html; charset=utf-8 ⨯ The requested resource isn't a valid image for /human/aa5332e4434fcf0cc8bcb9a4d8122ad0.jpeg received text/html; charset=utf-8
|
||||
@@ -0,0 +1,15 @@
|
||||
paddleOCR服务
|
||||
|
||||
根据后端服务接口 curl -X POST "http://192.168.10.236:8000/ocr/predict-pdf-file" -H "Content-Type: multipart/form-data" -F "file=@/home/ai/01.pdf",
|
||||
返回数据格式
|
||||
|
||||
{"resultcode":200,"message":"PDF共1页,识别完成","data":[{"page":1,"ocr_result":[[[[[131.0,78.0],[514.0,80.0],[514.0,97.0],[131.0,95.0]],["第五条差旅费是指离开淮安市城区(清江浦区、生态",0.9986465573310852]],[[[100.0,106.0],[513.0,107.0],[513.0,124.0],[100.0,123.0]],["文旅区、经济技术开发区、淮阴区、淮安区、洪泽区)因办",0.9986475706100464]],[[[99.0,135.0],[513.0,136.0],[513.0,153.0],[99.0,152.0]],["理公务而产生的城市间交通费、住宿费、伙食补助费和市内",0.9990642070770264]],[[[101.0,165.0],[365.0,165.0],[365.0,179.0],[101.0,179.0]],["交通费等各项费用(不含出国出境)。",0.9985750913619995]],[[[130.0,189.0],[513.0,191.0],[513.0,209.0],[130.0,206.0]],["第六条城市间交通费是指工作人员因公出差期间乘坐",0.9991821646690369]],[[[98.0,218.0],[459.0,220.0],[459.0,237.0],[98.0,235.0]],["客车、火车、飞机、轮船等交通工具所产生的费用。",0.9996863603591919]],[[[134.0,246.0],[512.0,247.0],[512.0,264.0],[134.0,263.0]],["(一)出差人员应当按照规定等级乘坐交通工具, 未按",0.9989835619926453]],[[[98.0,273.0],[511.0,275.0],[511.0,293.0],[98.0,290.0]],["规定等级乘坐交通工具的,费用超支部分自理【乘坐交通工",0.9781439900398254]],[[[97.0,302.0],[511.0,304.0],[511.0,320.0],[97.0,318.0]],["具规定等级:①火车:硬座、硬卧;②高铁/动车:二等座、",0.9936482310295105]],[[[97.0,329.0],[512.0,331.0],[512.0,348.0],[97.0,346.0]],["二等 软座(全列软席列车);③飞机:经济舱;④轮船(不",0.9898215532302856]],[[[98.0,358.0],[253.0,358.0],[253.0,375.0],[98.0,375.0]],["包含游船):三等舱]",0.9515863656997681]],[[[134.0,385.0],[511.0,387.0],[511.0,404.0],[134.0,402.0]],["(二) 到出差目的地有多种交通工具可选择时,出差人",0.9992549419403076]],[[[98.0,413.0],[511.0,414.0],[511.0,431.0],[98.0,430.0]],["员在不影响公务、确保安全的前提下,应当选乘经济便捷的",0.9831101894378662]],[[[99.0,443.0],[168.0,443.0],[168.0,457.0],[99.0,457.0]],["交通工具。",0.9986249804496765]],[[[132.0,469.0],[510.0,470.0],[510.0,487.0],[132.0,486.0]],["(三)乘坐客车、火车、飞机、轮船等交通工具的,每",0.999337375164032]],[[[98.0,499.0],[327.0,499.0],[327.0,513.0],[98.0,513.0]],["人每次可购买交通意外保险一份。",0.9981963038444519]],[[[127.0,524.0],[511.0,525.0],[511.0,542.0],[127.0,541.0]],["第七 条住宿费是指公司员工因公出差期间入住宾馆",0.9995073080062866]],[[[102.0,555.0],[439.0,555.0],[439.0,569.0],[102.0,569.0]],["(包括饭店、招待所,下同)所发生的住房费用。",0.9976102709770203]],[[[132.0,579.0],[509.0,581.0],[509.0,598.0],[132.0,596.0]],["(一)出差人员住宿费参照淮安市财政局关于印发《淮",0.9953774809837341]],[[[98.0,609.0],[508.0,609.0],[508.0,623.0],[98.0,623.0]],["安市市级机关国内差旅住宿费标准明细表》的通知(淮财行",0.995916485786438]],[[[100.0,635.0],[381.0,636.0],[381.0,653.0],[100.0,652.0]],["[2017]21号)执行。(具体见附件四)",0.960132896900177]],[[[131.0,663.0],[508.0,664.0],[508.0,681.0],[131.0,680.0]],["(二)出差人员住宿,原则上两人一个标准间。单人出",0.9996628761291504]],[[[95.0,690.0],[509.0,692.0],[509.0,708.0],[95.0,706.0]],["差或男、女出差人员为单数,其单个人员可选择单间住宿或",0.999721884727478]],[[[96.0,721.0],[515.0,721.0],[515.0,735.0],[96.0,735.0]],["标准间入住,其住宿费按照不超过上述规定标准内凭据报销,",0.998518705368042]]]]}]}
|
||||
|
||||
生成前端OCR识别界面
|
||||
|
||||
支持功能
|
||||
+ 图片上传识别
|
||||
+ 截图粘贴识别(Ctrl+V)
|
||||
+ 拖拽上传
|
||||
+ 结果高亮展示、可复制
|
||||
+ 简洁美观、企业内网风格
|
||||
Reference in New Issue
Block a user