diff --git a/pov-doc/SKILL.md b/pov-doc/SKILL.md index d6838dc..05ab8e2 100644 --- a/pov-doc/SKILL.md +++ b/pov-doc/SKILL.md @@ -1,7 +1,7 @@ --- name: pov-doc description: This skill should be used when the user asks to "write a POV doc," "create a proof of value document," "POV criteria," "proof of concept criteria," "generate a POC document," or discusses creating a proof-of-value or proof-of-concept document for a Verkada customer evaluation. Generates a customer-facing POC/POV criteria document for Verkada physical security product evaluations. -version: 1.0.0 +version: 1.1.0 --- # POV Doc @@ -23,13 +23,50 @@ Before generating the document, gather the following from the user's notes and c Search `~/notes` for account notes, meeting notes, and inbox items related to the customer to populate these fields. Use ripgrep to find relevant content across all note files. +### Logo and Mission Statement + +In addition to the fields above, gather these for the document header: + +**Customer logo:** Search the web for the customer's official logo. Prefer high-resolution PNG files (at least 1000px wide). Good sources include the customer's website, Wikipedia/Wikimedia Commons, and official brand asset portals. Download the logo to a temporary location (e.g., `/tmp/`). If no high-res version is available, download what you can find and upscale it 4x using Pillow. + +**Customer mission statement:** Search the web for the customer's official mission statement. Prefer the most recent version from their website or official publications (district improvement plans, annual reports, etc.). If the customer has both a mission and vision statement, use the mission statement. + +Leave both as placeholders in the markdown (`` and `*"..."*`). The rendering step below will inject them into the final document. + +## Document Generation and Rendering + +### Step 1: Generate Markdown + +Follow the Document Structure section below to write the markdown document. + +### Step 2: Render to .odt + +After generating the markdown, render it to a formatted .odt file using the bundled `scripts/render_poc.py` script: + +```bash +python3 skills/pov-doc/scripts/render_poc.py \ + --input "path/to/[Customer Name] POC Criteria.md" \ + --customer-logo /tmp/customer-logo.png \ + --mission "Customer mission statement here" \ + --output "path/to/[Customer Name] POC Criteria.odt" +``` + +The script does the following: +1. Replaces logo HTML comments with the actual customer logo and Verkada logo images +2. Updates the mission statement if `--mission` is provided +3. Converts the markdown to .odt using pandoc with the clean reference template (`examples/POC Reference Template.odt`) + +The reference template carries over professional formatting (fonts, heading sizes, table styles, page margins) from the City of El Paso example without any customer-specific content. + +Dependencies: `pandoc` (on PATH), `Pillow` (for logo resizing). + ## Document Structure The output document must follow this structure. Each section is required unless the user explicitly says it does not apply. ### Header - Document title: `# **Verkada Proof of Concept Criteria**` -- Customer logo placeholder: `` +- Customer logo placeholder: `` - Verkada logo placeholder: `` - Customer mission statement (if known), formatted as a blockquote: `*"MISSION STATEMENT HERE"*` - Table of contents with anchor links to each numbered section @@ -96,12 +133,15 @@ Numbered list of concrete next actions. Include any steps that have already been ## Examples -Two example documents are provided in the `examples/` folder: +Example documents are provided in the `examples/` folder: - `examples/City of El Paso POC Criteria.md` - A completed POV document for a municipal access control evaluation - `examples/Proof of Concept Criteria - TEMPLATE.md` - A template-style POV document for a retail environment +- `examples/POC Reference Template.odt` - Clean ODT reference template for pandoc rendering (styles only, no customer content) -Review these examples to match tone, structure, and formatting conventions. The El Paso example shows how to write a finalized document with specific details filled in. The template example shows the structure with placeholder content for a broader product evaluation. +Review the markdown examples to match tone, structure, and formatting conventions. The El Paso example shows how to write a finalized document with specific details filled in. The template example shows the structure with placeholder content for a broader product evaluation. -## Output Location +## Output -Write the generated document to `~/notes/Inbox/` with the filename format `[Customer Name] POC Criteria.md`. +Write the generated markdown document to `~/notes/Inbox/` with the filename format `[Customer Name] POC Criteria.md`. + +After rendering, the .odt file should be placed alongside the markdown or in the same location the user specifies. diff --git a/pov-doc/assets/verkada-logo.png b/pov-doc/assets/verkada-logo.png new file mode 100644 index 0000000..66bde2b Binary files /dev/null and b/pov-doc/assets/verkada-logo.png differ diff --git a/pov-doc/examples/City of El Paso POC Criteria.odt b/pov-doc/examples/City of El Paso POC Criteria.odt new file mode 100644 index 0000000..02a1db5 Binary files /dev/null and b/pov-doc/examples/City of El Paso POC Criteria.odt differ diff --git a/pov-doc/examples/Proof of Concept Criteria - TEMPLATE.odt b/pov-doc/examples/Proof of Concept Criteria - TEMPLATE.odt new file mode 100644 index 0000000..1427ef2 Binary files /dev/null and b/pov-doc/examples/Proof of Concept Criteria - TEMPLATE.odt differ diff --git a/pov-doc/scripts/render_poc.py b/pov-doc/scripts/render_poc.py new file mode 100644 index 0000000..180075e --- /dev/null +++ b/pov-doc/scripts/render_poc.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +Render a Verkada POC Criteria markdown document as a formatted .odt file. + +This script takes a POC Criteria markdown file (generated by the pov-doc skill), +replaces logo placeholders and mission statement with actual content, and converts +the result to a professionally formatted .odt using pandoc with a reference document. + +Usage: + python render_poc.py --input --customer-logo \ + [--verkada-logo ] [--mission <"mission statement">] \ + [--reference-doc ] [--output ] + +If --verkada-logo is not provided, the Verkada logo placeholder is removed. +If --mission is not provided, the existing mission statement in the markdown is kept. +If --reference-doc is not provided, defaults to the included City of El Paso reference. +If --output is not provided, defaults to the input filename with .odt extension. + +Dependencies: + - pandoc (must be installed and on PATH) + - Pillow (for logo resizing) +""" + +import argparse +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + + +def find_skill_dir() -> Path: + """Locate the pov-doc skill directory relative to this script.""" + return Path(__file__).resolve().parent.parent + + +def find_reference_doc() -> Path: + """Find the default reference ODT template in the skill's examples directory.""" + skill_dir = find_skill_dir() + ref = skill_dir / "examples" / "POC Reference Template.odt" + if ref.exists(): + return ref + # Fallback: look for the El Paso doc and warn + alt = skill_dir / "examples" / "City of El Paso POC Criteria.odt" + if alt.exists(): + print("Warning: using City of El Paso reference (may carry over logos). " + "Run create_reference_template() to generate a clean template.", + file=sys.stderr) + return alt + return None + + +def find_verkada_logo() -> Path: + """Find the bundled Verkada logo in the skill's assets directory.""" + skill_dir = find_skill_dir() + logo = skill_dir / "assets" / "verkada-logo.png" + if logo.exists(): + return logo + return None + + +def resize_logo_if_needed(input_path: str, max_width_inches: float = 2.75, dpi: int = 150) -> str: + """ + Resize a logo image if it exceeds the target width for print. + Returns the path to the (possibly new) resized image. + """ + try: + from PIL import Image + img = Image.open(input_path) + width_px, height_px = img.size + max_width_px = int(max_width_inches * dpi) + + if width_px > max_width_px: + ratio = max_width_px / width_px + new_size = (max_width_px, int(height_px * ratio)) + img_resized = img.resize(new_size, Image.LANCZOS) + + # Write resized version to a temp file + fd, out_path = tempfile.mkstemp(suffix=".png") + os.close(fd) + img_resized.save(out_path, "PNG") + return out_path + + return input_path + except ImportError: + print("Warning: Pillow not installed, skipping logo resize", file=sys.stderr) + return input_path + + +def process_markdown( + md_content: str, + customer_logo_path: str, + verkada_logo_path: str | None, + mission_statement: str | None, +) -> str: + """ + Process the markdown content: + 1. Replace logo HTML comments with markdown image syntax + 2. Optionally update the mission statement + 3. Remove the Table of Contents section (pandoc can generate one) + """ + + # Replace customer logo placeholder (match any logo comment that is NOT Verkada) + md_content = re.sub( + r'\s*\n?', + f'![Customer Logo]({customer_logo_path})\n\n', + md_content, + flags=re.IGNORECASE, + ) + + # Replace Verkada logo placeholder + if verkada_logo_path: + md_content = re.sub( + r'\s*\n?', + f'![Verkada Logo]({verkada_logo_path})\n', + md_content, + flags=re.IGNORECASE, + ) + else: + # Remove the Verkada logo placeholder entirely + md_content = re.sub( + r'\s*\n?', + '', + md_content, + flags=re.IGNORECASE, + ) + + # Update mission statement if provided + if mission_statement: + # Match the existing italicized mission statement + md_content = re.sub( + r'\*"[^"]*"\*\s*\n', + f'*"{mission_statement}"*\n\n', + md_content, + ) + + return md_content + + +def convert_with_pandoc(md_path: str, output_path: str, reference_doc: str) -> bool: + """Convert markdown to .odt using pandoc with a reference document.""" + cmd = [ + "pandoc", + md_path, + "-o", output_path, + "--from", "markdown", + "--to", "odt", + "--reference-doc", reference_doc, + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode != 0: + print(f"pandoc error: {result.stderr}", file=sys.stderr) + return False + return True + except FileNotFoundError: + print("Error: pandoc not found. Install it with your package manager.", file=sys.stderr) + return False + except subprocess.TimeoutExpired: + print("Error: pandoc timed out", file=sys.stderr) + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Render a Verkada POC Criteria markdown document as a formatted .odt file." + ) + parser.add_argument("--input", "-i", required=True, help="Path to the POC Criteria markdown file") + parser.add_argument("--customer-logo", required=True, help="Path to the customer logo image") + parser.add_argument("--verkada-logo", help="Path to the Verkada logo image (optional)") + parser.add_argument("--mission", help="Customer mission statement (replaces existing)") + parser.add_argument("--reference-doc", help="Path to reference .odt for formatting") + parser.add_argument("--output", "-o", help="Output .odt path (default: same as input with .odt)") + + args = parser.parse_args() + + # Validate input file + input_path = Path(args.input) + if not input_path.exists(): + print(f"Error: input file not found: {input_path}", file=sys.stderr) + sys.exit(1) + + # Validate customer logo + if not os.path.isfile(args.customer_logo): + print(f"Error: customer logo not found: {args.customer_logo}", file=sys.stderr) + sys.exit(1) + + # Find reference doc + reference_doc = args.reference_doc or find_reference_doc() + if not reference_doc or not Path(reference_doc).exists(): + print("Error: reference document not found. Provide --reference-doc or place the " + "City of El Paso POC Criteria.odt in the skill's examples directory.", + file=sys.stderr) + sys.exit(1) + + # Find Verkada logo + verkada_logo = args.verkada_logo or find_verkada_logo() + if verkada_logo and not Path(verkada_logo).exists(): + verkada_logo = None + + # Determine output path + if args.output: + output_path = args.output + else: + output_path = str(input_path.with_suffix(".odt")) + + # Resize logos for print + customer_logo_resized = resize_logo_if_needed(args.customer_logo) + verkada_logo_resized = resize_logo_if_needed(verkada_logo) if verkada_logo else None + + # Read and process markdown + md_content = input_path.read_text(encoding="utf-8") + processed = process_markdown( + md_content, + customer_logo_resized, + verkada_logo_resized, + args.mission, + ) + + # Write processed markdown to temp file (so image paths work) + fd, temp_md = tempfile.mkstemp(suffix=".md") + os.close(fd) + try: + with open(temp_md, "w", encoding="utf-8") as f: + f.write(processed) + + # Convert with pandoc + print(f"Converting {input_path.name} to {output_path}...") + print(f" Reference doc: {reference_doc}") + if verkada_logo: + print(f" Verkada logo: {verkada_logo}") + + success = convert_with_pandoc(temp_md, output_path, reference_doc) + + if success: + out_size = os.path.getsize(output_path) + print(f"Done: {output_path} ({out_size:,} bytes)") + else: + sys.exit(1) + finally: + # Clean up temp files + os.unlink(temp_md) + if customer_logo_resized != args.customer_logo: + os.unlink(customer_logo_resized) + if verkada_logo_resized and verkada_logo_resized != verkada_logo: + os.unlink(verkada_logo_resized) + + +if __name__ == "__main__": + main()