增加图片文本化接口、切片图片文本抽取‘
This commit is contained in:
parent
fdb6642b8b
commit
6294652de8
9700
122-06A0014-B01001_发动机-维修手册_content_list.json
Normal file
9700
122-06A0014-B01001_发动机-维修手册_content_list.json
Normal file
File diff suppressed because one or more lines are too long
BIN
__pycache__/app copy.cpython-311.pyc
Normal file
BIN
__pycache__/app copy.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/app.cpython-311.pyc
Normal file
BIN
__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/app_v2.cpython-311.pyc
Normal file
BIN
__pycache__/app_v2.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/checkpointer_config.cpython-311.pyc
Normal file
BIN
__pycache__/checkpointer_config.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/chunk_text.cpython-311.pyc
Normal file
BIN
__pycache__/chunk_text.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/config.cpython-311.pyc
Normal file
BIN
__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/doc2pdf.cpython-311.pyc
Normal file
BIN
__pycache__/doc2pdf.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
__pycache__/filename_proceess_and_kgquery.cpython-311.pyc
Normal file
BIN
__pycache__/filename_proceess_and_kgquery.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/fileparse_util.cpython-311.pyc
Normal file
BIN
__pycache__/fileparse_util.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/main_agent.cpython-311.pyc
Normal file
BIN
__pycache__/main_agent.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18
apitest.py
Normal file
18
apitest.py
Normal file
@ -0,0 +1,18 @@
|
||||
import base64
|
||||
import requests
|
||||
|
||||
url = "http://192.168.0.46:59085/split_image"
|
||||
image_path = r"E:\ZKYNLP\aidocproject\audiotransaction\gwdoc\船舶图片.png"
|
||||
|
||||
with open(image_path, "rb") as f:
|
||||
image_base64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
payload = {
|
||||
"content": "船舶主发动机为低速二冲程船用推进柴油机,是船舶推进系统的核心动力装置,其主要功能是将燃油化学能转化为机械能,通过曲轴输出转矩,驱动推进轴系和螺旋桨,实现船舶航行。",
|
||||
"filename": "船舶图片.png",
|
||||
"image_base64": image_base64,
|
||||
}
|
||||
|
||||
resp = requests.post(url, json=payload, timeout=120)
|
||||
print(resp.status_code)
|
||||
print(resp.json())
|
||||
112
app.py
112
app.py
@ -15,7 +15,7 @@ try:
|
||||
except ImportError:
|
||||
from langchain.prompts import FewShotPromptTemplate, PromptTemplate
|
||||
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam, ChatCompletionMessageParam
|
||||
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file
|
||||
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file,safe_original_filename,image_base64_to_data_url,extract_image_text,prepare_split_image
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
import requests
|
||||
from pathlib import PurePath, Path as PathLib
|
||||
@ -63,14 +63,20 @@ from doc2pdf import Doc2PDF
|
||||
from workflow_registry import WORKFLOW_CONFIG, VALID_ROUTE_FLAGS
|
||||
from main_agent import extract_conversation_title
|
||||
from workflows.history_manager import filter_image_urls, preprocess_from_request
|
||||
from config import set_request_user_config
|
||||
from config import VLM_CONCURRENCY, set_request_user_config
|
||||
from checkpointer_config import (
|
||||
CheckpointerManager,
|
||||
checkpointer_manager
|
||||
)
|
||||
app = FastAPI(max_request_size=1024 * 1024 * 10)
|
||||
|
||||
converter = Doc2PDF()
|
||||
# =============== PDF 文件目录配置 ===============
|
||||
# 创建 PDF 文件目录(用于存储生成的 PDF 等文件)
|
||||
PDF_DIR = os.path.join(os.path.dirname(__file__), "created_pdf")
|
||||
# 创建 word 文件目录(用于存储生成的 word 等文件)
|
||||
WORD_DIR = os.path.join(os.path.dirname(__file__), "created_word")
|
||||
os.makedirs(PDF_DIR, exist_ok=True)
|
||||
VLM_SEMAPHORE = asyncio.Semaphore(max(1, VLM_CONCURRENCY))
|
||||
converter = None
|
||||
DATA_DIR = "/app/files"
|
||||
load_dotenv()
|
||||
|
||||
@ -93,6 +99,13 @@ STREAM_RESPONSE_HEADERS = {
|
||||
}
|
||||
|
||||
|
||||
def get_doc_converter() -> Doc2PDF:
|
||||
global converter
|
||||
if converter is None:
|
||||
converter = Doc2PDF()
|
||||
return converter
|
||||
|
||||
|
||||
def llm_text_stream_response(content) -> StreamingResponse:
|
||||
return StreamingResponse(
|
||||
content,
|
||||
@ -104,20 +117,16 @@ def llm_text_stream_response(content) -> StreamingResponse:
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""应用启动时初始化"""
|
||||
await checkpointer_manager.setup()
|
||||
# 初始化智能体使用统计表
|
||||
from tools.agent_usage_statistics import init_agent_usage_table
|
||||
await init_agent_usage_table()
|
||||
logger.info("应用启动完成,PostgreSQL Checkpointer 已配置")
|
||||
try:
|
||||
await checkpointer_manager.setup()
|
||||
# 初始化智能体使用统计表
|
||||
from tools.agent_usage_statistics import init_agent_usage_table
|
||||
await init_agent_usage_table()
|
||||
logger.info("应用启动完成,PostgreSQL Checkpointer 已配置")
|
||||
except Exception as e:
|
||||
logger.warning(f"Checkpointer 初始化失败,非 Agent 接口仍可使用: {e}")
|
||||
|
||||
|
||||
# =============== PDF 文件目录配置 ===============
|
||||
# 创建 PDF 文件目录(用于存储生成的 PDF 等文件)
|
||||
PDF_DIR = os.path.join(os.path.dirname(__file__), "created_pdf")
|
||||
# 创建 word 文件目录(用于存储生成的 word 等文件)
|
||||
WORD_DIR = os.path.join(os.path.dirname(__file__), "created_word")
|
||||
os.makedirs(PDF_DIR, exist_ok=True)
|
||||
|
||||
class RewriteRequest(BaseModel):
|
||||
rewrite_type: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
@ -1509,6 +1518,75 @@ class RequestWrapper(BaseModel):
|
||||
pdf_contents: List[PdfContentItem]
|
||||
|
||||
|
||||
class Base64ImageRequest(BaseModel):
|
||||
content: Optional[str] = Field(
|
||||
None,
|
||||
description="Optional text around the image, for example: 图3.1 船舶主发动机组成图(images/test.jpg)",
|
||||
)
|
||||
filename: str = Field(..., description="Original image filename, for example test.png")
|
||||
image_base64: str = Field(..., description="Base64 image content, with or without data:image/... prefix")
|
||||
|
||||
|
||||
async def analyze_image_with_vlm(image_bytes: bytes, suffix: str) -> str:
|
||||
image_url = image_base64_to_data_url(image_bytes, suffix)
|
||||
async with VLM_SEMAPHORE:
|
||||
return await model_api.OpenaiAPI.open_api_vl_without_thinking(image_url)
|
||||
|
||||
|
||||
@app.post("/split_image")
|
||||
async def image_base64(request_data: Base64ImageRequest):
|
||||
filename = safe_original_filename(request_data.filename)
|
||||
suffix = PathLib(filename).suffix.lower()
|
||||
if suffix not in {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".gif", ".tif", ".tiff"}:
|
||||
raise HTTPException(status_code=400, detail="Unsupported image type")
|
||||
|
||||
image_bytes, processed_suffix = prepare_split_image(request_data.image_base64, suffix)
|
||||
if not image_bytes:
|
||||
raise HTTPException(status_code=400, detail="decoded image is empty")
|
||||
|
||||
temp_path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=processed_suffix) as tmp:
|
||||
tmp.write(image_bytes)
|
||||
temp_path = PathLib(tmp.name)
|
||||
|
||||
ocr_data = await extract_image_text(temp_path)
|
||||
try:
|
||||
temp_path.unlink()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
|
||||
finally:
|
||||
temp_path = None
|
||||
|
||||
content = (request_data.content or "").strip()
|
||||
ocr_full_text = (ocr_data.get("full_text") or "").strip()
|
||||
|
||||
if not content and not ocr_full_text:
|
||||
vlm_text = await analyze_image_with_vlm(image_bytes, processed_suffix)
|
||||
image_content = "图片文本描述为:" + (vlm_text or "").strip()
|
||||
elif content and ocr_full_text:
|
||||
image_content = f"图片上下文内容为:{content}, {ocr_full_text}"
|
||||
elif content:
|
||||
image_content = "图片上下文内容为:" + content
|
||||
else:
|
||||
image_content = ocr_full_text
|
||||
image_content = "图片文件名为:" + filename + ",图片相关内容为:" + image_content
|
||||
return JSONResponse(
|
||||
{
|
||||
"code": 200,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"image_content": image_content,
|
||||
"filename": filename,
|
||||
},
|
||||
}
|
||||
)
|
||||
finally:
|
||||
if temp_path and temp_path.exists():
|
||||
try:
|
||||
temp_path.unlink()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
|
||||
|
||||
|
||||
@app.post("/split_content_list")
|
||||
@ -1590,7 +1668,7 @@ async def split_result(
|
||||
logger.info(f"转换 DOCX -> PDF: {filename}")
|
||||
|
||||
# 执行转换
|
||||
converter.convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
||||
get_doc_converter().convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
||||
|
||||
# ? 使用临时文件的基础名查找 PDF
|
||||
temp_base_name = temp_input_path.stem # 如 tmp2bb0l4cn
|
||||
|
||||
387
chunk_text.py
387
chunk_text.py
@ -8,20 +8,293 @@ import re
|
||||
from typing import List, Dict, Optional, Union, Any, Tuple
|
||||
# 指定 JSON 文件路径
|
||||
# file_path = '/storage01/home/hdf/project/wxkgrag/kgextract/output/122-06A0014-B01001_发动机-维修手册_content_list.json'
|
||||
from pathlib import PurePath, Path as PathLib
|
||||
try:
|
||||
from filename_proceess_and_kgquery import get_entity
|
||||
except ImportError:
|
||||
get_entity = None
|
||||
from pathlib import PurePath, PurePosixPath, Path as PathLib
|
||||
from fastapi import FastAPI, File, Path, UploadFile, HTTPException, Form, Request, Header, Body
|
||||
from fileparse_util import process_document
|
||||
from io import BytesIO
|
||||
from urllib.parse import unquote, urlparse
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
import threading
|
||||
import aiofiles
|
||||
import httpx
|
||||
import logging
|
||||
from config import API_URLS
|
||||
from rapidocr_onnxruntime import RapidOCR
|
||||
from PIL import Image, ImageOps, UnidentifiedImageError
|
||||
from config import (
|
||||
API_URLS,
|
||||
IMAGE_DIR,
|
||||
MAX_SPLIT_IMAGE_BASE64_CHARS,
|
||||
MAX_SPLIT_IMAGE_BYTES,
|
||||
MAX_SPLIT_IMAGE_INPUT_BYTES,
|
||||
OCR_CONCURRENCY,
|
||||
SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES,
|
||||
SPLIT_IMAGE_JPEG_QUALITY,
|
||||
SPLIT_IMAGE_MAX_DIMENSION,
|
||||
)
|
||||
try:
|
||||
from config import SHIP_MODEL_NAME
|
||||
except ImportError:
|
||||
SHIP_MODEL_NAME = ""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ocr_engine = RapidOCR()
|
||||
OCR_SEMAPHORE = asyncio.Semaphore(max(1, OCR_CONCURRENCY))
|
||||
_ocr_thread_lock = threading.Lock()
|
||||
_image_ocr_cache: Dict[str, str] = {}
|
||||
_image_ocr_cache_lock = threading.Lock()
|
||||
_api_url_inflight = [0] * len(API_URLS)
|
||||
_api_url_lock = threading.Lock()
|
||||
|
||||
|
||||
def _image_mime_type(suffix: str) -> str:
|
||||
return {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".bmp": "image/bmp",
|
||||
".webp": "image/webp",
|
||||
".gif": "image/gif",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
}.get((suffix or "").lower(), "image/png")
|
||||
|
||||
|
||||
def _image_save_format(suffix: str) -> tuple[str, dict]:
|
||||
suffix = (suffix or "").lower()
|
||||
if suffix in {".jpg", ".jpeg"}:
|
||||
return "JPEG", {"quality": SPLIT_IMAGE_JPEG_QUALITY, "optimize": True}
|
||||
if suffix == ".webp":
|
||||
return "WEBP", {"quality": SPLIT_IMAGE_JPEG_QUALITY, "method": 6}
|
||||
if suffix == ".png":
|
||||
return "PNG", {"optimize": True, "compress_level": 9}
|
||||
if suffix in {".tif", ".tiff"}:
|
||||
return "TIFF", {"compression": "tiff_lzw"}
|
||||
if suffix == ".gif":
|
||||
return "GIF", {"optimize": True}
|
||||
if suffix == ".bmp":
|
||||
return "BMP", {}
|
||||
return "PNG", {"optimize": True, "compress_level": 9}
|
||||
|
||||
|
||||
def _compression_suffix(suffix: str) -> str:
|
||||
suffix = (suffix or "").lower()
|
||||
if suffix in {".jpg", ".jpeg", ".png", ".webp"}:
|
||||
return suffix
|
||||
if suffix in {".bmp", ".gif", ".tif", ".tiff"}:
|
||||
return ".png"
|
||||
return ".png"
|
||||
|
||||
|
||||
def _save_image_to_bytes(img: Image.Image, suffix: str) -> bytes:
|
||||
image_format, save_options = _image_save_format(suffix)
|
||||
if image_format in {"JPEG", "WEBP", "BMP"} and img.mode not in {"RGB", "L"}:
|
||||
img = img.convert("RGB")
|
||||
elif image_format in {"PNG", "TIFF", "GIF"} and img.mode not in {"RGB", "RGBA", "L", "P"}:
|
||||
img = img.convert("RGBA")
|
||||
|
||||
output = BytesIO()
|
||||
img.save(output, format=image_format, **save_options)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def _save_image_as_jpeg(img: Image.Image) -> bytes:
|
||||
if img.mode not in {"RGB", "L"}:
|
||||
img = img.convert("RGB")
|
||||
last_bytes = b""
|
||||
for quality in (SPLIT_IMAGE_JPEG_QUALITY, 82, 74):
|
||||
output = BytesIO()
|
||||
img.save(output, format="JPEG", quality=quality, optimize=True)
|
||||
last_bytes = output.getvalue()
|
||||
if len(last_bytes) <= MAX_SPLIT_IMAGE_BYTES:
|
||||
break
|
||||
return last_bytes
|
||||
|
||||
|
||||
def compress_image_bytes(image_bytes: bytes, suffix: str = "") -> tuple[bytes, str]:
|
||||
if not image_bytes:
|
||||
raise HTTPException(status_code=400, detail="decoded image is empty")
|
||||
if len(image_bytes) > MAX_SPLIT_IMAGE_INPUT_BYTES:
|
||||
raise HTTPException(status_code=413, detail="image input is too large")
|
||||
|
||||
suffix = (suffix or "").lower()
|
||||
try:
|
||||
with Image.open(BytesIO(image_bytes)) as img:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
max_side = max(img.size)
|
||||
should_resize = SPLIT_IMAGE_MAX_DIMENSION > 0 and max_side > SPLIT_IMAGE_MAX_DIMENSION
|
||||
should_reencode = len(image_bytes) > SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES
|
||||
|
||||
if not should_resize and not should_reencode:
|
||||
if len(image_bytes) > MAX_SPLIT_IMAGE_BYTES:
|
||||
raise HTTPException(status_code=413, detail="image is too large")
|
||||
return image_bytes, suffix
|
||||
|
||||
if should_resize:
|
||||
scale = SPLIT_IMAGE_MAX_DIMENSION / max_side
|
||||
new_size = (
|
||||
max(1, int(img.width * scale)),
|
||||
max(1, int(img.height * scale)),
|
||||
)
|
||||
resample = getattr(getattr(Image, "Resampling", Image), "LANCZOS")
|
||||
img = img.resize(new_size, resample)
|
||||
|
||||
compressed_suffix = _compression_suffix(suffix)
|
||||
compressed_bytes = _save_image_to_bytes(img, compressed_suffix)
|
||||
|
||||
if len(compressed_bytes) > MAX_SPLIT_IMAGE_BYTES:
|
||||
compressed_bytes = _save_image_as_jpeg(img)
|
||||
compressed_suffix = ".jpg"
|
||||
except UnidentifiedImageError as exc:
|
||||
raise HTTPException(status_code=400, detail="image content is not a supported image") from exc
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to compress image, using original bytes: {exc}")
|
||||
compressed_bytes = image_bytes
|
||||
compressed_suffix = suffix
|
||||
|
||||
if len(compressed_bytes) >= len(image_bytes) and len(image_bytes) <= MAX_SPLIT_IMAGE_BYTES:
|
||||
return image_bytes, suffix
|
||||
if len(compressed_bytes) > MAX_SPLIT_IMAGE_BYTES:
|
||||
raise HTTPException(status_code=413, detail="image is too large after compression")
|
||||
return compressed_bytes, compressed_suffix
|
||||
|
||||
|
||||
def prepare_split_image(image_base64: str, suffix: str = "") -> tuple[bytes, str]:
|
||||
raw = (image_base64 or "").strip()
|
||||
if not raw:
|
||||
raise HTTPException(status_code=400, detail="image_base64 is empty")
|
||||
|
||||
if "," in raw and raw.split(",", 1)[0].lower().startswith("data:image"):
|
||||
raw = raw.split(",", 1)[1]
|
||||
|
||||
raw = "".join(raw.split())
|
||||
if len(raw) > MAX_SPLIT_IMAGE_BASE64_CHARS:
|
||||
raise HTTPException(status_code=413, detail="image_base64 is too large")
|
||||
|
||||
try:
|
||||
image_bytes = base64.b64decode(raw, validate=True)
|
||||
except (binascii.Error, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail="image_base64 is not valid base64") from exc
|
||||
|
||||
return compress_image_bytes(image_bytes, suffix)
|
||||
|
||||
|
||||
def decode_base64_image(image_base64: str, suffix: str = "") -> bytes:
|
||||
image_bytes, _ = prepare_split_image(image_base64, suffix)
|
||||
return image_bytes
|
||||
|
||||
|
||||
def safe_original_filename(filename: str) -> str:
|
||||
name = os.path.basename((filename or "").strip())
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="filename is empty")
|
||||
safe_name = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", name).strip("._")
|
||||
if not safe_name:
|
||||
raise HTTPException(status_code=400, detail="filename is invalid")
|
||||
return safe_name
|
||||
|
||||
|
||||
def _strip_markdown_image_path(img_path: str) -> str:
|
||||
value = (img_path or "").strip()
|
||||
match = re.match(r"!\[[^\]]*\]\((.*?)\)", value)
|
||||
return match.group(1).strip() if match else value
|
||||
|
||||
|
||||
def resolve_local_image_path(img_path: str) -> Optional[PathLib]:
|
||||
value = _strip_markdown_image_path(str(img_path or ""))
|
||||
if not value:
|
||||
return None
|
||||
|
||||
parsed_path = unquote(urlparse(value).path or value)
|
||||
marker = "/images/"
|
||||
if marker in parsed_path:
|
||||
relative_path = parsed_path.split(marker, 1)[1]
|
||||
else:
|
||||
normalized = parsed_path.replace("\\", "/").lstrip("/")
|
||||
if normalized.startswith("images/"):
|
||||
relative_path = normalized.split("/", 1)[1]
|
||||
else:
|
||||
relative_path = normalized
|
||||
|
||||
relative_path = relative_path.replace("\\", "/").lstrip("/")
|
||||
if not relative_path:
|
||||
return None
|
||||
|
||||
image_root = PathLib(IMAGE_DIR).resolve()
|
||||
local_path = (image_root / PurePosixPath(relative_path)).resolve()
|
||||
try:
|
||||
local_path.relative_to(image_root)
|
||||
except ValueError:
|
||||
logger.warning(f"Image path is outside IMAGE_DIR and will be ignored: {img_path}")
|
||||
return None
|
||||
|
||||
return local_path if local_path.is_file() else None
|
||||
|
||||
|
||||
def extract_image_text_sync(image_path: Union[str, PathLib]) -> dict:
|
||||
resolved_path = str(PathLib(image_path).resolve())
|
||||
with _image_ocr_cache_lock:
|
||||
cached_text = _image_ocr_cache.get(resolved_path)
|
||||
if cached_text is not None:
|
||||
return {"texts": [], "full_text": cached_text, "elapse": None}
|
||||
|
||||
with _ocr_thread_lock:
|
||||
ocr_result, elapse = ocr_engine(resolved_path)
|
||||
|
||||
texts = [item[1] for item in (ocr_result or [])]
|
||||
full_text = "图片包含的文字内容为:" + ",".join(texts) if texts else ""
|
||||
with _image_ocr_cache_lock:
|
||||
_image_ocr_cache[resolved_path] = full_text
|
||||
return {
|
||||
"texts": texts,
|
||||
"full_text": full_text,
|
||||
"elapse": elapse,
|
||||
}
|
||||
|
||||
|
||||
def build_image_markdown_with_ocr(markdown_image: str, local_image_path: Optional[PathLib]) -> str:
|
||||
if "图片包含的文字内容为:" in markdown_image:
|
||||
return markdown_image
|
||||
if not local_image_path:
|
||||
return markdown_image
|
||||
|
||||
try:
|
||||
ocr_data = extract_image_text_sync(local_image_path)
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to OCR local image {local_image_path}: {exc}")
|
||||
return markdown_image
|
||||
|
||||
ocr_full_text = (ocr_data.get("full_text") or "").strip()
|
||||
if not ocr_full_text:
|
||||
return markdown_image
|
||||
return f"{markdown_image}\n{ocr_full_text}"
|
||||
|
||||
|
||||
def image_base64_to_data_url(image_base64: Union[str, bytes], suffix: str) -> str:
|
||||
if isinstance(image_base64, bytes):
|
||||
raw = base64.b64encode(image_base64).decode("ascii")
|
||||
else:
|
||||
raw = (image_base64 or "").strip()
|
||||
if "," in raw and raw.split(",", 1)[0].lower().startswith("data:image"):
|
||||
raw = raw.split(",", 1)[1]
|
||||
raw = "".join(raw.split())
|
||||
|
||||
mime_type = _image_mime_type(suffix)
|
||||
return f"data:{mime_type};base64,{raw}"
|
||||
|
||||
|
||||
async def extract_image_text(image_path: PathLib) -> dict:
|
||||
async with OCR_SEMAPHORE:
|
||||
return await asyncio.to_thread(extract_image_text_sync, image_path)
|
||||
|
||||
def format_entity_text(data):
|
||||
# 1. 定义实体类型与属性的映射关系
|
||||
# 格式: '实体类型': [ ('前缀1', '属性1'), ('前缀2', '属性2') ]
|
||||
@ -709,18 +982,35 @@ def data_replace(data, prefix):
|
||||
遍历 data 列表,若元素包含 'img_path' 字段,则移除前缀 'images/' 并拼接新前缀。
|
||||
使用 pathlib 安全处理路径。
|
||||
"""
|
||||
from pathlib import PurePosixPath
|
||||
for ins in data:
|
||||
if isinstance(ins, dict) and "img_path" in ins:
|
||||
img_path = ins["img_path"]
|
||||
if isinstance(img_path, list):
|
||||
ins["img_path"] = "\n".join(
|
||||
data_replace([{"img_path": str(item)}], prefix)[0]["img_path"]
|
||||
for item in img_path
|
||||
if str(item).strip()
|
||||
)
|
||||
continue
|
||||
|
||||
img_path = str(img_path)
|
||||
if img_path.strip().startswith("!["):
|
||||
local_image_path = resolve_local_image_path(img_path)
|
||||
ins["img_path"] = build_image_markdown_with_ocr(img_path, local_image_path)
|
||||
continue
|
||||
|
||||
p = PurePosixPath(img_path)
|
||||
# 如果以 images/ 开头,去掉第一级目录
|
||||
if p.parts and p.parts[0] == "images":
|
||||
relative_path = str(PurePosixPath(*p.parts[1:]))
|
||||
ins["img_path"] = f""
|
||||
markdown_image = f""
|
||||
local_image_path = resolve_local_image_path(relative_path)
|
||||
ins["img_path"] = build_image_markdown_with_ocr(markdown_image, local_image_path)
|
||||
else:
|
||||
# 否则保留原路径(或按需处理)
|
||||
ins["img_path"] = f""
|
||||
markdown_image = f""
|
||||
local_image_path = resolve_local_image_path(img_path)
|
||||
ins["img_path"] = build_image_markdown_with_ocr(markdown_image, local_image_path)
|
||||
return data
|
||||
|
||||
def filter_content_bbox(data, max_length=3000):
|
||||
@ -956,6 +1246,42 @@ def merge_short_slices(slices, min_length=30,filename="122-06A0014-B01003_雷达
|
||||
"positions": pending_positions
|
||||
})
|
||||
|
||||
try:
|
||||
final_result = get_entity(filename)
|
||||
xinghao = find_ship_info_by_hull(SHIP_MODEL_NAME, final_result)
|
||||
if xinghao is None: # 确保 xinghao 为 None 时不会报错
|
||||
xinghao = {"model_name": "", "ship_name": ""}
|
||||
print("未找到匹配的舰船信息")
|
||||
|
||||
# 安全处理 xinghao 为 None 的情况
|
||||
model_name = ""
|
||||
if xinghao and 'model_name' in xinghao:
|
||||
model_name = xinghao['model_name']
|
||||
|
||||
other_data = format_entity_text(final_result)
|
||||
logger.info(f"文件名实体提取成功: {other_data}")
|
||||
except Exception as e:
|
||||
logger.warning(f"文件名实体提取失败: {e}")
|
||||
other_data = ""
|
||||
model_name = "" # 确保异常时 model_name 有定义
|
||||
|
||||
# 将提取的信息(如舰艇名、型号)注入到每个切片的开头
|
||||
for ins in result:
|
||||
content = ins['content']
|
||||
# 如果没有提取到有效数据,则跳过注入
|
||||
|
||||
newline_idx = content.find('\n')
|
||||
info = other_data
|
||||
if model_name:
|
||||
info += f',型号为{model_name}'
|
||||
suffix = f'({info})'
|
||||
|
||||
# 将信息插入到第一行末尾
|
||||
if newline_idx == -1:
|
||||
ins['content'] = content + suffix
|
||||
else:
|
||||
ins['content'] = content[:newline_idx] + suffix + content[newline_idx:]
|
||||
|
||||
return result
|
||||
def find_and_read_content_list(directory, original_filename, encoding='utf-8'):
|
||||
"""
|
||||
@ -998,8 +1324,7 @@ async def process_pdf_file(pdf_path: Path, image_prefix: str,filename:str) -> Op
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
slices_check = merge_short_slices(slices_check, min_length=30,filename=filename)
|
||||
return {"slices": slices_check, "images": images}
|
||||
else:
|
||||
return {"slices": [], "images": {}}
|
||||
|
||||
api_url = get_api_url()
|
||||
try:
|
||||
async with aiofiles.open(pdf_path, "rb") as f:
|
||||
@ -1021,8 +1346,9 @@ async def process_pdf_file(pdf_path: Path, image_prefix: str,filename:str) -> Op
|
||||
slices = get_chunk_bbox(replaced_content_list)
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
slices_check = merge_short_slices(slices_check, min_length=30,filename=filename)
|
||||
|
||||
return {"slices": [], "images": {}}
|
||||
print(11111111111111111111111111111111111111)
|
||||
print(len(images))
|
||||
return {"slices": slices_check, "images": images}
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error("❌ 响应不是有效的 JSON 格式")
|
||||
@ -1044,10 +1370,49 @@ async def process_other_file(pdf_path: Path, image_prefix: str, filename: str) -
|
||||
replaced_content_list = data_replace(content_list, image_prefix)
|
||||
slices = get_chunk_bbox(replaced_content_list)
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
slices_check = merge_short_slices(slices_check, min_length=30,filename=filename)
|
||||
return {"slices": slices_check, "images": images}
|
||||
else:
|
||||
|
||||
api_url = get_api_url()
|
||||
analyze_other_url = api_url.replace("/analyze-pdf", "/analyze-otherfile")
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(pdf_path.name)
|
||||
content_type = mime_type or "application/octet-stream"
|
||||
|
||||
try:
|
||||
async with aiofiles.open(pdf_path, "rb") as f:
|
||||
file_content = await f.read()
|
||||
|
||||
files = {"file": (pdf_path.name, file_content, content_type)}
|
||||
|
||||
async with httpx.AsyncClient(timeout=60 * 20) as client:
|
||||
response = await client.post(analyze_other_url, files=files)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"❌ Office 文件 API 请求失败,状态码: {response.status_code}, 响应: {response.text}")
|
||||
return None
|
||||
|
||||
result = response.json()
|
||||
data = result.get("data", {})
|
||||
content_list = data.get("content_list", [])
|
||||
images = data.get("images", {})
|
||||
|
||||
# 后续的数据替换与切片处理逻辑
|
||||
replaced_content_list = data_replace(content_list, image_prefix)
|
||||
slices = get_chunk_bbox(replaced_content_list)
|
||||
slices_check = chunk_check(slices, 8000)
|
||||
|
||||
return {"slices": slices_check, "images": images}
|
||||
|
||||
except json.JSONDecodeError:
|
||||
error_text = response.text if 'response' in locals() else "请求未到达服务器"
|
||||
logger.error(f"❌ 响应不是有效的 JSON 格式: {error_text}")
|
||||
return {"slices": [], "images": {}}
|
||||
except Exception as e:
|
||||
logger.error(f"处理 Office 文件时出错: {e}", exc_info=True)
|
||||
return None
|
||||
finally:
|
||||
release_api_url(api_url)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
filepath = r"E:\ZKYNLP\Hjunproject\project0506\kgrag\122-06A0014-B01001_发动机-维修手册_content_list.json"
|
||||
|
||||
15
config.py
15
config.py
@ -228,3 +228,18 @@ API_URLS = [
|
||||
#"http://192.168.0.111:9980/analyze-pdf",
|
||||
# "http://192.168.0.111:9975/analyze-pdf",
|
||||
]
|
||||
# ==================== 切片图片处理配置 ====================
|
||||
MAX_SPLIT_IMAGE_INPUT_BYTES = 50 * 1024 * 1024
|
||||
MAX_SPLIT_IMAGE_BYTES = 10 * 1024 * 1024
|
||||
MAX_SPLIT_IMAGE_BASE64_CHARS = (MAX_SPLIT_IMAGE_INPUT_BYTES * 4 // 3) + 4096
|
||||
SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES = MAX_SPLIT_IMAGE_BYTES
|
||||
SPLIT_IMAGE_MAX_DIMENSION = 4096
|
||||
SPLIT_IMAGE_JPEG_QUALITY = 88
|
||||
OCR_CONCURRENCY = 1
|
||||
VLM_CONCURRENCY = 2
|
||||
|
||||
SHIP_MODEL_NAME = "/app/ship_xinghao.json"
|
||||
TREE_JSON_PATH = "/app/tree_data.json"
|
||||
# ==================== mineru和kgrag目录映射地址 ====================
|
||||
SEARCH_DIR ="/app/mineru_output"
|
||||
IMAGE_DIR = "/app/mineru_output/images"
|
||||
|
||||
857
filename_proceess_and_kgquery.py
Normal file
857
filename_proceess_and_kgquery.py
Normal file
@ -0,0 +1,857 @@
|
||||
"""
|
||||
PDF 用户上传接口(FastAPI)
|
||||
- 处理文件名和其对应关系
|
||||
"""
|
||||
|
||||
from typing import Dict, Tuple
|
||||
import os
|
||||
import re
|
||||
from typing import Dict
|
||||
from fastapi import FastAPI, HTTPException
|
||||
import logging
|
||||
import json
|
||||
from modelsAPI.model_api import OpenaiAPI
|
||||
from openai import OpenAI
|
||||
from typing import Dict, List, Any
|
||||
from config import TREE_JSON_PATH
|
||||
|
||||
app = FastAPI(title="PDF Upload Service")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
||||
logger = logging.getLogger("filename_neo4j_process")
|
||||
|
||||
|
||||
# =========================
|
||||
# 图方法
|
||||
# =========================
|
||||
|
||||
|
||||
def node_to_json(node):
|
||||
props = dict(node)
|
||||
# ks_raw = props.get("knowledge_source")
|
||||
# knowledge_source = json.loads(ks_raw) if ks_raw else []
|
||||
|
||||
ks_raw = props.get("knowledge_source")
|
||||
knowledge_source = []
|
||||
|
||||
if ks_raw and isinstance(ks_raw, str):
|
||||
try:
|
||||
ks_list = json.loads(ks_raw)
|
||||
if isinstance(ks_list, list):
|
||||
for ks in ks_list:
|
||||
if isinstance(ks, dict):
|
||||
ks.pop("info", None)
|
||||
knowledge_source.append(ks)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# 解析失败或类型错误时,保持 knowledge_source 为空列表
|
||||
pass
|
||||
|
||||
name = props.get("名称")
|
||||
props.pop("embedding", None)
|
||||
props.pop("knowledge_source", None)
|
||||
props.pop("name", None) # 现在安全地移除
|
||||
props.pop("fulltext", None)
|
||||
if "last_updated" in props:
|
||||
props["最后更新时间"] = props.pop("last_updated")
|
||||
|
||||
if "created_at" in props:
|
||||
props["创建时间"] = props.pop("created_at")
|
||||
return {
|
||||
"id": node.element_id,
|
||||
"name": name, # 使用提前保存的值
|
||||
"labels": list(node.labels),
|
||||
"knowledge_source": knowledge_source,
|
||||
"properties": props,
|
||||
}
|
||||
|
||||
|
||||
def rel_to_json(rel):
|
||||
props = dict(rel)
|
||||
props.pop("embedding", None)
|
||||
props.pop("fact_timeline", None)
|
||||
return {"id": rel.element_id, "label": rel.type, "source": rel.start_node.element_id, "target": rel.end_node.element_id, "properties": props}
|
||||
|
||||
|
||||
def is_simple_path(path):
|
||||
"""
|
||||
判断是否为无环路径(simple path)
|
||||
"""
|
||||
seen = set()
|
||||
for n in path.nodes:
|
||||
nid = n.element_id
|
||||
if nid in seen:
|
||||
return False
|
||||
seen.add(nid)
|
||||
return True
|
||||
|
||||
def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
按照固定层级构建树:舰船(根) -> 舰艇 -> 系统 -> 子系统
|
||||
不再依赖 links 的连线,直接根据 labels 归类,彻底避免环状数据导致的死循环。
|
||||
默认只展开第一艘舰艇的系统/子系统,其它舰艇 children 置空(前端可点击按需加载)。
|
||||
"""
|
||||
|
||||
# 1. 辅助函数:判断标签是否包含关键字
|
||||
def has_label(node, keyword):
|
||||
labels = node.get("labels", [])
|
||||
if isinstance(labels, list):
|
||||
return any(keyword in lbl for lbl in labels)
|
||||
return False
|
||||
|
||||
# 2. 辅助函数:构建标准的树节点格式
|
||||
def make_node(node_data):
|
||||
return {
|
||||
"id": node_data["id"],
|
||||
"name": node_data.get("name", ""),
|
||||
"labels": node_data.get("labels", []),
|
||||
|
||||
"properties": node_data.get("properties", {}),
|
||||
"color": node_data.get("color"),
|
||||
"size": node_data.get("size"),
|
||||
"children": []
|
||||
}
|
||||
|
||||
# 3. 将节点按层级分类存储到字典中,方便快速查找
|
||||
fleet_roots = {} # 第零层:舰船/舰号(父根节点)
|
||||
ships = {} # 第一层:舰艇
|
||||
systems = {} # 第二层:系统(不含子系统)
|
||||
sub_systems = {} # 第三层:子系统
|
||||
|
||||
for node in nodes:
|
||||
# 注意:判断逻辑要互斥,防止"子系统"被误判为"系统"
|
||||
if has_label(node, "舰船") or has_label(node, "舰号"):
|
||||
fleet_roots[node["id"]] = make_node(node)
|
||||
elif has_label(node, "舰艇"):
|
||||
ships[node["id"]] = make_node(node)
|
||||
elif has_label(node, "子系统"):
|
||||
sub_systems[node["id"]] = make_node(node)
|
||||
elif has_label(node, "系统"):
|
||||
systems[node["id"]] = make_node(node)
|
||||
else:
|
||||
pass
|
||||
|
||||
# 4. 组装系统 -> 子系统、舰艇 -> 系统 关系(先把完整子树挂好)
|
||||
for link in links:
|
||||
if link.get("label") != "包含":
|
||||
continue
|
||||
|
||||
src_id = link["source"]
|
||||
tgt_id = link["target"]
|
||||
|
||||
if src_id in ships and tgt_id in systems:
|
||||
ships[src_id]["children"].append(systems[tgt_id])
|
||||
elif src_id in systems and tgt_id in sub_systems:
|
||||
systems[src_id]["children"].append(sub_systems[tgt_id])
|
||||
elif src_id in ships and tgt_id in sub_systems:
|
||||
ships[src_id]["children"].append(sub_systems[tgt_id])
|
||||
|
||||
# 5. 所有舰艇 children 全部保留,仅用 collapsed 标记默认折叠状态。
|
||||
# 前端读取 collapsed=False 的舰艇默认展开,其它折叠;
|
||||
# 用户点击折叠的舰艇时,前端切换 collapsed 即可展示子树。
|
||||
# 只保留名称包含 "122" 的舰艇(隐藏 JZ贱 等其它分支)
|
||||
#sorted_ships = sorted(
|
||||
# (s for s in ships.values() if "122" in s.get("name", "")),
|
||||
# key=lambda s: s.get("name", ""),
|
||||
#)
|
||||
sorted_ships = sorted(ships.values(), key=lambda s: s.get("name", ""))
|
||||
for idx, ship in enumerate(sorted_ships):
|
||||
ship["collapsed"] = (idx != 0)
|
||||
|
||||
# 6. 挂载到舰船根节点;如果数据里没有舰船根,就创建一个虚拟根
|
||||
if fleet_roots:
|
||||
# 取第一个舰船根节点作为父(一般场景下只有一个)
|
||||
root = next(iter(fleet_roots.values()))
|
||||
root["children"] = sorted_ships
|
||||
return [root]
|
||||
else:
|
||||
virtual_root = {
|
||||
"id": "__fleet_root__",
|
||||
"name": "舰号",
|
||||
"labels": ["舰船"],
|
||||
"properties": {},
|
||||
"color": None,
|
||||
"size": None,
|
||||
"children": sorted_ships,
|
||||
}
|
||||
return [virtual_root]
|
||||
def build_tree1(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
从 nodes + links 构建树结构。
|
||||
- 根节点:label 包含 '舰艇' 的节点
|
||||
- 父子关系:links 中 label == '包含' 的边 (source -> target)
|
||||
- 支持多棵树(多个舰艇根节点)
|
||||
"""
|
||||
|
||||
# 1. 建立 id -> node 的映射
|
||||
node_map: Dict[str, Dict] = {n["id"]: n for n in nodes}
|
||||
|
||||
# 2. 建立 父id -> [子id列表] 的邻接表(只处理"包含"关系)
|
||||
children_map: Dict[str, List[str]] = {n["id"]: [] for n in nodes}
|
||||
parent_map: Dict[str, str] = {} # 子id -> 父id,用于识别根节点
|
||||
|
||||
for link in links:
|
||||
if link.get("label") == "包含":
|
||||
src = link["source"]
|
||||
tgt = link["target"]
|
||||
if src in children_map:
|
||||
children_map[src].append(tgt)
|
||||
if tgt not in parent_map:
|
||||
parent_map[tgt] = src
|
||||
|
||||
# 3. 递归构建子树
|
||||
def build_subtree(node_id: str, ancestors: set = None) -> Dict[str, Any]:
|
||||
if ancestors is None:
|
||||
ancestors = set()
|
||||
if node_id in ancestors:
|
||||
return None
|
||||
ancestors.add(node_id)
|
||||
node = node_map[node_id]
|
||||
subtree = {"id": node_id, "name": node.get("name", ""), "labels": node.get("labels", []), "properties": node.get("properties", {}), "color": node.get("color"), "size": node.get("size"), "children": []}
|
||||
current_ancestors = ancestors | {node_id}
|
||||
for child_id in children_map.get(node_id, []):
|
||||
child = build_subtree(child_id, current_ancestors)
|
||||
if child is not None:
|
||||
subtree["children"].append(child)
|
||||
return subtree
|
||||
|
||||
# def build_subtree(node_id: str) -> Dict[str, Any]:
|
||||
# node = node_map[node_id]
|
||||
# subtree = {
|
||||
# "id": node_id,
|
||||
# "name": node.get("name", ""),
|
||||
# "labels": node.get("labels", []),
|
||||
# "properties": node.get("properties", {}),
|
||||
# "color": node.get("color"),
|
||||
# "size": node.get("size"),
|
||||
# "children": []
|
||||
# }
|
||||
# for child_id in children_map.get(node_id, []):
|
||||
# subtree["children"].append(build_subtree(child_id))
|
||||
# return subtree
|
||||
|
||||
# 4. 找出所有根节点:label 包含 '舰艇',且没有父节点
|
||||
roots = [n["id"] for n in nodes if "舰艇" in n.get("labels", []) and n["id"] not in parent_map]
|
||||
|
||||
# 5. 为每棵树构建结构,并打上树编号
|
||||
forest = []
|
||||
for i, root_id in enumerate(roots):
|
||||
tree = build_subtree(root_id)
|
||||
tree["tree_index"] = i # 区分不同树
|
||||
forest.append(tree)
|
||||
|
||||
return forest
|
||||
|
||||
|
||||
def _filter_searchable(graph: dict) -> dict:
|
||||
"""剥掉节点的 Searchable 标签;若剥完后 labels 为空则整体移除该节点及其关联边"""
|
||||
for n in graph["nodes"]:
|
||||
n["labels"] = [l for l in n.get("labels", []) if l != "Searchable"]
|
||||
valid_ids = {n["id"] for n in graph["nodes"] if n["labels"]}
|
||||
return {
|
||||
"nodes": [n for n in graph["nodes"] if n["id"] in valid_ids],
|
||||
"links": [
|
||||
l for l in graph["links"]
|
||||
if l["source"] in valid_ids and l["target"] in valid_ids
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_graph_from_paths(paths):
|
||||
"""
|
||||
核心公共方法
|
||||
从 Neo4j 路径结果构建 graphData
|
||||
"""
|
||||
node_map: Dict[str, dict] = {}
|
||||
link_map: Dict[str, dict] = {}
|
||||
|
||||
def add_edge(rel):
|
||||
s = rel.start_node
|
||||
t = rel.end_node
|
||||
|
||||
if s.element_id not in node_map:
|
||||
node_map[s.element_id] = node_to_json(s)
|
||||
|
||||
if t.element_id not in node_map:
|
||||
node_map[t.element_id] = node_to_json(t)
|
||||
|
||||
if rel.element_id not in link_map:
|
||||
link_map[rel.element_id] = rel_to_json(rel)
|
||||
|
||||
for record in paths:
|
||||
p = record["p"]
|
||||
|
||||
if not is_simple_path(p):
|
||||
continue
|
||||
|
||||
for r in p.relationships:
|
||||
add_edge(r)
|
||||
|
||||
return _filter_searchable({"nodes": list(node_map.values()), "links": list(link_map.values())})
|
||||
|
||||
|
||||
# def fetch_graph_sample(driver,limit: int = 200):
|
||||
# """提取采样逻辑为可复用函数"""
|
||||
# with driver.session() as session:
|
||||
# result = session.run("""
|
||||
# MATCH (n)
|
||||
# WITH n
|
||||
# ORDER BY elementId(n) ASC
|
||||
# LIMIT $limit
|
||||
# MATCH (n)-[rel]->(m)
|
||||
# RETURN n, rel, m
|
||||
# """, limit=limit)
|
||||
|
||||
# node_map, link_map = {}, {}
|
||||
# for rec in result:
|
||||
# n, r, m = rec["n"], rec["rel"], rec["m"]
|
||||
# node_map[n.element_id] = node_to_json(n)
|
||||
# node_map[m.element_id] = node_to_json(m)
|
||||
# link_map[r.element_id] = rel_to_json(r)
|
||||
|
||||
# return {
|
||||
# "nodes": list(node_map.values()),
|
||||
# "links": list(link_map.values())
|
||||
# }
|
||||
|
||||
# def fetch_graph_sample(driver, limit: int = 200):
|
||||
# """
|
||||
# 获取采样数据:
|
||||
# 1. 选取前 $limit 个种子节点(Level 0 或 1,取决于业务定义,这里定义种子为 Level 0)。
|
||||
# 2. 对每个节点向下探索最多 3 跳。
|
||||
# 3. 每个节点对象中增加 level 字段,表示其距离种子节点的深度。
|
||||
# """
|
||||
# with driver.session() as session:
|
||||
# # Cypher 逻辑:
|
||||
# # - 选出种子节点 n
|
||||
# # - 匹配 1-3 跳路径
|
||||
# # - 返回种子节点 n,路径 path,以及该路径的长度(即层级)
|
||||
# result = session.run("""
|
||||
# MATCH (n)
|
||||
# WITH n ORDER BY elementId(n) ASC LIMIT $limit
|
||||
# OPTIONAL MATCH path = (n)-[*1..3]-(m)
|
||||
# UNWIND nodes(path) AS x
|
||||
# WITH n, path, m, COUNT(DISTINCT x) AS nodeCount
|
||||
# WHERE path IS NULL OR nodeCount = LENGTH(path) + 1
|
||||
# RETURN n, path, LENGTH(path) AS depth
|
||||
# """, limit=limit)
|
||||
|
||||
# node_map, link_map = {}, {}
|
||||
|
||||
# for rec in result:
|
||||
# seed_node = rec["n"]
|
||||
# path = rec["path"]
|
||||
# depth = rec["depth"] or 0 # 如果没有路径,深度为 0
|
||||
|
||||
# # 1. 处理种子节点 (Level 0)
|
||||
# if seed_node.element_id not in node_map:
|
||||
# node_data = node_to_json(seed_node)
|
||||
# node_data["level"] = 0 # 种子节点设为 0 层
|
||||
# node_map[seed_node.element_id] = node_data
|
||||
|
||||
# # 2. 如果存在路径,解析路径中的所有节点和关系
|
||||
# if path:
|
||||
# # 路径中的节点处理
|
||||
# # path.nodes 包含了从起始到终点的所有节点
|
||||
# for i, node in enumerate(path.nodes):
|
||||
# if node.element_id not in node_map:
|
||||
# node_data = node_to_json(node)
|
||||
# # 层级即为该节点在当前路径中的索引
|
||||
# node_data["level"] = i
|
||||
# node_map[node.element_id] = node_data
|
||||
# else:
|
||||
# # 如果节点已存在,保留最小的 level (即最靠近种子的距离)
|
||||
# node_map[node.element_id]["level"] = min(node_map[node.element_id].get("level", 3), i)
|
||||
|
||||
# # 路径中的关系处理
|
||||
# for rel in path.relationships:
|
||||
# if rel.element_id not in link_map:
|
||||
# link_map[rel.element_id] = rel_to_json(rel)
|
||||
|
||||
# return {
|
||||
# "nodes": list(node_map.values()),
|
||||
# "links": list(link_map.values())
|
||||
# }
|
||||
|
||||
|
||||
# def fetch_graph_sample(driver, limit=200, max_depth=3):
|
||||
# with driver.session() as session:
|
||||
# result = session.run("""
|
||||
# MATCH (c)
|
||||
# WITH c ORDER BY elementId(c) ASC LIMIT $limit
|
||||
|
||||
# MATCH path = (c)-[*0..3]-(n)
|
||||
# WITH n, MIN(length(path)) AS dist
|
||||
# OPTIONAL MATCH (n)-[r]-(m)
|
||||
|
||||
# RETURN n, dist, r, m
|
||||
# """, limit=limit)
|
||||
|
||||
# node_map, link_map = {}, {}
|
||||
|
||||
# for rec in result:
|
||||
# n = rec["n"]
|
||||
# dist = rec["dist"]
|
||||
# r = rec["r"]
|
||||
# m = rec["m"]
|
||||
|
||||
# # ---- 节点 ----
|
||||
# nid = n.element_id
|
||||
# if nid not in node_map:
|
||||
# node_data = node_to_json(n)
|
||||
# node_data["level"] = min(dist, 3) if dist is not None else 0
|
||||
# node_map[nid] = node_data
|
||||
|
||||
# # ---- 边 ----
|
||||
# if r and m:
|
||||
# mid = m.element_id
|
||||
# if mid not in node_map:
|
||||
# node_map[mid] = node_to_json(m)
|
||||
|
||||
# link_map.setdefault(r.element_id, rel_to_json(r))
|
||||
|
||||
# return {
|
||||
# "nodes": list(node_map.values()),
|
||||
# "links": list(link_map.values())
|
||||
# }
|
||||
|
||||
def fetch_graph_sample(driver):
|
||||
"""
|
||||
查询所有舰艇及其关联的系统和子系统
|
||||
路径结构:(舰艇) -> (系统) -> (子系统)
|
||||
"""
|
||||
|
||||
with driver.session() as session:
|
||||
# =========================
|
||||
# 统一查询:舰艇 -> 系统 -> 子系统
|
||||
# =========================
|
||||
# 逻辑说明:
|
||||
# 1. MATCH (ship:舰艇):选中所有舰艇
|
||||
# 2. -[*1..3]->(target):查找深度为 1 到 3 的路径
|
||||
# - 深度 1:舰艇 -> 系统
|
||||
# - 深度 2:舰艇 -> 系统 -> 子系统
|
||||
# 3. WHERE target:系统 OR target:子系统:确保终点是我们关心的节点类型
|
||||
# (防止查询出其他无关的深层节点)
|
||||
|
||||
query = """
|
||||
MATCH path = (ship:舰艇)-[*1..3]->(target)
|
||||
WHERE target:系统 OR target:子系统
|
||||
RETURN path
|
||||
"""
|
||||
|
||||
result = session.run(query)
|
||||
|
||||
# =========================
|
||||
# 解析 paths
|
||||
# =========================
|
||||
all_nodes = {}
|
||||
all_links = {}
|
||||
|
||||
for record in result:
|
||||
path = record["path"]
|
||||
|
||||
# 解析节点
|
||||
for node in path.nodes:
|
||||
# 兼容 ID 获取
|
||||
node_key = node.element_id if hasattr(node, 'element_id') else node.id
|
||||
if node_key not in all_nodes:
|
||||
all_nodes[node_key] = node_to_json(node)
|
||||
|
||||
# 解析关系
|
||||
for rel in path.relationships:
|
||||
rel_key = rel.element_id if hasattr(rel, 'element_id') else rel.id
|
||||
if rel_key not in all_links:
|
||||
all_links[rel_key] = rel_to_json(rel)
|
||||
|
||||
return _filter_searchable({
|
||||
"nodes": list(all_nodes.values()),
|
||||
"links": list(all_links.values())
|
||||
})
|
||||
def fetch_graph_sample1(driver):
|
||||
"""
|
||||
查找路径:
|
||||
1. 优先:舰艇 -> 子系统
|
||||
2. fallback:舰艇 -> 系统
|
||||
|
||||
注意:根据数据库实际结构,使用 Label (:舰艇, :子系统) 进行匹配,
|
||||
而不是 WHERE category = '...'
|
||||
"""
|
||||
|
||||
# 定义采样数量
|
||||
SAMPLE_LIMIT = 400
|
||||
|
||||
with driver.session() as session:
|
||||
# =========================
|
||||
# 第一阶段:舰艇 -> 子系统
|
||||
# =========================
|
||||
# 修改点:直接使用 (:舰艇) 和 (:子系统) 标签匹配,去掉 WHERE 子句
|
||||
result = session.run(
|
||||
"""
|
||||
MATCH (ship:舰艇)
|
||||
WITH ship LIMIT $limit
|
||||
|
||||
MATCH path = (ship)-[*1..3]->(sub:子系统)
|
||||
RETURN path
|
||||
""",
|
||||
limit=SAMPLE_LIMIT,
|
||||
)
|
||||
|
||||
paths = [record["path"] for record in result]
|
||||
|
||||
# =========================
|
||||
# fallback:舰艇 -> 系统
|
||||
# =========================
|
||||
if not paths:
|
||||
# 修改点:直接使用 (:舰艇) 和 (:系统) 标签匹配
|
||||
result = session.run(
|
||||
"""
|
||||
MATCH (ship:舰艇)
|
||||
WITH ship LIMIT $limit
|
||||
|
||||
MATCH path = (ship)-[*1..2]->(sys:系统)
|
||||
RETURN path
|
||||
""",
|
||||
limit=SAMPLE_LIMIT,
|
||||
)
|
||||
|
||||
paths = [record["path"] for record in result]
|
||||
|
||||
# =========================
|
||||
# 解析 paths
|
||||
# =========================
|
||||
all_nodes = {}
|
||||
all_links = {}
|
||||
|
||||
for path in paths:
|
||||
# 节点
|
||||
for node in path.nodes:
|
||||
# 兼容 Neo4j 5.x (element_id) 和 4.x (id)
|
||||
node_key = node.element_id if hasattr(node, "element_id") else node.id
|
||||
if node_key not in all_nodes:
|
||||
all_nodes[node_key] = node_to_json(node)
|
||||
|
||||
# 关系
|
||||
for rel in path.relationships:
|
||||
rel_key = rel.element_id if hasattr(rel, "element_id") else rel.id
|
||||
if rel_key not in all_links:
|
||||
all_links[rel_key] = rel_to_json(rel)
|
||||
|
||||
return {"nodes": list(all_nodes.values()), "links": list(all_links.values())}
|
||||
|
||||
|
||||
# =============================
|
||||
# 文件名处理方法
|
||||
# =============================
|
||||
|
||||
# BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
|
||||
|
||||
|
||||
def _load_tree_data() -> Dict:
|
||||
# 注意:实际运行时请确保 result.json 存在,此处仅为代码结构展示
|
||||
if not os.path.exists(TREE_JSON_PATH):
|
||||
return {}
|
||||
with open(TREE_JSON_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _resolve_ship_root(data: Dict, ship_code: str) -> Dict:
|
||||
raw = data.get(ship_code)
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
|
||||
# 结构 1:直接是节点 {"code","name","children"}
|
||||
if {"code", "name", "children"}.issubset(raw.keys()):
|
||||
return raw
|
||||
|
||||
# 结构 2:包装层 {"101": {...root node...}}
|
||||
if ship_code in raw and isinstance(raw[ship_code], dict):
|
||||
node = raw[ship_code]
|
||||
if {"code", "name", "children"}.issubset(node.keys()):
|
||||
return node
|
||||
|
||||
# 兜底:取第一个 node-like 值
|
||||
for value in raw.values():
|
||||
if isinstance(value, dict) and {"code", "name", "children"}.issubset(value.keys()):
|
||||
return value
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _longest_common_substring_len(a: str, b: str) -> int:
|
||||
if not a or not b:
|
||||
return 0
|
||||
# 优化:确保 a 是较短的字符串以节省空间(可选),原逻辑保持不动也没问题
|
||||
dp = [0] * (len(b) + 1)
|
||||
best = 0
|
||||
for i in range(1, len(a) + 1):
|
||||
prev = 0
|
||||
for j in range(1, len(b) + 1):
|
||||
temp = dp[j]
|
||||
if a[i - 1] == b[j - 1]:
|
||||
dp[j] = prev + 1
|
||||
if dp[j] > best:
|
||||
best = dp[j]
|
||||
else:
|
||||
dp[j] = 0
|
||||
prev = temp
|
||||
return best
|
||||
|
||||
|
||||
def _match_level4_tool(filename_text: str, device_node: Dict) -> Tuple[str, str]:
|
||||
"""
|
||||
匹配 Level 4 零件。
|
||||
规则:如果文件名与 Level 3 (设备名) 的最大共同字符串长度 >= 与任何 Level 4 (零件名) 的长度,
|
||||
则只匹配到 Level 3,返回空。
|
||||
只有当 Level 4 的匹配度严格高于 Level 3 时,才返回 Level 4 信息。
|
||||
"""
|
||||
children = device_node.get("children") or {}
|
||||
|
||||
# 1. 计算文件名与当前设备 (Level 3) 名称的匹配度
|
||||
device_name = str(device_node.get("name") or "")
|
||||
level3_score = _longest_common_substring_len(filename_text, device_name)
|
||||
|
||||
best_name = ""
|
||||
best_code = ""
|
||||
best_score = 0 # 记录 Level 4 中的最佳得分
|
||||
|
||||
# 2. 遍历 Level 4 子节点
|
||||
for child in children.values():
|
||||
tool_name = str(child.get("name") or "")
|
||||
tool_code = str(child.get("code") or "")
|
||||
|
||||
score = _longest_common_substring_len(filename_text, tool_name)
|
||||
|
||||
# 关键修改:只有当 Level 4 的得分严格大于 Level 3 的得分时,才视为有效匹配
|
||||
# 并且要比当前找到的其他 Level 4 更好
|
||||
if score > level3_score and score > best_score:
|
||||
best_score = score
|
||||
best_name = tool_name
|
||||
best_code = tool_code
|
||||
|
||||
# 如果没有找到比 Level 3 匹配度更高的 Level 4,则返回空
|
||||
if best_score <= level3_score:
|
||||
return "", ""
|
||||
|
||||
return best_name, best_code
|
||||
|
||||
|
||||
def process_filename(filename: str) -> Dict[str, str]:
|
||||
name, _ = os.path.splitext(os.path.basename(filename))
|
||||
parts = re.split(r"[_-]", name)
|
||||
|
||||
xian_number = ""
|
||||
level_1_system_name = ""
|
||||
level_2_system_name = ""
|
||||
device_name = ""
|
||||
tool_name = ""
|
||||
tool_code = ""
|
||||
|
||||
if not parts:
|
||||
return {}
|
||||
|
||||
xian_number_code = parts[0].strip()
|
||||
level1_code = ""
|
||||
level2_code = ""
|
||||
device_code = ""
|
||||
|
||||
if len(parts) > 1:
|
||||
part = parts[1].strip()
|
||||
# 防止索引越界
|
||||
if len(part) < 2:
|
||||
return {}
|
||||
|
||||
level1_code = part[:2]
|
||||
level2_code = part[2:4] if len(part) > 2 else ""
|
||||
device_code = part[4:] if len(part) > 4 else ""
|
||||
# 若 parts[2] 存在,则用 parts[2] 作为 device_code(支持 122-06A0014-B01001_发动机 格式)
|
||||
if len(parts) > 2:
|
||||
device_code_alt = parts[2].strip()
|
||||
if device_code_alt:
|
||||
device_code = device_code_alt
|
||||
|
||||
data = _load_tree_data()
|
||||
root = _resolve_ship_root(data, xian_number_code)
|
||||
if not root:
|
||||
return {}
|
||||
|
||||
xian_number = root.get("name", "")
|
||||
|
||||
# 一级
|
||||
s1 = root.get("children", {}).get(level1_code)
|
||||
if s1:
|
||||
level_1_system_name = s1.get("name", "")
|
||||
|
||||
# 二级
|
||||
s2 = s1.get("children", {}).get(level2_code)
|
||||
if s2:
|
||||
level_2_system_name = s2.get("name", "")
|
||||
|
||||
# 三级设备
|
||||
device = s2.get("children", {}).get(device_code)
|
||||
if device:
|
||||
device_name = device.get("name", "")
|
||||
# 若存在 level4,匹配与文件名最大相同字符串的零件
|
||||
# 内部已包含逻辑:如果设备名匹配度更高,则不返回零件
|
||||
tool_name, tool_code = _match_level4_tool(name, device)
|
||||
|
||||
return {
|
||||
"Xian_Number": xian_number,
|
||||
"Xian_Number_code": xian_number_code,
|
||||
"Level_1_System_Name": level_1_system_name,
|
||||
"level1_code": level1_code,
|
||||
"Level_2_System_Name": level_2_system_name,
|
||||
"level2_code": level2_code,
|
||||
"Device_Name": device_name,
|
||||
"device_code": device_code,
|
||||
"tool_name": tool_name,
|
||||
"tool_code": tool_code,
|
||||
}
|
||||
|
||||
|
||||
def get_entity(filename):
|
||||
result = process_filename(filename)
|
||||
entities = []
|
||||
relationships = []
|
||||
low_level = ""
|
||||
|
||||
if result:
|
||||
xian_number = result.get("Xian_Number")
|
||||
xian_number_code = result.get("Xian_Number_code")
|
||||
level_1_system_name = result.get("Level_1_System_Name")
|
||||
level1_code = result.get("level1_code")
|
||||
level_2_system_name = result.get("Level_2_System_Name")
|
||||
level2_code = result.get("level2_code")
|
||||
device_name = result.get("Device_Name")
|
||||
device_code = result.get("device_code")
|
||||
tool_name = result.get("tool_name")
|
||||
tool_code = result.get("tool_code")
|
||||
|
||||
if xian_number:
|
||||
entities.append(
|
||||
{
|
||||
"type": "舰艇",
|
||||
"properties": {
|
||||
"名称": xian_number,
|
||||
"舷号": xian_number_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = xian_number
|
||||
|
||||
if level_1_system_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "系统",
|
||||
"properties": {
|
||||
"名称": level_1_system_name,
|
||||
"系统编码": level1_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = level_1_system_name
|
||||
|
||||
if xian_number and level_1_system_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": xian_number,
|
||||
"to_entity": level_1_system_name,
|
||||
}
|
||||
)
|
||||
|
||||
if level_2_system_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "子系统",
|
||||
"properties": {
|
||||
"名称": level_2_system_name,
|
||||
"子系统编码": level2_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = level_2_system_name
|
||||
|
||||
if level_1_system_name and level_2_system_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": level_1_system_name,
|
||||
"to_entity": level_2_system_name,
|
||||
}
|
||||
)
|
||||
|
||||
if device_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "设备",
|
||||
"properties": {
|
||||
"名称": device_name,
|
||||
"设备编码": device_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = device_name
|
||||
|
||||
if level_2_system_name and device_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": level_2_system_name,
|
||||
"to_entity": device_name,
|
||||
}
|
||||
)
|
||||
|
||||
if tool_name:
|
||||
entities.append(
|
||||
{
|
||||
"type": "零件",
|
||||
"properties": {
|
||||
"名称": tool_name,
|
||||
"零件编码": tool_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
low_level = tool_name
|
||||
|
||||
if device_name and tool_name:
|
||||
relationships.append(
|
||||
{
|
||||
"type": "包含",
|
||||
"from_entity": device_name,
|
||||
"to_entity": tool_name,
|
||||
}
|
||||
)
|
||||
|
||||
final_result = {
|
||||
"entities": entities,
|
||||
"relationships": relationships,
|
||||
"low_level": low_level,
|
||||
}
|
||||
|
||||
return final_result
|
||||
|
||||
|
||||
# 示例测试逻辑(非必须,仅供验证)
|
||||
if __name__ == "__main__":
|
||||
# 阀控蓄电池脉冲式快速充电装置
|
||||
test_list = [
|
||||
"163-06A0015-B01001_发动机-维修手册.pdf",
|
||||
|
||||
]
|
||||
for i in test_list:
|
||||
result = get_entity(i)
|
||||
lower_entity = result.get("low_level")
|
||||
print(result)
|
||||
print(lower_entity)
|
||||
#from neo4j import GraphDatabase
|
||||
|
||||
#NEO4J_URI = "bolt://192.168.0.111:7687"
|
||||
#NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
||||
#NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD","zdht123@") # 不设默认值,强制要求提供
|
||||
#NEO4J_DATABASE = os.getenv("NEO4J_DATABASE", "neo4j")
|
||||
#driver = GraphDatabase.driver(
|
||||
# NEO4J_URI,
|
||||
# auth=(NEO4J_USER, NEO4J_PASSWORD),
|
||||
# database=NEO4J_DATABASE
|
||||
#)
|
||||
#res = fetch_graph_sample(driver)
|
||||
#print(res)
|
||||
@ -5,7 +5,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Optional, Tuple, Dict
|
||||
import aiofiles # pip install aiofiles
|
||||
|
||||
from config import SEARCH_DIR,IMAGE_DIR
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -103,7 +103,6 @@ async def encode_images_to_base64(
|
||||
|
||||
async def process_document(
|
||||
input_file_name: str,
|
||||
search_dir: Optional[str] = "/app/mineru_output",
|
||||
local_image_dir: Optional[str] = None,
|
||||
concurrency: int = 32,
|
||||
) -> Optional[Tuple[list, Dict[str, str]]]:
|
||||
@ -120,6 +119,7 @@ async def process_document(
|
||||
成功: (content_list, images_dict)
|
||||
失败: None
|
||||
"""
|
||||
search_dir = SEARCH_DIR
|
||||
content, content_path = await find_and_read_content_list(search_dir, input_file_name)
|
||||
if content is None:
|
||||
logger.error("❌ 未找到指定的 _content_list.json 文件。")
|
||||
|
||||
14
jpgextract.py
Normal file
14
jpgextract.py
Normal file
@ -0,0 +1,14 @@
|
||||
from rapidocr_onnxruntime import RapidOCR
|
||||
|
||||
engine = RapidOCR()
|
||||
ocr_result, elapse = engine(r"E:\ZKYNLP\aidocproject\audiotransaction\gwdoc\图片.png")
|
||||
|
||||
texts = [item[1] for item in (ocr_result or [])]
|
||||
full_text = "图片包含的文字内容为:"+",".join(texts)
|
||||
|
||||
|
||||
|
||||
print("\n合并文本:")
|
||||
print(full_text)
|
||||
|
||||
print("\n耗时:", elapse)
|
||||
BIN
modelsAPI/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
modelsAPI/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
modelsAPI/__pycache__/model_api.cpython-311.pyc
Normal file
BIN
modelsAPI/__pycache__/model_api.cpython-311.pyc
Normal file
Binary file not shown.
@ -5,7 +5,6 @@ import os
|
||||
import numpy as np
|
||||
from openai import OpenAI
|
||||
from config import LLM_CONFIG, EMBEDDING_CONFIG
|
||||
from neo4j_graphrag.embeddings.base import Embedder
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
@ -141,7 +140,7 @@ class OpenaiAPI:
|
||||
# 2. 构建强约束的 System Prompt
|
||||
# 核心目标:禁止思考标签,禁止废话,只给结果
|
||||
system_prompt = (
|
||||
"你是一个工业视觉分析专家。你的任务是从图片中识别设备并诊断故障。\n"
|
||||
"你是一个视觉分析专家。你的任务是从图片中识别内容并进行描述。\n"
|
||||
"【严格约束】\n"
|
||||
"1. 绝对禁止输出 <think>, <think>, <reasoning> 等任何思考过程标签。\n"
|
||||
"2. 绝对禁止输出'好的'、'根据图片'、'分析如下'等开场白或结束语。\n"
|
||||
@ -151,10 +150,7 @@ class OpenaiAPI:
|
||||
# 3. 构建针对性的 User Prompt
|
||||
default_task = (
|
||||
"请分析这张图片,描述图片内容,重点关注以下信息\n"
|
||||
"1. 设备识别:图中是什么设备?型号或标签是否可见?\n"
|
||||
"2. 状态评估:仪表盘数值是否在正常区间?指示灯颜色是什么?\n"
|
||||
"3. 异常检测:是否存在漏油、生锈、电缆破损、螺丝松动或烟雾?\n"
|
||||
"4. 环境风险:周围是否有杂物堆放或安全隐患?\n"
|
||||
"1. 图片整体阐述了什么主题,描述了哪些内容\n"
|
||||
"请用专业、客观、简短的语言描述。"
|
||||
)
|
||||
|
||||
@ -185,7 +181,7 @@ class OpenaiAPI:
|
||||
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
|
||||
|
||||
try:
|
||||
print(f"🚀 正在调用 {model} 进行设备故障分析...")
|
||||
print(f"🚀 正在调用 {model} 进行图片内容分析...")
|
||||
response = await client.chat.completions.create(**kwargs)
|
||||
raw_content = response.choices[0].message.content or ""
|
||||
|
||||
@ -194,7 +190,7 @@ class OpenaiAPI:
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_msg = f"❌ 设备故障分析失败:{str(e)}\n{traceback.format_exc()}"
|
||||
error_msg = f"❌ 图片内容分析失败:{str(e)}\n{traceback.format_exc()}"
|
||||
print(error_msg)
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
@ -353,24 +349,7 @@ class OpenaiAPI:
|
||||
|
||||
return 0.5 * jaccard + 0.5 * len_penalty
|
||||
|
||||
class LocalBgeM3Embeddings(Embedder):
|
||||
def __init__(self, base_url: str, api_key: str, model_name: str = "bge-m3"):
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
self.model_name = model_name
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
return self.embed_documents([text])[0]
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
url = self.base_url
|
||||
with httpx.Client(timeout=60) as client:
|
||||
response = client.post(
|
||||
url,
|
||||
json={"model": self.model_name, "input": texts},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
)
|
||||
return [item["embedding"] for item in response.json()["data"]]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
BIN
received_images/88095d35078c40adb36984cb5090527d/图片.png
Normal file
BIN
received_images/88095d35078c40adb36984cb5090527d/图片.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
BIN
tools/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
tools/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/fault_record_db.cpython-311.pyc
Normal file
BIN
tools/__pycache__/fault_record_db.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/fault_statistics.cpython-311.pyc
Normal file
BIN
tools/__pycache__/fault_statistics.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/function_tool.cpython-311.pyc
Normal file
BIN
tools/__pycache__/function_tool.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/graph_tools.cpython-311.pyc
Normal file
BIN
tools/__pycache__/graph_tools.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/rag_tools.cpython-311.pyc
Normal file
BIN
tools/__pycache__/rag_tools.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/vlm_tools.cpython-311.pyc
Normal file
BIN
tools/__pycache__/vlm_tools.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/function_tracker.cpython-311.pyc
Normal file
BIN
utils/__pycache__/function_tracker.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/image_utils.cpython-311.pyc
Normal file
BIN
utils/__pycache__/image_utils.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/intent_matcher.cpython-311.pyc
Normal file
BIN
utils/__pycache__/intent_matcher.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/word_generator.cpython-311.pyc
Normal file
BIN
utils/__pycache__/word_generator.cpython-311.pyc
Normal file
Binary file not shown.
BIN
workflows/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
workflows/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
workflows/__pycache__/history_manager.cpython-311.pyc
Normal file
BIN
workflows/__pycache__/history_manager.cpython-311.pyc
Normal file
Binary file not shown.
BIN
workflows/__pycache__/workflow_baike.cpython-311.pyc
Normal file
BIN
workflows/__pycache__/workflow_baike.cpython-311.pyc
Normal file
Binary file not shown.
BIN
workflows/__pycache__/workflow_fault_diagnosis.cpython-311.pyc
Normal file
BIN
workflows/__pycache__/workflow_fault_diagnosis.cpython-311.pyc
Normal file
Binary file not shown.
BIN
workflows/__pycache__/workflow_utils.cpython-311.pyc
Normal file
BIN
workflows/__pycache__/workflow_utils.cpython-311.pyc
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user