|
@@ -8,10 +8,16 @@ import logging
|
|
|
from datetime import datetime
|
|
|
import json
|
|
|
import os
|
|
|
+import uuid
|
|
|
from typing import Dict, Any, Optional, List, Tuple
|
|
|
import base64
|
|
|
from PIL import Image
|
|
|
import io
|
|
|
+from openai import OpenAI
|
|
|
+from app.config.config import DevelopmentConfig, ProductionConfig
|
|
|
+
|
|
|
+# 使用配置变量
|
|
|
+config = ProductionConfig()
|
|
|
|
|
|
|
|
|
def parse_business_card_image(image_path: str, task_id: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -400,33 +406,289 @@ def resize_image(image_path: str, max_width: int = 800, max_height: int = 600,
|
|
|
}
|
|
|
|
|
|
|
|
|
-def batch_process_images(image_paths: List[str], process_type: str = 'business_card') -> Dict[str, Any]:
|
|
|
+def parse_table_image(image_path: str, task_id: Optional[str] = None) -> Dict[str, Any]:
|
|
|
+ """
|
|
|
+ 解析包含表格的图片,提取人员信息
|
|
|
+
|
|
|
+ Args:
|
|
|
+ image_path (str): 表格图片路径
|
|
|
+ task_id (str, optional): 关联的任务ID
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ Dict[str, Any]: 解析结果
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ logging.info(f"开始解析表格图片: {image_path}")
|
|
|
+
|
|
|
+ # 验证文件存在性和格式
|
|
|
+ validation_result = validate_image_file(image_path)
|
|
|
+ if not validation_result['is_valid']:
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'error': validation_result['error'],
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ # 获取图片信息
|
|
|
+ image_info = get_image_info(image_path)
|
|
|
+
|
|
|
+ # 将图片转换为Base64进行千问模型调用
|
|
|
+ base64_image = convert_image_to_base64(image_path)
|
|
|
+ if not base64_image:
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'error': '图片Base64转换失败',
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ # 调用千问模型解析表格
|
|
|
+ try:
|
|
|
+ table_data = parse_table_with_qwen(base64_image)
|
|
|
+ logging.info("千问模型表格解析完成")
|
|
|
+ except Exception as e:
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'error': f"大模型解析失败: {str(e)}",
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+ # 构建完整的解析结果
|
|
|
+ result = {
|
|
|
+ 'success': True,
|
|
|
+ 'error': None,
|
|
|
+ 'data': {
|
|
|
+ 'extracted_data': table_data,
|
|
|
+ 'parse_time': datetime.now().isoformat(),
|
|
|
+ 'image_info': image_info,
|
|
|
+ 'extraction_info': {
|
|
|
+ 'extraction_method': 'Qwen-VL-Max',
|
|
|
+ 'process_type': 'table',
|
|
|
+ 'task_id': task_id
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logging.info(f"表格图片解析完成: {image_path}")
|
|
|
+ return result
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"解析表格图片失败: {str(e)}"
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'error': error_msg,
|
|
|
+ 'data': None
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def parse_table_with_qwen(base64_image: str) -> List[Dict[str, Any]]:
|
|
|
+ """
|
|
|
+ 使用阿里云千问大模型解析表格图片中的人员信息
|
|
|
+
|
|
|
+ Args:
|
|
|
+ base64_image (str): 图片的Base64编码
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ List[Dict[str, Any]]: 解析的人员信息列表
|
|
|
+ """
|
|
|
+ # 阿里云 Qwen API 配置
|
|
|
+ QWEN_API_KEY = os.environ.get('QWEN_API_KEY', 'sk-8f2320dafc9e4076968accdd8eebd8e9')
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 初始化 OpenAI 客户端,配置为阿里云 API
|
|
|
+ client = OpenAI(
|
|
|
+ api_key=QWEN_API_KEY,
|
|
|
+ base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
|
+ )
|
|
|
+
|
|
|
+ # 构建针对表格解析的专业提示语
|
|
|
+ prompt = """你是表格信息提取专家。请仔细分析提供的图片中的表格内容,精确提取其中的人员信息。
|
|
|
+
|
|
|
+## 提取要求
|
|
|
+- 识别表格中的所有人员记录
|
|
|
+- 区分中英文内容,分别提取
|
|
|
+- 保持提取信息的原始格式(如大小写、标点)
|
|
|
+- 对于无法识别或表格中不存在的信息,返回空字符串
|
|
|
+- 表格中没有的信息,请不要猜测
|
|
|
+- 如果表格中有多个人员,请全部提取
|
|
|
+
|
|
|
+## 需提取的字段(每个人员一条记录)
|
|
|
+1. 姓名 (name) - 中文姓名优先,如果只有英文则提取英文姓名
|
|
|
+2. 工作单位 (work_unit) - 公司名称、酒店名称或机构名称
|
|
|
+3. 职务头衔 (position) - 职位、头衔或职务名称
|
|
|
+4. 手机号码 (mobile) - 手机号码,如有多个用逗号分隔
|
|
|
+5. 邮箱 (email) - 电子邮箱地址
|
|
|
+
|
|
|
+## 输出格式
|
|
|
+请以严格的JSON数组格式返回结果,每个人员一个JSON对象。不要添加任何额外解释文字。
|
|
|
+
|
|
|
+示例格式:
|
|
|
+```json
|
|
|
+[
|
|
|
+ {
|
|
|
+ "name": "张三",
|
|
|
+ "work_unit": "北京万豪酒店",
|
|
|
+ "position": "总经理",
|
|
|
+ "mobile": "13800138000",
|
|
|
+ "email": "zhangsan@marriott.com"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "name": "李四",
|
|
|
+ "work_unit": "上海希尔顿酒店",
|
|
|
+ "position": "市场总监",
|
|
|
+ "mobile": "13900139000",
|
|
|
+ "email": "lisi@hilton.com"
|
|
|
+ }
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+如果表格中只有一个人员,也要返回数组格式:
|
|
|
+```json
|
|
|
+[
|
|
|
+ {
|
|
|
+ "name": "王五",
|
|
|
+ "work_unit": "深圳凯悦酒店",
|
|
|
+ "position": "人事经理",
|
|
|
+ "mobile": "13700137000",
|
|
|
+ "email": "wangwu@hyatt.com"
|
|
|
+ }
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+请分析以下表格图片:"""
|
|
|
+
|
|
|
+ # 调用 Qwen VL Max API
|
|
|
+ logging.info("发送表格图片请求到 Qwen VL Max 模型")
|
|
|
+ completion = client.chat.completions.create(
|
|
|
+ model="qwen-vl-max-latest",
|
|
|
+ messages=[
|
|
|
+ {
|
|
|
+ "role": "user",
|
|
|
+ "content": [
|
|
|
+ {"type": "text", "text": prompt},
|
|
|
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ temperature=0.1, # 降低温度增加精确性
|
|
|
+ response_format={"type": "json_object"} # 要求输出JSON格式
|
|
|
+ )
|
|
|
+
|
|
|
+ # 解析响应
|
|
|
+ response_content = completion.choices[0].message.content
|
|
|
+ logging.info(f"成功从 Qwen 模型获取表格解析响应")
|
|
|
+
|
|
|
+ # 直接解析 QWen 返回的 JSON 响应
|
|
|
+ try:
|
|
|
+ parsed_data = json.loads(response_content)
|
|
|
+ logging.info("成功解析 Qwen 表格响应中的 JSON")
|
|
|
+ except json.JSONDecodeError as e:
|
|
|
+ error_msg = f"JSON 解析失败: {str(e)}"
|
|
|
+ logging.error(error_msg)
|
|
|
+ raise Exception(error_msg)
|
|
|
+
|
|
|
+ # 确保返回的是数组格式
|
|
|
+ if not isinstance(parsed_data, list):
|
|
|
+ # 如果返回的不是数组,尝试提取数组或包装成数组
|
|
|
+ if isinstance(parsed_data, dict):
|
|
|
+ # 检查是否有数组字段
|
|
|
+ for key, value in parsed_data.items():
|
|
|
+ if isinstance(value, list):
|
|
|
+ parsed_data = value
|
|
|
+ break
|
|
|
+ else:
|
|
|
+ # 如果没有数组字段,将对象包装成数组
|
|
|
+ parsed_data = [parsed_data]
|
|
|
+ else:
|
|
|
+ parsed_data = []
|
|
|
+
|
|
|
+ # 处理每个人员记录
|
|
|
+ processed_data = []
|
|
|
+ for person_data in parsed_data:
|
|
|
+ if not isinstance(person_data, dict):
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 确保所有必要字段存在
|
|
|
+ required_fields = ['name', 'work_unit', 'position', 'mobile', 'email']
|
|
|
+ for field in required_fields:
|
|
|
+ if field not in person_data:
|
|
|
+ person_data[field] = ""
|
|
|
+
|
|
|
+ # 创建职业轨迹记录
|
|
|
+ career_entry = {
|
|
|
+ 'date': datetime.now().strftime('%Y-%m-%d'),
|
|
|
+ 'hotel_en': '',
|
|
|
+ 'hotel_zh': person_data.get('work_unit', ''),
|
|
|
+ 'image_path': '',
|
|
|
+ 'source': 'table_extraction',
|
|
|
+ 'title_en': '',
|
|
|
+ 'title_zh': person_data.get('position', '')
|
|
|
+ }
|
|
|
+
|
|
|
+ # 将字段映射到标准格式
|
|
|
+ standardized_person = {
|
|
|
+ 'name_zh': person_data.get('name', ''),
|
|
|
+ 'name_en': '',
|
|
|
+ 'title_zh': person_data.get('position', ''),
|
|
|
+ 'title_en': '',
|
|
|
+ 'hotel_zh': person_data.get('work_unit', ''),
|
|
|
+ 'hotel_en': '',
|
|
|
+ 'mobile': person_data.get('mobile', ''),
|
|
|
+ 'phone': '',
|
|
|
+ 'email': person_data.get('email', ''),
|
|
|
+ 'address_zh': '',
|
|
|
+ 'address_en': '',
|
|
|
+ 'postal_code_zh': '',
|
|
|
+ 'postal_code_en': '',
|
|
|
+ 'birthday': '',
|
|
|
+ 'age': 0,
|
|
|
+ 'native_place': '',
|
|
|
+ 'residence': '',
|
|
|
+ 'brand_group': '',
|
|
|
+ 'career_path': [career_entry],
|
|
|
+ 'affiliation': []
|
|
|
+ }
|
|
|
+
|
|
|
+ processed_data.append(standardized_person)
|
|
|
+ logging.info(f"处理人员记录: {person_data.get('name', 'Unknown')}")
|
|
|
+
|
|
|
+ return processed_data
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"Qwen VL Max 模型表格解析失败: {str(e)}"
|
|
|
+ logging.error(error_msg, exc_info=True)
|
|
|
+ raise Exception(error_msg)
|
|
|
+
|
|
|
+
|
|
|
+def batch_process_images(image_paths: List[str], process_type: str = 'table') -> Dict[str, Any]:
|
|
|
"""
|
|
|
批量处理图片
|
|
|
|
|
|
Args:
|
|
|
image_paths (List[str]): 图片路径列表
|
|
|
- process_type (str): 处理类型,可选值:'business_card', 'portrait'
|
|
|
+ process_type (str): 处理类型,只支持 'table'
|
|
|
|
|
|
Returns:
|
|
|
Dict[str, Any]: 批量处理结果
|
|
|
"""
|
|
|
try:
|
|
|
+ # 验证处理类型
|
|
|
+ if process_type != 'table':
|
|
|
+ return {
|
|
|
+ 'success': False,
|
|
|
+ 'error': f'不支持的处理类型: {process_type},只支持 "table" 类型',
|
|
|
+ 'results': []
|
|
|
+ }
|
|
|
+
|
|
|
results = []
|
|
|
success_count = 0
|
|
|
failed_count = 0
|
|
|
|
|
|
for image_path in image_paths:
|
|
|
try:
|
|
|
- if process_type == 'business_card':
|
|
|
- result = parse_business_card_image(image_path)
|
|
|
- elif process_type == 'portrait':
|
|
|
- result = parse_portrait_image(image_path)
|
|
|
- else:
|
|
|
- result = {
|
|
|
- 'success': False,
|
|
|
- 'error': f'不支持的处理类型: {process_type}'
|
|
|
- }
|
|
|
+ # 只支持表格处理
|
|
|
+ result = parse_table_image(image_path)
|
|
|
|
|
|
results.append({
|
|
|
'image_path': image_path,
|
|
@@ -454,7 +716,8 @@ def batch_process_images(image_paths: List[str], process_type: str = 'business_c
|
|
|
'total_images': len(image_paths),
|
|
|
'success_count': success_count,
|
|
|
'failed_count': failed_count,
|
|
|
- 'success_rate': (success_count / len(image_paths)) * 100 if image_paths else 0
|
|
|
+ 'success_rate': (success_count / len(image_paths)) * 100 if image_paths else 0,
|
|
|
+ 'process_type': process_type
|
|
|
},
|
|
|
'results': results
|
|
|
}
|