feat(inbox): .pptx extraction — speaker notes, off-slide box, alt-text (stage 2e)
The pptx payload hides where an audience watching the slides does not look. The extractor surfaces all three regions into the concept text so the stage-2 scan catches the injection: - speaker notes (slide.notes_slide.notes_text_frame.text); - off-slide (off-canvas) text boxes (shape.text_frame.text, position-agnostic); - image/shape alt-text (cNvPr@descr, read off the XML — python-pptx 1.0.2 has no stable public accessor across shape types). Detach-proof: same visible slide without notes ADMITs. python-pptx imported lazily (dev/showcase-scoped, not a core dep). Tests 305 -> 310.
This commit is contained in:
parent
26a231d6c4
commit
d8a95e465f
2 changed files with 113 additions and 0 deletions
|
|
@ -150,6 +150,38 @@ def _extract_docx_text(fs_path) -> str:
|
|||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _shape_alt_text(shape) -> str:
|
||||
"""Read a shape's alt-text (cNvPr@descr). python-pptx 1.0.2 has no stable
|
||||
public accessor across shape types, so read it off the XML directly."""
|
||||
for element in shape._element.iter():
|
||||
if element.tag.endswith("}cNvPr"):
|
||||
return element.get("descr") or ""
|
||||
return ""
|
||||
|
||||
|
||||
def _extract_pptx_text(fs_path) -> str:
|
||||
"""Extract text from a ``.pptx``, including the regions an audience watching
|
||||
the slides does not see: speaker notes, off-slide (off-canvas) text boxes, and
|
||||
image/shape alt-text. ``python-pptx`` is imported lazily (dev/showcase-scoped).
|
||||
"""
|
||||
from pptx import Presentation
|
||||
|
||||
prs = Presentation(str(fs_path))
|
||||
parts: list = []
|
||||
for slide in prs.slides:
|
||||
for shape in slide.shapes:
|
||||
if shape.has_text_frame and shape.text_frame.text:
|
||||
parts.append(shape.text_frame.text) # incl. off-slide boxes
|
||||
alt = _shape_alt_text(shape)
|
||||
if alt:
|
||||
parts.append(alt)
|
||||
if slide.has_notes_slide:
|
||||
notes = slide.notes_slide.notes_text_frame.text
|
||||
if notes:
|
||||
parts.append(notes)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _ingest_regular_file(fs_path, rel_name, bundle, provenance, rejected, *, strict):
|
||||
"""Dispatch one on-disk file by suffix. ``strict`` raises on an unsupported
|
||||
suffix (a top-level drop); a folder walk passes ``strict=False`` to skip it."""
|
||||
|
|
@ -159,6 +191,8 @@ def _ingest_regular_file(fs_path, rel_name, bundle, provenance, rejected, *, str
|
|||
_ingest_csv(rel_name, text, bundle, provenance, rejected)
|
||||
elif suffix == ".docx":
|
||||
_materialize_text(rel_name, _extract_docx_text(fs_path), "docx", bundle, provenance)
|
||||
elif suffix == ".pptx":
|
||||
_materialize_text(rel_name, _extract_pptx_text(fs_path), "pptx", bundle, provenance)
|
||||
elif suffix in _TEXT_SUFFIXES:
|
||||
text = Path(fs_path).read_text(encoding="utf-8", errors="replace")
|
||||
_materialize_text(rel_name, text, suffix.lstrip("."), bundle, provenance)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue