| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- #!/usr/bin/env python3
- from __future__ import annotations
- import argparse
- import json
- import shutil
- import subprocess
- import sys
- import tempfile
- import time
- from collections import Counter, defaultdict
- from datetime import date
- from pathlib import Path
- from urllib.parse import quote
- from urllib.request import urlopen
- from PIL import Image
- from docx import Document
- from docx.enum.section import WD_ORIENT
- from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
- from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
- from docx.oxml import OxmlElement
- from docx.oxml.ns import qn
- from docx.shared import Cm, Pt, RGBColor
- ROOT = Path(__file__).resolve().parent
- OUT_DIR = ROOT / "总结文档"
- FIG_DIR = OUT_DIR / "figures"
- DEFAULT_DOCX = OUT_DIR / "组态数据高质量演化过程故障标签手册.docx"
- BASE_URL = "http://127.0.0.1:5173/preview.html"
- API_URL = "http://127.0.0.1:8000/api/tag-manual/labels"
- CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
- BODY_FONT = "宋体"
- HEAD_FONT = "黑体"
- BODY_SIZE = 12.0
- TABLE_SIZE = 9.0
- DEVICE_ORDER = {"7号机": 7, "8号机": 8, "9号机": 9}
- STAGE_ORDER = {"轻微": 0, "异常": 1, "严重异常": 2}
- KIND_ORDER = {"pressure": 0, "vibration": 1}
- KIND_LABEL = {"pressure": "润滑油压力", "vibration": "振动"}
- def set_run_font(run, name: str = BODY_FONT, size: float = BODY_SIZE, bold: bool = False) -> None:
- run.font.name = name
- run.font.size = Pt(size)
- run.font.bold = bold
- run.font.color.rgb = RGBColor(0, 0, 0)
- run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), name)
- def set_style_font(style, name: str, size: float, bold: bool) -> None:
- style.font.name = name
- style.font.size = Pt(size)
- style.font.bold = bold
- style.font.color.rgb = RGBColor(0, 0, 0)
- style.element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), name)
- def configure_document(doc: Document) -> None:
- section = doc.sections[0]
- section.orientation = WD_ORIENT.LANDSCAPE
- section.page_width = Cm(29.7)
- section.page_height = Cm(21.0)
- section.top_margin = Cm(2.0)
- section.bottom_margin = Cm(2.0)
- section.left_margin = Cm(2.0)
- section.right_margin = Cm(2.0)
- normal = doc.styles["Normal"]
- set_style_font(normal, BODY_FONT, BODY_SIZE, False)
- normal.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
- normal.paragraph_format.space_before = Pt(0)
- normal.paragraph_format.space_after = Pt(0)
- heading1 = doc.styles["Heading 1"]
- set_style_font(heading1, HEAD_FONT, 16, True)
- heading1.paragraph_format.space_before = Pt(18)
- heading1.paragraph_format.space_after = Pt(10)
- heading1.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
- heading1.paragraph_format.keep_with_next = True
- heading2 = doc.styles["Heading 2"]
- set_style_font(heading2, HEAD_FONT, 14, True)
- heading2.paragraph_format.space_before = Pt(12)
- heading2.paragraph_format.space_after = Pt(6)
- heading2.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
- heading2.paragraph_format.keep_with_next = True
- add_footer(doc)
- def add_footer(doc: Document) -> None:
- section = doc.sections[0]
- footer = section.footer
- paragraph = footer.paragraphs[0]
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- run = paragraph.add_run("组态数据高质量演化过程故障标签手册 第 ")
- set_run_font(run, BODY_FONT, 9)
- field = OxmlElement("w:fldSimple")
- field.set(qn("w:instr"), "PAGE")
- paragraph._p.append(field)
- tail = paragraph.add_run(" 页")
- set_run_font(tail, BODY_FONT, 9)
- def body_paragraph(doc: Document, text: str, bold_prefix: str = "") -> None:
- paragraph = doc.add_paragraph()
- paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
- paragraph.paragraph_format.space_before = Pt(0)
- paragraph.paragraph_format.space_after = Pt(6)
- if bold_prefix and text.startswith(bold_prefix):
- first = paragraph.add_run(bold_prefix)
- set_run_font(first, BODY_FONT, BODY_SIZE, True)
- rest = paragraph.add_run(text[len(bold_prefix):])
- set_run_font(rest, BODY_FONT, BODY_SIZE)
- else:
- run = paragraph.add_run(text)
- set_run_font(run, BODY_FONT, BODY_SIZE)
- def set_cell_margins(cell, left: int = 60, right: int = 60, top: int = 30, bottom: int = 30) -> None:
- tc_pr = cell._tc.get_or_add_tcPr()
- mar = tc_pr.first_child_found_in("w:tcMar")
- if mar is None:
- mar = OxmlElement("w:tcMar")
- tc_pr.append(mar)
- for key, value in (("w:left", left), ("w:right", right), ("w:top", top), ("w:bottom", bottom)):
- node = mar.find(qn(key))
- if node is None:
- node = OxmlElement(key)
- mar.append(node)
- node.set(qn("w:w"), str(value))
- node.set(qn("w:type"), "dxa")
- def fix_table_layout(table, widths: list[float]) -> None:
- tbl_pr = table._tbl.tblPr
- layout = OxmlElement("w:tblLayout")
- layout.set(qn("w:type"), "fixed")
- tbl_pr.append(layout)
- tbl_w = tbl_pr.find(qn("w:tblW"))
- if tbl_w is None:
- tbl_w = OxmlElement("w:tblW")
- tbl_pr.append(tbl_w)
- tbl_w.set(qn("w:w"), str(int(sum(widths) * 567)))
- tbl_w.set(qn("w:type"), "dxa")
- for index, column in enumerate(table._tbl.tblGrid.gridCol_lst):
- column.set(qn("w:w"), str(int(widths[index] * 567)))
- def no_split(row) -> None:
- tr_pr = row._tr.get_or_add_trPr()
- cant = OxmlElement("w:cantSplit")
- tr_pr.append(cant)
- def repeat_header(row) -> None:
- tr_pr = row._tr.get_or_add_trPr()
- header = OxmlElement("w:tblHeader")
- tr_pr.append(header)
- def add_table(
- doc: Document,
- headers: list[str],
- rows: list[list[str]],
- widths: list[float],
- font_size: float = TABLE_SIZE,
- align_center: bool = False,
- ) -> None:
- table = doc.add_table(rows=1, cols=len(headers))
- table.style = "Table Grid"
- table.alignment = WD_TABLE_ALIGNMENT.CENTER
- table.autofit = False
- fix_table_layout(table, widths)
- header = table.rows[0]
- repeat_header(header)
- no_split(header)
- for index, text in enumerate(headers):
- cell = header.cells[index]
- cell.width = Cm(widths[index])
- set_cell_margins(cell)
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
- paragraph = cell.paragraphs[0]
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
- run = paragraph.add_run(text)
- set_run_font(run, BODY_FONT, font_size, True)
- for values in rows:
- row = table.add_row()
- no_split(row)
- for index, text in enumerate(values):
- cell = row.cells[index]
- cell.width = Cm(widths[index])
- set_cell_margins(cell)
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
- paragraph = cell.paragraphs[0]
- paragraph.alignment = (
- WD_ALIGN_PARAGRAPH.CENTER if align_center or index > 1 else WD_ALIGN_PARAGRAPH.LEFT
- )
- paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
- run = paragraph.add_run(str(text))
- set_run_font(run, BODY_FONT, font_size)
- doc.add_paragraph()
- def add_attr_table(doc: Document, fields: list[dict]) -> None:
- widths = [2.0, 6.5, 2.0, 6.5, 2.0, 6.5]
- table = doc.add_table(rows=0, cols=6)
- table.style = "Table Grid"
- table.alignment = WD_TABLE_ALIGNMENT.CENTER
- table.autofit = False
- fix_table_layout(table, widths)
- row = None
- pair = 0
- def new_row():
- nonlocal row, pair
- row = table.add_row()
- no_split(row)
- pair = 0
- for index, width in enumerate(widths):
- cell = row.cells[index]
- cell.width = Cm(width)
- set_cell_margins(cell)
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
- for field in fields:
- label = str(field.get("label") or field.get("key") or "")
- value = str(field.get("value") or "—")
- span = int(field.get("span") or 1)
- if span >= 2:
- if row is not None and pair > 0:
- row = None
- new_row()
- label_cell = row.cells[0]
- value_cell = row.cells[1].merge(row.cells[5])
- for cell, text, bold in ((label_cell, label, True), (value_cell, value, False)):
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
- paragraph = cell.paragraphs[0]
- paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
- run = paragraph.add_run(text)
- set_run_font(run, BODY_FONT, TABLE_SIZE, bold)
- row = None
- pair = 0
- continue
- if row is None or pair >= 3:
- new_row()
- label_cell = row.cells[pair * 2]
- value_cell = row.cells[pair * 2 + 1]
- for cell, text, bold in ((label_cell, label, True), (value_cell, value, False)):
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
- paragraph = cell.paragraphs[0]
- paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
- run = paragraph.add_run(text)
- set_run_font(run, BODY_FONT, TABLE_SIZE, bold)
- pair += 1
- doc.add_paragraph()
- def load_labels() -> list[dict]:
- with urlopen(API_URL, timeout=20) as response:
- payload = json.load(response)
- return payload["items"]
- def fetch_ready() -> None:
- for name, url in (
- ("后端服务", "http://127.0.0.1:8000/api/tag-manual/labels"),
- ("前端预览", "http://127.0.0.1:5173/preview.html"),
- ):
- try:
- with urlopen(url, timeout=5):
- pass
- except Exception as error:
- raise SystemExit(
- f"{name}未启动或不可访问:{error}\n"
- "请先启动:uvicorn app.main:app --app-dir backend --port 8000\n"
- "并启动:cd frontend && npm run dev"
- ) from error
- def screenshot(label: dict, profile_dir: Path, force: bool = False) -> Path:
- FIG_DIR.mkdir(parents=True, exist_ok=True)
- jpg_path = FIG_DIR / f"{label['key'].replace(':', '_')}.jpg"
- if jpg_path.exists() and not force:
- return jpg_path
- png_path = FIG_DIR / f"{label['key'].replace(':', '_')}.png"
- url = f"{BASE_URL}?key={quote(label['key'])}&chart=1"
- command = [
- CHROME,
- "--headless=new",
- "--disable-gpu",
- "--hide-scrollbars",
- "--window-size=1800,900",
- "--force-device-scale-factor=2",
- "--virtual-time-budget=20000",
- "--run-all-compositor-stages-before-draw",
- f"--user-data-dir={profile_dir}",
- f"--screenshot={png_path}",
- url,
- ]
- for attempt in range(2):
- subprocess.run(
- command,
- check=True,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- timeout=120,
- )
- if png_path.exists() and png_path.stat().st_size > 5000:
- break
- time.sleep(1)
- if not png_path.exists() or png_path.stat().st_size <= 5000:
- raise RuntimeError(f"截图失败:{label['key']}")
- image = Image.open(png_path).convert("RGB")
- image = image.resize((2200, 1100), Image.LANCZOS)
- image.save(jpg_path, quality=90, optimize=True)
- png_path.unlink(missing_ok=True)
- return jpg_path
- def cycle_name(label: dict) -> str:
- cycle = label.get("cycle") or ""
- unit = label.get("device") or ""
- prefix = f"{unit}-"
- return cycle[len(prefix):] if cycle.startswith(prefix) else cycle
- def sort_key(label: dict) -> tuple:
- unit = label.get("device") or ""
- return (
- DEVICE_ORDER.get(unit, 99),
- KIND_ORDER.get(label.get("kind"), 9),
- label.get("start") or "",
- )
- def add_cover(doc: Document, labels: list[dict]) -> None:
- for _ in range(4):
- doc.add_paragraph()
- paragraph = doc.add_paragraph()
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- run = paragraph.add_run("组态数据高质量演化过程")
- set_run_font(run, HEAD_FONT, 26, True)
- paragraph = doc.add_paragraph()
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- run = paragraph.add_run("故障标签手册")
- set_run_font(run, HEAD_FONT, 26, True)
- doc.add_paragraph()
- paragraph = doc.add_paragraph()
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- run = paragraph.add_run("润滑油压力故障 · 振动故障")
- set_run_font(run, BODY_FONT, 16, False)
- for _ in range(6):
- doc.add_paragraph()
- paragraph = doc.add_paragraph()
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- run = paragraph.add_run(f"共 {len(labels)} 条演化标签")
- set_run_font(run, BODY_FONT, 12, False)
- paragraph = doc.add_paragraph()
- paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- run = paragraph.add_run(f"{date.today().year} 年 {date.today().month} 月")
- set_run_font(run, BODY_FONT, 12, False)
- doc.add_page_break()
- def add_reading_guide(doc: Document) -> None:
- doc.add_heading("文档阅读说明", level=1)
- body_paragraph(
- doc,
- "用途:本手册汇总设备运行过程中由规则扫描形成的润滑油压力、振动两类演化标签,"
- "逐条给出标签属性、演化阶段和标签前后运行曲线,用于复核、诊断和维修决策辅助。",
- "用途:",
- )
- body_paragraph(
- doc,
- "阅读方法:先阅读标签数量总结,了解总体分布和演化过程;再按章节、机组或阶段查阅详情。"
- "在 Word 中打开导航窗格,可通过一级、二级标题直接跳转到对应章节或标签。",
- "阅读方法:",
- )
- body_paragraph(
- doc,
- "排序方式:详情部分分为润滑油压力、振动两章,章内按机组和开始时间排序;"
- "同一标签的所有属性集中在一张表中,图表位于属性表下方。",
- "排序方式:",
- )
- body_paragraph(
- doc,
- "数据口径:标签来自演化阶段统计记录;曲线来自设备运行监测数据,只使用运行样本,"
- "停机、坏点和无效值不参与曲线绘制。标签是规则扫描结果,不等同于物理故障结论。",
- "数据口径:",
- )
- doc.add_heading("图形解释", level=2)
- rows = [
- ["红色背景", "当前标签对应的故障阶段,画在当前类型子图上。"],
- ["淡黄色背景", "总表中其他故障标签与当前展示窗口重合的时间段;与红色故障段重叠的部分不重复标注。"],
- ["竖向虚线", "停机边界。两条虚线为一组,左侧为关机,右侧为开机。"],
- ["标题下备注", "故障段时间、图表展示时间,以及各停机段的关机时间和开机时间。"],
- ["相对基线子图", "润滑油压力按周期基线计算百分比;振动按近期基线计算倍数;虚线为基线。"],
- ["原值子图", "测点实际值,用于核对曲线形态和数值范围。"],
- ["右上角角标", "标签阶段:轻微、异常或严重异常。"],
- ["底部滑块", "时间轴缩放控件,默认显示全部时间范围。"],
- ["子图", "每个子图只画一条曲线,所有子图共用同一时间轴。"],
- ]
- add_table(doc, ["图形元素", "含义"], rows, [5.0, 20.5], font_size=9.5)
- body_paragraph(
- doc,
- "展示范围:故障段前后各补充 6 小时有效运行数据。若期间发生停机,会继续向更早或更晚的"
- "运行数据取数,停机段压缩为空档并用竖向虚线标出。停机后 20 分钟的启动瞬态在故障段外"
- "不展示,故障段内的数据不剔除。",
- "展示范围:",
- )
- body_paragraph(
- doc,
- "每条标签均附属性表和曲线图;曲线用于核对标签前后的运行状态和演化过程。",
- "图表说明:",
- )
- def add_summary(doc: Document, labels: list[dict]) -> None:
- doc.add_heading("一、标签数量总结", level=1)
- body_paragraph(
- doc,
- f"本手册共收录 {len(labels)} 条演化标签,覆盖润滑油压力故障和振动故障两类,"
- "按机组和阶段统计如下。",
- )
- kind_counts = Counter(label["kindLabel"] for label in labels)
- device_kind = Counter((label["device"], label["kindLabel"]) for label in labels)
- device_stage = Counter((label["device"], label["stage"]) for label in labels)
- doc.add_heading("1.1 按机组统计", level=2)
- rows = []
- for device in ("7号机", "8号机", "9号机"):
- pressure = device_kind.get((device, "润滑油压力"), 0)
- vibration = device_kind.get((device, "振动"), 0)
- rows.append([device, str(pressure), str(vibration), str(pressure + vibration)])
- rows.append([
- "合计",
- str(kind_counts.get("润滑油压力", 0)),
- str(kind_counts.get("振动", 0)),
- str(len(labels)),
- ])
- add_table(doc, ["机组", "润滑油压力", "振动", "合计"], rows, [6.0, 6.0, 6.0, 6.0], align_center=True)
- doc.add_heading("1.2 按机组和阶段统计", level=2)
- rows = []
- for device in ("7号机", "8号机", "9号机"):
- values = [device_stage.get((device, stage), 0) for stage in ("轻微", "异常", "严重异常")]
- rows.append([device, *[str(value) for value in values], str(sum(values))])
- total_stage = Counter(label["stage"] for label in labels)
- rows.append([
- "合计",
- *[str(total_stage.get(stage, 0)) for stage in ("轻微", "异常", "严重异常")],
- str(len(labels)),
- ])
- add_table(
- doc,
- ["机组", "轻微", "异常", "严重异常", "合计"],
- rows,
- [6.0, 4.5, 4.5, 5.0, 5.0],
- align_center=True,
- )
- doc.add_heading("1.3 总体演化过程", level=2)
- pressure = kind_counts.get("润滑油压力", 0)
- vibration = kind_counts.get("振动", 0)
- slight = total_stage.get("轻微", 0)
- abnormal = total_stage.get("异常", 0)
- severe = total_stage.get("严重异常", 0)
- min_start = min(label["start"] for label in labels)
- max_end = max(label["end"] for label in labels)
- body_paragraph(
- doc,
- f"标签时间覆盖 {min_start[:10]} 至 {max_end[:10]}。润滑油压力 {pressure} 条,"
- f"占 {pressure / len(labels) * 100:.1f}%;振动 {vibration} 条,占 "
- f"{vibration / len(labels) * 100:.1f}%。",
- )
- body_paragraph(
- doc,
- f"按阶段看,轻微 {slight} 条、异常 {abnormal} 条、严重异常 {severe} 条。"
- "多数标签处于轻微阶段,异常和严重异常数量较少,符合演化过程由轻到重逐步发展的特点。",
- )
- body_paragraph(
- doc,
- "润滑油压力标签以轻微阶段为主,异常和严重异常主要出现在持续下降过程中;"
- "振动标签在轻微、异常和严重异常三个阶段均有分布,说明振动异常既可能表现为短时冲击,"
- "也可能持续发展。",
- )
- body_paragraph(
- doc,
- "阅读详情时,应结合周期基线、相对变化、连续有效小时数、报警小时数和阶段触发依据,"
- "判断标签是单次波动还是持续演化,并结合现场维修记录进行确认。",
- )
- rows = [
- ["轻微", "相对基线偏离达到轻微门槛,持续时间较短或尚未形成连续确认。"],
- ["异常", "相对基线偏离扩大,连续有效小时数增加,形成异常阶段。"],
- ["严重异常", "达到严重门槛或触发低低报、高高报,应优先复核。"],
- ]
- add_table(doc, ["阶段", "含义"], rows, [4.0, 21.5], font_size=9.5)
- def add_detail_chapters(
- doc: Document,
- labels: list[dict],
- profile_dir: Path,
- force: bool,
- limit: int,
- single_image: str,
- ) -> None:
- chapters = (
- ("pressure", "二、润滑油压力故障标签详情", "润滑油压力"),
- ("vibration", "三、振动故障标签详情", "振动"),
- )
- for chapter_index, (kind, title, kind_label) in enumerate(chapters):
- items = sorted(
- [label for label in labels if label.get("kind") == kind],
- key=sort_key,
- )
- if limit:
- items = items[:limit]
- doc.add_page_break()
- doc.add_heading(title, level=1)
- stage_counts = Counter(item.get("stage") for item in items)
- body_paragraph(
- doc,
- f"本章共 {len(items)} 条{kind_label}故障标签,其中轻微 {stage_counts.get('轻微', 0)} 条、"
- f"异常 {stage_counts.get('异常', 0)} 条、严重异常 {stage_counts.get('严重异常', 0)} 条。"
- "章内按机组和开始时间排列;每个二级标题对应一条标签,标题下方为属性表。"
- "每条标签均附属性表和曲线图。",
- )
- for index, label in enumerate(items, start=1):
- print(f"[{kind} {index}/{len(items)}] {label['title']}")
- if index > 1:
- doc.add_page_break()
- heading = (
- f"{label['device']}-{label['kindLabel']}-{cycle_name(label)}-{label['start'][:16]}"
- )
- doc.add_heading(heading, level=2)
- add_attr_table(doc, label.get("fields") or [])
- if not single_image or label["key"] == single_image:
- image_path = screenshot(label, profile_dir, force=force)
- picture_paragraph = doc.add_paragraph()
- picture_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
- picture_paragraph.paragraph_format.space_before = Pt(4)
- picture_paragraph.paragraph_format.space_after = Pt(2)
- picture_paragraph.paragraph_format.keep_with_next = True
- run = picture_paragraph.add_run()
- run.add_picture(str(image_path), width=Cm(24.0))
- caption = doc.add_paragraph()
- caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
- caption.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
- caption.paragraph_format.space_after = Pt(6)
- run = caption.add_run("图:标签前后运行曲线(红色为故障段,竖向虚线为关机/开机边界)")
- set_run_font(run, BODY_FONT, 9.0)
- def main() -> None:
- parser = argparse.ArgumentParser()
- parser.add_argument("--limit", type=int, default=0, help="只生成前 N 条,用于冒烟测试")
- parser.add_argument("--output", type=Path, default=DEFAULT_DOCX)
- parser.add_argument("--force", action="store_true", help="强制重新截图")
- parser.add_argument(
- "--single-image",
- default="",
- help="只插入该标签的曲线图;传空字符串表示全部插入",
- )
- args = parser.parse_args()
- fetch_ready()
- labels = load_labels()
- print(f"标签总数:{len(labels)}")
- args.output.parent.mkdir(parents=True, exist_ok=True)
- profile_dir = Path(tempfile.mkdtemp(prefix="tag_manual_chrome_"))
- try:
- doc = Document()
- configure_document(doc)
- add_cover(doc, labels)
- add_reading_guide(doc)
- add_summary(doc, labels)
- add_detail_chapters(
- doc,
- labels,
- profile_dir,
- args.force,
- args.limit,
- args.single_image,
- )
- doc.save(args.output)
- print(f"已生成:{args.output}")
- print(f"截图目录:{FIG_DIR}")
- finally:
- shutil.rmtree(profile_dir, ignore_errors=True)
- if __name__ == "__main__":
- main()
|