修改问答图片展示
This commit is contained in:
parent
f738de52de
commit
accd1e5ad2
@ -1,20 +1,110 @@
|
||||
"""
|
||||
图片处理工具模块 - 最终版 v4
|
||||
核心思路(最简单直接):
|
||||
1. 找到所有 .jpg/.png 文件名及其周围的 
|
||||
"""
|
||||
"""图片处理工具模块。"""
|
||||
import re
|
||||
from typing import List
|
||||
from typing import List, Tuple
|
||||
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]:
|
||||
"""从文本中提取图片路径"""
|
||||
pattern = r'images/[a-zA-Z0-9_\-./]+\.(?:jpg|jpeg|png|gif|bmp|webp)'
|
||||
matches = re.findall(pattern, text, re.IGNORECASE)
|
||||
return list(set(matches))
|
||||
"""从文本中提取图片路径或 URL,并保持原始出现顺序。"""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
patterns = [
|
||||
# Markdown 图片:
|
||||
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:
|
||||
@ -37,7 +127,7 @@ def find_closest_image_path(generated_path: str, original_paths: List[str]) -> s
|
||||
best_score = score
|
||||
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 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:
|
||||
return text
|
||||
trusted_paths = [path for path in original_paths if path]
|
||||
if not trusted_paths:
|
||||
return _remove_untrusted_image_references(text)
|
||||
|
||||
# ========== 第一轮:检查和修改格式(包括固定URL前缀) ==========
|
||||
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
|
||||
|
||||
@ -71,10 +164,35 @@ def _normalize_image_format(text: str, base_prefix: str) -> str:
|
||||
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
|
||||
|
||||
def build_standard_tag(path: str) -> str:
|
||||
full_url = _path_to_image_url(path, base_prefix)
|
||||
return f''
|
||||
|
||||
# 先处理已经是 Markdown 图片语法的内容,包含任意 alt 文本。
|
||||
# 例如:
|
||||
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
|
||||
)
|
||||
|
||||
# 再兜底清理模型或前序处理产生的嵌套图片语法。
|
||||
# 例如:
|
||||
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))
|
||||
|
||||
if not matches:
|
||||
@ -108,12 +226,8 @@ def _normalize_image_format(text: str, base_prefix: str) -> str:
|
||||
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''
|
||||
standard_format = build_standard_tag(filename_with_ext)
|
||||
|
||||
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_score = 0
|
||||
|
||||
generated_filename = filename.lower()
|
||||
generated_filename = _strip_url_suffix(filename).lower()
|
||||
|
||||
for orig_path in original_paths:
|
||||
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_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前缀)
|
||||
# 只替换最后文件名部分,保持前缀不变
|
||||
pure_orig_filename = best_match.split('/')[-1]
|
||||
full_url = base_prefix.rstrip('/') + '/images/' + pure_orig_filename
|
||||
full_url = _path_to_image_url(best_match, base_prefix)
|
||||
|
||||
standard_format = f''
|
||||
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:
|
||||
"""获取图片格式处理的提示词指导"""
|
||||
return """【图片格式特别要求】
|
||||
- 检索结果中的图片链接格式通常为:``
|
||||
- 输出时必须严格保留此格式,不得修改、省略或截断
|
||||
- 图片的 alt 文本必须保持为「图片」
|
||||
- 图片 URL 必须完整包含 `/api/v1/knowledge/files/images` 前缀
|
||||
- 不要将图片链接转换为纯文本描述
|
||||
- 如果有多个图片,在对应位置分别引用,保持原始顺序
|
||||
- 严禁修改图片文件名或路径结构
|
||||
- 如果需要展示图片,直接使用原始的 `` 格式即可"""
|
||||
- 只允许引用本次参考资料中真实出现的图片链接
|
||||
- 输出图片时使用 Markdown 图片标签:alt 文本固定为「图片」,括号内必须是参考资料里的原始完整 URL
|
||||
- 图片文件名、路径和 URL 前缀必须逐字复制,不得补全、改写、截断、转义或根据记忆/示例构造
|
||||
- 不要输出提示词中的占位符或示例文件名,不要把普通文字描述改写成图片链接
|
||||
- 图片规则只用于生成图片标签,不要围绕图片资源状态输出任何文字说明
|
||||
- 多个图片可在对应位置分别引用并保持原始顺序;同一图片只引用一次"""
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user