feat: 添加封面图压字花功能和启动脚本
- 新增封面图本地化压字花处理(深褐色文字+白色描边,居中显示) - 支持Linux/Windows跨平台字体加载 - 新增启动脚本 start_article_auto_image_matching.sh - 优化图片生成策略(0张图/1张图/多张图不同处理) - 绕过网络接口IncompleteRead问题,本地化处理更稳定 - 更新README文档,完善使用说明
This commit is contained in:
@@ -95,7 +95,8 @@ class ArticleImageMatcher:
|
||||
a.title,
|
||||
a.content,
|
||||
a.coze_tag,
|
||||
a.department
|
||||
a.department,
|
||||
a.department_id
|
||||
FROM ai_articles a
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM ai_article_images ai
|
||||
@@ -135,7 +136,7 @@ class ArticleImageMatcher:
|
||||
connection = self.db_manager.get_connection()
|
||||
try:
|
||||
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
# 查询指定科室ID、状态为generate且附加文章数量小于5的图片(不使用JOIN)
|
||||
# 查询指定科室ID且状态为generate且附加文章数量小于5的图片
|
||||
# 包含image_source字段用于区分实拍图和模板图
|
||||
if article_department_id > 0:
|
||||
sql = """
|
||||
@@ -156,11 +157,7 @@ class ArticleImageMatcher:
|
||||
FROM ai_image_tags it
|
||||
WHERE it.image_attached_article_count < 5
|
||||
AND it.department_id = %s
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ai_images i
|
||||
WHERE i.id = it.image_id
|
||||
AND i.status = 'generate'
|
||||
)
|
||||
AND it.status = 'generate'
|
||||
ORDER BY it.image_attached_article_count ASC, it.id DESC
|
||||
"""
|
||||
cursor.execute(sql, (article_department_id,))
|
||||
@@ -183,11 +180,7 @@ class ArticleImageMatcher:
|
||||
it.image_source
|
||||
FROM ai_image_tags it
|
||||
WHERE it.image_attached_article_count < 5
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ai_images i
|
||||
WHERE i.id = it.image_id
|
||||
AND i.status = 'generate'
|
||||
)
|
||||
AND it.status = 'generate'
|
||||
ORDER BY it.image_attached_article_count ASC, it.id DESC
|
||||
"""
|
||||
cursor.execute(sql)
|
||||
@@ -198,6 +191,9 @@ class ArticleImageMatcher:
|
||||
self.log_to_database('INFO', f"查询到可用图片", f"数量: {len(results)}")
|
||||
else:
|
||||
logger.info("未查询到可用图片")
|
||||
# 如果相关科室下没有可使用的图片,记录日志
|
||||
if article_department_id > 0:
|
||||
logger.info(f"科室ID {article_department_id} 下没有可使用的图片,将进行Gemini生图")
|
||||
|
||||
return results
|
||||
finally:
|
||||
@@ -328,6 +324,36 @@ class ArticleImageMatcher:
|
||||
self.log_to_database('ERROR', error_msg, traceback.format_exc())
|
||||
return False, 0.0
|
||||
|
||||
def get_article_image_count(self, article_id: int) -> int:
|
||||
"""
|
||||
获取文章当前已关联的图片数量
|
||||
|
||||
Args:
|
||||
article_id: 文章ID
|
||||
|
||||
Returns:
|
||||
图片数量
|
||||
"""
|
||||
try:
|
||||
connection = self.db_manager.get_connection()
|
||||
try:
|
||||
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
sql = """
|
||||
SELECT COUNT(*) as image_count
|
||||
FROM ai_article_images
|
||||
WHERE article_id = %s
|
||||
"""
|
||||
cursor.execute(sql, (article_id,))
|
||||
result = cursor.fetchone()
|
||||
return result['image_count'] if result else 0
|
||||
finally:
|
||||
connection.close()
|
||||
except Exception as e:
|
||||
error_msg = f"查询文章图片数量异常: {e}"
|
||||
logger.error(error_msg)
|
||||
self.log_to_database('ERROR', error_msg, traceback.format_exc())
|
||||
return 0
|
||||
|
||||
def update_article_status(self, article_id: int, new_status: str) -> bool:
|
||||
"""
|
||||
更新文章状态
|
||||
@@ -409,7 +435,7 @@ class ArticleImageMatcher:
|
||||
image_data['keywords_name'],
|
||||
image_data['department_id'],
|
||||
image_data['department_name'],
|
||||
1 # image_source: 1表示tag匹配
|
||||
image_data['image_source'] # 使用原始图片的image_source值
|
||||
))
|
||||
|
||||
# 更新图片附加文章计数
|
||||
@@ -428,8 +454,12 @@ class ArticleImageMatcher:
|
||||
"""
|
||||
cursor.execute(update_image_status_sql, (image_data['image_id'],))
|
||||
|
||||
# 更新文章状态为published_review
|
||||
self.update_article_status(article_id, 'published_review')
|
||||
# 调用RPA审核接口更新文章状态
|
||||
if self.call_rpa_review_api([article_id], 13):
|
||||
logger.info(f"已通过RPA接口更新文章 {article_id} 状态")
|
||||
else:
|
||||
logger.error(f"通过RPA接口更新文章 {article_id} 状态失败")
|
||||
return False
|
||||
|
||||
connection.commit()
|
||||
logger.info(f"成功插入文章图片关联 - 文章ID: {article_id}, 图片ID: {image_data['image_id']}, 分数: {match_score}")
|
||||
@@ -443,7 +473,295 @@ class ArticleImageMatcher:
|
||||
self.log_to_database('ERROR', error_msg, traceback.format_exc())
|
||||
return False
|
||||
|
||||
def generate_image_with_gemini(self, prompt: str, article_tags: List[str], article_id: int) -> Optional[str]:
|
||||
def get_article_info(self, article_id: int) -> Optional[Dict]:
|
||||
"""
|
||||
获取文章信息,包括部门和关键词信息
|
||||
|
||||
Args:
|
||||
article_id: 文章ID
|
||||
|
||||
Returns:
|
||||
文章信息字典
|
||||
"""
|
||||
try:
|
||||
connection = self.db_manager.get_connection()
|
||||
try:
|
||||
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
sql = """
|
||||
SELECT id, title, content, coze_tag, department, department_id
|
||||
FROM ai_articles
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(sql, (article_id,))
|
||||
result = cursor.fetchone()
|
||||
return result
|
||||
finally:
|
||||
connection.close()
|
||||
except Exception as e:
|
||||
error_msg = f"查询文章信息异常: {e}"
|
||||
logger.error(error_msg)
|
||||
self.log_to_database('ERROR', error_msg, traceback.format_exc())
|
||||
return None
|
||||
|
||||
def get_article_image_sources(self, article_id: int) -> List[int]:
|
||||
"""
|
||||
获取文章现有图片的image_source值列表
|
||||
|
||||
Args:
|
||||
article_id: 文章ID
|
||||
|
||||
Returns:
|
||||
image_source值列表
|
||||
"""
|
||||
try:
|
||||
connection = self.db_manager.get_connection()
|
||||
try:
|
||||
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
sql = """
|
||||
SELECT image_source
|
||||
FROM ai_article_images
|
||||
WHERE article_id = %s
|
||||
"""
|
||||
cursor.execute(sql, (article_id,))
|
||||
results = cursor.fetchall()
|
||||
return [row['image_source'] for row in results] if results else []
|
||||
finally:
|
||||
connection.close()
|
||||
except Exception as e:
|
||||
error_msg = f"查询文章图片source异常: {e}"
|
||||
logger.error(error_msg)
|
||||
self.log_to_database('ERROR', error_msg, traceback.format_exc())
|
||||
return []
|
||||
|
||||
def add_text_to_image_local(self, image_path: str, text: str, output_path: str) -> bool:
|
||||
"""
|
||||
本地图片文字融合处理(压字花)
|
||||
|
||||
Args:
|
||||
image_path: 原图路径
|
||||
text: 要添加的文字(文章标题)
|
||||
output_path: 输出路径
|
||||
|
||||
Returns:
|
||||
是否处理成功
|
||||
"""
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import textwrap
|
||||
|
||||
# 打开图片
|
||||
image = Image.open(image_path)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# 获取图片尺寸
|
||||
img_width, img_height = image.size
|
||||
|
||||
# 计算自适应字体大小(基础120px)
|
||||
base_font_size = 120
|
||||
font_size = int(base_font_size * min(img_width / 1920, img_height / 1080))
|
||||
font_size = max(40, min(font_size, 150)) # 限制范围40-150px
|
||||
|
||||
# 尝试加载字体(支持Windows和Linux)
|
||||
font = None
|
||||
font_loaded = False
|
||||
|
||||
try:
|
||||
# 根据操作系统选择字体路径
|
||||
import platform
|
||||
system = platform.system()
|
||||
|
||||
if system == 'Windows':
|
||||
font_paths = [
|
||||
'C:/Windows/Fonts/msyh.ttc', # 微软雅黑
|
||||
'C:/Windows/Fonts/simhei.ttf', # 黑体
|
||||
'C:/Windows/Fonts/simsun.ttc', # 宋体
|
||||
'C:/Windows/Fonts/msyhbd.ttc', # 微软雅黑Bold
|
||||
'C:/Windows/Fonts/simkai.ttf', # 楷体
|
||||
]
|
||||
else: # Linux/Unix
|
||||
font_paths = [
|
||||
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', # 文泉驿正黑
|
||||
'/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc', # 文泉驿正黑(旧路径)
|
||||
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', # 文泉驿微米黑
|
||||
'/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf', # Droid Sans
|
||||
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc', # Noto Sans CJK
|
||||
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', # Noto Sans CJK
|
||||
'/usr/share/fonts/truetype/arphic/uming.ttc', # AR PL UMing
|
||||
'/usr/share/fonts/truetype/arphic/ukai.ttc', # AR PL UKai
|
||||
]
|
||||
|
||||
logger.info(f"[压字花] 检测到操作系统: {system},尝试加载中文字体...")
|
||||
|
||||
for font_path in font_paths:
|
||||
try:
|
||||
if os.path.exists(font_path):
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
font_loaded = True
|
||||
logger.info(f"[压字花] 成功加载字体: {font_path}")
|
||||
break
|
||||
else:
|
||||
logger.debug(f"[压字花] 字体文件不存在: {font_path}")
|
||||
except Exception as font_err:
|
||||
logger.debug(f"[压字花] 字体加载失败 {font_path}: {font_err}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"[压字花] 字体加载异常: {e}")
|
||||
|
||||
# 如果所有字体都加载失败,给出安装提示
|
||||
if not font_loaded or font is None:
|
||||
if system == 'Linux':
|
||||
error_msg = (
|
||||
"无法加载任何中文字体。请在Linux服务器上安装中文字体:\n"
|
||||
"Ubuntu/Debian: sudo apt-get install fonts-wqy-zenhei fonts-wqy-microhei\n"
|
||||
"CentOS/RHEL: sudo yum install wqy-zenhei-fonts wqy-microhei-fonts\n"
|
||||
"或: sudo yum install google-noto-sans-cjk-fonts"
|
||||
)
|
||||
else:
|
||||
error_msg = "无法加载任何中文字体,压字花功能需要中文字体支持"
|
||||
logger.error(f"[压字花] {error_msg}")
|
||||
raise Exception(error_msg)
|
||||
|
||||
# 文字自动换行(每行最多12个字符,避免过长)
|
||||
max_chars_per_line = 12
|
||||
lines = textwrap.wrap(text, width=max_chars_per_line)
|
||||
|
||||
# 如果标题过长,手动分割
|
||||
if not lines:
|
||||
lines = [text]
|
||||
|
||||
# 计算文字总高度
|
||||
line_height = font_size + 20 # 行间距
|
||||
total_text_height = len(lines) * line_height
|
||||
|
||||
# 计算文字起始Y坐标(居中)
|
||||
start_y = (img_height - total_text_height) // 2
|
||||
|
||||
# 深褐色文字颜色 RGB(180, 60, 50)
|
||||
text_color = (180, 60, 50)
|
||||
# 白色描边颜色
|
||||
outline_color = (255, 255, 255)
|
||||
outline_width = 3 # 描边宽度
|
||||
|
||||
# 绘制每一行文字
|
||||
for i, line in enumerate(lines):
|
||||
# 计算文字宽度(居中)
|
||||
try:
|
||||
bbox = draw.textbbox((0, 0), line, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
except:
|
||||
# 兼容旧版本Pillow
|
||||
text_width = len(line) * font_size * 0.6
|
||||
|
||||
x = (img_width - text_width) // 2
|
||||
y = start_y + i * line_height
|
||||
|
||||
# 绘制白色描边(多次绘制形成描边效果)
|
||||
for offset_x in range(-outline_width, outline_width + 1):
|
||||
for offset_y in range(-outline_width, outline_width + 1):
|
||||
if offset_x != 0 or offset_y != 0:
|
||||
draw.text((x + offset_x, y + offset_y), line, font=font, fill=outline_color)
|
||||
|
||||
# 绘制深褐色文字
|
||||
draw.text((x, y), line, font=font, fill=text_color)
|
||||
|
||||
# 保存图片
|
||||
image.save(output_path, 'PNG', optimize=True, compress_level=6)
|
||||
logger.info(f"[压字花] 文字融合成功: {output_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[压字花] 文字融合失败: {e}")
|
||||
return False
|
||||
|
||||
def upload_cover_image_with_text_fusion(self, image_path: str, article_id: int, article_title: str,
|
||||
image_info: Dict) -> bool:
|
||||
"""
|
||||
本地化处理封面图(压字花 + 上传 + 关联)
|
||||
|
||||
Args:
|
||||
image_path: 本地图片路径
|
||||
article_id: 文章ID
|
||||
article_title: 文章标题(用于压字花)
|
||||
image_info: 图片信息字典
|
||||
|
||||
Returns:
|
||||
是否上传成功
|
||||
"""
|
||||
try:
|
||||
# 1. 本地压字花处理
|
||||
logger.info(f"[封面图本地化] 开始压字花处理,文章ID: {article_id}, 标题: {article_title}")
|
||||
|
||||
# 生成带文字的临时文件
|
||||
import uuid
|
||||
text_temp_path = f"temp_text_{uuid.uuid4().hex}.png"
|
||||
|
||||
fusion_success = self.add_text_to_image_local(
|
||||
image_path=image_path,
|
||||
text=article_title,
|
||||
output_path=text_temp_path
|
||||
)
|
||||
|
||||
if not fusion_success:
|
||||
logger.error(f"[封面图本地化] 压字花处理失败")
|
||||
return False
|
||||
|
||||
# 2. 使用通用图片上传接口
|
||||
logger.info(f"[封面图本地化] 开始上传压字花图片")
|
||||
upload_result = self.upload_image_to_server(text_temp_path, image_info['tag_image_id'])
|
||||
|
||||
if not upload_result:
|
||||
# 删除临时文件
|
||||
if os.path.exists(text_temp_path):
|
||||
os.remove(text_temp_path)
|
||||
logger.error(f"[封面图本地化] 上传失败")
|
||||
return False
|
||||
|
||||
# 获取上传后的真实路径
|
||||
uploaded_relative_path = upload_result.get('relative_path') or upload_result.get('image_url')
|
||||
uploaded_thumb_path = upload_result.get('thumb_relative_path') or upload_result.get('image_thumb_url', '')
|
||||
|
||||
logger.info(f"[封面图本地化] 上传成功,相对路径: {uploaded_relative_path}")
|
||||
|
||||
# 3. 更新数据库中的图片URL
|
||||
self.update_image_urls_after_upload(
|
||||
image_id=image_info['image_id'],
|
||||
tag_image_id=image_info['tag_image_id'],
|
||||
image_url=uploaded_relative_path,
|
||||
image_thumb_url=uploaded_thumb_path
|
||||
)
|
||||
|
||||
# 4. 插入文章图片关联记录(image_source=12表示封面图)
|
||||
article_image_id = self.insert_article_image_relation_for_generated(
|
||||
article_id=article_id,
|
||||
image_id=image_info['image_id'],
|
||||
image_url=uploaded_relative_path,
|
||||
image_thumb_url=uploaded_thumb_path,
|
||||
tag_image_id=image_info['tag_image_id'],
|
||||
keywords_id=image_info['keywords_id'],
|
||||
keywords_name=image_info['keywords_name'],
|
||||
department_id=image_info['department_id'],
|
||||
department_name=image_info['department_name'],
|
||||
image_source=12 # 封面图固定为12(实拍图)
|
||||
)
|
||||
|
||||
if article_image_id:
|
||||
logger.info(f"[封面图本地化] 文章图片关联信息已创建,ai_article_images.id: {article_image_id}")
|
||||
else:
|
||||
logger.error(f"[封面图本地化] 文章图片关联创建失败")
|
||||
return False
|
||||
|
||||
# 删除临时文件
|
||||
if os.path.exists(text_temp_path):
|
||||
os.remove(text_temp_path)
|
||||
|
||||
logger.info(f"[封面图本地化] 处理完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[封面图本地化] 处理失败: {e}")
|
||||
return False
|
||||
|
||||
def generate_image_with_gemini(self, prompt: str, article_tags: List[str], article_id: int, image_type: str = "默认") -> Optional[str]:
|
||||
"""
|
||||
使用Gemini生成图片并上传到服务器
|
||||
|
||||
@@ -451,11 +769,17 @@ class ArticleImageMatcher:
|
||||
prompt: 图片生成提示词
|
||||
article_tags: 文章标签列表,用于查询department和keywords
|
||||
article_id: 文章ID,用于关联图片
|
||||
image_type: 图片类型(封面图/详情图/海报图)
|
||||
|
||||
Returns:
|
||||
上传后的图片URL,失败返回None
|
||||
"""
|
||||
try:
|
||||
# 从文章表获取文章的部门信息
|
||||
article_info = self.get_article_info(article_id)
|
||||
article_department = article_info.get('department', '') if article_info else ''
|
||||
article_department_id = article_info.get('department_id', 0) if article_info else 0
|
||||
|
||||
# 导入必要的库
|
||||
from google import genai
|
||||
from google.genai.client import HttpOptions
|
||||
@@ -486,51 +810,109 @@ class ArticleImageMatcher:
|
||||
if hasattr(part, 'inline_data') and part.inline_data is not None:
|
||||
image_data = part.inline_data
|
||||
if image_data.data is not None:
|
||||
# 生成唯一的文件名(基于时间戳)
|
||||
# 生成临时文件名
|
||||
timestamp_ms = int(time.time() * 1000)
|
||||
image_filename = f"{timestamp_ms}.png"
|
||||
today_date = datetime.now().strftime("%Y%m%d")
|
||||
image_url_path = f"{today_date}/{image_filename}"
|
||||
|
||||
temp_filename = f"temp_generated_image_{timestamp_ms}.png"
|
||||
|
||||
# 保存图片数据到临时文件
|
||||
with open(temp_filename, 'wb') as f:
|
||||
f.write(image_data.data)
|
||||
logger.info(f"Gemini生成图片成功: {temp_filename}")
|
||||
|
||||
# 先将图片信息插入数据库
|
||||
image_info = self.insert_generated_image_to_db(image_filename, image_url_path, article_tags)
|
||||
# 先上传图片到服务器,获取真实的文件名和路径
|
||||
# 注意:需要先插入ai_image_tags表获取tag_image_id,才能上传
|
||||
# 所以这里先使用临时路径插入数据库
|
||||
today_date = datetime.now().strftime("%Y%m%d")
|
||||
temp_image_path = f"{today_date}/{timestamp_ms}.png"
|
||||
|
||||
# 先将图片信息插入数据库(使用临时路径)
|
||||
image_info = self.insert_generated_image_to_db(
|
||||
f"{timestamp_ms}.png", # 临时文件名
|
||||
temp_image_path, # 临时路径
|
||||
article_department=article_department,
|
||||
article_department_id=article_department_id,
|
||||
article_keywords='',
|
||||
article_keywords_id=0,
|
||||
article_tags=article_tags
|
||||
)
|
||||
|
||||
if not image_info:
|
||||
os.remove(temp_filename)
|
||||
raise Exception("插入图片信息到数据库失败")
|
||||
|
||||
logger.info(f"图片信息已插入数据库,tag_image_id: {image_info['tag_image_id']}, image_id: {image_info['image_id']}")
|
||||
|
||||
# 使用tag_image_id上传图片到服务器
|
||||
uploaded_url = self.upload_image_to_server(temp_filename, image_info['tag_image_id'])
|
||||
upload_result = self.upload_image_to_server(temp_filename, image_info['tag_image_id'])
|
||||
|
||||
# 将文章与图片的关联信息插入ai_article_images表
|
||||
article_image_id = self.insert_article_image_relation_for_generated(
|
||||
article_id=article_id,
|
||||
if not upload_result:
|
||||
os.remove(temp_filename)
|
||||
raise Exception("图片上传失败")
|
||||
|
||||
# 从上传响应中获取真实的文件名和路径
|
||||
uploaded_relative_path = upload_result.get('relative_path') or upload_result.get('image_url')
|
||||
uploaded_thumb_path = upload_result.get('thumb_relative_path') or upload_result.get('image_thumb_url', '')
|
||||
|
||||
logger.info(f"图片上传成功,真实相对路径: {uploaded_relative_path}")
|
||||
|
||||
# 更新数据库中的图片URL为上传后的真实路径
|
||||
self.update_image_urls_after_upload(
|
||||
image_id=image_info['image_id'],
|
||||
image_url=image_info['image_url'],
|
||||
image_thumb_url=image_info['image_thumb_url'],
|
||||
tag_image_id=image_info['tag_image_id'],
|
||||
keywords_id=image_info['keywords_id'],
|
||||
keywords_name=image_info['keywords_name'],
|
||||
department_id=image_info['department_id'],
|
||||
department_name=image_info['department_name'],
|
||||
image_source=0 # 默认值
|
||||
image_url=uploaded_relative_path,
|
||||
image_thumb_url=uploaded_thumb_path
|
||||
)
|
||||
|
||||
if article_image_id:
|
||||
logger.info(f"文章图片关联信息已创建,ai_article_images.id: {article_image_id}")
|
||||
# 更新image_info为上传后的真实路径
|
||||
image_info['image_url'] = uploaded_relative_path
|
||||
image_info['image_thumb_url'] = uploaded_thumb_path
|
||||
|
||||
# ⭐ 关键修改:封面图本地化处理(本地压字花 + 通用上传 + 关联)
|
||||
if image_type == '封面图':
|
||||
# 获取文章标题用于压字花
|
||||
article_info = self.get_article_info(article_id)
|
||||
article_title = article_info.get('title', '') if article_info else ''
|
||||
|
||||
# 本地化处理封面图(绕过网络接口问题)
|
||||
upload_success = self.upload_cover_image_with_text_fusion(
|
||||
image_path=temp_filename,
|
||||
article_id=article_id,
|
||||
article_title=article_title,
|
||||
image_info=image_info
|
||||
)
|
||||
|
||||
if upload_success:
|
||||
logger.info(f"[封面图] 文章 {article_id} 封面图本地化处理成功(已完成压字花和数据库关联)")
|
||||
else:
|
||||
logger.error(f"[封面图] 文章 {article_id} 封面图本地化处理失败")
|
||||
# 删除临时文件
|
||||
if os.path.exists(temp_filename):
|
||||
os.remove(temp_filename)
|
||||
return None
|
||||
else:
|
||||
# 详情图:使用原有逻辑插入关联(image_source=13)
|
||||
article_image_id = self.insert_article_image_relation_for_generated(
|
||||
article_id=article_id,
|
||||
image_id=image_info['image_id'],
|
||||
image_url=image_info['image_url'],
|
||||
image_thumb_url=image_info['image_thumb_url'],
|
||||
tag_image_id=image_info['tag_image_id'],
|
||||
keywords_id=image_info['keywords_id'],
|
||||
keywords_name=image_info['keywords_name'],
|
||||
department_id=image_info['department_id'],
|
||||
department_name=image_info['department_name'],
|
||||
image_source=13 # 详情图固定为13(AI生成图)
|
||||
)
|
||||
|
||||
if article_image_id:
|
||||
logger.info(f"[详情图] 文章图片关联信息已创建,ai_article_images.id: {article_image_id}")
|
||||
else:
|
||||
logger.error(f"[详情图] 文章 {article_id} 图片关联创建失败")
|
||||
|
||||
# 删除临时文件
|
||||
os.remove(temp_filename)
|
||||
|
||||
logger.info(f"图片已上传到服务器: {uploaded_url}")
|
||||
return uploaded_url
|
||||
return uploaded_relative_path
|
||||
|
||||
raise Exception("Gemini API未返回有效的图片数据")
|
||||
|
||||
@@ -543,13 +925,17 @@ class ArticleImageMatcher:
|
||||
self.log_to_database('ERROR', error_msg, traceback.format_exc())
|
||||
return None
|
||||
|
||||
def insert_generated_image_to_db(self, image_name: str, image_url: str, article_tags: List[str]) -> Optional[Dict]:
|
||||
def insert_generated_image_to_db(self, image_name: str, image_url: str, article_department: str = "", article_department_id: int = 0, article_keywords: str = "", article_keywords_id: int = 0, article_tags: List[str] = []) -> Optional[Dict]:
|
||||
"""
|
||||
将Gemini生成的图片信息插入数据库
|
||||
|
||||
Args:
|
||||
image_name: 图片文件名
|
||||
image_url: 图片URL路径
|
||||
article_department: 文章部门名称
|
||||
article_department_id: 文章部门ID
|
||||
article_keywords: 文章关键词名称
|
||||
article_keywords_id: 文章关键词ID
|
||||
article_tags: 文章标签列表
|
||||
|
||||
Returns:
|
||||
@@ -559,39 +945,43 @@ class ArticleImageMatcher:
|
||||
connection = self.db_manager.get_connection()
|
||||
try:
|
||||
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
# 根据文章标签查询ai_image_tags表
|
||||
# 使用文章的部门和关键词信息,如果没有则使用默认值
|
||||
department_id = article_department_id if article_department_id > 0 else 1
|
||||
keywords_id = article_keywords_id if article_keywords_id > 0 else 1
|
||||
department = article_department if article_department else 'AI生成'
|
||||
keywords = article_keywords if article_keywords else 'AI图片'
|
||||
|
||||
# 先确保文章的标签存在于ai_tags表中
|
||||
tag_id = 1 # 默认tag_id
|
||||
|
||||
if article_tags:
|
||||
query = """
|
||||
SELECT department_name, keywords_name, department_id, keywords_id, tag_id
|
||||
FROM ai_image_tags
|
||||
# 首先查询ai_tags表中是否已存在该标签
|
||||
query_tag = """
|
||||
SELECT id
|
||||
FROM ai_tags
|
||||
WHERE tag_name = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query, (article_tags[0],))
|
||||
cursor.execute(query_tag, (article_tags[0],))
|
||||
tag_info = cursor.fetchone()
|
||||
|
||||
if tag_info:
|
||||
department = tag_info['department_name']
|
||||
keywords = tag_info['keywords_name']
|
||||
department_id = tag_info['department_id']
|
||||
keywords_id = tag_info['keywords_id']
|
||||
tag_id = tag_info['tag_id']
|
||||
# 如果标签已存在,使用现有的信息
|
||||
tag_id = tag_info['id']
|
||||
tag_name = article_tags[0]
|
||||
else:
|
||||
department = "AI生成"
|
||||
keywords = "AI图片"
|
||||
department_id = 1
|
||||
keywords_id = 1
|
||||
tag_id = 1
|
||||
tag_name = article_tags[0] if article_tags else "AI生成"
|
||||
# 如果标签不存在,则插入新标签
|
||||
insert_tag_query = """
|
||||
INSERT INTO ai_tags (tag_name, created_at, updated_at)
|
||||
VALUES (%s, NOW(), NOW())
|
||||
"""
|
||||
cursor.execute(insert_tag_query, (article_tags[0],))
|
||||
tag_id = cursor.lastrowid
|
||||
tag_name = article_tags[0]
|
||||
else:
|
||||
department = "AI生成"
|
||||
keywords = "AI图片"
|
||||
department_id = 1
|
||||
keywords_id = 1
|
||||
tag_id = 1
|
||||
# 如果没有文章标签,使用默认值
|
||||
tag_name = "AI生成"
|
||||
|
||||
|
||||
# 插入ai_images表
|
||||
insert_image_query = """
|
||||
INSERT INTO ai_images
|
||||
@@ -638,7 +1028,7 @@ class ArticleImageMatcher:
|
||||
logger.error(f"插入图片信息到数据库失败: {e}")
|
||||
return None
|
||||
|
||||
def upload_image_to_server(self, image_path: str, tag_image_id: int) -> str:
|
||||
def upload_image_to_server(self, image_path: str, tag_image_id: int) -> Optional[Dict]:
|
||||
"""
|
||||
上传图片到服务器
|
||||
|
||||
@@ -647,7 +1037,7 @@ class ArticleImageMatcher:
|
||||
tag_image_id: 图片标签ID
|
||||
|
||||
Returns:
|
||||
服务器上的图片URL
|
||||
上传响应数据字典,包含relative_path等字段
|
||||
"""
|
||||
base_url = "http://47.99.184.230:8324"
|
||||
jwt_token = self.login_and_get_jwt_token(base_url)
|
||||
@@ -669,12 +1059,57 @@ class ArticleImageMatcher:
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get('code') == 200:
|
||||
return result['data']['http_image_url']
|
||||
upload_data = result['data']
|
||||
logger.info(f"上传成功,相对路径: {upload_data.get('relative_path')}")
|
||||
return upload_data
|
||||
else:
|
||||
raise Exception(f"图片上传失败: {result.get('message', '未知错误')}")
|
||||
else:
|
||||
raise Exception(f"图片上传请求失败,状态码: {response.status_code}")
|
||||
|
||||
def update_image_urls_after_upload(self, image_id: int, tag_image_id: int, image_url: str, image_thumb_url: str) -> bool:
|
||||
"""
|
||||
上传成功后更新数据库中的图片URL
|
||||
|
||||
Args:
|
||||
image_id: ai_images表的图片ID
|
||||
tag_image_id: ai_image_tags表的标签ID
|
||||
image_url: 上传后的相对路径
|
||||
image_thumb_url: 缩略图相对路径
|
||||
|
||||
Returns:
|
||||
是否更新成功
|
||||
"""
|
||||
try:
|
||||
connection = self.db_manager.get_connection()
|
||||
try:
|
||||
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
# 更新ai_images表
|
||||
update_images_sql = """
|
||||
UPDATE ai_images
|
||||
SET image_url = %s, image_thumb_url = %s
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(update_images_sql, (image_url, image_thumb_url, image_id))
|
||||
logger.info(f"已更新ai_images表,image_id: {image_id}, URL: {image_url}")
|
||||
|
||||
# 更新ai_image_tags表
|
||||
update_tags_sql = """
|
||||
UPDATE ai_image_tags
|
||||
SET image_url = %s, image_thumb_url = %s
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(update_tags_sql, (image_url, image_thumb_url, tag_image_id))
|
||||
logger.info(f"已更新ai_image_tags表,tag_image_id: {tag_image_id}, URL: {image_url}")
|
||||
|
||||
connection.commit()
|
||||
return True
|
||||
finally:
|
||||
connection.close()
|
||||
except Exception as e:
|
||||
logger.error(f"更新图片URL失败: {e}")
|
||||
return False
|
||||
|
||||
def login_and_get_jwt_token(self, base_url: str) -> Optional[str]:
|
||||
"""登录获取JWT token"""
|
||||
login_url = f"{base_url}/api/auth/login"
|
||||
@@ -695,14 +1130,74 @@ class ArticleImageMatcher:
|
||||
logger.error(f"登录异常: {e}")
|
||||
return None
|
||||
|
||||
def call_rpa_review_api(self, article_ids: List[int], image_source: int = 0) -> bool:
|
||||
"""
|
||||
调用RPA审核接口
|
||||
|
||||
Args:
|
||||
article_ids: 文章ID列表
|
||||
image_source: 图片来源类型 (11=模板图, 12=实拍图, 13=AI生成图)
|
||||
|
||||
Returns:
|
||||
是否调用成功
|
||||
"""
|
||||
try:
|
||||
base_url = "http://47.99.184.230:8324" # API基础URL
|
||||
jwt_token = self.login_and_get_jwt_token(base_url)
|
||||
|
||||
if not jwt_token:
|
||||
logger.error("获取JWT token失败,无法调用RPA审核接口")
|
||||
return False
|
||||
|
||||
# 准备请求数据
|
||||
api_url = f"{base_url}/api/articles/rpa/review"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {jwt_token}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
payload = {
|
||||
"article_ids": article_ids,
|
||||
"image_source": image_source
|
||||
}
|
||||
|
||||
logger.info(f"调用RPA审核接口: {api_url}")
|
||||
logger.info(f"请求参数: article_ids={article_ids}, image_source={image_source}")
|
||||
|
||||
response = requests.post(api_url, json=payload, headers=headers, timeout=30)
|
||||
|
||||
logger.info(f"RPA审核接口响应状态码: {response.status_code}")
|
||||
logger.info(f"RPA审核接口响应内容: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get('code') == 200:
|
||||
logger.info(f"RPA审核接口调用成功: {result.get('message', '操作完成')}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"RPA审核接口返回错误: {result.get('message', '未知错误')}")
|
||||
return False
|
||||
else:
|
||||
logger.error(f"RPA审核接口调用失败,状态码: {response.status_code}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("RPA审核接口调用超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"调用RPA审核接口异常: {e}")
|
||||
return False
|
||||
|
||||
def insert_article_image_relation_for_generated(self, article_id: int, image_id: int, image_url: str,
|
||||
image_thumb_url: str, tag_image_id: int, keywords_id: int,
|
||||
keywords_name: str, department_id: int, department_name: str,
|
||||
image_source: int = 0) -> Optional[int]:
|
||||
"""
|
||||
将文章与生成图片的关联信息插入ai_article_images表
|
||||
将文章与生成图片的关联信息处理(不调用RPA接口)
|
||||
注意:根据新要求,只插入关联信息,等所有图片生成完成后统一调用RPA接口
|
||||
"""
|
||||
try:
|
||||
# 1. 首先插入ai_article_images表(保持原有逻辑)
|
||||
connection = self.db_manager.get_connection()
|
||||
try:
|
||||
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
@@ -726,36 +1221,25 @@ class ArticleImageMatcher:
|
||||
"""
|
||||
cursor.execute(insert_query, (
|
||||
article_id, image_id, image_url, image_thumb_url, tag_image_id, new_sort_order,
|
||||
keywords_id, keywords_name, department_id, department_name, image_source
|
||||
keywords_id, keywords_name, department_id, department_name, image_source # 使用传入的image_source值
|
||||
))
|
||||
|
||||
article_image_id = cursor.lastrowid
|
||||
logger.info(f"文章图片关联信息已插入ai_article_images表,id: {article_image_id}")
|
||||
|
||||
# 更新图片附加文章计数
|
||||
update_count_sql = """
|
||||
UPDATE ai_image_tags
|
||||
SET image_attached_article_count = image_attached_article_count + 1
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(update_count_sql, (tag_image_id,))
|
||||
|
||||
# 更新图片状态为published
|
||||
update_image_status_sql = """
|
||||
UPDATE ai_images
|
||||
SET status = 'published'
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(update_image_status_sql, (image_id,))
|
||||
|
||||
# 更新文章状态为published_review
|
||||
self.update_article_status(article_id, 'published_review')
|
||||
logger.info(f"文章图片关联信息已插入ai_article_images表,id: {article_image_id}, sort_order: {new_sort_order}, image_source: {image_source}")
|
||||
|
||||
# 提交插入操作
|
||||
connection.commit()
|
||||
return article_image_id
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
# 注意:不再在这里调用RPA接口,而是在所有图片都生成完成后统一调用
|
||||
|
||||
# 记录操作日志到ai_logs表
|
||||
self.log_to_database('INFO', f"AI生成图片关联完成", f"文章ID: {article_id}, 图片ID: {image_id}, 关联记录ID: {article_image_id}")
|
||||
|
||||
return article_image_id
|
||||
except Exception as e:
|
||||
logger.error(f"插入文章图片关联信息失败: {e}")
|
||||
logger.error(f"处理文章图片关联信息失败: {e}")
|
||||
return None
|
||||
|
||||
def match_article_with_images(self, article_data: Dict) -> bool:
|
||||
@@ -792,8 +1276,7 @@ class ArticleImageMatcher:
|
||||
available_images = self.get_available_images_with_tags(article_department_id)
|
||||
|
||||
if not available_images:
|
||||
logger.warning(f"文章 {article_id} 没有找到对应科室的可用图片,跳过")
|
||||
return False
|
||||
logger.info(f"文章 {article_id} 没有找到对应科室的可用图片,将进行Gemini生图")
|
||||
|
||||
# 根据图片类型(实拍图/模板图)进行分类处理
|
||||
# 根据image_source字段:1=clean_images(模板图), 2=Flower_character(实拍图)
|
||||
@@ -846,22 +1329,85 @@ class ArticleImageMatcher:
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
# 未找到合适的匹配图片,使用Gemini生成图片
|
||||
logger.info(f"文章 {article_id} 未找到合适的匹配图片,调用Gemini生成图片")
|
||||
self.log_to_database('WARNING', f"文章未找到匹配图片,尝试生成图片", f"文章ID: {article_id}")
|
||||
# 未找到合适的匹配图片,根据当前图片数量采用不同策略生成图片
|
||||
current_image_count = self.get_article_image_count(article_id)
|
||||
logger.info(f"文章 {article_id} 当前已有 {current_image_count} 张图片,采用相应生成策略")
|
||||
|
||||
# 构建生成提示词
|
||||
prompt = f"与'{article_title}'相关的插图,标签: {', '.join(article_tags)}"
|
||||
generated_image_url = self.generate_image_with_gemini(prompt, article_tags, article_id)
|
||||
|
||||
if generated_image_url:
|
||||
logger.info(f"文章 {article_id} 成功生成图片: {generated_image_url}")
|
||||
self.log_to_database('INFO', f"文章生成图片成功",
|
||||
f"文章ID: {article_id}, 图片URL: {generated_image_url}")
|
||||
return True
|
||||
images_to_generate = []
|
||||
if current_image_count == 0:
|
||||
# 0张图:生成1张封面图(实拍图) + 2张详情图(AI生成图)
|
||||
images_to_generate = ['封面图', '详情图', '详情图']
|
||||
logger.info(f"文章 {article_id} 无图片,将生成1张封面图(image_source=12)和2张详情图(image_source=13)")
|
||||
elif current_image_count == 1:
|
||||
# 1张图:根据现有图片类型决定生成策略
|
||||
# 查询现有图片的image_source
|
||||
existing_image_sources = self.get_article_image_sources(article_id)
|
||||
if 12 not in existing_image_sources:
|
||||
# 缺少实拍图,生成1张封面图
|
||||
images_to_generate = ['封面图']
|
||||
logger.info(f"文章 {article_id} 缺少实拍图,将生成1张封面图(image_source=12)")
|
||||
elif existing_image_sources.count(13) < 2:
|
||||
# 缺少AI生成图,生成详情图补充到2张
|
||||
need_count = 2 - existing_image_sources.count(13)
|
||||
images_to_generate = ['详情图'] * need_count
|
||||
logger.info(f"文章 {article_id} 缺少AI生成图,将生成{need_count}张详情图(image_source=13)")
|
||||
else:
|
||||
logger.info(f"文章 {article_id} 已满足图片要求,无需生成更多图片")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"文章 {article_id} 生成图片失败")
|
||||
self.log_to_database('ERROR', f"文章生成图片失败", f"文章ID: {article_id}")
|
||||
# 2张或以上:检查是否满足要求
|
||||
existing_image_sources = self.get_article_image_sources(article_id)
|
||||
need_cover = 12 not in existing_image_sources
|
||||
need_template = existing_image_sources.count(13) < 2
|
||||
|
||||
if need_cover or need_template:
|
||||
if need_cover:
|
||||
images_to_generate.append('封面图')
|
||||
if need_template:
|
||||
need_count = 2 - existing_image_sources.count(13)
|
||||
images_to_generate.extend(['详情图'] * need_count)
|
||||
logger.info(f"文章 {article_id} 需要补充图片: 实拍图={need_cover}, AI生成图={need_template}, 将生成{len(images_to_generate)}张图片")
|
||||
else:
|
||||
logger.info(f"文章 {article_id} 已满足图片要求,无需生成更多图片")
|
||||
return True
|
||||
|
||||
# 生成相应数量和类型的图片
|
||||
generated_count = 0
|
||||
for image_type in images_to_generate:
|
||||
# 构建针对不同类型图片的生成提示词
|
||||
if image_type == '封面图':
|
||||
prompt = f"为文章'{article_title}'生成封面图,要求:主题突出、视觉冲击力强、适合首页展示,标签: {', '.join(article_tags)}"
|
||||
elif image_type == '详情图':
|
||||
prompt = f"为文章'{article_title}'生成详情说明图,要求:内容相关、清晰易懂、辅助理解文章内容,标签: {', '.join(article_tags)}"
|
||||
else: # 海报图
|
||||
prompt = f"为文章'{article_title}'生成宣传海报图,要求:吸引眼球、信息明确、适合推广传播,标签: {', '.join(article_tags)}"
|
||||
|
||||
generated_image_url = self.generate_image_with_gemini(prompt, article_tags, article_id, image_type)
|
||||
|
||||
if generated_image_url:
|
||||
generated_count += 1
|
||||
logger.info(f"文章 {article_id} 成功生成{image_type}: {generated_image_url}")
|
||||
self.log_to_database('INFO', f"文章生成{image_type}成功",
|
||||
f"文章ID: {article_id}, 图片URL: {generated_image_url}, 类型: {image_type}")
|
||||
else:
|
||||
logger.error(f"文章 {article_id} 生成{image_type}失败")
|
||||
self.log_to_database('ERROR', f"文章生成{image_type}失败", f"文章ID: {article_id}, 类型: {image_type}")
|
||||
|
||||
# 检查是否所有图片都生成成功
|
||||
if generated_count == len(images_to_generate):
|
||||
logger.info(f"文章 {article_id} 共成功生成 {generated_count} 张图片,所有图片都已生成,现在调用RPA接口")
|
||||
# 所有图片都生成成功后,才调用RPA接口
|
||||
if self.call_rpa_review_api([article_id]):
|
||||
logger.info(f"文章 {article_id} RPA审核接口调用成功")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"文章 {article_id} RPA审核接口调用失败")
|
||||
return False
|
||||
elif generated_count > 0:
|
||||
logger.warning(f"文章 {article_id} 只成功生成 {generated_count}/{len(images_to_generate)} 张图片,未达到要求,不调用RPA接口")
|
||||
return False
|
||||
else:
|
||||
logger.error(f"文章 {article_id} 生成图片全部失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user