版本更新:
1、已实现多图识别并入库 2、增加图片上传时删除图片功能 3、改用模型glm-4.6v预计5月份到期 4、已对环境txt做更改
This commit is contained in:
@@ -328,7 +328,7 @@ function renderTable(data) {
|
|||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td style="max-width:140px; word-break:break-all; font-size: 12px;">${item._id || item.id || ''}</td>
|
<td style="max-width:140px; word-break:break-all; font-size: 12px;">${item._id || item.id || ''}</td>
|
||||||
<td>
|
<td>
|
||||||
${item.image_url ? `<img src="${item.image_url}" onerror="this.src=''; this.alt='图片加载失败'" class="clickable-image" data-image="${item.image_url}" />` : '无图片'}
|
<div style="display:flex;gap:6px;flex-wrap:wrap;">${buildImageCell(item)}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<pre style="white-space:pre-wrap; word-wrap:break-word; max-height: 100px; overflow-y: auto; font-size: 12px; margin: 0;">${escapeHtml(displayData)}</pre>
|
<pre style="white-space:pre-wrap; word-wrap:break-word; max-height: 100px; overflow-y: auto; font-size: 12px; margin: 0;">${escapeHtml(displayData)}</pre>
|
||||||
@@ -343,6 +343,12 @@ function renderTable(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildImageCell(item) {
|
||||||
|
const urls = Array.isArray(item.image_urls) ? item.image_urls : (item.image_url ? [item.image_url] : []);
|
||||||
|
if (!urls.length) return '无图片';
|
||||||
|
return urls.map(u => `<img src="${u}" onerror="this.src=''; this.alt='图片加载失败'" class="clickable-image" data-image="${u}" />`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
// 转义HTML以防止XSS
|
// 转义HTML以防止XSS
|
||||||
function escapeHtml(unsafe) {
|
function escapeHtml(unsafe) {
|
||||||
return unsafe
|
return unsafe
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
.preview-box {flex: 1; text-align: center; }
|
.preview-box {flex: 1; text-align: center; }
|
||||||
.preview-box h3 {margin-top: 0;color: #334155; }
|
.preview-box h3 {margin-top: 0;color: #334155; }
|
||||||
.preview-box img { max-width: 100%;max-height: 300px;border: 1px solid #e2e8f0;border-radius: 8px;object-fit: contain;}
|
.preview-box img { max-width: 100%;max-height: 300px;border: 1px solid #e2e8f0;border-radius: 8px;object-fit: contain;}
|
||||||
|
.preview-list {display: grid;grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));gap: 12px;}
|
||||||
|
.preview-item {position: relative;}
|
||||||
|
.preview-item img {width: 100%;max-height: 220px;border: 1px solid #e2e8f0;border-radius: 8px;object-fit: contain;}
|
||||||
|
.preview-remove {position: absolute;top: 6px;right: 6px;border: none;border-radius: 999px;background: rgba(15,23,42,0.8);color: #fff;width: 24px;height: 24px;cursor: pointer;display: flex;align-items: center;justify-content: center;font-size: 14px;line-height: 1;}
|
||||||
.result-box {flex: 1;}
|
.result-box {flex: 1;}
|
||||||
.result-box h3 { margin-top: 0; color: #334155;}
|
.result-box h3 { margin-top: 0; color: #334155;}
|
||||||
.form-controls { display: flex;gap: 8px;margin-bottom: 12px;flex-wrap: wrap;}
|
.form-controls { display: flex;gap: 8px;margin-bottom: 12px;flex-wrap: wrap;}
|
||||||
@@ -89,7 +93,8 @@
|
|||||||
<p>点击下方按钮选择图片,或拖拽图片到此区域</p>
|
<p>点击下方按钮选择图片,或拖拽图片到此区域</p>
|
||||||
<form id="uploadForm" enctype="multipart/form-data">
|
<form id="uploadForm" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="file" id="fileInput" name="file" accept="image/*" required />
|
<input type="file" id="fileInput" name="file" accept="image/*" multiple />
|
||||||
|
<span id="fileHint" class="muted"></span>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit" class="btn btn-primary">上传并识别</button>
|
<button type="submit" class="btn btn-primary">上传并识别</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -103,7 +108,7 @@
|
|||||||
<div class="preview-container">
|
<div class="preview-container">
|
||||||
<div class="preview-box">
|
<div class="preview-box">
|
||||||
<h3>图片预览</h3>
|
<h3>图片预览</h3>
|
||||||
<img id="preview" alt="预览" />
|
<div id="previewList" class="preview-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-box">
|
<div class="result-box">
|
||||||
@@ -134,7 +139,8 @@ function getCookie(name) {
|
|||||||
|
|
||||||
const uploadForm = document.getElementById('uploadForm');
|
const uploadForm = document.getElementById('uploadForm');
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const preview = document.getElementById('preview');
|
const fileHint = document.getElementById('fileHint');
|
||||||
|
const previewList = document.getElementById('previewList');
|
||||||
const resultBox = document.getElementById('resultBox');
|
const resultBox = document.getElementById('resultBox');
|
||||||
const uploadMsg = document.getElementById('uploadMsg');
|
const uploadMsg = document.getElementById('uploadMsg');
|
||||||
const confirmBtn = document.getElementById('confirmBtn');
|
const confirmBtn = document.getElementById('confirmBtn');
|
||||||
@@ -148,7 +154,8 @@ const progressWrap = document.getElementById('progressWrap');
|
|||||||
const progressBar = document.getElementById('progressBar');
|
const progressBar = document.getElementById('progressBar');
|
||||||
const progressText = document.getElementById('progressText');
|
const progressText = document.getElementById('progressText');
|
||||||
|
|
||||||
let currentImageRel = '';
|
let currentImageRel = [];
|
||||||
|
let selectedFiles = [];
|
||||||
|
|
||||||
function setProgress(p, text){
|
function setProgress(p, text){
|
||||||
const v = Math.max(0, Math.min(100, Math.round(p||0)));
|
const v = Math.max(0, Math.min(100, Math.round(p||0)));
|
||||||
@@ -221,22 +228,64 @@ function handleDrop(e) {
|
|||||||
const dt = e.dataTransfer;
|
const dt = e.dataTransfer;
|
||||||
const files = dt.files;
|
const files = dt.files;
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
fileInput.files = files;
|
addFiles(files);
|
||||||
const event = new Event('change', { bubbles: true });
|
|
||||||
fileInput.dispatchEvent(event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件选择后预览
|
function setPreviewList(urls) {
|
||||||
fileInput.addEventListener('change', function(e) {
|
previewList.innerHTML = '';
|
||||||
const file = e.target.files[0];
|
(urls || []).forEach((url, index) => {
|
||||||
if (file && file.type.startsWith('image/')) {
|
if (!url) return;
|
||||||
const reader = new FileReader();
|
const item = document.createElement('div');
|
||||||
reader.onload = function(e) {
|
item.className = 'preview-item';
|
||||||
preview.src = e.target.result;
|
item.dataset.index = String(index);
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.alt = '预览';
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'preview-remove';
|
||||||
|
btn.textContent = '×';
|
||||||
|
btn.onclick = () => {
|
||||||
|
const idx = Number(item.dataset.index);
|
||||||
|
if (!Number.isNaN(idx)) {
|
||||||
|
selectedFiles.splice(idx, 1);
|
||||||
|
const urls = selectedFiles.map(f => URL.createObjectURL(f));
|
||||||
|
setPreviewList(urls);
|
||||||
|
updateFileHint();
|
||||||
|
setTimeout(() => urls.forEach(u => URL.revokeObjectURL(u)), 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
item.appendChild(img);
|
||||||
}
|
item.appendChild(btn);
|
||||||
|
previewList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileHint() {
|
||||||
|
const count = selectedFiles.length;
|
||||||
|
fileHint.textContent = count ? `已选择 ${count} 张` : '未选择文件';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFiles(files) {
|
||||||
|
const incoming = Array.from(files || []).filter(f => f && f.type.startsWith('image/'));
|
||||||
|
const existingKeys = new Set(selectedFiles.map(f => `${f.name}|${f.size}|${f.lastModified}`));
|
||||||
|
incoming.forEach(f => {
|
||||||
|
const key = `${f.name}|${f.size}|${f.lastModified}`;
|
||||||
|
if (!existingKeys.has(key)) {
|
||||||
|
existingKeys.add(key);
|
||||||
|
selectedFiles.push(f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const urls = selectedFiles.map(f => URL.createObjectURL(f));
|
||||||
|
setPreviewList(urls);
|
||||||
|
updateFileHint();
|
||||||
|
setTimeout(() => urls.forEach(u => URL.revokeObjectURL(u)), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', function(e) {
|
||||||
|
addFiles(e.target.files || []);
|
||||||
|
fileInput.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
function createRow(k = '', v = '') {
|
function createRow(k = '', v = '') {
|
||||||
@@ -329,10 +378,10 @@ uploadForm.addEventListener('submit', async (e) => {
|
|||||||
confirmMsg.textContent = '';
|
confirmMsg.textContent = '';
|
||||||
confirmBtn.disabled = true;
|
confirmBtn.disabled = true;
|
||||||
resultBox.value = '';
|
resultBox.value = '';
|
||||||
currentImageRel = '';
|
currentImageRel = [];
|
||||||
|
|
||||||
const file = fileInput.files[0];
|
const files = Array.from(selectedFiles || []).filter(f => f && f.type.startsWith('image/'));
|
||||||
if (!file) {
|
if (!files.length) {
|
||||||
uploadMsg.textContent = '请选择图片文件';
|
uploadMsg.textContent = '请选择图片文件';
|
||||||
uploadMsg.className = 'status-message error';
|
uploadMsg.className = 'status-message error';
|
||||||
uploadMsg.style.display = 'block';
|
uploadMsg.style.display = 'block';
|
||||||
@@ -341,17 +390,21 @@ uploadForm.addEventListener('submit', async (e) => {
|
|||||||
|
|
||||||
showProgress();
|
showProgress();
|
||||||
setProgress(5, '转换为JPG');
|
setProgress(5, '转换为JPG');
|
||||||
let jpegFile = file;
|
|
||||||
try {
|
|
||||||
jpegFile = await convertToJpeg(file);
|
|
||||||
setProgress(50, '转换为JPG');
|
|
||||||
preview.src = URL.createObjectURL(jpegFile);
|
|
||||||
} catch (_) {
|
|
||||||
jpegFile = file;
|
|
||||||
setProgress(50, '转换为JPG');
|
|
||||||
}
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', jpegFile);
|
const converted = [];
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
let jpegFile = file;
|
||||||
|
try {
|
||||||
|
jpegFile = await convertToJpeg(file);
|
||||||
|
} catch (_) {
|
||||||
|
jpegFile = file;
|
||||||
|
}
|
||||||
|
converted.push(jpegFile);
|
||||||
|
const pct = 5 + Math.round(((i + 1) / files.length) * 45);
|
||||||
|
setProgress(pct, '转换为JPG');
|
||||||
|
}
|
||||||
|
converted.forEach(f => formData.append('file', f));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let prog = 50;
|
let prog = 50;
|
||||||
@@ -375,9 +428,10 @@ uploadForm.addEventListener('submit', async (e) => {
|
|||||||
uploadMsg.textContent = data.message || '识别成功';
|
uploadMsg.textContent = data.message || '识别成功';
|
||||||
uploadMsg.className = 'status-message success';
|
uploadMsg.className = 'status-message success';
|
||||||
uploadMsg.style.display = 'block';
|
uploadMsg.style.display = 'block';
|
||||||
preview.src = data.image_url;
|
const urls = data.image_urls || (data.image_url ? [data.image_url] : []);
|
||||||
|
setPreviewList(urls);
|
||||||
renderFormFromObject(data.data || {});
|
renderFormFromObject(data.data || {});
|
||||||
currentImageRel = data.image;
|
currentImageRel = data.images || (data.image ? [data.image] : []);
|
||||||
confirmBtn.disabled = false;
|
confirmBtn.disabled = false;
|
||||||
setTimeout(hideProgress, 800);
|
setTimeout(hideProgress, 800);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -415,15 +469,20 @@ confirmBtn.addEventListener('click', async () => {
|
|||||||
|
|
||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', () => {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
preview.src = '';
|
previewList.innerHTML = '';
|
||||||
resultBox.value = '';
|
resultBox.value = '';
|
||||||
kvForm.innerHTML = '';
|
kvForm.innerHTML = '';
|
||||||
kvForm.appendChild(createRow()); // 保留一个空行
|
kvForm.appendChild(createRow()); // 保留一个空行
|
||||||
uploadMsg.textContent = '';
|
uploadMsg.textContent = '';
|
||||||
confirmMsg.textContent = '';
|
confirmMsg.textContent = '';
|
||||||
confirmBtn.disabled = true;
|
confirmBtn.disabled = true;
|
||||||
|
currentImageRel = [];
|
||||||
|
selectedFiles = [];
|
||||||
|
updateFileHint();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateFileHint();
|
||||||
|
|
||||||
// 退出登录处理
|
// 退出登录处理
|
||||||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||||||
const msg = document.getElementById('logoutMsg');
|
const msg = document.getElementById('logoutMsg');
|
||||||
|
|||||||
219
elastic/views.py
219
elastic/views.py
@@ -42,6 +42,29 @@ def _image_ref_to_url(request, image_ref: str) -> str:
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_image_refs(image_ref):
|
||||||
|
if not image_ref:
|
||||||
|
return []
|
||||||
|
if isinstance(image_ref, (list, tuple)):
|
||||||
|
return [str(x) for x in image_ref if str(x).strip()]
|
||||||
|
if isinstance(image_ref, str):
|
||||||
|
s = image_ref.strip()
|
||||||
|
if not s:
|
||||||
|
return []
|
||||||
|
parsed = None
|
||||||
|
if s[:1] in ('[', '"'):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(s)
|
||||||
|
except Exception:
|
||||||
|
parsed = None
|
||||||
|
if isinstance(parsed, list):
|
||||||
|
return [str(x) for x in parsed if str(x).strip()]
|
||||||
|
if isinstance(parsed, str):
|
||||||
|
s = parsed.strip()
|
||||||
|
return [s] if s else []
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _attach_image_urls(request, items):
|
def _attach_image_urls(request, items):
|
||||||
out = []
|
out = []
|
||||||
for it in list(items or []):
|
for it in list(items or []):
|
||||||
@@ -49,7 +72,11 @@ def _attach_image_urls(request, items):
|
|||||||
d = dict(it or {})
|
d = dict(it or {})
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
d['image_url'] = _image_ref_to_url(request, d.get('image', ''))
|
refs = _parse_image_refs(d.get('image', ''))
|
||||||
|
urls = [_image_ref_to_url(request, r) for r in refs if str(r).strip()]
|
||||||
|
urls = [u for u in urls if u]
|
||||||
|
d['image_urls'] = urls
|
||||||
|
d['image_url'] = urls[0] if urls else _image_ref_to_url(request, d.get('image', ''))
|
||||||
out.append(d)
|
out.append(d)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -180,7 +207,11 @@ def update_data(request, doc_id):
|
|||||||
if "writer_id" in payload:
|
if "writer_id" in payload:
|
||||||
updated["writer_id"] = payload["writer_id"]
|
updated["writer_id"] = payload["writer_id"]
|
||||||
if "image" in payload:
|
if "image" in payload:
|
||||||
updated["image"] = payload["image"]
|
img_val = payload["image"]
|
||||||
|
if isinstance(img_val, list):
|
||||||
|
updated["image"] = json_to_string(img_val)
|
||||||
|
else:
|
||||||
|
updated["image"] = img_val
|
||||||
if "data" in payload:
|
if "data" in payload:
|
||||||
v = payload["data"]
|
v = payload["data"]
|
||||||
if isinstance(v, dict):
|
if isinstance(v, dict):
|
||||||
@@ -359,7 +390,7 @@ def string_to_json(s):
|
|||||||
|
|
||||||
# 移植自 a.py 的核心:调用大模型进行 OCR/信息抽取
|
# 移植自 a.py 的核心:调用大模型进行 OCR/信息抽取
|
||||||
def ocr_and_extract_info(image_path: str):
|
def ocr_and_extract_info(image_path: str):
|
||||||
from openai import OpenAI
|
# from openai import OpenAI
|
||||||
def encode_image(path: str) -> str:
|
def encode_image(path: str) -> str:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
return base64.b64encode(f.read()).decode("utf-8")
|
return base64.b64encode(f.read()).decode("utf-8")
|
||||||
@@ -372,12 +403,42 @@ def ocr_and_extract_info(image_path: str):
|
|||||||
# raise RuntimeError("缺少 AISTUDIO_API_KEY,请在环境变量或 settings 中配置")
|
# raise RuntimeError("缺少 AISTUDIO_API_KEY,请在环境变量或 settings 中配置")
|
||||||
|
|
||||||
|
|
||||||
api_key = getattr(settings, "AISTUDIO_API_KEY", "")
|
# api_key = getattr(settings, "AISTUDIO_API_KEY", "")
|
||||||
base_url = getattr(settings, "OPENAI_BASE_URL", "")
|
# base_url = getattr(settings, "OPENAI_BASE_URL", "")
|
||||||
if not api_key or not base_url:
|
# if not api_key or not base_url:
|
||||||
raise RuntimeError("缺少模型服务配置,请设置 AISTUDIO_API_KEY 与 OPENAI_BASE_URL")
|
# raise RuntimeError("缺少模型服务配置,请设置 AISTUDIO_API_KEY 与 OPENAI_BASE_URL")
|
||||||
client = OpenAI(api_key=api_key, base_url=base_url)
|
# client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
# types = get_type_list()
|
||||||
|
# chat_completion = client.chat.completions.create(
|
||||||
|
# messages=[
|
||||||
|
# {"role": "system", "content": "你是一个能理解图片和文本的助手,请根据用户提供的信息进行回答。"},
|
||||||
|
# {
|
||||||
|
# "role": "user",
|
||||||
|
# "content": [
|
||||||
|
# {"type": "text", "text": f"请识别这张图片中的信息,将你认为重要的数据转换为不包含嵌套的json,不要显示其它信息以便于解析,直接输出json结果即可。使用“数据类型”字段表示这个东西的大致类型,除此之外你可以自行决定使用哪些json字段。“数据类型”的内容有严格规定,请查看{json.dumps(types, ensure_ascii=False)}中是否包含你所需要的类型,确定不包含后你才可以填入你觉得合适的大致分类。"},
|
||||||
|
# {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}},
|
||||||
|
# ],
|
||||||
|
# },
|
||||||
|
# ],
|
||||||
|
# model="glm-5",
|
||||||
|
# )
|
||||||
|
# response_text = chat_completion.choices[0].message.content
|
||||||
|
|
||||||
|
from zai import ZhipuAiClient
|
||||||
|
import httpx
|
||||||
|
# api_key = (
|
||||||
|
# getattr(settings, "ZHIPU_API_KEY", "")
|
||||||
|
# or getattr(settings, "ZAI_API_KEY", "")
|
||||||
|
# or getattr(settings, "AISTUDIO_API_KEY", "")
|
||||||
|
# )
|
||||||
|
# if not api_key:
|
||||||
|
# raise RuntimeError("缺少模型服务配置,请设置 ZHIPU_API_KEY")
|
||||||
|
# base_url = (
|
||||||
|
# getattr(settings, "ZHIPU_BASE_URL", "")
|
||||||
|
# or getattr(settings, "ZAI_BASE_URL", "")
|
||||||
|
# or "https://open.bigmodel.cn/api/paas/v4/"
|
||||||
|
# )
|
||||||
|
client = ZhipuAiClient(api_key="fb83a3f91e8c4e45af811236548765a2.cX4kUhigHm7VNowf")
|
||||||
types = get_type_list()
|
types = get_type_list()
|
||||||
chat_completion = client.chat.completions.create(
|
chat_completion = client.chat.completions.create(
|
||||||
messages=[
|
messages=[
|
||||||
@@ -390,9 +451,8 @@ def ocr_and_extract_info(image_path: str):
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
model=getattr(settings, "OPENAI_MODEL_NAME", "ernie-4.5-turbo-vl-32k"),
|
model="glm-4.6v",
|
||||||
)
|
)
|
||||||
|
|
||||||
response_text = chat_completion.choices[0].message.content
|
response_text = chat_completion.choices[0].message.content
|
||||||
|
|
||||||
def parse_response(text: str):
|
def parse_response(text: str):
|
||||||
@@ -448,35 +508,66 @@ def upload(request):
|
|||||||
else:
|
else:
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
|
|
||||||
file = request.FILES.get("file")
|
files = request.FILES.getlist("file")
|
||||||
if not file:
|
if not files:
|
||||||
|
one = request.FILES.get("file")
|
||||||
|
if one:
|
||||||
|
files = [one]
|
||||||
|
if not files:
|
||||||
return JsonResponse({"status": "error", "message": "未选择文件"}, status=400)
|
return JsonResponse({"status": "error", "message": "未选择文件"}, status=400)
|
||||||
|
|
||||||
images_dir = os.path.join(settings.MEDIA_ROOT, "images")
|
images_dir = os.path.join(settings.MEDIA_ROOT, "images")
|
||||||
os.makedirs(images_dir, exist_ok=True)
|
os.makedirs(images_dir, exist_ok=True)
|
||||||
filename = f"{uuid.uuid4()}_{file.name}"
|
rel_paths = []
|
||||||
abs_path = os.path.join(images_dir, filename)
|
image_urls = []
|
||||||
|
data_list = []
|
||||||
with open(abs_path, "wb") as dst:
|
for file in files:
|
||||||
for chunk in file.chunks():
|
filename = f"{uuid.uuid4()}_{file.name}"
|
||||||
dst.write(chunk)
|
abs_path = os.path.join(images_dir, filename)
|
||||||
|
with open(abs_path, "wb") as dst:
|
||||||
try:
|
for chunk in file.chunks():
|
||||||
data = ocr_and_extract_info(abs_path)
|
dst.write(chunk)
|
||||||
if not data:
|
try:
|
||||||
return JsonResponse({"status": "error", "message": "无法识别图片内容"}, status=400)
|
data = ocr_and_extract_info(abs_path)
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||||
|
if data:
|
||||||
|
data_list.append(data)
|
||||||
rel_path = f"images/{filename}"
|
rel_path = f"images/{filename}"
|
||||||
image_url = request.build_absolute_uri(settings.MEDIA_URL + rel_path)
|
rel_paths.append(rel_path)
|
||||||
return JsonResponse({
|
image_urls.append(request.build_absolute_uri(settings.MEDIA_URL + rel_path))
|
||||||
"status": "success",
|
|
||||||
"message": "识别成功,请确认数据后点击录入",
|
if not data_list:
|
||||||
"data": data,
|
return JsonResponse({"status": "error", "message": "无法识别图片内容"}, status=400)
|
||||||
"image": rel_path,
|
|
||||||
"image_url": image_url,
|
merged = {}
|
||||||
})
|
for item in data_list:
|
||||||
except Exception as e:
|
if not isinstance(item, dict):
|
||||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
continue
|
||||||
|
for k, v in item.items():
|
||||||
|
key = str(k).strip()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
if key not in merged or merged.get(key) in (None, ''):
|
||||||
|
merged[key] = v
|
||||||
|
continue
|
||||||
|
if merged.get(key) == v:
|
||||||
|
continue
|
||||||
|
base = key
|
||||||
|
idx = 2
|
||||||
|
while f"{base}_{idx}" in merged:
|
||||||
|
idx += 1
|
||||||
|
merged[f"{base}_{idx}"] = v
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
"status": "success",
|
||||||
|
"message": "识别成功,请确认数据后点击录入",
|
||||||
|
"data": merged,
|
||||||
|
"images": rel_paths,
|
||||||
|
"image_urls": image_urls,
|
||||||
|
"image": rel_paths[0] if rel_paths else "",
|
||||||
|
"image_url": image_urls[0] if image_urls else "",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# 确认并入库
|
# 确认并入库
|
||||||
@@ -508,38 +599,44 @@ def confirm(request):
|
|||||||
ensure_type_in_list(edited.get("数据类型"))
|
ensure_type_in_list(edited.get("数据类型"))
|
||||||
image_ref_to_store = ""
|
image_ref_to_store = ""
|
||||||
temp_files_to_delete = []
|
temp_files_to_delete = []
|
||||||
if image_rel:
|
image_rels = _parse_image_refs(image_rel)
|
||||||
|
if image_rels:
|
||||||
images_dir = os.path.join(settings.MEDIA_ROOT, "images")
|
images_dir = os.path.join(settings.MEDIA_ROOT, "images")
|
||||||
os.makedirs(images_dir, exist_ok=True)
|
os.makedirs(images_dir, exist_ok=True)
|
||||||
src_abs = os.path.join(settings.MEDIA_ROOT, image_rel)
|
image_refs = []
|
||||||
if not os.path.isfile(src_abs):
|
for rel in image_rels:
|
||||||
return JsonResponse({"status": "error", "message": "图片文件不存在"}, status=400)
|
src_abs = os.path.join(settings.MEDIA_ROOT, rel)
|
||||||
|
if not os.path.isfile(src_abs):
|
||||||
webp_name = f"{uuid.uuid4().hex}.webp"
|
return JsonResponse({"status": "error", "message": "图片文件不存在"}, status=400)
|
||||||
webp_abs = os.path.join(images_dir, webp_name)
|
webp_name = f"{uuid.uuid4().hex}.webp"
|
||||||
try:
|
webp_abs = os.path.join(images_dir, webp_name)
|
||||||
with Image.open(src_abs) as im:
|
|
||||||
if im.mode in ("RGBA", "LA", "P"):
|
|
||||||
im = im.convert("RGBA")
|
|
||||||
else:
|
|
||||||
im = im.convert("RGB")
|
|
||||||
im.save(webp_abs, format="WEBP", quality=80)
|
|
||||||
except Exception:
|
|
||||||
try:
|
try:
|
||||||
if os.path.isfile(webp_abs):
|
with Image.open(src_abs) as im:
|
||||||
os.remove(webp_abs)
|
if im.mode in ("RGBA", "LA", "P"):
|
||||||
|
im = im.convert("RGBA")
|
||||||
|
else:
|
||||||
|
im = im.convert("RGB")
|
||||||
|
im.save(webp_abs, format="WEBP", quality=80)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
try:
|
||||||
return JsonResponse({"status": "error", "message": "图片转换WEBP失败"}, status=500)
|
if os.path.isfile(webp_abs):
|
||||||
|
os.remove(webp_abs)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return JsonResponse({"status": "error", "message": "图片转换WEBP失败"}, status=500)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
object_name = f"images/{webp_name}"
|
object_name = f"images/{webp_name}"
|
||||||
from minio_storage.minio_connect import upload_file
|
from minio_storage.minio_connect import upload_file
|
||||||
upload_file(webp_abs, object_name, content_type="image/webp")
|
upload_file(webp_abs, object_name, content_type="image/webp")
|
||||||
image_ref_to_store = f"minio:{object_name}"
|
image_refs.append(f"minio:{object_name}")
|
||||||
temp_files_to_delete.extend([src_abs, webp_abs])
|
temp_files_to_delete.extend([src_abs, webp_abs])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"status": "error", "message": f"上传到MinIO失败: {e}"}, status=500)
|
return JsonResponse({"status": "error", "message": f"上传到MinIO失败: {e}"}, status=500)
|
||||||
|
if len(image_refs) == 1:
|
||||||
|
image_ref_to_store = image_refs[0]
|
||||||
|
elif len(image_refs) > 1:
|
||||||
|
image_ref_to_store = json_to_string(image_refs)
|
||||||
|
|
||||||
to_store = {
|
to_store = {
|
||||||
"writer_id": str(request.session.get("user_id")),
|
"writer_id": str(request.session.get("user_id")),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ elasticsearch-dsl==7.4.1
|
|||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
openai==1.52.2
|
openai==1.52.2
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
|
zai-sdk==0.3.0
|
||||||
Pillow==10.4.0
|
Pillow==10.4.0
|
||||||
minio>=7.2.0,<8
|
minio>=7.2.0,<8
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user