51570f4da7
The bundled docx, xlsx, powerpoint, and pdf skills were adapted from Anthropic's document skills and carried their proprietary LICENSE.txt (no derivatives, no redistribution). Flagged as critical license findings by the SkillEvaluator Tier 1 scan of our skill tree. This replaces all four with clean-room rewrites: - Authored from scratch against library knowledge only (python-docx, openpyxl, python-pptx, pypdf/reportlab/pdfplumber — all MIT/BSD) by isolated subagents given functional specs, with an explicit prohibition on reading the prior skill content or anthropics/skills; session transcripts retained as provenance evidence. - MIT licensed (LICENSE file per skill), author: Nous Research. - Each skill: SKILL.md to house standards + argparse helper scripts with UTF-8-explicit I/O + its own e2e pytest suite (fixtures built on the fly, non-ASCII round-trips run under LC_ALL=C). - All four pass SkillEvaluator Tier 1 pii+unicode+lint 3/3. tests/skills/test_office_document_skills.py rewritten against the new contracts: MIT/no-Anthropic-text invariants, scripts documented in SKILL.md, argparse CLI shape, and a no-locale-default-open() check (which caught and fixed a real gap: pdfplumber text reads are fine, but the invariant scan now guards every future script). Docs pages regenerated for the four skills (scoped; unrelated generator drift excluded). Honest capability deltas vs the old versions are documented per SKILL.md (e.g. tracked-changes accept/reject and OOXML XSD validation are not reimplemented; form flattening limits stated).
89 lines
3.4 KiB
Python
89 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Build a deck from a .pptx template (brand deck) and fill placeholders.
|
|
|
|
Two modes:
|
|
1) Token fill (default): open TEMPLATE, replace every {{token}} across
|
|
slides, tables, and notes using --values JSON ({"token": "value"}),
|
|
save to OUTPUT. Formatting of the token's run is preserved.
|
|
2) --add-slides SPEC.json: additionally append slides built from the
|
|
template's own layouts (referenced by layout name or index), so new
|
|
slides inherit the brand master. Spec:
|
|
{"slides": [{"layout": "Title and Content", "title": "New",
|
|
"bullets": ["a", {"text": "b", "level": 1}],
|
|
"notes": "presenter text"}]}
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
from pptx import Presentation
|
|
|
|
|
|
def fill_tokens(prs, values):
|
|
from pptx_edit import replace_text # same scripts/ directory
|
|
total = 0
|
|
for token, value in values.items():
|
|
total += replace_text(prs, "{{%s}}" % token, str(value))
|
|
return total
|
|
|
|
|
|
def find_layout(prs, ref):
|
|
if isinstance(ref, int):
|
|
return prs.slide_layouts[ref]
|
|
for layout in prs.slide_layouts:
|
|
if layout.name == ref:
|
|
return layout
|
|
raise SystemExit(f"layout {ref!r} not found; available: "
|
|
f"{[la.name for la in prs.slide_layouts]}")
|
|
|
|
|
|
def add_slides(prs, spec):
|
|
from pptx_create import add_bullets
|
|
for slide_spec in spec.get("slides", []):
|
|
layout = find_layout(prs, slide_spec.get("layout", 1))
|
|
slide = prs.slides.add_slide(layout)
|
|
if slide_spec.get("title") is not None and slide.shapes.title:
|
|
slide.shapes.title.text = slide_spec["title"]
|
|
if slide_spec.get("bullets"):
|
|
body = next((ph for ph in slide.placeholders
|
|
if ph.placeholder_format.idx != 0), None)
|
|
if body is not None:
|
|
add_bullets(body.text_frame, slide_spec["bullets"])
|
|
if slide_spec.get("notes"):
|
|
slide.notes_slide.notes_text_frame.text = slide_spec["notes"]
|
|
|
|
|
|
def main(argv=None):
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
parser = argparse.ArgumentParser(
|
|
description="Fill {{tokens}} in a .pptx template and optionally "
|
|
"append slides using the template's own layouts.",
|
|
epilog=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("template", help="path to the template .pptx")
|
|
parser.add_argument("output", help="output .pptx path")
|
|
parser.add_argument("--values", metavar="JSON",
|
|
help="JSON file mapping token -> replacement value")
|
|
parser.add_argument("--add-slides", metavar="SPEC_JSON",
|
|
help="JSON spec of slides to append")
|
|
args = parser.parse_args(argv)
|
|
|
|
prs = Presentation(args.template)
|
|
filled = 0
|
|
if args.values:
|
|
with open(args.values, encoding="utf-8") as fh:
|
|
filled = fill_tokens(prs, json.load(fh))
|
|
if args.add_slides:
|
|
with open(args.add_slides, encoding="utf-8") as fh:
|
|
add_slides(prs, json.load(fh))
|
|
prs.save(args.output)
|
|
print(json.dumps({"ok": True, "output": args.output,
|
|
"tokens_filled": filled}))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
sys.exit(main())
|