提交 a98c002c authored 作者: 贺阳's avatar 贺阳

1、ocr识别之后查找是否还包含文字,包含的话用ai处理,再检查,还有的话返回错误提示,不显示同步按钮,没有的话出现同步按钮

上级 264bf0fd
# -*- coding: utf-8 -*-
import base64
import requests
from dashscope import MultiModalConversation
from dashscope import ImageSynthesis
import dashscope
import logging
from http import HTTPStatus
_logger = logging.getLogger(__name__)
......@@ -12,11 +13,13 @@ dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1'
class AIImageEditService:
"""AI图片编辑服务 - 使用阿里云百炼的qwen-image-edit模型"""
"""AI图片编辑服务 - 使用阿里云百炼的wan2.5-i2i-preview模型"""
def __init__(self, api_key='sk-e41914f0d9c94035a5ae1322e9a61fb1'):
self.api_key = api_key
self.model = "qwen-image-edit"
self.model = "wan2.5-i2i-preview"
# 设置DashScope的API密钥
dashscope.api_key = self.api_key
def edit_image_remove_text(self, image_base64, text_to_remove="AGN UCLINK LOGISITICS LTD"):
"""
......@@ -25,40 +28,60 @@ class AIImageEditService:
:param text_to_remove: 要移除的文字
:return: 处理后的图片base64编码,失败返回None
"""
import time
start_time = time.time()
_logger.info(f"开始AI图片编辑,目标文字: {text_to_remove}")
try:
# 构建消息
messages = [
{
"role": "user",
"content": [
{"image": f"data:image/png;base64,{image_base64}"},
{"text": f"将图片中的{text_to_remove}这一段文字抹去,保持背景完全一致"}
]
}
]
# 根据要移除的文字构建不同的提示词,强调保持清晰度
if "AGN" in text_to_remove and "UCLINK" in text_to_remove:
prompt = "将图片中的AGN和UCLINK LOGISITICS LTD这一段文字完全抹去,保持背景完全一致,保持图片清晰度和分辨率,不要模糊"
elif "AGN" in text_to_remove:
prompt = "将图片中的AGN这一段文字完全抹去,保持背景完全一致,保持图片清晰度和分辨率,不要模糊"
elif "UCLINK" in text_to_remove:
prompt = "将图片中的UCLINK LOGISITICS LTD这一段文字完全抹去,保持背景完全一致,保持图片清晰度和分辨率,不要模糊"
else:
prompt = f"将图片中的{text_to_remove}这一段文字完全抹去,保持背景完全一致,保持图片清晰度和分辨率,不要模糊"
# 调用AI模型
response = MultiModalConversation.call(
api_key=self.api_key,
# 调用ImageSynthesis模型,使用更高质量的设置
api_start_time = time.time()
rsp = ImageSynthesis.call(
model=self.model,
messages=messages,
stream=False,
prompt=prompt,
images=[f"data:image/png;base64,{image_base64}"],
negative_prompt="模糊、不清晰、低质量、文字、文本、字母、数字、噪点、失真",
n=1,
watermark=False,
negative_prompt=" "
seed=12345,
# 添加质量相关参数
size="1024*1024", # 使用较高分辨率
quality="hd" # 使用高清质量
)
api_end_time = time.time()
api_processing_time = api_end_time - api_start_time
_logger.info(f"AI API调用耗时: {api_processing_time:.2f}秒")
if response.status_code == 200:
if rsp.status_code == HTTPStatus.OK:
# 获取处理后图片的URL
image_url = response.output.choices[0].message.content[0]['image']
image_url = rsp.output.results[0].url
_logger.info(f"AI图片编辑成功,图片URL: {image_url}")
# 下载图片并转换为base64
download_start_time = time.time()
edited_image_base64 = self.download_and_convert_to_base64(image_url)
download_end_time = time.time()
download_time = download_end_time - download_start_time
_logger.info(f"图片下载耗时: {download_time:.2f}秒")
total_time = time.time() - start_time
_logger.info(f"AI图片编辑总耗时: {total_time:.2f}秒")
return edited_image_base64
else:
_logger.error(f"AI图片编辑失败,HTTP返回码:{response.status_code}")
_logger.error(f"错误码:{response.code}")
_logger.error(f"错误信息:{response.message}")
_logger.error(f"AI图片编辑失败,HTTP返回码:{rsp.status_code}")
_logger.error(f"错误码:{rsp.code}")
_logger.error(f"错误信息:{rsp.message}")
return None
except Exception as e:
......
......@@ -61,113 +61,17 @@ class BatchGetPodInfoWizard(models.TransientModel):
pdf_filename = fields.Char(string='PDF文件名称')
processed_files_data = fields.Text(string='已处理的文件数据', help='存储已处理的文件信息(JSON格式)')
def _cleanup_temp_attachments(self):
"""
清理与当前向导相关的临时附件
"""
try:
attachments = self.env['ir.attachment'].search([
('res_model', '=', self._name),
('res_id', '=', self.id),
('name', 'like', 'temp_pod_%')
])
if attachments:
attachment_names = [att.name for att in attachments]
attachments.unlink()
_logger.info(f"已清理临时附件: {attachment_names}")
except Exception as e:
_logger.error(f"清理临时附件失败: {str(e)}")
def _serialize_processed_files(self, processed_files):
"""
将processed_files序列化为JSON字符串,文件数据存储到临时附件中
:param processed_files: 处理后的文件数组
:return: JSON字符串(只包含引用信息,不包含文件数据)
"""
# 清理旧的临时附件
self._cleanup_temp_attachments()
serialized_data = []
for file_info in processed_files:
if not file_info.get('bl'):
continue
bl = file_info['bl']
file_data = file_info.get('file_data', '')
file_name = file_info.get('file_name', f"{bl.bl_no}.pdf")
# 将文件数据存储到临时附件中
attachment_id = None
if file_data:
try:
attachment = self.env['ir.attachment'].create({
'name': f"temp_pod_{bl.bl_no}_{int(time.time())}.pdf",
'datas': file_data,
'type': 'binary',
'res_model': self._name,
'res_id': self.id,
'delete_old': True,
})
attachment_id = attachment.id
_logger.info(f"已创建临时附件存储文件: {attachment.name}, ID: {attachment_id}")
except Exception as e:
_logger.error(f"创建临时附件失败: {str(e)}")
data = {
'bl_id': bl.id,
'bl_no': bl.bl_no,
'file_name': file_name,
'attachment_id': attachment_id, # 存储附件ID而不是文件数据
}
# OCR文本数据量小,可以直接存储
if 'ocr_texts' in file_info:
data['ocr_texts'] = file_info['ocr_texts']
serialized_data.append(data)
return json.dumps(serialized_data, ensure_ascii=False)
def _deserialize_processed_files(self, json_data):
"""
将JSON字符串反序列化为processed_files(从附件中读取文件数据)
:param json_data: JSON字符串
:return: 处理后的文件数组
"""
if not json_data:
return []
try:
serialized_data = json.loads(json_data)
processed_files = []
for data in serialized_data:
bl_id = data.get('bl_id')
attachment_id = data.get('attachment_id')
if bl_id:
bl = self.env['cc.bl'].browse(bl_id)
if bl.exists():
# 从附件中读取文件数据
file_data = ''
if attachment_id:
try:
attachment = self.env['ir.attachment'].browse(attachment_id)
if attachment.exists():
file_data = attachment.datas
_logger.info(f"从附件读取文件: {attachment.name}, ID: {attachment_id}")
else:
_logger.warning(f"附件不存在: {attachment_id}")
except Exception as e:
_logger.error(f"读取附件失败: {str(e)}")
file_info = {
'bl': bl,
'bl_no': data.get('bl_no', ''),
'file_name': data.get('file_name', ''),
'file_data': file_data,
}
# 如果有OCR文本,也恢复
if 'ocr_texts' in data:
file_info['ocr_texts'] = data['ocr_texts']
processed_files.append(file_info)
return processed_files
except Exception as e:
_logger.error(f"反序列化processed_files失败: {str(e)}")
return []
# def __del__(self):
# """
# 析构方法:确保在对象被销毁时清理临时文件
# """
# try:
# logging.info(f"__del__: {self._name}")
# if hasattr(self, '_name') and self._name == 'batch.get.pod.info.wizard':
# self._cleanup_all_temp_files()
# except Exception as e:
# # 析构方法中不能抛出异常
# logging.error(f"__del__: {str(e)}")
def action_preview(self):
"""
......@@ -203,6 +107,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
# 序列化并存储处理后的文件数据
if processed_files:
self.processed_files_data = self._serialize_processed_files(processed_files)
logging.info(f"processed_files_data: {self.processed_files_data}")
_logger.info(f"预览完成,已处理 {len(processed_files)} 个文件")
else:
self.processed_files_data = ''
......@@ -243,6 +148,16 @@ class BatchGetPodInfoWizard(models.TransientModel):
if self.processed_files_data:
processed_files = self._deserialize_processed_files(self.processed_files_data)
_logger.info(f"使用已处理的文件数据,共 {len(processed_files)} 个文件")
# 检查文件数据是否完整
valid_files = []
for file_info in processed_files:
if file_info.get('file_data'):
valid_files.append(file_info)
else:
_logger.warning(f"提单 {file_info.get('bl', {}).get('bl_no', 'Unknown')} 的文件数据为空")
processed_files = valid_files
_logger.info(f"有效文件数量: {len(processed_files)}")
# 如果没有已处理的数据,则执行处理流程
if not processed_files:
......@@ -281,6 +196,18 @@ class BatchGetPodInfoWizard(models.TransientModel):
'context': {'active_id': bl_objs.ids,}
}
# 检查是否有文字清除失败的错误
if self.show_error_message and any('仍存在目标文字' in str(self.show_error_message) or '未完全清除文字' in str(self.show_error_message)):
_logger.error(f"检测到文字清除失败,停止处理: {self.show_error_message}")
return {
'type': 'ir.actions.act_window',
'res_model': 'batch.get.pod.info.wizard',
'view_mode': 'form',
'res_id': self.id,
'target': 'new',
'context': {'default_show_error_message': self.show_error_message, 'active_id': bl_objs.ids}
}
# 回写到附件信息
if processed_files:
logging.info(f"回写PDF文件到清关文件,共 {len(processed_files)} 个文件")
......@@ -297,8 +224,9 @@ class BatchGetPodInfoWizard(models.TransientModel):
logging.info(f"同步推送匹配节点,共 {len(processed_files)} 个文件")
self.get_date_sync_match_node(processed_files)
# 清理临时附件
self._cleanup_temp_attachments()
# 清理所有临时文件(包括数据库记录和物理文件)
self._cleanup_all_temp_files(bl_objs)
end_time = time.time()
_logger.info(f"批量获取POD信息操作完成,耗时: {end_time - start_time}秒")
......@@ -396,13 +324,19 @@ class BatchGetPodInfoWizard(models.TransientModel):
logging.info('processed_files: %s' % processed_files)
logging.info('processed_files type: %s' % type(processed_files))
for file_info in processed_files:
if not file_info['bl']:
if not file_info.get('bl'):
_logger.warning("跳过没有提单信息的文件")
continue
bl = file_info['bl']
file_name = file_info['file_name']
file_data = file_info['file_data']
file_name = file_info.get('file_name', '')
file_data = file_info.get('file_data', '')
_logger.info(f"处理提单 {bl.bl_no}, 文件名: {file_name}, 文件数据长度: {len(file_data) if file_data else 0}")
if not file_data:
_logger.warning(f"提单 {bl.bl_no} 的文件数据为空,跳过回写")
continue
# 如果有文件为空的就回写,否则就创建新的清关文件记录
fix_name = '尾程交接POD(待大包数量和箱号)'
clearance_file = self.env['cc.clearance.file'].search(
......@@ -412,6 +346,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
'attachment_name': file_name,
'file': file_data
})
_logger.info(f"更新清关文件记录: 提单 {bl.bl_no}")
else:
# 创建新的清关文件记录
clearance_file = self.env['cc.clearance.file'].create({
......@@ -420,6 +355,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
'attachment_name': file_name,
'file': file_data
})
_logger.info(f"创建新的清关文件记录: 提单 {bl.bl_no}")
file_info['clearance_file'] = clearance_file
def _merge_pdf_files(self, processed_files):
......@@ -431,15 +367,45 @@ class BatchGetPodInfoWizard(models.TransientModel):
from datetime import datetime
try:
# 过滤有效的PDF文件
valid_files = []
for file_info in processed_files:
if file_info.get('bl') and file_info.get('file_data'):
valid_files.append(file_info)
if not valid_files:
_logger.warning("没有有效的PDF文件可以合并")
return
# 如果只有一个PDF文件,直接使用,不需要合并
if len(valid_files) == 1:
file_info = valid_files[0]
bl = file_info['bl']
file_data = file_info['file_data']
file_name = file_info.get('file_name', f"{bl.bl_no}.pdf")
# 生成文件名(包含提单号和日期)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
pdf_filename = f"POD文件_{bl.bl_no}_{timestamp}.pdf"
# 直接保存到字段
self.write({
'pdf_file': file_data,
'pdf_filename': pdf_filename
})
_logger.info(f"单个PDF文件直接保存: {pdf_filename}")
return
# 多个PDF文件需要合并
_logger.info(f"开始合并 {len(valid_files)} 个PDF文件")
# 创建新的PDF文档用于合并
merged_pdf = fitz.open()
bl_numbers = []
# 遍历所有处理后的PDF文件
for file_info in processed_files:
if not file_info.get('bl') or not file_info.get('file_data'):
continue
for file_info in valid_files:
bl = file_info['bl']
file_data = file_info['file_data']
bl_numbers.append(bl.bl_no)
......@@ -549,7 +515,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
import re
# 定义目标文字(与_find_target_texts一致)
TARGET_TEXTS = ['AGN', 'UCLINK LOGISITICS LTD', 'UCLINK LOGISITICS', 'UCLINK', 'LOGISITICS', 'LOGISTICS', 'LTD',
TARGET_TEXTS = ['AGN', 'ACN', 'UCLINK LOGISITICS LTD', 'UCLINK LOGISITICS', 'UCLINK', 'LOGISITICS', 'LOGISTICS', 'LTD',
'UCLINKLOGISITICSLTD']
EXCLUDE_TEXTS = ['AIR EQK', 'ARN', 'EQK', 'AIR', 'Page 1 of 1', 'Page 2 of 2', 'Page 3 of 3', 'Page 4 of 4',
'Page 5 of 5']
......@@ -577,7 +543,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
page_text_pdf = page.get_text().upper()
# 将页面转换为图像进行OCR识别
mat = fitz.Matrix(2.0, 2.0) # 提高分辨率
mat = fitz.Matrix(3.0, 3.0) # 进一步提高分辨率,从2.0提升到3.0
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
......@@ -612,6 +578,10 @@ class BatchGetPodInfoWizard(models.TransientModel):
# AGN使用精确匹配
if re.search(r'\bAGN\b', combined_text):
is_match = True
elif target_text == 'ACN':
# ACN使用精确匹配
if re.search(r'\bACN\b', combined_text):
is_match = True
elif target_text == 'LTD':
# LTD使用精确匹配,但要排除其他包含LTD的文字
if re.search(r'\bLTD\b', combined_text) and 'UCLINK' in combined_text:
......@@ -692,11 +662,10 @@ class BatchGetPodInfoWizard(models.TransientModel):
)
if processed_pdf:
processed_file_data = base64.b64encode(processed_pdf).decode('utf-8')
# 第二步:检查是否还存在目标文字
pdf_for_check = base64.b64decode(processed_file_data)
text_exists, found_texts = self._check_target_texts_exist(pdf_for_check, bl.bl_no)
logging.info(f"ocr处理之后的text_exists: {text_exists}")
if text_exists:
# 第三步:如果还存在,使用AI图片编辑处理
_logger.info(f"提单 {bl.bl_no} OCR处理后仍存在目标文字,使用AI图片编辑处理")
......@@ -713,20 +682,34 @@ class BatchGetPodInfoWizard(models.TransientModel):
text_still_exists, final_found_texts = self._check_target_texts_exist(final_check_pdf, bl.bl_no)
if text_still_exists:
# 第五步:如果仍然存在,记录错误信息
error_msg = f"提单 {bl.bl_no} 处理后仍存在目标文字: {', '.join(final_found_texts)}"
# 第五步:如果仍然存在,记录错误信息并停止处理
error_msg = f"提单 {bl.bl_no} 经过OCR和AI处理后仍存在目标文字: {', '.join(final_found_texts)},请手动检查"
_logger.error(error_msg)
error_messages.append(error_msg)
# 不更新文件数据,保持原始状态
processed_file_data = file_data
else:
_logger.warning(f"提单 {bl.bl_no} AI处理失败,保持OCR处理结果")
_logger.warning(f"提单 {bl.bl_no} AI处理失败,检查OCR处理结果")
# AI处理失败,检查OCR结果是否真的清除了目标文字
ocr_check_pdf = base64.b64decode(processed_file_data)
text_still_exists, ocr_found_texts = self._check_target_texts_exist(ocr_check_pdf, bl.bl_no)
if text_still_exists:
error_msg = f"提单 {bl.bl_no} OCR处理未完全清除文字,AI处理失败: {', '.join(ocr_found_texts)},请手动检查"
error_messages.append(error_msg)
# 不更新文件数据,保持原始状态
processed_file_data = file_data
else:
_logger.info(f"提单 {bl.bl_no} OCR处理成功,目标文字已清除")
except Exception as e:
_logger.error(f"提单 {bl.bl_no} AI处理异常: {str(e)}")
# AI处理失败,使用OCR结果,但需要检查
final_check_pdf = base64.b64decode(processed_file_data)
text_still_exists, final_found_texts = self._check_target_texts_exist(final_check_pdf, bl.bl_no)
if text_still_exists:
error_msg = f"提单 {bl.bl_no} OCR处理未完全清除文字,AI处理失败: {', '.join(final_found_texts)}"
error_msg = f"提单 {bl.bl_no} OCR处理未完全清除文字,AI处理失败: {', '.join(final_found_texts)},请手动检查"
error_messages.append(error_msg)
# 不更新文件数据,保持原始状态
processed_file_data = file_data
else:
_logger.info(f"提单 {bl.bl_no} OCR处理成功,目标文字已清除")
else:
......@@ -780,7 +763,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
page = pdf_document[page_num]
# 将页面转换为图像
mat = fitz.Matrix(2.0, 2.0) # 提高分辨率
mat = fitz.Matrix(3.0, 3.0) # 进一步提高分辨率,从2.0提升到3.0
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
......@@ -818,7 +801,9 @@ class BatchGetPodInfoWizard(models.TransientModel):
import fitz # PyMuPDF
import base64
from PIL import Image
import time
start_time = time.time()
_logger.info(f"开始使用AI图片编辑处理PDF,提单号: {bl_no}")
# 初始化AI服务
......@@ -827,14 +812,16 @@ class BatchGetPodInfoWizard(models.TransientModel):
# 打开PDF文档
pdf_document = fitz.open(stream=pdf_data, filetype="pdf")
processed_pages = []
total_pages = len(pdf_document)
# 遍历每一页
for page_num in range(len(pdf_document)):
for page_num in range(total_pages):
page_start_time = time.time()
page = pdf_document[page_num]
_logger.info(f"正在处理第{page_num + 1}页")
# 将页面转换为图像
mat = fitz.Matrix(2.0, 2.0) # 提高分辨率
# 将页面转换为图像,使用更高分辨率确保清晰度
mat = fitz.Matrix(3.0, 3.0) # 进一步提高分辨率,从2.0提升到3.0
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
......@@ -842,10 +829,13 @@ class BatchGetPodInfoWizard(models.TransientModel):
img_base64 = base64.b64encode(img_data).decode('utf-8')
# 使用AI编辑图片,移除指定文字
ai_start_time = time.time()
edited_img_base64 = ai_service.edit_image_remove_text(
img_base64,
text_to_remove="AGN UCLINK LOGISITICS LTD"
)
ai_end_time = time.time()
ai_processing_time = ai_end_time - ai_start_time
if edited_img_base64:
# 解码base64图片数据
......@@ -856,16 +846,21 @@ class BatchGetPodInfoWizard(models.TransientModel):
'img_data': edited_img_data,
'is_edited': True
})
_logger.info(f"第{page_num + 1}页AI处理成功")
_logger.info(f"第{page_num + 1}页AI处理成功,耗时: {ai_processing_time:.2f}秒")
else:
_logger.warning(f"第{page_num + 1}页AI处理失败,使用原始页面")
_logger.warning(f"第{page_num + 1}页AI处理失败,使用原始页面,耗时: {ai_processing_time:.2f}秒")
# 如果AI处理失败,使用原始图片
processed_pages.append({
'img_data': img_data,
'is_edited': False
})
page_end_time = time.time()
page_processing_time = page_end_time - page_start_time
_logger.info(f"第{page_num + 1}页总处理时间: {page_processing_time:.2f}秒")
# 创建新的PDF文档
pdf_creation_start = time.time()
output_doc = fitz.open()
for page_info in processed_pages:
img_data = page_info['img_data']
......@@ -892,9 +887,16 @@ class BatchGetPodInfoWizard(models.TransientModel):
output_doc.save(output_buffer, garbage=4, deflate=True)
output_doc.close()
pdf_document.close()
pdf_creation_end = time.time()
result_data = output_buffer.getvalue()
total_time = time.time() - start_time
pdf_creation_time = pdf_creation_end - pdf_creation_start
_logger.info(f"AI图片编辑PDF处理完成,提单号: {bl_no}")
_logger.info(f"总处理时间: {total_time:.2f}秒")
_logger.info(f"PDF创建时间: {pdf_creation_time:.2f}秒")
_logger.info(f"平均每页AI处理时间: {total_time/total_pages:.2f}秒")
return result_data
......@@ -910,6 +912,10 @@ class BatchGetPodInfoWizard(models.TransientModel):
import numpy as np
from PIL import Image
import pytesseract
import time
start_time = time.time()
_logger.info(f"开始OCR处理PDF,提单号: {bl_no}")
# 尝试导入OpenCV,如果失败则使用PIL替代
try:
......@@ -930,13 +936,16 @@ class BatchGetPodInfoWizard(models.TransientModel):
detected_texts = []
all_recognized_texts = []
result_data = False
total_pages = len(pdf_document)
# 处理每一页(完全按照HTML逻辑)
for page_num in range(len(pdf_document)):
for page_num in range(total_pages):
page_start_time = time.time()
page = pdf_document[page_num]
_logger.info(f"正在OCR识别第{page_num + 1}页")
# 将页面转换为图像(与HTML完全一致)
mat = fitz.Matrix(2.0, 2.0) # 提高分辨率
# 将页面转换为图像,使用更高分辨率确保清晰度
mat = fitz.Matrix(3.0, 3.0) # 进一步提高分辨率,从2.0提升到3.0
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
......@@ -956,6 +965,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
config = '--psm 6 --oem 1 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,- -c preserve_interword_spaces=1 -c tessedit_do_invert=0 -c textord_min_linesize=1.0 -c classify_bln_numeric_mode=0 -c textord_force_make_prop_words=F -c textord_min_xheight=8 -c textord_tabfind_show_vlines=0'
# 使用Tesseract进行OCR识别
ocr_start_time = time.time()
try:
ocr_data = pytesseract.image_to_data(
pil_img,
......@@ -963,6 +973,9 @@ class BatchGetPodInfoWizard(models.TransientModel):
lang='eng',
config=config
)
ocr_end_time = time.time()
ocr_processing_time = ocr_end_time - ocr_start_time
_logger.info(f"第{page_num + 1}页OCR识别耗时: {ocr_processing_time:.2f}秒")
except Exception as e:
_logger.error(f"OCR识别失败: {str(e)}")
continue
......@@ -1056,6 +1069,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
processed_pages += 1
# 保存处理后的PDF
pdf_save_start = time.time()
try:
output_buffer = io.BytesIO()
pdf_document.save(output_buffer, garbage=4, deflate=True, clean=True)
......@@ -1063,10 +1077,17 @@ class BatchGetPodInfoWizard(models.TransientModel):
result_data = output_buffer.getvalue()
output_buffer.close()
pdf_save_end = time.time()
pdf_save_time = pdf_save_end - pdf_save_start
# 计算总处理时间
total_time = time.time() - start_time
# 输出处理总结
_logger.info(
f"PDF处理完成 - 提单号: {bl_no}, 处理页数: {processed_pages}, 删除矩形数: {total_rectangles}, 检测到文字数: {len(detected_texts)}")
_logger.info(f"OCR处理完成 - 提单号: {bl_no}, 处理页数: {processed_pages}, 删除矩形数: {total_rectangles}, 检测到文字数: {len(detected_texts)}")
_logger.info(f"OCR总处理时间: {total_time:.2f}秒")
_logger.info(f"PDF保存时间: {pdf_save_time:.2f}秒")
_logger.info(f"平均每页OCR处理时间: {total_time/total_pages:.2f}秒")
if detected_texts:
_logger.info(f"检测到的目标文字: {[text['text'] for text in detected_texts]}")
except Exception as e:
......@@ -1154,7 +1175,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
Find target texts using OCR results (完全按照HTML逻辑) # 使用OCR结果查找目标文字
"""
# 定义目标文字和排除文字(与HTML文件完全一致)
TARGET_TEXTS = ['AGN', 'UCLINK LOGISITICS LTD', 'UCLINK LOGISITICS', 'UCLINK', 'LOGISITICS', 'LOGISTICS', 'LTD',
TARGET_TEXTS = ['AGN', 'ACN', 'UCLINK LOGISITICS LTD', 'UCLINK LOGISITICS', 'UCLINK', 'LOGISITICS', 'LOGISTICS', 'LTD',
'UCLINKLOGISITICSLTD']
EXCLUDE_TEXTS = ['AIR EQK', 'ARN', 'EQK', 'AIR', 'Page 1 of 1', 'Page 2 of 2', 'Page 3 of 3', 'Page 4 of 4',
'Page 5 of 5']
......@@ -1189,6 +1210,9 @@ class BatchGetPodInfoWizard(models.TransientModel):
if target_text == 'AGN':
# AGN使用精确匹配
is_match = text == 'AGN'
elif target_text == 'ACN':
# ACN使用精确匹配
is_match = text == 'ACN'
elif target_text == 'LTD':
# LTD使用精确匹配
is_match = text == 'LTD'
......@@ -1200,7 +1224,7 @@ class BatchGetPodInfoWizard(models.TransientModel):
'ARN' not in text
# 如果精确匹配失败,尝试模糊匹配(与HTML完全一致)
if not is_match and target_text != 'AGN' and target_text != 'LTD':
if not is_match and target_text not in ['AGN', 'ACN', 'LTD']:
is_match = self._fuzzy_match(text, target_upper)
if is_match:
......@@ -1558,3 +1582,386 @@ class BatchGetPodInfoWizard(models.TransientModel):
_logger.error(f"提取PDF时间信息失败,提单号: {bl_no}, 错误: {str(e)}")
return extracted_times
def _check_target_texts_exist(self, pdf_binary, bl_no):
"""
检查PDF中是否还存在目标文字(使用与_find_target_texts相同的逻辑)
:param pdf_binary: PDF二进制数据
:param bl_no: 提单号(用于日志)
:return: (是否存在目标文字, 找到的文字列表)
"""
import fitz # PyMuPDF
import pytesseract
import numpy as np
from PIL import Image
import re
# 定义目标文字(与_find_target_texts一致)
TARGET_TEXTS = ['AGN', 'ACN', 'UCLINK LOGISITICS LTD', 'UCLINK LOGISITICS', 'UCLINK', 'LOGISITICS', 'LOGISTICS', 'LTD',
'UCLINKLOGISITICSLTD']
EXCLUDE_TEXTS = ['AIR EQK', 'ARN', 'EQK', 'AIR', 'Page 1 of 1', 'Page 2 of 2', 'Page 3 of 3', 'Page 4 of 4',
'Page 5 of 5']
try:
# 设置Tesseract路径
self._setup_tesseract_path()
# 打开PDF文档
pdf_document = fitz.open(stream=pdf_binary, filetype="pdf")
found_texts = []
# 尝试导入OpenCV,如果失败则使用PIL替代
try:
import cv2
cv2_available = True
except ImportError:
cv2_available = False
# 遍历每一页
for page_num in range(len(pdf_document)):
page = pdf_document[page_num]
# 首先尝试从PDF文本层提取(如果是文本型PDF)
page_text_pdf = page.get_text().upper()
# 将页面转换为图像进行OCR识别
mat = fitz.Matrix(3.0, 3.0) # 进一步提高分辨率,从2.0提升到3.0
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
# 转换为PIL图像
if cv2_available:
nparr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
else:
pil_img = Image.open(io.BytesIO(img_data))
if pil_img.mode != 'RGB':
pil_img = pil_img.convert('RGB')
# OCR识别
try:
config = '--psm 6 --oem 1 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,- -c preserve_interword_spaces=1'
ocr_text = pytesseract.image_to_string(pil_img, config=config, lang='eng').upper()
except Exception as e:
_logger.warning(f"OCR识别失败,第{page_num + 1}页,使用PDF文本: {str(e)}")
ocr_text = page_text_pdf
# 合并PDF文本和OCR文本进行检查
combined_text = (page_text_pdf + ' ' + ocr_text).upper()
# 使用与_find_target_texts完全相同的逻辑:先进行OCR单词识别
try:
# 获取OCR识别的单词列表
words = pytesseract.image_to_data(pil_img, output_type=pytesseract.Output.DICT, lang='eng')
# 过滤出有效的单词
valid_words = []
for i in range(len(words['text'])):
word_text = words['text'][i].strip()
if word_text and int(words['conf'][i]) > 30: # 置信度大于30
valid_words.append({
'text': word_text,
'confidence': int(words['conf'][i]),
'bbox': {
'x0': words['left'][i],
'y0': words['top'][i],
'x1': words['left'][i] + words['width'][i],
'y1': words['top'][i] + words['height'][i]
}
})
# 使用与_find_target_texts相同的匹配逻辑
page_found_texts = self._find_target_texts(valid_words, page_num, 800, 600, 800, 600)
if page_found_texts:
for found_text in page_found_texts:
found_texts.append(f"第{page_num + 1}页: {found_text['text']}")
except Exception as e:
_logger.warning(f"OCR单词识别失败,第{page_num + 1}页,使用文本匹配: {str(e)}")
# 如果OCR单词识别失败,回退到文本匹配
for target_text in TARGET_TEXTS:
target_upper = target_text.upper()
# 检查是否包含目标文字
is_match = False
if target_text == 'AGN':
# AGN使用精确匹配
if re.search(r'\bAGN\b', combined_text):
is_match = True
elif target_text == 'ACN':
# ACN使用精确匹配
if re.search(r'\bACN\b', combined_text):
is_match = True
elif target_text == 'LTD':
# LTD使用精确匹配,但要排除其他包含LTD的文字
if re.search(r'\bLTD\b', combined_text) and 'UCLINK' in combined_text:
is_match = True
else:
# 其他文字使用包含匹配
if target_upper in combined_text:
# 排除AIR、EQK、ARN等(需要这些词都不存在)
if 'AIR EQK' not in combined_text and 'ARN' not in combined_text:
is_match = True
# 如果匹配,检查是否在排除列表中
if is_match:
is_excluded = False
for exclude_text in EXCLUDE_TEXTS:
exclude_upper = exclude_text.upper()
if exclude_upper in combined_text and target_upper in combined_text:
# 检查是否是页码
if re.search(r'PAGE\s+\d+\s+OF\s+\d+', combined_text) or re.search(r'\d+\s*/\s*\d+', combined_text):
is_excluded = True
break
# 检查是否是AIR EQK等排除项
if 'AIR EQK' in combined_text or 'ARN' in combined_text:
is_excluded = True
break
if not is_excluded:
found_texts.append(f"第{page_num + 1}页: {target_text}")
break # 找到就跳出,避免重复
pdf_document.close()
if found_texts:
_logger.warning(f"提单 {bl_no} 仍存在目标文字: {', '.join(found_texts)}")
return True, found_texts
else:
_logger.info(f"提单 {bl_no} 未发现目标文字")
return False, []
except Exception as e:
_logger.error(f"检查目标文字失败,提单号: {bl_no}, 错误: {str(e)}")
# 检查失败时,假设不存在(避免误报)
return False, []
def _cleanup_temp_attachments(self,bl_objs=None):
"""
清理与当前向导相关的临时附件,包括服务器和本地开发环境的物理文件
"""
try:
attachments = self.env['ir.attachment'].search([
('res_model', '=', bl_objs[0]._name),
('res_id', 'in', bl_objs.ids),
('name', 'like', 'temp_pod_%')
])
if attachments:
attachment_names = [att.name for att in attachments]
# 删除物理文件(服务器和本地开发环境)
for attachment in attachments:
try:
# 获取附件的物理文件路径
if hasattr(attachment, 'store_fname') and attachment.store_fname:
# Odoo 12+ 使用 store_fname
file_path = attachment.store_fname
elif hasattr(attachment, 'datas_fname') and attachment.datas_fname:
# 旧版本使用 datas_fname
file_path = attachment.datas_fname
else:
# 尝试从 name 字段构建路径
file_path = attachment.name
# 构建完整的文件路径
import os
from odoo.tools import config
# 获取 Odoo 数据目录
data_dir = config.filestore(self.env.cr.dbname)
if data_dir and file_path:
full_path = os.path.join(data_dir, file_path)
if os.path.exists(full_path):
os.remove(full_path)
_logger.info(f"已删除物理文件: {full_path}")
else:
_logger.warning(f"物理文件不存在: {full_path}")
# 检查本地开发环境的文件(如果存在)
local_paths = [
f"./temp_pod_files/{attachment.name}",
f"./addons/ccs_base/wizard/temp_files/{attachment.name}",
f"./temp_files/{attachment.name}",
f"./ccs_base/wizard/temp_files/{attachment.name}",
f"../temp_files/{attachment.name}",
]
for local_path in local_paths:
try:
if os.path.exists(local_path):
os.remove(local_path)
_logger.info(f"已删除本地文件: {local_path}")
except Exception as local_e:
_logger.warning(f"删除本地文件失败 {local_path}: {str(local_e)}")
except Exception as file_e:
_logger.warning(f"删除物理文件失败 {attachment.name}: {str(file_e)}")
# 删除数据库记录
attachments.unlink()
_logger.info(f"已清理临时附件: {attachment_names}")
except Exception as e:
_logger.error(f"清理临时附件失败: {str(e)}")
def _cleanup_all_temp_files(self,bl_objs=None):
"""
清理所有临时文件,包括数据库记录和物理文件
"""
try:
# 清理临时附件
self._cleanup_temp_attachments(bl_objs)
# 清理可能存在的其他临时文件
import os
import glob
# 清理当前目录下的临时文件
temp_patterns = [
"./temp_pod_*.pdf",
"./temp_files/temp_pod_*.pdf",
"./addons/ccs_base/wizard/temp_files/temp_pod_*.pdf",
"./ccs_base/wizard/temp_files/temp_pod_*.pdf",
]
for pattern in temp_patterns:
try:
files = glob.glob(pattern)
for file_path in files:
if os.path.exists(file_path):
os.remove(file_path)
_logger.info(f"已删除临时文件: {file_path}")
except Exception as e:
_logger.warning(f"清理临时文件失败 {pattern}: {str(e)}")
except Exception as e:
_logger.error(f"清理所有临时文件失败: {str(e)}")
def _serialize_processed_files(self, processed_files):
"""
将processed_files序列化为JSON字符串,文件数据存储到临时附件中
:param processed_files: 处理后的文件数组
:return: JSON字符串(只包含引用信息,不包含文件数据)
"""
# 注意:不在这里清理临时附件,因为预览时需要保留附件数据
# 只有在确认操作完成后才清理临时附件
serialized_data = []
for file_info in processed_files:
if not file_info.get('bl'):
continue
bl = file_info['bl']
file_data = file_info.get('file_data', '')
file_name = file_info.get('file_name', f"{bl.bl_no}.pdf")
# 将文件数据存储到临时附件中
attachment_id = None
if file_data:
try:
attachment = self.env['ir.attachment'].create({
'name': f"temp_pod_{bl.bl_no}_{int(time.time())}.pdf",
'datas': file_data,
'type': 'binary',
'res_model': bl._name,
'res_id': bl.id,
})
attachment_id = attachment.id
_logger.info(f"已创建临时附件存储文件: {attachment.name}, ID: {attachment_id}")
# 验证附件创建后数据是否正确
created_attachment = self.env['ir.attachment'].browse(attachment_id)
if created_attachment.datas:
# 比较解码后的数据长度,而不是直接比较字符串
try:
original_decoded = base64.b64decode(file_data)
attachment_decoded = base64.b64decode(created_attachment.datas)
if len(original_decoded) == len(attachment_decoded):
_logger.info(f"附件数据验证成功,解码后长度: {len(original_decoded)}")
else:
_logger.warning(f"附件数据长度不匹配: 原始={len(original_decoded)}, 附件={len(attachment_decoded)}")
except Exception as e:
_logger.warning(f"附件数据验证失败: {str(e)}")
else:
_logger.error(f"附件数据为空")
except Exception as e:
_logger.error(f"创建临时附件失败: {str(e)}")
else:
_logger.warning(f"提单 {bl.bl_no} 的文件数据为空,无法创建附件")
data = {
'bl_id': bl.id,
'bl_no': bl.bl_no,
'file_name': file_name,
'attachment_id': attachment_id, # 存储附件ID而不是文件数据
}
# OCR文本数据量小,可以直接存储
if 'ocr_texts' in file_info:
data['ocr_texts'] = file_info['ocr_texts']
serialized_data.append(data)
return json.dumps(serialized_data, ensure_ascii=False)
def _deserialize_processed_files(self, json_data):
"""
将JSON字符串反序列化为processed_files(从附件中读取文件数据)
:param json_data: JSON字符串
:return: 处理后的文件数组
"""
if not json_data:
return []
try:
serialized_data = json.loads(json_data)
processed_files = []
for data in serialized_data:
bl_id = data.get('bl_id')
attachment_id = data.get('attachment_id')
if bl_id:
bl = self.env['cc.bl'].browse(bl_id)
if bl.exists():
# 从附件中读取文件数据
file_data = ''
if attachment_id:
try:
attachment = self.env['ir.attachment'].browse(attachment_id)
if attachment.exists():
# attachment.datas 已经是 base64 编码的字符串
file_data = attachment.datas
_logger.info(f"从附件读取文件: {attachment.name}, ID: {attachment_id}, 数据长度: {len(file_data) if file_data else 0}")
# 验证数据格式
if file_data:
_logger.info(f"附件数据格式: 前100个字符: {file_data[:100]}")
# 验证是否为有效的 base64 数据
try:
import base64
# 尝试解码验证 base64 格式
decoded = base64.b64decode(file_data)
_logger.info(f"Base64 解码成功,解码后数据长度: {len(decoded)}")
except Exception as e:
_logger.error(f"Base64 解码失败: {str(e)}")
else:
_logger.warning(f"附件 {attachment_id} 的数据为空")
else:
_logger.warning(f"附件不存在: {attachment_id}")
except Exception as e:
_logger.error(f"读取附件失败: {str(e)}")
else:
_logger.warning(f"提单 {bl.bl_no} 没有附件ID,无法读取文件数据")
file_info = {
'bl': bl,
'bl_no': data.get('bl_no', ''),
'file_name': data.get('file_name', ''),
'file_data': file_data,
}
# 如果有OCR文本,也恢复
if 'ocr_texts' in data:
file_info['ocr_texts'] = data['ocr_texts']
processed_files.append(file_info)
return processed_files
except Exception as e:
_logger.error(f"反序列化processed_files失败: {str(e)}")
return []
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Batch Get POD Info Wizard Form View 批量获取POD信息向导表单视图 -->
<record id="view_batch_get_pod_info_wizard_form" model="ir.ui.view">
<field name="name">batch.get.pod.info.wizard.form</field>
......@@ -11,35 +11,54 @@
<sheet>
<!-- <group> -->
<group>
<field name="sync_last_mile_pod" widget="boolean_toggle" attrs="{'invisible': [('pdf_file', '=', False)]}"/>
<field name="remove_specified_text" readonly="1" widget="boolean_toggle"
attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
</group>
<group>
<field name="remove_specified_text" widget="boolean_toggle" attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
<field name="sync_last_mile_pod" widget="boolean_toggle"
attrs="{'invisible': ['|',('pdf_file', '=', False),('show_error_message', '!=', False)]}"/>
</group>
<group>
<field name="sync_match_node" widget="boolean_toggle" attrs="{'invisible': [('pdf_file', '=', False)]}"/>
<field name="sync_match_node" widget="boolean_toggle"
attrs="{'invisible': ['|',('pdf_file', '=', False),('show_error_message', '!=', False)]}"/>
</group>
<!-- </group> -->
<div class="alert alert-info" role="alert">
<strong>Description:</strong> <!-- 说明: -->
<ul>
<li><strong>Sync Last Mile POD:</strong> Synchronize POD (Proof of Delivery) attachment information with TK system, including big package quantities and container numbers</li> <!-- 同步尾程POD:向TK同步尾程交接POD(待大包数量和箱号)的附件信息 -->
<li><strong>Remove Specified Text:</strong> Remove specified text (AGN, UCLINK LOGISITICS LTD) from PDF files</li> <!-- 涂抹指定文字:对PDF文件中的指定文字进行涂抹处理 -->
<li><strong>Sync Push Match Node:</strong> Synchronize and push matched node information based on POD file, extract time from red boxes as node operation time</li> <!-- 同步推送匹配节点:根据POD文件获取对应的清关节点,提取红色框时间作为节点操作时间 -->
<li>
<strong>Sync Last Mile POD:</strong>
Synchronize POD (Proof of Delivery) attachment information with TK system, including
big package quantities and container numbers
</li> <!-- 同步尾程POD:向TK同步尾程交接POD(待大包数量和箱号)的附件信息 -->
<li>
<strong>Remove Specified Text:</strong>
Remove specified text (AGN, UCLINK LOGISITICS LTD) from PDF files
</li> <!-- 涂抹指定文字:对PDF文件中的指定文字进行涂抹处理 -->
<li>
<strong>Sync Push Match Node:</strong>
Synchronize and push matched node information based on POD file, extract time from
red boxes as node operation time
</li> <!-- 同步推送匹配节点:根据POD文件获取对应的清关节点,提取红色框时间作为节点操作时间 -->
</ul>
</div>
<div class="alert alert-danger" role="alert" attrs="{'invisible': [('show_error_message', '=', False)]}">
<div class="alert alert-danger" role="alert"
attrs="{'invisible': [('show_error_message', '=', False)]}">
<field name="show_error_message"/>
</div>
<div>
<field name="pdf_file" filename="pdf_filename" widget="pdf_viewer" readonly="1" attrs="{'invisible': [('pdf_file', '=', False)]}"/>
<field name="pdf_file" filename="pdf_filename" widget="pdf_viewer" readonly="1"
attrs="{'invisible': [('pdf_file', '=', False)]}"/>
</div>
<footer>
<!-- 预览按钮:处理PDF并显示合并后的文件 -->
<button string="Preview" type="object" name="action_preview" class="btn-primary" attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
<button string="Preview" type="object" name="action_preview" class="btn-primary"
attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
<!-- 确认按钮:使用已处理的文件数据进行回写和同步 -->
<button string="Confirm" type="object" name="confirm" class="btn-primary" attrs="{'invisible': [('pdf_file', '=', False)]}"/>
<button string="Confirm" type="object" name="confirm" class="btn-primary"
attrs="{'invisible': [('pdf_file', '=', False)]}"/>
<button string="Close" special="cancel"/>
</footer>
</sheet>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论