修改问答图片展示

This commit is contained in:
ChenRui 2026-07-24 16:51:26 +08:00
parent f738de52de
commit accd1e5ad2

View File

@ -1,20 +1,110 @@
""" """图片处理工具模块。"""
图片处理工具模块 - 最终版 v4
核心思路最简单直接
1. 找到所有 .jpg/.png 文件名及其周围的 ![图片]( 上下文
2. 不管周围格式如何也不管文件名是否被模型改过
3. 统一提取文件名 -> original_paths 模匹 -> 替换成正确的 ![图片](完整URL)
"""
import re import re
from typing import List from typing import List, Tuple
from difflib import SequenceMatcher from difflib import SequenceMatcher
IMG_EXTENSIONS = r'(?:jpg|jpeg|png|gif|bmp|webp)'
def _dedupe_keep_order(values: List[str]) -> List[str]:
seen = set()
result = []
for value in values:
cleaned = (value or "").strip().strip("`'\",。;;")
if not cleaned:
continue
key = cleaned.lower()
if key in seen:
continue
seen.add(key)
result.append(cleaned)
return result
def extract_image_paths(text: str) -> List[str]: def extract_image_paths(text: str) -> List[str]:
"""从文本中提取图片路径""" """从文本中提取图片路径或 URL并保持原始出现顺序。"""
pattern = r'images/[a-zA-Z0-9_\-./]+\.(?:jpg|jpeg|png|gif|bmp|webp)' if not text:
matches = re.findall(pattern, text, re.IGNORECASE) return []
return list(set(matches))
patterns = [
# Markdown 图片:![alt](url)
rf'!\[[^\]\n]*\]\(\s*([^)\s]+?\.{IMG_EXTENSIONS}(?:[?#][^)\s]*)?)\s*\)',
# 完整 URL。
rf'(https?://[^\s<>"\'\]\)]+?\.{IMG_EXTENSIONS}(?:[?#][^\s<>"\'\]\)]*)?)',
# API 绝对路径。
rf'(?<![\w:/.%-])(/api/v1/knowledge/files/images/[^\s<>"\'\]\)]+?\.{IMG_EXTENSIONS}(?:[?#][^\s<>"\'\]\)]*)?)',
# 相对 images 路径。
rf'(?<![\w/.-])(images/[A-Za-z0-9_\-./%]+?\.{IMG_EXTENSIONS}(?:[?#][^\s<>"\'\]\)]*)?)',
# 裸文件名,兼容检索结果只给文件名的情况。
rf'(?<![\w/.-])([A-Za-z0-9_-]{{8,}}\.{IMG_EXTENSIONS})(?![\w/.-])',
]
matches: List[Tuple[int, str]] = []
for pattern in patterns:
matches.extend((m.start(1), m.group(1)) for m in re.finditer(pattern, text, re.IGNORECASE))
matches.sort(key=lambda item: item[0])
return _dedupe_keep_order([value for _, value in matches])
def _strip_url_suffix(filename: str) -> str:
return re.sub(r'[?#].*$', '', filename or '').strip()
def _split_filename(filename: str) -> Tuple[str, str]:
filename = _strip_url_suffix(filename).lower()
if "." not in filename:
return filename, ""
stem, ext = filename.rsplit(".", 1)
return stem, ext
def _path_to_image_url(path: str, base_prefix: str) -> str:
cleaned = (path or "").strip()
if cleaned.startswith(("http://", "https://", "/")):
return cleaned
if cleaned.lower().startswith("images/"):
return base_prefix.rstrip("/") + "/" + cleaned
return base_prefix.rstrip("/") + "/images/" + cleaned.split("/")[-1]
def _is_acceptable_image_match(generated_filename: str, original_filename: str, score: float) -> bool:
"""只接受完全一致、明确截断或高度相似的文件名,避免把幻觉图片错配到真实图片。"""
generated_stem, generated_ext = _split_filename(generated_filename)
original_stem, original_ext = _split_filename(original_filename)
if not generated_stem or not original_stem or generated_ext != original_ext:
return False
if generated_stem == original_stem:
return True
if len(generated_stem) >= 12 and original_stem.startswith(generated_stem):
return True
if len(original_stem) >= 12 and generated_stem.startswith(original_stem):
return True
return score >= 0.85
def _remove_untrusted_image_references(text: str) -> str:
"""没有可信原图列表时,删除模型输出的图片标签和裸图片路径。"""
if not text:
return text
result = re.sub(
rf'!\[[^\]\n]*\]\([^\)\n]*\.{IMG_EXTENSIONS}(?:[?#][^\)\n]*)?\)',
'',
text,
flags=re.IGNORECASE,
)
result = re.sub(
rf'(?<![\w/.-])(?:https?://[^\s<>)]+/images/[^\s<>)]+|/api/v1/knowledge/files/images/[^\s<>)]+|images/[^\s<>)]+|[A-Za-z0-9_-]+\.{IMG_EXTENSIONS})(?:[?#][^\s<>)]+)?',
'',
result,
flags=re.IGNORECASE,
)
result = re.sub(r'[ \t]+\n', '\n', result)
result = re.sub(r'\n{3,}', '\n\n', result)
return result.strip()
def find_closest_image_path(generated_path: str, original_paths: List[str]) -> str: def find_closest_image_path(generated_path: str, original_paths: List[str]) -> str:
@ -37,7 +127,7 @@ def find_closest_image_path(generated_path: str, original_paths: List[str]) -> s
best_score = score best_score = score
best_match = orig_path best_match = orig_path
if best_match and best_score > 0.5: if best_match and _is_acceptable_image_match(generated_filename, best_match.split('/')[-1], best_score):
return best_match return best_match
return generated_path return generated_path
@ -52,12 +142,15 @@ def normalize_markdown_images(text: str, base_prefix: str = "/api/v1/knowledge/f
""" """
if not text or original_paths is None: if not text or original_paths is None:
return text return text
trusted_paths = [path for path in original_paths if path]
if not trusted_paths:
return _remove_untrusted_image_references(text)
# ========== 第一轮检查和修改格式包括固定URL前缀 ========== # ========== 第一轮检查和修改格式包括固定URL前缀 ==========
result = _normalize_image_format(text, base_prefix) result = _normalize_image_format(text, base_prefix)
# ========== 第二轮:只检查和替换最后文件名部分 ========== # ========== 第二轮:只检查和替换最后文件名部分 ==========
result = _normalize_image_paths(result, base_prefix, original_paths) result = _normalize_image_paths(result, base_prefix, trusted_paths)
return result return result
@ -71,10 +164,35 @@ def _normalize_image_format(text: str, base_prefix: str) -> str:
if not text: if not text:
return text return text
IMG_EXTENSIONS = r'(?:jpg|jpeg|png|gif|bmp|webp)'
img_pattern = rf'([a-zA-Z0-9_\-./]+)\.({IMG_EXTENSIONS})' img_pattern = rf'([a-zA-Z0-9_\-./]+)\.({IMG_EXTENSIONS})'
result = text result = text
def build_standard_tag(path: str) -> str:
full_url = _path_to_image_url(path, base_prefix)
return f'![图片]({full_url})'
# 先处理已经是 Markdown 图片语法的内容,包含任意 alt 文本。
# 例如:![图2-3 消磁电源柜功能框图](images/xxx.jpg)
markdown_img_pattern = rf'!\[[^\]\n]*\]\(([^)\n]*?\.{IMG_EXTENSIONS})\)'
result = re.sub(
markdown_img_pattern,
lambda m: build_standard_tag(m.group(1)),
result,
flags=re.IGNORECASE
)
# 再兜底清理模型或前序处理产生的嵌套图片语法。
# 例如:![图2-3](![图片](/api/v1/knowledge/files/images/xxx.jpg)
nested_any_alt_pattern = rf'!\[[^\]\n]*\]\([^)\n]*!\[[^\]\n]*\]\(([^)\n]+\.{IMG_EXTENSIONS})\)\)?'
while re.search(nested_any_alt_pattern, result, re.IGNORECASE):
result = re.sub(
nested_any_alt_pattern,
lambda m: build_standard_tag(m.group(1)),
result,
flags=re.IGNORECASE
)
matches = list(re.finditer(img_pattern, result, re.IGNORECASE)) matches = list(re.finditer(img_pattern, result, re.IGNORECASE))
if not matches: if not matches:
@ -108,12 +226,8 @@ def _normalize_image_format(text: str, base_prefix: str) -> str:
if '\n' not in middle and '![图片](' not in middle: if '\n' not in middle and '![图片](' not in middle:
mark_end = file_end + close_paren_pos + 1 mark_end = file_end + close_paren_pos + 1
# 提取纯文件名(去掉可能的路径前缀)
pure_filename = filename_with_ext.split('/')[-1]
# 构建完整的标准格式包括固定URL前缀 # 构建完整的标准格式包括固定URL前缀
full_url = base_prefix.rstrip('/') + '/images/' + pure_filename standard_format = build_standard_tag(filename_with_ext)
standard_format = f'![图片]({full_url})'
result = result[:mark_start] + standard_format + result[mark_end:] result = result[:mark_start] + standard_format + result[mark_end:]
@ -163,7 +277,7 @@ def _normalize_image_paths(text: str, base_prefix: str, original_paths: List[str
best_match = None best_match = None
best_score = 0 best_score = 0
generated_filename = filename.lower() generated_filename = _strip_url_suffix(filename).lower()
for orig_path in original_paths: for orig_path in original_paths:
orig_filename = orig_path.split('/')[-1].lower() orig_filename = orig_path.split('/')[-1].lower()
@ -172,11 +286,10 @@ def _normalize_image_paths(text: str, base_prefix: str, original_paths: List[str
best_score = score best_score = score
best_match = orig_path best_match = orig_path
if best_match and best_score > 0.5: if best_match and _is_acceptable_image_match(generated_filename, best_match.split('/')[-1], best_score):
# 匹配成功替换为正确路径保持固定URL前缀 # 匹配成功替换为正确路径保持固定URL前缀
# 只替换最后文件名部分,保持前缀不变 # 只替换最后文件名部分,保持前缀不变
pure_orig_filename = best_match.split('/')[-1] full_url = _path_to_image_url(best_match, base_prefix)
full_url = base_prefix.rstrip('/') + '/images/' + pure_orig_filename
standard_format = f'![图片]({full_url})' standard_format = f'![图片]({full_url})'
result = result[:tag_start] + standard_format + result[tag_end:] result = result[:tag_start] + standard_format + result[tag_end:]
@ -190,12 +303,10 @@ def _normalize_image_paths(text: str, base_prefix: str, original_paths: List[str
def get_image_prompt_guidance() -> str: def get_image_prompt_guidance() -> str:
"""获取图片格式处理的提示词指导""" """获取图片格式处理的提示词指导"""
return """【图片格式特别要求】 return """【图片格式特别要求】
- 检索结果中的图片链接格式通常为`![图片](/api/v1/knowledge/files/images/xxx.jpg)` - 只允许引用本次参考资料中真实出现的图片链接
- 输出时必须严格保留此格式不得修改省略或截断 - 输出图片时使用 Markdown 图片标签alt 文本固定为图片括号内必须是参考资料里的原始完整 URL
- 图片的 alt 文本必须保持为图片 - 图片文件名路径和 URL 前缀必须逐字复制不得补全改写截断转义或根据记忆/示例构造
- 图片 URL 必须完整包含 `/api/v1/knowledge/files/images` 前缀 - 不要输出提示词中的占位符或示例文件名不要把普通文字描述改写成图片链接
- 不要将图片链接转换为纯文本描述 - 图片规则只用于生成图片标签不要围绕图片资源状态输出任何文字说明
- 如果有多个图片在对应位置分别引用保持原始顺序 - 多个图片可在对应位置分别引用并保持原始顺序同一图片只引用一次"""
- 严禁修改图片文件名或路径结构
- 如果需要展示图片直接使用原始的 `![图片](URL)` 格式即可"""