aps-agent/server/aps_domain/drawing_process.py

330 lines
14 KiB
Python
Raw Normal View History

# ============================================================
# 图纸/工艺文档关键信息识别(moduleId: domain-drawing-process, 可重生 ✅)
# 只产出 PENDING_REVIEW 候选:工艺/工序、分段/组立、场地、装配顺序、
# 表面粗糙度、公差、螺纹、深度、材料等。标准工时/资源/数量一律不推断。
# ============================================================
from __future__ import annotations
import re
from hashlib import sha256
from typing import Any
PROCESS_VERSION = "drawing-process.v1"
_PROCESS_TERMS: tuple[tuple[re.Pattern[str], str, str, str], ...] = (
(re.compile(r"\bDREHEN\b|\bTURNING\b|\bTURN\b", re.IGNORECASE), "TURNING", "车削", "MACHINING"),
(re.compile(r"\bFR[AÄ]SEN\b|\bMILLING\b|\bMILL\b", re.IGNORECASE), "MILLING", "铣削", "MACHINING"),
(re.compile(r"\bBOHREN\b|\bDRILLING\b|\bDRILL\b", re.IGNORECASE), "DRILLING", "钻孔", "MACHINING"),
(re.compile(r"\bSCHLEIFEN\b|\bGRINDING\b|\bGRIND\b", re.IGNORECASE), "GRINDING", "磨削", "MACHINING"),
(
re.compile(r"\bGEWINDE\b|\bTHREAD(?:ING)?\b|[MG]\d+(?:[xX*]\d+(?:\.\d+)?)?(?!\s*[::]\s*\d)", re.IGNORECASE),
"THREADING", "螺纹加工", "MACHINING",
),
(re.compile(r"\bSCHWEISSEN\b|\bWELD(?:ING)?\b|焊接", re.IGNORECASE), "WELDING", "焊接", "JOINING"),
(re.compile(r"拼板|\bPANEL\s*FABRICATION\b", re.IGNORECASE), "PANEL_FABRICATION", "拼板", "FABRICATION"),
(re.compile(r"装焊|\bFIT\s*-?\s*UP\s*WELD", re.IGNORECASE), "FIT_UP_WELDING", "装焊", "FABRICATION"),
(re.compile(r"划线|\bMARK(?:ING)?\b", re.IGNORECASE), "MARKING", "划线", "FABRICATION"),
(re.compile(r"下料|\bCUTTING\b|\bCUT\b", re.IGNORECASE), "CUTTING", "下料", "FABRICATION"),
(re.compile(r"卷制|\bROLL(?:ING)?\b", re.IGNORECASE), "ROLLING", "卷制", "FABRICATION"),
(re.compile(r"翻身|\bTURN\s*-?\s*OVER\b", re.IGNORECASE), "TURNOVER", "翻身", "FABRICATION"),
(re.compile(r"反造|\bINVERTED\s*BUILD\b", re.IGNORECASE), "INVERTED_BUILD", "反造", "FABRICATION"),
(re.compile(r"舾装|\bOUTFIT(?:TING)?\b", re.IGNORECASE), "OUTFITTING", "舾装", "OUTFITTING"),
(re.compile(r"管系|\bPIPING\b|\bPIPE\b", re.IGNORECASE), "PIPING", "管系", "OUTFITTING"),
(re.compile(r"胎架|平胎位|\bJIG\s*SETUP\b", re.IGNORECASE), "JIG_SETUP", "胎架定位", "FABRICATION"),
(re.compile(r"大组|\bMAJOR\s*ASSEMBLY\b", re.IGNORECASE), "MAJOR_ASSEMBLY", "大组", "FABRICATION"),
(re.compile(r"单面焊|\bSINGLE\s*-?\s*SIDE\s*WELD", re.IGNORECASE), "SINGLE_SIDE_WELDING", "单面焊", "WELDING"),
(
re.compile(
r"\bMONTAGE\b|\bASSEMBLY\b|\bEINBAU\b|\bINSTALLATION\b|\bMOUNT(?:ING)?\b|"
r"\bANSCHLUSS\b|\bCONNECTION\b|装配|组装",
re.IGNORECASE,
),
"ASSEMBLY", "装配/连接", "ASSEMBLY",
),
(re.compile(r"\bENTGRATEN\b|\bDEBURRING\b|\bDEBURR\b", re.IGNORECASE), "DEBURRING", "去毛刺", "FINISH"),
(re.compile(r"\bOBERFL[AÄ]CHE\b|\bSURFACE\b", re.IGNORECASE), "SURFACE_TREATMENT", "表面处理", "SURFACE"),
(
re.compile(r"\bKORROSION\b|\bCORROSION\b|\bSCHUTZ\b|\bPROTECTION\b", re.IGNORECASE),
"CORROSION_PROTECTION", "防腐保护", "SURFACE",
),
)
_DETAIL_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
("surfaceRoughness", re.compile(r"\bR[aA]\s*\d+(?:\.\d+)?\b", re.IGNORECASE)),
("tolerance", re.compile(r"±\s*\d+(?:\.\d+)?|\bISO\s*2768\b|\bTOLERANZ\b|\bTOLERANCE\b", re.IGNORECASE)),
("threadSpecification", re.compile(r"\b[MG]\d+(?:[xX*]\d+(?:\.\d+)?)?(?!\s*[::]\s*\d)", re.IGNORECASE)),
("depthMm", re.compile(r"\b(\d+)\s*(?:DEEP|TIEF|T)\b", re.IGNORECASE)),
(
"materialSpecification",
re.compile(
r"\b(?:STAHL|STEEL|ALU(?:MINIUM)?|KUPFER|COPPER|EDELSTAHL|STAINLESS|MESSING|BRASS)\b",
re.IGNORECASE,
),
),
)
_BLOCK_GROUP_RE = re.compile(
r"(?P<name1>[\u4e00-\u9fff]{1,16}?)(?:分段)?[((]?(?P<code1>\d{3,}(?:[、,,、/\s]*\d{3,})*)[))]?(?:\s*分段)?"
r"|(?P<code2>\d{3,}(?:[、,,、/\s]*\d{3,})*)[\u4e00-\u9fff]{0,16}分段"
r"|FR\s*\d+[\u4e00-\u9fff]*组立",
re.IGNORECASE,
)
_ASSEMBLY_CUTS = (
"完成焊接后翻身装配到",
"翻身装配到",
"装配到",
"为基装配",
"先以",
"再装配",
"依次",
"然后",
"完成后",
"形成",
"分为",
"包含",
"主要",
"再",
"先",
"以",
"等",
)
_LOCATION_RE = re.compile(r"上建场地|联合厂房|平胎位|大组胎架|分片制作场地|片制作场地|分片场地")
_SEQUENCE_MARKER_RE = re.compile(r"先|再|然后|依次|完成后|完成|以[\u4e00-\u9fff]{1,12}为胎|根据|最后|随后|之后")
def _stable_id(kind: str, digest: str, suffix: str) -> str:
seed = f"{kind}:{digest}:{suffix}".encode()
return f"{kind}_{sha256(seed).hexdigest()[:20]}"
def _line_key(line: str) -> str:
return re.sub(r"\s+", " ", str(line or "").strip()).upper()
def _evidence(evidence: dict[str, Any], source: str, text: str) -> dict[str, Any]:
payload = {"source": source, "text": text}
for key, value in evidence.items():
if key == "source":
continue
payload[key] = value
return payload
def _scan_sources(
digest: str,
sources: list[tuple[str, dict[str, Any]]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
operations: dict[str, dict[str, Any]] = {}
details: dict[str, dict[str, Any]] = {}
for raw, evidence in sources:
line = str(raw or "").strip()
if not line:
continue
for pattern, code, name, category in _PROCESS_TERMS:
if not pattern.search(line):
continue
key = f"{code}:{_line_key(line)}"
if key in operations:
continue
operations[key] = {
"candidateId": _stable_id("processOperation", digest, key),
"candidateType": "PROCESS_OPERATION",
"status": "PENDING_REVIEW",
"reviewRequired": True,
"processCode": code,
"processName": name,
"category": category,
"operationCode": code,
"operationName": name,
"standardTime": None,
"resourceCode": None,
"confidence": 0.7 if evidence.get("source") == "drawingText" else 0.62,
"unknownFields": ["standardTime", "resourceCode", "operationDescription"],
"evidence": _evidence(evidence, str(evidence.get("source") or "drawingText"), line),
}
for field, pattern in _DETAIL_PATTERNS:
match = pattern.search(line)
if field == "tolerance":
numeric = re.search(r"±\s*\d+(?:\.\d+)?", line)
match = numeric or match
if not match:
continue
value = match.group(0)
key = f"{field}:{_line_key(line)}"
if key in details:
continue
details[key] = {
"candidateId": _stable_id("processDetail", digest, key),
"candidateType": "PROCESS_DETAIL",
"status": "PENDING_REVIEW",
"reviewRequired": True,
"field": field,
"value": value,
"confidence": 0.62,
"unknownFields": ["mappedMasterField"],
"evidence": _evidence(evidence, str(evidence.get("source") or "drawingText"), line),
}
ordered_ops = list(operations.values())
for index, row in enumerate(ordered_ops):
row["sequenceHint"] = (index + 1) * 10
return ordered_ops, list(details.values())
def _scan_structured(
blob: str,
digest: str,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
blocks: dict[str, dict[str, Any]] = {}
for match in _BLOCK_GROUP_RE.finditer(blob):
raw = match.group(0)
code = (match.group("code1") or match.group("code2") or "").strip()
if not code and raw.upper().startswith("FR"):
code = re.match(r"FR\s*\d+", raw, re.IGNORECASE).group(0)
if not code:
continue
name = (match.group("name1") or "").strip()
if not name and match.group("code2"):
name = re.sub(r"^[0-9、,,、/\s]+", "", raw).replace("分段", "").strip()
if not name and raw.upper().startswith("FR"):
name = re.sub(r"^FR\s*\d+", "", raw).replace("组立", "").strip()
key = _line_key(code)
if key in blocks:
continue
blocks[key] = {
"candidateId": _stable_id("processBlock", digest, key),
"candidateType": "BLOCK_GROUP",
"status": "PENDING_REVIEW",
"reviewRequired": True,
"blockCode": re.sub(r"\s+", "", code),
"blockName": name or None,
"confidence": 0.66,
"unknownFields": ["blockDrawing", "weight", "quantity"],
"evidence": {"source": "processText", "text": match.group(0)},
}
assemblies: dict[str, dict[str, Any]] = {}
for match in re.finditer(r"组立", blob):
start = max(0, match.start() - 20)
raw = blob[start:match.end()]
segments = re.split(r"[、,。;:/\s]+", raw)
name = segments[-1].strip() if segments else raw
name = re.sub(r"^\d+(?:\.\d+)?", "", name)
for separator in _ASSEMBLY_CUTS:
index = name.find(separator)
if index >= 0:
name = name[index + len(separator):]
break
name = re.split(r"[,,和)::]+", name)[-1].strip()
name = re.sub(r"(?:等|以及|、|。|;|:)$", "", name).strip("(())")[:16]
if name == "组立" or not name.endswith("组立") or not re.search(r"[\u4e00-\u9fff]{2}", name):
continue
key = _line_key(name)
if key in assemblies:
continue
assemblies[key] = {
"candidateId": _stable_id("processAssembly", digest, key),
"candidateType": "ASSEMBLY_GROUP",
"status": "PENDING_REVIEW",
"reviewRequired": True,
"assemblyName": name,
"confidence": 0.64,
"unknownFields": ["memberParts", "sequence", "drawing"],
"evidence": {"source": "processText", "text": name},
}
locations: dict[str, dict[str, Any]] = {}
for match in _LOCATION_RE.finditer(blob):
name = match.group(0)
key = _line_key(name)
if key in locations:
continue
locations[key] = {
"candidateId": _stable_id("processLocation", digest, key),
"candidateType": "PROCESS_LOCATION",
"status": "PENDING_REVIEW",
"reviewRequired": True,
"location": name,
"confidence": 0.7,
"evidence": {"source": "processText", "text": name},
}
sequence_steps: list[dict[str, Any]] = []
for index, sentence in enumerate(re.split(r"[。;\n]+", blob), start=1):
sentence = sentence.strip()
if not sentence:
continue
markers = sorted({m for m in _SEQUENCE_MARKER_RE.findall(sentence) if m})
if not markers:
continue
sequence_steps.append({
"candidateId": _stable_id("processSequence", digest, f"{index}:{_line_key(sentence)}"),
"candidateType": "PROCESS_SEQUENCE_STEP",
"status": "PENDING_REVIEW",
"reviewRequired": True,
"stepIndex": index,
"sentence": sentence,
"markers": markers,
"confidence": 0.6,
"unknownFields": ["parentBlock", "workstation", "duration"],
"evidence": {"source": "processText", "text": sentence},
})
return list(blocks.values()), list(assemblies.values()), list(locations.values()), sequence_steps
def recognize_process_text(text: str, *, source: str = "processDocument") -> dict[str, Any]:
"""Parse free-form process/construction text into structured reviewable candidates."""
digest = sha256(str(text or "").encode("utf-8")).hexdigest()
sentences = [part.strip() for part in re.split(r"[。;\n]+", str(text or "")) if part.strip()]
sources = [(sentence, {"source": source}) for sentence in sentences]
operations, details = _scan_sources(digest, sources)
blocks, assemblies, locations, sequence_steps = _scan_structured(str(text or ""), digest)
return {
"contractVersion": PROCESS_VERSION,
"operations": operations,
"details": details,
"blocks": blocks,
"assemblies": assemblies,
"locations": locations,
"sequenceSteps": sequence_steps,
"status": "PENDING_REVIEW",
"reviewRequired": True,
}
def recognize_process_candidates(
parsed: dict[str, Any],
part_list_rows: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Scan drawing text and part-list names for reviewable process/operation/detail candidates."""
asset = parsed.get("asset") or {}
digest = str(asset.get("sha256") or "")
sources: list[tuple[str, dict[str, Any]]] = []
text_parts: list[str] = []
for row in parsed.get("texts") or []:
evidence = dict(row.get("evidence") or {})
raw = str(row.get("text") or "")
sources.append((raw, {"source": "drawingText", **evidence}))
text_parts.append(raw)
for row in part_list_rows or []:
name = str(row.get("componentName") or "").strip()
if name:
evidence = dict(row.get("evidence") or {})
sources.append((name, {"source": "partListName", **evidence}))
text_parts.append(name)
operations, details = _scan_sources(digest, sources)
blocks, assemblies, locations, sequence_steps = _scan_structured("\n".join(text_parts), digest)
return {
"contractVersion": PROCESS_VERSION,
"operations": operations,
"details": details,
"blocks": blocks,
"assemblies": assemblies,
"locations": locations,
"sequenceSteps": sequence_steps,
"status": "PENDING_REVIEW",
"reviewRequired": True,
}
__all__ = ["PROCESS_VERSION", "recognize_process_candidates", "recognize_process_text"]