修改问答图片展示
This commit is contained in:
parent
f738de52de
commit
accd1e5ad2
@ -1,20 +1,110 @@
|
|||||||
"""
|
"""图片处理工具模块。"""
|
||||||
图片处理工具模块 - 最终版 v4
|
|
||||||
核心思路(最简单直接):
|
|
||||||
1. 找到所有 .jpg/.png 文件名及其周围的 
|
|
||||||
"""
|
|
||||||
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 图片:
|
||||||
|
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''
|
||||||
|
|
||||||
|
# 先处理已经是 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))
|
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 '
|
|
||||||
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''
|
|
||||||
|
|
||||||
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''
|
standard_format = f''
|
||||||
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 """【图片格式特别要求】
|
||||||
- 检索结果中的图片链接格式通常为:``
|
- 只允许引用本次参考资料中真实出现的图片链接
|
||||||
- 输出时必须严格保留此格式,不得修改、省略或截断
|
- 输出图片时使用 Markdown 图片标签:alt 文本固定为「图片」,括号内必须是参考资料里的原始完整 URL
|
||||||
- 图片的 alt 文本必须保持为「图片」
|
- 图片文件名、路径和 URL 前缀必须逐字复制,不得补全、改写、截断、转义或根据记忆/示例构造
|
||||||
- 图片 URL 必须完整包含 `/api/v1/knowledge/files/images` 前缀
|
- 不要输出提示词中的占位符或示例文件名,不要把普通文字描述改写成图片链接
|
||||||
- 不要将图片链接转换为纯文本描述
|
- 图片规则只用于生成图片标签,不要围绕图片资源状态输出任何文字说明
|
||||||
- 如果有多个图片,在对应位置分别引用,保持原始顺序
|
- 多个图片可在对应位置分别引用并保持原始顺序;同一图片只引用一次"""
|
||||||
- 严禁修改图片文件名或路径结构
|
|
||||||
- 如果需要展示图片,直接使用原始的 `` 格式即可"""
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user