Repository navigation
How It Works
SlideBridge divides a hard problem into five small ones and puts a file between each pair. A deck is read for its words, rendered for its appearance, reconciled so that the words follow a sensible order, assembled into a Word table pack, and audited, and because every stage reads and writes plain files under one work folder, any stage can be run again alone without repeating the others.
flowchart LR
P["deck.pptx"] --> E["extract<br/>python-pptx"]
P --> R["render<br/>LibreOffice to PDF,<br/>PyMuPDF to PNG"]
E --> J["work/json/deck.json"]
R --> I["work/pdf and<br/>work/images/deck/slide_001.png"]
J --> C["reconcile<br/>reading order, fragments,<br/>confidence, warnings"]
I --> C
C --> M["work/reconciled/manifest.json"]
M --> B["build-docx<br/>python-docx"]
I --> B
M --> A["audit<br/>slides.csv, warnings.csv,<br/>run_report.json"]
B --> D["output/review_pack.docx"]
D --> L["align<br/>with an edited DOCX"]
L --> D2["aligned.docx"]
The manifest is the hinge. It is a pydantic model, RunManifest, serialized to JSON and checked against a JSON Schema by slidebridge validate, and it carries one SlideRecord per slide with its text blocks, table count, notes, confidence, warnings, and the path of its image.
| Package | Role |
|---|---|
extract |
Opens the deck, classifies each shape (title, body, footer, table, picture, group, other), detects the slide title by a three-tier fallback, and pulls tables and speaker notes |
render |
Converts the deck to PDF with headless LibreOffice, with two attempts and a two-second wait, then renders one PNG per page with PyMuPDF and validates the result |
reconcile |
Puts text blocks in reading order, merges short fragments, scores confidence, and attaches warning flags |
build |
Writes the DOCX: styles and margins, a heading, a metadata line, the image, an optional warning block, and the four-column table for each slide |
audit |
Writes slides.csv, warnings.csv, and run_report.json from the manifest |
align |
Reads a review pack and an edited document, splits the edited text into segments, and matches each original row to a segment |
orchestration |
The run pipeline, one function per command, and the retry helper |
models, config, constants
|
Typed records, the YAML configuration, and the named numeric constants |
gui |
A Tkinter window over the same orchestration |
Three small algorithms carry most of the behavior, and each is a heuristic with its numbers written down in constants.py.
Title detection tries a native title placeholder first, then a text block that the shape classifier marked as a title, then the first non-empty block, truncated at 120 characters, and records which tier answered in title_source. A slide on which all three fail gets the source fallback, which later costs it confidence.
Reading order puts the title first and footers last. The remaining blocks are sorted by their top edge and grouped into rows: a block belongs to the current row when its top is within 457,200 EMU, half an inch, of the row's first block, and rows are then read left to right. Tables are atomic. Body blocks shorter than 20 characters merge into the block that follows them.
Confidence starts at 1.0 and loses a fixed penalty for each of these conditions: no detected title (0.10), no text on a slide that has shapes (0.35), eight or more blocks under 15 characters (0.10), a grouped shape (0.15), and more than five shapes with fewer than 50 characters of text (0.20). The score is floored at 0, and a slide below 0.60 receives the flag confidence_below_threshold. The other flags are no_detected_title, image_without_text, grouped_shapes_present, table_detected, notes_present_not_extracted, and possible_missing_chart_labels.
Slide 3 of the sample deck carries a title placeholder reading "Deck A slide 3", a body placeholder with two paragraphs, and a 2 by 2 table whose cells read r0c0, r0c1, r1c0, and r1c1. Its path through the pipeline can be checked in the output files of the run described on Getting Started.
-
extractclassifies three shapes as title, body, and table, takes the title from the placeholder (title_sourceisplaceholder), and records a table count of 1. -
renderproduceswork/images/sample/slide_003.pngfrom page 3 of the PDF, and the stage passes only because the PDF has 17 pages for 17 slides and no image has zero bytes. -
reconcileorders the blocks title, body, table, applies no penalty, and so leaves the confidence at 1.0, and adds the flagtable_detectedbecause the table count is above zero. -
build-docxwrites the heading, the metadata line, the picture, and a table with three rows:1 | Deck A slide 3 | | title, then2 | First point on slide 3 of deck A / Second point on slide 3 | | body, then3 | r0c0 | r0c1 / r1c0 | r1c1 | | table. The third column is empty and the fourth names the role. -
auditadds one row toslides.csv(title, confidence 1.0, warning count 1, 95 characters of raw text) and one row towarnings.csvfortable_detected.
align tries five strategies for each original row, in this order: an exact match after normalization, a containment match where the original lies inside a longer edited segment, a fuzzy match on a single segment with a score of at least 78, a window of up to three adjacent segments with a score of at least 74, and finally nothing. The result is labeled Unchanged, Grammar, Clarity, Rewritten, Merged, Split, or Not Found, according to the score and the relative lengths. The matching depends on how the edited text is first cut into segments, and that step has two reproducible faults, which Evidence describes.
Designed and authored by Daniel Boles; implemented with coding agents. Licence: Apache-2.0.