202 lines
7.5 KiB
Python
202 lines
7.5 KiB
Python
"""
|
||
图片处理工具模块 - 最终版 v4
|
||
核心思路(最简单直接):
|
||
1. 找到所有 .jpg/.png 文件名及其周围的 
|
||
"""
|
||
import re
|
||
from typing import List
|
||
from difflib import SequenceMatcher
|
||
|
||
|
||
def extract_image_paths(text: str) -> List[str]:
|
||
"""从文本中提取图片路径"""
|
||
pattern = r'images/[a-zA-Z0-9_\-./]+\.(?:jpg|jpeg|png|gif|bmp|webp)'
|
||
matches = re.findall(pattern, text, re.IGNORECASE)
|
||
return list(set(matches))
|
||
|
||
|
||
def find_closest_image_path(generated_path: str, original_paths: List[str]) -> str:
|
||
"""
|
||
找到与生成路径最接近的原始图片路径
|
||
|
||
即使模型输出的是错误/近似/截断的文件名,也能匹配到正确的原始路径
|
||
"""
|
||
if not original_paths:
|
||
return generated_path
|
||
|
||
generated_filename = generated_path.split('/')[-1].lower()
|
||
best_match = None
|
||
best_score = 0
|
||
|
||
for orig_path in original_paths:
|
||
orig_filename = orig_path.split('/')[-1].lower()
|
||
score = SequenceMatcher(None, generated_filename, orig_filename).ratio()
|
||
if score > best_score:
|
||
best_score = score
|
||
best_match = orig_path
|
||
|
||
if best_match and best_score > 0.5:
|
||
return best_match
|
||
return generated_path
|
||
|
||
|
||
def normalize_markdown_images(text: str, base_prefix: str = "/api/v1/knowledge/files/", original_paths: List[str] = None) -> str:
|
||
"""
|
||
规范化 Markdown 图片链接 - 两轮处理版
|
||
|
||
算法:
|
||
第一轮:检查和修改格式,确保是  格式
|
||
第二轮:只检查和替换最后文件名部分(xxx.jpg),与原始路径匹配,匹配度低的删除
|
||
"""
|
||
if not text or original_paths is None:
|
||
return text
|
||
|
||
# ========== 第一轮:检查和修改格式(包括固定URL前缀) ==========
|
||
result = _normalize_image_format(text, base_prefix)
|
||
|
||
# ========== 第二轮:只检查和替换最后文件名部分 ==========
|
||
result = _normalize_image_paths(result, base_prefix, original_paths)
|
||
|
||
return result
|
||
|
||
|
||
def _normalize_image_format(text: str, base_prefix: str) -> str:
|
||
"""
|
||
第一轮:规范化图片格式,确保所有图片都是正确的  格式
|
||
|
||
处理各种格式破损的情况,并确保有正确的URL前缀
|
||
"""
|
||
if not text:
|
||
return text
|
||
|
||
IMG_EXTENSIONS = r'(?:jpg|jpeg|png|gif|bmp|webp)'
|
||
img_pattern = rf'([a-zA-Z0-9_\-./]+)\.({IMG_EXTENSIONS})'
|
||
|
||
result = text
|
||
matches = list(re.finditer(img_pattern, result, re.IGNORECASE))
|
||
|
||
if not matches:
|
||
return result
|
||
|
||
# 从后往前处理,避免位置偏移
|
||
for match in reversed(matches):
|
||
filename_with_ext = match.group(0)
|
||
file_start = match.start()
|
||
file_end = match.end()
|
||
|
||
mark_start = file_start
|
||
mark_end = file_end
|
||
|
||
# 向前查找是否已经有 
|
||
before = result[lookback_limit:file_start]
|
||
open_marker_pos = before.rfind('
|
||
|
||
if open_marker_pos != -1:
|
||
actual_open_pos = lookback_limit + open_marker_pos
|
||
check_segment = result[actual_open_pos:file_start]
|
||
if check_segment.startswith(':
|
||
mark_start = actual_open_pos
|
||
|
||
# 向后查找是否有 )
|
||
after = result[file_end:]
|
||
close_paren_pos = after.find(')')
|
||
if close_paren_pos != -1 and close_paren_pos < 20:
|
||
middle = after[:close_paren_pos]
|
||
if '\n' not in middle and '
|
||
pure_filename = filename_with_ext.split('/')[-1]
|
||
|
||
# 构建完整的标准格式(包括固定URL前缀)
|
||
full_url = base_prefix.rstrip('/') + '/images/' + pure_filename
|
||
standard_format = f''
|
||
|
||
result = result[:mark_start] + standard_format + result[mark_end:]
|
||
|
||
# 清理嵌套格式
|
||
nested_pattern = r'!\[图片\]\([^)]*!\[图片\]\(([^)]+\.(?:jpg|jpeg|png|gif|bmp|webp))\)[^)]*\)'
|
||
while re.search(nested_pattern, result, re.IGNORECASE):
|
||
result = re.sub(nested_pattern, r'', result, flags=re.IGNORECASE)
|
||
|
||
return result
|
||
|
||
|
||
def _normalize_image_paths(text: str, base_prefix: str, original_paths: List[str]) -> str:
|
||
"""
|
||
第二轮:只规范化最后文件名部分(xxx.jpg),与原始路径匹配,匹配度低的删除
|
||
|
||
步骤:
|
||
1. 找到所有  格式的图片
|
||
2. 对每个图片:
|
||
a. 提取最后文件名部分(xxx.jpg)
|
||
b. 只与 original_paths 中的最后文件名部分匹配
|
||
c. 如果匹配度高(>0.5),替换为正确路径
|
||
d. 如果匹配度低(<=0.5),删除整个图片
|
||
"""
|
||
if not text or not original_paths:
|
||
return text
|
||
|
||
# 匹配所有  格式
|
||
img_tag_pattern = r'!\[图片\]\(([^)]+)\)'
|
||
matches = list(re.finditer(img_tag_pattern, text, re.IGNORECASE))
|
||
|
||
if not matches:
|
||
return text
|
||
|
||
result = text
|
||
|
||
# 从后往前处理,避免位置偏移
|
||
for match in reversed(matches):
|
||
full_tag = match.group(0)
|
||
url = match.group(1)
|
||
tag_start = match.start()
|
||
tag_end = match.end()
|
||
|
||
# 从 URL 中只提取最后的文件名部分(xxx.jpg)
|
||
filename = url.split('/')[-1]
|
||
|
||
# 尝试匹配 - 只匹配最后文件名部分
|
||
best_match = None
|
||
best_score = 0
|
||
|
||
generated_filename = filename.lower()
|
||
|
||
for orig_path in original_paths:
|
||
orig_filename = orig_path.split('/')[-1].lower()
|
||
score = SequenceMatcher(None, generated_filename, orig_filename).ratio()
|
||
if score > best_score:
|
||
best_score = score
|
||
best_match = orig_path
|
||
|
||
if best_match and best_score > 0.5:
|
||
# 匹配成功,替换为正确路径(保持固定URL前缀)
|
||
# 只替换最后文件名部分,保持前缀不变
|
||
pure_orig_filename = best_match.split('/')[-1]
|
||
full_url = base_prefix.rstrip('/') + '/images/' + pure_orig_filename
|
||
|
||
standard_format = f''
|
||
result = result[:tag_start] + standard_format + result[tag_end:]
|
||
else:
|
||
# 匹配度低,删除整个图片
|
||
result = result[:tag_start] + result[tag_end:]
|
||
|
||
return result
|
||
|
||
|
||
def get_image_prompt_guidance() -> str:
|
||
"""获取图片格式处理的提示词指导"""
|
||
return """【图片格式特别要求】
|
||
- 检索结果中的图片链接格式通常为:``
|
||
- 输出时必须严格保留此格式,不得修改、省略或截断
|
||
- 图片的 alt 文本必须保持为「图片」
|
||
- 图片 URL 必须完整包含 `/api/v1/knowledge/files/images` 前缀
|
||
- 不要将图片链接转换为纯文本描述
|
||
- 如果有多个图片,在对应位置分别引用,保持原始顺序
|
||
- 严禁修改图片文件名或路径结构
|
||
- 如果需要展示图片,直接使用原始的 `` 格式即可"""
|
||
|