docs: build company profile knowledge base
This commit is contained in:
597
博志金钻项目基本情况_知识库/wiki/_system/build_company_kb.py
Normal file
597
博志金钻项目基本情况_知识库/wiki/_system/build_company_kb.py
Normal file
@@ -0,0 +1,597 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a local multimodal KB from BZJZ company PPTX files.
|
||||
|
||||
The script reads PPTX files as zip/XML packages, extracts slide text and
|
||||
embedded images, renders each slide through LibreOffice + pdftoppm, and writes
|
||||
Markdown source notes plus image indexes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as _dt
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import zipfile
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SOURCE_DIR = ROOT / "博志金钻项目基本情况"
|
||||
KB_DIR = ROOT / "博志金钻项目基本情况_知识库"
|
||||
WIKI_DIR = KB_DIR / "wiki"
|
||||
ASSET_DIR = KB_DIR / "assets"
|
||||
TMP_DIR = KB_DIR / "_tmp_render"
|
||||
INGEST_TIME = os.environ.get("KB_INGEST_TIME") or _dt.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
|
||||
|
||||
PPT_SLUGS = {
|
||||
"2025_3_5_苏州博志金钻-MEMS传感器智能制造基地-项目计划书.pptx": (
|
||||
"ppt01_mems_sensor_base_plan",
|
||||
"MEMS传感器智能制造基地项目计划书",
|
||||
),
|
||||
"※2024_2_24-苏州博志金钻-V终-苹果验厂产品介绍.pptx": (
|
||||
"ppt02_apple_audit_product_intro",
|
||||
"苹果验厂产品介绍",
|
||||
),
|
||||
"※2025_4_12_苏州博志金钻科技有限责任公司介绍材料 .pptx": (
|
||||
"ppt03_company_intro_20250412",
|
||||
"苏州博志金钻科技有限责任公司介绍材料 2025-04-12",
|
||||
),
|
||||
"※2025_4_15_苏州博志金钻科技有限责任公司介绍(tc).pptx": (
|
||||
"ppt04_company_intro_tc_20250415",
|
||||
"苏州博志金钻科技有限责任公司介绍 2025-04-15 tc",
|
||||
),
|
||||
}
|
||||
|
||||
NS = {
|
||||
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
||||
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
|
||||
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
"rel": "http://schemas.openxmlformats.org/package/2006/relationships",
|
||||
}
|
||||
|
||||
SECTION_KEYWORDS = {
|
||||
"目录",
|
||||
"市场分析",
|
||||
"投资规划",
|
||||
"产品与技术方案",
|
||||
"建设与运营方案",
|
||||
"财务预测",
|
||||
"About Us",
|
||||
}
|
||||
|
||||
CORE_KEYWORDS = [
|
||||
"About Us",
|
||||
"公司",
|
||||
"创新",
|
||||
"技术",
|
||||
"产品",
|
||||
"产品管线",
|
||||
"陶瓷载板",
|
||||
"TEC",
|
||||
"DPC",
|
||||
"激光热沉",
|
||||
"光通讯",
|
||||
"传感器",
|
||||
"MEMS",
|
||||
"市场",
|
||||
"投资",
|
||||
"设备",
|
||||
"工艺",
|
||||
"产线",
|
||||
"制造",
|
||||
"质量",
|
||||
"ISO",
|
||||
"客户",
|
||||
"600+",
|
||||
"芯片",
|
||||
"散热",
|
||||
"热沉",
|
||||
"基地",
|
||||
"规划",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageRecord:
|
||||
image_id: str
|
||||
source_type: str
|
||||
source_ppt: str
|
||||
ppt_slug: str
|
||||
slide_no: int
|
||||
rel_path: str
|
||||
abs_path: Path
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
sha256: str | None = None
|
||||
note_path: str | None = None
|
||||
title: str | None = None
|
||||
reuse: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SlideRecord:
|
||||
no: int
|
||||
title: str
|
||||
texts: list[str]
|
||||
notes: list[str] = field(default_factory=list)
|
||||
embedded_images: list[ImageRecord] = field(default_factory=list)
|
||||
preview: ImageRecord | None = None
|
||||
core_score: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class PPTRecord:
|
||||
path: Path
|
||||
slug: str
|
||||
title: str
|
||||
slides: list[SlideRecord]
|
||||
|
||||
|
||||
def rel(path: Path) -> str:
|
||||
return path.relative_to(WIKI_DIR).as_posix()
|
||||
|
||||
|
||||
def kb_rel(path: Path) -> str:
|
||||
return path.relative_to(KB_DIR).as_posix()
|
||||
|
||||
|
||||
def clean_text(s: str) -> str:
|
||||
s = re.sub(r"\s+", " ", s or "").strip()
|
||||
s = s.replace(" :", ":").replace(" ,", ",").replace(" 。", "。")
|
||||
return s
|
||||
|
||||
|
||||
def md_escape(s: str) -> str:
|
||||
return s.replace("|", "\\|")
|
||||
|
||||
|
||||
def slide_sort_key(name: str) -> int:
|
||||
return int(re.search(r"slide(\d+)\.xml$", name).group(1))
|
||||
|
||||
|
||||
def image_info(path: Path) -> tuple[int | None, int | None]:
|
||||
try:
|
||||
with Image.open(path) as im:
|
||||
return im.size
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def resolve_target(base: str, target: str) -> str:
|
||||
if target.startswith("/"):
|
||||
return target.lstrip("/")
|
||||
base_dir = Path(base).parent
|
||||
return (base_dir / target).as_posix().replace("ppt/slides/../", "ppt/")
|
||||
|
||||
|
||||
def read_rels(z: zipfile.ZipFile, rels_name: str) -> dict[str, str]:
|
||||
if rels_name not in z.namelist():
|
||||
return {}
|
||||
root = ET.fromstring(z.read(rels_name))
|
||||
out: dict[str, str] = {}
|
||||
for rel_el in root.findall("rel:Relationship", NS):
|
||||
rid = rel_el.attrib.get("Id")
|
||||
target = rel_el.attrib.get("Target")
|
||||
if rid and target:
|
||||
out[rid] = target
|
||||
return out
|
||||
|
||||
|
||||
def extract_texts(root: ET.Element) -> list[str]:
|
||||
raw = [clean_text(t.text or "") for t in root.findall(".//a:t", NS)]
|
||||
texts = [t for t in raw if t]
|
||||
merged: list[str] = []
|
||||
for t in texts:
|
||||
if not merged or merged[-1] != t:
|
||||
merged.append(t)
|
||||
return merged
|
||||
|
||||
|
||||
def parse_xml(data: bytes) -> ET.Element:
|
||||
try:
|
||||
return ET.fromstring(data)
|
||||
except ET.ParseError:
|
||||
text = data.decode("utf-8", errors="ignore").strip()
|
||||
if text.endswith("<p:sld/>") or text.endswith("<p:sld />"):
|
||||
return ET.fromstring(f'<p:sld xmlns:p="{NS["p"]}"/>')
|
||||
raise
|
||||
|
||||
|
||||
def title_from_texts(texts: list[str], fallback: str) -> str:
|
||||
for t in texts:
|
||||
if len(t) >= 2 and not re.fullmatch(r"\d+|[A-Za-z]{1,3}", t):
|
||||
return t[:80]
|
||||
return fallback
|
||||
|
||||
|
||||
def core_score(text: str, slide_no: int) -> int:
|
||||
score = 0
|
||||
for kw in CORE_KEYWORDS:
|
||||
if kw.lower() in text.lower():
|
||||
score += 2
|
||||
if any(k in text for k in SECTION_KEYWORDS) and len(text) < 120:
|
||||
score -= 3
|
||||
if slide_no <= 2:
|
||||
score -= 1
|
||||
return score
|
||||
|
||||
|
||||
def extract_pptx(ppt: Path) -> PPTRecord:
|
||||
slug, title = PPT_SLUGS.get(ppt.name, (ppt.stem, ppt.stem))
|
||||
slides: list[SlideRecord] = []
|
||||
media_dir = ASSET_DIR / "media" / slug
|
||||
media_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(ppt) as z:
|
||||
names = set(z.namelist())
|
||||
slide_names = sorted(
|
||||
[n for n in names if re.match(r"ppt/slides/slide\d+\.xml$", n)],
|
||||
key=slide_sort_key,
|
||||
)
|
||||
for slide_name in slide_names:
|
||||
slide_no = slide_sort_key(slide_name)
|
||||
root = parse_xml(z.read(slide_name))
|
||||
texts = extract_texts(root)
|
||||
title_text = title_from_texts(texts, f"Slide {slide_no}")
|
||||
record = SlideRecord(
|
||||
no=slide_no,
|
||||
title=title_text,
|
||||
texts=texts,
|
||||
core_score=core_score(" ".join(texts), slide_no),
|
||||
)
|
||||
|
||||
rels_name = f"ppt/slides/_rels/slide{slide_no}.xml.rels"
|
||||
rels = read_rels(z, rels_name)
|
||||
seen: set[str] = set()
|
||||
pic_no = 0
|
||||
for blip in root.findall(".//a:blip", NS):
|
||||
rid = blip.attrib.get(f"{{{NS['r']}}}embed")
|
||||
if not rid or rid not in rels:
|
||||
continue
|
||||
target = resolve_target(slide_name, rels[rid])
|
||||
if target not in names or target in seen:
|
||||
continue
|
||||
seen.add(target)
|
||||
pic_no += 1
|
||||
ext = Path(target).suffix or ".bin"
|
||||
out_name = f"{slug}_S{slide_no:03d}_IMG{pic_no:02d}{ext.lower()}"
|
||||
out_path = media_dir / out_name
|
||||
out_path.write_bytes(z.read(target))
|
||||
w, h = image_info(out_path)
|
||||
rec = ImageRecord(
|
||||
image_id=f"IMG-{slug}-S{slide_no:03d}-{pic_no:02d}",
|
||||
source_type="embedded",
|
||||
source_ppt=ppt.name,
|
||||
ppt_slug=slug,
|
||||
slide_no=slide_no,
|
||||
rel_path=kb_rel(out_path),
|
||||
abs_path=out_path,
|
||||
width=w,
|
||||
height=h,
|
||||
sha256=sha256(out_path),
|
||||
title=f"{title_text} - 内嵌图片 {pic_no}",
|
||||
reuse="按图片内容复核后,可用于公司介绍、产品展示、设备/工艺说明或客户验厂材料。",
|
||||
)
|
||||
record.embedded_images.append(rec)
|
||||
|
||||
slides.append(record)
|
||||
|
||||
return PPTRecord(path=ppt, slug=slug, title=title, slides=slides)
|
||||
|
||||
|
||||
def render_slide_previews(records: list[PPTRecord]) -> None:
|
||||
libreoffice = shutil.which("libreoffice") or shutil.which("soffice")
|
||||
pdftoppm = shutil.which("pdftoppm")
|
||||
if not libreoffice or not pdftoppm:
|
||||
print("WARN: LibreOffice or pdftoppm missing; slide previews skipped", file=sys.stderr)
|
||||
return
|
||||
TMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(ASSET_DIR / "slides").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for ppt in records:
|
||||
pdf_dir = TMP_DIR / ppt.slug
|
||||
pdf_dir.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(
|
||||
[libreoffice, "--headless", "--convert-to", "pdf", "--outdir", str(pdf_dir), str(ppt.path)],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
pdfs = sorted(pdf_dir.glob("*.pdf"))
|
||||
if not pdfs:
|
||||
print(f"WARN: no pdf generated for {ppt.path}", file=sys.stderr)
|
||||
continue
|
||||
pdf = pdfs[0]
|
||||
out_dir = ASSET_DIR / "slides" / ppt.slug
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
prefix = out_dir / "page"
|
||||
subprocess.run(
|
||||
[pdftoppm, "-jpeg", "-jpegopt", "quality=90", "-r", "144", str(pdf), str(prefix)],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
rendered = sorted(out_dir.glob("page-*.jpg"), key=lambda p: int(re.search(r"-(\d+)\.jpg$", p.name).group(1)))
|
||||
for idx, path in enumerate(rendered, start=1):
|
||||
target = out_dir / f"slide-{idx:03d}.jpg"
|
||||
path.replace(target)
|
||||
for leftover in out_dir.glob("page-*.jpg"):
|
||||
leftover.unlink(missing_ok=True)
|
||||
|
||||
for slide in ppt.slides:
|
||||
image_path = out_dir / f"slide-{slide.no:03d}.jpg"
|
||||
if not image_path.exists():
|
||||
continue
|
||||
w, h = image_info(image_path)
|
||||
rec = ImageRecord(
|
||||
image_id=f"SLIDE-{ppt.slug}-S{slide.no:03d}",
|
||||
source_type="slide-preview",
|
||||
source_ppt=ppt.path.name,
|
||||
ppt_slug=ppt.slug,
|
||||
slide_no=slide.no,
|
||||
rel_path=kb_rel(image_path),
|
||||
abs_path=image_path,
|
||||
width=w,
|
||||
height=h,
|
||||
sha256=sha256(image_path),
|
||||
title=slide.title,
|
||||
reuse="可直接作为路演素材检索预览;正式交付前可按版式重绘或截取局部。",
|
||||
)
|
||||
slide.preview = rec
|
||||
|
||||
|
||||
def paragraph(lines: list[str]) -> str:
|
||||
return "\n".join(lines).strip() + "\n"
|
||||
|
||||
|
||||
def write_source_notes(records: list[PPTRecord]) -> list[ImageRecord]:
|
||||
source_dir = WIKI_DIR / "Sources" / "PPT"
|
||||
source_dir.mkdir(parents=True, exist_ok=True)
|
||||
all_images: list[ImageRecord] = []
|
||||
for ppt in records:
|
||||
source_rel = Path("..") / ".." / ".." / ppt.path.relative_to(ROOT)
|
||||
out = source_dir / f"{ppt.slug}.md"
|
||||
lines = [
|
||||
"---",
|
||||
"source_type: pptx",
|
||||
f"title: {ppt.title}",
|
||||
f"source_path: {source_rel.as_posix()}",
|
||||
f"date_ingested: {INGEST_TIME}",
|
||||
f"slides: {len(ppt.slides)}",
|
||||
"---",
|
||||
"",
|
||||
f"# {ppt.title}",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
"本页为 PPT 来源笔记,保留逐页文本、幻灯片预览图和内嵌图片索引。稳定结论已提升到 `Knowledge/` 页面;需要复用图片时优先检索 `Sources/Images/图片快速索引.md`。",
|
||||
"",
|
||||
"## Source",
|
||||
"",
|
||||
f"- 原始文件:`{source_rel.as_posix()}`",
|
||||
f"- 入库时间:`{INGEST_TIME}`",
|
||||
f"- 幻灯片数量:{len(ppt.slides)}",
|
||||
"",
|
||||
"## Slide Inventory",
|
||||
"",
|
||||
]
|
||||
for slide in ppt.slides:
|
||||
full_text = " ".join(slide.texts)
|
||||
lines.extend(
|
||||
[
|
||||
f"### Slide {slide.no:03d}: {slide.title}",
|
||||
"",
|
||||
f"- Core score: `{slide.core_score}`",
|
||||
]
|
||||
)
|
||||
if slide.preview:
|
||||
lines.append(f"- Preview ID: `{slide.preview.image_id}`")
|
||||
lines.append(f"- Preview path: `{slide.preview.rel_path}`")
|
||||
lines.append(f"")
|
||||
all_images.append(slide.preview)
|
||||
if slide.embedded_images:
|
||||
lines.append("- Embedded images:")
|
||||
for img in slide.embedded_images:
|
||||
lines.append(f" - `{img.image_id}`:`{img.rel_path}` ({img.width or '?'}x{img.height or '?'})")
|
||||
all_images.append(img)
|
||||
lines.extend(["", "Text:", ""])
|
||||
if full_text:
|
||||
wrapped = textwrap.fill(full_text, width=120)
|
||||
lines.extend([wrapped, ""])
|
||||
else:
|
||||
lines.extend(["(本页未抽取到可编辑文本,需查看预览图。)", ""])
|
||||
out.write_text("\n".join(lines), encoding="utf-8")
|
||||
return all_images
|
||||
|
||||
|
||||
def image_note_name(image_id: str) -> str:
|
||||
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", image_id)
|
||||
return f"{safe}.md"
|
||||
|
||||
|
||||
def write_image_notes(images: list[ImageRecord], slide_text_by_id: dict[str, str]) -> None:
|
||||
img_dir = WIKI_DIR / "Sources" / "Images"
|
||||
img_dir.mkdir(parents=True, exist_ok=True)
|
||||
index_lines = [
|
||||
"# 图片快速索引",
|
||||
"",
|
||||
f"Date ingested: `{INGEST_TIME}`",
|
||||
"",
|
||||
"用途:为公司介绍、路演材料、验厂材料、产品展示和技术路线图快速定位 PPT 图片。调用时优先使用 Image ID,并读取对应图片笔记。",
|
||||
"",
|
||||
"## 快速调用口令",
|
||||
"",
|
||||
"```text",
|
||||
"请读取 博志金钻项目基本情况_知识库/wiki/Sources/Images/图片快速索引.md,按 Image ID 调用指定图片,并结合对应 Sources/PPT 来源页核对上下文。",
|
||||
"```",
|
||||
"",
|
||||
"## 核心幻灯片预览图",
|
||||
"",
|
||||
"| Image ID | 来源 | 页码 | 标题/用途 | 尺寸 | 路径 |",
|
||||
"|---|---|---:|---|---:|---|",
|
||||
]
|
||||
embedded_lines = [
|
||||
"",
|
||||
"## PPT内嵌原图",
|
||||
"",
|
||||
"| Image ID | 来源 | 页码 | 标题/用途 | 尺寸 | 路径 |",
|
||||
"|---|---|---:|---|---:|---|",
|
||||
]
|
||||
|
||||
for img in images:
|
||||
note_rel = Path("Sources") / "Images" / image_note_name(img.image_id)
|
||||
img.note_path = note_rel.as_posix()
|
||||
text = slide_text_by_id.get(f"{img.ppt_slug}:{img.slide_no}", "")
|
||||
note = [
|
||||
"---",
|
||||
f"image_id: {img.image_id}",
|
||||
f"source_type: {img.source_type}",
|
||||
f"source_ppt: {img.source_ppt}",
|
||||
f"slide_no: {img.slide_no}",
|
||||
f"image_path: ../../../{img.rel_path}",
|
||||
f"date_ingested: {INGEST_TIME}",
|
||||
"---",
|
||||
"",
|
||||
f"# Image: {img.title or img.image_id}",
|
||||
"",
|
||||
f"Image path: `../../../{img.rel_path}`",
|
||||
f"Source document: `{img.source_ppt}`",
|
||||
f"Original slide: `{img.slide_no}`",
|
||||
f"Date ingested: `{INGEST_TIME}`",
|
||||
f"Image ID: `{img.image_id}`",
|
||||
"",
|
||||
"## Preview",
|
||||
"",
|
||||
f"",
|
||||
"",
|
||||
"## Visual Description",
|
||||
"",
|
||||
("整页幻灯片预览图,包含该页的版式、文字、图片和图表。" if img.source_type == "slide-preview" else "PPT 内嵌原图;需结合所在幻灯片预览核对语境。"),
|
||||
"",
|
||||
"## Extracted Labels / OCR",
|
||||
"",
|
||||
textwrap.fill(text[:1800], width=120) if text else "(未抽取到同页文本。)",
|
||||
"",
|
||||
"## What This Image Supports",
|
||||
"",
|
||||
"- 支撑公司基本情况知识库中的来源追溯、素材复用和路演图像定位。",
|
||||
"- 若用于外部交付,应回到原始 PPT 或高清原图确认分辨率、版权授权和上下文。",
|
||||
"",
|
||||
"## Limitations",
|
||||
"",
|
||||
"- 自动抽取不能替代人工审图;小图标、Logo、装饰元素可能被作为内嵌图片列出。",
|
||||
"- 幻灯片预览图适合快速调用和语境确认,正式设计建议重绘或使用源文件导出高清版本。",
|
||||
"",
|
||||
"## Reuse Plan",
|
||||
"",
|
||||
f"- {img.reuse}",
|
||||
"",
|
||||
"## Links",
|
||||
"",
|
||||
f"- Source PPT note: `../PPT/{img.ppt_slug}.md`",
|
||||
"",
|
||||
]
|
||||
(WIKI_DIR / note_rel).write_text("\n".join(note), encoding="utf-8")
|
||||
size = f"{img.width or '?'}x{img.height or '?'}"
|
||||
row = f"| `{img.image_id}` | {md_escape(img.source_ppt)} | {img.slide_no} | {md_escape(img.title or '')} | {size} | `../../../{img.rel_path}` |"
|
||||
if img.source_type == "slide-preview":
|
||||
index_lines.append(row)
|
||||
else:
|
||||
embedded_lines.append(row)
|
||||
|
||||
(img_dir / "图片快速索引.md").write_text("\n".join(index_lines + embedded_lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def write_registry(records: list[PPTRecord], images: list[ImageRecord]) -> None:
|
||||
sys_dir = WIKI_DIR / "_system"
|
||||
sys_dir.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
"# Registry",
|
||||
"",
|
||||
f"Date ingested: `{INGEST_TIME}`",
|
||||
"",
|
||||
"## PPT Sources",
|
||||
"",
|
||||
"| Source ID | Title | Slides | Path |",
|
||||
"|---|---|---:|---|",
|
||||
]
|
||||
for rec in records:
|
||||
lines.append(f"| `{rec.slug}` | {md_escape(rec.title)} | {len(rec.slides)} | `Sources/PPT/{rec.slug}.md` |")
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Image Counts",
|
||||
"",
|
||||
f"- 幻灯片预览图:{sum(1 for i in images if i.source_type == 'slide-preview')}",
|
||||
f"- PPT 内嵌原图:{sum(1 for i in images if i.source_type == 'embedded')}",
|
||||
f"- 图片总数:{len(images)}",
|
||||
]
|
||||
)
|
||||
(sys_dir / "registry.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
schema = [
|
||||
"# Schema",
|
||||
"",
|
||||
"- 原始 PPT 位于 `博志金钻项目基本情况/`,作为 Raw Sources,不由 Agent 改写。",
|
||||
"- 新增或更新 PPT 后,先运行 `_system/build_company_kb.py` 重新抽取来源笔记和图片索引。",
|
||||
"- 稳定事实写入 `Knowledge/`;直接引用 PPT 内容时回链到 `Sources/PPT/` 和 `Sources/Images/`。",
|
||||
"- 所有可复用图片必须使用本地路径,优先用 Image ID 调用。",
|
||||
"- `log.md` 只追加,不重写历史。",
|
||||
]
|
||||
(sys_dir / "schema.md").write_text("\n".join(schema) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not SOURCE_DIR.exists():
|
||||
raise SystemExit(f"missing source dir: {SOURCE_DIR}")
|
||||
for p in [
|
||||
WIKI_DIR / "Sources" / "PPT",
|
||||
WIKI_DIR / "Sources" / "Images",
|
||||
WIKI_DIR / "Knowledge",
|
||||
WIKI_DIR / "Writing",
|
||||
WIKI_DIR / "Maps",
|
||||
WIKI_DIR / "Daily",
|
||||
WIKI_DIR / "Archive",
|
||||
WIKI_DIR / "_system",
|
||||
ASSET_DIR / "media",
|
||||
ASSET_DIR / "slides",
|
||||
]:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pptx_files = sorted(SOURCE_DIR.glob("*.pptx"))
|
||||
records = [extract_pptx(p) for p in pptx_files]
|
||||
render_slide_previews(records)
|
||||
all_images = write_source_notes(records)
|
||||
slide_text_by_id = {
|
||||
f"{ppt.slug}:{slide.no}": " ".join(slide.texts)
|
||||
for ppt in records
|
||||
for slide in ppt.slides
|
||||
}
|
||||
write_image_notes(all_images, slide_text_by_id)
|
||||
write_registry(records, all_images)
|
||||
shutil.rmtree(TMP_DIR, ignore_errors=True)
|
||||
print(f"ingested {len(records)} pptx files")
|
||||
print(f"slides: {sum(len(r.slides) for r in records)}")
|
||||
print(f"images: {len(all_images)}")
|
||||
print(f"kb: {KB_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user