From d91121b3378d4a469f4144addf8a10b7eb4ed0c6 Mon Sep 17 00:00:00 2001 From: kimtth Date: Mon, 25 May 2026 16:27:34 +0900 Subject: [PATCH 1/4] feat: add pptify plugin Add PPTify as an Awesome Copilot plugin with the full generated runtime artifact, including the .agent bundle, PPTX generation tools, design context, workflow, policy, instructions, and runtime skills. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/plugin/marketplace.json | 6 + docs/README.instructions.md | 2 +- docs/README.plugins.md | 1 + plugins/pptify/.agent/.env.template | 25 + plugins/pptify/.agent/copilot-instruction.md | 35 ++ plugins/pptify/.agent/pptify-design/README.md | 56 ++ .../awesome-copilot-design-agents.md | 101 ++++ .../contexts/alchaincyf-huashu-design.md | 86 +++ .../contexts/corazzon-pptx-design-styles.md | 342 +++++++++++ .../contexts/erickittelson-slidemason.md | 92 +++ .../contexts/fluent-ui-design-tokens.md | 80 +++ .../gabberflast-academic-pptx-skill.md | 110 ++++ .../contexts/likaku-mck-ppt-design-skill.md | 91 +++ .../contexts/nexu-io-open-design.md | 71 +++ .../contexts/pptwork-oh-my-slides.md | 102 ++++ .../contexts/primer-primitives.md | 137 +++++ .../contexts/sunbigfly-ppt-agent-skills.md | 104 ++++ .../pptify/.agent/pptify-design/sources.json | 258 +++++++++ .../pptify-design/third-party-notices.md | 41 ++ plugins/pptify/.agent/pptify-plugin/README.md | 94 +++ .../.agent/pptify-plugin/audit/audit.py | 152 +++++ .../.agent/pptify-plugin/design/__init__.py | 1 + .../design/design_context_catalog.py | 128 ++++ .../pptify-plugin/documents/__init__.py | 1 + .../documents/document_to_markdown.py | 77 +++ .../documents/document_to_raptor_tree.py | 490 ++++++++++++++++ .../download-external-assets.ps1 | 74 +++ .../.agent/pptify-plugin/external/README.md | 13 + .../external/all-MiniLM-L6-v2/.gitkeep | 1 + .../extraction/pptx_extractor.py | 545 ++++++++++++++++++ .../extraction/pptx_style_master.py | 505 ++++++++++++++++ .../.agent/pptify-plugin/images/__init__.py | 1 + .../pptify-plugin/images/iconfy_search.py | 179 ++++++ .../images/raster_image_to_svg.py | 289 ++++++++++ .../images/text_prompt_to_infographic.py | 318 ++++++++++ .../pptify-plugin/images/web_image_search.py | 286 +++++++++ plugins/pptify/.agent/pptify-policy.md | 60 ++ .../skills/pptify-context-prep/SKILL.md | 72 +++ .../skills/pptify-deck-generation/SKILL.md | 161 ++++++ .../skills/pptify-quality-gates/SKILL.md | 47 ++ .../.agent/skills/pptify-slide-spec/SKILL.md | 125 ++++ .../.agent/skills/pptify-tooling/SKILL.md | 38 ++ .../skills/pptify-visual-assets/SKILL.md | 49 ++ .../.agent/workflows/deck-generation.md | 115 ++++ plugins/pptify/.github/plugin/plugin.json | 21 + plugins/pptify/README.md | 54 ++ 46 files changed, 5635 insertions(+), 1 deletion(-) create mode 100644 plugins/pptify/.agent/.env.template create mode 100644 plugins/pptify/.agent/copilot-instruction.md create mode 100644 plugins/pptify/.agent/pptify-design/README.md create mode 100644 plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md create mode 100644 plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md create mode 100644 plugins/pptify/.agent/pptify-design/sources.json create mode 100644 plugins/pptify/.agent/pptify-design/third-party-notices.md create mode 100644 plugins/pptify/.agent/pptify-plugin/README.md create mode 100644 plugins/pptify/.agent/pptify-plugin/audit/audit.py create mode 100644 plugins/pptify/.agent/pptify-plugin/design/__init__.py create mode 100644 plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py create mode 100644 plugins/pptify/.agent/pptify-plugin/documents/__init__.py create mode 100644 plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py create mode 100644 plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py create mode 100644 plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 create mode 100644 plugins/pptify/.agent/pptify-plugin/external/README.md create mode 100644 plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep create mode 100644 plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py create mode 100644 plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py create mode 100644 plugins/pptify/.agent/pptify-plugin/images/__init__.py create mode 100644 plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py create mode 100644 plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py create mode 100644 plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py create mode 100644 plugins/pptify/.agent/pptify-plugin/images/web_image_search.py create mode 100644 plugins/pptify/.agent/pptify-policy.md create mode 100644 plugins/pptify/.agent/skills/pptify-context-prep/SKILL.md create mode 100644 plugins/pptify/.agent/skills/pptify-deck-generation/SKILL.md create mode 100644 plugins/pptify/.agent/skills/pptify-quality-gates/SKILL.md create mode 100644 plugins/pptify/.agent/skills/pptify-slide-spec/SKILL.md create mode 100644 plugins/pptify/.agent/skills/pptify-tooling/SKILL.md create mode 100644 plugins/pptify/.agent/skills/pptify-visual-assets/SKILL.md create mode 100644 plugins/pptify/.agent/workflows/deck-generation.md create mode 100644 plugins/pptify/.github/plugin/plugin.json create mode 100644 plugins/pptify/README.md diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 0060349c3..6ee0b103f 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -630,6 +630,12 @@ "description": "Complete toolkit for developing Power Platform custom connectors with Model Context Protocol integration for Microsoft Copilot Studio", "version": "1.0.0" }, + { + "name": "pptify", + "source": "pptify", + "description": "Generate production-ready PowerPoint decks with pptify skills, source ingestion, design-context selection, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates.", + "version": "1.0.0" + }, { "name": "project-documenter", "source": "project-documenter", diff --git a/docs/README.instructions.md b/docs/README.instructions.md index f31a6e3d9..55e5a606f 100644 --- a/docs/README.instructions.md +++ b/docs/README.instructions.md @@ -46,8 +46,8 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-instructions) for guidelines on | [Blazor](../instructions/blazor.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor.instructions.md) | Blazor component and application patterns | | [C# Development](../instructions/csharp.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp.instructions.md) | Guidelines for building C# applications | | [C# MCP Server Development](../instructions/csharp-mcp-server.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-mcp-server.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-mcp-server.instructions.md) | Instructions for building Model Context Protocol (MCP) servers using the C# SDK | -| [C# 코드 작성 규칙](../instructions/csharp-ko.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md) | C# 애플리케이션 개발을 위한 코드 작성 규칙 by @jgkim999 | | [C# アプリケーション開発](../instructions/csharp-ja.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ja.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ja.instructions.md) | C# アプリケーション構築指針 by @tsubakimoto | +| [C# 코드 작성 규칙](../instructions/csharp-ko.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md) | C# 애플리케이션 개발을 위한 코드 작성 규칙 by @jgkim999 | | [Caveman Mode](../instructions/caveman-mode.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcaveman-mode.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcaveman-mode.instructions.md) | Terse, low-token responses. Minimal words, no fluff. Full capabilities preserved. Use when: optimize token usage, low-token mode, concise output, caveman mode, reduce verbosity, token-efficient, brief responses. | | [CentOS Administration Guidelines](../instructions/centos-linux.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcentos-linux.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcentos-linux.instructions.md) | Guidance for CentOS administration, RHEL-compatible tooling, and SELinux-aware operations. | | [Clojure Development Instructions](../instructions/clojure.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fclojure.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fclojure.instructions.md) | Clojure-specific coding patterns, inline def usage, code block templates, and namespace handling for Clojure development. | diff --git a/docs/README.plugins.md b/docs/README.plugins.md index 8568d9d11..e80f7e431 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -72,6 +72,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t | [power-bi-development](../plugins/power-bi-development/README.md) | Comprehensive Power BI development resources including data modeling, DAX optimization, performance tuning, visualization design, security best practices, and DevOps/ALM guidance for building enterprise-grade Power BI solutions. | 8 items | power-bi, dax, data-modeling, performance, visualization, security, devops, business-intelligence | | [power-platform-architect](../plugins/power-platform-architect/README.md) | Solution Architect for the Microsoft Power Platform, turning business requirements into functioning Power Platform solution architectures. | 1 items | power-platform, power-platform-architect, power-apps, dataverse, power-automate, power-pages, power-bi | | [power-platform-mcp-connector-development](../plugins/power-platform-mcp-connector-development/README.md) | Complete toolkit for developing Power Platform custom connectors with Model Context Protocol integration for Microsoft Copilot Studio | 3 items | power-platform, mcp, copilot-studio, custom-connector, json-rpc | +| [pptify](../plugins/pptify/README.md) | Generate production-ready PowerPoint decks with pptify skills, source ingestion, design-context selection, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates. | 0 items | pptify, powerpoint, pptx, presentations, deck-generation, slides, design-context, visual-assets, quality-gates | | [project-documenter](../plugins/project-documenter/README.md) | Generate professional project documentation with draw.io architecture diagrams and Word (.docx) output with embedded images. Automatically discovers any project's technology stack and produces Markdown, diagrams, PNG exports, and a formatted Word document. | 3 items | documentation, architecture-diagrams, drawio, word-document, docx, png-images, c4-model, project-summary, auto-discovery | | [project-planning](../plugins/project-planning/README.md) | Tools and guidance for software project planning, feature breakdown, epic management, implementation planning, and task organization for development teams. | 15 items | planning, project-management, epic, feature, implementation, task, architecture, technical-spike | | [python-mcp-development](../plugins/python-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in Python using the official SDK with FastMCP. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | python, mcp, model-context-protocol, fastmcp, server-development | diff --git a/plugins/pptify/.agent/.env.template b/plugins/pptify/.agent/.env.template new file mode 100644 index 000000000..8613cf3d9 --- /dev/null +++ b/plugins/pptify/.agent/.env.template @@ -0,0 +1,25 @@ +# Copy this file to .env when image generation needs provider credentials. +# Never commit .env. It is ignored by git. + +# Provider selection: auto, openai, or azure-openai. +PPTIFY_IMAGE_PROVIDER=auto + +# OpenAI image generation. +OPENAI_API_KEY= +OPENAI_IMAGE_MODEL=gpt-image-1 + +# Azure OpenAI / Azure AI Foundry image generation. +# For gpt-image-2, the endpoint often ends with /openai/v1. +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_IMAGE_DEPLOYMENT=gpt-image-2 +# Optional compatibility alias used when AZURE_OPENAI_IMAGE_DEPLOYMENT is blank. +MODEL_NAME= +AZURE_OPENAI_API_VERSION=2024-02-01 +AZURE_OPENAI_TIMEOUT=300 + +# Use one of these only when key auth is required. Leave blank for Azure CLI / Entra auth. +AZURE_OPENAI_API_KEY= +AZURE_AI_API_KEY= + +# Optional explicit Azure CLI path if az is not discoverable. +AZURE_CLI_PATH= \ No newline at end of file diff --git a/plugins/pptify/.agent/copilot-instruction.md b/plugins/pptify/.agent/copilot-instruction.md new file mode 100644 index 000000000..7b01b27ca --- /dev/null +++ b/plugins/pptify/.agent/copilot-instruction.md @@ -0,0 +1,35 @@ + +# pptify Generic Coding-Agent Instructions + +Use the installed `./.agent` assets as the local pptify runtime context. + +## Installed Context + +- Skills: `./.agent/skills/pptify-*` +- Workflows: `./.agent/workflows` +- Design profiles and predefined templates: `./.agent/pptify-design` +- Plugin tool set: `./.agent/pptify-plugin` +- Image provider environment template: `./.agent/.env.template` +- Developer-protection policy: `./.agent/pptify-policy.md` + +## Agent Rules + +- Read `./.agent/pptify-policy.md` before generating or repairing a deck. +- For every new generated deck, choose and load a `./.agent/pptify-design` + profile before authoring slides unless a user-provided brand guide or + reference PPTX is the primary style source. Default to + `fluent-ui-design-tokens`; for developer decks use `primer-primitives`; for + consulting/governance decks use `likaku-mck-ppt-design-skill`; use + `corazzon-pptx-design-styles` only when a broader modern style catalog is + explicitly useful. +- Record selected profile IDs, source URLs, palette, typography, spacing rhythm, + and signature elements in `summary.design_context`. +- Treat plain white, Calibri-only, bullet-heavy `python-pptx`-looking output as + not production-ready. +- Use scripts under `./.agent/pptify-plugin` for source ingestion, design + context loading, visual assets, PPTX extraction, and audit checks. +- When image generation needs provider configuration or credentials, create + `./.agent/.env` from `./.agent/.env.template` and have the user fill secrets + directly in that file. Do not ask for secrets in chat or prompt dialogs. +- Keep generated specs coordinate-explicit and preserve source/license metadata + from the selected design profile. diff --git a/plugins/pptify/.agent/pptify-design/README.md b/plugins/pptify/.agent/pptify-design/README.md new file mode 100644 index 000000000..a80f389ca --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/README.md @@ -0,0 +1,56 @@ +# pptify-design + +Source-backed design context packs for `pptify` agents live here. The profiles in this folder are curated from public GitHub projects and web-accessible source files; the actual design templates are not invented by this repository. + +Agents use these files as LLM context before authoring a coordinate-explicit deck spec or generation script. The current plugin toolkit does not load these files automatically. + +## Catalog + +The catalog is [sources.json](sources.json). Each profile includes source URLs, license metadata, local context, and extracted source signals that can be translated into explicit `pptify` layout-tree objects, colors, typography, spacing, and slide composition choices. + +Current workflow-aligned profiles: + +| Profile | Source | Use when | +| --- | --- | --- | +| `primer-primitives` | GitHub Primer Primitives | Product, engineering, and developer-facing decks that need token discipline. | +| `fluent-ui-design-tokens` | Microsoft Fluent UI token guidance | Microsoft, enterprise, M365, Teams, or Power Platform-aligned decks. | +| `awesome-copilot-design-agents` | GitHub Awesome Copilot design agents/skills | Agent prompt context for design review, UX discovery, and visual hierarchy. | +| `corazzon-pptx-design-styles` | corazzon/pptx-design-styles | A 30-style modern PPTX template catalog for selecting and translating a visual direction into explicit pptify primitives. | + +Additional profiles in [sources.json](sources.json) cover staged deck-generation pipelines, consulting-style layout taxonomies, HTML-to-PPTX export constraints, and artifact critique workflows. + +## Agent Usage + +List available profiles: + +```powershell +uv run python pptify-plugin/design/design_context_catalog.py --list --pretty +``` + +Load one profile with local context text: + +```powershell +uv run python pptify-plugin/design/design_context_catalog.py --profile primer-primitives --include-context --pretty +``` + +Load multiple profiles when a deck needs both a presentation theme and a design-system prompt: + +```powershell +uv run python pptify-plugin/design/design_context_catalog.py --profile fluent-ui-design-tokens --profile awesome-copilot-design-agents --include-context --pretty +``` + +Load the modern PPTX style catalog: + +```powershell +uv run python pptify-plugin/design/design_context_catalog.py --profile corazzon-pptx-design-styles --include-context --pretty +``` + +## Rules + +- Treat these as LLM context, not executable renderer config. +- Keep source attribution and license metadata with any copied or adapted context. +- Do not invent a new design template when a user asks for predefined templates; choose a catalog profile, analyze a reference PPTX, or ask the user for a source. +- Translate source signals into explicit `layout_tree` primitives, bboxes, z-order, colors, and typography. Do not rely on a runtime theme or layout-pattern engine in this workspace snapshot. +- Do not copy external fonts, icons, images, or binary assets unless their license and source are explicitly added. + +See [third-party-notices.md](third-party-notices.md) for source notices. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md b/plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md new file mode 100644 index 000000000..5c60df8f1 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md @@ -0,0 +1,101 @@ +# Awesome Copilot Design Agent and Prompt Context + +Source-backed predefined agent prompt context. + +- Source repository: https://github.com/github/awesome-copilot +- Source files: + - `agents/gem-designer.agent.md` + - `agents/se-ux-ui-designer.agent.md` + - `skills/penpot-uiux-design/SKILL.md` + - `skills/prompt-optimizer/SKILL.md` +- License: MIT; see [third-party-notices.md](../third-party-notices.md) +- Retrieved: 2026-05-18 + +## Selected Source Excerpts + +From `agents/gem-designer.agent.md`: + +```md +## Role + +DESIGNER. Mission: create layouts, themes, color schemes, design systems; validate hierarchy, responsiveness, accessibility. Deliver: design specs. Constraints: never implement code. +``` + +```md +## Knowledge Sources + +1. `./docs/PRD.yaml` +2. Codebase patterns +3. `AGENTS.md` +4. Official docs (online or llms.txt) +5. Existing design system (tokens, components, style guides) +``` + +```md +#### 2.1 Requirements Analysis + +- Understand: component, page, theme, or system +- Check existing design system for reusable patterns +- Identify constraints: framework, library, existing tokens +- Review PRD for UX goals +``` + +From `agents/se-ux-ui-designer.agent.md`: + +```md +## Your Mission: Understand Jobs-to-be-Done + +Before any UI design work, identify what "job" users are hiring your product to do. Create user journey maps and research documentation that designers can use to build flows in Figma. +``` + +```md +## Step 1: Always Ask About Users First + +**Before designing anything, understand who you're designing for:** + +### Who are the users? +- "What's their role? (developer, manager, end customer?)" +- "What's their skill level with similar tools? (beginner, expert, somewhere in between?)" +- "What device will they primarily use? (mobile, desktop, tablet?)" +- "Any known accessibility needs? (screen readers, keyboard-only navigation, motor limitations?)" +``` + +From `skills/penpot-uiux-design/SKILL.md`: + +```md +### The Golden Rules + +1. **Clarity over cleverness**: Every element must have a purpose +2. **Consistency builds trust**: Reuse patterns, colors, and components +3. **User goals first**: Design for tasks, not features +4. **Accessibility is not optional**: Design for everyone +5. **Test with real users**: Validate assumptions early +``` + +```md +### Visual Hierarchy (Priority Order) + +1. **Size**: Larger = more important +2. **Color/Contrast**: High contrast draws attention +3. **Position**: Top-left (LTR) gets seen first +4. **Whitespace**: Isolation emphasizes importance +5. **Typography weight**: Bold stands out +``` + +From `skills/prompt-optimizer/SKILL.md`: + +```md +**Document creation (slides, reports).** Ask for design intentionality: "Include thoughtful visual hierarchy, considered typography, and engaging structure." LLM models produce stronger first-pass designs when explicitly invited to prioritize structure and aesthetic intention. +``` + +## Source Signals for LLM Context + +- Agent prompt focus from source: existing design system first, visual hierarchy, UX discovery, accessibility, and prompt clarity. +- Deck-planning cue from source: for slides and reports, explicitly ask for thoughtful visual hierarchy, considered typography, and engaging structure. +- UX discovery cue from source: identify users, context, pain points, and Jobs-to-be-Done before visual design choices. + +## PPTify Translation Guardrails + +- Use this context when the user asks for an agent prompt, sub-agent guidance, or design-review framing for a `pptify` deck. +- Treat it as prompt context; it is not a `pptify` renderer plugin and not a complete local agent mode. +- Combine with a current presentation or design-system profile such as `primer-primitives` or `fluent-ui-design-tokens` when the deck also needs concrete visual tokens. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md b/plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md new file mode 100644 index 000000000..86c488721 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md @@ -0,0 +1,86 @@ +# alchaincyf/huashu-design — Design Context + +**Source repo:** alchaincyf/huashu-design +**Style reference:** HTML-native design pipeline with brand-asset protocol +**Retrieved:** 2026-05-19 + +--- + +## What this repo teaches + +`alchaincyf/huashu-design` builds presentation decks HTML-first and treats PPTX as a constrained export target. The key contribution is a structured brand-asset protocol that governs how visual directions are defined, how multiple directions are run in parallel, and how HTML output is verified with Playwright before being exported to editable PPTX. + +--- + +## Key patterns + +### Brand asset protocol (`brand-asset-protocol`, `brand-assets`) +Before any layout work, codify the brand in a structured block: + +```json +{ + "brand_name": "Contoso", + "primary_palette": ["#0F6CBD", "#009C9C", "#6B5DD3"], + "neutral_palette": ["#F7FAFC", "#FFFFFF", "#17233A"], + "typeface_display": "Segoe UI", + "typeface_body": "Segoe UI", + "logo_description": "wordmark, dark navy on white", + "tone": "confident, technical, concise" +} +``` + +This block is passed into every slide generation prompt as a locked context. No colors, fonts, or geometric treatments are allowed to deviate from it. + +### Parallel visual directions (`visual-directions`, `five-schools`) +Generate 3–5 distinct visual direction options from the same content outline. Each direction varies: +- Background lightness (light, mid, dark) +- Accent geometry (circles, bars, diagonals, sharp rectangles) +- Typography weight (light/thin vs. bold/heavy) +- Information density (spacious vs. dense) + +Directions are rendered as HTML thumbnail cards. The selected direction becomes the locked style for the full deck. + +### HTML-to-PPTX export (`html-native`, `html-to-editable-pptx`, `editable-html-export`) +HTML is the design source. PPTX is the delivery format. The export pipeline: +1. Render full slide HTML +2. Run Playwright visual QA (check for overflow, font rendering, color contrast) +3. Map HTML layout to PPTX native shapes — every text frame must be individually editable +4. Verify no raster screenshots survive in the PPTX (all shapes must be vector/native) + +### Playwright checking (`playwright-check`) +After HTML render: +- Screenshot each slide at 1280×720 +- Check bounding box of every text container for overflow +- Check color contrast ratio (WCAG AA: ≥ 4.5:1 for body text) +- Check that no element is clipped by slide boundaries +- Fail the build if any check fails + +--- + +## Agent-level lessons for pptify + +1. **HTML layout ≠ editable PPTX.** HTML absolute positioning does not map 1:1 to PPTX native shapes. Constrain layout to pptify's coordinate system (13.333" × 7.5", absolute inches) rather than pixel-based CSS. +2. **Brand lock is non-negotiable.** Once `style_lock` is set from the brand asset protocol, every subsequent prompt must pass it through unchanged. +3. **Parallel directions reduce iteration.** Show 3 style options before the deck plan, not after. Post-hoc redesign is expensive. +4. **Every text frame must be individually editable.** Raster screenshots of styled text are never acceptable in pptify output. + +--- + +## Design guidance for pptify themes + +| Signal from huashu-design | pptify equivalent | +|---|---| +| `brand_asset_protocol` block | `theme` / `style_lock` dict in slide spec | +| HTML `background-color` | `theme.background` | +| Accent `border-color` | `theme.secondary` on shape border | +| `font-family` | `theme.font` | +| Playwright overflow check | pptify audit collision/overflow gate | + +--- + +## Best for + +- Brand-constrained enterprise decks requiring exact color/type fidelity +- Workflows where an agent uses a design mockup as evidence, then authors final PPTX coordinates explicitly +- Multi-direction style exploration before committing to a palette +- Decks requiring strong Playwright/visual verification discipline diff --git a/plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md b/plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md new file mode 100644 index 000000000..aeb2bdd88 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md @@ -0,0 +1,342 @@ +# corazzon/pptx-design-styles - Design Context + +**Source repo:** corazzon/pptx-design-styles +**Style reference:** 30 modern PPTX visual style templates with colors, fonts, layouts, signature elements, and anti-patterns +**Retrieved:** 2026-05-20 + +--- + +## What this repo teaches + +`corazzon/pptx-design-styles` is a curated design-template skill for presentation generation. Its value to pptify is not a runtime theme engine; it is a compact visual vocabulary that agents can translate into explicit `layout_tree` primitives: fills, text boxes, rules, cards, panels, diagrams, grids, and image-backed effects when needed. + +The upstream project includes English and Korean README files plus a Korean preview page. This local context is normalized to English and references the English `README.md`, `SKILL.md`, and `references/styles.md` only. + +No upstream binary assets are copied into this repository. Treat the upstream preview page and images as reference material only. + +--- + +## Core agent rule for pptify + +Choose one style before layout planning, lock its palette and typography, then translate the style into editable PowerPoint primitives wherever possible. + +- Use the exact HEX colors listed in the style lock. +- Keep one visual signature element present across the deck. +- Every slide should contain a visual element: shape, color block, card, diagram, rule, pattern, or image-backed exhibit. +- Avoid text-only slides unless the chosen style is explicitly typographic. +- Do not mix multiple styles in one deck unless the user asks for a deliberate contrast. +- CSS effects such as blur, backdrop-filter, blend modes, gradient text, or animated scan lines must be translated into PowerPoint-safe approximations: transparent shapes, raster background accents, layered fills, or editable lines. + +--- + +## Style recommendation matrix + +| Deck goal | Recommended styles | +|---|---| +| Tech, AI, startup | Glassmorphism, Aurora Neon Glow, Cyberpunk Outline, SciFi Holographic Data | +| Corporate, consulting, finance | Swiss International, Monochrome Minimal, Editorial Magazine, Architectural Blueprint | +| Education, research, history | Dark Academia, Nordic Minimalism, Brutalist Newspaper | +| Brand or marketing launch | Gradient Mesh, Typographic Bold, Duotone Color Split, Risograph Print | +| Product, app, UX | Bento Grid, Claymorphism, Pastel Soft UI, Liquid Blob Morphing | +| Entertainment or gaming | Retro Y2K, Dark Neon Miami, Vaporwave, Memphis Pop Pattern | +| Eco, wellness, culture | Hand-crafted Organic, Nordic Minimalism, Dark Forest Nature | +| Infrastructure or architecture | Isometric 3D Flat, Cyberpunk Outline, Architectural Blueprint | +| Portfolio, art, creative | Monochrome Minimal, Editorial Magazine, Risograph Print, Maximalist Collage | +| Pitch or strategy | Neo-Brutalism, Duotone Color Split, Bento Grid, Art Deco Luxe | +| Luxury, premium event | Art Deco Luxe, Monochrome Minimal, Dark Academia | +| Science, biotech, innovation | Liquid Blob Morphing, SciFi Holographic Data, Aurora Neon Glow | + +--- + +## Normalized style taxonomy + +### 01. Glassmorphism +- Mood: premium, tech, futuristic. +- Best for: SaaS, app launches, AI products. +- Palette: deep gradient `#1A1A4E`, `#6B21A8`, `#1E3A5F`; glass white at 15-20% opacity; border white at 25%; accents `#67E8F9` or `#A78BFA`. +- Typography: Segoe UI Light or Calibri Light titles, 36-44pt; Segoe UI body, 14-16pt; large KPI numbers, 52-64pt. +- Layout: translucent rounded cards, offset layering, dark gradient field, soft glow ellipses. +- Signature: consistent frosted-card treatment. +- Avoid: white backgrounds, opaque cards, saturated solid fills. + +### 02. Neo-Brutalism +- Mood: bold, raw, provocative, startup energy. +- Best for: pitch decks, marketing, creative agencies. +- Palette: yellow `#F5F500`, lime `#CCFF00`, hot pink `#FF2D55`, black `#000000`, red `#FF3B30`, blue `#0000FF`. +- Typography: Arial Black, Impact, or Bebas Neue titles, 40-56pt; Courier New or Space Mono body, 13-16pt. +- Layout: thick black borders, hard offset shadows, intentional misalignment, oversized words or numbers. +- Signature: pure-black no-blur shadows on every card. +- Avoid: soft shadows, gradients, rounded corners, muted colors. + +### 03. Bento Grid +- Mood: modular, structured, Apple-inspired. +- Best for: feature comparisons, product overviews, data summaries. +- Palette: off-white `#F8F8F2`, navy `#1A1A2E`, yellow `#E8FF3B`, coral `#FF6B6B`, teal `#4ECDC4`, warm yellow `#FFE66D`. +- Typography: SF Pro or Inter titles, 18-24pt; Inter body, 12-14pt; large stats, 48-64pt. +- Layout: asymmetric grid cells with varied spans and 8-12pt gaps. +- Signature: one dark anchor cell with white text plus color-coded supporting cells. +- Avoid: equal-size grids, dense text, more than five colors. + +### 04. Dark Academia +- Mood: scholarly, vintage, refined. +- Best for: education, historical research, book or university talks. +- Palette: deep brown `#1A1208`, near-black `#0E0A05`, antique gold `#C9A84C`, parchment `#D4BF9A`, muted gold `#8A7340`. +- Typography: Playfair Display Italic or Georgia Italic titles, 36-48pt; EB Garamond or Georgia body, 13-16pt; Space Mono labels. +- Layout: inset border frames, centered serif title, decorative horizontal rules, generous leading. +- Signature: double gold border and italic serif title. +- Avoid: modern sans-serif display type, bright colors, overly clean minimalism. + +### 05. Gradient Mesh +- Mood: artistic, vibrant, brand-forward. +- Best for: brand launches, creative portfolios, music or film promotions. +- Palette: hot pink `#FF6EC7`, violet `#7B61FF`, cyan `#00D4FF`, warm orange `#FFB347`, white text. +- Typography: Bebas Neue or Barlow Condensed ExtraBold titles, 48-72pt; Outfit or Poppins Light body, 14-16pt. +- Layout: full-bleed multi-radial gradient, minimal white text, optional frosted body panel. +- Signature: painterly multi-color mesh with large type. +- Avoid: simple two-color linear gradients, dark text, overcrowding. + +### 06. Claymorphism +- Mood: friendly, tactile, soft 3D. +- Best for: product launches, education, app UI decks. +- Palette: peach gradient `#FFECD2` to `#FCB69F`, teal `#A8EDEA`, blush `#FED6E3`, yellow `#FFEAA7`. +- Typography: Nunito ExtraBold or rounded display titles, 32-48pt; Nunito or DM Sans body, 14-16pt. +- Layout: high-radius rounded shapes, colored shadows, top-edge highlights, asymmetric bubbles. +- Signature: color-matched soft shadows and inner highlights. +- Avoid: sharp corners, grey shadows, mixed flat elements. + +### 07. Swiss International +- Mood: functional, authoritative, timeless. +- Best for: consulting, finance, government, institutional decks. +- Palette: white `#FFFFFF`, near-black `#111111`, signal red `#E8000D`, dark grey `#444444`, light grey `#DDDDDD`. +- Typography: Helvetica Neue or Arial titles, 32-44pt; Arial body, 12-14pt; Space Mono captions. +- Layout: strict 5-column or 12-column grid, left red rule, horizontal divider, generous margins. +- Signature: left-edge red bar and grid-aligned text blocks. +- Avoid: decorative illustration, rounded corners, more than two fonts. + +### 08. Aurora Neon Glow +- Mood: futuristic, electric, AI-oriented. +- Best for: AI products, cybersecurity, innovation summits. +- Palette: deep black `#050510`, neon green `#00FF88`, violet `#7B00FF`, cyan `#00B4FF`, soft white `#D0D0F0`. +- Typography: Bebas Neue or Barlow Condensed titles, 44-60pt; DM Mono or Space Mono body, 12-14pt. +- Layout: dark field, blurred neon circles, gradient title approximation, transparent dark panels. +- Signature: green-cyan-violet glow system. +- Avoid: light backgrounds, flat non-glowing colors, dense body text without panels. + +### 09. Retro Y2K +- Mood: nostalgic, pop, chaotic. +- Best for: events, lifestyle marketing, fashion campaigns. +- Palette: navy `#000080`, electric blue `#0020C2`, rainbow stripe, white title, cyan `#00FFFF`, magenta `#FF00FF`, yellow `#FFFF00`. +- Typography: Bebas Neue or Impact titles, 36-52pt; VT323 or Space Mono body, 12-14pt. +- Layout: top and bottom rainbow bars, centered title, sparkle motifs, double-color shadow effects. +- Signature: rainbow stripe bars and cyan/magenta title shadow. +- Avoid: minimalism, muted colors, serif fonts. + +### 10. Nordic Minimalism +- Mood: calm, natural, Scandinavian. +- Best for: wellness, lifestyle, nonprofit, sustainable brands. +- Palette: cream `#F4F1EC`, warm grey `#D9CFC4`, dark brown `#3D3530`, taupe `#8A7A6A`. +- Typography: Canela, Freight Display, or DM Serif Display titles, 36-52pt; Inter Light or Lato Light body, 13-15pt. +- Layout: at least 40% negative space, one organic background shape, small dot accent set, bottom rule and caption. +- Signature: organic blob, three-dot accent, letter-spaced monospace caption. +- Avoid: bright colors, dense layouts, sans-serif display type. + +### 11. Typographic Bold +- Mood: editorial, impactful, authoritative. +- Best for: manifestos, brand statements, headline announcements. +- Palette: off-white `#F0EDE8`, black `#0A0A0A`, near-black `#1A1A1A`, signal red `#E63030`, light grey `#AAAAAA`. +- Typography: Bebas Neue or Anton, 80-120pt; Space Mono footnotes. +- Layout: type fills the slide; two or three lines maximum; one accent word. +- Signature: oversized display typography as the visual. +- Avoid: images, icons, more than three large text lines, multiple font families. + +### 12. Duotone Color Split +- Mood: dramatic, comparative, energetic. +- Best for: strategy, before-after, compare-contrast slides. +- Palette: orange-red `#FF4500`, deep navy `#1A1A2E`, white divider and text. +- Typography: Bebas Neue panel text, 40-56pt; Space Mono captions. +- Layout: exact 50/50 split with white divider; one idea per side. +- Signature: cross-panel color echo. +- Avoid: three or more panels, weak contrast, busy content. + +### 13. Monochrome Minimal +- Mood: restrained, luxury, gallery-like. +- Best for: luxury brands, portfolios, high-end consulting. +- Palette: near-white `#FAFAFA`, black `#0A0A0A`, near-black `#1A1A1A`, greys `#E0E0E0`, `#888888`, `#CCCCCC`. +- Typography: Helvetica Neue Thin or Futura Light display, 24-36pt; Helvetica Neue body; Space Mono accent. +- Layout: thin circle focal point, descending-width bars, extreme negative space. +- Signature: pure monochrome with precise thin-line geometry. +- Avoid: color, decorative illustration, crowded layouts. + +### 14. Cyberpunk Outline +- Mood: HUD, sci-fi, dark tech. +- Best for: gaming, AI infrastructure, security, data engineering. +- Palette: near-black `#0D0D0D`, neon cyan `#00FFC8` at varied opacities. +- Typography: Bebas Neue outline-style titles, 44-60pt; Space Mono body and labels. +- Layout: subtle grid, four corner brackets, centered outline title, bottom data label. +- Signature: stroke-only title and corner markers. +- Avoid: white backgrounds, filled title text, warm colors. + +### 15. Editorial Magazine +- Mood: journalistic, narrative, sophisticated. +- Best for: annual reports, brand stories, long-form deck narratives. +- Palette: white `#FFFFFF`, near-black `#1A1A1A`, signal red `#E63030`, light grey `#BBBBBB`. +- Typography: Playfair Display Italic titles, 34-48pt; Space Mono subheads; Georgia body. +- Layout: asymmetric white/dark split, short red rule, rotated vertical label, column-style body copy. +- Signature: magazine split layout and red rule. +- Avoid: symmetry, sans-serif display type, full-bleed colored backgrounds. + +### 16. Pastel Soft UI +- Mood: gentle, app-like, healthcare-friendly. +- Best for: healthcare, beauty, education startups, consumer apps. +- Palette: pink-blue-mint gradient, white cards at 70%, blush `#F9C6E8`, sky blue `#C6E8F9`. +- Typography: Nunito or DM Sans titles, 28-36pt; Nunito or DM Sans body, 13-15pt. +- Layout: floating translucent cards, central pill or circular card, corner blobs, soft colored shadows. +- Signature: frosted white cards over pastel gradient. +- Avoid: dark backgrounds, saturated primaries, hard shadows. + +### 17. Dark Neon Miami +- Mood: synthwave, nightlife, 1980s neon. +- Best for: entertainment, music festivals, events. +- Palette: purple-black `#0A0014`, orange `#FF6B35`, hot pink `#FF0080`, purple `#9B00FF`. +- Typography: Bebas Neue titles, 36-52pt; Space Mono body. +- Layout: lower-center sunset semicircle, converging perspective grid, centered top title. +- Signature: sunset semicircle and neon grid. +- Avoid: blue-green dominant palettes, daylight backgrounds, plain body type. + +### 18. Hand-crafted Organic +- Mood: artisanal, natural, human. +- Best for: eco brands, food and beverage, craft studios, wellness. +- Palette: craft paper `#FDF6EE`, tan `#C8A882`, brown `#A87850`, dark brown `#6B4C2A`, natural greens. +- Typography: Playfair Display Italic or Cormorant Garamond Italic titles, 22-34pt; EB Garamond body. +- Layout: nested circles, botanical line-art accents, dashed rules, italic serif title. +- Signature: imperfect dashed outer circle and natural accents. +- Avoid: clean geometry, synthetic colors, sans-serif fonts. + +### 19. Isometric 3D Flat +- Mood: technical, structured, architectural. +- Best for: IT architecture, data flow, system diagrams. +- Palette: navy `#1E1E2E`, violet faces `#7C6FFF`, `#4A3FCC`, `#6254E8`, highlight `#A594FF`. +- Typography: Space Mono labels, 10-12pt; Bebas Neue or Barlow Condensed titles, 28-40pt. +- Layout: 30-degree isometric block clusters, thin connectors, title upper-right. +- Signature: three-face shading system. +- Avoid: perspective 3D, rounded shapes, light backgrounds. + +### 20. Vaporwave +- Mood: dreamy, surreal, internet-nostalgic. +- Best for: creative agencies, music, art portfolios. +- Palette: purple gradient `#1A0533` to `#570038`, sun colors `#FF9F43`, `#FF6B9D`, `#C44DFF`, grid `#FF64C8`. +- Typography: Bebas Neue ghost and gradient text; Space Mono body. +- Layout: perspective grid floor, sliced sunset semicircle, ghost watermark title, bottom gradient text. +- Signature: sliced sun and grid floor. +- Avoid: corporate layouts, muted earth tones, conventional typography. + +### 21. Art Deco Luxe +- Mood: gilded, prestigious, 1920s grandeur. +- Best for: luxury brands, gala events, premium reports. +- Palette: black-brown `#0E0A05`, gold `#B8960C`, rich gold `#D4AA2A`, muted gold `#8A7020`. +- Typography: Cormorant Garamond, Trajan, or Didot titles, 26-36pt, all caps and wide-spaced; Space Mono captions. +- Layout: double inset border, side fan ornaments, center rule and diamond, centered uppercase title. +- Signature: gold frame, fan ornaments, diamond divider. +- Avoid: modern sans-serif fonts, colorful or pastel tones, asymmetric layouts. + +### 22. Brutalist Newspaper +- Mood: raw journalism, editorial authority. +- Best for: media, research institutes, content industry decks. +- Palette: aged paper `#F2EFE8`, warm black `#1A1208`, body brown `#3A3020`. +- Typography: Space Mono masthead, Georgia or Playfair headline, Georgia body. +- Layout: dark masthead bar, double rules, two columns with divider, photo placeholder and caption. +- Signature: newspaper nameplate and dense two-column editorial layout. +- Avoid: modern sans-serif fonts, colorful elements, sparse white space. + +### 23. Stained Glass Mosaic +- Mood: vibrant, artistic, cathedral-rich. +- Best for: museums, culture, arts organizations. +- Palette: grout `#0A0A12`, blue `#1A3A6E`, crimson `#E63030`, yellow `#F5D020`, green `#2A6E1A`, purple `#6E1A4E`. +- Typography: Cormorant Garamond Bold or Trajan overlay titles; Georgia body. +- Layout: 6x4 mosaic grid with dark gaps, varied color rhythm, dark overlay for legibility. +- Signature: stained-glass cells with no matching adjacent colors. +- Avoid: pastel cells, large empty cells, sans-serif overlay text. + +### 24. Liquid Blob Morphing +- Mood: organic, fluid, bio-digital. +- Best for: biotech, environmental tech, innovation labs. +- Palette: ocean gradient `#0F2027` to `#2C5364`, teal `#00D2BE`, blue `#0078FF`, violet `#7800FF`, near-white `#F0FFFE`. +- Typography: Bebas Neue titles, 36-48pt; DM Mono or Space Mono body. +- Layout: three overlapping translucent blob shapes, dark ocean field, glowing centered title. +- Signature: overlapped low-opacity blobs and teal halo. +- Avoid: sharp geometry, bright warm backgrounds, dense text. + +### 25. Memphis Pop Pattern +- Mood: energetic, geometric, anti-minimalist. +- Best for: fashion, lifestyle, retail, youth marketing. +- Palette: warm off-white `#FFF5E0`, red `#E8344A`, blue `#1E90FF`, mint `#22BB88`, yellow `#FFD700`. +- Typography: Bebas Neue or Futura ExtraBold titles, 32-44pt; Futura or DM Sans body. +- Layout: scattered triangles, circles, dots, zigzag bar, asymmetric balance. +- Signature: all key geometric motifs present on warm background. +- Avoid: minimalism, monochrome palettes, overly clean fonts. + +### 26. Dark Forest Nature +- Mood: mysterious, atmospheric, eco-premium. +- Best for: environmental brands, adventure, sustainable luxury. +- Palette: forest gradient `#0D2B14` to `#060E08`, tree greens `#0A3D1A` and `#0D4D20`, moon sage `#E8F4D0` to `#B8CC80`, stars `#D4F0B0`. +- Typography: Playfair Display Italic or DM Serif Display Italic titles, 20-28pt; EB Garamond body; Space Mono captions. +- Layout: layered tree silhouettes, glowing moon top-right, sparse star dots, bottom mist overlay. +- Signature: three-depth forest silhouette plus moon. +- Avoid: bright greens, hard tree edges, sans-serif fonts. + +### 27. Architectural Blueprint +- Mood: precise, technical, professional. +- Best for: architecture, planning, engineering, spatial design. +- Palette: blueprint navy `#0D2240`, grid cyan-white `#64B4FF`, line cyan `#64C8FF`, title `#96DCFF`. +- Typography: Space Mono only, 8-13pt. +- Layout: fine and major grid, dimension lines, annotation marks, circular stamp, bottom title label. +- Signature: dual grid and dimension annotations. +- Avoid: decorative color, non-monospace fonts, photography. + +### 28. Maximalist Collage +- Mood: chaotic, irreverent, advertising-bold. +- Best for: advertising, fashion, music, editorial. +- Palette: antique cream `#E8DDD0`, red `#E83030`, near-black `#1A1A1A`, acid yellow `#F5D020`, white text. +- Typography: Bebas Neue words, 24-34pt; Playfair Display Italic secondary text; large ghost numbers; Space Mono captions. +- Layout: overlapping rotated color blocks, diagonal background pattern, ghost number, circle outline accent. +- Signature: three or more rotated blocks with vertical text in one block. +- Avoid: symmetry, clean uncluttered layouts, muted backgrounds. + +### 29. SciFi Holographic Data +- Mood: military HUD, quantum, precision. +- Best for: defense tech, AI research, quantum, advanced data engineering. +- Palette: space black `#03050D`, cyan `#00C8FF` at varied opacities. +- Typography: Space Mono only, 8-11pt. +- Layout: three concentric rings, one rotated ring, horizontal scan line, glowing horizontal bars, top-left and bottom-right labels. +- Signature: monochrome cyan HUD rings and scan line. +- Avoid: multiple hues, warm colors, decorative illustration. + +### 30. Risograph Print +- Mood: indie, analog, artisanal print. +- Best for: publishers, music labels, art zines, boutique studios. +- Palette: aged paper `#F7F2E8`, riso red `#E8344A`, riso blue `#0D5C9E`, riso yellow `#F5D020`. +- Typography: Bebas Neue main title, 34-44pt; Space Mono caption. +- Layout: three overlapping circles in the center third, offset ghost title, bottom monospace caption. +- Signature: CMYK-like overlaps and registration-offset title. +- Avoid: dark backgrounds, overly crisp digital treatment, screen-style blending. + +--- + +## Translation guidance for pptify layout trees + +- Cards: represent as `shape` rectangles with explicit fill opacity, border color, radius, and z-order. +- Rules and gridlines: use explicit line objects with consistent stroke width and opacity. +- Background gradients or mesh effects: use raster background only when vector approximation would be misleading; otherwise use large layered translucent shapes. +- Blend modes: approximate with semi-transparent overlapping fills; keep editability where possible. +- Outline text or gradient text: use a nearby editable fallback plus a documented visual approximation; do not depend on unsupported PowerPoint effects. +- Organic blobs, trees, mosaic cells, and Memphis shapes: author as editable primitive shapes when possible. +- Complex previews: keep the deck build source-backed by the style description, not by copied preview images. + +--- + +## Best for + +- Quickly selecting a predefined visual direction for a new deck. +- Matching a user request for a modern, trendy, stylish, or visually striking PPTX. +- Teaching agents a palette, typography, and signature-element vocabulary before authoring coordinates. +- Generating diverse style directions for user choice before committing to a full deck. diff --git a/plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md b/plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md new file mode 100644 index 000000000..1d7f7b546 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md @@ -0,0 +1,92 @@ +# erickittelson/slidemason — Design Context + +**Source repo:** erickittelson/slidemason +**Style reference:** JSX primitive composition for bespoke slide layouts +**Retrieved:** 2026-05-19 + +--- + +## What this repo teaches + +`slidemason` demonstrates JSX-based slide composition using a primitive component library (shapes, text, images, connectors). The approach allows fully bespoke layouts by combining primitives programmatically. The key cautionary lesson is that JSX flexibility breaks down at the PPTX editability boundary: layouts that are easy to express in JSX are often impossible to round-trip as fully editable native PPTX. + +--- + +## Core approach + +```jsx + + + + Insight title + Detail text here. + + +``` + +Coordinates are in inches, absolute-positioned, matching the pptify coordinate system. Primitives map directly to pptify builder functions. + +--- + +## JSX primitives → pptify equivalents + +| Slidemason JSX | pptify function | +|---|---| +| `` | `_background(fill)` + `_tree(...)` | +| `` | `_shape(..., shape="round_rect")` | +| `` | `_text(...)` | +| `` | `_line(...)` | +| `` | `_image(...)` | +| `` | `_shape(..., shape="oval")` | +| `` | `_shape(..., shape="hexagon")` | + +--- + +## Key patterns + +### JSX bento (`jsx-bento`, `slidemason-bento`) +A bento-grid card layout composed from `` primitives arranged in a CSS-like grid. Each card carries a tag, title, and detail text. In current pptify, agents translate this idea into explicit `layout_tree` card shapes and text boxes. + +**Caution:** Slidemason bento uses flexbox-inspired auto-sizing that does not survive PPTX export. In pptify, all bento card positions must be hardcoded to absolute inch coordinates. + +### Bespoke slide (`bespoke-slide`, `custom-jsx-slide`) +For one-off layouts not covered by the standard catalogue, compose primitives directly. Useful for complex infographic or diagram slides. + +**Caution:** Bespoke JSX slides are the hardest to keep editable. Every text frame must be individually positioned and sized; do not rely on auto-layout. + +### Primitive composition (`jsx-primitives`, `primitive-composition`) +The primitive library provides the building blocks. The composition layer arranges them. Separating these concerns means the same primitives can be recombined into new patterns without rebuilding the rendering infrastructure. + +--- + +## Editability failure modes + +Slidemason layouts commonly fail PPTX editability when: + +| Failure mode | Why it happens | +|---|---| +| Nested flex containers | PPTX has no flex layout; nested containers collapse | +| Auto-sized text frames | PPTX text frames need explicit height in inches | +| SVG filter effects | Drop shadows, blurs, and filters cannot be exported as native shapes | +| Rotated text boxes | Rotated text frames lose editability in most PPTX viewers | +| Z-index stacking of text | Text below z-index 10 is not selectable in PowerPoint | +| Image fills on shapes | Image-filled shape backgrounds cannot be reflowed as editable text | + +--- + +## Agent-level lessons for pptify + +1. **JSX flexibility is a design-time tool only.** At export time, every element must resolve to an absolute-positioned, explicitly-sized native PPTX object. +2. **Auto-layout is the enemy of editability.** In pptify specs, never use relative sizing (percentages, `auto`, `fr` units). All coordinates are in inches. +3. **The primitive catalogue should be closed at generation time.** The agent should choose from known editable primitives and validate every object before rendering. +4. **Bespoke is expected but must be explicit.** A bespoke layout requires manual quality-gate verification and coordinate-explicit objects. +5. **Text z_index ≥ 20.** In pptify, all text objects use `z_index=20` by default to ensure they render above decorative shapes. + +--- + +## Best for + +- Understanding the limits of programmatic slide composition +- Designing new pptify slide forms by sketching primitives before formalising geometry +- Cautionary reference for why auto-layout and PPTX editability are incompatible +- Component-level thinking about slide layout and primitive composition diff --git a/plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md b/plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md new file mode 100644 index 000000000..f826169a0 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md @@ -0,0 +1,80 @@ +# Fluent UI Design Token Context + +Source-backed predefined design-system context. + +- Source repository: https://github.com/microsoft/fluentui +- Source docs: https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md +- Source agent instructions: https://github.com/microsoft/fluentui/blob/master/AGENTS.md +- License: MIT; see [third-party-notices.md](../third-party-notices.md) +- Retrieved: 2026-05-18 + +## Selected Source Excerpts + +From `docs/architecture/design-tokens.md`: + +```md +# Design Tokens + +## Rule + +**Always use design tokens** from `@fluentui/react-theme` instead of hardcoded values. +Hardcoded values break theming, high contrast mode, and dark mode. +``` + +```md +## Token Categories + +| Category | Example tokens | Use for | +| ------------- | ------------------------------------------------------------- | -------------------- | +| Color | `tokens.colorNeutralForeground1`, `tokens.colorBrandBackground` | All colors | +| Spacing | `tokens.spacingVerticalM`, `tokens.spacingHorizontalL` | Padding, margin, gap | +| Border radius | `tokens.borderRadiusMedium`, `tokens.borderRadiusLarge` | Border radius | +| Font | `tokens.fontSizeBase300`, `tokens.fontWeightSemibold` | Typography | +| Line height | `tokens.lineHeightBase300` | Line height | +| Stroke | `tokens.strokeWidthThin`, `tokens.strokeWidthThick` | Border width | +| Shadow | `tokens.shadow4`, `tokens.shadow16` | Box shadow | +| Duration | `tokens.durationNormal`, `tokens.durationFast` | Animations | +| Easing | `tokens.curveEasyEase` | Animation timing | +``` + +```md +## Available Themes + +- `webLightTheme` - Default light +- `webDarkTheme` - Default dark +- `teamsLightTheme` / `teamsDarkTheme` / `teamsHighContrastTheme` - Teams variants +``` + +From `AGENTS.md`: + +```md +## Critical Rules (never violate) + +1. **Never hardcode colors, spacing, or typography values.** Always use design tokens from `@fluentui/react-theme`. See [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md). +``` + +```tsx +// Styles - always use tokens, never hardcoded values +import { makeStyles } from '@griffel/react'; +import { tokens } from '@fluentui/react-theme'; + +export const useComponentNameStyles = makeStyles({ + root: { + color: tokens.colorNeutralForeground1, + padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, + }, +}); +``` + +## Source Signals for LLM Context + +- Token categories from source: color, spacing, border radius, font, line height, stroke, shadow, duration, easing. +- Theme variants from source: `webLightTheme`, `webDarkTheme`, `teamsLightTheme`, `teamsDarkTheme`, `teamsHighContrastTheme`. +- Agent behavior from source: avoid hardcoded styling when the target design system exposes tokens. + +## PPTify Translation Guardrails + +- Use this context for Microsoft, Teams, M365, Power Platform, and enterprise product decks. +- `pptify` JSON uses concrete values, so include Fluent token names in `summary.design_context` when translating to theme values. +- Prefer restrained, utilitarian, accessibility-aware slide structure for operational and enterprise decks. +- Do not copy Fluent fonts or icons as assets from this context pack; use separately licensed assets only when explicitly sourced. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md b/plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md new file mode 100644 index 000000000..1032d30a9 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md @@ -0,0 +1,110 @@ +# Gabberflast/academic-pptx-skill — Design Context + +**Source repo:** Gabberflast/academic-pptx-skill +**Style reference:** Narrative discipline gates for evidence-based presentations +**Retrieved:** 2026-05-19 + +--- + +## What this repo teaches + +`academic-pptx-skill` adds a layer of *narrative gates* to deck generation: structural checks that prevent common storytelling failures before a slide ever reaches the audience. The three core gates are the action title discipline, the ghost deck test, and the one-exhibit discipline. These are content-quality checks, not visual-quality checks — they enforce that the deck tells a clear, evidence-grounded story. + +--- + +## Narrative gate 1 — Action titles (`action-title`) + +Every content slide must have an action title: a headline that states the conclusion, not the topic. + +### Test +For each slide title, ask: *"If someone reads only the title, do they know what to think or do?"* + +| Fails the test | Passes the test | +|---|---| +| "Q2 Sales Data" | "Q2 sales missed target by 18% due to APAC pipeline weakness" | +| "Risk Assessment" | "Three risks require immediate board escalation" | +| "Architecture Overview" | "Zero-trust architecture closes the 62% identity gap" | +| "Team Update" | "Engineering is on track; delivery confidence is High for Q3" | + +**Agent rule:** Reject any spec where a content slide title is purely descriptive. Rewrite it as an action title before proceeding. + +--- + +## Narrative gate 2 — Ghost deck test (`ghost-deck-test`) + +The ghost deck test: read only the slide titles in sequence. The titles alone should tell the complete story. + +### How to run it +1. Extract all slide titles from the deck spec +2. Read them in order, without any body content +3. Ask: *"Does this sequence make a coherent, complete argument?"* +4. If the answer is no, the outline is wrong — fix it before building slides + +### Common failures +- Title sequence has no logical progression (problem → evidence → recommendation) +- Two consecutive titles make the same point +- A title sequence assumes knowledge that hasn't been established earlier in the deck +- The final slide title does not name a specific next action or decision + +--- + +## Narrative gate 3 — One-exhibit discipline (`one-exhibit-discipline`) + +Each content slide may carry at most one primary data exhibit (chart, table, diagram, or image). Supporting text is allowed, but a second data exhibit is not. + +### Why this matters +Two exhibits on one slide force the audience to choose which to look at first. This splits attention and dilutes the slide's single point. If you have two exhibits, you have two slides. + +### Exceptions +- Dashboard-style overview slides are explicitly designed for multi-exhibit status summaries; use sparingly. +- Comparison-style two-column slides are acceptable when both panels serve the same comparative point. This is one exhibit with two panels, not two separate exhibits. + +--- + +## Evidence discipline (`evidence-slide`, `citation-slide`) + +For decks grounded in research or data: +- Every quantitative claim on a slide must have a source annotation +- Sources go in the slide notes or a dedicated appendix slide, not in body text +- Distinguish between primary data (your own measurements) and secondary data (cited sources) +- Flag any claim marked "estimated" or "approximate" with a visible qualifier + +--- + +## Ghost deck template + +Use this structure as the default ghost deck for a 12-slide consulting or governance deck: + +| # | Role | Action title example | +|---|---|---| +| 1 | Cover | — | +| 2 | TOC | — | +| 3 | Context | "The current operating model creates three material gaps" | +| 4 | Evidence 1 | "Gap 1: Identity controls cover only 62% of workloads" | +| 5 | Evidence 2 | "Gap 2: Patch latency averages 42 days against a 14-day SLA" | +| 6 | Evidence 3 | "Gap 3: DLP policy does not cover 28% of sensitive data stores" | +| 7 | Framework | "A zero-trust operating model closes all three gaps" | +| 8 | Recommendation | "Three initiatives deliver full coverage by Q4" | +| 9 | Roadmap | "Q1–Q2 identity hardening, Q3 DLP expansion, Q4 network segmentation" | +| 10 | Risk | "Two risks require active management: vendor dependency and staff capacity" | +| 11 | Investment | "Total investment is £2.4M; breakeven is 14 months" | +| 12 | Next steps | "Board to approve budget by 30 June; CISO to brief team by 7 July" | + +--- + +## Agent-level lessons for pptify + +1. **Run the ghost deck test before building any slides.** Extract titles from the outline JSON, read them in sequence, verify they tell a complete story. +2. **Rewrite descriptive titles as action titles.** This is the highest-leverage edit in any deck review. +3. **One exhibit per slide is a hard rule.** If you find yourself combining a bar chart and a table on the same slide, split them. +4. **The last slide must have a named next action.** "Thank you" is not an action. The closing slide should name a decision, deadline, and owner. +5. **Source every quantitative claim.** Use slide notes for source citations; keep the slide body clean. + +--- + +## Best for + +- High-stakes governance or board presentations requiring narrative rigour +- Decks that will be reviewed by a critical audience (investors, regulators, boards) +- Training agents to apply consulting-grade storytelling discipline +- Post-generation review gates before delivering a deck diff --git a/plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md b/plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md new file mode 100644 index 000000000..d947a96d3 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md @@ -0,0 +1,91 @@ +# likaku/Mck-ppt-design-skill - Design Context + +**Source repo:** likaku/Mck-ppt-design-skill +**Style reference:** McKinsey-style consulting deck layout taxonomy +**Retrieved:** 2026-05-19 + +--- + +## What this repo teaches + +`likaku/Mck-ppt-design-skill` is a public example of disciplined consulting-slide taxonomy: action titles, strict grids, repeated exhibit forms, and predictable evidence structures. Current pptify does not import those fixed layouts. Agents use this source as design inspiration, then author the exact `layout_tree` coordinates, sizes, objects, and z-order directly. + +--- + +## Core design principle for current pptify + +> The agent owns both the slide form and the coordinates. + +This means: + +- Layout geometry must be explicit before render time. +- Visual consistency comes from agent-authored coordinate discipline, not a runtime pattern engine. +- Each slide should still follow a recognizable consulting form so the deck feels deliberate. + +--- + +## Slide Form Families + +### Structure and navigation + +Use these ideas for covers, section dividers, agendas, appendices, and closings. Author them as explicit title text boxes, divider rules, section labels, and footer objects. + +### Data and metrics + +Use metric strips, KPI cards, data tables, RAG status rows, bar/line/donut-style exhibits, or dashboard-style overviews only when the source evidence contains quantitative data. Render them as explicit editable tables, labels, shapes, lines, and image-backed exhibits when exact chart fidelity is required. + +### Frameworks and matrices + +Use decision matrices, maturity ladders, process rails, timelines, cycles, funnels, and architecture maps as coordinate-explicit compositions. Every lane, node, connector, axis label, and callout needs its own bbox or line endpoints. + +### Content and narrative + +Use comparison, executive summary, quote, image exhibit, decision tree, icon grid, and team-style forms when they support the slide's single message. Keep the exhibit count low and make the action title carry the conclusion. + +--- + +## Action Title Discipline + +McKinsey decks use action titles: slide titles that state the conclusion, not the topic. + +| Descriptive | Action title | +|---|---| +| "Revenue by Region" | "APAC revenue grew 34% driven by cloud workloads" | +| "Risk Matrix" | "Three critical risks require board attention in Q3" | +| "Team" | "Engineering capacity is insufficient for H2 commitments" | + +**Agent rule:** Every content slide title must be an action title. Only cover, divider, agenda, and closing slides may use descriptive titles. + +--- + +## Consulting Layout Geometry Norms + +Use these as starting coordinates when authoring `layout_tree` objects for a 13.333 by 7.5 inch slide: + +| Element | Position | +|---|---| +| Kicker or eyebrow | y = 0.48, font 8.5pt | +| Slide title | y = 0.72, h = 0.36, font 22pt bold | +| Title rule | y = 1.12, h = 0.04 | +| Content area top | y >= 1.30 | +| Takeaway band top | y >= 5.50 | +| Page number | top-right, font 9pt muted | + +--- + +## Agent-level Lessons for pptify + +1. **Action titles on every content slide.** This is the highest-leverage improvement over generic deck generation. +2. **Choose a slide form, then write coordinates.** The taxonomy guides composition; the final output is still explicit `layout_tree` JSON. +3. **RAG status and checklist slides are tables.** For status dashboards, use editable table cells with explicit fill colors. +4. **Harvey balls and progress indicators are primitives.** Use donut-like arcs only as explicit shapes or image assets; simple filled circles often communicate more reliably. +5. **One data exhibit per slide.** Never combine two data charts on one slide unless the slide is intentionally a dashboard-style overview. + +--- + +## Best for + +- Consulting-style decks for strategy, governance, or operations reviews +- Decks requiring strict action-title discipline +- Workflows where reusable slide-form ideas are translated into explicit coordinates +- Teaching agents the taxonomy of PPTX slide forms diff --git a/plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md b/plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md new file mode 100644 index 000000000..27b15a155 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md @@ -0,0 +1,71 @@ +# nexu-io/open-design — Design Context + +**Source repo:** nexu-io/open-design +**Style reference:** Claude Design (Anthropic artifact conventions) +**Retrieved:** 2026-05-19 + +--- + +## What this repo teaches + +`nexu-io/open-design` is the closest public approximation to the "Claude Design" artifact pattern. It structures design work through agent skill files, a `DESIGN.md` contract, direction-picker routines, sandboxed preview loops, and artifact lint gates. The central idea is that design decisions are staged through explicit handoffs rather than collapsed into one prompt. + +--- + +## Key patterns + +### Direction picker (`design-direction-picker`, `direction-picker`) +Before committing to a visual language, present 2–4 parallel directional options as lightweight thumbnails or descriptive specs. Each option has a name, palette, typographic posture, and layout personality. The user (or an automated gate) selects one, which becomes the `style_lock` for the rest of the deck. + +**Agent rule:** Never start layout without a locked direction. Parallel directions should differ in at least two of: palette darkness, type scale, information density, accent geometry. + +### Artifact lint (`artifact-lint`, `visual-critique`) +After every render, run a structured lint pass before delivering the artifact: +- No hardcoded hex values that escape the locked design token set +- No placeholder text surviving into final output (`Lorem ipsum`, `TBD`, `TODO`) +- No font-size below 8pt in slides +- No content bounding box that overflows the slide canvas +- Action titles present on every content slide (not descriptive titles) + +### Sandbox preview (`sandbox-preview`) +Render a minimal single-slide preview of the proposed design direction before building the full deck. Use it as a visual contract to confirm color, type, and layout feel. Only proceed to full generation after preview approval. + +### Design critique gate (`design-critique`) +At the end of a deck generation run, score the output against the original brief: +- Does every slide carry exactly one decision or takeaway? +- Is the visual hierarchy consistent across slides? +- Are all data exhibits editable (not screenshots)? +- Is the action title parallel in grammatical structure across slides? + +--- + +## Design token conventions (from DESIGN.md) + +| Token | Role | +|---|---| +| `--color-bg` | Slide background | +| `--color-surface` | Card and panel fill | +| `--color-accent` | Primary interactive / emphasis color | +| `--color-text` | Body text | +| `--color-muted` | Kicker, caption, secondary text | +| `--font-display` | Title typeface | +| `--font-body` | Body copy typeface | +| `--radius-card` | Corner radius for cards and panels | + +--- + +## Agent-level lessons for pptify + +1. **Direction picker before deck plan.** Present explicit visual direction options before full slide authoring; only after user selection, proceed to the full coordinate plan. +2. **Style lock is a first-class artefact.** The `style_lock` JSON block (with `background`, `primary`, `secondary`, `tertiary`, `font`) should be emitted and confirmed before any `DeckBuilder.build()` call. +3. **Artifact lint is mandatory.** Run pptify render and audit checks after every build; treat collisions, overflows, or unexpected warnings as blocking failures. +4. **Preview before full deck.** Generate a 2–3 slide cover+section preview to confirm visual direction before rendering all slides. + +--- + +## Best for + +- Reasoning about deck design direction before committing to a palette +- Structuring parallel design options for human or automated selection +- Running structured lint and critique gates on generated decks +- Claude-native artifact workflows with explicit handoffs diff --git a/plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md b/plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md new file mode 100644 index 000000000..8a8d71c50 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md @@ -0,0 +1,102 @@ +# PPTWork / oh-my-slides — Design Context + +**Source repo:** PPTWork / oh-my-slides +**Style reference:** HTML-as-source, PPTX-as-build-artifact pipeline +**Retrieved:** 2026-05-19 + +--- + +## What this repo teaches + +`oh-my-slides` treats HTML as the canonical design source and PPTX as a downstream build artifact. The central insight is that HTML rendering gives you pixel-accurate layout control and visual fidelity checking before committing to the constraints of the PPTX format. The raster export (PNG/PDF) is the fidelity reference; the editable PPTX export is produced only under strict constraints that guarantee editability. + +--- + +## Core model + +``` +Source (markdown / JSON / data) + ↓ +HTML render ←── design source of truth + ↓ ↓ +Raster export Constrained editable PPTX +(PNG / PDF) (pptify explicit primitives) +(for fidelity) (for PowerPoint editability) +``` + +The two exports serve different needs: +- **Raster export:** pixel-perfect fidelity, safe for sharing as PDF, but not editable +- **Editable PPTX:** every text frame independently editable, every shape native, no raster embeds + +**Agent rule:** Never promise both pixel fidelity and full editability from the same export path. Choose one or declare a constrained hybrid. + +--- + +## Key patterns + +### HTML source with preset picker (`html-source`, `preset-picker`) +The HTML template is chosen from a catalogue of pre-approved presets. Each preset defines: +- Slide dimensions (always 1280 × 720px for 16:9) +- Background color and surface card color +- Typography scale (h1, h2, h3, body, caption) +- Accent geometry (border style, icon style, card corner radius) + +Agents select a preset, not a freeform CSS specification. This mirrors the pptify `style_lock` concept. + +### Mini preview (`mini-preview`) +Before full render, generate a single-slide HTML preview: +- Use the real preset +- Populate with real content (no lorem ipsum) +- Check bounding boxes: no text overflow, no element outside slide bounds +- Confirm color contrast passes WCAG AA + +Only after preview passes does the full deck render proceed. + +### Raster export for fidelity (`raster-export`, `pptx-build-artifact`) +The HTML slides are rendered to PNG at 2× resolution for fidelity: +- Each PNG is 2560 × 1440px +- Verified against the preset's expected layout grid +- Used as the reference for visual QA + +### Constrained editable PPTX (`constrained-editable`, `editable-export`) +When an editable PPTX is required: +- Map the HTML layout to pptify's coordinate system +- Replace any CSS-positioned elements with absolute-coordinate native shapes +- Replace styled HTML text with PPTX text frames +- Verify with pptify audit checks; zero collisions, overflows, and unexpected warnings required +- Raster screenshots of HTML are never inserted as images in the PPTX + +--- + +## Constraints for editable PPTX export + +When converting HTML → editable PPTX: + +| HTML construct | PPTX equivalent | +|---|---| +| `
` with background-color | `_shape()` with fill color | +| `

`, `

`–`

` | `_text()` with matching font size | +| `` | `_table()` | +| `
` or border-bottom | `_line()` or thin `_shape()` | +| Background image | Not permitted in editable export | +| SVG icon | `_shape()` geometric approximation | +| CSS `transform: rotate()` | Not permitted — use supported shape types only | + +--- + +## Agent-level lessons for pptify + +1. **HTML is a design tool, not a delivery format.** Use it for visual exploration and QA, then re-express as native PPTX shapes. +2. **Preset picker mirrors style_lock.** The HTML preset and the pptify `style_lock` should be derived from the same brand token source. +3. **Mini preview before full deck.** Generate a 1–2 slide preview render before committing to the full spec. +4. **Raster embeds are forbidden in editable output.** Any PPTX delivered with image embeds of slide content is a quality failure. +5. **Both export paths need their own quality gate.** Raster export: visual diff check. Editable PPTX: pptify audit and package checks with zero issues. + +--- + +## Best for + +- Workflows that need a visual HTML prototype before PPTX delivery +- Decks where design fidelity (raster) and PowerPoint editability are separate deliverables +- Agents that use Playwright or browser preview as part of the generation loop +- Teaching the constraint model of HTML → PPTX conversion diff --git a/plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md b/plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md new file mode 100644 index 000000000..e917f3503 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md @@ -0,0 +1,137 @@ +# Primer Primitives Context + +Source-backed predefined design-system context. + +- Source repository: https://github.com/primer/primitives +- Source docs: https://github.com/primer/primitives/blob/main/DESIGN_TOKENS_GUIDE.md +- Source tokens: `src/tokens/base/color/light/light.json5`, `src/tokens/functional/spacing/space.json5`, `src/tokens/functional/typography/typography.json5` +- License: MIT; see [third-party-notices.md](../third-party-notices.md) +- Retrieved: 2026-05-18 + +## Selected Source Excerpts + +From `README.md`: + +```md +This repo contains values for color, spacing, and typography primitives for use with Primer, GitHub's design system. + +Data is served from the `dist/` folder: + +- `dist/css` contains CSS files with values available as CSS variables +``` + +From `DESIGN_TOKENS_GUIDE.md`: + +```md +## Core Rule + +**You are a CSS expert. Never use raw values (hex, px, etc.). Only use semantic tokens.** +``` + +```md +### Typography + +| Keyword | Rule | +| ---------- | ---- | +| **MUST** | Use **shorthand** tokens (e.g., `font: var(...)`) to ensure `line-height` and `font-weight` are synchronized. | +| **SHOULD** | Match the token to the **semantic role** (e.g., use `title` tokens for headers, not just a large `body` token). | +``` + +From `src/tokens/functional/spacing/space.json5`: + +```json5 +{ + space: { + sm: { + $value: '{base.size.8}', + $type: 'dimension', + $description: 'Default spacing for most UI elements. Comfortable visual density for standard component layouts.', + $extensions: { + 'org.primer.llm': { + usage: ['gap', 'padding', 'margin', 'flex-gap', 'grid-gap', 'card-padding'], + rules: 'Default (8px). Use for standard component spacing, flex/grid item separation, container padding, and most element margins.', + }, + }, + }, + lg: { + $value: '{base.size.16}', + $type: 'dimension', + $description: 'Spacious spacing for major layout divisions and visual separation between content blocks.', + }, + }, +} +``` + +From `src/tokens/functional/typography/typography.json5`: + +```json5 +{ + text: { + title: { + shorthand: { + medium: { + $description: 'Default page title. The 32px-equivalent line-height matches with button and other medium control heights. Great for page header composition.', + $extensions: { + 'org.primer.llm': { + usage: ['section-heading', 'card-title', 'dialog-title', 'h2'], + rules: 'RECOMMENDED default for page titles. Use for section headings and dialog titles.', + }, + }, + }, + }, + }, + body: { + shorthand: { + medium: { + $description: 'Default UI font. Most commonly used for body text.', + $extensions: { + 'org.primer.llm': { + usage: ['body-text', 'ui-text', 'form-label', 'button-text', 'navigation'], + rules: 'RECOMMENDED default for UI text. Use for buttons, labels, and general interface text.', + }, + }, + }, + }, + }, + }, +} +``` + +From `src/tokens/base/color/light/light.json5`: + +```json5 +{ + base: { + color: { + black: { $value: { hex: '#1f2328' }, $type: 'color' }, + white: { $value: { hex: '#ffffff' }, $type: 'color' }, + neutral: { + '1': { $value: { hex: '#F6F8FA' }, $type: 'color' }, + '12': { $value: { hex: '#25292E' }, $type: 'color' }, + }, + blue: { + '5': { $value: { hex: '#0969da' }, $type: 'color' }, + }, + green: { + '5': { $value: { hex: '#1a7f37' }, $type: 'color' }, + }, + red: { + '5': { $value: { hex: '#cf222e' }, $type: 'color' }, + }, + }, + }, +} +``` + +## Source Signals for LLM Context + +- Token discipline: choose semantic roles for typography and spacing instead of arbitrary size jumps. +- Spacing signals from source: `xxs`, `xs`, `sm`, `md`, `lg`, `xl`, with `sm` as 8px and `lg` as 16px in the source token scale. +- Typography signals from source: `display`, `title`, `subtitle`, `body`, `caption`, `codeBlock`, `codeInline` roles. +- Color signals from source: neutral GitHub-like surfaces and a clear blue accent around `#0969da`. + +## PPTify Translation Guardrails + +- Use this context for developer-facing, GitHub-style, or product/engineering decks. +- `pptify` JSON needs concrete theme values; when converting Primer tokens to concrete colors, keep source token names in the deck `summary.design_context` so the LLM-grounding remains visible. +- Prefer cards, tables, and compact sections with consistent spacing over decorative flourishes. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md b/plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md new file mode 100644 index 000000000..0e5ed06e6 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md @@ -0,0 +1,104 @@ +# sunbigfly/ppt-agent-skills — Design Context + +**Source repo:** sunbigfly/ppt-agent-skills +**Style reference:** Staged deck-generation pipeline (interview → plan → build → QA → export) +**Retrieved:** 2026-05-19 + +--- + +## What this repo teaches + +`sunbigfly/ppt-agent-skills` provides the most rigorous staged generation pipeline of any public PPTX skill repo. It enforces explicit handoffs between six phases: audience interview, source compression, outline, style lock, per-slide planning, and dual export (raster + editable). Each phase is a separate agent turn with a defined input, output schema, and gate condition before the next phase begins. + +--- + +## Pipeline stages + +### Stage 1 — Audience interview (`interview`) +Before any content work, collect: +- **Audience:** role, seniority, prior context +- **Goal:** decision to be made or information to be conveyed +- **Tone:** formal/informal, data-heavy/narrative +- **Constraints:** slide count, time limit, branding requirements + +Output: structured brief JSON that gates entry to stage 2. + +### Stage 2 — Source compression (`research-outline`, `source-compression`) +Ingest source materials (documents, URLs, data files). For each source: +1. Summarise key claims and evidence +2. Tag each claim with its source and confidence level +3. Identify the 3–5 most decision-relevant signals +4. Discard anything that doesn't serve the stated audience goal + +Output: compressed source summary, ≤ 800 words, structured by decision relevance. + +### Stage 3 — Outline (`outline`) +From the compressed source and the audience brief, build a slide-by-slide outline: +- Slide number, slide form, action title, one-sentence content description +- Explicit "so-what" statement per slide (what decision or belief should change?) +- Flag slides that depend on data that needs verification + +Output: outline JSON. Agent must not proceed to stage 4 without explicit outline approval. + +### Stage 4 — Style lock (`style-lock`) +Choose a visual direction and emit a locked `style_lock` block: +```json +{ + "background": "#F7FAFC", + "surface": "#FFFFFF", + "dark": "#17233A", + "primary": "#0F6CBD", + "secondary": "#009C9C", + "tertiary": "#6B5DD3", + "font": "Segoe UI" +} +``` +The style lock is immutable for the rest of the pipeline. No per-slide overrides. + +### Stage 5 — Per-slide planning (`slide-plan`) +For each slide in the outline, emit a full slide spec with: +- `layout_tree`: complete pptify layout tree with explicit coordinates +- `title`, `subtitle`, `kicker`, `takeaway` represented as editable text objects +- items, rows, metrics, and exhibits represented as explicit editable primitives +- All content pre-populated from the compressed source (no placeholders) + +Output: complete `spec.json` ready to pass to `DeckBuilder.build()`. + +### Stage 6 — Dual export + visual QA (`visual-qa`, `dual-export`, `export-check`, `qa-report`) +After render: +1. Run pptify render and audit checks; zero collisions, overflows, or unexpected warnings allowed +2. Check collision and overflow counts in audit — must both be zero +3. Verify slide count matches outline +4. Confirm every slide has an action title (not a descriptive label) +5. If any check fails, return to stage 5 and patch the affected slides + +--- + +## Agent-level lessons for pptify + +1. **Never skip the interview.** Decks built without a structured brief produce the wrong content for the wrong audience. +2. **Source compression before outline.** Raw source material is too noisy for direct slide mapping. Compress first; then outline. +3. **Style lock is stage-gated.** Emit style_lock once, early. Treat any mid-deck color change as a pipeline failure. +4. **Per-slide plans are full specs.** The slide plan stage should produce a `spec.json` that requires zero editing before `DeckBuilder.build()`. +5. **Dual export is the quality gate.** A deck is not done until both the pptify quality gate and a visual review have passed. +6. **Action titles are mandatory.** Every content slide title must answer "so what?" not just label the content. + +--- + +## Slide count guidelines (from pipeline experience) + +| Audience | Recommended slide count | Max density | +|---|---|---| +| Board / C-suite | 8–12 | 1 decision per slide | +| Senior leadership | 12–18 | 1 insight per slide | +| Working-level review | 18–30 | 1 data exhibit per slide | +| Technical deep-dive | No limit | 1 concept per slide | + +--- + +## Best for + +- Decks that must be grounded in specific source documents or data +- High-stakes presentations where source provenance matters +- Workflows requiring explicit human approval at each phase +- Quality-controlled delivery pipelines for enterprise clients diff --git a/plugins/pptify/.agent/pptify-design/sources.json b/plugins/pptify/.agent/pptify-design/sources.json new file mode 100644 index 000000000..b4fc15de0 --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/sources.json @@ -0,0 +1,258 @@ +{ + "schema": "pptify-design.context-catalog.v1", + "updated": "2026-05-20", + "purpose": "Source-backed design template and agent-prompt context packs for LLM-assisted pptify deck authoring.", + "profiles": [ + { + "id": "primer-primitives", + "name": "Primer Primitives Design Tokens", + "kind": "design-system-context", + "license": { + "spdx": "MIT", + "notice": "Copyright (c) 2018 GitHub Inc." + }, + "source": { + "repo": "primer/primitives", + "url": "https://github.com/primer/primitives", + "license_url": "https://github.com/primer/primitives/blob/main/LICENSE", + "retrieved": "2026-05-18" + }, + "local_context": "contexts/primer-primitives.md", + "source_signals": { + "token_categories": ["color", "spacing", "typography", "motion", "z-index"], + "spacing_scale": ["xxs", "xs", "sm", "md", "lg", "xl"], + "typography_roles": ["display", "title", "subtitle", "body", "caption", "codeBlock", "codeInline"], + "color_examples": ["#ffffff", "#1f2328", "#F6F8FA", "#0969da", "#1a7f37", "#cf222e"] + }, + "best_for": ["GitHub-style decks", "developer products", "token-driven UI reviews", "engineering documentation"] + }, + { + "id": "fluent-ui-design-tokens", + "name": "Fluent UI Design Token Guidance", + "kind": "design-system-context", + "license": { + "spdx": "MIT", + "notice": "Copyright (c) Microsoft Corporation." + }, + "source": { + "repo": "microsoft/fluentui", + "url": "https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md", + "license_url": "https://github.com/microsoft/fluentui/blob/master/LICENSE", + "retrieved": "2026-05-18" + }, + "local_context": "contexts/fluent-ui-design-tokens.md", + "source_signals": { + "token_categories": ["color", "spacing", "border radius", "font", "line height", "stroke", "shadow", "duration", "easing"], + "theme_variants": ["webLightTheme", "webDarkTheme", "teamsLightTheme", "teamsDarkTheme", "teamsHighContrastTheme"], + "agent_rule": "Use design tokens instead of hardcoded colors, spacing, or typography values." + }, + "best_for": ["Microsoft-aligned decks", "Teams decks", "M365 decks", "Power Platform governance", "enterprise product reviews"] + }, + { + "id": "awesome-copilot-design-agents", + "name": "Awesome Copilot Design Agent and Prompt Context", + "kind": "agent-prompt-context", + "license": { + "spdx": "MIT", + "notice": "Copyright GitHub, Inc." + }, + "source": { + "repo": "github/awesome-copilot", + "url": "https://github.com/github/awesome-copilot", + "license_url": "https://github.com/github/awesome-copilot/blob/main/LICENSE", + "retrieved": "2026-05-18" + }, + "local_context": "agent-prompts/awesome-copilot-design-agents.md", + "source_signals": { + "agent_files": ["agents/gem-designer.agent.md", "agents/se-ux-ui-designer.agent.md"], + "skill_files": ["skills/penpot-uiux-design/SKILL.md", "skills/prompt-optimizer/SKILL.md"], + "prompt_focus": ["existing design systems", "visual hierarchy", "UX discovery", "accessibility", "slides and reports design intentionality"] + }, + "best_for": ["prompting an LLM to reason about deck design", "UX discovery before deck planning", "design review checklists", "visual hierarchy guidance"] + }, + { + "id": "nexu-io-open-design", + "name": "nexu-io/open-design — Claude Design Style", + "kind": "agent-skill-context", + "license": { + "spdx": "MIT", + "notice": "Copyright nexu-io contributors." + }, + "source": { + "repo": "nexu-io/open-design", + "url": "https://github.com/nexu-io/open-design", + "license_url": "https://github.com/nexu-io/open-design/blob/main/LICENSE", + "retrieved": "2026-05-19" + }, + "local_context": "contexts/nexu-io-open-design.md", + "source_signals": { + "key_patterns": ["direction-picker", "sandbox-preview", "artifact-lint", "design-critique"], + "style": "Claude Design artifact conventions", + "stage_gates": ["direction selection", "style lock", "preview approval", "artifact lint", "critique gate"], + "agent_rule": "Never start layout without a locked direction. Run artifact lint after every build. Preview before full deck." + }, + "best_for": ["reasoning about deck design direction before committing to a palette", "structuring parallel design options for selection", "running structured lint and critique gates on generated decks", "Claude-native artifact workflows with explicit handoffs"] + }, + { + "id": "alchaincyf-huashu-design", + "name": "alchaincyf/huashu-design — HTML-Native Brand Design Pipeline", + "kind": "agent-skill-context", + "license": { + "spdx": "MIT", + "notice": "Copyright alchaincyf contributors." + }, + "source": { + "repo": "alchaincyf/huashu-design", + "url": "https://github.com/alchaincyf/huashu-design", + "license_url": "https://github.com/alchaincyf/huashu-design/blob/main/LICENSE", + "retrieved": "2026-05-19" + }, + "local_context": "contexts/alchaincyf-huashu-design.md", + "source_signals": { + "key_patterns": ["brand-asset-protocol", "visual-directions", "html-to-editable-pptx", "playwright-check"], + "style": "HTML-native design with brand lock and parallel directions", + "brand_lock_fields": ["primary_palette", "neutral_palette", "typeface_display", "typeface_body", "tone"], + "agent_rule": "Brand lock is non-negotiable. Parallel directions before deck plan. Every text frame must be individually editable." + }, + "best_for": ["brand-constrained enterprise decks requiring exact color/type fidelity", "multi-direction style exploration before committing to a palette", "workflows where agents author complete coordinates from design mockups", "decks requiring visual verification"] + }, + { + "id": "sunbigfly-ppt-agent-skills", + "name": "sunbigfly/ppt-agent-skills — Staged Deck Generation Pipeline", + "kind": "agent-pipeline-context", + "license": { + "spdx": "MIT", + "notice": "Copyright sunbigfly contributors." + }, + "source": { + "repo": "sunbigfly/ppt-agent-skills", + "url": "https://github.com/sunbigfly/ppt-agent-skills", + "license_url": "https://github.com/sunbigfly/ppt-agent-skills/blob/main/LICENSE", + "retrieved": "2026-05-19" + }, + "local_context": "contexts/sunbigfly-ppt-agent-skills.md", + "source_signals": { + "pipeline_stages": ["interview", "source-compression", "outline", "style-lock", "slide-plan", "visual-qa", "dual-export"], + "stage_outputs": ["structured brief JSON", "compressed source ≤800 words", "outline JSON", "style_lock JSON", "complete spec.json", "qa-report", "pptx + raster"], + "agent_rule": "Never skip the interview. Source compression before outline. Style lock is stage-gated. Per-slide plans are full specs. Action titles are mandatory." + }, + "best_for": ["decks grounded in specific source documents or data", "high-stakes presentations where source provenance matters", "workflows requiring explicit human approval at each phase", "quality-controlled delivery pipelines for enterprise clients"] + }, + { + "id": "likaku-mck-ppt-design-skill", + "name": "likaku/Mck-ppt-design-skill — McKinsey-Style Native PPTX Layout Runtime", + "kind": "pptx-pattern-context", + "license": { + "spdx": "MIT", + "notice": "Copyright likaku contributors." + }, + "source": { + "repo": "likaku/Mck-ppt-design-skill", + "url": "https://github.com/likaku/Mck-ppt-design-skill", + "license_url": "https://github.com/likaku/Mck-ppt-design-skill/blob/main/LICENSE", + "retrieved": "2026-05-19" + }, + "local_context": "contexts/likaku-mck-ppt-design-skill.md", + "source_signals": { + "pattern_count": "~70 consulting-style layout patterns", + "pattern_families": ["structure-navigation", "data-metrics", "frameworks-matrices", "content-narrative"], + "action_title_discipline": true, + "geometry_norms": {"kicker_y": 0.48, "title_y": 0.72, "rule_y": 1.12, "content_top_y": 1.30}, + "agent_rule": "Use the source taxonomy as design inspiration only. The agent authors exact pptify layout_tree coordinates, sizes, and object primitives. Action titles on every content slide." + }, + "best_for": ["consulting-style decks for strategy, governance, or operations reviews", "decks requiring strict action-title discipline", "workflows where reusable layout ideas are translated into explicit coordinates", "teaching agents the taxonomy of PPTX slide forms"] + }, + { + "id": "corazzon-pptx-design-styles", + "name": "corazzon/pptx-design-styles - 30 Modern PPTX Style Templates", + "kind": "pptx-style-template-context", + "license": { + "spdx": "MIT", + "notice": "Copyright TodayCode / corazzon contributors." + }, + "source": { + "repo": "corazzon/pptx-design-styles", + "url": "https://github.com/corazzon/pptx-design-styles", + "license_url": "https://github.com/corazzon/pptx-design-styles/blob/main/README.md#license", + "retrieved": "2026-05-20" + }, + "local_context": "contexts/corazzon-pptx-design-styles.md", + "source_signals": { + "style_count": 30, + "style_names": ["Glassmorphism", "Neo-Brutalism", "Bento Grid", "Dark Academia", "Gradient Mesh", "Claymorphism", "Swiss International", "Aurora Neon Glow", "Retro Y2K", "Nordic Minimalism", "Typographic Bold", "Duotone Color Split", "Monochrome Minimal", "Cyberpunk Outline", "Editorial Magazine", "Pastel Soft UI", "Dark Neon Miami", "Hand-crafted Organic", "Isometric 3D Flat", "Vaporwave", "Art Deco Luxe", "Brutalist Newspaper", "Stained Glass Mosaic", "Liquid Blob Morphing", "Memphis Pop Pattern", "Dark Forest Nature", "Architectural Blueprint", "Maximalist Collage", "SciFi Holographic Data", "Risograph Print"], + "style_families": ["modern-ui", "editorial", "retro", "technical", "luxury", "organic", "experimental"], + "source_inputs": ["hex colors", "font pairings", "layout rules", "signature elements", "avoid lists"], + "agent_rule": "Pick one style, lock its palette and typography, then translate visual effects into explicit pptify layout_tree primitives or documented raster accents. Do not mix styles accidentally." + }, + "best_for": ["choosing a predefined modern deck style from a broad template catalog", "responding to user requests for stylish or visually striking presentations", "generating multiple visual direction options before deck production", "translating style-specific palettes, fonts, layouts, and signature elements into explicit pptify coordinates"] + }, + { + "id": "pptwork-oh-my-slides", + "name": "PPTWork / oh-my-slides — HTML-as-Source PPTX Build Artifact Pipeline", + "kind": "pptx-export-context", + "license": { + "spdx": "MIT", + "notice": "Copyright PPTWork contributors." + }, + "source": { + "repo": "PPTWork/oh-my-slides", + "url": "https://github.com/PPTWork/oh-my-slides", + "license_url": "https://github.com/PPTWork/oh-my-slides/blob/main/LICENSE", + "retrieved": "2026-05-19" + }, + "local_context": "contexts/pptwork-oh-my-slides.md", + "source_signals": { + "key_patterns": ["html-source", "preset-picker", "mini-preview", "raster-export", "constrained-editable"], + "export_model": "HTML (design source) → raster export (fidelity) + constrained PPTX (editability)", + "forbidden_in_editable_pptx": ["background images", "raster embeds of slide content", "CSS transform rotate", "SVG filter effects"], + "agent_rule": "Never promise both pixel fidelity and full editability from the same export path. Raster embeds in editable PPTX are a quality failure." + }, + "best_for": ["workflows that need HTML prototype before PPTX delivery", "decks where design fidelity and PowerPoint editability are separate deliverables", "agents using Playwright as part of the generation loop", "teaching the constraint model of HTML-to-PPTX conversion"] + }, + { + "id": "erickittelson-slidemason", + "name": "erickittelson/slidemason — JSX Primitive Composition (Cautionary Reference)", + "kind": "agent-skill-context", + "license": { + "spdx": "MIT", + "notice": "Copyright erickittelson contributors." + }, + "source": { + "repo": "erickittelson/slidemason", + "url": "https://github.com/erickittelson/slidemason", + "license_url": "https://github.com/erickittelson/slidemason/blob/main/LICENSE", + "retrieved": "2026-05-19" + }, + "local_context": "contexts/erickittelson-slidemason.md", + "source_signals": { + "key_patterns": ["jsx-primitives", "jsx-bento", "bespoke-slide", "primitive-composition"], + "primitive_map": {"Card": "_shape(round_rect)", "Text": "_text()", "Line": "_line()", "Image": "_image()", "Oval": "_shape(oval)"}, + "editability_failure_modes": ["nested flex containers", "auto-sized text frames", "SVG filter effects", "rotated text boxes", "image fills on shapes"], + "agent_rule": "Auto-layout is the enemy of editability. All coordinates are in inches. Bespoke layout is a last resort." + }, + "best_for": ["understanding the limits of programmatic slide composition", "designing new pptify patterns by sketching primitives first", "cautionary reference for why auto-layout and PPTX editability are incompatible", "component-level thinking about slide layout"] + }, + { + "id": "gabberflast-academic-pptx-skill", + "name": "Gabberflast/academic-pptx-skill — Narrative Discipline Gates", + "kind": "agent-skill-context", + "license": { + "spdx": "MIT", + "notice": "Copyright Gabberflast contributors." + }, + "source": { + "repo": "Gabberflast/academic-pptx-skill", + "url": "https://github.com/Gabberflast/academic-pptx-skill", + "license_url": "https://github.com/Gabberflast/academic-pptx-skill/blob/main/LICENSE", + "retrieved": "2026-05-19" + }, + "local_context": "contexts/gabberflast-academic-pptx-skill.md", + "source_signals": { + "key_patterns": ["action-title", "ghost-deck-test", "one-exhibit-discipline", "evidence-slide", "citation-slide"], + "narrative_gates": ["action title on every content slide", "ghost deck test passes", "one exhibit per slide", "last slide names a specific next action", "every quantitative claim has a source"], + "agent_rule": "Run ghost deck test before building slides. Rewrite descriptive titles as action titles. One exhibit per slide is a hard rule. The closing slide must name a decision, deadline, and owner." + }, + "best_for": ["high-stakes governance or board presentations requiring narrative rigour", "decks reviewed by critical audiences (investors, regulators, boards)", "training agents to apply consulting-grade storytelling discipline", "post-generation review gates before delivering a deck"] + } + ] +} \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/third-party-notices.md b/plugins/pptify/.agent/pptify-design/third-party-notices.md new file mode 100644 index 000000000..4892ec2df --- /dev/null +++ b/plugins/pptify/.agent/pptify-design/third-party-notices.md @@ -0,0 +1,41 @@ +# Third-Party Notices for pptify-design + +The design context packs in this folder include selected source excerpts and source-derived metadata from the projects below. They are included to provide predefined design context to LLM agents. + +## Primer Primitives + +- Source: https://github.com/primer/primitives +- Files referenced: `README.md`, `DESIGN_TOKENS_GUIDE.md`, `src/tokens/base/color/light/light.json5`, `src/tokens/functional/spacing/space.json5`, `src/tokens/functional/typography/typography.json5`, `LICENSE` +- License: MIT +- Notice: Copyright (c) 2018 GitHub Inc. + +## Fluent UI + +- Source: https://github.com/microsoft/fluentui +- Files referenced: `AGENTS.md`, `docs/architecture/design-tokens.md`, `LICENSE` +- License: MIT +- Notice: Copyright (c) Microsoft Corporation. +- Note: Usage of fonts and icons referenced in Fluent UI React is subject to the terms listed by Fluent UI at https://aka.ms/fluentui-assets-license. The `pptify-design` context pack references token guidance only and does not copy those assets. + +## Awesome Copilot + +- Source: https://github.com/github/awesome-copilot +- Files referenced: `agents/gem-designer.agent.md`, `agents/se-ux-ui-designer.agent.md`, `skills/penpot-uiux-design/SKILL.md`, `skills/prompt-optimizer/SKILL.md`, `LICENSE` +- License: MIT +- Notice: Copyright GitHub, Inc. + +## corazzon/pptx-design-styles + +- Source: https://github.com/corazzon/pptx-design-styles +- Files referenced: `README.md`, `SKILL.md`, `references/styles.md` +- License: MIT, as stated in the upstream `README.md` license section +- Notice: Copyright TodayCode / corazzon contributors. +- Note: The upstream repository also contains Korean-language documentation, a Korean preview page, and preview images. The local `pptify-design` context normalizes the design guidance to English and does not copy upstream binary assets. + +## MIT License Text + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/README.md b/plugins/pptify/.agent/pptify-plugin/README.md new file mode 100644 index 000000000..a18de0659 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/README.md @@ -0,0 +1,94 @@ +# pptify-plugin + +Standalone plugin scripts for source ingestion, design context, image assets, extraction, and audit helpers used by pptify workflows. CLI-style tools can be called directly by path and write a JSON payload to stdout. Extraction helpers are import APIs. + +## Optional Dependencies + +The base `pptify` package stays lightweight. Install plugin dependencies only when these integrations are needed: + +```powershell +uv sync --extra plugins +``` + +Some scripts also work without optional packages: + +- `documents/document_to_raptor_tree.py` uses stable local embeddings and requires only the standard library. +- `design/design_context_catalog.py` reads local `pptify-design` metadata and requires only the standard library. +- `images/raster_image_to_svg.py` defaults to wrapping raster bytes in an SVG and requires only the standard library. Its `--mode vector-trace` path uses optional `vtracer`. + +## External Assets + +Large optional runtime assets are restored on demand instead of being maintained as source files. Run the downloader from the repository root when you need local ONNX embeddings or other optional helper assets: + +```powershell +.\pptify-plugin\download-external-assets.ps1 +``` + +The script downloads MiniLM tokenizer files plus the selected ONNX model into `pptify-plugin/external/all-MiniLM-L6-v2`. The default MiniLM model is `onnx/model_quint8_avx2.onnx`, saved locally as `model_quantized.onnx`; pass `-MiniLmModelPath onnx/model.onnx` for the larger non-quantized model. + +## Image Provider Access + +`images/text_prompt_to_infographic.py` loads image-provider settings from `.env` before it runs. When image generation needs credentials or provider configuration, copy `.env.template` to `.env`, ask the user to fill the required values directly in that file, then run the helper. Do not ask the user to paste API keys, tokens, or connection strings into chat or a prompt input dialog. + +The helper does not provide a built-in local fallback image provider. If `--provider auto` is used and neither OpenAI nor Azure OpenAI is configured, the command fails with `missing_provider_config` instead of generating a substitute asset. + +For OpenAI text-to-image generation, configure these values in `.env`: + +- `PPTIFY_IMAGE_PROVIDER=openai` or pass `--provider openai`. +- `OPENAI_API_KEY`. +- `OPENAI_IMAGE_MODEL`, defaulting to `gpt-image-1` when unspecified. +- Image size: default to `1024x1024` when unspecified. +- Text prompt and output path. + +For Azure `gpt-image-2` or `gpt-image-2.0` deployments, configure these values in `.env`: + +- `PPTIFY_IMAGE_PROVIDER=azure-openai` or pass `--provider azure-openai`. +- `AZURE_OPENAI_ENDPOINT`, for example `https://.services.ai.azure.com/openai/v1`. +- `AZURE_OPENAI_IMAGE_DEPLOYMENT`, for example `gpt-image-2` or the user's exact deployment name. +- Image size: default to `1024x1024` when unspecified. +- Auth method: Azure CLI/Entra auth or API-key auth. +- `AZURE_OPENAI_TIMEOUT`, optional, default `300` seconds for large image generations. + +For Azure CLI/Entra auth, run `az login`. For API-key auth, fill `AZURE_OPENAI_API_KEY` or `AZURE_AI_API_KEY` in `.env`. `.env` is git-ignored; never commit it. + +Example: + +```powershell +Copy-Item .env.template .env +# Edit .env, then run: +uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty +``` + +For OpenAI image generation, fill `OPENAI_API_KEY` in `.env` and run the helper. + +Example: + +```powershell +uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty +``` + +## Scripts + +- `documents/document_to_markdown.py` - convert PDF, DOCX, PPTX, XLSX, HTML, or TXT with MarkItDown. +- `documents/document_to_raptor_tree.py` - split markdown, embed sections, and build a RAPTOR-style JSON tree. +- `design/design_context_catalog.py` - list or emit source-backed predefined design template and agent-prompt context from `pptify-design`. +- `images/web_image_search.py` - search Google when `icrawler` is available, then fall back to Bing HTML candidates. +- `images/iconfy_search.py` - search Iconify and return candidate SVG URLs. The filename keeps the existing `iconfy` spelling. +- `images/raster_image_to_svg.py` - create an SVG wrapper around a raster image, or trace it into vector paths with optional `vtracer`. +- `images/text_prompt_to_infographic.py` - generate an infographic via OpenAI or Azure OpenAI. +- `extraction/pptx_extractor.py` - importable helper for PPTX prompt context and extraction. +- `extraction/pptx_style_master.py` - importable helper for compact style, theme, and layout-rhythm analysis. + +`images/notebooklm_infographic.py` is not present in this workspace snapshot. Do not document or call a NotebookLM bridge unless that script is restored. + +## Examples + +```powershell +uv run python pptify-plugin/documents/document_to_markdown.py --source source.pdf --output-path source.md +uv run python pptify-plugin/documents/document_to_raptor_tree.py --markdown-path source.md --output-path source.structured-summary.json --title "Source" +uv run python pptify-plugin/design/design_context_catalog.py --profile primer-primitives --include-context --pretty +uv run python pptify-plugin/images/web_image_search.py --query "Power Platform governance" --max-num 8 +uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 +uv run python pptify-plugin/images/raster_image_to_svg.py --source logo.png --mode vector-trace --output-path logo.svg +uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider openai --prompt "Cloud governance roadmap" --output-path infographic.png +``` \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/audit/audit.py b/plugins/pptify/.agent/pptify-plugin/audit/audit.py new file mode 100644 index 000000000..599ba413e --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/audit/audit.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +"""Standalone collision-detection audit for layout_tree specs. + +Works entirely on plain dicts — no pptify package required. +Accepts a layout_tree dict (as produced by the agent or by pptx_extractor) +and returns a list of colliding object pairs. + +Usage (as a script): + python audit.py spec.json # prints collisions for each slide + python audit.py spec.json --json # prints JSON audit report +""" + +import json +import sys +from pathlib import Path +from typing import Any + + +def detect_content_collisions(tree: dict[str, Any]) -> list[dict[str, str]]: + """Return pairs of objects whose bounding boxes overlap. + + Args: + tree: A layout_tree dict with an ``objects`` mapping. + + Returns: + A list of ``{"first_object_id": ..., "second_object_id": ...}`` dicts. + """ + positioned_objects = [ + obj + for obj in tree.get("objects", {}).values() + if obj.get("bbox") + ] + collisions: list[dict[str, str]] = [] + for first_index, first_obj in enumerate(positioned_objects): + for second_obj in positioned_objects[first_index + 1 :]: + if _intersects(first_obj["bbox"], second_obj["bbox"], padding=0.01): + collisions.append({"first_object_id": first_obj["id"], "second_object_id": second_obj["id"]}) + return collisions + + +FONT_SIZE_FLOOR = 9.0 # pt — below this threshold text is unreadable at presentation scale + + +def detect_small_fonts(tree: dict[str, Any], floor: float = FONT_SIZE_FLOOR) -> list[dict[str, Any]]: + """Return content objects whose ``style.font_size`` is below *floor* pt. + + Only objects with ``classification: "content"`` are checked; decorative + ``layout_design`` objects (dots, rule lines, etc.) are skipped. + """ + violations: list[dict[str, Any]] = [] + for obj in tree.get("objects", {}).values(): + if obj.get("classification") == "layout_design": + continue + size = obj.get("style", {}).get("font_size") + if size is not None and float(size) < floor: + violations.append({"object_id": obj["id"], "font_size": size, "floor": floor}) + return violations + + +def audit_spec(spec: dict[str, Any]) -> dict[str, Any]: + """Audit all slides in a deck spec (``{"slides": [...]}``). + + Each slide must have a ``layout_tree`` key. + Returns a summary with per-slide collision lists and small-font warnings. + """ + results: list[dict[str, Any]] = [] + total_collisions = 0 + total_small_fonts = 0 + for slide in spec.get("slides", []): + tree = slide.get("layout_tree") or {} + slide_id = str(slide.get("id") or tree.get("id") or "unknown") + collisions = detect_content_collisions(tree) + small_fonts = detect_small_fonts(tree) + total_collisions += len(collisions) + total_small_fonts += len(small_fonts) + results.append( + { + "slide_id": slide_id, + "collision_count": len(collisions), + "collisions": collisions, + "small_font_count": len(small_fonts), + "small_fonts": small_fonts, + } + ) + return { + "slide_count": len(results), + "total_collisions": total_collisions, + "total_small_fonts": total_small_fonts, + "slides": results, + } + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _intersects(a: dict[str, float], b: dict[str, float], padding: float = 0.0) -> bool: + """Return True if bbox *a* overlaps expanded bbox *b* (by *padding* on each side).""" + bx = b["x"] - padding + by = b["y"] - padding + br = b["x"] + b["width"] + padding + bb = b["y"] + b["height"] + padding + epsilon = 0.0001 + ax, ay = a["x"], a["y"] + ar = a["x"] + a["width"] + ab_ = a["y"] + a["height"] + return ax < br - epsilon and ar > bx + epsilon and ay < bb - epsilon and ab_ > by + epsilon + + +# --------------------------------------------------------------------------- +# CLI entry-point +# --------------------------------------------------------------------------- + + +def _main(argv: list[str]) -> None: + if not argv: + print("Usage: python audit.py [--json]", file=sys.stderr) + sys.exit(1) + spec_path = Path(argv[0]) + as_json = "--json" in argv + spec = json.loads(spec_path.read_text(encoding="utf-8")) + # Support both a full spec with "slides" and a bare layout_tree + if "slides" in spec: + report = audit_spec(spec) + else: + collisions = detect_content_collisions(spec) + small_fonts = detect_small_fonts(spec) + report = { + "slide_count": 1, + "total_collisions": len(collisions), + "total_small_fonts": len(small_fonts), + "slides": [{"slide_id": "root", "collision_count": len(collisions), "collisions": collisions, "small_font_count": len(small_fonts), "small_fonts": small_fonts}], + } + if as_json: + print(json.dumps(report, indent=2, ensure_ascii=False)) + else: + print(f"Slides: {report['slide_count']} Total collisions: {report['total_collisions']} Small fonts (<{FONT_SIZE_FLOOR}pt): {report['total_small_fonts']}") + for slide in report["slides"]: + if slide["collision_count"]: + print(f" Slide {slide['slide_id']}: {slide['collision_count']} collision(s)") + for c in slide["collisions"]: + print(f" {c['first_object_id']} <-> {c['second_object_id']}") + if slide["small_font_count"]: + print(f" Slide {slide['slide_id']}: {slide['small_font_count']} small-font object(s)") + for f in slide["small_fonts"]: + print(f" {f['object_id']}: {f['font_size']}pt (floor {f['floor']}pt)") + + +if __name__ == "__main__": + _main(sys.argv[1:]) diff --git a/plugins/pptify/.agent/pptify-plugin/design/__init__.py b/plugins/pptify/.agent/pptify-plugin/design/__init__.py new file mode 100644 index 000000000..afee1cf02 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/design/__init__.py @@ -0,0 +1 @@ +"""Design-context plugin helpers for pptify workflows.""" \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py b/plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py new file mode 100644 index 000000000..df5a784ff --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + + +DEFAULT_MANIFEST_PATH = Path(__file__).resolve().parents[2] / "pptify-design" / "sources.json" + + +def load_catalog(manifest_path: Path = DEFAULT_MANIFEST_PATH) -> dict[str, Any]: + with manifest_path.open("r", encoding="utf-8") as handle: + catalog = json.load(handle) + profiles = catalog.get("profiles") + if not isinstance(profiles, list): + raise ValueError(f"Catalog does not contain a profiles list: {manifest_path}") + return catalog + + +def select_profiles(catalog: dict[str, Any], profile_ids: list[str] | None = None) -> list[dict[str, Any]]: + profiles = [profile for profile in catalog.get("profiles", []) if isinstance(profile, dict)] + if not profile_ids: + return profiles + + by_id = {str(profile.get("id")): profile for profile in profiles if profile.get("id")} + selected: list[dict[str, Any]] = [] + missing: list[str] = [] + for profile_id in profile_ids: + profile = by_id.get(profile_id) + if profile is None: + missing.append(profile_id) + else: + selected.append(profile) + + if missing: + available = ", ".join(sorted(by_id)) + raise ValueError(f"Unknown design context profile(s): {', '.join(missing)}. Available: {available}") + return selected + + +def read_context_files(profiles: list[dict[str, Any]], *, base_dir: Path) -> list[dict[str, str]]: + contexts: list[dict[str, str]] = [] + for profile in profiles: + context_path_value = profile.get("local_context") + if not isinstance(context_path_value, str) or not context_path_value: + continue + context_path = (base_dir / context_path_value).resolve() + try: + content = context_path.read_text(encoding="utf-8") + except FileNotFoundError: + content = "" + contexts.append( + { + "id": str(profile.get("id", "")), + "path": context_path.relative_to(base_dir).as_posix(), + "content": content, + } + ) + return contexts + + +def build_payload( + *, + manifest_path: Path = DEFAULT_MANIFEST_PATH, + profile_ids: list[str] | None = None, + include_context: bool = False, + list_only: bool = False, +) -> dict[str, Any]: + catalog = load_catalog(manifest_path) + profiles = select_profiles(catalog, profile_ids) + base_dir = manifest_path.resolve().parent + + payload: dict[str, Any] = { + "ok": True, + "schema": catalog.get("schema"), + "updated": catalog.get("updated"), + "manifest": str(manifest_path), + "profiles": profiles, + } + if list_only: + payload["profiles"] = [ + { + "id": profile.get("id"), + "name": profile.get("name"), + "kind": profile.get("kind"), + "best_for": profile.get("best_for", []), + } + for profile in profiles + ] + if include_context: + payload["contexts"] = read_context_files(profiles, base_dir=base_dir) + return payload + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Emit source-backed pptify design context catalog entries as JSON.") + parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST_PATH, help="Path to pptify-design/sources.json.") + parser.add_argument("--profile", action="append", help="Profile ID to select. Can be repeated. Defaults to all profiles.") + parser.add_argument("--include-context", action="store_true", help="Include local context file contents in the JSON payload.") + parser.add_argument("--list", action="store_true", help="Return a compact list of profiles.") + parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") + return parser + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + args = build_parser().parse_args() + try: + payload = build_payload( + manifest_path=args.manifest, + profile_ids=args.profile, + include_context=args.include_context, + list_only=args.list, + ) + except Exception as exc: + payload = {"ok": False, "error": str(exc)} + print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) + return 1 + + print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/documents/__init__.py b/plugins/pptify/.agent/pptify-plugin/documents/__init__.py new file mode 100644 index 000000000..6634a9482 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/documents/__init__.py @@ -0,0 +1 @@ +"""Document ingestion helpers for pptify plugins.""" \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py b/plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py new file mode 100644 index 000000000..1bdb27170 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + + +def emit_payload(payload: dict[str, Any], *, pretty: bool = False) -> None: + print(json.dumps(payload, ensure_ascii=False, indent=2 if pretty else None)) + + +def convert_document(source: str | Path, *, enable_plugins: bool = False) -> dict[str, Any]: + """Convert a document supported by MarkItDown into markdown text.""" + source_path = Path(source).expanduser() + if not source_path.exists(): + return { + "ok": False, + "error": f"Source file does not exist: {source_path}", + "code": "source_not_found", + } + + try: + from markitdown import MarkItDown + except Exception as exc: + return { + "ok": False, + "error": "MarkItDown is not installed. Install the plugin dependencies with `uv sync --extra plugins`.", + "detail": str(exc), + "code": "dependency_missing", + } + + try: + result = MarkItDown(enable_plugins=enable_plugins).convert(str(source_path)) + except Exception as exc: + return {"ok": False, "error": str(exc), "code": "conversion_failed"} + + markdown = getattr(result, "text_content", "") or "" + title = getattr(result, "title", "") or source_path.stem + return { + "ok": True, + "source": str(source_path), + "title": title, + "markdown": markdown, + "charCount": len(markdown), + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Convert a document to markdown with MarkItDown.") + parser.add_argument("--source", required=True, help="Path to a document such as PDF, DOCX, PPTX, XLSX, HTML, or TXT.") + parser.add_argument("--output-path", help="Optional path where markdown should be written.") + parser.add_argument("--enable-plugins", action="store_true", help="Enable MarkItDown plugins.") + parser.add_argument("--pretty", action="store_true", help="Pretty-print the JSON response.") + return parser + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + + args = build_parser().parse_args() + payload = convert_document(args.source, enable_plugins=args.enable_plugins) + + if payload.get("ok") and args.output_path: + output_path = Path(args.output_path).expanduser() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(str(payload["markdown"]), encoding="utf-8") + payload["outputPath"] = str(output_path) + + emit_payload(payload, pretty=args.pretty) + return 0 if payload.get("ok") else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py b/plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py new file mode 100644 index 000000000..168ea814b --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py @@ -0,0 +1,490 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import re +import sys +import time +from pathlib import Path +from typing import Any, TypedDict + + +class RaptorNode(TypedDict): + id: str + level: int + heading: str + text: str + embedding: list[float] + children: list[str] + + +class RaptorTree(TypedDict): + nodes: list[RaptorNode] + + +class StructuredSummary(TypedDict): + documentTitle: str + globalSummary: dict[str, Any] + raptorTree: RaptorTree + + +_HEADING_RE = re.compile( + r""" + ^(?: + (?P\#{1,6})\s+(?P.+) + | (?P\d+(?:\.\d+)*)[.)]\s+(?P.+) + | (?P[A-Z][A-Z\s]{4,})$ + ) + """, + re.VERBOSE | re.MULTILINE, +) +_PARA_SPLIT_RE = re.compile(r"\n\s*\n") +_TOKEN_RE = re.compile(r"[A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)*|[^\W\s_]+", re.UNICODE) +DEFAULT_EMBEDDING_DIM = 384 +CLUSTER_THRESHOLD = 0.55 +MAX_CLUSTER_SUMMARY_CHARS = 2000 + + +def _heading_level(match: re.Match[str]) -> tuple[int, str]: + if match.group("md"): + return len(match.group("md")), match.group("md_title").strip() + if match.group("num"): + depth = match.group("num").count(".") + 1 + return min(depth + 1, 6), match.group("num_title").strip() + if match.group("caps"): + return 2, match.group("caps").strip().title() + return 2, "" + + +def _split_sections_by_headings(markdown: str, min_chars: int = 100) -> list[dict[str, Any]]: + sections: list[dict[str, Any]] = [] + current_heading = "Introduction" + current_level = 1 + current_lines: list[str] = [] + + for line in markdown.split("\n"): + match = _HEADING_RE.match(line.strip()) + if match: + text = "\n".join(current_lines).strip() + if text and len(text) >= min_chars: + sections.append({"heading": current_heading, "text": text, "level": current_level}) + current_level, current_heading = _heading_level(match) + current_lines = [] + else: + current_lines.append(line) + + text = "\n".join(current_lines).strip() + if text and len(text) >= min_chars: + sections.append({"heading": current_heading, "text": text, "level": current_level}) + return sections + + +def _extract_paragraph_heading(text: str, max_len: int = 80) -> str: + first_line = text.split("\n", 1)[0].strip() + heading = re.sub(r"[*_`#]", "", first_line).strip() + if heading.startswith("|"): + cells = [cell.strip() for cell in heading.split("|") if cell.strip()] + heading = cells[0] if cells else heading + if len(heading) > max_len: + shortened = heading[:max_len].rsplit(" ", 1)[0].strip() + heading = f"{shortened or heading[:max_len]}..." + return heading or "Section" + + +def _tokenize(text: str) -> list[str]: + return [token.lower() for token in _TOKEN_RE.findall(text) if token.strip()] + + +def _normalize(vector: list[float]) -> list[float]: + norm = math.sqrt(sum(value * value for value in vector)) + if norm <= 1e-12: + return vector + return [value / norm for value in vector] + + +def _hash_embedding(text: str, *, dim: int = DEFAULT_EMBEDDING_DIM) -> list[float]: + tokens = _tokenize(text) + vector = [0.0] * dim + if not tokens: + return vector + + features = list(tokens) + features.extend(f"{left} {right}" for left, right in zip(tokens, tokens[1:])) + + for feature in features: + digest = hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest() + index = int.from_bytes(digest[:4], "little") % dim + sign = 1.0 if digest[4] & 1 else -1.0 + weight = 1.0 + min(len(feature), 20) / 80.0 + vector[index] += sign * weight + return _normalize(vector) + + +_ONNX_SESSION: Any = None # ort.InferenceSession once loaded +_ONNX_TOKENIZER: Any = None # tokenizers.Tokenizer once loaded +_ONNX_TRIED: bool = False + +_EXTERNAL_MODEL_DIR = Path(__file__).parent.parent / "external" / "all-MiniLM-L6-v2" + + +def _try_load_onnx() -> bool: + """Load the ONNX session and tokenizer from external/. Returns True on success.""" + global _ONNX_SESSION, _ONNX_TOKENIZER, _ONNX_TRIED + if _ONNX_TRIED: + return _ONNX_SESSION is not None + _ONNX_TRIED = True + + model_path = _EXTERNAL_MODEL_DIR / "model_quantized.onnx" + tokenizer_path = _EXTERNAL_MODEL_DIR / "tokenizer.json" + if not model_path.exists() or not tokenizer_path.exists(): + return False + + try: + import onnxruntime as ort + from tokenizers import Tokenizer + + session = ort.InferenceSession(str(model_path), providers=["CPUExecutionProvider"]) + tok = Tokenizer.from_file(str(tokenizer_path)) + tok.enable_truncation(max_length=256) + tok.enable_padding(pad_id=0, pad_token="[PAD]") + _ONNX_SESSION = session + _ONNX_TOKENIZER = tok + print("[raptor] Using local ONNX embedding model (all-MiniLM-L6-v2).", file=sys.stderr) + return True + except Exception as exc: + print(f"[raptor] Could not load ONNX model, falling back to hash embeddings: {exc}", file=sys.stderr) + return False + + +def _embed_with_onnx(texts: list[str], *, prefix: str, dim: int) -> list[list[float]]: + import numpy as np + + prefixed = [f"{prefix}{text}" for text in texts] + encodings = _ONNX_TOKENIZER.encode_batch(prefixed) + input_ids = np.array([enc.ids for enc in encodings], dtype=np.int64) + attention_mask = np.array([enc.attention_mask for enc in encodings], dtype=np.int64) + input_names = {inp.name for inp in _ONNX_SESSION.get_inputs()} + feed: dict[str, Any] = {"input_ids": input_ids, "attention_mask": attention_mask} + if "token_type_ids" in input_names: + feed["token_type_ids"] = np.zeros_like(input_ids) + + outputs = _ONNX_SESSION.run(None, feed) + token_embeddings: Any = outputs[0] # (batch, seq_len, hidden_dim) + mask = attention_mask[..., np.newaxis].astype(np.float32) + mean_embeddings = (token_embeddings * mask).sum(axis=1) / mask.sum(axis=1) + norms = np.linalg.norm(mean_embeddings, axis=1, keepdims=True) + normed = mean_embeddings / np.where(norms < 1e-12, 1.0, norms) + + # Adjust output dimension if needed (model native dim is 384) + native_dim = normed.shape[1] + if dim < native_dim: + normed = normed[:, :dim] + elif dim > native_dim: + pad = np.zeros((normed.shape[0], dim - native_dim), dtype=normed.dtype) + normed = np.concatenate([normed, pad], axis=1) + + return normed.tolist() + + +def embed(texts: list[str], *, prefix: str = "passage: ", dim: int = DEFAULT_EMBEDDING_DIM) -> list[list[float]]: + if _try_load_onnx(): + return _embed_with_onnx(texts, prefix=prefix, dim=dim) + return [_hash_embedding(f"{prefix}{text}", dim=dim) for text in texts] + + +def _cosine(a: list[float], b: list[float]) -> float: + return sum(left * right for left, right in zip(a, b)) + + +def _weighted_average(a: list[float], a_count: int, b: list[float], b_count: int) -> list[float]: + total = max(a_count + b_count, 1) + return _normalize([(left * a_count + right * b_count) / total for left, right in zip(a, b)]) + + +def _split_sections_by_char_budget(paragraphs: list[str], target_chars: int = 2000) -> list[dict[str, Any]]: + sections: list[dict[str, Any]] = [] + group: list[str] = [] + group_len = 0 + + for paragraph in paragraphs: + if group and group_len + len(paragraph) > target_chars: + combined = "\n\n".join(group) + sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) + group = [] + group_len = 0 + group.append(paragraph) + group_len += len(paragraph) + + if group: + combined = "\n\n".join(group) + sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) + return sections + + +def _split_sections_by_semantics( + markdown: str, + *, + target_chars: int = 2000, + max_chars: int = 4000, + similarity_threshold: float = 0.25, + embedding_dim: int = DEFAULT_EMBEDDING_DIM, +) -> list[dict[str, Any]]: + paragraphs = [paragraph.strip() for paragraph in _PARA_SPLIT_RE.split(markdown) if paragraph.strip()] + if not paragraphs: + return [] + if len(paragraphs) == 1: + return [{"heading": _extract_paragraph_heading(paragraphs[0]), "text": paragraphs[0], "level": 1}] + + embeddings = embed([paragraph[:700] for paragraph in paragraphs], dim=embedding_dim) + consecutive_sims = [_cosine(embeddings[index], embeddings[index + 1]) for index in range(len(embeddings) - 1)] + + sections: list[dict[str, Any]] = [] + group = [paragraphs[0]] + group_len = len(paragraphs[0]) + + for index in range(1, len(paragraphs)): + paragraph = paragraphs[index] + similarity = consecutive_sims[index - 1] if index - 1 < len(consecutive_sims) else 1.0 + budget_exceeded = group_len + len(paragraph) > max_chars + topic_shift = group_len >= target_chars and similarity < similarity_threshold + + if budget_exceeded or topic_shift: + combined = "\n\n".join(group) + sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) + group = [paragraph] + group_len = len(paragraph) + else: + group.append(paragraph) + group_len += len(paragraph) + + if group: + combined = "\n\n".join(group) + sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) + return sections or _split_sections_by_char_budget(paragraphs, target_chars=target_chars) + + +def _heading_section_threshold(content_len: int) -> int: + return max(2, min(8, content_len // 3000)) + + +def split_sections(markdown: str, *, min_chars: int = 100, embedding_dim: int = DEFAULT_EMBEDDING_DIM) -> list[dict[str, Any]]: + heading_sections = _split_sections_by_headings(markdown, min_chars=min_chars) + threshold = _heading_section_threshold(len(markdown)) + if len(heading_sections) >= threshold: + return heading_sections + + total_chars = sum(len(section["text"]) for section in heading_sections) if heading_sections else len(markdown) + if total_chars < min_chars * 2: + return heading_sections or [{"heading": "Document", "text": markdown.strip(), "level": 1}] + + print( + f"[raptor] Heading split yielded {len(heading_sections)} section(s); using semantic paragraph grouping.", + file=sys.stderr, + ) + return _split_sections_by_semantics(markdown, embedding_dim=embedding_dim) + + +def _agglomerative_cluster( + ids: list[str], + embeddings: list[list[float]], + *, + threshold: float = CLUSTER_THRESHOLD, +) -> list[list[str]]: + if len(ids) <= 1: + return [ids] if ids else [] + + clusters = [[node_id] for node_id in ids] + centroids = [list(embedding) for embedding in embeddings] + + while len(clusters) > 1: + best_i = -1 + best_j = -1 + best_similarity = -1.0 + for i in range(len(clusters)): + for j in range(i + 1, len(clusters)): + similarity = _cosine(centroids[i], centroids[j]) + if similarity > best_similarity: + best_i, best_j, best_similarity = i, j, similarity + + if best_i < 0 or best_j < 0 or best_similarity < threshold: + break + + left_count = len(clusters[best_i]) + right_count = len(clusters[best_j]) + clusters[best_i].extend(clusters[best_j]) + centroids[best_i] = _weighted_average(centroids[best_i], left_count, centroids[best_j], right_count) + del clusters[best_j] + del centroids[best_j] + + return clusters + + +def _make_cluster_summary(sections: list[dict[str, Any]]) -> str: + parts: list[str] = [] + for section in sections: + parts.append(f"## {section['heading']}") + text = str(section.get("text", ""))[:500].strip() + if text: + parts.append(text) + return "\n".join(parts)[:MAX_CLUSTER_SUMMARY_CHARS] + + +def build_raptor_tree( + sections: list[dict[str, Any]], + embeddings: list[list[float]], + *, + cluster_threshold: float = CLUSTER_THRESHOLD, + embedding_dim: int = DEFAULT_EMBEDDING_DIM, +) -> tuple[RaptorTree, dict[str, Any]]: + all_nodes: list[RaptorNode] = [] + leaf_ids: list[str] = [] + leaf_embeddings: list[list[float]] = [] + + for index, (section, embedding) in enumerate(zip(sections, embeddings)): + node_id = f"L0-{index}" + node: RaptorNode = { + "id": node_id, + "level": 0, + "heading": str(section["heading"]), + "text": str(section["text"])[:3000], + "embedding": embedding, + "children": [], + } + all_nodes.append(node) + leaf_ids.append(node_id) + leaf_embeddings.append(embedding) + + current_ids = leaf_ids + current_embeddings = leaf_embeddings + level = 1 + + while len(current_ids) > 1 and level <= 5: + clusters = _agglomerative_cluster(current_ids, current_embeddings, threshold=cluster_threshold) + if all(len(cluster) == 1 for cluster in clusters): + break + + node_map = {node["id"]: node for node in all_nodes} + next_ids: list[str] = [] + next_embeddings: list[list[float]] = [] + + for cluster_index, cluster_member_ids in enumerate(clusters): + if len(cluster_member_ids) == 1: + node = node_map[cluster_member_ids[0]] + next_ids.append(node["id"]) + next_embeddings.append(node["embedding"]) + continue + + cluster_sections = [ + {"heading": node_map[member_id]["heading"], "text": node_map[member_id]["text"]} + for member_id in cluster_member_ids + if member_id in node_map + ] + summary_text = _make_cluster_summary(cluster_sections) + headings = [str(section["heading"]) for section in cluster_sections] + cluster_embedding = embed([summary_text], dim=embedding_dim)[0] + cluster_id = f"L{level}-{cluster_index}" + cluster_node: RaptorNode = { + "id": cluster_id, + "level": level, + "heading": f"Cluster: {', '.join(headings[:3])}{'...' if len(headings) > 3 else ''}", + "text": summary_text, + "embedding": cluster_embedding, + "children": cluster_member_ids, + } + all_nodes.append(cluster_node) + next_ids.append(cluster_id) + next_embeddings.append(cluster_embedding) + + current_ids = next_ids + current_embeddings = next_embeddings + level += 1 + + node_map = {node["id"]: node for node in all_nodes} + top_nodes = [node_map[node_id] for node_id in current_ids if node_id in node_map] + main_theme = " | ".join(node["heading"] for node in top_nodes[:5])[:500] + global_summary = { + "mainTheme": main_theme, + "sectionCount": len(sections), + "topNodeCount": len(top_nodes), + "embedding": embed([main_theme or "document summary"], dim=embedding_dim)[0], + } + return {"nodes": all_nodes}, global_summary + + +def build_from_markdown( + markdown: str, + *, + title: str = "Untitled", + min_chars: int = 100, + embedding_dim: int = DEFAULT_EMBEDDING_DIM, + cluster_threshold: float = CLUSTER_THRESHOLD, +) -> StructuredSummary: + started_at = time.perf_counter() + sections = split_sections(markdown, min_chars=min_chars, embedding_dim=embedding_dim) + if not sections: + sections = [{"heading": title, "text": markdown[:5000], "level": 1}] + split_at = time.perf_counter() + print(f"[raptor] Split into {len(sections)} sections ({split_at - started_at:.2f}s)", file=sys.stderr) + + texts_to_embed = [f"{section['heading']}. {str(section['text'])[:1000]}" for section in sections] + embeddings = embed(texts_to_embed, dim=embedding_dim) + embed_at = time.perf_counter() + print(f"[raptor] Embedded {len(embeddings)} sections ({embed_at - split_at:.2f}s)", file=sys.stderr) + + tree, global_summary = build_raptor_tree( + sections, + embeddings, + cluster_threshold=cluster_threshold, + embedding_dim=embedding_dim, + ) + done_at = time.perf_counter() + print(f"[raptor] Built tree with {len(tree['nodes'])} nodes ({done_at - embed_at:.2f}s)", file=sys.stderr) + + return { + "documentTitle": title, + "globalSummary": global_summary, + "raptorTree": tree, + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Build a RAPTOR-style tree from markdown.") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--markdown-path", help="Path to a markdown file.") + group.add_argument("--markdown", help="Raw markdown text.") + parser.add_argument("--output-path", required=True, help="Where to write the structured summary JSON.") + parser.add_argument("--title", default="Untitled", help="Document title.") + parser.add_argument("--min-chars", type=int, default=100, help="Minimum characters required for a section.") + parser.add_argument("--embedding-dim", type=int, default=DEFAULT_EMBEDDING_DIM, help="Stable embedding vector size.") + parser.add_argument("--cluster-threshold", type=float, default=CLUSTER_THRESHOLD, help="Cosine threshold for cluster merges.") + parser.add_argument("--pretty", action="store_true", help="Pretty-print the output JSON file.") + return parser + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8") + + args = build_parser().parse_args() + markdown = Path(args.markdown_path).read_text(encoding="utf-8") if args.markdown_path else args.markdown + summary = build_from_markdown( + markdown, + title=args.title, + min_chars=args.min_chars, + embedding_dim=args.embedding_dim, + cluster_threshold=args.cluster_threshold, + ) + + output_path = Path(args.output_path).expanduser() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2 if args.pretty else None), encoding="utf-8") + print(json.dumps({"ok": True, "nodes": len(summary["raptorTree"]["nodes"]), "path": str(output_path)}, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 b/plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 new file mode 100644 index 000000000..b9b0caddb --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 @@ -0,0 +1,74 @@ +#Requires -Version 5.1 +[CmdletBinding()] +param( + [string]$MiniLmRepo = "sentence-transformers/all-MiniLM-L6-v2", + [string]$MiniLmRevision = "main", + [string]$MiniLmModelPath = "onnx/model_quint8_avx2.onnx", + + [switch]$Force +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Get-RepoRoot { + $scriptDir = Split-Path -Parent $PSCommandPath + return (Resolve-Path (Join-Path $scriptDir "..")).Path +} + +function Save-RemoteFile { + param( + [Parameter(Mandatory = $true)][string]$Uri, + [Parameter(Mandatory = $true)][string]$Destination, + [Parameter(Mandatory = $true)][bool]$Overwrite + ) + + if ((Test-Path -LiteralPath $Destination) -and -not $Overwrite) { + Write-Host "Exists: $Destination" + return + } + + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Destination) | Out-Null + $downloadPath = "$Destination.download" + if (Test-Path -LiteralPath $downloadPath) { + Remove-Item -LiteralPath $downloadPath -Force + } + + Write-Host "Downloading $Uri" + Invoke-WebRequest -Uri $Uri -OutFile $downloadPath -Headers @{ "User-Agent" = "pptify-external-assets" } + Move-Item -LiteralPath $downloadPath -Destination $Destination -Force + Write-Host "Wrote: $Destination" +} + +function Join-HuggingFaceResolveUrl { + param( + [Parameter(Mandatory = $true)][string]$Repo, + [Parameter(Mandatory = $true)][string]$Revision, + [Parameter(Mandatory = $true)][string]$Path + ) + + $encodedPath = (($Path -split "/") | ForEach-Object { [uri]::EscapeDataString($_) }) -join "/" + return "https://huggingface.co/$Repo/resolve/$Revision/$encodedPath" +} + +function Install-MiniLm { + param([Parameter(Mandatory = $true)][string]$RepoRoot) + + $targetDir = Join-Path $RepoRoot "pptify-plugin\external\all-MiniLM-L6-v2" + $files = @( + @{ Source = $MiniLmModelPath; Target = "model_quantized.onnx" }, + @{ Source = "tokenizer.json"; Target = "tokenizer.json" }, + @{ Source = "tokenizer_config.json"; Target = "tokenizer_config.json" } + ) + + foreach ($file in $files) { + $uri = Join-HuggingFaceResolveUrl -Repo $MiniLmRepo -Revision $MiniLmRevision -Path $file.Source + $destination = Join-Path $targetDir $file.Target + Save-RemoteFile -Uri $uri -Destination $destination -Overwrite ([bool]$Force) + } +} + +$repoRoot = Get-RepoRoot + +Install-MiniLm -RepoRoot $repoRoot \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/external/README.md b/plugins/pptify/.agent/pptify-plugin/external/README.md new file mode 100644 index 000000000..4cef8dd63 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/external/README.md @@ -0,0 +1,13 @@ +# External Assets + +This directory is for optional binary/model assets that are re-downloadable and ignored by git. + +Run from the repository root to restore the MiniLM ONNX model and tokenizer files: + +```powershell +.\pptify-plugin\download-external-assets.ps1 +``` + +Restores `all-MiniLM-L6-v2/model_quantized.onnx`, `tokenizer.json`, and `tokenizer_config.json` from the Hugging Face `sentence-transformers/all-MiniLM-L6-v2` repository. + +Pass `-MiniLmModelPath onnx/model.onnx` for the larger non-quantized model. Use `-Force` to overwrite existing files. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep b/plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep new file mode 100644 index 000000000..3c21ce7e3 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep @@ -0,0 +1 @@ +Download MiniLM ONNX/tokenizer assets with pptify-plugin/download-external-assets.ps1. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py b/plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py new file mode 100644 index 000000000..c006a9a75 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py @@ -0,0 +1,545 @@ +from __future__ import annotations + +import json +import posixpath +import sys +import zipfile +from base64 import b64encode +from collections import Counter +from pathlib import Path +from typing import Any +from xml.etree import ElementTree + +# Allow importing sibling pptx_style_master when run as a standalone script +sys.path.insert(0, str(Path(__file__).parent)) +from pptx_style_master import PptxStyleMaster + +EMU_PER_INCH = 914400 +DRAWING_NS = "{http://schemas.openxmlformats.org/drawingml/2006/main}" + + +class PptxExtractor: + def prompt_context(self, path: str | Path, max_chars: int = 16000) -> dict[str, Any]: + from pptx import Presentation + + pptx_path = Path(path) + presentation = Presentation(str(pptx_path)) + style_context = PptxStyleMaster().analyze(pptx_path) + slides: list[dict[str, Any]] = [] + used_chars = 0 + for slide_index, slide in enumerate(presentation.slides, start=1): + texts = _slide_text_fragments(slide.shapes) + trimmed_texts: list[str] = [] + for text in texts: + if used_chars >= max_chars: + break + cleaned = _compact_text(text) + if not cleaned: + continue + remaining = max_chars - used_chars + clipped = cleaned[: min(500, remaining)] + trimmed_texts.append(clipped) + used_chars += len(clipped) + title = trimmed_texts[0] if trimmed_texts else f"Slide {slide_index}" + slides.append( + { + "index": slide_index, + "title": title[:120], + "text": trimmed_texts[:12], + "shape_count": len(slide.shapes), + } + ) + media_files, embedded_files = _package_asset_counts(pptx_path) + return { + "source": str(pptx_path), + "slide_count": len(slides), + "slide_size": { + "width": _inches(presentation.slide_width), + "height": _inches(presentation.slide_height), + }, + "package_media_files": media_files, + "embedded_files": embedded_files, + "styles": style_context["styles"], + "brands": style_context["brands"], + "template": style_context["template"], + "layout": style_context["layout"], + "slides": slides, + } + + def extract_file(self, path: str | Path, output_dir: str | Path | None = None, extract_media: bool = True) -> dict[str, Any]: + from pptx import Presentation + + pptx_path = Path(path) + presentation = Presentation(str(pptx_path)) + asset_dir = None + embed_media = extract_media and output_dir is None + if output_dir and extract_media: + asset_dir = Path(output_dir) / f"{pptx_path.stem}_assets" + asset_dir.mkdir(parents=True, exist_ok=True) + _extract_package_media(pptx_path, asset_dir) + + notes_by_slide = _notes_by_slide(pptx_path) + slides: list[dict[str, Any]] = [] + stats: Counter[str] = Counter() + max_shapes = 0 + max_nested = 0 + for slide_index, slide in enumerate(presentation.slides, start=1): + tree, slide_stats, render_elements = self._extract_slide( + slide=slide, + slide_index=slide_index, + slide_size=(_inches(presentation.slide_width), _inches(presentation.slide_height)), + source_path=pptx_path, + asset_dir=asset_dir, + embed_media=embed_media, + notes=notes_by_slide.get(slide_index, []), + ) + stats.update(slide_stats) + max_shapes = max(max_shapes, slide_stats["top_level_shapes"]) + max_nested = max(max_nested, slide_stats["nested_shapes"]) + slides.append( + { + "id": tree["id"], + "title": tree["title"], + "slide_size": tree["slide_size"], + "preserve_coordinates": True, + "render_mode": "ooxml", + "ooxml_elements": render_elements, + "layout_tree": tree, + } + ) + + media_files, embedded_files = _package_asset_counts(pptx_path) + style_context = PptxStyleMaster().analyze(pptx_path) + summary = { + "source": str(pptx_path), + "slide_count": len(slides), + "slide_size": { + "width": _inches(presentation.slide_width), + "height": _inches(presentation.slide_height), + }, + "top_level_shapes": int(stats["top_level_shapes"]), + "nested_shapes": int(stats["nested_shapes"]), + "max_shapes_on_slide": max_shapes, + "max_nested_shapes_on_slide": max_nested, + "groups": int(stats["groups"]), + "tables": int(stats["tables"]), + "charts": int(stats["charts"]), + "images": int(stats["images"]), + "text_objects": int(stats["text_objects"]), + "placeholders": int(stats["placeholders"]), + "lines_or_freeforms": int(stats["lines_or_freeforms"]), + "non_ascii_text": bool(stats["non_ascii_text"]), + "notes_slides": int(stats["notes_slides"]), + "package_media_files": media_files, + "embedded_files": embedded_files, + "styles": style_context["styles"], + "brands": style_context["brands"], + "template": style_context["template"], + "layout": style_context["layout"], + } + return { + "source_pptx": str(pptx_path.resolve()), + "render_mode": "ooxml", + "summary": summary, + "slides": slides, + } + + def extract_path(self, path: str | Path, output_dir: str | Path, extract_media: bool = True) -> dict[str, Any]: + source = Path(path) + output = Path(output_dir) + output.mkdir(parents=True, exist_ok=True) + files = sorted(source.glob("*.pptx")) if source.is_dir() else [source] + decks = [] + for pptx_file in files: + deck = self.extract_file(pptx_file, output, extract_media=extract_media) + json_path = output / f"{pptx_file.stem}.pptify.json" + json_path.write_text(json.dumps(deck, indent=2, ensure_ascii=False), encoding="utf-8") + decks.append({"pptx": str(pptx_file), "json": str(json_path), "summary": deck["summary"]}) + manifest = {"source": str(source), "decks": decks} + (output / "manifest.json").write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8") + return manifest + + def analyze_path(self, path: str | Path) -> dict[str, Any]: + source = Path(path) + files = sorted(source.glob("*.pptx")) if source.is_dir() else [source] + return {"source": str(source), "decks": [self.extract_file(file, extract_media=False)["summary"] for file in files]} + + def _extract_slide( + self, + slide, + slide_index: int, + slide_size: tuple[float, float], + source_path: Path, + asset_dir: Path | None, + embed_media: bool, + notes: list[str], + ) -> tuple[dict[str, Any], Counter[str], list[dict[str, Any]]]: + root_id = f"slide_{slide_index}_root" + root_group: dict[str, Any] = { + "id": root_id, + "role": "slide", + "layout_mode": "absolute", + "object_ids": [], + "group_ids": [], + "constraints": {}, + "collision_policy": "relaxed", + "bbox": {"x": 0, "y": 0, "width": slide_size[0], "height": slide_size[1]}, + } + groups: dict[str, dict[str, Any]] = {root_id: root_group} + objects: dict[str, dict[str, Any]] = {} + stats: Counter[str] = Counter(top_level_shapes=len(slide.shapes), notes_slides=1 if notes else 0) + z_index = 0 + + def walk(shapes, parent_group_id: str, prefix: str) -> None: + nonlocal z_index + for shape_index, shape in enumerate(shapes, start=1): + z_index += 1 + stats["nested_shapes"] += 1 + shape_type = _shape_type_name(shape) + if _is_group(shape): + group_id = f"{prefix}_group_{shape_index}" + groups[parent_group_id]["group_ids"].append(group_id) + groups[group_id] = { + "id": group_id, + "role": "extracted_group", + "layout_mode": "absolute", + "object_ids": [], + "group_ids": [], + "constraints": {}, + "collision_policy": "relaxed", + "bbox": _bbox(shape), + } + stats["groups"] += 1 + walk(shape.shapes, group_id, group_id) + continue + + object_id = f"{prefix}_shape_{shape_index}" + slide_object = self._extract_object(shape, object_id, z_index, shape_type, source_path, asset_dir) + objects[object_id] = slide_object + groups[parent_group_id]["object_ids"].append(object_id) + stats[_stat_key(slide_object)] += 1 + if getattr(shape, "is_placeholder", False): + stats["placeholders"] += 1 + if _contains_non_ascii(slide_object["content"].get("text", "")): + stats["non_ascii_text"] += 1 + + walk(slide.shapes, root_id, f"slide_{slide_index}") + render_elements = [ + _render_element(shape, f"slide_{slide_index}_element_{element_index}", source_path, asset_dir, embed_media) + for element_index, shape in enumerate(slide.shapes, start=1) + ] + title = _slide_title(objects.values()) or f"Slide {slide_index}" + tree: dict[str, Any] = { + "id": f"slide_{slide_index}", + "title": title, + "slide_size": {"width": slide_size[0], "height": slide_size[1]}, + "root_group_id": root_id, + "groups": groups, + "objects": objects, + "notes": notes, + } + return tree, stats, render_elements + + def _extract_object(self, shape, object_id: str, z_index: int, shape_type: str, source_path: Path, asset_dir: Path | None) -> dict[str, Any]: + kind = _kind(shape, shape_type) + content: dict[str, Any] = {"source_shape_type": shape_type} + style: dict[str, Any] = {} + if kind == "text": + content["text"] = getattr(shape, "text", "") + style.update(_text_style(shape)) + elif kind == "table": + content["rows"] = [[cell.text for cell in row.cells] for row in shape.table.rows] + style["font_size"] = 8 + elif kind == "image": + content["alt"] = getattr(shape, "name", "image") + if asset_dir is not None: + image_data = _image_data(shape) + if image_data is None: + content["missing_embedded_image"] = True + else: + blob, extension, relationship_id, content_type = image_data + asset_path = asset_dir / f"{source_path.stem}_{object_id}.{extension}" + asset_path.write_bytes(blob) + content["path"] = str(asset_path) + content["content_type"] = content_type + if relationship_id: + content["media_relationship_id"] = relationship_id + elif kind == "chart": + content["title"] = getattr(shape, "name", "chart") + elif kind == "line": + box = _bbox(shape) + x, y, w, h = box["x"], box["y"], box["width"], box["height"] + content.update({"x1": x, "y1": y, "x2": x + w, "y2": y + h}) + style["line"] = "#6B7280" + elif getattr(shape, "has_text_frame", False) and getattr(shape, "text", ""): + content["text"] = shape.text + style.update(_text_style(shape)) + else: + content["shape"] = "rect" + + classification = "content" if kind in {"text", "table", "image", "chart"} else "layout_design" + if kind == "shape" and content.get("text"): + classification = "content" + return { + "id": object_id, + "kind": kind, + "role": _role(shape, kind), + "classification": classification, + "content": content, + "style": style, + "constraints": {"source_name": getattr(shape, "name", "")}, + "bbox": _bbox(shape), + "z_index": z_index, + } + + +def _inches(value: int) -> float: + return round(int(value) / EMU_PER_INCH, 4) + + +def _bbox(shape) -> dict[str, float]: + return { + "x": _inches(getattr(shape, "left", 0) or 0), + "y": _inches(getattr(shape, "top", 0) or 0), + "width": max(0.0, _inches(getattr(shape, "width", 0) or 0)), + "height": max(0.0, _inches(getattr(shape, "height", 0) or 0)), + } + + +def _shape_type_name(shape) -> str: + shape_type = getattr(shape, "shape_type", "unknown") + return str(getattr(shape_type, "name", shape_type)).lower() + + +def _is_group(shape) -> bool: + return hasattr(shape, "shapes") and "group" in _shape_type_name(shape) + + +def _kind(shape, shape_type: str) -> str: + if getattr(shape, "has_table", False): + return "table" + if getattr(shape, "has_chart", False): + return "chart" + if "picture" in shape_type or hasattr(shape, "image"): + return "image" + if "line" in shape_type or "freeform" in shape_type or "connector" in shape_type: + return "line" + if getattr(shape, "has_text_frame", False) and getattr(shape, "text", "").strip(): + return "text" + return "shape" + + +def _role(shape, kind: str) -> str: + if getattr(shape, "is_placeholder", False): + return "placeholder" + if kind == "text": + return "text" + return kind + + +def _text_style(shape) -> dict[str, Any]: + style: dict[str, Any] = {"font_size": 12} + try: + paragraph = shape.text_frame.paragraphs[0] + run = paragraph.runs[0] if paragraph.runs else None + font = run.font if run is not None else paragraph.font + if font.size is not None: + style["font_size"] = round(font.size.pt, 2) + if font.bold is not None: + style["bold"] = bool(font.bold) + if font.name: + style["font"] = font.name + except (AttributeError, IndexError): + pass + return style + + +def _image_data(shape) -> tuple[bytes, str, str | None, str] | None: + try: + image = shape.image + except ValueError: + image = None + if image is not None: + return image.blob, str(image.ext), None, image.content_type + + for relationship_id in _embedded_relationship_ids(shape): + try: + part = shape.part.related_part(relationship_id) + except KeyError: + continue + blob = getattr(part, "blob", None) + if not blob: + continue + extension = Path(str(getattr(part, "partname", "media.bin"))).suffix.lstrip(".") or _extension_from_content_type( + getattr(part, "content_type", "") + ) + return blob, extension or "bin", relationship_id, getattr(part, "content_type", "") + return None + + +def _render_element(shape, element_id: str, source_path: Path, asset_dir: Path | None, embed_media: bool) -> dict[str, Any]: + return { + "id": element_id, + "xml": shape._element.xml, + "relationships": _relationship_payloads(shape, element_id, source_path, asset_dir, embed_media), + } + + +def _relationship_payloads( + shape, + element_id: str, + source_path: Path, + asset_dir: Path | None, + embed_media: bool, +) -> list[dict[str, Any]]: + payloads: list[dict[str, Any]] = [] + for relationship_id in _embedded_relationship_ids(shape): + try: + relationship = shape.part.rels[relationship_id] + except KeyError: + continue + payload: dict[str, Any] = { + "source_rid": relationship_id, + "reltype": relationship.reltype, + "target_ref": relationship.target_ref, + "is_external": bool(relationship.is_external), + } + if relationship.is_external: + payloads.append(payload) + continue + part = relationship.target_part + blob = getattr(part, "blob", None) + if blob: + content_type = getattr(part, "content_type", "") + extension = Path(str(getattr(part, "partname", "media.bin"))).suffix.lstrip(".") or _extension_from_content_type(content_type) + payload.update({"content_type": content_type, "extension": extension or "bin"}) + if asset_dir is not None: + asset_path = asset_dir / f"{source_path.stem}_{element_id}_{relationship_id}.{payload['extension']}" + asset_path.write_bytes(blob) + payload["path"] = str(asset_path) + elif embed_media: + payload["blob_base64"] = b64encode(blob).decode("ascii") + payloads.append(payload) + return payloads + + +def _embedded_relationship_ids(shape) -> list[str]: + relationship_ids: list[str] = [] + try: + nodes = shape._element.iter() + except AttributeError: + return relationship_ids + for node in nodes: + for attribute_name, value in node.attrib.items(): + if attribute_name.endswith("}embed") and value not in relationship_ids: + relationship_ids.append(value) + return relationship_ids + + +def _extension_from_content_type(content_type: str) -> str: + mapping = { + "image/svg+xml": "svg", + "image/png": "png", + "image/jpeg": "jpg", + "image/jpg": "jpg", + "image/gif": "gif", + "image/bmp": "bmp", + "image/x-emf": "emf", + "image/x-wmf": "wmf", + } + return mapping.get(content_type.lower(), "bin") + + +def _stat_key(slide_object: dict[str, Any]) -> str: + kind = slide_object["kind"] + if kind == "table": + return "tables" + if kind == "chart": + return "charts" + if kind == "image": + return "images" + if kind == "text": + return "text_objects" + if kind == "line": + return "lines_or_freeforms" + return "shapes" + + +def _slide_title(objects) -> str: + for slide_object in objects: + text = str(slide_object["content"].get("text", "")).strip() + if text: + return text.splitlines()[0][:100] + return "" + + +def _contains_non_ascii(value: str) -> bool: + return any(ord(character) > 127 for character in value) + + +def _slide_text_fragments(shapes) -> list[str]: + fragments: list[str] = [] + for shape in shapes: + if hasattr(shape, "shapes"): + fragments.extend(_slide_text_fragments(shape.shapes)) + if getattr(shape, "has_table", False): + for row in shape.table.rows: + row_text = " | ".join(_compact_text(cell.text) for cell in row.cells if _compact_text(cell.text)) + if row_text: + fragments.append(row_text) + text = getattr(shape, "text", "") + if text: + fragments.append(text) + deduped: list[str] = [] + seen: set[str] = set() + for fragment in fragments: + cleaned = _compact_text(fragment) + if cleaned and cleaned not in seen: + seen.add(cleaned) + deduped.append(cleaned) + return deduped + + +def _compact_text(value: str) -> str: + return " ".join(str(value).split()) + + +def _package_asset_counts(path: Path) -> tuple[int, int]: + with zipfile.ZipFile(path) as package: + names = package.namelist() + media = sum(1 for name in names if name.startswith("ppt/media/")) + embedded = sum(1 for name in names if name.startswith("ppt/embeddings/")) + return media, embedded + + +def _extract_package_media(path: Path, asset_dir: Path) -> None: + with zipfile.ZipFile(path) as package: + for name in package.namelist(): + if not name.startswith("ppt/media/"): + continue + target = asset_dir / Path(name).name + target.write_bytes(package.read(name)) + + +def _notes_by_slide(path: Path) -> dict[int, list[str]]: + notes: dict[int, list[str]] = {} + with zipfile.ZipFile(path) as package: + names = set(package.namelist()) + slide_rels = sorted(name for name in names if name.startswith("ppt/slides/_rels/slide") and name.endswith(".xml.rels")) + for rel_path in slide_rels: + slide_number = int(Path(rel_path).name.removeprefix("slide").removesuffix(".xml.rels")) + rel_root = ElementTree.fromstring(package.read(rel_path)) + for rel in rel_root: + if "notesSlide" not in rel.attrib.get("Type", ""): + continue + target = rel.attrib.get("Target", "") + notes_path = posixpath.normpath(posixpath.join("ppt/slides", target)) + if notes_path not in names: + notes_path = posixpath.normpath(posixpath.join("ppt/slides/_rels", target)) + if notes_path not in names: + continue + notes_root = ElementTree.fromstring(package.read(notes_path)) + text = "\n".join(node.text for node in notes_root.iter(f"{DRAWING_NS}t") if node.text) + if text.strip(): + notes[slide_number] = [text.strip()] + return notes diff --git a/plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py b/plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py new file mode 100644 index 000000000..e5f02874a --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py @@ -0,0 +1,505 @@ +from __future__ import annotations + +import zipfile +from collections import Counter +from pathlib import Path +from typing import Any, Iterable +from xml.etree import ElementTree + +EMU_PER_INCH = 914400 +DRAWING_NS = "{http://schemas.openxmlformats.org/drawingml/2006/main}" + + +class PptxStyleMaster: + """Extract compact design context from a reference PPTX for prompt-based generation.""" + + def __init__(self, max_slides: int = 12, max_items: int = 10) -> None: + self.max_slides = max_slides + self.max_items = max_items + + def analyze(self, path: str | Path) -> dict[str, Any]: + from pptx import Presentation + + pptx_path = Path(path) + presentation = Presentation(str(pptx_path)) + slide_size = { + "width": _inches(presentation.slide_width), + "height": _inches(presentation.slide_height), + } + theme = _theme_from_package(pptx_path) + + colors: Counter[str] = Counter() + fonts: Counter[str] = Counter() + font_sizes: Counter[float] = Counter() + shape_styles: Counter[str] = Counter() + layout_names: Counter[str] = Counter() + master_names: Counter[str] = Counter() + slide_layouts: list[dict[str, Any]] = [] + + _count_theme_tokens(theme, colors, fonts) + for slide_index, slide in enumerate(presentation.slides, start=1): + if slide_index > self.max_slides: + break + slide_context = _slide_design_context(slide, slide_index, slide_size, self.max_items) + slide_layouts.append(slide_context) + layout_names[slide_context["template_layout"]] += 1 + master_names[slide_context["template_master"]] += 1 + colors.update(slide_context.pop("_colors")) + fonts.update(slide_context.pop("_fonts")) + font_sizes.update(slide_context.pop("_font_sizes")) + shape_styles.update(slide_context.pop("_shape_styles")) + + return { + "styles": { + "colors": _top_items(colors, self.max_items), + "fonts": _top_items(fonts, self.max_items), + "font_sizes": _top_items(font_sizes, self.max_items), + "shape_styles": _top_items(shape_styles, self.max_items), + }, + "brands": _brand_context(colors, fonts, theme, self.max_items), + "template": _template_context(presentation, slide_size, theme, layout_names, master_names, self.max_items), + "layout": { + "analyzed_slide_count": len(slide_layouts), + "layout_usage": _top_items(layout_names, self.max_items), + "master_usage": _top_items(master_names, self.max_items), + "slides": slide_layouts, + }, + } + + +def extract_pptx_style_master(path: str | Path, max_slides: int = 12, max_items: int = 10) -> dict[str, Any]: + return PptxStyleMaster(max_slides=max_slides, max_items=max_items).analyze(path) + + +def _slide_design_context(slide, slide_index: int, slide_size: dict[str, float], max_items: int) -> dict[str, Any]: + colors: Counter[str] = Counter() + fonts: Counter[str] = Counter() + font_sizes: Counter[float] = Counter() + shape_styles: Counter[str] = Counter() + object_counts: Counter[str] = Counter() + regions: Counter[str] = Counter() + placeholders: list[dict[str, Any]] = [] + object_samples: list[dict[str, Any]] = [] + boxes: list[dict[str, float]] = [] + + for shape in _iter_shapes(slide.shapes): + kind = _shape_kind(shape) + bbox = _bbox(shape) + boxes.append(bbox) + object_counts[kind] += 1 + regions[_region(bbox, slide_size)] += 1 + + shape_colors = _shape_colors(shape) + colors.update(shape_colors.values()) + shape_text = _text_preview(shape) + text_styles = _text_styles(shape) + fonts.update(text_styles["fonts"]) + font_sizes.update(text_styles["font_sizes"]) + colors.update(text_styles["colors"]) + + style_signature = _style_signature(shape_colors, text_styles) + if style_signature: + shape_styles[style_signature] += 1 + + if getattr(shape, "is_placeholder", False) and len(placeholders) < max_items: + placeholders.append(_placeholder_context(shape, bbox)) + + if len(object_samples) < max_items: + sample: dict[str, Any] = { + "kind": kind, + "role": _shape_role(shape, kind), + "bbox": bbox, + "region": _region(bbox, slide_size), + } + if shape_text: + sample["text"] = shape_text + if shape_colors: + sample["colors"] = shape_colors + if text_styles["fonts"]: + sample["fonts"] = _top_items(text_styles["fonts"], 3) + if text_styles["font_sizes"]: + sample["font_sizes"] = _top_items(text_styles["font_sizes"], 3) + object_samples.append(sample) + + return { + "index": slide_index, + "template_layout": _slide_layout_name(slide), + "template_master": _slide_master_name(slide), + "object_counts": dict(sorted(object_counts.items())), + "placeholder_count": len(placeholders), + "placeholders": placeholders, + "dominant_regions": _top_items(regions, max_items), + "dominant_flow": _dominant_flow(boxes, slide_size), + "occupied_area_ratio": _occupied_area_ratio(boxes, slide_size), + "objects": object_samples, + "_colors": colors, + "_fonts": fonts, + "_font_sizes": font_sizes, + "_shape_styles": shape_styles, + } + + +def _template_context( + presentation, + slide_size: dict[str, float], + theme: dict[str, Any], + layout_names: Counter[str], + master_names: Counter[str], + max_items: int, +) -> dict[str, Any]: + masters: list[dict[str, Any]] = [] + try: + for master_index, master in enumerate(presentation.slide_masters, start=1): + masters.append( + { + "index": master_index, + "name": str(getattr(master, "name", f"Master {master_index}") or f"Master {master_index}"), + "layout_count": len(master.slide_layouts), + } + ) + except (AttributeError, TypeError): + masters = [] + + return { + "slide_size": slide_size, + "theme": theme, + "masters": masters[:max_items], + "layout_usage": _top_items(layout_names, max_items), + "master_usage": _top_items(master_names, max_items), + } + + +def _brand_context(colors: Counter[str], fonts: Counter[str], theme: dict[str, Any], max_items: int) -> dict[str, Any]: + theme_colors = theme.get("colors", {}) if isinstance(theme.get("colors"), dict) else {} + theme_accents = [value for name, value in theme_colors.items() if str(name).startswith("accent")] + palette = _ranked_colors(colors, include_neutral=False) + if not palette: + palette = [color for color in theme_accents if _is_hex_color(color)] + neutral_palette = _ranked_colors(colors, include_neutral=True, only_neutral=True) + font_values = [str(item["value"]) for item in _top_items(fonts, max_items)] + primary_color = palette[0] if palette else None + accent_colors = _dedupe([*palette, *theme_accents])[:max_items] + + return { + "theme_name": theme.get("name"), + "primary_color": primary_color, + "accent_colors": accent_colors, + "neutral_colors": neutral_palette[:max_items], + "fonts": font_values[:max_items], + "theme_colors": theme_colors, + "theme_fonts": theme.get("fonts", {}), + } + + +def _theme_from_package(path: Path) -> dict[str, Any]: + theme_paths: list[str] + try: + with zipfile.ZipFile(path) as package: + theme_paths = sorted(name for name in package.namelist() if name.startswith("ppt/theme/theme") and name.endswith(".xml")) + if not theme_paths: + return {"name": None, "colors": {}, "fonts": {}} + root = ElementTree.fromstring(package.read(theme_paths[0])) + except (zipfile.BadZipFile, KeyError, ElementTree.ParseError): + return {"name": None, "colors": {}, "fonts": {}} + + theme = { + "name": root.attrib.get("name"), + "path": theme_paths[0], + "colors": {}, + "fonts": {}, + } + color_scheme = root.find(f".//{DRAWING_NS}clrScheme") + if color_scheme is not None: + colors: dict[str, str] = {} + for color_node in list(color_scheme): + color_value = _theme_color_value(color_node) + if color_value: + colors[color_node.tag.rsplit("}", 1)[-1]] = color_value + theme["colors"] = colors + + font_scheme = root.find(f".//{DRAWING_NS}fontScheme") + if font_scheme is not None: + fonts: dict[str, str] = {} + for key, node_name in (("major", "majorFont"), ("minor", "minorFont")): + latin = font_scheme.find(f".//{DRAWING_NS}{node_name}/{DRAWING_NS}latin") + if latin is not None and latin.attrib.get("typeface"): + fonts[key] = latin.attrib["typeface"] + theme["fonts"] = fonts + return theme + + +def _theme_color_value(color_node: ElementTree.Element) -> str | None: + srgb = color_node.find(f".//{DRAWING_NS}srgbClr") + if srgb is not None and srgb.attrib.get("val"): + return _normalize_hex(srgb.attrib["val"]) + system = color_node.find(f".//{DRAWING_NS}sysClr") + if system is not None and system.attrib.get("lastClr"): + return _normalize_hex(system.attrib["lastClr"]) + return None + + +def _count_theme_tokens(theme: dict[str, Any], colors: Counter[str], fonts: Counter[str]) -> None: + for color in theme.get("colors", {}).values() if isinstance(theme.get("colors"), dict) else []: + if _is_hex_color(color): + colors[color] += 1 + for font in theme.get("fonts", {}).values() if isinstance(theme.get("fonts"), dict) else []: + if font: + fonts[str(font)] += 1 + + +def _iter_shapes(shapes) -> Iterable[Any]: + for shape in shapes: + yield shape + if hasattr(shape, "shapes"): + yield from _iter_shapes(shape.shapes) + + +def _shape_kind(shape) -> str: + shape_type = str(getattr(getattr(shape, "shape_type", "unknown"), "name", "unknown")).lower() + if getattr(shape, "has_table", False): + return "table" + if getattr(shape, "has_chart", False): + return "chart" + if "picture" in shape_type or _has_image(shape): + return "image" + if "line" in shape_type or "connector" in shape_type or "freeform" in shape_type: + return "line" + if getattr(shape, "has_text_frame", False) and getattr(shape, "text", "").strip(): + return "text" + if hasattr(shape, "shapes"): + return "group" + return "shape" + + +def _has_image(shape) -> bool: + try: + return getattr(shape, "image", None) is not None + except (AttributeError, TypeError, ValueError): + return False + + +def _shape_role(shape, kind: str) -> str: + if getattr(shape, "is_placeholder", False): + try: + return str(shape.placeholder_format.type).split(".")[-1].lower() + except (AttributeError, ValueError): + return "placeholder" + name = str(getattr(shape, "name", "") or "").strip().lower() + if "title" in name: + return "title" + return kind + + +def _shape_colors(shape) -> dict[str, str]: + colors: dict[str, str] = {} + fill = _format_color(_safe_attr(_safe_attr(shape, "fill"), "fore_color")) + if fill: + colors["fill"] = fill + line = _format_color(_safe_attr(_safe_attr(shape, "line"), "color")) + if line: + colors["line"] = line + return colors + + +def _safe_attr(value: Any, name: str) -> Any: + if value is None: + return None + try: + return getattr(value, name) + except (AttributeError, TypeError, ValueError): + return None + + +def _text_styles(shape) -> dict[str, Counter[Any]]: + fonts: Counter[str] = Counter() + font_sizes: Counter[float] = Counter() + colors: Counter[str] = Counter() + text_frame = getattr(shape, "text_frame", None) + if text_frame is None: + return {"fonts": fonts, "font_sizes": font_sizes, "colors": colors} + + for paragraph in text_frame.paragraphs: + _count_font(paragraph.font, fonts, font_sizes, colors) + for run in paragraph.runs: + _count_font(run.font, fonts, font_sizes, colors) + return {"fonts": fonts, "font_sizes": font_sizes, "colors": colors} + + +def _count_font(font, fonts: Counter[str], font_sizes: Counter[float], colors: Counter[str]) -> None: + name = getattr(font, "name", None) + if name: + fonts[str(name)] += 1 + size = getattr(font, "size", None) + if size is not None: + font_sizes[round(size.pt, 2)] += 1 + color = _format_color(getattr(font, "color", None)) + if color: + colors[color] += 1 + + +def _format_color(color_format) -> str | None: + if color_format is None: + return None + try: + rgb = color_format.rgb + except (AttributeError, TypeError, ValueError): + rgb = None + if rgb is not None: + return _normalize_hex(str(rgb)) + try: + theme_color = color_format.theme_color + except (AttributeError, TypeError, ValueError): + theme_color = None + if theme_color: + token = str(theme_color).split(".")[-1].lower() + return f"theme:{token}" + return None + + +def _style_signature(shape_colors: dict[str, str], text_styles: dict[str, Counter[Any]]) -> str: + parts: list[str] = [] + if shape_colors.get("fill"): + parts.append(f"fill={shape_colors['fill']}") + if shape_colors.get("line"): + parts.append(f"line={shape_colors['line']}") + font = _top_value(text_styles["fonts"]) + if font: + parts.append(f"font={font}") + font_size = _top_value(text_styles["font_sizes"]) + if font_size: + parts.append(f"font_size={font_size}") + return "; ".join(parts) + + +def _placeholder_context(shape, bbox: dict[str, float]) -> dict[str, Any]: + context: dict[str, Any] = { + "name": str(getattr(shape, "name", "") or ""), + "bbox": bbox, + } + try: + context["type"] = str(shape.placeholder_format.type).split(".")[-1].lower() + context["idx"] = int(shape.placeholder_format.idx) + except (AttributeError, ValueError): + context["type"] = "placeholder" + return context + + +def _text_preview(shape, max_chars: int = 120) -> str: + text = " ".join(str(getattr(shape, "text", "")).split()) + return text[:max_chars] + + +def _bbox(shape) -> dict[str, float]: + return { + "x": _inches(getattr(shape, "left", 0)), + "y": _inches(getattr(shape, "top", 0)), + "width": max(0.0, _inches(getattr(shape, "width", 0))), + "height": max(0.0, _inches(getattr(shape, "height", 0))), + } + + +def _region(bbox: dict[str, float], slide_size: dict[str, float]) -> str: + width = max(slide_size["width"], 0.01) + height = max(slide_size["height"], 0.01) + center_x = (bbox["x"] + bbox["width"] / 2) / width + center_y = (bbox["y"] + bbox["height"] / 2) / height + horizontal = "left" if center_x < 0.34 else "right" if center_x > 0.66 else "center" + vertical = "top" if center_y < 0.34 else "bottom" if center_y > 0.66 else "middle" + return f"{vertical}_{horizontal}" + + +def _dominant_flow(boxes: list[dict[str, float]], slide_size: dict[str, float]) -> str: + if len(boxes) < 2: + return "single" + centers_x = [(box["x"] + box["width"] / 2) / max(slide_size["width"], 0.01) for box in boxes] + centers_y = [(box["y"] + box["height"] / 2) / max(slide_size["height"], 0.01) for box in boxes] + spread_x = max(centers_x) - min(centers_x) + spread_y = max(centers_y) - min(centers_y) + if len(boxes) >= 4 and spread_x > 0.32 and spread_y > 0.32: + return "grid" + if spread_x > 0.42 and spread_y > 0.42: + return "grid" + if len(boxes) >= 3 and spread_x > 0.42: + return "grid" + if spread_x > spread_y * 1.4: + return "row" + if spread_y > spread_x * 1.4: + return "column" + return "overlay_or_balanced" + + +def _occupied_area_ratio(boxes: list[dict[str, float]], slide_size: dict[str, float]) -> float: + slide_area = max(slide_size["width"] * slide_size["height"], 0.01) + object_area = sum(box["width"] * box["height"] for box in boxes) + return round(min(object_area / slide_area, 1.0), 3) + + +def _slide_layout_name(slide) -> str: + try: + return str(slide.slide_layout.name or "unnamed_layout") + except AttributeError: + return "unknown_layout" + + +def _slide_master_name(slide) -> str: + try: + master = slide.slide_layout.slide_master + return str(master.name or "unnamed_master") + except AttributeError: + return "unknown_master" + + +def _top_items(counter: Counter[Any], limit: int) -> list[dict[str, Any]]: + return [{"value": value, "count": count} for value, count in counter.most_common(limit)] + + +def _top_value(counter: Counter[Any]) -> Any | None: + if not counter: + return None + return counter.most_common(1)[0][0] + + +def _ranked_colors(colors: Counter[str], include_neutral: bool, only_neutral: bool = False) -> list[str]: + ranked: list[str] = [] + for color, _count in colors.most_common(): + if not _is_hex_color(color): + continue + neutral = _is_neutral(color) + if only_neutral and not neutral: + continue + if not include_neutral and neutral: + continue + ranked.append(color) + return ranked + + +def _is_neutral(color: str) -> bool: + if not _is_hex_color(color): + return False + red = int(color[1:3], 16) + green = int(color[3:5], 16) + blue = int(color[5:7], 16) + return max(red, green, blue) - min(red, green, blue) <= 18 + + +def _is_hex_color(value: Any) -> bool: + return isinstance(value, str) and len(value) == 7 and value.startswith("#") + + +def _normalize_hex(value: str) -> str: + stripped = value.strip().lstrip("#") + if len(stripped) >= 6: + return f"#{stripped[:6].upper()}" + return f"#{stripped.upper()}" + + +def _dedupe(values: Iterable[str]) -> list[str]: + deduped: list[str] = [] + for value in values: + if value and value not in deduped: + deduped.append(value) + return deduped + + +def _inches(value: int) -> float: + return round(int(value) / EMU_PER_INCH, 4) diff --git a/plugins/pptify/.agent/pptify-plugin/images/__init__.py b/plugins/pptify/.agent/pptify-plugin/images/__init__.py new file mode 100644 index 000000000..cd7df68fc --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/images/__init__.py @@ -0,0 +1 @@ +"""Image and infographic helpers for pptify plugins.""" \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py b/plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py new file mode 100644 index 000000000..3c5093731 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import argparse +import json +import re +import sys +from typing import Any +from urllib.parse import urlencode +from urllib.request import Request, urlopen + + +ICONIFY_API_HOSTS = ( + "https://api.iconify.design", + "https://api.simplesvg.com", + "https://api.unisvg.com", +) + +ICONIFY_COLLECTIONS: dict[str, list[str]] = { + "all": [ + "mdi:trending-up", + "lucide:brain", + "tabler:building-skyscraper", + "fa6-solid:rocket", + "ph:chart-line-up-bold", + "fluent:people-team-24-regular", + ], + "mdi": ["mdi:trending-up", "mdi:brain", "mdi:domain", "mdi:rocket-outline"], + "lucide": ["lucide:brain", "lucide:line-chart", "lucide:building-2", "lucide:rocket"], + "tabler": ["tabler:building-skyscraper", "tabler:chart-line", "tabler:bulb", "tabler:target-arrow"], + "ph": ["ph:chart-line-up-bold", "ph:brain-bold", "ph:buildings-bold", "ph:rocket-launch-bold"], + "fa6-solid": ["fa6-solid:rocket", "fa6-solid:chart-line", "fa6-solid:building", "fa6-solid:lightbulb"], + "fluent": [ + "fluent:people-team-24-regular", + "fluent:brain-circuit-24-regular", + "fluent:building-24-regular", + "fluent:arrow-trending-24-regular", + ], +} + + +def get_default_iconify_prefix(collection: str = "all") -> str: + return "mdi" if collection == "all" else collection + + +def normalize_icon_name(value: str | None, collection: str = "all") -> str | None: + raw = (value or "").strip().lower() + if not raw: + return None + raw = raw.replace(" ", "-") + if ":" in raw: + return raw + return f"{get_default_iconify_prefix(collection)}:{raw}" + + +def build_iconify_svg_url(icon_name: str, color_hex: str | None = None, *, host_index: int = 0, collection: str = "all") -> str: + normalized = normalize_icon_name(icon_name, collection) + if not normalized or ":" not in normalized: + raise ValueError(f"Invalid Iconify icon name: {icon_name}") + prefix, name = normalized.split(":", 1) + query: dict[str, str] = {"box": "1"} + if color_hex: + query["color"] = color_hex if color_hex.startswith("#") else f"#{color_hex}" + host = ICONIFY_API_HOSTS[min(max(host_index, 0), len(ICONIFY_API_HOSTS) - 1)] + return f"{host}/{prefix}/{name}.svg?{urlencode(query)}" + + +def _request_json(host: str, path: str, params: dict[str, str]) -> dict[str, Any]: + url = f"{host}{path}?{urlencode(params)}" + request = Request(url, headers={"User-Agent": "pptify-plugin/0.1"}) + with urlopen(request, timeout=15) as response: + return json.loads(response.read().decode("utf-8", errors="replace")) + + +def _title_for_icon(icon_id: str) -> str: + name = icon_id.split(":", 1)[-1] + return re.sub(r"[-_]+", " ", name).strip().title() or icon_id + + +def _candidate(icon_id: str, query: str, color_hex: str | None) -> dict[str, str | None]: + prefix = icon_id.split(":", 1)[0] if ":" in icon_id else None + return { + "provider": "iconify", + "searchQuery": query, + "iconId": icon_id, + "prefix": prefix, + "name": icon_id.split(":", 1)[-1], + "title": _title_for_icon(icon_id), + "svgUrl": build_iconify_svg_url(icon_id, color_hex), + "sourceUrl": f"https://icon-sets.iconify.design/{icon_id.replace(':', '/')}/", + } + + +def _fallback_icons(query: str, collection: str, max_num: int) -> list[str]: + examples = ICONIFY_COLLECTIONS.get(collection, ICONIFY_COLLECTIONS["all"]) + terms = {term for term in re.split(r"[^a-z0-9]+", query.lower()) if term} + matched = [icon for icon in examples if any(term in icon for term in terms)] + icons = matched or examples + return icons[:max_num] + + +def search_icons(query: str, *, collection: str = "all", max_num: int = 12, color_hex: str | None = None) -> tuple[list[dict[str, str | None]], list[str]]: + collection = collection.strip().lower() or "all" + max_num = max(1, min(max_num, 50)) + errors: list[str] = [] + icon_ids: list[str] = [] + + normalized_direct = normalize_icon_name(query, collection) + if normalized_direct and ":" in normalized_direct and " " not in query.strip(): + icon_ids.append(normalized_direct) + + params = {"query": query, "limit": str(max_num)} + if collection != "all": + params["prefix"] = collection + + for host in ICONIFY_API_HOSTS: + if len(icon_ids) >= max_num: + break + try: + data = _request_json(host, "/search", params) + except Exception as exc: + errors.append(f"{host}: {exc}") + continue + + for icon in data.get("icons", []): + if not isinstance(icon, str): + continue + normalized = normalize_icon_name(icon, collection) + if normalized and normalized not in icon_ids: + icon_ids.append(normalized) + if len(icon_ids) >= max_num: + break + + if not icon_ids: + icon_ids.extend(_fallback_icons(query, collection, max_num)) + + candidates = [_candidate(icon_id, query, color_hex) for icon_id in icon_ids[:max_num]] + return candidates, errors + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Search Iconify icons and return SVG URLs.") + parser.add_argument("--query", action="append", required=True, help="Icon search query or full icon ID. Can be repeated.") + parser.add_argument("--collection", default="all", help="Iconify collection prefix such as mdi, lucide, tabler, ph, fa6-solid, fluent, or all.") + parser.add_argument("--color", help="Optional SVG color hex value.") + parser.add_argument("--max-num", type=int, default=12, help="Maximum candidates per query.") + parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") + return parser + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + args = build_parser().parse_args() + queries = [query.strip() for query in args.query if query.strip()] + candidates: list[dict[str, str | None]] = [] + errors: list[str] = [] + for query in queries: + query_candidates, query_errors = search_icons( + query, + collection=args.collection, + max_num=args.max_num, + color_hex=args.color, + ) + candidates.extend(query_candidates) + errors.extend(query_errors) + + payload = { + "ok": bool(candidates), + "query": "\n".join(queries), + "collection": args.collection, + "candidates": candidates, + "errors": errors, + } + print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) + return 0 if payload["ok"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py b/plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py new file mode 100644 index 000000000..1f74bf429 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import argparse +import base64 +import html +import importlib +import json +import struct +import sys +from pathlib import Path +from typing import Any + + +MIME_BY_SUFFIX = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".bmp": "image/bmp", + ".webp": "image/webp", +} + +RASTER_MODE = "embedded-raster" +VECTOR_MODE = "vector-trace" + + +def _png_size(data: bytes) -> tuple[int, int] | None: + if data.startswith(b"\x89PNG\r\n\x1a\n") and len(data) >= 24: + return struct.unpack(">II", data[16:24]) + return None + + +def _gif_size(data: bytes) -> tuple[int, int] | None: + if data[:6] in {b"GIF87a", b"GIF89a"} and len(data) >= 10: + return struct.unpack(" tuple[int, int] | None: + if not data.startswith(b"\xff\xd8"): + return None + index = 2 + while index + 9 < len(data): + if data[index] != 0xFF: + index += 1 + continue + marker = data[index + 1] + index += 2 + if marker in {0xD8, 0xD9}: + continue + if index + 2 > len(data): + break + segment_length = struct.unpack(">H", data[index:index + 2])[0] + if marker in {0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF}: + if index + 7 <= len(data): + height, width = struct.unpack(">HH", data[index + 3:index + 7]) + return width, height + break + index += max(segment_length, 2) + return None + + +def _webp_size(data: bytes) -> tuple[int, int] | None: + if len(data) < 30 or not data.startswith(b"RIFF") or data[8:12] != b"WEBP": + return None + chunk = data[12:16] + if chunk == b"VP8X" and len(data) >= 30: + width = int.from_bytes(data[24:27], "little") + 1 + height = int.from_bytes(data[27:30], "little") + 1 + return width, height + if chunk == b"VP8L" and len(data) >= 25: + bits = int.from_bytes(data[21:25], "little") + width = (bits & 0x3FFF) + 1 + height = ((bits >> 14) & 0x3FFF) + 1 + return width, height + return None + + +def image_dimensions(data: bytes) -> tuple[int, int]: + for reader in (_png_size, _jpeg_size, _gif_size, _webp_size): + size = reader(data) + if size: + return size + try: + from io import BytesIO + + from PIL import Image + + with Image.open(BytesIO(data)) as image: + return image.size + except Exception: + return 1, 1 + + +def _trace_with_vtracer( + source_path: Path, + destination: Path, + *, + colormode: str, + hierarchical: str, + trace_mode: str, + filter_speckle: int, + color_precision: int, + layer_difference: int, + corner_threshold: int, + length_threshold: float, + max_iterations: int, + splice_threshold: int, + path_precision: int, +) -> dict[str, Any] | None: + try: + vtracer = importlib.import_module("vtracer") + except ImportError: + return { + "ok": False, + "error": "vtracer is not installed. Install plugin dependencies with vtracer support, or use --mode embedded-raster.", + "code": "vtracer_not_installed", + } + + try: + vtracer.convert_image_to_svg_py( + str(source_path), + str(destination), + colormode=colormode, + hierarchical=hierarchical, + mode=trace_mode, + filter_speckle=filter_speckle, + color_precision=color_precision, + layer_difference=layer_difference, + corner_threshold=corner_threshold, + length_threshold=length_threshold, + max_iterations=max_iterations, + splice_threshold=splice_threshold, + path_precision=path_precision, + ) + except Exception as exc: + return {"ok": False, "error": f"vtracer conversion failed: {exc}", "code": "vtracer_failed"} + + return None + + +def raster_to_svg( + source: str | Path, + output_path: str | Path | None = None, + *, + title: str | None = None, + mode: str = RASTER_MODE, + colormode: str = "color", + hierarchical: str = "stacked", + trace_mode: str = "spline", + filter_speckle: int = 4, + color_precision: int = 6, + layer_difference: int = 16, + corner_threshold: int = 60, + length_threshold: float = 4.0, + max_iterations: int = 10, + splice_threshold: int = 45, + path_precision: int = 3, +) -> dict[str, Any]: + source_path = Path(source).expanduser() + if not source_path.exists(): + return {"ok": False, "error": f"Source file does not exist: {source_path}", "code": "source_not_found"} + + destination = Path(output_path).expanduser() if output_path else source_path.with_suffix(".svg") + destination.parent.mkdir(parents=True, exist_ok=True) + + data = source_path.read_bytes() + width, height = image_dimensions(data) + + if mode == VECTOR_MODE: + error = _trace_with_vtracer( + source_path, + destination, + colormode=colormode, + hierarchical=hierarchical, + trace_mode=trace_mode, + filter_speckle=filter_speckle, + color_precision=color_precision, + layer_difference=layer_difference, + corner_threshold=corner_threshold, + length_threshold=length_threshold, + max_iterations=max_iterations, + splice_threshold=splice_threshold, + path_precision=path_precision, + ) + if error: + return error + return { + "ok": True, + "source": str(source_path), + "path": str(destination), + "width": width, + "height": height, + "mode": VECTOR_MODE, + "vectorizer": "vtracer", + "trace_options": { + "colormode": colormode, + "hierarchical": hierarchical, + "trace_mode": trace_mode, + "filter_speckle": filter_speckle, + "color_precision": color_precision, + "layer_difference": layer_difference, + "corner_threshold": corner_threshold, + "length_threshold": length_threshold, + "max_iterations": max_iterations, + "splice_threshold": splice_threshold, + "path_precision": path_precision, + }, + } + + if mode != RASTER_MODE: + return {"ok": False, "error": f"Unsupported mode: {mode}", "code": "unsupported_mode"} + + mime = MIME_BY_SUFFIX.get(source_path.suffix.lower(), "application/octet-stream") + encoded = base64.b64encode(data).decode("ascii") + safe_title = html.escape(title or source_path.stem) + svg = ( + f'\n' + f" {safe_title}\n" + f' \n' + "\n" + ) + + destination.write_text(svg, encoding="utf-8") + return { + "ok": True, + "source": str(source_path), + "path": str(destination), + "width": width, + "height": height, + "mode": "embedded-raster", + "mime": mime, + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Wrap a raster image in a valid SVG image element.") + parser.add_argument("--source", required=True, help="Path to a raster image.") + parser.add_argument("--output-path", help="Destination SVG path. Defaults to the source name with .svg.") + parser.add_argument("--title", help="Accessible SVG title.") + parser.add_argument( + "--mode", + choices=(RASTER_MODE, VECTOR_MODE), + default=RASTER_MODE, + help="Conversion mode. embedded-raster wraps the source bytes; vector-trace uses optional vtracer paths.", + ) + parser.add_argument("--colormode", choices=("color", "binary"), default="color", help="vtracer color mode.") + parser.add_argument("--hierarchical", choices=("stacked", "cutout"), default="stacked", help="vtracer color clustering mode.") + parser.add_argument("--trace-mode", choices=("spline", "polygon", "none"), default="spline", help="vtracer curve fitting mode.") + parser.add_argument("--filter-speckle", type=int, default=4, help="Discard traced patches smaller than this pixel count.") + parser.add_argument("--color-precision", type=int, default=6, help="Number of significant bits per RGB channel for vtracer.") + parser.add_argument("--layer-difference", type=int, default=16, help="Color difference between vtracer gradient layers.") + parser.add_argument("--corner-threshold", type=int, default=60, help="Minimum angle in degrees to be considered a corner.") + parser.add_argument("--length-threshold", type=float, default=4.0, help="vtracer segment length threshold.") + parser.add_argument("--max-iterations", type=int, default=10, help="vtracer smoothing iteration cap.") + parser.add_argument("--splice-threshold", type=int, default=45, help="Minimum angle displacement in degrees to splice a spline.") + parser.add_argument("--path-precision", type=int, default=3, help="Decimal places to use in traced SVG path data.") + parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") + return parser + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + args = build_parser().parse_args() + payload = raster_to_svg( + args.source, + args.output_path, + title=args.title, + mode=args.mode, + colormode=args.colormode, + hierarchical=args.hierarchical, + trace_mode=args.trace_mode, + filter_speckle=args.filter_speckle, + color_precision=args.color_precision, + layer_difference=args.layer_difference, + corner_threshold=args.corner_threshold, + length_threshold=args.length_threshold, + max_iterations=args.max_iterations, + splice_threshold=args.splice_threshold, + path_precision=args.path_precision, + ) + print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) + return 0 if payload.get("ok") else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py b/plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py new file mode 100644 index 000000000..2bb4c2899 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import argparse +import base64 +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Any +from urllib.error import HTTPError +from urllib.parse import urljoin +from urllib.request import Request, urlopen + +_ENV_TEMPLATE = ".env.template" + + +def _env_guidance() -> str: + return f"Copy {_ENV_TEMPLATE} to .env, fill the required image-provider values, then rerun the command." + + +def _dotenv_paths() -> list[Path]: + paths: list[Path] = [] + for base in (Path.cwd(), Path(__file__).resolve().parents[2]): + candidate = base / ".env" + if candidate not in paths: + paths.append(candidate) + return paths + + +def _load_dotenv() -> list[str]: + loaded: list[str] = [] + for env_path in _dotenv_paths(): + if not env_path.is_file(): + continue + for line in env_path.read_text(encoding="utf-8").splitlines(): + item = line.strip() + if not item or item.startswith("#"): + continue + if item.startswith("export "): + item = item[7:].strip() + if "=" not in item: + continue + name, value = item.split("=", 1) + name = name.strip() + value = value.strip().strip('"').strip("'") + if name and name not in os.environ: + os.environ[name] = value + if str(env_path) not in loaded: + loaded.append(str(env_path)) + return loaded + + +def _post_json(url: str, headers: dict[str, str], payload: dict[str, Any], *, timeout: int = 180) -> dict[str, Any]: + body = json.dumps(payload).encode("utf-8") + request = Request(url, data=body, headers={**headers, "Content-Type": "application/json"}, method="POST") + try: + with urlopen(request, timeout=timeout) as response: + return json.loads(response.read().decode("utf-8", errors="replace")) + except HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc + + +def _download_bytes(url: str, *, timeout: int = 120) -> bytes: + request = Request(url, headers={"User-Agent": "pptify-plugin/0.1"}) + with urlopen(request, timeout=timeout) as response: + return response.read() + + +def _extract_image_bytes(response: dict[str, Any]) -> bytes: + data = response.get("data") + if not isinstance(data, list) or not data: + raise RuntimeError(f"Image response did not include data: {response}") + first = data[0] + if not isinstance(first, dict): + raise RuntimeError(f"Unexpected image response item: {first}") + if isinstance(first.get("b64_json"), str): + return base64.b64decode(first["b64_json"]) + if isinstance(first.get("url"), str): + return _download_bytes(first["url"]) + raise RuntimeError(f"Image response did not include b64_json or url: {first}") + + +def build_infographic_prompt(content: str, *, style: str = "", audience: str = "") -> str: + parts = [ + "Create a clean, presentation-ready infographic image.", + "Use clear hierarchy, concise labels, business-safe visuals, and enough whitespace for slide use.", + ] + if style.strip(): + parts.append(f"Style preferences: {style.strip()}") + if audience.strip(): + parts.append(f"Audience: {audience.strip()}") + parts.append("Content to visualize:") + parts.append(content.strip()) + return "\n".join(parts) + + +def generate_with_openai(prompt: str, output_path: Path, *, model: str | None, size: str) -> dict[str, Any]: + api_key = os.environ.get("OPENAI_API_KEY", "").strip() + if not api_key: + return {"ok": False, "error": f"OPENAI_API_KEY is required for the OpenAI image provider. {_env_guidance()}", "code": "missing_credentials"} + + selected_model = model or os.environ.get("OPENAI_IMAGE_MODEL", "gpt-image-1") + payload: dict[str, Any] = {"model": selected_model, "prompt": prompt, "size": size, "n": 1} + if not selected_model.startswith("gpt-image-1"): + payload["response_format"] = "b64_json" + response = _post_json( + "https://api.openai.com/v1/images/generations", + {"Authorization": f"Bearer {api_key}"}, + payload, + ) + image_bytes = _extract_image_bytes(response) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_bytes(image_bytes) + return {"ok": True, "provider": "openai", "model": selected_model, "path": str(output_path), "size": size} + + +def generate_with_azure_openai(prompt: str, output_path: Path, *, model: str | None, size: str) -> dict[str, Any]: + endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT", "").strip() + api_key = os.environ.get("AZURE_OPENAI_API_KEY", "").strip() or os.environ.get("AZURE_AI_API_KEY", "").strip() + deployment = model or os.environ.get("AZURE_OPENAI_IMAGE_DEPLOYMENT", "").strip() or os.environ.get("MODEL_NAME", "").strip() + api_version = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-02-01").strip() + timeout_seconds = _env_int("AZURE_OPENAI_TIMEOUT", 300) + if not endpoint or not deployment: + return { + "ok": False, + "error": f"AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_IMAGE_DEPLOYMENT or MODEL_NAME are required. Set AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY or sign in with Azure CLI for Entra auth. {_env_guidance()}", + "code": "missing_credentials", + } + + endpoint = endpoint.rstrip("/") + if _is_openai_v1_endpoint(endpoint): + url = f"{endpoint}/images/generations" + payload = {"model": deployment, "prompt": prompt, "size": size, "n": 1} + provider = "azure-openai-v1" + else: + url = urljoin(f"{endpoint}/", f"openai/deployments/{deployment}/images/generations?api-version={api_version}") + payload = {"prompt": prompt, "size": size, "n": 1} + provider = "azure-openai" + response = _post_json(url, _azure_auth_headers(api_key), payload, timeout=timeout_seconds) + image_bytes = _extract_image_bytes(response) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_bytes(image_bytes) + return {"ok": True, "provider": provider, "model": deployment, "path": str(output_path), "size": size} + + +def _is_openai_v1_endpoint(endpoint: str) -> bool: + return endpoint.rstrip("/").lower().endswith("/openai/v1") + + +def _env_int(name: str, default: int) -> int: + try: + return int(os.environ.get(name, str(default))) + except ValueError: + return default + + +def _azure_auth_headers(api_key: str) -> dict[str, str]: + if api_key: + return {"api-key": api_key} + return {"Authorization": f"Bearer {_azure_access_token()}"} + + +def _azure_access_token() -> str: + az_command = _azure_cli_command() + if az_command is None: + raise RuntimeError("AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY is required when Azure CLI is not available.") + try: + completed = subprocess.run( + [ + az_command, + "account", + "get-access-token", + "--resource", + "https://cognitiveservices.azure.com", + "--query", + "accessToken", + "-o", + "tsv", + ], + capture_output=True, + text=True, + encoding="utf-8", + timeout=30, + check=False, + ) + except FileNotFoundError as exc: + raise RuntimeError("AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY is required when Azure CLI is not available.") from exc + token = completed.stdout.strip() + if completed.returncode != 0 or not token: + raise RuntimeError("Could not acquire an Azure CLI token. Run `az login` or set AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY.") + return token + + +def _azure_cli_command() -> str | None: + candidates = [ + os.environ.get("AZURE_CLI_PATH", "").strip(), + shutil.which("az"), + shutil.which("az.cmd"), + r"C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd", + ] + for candidate in candidates: + if not candidate: + continue + candidate_path = Path(candidate) + if candidate_path.is_file(): + return str(candidate_path) + resolved = shutil.which(candidate) + if resolved: + return resolved + return None + + +def generate_infographic( + content: str, + output_path: str | Path, + *, + style: str = "", + audience: str = "", + provider: str = "auto", + model: str | None = None, + size: str = "1024x1024", +) -> dict[str, Any]: + path = Path(output_path).expanduser() + prompt = build_infographic_prompt(content, style=style, audience=audience) + selected_provider = provider + if provider == "auto": + env_provider = os.environ.get("PPTIFY_IMAGE_PROVIDER", "").strip() + if env_provider in {"openai", "azure-openai"}: + selected_provider = env_provider + elif os.environ.get("OPENAI_API_KEY"): + selected_provider = "openai" + elif os.environ.get("AZURE_OPENAI_ENDPOINT") and (model or os.environ.get("AZURE_OPENAI_IMAGE_DEPLOYMENT") or os.environ.get("MODEL_NAME")): + selected_provider = "azure-openai" + else: + return { + "ok": False, + "error": f"No image provider is configured. {_env_guidance()} No built-in local fallback is available.", + "code": "missing_provider_config", + } + + try: + if selected_provider == "openai": + return generate_with_openai(prompt, path, model=model, size=size) + if selected_provider == "azure-openai": + return generate_with_azure_openai(prompt, path, model=model, size=size) + return {"ok": False, "error": f"Unknown provider: {provider}", "code": "unknown_provider"} + except Exception as exc: + return {"ok": False, "error": str(exc), "provider": selected_provider, "code": "generation_failed"} + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Generate an infographic image from a text prompt.") + prompt_group = parser.add_mutually_exclusive_group() + prompt_group.add_argument("--prompt", help="Prompt or source content to visualize.") + prompt_group.add_argument("--prompt-path", help="Path to a UTF-8 text prompt file.") + parser.add_argument("--output-path", required=True, help="Where to write the generated image.") + parser.add_argument("--style", default="", help="Optional visual style preferences.") + parser.add_argument("--audience", default="", help="Optional target audience.") + parser.add_argument("--provider", default="auto", choices=["auto", "openai", "azure-openai"], help="Image provider.") + parser.add_argument("--model", help="Provider-specific image model or deployment.") + parser.add_argument("--size", default="1024x1024", help="Provider image size, for example 1024x1024.") + parser.add_argument("--azure-endpoint", help="Azure OpenAI / Azure AI Foundry endpoint, for example https://.services.ai.azure.com/openai/v1.") + parser.add_argument("--azure-api-version", help="Azure OpenAI API version for legacy deployment endpoints. Defaults to AZURE_OPENAI_API_VERSION or 2024-02-01.") + parser.add_argument("--timeout", type=int, help="Provider request timeout in seconds. Defaults to AZURE_OPENAI_TIMEOUT or 300 for Azure.") + parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") + return parser + + +def _apply_runtime_settings(args: argparse.Namespace) -> None: + if args.azure_endpoint: + os.environ["AZURE_OPENAI_ENDPOINT"] = args.azure_endpoint + if args.azure_api_version: + os.environ["AZURE_OPENAI_API_VERSION"] = args.azure_api_version + if args.timeout is not None: + os.environ["AZURE_OPENAI_TIMEOUT"] = str(args.timeout) + + +def _read_prompt(args: argparse.Namespace) -> str: + if args.prompt_path: + return Path(args.prompt_path).read_text(encoding="utf-8") + if args.prompt: + return args.prompt + if not sys.stdin.isatty(): + return sys.stdin.read() + raise ValueError("Provide --prompt, --prompt-path, or stdin content.") + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + _load_dotenv() + args = build_parser().parse_args() + _apply_runtime_settings(args) + try: + content = _read_prompt(args) + except Exception as exc: + payload = {"ok": False, "error": str(exc), "code": "missing_prompt"} + print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) + return 1 + + payload = generate_infographic( + content, + args.output_path, + style=args.style, + audience=args.audience, + provider=args.provider, + model=args.model, + size=args.size, + ) + print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) + return 0 if payload.get("ok") else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/images/web_image_search.py b/plugins/pptify/.agent/pptify-plugin/images/web_image_search.py new file mode 100644 index 000000000..f9b415b03 --- /dev/null +++ b/plugins/pptify/.agent/pptify-plugin/images/web_image_search.py @@ -0,0 +1,286 @@ +from __future__ import annotations + +import argparse +import html +import json +import re +import sys +from urllib.error import HTTPError, URLError +from urllib.parse import parse_qs, urlencode, urlsplit +from urllib.request import Request, urlopen + + +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/134.0.0.0 Safari/537.36" +) + + +def browser_headers(referer: str | None = None) -> dict[str, str]: + headers = { + "Accept-Language": "en-US,en;q=0.9", + "User-Agent": USER_AGENT, + } + if referer: + headers["Referer"] = referer + return headers + + +def is_http_url(value: str) -> bool: + try: + parsed = urlsplit(value) + return parsed.scheme in {"http", "https"} and bool(parsed.netloc) + except Exception: + return False + + +def candidate_key(candidate: dict[str, str | None]) -> str | None: + return candidate.get("imageUrl") or candidate.get("sourcePageUrl") or candidate.get("thumbnailUrl") + + +def append_candidate( + candidates: list[dict[str, str | None]], + seen_keys: set[str], + candidate: dict[str, str | None], + max_num: int, +) -> None: + if len(candidates) >= max_num: + return + key = candidate_key(candidate) + if not key or key in seen_keys: + return + seen_keys.add(key) + candidates.append(candidate) + + +def _get_text(url: str, *, params: dict[str, str] | None = None, referer: str | None = None, timeout: int = 15) -> str: + full_url = f"{url}?{urlencode(params)}" if params else url + request = Request(full_url, headers=browser_headers(referer)) + with urlopen(request, timeout=timeout) as response: + content_type = response.headers.get_content_charset() or "utf-8" + return response.read().decode(content_type, errors="replace") + + +def _direct_candidate(url: str, query: str) -> dict[str, str | None]: + parsed = urlsplit(url) + title = parsed.path.split("/")[-1] or parsed.netloc or "Direct image" + return { + "provider": "direct", + "imageUrl": url, + "thumbnailUrl": url, + "sourcePageUrl": url, + "title": title, + "attribution": parsed.netloc or None, + "searchQuery": query, + } + + +def build_google_candidates(query: str, max_num: int) -> list[dict[str, str | None]]: + try: + from bs4 import BeautifulSoup + from icrawler.builtin.google import GoogleFeeder, GoogleParser + from icrawler.utils import ProxyPool, Session, Signal + except Exception: + return [] + + session = Session(ProxyPool()) + signal = Signal() + signal.set(feeder_exited=False, parser_exited=False, reach_max_num=False) + feeder = GoogleFeeder(1, signal, session) + parser = GoogleParser(1, signal, session) + feeder.feed(keyword=query, offset=0, max_num=max_num) + + seen_pages: set[str] = set() + seen_images: set[str] = set() + seen_keys: set[str] = set() + candidates: list[dict[str, str | None]] = [] + + while not feeder.out_queue.empty() and len(candidates) < max_num: + search_url = feeder.out_queue.get() + base_url = "{0.scheme}://{0.netloc}".format(urlsplit(search_url)) + response = session.get(search_url, timeout=10, headers=browser_headers(base_url)) + if not response.text: + continue + + soup = BeautifulSoup(response.text, "html.parser") + thumbnails = [] + for image in soup.find_all("img"): + src = image.get("src") + if isinstance(src, str) and src.startswith("https://encrypted-tbn0.gstatic.com/images"): + thumbnails.append(src) + + source_pages = [] + for anchor in soup.find_all("a"): + href = anchor.get("href") + if not isinstance(href, str) or not href.startswith("/url?"): + continue + target = parse_qs(urlsplit(href).query).get("q", [None])[0] + if not target or not target.startswith(("http://", "https://")) or target in seen_pages: + continue + seen_pages.add(target) + source_pages.append(target) + + pair_count = min(len(thumbnails), len(source_pages), max_num - len(candidates)) + for index in range(pair_count): + page_url = source_pages[index] + thumb_url = thumbnails[index] + parsed = urlsplit(page_url) + append_candidate( + candidates, + seen_keys, + { + "provider": "google", + "imageUrl": None, + "thumbnailUrl": thumb_url, + "sourcePageUrl": page_url, + "title": parsed.path.split("/")[-1] or parsed.netloc or "Google image", + "attribution": parsed.netloc or None, + }, + max_num, + ) + + tasks = parser.parse(response) or [] + for task in tasks: + image_url = task.get("file_url") + if not image_url or image_url in seen_images: + continue + seen_images.add(image_url) + parsed = urlsplit(image_url) + append_candidate( + candidates, + seen_keys, + { + "provider": "google", + "imageUrl": image_url, + "thumbnailUrl": image_url, + "sourcePageUrl": None, + "title": parsed.path.split("/")[-1] or parsed.netloc or "Google image", + "attribution": parsed.netloc or None, + }, + max_num, + ) + if len(candidates) >= max_num: + break + + return candidates + + +def _extract_bing_metadata(html_text: str) -> list[str]: + try: + from bs4 import BeautifulSoup + except Exception: + BeautifulSoup = None + + if BeautifulSoup is not None: + soup = BeautifulSoup(html_text, "html.parser") + return [str(anchor.get("m")) for anchor in soup.select("a.iusc") if anchor.get("m")] + + metadata_values: list[str] = [] + for match in re.finditer(r"]*\bclass=[\"'][^\"']*\biusc\b[^\"']*[\"'][^>]*>", html_text, re.IGNORECASE): + tag = match.group(0) + attr_match = re.search(r"\bm=([\"'])(.*?)\1", tag, re.IGNORECASE | re.DOTALL) + if attr_match: + metadata_values.append(html.unescape(attr_match.group(2))) + return metadata_values + + +def build_bing_candidates(query: str, max_num: int) -> list[dict[str, str | None]]: + html_text = _get_text( + "https://www.bing.com/images/search", + params={"q": query, "form": "HDRSC3"}, + referer="https://www.bing.com/", + ) + seen_keys: set[str] = set() + candidates: list[dict[str, str | None]] = [] + + for metadata_text in _extract_bing_metadata(html_text): + try: + metadata = json.loads(html.unescape(metadata_text)) + except json.JSONDecodeError: + continue + + image_url = metadata.get("murl") + thumb_url = metadata.get("turl") or image_url + page_url = metadata.get("purl") + title = metadata.get("t") or metadata.get("desc") or "Bing image" + attribution = urlsplit(page_url).netloc if isinstance(page_url, str) and page_url else None + append_candidate( + candidates, + seen_keys, + { + "provider": "bing", + "imageUrl": image_url, + "thumbnailUrl": thumb_url, + "sourcePageUrl": page_url, + "title": title, + "attribution": attribution, + }, + max_num, + ) + if len(candidates) >= max_num: + break + return candidates + + +def build_candidates(query: str, max_num: int) -> tuple[list[dict[str, str | None]], list[str]]: + if is_http_url(query): + return [_direct_candidate(query, query)], [] + + errors: list[str] = [] + candidates: list[dict[str, str | None]] = [] + try: + candidates = build_google_candidates(query, max_num) + except Exception as exc: + errors.append(f"google: {exc}") + + if len(candidates) < max_num: + try: + fallback_candidates = build_bing_candidates(query, max_num) + seen_keys = {key for candidate in candidates if (key := candidate_key(candidate))} + for candidate in fallback_candidates: + append_candidate(candidates, seen_keys, candidate, max_num) + if len(candidates) >= max_num: + break + except (HTTPError, URLError, TimeoutError, OSError) as exc: + errors.append(f"bing: {exc}") + + for candidate in candidates: + candidate.setdefault("searchQuery", query) + return candidates, errors + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Search web image candidates for one or more queries.") + parser.add_argument("--query", action="append", required=True, help="Search query. Can be repeated.") + parser.add_argument("--max-num", type=int, default=12, help="Maximum candidates per query.") + parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") + return parser + + +def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + args = build_parser().parse_args() + queries = [query.strip() for query in args.query if query.strip()] + max_num = max(1, min(args.max_num, 32)) + payload_candidates: list[dict[str, str | None]] = [] + errors: list[str] = [] + + for query in queries: + candidates, query_errors = build_candidates(query, max_num) + payload_candidates.extend(candidates) + errors.extend(query_errors) + + payload = { + "ok": not errors or bool(payload_candidates), + "query": "\n".join(queries), + "candidates": payload_candidates, + "errors": errors, + } + print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) + return 0 if payload["ok"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-policy.md b/plugins/pptify/.agent/pptify-policy.md new file mode 100644 index 000000000..16e71c6e8 --- /dev/null +++ b/plugins/pptify/.agent/pptify-policy.md @@ -0,0 +1,60 @@ + +# pptify Developer-Protection Policy + +Installed by pptify-cli. Do not edit manually; run `pptify install` to refresh. + +## Secret and Credential Safety + +- Never embed API keys, tokens, connection strings, or passphrases in deck + specs, prompt assets, audit files, attempt manifests, or version-controlled + files. +- Never collect API keys or tokens through chat or the VS Code prompt input + dialog. Require the user to type secrets directly into a terminal or a + managed secret environment (e.g. `az login`, `$env:OPENAI_API_KEY = ...`). +- If an image-generation helper fails due to missing credentials, persist a + failure manifest with `provider`, `status: missing_credentials`, and `error`, + and do not describe placeholder artwork as model-generated. + +## Coordinate Contract + +- All generated slides must use explicit `layout_tree` with final bboxes, font + sizes, text colors, line endpoints/styles, shape names, and z-order. +- Prohibited obsolete shorthand keys: `pattern`, `layout_pattern`, + `composition.pattern`, `layout`, `sections`, `bullets`, `objects`, `theme`. +- Every text-bearing object must carry explicit `style.font_size` and + `style.color`. +- Every line object must carry `content.x1`, `content.y1`, `content.x2`, + `content.y2` and explicit `style.line`/`style.line_width`. +- Every shape object must carry `content.shape`, `style.fill`, and + `style.line`. + +## Quality Gates + +- Production-ready decks must have zero content collisions (verified by + `pptify-plugin/audit/audit.py`). +- Production-ready decks must have zero text overflows. +- No `classification: "content"` object may use `style.font_size` below 9 pt. + +## Asset and Design Boundaries + +- Do not copy external fonts, icons, images, or binary assets without explicit + license metadata and source attribution in `pptify-design/sources.json` or + `pptify-design/third-party-notices.md`. +- For every new generated deck, load a profile from `pptify-design/sources.json` + unless a user-provided brand guide or reference PPTX is the primary style + source. Do not invent a new design template. +- Production-ready decks must record selected profile IDs, source URLs, and + style lock details in `summary.design_context`. +- Plain white, Calibri-only, bullet-heavy, default-theme PPTX output is a design + failure even when collision and overflow audits pass. +- Keep source attribution and license metadata attached to any copied or + adapted design context. + +## Rendering Boundary + +- The importable `pptify/` core renderer package and `python -m pptify` CLI + are not present in this workspace snapshot. Use plugin scripts for + extraction, conversion, design context, image helpers, and audit. +- Do not use obsolete renderer flags (`--provider copilot`, `--prompt`, + `--prompt-file`, `--model`, `--spec-out`) unless a restored core CLI + explicitly supports them. diff --git a/plugins/pptify/.agent/skills/pptify-context-prep/SKILL.md b/plugins/pptify/.agent/skills/pptify-context-prep/SKILL.md new file mode 100644 index 000000000..9ede9f2a8 --- /dev/null +++ b/plugins/pptify/.agent/skills/pptify-context-prep/SKILL.md @@ -0,0 +1,72 @@ +--- +name: pptify-context-prep +description: "Prepare source material and design context before authoring a pptify deck spec. Use when converting documents, building RAPTOR summaries, analyzing reference PPTX decks, or selecting and loading pptify-design profiles." +--- + +# PPTify Context Prep + +Use this skill before writing a deck spec. It covers two parallel preparation tracks: **source context** (documents, research, reference PPTX) and **design context** (predefined style profiles from `pptify-design`). + +## Source Documents + +- Convert long source documents to markdown before planning slides: `uv run python pptify-plugin/documents/document_to_markdown.py --source source.pdf --output-path source.md`. +- Build a structured summary tree when a source is long or multi-topic: `uv run python pptify-plugin/documents/document_to_raptor_tree.py --markdown-path source.md --output-path source-summary.json --title "Source" --pretty`. +- For URL-based, topic-plus-research, source-backed, or multi-source decks, combine converted/downloaded source markdown into a corpus and run the RAPTOR summary before slide planning, even when individual sources are short. +- Record the corpus path, summary path, source count, and source URLs in `summary.source_enrichment` so enrichment evidence survives review. +- Use the summary tree to identify audience, thesis, slide sequence, evidence, risks, and decision points. +- Do not paste entire long documents into the deck spec; summarize into concise slide messages and cite sources in footers when needed. + +## Reference PPTX + +- Use the importable helpers in `pptify-plugin/extraction` or package inspection to inspect production complexity, slide text, style, brand, template, and layout-rhythm facts. The `python -m pptify --analyze-pptx` command is unavailable unless the core renderer package is restored. +- Use the extracted facts as agent context when the new deck should follow a source deck's language, slide count, topic sequence, executive tone, colors, fonts, template conventions, and layout rhythm. +- When authoring the new spec, translate `brands.primary_color`, `brands.accent_colors`, `brands.fonts`, `template.slide_size`, `template.layout_usage`, and `layout.slides[*].dominant_flow` into explicit `layout_tree` primitives, colors, typography, spacing, and coordinates. +- Use extraction helpers when the goal is reconstructing or preserving an existing production deck rather than authoring a new editable deck. +- For new editable decks, treat reference layout rhythm as prompt context; generated coordinates must be authored directly by the agent in `layout_tree`. +- Never copy or mutate a referenced PPTX as the generation strategy. Use analysis as context and build a new PPTX artifact. + +## Design Profile Selection + +Use profiles from `pptify-design/sources.json`; do not invent a new design template when the user asks for predefined templates. + +- Use `fluent-ui-design-tokens` as the default for new decks, including Microsoft, M365, Teams, Power Platform, enterprise-aligned, general modern, stylish, product, app, pitch, or unspecified visual style requests. +- Use `primer-primitives` for GitHub-style product, developer, or token-driven engineering decks. +- Use `corazzon-pptx-design-styles` when a broader modern style catalog or multiple visual direction options are explicitly useful. Pick one style from the catalog and lock its palette, typography, spacing, and signature element before layout planning. +- Use `likaku-mck-ppt-design-skill` for consulting, strategy, governance, or operations decks that need action-title discipline and structured native PPTX layouts. +- Use `awesome-copilot-design-agents` when the agent prompt itself needs design review, UX discovery, visual hierarchy, or accessibility framing. +- Keep source attribution and license metadata attached to the context used. +- If no catalog profile fits, use reference PPTX analysis, search for another public source, or ask the user for a source template. +- Record selected profile IDs, source URLs, and style lock details in `summary.design_context` before building the PPTX. + +Load profiles: + +```powershell +uv run python pptify-plugin/design/design_context_catalog.py --list --pretty +uv run python pptify-plugin/design/design_context_catalog.py --profile fluent-ui-design-tokens --include-context --pretty +uv run python pptify-plugin/design/design_context_catalog.py --profile primer-primitives --include-context --pretty +uv run python pptify-plugin/design/design_context_catalog.py --profile corazzon-pptx-design-styles --include-context --pretty +``` + +## Applying Context to Spec Authoring + +1. Put the selected profile payload into the agent context before writing `deck-spec.json`. +2. Translate source signals into explicit `layout_tree` objects, colors, fills, lines, typography, spacing, bboxes, and z-order. +3. Keep meaningful slide content as `classification: "content"` objects and decorative/background elements as `classification: "layout_design"` objects. +4. Use source CSS or reference deck rhythm only as design evidence; final coordinates must be authored directly in inches. +5. Add at least one style-derived visible design element to every normal content slide: accent band, rule, card shell, grid cell, diagram primitive, shape motif, image treatment, or pattern. A plain title-plus-bullets slide fails the design gate. +6. Do not treat `pptify-design` profiles as content source material; they are design context only. + +## Source-to-Deck Planning + +- Convert source material into one message per slide before authoring visual structure. +- Treat charts and dashboard-style slides as source-evidence-driven exhibits; do not create generic metric or dashboard slides when the source corpus does not provide relevant data. +- Preserve important terminology, product names, metrics, dates, and user-provided wording. +- Reduce dense narrative into executive slide titles plus short sections. +- Track open assumptions in speaker notes or audit-facing summary fields instead of overcrowding slides. + +## Restrictions + +- Do not copy external fonts, icon packs, photos, or binary assets unless their license and source are explicitly added. +- Do not claim the output is a Primer, Fluent UI, or Awesome Copilot artifact; these are context sources for a new `pptify` deck. +- Do not let source CSS override pptify quality gates: built decks still need zero content collisions and zero text overflows. +- Do not accept default PowerPoint theme colors, Calibri-only text boxes, plain white backgrounds, or placeholder-style bullet layouts as a finished design. diff --git a/plugins/pptify/.agent/skills/pptify-deck-generation/SKILL.md b/plugins/pptify/.agent/skills/pptify-deck-generation/SKILL.md new file mode 100644 index 000000000..c80072271 --- /dev/null +++ b/plugins/pptify/.agent/skills/pptify-deck-generation/SKILL.md @@ -0,0 +1,161 @@ +--- +name: pptify-deck-generation +description: "Generate PPTX decks end to end with pptify. Use when creating PowerPoint slides from prompts, source material, reference PPTX analysis, coordinate-explicit layout trees, or pptify JSON specs." +--- + +# PPTify Deck Generation + +Use this skill when a Copilot or coding agent needs to generate PPTX slides with `pptify`. + +## Intake + +1. Before creating workflow artifacts, collect any missing required inputs with the VS Code prompt input dialog (`vscode_askQuestions` or equivalent). Batch concise questions, offer sensible defaults for optional fields, and continue after the user answers. +2. Identify the audience, decision, business framework, core narrative, required language, target slide count, source material, reference PPTX, branding constraints, output artifact paths, and delivery deadline. +3. If the user gives only a topic, create a reasonable executive narrative and mark assumptions in the generated spec summary. When the request asks for web research, source-backed content, or data enrichment, persist source material and run the source pipeline before authoring slides. +4. If the user provides source files, URLs, research material, or a reference deck, prepare them before generating the slide spec. +5. If the user requests text-to-image or generated images with OpenAI, Azure OpenAI, or Azure AI Foundry, create `.env` from `.env.template` when needed and have the user fill provider settings or secrets directly in `.env`. Do not ask for API keys or tokens in chat or in the dialog. +6. Do not claim an infographic is model-generated until provider, model or deployment, auth mode, prompt, output path, and attempt status are known. The image helper has no local fallback provider. + +## Required Input Dialog + +- Use the prompt input dialog for missing required workflow values such as audience, slide count, source material, design/reference context, output filenames, and artifact destinations. +- Use `.env` for missing text-to-image provider values such as provider, model or deployment, endpoint, auth method, timeout, and required API keys. Collect only non-secret prompt, image size, and output path values through the dialog when needed. +- Treat API keys, tokens, connection strings, and passphrases as secrets. Do not collect them through chat or the dialog; the user must enter them directly in `.env` or authenticate with a managed tool such as `az login`. + +## Prepare Sources and References + +Follow `pptify-context-prep` to convert source documents, build RAPTOR summaries, analyze reference PPTX files, and select and load a design profile. Record results in `summary.source_enrichment` and `summary.design_context` before authoring `deck-spec.json`. + +## Confirm Business Framework + +The business framework is defined by the user, not by the assistant. If the user has already specified a framework, use it directly. If no framework has been specified, present the available options and ask which one to use before planning the deck. Include `custom` when the user wants to provide their own structure, naming convention, or slide sequence. Do not auto-select a framework on the user's behalf. + +| Framework | Best for | +|---|---| +| `mckinsey` | Executive proposals, consulting deliverables, strategic recommendations | +| `scqa` | Problem-solving presentations, situation analysis, incident reports | +| `pyramid` | Complex arguments requiring strong logical structure | +| `mece` | Issue decomposition, audits, multi-workstream analysis | +| `action-title` | Executive communications where every slide must drive action | +| `assertion-evidence` | Technical or academic presentations, research findings | +| `exec-summary-first` | C-suite briefings, board decks, press releases | +| `custom` | User-defined structures, organization-specific playbooks, hybrid narrative patterns | + +## Framework Story Templates + +Use the selected framework as the starting narrative spine, then adapt slide count and evidence density to the user's source material and requested deliverable. + +### McKinsey Structure (`mckinsey`) + +1. Title (`title`) - Conclusion in one sentence. +2. Executive Summary (`agenda`) - Three bullets: situation, insight, recommendation. +3. Situation (`bullets`) - Context that audience already knows. +4. Complication (`bullets`) - What changed or what problem emerged. +5. Key Question (`section`) - The central question this deck answers. +6. Recommendation (`cards`) - The answer, clearly stated. +7. Evidence 1 (`stats` or `cards`) - Data supporting the recommendation. +8. Evidence 2 (`comparison` or `timeline`) - Further evidence. +9. Evidence 3 (`bullets` or `diagram`) - Additional support. +10. Options Considered (`comparison`) - Why this option versus alternatives. +11. Implementation Roadmap (`timeline`) - Next steps with owners. +12. Appendix (`section`) - Label for backup slides. + +### SCQA Structure (`scqa`) + +1. Title (`title`). +2. Situation (`bullets`) - Agreed context. +3. Complication (`stats`) - What disrupted the situation. +4. Question (`section`) - What question this raises. +5. Answer / Recommendation (`cards`) - Direct answer to the question. +6. Supporting Evidence (`stats` or `comparison`). +7. Implementation Plan (`timeline`). +8. Summary (`summary`). + +### Pyramid Structure (`pyramid`) + +1. Title (`title`). +2. Main Answer (`agenda`) - Top of pyramid: the governing thought. +3. Argument 1 (`cards` or `bullets`) - First key line of reasoning. +4. Argument 2 (`cards` or `bullets`) - Second key line of reasoning. +5. Argument 3 (`cards` or `bullets`) - Third key line of reasoning. +6. Evidence (`stats`) - Data underpinning the arguments. +7. Summary (`summary`) - Pyramid restated. + +### MECE Structure (`mece`) + +1. Title (`title`). +2. Problem Decomposition (`diagram`) - The MECE issue tree. +3. Workstream 1 (`bullets` or `cards`) - Sub-issue and findings. +4. Workstream 2 (`bullets` or `cards`). +5. Workstream 3 (`bullets` or `cards`). +6. Synthesis (`summary`) - Integrated conclusion. + +### Action-Title Structure (`action-title`) + +Rule: every slide title must be an action statement or concluded insight. + +1. Title (`title`) - Action-oriented title. +2. Summary of Actions (`agenda`). +3. Content slides (`any supported slide form`) - Title follows the pattern "We must X" or "X increased by Y%." +4. Next Steps (`timeline`) - Owners and dates. + +### Assertion-Evidence Structure (`assertion-evidence`) + +Rule: each slide has an assertion title in one sentence and an evidence body with visual or data support. + +1. Title (`title`). +2. Overview Assertion (`bullets`). +3. Assertion slides (`stats`, `cards`, or `comparison`). +4. Conclusion (`summary`). + +### Exec-Summary-First Structure (`exec-summary-first`) + +1. Title (`title`). +2. Executive Summary (`agenda`) - Full answer on slide 2. +3. Supporting Detail (`any supported slide form`) - For readers who want depth. +4. Appendix Section (`section`). + +### Custom Structure (`custom`) + +Use `custom` only when the user supplies or explicitly requests a user-defined framework. Before planning slides, collect the framework name, objective, required slide sequence, title rules, layout preferences, evidence expectations, and any mandatory sections. If the custom framework is incomplete, ask for the missing structure rather than filling it in silently. + +Record the resolved custom framework in `summary.business_framework`, including its name, source, slide sequence, title rules, and any assumptions approved by the user. + +## Storytelling Principles + +- Apply the Pyramid Principle: put the conclusion first, make each slide title state the slide's conclusion or assertion, and avoid questions or vague labels. +- Make every `keyMessage` answer "So what?" for the audience. +- Keep topics MECE: mutually exclusive and collectively exhaustive. +- Write specific slide titles, such as "Azure AI cuts development costs by 40%" or "3 implementation patterns enable rapid onboarding," instead of generic labels like "About Azure AI" or "Implementation Patterns Overview." +- Include concrete data, numbers, dates, owners, sources, or quantified directional signals in bullets when the source material supports them. +- Keep speaker notes useful: two to three sentences, never empty and never just a dash. +- Avoid generic statements; every bullet should be specific, defensible, and tied to the selected framework's role in the story. + +## Plan the Deck + +1. Produce one clear message per slide before choosing visuals. +2. Map the selected business framework to the deck outline and document the selected framework in `summary.business_framework`. +3. Choose a slide form for each message: title, agenda, comparison, process, metrics, roadmap, risk, architecture, evidence, decision, infographic, dashboard-style overview, or appendix. +4. Use charts and dashboard-style slides only when source evidence contains relevant quantitative or structured data. Represent them as explicit tables, labels, shapes, lines, or image-backed exhibits. +5. Keep each slide to three to five major content groups. +6. Preserve user-provided terminology, names, metrics, dates, and executive tone. +7. Decide the exact coordinates, dimensions, z-order, colors, fonts, and font sizes before building. Current plugin scripts will not generate layout or resize text for you. +8. Record the selected profile ID, source URL, style lock, palette, typography, spacing rhythm, and signature elements in `summary.design_context` and translate source signals into explicit primitives in `layout_tree`. +9. Every normal content slide should include at least one style-derived design element such as an accent band, card shell, grid, divider, shape motif, image treatment, or pattern. Plain white title-plus-bullet slides are not production-ready. + +## Build and Validate + +1. Current workspace reality check: no importable `pptify/` package or `python -m pptify` CLI is present in this snapshot. Restore the core renderer package before relying on renderer-specific commands. +2. Every generated slide must include `layout_tree`. Follow `pptify-slide-spec` for the full coordinate contract. +3. If the core renderer is restored, render the authored spec with `uv run python -m pptify deck-spec.json --out deck.pptx --audit deck-audit.json`. Otherwise create the PPTX through direct PowerPoint generation and keep plugin evidence/audits alongside it. Direct `python-pptx` generation must still implement the locked design context; default placeholders and bullet-only slides are failures. +4. For reference-guided generation, include analysis/source summaries and the extracted `styles`, `brands`, `template`, and `layout` context before writing `deck-spec.json`. +5. Include the selected `pptify-design` context before writing `deck-spec.json` for every new deck unless a user-provided brand guide or reference PPTX is the primary style source. +6. Inspect the audit for content collisions, text overflows, and warnings. +7. Verify workflow gates: source-backed decks include source corpus and RAPTOR summary metadata; requested generated images include a provider attempt manifest; successful generated raster infographics include a final hidden SVG appendix slide; generated decks include `summary.design_context` and style-derived visual elements. +8. Rebuild after each repair until generated slides have zero collisions, zero overflows, and no default-theme design failures, or clearly report the residual issue. + +## Response Contract + +1. When asked to author the deck spec, write strict JSON with no markdown fences unless the user explicitly asks for prose. +2. When required workflow or artifact inputs are missing, prompt for them with the input dialog before authoring or building. +3. When acting as a coding agent in the workspace, create or update the spec or generation script, produce the PPTX with the available PowerPoint path, validate the audit and PPTX package, and report the generated artifact paths. diff --git a/plugins/pptify/.agent/skills/pptify-quality-gates/SKILL.md b/plugins/pptify/.agent/skills/pptify-quality-gates/SKILL.md new file mode 100644 index 000000000..2c4498ca8 --- /dev/null +++ b/plugins/pptify/.agent/skills/pptify-quality-gates/SKILL.md @@ -0,0 +1,47 @@ +--- +name: pptify-quality-gates +description: "Validate and repair pptify PPTX artifacts. Use when checking deck specs, PPTX packages, audits, coordinate-explicit layout trees, collisions, text overflows, warnings, visual hierarchy, asset layering, or reference deck alignment." +--- + +# PPTify Quality Gates + +Use this skill before considering a generated PPTX complete. + +## Required Artifacts + +- If required artifact paths or names are missing, collect them with the VS Code prompt input dialog (`vscode_askQuestions` or equivalent) before building, validating, or repairing. +- Keep the generated spec, PPTX, and audit together: `deck-spec.json`, `deck.pptx`, and `deck-audit.json`. +- Keep the agent-authored JSON spec or generation script on disk so it can be reviewed, repaired, and rebuilt. +- Save analysis or extraction manifests when reference PPTX context was used. +- Save the selected `pptify-design` profile IDs, source URLs, license IDs, and style lock details in `summary.design_context` for every newly generated deck unless a user-provided brand guide or reference PPTX is the primary style source. + +## Audit Checks + +- A production-ready generated deck should have zero content collisions. +- A production-ready generated deck should have zero text overflows. +- A production-ready generated deck should have zero `classification: "content"` objects with `style.font_size` below 9 pt. Run `uv run python pptify-plugin/audit/audit.py deck-spec.json --json` and check `total_small_fonts`. +- Review audit `warnings` for each slide even when collisions and overflows are zero. +- Check that slide count, language, tone, and major topic sequence match the user request or reference context. +- Check that the selected design context profile matches the user request and that source-backed context was translated into explicit primitives, colors, spacing, typography, and bboxes. +- Fail generated decks that have no `summary.design_context`, plain white backgrounds throughout, Calibri-only text, default theme colors, or placeholder-like title-plus-bullet layouts unless the user explicitly requested that style. +- Confirm every normal content slide contains at least one style-derived visual element such as an accent band, card shell, grid, divider, shape motif, image treatment, or pattern. +- When a deck includes hidden appendix slides, inspect `ppt/presentation.xml` for `p:sldId show="0"` and confirm the hidden slides are last unless the user asked otherwise. +- When a generated infographic has both raster and SVG assets, verify the visible slide uses the raster for text fidelity and the SVG appears only in the hidden appendix slide. +- For important deliverables, open the generated PPTX with `python-pptx` or inspect the zip package to confirm slide count, relationships, media, and hidden-slide metadata. + +## Repair Loop + +- If content collides, move or resize objects, reduce content density, split slides, or change the coordinate plan. +- If text overflows, shorten bullets, split sections, enlarge target bboxes, or split slides. **Lower explicit `font_size` only as a last resort, and never below 9 pt for content objects.** +- If visual hierarchy is weak, edit explicit colors, type scale, dividers, metric cards, callouts, or whitespace in the layout tree. +- If the deck looks like default `python-pptx`, load a `pptify-design` profile, add `summary.design_context`, choose a style lock, and rebuild with explicit background/accent/card/rule primitives. +- If an asset covers text, lower its `z_index`, move it to `layout_design`, resize it, or change its bbox. +- If coordinates are cramped or inconsistent, repair the agent-authored bboxes directly; current plugin scripts will not run a browser or auto-layout pass. +- Rebuild after each spec repair and inspect the new audit or package checks. + +## Verification Commands + +- Current workspace reality check: no importable `pptify/` package or `python -m pptify` CLI is present in this snapshot. Use the standalone audit plugin and package inspection unless the core renderer package is restored. +- Audit a layout-tree spec with `uv run python pptify-plugin/audit/audit.py deck-spec.json --json`. +- Run the full current test suite with `uv run python -m unittest discover -s tests -v`. +- If the core renderer package is restored, add renderer/CLI smoke tests before considering rendered deck behavior covered. diff --git a/plugins/pptify/.agent/skills/pptify-slide-spec/SKILL.md b/plugins/pptify/.agent/skills/pptify-slide-spec/SKILL.md new file mode 100644 index 000000000..eabf8c874 --- /dev/null +++ b/plugins/pptify/.agent/skills/pptify-slide-spec/SKILL.md @@ -0,0 +1,125 @@ +--- +name: pptify-slide-spec +description: "Author or repair coordinate-explicit pptify JSON deck specs. Use when writing layout_tree groups, objects, bboxes, tables, images, lines, shapes, type scale, or collision-safe content." +--- + +# PPTify Slide Spec + +Use this skill when writing or repairing a coordinate-explicit JSON deck spec. + +Author final coordinates directly in `layout_tree`; current plugin scripts will not choose layouts, measure browser boxes, or shrink text to fit. Split dense material across slides rather than relying on tiny fonts. + +## Deck Shape + +- Return a JSON object with a top-level `slides` array for generated decks. +- Keep slide IDs stable and readable, such as `s01_overview`. +- Use top-level `summary` for deck metadata that belongs in the audit but not on slides. +- When source-backed design context from `pptify-design` is used, record selected profile IDs, source URLs, and license IDs in `summary.design_context`. +- For newly generated decks, `summary.design_context` is required unless a user-provided brand guide or reference PPTX is documented as the primary style source. +- Use `render_mode: "layout"` or omit it for generated decks; OOXML mode is for extracted specs with `ooxml_elements`. +- Every generated slide must include `layout_tree`; do not rely on shorthand layout specs. + +## Slide Fields + +- Each generated slide must include `id`, `title`, and `layout_tree`. +- Use `hidden: true` only for appendix/reference slides that should remain in the PPTX package but not appear during normal presentation. +- Do not use `pattern`, `layout_pattern`, `composition.pattern`, `layout`, `sections`, `bullets`, `objects`, or `theme` as render-time shorthand. +- Do not overfill a slide: prefer three to five major content groups. +- Decide all positions, sizes, z-order, colors, font sizes, and object relationships in the JSON before rendering. +- Do not ship default `python-pptx`-looking slides: plain white background, Calibri-only text, default theme colors, and bullet-only layouts are design failures unless explicitly requested. + +## Layout Tree + +- Include `slide_size` with explicit `width` and `height` in inches. +- Include `root_group_id`. +- Include `groups`, keyed by group ID. +- Include `objects`, keyed by object ID. +- Add `notes` only when notes are useful for audit or speaker context. + +Example skeleton: + +```json +{ + "id": "s01_overview", + "title": "Overview", + "layout_tree": { + "id": "s01_overview", + "title": "Overview", + "slide_size": { "width": 13.333, "height": 7.5 }, + "root_group_id": "root", + "groups": { + "root": { + "id": "root", + "role": "slide", + "layout_mode": "absolute", + "object_ids": ["title"], + "group_ids": [], + "bbox": { "x": 0, "y": 0, "width": 13.333, "height": 7.5 } + } + }, + "objects": { + "title": { + "id": "title", + "kind": "text", + "role": "title", + "classification": "content", + "content": { "text": "Overview" }, + "style": { "font_size": 30, "bold": true, "color": "#111827" }, + "bbox": { "x": 0.75, "y": 0.55, "width": 8.5, "height": 0.65 }, + "z_index": 2 + } + }, + "notes": [] + } +} +``` + +## Groups + +- Each group must include `id`, `role`, `layout_mode`, `object_ids`, `group_ids`, and `bbox`. +- Use `layout_mode: "absolute"` for generated slides to make the coordinate contract explicit. +- Keep group IDs unique and stable so audit repairs can target them. +- Use groups for semantic organization and audit readability; coordinates are still final object coordinates. + +## Objects + +- Every object must include `id`, `kind`, `role`, `classification`, `content`, `style`, `bbox`, and `z_index`. +- Supported `kind` values: `text`, `shape`, `image`, `line`, `table`. +- Supported shape names (`content.shape`): `rect`, `round_rect`, `oval`, `triangle`, `diamond`, `hexagon`, `parallelogram`, `chevron`, `pentagon`, `trapezoid`, and arrow variants. +- Use `classification: "layout_design"` for decorative or background objects. +- Use `classification: "content"` for meaningful text, tables, lines, and media. +- Shape content must include `content.shape`; text on a shape uses `content.text`. +- Image content uses `content.path`, `content.blob_base64`, and `content.alt`. +- Table content uses `content.rows` as a list of row arrays. +- Line content must include `content.x1`, `content.y1`, `content.x2`, and `content.y2`. +- Do not use `chart` objects; render charts as explicit shapes, labels, lines, tables, or file-backed images. + +## Styling + +- Every text-bearing object and table must include `style.font_size` and `style.color`. +- Every line object must include `style.line` and `style.line_width`. +- Every shape object must include `content.shape`, `style.fill`, and `style.line`. +- Specify text color with `style.color`; do not rely on a later tool to infer contrast or default text color. +- Use `z_index` intentionally: low values for backgrounds and decorations, higher for text and foreground content. +- Every normal content slide must include at least one `layout_design` object or style-derived visual structure such as an accent band, card shell, grid, divider rule, signature shape, or image treatment. +- If a vector-traced SVG is provided only for editability, keep the readable raster image in the visible slide and put the SVG on a separate hidden final slide. + +### Type Scale + +| Role | Recommended (pt) | Minimum (pt) | +|---|---|---| +| Slide title | 24–32 | 20 | +| Section heading / H2 | 16–20 | 14 | +| Claim / callout | 13–15 | 12 | +| Body / narrative | 11–12 | 10 | +| Evidence / bullet | 10–11 | 10 | +| Label / caption | 9–10 | 9 | +| Footer / meta (Courier) | 8–9 | 8 | + +Decorative text (`classification: "layout_design"`) such as monogram numerals, rule labels, or background watermarks is exempt from the minimum floor. + +## Repair Rules + +- If content collides, edit bboxes, z-order, grouping, slide density, or split the slide. +- If text overflows, shorten copy, enlarge the bbox, or split content across slides. **Lower `font_size` only as a last resort, and never below the type scale minimum.** +- If an object is misplaced, repair the final coordinates directly; do not add layout hints expecting a later tool to resolve them. diff --git a/plugins/pptify/.agent/skills/pptify-tooling/SKILL.md b/plugins/pptify/.agent/skills/pptify-tooling/SKILL.md new file mode 100644 index 000000000..6207e82a5 --- /dev/null +++ b/plugins/pptify/.agent/skills/pptify-tooling/SKILL.md @@ -0,0 +1,38 @@ +--- +name: pptify-tooling +description: "Command reference for pptify plugin tools. Use when looking up install commands, plugin script syntax, or the workspace reality check." +--- + +# PPTify Tooling + +## Install + +```powershell +uv sync # base project +uv sync --extra plugins # add source ingestion and image helpers +``` + +## Workspace Reality Check + +No importable `pptify/` package or `python -m pptify` CLI is present in this snapshot. Use the standalone plugin scripts below, or restore the core renderer package before using documented render/analyze/extract CLI commands. + +If the core renderer is restored: + +```powershell +uv run python -m pptify deck-spec.json --out deck.pptx --audit deck-audit.json +``` + +## Plugin Scripts + +| Purpose | Command | +|---|---| +| Convert document to markdown | `uv run python pptify-plugin/documents/document_to_markdown.py --source --output-path out.md` | +| Build RAPTOR summary tree | `uv run python pptify-plugin/documents/document_to_raptor_tree.py --markdown-path source.md --output-path summary.json --title "Title" --pretty` | +| List design profiles | `uv run python pptify-plugin/design/design_context_catalog.py --list --pretty` | +| Load design profile context | `uv run python pptify-plugin/design/design_context_catalog.py --profile fluent-ui-design-tokens --include-context --pretty` | +| Search web images | `uv run python pptify-plugin/images/web_image_search.py --query "topic" --max-num 8 --pretty` | +| Search Iconify icons | `uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 --max-num 8 --pretty` | +| Raster to SVG | `uv run python pptify-plugin/images/raster_image_to_svg.py --source logo.png --output-path logo.svg --pretty` | +| Generate infographic | `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "..." --output-path out.png --pretty` | +| Audit spec | `uv run python pptify-plugin/audit/audit.py deck-spec.json --json` | +| Run tests | `uv run python -m unittest discover -s tests -v` | diff --git a/plugins/pptify/.agent/skills/pptify-visual-assets/SKILL.md b/plugins/pptify/.agent/skills/pptify-visual-assets/SKILL.md new file mode 100644 index 000000000..ac97fbe8d --- /dev/null +++ b/plugins/pptify/.agent/skills/pptify-visual-assets/SKILL.md @@ -0,0 +1,49 @@ +--- +name: pptify-visual-assets +description: "Find, generate, and place visual assets for pptify PPTX decks. Use when adding icons, images, SVGs, raster conversions, infographics, image placeholders, or asset-backed slide objects." +--- + +# PPTify Visual Assets + +Use this skill when a deck needs icons, images, diagrams, infographics, or media-backed slide objects. + +## Icons + +- Search Iconify when an icon improves scanning: `uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 --max-num 8 --pretty`. +- Prefer simple, single-color SVG icons that match the theme accent. +- Use icons as supporting visual cues, not as replacements for required text. + +## Images + +- Search candidate images with `uv run python pptify-plugin/images/web_image_search.py --query "factory traceability dashboard" --max-num 8 --pretty`. +- Use local file paths in image objects when an image is selected: `content.path` plus `content.alt`. +- Give images an explicit `bbox` and use concise adjacent text. +- Do not use image placeholders as fallback assets; select an approved asset or omit the image object. + +## SVG and Raster Handling + +- Convert raster images to SVG wrappers with `uv run python pptify-plugin/images/raster_image_to_svg.py --source logo.png --output-path logo.svg --pretty`. +- Use `--mode vector-trace` only when optional tracing dependencies are installed and a true vector result is needed. +- Keep generated SVGs simple and readable when they are intended for PowerPoint editing. +- Vector tracing raster infographics can lose or distort text. Keep the original generated raster on visible slides when text fidelity matters, and place the traced SVG on a separate hidden final appendix slide for editability/reference. + +## Infographics + +- Generate text-to-image infographics only through OpenAI or Azure OpenAI providers configured by the user. +- Do not substitute a local fallback image provider when user-managed provider access is missing. +- Before generating any text-to-image artifact, collect missing required values with the VS Code prompt input dialog (`vscode_askQuestions` or equivalent): provider, prompt, model or deployment, image size, output path, and any non-secret access settings. +- For OpenAI image generation, create `.env` from `.env.template` when needed and have the user fill `PPTIFY_IMAGE_PROVIDER=openai`, `OPENAI_API_KEY`, and optionally `OPENAI_IMAGE_MODEL` directly in `.env`. +- For Azure `gpt-image-2` or `gpt-image-2.0` requests, create `.env` from `.env.template` when needed and have the user fill `PPTIFY_IMAGE_PROVIDER=azure-openai`, `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_IMAGE_DEPLOYMENT`, optional timeout, and any required key auth value directly in `.env`. +- Never ask the user to paste API keys, tokens, or connection strings into chat or into the prompt input dialog. If Entra auth is preferred, tell them to run `az login` in a terminal. +- The image helper loads `.env` automatically before generation. Example: `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty`. +- Save an attempt manifest beside the image output with provider, model or deployment, auth mode, prompt path, output path, success status, and error details on failure. A failed model call should be reported as failed, not replaced by a local placeholder and described as generated. +- If a provider call times out after authentication succeeds, retry once with a longer timeout when the user has already provided non-secret provider details. +- NotebookLM bridge commands are unavailable in this workspace snapshot because `pptify-plugin/images/notebooklm_infographic.py` is absent. Do not call a NotebookLM bridge unless that script is restored. +- For generated infographics, prefer the raster output as the visible slide asset. Add any raster-to-SVG vector trace as `hidden: true` on the final slide rather than replacing the visible infographic. + +## Asset Placement + +- Put decorative asset containers in `layout_tree.objects` with `classification: "layout_design"`. +- Put meaningful icons, diagrams, images, and infographics in `layout_tree.objects` with `classification: "content"`. +- Every asset object needs final inch-based `bbox` coordinates and a deliberate `z_index`. +- Use `z_index` so assets do not cover readable text. \ No newline at end of file diff --git a/plugins/pptify/.agent/workflows/deck-generation.md b/plugins/pptify/.agent/workflows/deck-generation.md new file mode 100644 index 000000000..6c08dd07a --- /dev/null +++ b/plugins/pptify/.agent/workflows/deck-generation.md @@ -0,0 +1,115 @@ +# Deck Generation E2E Workflow + +Use this workflow when a Copilot or coding agent needs to generate PPTX slides with `pptify`. + +## 1. Intake + +1. Before creating workflow artifacts, collect any missing required inputs with the VS Code prompt input dialog (`vscode_askQuestions` or equivalent). Batch concise questions, offer sensible defaults for optional fields, and continue after the user answers. +2. Identify the audience, decision, core narrative, required language, target slide count, source material, reference PPTX, branding constraints, output artifact paths, and delivery deadline. +3. If the user gives only a topic, create a reasonable executive narrative and mark assumptions in the generated spec summary. When the user asks for web images, sources, data enrichment, or a source-backed deck, gather and persist source material before authoring slides. +4. If the user provides source files, URLs, research material, or a reference deck, prepare them before generating the slide spec. +5. If the user requests text-to-image or generated images with OpenAI, Azure OpenAI, or Azure AI Foundry, create `.env` from `.env.template` when it is missing and have the user fill provider settings or secrets directly in `.env`. Never ask for API keys, tokens, or connection strings in chat or in the dialog. +6. Do not author a slide or summary that claims a model-generated infographic exists until provider, model or deployment, auth mode, prompt, output path, and attempt status are known. + +## 1A. Image Access Intake + +For any text-to-image request, prepare `.env` before invoking `pptify-plugin/images/text_prompt_to_infographic.py`. + +The infographic helper has no local fallback provider. If OpenAI or Azure OpenAI access is missing, record `missing_provider_config` or the provider failure in an attempt manifest and do not describe placeholder artwork as generated. + +For OpenAI image generation, configure these values in `.env`: + +1. `PPTIFY_IMAGE_PROVIDER=openai` or pass `--provider openai`. +2. `OPENAI_API_KEY`. +3. `OPENAI_IMAGE_MODEL`, defaulting to `gpt-image-1` when unspecified. +4. Image size, defaulting to `1024x1024` when unspecified. +5. Text prompt and output path. +6. Run image generation after `.env` is filled, for example `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty`. + +For Azure OpenAI / Azure AI Foundry image generation, configure these values in `.env`: + +1. `PPTIFY_IMAGE_PROVIDER=azure-openai` or pass `--provider azure-openai`. +2. `AZURE_OPENAI_ENDPOINT`, for example `https://.services.ai.azure.com/openai/v1`. +3. `AZURE_OPENAI_IMAGE_DEPLOYMENT`, for example `gpt-image-2` or the user's exact `gpt-image-2.0` deployment name. +4. Ask whether the user wants Azure CLI/Entra auth or API-key auth. +5. `AZURE_OPENAI_TIMEOUT`, defaulting to `300` when unspecified. +6. For Azure CLI/Entra auth, tell the user to run `az login`; for API-key auth, have the user fill `AZURE_OPENAI_API_KEY` or `AZURE_AI_API_KEY` in `.env`. +7. Run image generation after `.env` is filled, for example `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty`. +8. Save a small attempt manifest next to the asset with provider, endpoint or model name, auth mode, prompt path, output path, status, and error details when generation fails. Do not silently replace a failed or missing model output with local artwork. + +## 2. Prepare Sources and References + +1. Convert long documents and downloaded HTML pages with `pptify-plugin/documents/document_to_markdown.py`. +2. For URL-based, topic-plus-research, source-backed, or multi-source decks, build a combined markdown corpus and run `pptify-plugin/documents/document_to_raptor_tree.py` before planning slides. This is required even when the source files are individually short, because the deck should use synthesized evidence rather than a few searched keywords. +3. Record the source corpus path, RAPTOR summary path, source count, and source URLs in `summary.source_enrichment` in the generated spec. +4. When a reference deck should influence content or style, use the importable helpers in `pptify-plugin/extraction` or package inspection to collect style, brand, template, and layout-rhythm context. The `python -m pptify --analyze-pptx` command is unavailable unless the core renderer package is restored. +5. Use the analysis facts as LLM context when the new deck should preserve language, slide count, topic sequence, executive tone, colors, fonts, template conventions, and layout rhythm. +6. Use extraction helpers only when the task is preservation or reconstruction of the source deck. + +## 2A. Prepare Design Context (Required) + +1. Every new deck must choose a design direction before slide planning. Do not wait for the user to explicitly ask for `pptify-design`. +2. If the user supplies a brand guide or reference PPTX, use that as the primary style source and optionally add a compatible `pptify-design` profile for layout vocabulary. +3. If the user does not supply a style source, load at least one source-backed profile from `pptify-design/sources.json` with `uv run python pptify-plugin/design/design_context_catalog.py --profile --include-context --pretty`. +4. Default profile selection: + - Default, general modern, stylish, product, app, pitch, Microsoft, M365, Teams, Power Platform, or enterprise product decks: `fluent-ui-design-tokens`. + - Developer, GitHub, code, or engineering-system decks: `primer-primitives`. + - Consulting, strategy, governance, or operations reviews: `likaku-mck-ppt-design-skill` plus a conservative `corazzon-pptx-design-styles` style such as Swiss International, Monochrome Minimal, Editorial Magazine, or Architectural Blueprint. + - Broader modern style exploration or explicitly visual direction selection: `corazzon-pptx-design-styles`. + - Design reasoning or preflight critique: add `awesome-copilot-design-agents` as a secondary prompt context. +5. Lock exactly one visual style or design system before authoring slide coordinates. Record the selected profile ID, style name when applicable, palette, typography, spacing rhythm, signature elements, and source URLs in `summary.design_context`. +6. Include the returned context payload in the LLM context before writing `deck-spec.json`; do not summarize it away into a vague phrase such as "modern design". +7. A deck that uses default PowerPoint theme colors, Calibri-only text boxes, plain white backgrounds, or bullet-only layouts without a selected `summary.design_context` is not production-ready. + +## 3. Plan the Deck + +1. Produce one clear message per slide before choosing visuals. +2. Choose a slide form for each message, such as title, agenda, comparison, process, metrics, roadmap, risk, architecture, evidence, decision, infographic, dashboard-style overview, or appendix. +3. Use charts and dashboard-style slides only when the source corpus contains relevant quantitative or structured evidence. Represent them as explicit editable primitives or image-backed exhibits. +4. Keep each slide to three to five major content groups. +5. Preserve user-provided terminology, names, metrics, dates, and executive tone. +6. Decide the slide composition, hierarchy, coordinates, object sizes, z-order, colors, fonts, and font sizes during planning. The available plugin scripts will not do this later. +7. Every normal content slide should contain at least one visible design element derived from the locked style: a color band, card system, grid, rule, accent shape, diagram primitive, image treatment, pattern, or data exhibit. Avoid text-only slides unless the locked style is explicitly typographic. + +## 4. Author the JSON Spec + +1. Return a top-level object with `slides` and optional `summary`. +2. For each generated slide, include `id`, `title`, and `layout_tree`. +3. Do not use `pattern`, `layout_pattern`, `composition.pattern`, `layout`, `sections`, `bullets`, `objects`, `theme`, chart placeholders, or browser layout requests as render-time shorthand. +4. Each `layout_tree` must include `slide_size`, `root_group_id`, `groups`, `objects`, and optional `notes`. +5. Each group must include `id`, `role`, `layout_mode`, `object_ids`, `group_ids`, and a `bbox` when it represents a visible or bounded region. +6. Each object must include `id`, `kind`, `role`, `classification`, `content`, `style`, `bbox`, and `z_index`. +7. Treat decorative shapes as `layout_design`; treat meaningful text, tables, lines, and media as `content`. +8. Give every text-bearing object and table explicit `style.font_size` and `style.color`; do not rely on a later tool to shrink text to fit or infer contrast. Body and evidence text must be at least 10 pt; labels and captions at least 9 pt; footers at least 8 pt. +9. Give every line object explicit `content.x1`, `content.y1`, `content.x2`, and `content.y2`. +10. Give every line object explicit `style.line` and `style.line_width`. +11. Give every shape object explicit `content.shape`, `style.fill`, and `style.line`. +12. Translate the locked design context into explicit objects, colors, spacing, typography, and coordinates; do not rely on runtime pattern selection. +13. If a generated raster infographic is created, use that raster on the visible slide for fidelity, convert it with `raster_image_to_svg.py`, and add the SVG as a final `hidden: true` appendix slide. Record both paths in `summary.text_to_image`. + +## 5. Build the PPTX + +Current workspace reality check: this snapshot does not contain an importable `pptify/` package or `python -m pptify` CLI. Restore the core renderer package before using core render commands, or produce PPTX artifacts through direct PowerPoint generation plus standalone plugin evidence. + +1. As the Copilot CLI or VS Code agent, author or update `deck-spec.json` or a generation script directly; plugin scripts do not perform prompt-to-spec generation or full-deck rendering. +2. If the core renderer is restored, render the authored spec with `uv run python -m pptify deck-spec.json --out deck.pptx --audit deck-audit.json`. Otherwise build with the available PowerPoint generation path and keep plugin evidence/audits alongside the PPTX. Using `python-pptx` is only a serialization path; it must still implement the locked `pptify-design` coordinates, colors, typography, and decorative primitives. +3. For reference-guided generation, include analysis/source summaries and extracted `styles`, `brands`, `template`, and `layout` context in the agent prompt before writing `deck-spec.json`. +4. For predefined-template generation, include selected `pptify-design` context in the agent prompt before writing `deck-spec.json`. +5. Never copy, mutate, or save over a referenced PPTX as the deck generation strategy. + +## 6. Validate and Repair + +1. Inspect the audit for content collisions, text overflows, and warnings. +2. If collisions remain, move or resize objects, reduce density, split slides, or change the coordinate plan. +3. If text overflows, shorten copy, split content across slides, or enlarge object bboxes. Lower explicit font sizes only as a last resort and never below 9 pt for content objects. +4. Verify source and image workflow gates before final response: source-backed decks have `summary.source_enrichment` with corpus and RAPTOR paths; generated-image requests have an attempt manifest; successful raster infographics have a hidden SVG appendix slide. +5. Verify design-context gates before final response: `summary.design_context` exists, names the selected profile/style, and every visible content slide has at least one style-derived design element. +6. Treat plain white Calibri slides, default theme placeholders, unstyled bullet lists, and missing `summary.design_context` as quality failures even when collision audit passes. +7. Rebuild after each repair until generated slides have zero collisions, zero overflows, no unexpected warnings, and pass the design-context gate, or clearly report the residual issue. +8. For important deliverables, inspect the produced PPTX package with `python-pptx` or zip checks in addition to unit tests. + +## 7. Response Contract + +1. When asked to author the deck spec, write strict JSON with no markdown fences unless the user explicitly asks for prose. +2. When required workflow or artifact inputs are missing, prompt for them with the input dialog before authoring or building. +3. When acting as a coding agent in the workspace, create or update the spec or generation script, build with the available PowerPoint path, validate the audit and produced PPTX package, and report the generated artifact paths. diff --git a/plugins/pptify/.github/plugin/plugin.json b/plugins/pptify/.github/plugin/plugin.json new file mode 100644 index 000000000..0b8d17114 --- /dev/null +++ b/plugins/pptify/.github/plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "pptify", + "description": "Generate production-ready PowerPoint decks with pptify skills, source ingestion, design-context selection, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates.", + "version": "1.0.0", + "author": { + "name": "PPTify maintainers" + }, + "repository": "https://github.com/kimtth/agent-pptify-kit", + "license": "MIT", + "keywords": [ + "pptify", + "powerpoint", + "pptx", + "presentations", + "deck-generation", + "slides", + "design-context", + "visual-assets", + "quality-gates" + ] +} \ No newline at end of file diff --git a/plugins/pptify/README.md b/plugins/pptify/README.md new file mode 100644 index 000000000..e0e0b69df --- /dev/null +++ b/plugins/pptify/README.md @@ -0,0 +1,54 @@ +# PPTify Plugin + +Generate production-ready PowerPoint decks with pptify skills, source ingestion, design context, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates. + +## Installation + +```bash +copilot plugin install pptify@awesome-copilot +``` + +## What's Included + +### Skills + +| Skill | Description | +| --- | --- | +| `pptify-context-prep` | Prepare source material and design context before authoring a pptify deck spec. | +| `pptify-deck-generation` | Generate PPTX decks end to end from prompts, source material, reference PPTX analysis, coordinate-explicit layout trees, or pptify JSON specs. | +| `pptify-quality-gates` | Validate and repair PPTX artifacts by checking specs, PPTX packages, audits, layout trees, collisions, text overflows, warnings, visual hierarchy, asset layering, and reference deck alignment. | +| `pptify-slide-spec` | Author or repair coordinate-explicit pptify JSON deck specs with layout tree groups, objects, bounding boxes, tables, images, lines, shapes, type scale, and collision-safe content. | +| `pptify-tooling` | Look up pptify install commands, plugin script syntax, and workspace reality checks. | +| `pptify-visual-assets` | Find, generate, and place icons, images, SVGs, raster conversions, infographics, image placeholders, and asset-backed slide objects. | + +### Runtime artifacts generated by `pptify-cli` + +The plugin folder includes `.agent`, generated with: + +```powershell +uv run python pptify-cli install --home deploy\awesome-copilot\plugins\pptify +``` + +That runtime bundle contains: + +| Artifact | Purpose | +| --- | --- | +| `.agent\skills\pptify-*` | Installed pptify skill set. | +| `.agent\workflows\deck-generation.md` | End-to-end deck-generation workflow prompt. | +| `.agent\pptify-plugin` | Source ingestion, design context, image/SVG, extraction, and audit helper tools. | +| `.agent\pptify-design` | Source-backed design profiles and template context. | +| `.agent\.env.template` | Image-provider configuration template. | +| `.agent\pptify-policy.md` | Developer-protection and quality-gate policy. | +| `.agent\copilot-instruction.md` | Generic coding-agent instruction for using installed pptify assets. | + +## Usage + +Ask Copilot to create or repair a deck and mention `pptify`. The plugin guides the agent to collect required deck inputs, prepare source and reference context, select a design profile, author a coordinate-explicit JSON spec, build through the available PowerPoint path, and repair audit findings before reporting artifact paths. + +## Source + +This plugin is generated from [kimtth/agent-pptify-kit](https://github.com/kimtth/agent-pptify-kit) for submission to [Awesome Copilot](https://github.com/github/awesome-copilot). + +## License + +MIT \ No newline at end of file From 123cf859ba34e3ac651df49f7712a3368ddd8ac5 Mon Sep 17 00:00:00 2001 From: kimtth Date: Tue, 26 May 2026 11:00:39 +0900 Subject: [PATCH 2/4] fix: align pptify plugin structure with repository design --- docs/README.plugins.md | 2 +- docs/README.skills.md | 6 + plugins/pptify/.agent/.env.template | 25 - plugins/pptify/.agent/copilot-instruction.md | 35 -- plugins/pptify/.agent/pptify-design/README.md | 56 -- .../awesome-copilot-design-agents.md | 101 ---- .../contexts/alchaincyf-huashu-design.md | 86 --- .../contexts/corazzon-pptx-design-styles.md | 342 ----------- .../contexts/erickittelson-slidemason.md | 92 --- .../contexts/fluent-ui-design-tokens.md | 80 --- .../gabberflast-academic-pptx-skill.md | 110 ---- .../contexts/likaku-mck-ppt-design-skill.md | 91 --- .../contexts/nexu-io-open-design.md | 71 --- .../contexts/pptwork-oh-my-slides.md | 102 ---- .../contexts/primer-primitives.md | 137 ----- .../contexts/sunbigfly-ppt-agent-skills.md | 104 ---- .../pptify/.agent/pptify-design/sources.json | 258 --------- .../pptify-design/third-party-notices.md | 41 -- plugins/pptify/.agent/pptify-plugin/README.md | 94 --- .../.agent/pptify-plugin/audit/audit.py | 152 ----- .../.agent/pptify-plugin/design/__init__.py | 1 - .../design/design_context_catalog.py | 128 ---- .../pptify-plugin/documents/__init__.py | 1 - .../documents/document_to_markdown.py | 77 --- .../documents/document_to_raptor_tree.py | 490 ---------------- .../download-external-assets.ps1 | 74 --- .../.agent/pptify-plugin/external/README.md | 13 - .../external/all-MiniLM-L6-v2/.gitkeep | 1 - .../extraction/pptx_extractor.py | 545 ------------------ .../extraction/pptx_style_master.py | 505 ---------------- .../.agent/pptify-plugin/images/__init__.py | 1 - .../pptify-plugin/images/iconfy_search.py | 179 ------ .../images/raster_image_to_svg.py | 289 ---------- .../images/text_prompt_to_infographic.py | 318 ---------- .../pptify-plugin/images/web_image_search.py | 286 --------- plugins/pptify/.agent/pptify-policy.md | 60 -- .../.agent/skills/pptify-tooling/SKILL.md | 38 -- .../.agent/workflows/deck-generation.md | 115 ---- plugins/pptify/.github/plugin/plugin.json | 46 +- plugins/pptify/README.md | 25 +- .../pptify-context-prep/SKILL.md | 4 + .../references/design-profiles.md | 174 ++++++ .../pptify-deck-generation/SKILL.md | 93 +-- .../pptify-quality-gates/SKILL.md | 19 +- .../references/audit-checklist.md | 88 +++ .../pptify-slide-spec/SKILL.md | 8 + skills/pptify-tooling/SKILL.md | 89 +++ .../references/toolkit-setup.md | 105 ++++ .../pptify-visual-assets/SKILL.md | 16 + 49 files changed, 562 insertions(+), 5211 deletions(-) delete mode 100644 plugins/pptify/.agent/.env.template delete mode 100644 plugins/pptify/.agent/copilot-instruction.md delete mode 100644 plugins/pptify/.agent/pptify-design/README.md delete mode 100644 plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md delete mode 100644 plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md delete mode 100644 plugins/pptify/.agent/pptify-design/sources.json delete mode 100644 plugins/pptify/.agent/pptify-design/third-party-notices.md delete mode 100644 plugins/pptify/.agent/pptify-plugin/README.md delete mode 100644 plugins/pptify/.agent/pptify-plugin/audit/audit.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/design/__init__.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/documents/__init__.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 delete mode 100644 plugins/pptify/.agent/pptify-plugin/external/README.md delete mode 100644 plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep delete mode 100644 plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/images/__init__.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py delete mode 100644 plugins/pptify/.agent/pptify-plugin/images/web_image_search.py delete mode 100644 plugins/pptify/.agent/pptify-policy.md delete mode 100644 plugins/pptify/.agent/skills/pptify-tooling/SKILL.md delete mode 100644 plugins/pptify/.agent/workflows/deck-generation.md rename {plugins/pptify/.agent/skills => skills}/pptify-context-prep/SKILL.md (92%) create mode 100644 skills/pptify-context-prep/references/design-profiles.md rename {plugins/pptify/.agent/skills => skills}/pptify-deck-generation/SKILL.md (70%) rename {plugins/pptify/.agent/skills => skills}/pptify-quality-gates/SKILL.md (81%) create mode 100644 skills/pptify-quality-gates/references/audit-checklist.md rename {plugins/pptify/.agent/skills => skills}/pptify-slide-spec/SKILL.md (93%) create mode 100644 skills/pptify-tooling/SKILL.md create mode 100644 skills/pptify-tooling/references/toolkit-setup.md rename {plugins/pptify/.agent/skills => skills}/pptify-visual-assets/SKILL.md (81%) diff --git a/docs/README.plugins.md b/docs/README.plugins.md index e80f7e431..7aa4fd6f1 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -72,7 +72,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t | [power-bi-development](../plugins/power-bi-development/README.md) | Comprehensive Power BI development resources including data modeling, DAX optimization, performance tuning, visualization design, security best practices, and DevOps/ALM guidance for building enterprise-grade Power BI solutions. | 8 items | power-bi, dax, data-modeling, performance, visualization, security, devops, business-intelligence | | [power-platform-architect](../plugins/power-platform-architect/README.md) | Solution Architect for the Microsoft Power Platform, turning business requirements into functioning Power Platform solution architectures. | 1 items | power-platform, power-platform-architect, power-apps, dataverse, power-automate, power-pages, power-bi | | [power-platform-mcp-connector-development](../plugins/power-platform-mcp-connector-development/README.md) | Complete toolkit for developing Power Platform custom connectors with Model Context Protocol integration for Microsoft Copilot Studio | 3 items | power-platform, mcp, copilot-studio, custom-connector, json-rpc | -| [pptify](../plugins/pptify/README.md) | Generate production-ready PowerPoint decks with pptify skills, source ingestion, design-context selection, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates. | 0 items | pptify, powerpoint, pptx, presentations, deck-generation, slides, design-context, visual-assets, quality-gates | +| [pptify](../plugins/pptify/README.md) | Generate production-ready PowerPoint decks with pptify skills, source ingestion, design-context selection, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates. | 6 items | pptify, powerpoint, pptx, presentations, deck-generation, slides, design-context, visual-assets, quality-gates | | [project-documenter](../plugins/project-documenter/README.md) | Generate professional project documentation with draw.io architecture diagrams and Word (.docx) output with embedded images. Automatically discovers any project's technology stack and produces Markdown, diagrams, PNG exports, and a formatted Word document. | 3 items | documentation, architecture-diagrams, drawio, word-document, docx, png-images, c4-model, project-summary, auto-discovery | | [project-planning](../plugins/project-planning/README.md) | Tools and guidance for software project planning, feature breakdown, epic management, implementation planning, and task organization for development teams. | 15 items | planning, project-management, epic, feature, implementation, task, architecture, technical-spike | | [python-mcp-development](../plugins/python-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in Python using the official SDK with FastMCP. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | python, mcp, model-context-protocol, fastmcp, server-development | diff --git a/docs/README.skills.md b/docs/README.skills.md index 9d7f9c1a6..f32abdf1e 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -272,6 +272,12 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [power-platform-architect](../skills/power-platform-architect/SKILL.md)
`gh skills install github/awesome-copilot power-platform-architect` | Use this skill when the user needs to transform business requirements, use case descriptions, or meeting transcripts into a technical Power Platform solution architecture, including component selection and Mermaid.js diagrams. | None | | [power-platform-mcp-connector-suite](../skills/power-platform-mcp-connector-suite/SKILL.md)
`gh skills install github/awesome-copilot power-platform-mcp-connector-suite` | Generate complete Power Platform custom connector with MCP integration for Copilot Studio - includes schema generation, troubleshooting, and validation | None | | [powerbi-modeling](../skills/powerbi-modeling/SKILL.md)
`gh skills install github/awesome-copilot powerbi-modeling` | Power BI semantic modeling assistant for building optimized data models. Use when working with Power BI semantic models, creating measures, designing star schemas, configuring relationships, implementing RLS, or optimizing model performance. Triggers on queries about DAX calculations, table relationships, dimension/fact table design, naming conventions, model documentation, cardinality, cross-filter direction, calculation groups, and data model best practices. Always connects to the active model first using power-bi-modeling MCP tools to understand the data structure before providing guidance. | `references/MEASURES-DAX.md`
`references/PERFORMANCE.md`
`references/RELATIONSHIPS.md`
`references/RLS.md`
`references/STAR-SCHEMA.md` | +| [pptify-context-prep](../skills/pptify-context-prep/SKILL.md)
`gh skills install github/awesome-copilot pptify-context-prep` | Prepare source material and design context before authoring a pptify deck spec. Use when converting documents, building RAPTOR summaries, analyzing reference PPTX decks, or selecting and loading pptify-design profiles. | `references/design-profiles.md` | +| [pptify-deck-generation](../skills/pptify-deck-generation/SKILL.md)
`gh skills install github/awesome-copilot pptify-deck-generation` | Generate PPTX decks end to end with pptify. Use when creating PowerPoint slides from prompts, source material, reference PPTX analysis, coordinate-explicit layout trees, or pptify JSON specs. | None | +| [pptify-quality-gates](../skills/pptify-quality-gates/SKILL.md)
`gh skills install github/awesome-copilot pptify-quality-gates` | Validate and repair pptify PPTX artifacts. Use when checking deck specs, PPTX packages, audits, coordinate-explicit layout trees, collisions, text overflows, warnings, visual hierarchy, asset layering, or reference deck alignment. | `references/audit-checklist.md` | +| [pptify-slide-spec](../skills/pptify-slide-spec/SKILL.md)
`gh skills install github/awesome-copilot pptify-slide-spec` | Author or repair coordinate-explicit pptify JSON deck specs. Use when writing layout_tree groups, objects, bboxes, tables, images, lines, shapes, type scale, or collision-safe content. | None | +| [pptify-tooling](../skills/pptify-tooling/SKILL.md)
`gh skills install github/awesome-copilot pptify-tooling` | Command reference for pptify plugin tools. Use when looking up install commands, plugin script syntax, or the workspace reality check. | `references/toolkit-setup.md` | +| [pptify-visual-assets](../skills/pptify-visual-assets/SKILL.md)
`gh skills install github/awesome-copilot pptify-visual-assets` | Find, generate, and place visual assets for pptify PPTX decks. Use when adding icons, images, SVGs, raster conversions, infographics, image placeholders, or asset-backed slide objects. | None | | [pr-dashboard](../skills/pr-dashboard/SKILL.md)
`gh skills install github/awesome-copilot pr-dashboard` | Open a GitHub PR dashboard in the browser. Use when the user asks to see their pull requests, open the PR dashboard, show PRs for a date range, or check PR status. Trigger phrases include "show my PRs", "open PR dashboard", "pull request dashboard". | `assets/dashboard.html`
`scripts/lib`
`scripts/pr-dashboard-cli.mjs` | | [pr-screenshots](../skills/pr-screenshots/SKILL.md)
`gh skills install github/awesome-copilot pr-screenshots` | Embed before/after screenshots and annotated images in pull request descriptions. Covers PR description patterns, image upload for Azure DevOps and GitHub, and sizing best practices. | None | | [prd](../skills/prd/SKILL.md)
`gh skills install github/awesome-copilot prd` | Generate high-quality Product Requirements Documents (PRDs) for software systems and AI-powered features. Includes executive summaries, user stories, technical specifications, and risk analysis. | None | diff --git a/plugins/pptify/.agent/.env.template b/plugins/pptify/.agent/.env.template deleted file mode 100644 index 8613cf3d9..000000000 --- a/plugins/pptify/.agent/.env.template +++ /dev/null @@ -1,25 +0,0 @@ -# Copy this file to .env when image generation needs provider credentials. -# Never commit .env. It is ignored by git. - -# Provider selection: auto, openai, or azure-openai. -PPTIFY_IMAGE_PROVIDER=auto - -# OpenAI image generation. -OPENAI_API_KEY= -OPENAI_IMAGE_MODEL=gpt-image-1 - -# Azure OpenAI / Azure AI Foundry image generation. -# For gpt-image-2, the endpoint often ends with /openai/v1. -AZURE_OPENAI_ENDPOINT= -AZURE_OPENAI_IMAGE_DEPLOYMENT=gpt-image-2 -# Optional compatibility alias used when AZURE_OPENAI_IMAGE_DEPLOYMENT is blank. -MODEL_NAME= -AZURE_OPENAI_API_VERSION=2024-02-01 -AZURE_OPENAI_TIMEOUT=300 - -# Use one of these only when key auth is required. Leave blank for Azure CLI / Entra auth. -AZURE_OPENAI_API_KEY= -AZURE_AI_API_KEY= - -# Optional explicit Azure CLI path if az is not discoverable. -AZURE_CLI_PATH= \ No newline at end of file diff --git a/plugins/pptify/.agent/copilot-instruction.md b/plugins/pptify/.agent/copilot-instruction.md deleted file mode 100644 index 7b01b27ca..000000000 --- a/plugins/pptify/.agent/copilot-instruction.md +++ /dev/null @@ -1,35 +0,0 @@ - -# pptify Generic Coding-Agent Instructions - -Use the installed `./.agent` assets as the local pptify runtime context. - -## Installed Context - -- Skills: `./.agent/skills/pptify-*` -- Workflows: `./.agent/workflows` -- Design profiles and predefined templates: `./.agent/pptify-design` -- Plugin tool set: `./.agent/pptify-plugin` -- Image provider environment template: `./.agent/.env.template` -- Developer-protection policy: `./.agent/pptify-policy.md` - -## Agent Rules - -- Read `./.agent/pptify-policy.md` before generating or repairing a deck. -- For every new generated deck, choose and load a `./.agent/pptify-design` - profile before authoring slides unless a user-provided brand guide or - reference PPTX is the primary style source. Default to - `fluent-ui-design-tokens`; for developer decks use `primer-primitives`; for - consulting/governance decks use `likaku-mck-ppt-design-skill`; use - `corazzon-pptx-design-styles` only when a broader modern style catalog is - explicitly useful. -- Record selected profile IDs, source URLs, palette, typography, spacing rhythm, - and signature elements in `summary.design_context`. -- Treat plain white, Calibri-only, bullet-heavy `python-pptx`-looking output as - not production-ready. -- Use scripts under `./.agent/pptify-plugin` for source ingestion, design - context loading, visual assets, PPTX extraction, and audit checks. -- When image generation needs provider configuration or credentials, create - `./.agent/.env` from `./.agent/.env.template` and have the user fill secrets - directly in that file. Do not ask for secrets in chat or prompt dialogs. -- Keep generated specs coordinate-explicit and preserve source/license metadata - from the selected design profile. diff --git a/plugins/pptify/.agent/pptify-design/README.md b/plugins/pptify/.agent/pptify-design/README.md deleted file mode 100644 index a80f389ca..000000000 --- a/plugins/pptify/.agent/pptify-design/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# pptify-design - -Source-backed design context packs for `pptify` agents live here. The profiles in this folder are curated from public GitHub projects and web-accessible source files; the actual design templates are not invented by this repository. - -Agents use these files as LLM context before authoring a coordinate-explicit deck spec or generation script. The current plugin toolkit does not load these files automatically. - -## Catalog - -The catalog is [sources.json](sources.json). Each profile includes source URLs, license metadata, local context, and extracted source signals that can be translated into explicit `pptify` layout-tree objects, colors, typography, spacing, and slide composition choices. - -Current workflow-aligned profiles: - -| Profile | Source | Use when | -| --- | --- | --- | -| `primer-primitives` | GitHub Primer Primitives | Product, engineering, and developer-facing decks that need token discipline. | -| `fluent-ui-design-tokens` | Microsoft Fluent UI token guidance | Microsoft, enterprise, M365, Teams, or Power Platform-aligned decks. | -| `awesome-copilot-design-agents` | GitHub Awesome Copilot design agents/skills | Agent prompt context for design review, UX discovery, and visual hierarchy. | -| `corazzon-pptx-design-styles` | corazzon/pptx-design-styles | A 30-style modern PPTX template catalog for selecting and translating a visual direction into explicit pptify primitives. | - -Additional profiles in [sources.json](sources.json) cover staged deck-generation pipelines, consulting-style layout taxonomies, HTML-to-PPTX export constraints, and artifact critique workflows. - -## Agent Usage - -List available profiles: - -```powershell -uv run python pptify-plugin/design/design_context_catalog.py --list --pretty -``` - -Load one profile with local context text: - -```powershell -uv run python pptify-plugin/design/design_context_catalog.py --profile primer-primitives --include-context --pretty -``` - -Load multiple profiles when a deck needs both a presentation theme and a design-system prompt: - -```powershell -uv run python pptify-plugin/design/design_context_catalog.py --profile fluent-ui-design-tokens --profile awesome-copilot-design-agents --include-context --pretty -``` - -Load the modern PPTX style catalog: - -```powershell -uv run python pptify-plugin/design/design_context_catalog.py --profile corazzon-pptx-design-styles --include-context --pretty -``` - -## Rules - -- Treat these as LLM context, not executable renderer config. -- Keep source attribution and license metadata with any copied or adapted context. -- Do not invent a new design template when a user asks for predefined templates; choose a catalog profile, analyze a reference PPTX, or ask the user for a source. -- Translate source signals into explicit `layout_tree` primitives, bboxes, z-order, colors, and typography. Do not rely on a runtime theme or layout-pattern engine in this workspace snapshot. -- Do not copy external fonts, icons, images, or binary assets unless their license and source are explicitly added. - -See [third-party-notices.md](third-party-notices.md) for source notices. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md b/plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md deleted file mode 100644 index 5c60df8f1..000000000 --- a/plugins/pptify/.agent/pptify-design/agent-prompts/awesome-copilot-design-agents.md +++ /dev/null @@ -1,101 +0,0 @@ -# Awesome Copilot Design Agent and Prompt Context - -Source-backed predefined agent prompt context. - -- Source repository: https://github.com/github/awesome-copilot -- Source files: - - `agents/gem-designer.agent.md` - - `agents/se-ux-ui-designer.agent.md` - - `skills/penpot-uiux-design/SKILL.md` - - `skills/prompt-optimizer/SKILL.md` -- License: MIT; see [third-party-notices.md](../third-party-notices.md) -- Retrieved: 2026-05-18 - -## Selected Source Excerpts - -From `agents/gem-designer.agent.md`: - -```md -## Role - -DESIGNER. Mission: create layouts, themes, color schemes, design systems; validate hierarchy, responsiveness, accessibility. Deliver: design specs. Constraints: never implement code. -``` - -```md -## Knowledge Sources - -1. `./docs/PRD.yaml` -2. Codebase patterns -3. `AGENTS.md` -4. Official docs (online or llms.txt) -5. Existing design system (tokens, components, style guides) -``` - -```md -#### 2.1 Requirements Analysis - -- Understand: component, page, theme, or system -- Check existing design system for reusable patterns -- Identify constraints: framework, library, existing tokens -- Review PRD for UX goals -``` - -From `agents/se-ux-ui-designer.agent.md`: - -```md -## Your Mission: Understand Jobs-to-be-Done - -Before any UI design work, identify what "job" users are hiring your product to do. Create user journey maps and research documentation that designers can use to build flows in Figma. -``` - -```md -## Step 1: Always Ask About Users First - -**Before designing anything, understand who you're designing for:** - -### Who are the users? -- "What's their role? (developer, manager, end customer?)" -- "What's their skill level with similar tools? (beginner, expert, somewhere in between?)" -- "What device will they primarily use? (mobile, desktop, tablet?)" -- "Any known accessibility needs? (screen readers, keyboard-only navigation, motor limitations?)" -``` - -From `skills/penpot-uiux-design/SKILL.md`: - -```md -### The Golden Rules - -1. **Clarity over cleverness**: Every element must have a purpose -2. **Consistency builds trust**: Reuse patterns, colors, and components -3. **User goals first**: Design for tasks, not features -4. **Accessibility is not optional**: Design for everyone -5. **Test with real users**: Validate assumptions early -``` - -```md -### Visual Hierarchy (Priority Order) - -1. **Size**: Larger = more important -2. **Color/Contrast**: High contrast draws attention -3. **Position**: Top-left (LTR) gets seen first -4. **Whitespace**: Isolation emphasizes importance -5. **Typography weight**: Bold stands out -``` - -From `skills/prompt-optimizer/SKILL.md`: - -```md -**Document creation (slides, reports).** Ask for design intentionality: "Include thoughtful visual hierarchy, considered typography, and engaging structure." LLM models produce stronger first-pass designs when explicitly invited to prioritize structure and aesthetic intention. -``` - -## Source Signals for LLM Context - -- Agent prompt focus from source: existing design system first, visual hierarchy, UX discovery, accessibility, and prompt clarity. -- Deck-planning cue from source: for slides and reports, explicitly ask for thoughtful visual hierarchy, considered typography, and engaging structure. -- UX discovery cue from source: identify users, context, pain points, and Jobs-to-be-Done before visual design choices. - -## PPTify Translation Guardrails - -- Use this context when the user asks for an agent prompt, sub-agent guidance, or design-review framing for a `pptify` deck. -- Treat it as prompt context; it is not a `pptify` renderer plugin and not a complete local agent mode. -- Combine with a current presentation or design-system profile such as `primer-primitives` or `fluent-ui-design-tokens` when the deck also needs concrete visual tokens. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md b/plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md deleted file mode 100644 index 86c488721..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/alchaincyf-huashu-design.md +++ /dev/null @@ -1,86 +0,0 @@ -# alchaincyf/huashu-design — Design Context - -**Source repo:** alchaincyf/huashu-design -**Style reference:** HTML-native design pipeline with brand-asset protocol -**Retrieved:** 2026-05-19 - ---- - -## What this repo teaches - -`alchaincyf/huashu-design` builds presentation decks HTML-first and treats PPTX as a constrained export target. The key contribution is a structured brand-asset protocol that governs how visual directions are defined, how multiple directions are run in parallel, and how HTML output is verified with Playwright before being exported to editable PPTX. - ---- - -## Key patterns - -### Brand asset protocol (`brand-asset-protocol`, `brand-assets`) -Before any layout work, codify the brand in a structured block: - -```json -{ - "brand_name": "Contoso", - "primary_palette": ["#0F6CBD", "#009C9C", "#6B5DD3"], - "neutral_palette": ["#F7FAFC", "#FFFFFF", "#17233A"], - "typeface_display": "Segoe UI", - "typeface_body": "Segoe UI", - "logo_description": "wordmark, dark navy on white", - "tone": "confident, technical, concise" -} -``` - -This block is passed into every slide generation prompt as a locked context. No colors, fonts, or geometric treatments are allowed to deviate from it. - -### Parallel visual directions (`visual-directions`, `five-schools`) -Generate 3–5 distinct visual direction options from the same content outline. Each direction varies: -- Background lightness (light, mid, dark) -- Accent geometry (circles, bars, diagonals, sharp rectangles) -- Typography weight (light/thin vs. bold/heavy) -- Information density (spacious vs. dense) - -Directions are rendered as HTML thumbnail cards. The selected direction becomes the locked style for the full deck. - -### HTML-to-PPTX export (`html-native`, `html-to-editable-pptx`, `editable-html-export`) -HTML is the design source. PPTX is the delivery format. The export pipeline: -1. Render full slide HTML -2. Run Playwright visual QA (check for overflow, font rendering, color contrast) -3. Map HTML layout to PPTX native shapes — every text frame must be individually editable -4. Verify no raster screenshots survive in the PPTX (all shapes must be vector/native) - -### Playwright checking (`playwright-check`) -After HTML render: -- Screenshot each slide at 1280×720 -- Check bounding box of every text container for overflow -- Check color contrast ratio (WCAG AA: ≥ 4.5:1 for body text) -- Check that no element is clipped by slide boundaries -- Fail the build if any check fails - ---- - -## Agent-level lessons for pptify - -1. **HTML layout ≠ editable PPTX.** HTML absolute positioning does not map 1:1 to PPTX native shapes. Constrain layout to pptify's coordinate system (13.333" × 7.5", absolute inches) rather than pixel-based CSS. -2. **Brand lock is non-negotiable.** Once `style_lock` is set from the brand asset protocol, every subsequent prompt must pass it through unchanged. -3. **Parallel directions reduce iteration.** Show 3 style options before the deck plan, not after. Post-hoc redesign is expensive. -4. **Every text frame must be individually editable.** Raster screenshots of styled text are never acceptable in pptify output. - ---- - -## Design guidance for pptify themes - -| Signal from huashu-design | pptify equivalent | -|---|---| -| `brand_asset_protocol` block | `theme` / `style_lock` dict in slide spec | -| HTML `background-color` | `theme.background` | -| Accent `border-color` | `theme.secondary` on shape border | -| `font-family` | `theme.font` | -| Playwright overflow check | pptify audit collision/overflow gate | - ---- - -## Best for - -- Brand-constrained enterprise decks requiring exact color/type fidelity -- Workflows where an agent uses a design mockup as evidence, then authors final PPTX coordinates explicitly -- Multi-direction style exploration before committing to a palette -- Decks requiring strong Playwright/visual verification discipline diff --git a/plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md b/plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md deleted file mode 100644 index aeb2bdd88..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/corazzon-pptx-design-styles.md +++ /dev/null @@ -1,342 +0,0 @@ -# corazzon/pptx-design-styles - Design Context - -**Source repo:** corazzon/pptx-design-styles -**Style reference:** 30 modern PPTX visual style templates with colors, fonts, layouts, signature elements, and anti-patterns -**Retrieved:** 2026-05-20 - ---- - -## What this repo teaches - -`corazzon/pptx-design-styles` is a curated design-template skill for presentation generation. Its value to pptify is not a runtime theme engine; it is a compact visual vocabulary that agents can translate into explicit `layout_tree` primitives: fills, text boxes, rules, cards, panels, diagrams, grids, and image-backed effects when needed. - -The upstream project includes English and Korean README files plus a Korean preview page. This local context is normalized to English and references the English `README.md`, `SKILL.md`, and `references/styles.md` only. - -No upstream binary assets are copied into this repository. Treat the upstream preview page and images as reference material only. - ---- - -## Core agent rule for pptify - -Choose one style before layout planning, lock its palette and typography, then translate the style into editable PowerPoint primitives wherever possible. - -- Use the exact HEX colors listed in the style lock. -- Keep one visual signature element present across the deck. -- Every slide should contain a visual element: shape, color block, card, diagram, rule, pattern, or image-backed exhibit. -- Avoid text-only slides unless the chosen style is explicitly typographic. -- Do not mix multiple styles in one deck unless the user asks for a deliberate contrast. -- CSS effects such as blur, backdrop-filter, blend modes, gradient text, or animated scan lines must be translated into PowerPoint-safe approximations: transparent shapes, raster background accents, layered fills, or editable lines. - ---- - -## Style recommendation matrix - -| Deck goal | Recommended styles | -|---|---| -| Tech, AI, startup | Glassmorphism, Aurora Neon Glow, Cyberpunk Outline, SciFi Holographic Data | -| Corporate, consulting, finance | Swiss International, Monochrome Minimal, Editorial Magazine, Architectural Blueprint | -| Education, research, history | Dark Academia, Nordic Minimalism, Brutalist Newspaper | -| Brand or marketing launch | Gradient Mesh, Typographic Bold, Duotone Color Split, Risograph Print | -| Product, app, UX | Bento Grid, Claymorphism, Pastel Soft UI, Liquid Blob Morphing | -| Entertainment or gaming | Retro Y2K, Dark Neon Miami, Vaporwave, Memphis Pop Pattern | -| Eco, wellness, culture | Hand-crafted Organic, Nordic Minimalism, Dark Forest Nature | -| Infrastructure or architecture | Isometric 3D Flat, Cyberpunk Outline, Architectural Blueprint | -| Portfolio, art, creative | Monochrome Minimal, Editorial Magazine, Risograph Print, Maximalist Collage | -| Pitch or strategy | Neo-Brutalism, Duotone Color Split, Bento Grid, Art Deco Luxe | -| Luxury, premium event | Art Deco Luxe, Monochrome Minimal, Dark Academia | -| Science, biotech, innovation | Liquid Blob Morphing, SciFi Holographic Data, Aurora Neon Glow | - ---- - -## Normalized style taxonomy - -### 01. Glassmorphism -- Mood: premium, tech, futuristic. -- Best for: SaaS, app launches, AI products. -- Palette: deep gradient `#1A1A4E`, `#6B21A8`, `#1E3A5F`; glass white at 15-20% opacity; border white at 25%; accents `#67E8F9` or `#A78BFA`. -- Typography: Segoe UI Light or Calibri Light titles, 36-44pt; Segoe UI body, 14-16pt; large KPI numbers, 52-64pt. -- Layout: translucent rounded cards, offset layering, dark gradient field, soft glow ellipses. -- Signature: consistent frosted-card treatment. -- Avoid: white backgrounds, opaque cards, saturated solid fills. - -### 02. Neo-Brutalism -- Mood: bold, raw, provocative, startup energy. -- Best for: pitch decks, marketing, creative agencies. -- Palette: yellow `#F5F500`, lime `#CCFF00`, hot pink `#FF2D55`, black `#000000`, red `#FF3B30`, blue `#0000FF`. -- Typography: Arial Black, Impact, or Bebas Neue titles, 40-56pt; Courier New or Space Mono body, 13-16pt. -- Layout: thick black borders, hard offset shadows, intentional misalignment, oversized words or numbers. -- Signature: pure-black no-blur shadows on every card. -- Avoid: soft shadows, gradients, rounded corners, muted colors. - -### 03. Bento Grid -- Mood: modular, structured, Apple-inspired. -- Best for: feature comparisons, product overviews, data summaries. -- Palette: off-white `#F8F8F2`, navy `#1A1A2E`, yellow `#E8FF3B`, coral `#FF6B6B`, teal `#4ECDC4`, warm yellow `#FFE66D`. -- Typography: SF Pro or Inter titles, 18-24pt; Inter body, 12-14pt; large stats, 48-64pt. -- Layout: asymmetric grid cells with varied spans and 8-12pt gaps. -- Signature: one dark anchor cell with white text plus color-coded supporting cells. -- Avoid: equal-size grids, dense text, more than five colors. - -### 04. Dark Academia -- Mood: scholarly, vintage, refined. -- Best for: education, historical research, book or university talks. -- Palette: deep brown `#1A1208`, near-black `#0E0A05`, antique gold `#C9A84C`, parchment `#D4BF9A`, muted gold `#8A7340`. -- Typography: Playfair Display Italic or Georgia Italic titles, 36-48pt; EB Garamond or Georgia body, 13-16pt; Space Mono labels. -- Layout: inset border frames, centered serif title, decorative horizontal rules, generous leading. -- Signature: double gold border and italic serif title. -- Avoid: modern sans-serif display type, bright colors, overly clean minimalism. - -### 05. Gradient Mesh -- Mood: artistic, vibrant, brand-forward. -- Best for: brand launches, creative portfolios, music or film promotions. -- Palette: hot pink `#FF6EC7`, violet `#7B61FF`, cyan `#00D4FF`, warm orange `#FFB347`, white text. -- Typography: Bebas Neue or Barlow Condensed ExtraBold titles, 48-72pt; Outfit or Poppins Light body, 14-16pt. -- Layout: full-bleed multi-radial gradient, minimal white text, optional frosted body panel. -- Signature: painterly multi-color mesh with large type. -- Avoid: simple two-color linear gradients, dark text, overcrowding. - -### 06. Claymorphism -- Mood: friendly, tactile, soft 3D. -- Best for: product launches, education, app UI decks. -- Palette: peach gradient `#FFECD2` to `#FCB69F`, teal `#A8EDEA`, blush `#FED6E3`, yellow `#FFEAA7`. -- Typography: Nunito ExtraBold or rounded display titles, 32-48pt; Nunito or DM Sans body, 14-16pt. -- Layout: high-radius rounded shapes, colored shadows, top-edge highlights, asymmetric bubbles. -- Signature: color-matched soft shadows and inner highlights. -- Avoid: sharp corners, grey shadows, mixed flat elements. - -### 07. Swiss International -- Mood: functional, authoritative, timeless. -- Best for: consulting, finance, government, institutional decks. -- Palette: white `#FFFFFF`, near-black `#111111`, signal red `#E8000D`, dark grey `#444444`, light grey `#DDDDDD`. -- Typography: Helvetica Neue or Arial titles, 32-44pt; Arial body, 12-14pt; Space Mono captions. -- Layout: strict 5-column or 12-column grid, left red rule, horizontal divider, generous margins. -- Signature: left-edge red bar and grid-aligned text blocks. -- Avoid: decorative illustration, rounded corners, more than two fonts. - -### 08. Aurora Neon Glow -- Mood: futuristic, electric, AI-oriented. -- Best for: AI products, cybersecurity, innovation summits. -- Palette: deep black `#050510`, neon green `#00FF88`, violet `#7B00FF`, cyan `#00B4FF`, soft white `#D0D0F0`. -- Typography: Bebas Neue or Barlow Condensed titles, 44-60pt; DM Mono or Space Mono body, 12-14pt. -- Layout: dark field, blurred neon circles, gradient title approximation, transparent dark panels. -- Signature: green-cyan-violet glow system. -- Avoid: light backgrounds, flat non-glowing colors, dense body text without panels. - -### 09. Retro Y2K -- Mood: nostalgic, pop, chaotic. -- Best for: events, lifestyle marketing, fashion campaigns. -- Palette: navy `#000080`, electric blue `#0020C2`, rainbow stripe, white title, cyan `#00FFFF`, magenta `#FF00FF`, yellow `#FFFF00`. -- Typography: Bebas Neue or Impact titles, 36-52pt; VT323 or Space Mono body, 12-14pt. -- Layout: top and bottom rainbow bars, centered title, sparkle motifs, double-color shadow effects. -- Signature: rainbow stripe bars and cyan/magenta title shadow. -- Avoid: minimalism, muted colors, serif fonts. - -### 10. Nordic Minimalism -- Mood: calm, natural, Scandinavian. -- Best for: wellness, lifestyle, nonprofit, sustainable brands. -- Palette: cream `#F4F1EC`, warm grey `#D9CFC4`, dark brown `#3D3530`, taupe `#8A7A6A`. -- Typography: Canela, Freight Display, or DM Serif Display titles, 36-52pt; Inter Light or Lato Light body, 13-15pt. -- Layout: at least 40% negative space, one organic background shape, small dot accent set, bottom rule and caption. -- Signature: organic blob, three-dot accent, letter-spaced monospace caption. -- Avoid: bright colors, dense layouts, sans-serif display type. - -### 11. Typographic Bold -- Mood: editorial, impactful, authoritative. -- Best for: manifestos, brand statements, headline announcements. -- Palette: off-white `#F0EDE8`, black `#0A0A0A`, near-black `#1A1A1A`, signal red `#E63030`, light grey `#AAAAAA`. -- Typography: Bebas Neue or Anton, 80-120pt; Space Mono footnotes. -- Layout: type fills the slide; two or three lines maximum; one accent word. -- Signature: oversized display typography as the visual. -- Avoid: images, icons, more than three large text lines, multiple font families. - -### 12. Duotone Color Split -- Mood: dramatic, comparative, energetic. -- Best for: strategy, before-after, compare-contrast slides. -- Palette: orange-red `#FF4500`, deep navy `#1A1A2E`, white divider and text. -- Typography: Bebas Neue panel text, 40-56pt; Space Mono captions. -- Layout: exact 50/50 split with white divider; one idea per side. -- Signature: cross-panel color echo. -- Avoid: three or more panels, weak contrast, busy content. - -### 13. Monochrome Minimal -- Mood: restrained, luxury, gallery-like. -- Best for: luxury brands, portfolios, high-end consulting. -- Palette: near-white `#FAFAFA`, black `#0A0A0A`, near-black `#1A1A1A`, greys `#E0E0E0`, `#888888`, `#CCCCCC`. -- Typography: Helvetica Neue Thin or Futura Light display, 24-36pt; Helvetica Neue body; Space Mono accent. -- Layout: thin circle focal point, descending-width bars, extreme negative space. -- Signature: pure monochrome with precise thin-line geometry. -- Avoid: color, decorative illustration, crowded layouts. - -### 14. Cyberpunk Outline -- Mood: HUD, sci-fi, dark tech. -- Best for: gaming, AI infrastructure, security, data engineering. -- Palette: near-black `#0D0D0D`, neon cyan `#00FFC8` at varied opacities. -- Typography: Bebas Neue outline-style titles, 44-60pt; Space Mono body and labels. -- Layout: subtle grid, four corner brackets, centered outline title, bottom data label. -- Signature: stroke-only title and corner markers. -- Avoid: white backgrounds, filled title text, warm colors. - -### 15. Editorial Magazine -- Mood: journalistic, narrative, sophisticated. -- Best for: annual reports, brand stories, long-form deck narratives. -- Palette: white `#FFFFFF`, near-black `#1A1A1A`, signal red `#E63030`, light grey `#BBBBBB`. -- Typography: Playfair Display Italic titles, 34-48pt; Space Mono subheads; Georgia body. -- Layout: asymmetric white/dark split, short red rule, rotated vertical label, column-style body copy. -- Signature: magazine split layout and red rule. -- Avoid: symmetry, sans-serif display type, full-bleed colored backgrounds. - -### 16. Pastel Soft UI -- Mood: gentle, app-like, healthcare-friendly. -- Best for: healthcare, beauty, education startups, consumer apps. -- Palette: pink-blue-mint gradient, white cards at 70%, blush `#F9C6E8`, sky blue `#C6E8F9`. -- Typography: Nunito or DM Sans titles, 28-36pt; Nunito or DM Sans body, 13-15pt. -- Layout: floating translucent cards, central pill or circular card, corner blobs, soft colored shadows. -- Signature: frosted white cards over pastel gradient. -- Avoid: dark backgrounds, saturated primaries, hard shadows. - -### 17. Dark Neon Miami -- Mood: synthwave, nightlife, 1980s neon. -- Best for: entertainment, music festivals, events. -- Palette: purple-black `#0A0014`, orange `#FF6B35`, hot pink `#FF0080`, purple `#9B00FF`. -- Typography: Bebas Neue titles, 36-52pt; Space Mono body. -- Layout: lower-center sunset semicircle, converging perspective grid, centered top title. -- Signature: sunset semicircle and neon grid. -- Avoid: blue-green dominant palettes, daylight backgrounds, plain body type. - -### 18. Hand-crafted Organic -- Mood: artisanal, natural, human. -- Best for: eco brands, food and beverage, craft studios, wellness. -- Palette: craft paper `#FDF6EE`, tan `#C8A882`, brown `#A87850`, dark brown `#6B4C2A`, natural greens. -- Typography: Playfair Display Italic or Cormorant Garamond Italic titles, 22-34pt; EB Garamond body. -- Layout: nested circles, botanical line-art accents, dashed rules, italic serif title. -- Signature: imperfect dashed outer circle and natural accents. -- Avoid: clean geometry, synthetic colors, sans-serif fonts. - -### 19. Isometric 3D Flat -- Mood: technical, structured, architectural. -- Best for: IT architecture, data flow, system diagrams. -- Palette: navy `#1E1E2E`, violet faces `#7C6FFF`, `#4A3FCC`, `#6254E8`, highlight `#A594FF`. -- Typography: Space Mono labels, 10-12pt; Bebas Neue or Barlow Condensed titles, 28-40pt. -- Layout: 30-degree isometric block clusters, thin connectors, title upper-right. -- Signature: three-face shading system. -- Avoid: perspective 3D, rounded shapes, light backgrounds. - -### 20. Vaporwave -- Mood: dreamy, surreal, internet-nostalgic. -- Best for: creative agencies, music, art portfolios. -- Palette: purple gradient `#1A0533` to `#570038`, sun colors `#FF9F43`, `#FF6B9D`, `#C44DFF`, grid `#FF64C8`. -- Typography: Bebas Neue ghost and gradient text; Space Mono body. -- Layout: perspective grid floor, sliced sunset semicircle, ghost watermark title, bottom gradient text. -- Signature: sliced sun and grid floor. -- Avoid: corporate layouts, muted earth tones, conventional typography. - -### 21. Art Deco Luxe -- Mood: gilded, prestigious, 1920s grandeur. -- Best for: luxury brands, gala events, premium reports. -- Palette: black-brown `#0E0A05`, gold `#B8960C`, rich gold `#D4AA2A`, muted gold `#8A7020`. -- Typography: Cormorant Garamond, Trajan, or Didot titles, 26-36pt, all caps and wide-spaced; Space Mono captions. -- Layout: double inset border, side fan ornaments, center rule and diamond, centered uppercase title. -- Signature: gold frame, fan ornaments, diamond divider. -- Avoid: modern sans-serif fonts, colorful or pastel tones, asymmetric layouts. - -### 22. Brutalist Newspaper -- Mood: raw journalism, editorial authority. -- Best for: media, research institutes, content industry decks. -- Palette: aged paper `#F2EFE8`, warm black `#1A1208`, body brown `#3A3020`. -- Typography: Space Mono masthead, Georgia or Playfair headline, Georgia body. -- Layout: dark masthead bar, double rules, two columns with divider, photo placeholder and caption. -- Signature: newspaper nameplate and dense two-column editorial layout. -- Avoid: modern sans-serif fonts, colorful elements, sparse white space. - -### 23. Stained Glass Mosaic -- Mood: vibrant, artistic, cathedral-rich. -- Best for: museums, culture, arts organizations. -- Palette: grout `#0A0A12`, blue `#1A3A6E`, crimson `#E63030`, yellow `#F5D020`, green `#2A6E1A`, purple `#6E1A4E`. -- Typography: Cormorant Garamond Bold or Trajan overlay titles; Georgia body. -- Layout: 6x4 mosaic grid with dark gaps, varied color rhythm, dark overlay for legibility. -- Signature: stained-glass cells with no matching adjacent colors. -- Avoid: pastel cells, large empty cells, sans-serif overlay text. - -### 24. Liquid Blob Morphing -- Mood: organic, fluid, bio-digital. -- Best for: biotech, environmental tech, innovation labs. -- Palette: ocean gradient `#0F2027` to `#2C5364`, teal `#00D2BE`, blue `#0078FF`, violet `#7800FF`, near-white `#F0FFFE`. -- Typography: Bebas Neue titles, 36-48pt; DM Mono or Space Mono body. -- Layout: three overlapping translucent blob shapes, dark ocean field, glowing centered title. -- Signature: overlapped low-opacity blobs and teal halo. -- Avoid: sharp geometry, bright warm backgrounds, dense text. - -### 25. Memphis Pop Pattern -- Mood: energetic, geometric, anti-minimalist. -- Best for: fashion, lifestyle, retail, youth marketing. -- Palette: warm off-white `#FFF5E0`, red `#E8344A`, blue `#1E90FF`, mint `#22BB88`, yellow `#FFD700`. -- Typography: Bebas Neue or Futura ExtraBold titles, 32-44pt; Futura or DM Sans body. -- Layout: scattered triangles, circles, dots, zigzag bar, asymmetric balance. -- Signature: all key geometric motifs present on warm background. -- Avoid: minimalism, monochrome palettes, overly clean fonts. - -### 26. Dark Forest Nature -- Mood: mysterious, atmospheric, eco-premium. -- Best for: environmental brands, adventure, sustainable luxury. -- Palette: forest gradient `#0D2B14` to `#060E08`, tree greens `#0A3D1A` and `#0D4D20`, moon sage `#E8F4D0` to `#B8CC80`, stars `#D4F0B0`. -- Typography: Playfair Display Italic or DM Serif Display Italic titles, 20-28pt; EB Garamond body; Space Mono captions. -- Layout: layered tree silhouettes, glowing moon top-right, sparse star dots, bottom mist overlay. -- Signature: three-depth forest silhouette plus moon. -- Avoid: bright greens, hard tree edges, sans-serif fonts. - -### 27. Architectural Blueprint -- Mood: precise, technical, professional. -- Best for: architecture, planning, engineering, spatial design. -- Palette: blueprint navy `#0D2240`, grid cyan-white `#64B4FF`, line cyan `#64C8FF`, title `#96DCFF`. -- Typography: Space Mono only, 8-13pt. -- Layout: fine and major grid, dimension lines, annotation marks, circular stamp, bottom title label. -- Signature: dual grid and dimension annotations. -- Avoid: decorative color, non-monospace fonts, photography. - -### 28. Maximalist Collage -- Mood: chaotic, irreverent, advertising-bold. -- Best for: advertising, fashion, music, editorial. -- Palette: antique cream `#E8DDD0`, red `#E83030`, near-black `#1A1A1A`, acid yellow `#F5D020`, white text. -- Typography: Bebas Neue words, 24-34pt; Playfair Display Italic secondary text; large ghost numbers; Space Mono captions. -- Layout: overlapping rotated color blocks, diagonal background pattern, ghost number, circle outline accent. -- Signature: three or more rotated blocks with vertical text in one block. -- Avoid: symmetry, clean uncluttered layouts, muted backgrounds. - -### 29. SciFi Holographic Data -- Mood: military HUD, quantum, precision. -- Best for: defense tech, AI research, quantum, advanced data engineering. -- Palette: space black `#03050D`, cyan `#00C8FF` at varied opacities. -- Typography: Space Mono only, 8-11pt. -- Layout: three concentric rings, one rotated ring, horizontal scan line, glowing horizontal bars, top-left and bottom-right labels. -- Signature: monochrome cyan HUD rings and scan line. -- Avoid: multiple hues, warm colors, decorative illustration. - -### 30. Risograph Print -- Mood: indie, analog, artisanal print. -- Best for: publishers, music labels, art zines, boutique studios. -- Palette: aged paper `#F7F2E8`, riso red `#E8344A`, riso blue `#0D5C9E`, riso yellow `#F5D020`. -- Typography: Bebas Neue main title, 34-44pt; Space Mono caption. -- Layout: three overlapping circles in the center third, offset ghost title, bottom monospace caption. -- Signature: CMYK-like overlaps and registration-offset title. -- Avoid: dark backgrounds, overly crisp digital treatment, screen-style blending. - ---- - -## Translation guidance for pptify layout trees - -- Cards: represent as `shape` rectangles with explicit fill opacity, border color, radius, and z-order. -- Rules and gridlines: use explicit line objects with consistent stroke width and opacity. -- Background gradients or mesh effects: use raster background only when vector approximation would be misleading; otherwise use large layered translucent shapes. -- Blend modes: approximate with semi-transparent overlapping fills; keep editability where possible. -- Outline text or gradient text: use a nearby editable fallback plus a documented visual approximation; do not depend on unsupported PowerPoint effects. -- Organic blobs, trees, mosaic cells, and Memphis shapes: author as editable primitive shapes when possible. -- Complex previews: keep the deck build source-backed by the style description, not by copied preview images. - ---- - -## Best for - -- Quickly selecting a predefined visual direction for a new deck. -- Matching a user request for a modern, trendy, stylish, or visually striking PPTX. -- Teaching agents a palette, typography, and signature-element vocabulary before authoring coordinates. -- Generating diverse style directions for user choice before committing to a full deck. diff --git a/plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md b/plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md deleted file mode 100644 index 1d7f7b546..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/erickittelson-slidemason.md +++ /dev/null @@ -1,92 +0,0 @@ -# erickittelson/slidemason — Design Context - -**Source repo:** erickittelson/slidemason -**Style reference:** JSX primitive composition for bespoke slide layouts -**Retrieved:** 2026-05-19 - ---- - -## What this repo teaches - -`slidemason` demonstrates JSX-based slide composition using a primitive component library (shapes, text, images, connectors). The approach allows fully bespoke layouts by combining primitives programmatically. The key cautionary lesson is that JSX flexibility breaks down at the PPTX editability boundary: layouts that are easy to express in JSX are often impossible to round-trip as fully editable native PPTX. - ---- - -## Core approach - -```jsx - - - - Insight title - Detail text here. - - -``` - -Coordinates are in inches, absolute-positioned, matching the pptify coordinate system. Primitives map directly to pptify builder functions. - ---- - -## JSX primitives → pptify equivalents - -| Slidemason JSX | pptify function | -|---|---| -| `` | `_background(fill)` + `_tree(...)` | -| `` | `_shape(..., shape="round_rect")` | -| `` | `_text(...)` | -| `` | `_line(...)` | -| `` | `_image(...)` | -| `` | `_shape(..., shape="oval")` | -| `` | `_shape(..., shape="hexagon")` | - ---- - -## Key patterns - -### JSX bento (`jsx-bento`, `slidemason-bento`) -A bento-grid card layout composed from `` primitives arranged in a CSS-like grid. Each card carries a tag, title, and detail text. In current pptify, agents translate this idea into explicit `layout_tree` card shapes and text boxes. - -**Caution:** Slidemason bento uses flexbox-inspired auto-sizing that does not survive PPTX export. In pptify, all bento card positions must be hardcoded to absolute inch coordinates. - -### Bespoke slide (`bespoke-slide`, `custom-jsx-slide`) -For one-off layouts not covered by the standard catalogue, compose primitives directly. Useful for complex infographic or diagram slides. - -**Caution:** Bespoke JSX slides are the hardest to keep editable. Every text frame must be individually positioned and sized; do not rely on auto-layout. - -### Primitive composition (`jsx-primitives`, `primitive-composition`) -The primitive library provides the building blocks. The composition layer arranges them. Separating these concerns means the same primitives can be recombined into new patterns without rebuilding the rendering infrastructure. - ---- - -## Editability failure modes - -Slidemason layouts commonly fail PPTX editability when: - -| Failure mode | Why it happens | -|---|---| -| Nested flex containers | PPTX has no flex layout; nested containers collapse | -| Auto-sized text frames | PPTX text frames need explicit height in inches | -| SVG filter effects | Drop shadows, blurs, and filters cannot be exported as native shapes | -| Rotated text boxes | Rotated text frames lose editability in most PPTX viewers | -| Z-index stacking of text | Text below z-index 10 is not selectable in PowerPoint | -| Image fills on shapes | Image-filled shape backgrounds cannot be reflowed as editable text | - ---- - -## Agent-level lessons for pptify - -1. **JSX flexibility is a design-time tool only.** At export time, every element must resolve to an absolute-positioned, explicitly-sized native PPTX object. -2. **Auto-layout is the enemy of editability.** In pptify specs, never use relative sizing (percentages, `auto`, `fr` units). All coordinates are in inches. -3. **The primitive catalogue should be closed at generation time.** The agent should choose from known editable primitives and validate every object before rendering. -4. **Bespoke is expected but must be explicit.** A bespoke layout requires manual quality-gate verification and coordinate-explicit objects. -5. **Text z_index ≥ 20.** In pptify, all text objects use `z_index=20` by default to ensure they render above decorative shapes. - ---- - -## Best for - -- Understanding the limits of programmatic slide composition -- Designing new pptify slide forms by sketching primitives before formalising geometry -- Cautionary reference for why auto-layout and PPTX editability are incompatible -- Component-level thinking about slide layout and primitive composition diff --git a/plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md b/plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md deleted file mode 100644 index f826169a0..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/fluent-ui-design-tokens.md +++ /dev/null @@ -1,80 +0,0 @@ -# Fluent UI Design Token Context - -Source-backed predefined design-system context. - -- Source repository: https://github.com/microsoft/fluentui -- Source docs: https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md -- Source agent instructions: https://github.com/microsoft/fluentui/blob/master/AGENTS.md -- License: MIT; see [third-party-notices.md](../third-party-notices.md) -- Retrieved: 2026-05-18 - -## Selected Source Excerpts - -From `docs/architecture/design-tokens.md`: - -```md -# Design Tokens - -## Rule - -**Always use design tokens** from `@fluentui/react-theme` instead of hardcoded values. -Hardcoded values break theming, high contrast mode, and dark mode. -``` - -```md -## Token Categories - -| Category | Example tokens | Use for | -| ------------- | ------------------------------------------------------------- | -------------------- | -| Color | `tokens.colorNeutralForeground1`, `tokens.colorBrandBackground` | All colors | -| Spacing | `tokens.spacingVerticalM`, `tokens.spacingHorizontalL` | Padding, margin, gap | -| Border radius | `tokens.borderRadiusMedium`, `tokens.borderRadiusLarge` | Border radius | -| Font | `tokens.fontSizeBase300`, `tokens.fontWeightSemibold` | Typography | -| Line height | `tokens.lineHeightBase300` | Line height | -| Stroke | `tokens.strokeWidthThin`, `tokens.strokeWidthThick` | Border width | -| Shadow | `tokens.shadow4`, `tokens.shadow16` | Box shadow | -| Duration | `tokens.durationNormal`, `tokens.durationFast` | Animations | -| Easing | `tokens.curveEasyEase` | Animation timing | -``` - -```md -## Available Themes - -- `webLightTheme` - Default light -- `webDarkTheme` - Default dark -- `teamsLightTheme` / `teamsDarkTheme` / `teamsHighContrastTheme` - Teams variants -``` - -From `AGENTS.md`: - -```md -## Critical Rules (never violate) - -1. **Never hardcode colors, spacing, or typography values.** Always use design tokens from `@fluentui/react-theme`. See [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md). -``` - -```tsx -// Styles - always use tokens, never hardcoded values -import { makeStyles } from '@griffel/react'; -import { tokens } from '@fluentui/react-theme'; - -export const useComponentNameStyles = makeStyles({ - root: { - color: tokens.colorNeutralForeground1, - padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, - }, -}); -``` - -## Source Signals for LLM Context - -- Token categories from source: color, spacing, border radius, font, line height, stroke, shadow, duration, easing. -- Theme variants from source: `webLightTheme`, `webDarkTheme`, `teamsLightTheme`, `teamsDarkTheme`, `teamsHighContrastTheme`. -- Agent behavior from source: avoid hardcoded styling when the target design system exposes tokens. - -## PPTify Translation Guardrails - -- Use this context for Microsoft, Teams, M365, Power Platform, and enterprise product decks. -- `pptify` JSON uses concrete values, so include Fluent token names in `summary.design_context` when translating to theme values. -- Prefer restrained, utilitarian, accessibility-aware slide structure for operational and enterprise decks. -- Do not copy Fluent fonts or icons as assets from this context pack; use separately licensed assets only when explicitly sourced. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md b/plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md deleted file mode 100644 index 1032d30a9..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/gabberflast-academic-pptx-skill.md +++ /dev/null @@ -1,110 +0,0 @@ -# Gabberflast/academic-pptx-skill — Design Context - -**Source repo:** Gabberflast/academic-pptx-skill -**Style reference:** Narrative discipline gates for evidence-based presentations -**Retrieved:** 2026-05-19 - ---- - -## What this repo teaches - -`academic-pptx-skill` adds a layer of *narrative gates* to deck generation: structural checks that prevent common storytelling failures before a slide ever reaches the audience. The three core gates are the action title discipline, the ghost deck test, and the one-exhibit discipline. These are content-quality checks, not visual-quality checks — they enforce that the deck tells a clear, evidence-grounded story. - ---- - -## Narrative gate 1 — Action titles (`action-title`) - -Every content slide must have an action title: a headline that states the conclusion, not the topic. - -### Test -For each slide title, ask: *"If someone reads only the title, do they know what to think or do?"* - -| Fails the test | Passes the test | -|---|---| -| "Q2 Sales Data" | "Q2 sales missed target by 18% due to APAC pipeline weakness" | -| "Risk Assessment" | "Three risks require immediate board escalation" | -| "Architecture Overview" | "Zero-trust architecture closes the 62% identity gap" | -| "Team Update" | "Engineering is on track; delivery confidence is High for Q3" | - -**Agent rule:** Reject any spec where a content slide title is purely descriptive. Rewrite it as an action title before proceeding. - ---- - -## Narrative gate 2 — Ghost deck test (`ghost-deck-test`) - -The ghost deck test: read only the slide titles in sequence. The titles alone should tell the complete story. - -### How to run it -1. Extract all slide titles from the deck spec -2. Read them in order, without any body content -3. Ask: *"Does this sequence make a coherent, complete argument?"* -4. If the answer is no, the outline is wrong — fix it before building slides - -### Common failures -- Title sequence has no logical progression (problem → evidence → recommendation) -- Two consecutive titles make the same point -- A title sequence assumes knowledge that hasn't been established earlier in the deck -- The final slide title does not name a specific next action or decision - ---- - -## Narrative gate 3 — One-exhibit discipline (`one-exhibit-discipline`) - -Each content slide may carry at most one primary data exhibit (chart, table, diagram, or image). Supporting text is allowed, but a second data exhibit is not. - -### Why this matters -Two exhibits on one slide force the audience to choose which to look at first. This splits attention and dilutes the slide's single point. If you have two exhibits, you have two slides. - -### Exceptions -- Dashboard-style overview slides are explicitly designed for multi-exhibit status summaries; use sparingly. -- Comparison-style two-column slides are acceptable when both panels serve the same comparative point. This is one exhibit with two panels, not two separate exhibits. - ---- - -## Evidence discipline (`evidence-slide`, `citation-slide`) - -For decks grounded in research or data: -- Every quantitative claim on a slide must have a source annotation -- Sources go in the slide notes or a dedicated appendix slide, not in body text -- Distinguish between primary data (your own measurements) and secondary data (cited sources) -- Flag any claim marked "estimated" or "approximate" with a visible qualifier - ---- - -## Ghost deck template - -Use this structure as the default ghost deck for a 12-slide consulting or governance deck: - -| # | Role | Action title example | -|---|---|---| -| 1 | Cover | — | -| 2 | TOC | — | -| 3 | Context | "The current operating model creates three material gaps" | -| 4 | Evidence 1 | "Gap 1: Identity controls cover only 62% of workloads" | -| 5 | Evidence 2 | "Gap 2: Patch latency averages 42 days against a 14-day SLA" | -| 6 | Evidence 3 | "Gap 3: DLP policy does not cover 28% of sensitive data stores" | -| 7 | Framework | "A zero-trust operating model closes all three gaps" | -| 8 | Recommendation | "Three initiatives deliver full coverage by Q4" | -| 9 | Roadmap | "Q1–Q2 identity hardening, Q3 DLP expansion, Q4 network segmentation" | -| 10 | Risk | "Two risks require active management: vendor dependency and staff capacity" | -| 11 | Investment | "Total investment is £2.4M; breakeven is 14 months" | -| 12 | Next steps | "Board to approve budget by 30 June; CISO to brief team by 7 July" | - ---- - -## Agent-level lessons for pptify - -1. **Run the ghost deck test before building any slides.** Extract titles from the outline JSON, read them in sequence, verify they tell a complete story. -2. **Rewrite descriptive titles as action titles.** This is the highest-leverage edit in any deck review. -3. **One exhibit per slide is a hard rule.** If you find yourself combining a bar chart and a table on the same slide, split them. -4. **The last slide must have a named next action.** "Thank you" is not an action. The closing slide should name a decision, deadline, and owner. -5. **Source every quantitative claim.** Use slide notes for source citations; keep the slide body clean. - ---- - -## Best for - -- High-stakes governance or board presentations requiring narrative rigour -- Decks that will be reviewed by a critical audience (investors, regulators, boards) -- Training agents to apply consulting-grade storytelling discipline -- Post-generation review gates before delivering a deck diff --git a/plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md b/plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md deleted file mode 100644 index d947a96d3..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/likaku-mck-ppt-design-skill.md +++ /dev/null @@ -1,91 +0,0 @@ -# likaku/Mck-ppt-design-skill - Design Context - -**Source repo:** likaku/Mck-ppt-design-skill -**Style reference:** McKinsey-style consulting deck layout taxonomy -**Retrieved:** 2026-05-19 - ---- - -## What this repo teaches - -`likaku/Mck-ppt-design-skill` is a public example of disciplined consulting-slide taxonomy: action titles, strict grids, repeated exhibit forms, and predictable evidence structures. Current pptify does not import those fixed layouts. Agents use this source as design inspiration, then author the exact `layout_tree` coordinates, sizes, objects, and z-order directly. - ---- - -## Core design principle for current pptify - -> The agent owns both the slide form and the coordinates. - -This means: - -- Layout geometry must be explicit before render time. -- Visual consistency comes from agent-authored coordinate discipline, not a runtime pattern engine. -- Each slide should still follow a recognizable consulting form so the deck feels deliberate. - ---- - -## Slide Form Families - -### Structure and navigation - -Use these ideas for covers, section dividers, agendas, appendices, and closings. Author them as explicit title text boxes, divider rules, section labels, and footer objects. - -### Data and metrics - -Use metric strips, KPI cards, data tables, RAG status rows, bar/line/donut-style exhibits, or dashboard-style overviews only when the source evidence contains quantitative data. Render them as explicit editable tables, labels, shapes, lines, and image-backed exhibits when exact chart fidelity is required. - -### Frameworks and matrices - -Use decision matrices, maturity ladders, process rails, timelines, cycles, funnels, and architecture maps as coordinate-explicit compositions. Every lane, node, connector, axis label, and callout needs its own bbox or line endpoints. - -### Content and narrative - -Use comparison, executive summary, quote, image exhibit, decision tree, icon grid, and team-style forms when they support the slide's single message. Keep the exhibit count low and make the action title carry the conclusion. - ---- - -## Action Title Discipline - -McKinsey decks use action titles: slide titles that state the conclusion, not the topic. - -| Descriptive | Action title | -|---|---| -| "Revenue by Region" | "APAC revenue grew 34% driven by cloud workloads" | -| "Risk Matrix" | "Three critical risks require board attention in Q3" | -| "Team" | "Engineering capacity is insufficient for H2 commitments" | - -**Agent rule:** Every content slide title must be an action title. Only cover, divider, agenda, and closing slides may use descriptive titles. - ---- - -## Consulting Layout Geometry Norms - -Use these as starting coordinates when authoring `layout_tree` objects for a 13.333 by 7.5 inch slide: - -| Element | Position | -|---|---| -| Kicker or eyebrow | y = 0.48, font 8.5pt | -| Slide title | y = 0.72, h = 0.36, font 22pt bold | -| Title rule | y = 1.12, h = 0.04 | -| Content area top | y >= 1.30 | -| Takeaway band top | y >= 5.50 | -| Page number | top-right, font 9pt muted | - ---- - -## Agent-level Lessons for pptify - -1. **Action titles on every content slide.** This is the highest-leverage improvement over generic deck generation. -2. **Choose a slide form, then write coordinates.** The taxonomy guides composition; the final output is still explicit `layout_tree` JSON. -3. **RAG status and checklist slides are tables.** For status dashboards, use editable table cells with explicit fill colors. -4. **Harvey balls and progress indicators are primitives.** Use donut-like arcs only as explicit shapes or image assets; simple filled circles often communicate more reliably. -5. **One data exhibit per slide.** Never combine two data charts on one slide unless the slide is intentionally a dashboard-style overview. - ---- - -## Best for - -- Consulting-style decks for strategy, governance, or operations reviews -- Decks requiring strict action-title discipline -- Workflows where reusable slide-form ideas are translated into explicit coordinates -- Teaching agents the taxonomy of PPTX slide forms diff --git a/plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md b/plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md deleted file mode 100644 index 27b15a155..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/nexu-io-open-design.md +++ /dev/null @@ -1,71 +0,0 @@ -# nexu-io/open-design — Design Context - -**Source repo:** nexu-io/open-design -**Style reference:** Claude Design (Anthropic artifact conventions) -**Retrieved:** 2026-05-19 - ---- - -## What this repo teaches - -`nexu-io/open-design` is the closest public approximation to the "Claude Design" artifact pattern. It structures design work through agent skill files, a `DESIGN.md` contract, direction-picker routines, sandboxed preview loops, and artifact lint gates. The central idea is that design decisions are staged through explicit handoffs rather than collapsed into one prompt. - ---- - -## Key patterns - -### Direction picker (`design-direction-picker`, `direction-picker`) -Before committing to a visual language, present 2–4 parallel directional options as lightweight thumbnails or descriptive specs. Each option has a name, palette, typographic posture, and layout personality. The user (or an automated gate) selects one, which becomes the `style_lock` for the rest of the deck. - -**Agent rule:** Never start layout without a locked direction. Parallel directions should differ in at least two of: palette darkness, type scale, information density, accent geometry. - -### Artifact lint (`artifact-lint`, `visual-critique`) -After every render, run a structured lint pass before delivering the artifact: -- No hardcoded hex values that escape the locked design token set -- No placeholder text surviving into final output (`Lorem ipsum`, `TBD`, `TODO`) -- No font-size below 8pt in slides -- No content bounding box that overflows the slide canvas -- Action titles present on every content slide (not descriptive titles) - -### Sandbox preview (`sandbox-preview`) -Render a minimal single-slide preview of the proposed design direction before building the full deck. Use it as a visual contract to confirm color, type, and layout feel. Only proceed to full generation after preview approval. - -### Design critique gate (`design-critique`) -At the end of a deck generation run, score the output against the original brief: -- Does every slide carry exactly one decision or takeaway? -- Is the visual hierarchy consistent across slides? -- Are all data exhibits editable (not screenshots)? -- Is the action title parallel in grammatical structure across slides? - ---- - -## Design token conventions (from DESIGN.md) - -| Token | Role | -|---|---| -| `--color-bg` | Slide background | -| `--color-surface` | Card and panel fill | -| `--color-accent` | Primary interactive / emphasis color | -| `--color-text` | Body text | -| `--color-muted` | Kicker, caption, secondary text | -| `--font-display` | Title typeface | -| `--font-body` | Body copy typeface | -| `--radius-card` | Corner radius for cards and panels | - ---- - -## Agent-level lessons for pptify - -1. **Direction picker before deck plan.** Present explicit visual direction options before full slide authoring; only after user selection, proceed to the full coordinate plan. -2. **Style lock is a first-class artefact.** The `style_lock` JSON block (with `background`, `primary`, `secondary`, `tertiary`, `font`) should be emitted and confirmed before any `DeckBuilder.build()` call. -3. **Artifact lint is mandatory.** Run pptify render and audit checks after every build; treat collisions, overflows, or unexpected warnings as blocking failures. -4. **Preview before full deck.** Generate a 2–3 slide cover+section preview to confirm visual direction before rendering all slides. - ---- - -## Best for - -- Reasoning about deck design direction before committing to a palette -- Structuring parallel design options for human or automated selection -- Running structured lint and critique gates on generated decks -- Claude-native artifact workflows with explicit handoffs diff --git a/plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md b/plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md deleted file mode 100644 index 8a8d71c50..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/pptwork-oh-my-slides.md +++ /dev/null @@ -1,102 +0,0 @@ -# PPTWork / oh-my-slides — Design Context - -**Source repo:** PPTWork / oh-my-slides -**Style reference:** HTML-as-source, PPTX-as-build-artifact pipeline -**Retrieved:** 2026-05-19 - ---- - -## What this repo teaches - -`oh-my-slides` treats HTML as the canonical design source and PPTX as a downstream build artifact. The central insight is that HTML rendering gives you pixel-accurate layout control and visual fidelity checking before committing to the constraints of the PPTX format. The raster export (PNG/PDF) is the fidelity reference; the editable PPTX export is produced only under strict constraints that guarantee editability. - ---- - -## Core model - -``` -Source (markdown / JSON / data) - ↓ -HTML render ←── design source of truth - ↓ ↓ -Raster export Constrained editable PPTX -(PNG / PDF) (pptify explicit primitives) -(for fidelity) (for PowerPoint editability) -``` - -The two exports serve different needs: -- **Raster export:** pixel-perfect fidelity, safe for sharing as PDF, but not editable -- **Editable PPTX:** every text frame independently editable, every shape native, no raster embeds - -**Agent rule:** Never promise both pixel fidelity and full editability from the same export path. Choose one or declare a constrained hybrid. - ---- - -## Key patterns - -### HTML source with preset picker (`html-source`, `preset-picker`) -The HTML template is chosen from a catalogue of pre-approved presets. Each preset defines: -- Slide dimensions (always 1280 × 720px for 16:9) -- Background color and surface card color -- Typography scale (h1, h2, h3, body, caption) -- Accent geometry (border style, icon style, card corner radius) - -Agents select a preset, not a freeform CSS specification. This mirrors the pptify `style_lock` concept. - -### Mini preview (`mini-preview`) -Before full render, generate a single-slide HTML preview: -- Use the real preset -- Populate with real content (no lorem ipsum) -- Check bounding boxes: no text overflow, no element outside slide bounds -- Confirm color contrast passes WCAG AA - -Only after preview passes does the full deck render proceed. - -### Raster export for fidelity (`raster-export`, `pptx-build-artifact`) -The HTML slides are rendered to PNG at 2× resolution for fidelity: -- Each PNG is 2560 × 1440px -- Verified against the preset's expected layout grid -- Used as the reference for visual QA - -### Constrained editable PPTX (`constrained-editable`, `editable-export`) -When an editable PPTX is required: -- Map the HTML layout to pptify's coordinate system -- Replace any CSS-positioned elements with absolute-coordinate native shapes -- Replace styled HTML text with PPTX text frames -- Verify with pptify audit checks; zero collisions, overflows, and unexpected warnings required -- Raster screenshots of HTML are never inserted as images in the PPTX - ---- - -## Constraints for editable PPTX export - -When converting HTML → editable PPTX: - -| HTML construct | PPTX equivalent | -|---|---| -| `
` with background-color | `_shape()` with fill color | -| `

`, `

`–`

` | `_text()` with matching font size | -| `
` | `_table()` | -| `
` or border-bottom | `_line()` or thin `_shape()` | -| Background image | Not permitted in editable export | -| SVG icon | `_shape()` geometric approximation | -| CSS `transform: rotate()` | Not permitted — use supported shape types only | - ---- - -## Agent-level lessons for pptify - -1. **HTML is a design tool, not a delivery format.** Use it for visual exploration and QA, then re-express as native PPTX shapes. -2. **Preset picker mirrors style_lock.** The HTML preset and the pptify `style_lock` should be derived from the same brand token source. -3. **Mini preview before full deck.** Generate a 1–2 slide preview render before committing to the full spec. -4. **Raster embeds are forbidden in editable output.** Any PPTX delivered with image embeds of slide content is a quality failure. -5. **Both export paths need their own quality gate.** Raster export: visual diff check. Editable PPTX: pptify audit and package checks with zero issues. - ---- - -## Best for - -- Workflows that need a visual HTML prototype before PPTX delivery -- Decks where design fidelity (raster) and PowerPoint editability are separate deliverables -- Agents that use Playwright or browser preview as part of the generation loop -- Teaching the constraint model of HTML → PPTX conversion diff --git a/plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md b/plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md deleted file mode 100644 index e917f3503..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/primer-primitives.md +++ /dev/null @@ -1,137 +0,0 @@ -# Primer Primitives Context - -Source-backed predefined design-system context. - -- Source repository: https://github.com/primer/primitives -- Source docs: https://github.com/primer/primitives/blob/main/DESIGN_TOKENS_GUIDE.md -- Source tokens: `src/tokens/base/color/light/light.json5`, `src/tokens/functional/spacing/space.json5`, `src/tokens/functional/typography/typography.json5` -- License: MIT; see [third-party-notices.md](../third-party-notices.md) -- Retrieved: 2026-05-18 - -## Selected Source Excerpts - -From `README.md`: - -```md -This repo contains values for color, spacing, and typography primitives for use with Primer, GitHub's design system. - -Data is served from the `dist/` folder: - -- `dist/css` contains CSS files with values available as CSS variables -``` - -From `DESIGN_TOKENS_GUIDE.md`: - -```md -## Core Rule - -**You are a CSS expert. Never use raw values (hex, px, etc.). Only use semantic tokens.** -``` - -```md -### Typography - -| Keyword | Rule | -| ---------- | ---- | -| **MUST** | Use **shorthand** tokens (e.g., `font: var(...)`) to ensure `line-height` and `font-weight` are synchronized. | -| **SHOULD** | Match the token to the **semantic role** (e.g., use `title` tokens for headers, not just a large `body` token). | -``` - -From `src/tokens/functional/spacing/space.json5`: - -```json5 -{ - space: { - sm: { - $value: '{base.size.8}', - $type: 'dimension', - $description: 'Default spacing for most UI elements. Comfortable visual density for standard component layouts.', - $extensions: { - 'org.primer.llm': { - usage: ['gap', 'padding', 'margin', 'flex-gap', 'grid-gap', 'card-padding'], - rules: 'Default (8px). Use for standard component spacing, flex/grid item separation, container padding, and most element margins.', - }, - }, - }, - lg: { - $value: '{base.size.16}', - $type: 'dimension', - $description: 'Spacious spacing for major layout divisions and visual separation between content blocks.', - }, - }, -} -``` - -From `src/tokens/functional/typography/typography.json5`: - -```json5 -{ - text: { - title: { - shorthand: { - medium: { - $description: 'Default page title. The 32px-equivalent line-height matches with button and other medium control heights. Great for page header composition.', - $extensions: { - 'org.primer.llm': { - usage: ['section-heading', 'card-title', 'dialog-title', 'h2'], - rules: 'RECOMMENDED default for page titles. Use for section headings and dialog titles.', - }, - }, - }, - }, - }, - body: { - shorthand: { - medium: { - $description: 'Default UI font. Most commonly used for body text.', - $extensions: { - 'org.primer.llm': { - usage: ['body-text', 'ui-text', 'form-label', 'button-text', 'navigation'], - rules: 'RECOMMENDED default for UI text. Use for buttons, labels, and general interface text.', - }, - }, - }, - }, - }, - }, -} -``` - -From `src/tokens/base/color/light/light.json5`: - -```json5 -{ - base: { - color: { - black: { $value: { hex: '#1f2328' }, $type: 'color' }, - white: { $value: { hex: '#ffffff' }, $type: 'color' }, - neutral: { - '1': { $value: { hex: '#F6F8FA' }, $type: 'color' }, - '12': { $value: { hex: '#25292E' }, $type: 'color' }, - }, - blue: { - '5': { $value: { hex: '#0969da' }, $type: 'color' }, - }, - green: { - '5': { $value: { hex: '#1a7f37' }, $type: 'color' }, - }, - red: { - '5': { $value: { hex: '#cf222e' }, $type: 'color' }, - }, - }, - }, -} -``` - -## Source Signals for LLM Context - -- Token discipline: choose semantic roles for typography and spacing instead of arbitrary size jumps. -- Spacing signals from source: `xxs`, `xs`, `sm`, `md`, `lg`, `xl`, with `sm` as 8px and `lg` as 16px in the source token scale. -- Typography signals from source: `display`, `title`, `subtitle`, `body`, `caption`, `codeBlock`, `codeInline` roles. -- Color signals from source: neutral GitHub-like surfaces and a clear blue accent around `#0969da`. - -## PPTify Translation Guardrails - -- Use this context for developer-facing, GitHub-style, or product/engineering decks. -- `pptify` JSON needs concrete theme values; when converting Primer tokens to concrete colors, keep source token names in the deck `summary.design_context` so the LLM-grounding remains visible. -- Prefer cards, tables, and compact sections with consistent spacing over decorative flourishes. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md b/plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md deleted file mode 100644 index 0e5ed06e6..000000000 --- a/plugins/pptify/.agent/pptify-design/contexts/sunbigfly-ppt-agent-skills.md +++ /dev/null @@ -1,104 +0,0 @@ -# sunbigfly/ppt-agent-skills — Design Context - -**Source repo:** sunbigfly/ppt-agent-skills -**Style reference:** Staged deck-generation pipeline (interview → plan → build → QA → export) -**Retrieved:** 2026-05-19 - ---- - -## What this repo teaches - -`sunbigfly/ppt-agent-skills` provides the most rigorous staged generation pipeline of any public PPTX skill repo. It enforces explicit handoffs between six phases: audience interview, source compression, outline, style lock, per-slide planning, and dual export (raster + editable). Each phase is a separate agent turn with a defined input, output schema, and gate condition before the next phase begins. - ---- - -## Pipeline stages - -### Stage 1 — Audience interview (`interview`) -Before any content work, collect: -- **Audience:** role, seniority, prior context -- **Goal:** decision to be made or information to be conveyed -- **Tone:** formal/informal, data-heavy/narrative -- **Constraints:** slide count, time limit, branding requirements - -Output: structured brief JSON that gates entry to stage 2. - -### Stage 2 — Source compression (`research-outline`, `source-compression`) -Ingest source materials (documents, URLs, data files). For each source: -1. Summarise key claims and evidence -2. Tag each claim with its source and confidence level -3. Identify the 3–5 most decision-relevant signals -4. Discard anything that doesn't serve the stated audience goal - -Output: compressed source summary, ≤ 800 words, structured by decision relevance. - -### Stage 3 — Outline (`outline`) -From the compressed source and the audience brief, build a slide-by-slide outline: -- Slide number, slide form, action title, one-sentence content description -- Explicit "so-what" statement per slide (what decision or belief should change?) -- Flag slides that depend on data that needs verification - -Output: outline JSON. Agent must not proceed to stage 4 without explicit outline approval. - -### Stage 4 — Style lock (`style-lock`) -Choose a visual direction and emit a locked `style_lock` block: -```json -{ - "background": "#F7FAFC", - "surface": "#FFFFFF", - "dark": "#17233A", - "primary": "#0F6CBD", - "secondary": "#009C9C", - "tertiary": "#6B5DD3", - "font": "Segoe UI" -} -``` -The style lock is immutable for the rest of the pipeline. No per-slide overrides. - -### Stage 5 — Per-slide planning (`slide-plan`) -For each slide in the outline, emit a full slide spec with: -- `layout_tree`: complete pptify layout tree with explicit coordinates -- `title`, `subtitle`, `kicker`, `takeaway` represented as editable text objects -- items, rows, metrics, and exhibits represented as explicit editable primitives -- All content pre-populated from the compressed source (no placeholders) - -Output: complete `spec.json` ready to pass to `DeckBuilder.build()`. - -### Stage 6 — Dual export + visual QA (`visual-qa`, `dual-export`, `export-check`, `qa-report`) -After render: -1. Run pptify render and audit checks; zero collisions, overflows, or unexpected warnings allowed -2. Check collision and overflow counts in audit — must both be zero -3. Verify slide count matches outline -4. Confirm every slide has an action title (not a descriptive label) -5. If any check fails, return to stage 5 and patch the affected slides - ---- - -## Agent-level lessons for pptify - -1. **Never skip the interview.** Decks built without a structured brief produce the wrong content for the wrong audience. -2. **Source compression before outline.** Raw source material is too noisy for direct slide mapping. Compress first; then outline. -3. **Style lock is stage-gated.** Emit style_lock once, early. Treat any mid-deck color change as a pipeline failure. -4. **Per-slide plans are full specs.** The slide plan stage should produce a `spec.json` that requires zero editing before `DeckBuilder.build()`. -5. **Dual export is the quality gate.** A deck is not done until both the pptify quality gate and a visual review have passed. -6. **Action titles are mandatory.** Every content slide title must answer "so what?" not just label the content. - ---- - -## Slide count guidelines (from pipeline experience) - -| Audience | Recommended slide count | Max density | -|---|---|---| -| Board / C-suite | 8–12 | 1 decision per slide | -| Senior leadership | 12–18 | 1 insight per slide | -| Working-level review | 18–30 | 1 data exhibit per slide | -| Technical deep-dive | No limit | 1 concept per slide | - ---- - -## Best for - -- Decks that must be grounded in specific source documents or data -- High-stakes presentations where source provenance matters -- Workflows requiring explicit human approval at each phase -- Quality-controlled delivery pipelines for enterprise clients diff --git a/plugins/pptify/.agent/pptify-design/sources.json b/plugins/pptify/.agent/pptify-design/sources.json deleted file mode 100644 index b4fc15de0..000000000 --- a/plugins/pptify/.agent/pptify-design/sources.json +++ /dev/null @@ -1,258 +0,0 @@ -{ - "schema": "pptify-design.context-catalog.v1", - "updated": "2026-05-20", - "purpose": "Source-backed design template and agent-prompt context packs for LLM-assisted pptify deck authoring.", - "profiles": [ - { - "id": "primer-primitives", - "name": "Primer Primitives Design Tokens", - "kind": "design-system-context", - "license": { - "spdx": "MIT", - "notice": "Copyright (c) 2018 GitHub Inc." - }, - "source": { - "repo": "primer/primitives", - "url": "https://github.com/primer/primitives", - "license_url": "https://github.com/primer/primitives/blob/main/LICENSE", - "retrieved": "2026-05-18" - }, - "local_context": "contexts/primer-primitives.md", - "source_signals": { - "token_categories": ["color", "spacing", "typography", "motion", "z-index"], - "spacing_scale": ["xxs", "xs", "sm", "md", "lg", "xl"], - "typography_roles": ["display", "title", "subtitle", "body", "caption", "codeBlock", "codeInline"], - "color_examples": ["#ffffff", "#1f2328", "#F6F8FA", "#0969da", "#1a7f37", "#cf222e"] - }, - "best_for": ["GitHub-style decks", "developer products", "token-driven UI reviews", "engineering documentation"] - }, - { - "id": "fluent-ui-design-tokens", - "name": "Fluent UI Design Token Guidance", - "kind": "design-system-context", - "license": { - "spdx": "MIT", - "notice": "Copyright (c) Microsoft Corporation." - }, - "source": { - "repo": "microsoft/fluentui", - "url": "https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md", - "license_url": "https://github.com/microsoft/fluentui/blob/master/LICENSE", - "retrieved": "2026-05-18" - }, - "local_context": "contexts/fluent-ui-design-tokens.md", - "source_signals": { - "token_categories": ["color", "spacing", "border radius", "font", "line height", "stroke", "shadow", "duration", "easing"], - "theme_variants": ["webLightTheme", "webDarkTheme", "teamsLightTheme", "teamsDarkTheme", "teamsHighContrastTheme"], - "agent_rule": "Use design tokens instead of hardcoded colors, spacing, or typography values." - }, - "best_for": ["Microsoft-aligned decks", "Teams decks", "M365 decks", "Power Platform governance", "enterprise product reviews"] - }, - { - "id": "awesome-copilot-design-agents", - "name": "Awesome Copilot Design Agent and Prompt Context", - "kind": "agent-prompt-context", - "license": { - "spdx": "MIT", - "notice": "Copyright GitHub, Inc." - }, - "source": { - "repo": "github/awesome-copilot", - "url": "https://github.com/github/awesome-copilot", - "license_url": "https://github.com/github/awesome-copilot/blob/main/LICENSE", - "retrieved": "2026-05-18" - }, - "local_context": "agent-prompts/awesome-copilot-design-agents.md", - "source_signals": { - "agent_files": ["agents/gem-designer.agent.md", "agents/se-ux-ui-designer.agent.md"], - "skill_files": ["skills/penpot-uiux-design/SKILL.md", "skills/prompt-optimizer/SKILL.md"], - "prompt_focus": ["existing design systems", "visual hierarchy", "UX discovery", "accessibility", "slides and reports design intentionality"] - }, - "best_for": ["prompting an LLM to reason about deck design", "UX discovery before deck planning", "design review checklists", "visual hierarchy guidance"] - }, - { - "id": "nexu-io-open-design", - "name": "nexu-io/open-design — Claude Design Style", - "kind": "agent-skill-context", - "license": { - "spdx": "MIT", - "notice": "Copyright nexu-io contributors." - }, - "source": { - "repo": "nexu-io/open-design", - "url": "https://github.com/nexu-io/open-design", - "license_url": "https://github.com/nexu-io/open-design/blob/main/LICENSE", - "retrieved": "2026-05-19" - }, - "local_context": "contexts/nexu-io-open-design.md", - "source_signals": { - "key_patterns": ["direction-picker", "sandbox-preview", "artifact-lint", "design-critique"], - "style": "Claude Design artifact conventions", - "stage_gates": ["direction selection", "style lock", "preview approval", "artifact lint", "critique gate"], - "agent_rule": "Never start layout without a locked direction. Run artifact lint after every build. Preview before full deck." - }, - "best_for": ["reasoning about deck design direction before committing to a palette", "structuring parallel design options for selection", "running structured lint and critique gates on generated decks", "Claude-native artifact workflows with explicit handoffs"] - }, - { - "id": "alchaincyf-huashu-design", - "name": "alchaincyf/huashu-design — HTML-Native Brand Design Pipeline", - "kind": "agent-skill-context", - "license": { - "spdx": "MIT", - "notice": "Copyright alchaincyf contributors." - }, - "source": { - "repo": "alchaincyf/huashu-design", - "url": "https://github.com/alchaincyf/huashu-design", - "license_url": "https://github.com/alchaincyf/huashu-design/blob/main/LICENSE", - "retrieved": "2026-05-19" - }, - "local_context": "contexts/alchaincyf-huashu-design.md", - "source_signals": { - "key_patterns": ["brand-asset-protocol", "visual-directions", "html-to-editable-pptx", "playwright-check"], - "style": "HTML-native design with brand lock and parallel directions", - "brand_lock_fields": ["primary_palette", "neutral_palette", "typeface_display", "typeface_body", "tone"], - "agent_rule": "Brand lock is non-negotiable. Parallel directions before deck plan. Every text frame must be individually editable." - }, - "best_for": ["brand-constrained enterprise decks requiring exact color/type fidelity", "multi-direction style exploration before committing to a palette", "workflows where agents author complete coordinates from design mockups", "decks requiring visual verification"] - }, - { - "id": "sunbigfly-ppt-agent-skills", - "name": "sunbigfly/ppt-agent-skills — Staged Deck Generation Pipeline", - "kind": "agent-pipeline-context", - "license": { - "spdx": "MIT", - "notice": "Copyright sunbigfly contributors." - }, - "source": { - "repo": "sunbigfly/ppt-agent-skills", - "url": "https://github.com/sunbigfly/ppt-agent-skills", - "license_url": "https://github.com/sunbigfly/ppt-agent-skills/blob/main/LICENSE", - "retrieved": "2026-05-19" - }, - "local_context": "contexts/sunbigfly-ppt-agent-skills.md", - "source_signals": { - "pipeline_stages": ["interview", "source-compression", "outline", "style-lock", "slide-plan", "visual-qa", "dual-export"], - "stage_outputs": ["structured brief JSON", "compressed source ≤800 words", "outline JSON", "style_lock JSON", "complete spec.json", "qa-report", "pptx + raster"], - "agent_rule": "Never skip the interview. Source compression before outline. Style lock is stage-gated. Per-slide plans are full specs. Action titles are mandatory." - }, - "best_for": ["decks grounded in specific source documents or data", "high-stakes presentations where source provenance matters", "workflows requiring explicit human approval at each phase", "quality-controlled delivery pipelines for enterprise clients"] - }, - { - "id": "likaku-mck-ppt-design-skill", - "name": "likaku/Mck-ppt-design-skill — McKinsey-Style Native PPTX Layout Runtime", - "kind": "pptx-pattern-context", - "license": { - "spdx": "MIT", - "notice": "Copyright likaku contributors." - }, - "source": { - "repo": "likaku/Mck-ppt-design-skill", - "url": "https://github.com/likaku/Mck-ppt-design-skill", - "license_url": "https://github.com/likaku/Mck-ppt-design-skill/blob/main/LICENSE", - "retrieved": "2026-05-19" - }, - "local_context": "contexts/likaku-mck-ppt-design-skill.md", - "source_signals": { - "pattern_count": "~70 consulting-style layout patterns", - "pattern_families": ["structure-navigation", "data-metrics", "frameworks-matrices", "content-narrative"], - "action_title_discipline": true, - "geometry_norms": {"kicker_y": 0.48, "title_y": 0.72, "rule_y": 1.12, "content_top_y": 1.30}, - "agent_rule": "Use the source taxonomy as design inspiration only. The agent authors exact pptify layout_tree coordinates, sizes, and object primitives. Action titles on every content slide." - }, - "best_for": ["consulting-style decks for strategy, governance, or operations reviews", "decks requiring strict action-title discipline", "workflows where reusable layout ideas are translated into explicit coordinates", "teaching agents the taxonomy of PPTX slide forms"] - }, - { - "id": "corazzon-pptx-design-styles", - "name": "corazzon/pptx-design-styles - 30 Modern PPTX Style Templates", - "kind": "pptx-style-template-context", - "license": { - "spdx": "MIT", - "notice": "Copyright TodayCode / corazzon contributors." - }, - "source": { - "repo": "corazzon/pptx-design-styles", - "url": "https://github.com/corazzon/pptx-design-styles", - "license_url": "https://github.com/corazzon/pptx-design-styles/blob/main/README.md#license", - "retrieved": "2026-05-20" - }, - "local_context": "contexts/corazzon-pptx-design-styles.md", - "source_signals": { - "style_count": 30, - "style_names": ["Glassmorphism", "Neo-Brutalism", "Bento Grid", "Dark Academia", "Gradient Mesh", "Claymorphism", "Swiss International", "Aurora Neon Glow", "Retro Y2K", "Nordic Minimalism", "Typographic Bold", "Duotone Color Split", "Monochrome Minimal", "Cyberpunk Outline", "Editorial Magazine", "Pastel Soft UI", "Dark Neon Miami", "Hand-crafted Organic", "Isometric 3D Flat", "Vaporwave", "Art Deco Luxe", "Brutalist Newspaper", "Stained Glass Mosaic", "Liquid Blob Morphing", "Memphis Pop Pattern", "Dark Forest Nature", "Architectural Blueprint", "Maximalist Collage", "SciFi Holographic Data", "Risograph Print"], - "style_families": ["modern-ui", "editorial", "retro", "technical", "luxury", "organic", "experimental"], - "source_inputs": ["hex colors", "font pairings", "layout rules", "signature elements", "avoid lists"], - "agent_rule": "Pick one style, lock its palette and typography, then translate visual effects into explicit pptify layout_tree primitives or documented raster accents. Do not mix styles accidentally." - }, - "best_for": ["choosing a predefined modern deck style from a broad template catalog", "responding to user requests for stylish or visually striking presentations", "generating multiple visual direction options before deck production", "translating style-specific palettes, fonts, layouts, and signature elements into explicit pptify coordinates"] - }, - { - "id": "pptwork-oh-my-slides", - "name": "PPTWork / oh-my-slides — HTML-as-Source PPTX Build Artifact Pipeline", - "kind": "pptx-export-context", - "license": { - "spdx": "MIT", - "notice": "Copyright PPTWork contributors." - }, - "source": { - "repo": "PPTWork/oh-my-slides", - "url": "https://github.com/PPTWork/oh-my-slides", - "license_url": "https://github.com/PPTWork/oh-my-slides/blob/main/LICENSE", - "retrieved": "2026-05-19" - }, - "local_context": "contexts/pptwork-oh-my-slides.md", - "source_signals": { - "key_patterns": ["html-source", "preset-picker", "mini-preview", "raster-export", "constrained-editable"], - "export_model": "HTML (design source) → raster export (fidelity) + constrained PPTX (editability)", - "forbidden_in_editable_pptx": ["background images", "raster embeds of slide content", "CSS transform rotate", "SVG filter effects"], - "agent_rule": "Never promise both pixel fidelity and full editability from the same export path. Raster embeds in editable PPTX are a quality failure." - }, - "best_for": ["workflows that need HTML prototype before PPTX delivery", "decks where design fidelity and PowerPoint editability are separate deliverables", "agents using Playwright as part of the generation loop", "teaching the constraint model of HTML-to-PPTX conversion"] - }, - { - "id": "erickittelson-slidemason", - "name": "erickittelson/slidemason — JSX Primitive Composition (Cautionary Reference)", - "kind": "agent-skill-context", - "license": { - "spdx": "MIT", - "notice": "Copyright erickittelson contributors." - }, - "source": { - "repo": "erickittelson/slidemason", - "url": "https://github.com/erickittelson/slidemason", - "license_url": "https://github.com/erickittelson/slidemason/blob/main/LICENSE", - "retrieved": "2026-05-19" - }, - "local_context": "contexts/erickittelson-slidemason.md", - "source_signals": { - "key_patterns": ["jsx-primitives", "jsx-bento", "bespoke-slide", "primitive-composition"], - "primitive_map": {"Card": "_shape(round_rect)", "Text": "_text()", "Line": "_line()", "Image": "_image()", "Oval": "_shape(oval)"}, - "editability_failure_modes": ["nested flex containers", "auto-sized text frames", "SVG filter effects", "rotated text boxes", "image fills on shapes"], - "agent_rule": "Auto-layout is the enemy of editability. All coordinates are in inches. Bespoke layout is a last resort." - }, - "best_for": ["understanding the limits of programmatic slide composition", "designing new pptify patterns by sketching primitives first", "cautionary reference for why auto-layout and PPTX editability are incompatible", "component-level thinking about slide layout"] - }, - { - "id": "gabberflast-academic-pptx-skill", - "name": "Gabberflast/academic-pptx-skill — Narrative Discipline Gates", - "kind": "agent-skill-context", - "license": { - "spdx": "MIT", - "notice": "Copyright Gabberflast contributors." - }, - "source": { - "repo": "Gabberflast/academic-pptx-skill", - "url": "https://github.com/Gabberflast/academic-pptx-skill", - "license_url": "https://github.com/Gabberflast/academic-pptx-skill/blob/main/LICENSE", - "retrieved": "2026-05-19" - }, - "local_context": "contexts/gabberflast-academic-pptx-skill.md", - "source_signals": { - "key_patterns": ["action-title", "ghost-deck-test", "one-exhibit-discipline", "evidence-slide", "citation-slide"], - "narrative_gates": ["action title on every content slide", "ghost deck test passes", "one exhibit per slide", "last slide names a specific next action", "every quantitative claim has a source"], - "agent_rule": "Run ghost deck test before building slides. Rewrite descriptive titles as action titles. One exhibit per slide is a hard rule. The closing slide must name a decision, deadline, and owner." - }, - "best_for": ["high-stakes governance or board presentations requiring narrative rigour", "decks reviewed by critical audiences (investors, regulators, boards)", "training agents to apply consulting-grade storytelling discipline", "post-generation review gates before delivering a deck"] - } - ] -} \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-design/third-party-notices.md b/plugins/pptify/.agent/pptify-design/third-party-notices.md deleted file mode 100644 index 4892ec2df..000000000 --- a/plugins/pptify/.agent/pptify-design/third-party-notices.md +++ /dev/null @@ -1,41 +0,0 @@ -# Third-Party Notices for pptify-design - -The design context packs in this folder include selected source excerpts and source-derived metadata from the projects below. They are included to provide predefined design context to LLM agents. - -## Primer Primitives - -- Source: https://github.com/primer/primitives -- Files referenced: `README.md`, `DESIGN_TOKENS_GUIDE.md`, `src/tokens/base/color/light/light.json5`, `src/tokens/functional/spacing/space.json5`, `src/tokens/functional/typography/typography.json5`, `LICENSE` -- License: MIT -- Notice: Copyright (c) 2018 GitHub Inc. - -## Fluent UI - -- Source: https://github.com/microsoft/fluentui -- Files referenced: `AGENTS.md`, `docs/architecture/design-tokens.md`, `LICENSE` -- License: MIT -- Notice: Copyright (c) Microsoft Corporation. -- Note: Usage of fonts and icons referenced in Fluent UI React is subject to the terms listed by Fluent UI at https://aka.ms/fluentui-assets-license. The `pptify-design` context pack references token guidance only and does not copy those assets. - -## Awesome Copilot - -- Source: https://github.com/github/awesome-copilot -- Files referenced: `agents/gem-designer.agent.md`, `agents/se-ux-ui-designer.agent.md`, `skills/penpot-uiux-design/SKILL.md`, `skills/prompt-optimizer/SKILL.md`, `LICENSE` -- License: MIT -- Notice: Copyright GitHub, Inc. - -## corazzon/pptx-design-styles - -- Source: https://github.com/corazzon/pptx-design-styles -- Files referenced: `README.md`, `SKILL.md`, `references/styles.md` -- License: MIT, as stated in the upstream `README.md` license section -- Notice: Copyright TodayCode / corazzon contributors. -- Note: The upstream repository also contains Korean-language documentation, a Korean preview page, and preview images. The local `pptify-design` context normalizes the design guidance to English and does not copy upstream binary assets. - -## MIT License Text - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/README.md b/plugins/pptify/.agent/pptify-plugin/README.md deleted file mode 100644 index a18de0659..000000000 --- a/plugins/pptify/.agent/pptify-plugin/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# pptify-plugin - -Standalone plugin scripts for source ingestion, design context, image assets, extraction, and audit helpers used by pptify workflows. CLI-style tools can be called directly by path and write a JSON payload to stdout. Extraction helpers are import APIs. - -## Optional Dependencies - -The base `pptify` package stays lightweight. Install plugin dependencies only when these integrations are needed: - -```powershell -uv sync --extra plugins -``` - -Some scripts also work without optional packages: - -- `documents/document_to_raptor_tree.py` uses stable local embeddings and requires only the standard library. -- `design/design_context_catalog.py` reads local `pptify-design` metadata and requires only the standard library. -- `images/raster_image_to_svg.py` defaults to wrapping raster bytes in an SVG and requires only the standard library. Its `--mode vector-trace` path uses optional `vtracer`. - -## External Assets - -Large optional runtime assets are restored on demand instead of being maintained as source files. Run the downloader from the repository root when you need local ONNX embeddings or other optional helper assets: - -```powershell -.\pptify-plugin\download-external-assets.ps1 -``` - -The script downloads MiniLM tokenizer files plus the selected ONNX model into `pptify-plugin/external/all-MiniLM-L6-v2`. The default MiniLM model is `onnx/model_quint8_avx2.onnx`, saved locally as `model_quantized.onnx`; pass `-MiniLmModelPath onnx/model.onnx` for the larger non-quantized model. - -## Image Provider Access - -`images/text_prompt_to_infographic.py` loads image-provider settings from `.env` before it runs. When image generation needs credentials or provider configuration, copy `.env.template` to `.env`, ask the user to fill the required values directly in that file, then run the helper. Do not ask the user to paste API keys, tokens, or connection strings into chat or a prompt input dialog. - -The helper does not provide a built-in local fallback image provider. If `--provider auto` is used and neither OpenAI nor Azure OpenAI is configured, the command fails with `missing_provider_config` instead of generating a substitute asset. - -For OpenAI text-to-image generation, configure these values in `.env`: - -- `PPTIFY_IMAGE_PROVIDER=openai` or pass `--provider openai`. -- `OPENAI_API_KEY`. -- `OPENAI_IMAGE_MODEL`, defaulting to `gpt-image-1` when unspecified. -- Image size: default to `1024x1024` when unspecified. -- Text prompt and output path. - -For Azure `gpt-image-2` or `gpt-image-2.0` deployments, configure these values in `.env`: - -- `PPTIFY_IMAGE_PROVIDER=azure-openai` or pass `--provider azure-openai`. -- `AZURE_OPENAI_ENDPOINT`, for example `https://.services.ai.azure.com/openai/v1`. -- `AZURE_OPENAI_IMAGE_DEPLOYMENT`, for example `gpt-image-2` or the user's exact deployment name. -- Image size: default to `1024x1024` when unspecified. -- Auth method: Azure CLI/Entra auth or API-key auth. -- `AZURE_OPENAI_TIMEOUT`, optional, default `300` seconds for large image generations. - -For Azure CLI/Entra auth, run `az login`. For API-key auth, fill `AZURE_OPENAI_API_KEY` or `AZURE_AI_API_KEY` in `.env`. `.env` is git-ignored; never commit it. - -Example: - -```powershell -Copy-Item .env.template .env -# Edit .env, then run: -uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty -``` - -For OpenAI image generation, fill `OPENAI_API_KEY` in `.env` and run the helper. - -Example: - -```powershell -uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty -``` - -## Scripts - -- `documents/document_to_markdown.py` - convert PDF, DOCX, PPTX, XLSX, HTML, or TXT with MarkItDown. -- `documents/document_to_raptor_tree.py` - split markdown, embed sections, and build a RAPTOR-style JSON tree. -- `design/design_context_catalog.py` - list or emit source-backed predefined design template and agent-prompt context from `pptify-design`. -- `images/web_image_search.py` - search Google when `icrawler` is available, then fall back to Bing HTML candidates. -- `images/iconfy_search.py` - search Iconify and return candidate SVG URLs. The filename keeps the existing `iconfy` spelling. -- `images/raster_image_to_svg.py` - create an SVG wrapper around a raster image, or trace it into vector paths with optional `vtracer`. -- `images/text_prompt_to_infographic.py` - generate an infographic via OpenAI or Azure OpenAI. -- `extraction/pptx_extractor.py` - importable helper for PPTX prompt context and extraction. -- `extraction/pptx_style_master.py` - importable helper for compact style, theme, and layout-rhythm analysis. - -`images/notebooklm_infographic.py` is not present in this workspace snapshot. Do not document or call a NotebookLM bridge unless that script is restored. - -## Examples - -```powershell -uv run python pptify-plugin/documents/document_to_markdown.py --source source.pdf --output-path source.md -uv run python pptify-plugin/documents/document_to_raptor_tree.py --markdown-path source.md --output-path source.structured-summary.json --title "Source" -uv run python pptify-plugin/design/design_context_catalog.py --profile primer-primitives --include-context --pretty -uv run python pptify-plugin/images/web_image_search.py --query "Power Platform governance" --max-num 8 -uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 -uv run python pptify-plugin/images/raster_image_to_svg.py --source logo.png --mode vector-trace --output-path logo.svg -uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider openai --prompt "Cloud governance roadmap" --output-path infographic.png -``` \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/audit/audit.py b/plugins/pptify/.agent/pptify-plugin/audit/audit.py deleted file mode 100644 index 599ba413e..000000000 --- a/plugins/pptify/.agent/pptify-plugin/audit/audit.py +++ /dev/null @@ -1,152 +0,0 @@ -from __future__ import annotations - -"""Standalone collision-detection audit for layout_tree specs. - -Works entirely on plain dicts — no pptify package required. -Accepts a layout_tree dict (as produced by the agent or by pptx_extractor) -and returns a list of colliding object pairs. - -Usage (as a script): - python audit.py spec.json # prints collisions for each slide - python audit.py spec.json --json # prints JSON audit report -""" - -import json -import sys -from pathlib import Path -from typing import Any - - -def detect_content_collisions(tree: dict[str, Any]) -> list[dict[str, str]]: - """Return pairs of objects whose bounding boxes overlap. - - Args: - tree: A layout_tree dict with an ``objects`` mapping. - - Returns: - A list of ``{"first_object_id": ..., "second_object_id": ...}`` dicts. - """ - positioned_objects = [ - obj - for obj in tree.get("objects", {}).values() - if obj.get("bbox") - ] - collisions: list[dict[str, str]] = [] - for first_index, first_obj in enumerate(positioned_objects): - for second_obj in positioned_objects[first_index + 1 :]: - if _intersects(first_obj["bbox"], second_obj["bbox"], padding=0.01): - collisions.append({"first_object_id": first_obj["id"], "second_object_id": second_obj["id"]}) - return collisions - - -FONT_SIZE_FLOOR = 9.0 # pt — below this threshold text is unreadable at presentation scale - - -def detect_small_fonts(tree: dict[str, Any], floor: float = FONT_SIZE_FLOOR) -> list[dict[str, Any]]: - """Return content objects whose ``style.font_size`` is below *floor* pt. - - Only objects with ``classification: "content"`` are checked; decorative - ``layout_design`` objects (dots, rule lines, etc.) are skipped. - """ - violations: list[dict[str, Any]] = [] - for obj in tree.get("objects", {}).values(): - if obj.get("classification") == "layout_design": - continue - size = obj.get("style", {}).get("font_size") - if size is not None and float(size) < floor: - violations.append({"object_id": obj["id"], "font_size": size, "floor": floor}) - return violations - - -def audit_spec(spec: dict[str, Any]) -> dict[str, Any]: - """Audit all slides in a deck spec (``{"slides": [...]}``). - - Each slide must have a ``layout_tree`` key. - Returns a summary with per-slide collision lists and small-font warnings. - """ - results: list[dict[str, Any]] = [] - total_collisions = 0 - total_small_fonts = 0 - for slide in spec.get("slides", []): - tree = slide.get("layout_tree") or {} - slide_id = str(slide.get("id") or tree.get("id") or "unknown") - collisions = detect_content_collisions(tree) - small_fonts = detect_small_fonts(tree) - total_collisions += len(collisions) - total_small_fonts += len(small_fonts) - results.append( - { - "slide_id": slide_id, - "collision_count": len(collisions), - "collisions": collisions, - "small_font_count": len(small_fonts), - "small_fonts": small_fonts, - } - ) - return { - "slide_count": len(results), - "total_collisions": total_collisions, - "total_small_fonts": total_small_fonts, - "slides": results, - } - - -# --------------------------------------------------------------------------- -# Internal helpers -# --------------------------------------------------------------------------- - - -def _intersects(a: dict[str, float], b: dict[str, float], padding: float = 0.0) -> bool: - """Return True if bbox *a* overlaps expanded bbox *b* (by *padding* on each side).""" - bx = b["x"] - padding - by = b["y"] - padding - br = b["x"] + b["width"] + padding - bb = b["y"] + b["height"] + padding - epsilon = 0.0001 - ax, ay = a["x"], a["y"] - ar = a["x"] + a["width"] - ab_ = a["y"] + a["height"] - return ax < br - epsilon and ar > bx + epsilon and ay < bb - epsilon and ab_ > by + epsilon - - -# --------------------------------------------------------------------------- -# CLI entry-point -# --------------------------------------------------------------------------- - - -def _main(argv: list[str]) -> None: - if not argv: - print("Usage: python audit.py [--json]", file=sys.stderr) - sys.exit(1) - spec_path = Path(argv[0]) - as_json = "--json" in argv - spec = json.loads(spec_path.read_text(encoding="utf-8")) - # Support both a full spec with "slides" and a bare layout_tree - if "slides" in spec: - report = audit_spec(spec) - else: - collisions = detect_content_collisions(spec) - small_fonts = detect_small_fonts(spec) - report = { - "slide_count": 1, - "total_collisions": len(collisions), - "total_small_fonts": len(small_fonts), - "slides": [{"slide_id": "root", "collision_count": len(collisions), "collisions": collisions, "small_font_count": len(small_fonts), "small_fonts": small_fonts}], - } - if as_json: - print(json.dumps(report, indent=2, ensure_ascii=False)) - else: - print(f"Slides: {report['slide_count']} Total collisions: {report['total_collisions']} Small fonts (<{FONT_SIZE_FLOOR}pt): {report['total_small_fonts']}") - for slide in report["slides"]: - if slide["collision_count"]: - print(f" Slide {slide['slide_id']}: {slide['collision_count']} collision(s)") - for c in slide["collisions"]: - print(f" {c['first_object_id']} <-> {c['second_object_id']}") - if slide["small_font_count"]: - print(f" Slide {slide['slide_id']}: {slide['small_font_count']} small-font object(s)") - for f in slide["small_fonts"]: - print(f" {f['object_id']}: {f['font_size']}pt (floor {f['floor']}pt)") - - -if __name__ == "__main__": - _main(sys.argv[1:]) diff --git a/plugins/pptify/.agent/pptify-plugin/design/__init__.py b/plugins/pptify/.agent/pptify-plugin/design/__init__.py deleted file mode 100644 index afee1cf02..000000000 --- a/plugins/pptify/.agent/pptify-plugin/design/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Design-context plugin helpers for pptify workflows.""" \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py b/plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py deleted file mode 100644 index df5a784ff..000000000 --- a/plugins/pptify/.agent/pptify-plugin/design/design_context_catalog.py +++ /dev/null @@ -1,128 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any - - -DEFAULT_MANIFEST_PATH = Path(__file__).resolve().parents[2] / "pptify-design" / "sources.json" - - -def load_catalog(manifest_path: Path = DEFAULT_MANIFEST_PATH) -> dict[str, Any]: - with manifest_path.open("r", encoding="utf-8") as handle: - catalog = json.load(handle) - profiles = catalog.get("profiles") - if not isinstance(profiles, list): - raise ValueError(f"Catalog does not contain a profiles list: {manifest_path}") - return catalog - - -def select_profiles(catalog: dict[str, Any], profile_ids: list[str] | None = None) -> list[dict[str, Any]]: - profiles = [profile for profile in catalog.get("profiles", []) if isinstance(profile, dict)] - if not profile_ids: - return profiles - - by_id = {str(profile.get("id")): profile for profile in profiles if profile.get("id")} - selected: list[dict[str, Any]] = [] - missing: list[str] = [] - for profile_id in profile_ids: - profile = by_id.get(profile_id) - if profile is None: - missing.append(profile_id) - else: - selected.append(profile) - - if missing: - available = ", ".join(sorted(by_id)) - raise ValueError(f"Unknown design context profile(s): {', '.join(missing)}. Available: {available}") - return selected - - -def read_context_files(profiles: list[dict[str, Any]], *, base_dir: Path) -> list[dict[str, str]]: - contexts: list[dict[str, str]] = [] - for profile in profiles: - context_path_value = profile.get("local_context") - if not isinstance(context_path_value, str) or not context_path_value: - continue - context_path = (base_dir / context_path_value).resolve() - try: - content = context_path.read_text(encoding="utf-8") - except FileNotFoundError: - content = "" - contexts.append( - { - "id": str(profile.get("id", "")), - "path": context_path.relative_to(base_dir).as_posix(), - "content": content, - } - ) - return contexts - - -def build_payload( - *, - manifest_path: Path = DEFAULT_MANIFEST_PATH, - profile_ids: list[str] | None = None, - include_context: bool = False, - list_only: bool = False, -) -> dict[str, Any]: - catalog = load_catalog(manifest_path) - profiles = select_profiles(catalog, profile_ids) - base_dir = manifest_path.resolve().parent - - payload: dict[str, Any] = { - "ok": True, - "schema": catalog.get("schema"), - "updated": catalog.get("updated"), - "manifest": str(manifest_path), - "profiles": profiles, - } - if list_only: - payload["profiles"] = [ - { - "id": profile.get("id"), - "name": profile.get("name"), - "kind": profile.get("kind"), - "best_for": profile.get("best_for", []), - } - for profile in profiles - ] - if include_context: - payload["contexts"] = read_context_files(profiles, base_dir=base_dir) - return payload - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Emit source-backed pptify design context catalog entries as JSON.") - parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST_PATH, help="Path to pptify-design/sources.json.") - parser.add_argument("--profile", action="append", help="Profile ID to select. Can be repeated. Defaults to all profiles.") - parser.add_argument("--include-context", action="store_true", help="Include local context file contents in the JSON payload.") - parser.add_argument("--list", action="store_true", help="Return a compact list of profiles.") - parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") - return parser - - -def main() -> int: - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - args = build_parser().parse_args() - try: - payload = build_payload( - manifest_path=args.manifest, - profile_ids=args.profile, - include_context=args.include_context, - list_only=args.list, - ) - except Exception as exc: - payload = {"ok": False, "error": str(exc)} - print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) - return 1 - - print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/documents/__init__.py b/plugins/pptify/.agent/pptify-plugin/documents/__init__.py deleted file mode 100644 index 6634a9482..000000000 --- a/plugins/pptify/.agent/pptify-plugin/documents/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Document ingestion helpers for pptify plugins.""" \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py b/plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py deleted file mode 100644 index 1bdb27170..000000000 --- a/plugins/pptify/.agent/pptify-plugin/documents/document_to_markdown.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any - - -def emit_payload(payload: dict[str, Any], *, pretty: bool = False) -> None: - print(json.dumps(payload, ensure_ascii=False, indent=2 if pretty else None)) - - -def convert_document(source: str | Path, *, enable_plugins: bool = False) -> dict[str, Any]: - """Convert a document supported by MarkItDown into markdown text.""" - source_path = Path(source).expanduser() - if not source_path.exists(): - return { - "ok": False, - "error": f"Source file does not exist: {source_path}", - "code": "source_not_found", - } - - try: - from markitdown import MarkItDown - except Exception as exc: - return { - "ok": False, - "error": "MarkItDown is not installed. Install the plugin dependencies with `uv sync --extra plugins`.", - "detail": str(exc), - "code": "dependency_missing", - } - - try: - result = MarkItDown(enable_plugins=enable_plugins).convert(str(source_path)) - except Exception as exc: - return {"ok": False, "error": str(exc), "code": "conversion_failed"} - - markdown = getattr(result, "text_content", "") or "" - title = getattr(result, "title", "") or source_path.stem - return { - "ok": True, - "source": str(source_path), - "title": title, - "markdown": markdown, - "charCount": len(markdown), - } - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Convert a document to markdown with MarkItDown.") - parser.add_argument("--source", required=True, help="Path to a document such as PDF, DOCX, PPTX, XLSX, HTML, or TXT.") - parser.add_argument("--output-path", help="Optional path where markdown should be written.") - parser.add_argument("--enable-plugins", action="store_true", help="Enable MarkItDown plugins.") - parser.add_argument("--pretty", action="store_true", help="Pretty-print the JSON response.") - return parser - - -def main() -> int: - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - - args = build_parser().parse_args() - payload = convert_document(args.source, enable_plugins=args.enable_plugins) - - if payload.get("ok") and args.output_path: - output_path = Path(args.output_path).expanduser() - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(str(payload["markdown"]), encoding="utf-8") - payload["outputPath"] = str(output_path) - - emit_payload(payload, pretty=args.pretty) - return 0 if payload.get("ok") else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py b/plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py deleted file mode 100644 index 168ea814b..000000000 --- a/plugins/pptify/.agent/pptify-plugin/documents/document_to_raptor_tree.py +++ /dev/null @@ -1,490 +0,0 @@ -from __future__ import annotations - -import argparse -import hashlib -import json -import math -import re -import sys -import time -from pathlib import Path -from typing import Any, TypedDict - - -class RaptorNode(TypedDict): - id: str - level: int - heading: str - text: str - embedding: list[float] - children: list[str] - - -class RaptorTree(TypedDict): - nodes: list[RaptorNode] - - -class StructuredSummary(TypedDict): - documentTitle: str - globalSummary: dict[str, Any] - raptorTree: RaptorTree - - -_HEADING_RE = re.compile( - r""" - ^(?: - (?P\#{1,6})\s+(?P.+) - | (?P\d+(?:\.\d+)*)[.)]\s+(?P.+) - | (?P[A-Z][A-Z\s]{4,})$ - ) - """, - re.VERBOSE | re.MULTILINE, -) -_PARA_SPLIT_RE = re.compile(r"\n\s*\n") -_TOKEN_RE = re.compile(r"[A-Za-z0-9]+(?:[-_][A-Za-z0-9]+)*|[^\W\s_]+", re.UNICODE) -DEFAULT_EMBEDDING_DIM = 384 -CLUSTER_THRESHOLD = 0.55 -MAX_CLUSTER_SUMMARY_CHARS = 2000 - - -def _heading_level(match: re.Match[str]) -> tuple[int, str]: - if match.group("md"): - return len(match.group("md")), match.group("md_title").strip() - if match.group("num"): - depth = match.group("num").count(".") + 1 - return min(depth + 1, 6), match.group("num_title").strip() - if match.group("caps"): - return 2, match.group("caps").strip().title() - return 2, "" - - -def _split_sections_by_headings(markdown: str, min_chars: int = 100) -> list[dict[str, Any]]: - sections: list[dict[str, Any]] = [] - current_heading = "Introduction" - current_level = 1 - current_lines: list[str] = [] - - for line in markdown.split("\n"): - match = _HEADING_RE.match(line.strip()) - if match: - text = "\n".join(current_lines).strip() - if text and len(text) >= min_chars: - sections.append({"heading": current_heading, "text": text, "level": current_level}) - current_level, current_heading = _heading_level(match) - current_lines = [] - else: - current_lines.append(line) - - text = "\n".join(current_lines).strip() - if text and len(text) >= min_chars: - sections.append({"heading": current_heading, "text": text, "level": current_level}) - return sections - - -def _extract_paragraph_heading(text: str, max_len: int = 80) -> str: - first_line = text.split("\n", 1)[0].strip() - heading = re.sub(r"[*_`#]", "", first_line).strip() - if heading.startswith("|"): - cells = [cell.strip() for cell in heading.split("|") if cell.strip()] - heading = cells[0] if cells else heading - if len(heading) > max_len: - shortened = heading[:max_len].rsplit(" ", 1)[0].strip() - heading = f"{shortened or heading[:max_len]}..." - return heading or "Section" - - -def _tokenize(text: str) -> list[str]: - return [token.lower() for token in _TOKEN_RE.findall(text) if token.strip()] - - -def _normalize(vector: list[float]) -> list[float]: - norm = math.sqrt(sum(value * value for value in vector)) - if norm <= 1e-12: - return vector - return [value / norm for value in vector] - - -def _hash_embedding(text: str, *, dim: int = DEFAULT_EMBEDDING_DIM) -> list[float]: - tokens = _tokenize(text) - vector = [0.0] * dim - if not tokens: - return vector - - features = list(tokens) - features.extend(f"{left} {right}" for left, right in zip(tokens, tokens[1:])) - - for feature in features: - digest = hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest() - index = int.from_bytes(digest[:4], "little") % dim - sign = 1.0 if digest[4] & 1 else -1.0 - weight = 1.0 + min(len(feature), 20) / 80.0 - vector[index] += sign * weight - return _normalize(vector) - - -_ONNX_SESSION: Any = None # ort.InferenceSession once loaded -_ONNX_TOKENIZER: Any = None # tokenizers.Tokenizer once loaded -_ONNX_TRIED: bool = False - -_EXTERNAL_MODEL_DIR = Path(__file__).parent.parent / "external" / "all-MiniLM-L6-v2" - - -def _try_load_onnx() -> bool: - """Load the ONNX session and tokenizer from external/. Returns True on success.""" - global _ONNX_SESSION, _ONNX_TOKENIZER, _ONNX_TRIED - if _ONNX_TRIED: - return _ONNX_SESSION is not None - _ONNX_TRIED = True - - model_path = _EXTERNAL_MODEL_DIR / "model_quantized.onnx" - tokenizer_path = _EXTERNAL_MODEL_DIR / "tokenizer.json" - if not model_path.exists() or not tokenizer_path.exists(): - return False - - try: - import onnxruntime as ort - from tokenizers import Tokenizer - - session = ort.InferenceSession(str(model_path), providers=["CPUExecutionProvider"]) - tok = Tokenizer.from_file(str(tokenizer_path)) - tok.enable_truncation(max_length=256) - tok.enable_padding(pad_id=0, pad_token="[PAD]") - _ONNX_SESSION = session - _ONNX_TOKENIZER = tok - print("[raptor] Using local ONNX embedding model (all-MiniLM-L6-v2).", file=sys.stderr) - return True - except Exception as exc: - print(f"[raptor] Could not load ONNX model, falling back to hash embeddings: {exc}", file=sys.stderr) - return False - - -def _embed_with_onnx(texts: list[str], *, prefix: str, dim: int) -> list[list[float]]: - import numpy as np - - prefixed = [f"{prefix}{text}" for text in texts] - encodings = _ONNX_TOKENIZER.encode_batch(prefixed) - input_ids = np.array([enc.ids for enc in encodings], dtype=np.int64) - attention_mask = np.array([enc.attention_mask for enc in encodings], dtype=np.int64) - input_names = {inp.name for inp in _ONNX_SESSION.get_inputs()} - feed: dict[str, Any] = {"input_ids": input_ids, "attention_mask": attention_mask} - if "token_type_ids" in input_names: - feed["token_type_ids"] = np.zeros_like(input_ids) - - outputs = _ONNX_SESSION.run(None, feed) - token_embeddings: Any = outputs[0] # (batch, seq_len, hidden_dim) - mask = attention_mask[..., np.newaxis].astype(np.float32) - mean_embeddings = (token_embeddings * mask).sum(axis=1) / mask.sum(axis=1) - norms = np.linalg.norm(mean_embeddings, axis=1, keepdims=True) - normed = mean_embeddings / np.where(norms < 1e-12, 1.0, norms) - - # Adjust output dimension if needed (model native dim is 384) - native_dim = normed.shape[1] - if dim < native_dim: - normed = normed[:, :dim] - elif dim > native_dim: - pad = np.zeros((normed.shape[0], dim - native_dim), dtype=normed.dtype) - normed = np.concatenate([normed, pad], axis=1) - - return normed.tolist() - - -def embed(texts: list[str], *, prefix: str = "passage: ", dim: int = DEFAULT_EMBEDDING_DIM) -> list[list[float]]: - if _try_load_onnx(): - return _embed_with_onnx(texts, prefix=prefix, dim=dim) - return [_hash_embedding(f"{prefix}{text}", dim=dim) for text in texts] - - -def _cosine(a: list[float], b: list[float]) -> float: - return sum(left * right for left, right in zip(a, b)) - - -def _weighted_average(a: list[float], a_count: int, b: list[float], b_count: int) -> list[float]: - total = max(a_count + b_count, 1) - return _normalize([(left * a_count + right * b_count) / total for left, right in zip(a, b)]) - - -def _split_sections_by_char_budget(paragraphs: list[str], target_chars: int = 2000) -> list[dict[str, Any]]: - sections: list[dict[str, Any]] = [] - group: list[str] = [] - group_len = 0 - - for paragraph in paragraphs: - if group and group_len + len(paragraph) > target_chars: - combined = "\n\n".join(group) - sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) - group = [] - group_len = 0 - group.append(paragraph) - group_len += len(paragraph) - - if group: - combined = "\n\n".join(group) - sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) - return sections - - -def _split_sections_by_semantics( - markdown: str, - *, - target_chars: int = 2000, - max_chars: int = 4000, - similarity_threshold: float = 0.25, - embedding_dim: int = DEFAULT_EMBEDDING_DIM, -) -> list[dict[str, Any]]: - paragraphs = [paragraph.strip() for paragraph in _PARA_SPLIT_RE.split(markdown) if paragraph.strip()] - if not paragraphs: - return [] - if len(paragraphs) == 1: - return [{"heading": _extract_paragraph_heading(paragraphs[0]), "text": paragraphs[0], "level": 1}] - - embeddings = embed([paragraph[:700] for paragraph in paragraphs], dim=embedding_dim) - consecutive_sims = [_cosine(embeddings[index], embeddings[index + 1]) for index in range(len(embeddings) - 1)] - - sections: list[dict[str, Any]] = [] - group = [paragraphs[0]] - group_len = len(paragraphs[0]) - - for index in range(1, len(paragraphs)): - paragraph = paragraphs[index] - similarity = consecutive_sims[index - 1] if index - 1 < len(consecutive_sims) else 1.0 - budget_exceeded = group_len + len(paragraph) > max_chars - topic_shift = group_len >= target_chars and similarity < similarity_threshold - - if budget_exceeded or topic_shift: - combined = "\n\n".join(group) - sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) - group = [paragraph] - group_len = len(paragraph) - else: - group.append(paragraph) - group_len += len(paragraph) - - if group: - combined = "\n\n".join(group) - sections.append({"heading": _extract_paragraph_heading(combined), "text": combined, "level": 1}) - return sections or _split_sections_by_char_budget(paragraphs, target_chars=target_chars) - - -def _heading_section_threshold(content_len: int) -> int: - return max(2, min(8, content_len // 3000)) - - -def split_sections(markdown: str, *, min_chars: int = 100, embedding_dim: int = DEFAULT_EMBEDDING_DIM) -> list[dict[str, Any]]: - heading_sections = _split_sections_by_headings(markdown, min_chars=min_chars) - threshold = _heading_section_threshold(len(markdown)) - if len(heading_sections) >= threshold: - return heading_sections - - total_chars = sum(len(section["text"]) for section in heading_sections) if heading_sections else len(markdown) - if total_chars < min_chars * 2: - return heading_sections or [{"heading": "Document", "text": markdown.strip(), "level": 1}] - - print( - f"[raptor] Heading split yielded {len(heading_sections)} section(s); using semantic paragraph grouping.", - file=sys.stderr, - ) - return _split_sections_by_semantics(markdown, embedding_dim=embedding_dim) - - -def _agglomerative_cluster( - ids: list[str], - embeddings: list[list[float]], - *, - threshold: float = CLUSTER_THRESHOLD, -) -> list[list[str]]: - if len(ids) <= 1: - return [ids] if ids else [] - - clusters = [[node_id] for node_id in ids] - centroids = [list(embedding) for embedding in embeddings] - - while len(clusters) > 1: - best_i = -1 - best_j = -1 - best_similarity = -1.0 - for i in range(len(clusters)): - for j in range(i + 1, len(clusters)): - similarity = _cosine(centroids[i], centroids[j]) - if similarity > best_similarity: - best_i, best_j, best_similarity = i, j, similarity - - if best_i < 0 or best_j < 0 or best_similarity < threshold: - break - - left_count = len(clusters[best_i]) - right_count = len(clusters[best_j]) - clusters[best_i].extend(clusters[best_j]) - centroids[best_i] = _weighted_average(centroids[best_i], left_count, centroids[best_j], right_count) - del clusters[best_j] - del centroids[best_j] - - return clusters - - -def _make_cluster_summary(sections: list[dict[str, Any]]) -> str: - parts: list[str] = [] - for section in sections: - parts.append(f"## {section['heading']}") - text = str(section.get("text", ""))[:500].strip() - if text: - parts.append(text) - return "\n".join(parts)[:MAX_CLUSTER_SUMMARY_CHARS] - - -def build_raptor_tree( - sections: list[dict[str, Any]], - embeddings: list[list[float]], - *, - cluster_threshold: float = CLUSTER_THRESHOLD, - embedding_dim: int = DEFAULT_EMBEDDING_DIM, -) -> tuple[RaptorTree, dict[str, Any]]: - all_nodes: list[RaptorNode] = [] - leaf_ids: list[str] = [] - leaf_embeddings: list[list[float]] = [] - - for index, (section, embedding) in enumerate(zip(sections, embeddings)): - node_id = f"L0-{index}" - node: RaptorNode = { - "id": node_id, - "level": 0, - "heading": str(section["heading"]), - "text": str(section["text"])[:3000], - "embedding": embedding, - "children": [], - } - all_nodes.append(node) - leaf_ids.append(node_id) - leaf_embeddings.append(embedding) - - current_ids = leaf_ids - current_embeddings = leaf_embeddings - level = 1 - - while len(current_ids) > 1 and level <= 5: - clusters = _agglomerative_cluster(current_ids, current_embeddings, threshold=cluster_threshold) - if all(len(cluster) == 1 for cluster in clusters): - break - - node_map = {node["id"]: node for node in all_nodes} - next_ids: list[str] = [] - next_embeddings: list[list[float]] = [] - - for cluster_index, cluster_member_ids in enumerate(clusters): - if len(cluster_member_ids) == 1: - node = node_map[cluster_member_ids[0]] - next_ids.append(node["id"]) - next_embeddings.append(node["embedding"]) - continue - - cluster_sections = [ - {"heading": node_map[member_id]["heading"], "text": node_map[member_id]["text"]} - for member_id in cluster_member_ids - if member_id in node_map - ] - summary_text = _make_cluster_summary(cluster_sections) - headings = [str(section["heading"]) for section in cluster_sections] - cluster_embedding = embed([summary_text], dim=embedding_dim)[0] - cluster_id = f"L{level}-{cluster_index}" - cluster_node: RaptorNode = { - "id": cluster_id, - "level": level, - "heading": f"Cluster: {', '.join(headings[:3])}{'...' if len(headings) > 3 else ''}", - "text": summary_text, - "embedding": cluster_embedding, - "children": cluster_member_ids, - } - all_nodes.append(cluster_node) - next_ids.append(cluster_id) - next_embeddings.append(cluster_embedding) - - current_ids = next_ids - current_embeddings = next_embeddings - level += 1 - - node_map = {node["id"]: node for node in all_nodes} - top_nodes = [node_map[node_id] for node_id in current_ids if node_id in node_map] - main_theme = " | ".join(node["heading"] for node in top_nodes[:5])[:500] - global_summary = { - "mainTheme": main_theme, - "sectionCount": len(sections), - "topNodeCount": len(top_nodes), - "embedding": embed([main_theme or "document summary"], dim=embedding_dim)[0], - } - return {"nodes": all_nodes}, global_summary - - -def build_from_markdown( - markdown: str, - *, - title: str = "Untitled", - min_chars: int = 100, - embedding_dim: int = DEFAULT_EMBEDDING_DIM, - cluster_threshold: float = CLUSTER_THRESHOLD, -) -> StructuredSummary: - started_at = time.perf_counter() - sections = split_sections(markdown, min_chars=min_chars, embedding_dim=embedding_dim) - if not sections: - sections = [{"heading": title, "text": markdown[:5000], "level": 1}] - split_at = time.perf_counter() - print(f"[raptor] Split into {len(sections)} sections ({split_at - started_at:.2f}s)", file=sys.stderr) - - texts_to_embed = [f"{section['heading']}. {str(section['text'])[:1000]}" for section in sections] - embeddings = embed(texts_to_embed, dim=embedding_dim) - embed_at = time.perf_counter() - print(f"[raptor] Embedded {len(embeddings)} sections ({embed_at - split_at:.2f}s)", file=sys.stderr) - - tree, global_summary = build_raptor_tree( - sections, - embeddings, - cluster_threshold=cluster_threshold, - embedding_dim=embedding_dim, - ) - done_at = time.perf_counter() - print(f"[raptor] Built tree with {len(tree['nodes'])} nodes ({done_at - embed_at:.2f}s)", file=sys.stderr) - - return { - "documentTitle": title, - "globalSummary": global_summary, - "raptorTree": tree, - } - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Build a RAPTOR-style tree from markdown.") - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument("--markdown-path", help="Path to a markdown file.") - group.add_argument("--markdown", help="Raw markdown text.") - parser.add_argument("--output-path", required=True, help="Where to write the structured summary JSON.") - parser.add_argument("--title", default="Untitled", help="Document title.") - parser.add_argument("--min-chars", type=int, default=100, help="Minimum characters required for a section.") - parser.add_argument("--embedding-dim", type=int, default=DEFAULT_EMBEDDING_DIM, help="Stable embedding vector size.") - parser.add_argument("--cluster-threshold", type=float, default=CLUSTER_THRESHOLD, help="Cosine threshold for cluster merges.") - parser.add_argument("--pretty", action="store_true", help="Pretty-print the output JSON file.") - return parser - - -def main() -> int: - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - if hasattr(sys.stderr, "reconfigure"): - sys.stderr.reconfigure(encoding="utf-8") - - args = build_parser().parse_args() - markdown = Path(args.markdown_path).read_text(encoding="utf-8") if args.markdown_path else args.markdown - summary = build_from_markdown( - markdown, - title=args.title, - min_chars=args.min_chars, - embedding_dim=args.embedding_dim, - cluster_threshold=args.cluster_threshold, - ) - - output_path = Path(args.output_path).expanduser() - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2 if args.pretty else None), encoding="utf-8") - print(json.dumps({"ok": True, "nodes": len(summary["raptorTree"]["nodes"]), "path": str(output_path)}, ensure_ascii=False)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 b/plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 deleted file mode 100644 index b9b0caddb..000000000 --- a/plugins/pptify/.agent/pptify-plugin/download-external-assets.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -#Requires -Version 5.1 -[CmdletBinding()] -param( - [string]$MiniLmRepo = "sentence-transformers/all-MiniLM-L6-v2", - [string]$MiniLmRevision = "main", - [string]$MiniLmModelPath = "onnx/model_quint8_avx2.onnx", - - [switch]$Force -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" -$ProgressPreference = "SilentlyContinue" - -function Get-RepoRoot { - $scriptDir = Split-Path -Parent $PSCommandPath - return (Resolve-Path (Join-Path $scriptDir "..")).Path -} - -function Save-RemoteFile { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Destination, - [Parameter(Mandatory = $true)][bool]$Overwrite - ) - - if ((Test-Path -LiteralPath $Destination) -and -not $Overwrite) { - Write-Host "Exists: $Destination" - return - } - - New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Destination) | Out-Null - $downloadPath = "$Destination.download" - if (Test-Path -LiteralPath $downloadPath) { - Remove-Item -LiteralPath $downloadPath -Force - } - - Write-Host "Downloading $Uri" - Invoke-WebRequest -Uri $Uri -OutFile $downloadPath -Headers @{ "User-Agent" = "pptify-external-assets" } - Move-Item -LiteralPath $downloadPath -Destination $Destination -Force - Write-Host "Wrote: $Destination" -} - -function Join-HuggingFaceResolveUrl { - param( - [Parameter(Mandatory = $true)][string]$Repo, - [Parameter(Mandatory = $true)][string]$Revision, - [Parameter(Mandatory = $true)][string]$Path - ) - - $encodedPath = (($Path -split "/") | ForEach-Object { [uri]::EscapeDataString($_) }) -join "/" - return "https://huggingface.co/$Repo/resolve/$Revision/$encodedPath" -} - -function Install-MiniLm { - param([Parameter(Mandatory = $true)][string]$RepoRoot) - - $targetDir = Join-Path $RepoRoot "pptify-plugin\external\all-MiniLM-L6-v2" - $files = @( - @{ Source = $MiniLmModelPath; Target = "model_quantized.onnx" }, - @{ Source = "tokenizer.json"; Target = "tokenizer.json" }, - @{ Source = "tokenizer_config.json"; Target = "tokenizer_config.json" } - ) - - foreach ($file in $files) { - $uri = Join-HuggingFaceResolveUrl -Repo $MiniLmRepo -Revision $MiniLmRevision -Path $file.Source - $destination = Join-Path $targetDir $file.Target - Save-RemoteFile -Uri $uri -Destination $destination -Overwrite ([bool]$Force) - } -} - -$repoRoot = Get-RepoRoot - -Install-MiniLm -RepoRoot $repoRoot \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/external/README.md b/plugins/pptify/.agent/pptify-plugin/external/README.md deleted file mode 100644 index 4cef8dd63..000000000 --- a/plugins/pptify/.agent/pptify-plugin/external/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# External Assets - -This directory is for optional binary/model assets that are re-downloadable and ignored by git. - -Run from the repository root to restore the MiniLM ONNX model and tokenizer files: - -```powershell -.\pptify-plugin\download-external-assets.ps1 -``` - -Restores `all-MiniLM-L6-v2/model_quantized.onnx`, `tokenizer.json`, and `tokenizer_config.json` from the Hugging Face `sentence-transformers/all-MiniLM-L6-v2` repository. - -Pass `-MiniLmModelPath onnx/model.onnx` for the larger non-quantized model. Use `-Force` to overwrite existing files. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep b/plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep deleted file mode 100644 index 3c21ce7e3..000000000 --- a/plugins/pptify/.agent/pptify-plugin/external/all-MiniLM-L6-v2/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -Download MiniLM ONNX/tokenizer assets with pptify-plugin/download-external-assets.ps1. \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py b/plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py deleted file mode 100644 index c006a9a75..000000000 --- a/plugins/pptify/.agent/pptify-plugin/extraction/pptx_extractor.py +++ /dev/null @@ -1,545 +0,0 @@ -from __future__ import annotations - -import json -import posixpath -import sys -import zipfile -from base64 import b64encode -from collections import Counter -from pathlib import Path -from typing import Any -from xml.etree import ElementTree - -# Allow importing sibling pptx_style_master when run as a standalone script -sys.path.insert(0, str(Path(__file__).parent)) -from pptx_style_master import PptxStyleMaster - -EMU_PER_INCH = 914400 -DRAWING_NS = "{http://schemas.openxmlformats.org/drawingml/2006/main}" - - -class PptxExtractor: - def prompt_context(self, path: str | Path, max_chars: int = 16000) -> dict[str, Any]: - from pptx import Presentation - - pptx_path = Path(path) - presentation = Presentation(str(pptx_path)) - style_context = PptxStyleMaster().analyze(pptx_path) - slides: list[dict[str, Any]] = [] - used_chars = 0 - for slide_index, slide in enumerate(presentation.slides, start=1): - texts = _slide_text_fragments(slide.shapes) - trimmed_texts: list[str] = [] - for text in texts: - if used_chars >= max_chars: - break - cleaned = _compact_text(text) - if not cleaned: - continue - remaining = max_chars - used_chars - clipped = cleaned[: min(500, remaining)] - trimmed_texts.append(clipped) - used_chars += len(clipped) - title = trimmed_texts[0] if trimmed_texts else f"Slide {slide_index}" - slides.append( - { - "index": slide_index, - "title": title[:120], - "text": trimmed_texts[:12], - "shape_count": len(slide.shapes), - } - ) - media_files, embedded_files = _package_asset_counts(pptx_path) - return { - "source": str(pptx_path), - "slide_count": len(slides), - "slide_size": { - "width": _inches(presentation.slide_width), - "height": _inches(presentation.slide_height), - }, - "package_media_files": media_files, - "embedded_files": embedded_files, - "styles": style_context["styles"], - "brands": style_context["brands"], - "template": style_context["template"], - "layout": style_context["layout"], - "slides": slides, - } - - def extract_file(self, path: str | Path, output_dir: str | Path | None = None, extract_media: bool = True) -> dict[str, Any]: - from pptx import Presentation - - pptx_path = Path(path) - presentation = Presentation(str(pptx_path)) - asset_dir = None - embed_media = extract_media and output_dir is None - if output_dir and extract_media: - asset_dir = Path(output_dir) / f"{pptx_path.stem}_assets" - asset_dir.mkdir(parents=True, exist_ok=True) - _extract_package_media(pptx_path, asset_dir) - - notes_by_slide = _notes_by_slide(pptx_path) - slides: list[dict[str, Any]] = [] - stats: Counter[str] = Counter() - max_shapes = 0 - max_nested = 0 - for slide_index, slide in enumerate(presentation.slides, start=1): - tree, slide_stats, render_elements = self._extract_slide( - slide=slide, - slide_index=slide_index, - slide_size=(_inches(presentation.slide_width), _inches(presentation.slide_height)), - source_path=pptx_path, - asset_dir=asset_dir, - embed_media=embed_media, - notes=notes_by_slide.get(slide_index, []), - ) - stats.update(slide_stats) - max_shapes = max(max_shapes, slide_stats["top_level_shapes"]) - max_nested = max(max_nested, slide_stats["nested_shapes"]) - slides.append( - { - "id": tree["id"], - "title": tree["title"], - "slide_size": tree["slide_size"], - "preserve_coordinates": True, - "render_mode": "ooxml", - "ooxml_elements": render_elements, - "layout_tree": tree, - } - ) - - media_files, embedded_files = _package_asset_counts(pptx_path) - style_context = PptxStyleMaster().analyze(pptx_path) - summary = { - "source": str(pptx_path), - "slide_count": len(slides), - "slide_size": { - "width": _inches(presentation.slide_width), - "height": _inches(presentation.slide_height), - }, - "top_level_shapes": int(stats["top_level_shapes"]), - "nested_shapes": int(stats["nested_shapes"]), - "max_shapes_on_slide": max_shapes, - "max_nested_shapes_on_slide": max_nested, - "groups": int(stats["groups"]), - "tables": int(stats["tables"]), - "charts": int(stats["charts"]), - "images": int(stats["images"]), - "text_objects": int(stats["text_objects"]), - "placeholders": int(stats["placeholders"]), - "lines_or_freeforms": int(stats["lines_or_freeforms"]), - "non_ascii_text": bool(stats["non_ascii_text"]), - "notes_slides": int(stats["notes_slides"]), - "package_media_files": media_files, - "embedded_files": embedded_files, - "styles": style_context["styles"], - "brands": style_context["brands"], - "template": style_context["template"], - "layout": style_context["layout"], - } - return { - "source_pptx": str(pptx_path.resolve()), - "render_mode": "ooxml", - "summary": summary, - "slides": slides, - } - - def extract_path(self, path: str | Path, output_dir: str | Path, extract_media: bool = True) -> dict[str, Any]: - source = Path(path) - output = Path(output_dir) - output.mkdir(parents=True, exist_ok=True) - files = sorted(source.glob("*.pptx")) if source.is_dir() else [source] - decks = [] - for pptx_file in files: - deck = self.extract_file(pptx_file, output, extract_media=extract_media) - json_path = output / f"{pptx_file.stem}.pptify.json" - json_path.write_text(json.dumps(deck, indent=2, ensure_ascii=False), encoding="utf-8") - decks.append({"pptx": str(pptx_file), "json": str(json_path), "summary": deck["summary"]}) - manifest = {"source": str(source), "decks": decks} - (output / "manifest.json").write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8") - return manifest - - def analyze_path(self, path: str | Path) -> dict[str, Any]: - source = Path(path) - files = sorted(source.glob("*.pptx")) if source.is_dir() else [source] - return {"source": str(source), "decks": [self.extract_file(file, extract_media=False)["summary"] for file in files]} - - def _extract_slide( - self, - slide, - slide_index: int, - slide_size: tuple[float, float], - source_path: Path, - asset_dir: Path | None, - embed_media: bool, - notes: list[str], - ) -> tuple[dict[str, Any], Counter[str], list[dict[str, Any]]]: - root_id = f"slide_{slide_index}_root" - root_group: dict[str, Any] = { - "id": root_id, - "role": "slide", - "layout_mode": "absolute", - "object_ids": [], - "group_ids": [], - "constraints": {}, - "collision_policy": "relaxed", - "bbox": {"x": 0, "y": 0, "width": slide_size[0], "height": slide_size[1]}, - } - groups: dict[str, dict[str, Any]] = {root_id: root_group} - objects: dict[str, dict[str, Any]] = {} - stats: Counter[str] = Counter(top_level_shapes=len(slide.shapes), notes_slides=1 if notes else 0) - z_index = 0 - - def walk(shapes, parent_group_id: str, prefix: str) -> None: - nonlocal z_index - for shape_index, shape in enumerate(shapes, start=1): - z_index += 1 - stats["nested_shapes"] += 1 - shape_type = _shape_type_name(shape) - if _is_group(shape): - group_id = f"{prefix}_group_{shape_index}" - groups[parent_group_id]["group_ids"].append(group_id) - groups[group_id] = { - "id": group_id, - "role": "extracted_group", - "layout_mode": "absolute", - "object_ids": [], - "group_ids": [], - "constraints": {}, - "collision_policy": "relaxed", - "bbox": _bbox(shape), - } - stats["groups"] += 1 - walk(shape.shapes, group_id, group_id) - continue - - object_id = f"{prefix}_shape_{shape_index}" - slide_object = self._extract_object(shape, object_id, z_index, shape_type, source_path, asset_dir) - objects[object_id] = slide_object - groups[parent_group_id]["object_ids"].append(object_id) - stats[_stat_key(slide_object)] += 1 - if getattr(shape, "is_placeholder", False): - stats["placeholders"] += 1 - if _contains_non_ascii(slide_object["content"].get("text", "")): - stats["non_ascii_text"] += 1 - - walk(slide.shapes, root_id, f"slide_{slide_index}") - render_elements = [ - _render_element(shape, f"slide_{slide_index}_element_{element_index}", source_path, asset_dir, embed_media) - for element_index, shape in enumerate(slide.shapes, start=1) - ] - title = _slide_title(objects.values()) or f"Slide {slide_index}" - tree: dict[str, Any] = { - "id": f"slide_{slide_index}", - "title": title, - "slide_size": {"width": slide_size[0], "height": slide_size[1]}, - "root_group_id": root_id, - "groups": groups, - "objects": objects, - "notes": notes, - } - return tree, stats, render_elements - - def _extract_object(self, shape, object_id: str, z_index: int, shape_type: str, source_path: Path, asset_dir: Path | None) -> dict[str, Any]: - kind = _kind(shape, shape_type) - content: dict[str, Any] = {"source_shape_type": shape_type} - style: dict[str, Any] = {} - if kind == "text": - content["text"] = getattr(shape, "text", "") - style.update(_text_style(shape)) - elif kind == "table": - content["rows"] = [[cell.text for cell in row.cells] for row in shape.table.rows] - style["font_size"] = 8 - elif kind == "image": - content["alt"] = getattr(shape, "name", "image") - if asset_dir is not None: - image_data = _image_data(shape) - if image_data is None: - content["missing_embedded_image"] = True - else: - blob, extension, relationship_id, content_type = image_data - asset_path = asset_dir / f"{source_path.stem}_{object_id}.{extension}" - asset_path.write_bytes(blob) - content["path"] = str(asset_path) - content["content_type"] = content_type - if relationship_id: - content["media_relationship_id"] = relationship_id - elif kind == "chart": - content["title"] = getattr(shape, "name", "chart") - elif kind == "line": - box = _bbox(shape) - x, y, w, h = box["x"], box["y"], box["width"], box["height"] - content.update({"x1": x, "y1": y, "x2": x + w, "y2": y + h}) - style["line"] = "#6B7280" - elif getattr(shape, "has_text_frame", False) and getattr(shape, "text", ""): - content["text"] = shape.text - style.update(_text_style(shape)) - else: - content["shape"] = "rect" - - classification = "content" if kind in {"text", "table", "image", "chart"} else "layout_design" - if kind == "shape" and content.get("text"): - classification = "content" - return { - "id": object_id, - "kind": kind, - "role": _role(shape, kind), - "classification": classification, - "content": content, - "style": style, - "constraints": {"source_name": getattr(shape, "name", "")}, - "bbox": _bbox(shape), - "z_index": z_index, - } - - -def _inches(value: int) -> float: - return round(int(value) / EMU_PER_INCH, 4) - - -def _bbox(shape) -> dict[str, float]: - return { - "x": _inches(getattr(shape, "left", 0) or 0), - "y": _inches(getattr(shape, "top", 0) or 0), - "width": max(0.0, _inches(getattr(shape, "width", 0) or 0)), - "height": max(0.0, _inches(getattr(shape, "height", 0) or 0)), - } - - -def _shape_type_name(shape) -> str: - shape_type = getattr(shape, "shape_type", "unknown") - return str(getattr(shape_type, "name", shape_type)).lower() - - -def _is_group(shape) -> bool: - return hasattr(shape, "shapes") and "group" in _shape_type_name(shape) - - -def _kind(shape, shape_type: str) -> str: - if getattr(shape, "has_table", False): - return "table" - if getattr(shape, "has_chart", False): - return "chart" - if "picture" in shape_type or hasattr(shape, "image"): - return "image" - if "line" in shape_type or "freeform" in shape_type or "connector" in shape_type: - return "line" - if getattr(shape, "has_text_frame", False) and getattr(shape, "text", "").strip(): - return "text" - return "shape" - - -def _role(shape, kind: str) -> str: - if getattr(shape, "is_placeholder", False): - return "placeholder" - if kind == "text": - return "text" - return kind - - -def _text_style(shape) -> dict[str, Any]: - style: dict[str, Any] = {"font_size": 12} - try: - paragraph = shape.text_frame.paragraphs[0] - run = paragraph.runs[0] if paragraph.runs else None - font = run.font if run is not None else paragraph.font - if font.size is not None: - style["font_size"] = round(font.size.pt, 2) - if font.bold is not None: - style["bold"] = bool(font.bold) - if font.name: - style["font"] = font.name - except (AttributeError, IndexError): - pass - return style - - -def _image_data(shape) -> tuple[bytes, str, str | None, str] | None: - try: - image = shape.image - except ValueError: - image = None - if image is not None: - return image.blob, str(image.ext), None, image.content_type - - for relationship_id in _embedded_relationship_ids(shape): - try: - part = shape.part.related_part(relationship_id) - except KeyError: - continue - blob = getattr(part, "blob", None) - if not blob: - continue - extension = Path(str(getattr(part, "partname", "media.bin"))).suffix.lstrip(".") or _extension_from_content_type( - getattr(part, "content_type", "") - ) - return blob, extension or "bin", relationship_id, getattr(part, "content_type", "") - return None - - -def _render_element(shape, element_id: str, source_path: Path, asset_dir: Path | None, embed_media: bool) -> dict[str, Any]: - return { - "id": element_id, - "xml": shape._element.xml, - "relationships": _relationship_payloads(shape, element_id, source_path, asset_dir, embed_media), - } - - -def _relationship_payloads( - shape, - element_id: str, - source_path: Path, - asset_dir: Path | None, - embed_media: bool, -) -> list[dict[str, Any]]: - payloads: list[dict[str, Any]] = [] - for relationship_id in _embedded_relationship_ids(shape): - try: - relationship = shape.part.rels[relationship_id] - except KeyError: - continue - payload: dict[str, Any] = { - "source_rid": relationship_id, - "reltype": relationship.reltype, - "target_ref": relationship.target_ref, - "is_external": bool(relationship.is_external), - } - if relationship.is_external: - payloads.append(payload) - continue - part = relationship.target_part - blob = getattr(part, "blob", None) - if blob: - content_type = getattr(part, "content_type", "") - extension = Path(str(getattr(part, "partname", "media.bin"))).suffix.lstrip(".") or _extension_from_content_type(content_type) - payload.update({"content_type": content_type, "extension": extension or "bin"}) - if asset_dir is not None: - asset_path = asset_dir / f"{source_path.stem}_{element_id}_{relationship_id}.{payload['extension']}" - asset_path.write_bytes(blob) - payload["path"] = str(asset_path) - elif embed_media: - payload["blob_base64"] = b64encode(blob).decode("ascii") - payloads.append(payload) - return payloads - - -def _embedded_relationship_ids(shape) -> list[str]: - relationship_ids: list[str] = [] - try: - nodes = shape._element.iter() - except AttributeError: - return relationship_ids - for node in nodes: - for attribute_name, value in node.attrib.items(): - if attribute_name.endswith("}embed") and value not in relationship_ids: - relationship_ids.append(value) - return relationship_ids - - -def _extension_from_content_type(content_type: str) -> str: - mapping = { - "image/svg+xml": "svg", - "image/png": "png", - "image/jpeg": "jpg", - "image/jpg": "jpg", - "image/gif": "gif", - "image/bmp": "bmp", - "image/x-emf": "emf", - "image/x-wmf": "wmf", - } - return mapping.get(content_type.lower(), "bin") - - -def _stat_key(slide_object: dict[str, Any]) -> str: - kind = slide_object["kind"] - if kind == "table": - return "tables" - if kind == "chart": - return "charts" - if kind == "image": - return "images" - if kind == "text": - return "text_objects" - if kind == "line": - return "lines_or_freeforms" - return "shapes" - - -def _slide_title(objects) -> str: - for slide_object in objects: - text = str(slide_object["content"].get("text", "")).strip() - if text: - return text.splitlines()[0][:100] - return "" - - -def _contains_non_ascii(value: str) -> bool: - return any(ord(character) > 127 for character in value) - - -def _slide_text_fragments(shapes) -> list[str]: - fragments: list[str] = [] - for shape in shapes: - if hasattr(shape, "shapes"): - fragments.extend(_slide_text_fragments(shape.shapes)) - if getattr(shape, "has_table", False): - for row in shape.table.rows: - row_text = " | ".join(_compact_text(cell.text) for cell in row.cells if _compact_text(cell.text)) - if row_text: - fragments.append(row_text) - text = getattr(shape, "text", "") - if text: - fragments.append(text) - deduped: list[str] = [] - seen: set[str] = set() - for fragment in fragments: - cleaned = _compact_text(fragment) - if cleaned and cleaned not in seen: - seen.add(cleaned) - deduped.append(cleaned) - return deduped - - -def _compact_text(value: str) -> str: - return " ".join(str(value).split()) - - -def _package_asset_counts(path: Path) -> tuple[int, int]: - with zipfile.ZipFile(path) as package: - names = package.namelist() - media = sum(1 for name in names if name.startswith("ppt/media/")) - embedded = sum(1 for name in names if name.startswith("ppt/embeddings/")) - return media, embedded - - -def _extract_package_media(path: Path, asset_dir: Path) -> None: - with zipfile.ZipFile(path) as package: - for name in package.namelist(): - if not name.startswith("ppt/media/"): - continue - target = asset_dir / Path(name).name - target.write_bytes(package.read(name)) - - -def _notes_by_slide(path: Path) -> dict[int, list[str]]: - notes: dict[int, list[str]] = {} - with zipfile.ZipFile(path) as package: - names = set(package.namelist()) - slide_rels = sorted(name for name in names if name.startswith("ppt/slides/_rels/slide") and name.endswith(".xml.rels")) - for rel_path in slide_rels: - slide_number = int(Path(rel_path).name.removeprefix("slide").removesuffix(".xml.rels")) - rel_root = ElementTree.fromstring(package.read(rel_path)) - for rel in rel_root: - if "notesSlide" not in rel.attrib.get("Type", ""): - continue - target = rel.attrib.get("Target", "") - notes_path = posixpath.normpath(posixpath.join("ppt/slides", target)) - if notes_path not in names: - notes_path = posixpath.normpath(posixpath.join("ppt/slides/_rels", target)) - if notes_path not in names: - continue - notes_root = ElementTree.fromstring(package.read(notes_path)) - text = "\n".join(node.text for node in notes_root.iter(f"{DRAWING_NS}t") if node.text) - if text.strip(): - notes[slide_number] = [text.strip()] - return notes diff --git a/plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py b/plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py deleted file mode 100644 index e5f02874a..000000000 --- a/plugins/pptify/.agent/pptify-plugin/extraction/pptx_style_master.py +++ /dev/null @@ -1,505 +0,0 @@ -from __future__ import annotations - -import zipfile -from collections import Counter -from pathlib import Path -from typing import Any, Iterable -from xml.etree import ElementTree - -EMU_PER_INCH = 914400 -DRAWING_NS = "{http://schemas.openxmlformats.org/drawingml/2006/main}" - - -class PptxStyleMaster: - """Extract compact design context from a reference PPTX for prompt-based generation.""" - - def __init__(self, max_slides: int = 12, max_items: int = 10) -> None: - self.max_slides = max_slides - self.max_items = max_items - - def analyze(self, path: str | Path) -> dict[str, Any]: - from pptx import Presentation - - pptx_path = Path(path) - presentation = Presentation(str(pptx_path)) - slide_size = { - "width": _inches(presentation.slide_width), - "height": _inches(presentation.slide_height), - } - theme = _theme_from_package(pptx_path) - - colors: Counter[str] = Counter() - fonts: Counter[str] = Counter() - font_sizes: Counter[float] = Counter() - shape_styles: Counter[str] = Counter() - layout_names: Counter[str] = Counter() - master_names: Counter[str] = Counter() - slide_layouts: list[dict[str, Any]] = [] - - _count_theme_tokens(theme, colors, fonts) - for slide_index, slide in enumerate(presentation.slides, start=1): - if slide_index > self.max_slides: - break - slide_context = _slide_design_context(slide, slide_index, slide_size, self.max_items) - slide_layouts.append(slide_context) - layout_names[slide_context["template_layout"]] += 1 - master_names[slide_context["template_master"]] += 1 - colors.update(slide_context.pop("_colors")) - fonts.update(slide_context.pop("_fonts")) - font_sizes.update(slide_context.pop("_font_sizes")) - shape_styles.update(slide_context.pop("_shape_styles")) - - return { - "styles": { - "colors": _top_items(colors, self.max_items), - "fonts": _top_items(fonts, self.max_items), - "font_sizes": _top_items(font_sizes, self.max_items), - "shape_styles": _top_items(shape_styles, self.max_items), - }, - "brands": _brand_context(colors, fonts, theme, self.max_items), - "template": _template_context(presentation, slide_size, theme, layout_names, master_names, self.max_items), - "layout": { - "analyzed_slide_count": len(slide_layouts), - "layout_usage": _top_items(layout_names, self.max_items), - "master_usage": _top_items(master_names, self.max_items), - "slides": slide_layouts, - }, - } - - -def extract_pptx_style_master(path: str | Path, max_slides: int = 12, max_items: int = 10) -> dict[str, Any]: - return PptxStyleMaster(max_slides=max_slides, max_items=max_items).analyze(path) - - -def _slide_design_context(slide, slide_index: int, slide_size: dict[str, float], max_items: int) -> dict[str, Any]: - colors: Counter[str] = Counter() - fonts: Counter[str] = Counter() - font_sizes: Counter[float] = Counter() - shape_styles: Counter[str] = Counter() - object_counts: Counter[str] = Counter() - regions: Counter[str] = Counter() - placeholders: list[dict[str, Any]] = [] - object_samples: list[dict[str, Any]] = [] - boxes: list[dict[str, float]] = [] - - for shape in _iter_shapes(slide.shapes): - kind = _shape_kind(shape) - bbox = _bbox(shape) - boxes.append(bbox) - object_counts[kind] += 1 - regions[_region(bbox, slide_size)] += 1 - - shape_colors = _shape_colors(shape) - colors.update(shape_colors.values()) - shape_text = _text_preview(shape) - text_styles = _text_styles(shape) - fonts.update(text_styles["fonts"]) - font_sizes.update(text_styles["font_sizes"]) - colors.update(text_styles["colors"]) - - style_signature = _style_signature(shape_colors, text_styles) - if style_signature: - shape_styles[style_signature] += 1 - - if getattr(shape, "is_placeholder", False) and len(placeholders) < max_items: - placeholders.append(_placeholder_context(shape, bbox)) - - if len(object_samples) < max_items: - sample: dict[str, Any] = { - "kind": kind, - "role": _shape_role(shape, kind), - "bbox": bbox, - "region": _region(bbox, slide_size), - } - if shape_text: - sample["text"] = shape_text - if shape_colors: - sample["colors"] = shape_colors - if text_styles["fonts"]: - sample["fonts"] = _top_items(text_styles["fonts"], 3) - if text_styles["font_sizes"]: - sample["font_sizes"] = _top_items(text_styles["font_sizes"], 3) - object_samples.append(sample) - - return { - "index": slide_index, - "template_layout": _slide_layout_name(slide), - "template_master": _slide_master_name(slide), - "object_counts": dict(sorted(object_counts.items())), - "placeholder_count": len(placeholders), - "placeholders": placeholders, - "dominant_regions": _top_items(regions, max_items), - "dominant_flow": _dominant_flow(boxes, slide_size), - "occupied_area_ratio": _occupied_area_ratio(boxes, slide_size), - "objects": object_samples, - "_colors": colors, - "_fonts": fonts, - "_font_sizes": font_sizes, - "_shape_styles": shape_styles, - } - - -def _template_context( - presentation, - slide_size: dict[str, float], - theme: dict[str, Any], - layout_names: Counter[str], - master_names: Counter[str], - max_items: int, -) -> dict[str, Any]: - masters: list[dict[str, Any]] = [] - try: - for master_index, master in enumerate(presentation.slide_masters, start=1): - masters.append( - { - "index": master_index, - "name": str(getattr(master, "name", f"Master {master_index}") or f"Master {master_index}"), - "layout_count": len(master.slide_layouts), - } - ) - except (AttributeError, TypeError): - masters = [] - - return { - "slide_size": slide_size, - "theme": theme, - "masters": masters[:max_items], - "layout_usage": _top_items(layout_names, max_items), - "master_usage": _top_items(master_names, max_items), - } - - -def _brand_context(colors: Counter[str], fonts: Counter[str], theme: dict[str, Any], max_items: int) -> dict[str, Any]: - theme_colors = theme.get("colors", {}) if isinstance(theme.get("colors"), dict) else {} - theme_accents = [value for name, value in theme_colors.items() if str(name).startswith("accent")] - palette = _ranked_colors(colors, include_neutral=False) - if not palette: - palette = [color for color in theme_accents if _is_hex_color(color)] - neutral_palette = _ranked_colors(colors, include_neutral=True, only_neutral=True) - font_values = [str(item["value"]) for item in _top_items(fonts, max_items)] - primary_color = palette[0] if palette else None - accent_colors = _dedupe([*palette, *theme_accents])[:max_items] - - return { - "theme_name": theme.get("name"), - "primary_color": primary_color, - "accent_colors": accent_colors, - "neutral_colors": neutral_palette[:max_items], - "fonts": font_values[:max_items], - "theme_colors": theme_colors, - "theme_fonts": theme.get("fonts", {}), - } - - -def _theme_from_package(path: Path) -> dict[str, Any]: - theme_paths: list[str] - try: - with zipfile.ZipFile(path) as package: - theme_paths = sorted(name for name in package.namelist() if name.startswith("ppt/theme/theme") and name.endswith(".xml")) - if not theme_paths: - return {"name": None, "colors": {}, "fonts": {}} - root = ElementTree.fromstring(package.read(theme_paths[0])) - except (zipfile.BadZipFile, KeyError, ElementTree.ParseError): - return {"name": None, "colors": {}, "fonts": {}} - - theme = { - "name": root.attrib.get("name"), - "path": theme_paths[0], - "colors": {}, - "fonts": {}, - } - color_scheme = root.find(f".//{DRAWING_NS}clrScheme") - if color_scheme is not None: - colors: dict[str, str] = {} - for color_node in list(color_scheme): - color_value = _theme_color_value(color_node) - if color_value: - colors[color_node.tag.rsplit("}", 1)[-1]] = color_value - theme["colors"] = colors - - font_scheme = root.find(f".//{DRAWING_NS}fontScheme") - if font_scheme is not None: - fonts: dict[str, str] = {} - for key, node_name in (("major", "majorFont"), ("minor", "minorFont")): - latin = font_scheme.find(f".//{DRAWING_NS}{node_name}/{DRAWING_NS}latin") - if latin is not None and latin.attrib.get("typeface"): - fonts[key] = latin.attrib["typeface"] - theme["fonts"] = fonts - return theme - - -def _theme_color_value(color_node: ElementTree.Element) -> str | None: - srgb = color_node.find(f".//{DRAWING_NS}srgbClr") - if srgb is not None and srgb.attrib.get("val"): - return _normalize_hex(srgb.attrib["val"]) - system = color_node.find(f".//{DRAWING_NS}sysClr") - if system is not None and system.attrib.get("lastClr"): - return _normalize_hex(system.attrib["lastClr"]) - return None - - -def _count_theme_tokens(theme: dict[str, Any], colors: Counter[str], fonts: Counter[str]) -> None: - for color in theme.get("colors", {}).values() if isinstance(theme.get("colors"), dict) else []: - if _is_hex_color(color): - colors[color] += 1 - for font in theme.get("fonts", {}).values() if isinstance(theme.get("fonts"), dict) else []: - if font: - fonts[str(font)] += 1 - - -def _iter_shapes(shapes) -> Iterable[Any]: - for shape in shapes: - yield shape - if hasattr(shape, "shapes"): - yield from _iter_shapes(shape.shapes) - - -def _shape_kind(shape) -> str: - shape_type = str(getattr(getattr(shape, "shape_type", "unknown"), "name", "unknown")).lower() - if getattr(shape, "has_table", False): - return "table" - if getattr(shape, "has_chart", False): - return "chart" - if "picture" in shape_type or _has_image(shape): - return "image" - if "line" in shape_type or "connector" in shape_type or "freeform" in shape_type: - return "line" - if getattr(shape, "has_text_frame", False) and getattr(shape, "text", "").strip(): - return "text" - if hasattr(shape, "shapes"): - return "group" - return "shape" - - -def _has_image(shape) -> bool: - try: - return getattr(shape, "image", None) is not None - except (AttributeError, TypeError, ValueError): - return False - - -def _shape_role(shape, kind: str) -> str: - if getattr(shape, "is_placeholder", False): - try: - return str(shape.placeholder_format.type).split(".")[-1].lower() - except (AttributeError, ValueError): - return "placeholder" - name = str(getattr(shape, "name", "") or "").strip().lower() - if "title" in name: - return "title" - return kind - - -def _shape_colors(shape) -> dict[str, str]: - colors: dict[str, str] = {} - fill = _format_color(_safe_attr(_safe_attr(shape, "fill"), "fore_color")) - if fill: - colors["fill"] = fill - line = _format_color(_safe_attr(_safe_attr(shape, "line"), "color")) - if line: - colors["line"] = line - return colors - - -def _safe_attr(value: Any, name: str) -> Any: - if value is None: - return None - try: - return getattr(value, name) - except (AttributeError, TypeError, ValueError): - return None - - -def _text_styles(shape) -> dict[str, Counter[Any]]: - fonts: Counter[str] = Counter() - font_sizes: Counter[float] = Counter() - colors: Counter[str] = Counter() - text_frame = getattr(shape, "text_frame", None) - if text_frame is None: - return {"fonts": fonts, "font_sizes": font_sizes, "colors": colors} - - for paragraph in text_frame.paragraphs: - _count_font(paragraph.font, fonts, font_sizes, colors) - for run in paragraph.runs: - _count_font(run.font, fonts, font_sizes, colors) - return {"fonts": fonts, "font_sizes": font_sizes, "colors": colors} - - -def _count_font(font, fonts: Counter[str], font_sizes: Counter[float], colors: Counter[str]) -> None: - name = getattr(font, "name", None) - if name: - fonts[str(name)] += 1 - size = getattr(font, "size", None) - if size is not None: - font_sizes[round(size.pt, 2)] += 1 - color = _format_color(getattr(font, "color", None)) - if color: - colors[color] += 1 - - -def _format_color(color_format) -> str | None: - if color_format is None: - return None - try: - rgb = color_format.rgb - except (AttributeError, TypeError, ValueError): - rgb = None - if rgb is not None: - return _normalize_hex(str(rgb)) - try: - theme_color = color_format.theme_color - except (AttributeError, TypeError, ValueError): - theme_color = None - if theme_color: - token = str(theme_color).split(".")[-1].lower() - return f"theme:{token}" - return None - - -def _style_signature(shape_colors: dict[str, str], text_styles: dict[str, Counter[Any]]) -> str: - parts: list[str] = [] - if shape_colors.get("fill"): - parts.append(f"fill={shape_colors['fill']}") - if shape_colors.get("line"): - parts.append(f"line={shape_colors['line']}") - font = _top_value(text_styles["fonts"]) - if font: - parts.append(f"font={font}") - font_size = _top_value(text_styles["font_sizes"]) - if font_size: - parts.append(f"font_size={font_size}") - return "; ".join(parts) - - -def _placeholder_context(shape, bbox: dict[str, float]) -> dict[str, Any]: - context: dict[str, Any] = { - "name": str(getattr(shape, "name", "") or ""), - "bbox": bbox, - } - try: - context["type"] = str(shape.placeholder_format.type).split(".")[-1].lower() - context["idx"] = int(shape.placeholder_format.idx) - except (AttributeError, ValueError): - context["type"] = "placeholder" - return context - - -def _text_preview(shape, max_chars: int = 120) -> str: - text = " ".join(str(getattr(shape, "text", "")).split()) - return text[:max_chars] - - -def _bbox(shape) -> dict[str, float]: - return { - "x": _inches(getattr(shape, "left", 0)), - "y": _inches(getattr(shape, "top", 0)), - "width": max(0.0, _inches(getattr(shape, "width", 0))), - "height": max(0.0, _inches(getattr(shape, "height", 0))), - } - - -def _region(bbox: dict[str, float], slide_size: dict[str, float]) -> str: - width = max(slide_size["width"], 0.01) - height = max(slide_size["height"], 0.01) - center_x = (bbox["x"] + bbox["width"] / 2) / width - center_y = (bbox["y"] + bbox["height"] / 2) / height - horizontal = "left" if center_x < 0.34 else "right" if center_x > 0.66 else "center" - vertical = "top" if center_y < 0.34 else "bottom" if center_y > 0.66 else "middle" - return f"{vertical}_{horizontal}" - - -def _dominant_flow(boxes: list[dict[str, float]], slide_size: dict[str, float]) -> str: - if len(boxes) < 2: - return "single" - centers_x = [(box["x"] + box["width"] / 2) / max(slide_size["width"], 0.01) for box in boxes] - centers_y = [(box["y"] + box["height"] / 2) / max(slide_size["height"], 0.01) for box in boxes] - spread_x = max(centers_x) - min(centers_x) - spread_y = max(centers_y) - min(centers_y) - if len(boxes) >= 4 and spread_x > 0.32 and spread_y > 0.32: - return "grid" - if spread_x > 0.42 and spread_y > 0.42: - return "grid" - if len(boxes) >= 3 and spread_x > 0.42: - return "grid" - if spread_x > spread_y * 1.4: - return "row" - if spread_y > spread_x * 1.4: - return "column" - return "overlay_or_balanced" - - -def _occupied_area_ratio(boxes: list[dict[str, float]], slide_size: dict[str, float]) -> float: - slide_area = max(slide_size["width"] * slide_size["height"], 0.01) - object_area = sum(box["width"] * box["height"] for box in boxes) - return round(min(object_area / slide_area, 1.0), 3) - - -def _slide_layout_name(slide) -> str: - try: - return str(slide.slide_layout.name or "unnamed_layout") - except AttributeError: - return "unknown_layout" - - -def _slide_master_name(slide) -> str: - try: - master = slide.slide_layout.slide_master - return str(master.name or "unnamed_master") - except AttributeError: - return "unknown_master" - - -def _top_items(counter: Counter[Any], limit: int) -> list[dict[str, Any]]: - return [{"value": value, "count": count} for value, count in counter.most_common(limit)] - - -def _top_value(counter: Counter[Any]) -> Any | None: - if not counter: - return None - return counter.most_common(1)[0][0] - - -def _ranked_colors(colors: Counter[str], include_neutral: bool, only_neutral: bool = False) -> list[str]: - ranked: list[str] = [] - for color, _count in colors.most_common(): - if not _is_hex_color(color): - continue - neutral = _is_neutral(color) - if only_neutral and not neutral: - continue - if not include_neutral and neutral: - continue - ranked.append(color) - return ranked - - -def _is_neutral(color: str) -> bool: - if not _is_hex_color(color): - return False - red = int(color[1:3], 16) - green = int(color[3:5], 16) - blue = int(color[5:7], 16) - return max(red, green, blue) - min(red, green, blue) <= 18 - - -def _is_hex_color(value: Any) -> bool: - return isinstance(value, str) and len(value) == 7 and value.startswith("#") - - -def _normalize_hex(value: str) -> str: - stripped = value.strip().lstrip("#") - if len(stripped) >= 6: - return f"#{stripped[:6].upper()}" - return f"#{stripped.upper()}" - - -def _dedupe(values: Iterable[str]) -> list[str]: - deduped: list[str] = [] - for value in values: - if value and value not in deduped: - deduped.append(value) - return deduped - - -def _inches(value: int) -> float: - return round(int(value) / EMU_PER_INCH, 4) diff --git a/plugins/pptify/.agent/pptify-plugin/images/__init__.py b/plugins/pptify/.agent/pptify-plugin/images/__init__.py deleted file mode 100644 index cd7df68fc..000000000 --- a/plugins/pptify/.agent/pptify-plugin/images/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Image and infographic helpers for pptify plugins.""" \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py b/plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py deleted file mode 100644 index 3c5093731..000000000 --- a/plugins/pptify/.agent/pptify-plugin/images/iconfy_search.py +++ /dev/null @@ -1,179 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import re -import sys -from typing import Any -from urllib.parse import urlencode -from urllib.request import Request, urlopen - - -ICONIFY_API_HOSTS = ( - "https://api.iconify.design", - "https://api.simplesvg.com", - "https://api.unisvg.com", -) - -ICONIFY_COLLECTIONS: dict[str, list[str]] = { - "all": [ - "mdi:trending-up", - "lucide:brain", - "tabler:building-skyscraper", - "fa6-solid:rocket", - "ph:chart-line-up-bold", - "fluent:people-team-24-regular", - ], - "mdi": ["mdi:trending-up", "mdi:brain", "mdi:domain", "mdi:rocket-outline"], - "lucide": ["lucide:brain", "lucide:line-chart", "lucide:building-2", "lucide:rocket"], - "tabler": ["tabler:building-skyscraper", "tabler:chart-line", "tabler:bulb", "tabler:target-arrow"], - "ph": ["ph:chart-line-up-bold", "ph:brain-bold", "ph:buildings-bold", "ph:rocket-launch-bold"], - "fa6-solid": ["fa6-solid:rocket", "fa6-solid:chart-line", "fa6-solid:building", "fa6-solid:lightbulb"], - "fluent": [ - "fluent:people-team-24-regular", - "fluent:brain-circuit-24-regular", - "fluent:building-24-regular", - "fluent:arrow-trending-24-regular", - ], -} - - -def get_default_iconify_prefix(collection: str = "all") -> str: - return "mdi" if collection == "all" else collection - - -def normalize_icon_name(value: str | None, collection: str = "all") -> str | None: - raw = (value or "").strip().lower() - if not raw: - return None - raw = raw.replace(" ", "-") - if ":" in raw: - return raw - return f"{get_default_iconify_prefix(collection)}:{raw}" - - -def build_iconify_svg_url(icon_name: str, color_hex: str | None = None, *, host_index: int = 0, collection: str = "all") -> str: - normalized = normalize_icon_name(icon_name, collection) - if not normalized or ":" not in normalized: - raise ValueError(f"Invalid Iconify icon name: {icon_name}") - prefix, name = normalized.split(":", 1) - query: dict[str, str] = {"box": "1"} - if color_hex: - query["color"] = color_hex if color_hex.startswith("#") else f"#{color_hex}" - host = ICONIFY_API_HOSTS[min(max(host_index, 0), len(ICONIFY_API_HOSTS) - 1)] - return f"{host}/{prefix}/{name}.svg?{urlencode(query)}" - - -def _request_json(host: str, path: str, params: dict[str, str]) -> dict[str, Any]: - url = f"{host}{path}?{urlencode(params)}" - request = Request(url, headers={"User-Agent": "pptify-plugin/0.1"}) - with urlopen(request, timeout=15) as response: - return json.loads(response.read().decode("utf-8", errors="replace")) - - -def _title_for_icon(icon_id: str) -> str: - name = icon_id.split(":", 1)[-1] - return re.sub(r"[-_]+", " ", name).strip().title() or icon_id - - -def _candidate(icon_id: str, query: str, color_hex: str | None) -> dict[str, str | None]: - prefix = icon_id.split(":", 1)[0] if ":" in icon_id else None - return { - "provider": "iconify", - "searchQuery": query, - "iconId": icon_id, - "prefix": prefix, - "name": icon_id.split(":", 1)[-1], - "title": _title_for_icon(icon_id), - "svgUrl": build_iconify_svg_url(icon_id, color_hex), - "sourceUrl": f"https://icon-sets.iconify.design/{icon_id.replace(':', '/')}/", - } - - -def _fallback_icons(query: str, collection: str, max_num: int) -> list[str]: - examples = ICONIFY_COLLECTIONS.get(collection, ICONIFY_COLLECTIONS["all"]) - terms = {term for term in re.split(r"[^a-z0-9]+", query.lower()) if term} - matched = [icon for icon in examples if any(term in icon for term in terms)] - icons = matched or examples - return icons[:max_num] - - -def search_icons(query: str, *, collection: str = "all", max_num: int = 12, color_hex: str | None = None) -> tuple[list[dict[str, str | None]], list[str]]: - collection = collection.strip().lower() or "all" - max_num = max(1, min(max_num, 50)) - errors: list[str] = [] - icon_ids: list[str] = [] - - normalized_direct = normalize_icon_name(query, collection) - if normalized_direct and ":" in normalized_direct and " " not in query.strip(): - icon_ids.append(normalized_direct) - - params = {"query": query, "limit": str(max_num)} - if collection != "all": - params["prefix"] = collection - - for host in ICONIFY_API_HOSTS: - if len(icon_ids) >= max_num: - break - try: - data = _request_json(host, "/search", params) - except Exception as exc: - errors.append(f"{host}: {exc}") - continue - - for icon in data.get("icons", []): - if not isinstance(icon, str): - continue - normalized = normalize_icon_name(icon, collection) - if normalized and normalized not in icon_ids: - icon_ids.append(normalized) - if len(icon_ids) >= max_num: - break - - if not icon_ids: - icon_ids.extend(_fallback_icons(query, collection, max_num)) - - candidates = [_candidate(icon_id, query, color_hex) for icon_id in icon_ids[:max_num]] - return candidates, errors - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Search Iconify icons and return SVG URLs.") - parser.add_argument("--query", action="append", required=True, help="Icon search query or full icon ID. Can be repeated.") - parser.add_argument("--collection", default="all", help="Iconify collection prefix such as mdi, lucide, tabler, ph, fa6-solid, fluent, or all.") - parser.add_argument("--color", help="Optional SVG color hex value.") - parser.add_argument("--max-num", type=int, default=12, help="Maximum candidates per query.") - parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") - return parser - - -def main() -> int: - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - args = build_parser().parse_args() - queries = [query.strip() for query in args.query if query.strip()] - candidates: list[dict[str, str | None]] = [] - errors: list[str] = [] - for query in queries: - query_candidates, query_errors = search_icons( - query, - collection=args.collection, - max_num=args.max_num, - color_hex=args.color, - ) - candidates.extend(query_candidates) - errors.extend(query_errors) - - payload = { - "ok": bool(candidates), - "query": "\n".join(queries), - "collection": args.collection, - "candidates": candidates, - "errors": errors, - } - print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) - return 0 if payload["ok"] else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py b/plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py deleted file mode 100644 index 1f74bf429..000000000 --- a/plugins/pptify/.agent/pptify-plugin/images/raster_image_to_svg.py +++ /dev/null @@ -1,289 +0,0 @@ -from __future__ import annotations - -import argparse -import base64 -import html -import importlib -import json -import struct -import sys -from pathlib import Path -from typing import Any - - -MIME_BY_SUFFIX = { - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".bmp": "image/bmp", - ".webp": "image/webp", -} - -RASTER_MODE = "embedded-raster" -VECTOR_MODE = "vector-trace" - - -def _png_size(data: bytes) -> tuple[int, int] | None: - if data.startswith(b"\x89PNG\r\n\x1a\n") and len(data) >= 24: - return struct.unpack(">II", data[16:24]) - return None - - -def _gif_size(data: bytes) -> tuple[int, int] | None: - if data[:6] in {b"GIF87a", b"GIF89a"} and len(data) >= 10: - return struct.unpack(" tuple[int, int] | None: - if not data.startswith(b"\xff\xd8"): - return None - index = 2 - while index + 9 < len(data): - if data[index] != 0xFF: - index += 1 - continue - marker = data[index + 1] - index += 2 - if marker in {0xD8, 0xD9}: - continue - if index + 2 > len(data): - break - segment_length = struct.unpack(">H", data[index:index + 2])[0] - if marker in {0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF}: - if index + 7 <= len(data): - height, width = struct.unpack(">HH", data[index + 3:index + 7]) - return width, height - break - index += max(segment_length, 2) - return None - - -def _webp_size(data: bytes) -> tuple[int, int] | None: - if len(data) < 30 or not data.startswith(b"RIFF") or data[8:12] != b"WEBP": - return None - chunk = data[12:16] - if chunk == b"VP8X" and len(data) >= 30: - width = int.from_bytes(data[24:27], "little") + 1 - height = int.from_bytes(data[27:30], "little") + 1 - return width, height - if chunk == b"VP8L" and len(data) >= 25: - bits = int.from_bytes(data[21:25], "little") - width = (bits & 0x3FFF) + 1 - height = ((bits >> 14) & 0x3FFF) + 1 - return width, height - return None - - -def image_dimensions(data: bytes) -> tuple[int, int]: - for reader in (_png_size, _jpeg_size, _gif_size, _webp_size): - size = reader(data) - if size: - return size - try: - from io import BytesIO - - from PIL import Image - - with Image.open(BytesIO(data)) as image: - return image.size - except Exception: - return 1, 1 - - -def _trace_with_vtracer( - source_path: Path, - destination: Path, - *, - colormode: str, - hierarchical: str, - trace_mode: str, - filter_speckle: int, - color_precision: int, - layer_difference: int, - corner_threshold: int, - length_threshold: float, - max_iterations: int, - splice_threshold: int, - path_precision: int, -) -> dict[str, Any] | None: - try: - vtracer = importlib.import_module("vtracer") - except ImportError: - return { - "ok": False, - "error": "vtracer is not installed. Install plugin dependencies with vtracer support, or use --mode embedded-raster.", - "code": "vtracer_not_installed", - } - - try: - vtracer.convert_image_to_svg_py( - str(source_path), - str(destination), - colormode=colormode, - hierarchical=hierarchical, - mode=trace_mode, - filter_speckle=filter_speckle, - color_precision=color_precision, - layer_difference=layer_difference, - corner_threshold=corner_threshold, - length_threshold=length_threshold, - max_iterations=max_iterations, - splice_threshold=splice_threshold, - path_precision=path_precision, - ) - except Exception as exc: - return {"ok": False, "error": f"vtracer conversion failed: {exc}", "code": "vtracer_failed"} - - return None - - -def raster_to_svg( - source: str | Path, - output_path: str | Path | None = None, - *, - title: str | None = None, - mode: str = RASTER_MODE, - colormode: str = "color", - hierarchical: str = "stacked", - trace_mode: str = "spline", - filter_speckle: int = 4, - color_precision: int = 6, - layer_difference: int = 16, - corner_threshold: int = 60, - length_threshold: float = 4.0, - max_iterations: int = 10, - splice_threshold: int = 45, - path_precision: int = 3, -) -> dict[str, Any]: - source_path = Path(source).expanduser() - if not source_path.exists(): - return {"ok": False, "error": f"Source file does not exist: {source_path}", "code": "source_not_found"} - - destination = Path(output_path).expanduser() if output_path else source_path.with_suffix(".svg") - destination.parent.mkdir(parents=True, exist_ok=True) - - data = source_path.read_bytes() - width, height = image_dimensions(data) - - if mode == VECTOR_MODE: - error = _trace_with_vtracer( - source_path, - destination, - colormode=colormode, - hierarchical=hierarchical, - trace_mode=trace_mode, - filter_speckle=filter_speckle, - color_precision=color_precision, - layer_difference=layer_difference, - corner_threshold=corner_threshold, - length_threshold=length_threshold, - max_iterations=max_iterations, - splice_threshold=splice_threshold, - path_precision=path_precision, - ) - if error: - return error - return { - "ok": True, - "source": str(source_path), - "path": str(destination), - "width": width, - "height": height, - "mode": VECTOR_MODE, - "vectorizer": "vtracer", - "trace_options": { - "colormode": colormode, - "hierarchical": hierarchical, - "trace_mode": trace_mode, - "filter_speckle": filter_speckle, - "color_precision": color_precision, - "layer_difference": layer_difference, - "corner_threshold": corner_threshold, - "length_threshold": length_threshold, - "max_iterations": max_iterations, - "splice_threshold": splice_threshold, - "path_precision": path_precision, - }, - } - - if mode != RASTER_MODE: - return {"ok": False, "error": f"Unsupported mode: {mode}", "code": "unsupported_mode"} - - mime = MIME_BY_SUFFIX.get(source_path.suffix.lower(), "application/octet-stream") - encoded = base64.b64encode(data).decode("ascii") - safe_title = html.escape(title or source_path.stem) - svg = ( - f'\n' - f" {safe_title}\n" - f' \n' - "\n" - ) - - destination.write_text(svg, encoding="utf-8") - return { - "ok": True, - "source": str(source_path), - "path": str(destination), - "width": width, - "height": height, - "mode": "embedded-raster", - "mime": mime, - } - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Wrap a raster image in a valid SVG image element.") - parser.add_argument("--source", required=True, help="Path to a raster image.") - parser.add_argument("--output-path", help="Destination SVG path. Defaults to the source name with .svg.") - parser.add_argument("--title", help="Accessible SVG title.") - parser.add_argument( - "--mode", - choices=(RASTER_MODE, VECTOR_MODE), - default=RASTER_MODE, - help="Conversion mode. embedded-raster wraps the source bytes; vector-trace uses optional vtracer paths.", - ) - parser.add_argument("--colormode", choices=("color", "binary"), default="color", help="vtracer color mode.") - parser.add_argument("--hierarchical", choices=("stacked", "cutout"), default="stacked", help="vtracer color clustering mode.") - parser.add_argument("--trace-mode", choices=("spline", "polygon", "none"), default="spline", help="vtracer curve fitting mode.") - parser.add_argument("--filter-speckle", type=int, default=4, help="Discard traced patches smaller than this pixel count.") - parser.add_argument("--color-precision", type=int, default=6, help="Number of significant bits per RGB channel for vtracer.") - parser.add_argument("--layer-difference", type=int, default=16, help="Color difference between vtracer gradient layers.") - parser.add_argument("--corner-threshold", type=int, default=60, help="Minimum angle in degrees to be considered a corner.") - parser.add_argument("--length-threshold", type=float, default=4.0, help="vtracer segment length threshold.") - parser.add_argument("--max-iterations", type=int, default=10, help="vtracer smoothing iteration cap.") - parser.add_argument("--splice-threshold", type=int, default=45, help="Minimum angle displacement in degrees to splice a spline.") - parser.add_argument("--path-precision", type=int, default=3, help="Decimal places to use in traced SVG path data.") - parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") - return parser - - -def main() -> int: - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - args = build_parser().parse_args() - payload = raster_to_svg( - args.source, - args.output_path, - title=args.title, - mode=args.mode, - colormode=args.colormode, - hierarchical=args.hierarchical, - trace_mode=args.trace_mode, - filter_speckle=args.filter_speckle, - color_precision=args.color_precision, - layer_difference=args.layer_difference, - corner_threshold=args.corner_threshold, - length_threshold=args.length_threshold, - max_iterations=args.max_iterations, - splice_threshold=args.splice_threshold, - path_precision=args.path_precision, - ) - print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) - return 0 if payload.get("ok") else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py b/plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py deleted file mode 100644 index 2bb4c2899..000000000 --- a/plugins/pptify/.agent/pptify-plugin/images/text_prompt_to_infographic.py +++ /dev/null @@ -1,318 +0,0 @@ -from __future__ import annotations - -import argparse -import base64 -import json -import os -import shutil -import subprocess -import sys -from pathlib import Path -from typing import Any -from urllib.error import HTTPError -from urllib.parse import urljoin -from urllib.request import Request, urlopen - -_ENV_TEMPLATE = ".env.template" - - -def _env_guidance() -> str: - return f"Copy {_ENV_TEMPLATE} to .env, fill the required image-provider values, then rerun the command." - - -def _dotenv_paths() -> list[Path]: - paths: list[Path] = [] - for base in (Path.cwd(), Path(__file__).resolve().parents[2]): - candidate = base / ".env" - if candidate not in paths: - paths.append(candidate) - return paths - - -def _load_dotenv() -> list[str]: - loaded: list[str] = [] - for env_path in _dotenv_paths(): - if not env_path.is_file(): - continue - for line in env_path.read_text(encoding="utf-8").splitlines(): - item = line.strip() - if not item or item.startswith("#"): - continue - if item.startswith("export "): - item = item[7:].strip() - if "=" not in item: - continue - name, value = item.split("=", 1) - name = name.strip() - value = value.strip().strip('"').strip("'") - if name and name not in os.environ: - os.environ[name] = value - if str(env_path) not in loaded: - loaded.append(str(env_path)) - return loaded - - -def _post_json(url: str, headers: dict[str, str], payload: dict[str, Any], *, timeout: int = 180) -> dict[str, Any]: - body = json.dumps(payload).encode("utf-8") - request = Request(url, data=body, headers={**headers, "Content-Type": "application/json"}, method="POST") - try: - with urlopen(request, timeout=timeout) as response: - return json.loads(response.read().decode("utf-8", errors="replace")) - except HTTPError as exc: - detail = exc.read().decode("utf-8", errors="replace") - raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc - - -def _download_bytes(url: str, *, timeout: int = 120) -> bytes: - request = Request(url, headers={"User-Agent": "pptify-plugin/0.1"}) - with urlopen(request, timeout=timeout) as response: - return response.read() - - -def _extract_image_bytes(response: dict[str, Any]) -> bytes: - data = response.get("data") - if not isinstance(data, list) or not data: - raise RuntimeError(f"Image response did not include data: {response}") - first = data[0] - if not isinstance(first, dict): - raise RuntimeError(f"Unexpected image response item: {first}") - if isinstance(first.get("b64_json"), str): - return base64.b64decode(first["b64_json"]) - if isinstance(first.get("url"), str): - return _download_bytes(first["url"]) - raise RuntimeError(f"Image response did not include b64_json or url: {first}") - - -def build_infographic_prompt(content: str, *, style: str = "", audience: str = "") -> str: - parts = [ - "Create a clean, presentation-ready infographic image.", - "Use clear hierarchy, concise labels, business-safe visuals, and enough whitespace for slide use.", - ] - if style.strip(): - parts.append(f"Style preferences: {style.strip()}") - if audience.strip(): - parts.append(f"Audience: {audience.strip()}") - parts.append("Content to visualize:") - parts.append(content.strip()) - return "\n".join(parts) - - -def generate_with_openai(prompt: str, output_path: Path, *, model: str | None, size: str) -> dict[str, Any]: - api_key = os.environ.get("OPENAI_API_KEY", "").strip() - if not api_key: - return {"ok": False, "error": f"OPENAI_API_KEY is required for the OpenAI image provider. {_env_guidance()}", "code": "missing_credentials"} - - selected_model = model or os.environ.get("OPENAI_IMAGE_MODEL", "gpt-image-1") - payload: dict[str, Any] = {"model": selected_model, "prompt": prompt, "size": size, "n": 1} - if not selected_model.startswith("gpt-image-1"): - payload["response_format"] = "b64_json" - response = _post_json( - "https://api.openai.com/v1/images/generations", - {"Authorization": f"Bearer {api_key}"}, - payload, - ) - image_bytes = _extract_image_bytes(response) - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_bytes(image_bytes) - return {"ok": True, "provider": "openai", "model": selected_model, "path": str(output_path), "size": size} - - -def generate_with_azure_openai(prompt: str, output_path: Path, *, model: str | None, size: str) -> dict[str, Any]: - endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT", "").strip() - api_key = os.environ.get("AZURE_OPENAI_API_KEY", "").strip() or os.environ.get("AZURE_AI_API_KEY", "").strip() - deployment = model or os.environ.get("AZURE_OPENAI_IMAGE_DEPLOYMENT", "").strip() or os.environ.get("MODEL_NAME", "").strip() - api_version = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-02-01").strip() - timeout_seconds = _env_int("AZURE_OPENAI_TIMEOUT", 300) - if not endpoint or not deployment: - return { - "ok": False, - "error": f"AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_IMAGE_DEPLOYMENT or MODEL_NAME are required. Set AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY or sign in with Azure CLI for Entra auth. {_env_guidance()}", - "code": "missing_credentials", - } - - endpoint = endpoint.rstrip("/") - if _is_openai_v1_endpoint(endpoint): - url = f"{endpoint}/images/generations" - payload = {"model": deployment, "prompt": prompt, "size": size, "n": 1} - provider = "azure-openai-v1" - else: - url = urljoin(f"{endpoint}/", f"openai/deployments/{deployment}/images/generations?api-version={api_version}") - payload = {"prompt": prompt, "size": size, "n": 1} - provider = "azure-openai" - response = _post_json(url, _azure_auth_headers(api_key), payload, timeout=timeout_seconds) - image_bytes = _extract_image_bytes(response) - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_bytes(image_bytes) - return {"ok": True, "provider": provider, "model": deployment, "path": str(output_path), "size": size} - - -def _is_openai_v1_endpoint(endpoint: str) -> bool: - return endpoint.rstrip("/").lower().endswith("/openai/v1") - - -def _env_int(name: str, default: int) -> int: - try: - return int(os.environ.get(name, str(default))) - except ValueError: - return default - - -def _azure_auth_headers(api_key: str) -> dict[str, str]: - if api_key: - return {"api-key": api_key} - return {"Authorization": f"Bearer {_azure_access_token()}"} - - -def _azure_access_token() -> str: - az_command = _azure_cli_command() - if az_command is None: - raise RuntimeError("AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY is required when Azure CLI is not available.") - try: - completed = subprocess.run( - [ - az_command, - "account", - "get-access-token", - "--resource", - "https://cognitiveservices.azure.com", - "--query", - "accessToken", - "-o", - "tsv", - ], - capture_output=True, - text=True, - encoding="utf-8", - timeout=30, - check=False, - ) - except FileNotFoundError as exc: - raise RuntimeError("AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY is required when Azure CLI is not available.") from exc - token = completed.stdout.strip() - if completed.returncode != 0 or not token: - raise RuntimeError("Could not acquire an Azure CLI token. Run `az login` or set AZURE_OPENAI_API_KEY/AZURE_AI_API_KEY.") - return token - - -def _azure_cli_command() -> str | None: - candidates = [ - os.environ.get("AZURE_CLI_PATH", "").strip(), - shutil.which("az"), - shutil.which("az.cmd"), - r"C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin\az.cmd", - ] - for candidate in candidates: - if not candidate: - continue - candidate_path = Path(candidate) - if candidate_path.is_file(): - return str(candidate_path) - resolved = shutil.which(candidate) - if resolved: - return resolved - return None - - -def generate_infographic( - content: str, - output_path: str | Path, - *, - style: str = "", - audience: str = "", - provider: str = "auto", - model: str | None = None, - size: str = "1024x1024", -) -> dict[str, Any]: - path = Path(output_path).expanduser() - prompt = build_infographic_prompt(content, style=style, audience=audience) - selected_provider = provider - if provider == "auto": - env_provider = os.environ.get("PPTIFY_IMAGE_PROVIDER", "").strip() - if env_provider in {"openai", "azure-openai"}: - selected_provider = env_provider - elif os.environ.get("OPENAI_API_KEY"): - selected_provider = "openai" - elif os.environ.get("AZURE_OPENAI_ENDPOINT") and (model or os.environ.get("AZURE_OPENAI_IMAGE_DEPLOYMENT") or os.environ.get("MODEL_NAME")): - selected_provider = "azure-openai" - else: - return { - "ok": False, - "error": f"No image provider is configured. {_env_guidance()} No built-in local fallback is available.", - "code": "missing_provider_config", - } - - try: - if selected_provider == "openai": - return generate_with_openai(prompt, path, model=model, size=size) - if selected_provider == "azure-openai": - return generate_with_azure_openai(prompt, path, model=model, size=size) - return {"ok": False, "error": f"Unknown provider: {provider}", "code": "unknown_provider"} - except Exception as exc: - return {"ok": False, "error": str(exc), "provider": selected_provider, "code": "generation_failed"} - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Generate an infographic image from a text prompt.") - prompt_group = parser.add_mutually_exclusive_group() - prompt_group.add_argument("--prompt", help="Prompt or source content to visualize.") - prompt_group.add_argument("--prompt-path", help="Path to a UTF-8 text prompt file.") - parser.add_argument("--output-path", required=True, help="Where to write the generated image.") - parser.add_argument("--style", default="", help="Optional visual style preferences.") - parser.add_argument("--audience", default="", help="Optional target audience.") - parser.add_argument("--provider", default="auto", choices=["auto", "openai", "azure-openai"], help="Image provider.") - parser.add_argument("--model", help="Provider-specific image model or deployment.") - parser.add_argument("--size", default="1024x1024", help="Provider image size, for example 1024x1024.") - parser.add_argument("--azure-endpoint", help="Azure OpenAI / Azure AI Foundry endpoint, for example https://.services.ai.azure.com/openai/v1.") - parser.add_argument("--azure-api-version", help="Azure OpenAI API version for legacy deployment endpoints. Defaults to AZURE_OPENAI_API_VERSION or 2024-02-01.") - parser.add_argument("--timeout", type=int, help="Provider request timeout in seconds. Defaults to AZURE_OPENAI_TIMEOUT or 300 for Azure.") - parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") - return parser - - -def _apply_runtime_settings(args: argparse.Namespace) -> None: - if args.azure_endpoint: - os.environ["AZURE_OPENAI_ENDPOINT"] = args.azure_endpoint - if args.azure_api_version: - os.environ["AZURE_OPENAI_API_VERSION"] = args.azure_api_version - if args.timeout is not None: - os.environ["AZURE_OPENAI_TIMEOUT"] = str(args.timeout) - - -def _read_prompt(args: argparse.Namespace) -> str: - if args.prompt_path: - return Path(args.prompt_path).read_text(encoding="utf-8") - if args.prompt: - return args.prompt - if not sys.stdin.isatty(): - return sys.stdin.read() - raise ValueError("Provide --prompt, --prompt-path, or stdin content.") - - -def main() -> int: - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - _load_dotenv() - args = build_parser().parse_args() - _apply_runtime_settings(args) - try: - content = _read_prompt(args) - except Exception as exc: - payload = {"ok": False, "error": str(exc), "code": "missing_prompt"} - print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) - return 1 - - payload = generate_infographic( - content, - args.output_path, - style=args.style, - audience=args.audience, - provider=args.provider, - model=args.model, - size=args.size, - ) - print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) - return 0 if payload.get("ok") else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-plugin/images/web_image_search.py b/plugins/pptify/.agent/pptify-plugin/images/web_image_search.py deleted file mode 100644 index f9b415b03..000000000 --- a/plugins/pptify/.agent/pptify-plugin/images/web_image_search.py +++ /dev/null @@ -1,286 +0,0 @@ -from __future__ import annotations - -import argparse -import html -import json -import re -import sys -from urllib.error import HTTPError, URLError -from urllib.parse import parse_qs, urlencode, urlsplit -from urllib.request import Request, urlopen - - -USER_AGENT = ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/134.0.0.0 Safari/537.36" -) - - -def browser_headers(referer: str | None = None) -> dict[str, str]: - headers = { - "Accept-Language": "en-US,en;q=0.9", - "User-Agent": USER_AGENT, - } - if referer: - headers["Referer"] = referer - return headers - - -def is_http_url(value: str) -> bool: - try: - parsed = urlsplit(value) - return parsed.scheme in {"http", "https"} and bool(parsed.netloc) - except Exception: - return False - - -def candidate_key(candidate: dict[str, str | None]) -> str | None: - return candidate.get("imageUrl") or candidate.get("sourcePageUrl") or candidate.get("thumbnailUrl") - - -def append_candidate( - candidates: list[dict[str, str | None]], - seen_keys: set[str], - candidate: dict[str, str | None], - max_num: int, -) -> None: - if len(candidates) >= max_num: - return - key = candidate_key(candidate) - if not key or key in seen_keys: - return - seen_keys.add(key) - candidates.append(candidate) - - -def _get_text(url: str, *, params: dict[str, str] | None = None, referer: str | None = None, timeout: int = 15) -> str: - full_url = f"{url}?{urlencode(params)}" if params else url - request = Request(full_url, headers=browser_headers(referer)) - with urlopen(request, timeout=timeout) as response: - content_type = response.headers.get_content_charset() or "utf-8" - return response.read().decode(content_type, errors="replace") - - -def _direct_candidate(url: str, query: str) -> dict[str, str | None]: - parsed = urlsplit(url) - title = parsed.path.split("/")[-1] or parsed.netloc or "Direct image" - return { - "provider": "direct", - "imageUrl": url, - "thumbnailUrl": url, - "sourcePageUrl": url, - "title": title, - "attribution": parsed.netloc or None, - "searchQuery": query, - } - - -def build_google_candidates(query: str, max_num: int) -> list[dict[str, str | None]]: - try: - from bs4 import BeautifulSoup - from icrawler.builtin.google import GoogleFeeder, GoogleParser - from icrawler.utils import ProxyPool, Session, Signal - except Exception: - return [] - - session = Session(ProxyPool()) - signal = Signal() - signal.set(feeder_exited=False, parser_exited=False, reach_max_num=False) - feeder = GoogleFeeder(1, signal, session) - parser = GoogleParser(1, signal, session) - feeder.feed(keyword=query, offset=0, max_num=max_num) - - seen_pages: set[str] = set() - seen_images: set[str] = set() - seen_keys: set[str] = set() - candidates: list[dict[str, str | None]] = [] - - while not feeder.out_queue.empty() and len(candidates) < max_num: - search_url = feeder.out_queue.get() - base_url = "{0.scheme}://{0.netloc}".format(urlsplit(search_url)) - response = session.get(search_url, timeout=10, headers=browser_headers(base_url)) - if not response.text: - continue - - soup = BeautifulSoup(response.text, "html.parser") - thumbnails = [] - for image in soup.find_all("img"): - src = image.get("src") - if isinstance(src, str) and src.startswith("https://encrypted-tbn0.gstatic.com/images"): - thumbnails.append(src) - - source_pages = [] - for anchor in soup.find_all("a"): - href = anchor.get("href") - if not isinstance(href, str) or not href.startswith("/url?"): - continue - target = parse_qs(urlsplit(href).query).get("q", [None])[0] - if not target or not target.startswith(("http://", "https://")) or target in seen_pages: - continue - seen_pages.add(target) - source_pages.append(target) - - pair_count = min(len(thumbnails), len(source_pages), max_num - len(candidates)) - for index in range(pair_count): - page_url = source_pages[index] - thumb_url = thumbnails[index] - parsed = urlsplit(page_url) - append_candidate( - candidates, - seen_keys, - { - "provider": "google", - "imageUrl": None, - "thumbnailUrl": thumb_url, - "sourcePageUrl": page_url, - "title": parsed.path.split("/")[-1] or parsed.netloc or "Google image", - "attribution": parsed.netloc or None, - }, - max_num, - ) - - tasks = parser.parse(response) or [] - for task in tasks: - image_url = task.get("file_url") - if not image_url or image_url in seen_images: - continue - seen_images.add(image_url) - parsed = urlsplit(image_url) - append_candidate( - candidates, - seen_keys, - { - "provider": "google", - "imageUrl": image_url, - "thumbnailUrl": image_url, - "sourcePageUrl": None, - "title": parsed.path.split("/")[-1] or parsed.netloc or "Google image", - "attribution": parsed.netloc or None, - }, - max_num, - ) - if len(candidates) >= max_num: - break - - return candidates - - -def _extract_bing_metadata(html_text: str) -> list[str]: - try: - from bs4 import BeautifulSoup - except Exception: - BeautifulSoup = None - - if BeautifulSoup is not None: - soup = BeautifulSoup(html_text, "html.parser") - return [str(anchor.get("m")) for anchor in soup.select("a.iusc") if anchor.get("m")] - - metadata_values: list[str] = [] - for match in re.finditer(r"]*\bclass=[\"'][^\"']*\biusc\b[^\"']*[\"'][^>]*>", html_text, re.IGNORECASE): - tag = match.group(0) - attr_match = re.search(r"\bm=([\"'])(.*?)\1", tag, re.IGNORECASE | re.DOTALL) - if attr_match: - metadata_values.append(html.unescape(attr_match.group(2))) - return metadata_values - - -def build_bing_candidates(query: str, max_num: int) -> list[dict[str, str | None]]: - html_text = _get_text( - "https://www.bing.com/images/search", - params={"q": query, "form": "HDRSC3"}, - referer="https://www.bing.com/", - ) - seen_keys: set[str] = set() - candidates: list[dict[str, str | None]] = [] - - for metadata_text in _extract_bing_metadata(html_text): - try: - metadata = json.loads(html.unescape(metadata_text)) - except json.JSONDecodeError: - continue - - image_url = metadata.get("murl") - thumb_url = metadata.get("turl") or image_url - page_url = metadata.get("purl") - title = metadata.get("t") or metadata.get("desc") or "Bing image" - attribution = urlsplit(page_url).netloc if isinstance(page_url, str) and page_url else None - append_candidate( - candidates, - seen_keys, - { - "provider": "bing", - "imageUrl": image_url, - "thumbnailUrl": thumb_url, - "sourcePageUrl": page_url, - "title": title, - "attribution": attribution, - }, - max_num, - ) - if len(candidates) >= max_num: - break - return candidates - - -def build_candidates(query: str, max_num: int) -> tuple[list[dict[str, str | None]], list[str]]: - if is_http_url(query): - return [_direct_candidate(query, query)], [] - - errors: list[str] = [] - candidates: list[dict[str, str | None]] = [] - try: - candidates = build_google_candidates(query, max_num) - except Exception as exc: - errors.append(f"google: {exc}") - - if len(candidates) < max_num: - try: - fallback_candidates = build_bing_candidates(query, max_num) - seen_keys = {key for candidate in candidates if (key := candidate_key(candidate))} - for candidate in fallback_candidates: - append_candidate(candidates, seen_keys, candidate, max_num) - if len(candidates) >= max_num: - break - except (HTTPError, URLError, TimeoutError, OSError) as exc: - errors.append(f"bing: {exc}") - - for candidate in candidates: - candidate.setdefault("searchQuery", query) - return candidates, errors - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Search web image candidates for one or more queries.") - parser.add_argument("--query", action="append", required=True, help="Search query. Can be repeated.") - parser.add_argument("--max-num", type=int, default=12, help="Maximum candidates per query.") - parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.") - return parser - - -def main() -> int: - if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8") - args = build_parser().parse_args() - queries = [query.strip() for query in args.query if query.strip()] - max_num = max(1, min(args.max_num, 32)) - payload_candidates: list[dict[str, str | None]] = [] - errors: list[str] = [] - - for query in queries: - candidates, query_errors = build_candidates(query, max_num) - payload_candidates.extend(candidates) - errors.extend(query_errors) - - payload = { - "ok": not errors or bool(payload_candidates), - "query": "\n".join(queries), - "candidates": payload_candidates, - "errors": errors, - } - print(json.dumps(payload, ensure_ascii=False, indent=2 if args.pretty else None)) - return 0 if payload["ok"] else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file diff --git a/plugins/pptify/.agent/pptify-policy.md b/plugins/pptify/.agent/pptify-policy.md deleted file mode 100644 index 16e71c6e8..000000000 --- a/plugins/pptify/.agent/pptify-policy.md +++ /dev/null @@ -1,60 +0,0 @@ - -# pptify Developer-Protection Policy - -Installed by pptify-cli. Do not edit manually; run `pptify install` to refresh. - -## Secret and Credential Safety - -- Never embed API keys, tokens, connection strings, or passphrases in deck - specs, prompt assets, audit files, attempt manifests, or version-controlled - files. -- Never collect API keys or tokens through chat or the VS Code prompt input - dialog. Require the user to type secrets directly into a terminal or a - managed secret environment (e.g. `az login`, `$env:OPENAI_API_KEY = ...`). -- If an image-generation helper fails due to missing credentials, persist a - failure manifest with `provider`, `status: missing_credentials`, and `error`, - and do not describe placeholder artwork as model-generated. - -## Coordinate Contract - -- All generated slides must use explicit `layout_tree` with final bboxes, font - sizes, text colors, line endpoints/styles, shape names, and z-order. -- Prohibited obsolete shorthand keys: `pattern`, `layout_pattern`, - `composition.pattern`, `layout`, `sections`, `bullets`, `objects`, `theme`. -- Every text-bearing object must carry explicit `style.font_size` and - `style.color`. -- Every line object must carry `content.x1`, `content.y1`, `content.x2`, - `content.y2` and explicit `style.line`/`style.line_width`. -- Every shape object must carry `content.shape`, `style.fill`, and - `style.line`. - -## Quality Gates - -- Production-ready decks must have zero content collisions (verified by - `pptify-plugin/audit/audit.py`). -- Production-ready decks must have zero text overflows. -- No `classification: "content"` object may use `style.font_size` below 9 pt. - -## Asset and Design Boundaries - -- Do not copy external fonts, icons, images, or binary assets without explicit - license metadata and source attribution in `pptify-design/sources.json` or - `pptify-design/third-party-notices.md`. -- For every new generated deck, load a profile from `pptify-design/sources.json` - unless a user-provided brand guide or reference PPTX is the primary style - source. Do not invent a new design template. -- Production-ready decks must record selected profile IDs, source URLs, and - style lock details in `summary.design_context`. -- Plain white, Calibri-only, bullet-heavy, default-theme PPTX output is a design - failure even when collision and overflow audits pass. -- Keep source attribution and license metadata attached to any copied or - adapted design context. - -## Rendering Boundary - -- The importable `pptify/` core renderer package and `python -m pptify` CLI - are not present in this workspace snapshot. Use plugin scripts for - extraction, conversion, design context, image helpers, and audit. -- Do not use obsolete renderer flags (`--provider copilot`, `--prompt`, - `--prompt-file`, `--model`, `--spec-out`) unless a restored core CLI - explicitly supports them. diff --git a/plugins/pptify/.agent/skills/pptify-tooling/SKILL.md b/plugins/pptify/.agent/skills/pptify-tooling/SKILL.md deleted file mode 100644 index 6207e82a5..000000000 --- a/plugins/pptify/.agent/skills/pptify-tooling/SKILL.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: pptify-tooling -description: "Command reference for pptify plugin tools. Use when looking up install commands, plugin script syntax, or the workspace reality check." ---- - -# PPTify Tooling - -## Install - -```powershell -uv sync # base project -uv sync --extra plugins # add source ingestion and image helpers -``` - -## Workspace Reality Check - -No importable `pptify/` package or `python -m pptify` CLI is present in this snapshot. Use the standalone plugin scripts below, or restore the core renderer package before using documented render/analyze/extract CLI commands. - -If the core renderer is restored: - -```powershell -uv run python -m pptify deck-spec.json --out deck.pptx --audit deck-audit.json -``` - -## Plugin Scripts - -| Purpose | Command | -|---|---| -| Convert document to markdown | `uv run python pptify-plugin/documents/document_to_markdown.py --source --output-path out.md` | -| Build RAPTOR summary tree | `uv run python pptify-plugin/documents/document_to_raptor_tree.py --markdown-path source.md --output-path summary.json --title "Title" --pretty` | -| List design profiles | `uv run python pptify-plugin/design/design_context_catalog.py --list --pretty` | -| Load design profile context | `uv run python pptify-plugin/design/design_context_catalog.py --profile fluent-ui-design-tokens --include-context --pretty` | -| Search web images | `uv run python pptify-plugin/images/web_image_search.py --query "topic" --max-num 8 --pretty` | -| Search Iconify icons | `uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 --max-num 8 --pretty` | -| Raster to SVG | `uv run python pptify-plugin/images/raster_image_to_svg.py --source logo.png --output-path logo.svg --pretty` | -| Generate infographic | `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "..." --output-path out.png --pretty` | -| Audit spec | `uv run python pptify-plugin/audit/audit.py deck-spec.json --json` | -| Run tests | `uv run python -m unittest discover -s tests -v` | diff --git a/plugins/pptify/.agent/workflows/deck-generation.md b/plugins/pptify/.agent/workflows/deck-generation.md deleted file mode 100644 index 6c08dd07a..000000000 --- a/plugins/pptify/.agent/workflows/deck-generation.md +++ /dev/null @@ -1,115 +0,0 @@ -# Deck Generation E2E Workflow - -Use this workflow when a Copilot or coding agent needs to generate PPTX slides with `pptify`. - -## 1. Intake - -1. Before creating workflow artifacts, collect any missing required inputs with the VS Code prompt input dialog (`vscode_askQuestions` or equivalent). Batch concise questions, offer sensible defaults for optional fields, and continue after the user answers. -2. Identify the audience, decision, core narrative, required language, target slide count, source material, reference PPTX, branding constraints, output artifact paths, and delivery deadline. -3. If the user gives only a topic, create a reasonable executive narrative and mark assumptions in the generated spec summary. When the user asks for web images, sources, data enrichment, or a source-backed deck, gather and persist source material before authoring slides. -4. If the user provides source files, URLs, research material, or a reference deck, prepare them before generating the slide spec. -5. If the user requests text-to-image or generated images with OpenAI, Azure OpenAI, or Azure AI Foundry, create `.env` from `.env.template` when it is missing and have the user fill provider settings or secrets directly in `.env`. Never ask for API keys, tokens, or connection strings in chat or in the dialog. -6. Do not author a slide or summary that claims a model-generated infographic exists until provider, model or deployment, auth mode, prompt, output path, and attempt status are known. - -## 1A. Image Access Intake - -For any text-to-image request, prepare `.env` before invoking `pptify-plugin/images/text_prompt_to_infographic.py`. - -The infographic helper has no local fallback provider. If OpenAI or Azure OpenAI access is missing, record `missing_provider_config` or the provider failure in an attempt manifest and do not describe placeholder artwork as generated. - -For OpenAI image generation, configure these values in `.env`: - -1. `PPTIFY_IMAGE_PROVIDER=openai` or pass `--provider openai`. -2. `OPENAI_API_KEY`. -3. `OPENAI_IMAGE_MODEL`, defaulting to `gpt-image-1` when unspecified. -4. Image size, defaulting to `1024x1024` when unspecified. -5. Text prompt and output path. -6. Run image generation after `.env` is filled, for example `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty`. - -For Azure OpenAI / Azure AI Foundry image generation, configure these values in `.env`: - -1. `PPTIFY_IMAGE_PROVIDER=azure-openai` or pass `--provider azure-openai`. -2. `AZURE_OPENAI_ENDPOINT`, for example `https://.services.ai.azure.com/openai/v1`. -3. `AZURE_OPENAI_IMAGE_DEPLOYMENT`, for example `gpt-image-2` or the user's exact `gpt-image-2.0` deployment name. -4. Ask whether the user wants Azure CLI/Entra auth or API-key auth. -5. `AZURE_OPENAI_TIMEOUT`, defaulting to `300` when unspecified. -6. For Azure CLI/Entra auth, tell the user to run `az login`; for API-key auth, have the user fill `AZURE_OPENAI_API_KEY` or `AZURE_AI_API_KEY` in `.env`. -7. Run image generation after `.env` is filled, for example `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "Cloud governance roadmap" --output-path infographic.png --pretty`. -8. Save a small attempt manifest next to the asset with provider, endpoint or model name, auth mode, prompt path, output path, status, and error details when generation fails. Do not silently replace a failed or missing model output with local artwork. - -## 2. Prepare Sources and References - -1. Convert long documents and downloaded HTML pages with `pptify-plugin/documents/document_to_markdown.py`. -2. For URL-based, topic-plus-research, source-backed, or multi-source decks, build a combined markdown corpus and run `pptify-plugin/documents/document_to_raptor_tree.py` before planning slides. This is required even when the source files are individually short, because the deck should use synthesized evidence rather than a few searched keywords. -3. Record the source corpus path, RAPTOR summary path, source count, and source URLs in `summary.source_enrichment` in the generated spec. -4. When a reference deck should influence content or style, use the importable helpers in `pptify-plugin/extraction` or package inspection to collect style, brand, template, and layout-rhythm context. The `python -m pptify --analyze-pptx` command is unavailable unless the core renderer package is restored. -5. Use the analysis facts as LLM context when the new deck should preserve language, slide count, topic sequence, executive tone, colors, fonts, template conventions, and layout rhythm. -6. Use extraction helpers only when the task is preservation or reconstruction of the source deck. - -## 2A. Prepare Design Context (Required) - -1. Every new deck must choose a design direction before slide planning. Do not wait for the user to explicitly ask for `pptify-design`. -2. If the user supplies a brand guide or reference PPTX, use that as the primary style source and optionally add a compatible `pptify-design` profile for layout vocabulary. -3. If the user does not supply a style source, load at least one source-backed profile from `pptify-design/sources.json` with `uv run python pptify-plugin/design/design_context_catalog.py --profile --include-context --pretty`. -4. Default profile selection: - - Default, general modern, stylish, product, app, pitch, Microsoft, M365, Teams, Power Platform, or enterprise product decks: `fluent-ui-design-tokens`. - - Developer, GitHub, code, or engineering-system decks: `primer-primitives`. - - Consulting, strategy, governance, or operations reviews: `likaku-mck-ppt-design-skill` plus a conservative `corazzon-pptx-design-styles` style such as Swiss International, Monochrome Minimal, Editorial Magazine, or Architectural Blueprint. - - Broader modern style exploration or explicitly visual direction selection: `corazzon-pptx-design-styles`. - - Design reasoning or preflight critique: add `awesome-copilot-design-agents` as a secondary prompt context. -5. Lock exactly one visual style or design system before authoring slide coordinates. Record the selected profile ID, style name when applicable, palette, typography, spacing rhythm, signature elements, and source URLs in `summary.design_context`. -6. Include the returned context payload in the LLM context before writing `deck-spec.json`; do not summarize it away into a vague phrase such as "modern design". -7. A deck that uses default PowerPoint theme colors, Calibri-only text boxes, plain white backgrounds, or bullet-only layouts without a selected `summary.design_context` is not production-ready. - -## 3. Plan the Deck - -1. Produce one clear message per slide before choosing visuals. -2. Choose a slide form for each message, such as title, agenda, comparison, process, metrics, roadmap, risk, architecture, evidence, decision, infographic, dashboard-style overview, or appendix. -3. Use charts and dashboard-style slides only when the source corpus contains relevant quantitative or structured evidence. Represent them as explicit editable primitives or image-backed exhibits. -4. Keep each slide to three to five major content groups. -5. Preserve user-provided terminology, names, metrics, dates, and executive tone. -6. Decide the slide composition, hierarchy, coordinates, object sizes, z-order, colors, fonts, and font sizes during planning. The available plugin scripts will not do this later. -7. Every normal content slide should contain at least one visible design element derived from the locked style: a color band, card system, grid, rule, accent shape, diagram primitive, image treatment, pattern, or data exhibit. Avoid text-only slides unless the locked style is explicitly typographic. - -## 4. Author the JSON Spec - -1. Return a top-level object with `slides` and optional `summary`. -2. For each generated slide, include `id`, `title`, and `layout_tree`. -3. Do not use `pattern`, `layout_pattern`, `composition.pattern`, `layout`, `sections`, `bullets`, `objects`, `theme`, chart placeholders, or browser layout requests as render-time shorthand. -4. Each `layout_tree` must include `slide_size`, `root_group_id`, `groups`, `objects`, and optional `notes`. -5. Each group must include `id`, `role`, `layout_mode`, `object_ids`, `group_ids`, and a `bbox` when it represents a visible or bounded region. -6. Each object must include `id`, `kind`, `role`, `classification`, `content`, `style`, `bbox`, and `z_index`. -7. Treat decorative shapes as `layout_design`; treat meaningful text, tables, lines, and media as `content`. -8. Give every text-bearing object and table explicit `style.font_size` and `style.color`; do not rely on a later tool to shrink text to fit or infer contrast. Body and evidence text must be at least 10 pt; labels and captions at least 9 pt; footers at least 8 pt. -9. Give every line object explicit `content.x1`, `content.y1`, `content.x2`, and `content.y2`. -10. Give every line object explicit `style.line` and `style.line_width`. -11. Give every shape object explicit `content.shape`, `style.fill`, and `style.line`. -12. Translate the locked design context into explicit objects, colors, spacing, typography, and coordinates; do not rely on runtime pattern selection. -13. If a generated raster infographic is created, use that raster on the visible slide for fidelity, convert it with `raster_image_to_svg.py`, and add the SVG as a final `hidden: true` appendix slide. Record both paths in `summary.text_to_image`. - -## 5. Build the PPTX - -Current workspace reality check: this snapshot does not contain an importable `pptify/` package or `python -m pptify` CLI. Restore the core renderer package before using core render commands, or produce PPTX artifacts through direct PowerPoint generation plus standalone plugin evidence. - -1. As the Copilot CLI or VS Code agent, author or update `deck-spec.json` or a generation script directly; plugin scripts do not perform prompt-to-spec generation or full-deck rendering. -2. If the core renderer is restored, render the authored spec with `uv run python -m pptify deck-spec.json --out deck.pptx --audit deck-audit.json`. Otherwise build with the available PowerPoint generation path and keep plugin evidence/audits alongside the PPTX. Using `python-pptx` is only a serialization path; it must still implement the locked `pptify-design` coordinates, colors, typography, and decorative primitives. -3. For reference-guided generation, include analysis/source summaries and extracted `styles`, `brands`, `template`, and `layout` context in the agent prompt before writing `deck-spec.json`. -4. For predefined-template generation, include selected `pptify-design` context in the agent prompt before writing `deck-spec.json`. -5. Never copy, mutate, or save over a referenced PPTX as the deck generation strategy. - -## 6. Validate and Repair - -1. Inspect the audit for content collisions, text overflows, and warnings. -2. If collisions remain, move or resize objects, reduce density, split slides, or change the coordinate plan. -3. If text overflows, shorten copy, split content across slides, or enlarge object bboxes. Lower explicit font sizes only as a last resort and never below 9 pt for content objects. -4. Verify source and image workflow gates before final response: source-backed decks have `summary.source_enrichment` with corpus and RAPTOR paths; generated-image requests have an attempt manifest; successful raster infographics have a hidden SVG appendix slide. -5. Verify design-context gates before final response: `summary.design_context` exists, names the selected profile/style, and every visible content slide has at least one style-derived design element. -6. Treat plain white Calibri slides, default theme placeholders, unstyled bullet lists, and missing `summary.design_context` as quality failures even when collision audit passes. -7. Rebuild after each repair until generated slides have zero collisions, zero overflows, no unexpected warnings, and pass the design-context gate, or clearly report the residual issue. -8. For important deliverables, inspect the produced PPTX package with `python-pptx` or zip checks in addition to unit tests. - -## 7. Response Contract - -1. When asked to author the deck spec, write strict JSON with no markdown fences unless the user explicitly asks for prose. -2. When required workflow or artifact inputs are missing, prompt for them with the input dialog before authoring or building. -3. When acting as a coding agent in the workspace, create or update the spec or generation script, build with the available PowerPoint path, validate the audit and produced PPTX package, and report the generated artifact paths. diff --git a/plugins/pptify/.github/plugin/plugin.json b/plugins/pptify/.github/plugin/plugin.json index 0b8d17114..8eaf08de7 100644 --- a/plugins/pptify/.github/plugin/plugin.json +++ b/plugins/pptify/.github/plugin/plugin.json @@ -1,21 +1,29 @@ { - "name": "pptify", - "description": "Generate production-ready PowerPoint decks with pptify skills, source ingestion, design-context selection, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates.", - "version": "1.0.0", - "author": { - "name": "PPTify maintainers" - }, - "repository": "https://github.com/kimtth/agent-pptify-kit", - "license": "MIT", - "keywords": [ - "pptify", - "powerpoint", - "pptx", - "presentations", - "deck-generation", - "slides", - "design-context", - "visual-assets", - "quality-gates" - ] + "name": "pptify", + "description": "Generate production-ready PowerPoint decks with pptify skills, source ingestion, design-context selection, coordinate-explicit slide specs, visual assets, runtime tooling, and audit-driven quality gates.", + "version": "1.0.0", + "author": { + "name": "PPTify maintainers" + }, + "repository": "https://github.com/github/awesome-copilot", + "license": "MIT", + "keywords": [ + "pptify", + "powerpoint", + "pptx", + "presentations", + "deck-generation", + "slides", + "design-context", + "visual-assets", + "quality-gates" + ], + "skills": [ + "./skills/pptify-context-prep/", + "./skills/pptify-deck-generation/", + "./skills/pptify-quality-gates/", + "./skills/pptify-slide-spec/", + "./skills/pptify-tooling/", + "./skills/pptify-visual-assets/" + ] } \ No newline at end of file diff --git a/plugins/pptify/README.md b/plugins/pptify/README.md index e0e0b69df..f01fdc5ef 100644 --- a/plugins/pptify/README.md +++ b/plugins/pptify/README.md @@ -21,33 +21,26 @@ copilot plugin install pptify@awesome-copilot | `pptify-tooling` | Look up pptify install commands, plugin script syntax, and workspace reality checks. | | `pptify-visual-assets` | Find, generate, and place icons, images, SVGs, raster conversions, infographics, image placeholders, and asset-backed slide objects. | -### Runtime artifacts generated by `pptify-cli` +## Optional Toolkit -The plugin folder includes `.agent`, generated with: +The bundled skills include reference material for design profile selection and manual quality checks. To run helper scripts for source prep, design context, visual assets, extraction, or audits, users can optionally install the PPTify toolkit from its source repository. The current external toolkit does not provide an importable `pptify` renderer module. + +Do not clone or install the external toolkit automatically. Install it only when the user explicitly asks to use helper scripts: ```powershell -uv run python pptify-cli install --home deploy\awesome-copilot\plugins\pptify +git clone https://github.com/kimtth/agent-pptify-kit +cd agent-pptify-kit +uv sync # base project +uv sync --extra plugins # add source ingestion and image helpers ``` -That runtime bundle contains: - -| Artifact | Purpose | -| --- | --- | -| `.agent\skills\pptify-*` | Installed pptify skill set. | -| `.agent\workflows\deck-generation.md` | End-to-end deck-generation workflow prompt. | -| `.agent\pptify-plugin` | Source ingestion, design context, image/SVG, extraction, and audit helper tools. | -| `.agent\pptify-design` | Source-backed design profiles and template context. | -| `.agent\.env.template` | Image-provider configuration template. | -| `.agent\pptify-policy.md` | Developer-protection and quality-gate policy. | -| `.agent\copilot-instruction.md` | Generic coding-agent instruction for using installed pptify assets. | - ## Usage Ask Copilot to create or repair a deck and mention `pptify`. The plugin guides the agent to collect required deck inputs, prepare source and reference context, select a design profile, author a coordinate-explicit JSON spec, build through the available PowerPoint path, and repair audit findings before reporting artifact paths. ## Source -This plugin is generated from [kimtth/agent-pptify-kit](https://github.com/kimtth/agent-pptify-kit) for submission to [Awesome Copilot](https://github.com/github/awesome-copilot). +Plugin skills are sourced from [kimtth/agent-pptify-kit](https://github.com/kimtth/agent-pptify-kit) for submission to [Awesome Copilot](https://github.com/github/awesome-copilot). ## License diff --git a/plugins/pptify/.agent/skills/pptify-context-prep/SKILL.md b/skills/pptify-context-prep/SKILL.md similarity index 92% rename from plugins/pptify/.agent/skills/pptify-context-prep/SKILL.md rename to skills/pptify-context-prep/SKILL.md index 9ede9f2a8..0b8aa65e6 100644 --- a/plugins/pptify/.agent/skills/pptify-context-prep/SKILL.md +++ b/skills/pptify-context-prep/SKILL.md @@ -5,6 +5,8 @@ description: "Prepare source material and design context before authoring a ppti # PPTify Context Prep +> **Prerequisite:** Before running any plugin script in this skill, run the workspace detection check in `pptify-tooling`. If `pptify-plugin/` is absent, follow the install or graceful-degradation steps there before continuing. + Use this skill before writing a deck spec. It covers two parallel preparation tracks: **source context** (documents, research, reference PPTX) and **design context** (predefined style profiles from `pptify-design`). ## Source Documents @@ -27,6 +29,8 @@ Use this skill before writing a deck spec. It covers two parallel preparation tr ## Design Profile Selection +Load [`references/design-profiles.md`](references/design-profiles.md) for the full profile catalog with IDs, `best_for` guidance, key style signals, and license information. Use it whether or not the toolkit is installed — when the toolkit is present, it cross-checks the catalog with the live output of `design_context_catalog.py`. + Use profiles from `pptify-design/sources.json`; do not invent a new design template when the user asks for predefined templates. - Use `fluent-ui-design-tokens` as the default for new decks, including Microsoft, M365, Teams, Power Platform, enterprise-aligned, general modern, stylish, product, app, pitch, or unspecified visual style requests. diff --git a/skills/pptify-context-prep/references/design-profiles.md b/skills/pptify-context-prep/references/design-profiles.md new file mode 100644 index 000000000..ed863c6e1 --- /dev/null +++ b/skills/pptify-context-prep/references/design-profiles.md @@ -0,0 +1,174 @@ +# PPTify Design Profile Catalog + +Source: `pptify-design/sources.json` — updated 2026-05-20. +Load this file when `pptify-plugin/design/design_context_catalog.py` is unavailable. + +## Quick-Select Guide + +| Profile ID | Best for | +|---|---| +| `fluent-ui-design-tokens` | Microsoft, M365, Teams, Power Platform, enterprise — **default for new decks** | +| `primer-primitives` | GitHub-style, developer products, token-driven UI reviews, engineering docs | +| `corazzon-pptx-design-styles` | 30 modern style catalog; use when visual variety or multiple direction options are needed | +| `likaku-mck-ppt-design-skill` | Consulting, strategy, governance, operations — strict action-title discipline | +| `sunbigfly-ppt-agent-skills` | Source-backed, stage-gated delivery pipelines with human approval at each phase | +| `awesome-copilot-design-agents` | Prompting agent for design review, UX discovery, visual hierarchy reasoning | +| `nexu-io-open-design` | Direction-picker workflows with explicit style-lock and artifact-lint gates | +| `alchaincyf-huashu-design` | Brand-constrained enterprise decks requiring exact color/type fidelity | +| `pptwork-oh-my-slides` | HTML-prototype-first workflows; raster fidelity + constrained PPTX editability as separate deliverables | +| `erickittelson-slidemason` | Cautionary reference: JSX primitive composition — auto-layout incompatibility | +| `gabberflast-academic-pptx-skill` | High-stakes governance, board, or investor presentations needing narrative rigour | + +## Profiles + +### `fluent-ui-design-tokens` +**Name:** Fluent UI Design Token Guidance +**Kind:** design-system-context +**License:** MIT — Copyright (c) Microsoft Corporation +**Source:** [microsoft/fluentui](https://github.com/microsoft/fluentui/blob/master/docs/architecture/design-tokens.md) +**Token categories:** color, spacing, border radius, font, line height, stroke, shadow, duration, easing +**Themes:** webLightTheme, webDarkTheme, teamsLightTheme, teamsDarkTheme, teamsHighContrastTheme +**Agent rule:** Use design tokens instead of hardcoded colors, spacing, or typography values. +**Best for:** Microsoft-aligned decks, Teams, M365, Power Platform governance, enterprise product reviews + +--- + +### `primer-primitives` +**Name:** Primer Primitives Design Tokens +**Kind:** design-system-context +**License:** MIT — Copyright (c) 2018 GitHub Inc. +**Source:** [primer/primitives](https://github.com/primer/primitives) +**Token categories:** color, spacing, typography, motion, z-index +**Spacing scale:** xxs, xs, sm, md, lg, xl +**Typography roles:** display, title, subtitle, body, caption, codeBlock, codeInline +**Color examples:** `#ffffff`, `#1f2328`, `#F6F8FA`, `#0969da`, `#1a7f37`, `#cf222e` +**Best for:** GitHub-style decks, developer products, token-driven UI reviews, engineering documentation + +--- + +### `corazzon-pptx-design-styles` +**Name:** corazzon/pptx-design-styles — 30 Modern PPTX Style Templates +**Kind:** pptx-style-template-context +**License:** MIT — Copyright TodayCode / corazzon contributors +**Source:** [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles) +**30 styles:** Glassmorphism, Neo-Brutalism, Bento Grid, Dark Academia, Gradient Mesh, Claymorphism, Swiss International, Aurora Neon Glow, Retro Y2K, Nordic Minimalism, Typographic Bold, Duotone Color Split, Monochrome Minimal, Cyberpunk Outline, Editorial Magazine, Pastel Soft UI, Dark Neon Miami, Hand-crafted Organic, Isometric 3D Flat, Vaporwave, Art Deco Luxe, Brutalist Newspaper, Stained Glass Mosaic, Liquid Blob Morphing, Memphis Pop Pattern, Dark Forest Nature, Architectural Blueprint, Maximalist Collage, SciFi Holographic Data, Risograph Print +**Style families:** modern-ui, editorial, retro, technical, luxury, organic, experimental +**Source inputs per style:** hex colors, font pairings, layout rules, signature elements, avoid lists +**Agent rule:** Pick one style, lock its palette and typography, then translate visual effects into explicit pptify `layout_tree` primitives or documented raster accents. Do not mix styles accidentally. +**Best for:** Choosing a predefined modern style from a broad catalog; generating multiple visual direction options before deck production + +--- + +### `likaku-mck-ppt-design-skill` +**Name:** likaku/Mck-ppt-design-skill — McKinsey-Style Native PPTX Layout Runtime +**Kind:** pptx-pattern-context +**License:** MIT — Copyright likaku contributors +**Source:** [likaku/Mck-ppt-design-skill](https://github.com/likaku/Mck-ppt-design-skill) +**Pattern count:** ~70 consulting-style layout patterns +**Pattern families:** structure-navigation, data-metrics, frameworks-matrices, content-narrative +**Action title discipline:** required on every content slide +**Geometry norms (inches):** kicker_y=0.48, title_y=0.72, rule_y=1.12, content_top_y=1.30 +**Agent rule:** Use the source taxonomy as design inspiration only. Author exact `layout_tree` coordinates, sizes, and primitives. Action titles on every content slide. +**Best for:** Consulting decks for strategy, governance, or operations reviews; strict action-title discipline + +--- + +### `sunbigfly-ppt-agent-skills` +**Name:** sunbigfly/ppt-agent-skills — Staged Deck Generation Pipeline +**Kind:** agent-pipeline-context +**License:** MIT — Copyright sunbigfly contributors +**Source:** [sunbigfly/ppt-agent-skills](https://github.com/sunbigfly/ppt-agent-skills) +**Pipeline stages:** interview → source-compression → outline → style-lock → slide-plan → visual-qa → dual-export +**Stage outputs:** structured brief JSON, compressed source ≤800 words, outline JSON, style_lock JSON, complete spec.json, qa-report, pptx + raster +**Agent rule:** Never skip the interview. Source compression before outline. Style lock is stage-gated. Per-slide plans are full specs. Action titles mandatory. +**Best for:** Source-grounded decks; high-stakes presentations; workflows requiring explicit human approval at each phase + +--- + +### `awesome-copilot-design-agents` +**Name:** Awesome Copilot Design Agent and Prompt Context +**Kind:** agent-prompt-context +**License:** MIT — Copyright GitHub, Inc. +**Source:** [github/awesome-copilot](https://github.com/github/awesome-copilot) +**Key files:** `agents/gem-designer.agent.md`, `agents/se-ux-ui-designer.agent.md`, `skills/penpot-uiux-design/SKILL.md`, `skills/prompt-optimizer/SKILL.md` +**Prompt focus:** existing design systems, visual hierarchy, UX discovery, accessibility, slides and reports design intentionality +**Best for:** Prompting an LLM to reason about deck design; UX discovery before deck planning; design review checklists; visual hierarchy guidance + +--- + +### `nexu-io-open-design` +**Name:** nexu-io/open-design — Claude Design Style +**Kind:** agent-skill-context +**License:** MIT — Copyright nexu-io contributors +**Source:** [nexu-io/open-design](https://github.com/nexu-io/open-design) +**Key patterns:** direction-picker, sandbox-preview, artifact-lint, design-critique +**Stage gates:** direction selection → style lock → preview approval → artifact lint → critique gate +**Agent rule:** Never start layout without a locked direction. Run artifact lint after every build. Preview before full deck. +**Best for:** Reasoning about deck design direction before committing; parallel design options for selection; lint and critique gates on generated decks + +--- + +### `alchaincyf-huashu-design` +**Name:** alchaincyf/huashu-design — HTML-Native Brand Design Pipeline +**Kind:** agent-skill-context +**License:** MIT — Copyright alchaincyf contributors +**Source:** [alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design) +**Key patterns:** brand-asset-protocol, visual-directions, html-to-editable-pptx, playwright-check +**Brand lock fields:** primary_palette, neutral_palette, typeface_display, typeface_body, tone +**Agent rule:** Brand lock is non-negotiable. Parallel directions before deck plan. Every text frame must be individually editable. +**Best for:** Brand-constrained enterprise decks requiring exact color/type fidelity; multi-direction style exploration before committing + +--- + +### `pptwork-oh-my-slides` +**Name:** PPTWork/oh-my-slides — HTML-as-Source PPTX Build Artifact Pipeline +**Kind:** pptx-export-context +**License:** MIT — Copyright PPTWork contributors +**Source:** [PPTWork/oh-my-slides](https://github.com/PPTWork/oh-my-slides) +**Key patterns:** html-source, preset-picker, mini-preview, raster-export, constrained-editable +**Export model:** HTML (design source) → raster export (fidelity) + constrained PPTX (editability) +**Forbidden in editable PPTX:** background images, raster embeds of slide content, CSS transform rotate, SVG filter effects +**Agent rule:** Never promise both pixel fidelity and full editability from the same export path. Raster embeds in editable PPTX are a quality failure. +**Best for:** HTML-prototype-first workflows; design fidelity and PowerPoint editability as separate deliverables; Playwright-in-the-loop generation + +--- + +### `erickittelson-slidemason` +**Name:** erickittelson/slidemason — JSX Primitive Composition (Cautionary Reference) +**Kind:** agent-skill-context +**License:** MIT — Copyright erickittelson contributors +**Source:** [erickittelson/slidemason](https://github.com/erickittelson/slidemason) +**Key patterns:** jsx-primitives, jsx-bento, bespoke-slide, primitive-composition +**Primitive map:** Card→`_shape(round_rect)`, Text→`_text()`, Line→`_line()`, Image→`_image()`, Oval→`_shape(oval)` +**Editability failure modes:** nested flex containers, auto-sized text frames, SVG filter effects, rotated text boxes, image fills on shapes +**Agent rule:** Auto-layout is the enemy of editability. All coordinates are in inches. Bespoke layout is a last resort. +**Best for:** Understanding limits of programmatic slide composition; cautionary reference for auto-layout / PPTX editability incompatibility + +--- + +### `gabberflast-academic-pptx-skill` +**Name:** Gabberflast/academic-pptx-skill — Narrative Discipline Gates +**Kind:** agent-skill-context +**License:** MIT — Copyright Gabberflast contributors +**Source:** [Gabberflast/academic-pptx-skill](https://github.com/Gabberflast/academic-pptx-skill) +**Key patterns:** action-title, ghost-deck-test, one-exhibit-discipline, evidence-slide, citation-slide +**Narrative gates:** action title on every content slide; ghost deck test passes; one exhibit per slide; last slide names a specific next action; every quantitative claim has a source +**Agent rule:** Run ghost deck test before building slides. Rewrite descriptive titles as action titles. One exhibit per slide is a hard rule. The closing slide must name a decision, deadline, and owner. +**Best for:** High-stakes governance or board presentations requiring narrative rigour; decks reviewed by investors, regulators, or boards + +--- + +## Using This Catalog + +**When the toolkit is present**, load full style context with: + +```powershell +uv run python pptify-plugin/design/design_context_catalog.py --profile --include-context --pretty +``` + +**When the toolkit is absent**, use the entries above to: + +1. Select the profile ID that best matches the user's audience, topic, and delivery context. +2. Lock the palette, typography, and signature element conventions described in the profile's `source_signals`. +3. Record the selected profile ID, source URL, and license in `summary.design_context` before building the deck spec. +4. Translate the style signals directly into explicit `layout_tree` primitives — colors, fills, rules, card shells, accent bands, and bboxes. diff --git a/plugins/pptify/.agent/skills/pptify-deck-generation/SKILL.md b/skills/pptify-deck-generation/SKILL.md similarity index 70% rename from plugins/pptify/.agent/skills/pptify-deck-generation/SKILL.md rename to skills/pptify-deck-generation/SKILL.md index c80072271..c222fad16 100644 --- a/plugins/pptify/.agent/skills/pptify-deck-generation/SKILL.md +++ b/skills/pptify-deck-generation/SKILL.md @@ -43,83 +43,20 @@ The business framework is defined by the user, not by the assistant. If the user ## Framework Story Templates -Use the selected framework as the starting narrative spine, then adapt slide count and evidence density to the user's source material and requested deliverable. +Use the selected framework as the starting narrative spine, then adapt slide count and evidence density to the user's source material. -### McKinsey Structure (`mckinsey`) - -1. Title (`title`) - Conclusion in one sentence. -2. Executive Summary (`agenda`) - Three bullets: situation, insight, recommendation. -3. Situation (`bullets`) - Context that audience already knows. -4. Complication (`bullets`) - What changed or what problem emerged. -5. Key Question (`section`) - The central question this deck answers. -6. Recommendation (`cards`) - The answer, clearly stated. -7. Evidence 1 (`stats` or `cards`) - Data supporting the recommendation. -8. Evidence 2 (`comparison` or `timeline`) - Further evidence. -9. Evidence 3 (`bullets` or `diagram`) - Additional support. -10. Options Considered (`comparison`) - Why this option versus alternatives. -11. Implementation Roadmap (`timeline`) - Next steps with owners. -12. Appendix (`section`) - Label for backup slides. - -### SCQA Structure (`scqa`) - -1. Title (`title`). -2. Situation (`bullets`) - Agreed context. -3. Complication (`stats`) - What disrupted the situation. -4. Question (`section`) - What question this raises. -5. Answer / Recommendation (`cards`) - Direct answer to the question. -6. Supporting Evidence (`stats` or `comparison`). -7. Implementation Plan (`timeline`). -8. Summary (`summary`). - -### Pyramid Structure (`pyramid`) - -1. Title (`title`). -2. Main Answer (`agenda`) - Top of pyramid: the governing thought. -3. Argument 1 (`cards` or `bullets`) - First key line of reasoning. -4. Argument 2 (`cards` or `bullets`) - Second key line of reasoning. -5. Argument 3 (`cards` or `bullets`) - Third key line of reasoning. -6. Evidence (`stats`) - Data underpinning the arguments. -7. Summary (`summary`) - Pyramid restated. - -### MECE Structure (`mece`) - -1. Title (`title`). -2. Problem Decomposition (`diagram`) - The MECE issue tree. -3. Workstream 1 (`bullets` or `cards`) - Sub-issue and findings. -4. Workstream 2 (`bullets` or `cards`). -5. Workstream 3 (`bullets` or `cards`). -6. Synthesis (`summary`) - Integrated conclusion. - -### Action-Title Structure (`action-title`) - -Rule: every slide title must be an action statement or concluded insight. - -1. Title (`title`) - Action-oriented title. -2. Summary of Actions (`agenda`). -3. Content slides (`any supported slide form`) - Title follows the pattern "We must X" or "X increased by Y%." -4. Next Steps (`timeline`) - Owners and dates. - -### Assertion-Evidence Structure (`assertion-evidence`) - -Rule: each slide has an assertion title in one sentence and an evidence body with visual or data support. - -1. Title (`title`). -2. Overview Assertion (`bullets`). -3. Assertion slides (`stats`, `cards`, or `comparison`). -4. Conclusion (`summary`). - -### Exec-Summary-First Structure (`exec-summary-first`) - -1. Title (`title`). -2. Executive Summary (`agenda`) - Full answer on slide 2. -3. Supporting Detail (`any supported slide form`) - For readers who want depth. -4. Appendix Section (`section`). - -### Custom Structure (`custom`) - -Use `custom` only when the user supplies or explicitly requests a user-defined framework. Before planning slides, collect the framework name, objective, required slide sequence, title rules, layout preferences, evidence expectations, and any mandatory sections. If the custom framework is incomplete, ask for the missing structure rather than filling it in silently. +| Framework | Default slide spine | +|---|---| +| `mckinsey` | Title → executive summary → situation → complication → key question → recommendation → 2-3 evidence slides → options → roadmap → appendix | +| `scqa` | Title → situation → complication → question → answer → evidence → implementation plan → summary | +| `pyramid` | Title → main answer → argument 1 → argument 2 → argument 3 → evidence → summary | +| `mece` | Title → issue tree → workstream slides → synthesis | +| `action-title` | Title → action summary → action-titled content slides → next steps | +| `assertion-evidence` | Title → overview assertion → assertion/evidence slides → conclusion | +| `exec-summary-first` | Title → full answer on slide 2 → supporting detail → appendix | +| `custom` | Ask for framework name, objective, slide sequence, title rules, layout preferences, and evidence expectations before planning | -Record the resolved custom framework in `summary.business_framework`, including its name, source, slide sequence, title rules, and any assumptions approved by the user. +Record the resolved framework in `summary.business_framework`, including source, slide sequence, title rules, and approved assumptions. ## Storytelling Principles @@ -154,6 +91,12 @@ Record the resolved custom framework in `summary.business_framework`, including 7. Verify workflow gates: source-backed decks include source corpus and RAPTOR summary metadata; requested generated images include a provider attempt manifest; successful generated raster infographics include a final hidden SVG appendix slide; generated decks include `summary.design_context` and style-derived visual elements. 8. Rebuild after each repair until generated slides have zero collisions, zero overflows, and no default-theme design failures, or clearly report the residual issue. +```powershell +# Preferred renderer when available; otherwise use direct PowerPoint generation and keep this audit step. +uv run python -m pptify deck-spec.json --out deck.pptx --audit deck-audit.json +uv run python pptify-plugin/audit/audit.py deck-spec.json --json +``` + ## Response Contract 1. When asked to author the deck spec, write strict JSON with no markdown fences unless the user explicitly asks for prose. diff --git a/plugins/pptify/.agent/skills/pptify-quality-gates/SKILL.md b/skills/pptify-quality-gates/SKILL.md similarity index 81% rename from plugins/pptify/.agent/skills/pptify-quality-gates/SKILL.md rename to skills/pptify-quality-gates/SKILL.md index 2c4498ca8..9be914ee1 100644 --- a/plugins/pptify/.agent/skills/pptify-quality-gates/SKILL.md +++ b/skills/pptify-quality-gates/SKILL.md @@ -5,8 +5,18 @@ description: "Validate and repair pptify PPTX artifacts. Use when checking deck # PPTify Quality Gates +> **Prerequisite:** Before running `audit.py`, run the workspace detection check in `pptify-tooling`. If `pptify-plugin/` is absent, load [`references/audit-checklist.md`](references/audit-checklist.md) and apply the manual checklist — it covers all 8 audit dimensions without requiring the script. + Use this skill before considering a generated PPTX complete. +## Workflow + +1. Confirm required artifacts exist or collect missing paths before validating. +2. Run the workspace detection check from `pptify-tooling`. +3. Run `audit.py` when available; otherwise load `references/audit-checklist.md` and apply the manual checks. +4. Repair the spec or generation script, rebuild the PPTX, and rerun the audit. +5. Stop only when collisions, overflows, small fonts, package checks, and design-context checks are clean or clearly reported. + ## Required Artifacts - If required artifact paths or names are missing, collect them with the VS Code prompt input dialog (`vscode_askQuestions` or equivalent) before building, validating, or repairing. @@ -42,6 +52,11 @@ Use this skill before considering a generated PPTX complete. ## Verification Commands - Current workspace reality check: no importable `pptify/` package or `python -m pptify` CLI is present in this snapshot. Use the standalone audit plugin and package inspection unless the core renderer package is restored. -- Audit a layout-tree spec with `uv run python pptify-plugin/audit/audit.py deck-spec.json --json`. -- Run the full current test suite with `uv run python -m unittest discover -s tests -v`. +- Audit a layout-tree spec and run the full test suite: + +```powershell +uv run python pptify-plugin/audit/audit.py deck-spec.json --json +uv run python -m unittest discover -s tests -v +``` + - If the core renderer package is restored, add renderer/CLI smoke tests before considering rendered deck behavior covered. diff --git a/skills/pptify-quality-gates/references/audit-checklist.md b/skills/pptify-quality-gates/references/audit-checklist.md new file mode 100644 index 000000000..4afc618bd --- /dev/null +++ b/skills/pptify-quality-gates/references/audit-checklist.md @@ -0,0 +1,88 @@ +# PPTify Manual Audit Checklist + +Load this file when `pptify-plugin/audit/audit.py` is unavailable. +Apply every check manually to `deck-spec.json` before considering a deck production-ready. + +## 1. Content Collisions + +For every slide, inspect all `layout_tree` objects. Two `classification: "content"` objects collide when their bounding boxes overlap: + +``` +A.x < B.x + B.w AND B.x < A.x + A.w +A.y < B.y + B.h AND B.y < A.y + A.h +``` + +- **Pass:** zero overlapping content objects per slide. +- **Fail:** any overlap → move objects, resize bboxes, reduce content density, or split the slide. + +## 2. Text Overflows + +For each text object estimate whether its text fits within its bbox. + +Rough capacity: +- Characters per line ≈ `(bbox.w × 10) / font_size` +- Lines available ≈ `(bbox.h × 72) / (font_size × 1.2)` + _(bbox in inches, font_size in pt)_ + +- **Pass:** estimated text volume ≤ available capacity. +- **Fail:** likely overflow → shorten bullets, enlarge bbox, or split slide. + **Never set `font_size` below 9 pt for `classification: "content"` objects.** + +## 3. Font Size Minimums + +Scan every object with `classification: "content"`. Check `style.font_size`. + +- **Pass:** all content objects ≥ 9 pt. +- **Fail:** any content object < 9 pt → increase font size and split content if needed. + +## 4. Design Context Presence + +Inspect `summary.design_context` in the spec root. + +- **Pass:** field present and contains `profile_id`, source URL, and license ID. +- **Fail — any of the following:** + - `summary.design_context` absent → load a `pptify-design` profile (see `references/design-profiles.md` in `pptify-context-prep`) and rebuild. + - Plain white backgrounds throughout with no accent elements. + - Calibri-only text with default theme colors across all slides. + - All slides are title-plus-bullets only (no cards, shapes, rules, or image treatments). + +## 5. Visual Design Per Slide + +For each normal content slide (exclude section headers and hidden appendix slides): + +- **Pass:** at least one style-derived visual element present — accent band, card shell, grid cell, rule/divider, shape motif, image treatment, or background pattern. +- **Fail:** slide is plain white with only text objects → add a design element derived from the selected profile's `source_signals`. + +## 6. Narrative and Count + +- Slide count is within ±2 of the user's requested count. +- Topic sequence matches the requested business framework (McKinsey, SCQA, pyramid, etc.) or the user's stated structure. +- If `likaku-mck-ppt-design-skill` or `gabberflast-academic-pptx-skill` context was used: every content slide has an **action title** (not a descriptive label). Run the ghost-deck test: read only slide titles — they must tell the full story on their own. + +## 7. Hidden Slides + +If the deck contains hidden slides (`hidden: true`): + +- **Pass:** hidden slides are last in the `slides` array unless the user specified otherwise. +- In the rendered PPTX, confirm `ppt/presentation.xml` contains `p:sldId show="0"` on the correct entries. + +## 8. Asset Layering + +For slides mixing image/SVG objects with text: + +- **Pass:** image/SVG `z_index` is lower than all overlapping text objects. +- **Fail:** image covers text → lower `z_index`, adjust bbox, or reclassify as `classification: "layout_design"`. +- When a generated infographic exists as both raster and SVG: the raster must be on the **visible** slide; the SVG must be in a **hidden appendix** slide only. + +## 9. Audit Script (when toolkit is present) + +```powershell +uv run python pptify-plugin/audit/audit.py deck-spec.json --json +``` + +Check `total_collisions`, `total_overflows`, `total_small_fonts`, and `warnings` per slide even when the numeric totals are zero. + +## Completion Criterion + +All 8 checks pass before delivery. +Any failure triggers the repair loop in `pptify-quality-gates`: fix the spec, rebuild, and re-audit. diff --git a/plugins/pptify/.agent/skills/pptify-slide-spec/SKILL.md b/skills/pptify-slide-spec/SKILL.md similarity index 93% rename from plugins/pptify/.agent/skills/pptify-slide-spec/SKILL.md rename to skills/pptify-slide-spec/SKILL.md index eabf8c874..590a48687 100644 --- a/plugins/pptify/.agent/skills/pptify-slide-spec/SKILL.md +++ b/skills/pptify-slide-spec/SKILL.md @@ -9,6 +9,14 @@ Use this skill when writing or repairing a coordinate-explicit JSON deck spec. Author final coordinates directly in `layout_tree`; current plugin scripts will not choose layouts, measure browser boxes, or shrink text to fit. Split dense material across slides rather than relying on tiny fonts. +## Workflow + +1. Define slide messages, design context, and slide size before writing objects. +2. Create each slide with `id`, `title`, and a complete `layout_tree`. +3. Place groups and objects with final inch-based bboxes, z-order, and style values. +4. Add at least one style-derived `layout_design` element on every normal content slide. +5. Audit collisions, text density, font sizes, and default-theme failures before shipping. + ## Deck Shape - Return a JSON object with a top-level `slides` array for generated decks. diff --git a/skills/pptify-tooling/SKILL.md b/skills/pptify-tooling/SKILL.md new file mode 100644 index 000000000..d1d641de8 --- /dev/null +++ b/skills/pptify-tooling/SKILL.md @@ -0,0 +1,89 @@ +--- +name: pptify-tooling +description: "Command reference for pptify plugin tools. Use when looking up install commands, plugin script syntax, or the workspace reality check." +--- + +# PPTify Tooling + +## Workflow + +1. Run the workspace detection check before invoking any plugin script. +2. If `pptify-plugin/` is missing, read `references/toolkit-setup.md` before responding. +3. Ask before cloning or installing the optional external toolkit. +4. Run helper scripts only after dependencies are present. +5. Treat the renderer import check as diagnostic; current external toolkit installs helper scripts, not `python -m pptify`. + +## Install + +```powershell +uv sync # base project +uv sync --extra plugins # add source ingestion and image helpers +``` + +## Workspace Detection + +Run this check **before** invoking any plugin script. Do not assume the toolkit is present. + +```powershell +# PowerShell +Test-Path "pptify-plugin\README.md" +``` +```bash +# bash / macOS / Linux +test -f pptify-plugin/README.md && echo "present" || echo "missing" +``` + +**Decision table — act on the result before continuing:** + +| `pptify-plugin/` found | `pyproject.toml` found | Action | +|---|---|---| +| Yes | Yes | Proceed: run `uv run python pptify-plugin/...` commands normally | +| Yes | No | Run `uv sync --extra plugins` in the repo root, then retry | +| No | — | **Read [`references/toolkit-setup.md`](references/toolkit-setup.md) now** (before responding), then ask the user whether to install the optional toolkit or apply graceful fallbacks | + +**Optional toolkit install:** + +Do not clone or install the external toolkit automatically. Ask the user before fetching code from `https://github.com/kimtth/agent-pptify-kit`. + +If the user approves installation: + +```powershell +# Clone into the workspace root (or a subdirectory if another project already occupies it) +git clone https://github.com/kimtth/agent-pptify-kit . +uv sync --extra plugins +``` + +If the workspace root already belongs to a different project, ask the user where to place the toolkit before cloning. + +**Graceful degradation — if install is not possible, apply these fallbacks:** + +| Affected skill | Blocked capability | Fallback | +|---|---|---| +| `pptify-context-prep` | Document-to-markdown conversion, RAPTOR summary, design profile loading | Ask the user to paste source content directly; load `references/design-profiles.md` from `pptify-context-prep` for bundled design profile guidance | +| `pptify-visual-assets` | Icon search, image search, raster→SVG, infographic generation | Use `bbox` placeholder objects with descriptive `content.alt`; omit image objects rather than leaving them empty | +| `pptify-quality-gates` | Spec audit via `audit.py` | Apply the manual checklist rules in that skill; skip the `audit.py` output check | +| `pptify-deck-generation` | End-to-end PPTX render via `pptify` CLI | Stop and inform the user — PPTX generation requires the renderer; do not produce a partial artifact | + +**Renderer reality check:** + +```powershell +# Diagnostic only; this currently fails in the external toolkit +uv run python -c "import pptify; print('renderer present')" +``` + +If the import fails with `ModuleNotFoundError: No module named 'pptify'`, the `python -m pptify` render command is unavailable. This is expected for the current external toolkit. Use standalone plugin scripts for all non-render steps, and do not claim that `uv sync` restores the renderer. + +## Plugin Scripts + +| Purpose | Command | +|---|---| +| Convert document to markdown | `uv run python pptify-plugin/documents/document_to_markdown.py --source --output-path out.md` | +| Build RAPTOR summary tree | `uv run python pptify-plugin/documents/document_to_raptor_tree.py --markdown-path source.md --output-path summary.json --title "Title" --pretty` | +| List design profiles | `uv run python pptify-plugin/design/design_context_catalog.py --list --pretty` | +| Load design profile context | `uv run python pptify-plugin/design/design_context_catalog.py --profile fluent-ui-design-tokens --include-context --pretty` | +| Search web images | `uv run python pptify-plugin/images/web_image_search.py --query "topic" --max-num 8 --pretty` | +| Search Iconify icons | `uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 --max-num 8 --pretty` | +| Raster to SVG | `uv run python pptify-plugin/images/raster_image_to_svg.py --source logo.png --output-path logo.svg --pretty` | +| Generate infographic | `uv run python pptify-plugin/images/text_prompt_to_infographic.py --provider azure-openai --size "1024x1024" --prompt "..." --output-path out.png --pretty` | +| Audit spec | `uv run python pptify-plugin/audit/audit.py deck-spec.json --json` | +| Run tests | `uv run python -m unittest discover -s tests -v` | diff --git a/skills/pptify-tooling/references/toolkit-setup.md b/skills/pptify-tooling/references/toolkit-setup.md new file mode 100644 index 000000000..60fbb3ea7 --- /dev/null +++ b/skills/pptify-tooling/references/toolkit-setup.md @@ -0,0 +1,105 @@ +# PPTify Toolkit Setup + +Load this file when `pptify-plugin/` is not found in the workspace and the user wants to install the optional PPTify toolkit. + +## Repository + +| Field | Value | +|---|---| +| URL | https://github.com/kimtth/agent-pptify-kit | +| License | MIT | +| Package manager | `uv` (Python) | + +## Install + +Do not clone or install this external repository automatically. First explain that the built-in awesome-copilot skill includes bundled references, while the external toolkit is only needed for helper-script execution. The current external toolkit does not provide an importable `pptify` renderer module. Continue only after the user explicitly asks to install it. + +```powershell +# 1. Clone into workspace root (use a subdirectory if the root already has a project) +git clone https://github.com/kimtth/agent-pptify-kit . + +# 2. Install base dependencies +uv sync + +# 3. Install plugin extras: source ingestion, image helpers, audit tools +uv sync --extra plugins +``` + +If `uv` is not available: + +```powershell +pip install uv +# or on macOS/Linux: +# curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +If the workspace root belongs to another project, ask the user before cloning. Suggest a named subdirectory, e.g. `git clone https://github.com/kimtth/agent-pptify-kit pptify-kit`. + +## Module Map + +| Module path | Requires extra | What it provides | +|---|---|---| +| `pptify-plugin/documents/document_to_markdown.py` | plugins | Convert PDF, DOCX, HTML, or plain text to markdown for source prep | +| `pptify-plugin/documents/document_to_raptor_tree.py` | plugins | Build a RAPTOR hierarchical summary tree from a markdown source | +| `pptify-plugin/design/design_context_catalog.py` | plugins | List and load pptify-design profiles; returns style context for spec authoring | +| `pptify-plugin/images/web_image_search.py` | plugins | Search the web for candidate images (SerpAPI or DuckDuckGo fallback) | +| `pptify-plugin/images/iconfy_search.py` | plugins | Search Iconify icon library for SVG icons by query, collection, and hex color | +| `pptify-plugin/images/raster_image_to_svg.py` | plugins | Convert raster images to SVG wrappers; optional vector trace mode | +| `pptify-plugin/images/text_prompt_to_infographic.py` | plugins | Generate infographic images via OpenAI or Azure OpenAI (no local fallback) | +| `pptify-plugin/audit/audit.py` | plugins | Validate a deck spec JSON for collisions, overflows, small fonts, and warnings | +| `pptify-plugin/extraction/pptx_extractor.py` | plugins | Extract slide text, style, layout, and media metadata from a reference PPTX | +| `pptify-plugin/extraction/pptx_style_master.py` | plugins | Extract brand, template, and master-slide style facts from a reference PPTX | + +## Image Generation `.env` + +Create `.env` in the workspace root before invoking `text_prompt_to_infographic.py`. Never put secrets in chat or in the prompt dialog — fill the file directly in the editor or terminal. + +```dotenv +# Provider: openai | azure-openai | azure-ai-foundry +PPTIFY_IMAGE_PROVIDER= + +# --- OpenAI --- +OPENAI_API_KEY= +OPENAI_IMAGE_MODEL=gpt-image-1 + +# --- Azure OpenAI --- +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_DEPLOYMENT= + +# --- Azure AI Foundry --- +AZURE_AI_FOUNDRY_ENDPOINT= +AZURE_AI_FOUNDRY_MODEL= +``` + +Use `az login` when managed identity or CLI auth is preferred over an API key. + +## Optional API Keys + +| Variable | Required for | +|---|---| +| `SERPAPI_API_KEY` | Web image search via SerpAPI (falls back to DuckDuckGo without it) | +| `ICONIFY_API_KEY` | Large-volume Iconify icon queries | + +Place these in `.env` alongside the image provider settings. + +## Verification After Install + +```powershell +# Confirm the helper CLI is available +uv run python pptify-cli help + +# List available design profiles +uv run python pptify-plugin/design/design_context_catalog.py --list --pretty + +# Run a spec audit smoke test +uv run python pptify-plugin/audit/audit.py deck-spec.json +``` + +Renderer check, for diagnostics only: + +```powershell +uv run python -c "import pptify; print('renderer present')" +``` + +If this fails with `ModuleNotFoundError: No module named 'pptify'`, that is expected for the current external toolkit. Use the standalone `pptify-plugin/` helper scripts and do not run `python -m pptify`. diff --git a/plugins/pptify/.agent/skills/pptify-visual-assets/SKILL.md b/skills/pptify-visual-assets/SKILL.md similarity index 81% rename from plugins/pptify/.agent/skills/pptify-visual-assets/SKILL.md rename to skills/pptify-visual-assets/SKILL.md index ac97fbe8d..dee379013 100644 --- a/plugins/pptify/.agent/skills/pptify-visual-assets/SKILL.md +++ b/skills/pptify-visual-assets/SKILL.md @@ -5,8 +5,24 @@ description: "Find, generate, and place visual assets for pptify PPTX decks. Use # PPTify Visual Assets +> **Prerequisite:** Before running any plugin script in this skill, run the workspace detection check in `pptify-tooling`. If `pptify-plugin/` is absent, apply the graceful-degradation fallbacks documented there. + Use this skill when a deck needs icons, images, diagrams, infographics, or media-backed slide objects. +## Workflow + +1. Run the workspace detection check from `pptify-tooling`. +2. Choose the asset type: icon, web image, raster/SVG conversion, or generated infographic. +3. Run the relevant helper script and save its output path or result JSON. +4. Add the asset to `layout_tree.objects` with final bbox, `z_index`, `content.alt`, and `classification`. +5. Recheck layering so assets do not cover readable text. + +```powershell +uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 --max-num 8 --pretty +uv run python pptify-plugin/images/web_image_search.py --query "factory traceability dashboard" --max-num 8 --pretty +uv run python pptify-plugin/images/raster_image_to_svg.py --source logo.png --output-path logo.svg --pretty +``` + ## Icons - Search Iconify when an icon improves scanning: `uv run python pptify-plugin/images/iconfy_search.py --query governance --collection fluent --color 0078D4 --max-num 8 --pretty`. From 0511f18fc0954daa7db0638c62e90cad7b28bd98 Mon Sep 17 00:00:00 2001 From: kimtth Date: Tue, 26 May 2026 11:18:25 +0900 Subject: [PATCH 3/4] fix: update pptify-tooling definition --- skills/pptify-tooling/references/toolkit-setup.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/skills/pptify-tooling/references/toolkit-setup.md b/skills/pptify-tooling/references/toolkit-setup.md index 60fbb3ea7..e3b5e1689 100644 --- a/skills/pptify-tooling/references/toolkit-setup.md +++ b/skills/pptify-tooling/references/toolkit-setup.md @@ -42,7 +42,7 @@ If the workspace root belongs to another project, ask the user before cloning. S | `pptify-plugin/documents/document_to_markdown.py` | plugins | Convert PDF, DOCX, HTML, or plain text to markdown for source prep | | `pptify-plugin/documents/document_to_raptor_tree.py` | plugins | Build a RAPTOR hierarchical summary tree from a markdown source | | `pptify-plugin/design/design_context_catalog.py` | plugins | List and load pptify-design profiles; returns style context for spec authoring | -| `pptify-plugin/images/web_image_search.py` | plugins | Search the web for candidate images (SerpAPI or DuckDuckGo fallback) | +| `pptify-plugin/images/web_image_search.py` | plugins | Search the web for candidate images without required API keys | | `pptify-plugin/images/iconfy_search.py` | plugins | Search Iconify icon library for SVG icons by query, collection, and hex color | | `pptify-plugin/images/raster_image_to_svg.py` | plugins | Convert raster images to SVG wrappers; optional vector trace mode | | `pptify-plugin/images/text_prompt_to_infographic.py` | plugins | Generate infographic images via OpenAI or Azure OpenAI (no local fallback) | @@ -74,15 +74,6 @@ AZURE_AI_FOUNDRY_MODEL= Use `az login` when managed identity or CLI auth is preferred over an API key. -## Optional API Keys - -| Variable | Required for | -|---|---| -| `SERPAPI_API_KEY` | Web image search via SerpAPI (falls back to DuckDuckGo without it) | -| `ICONIFY_API_KEY` | Large-volume Iconify icon queries | - -Place these in `.env` alongside the image provider settings. - ## Verification After Install ```powershell From 06b1f35f0e8c516cd81fdf07e0a50cc1371451a3 Mon Sep 17 00:00:00 2001 From: kimtth Date: Tue, 26 May 2026 12:29:48 +0900 Subject: [PATCH 4/4] fix: stabilize generated instructions README ordering The validate-readme workflow failed because npm start regenerated docs/README.instructions.md with a different row order on the Ubuntu GitHub Actions runner. The root cause was OS-dependent default locale behavior in String.prototype.localeCompare. On Windows, the generated README sorted the Japanese C# instruction before the Korean C# instruction. On Ubuntu, the same generator sorted the Korean row before the Japanese row, causing git diff --exit-code to fail after the build step. This patch makes the generator deterministic by passing an explicit "en" locale to localeCompare for instruction title sorting, then commits the regenerated README output expected by CI. --- docs/README.instructions.md | 2 +- eng/update-readme.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/README.instructions.md b/docs/README.instructions.md index 55e5a606f..f31a6e3d9 100644 --- a/docs/README.instructions.md +++ b/docs/README.instructions.md @@ -46,8 +46,8 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-instructions) for guidelines on | [Blazor](../instructions/blazor.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fblazor.instructions.md) | Blazor component and application patterns | | [C# Development](../instructions/csharp.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp.instructions.md) | Guidelines for building C# applications | | [C# MCP Server Development](../instructions/csharp-mcp-server.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-mcp-server.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-mcp-server.instructions.md) | Instructions for building Model Context Protocol (MCP) servers using the C# SDK | -| [C# アプリケーション開発](../instructions/csharp-ja.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ja.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ja.instructions.md) | C# アプリケーション構築指針 by @tsubakimoto | | [C# 코드 작성 규칙](../instructions/csharp-ko.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ko.instructions.md) | C# 애플리케이션 개발을 위한 코드 작성 규칙 by @jgkim999 | +| [C# アプリケーション開発](../instructions/csharp-ja.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ja.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcsharp-ja.instructions.md) | C# アプリケーション構築指針 by @tsubakimoto | | [Caveman Mode](../instructions/caveman-mode.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcaveman-mode.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcaveman-mode.instructions.md) | Terse, low-token responses. Minimal words, no fluff. Full capabilities preserved. Use when: optimize token usage, low-token mode, concise output, caveman mode, reduce verbosity, token-efficient, brief responses. | | [CentOS Administration Guidelines](../instructions/centos-linux.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcentos-linux.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fcentos-linux.instructions.md) | Guidance for CentOS administration, RHEL-compatible tooling, and SELinux-aware operations. | | [Clojure Development Instructions](../instructions/clojure.instructions.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fclojure.instructions.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fclojure.instructions.md) | Clojure-specific coding patterns, inline def usage, code block templates, and namespace handling for Clojure development. | diff --git a/eng/update-readme.mjs b/eng/update-readme.mjs index 147a91c14..fc90f0e65 100644 --- a/eng/update-readme.mjs +++ b/eng/update-readme.mjs @@ -302,8 +302,8 @@ function generateInstructionsSection(instructionsDir) { return { file, filePath, title }; }); - // Sort by title alphabetically - instructionEntries.sort((a, b) => a.title.localeCompare(b.title)); + // Sort by title alphabetically with a fixed locale so generated output is stable across OSes. + instructionEntries.sort((a, b) => a.title.localeCompare(b.title, "en")); console.log(`Found ${instructionEntries.length} instruction files`);