598 lines
20 KiB
Python
598 lines
20 KiB
Python
#!/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()
|