microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/1124-exclude-python-env-dirs-from-skill-validation

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/skills/experimental/powerpoint/scripts/invoke-pptx-pipeline.sh

354lines · modecode

1#!/usr/bin/env bash
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4#
5# invoke-pptx-pipeline.sh
6# Orchestrates PowerPoint slide deck operations via Python scripts.
7
8set -euo pipefail
9
10SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11SKILL_ROOT="$(dirname "${SCRIPT_DIR}")"
12VENV_DIR="${SKILL_ROOT}/.venv"
13
14# Defaults
15RESOLUTION=150
16VALIDATION_MODEL="claude-haiku-4.5"
17SKIP_VENV_SETUP=false
18
19err() {
20 printf "ERROR: %s\n" "$1" >&2
21 exit 1
22}
23
24usage() {
25 cat <<EOF
26Usage: $(basename "$0") --action <Action> [OPTIONS]
27
28Actions:
29 build Build a PowerPoint deck from YAML content
30 extract Extract content from an existing PPTX to YAML
31 validate Validate a PPTX deck (property checks + optional vision)
32 export Export PPTX slides to JPG images
33
34Options:
35 --action <action> Required. build|extract|validate|export
36 --content-dir <path> Content directory (required for build)
37 --style <path> Style YAML path (required for build)
38 --output <path> Output PPTX path (required for build)
39 --input <path> Input PPTX path (required for extract/validate/export)
40 --output-dir <path> Output directory (required for extract)
41 --template <path> Template PPTX for themed builds (optional, build)
42 --source <path> Source PPTX for partial rebuilds (optional, build)
43 --slides <list> Comma-separated slide numbers (optional)
44 --image-output-dir <path> Image output directory (required for export)
45 --resolution <dpi> DPI for exported images (default: 150)
46 --validation-prompt <text> Vision validation prompt text (optional)
47 --validation-prompt-file <path> Vision validation prompt file (optional)
48 --validation-model <model> Vision model name (default: claude-haiku-4.5)
49 --skip-venv-setup Skip virtual environment setup
50 -h, --help Show this help message
51EOF
52 exit 0
53}
54
55test_uv_availability() {
56 if ! command -v uv &>/dev/null; then
57 err "uv is required but was not found on PATH. Install with: curl -LsSf https://astral.sh/uv/install.sh | sh"
58 fi
59}
60
61initialize_python_environment() {
62 echo "Syncing Python environment via uv..."
63 uv sync --directory "${SKILL_ROOT}"
64 echo "Environment synchronized."
65}
66
67get_venv_python_path() {
68 echo "${VENV_DIR}/bin/python"
69}
70
71assert_build_parameters() {
72 [[ -z "${CONTENT_DIR:-}" ]] && err "Build action requires --content-dir."
73 [[ -z "${STYLE_PATH:-}" ]] && err "Build action requires --style."
74 [[ -z "${OUTPUT_PATH:-}" ]] && err "Build action requires --output."
75 if [[ -n "${SLIDES:-}" && -z "${SOURCE_PATH:-}" ]]; then
76 err "--slides requires --source for partial rebuilds."
77 fi
78}
79
80assert_extract_parameters() {
81 [[ -z "${INPUT_PATH:-}" ]] && err "Extract action requires --input."
82 [[ -z "${OUTPUT_DIR:-}" ]] && err "Extract action requires --output-dir."
83}
84
85assert_validate_parameters() {
86 [[ -z "${INPUT_PATH:-}" ]] && err "Validate action requires --input."
87}
88
89assert_export_parameters() {
90 [[ -z "${INPUT_PATH:-}" ]] && err "Export action requires --input."
91 [[ -z "${IMAGE_OUTPUT_DIR:-}" ]] && err "Export action requires --image-output-dir."
92}
93
94invoke_build_deck() {
95 local python
96 python="$(get_venv_python_path)"
97 local script="${SCRIPT_DIR}/build_deck.py"
98
99 local -a args=(
100 "${script}"
101 "--content-dir" "${CONTENT_DIR}"
102 "--style" "${STYLE_PATH}"
103 "--output" "${OUTPUT_PATH}"
104 )
105
106 [[ -n "${TEMPLATE_PATH:-}" ]] && args+=("--template" "${TEMPLATE_PATH}")
107 [[ -n "${SOURCE_PATH:-}" ]] && args+=("--source" "${SOURCE_PATH}")
108 [[ -n "${SLIDES:-}" ]] && args+=("--slides" "${SLIDES}")
109
110 echo "Building deck from ${CONTENT_DIR} -> ${OUTPUT_PATH}"
111 "${python}" "${args[@]}"
112}
113
114invoke_extract_content() {
115 local python
116 python="$(get_venv_python_path)"
117 local script="${SCRIPT_DIR}/extract_content.py"
118
119 local -a args=(
120 "${script}"
121 "--input" "${INPUT_PATH}"
122 "--output-dir" "${OUTPUT_DIR}"
123 )
124
125 [[ -n "${SLIDES:-}" ]] && args+=("--slides" "${SLIDES}")
126
127 echo "Extracting content from ${INPUT_PATH} -> ${OUTPUT_DIR}"
128 "${python}" "${args[@]}"
129}
130
131invoke_export_slides() {
132 local python
133 python="$(get_venv_python_path)"
134 local export_script="${SCRIPT_DIR}/export_slides.py"
135
136 if ! command -v libreoffice &>/dev/null && ! command -v soffice &>/dev/null; then
137 err "LibreOffice is required for PPTX-to-PDF export but was not found on PATH. Install with: brew install --cask libreoffice"
138 fi
139
140 mkdir -p "${IMAGE_OUTPUT_DIR}"
141
142 # Clear stale slide images from prior runs
143 local stale_count
144 stale_count="$(find "${IMAGE_OUTPUT_DIR}" -maxdepth 1 -name 'slide-*.jpg' 2>/dev/null | wc -l | tr -d ' ')"
145 if (( stale_count > 0 )); then
146 find "${IMAGE_OUTPUT_DIR}" -maxdepth 1 -name 'slide-*.jpg' -delete
147 echo "Cleared ${stale_count} stale slide image(s) from ${IMAGE_OUTPUT_DIR}"
148 fi
149
150 local pdf_output="${IMAGE_OUTPUT_DIR}/slides.pdf"
151
152 local -a args=(
153 "${export_script}"
154 "--input" "${INPUT_PATH}"
155 "--output" "${pdf_output}"
156 )
157
158 [[ -n "${SLIDES:-}" ]] && args+=("--slides" "${SLIDES}")
159
160 echo "Exporting slides from ${INPUT_PATH} to PDF"
161 "${python}" "${args[@]}"
162
163 # Convert PDF to JPG images
164 convert_to_slide_images "${pdf_output}" "${IMAGE_OUTPUT_DIR}" "${RESOLUTION}" "${SLIDES:-}"
165
166 # Clean up intermediate PDF
167 if [[ -f "${pdf_output}" ]]; then
168 rm -f "${pdf_output}"
169 echo "Cleaned up intermediate PDF."
170 fi
171}
172
173convert_to_slide_images() {
174 local pdf_path="$1"
175 local output_dir="$2"
176 local dpi="$3"
177 local slide_numbers="${4:-}"
178
179 if command -v pdftoppm &>/dev/null; then
180 echo "Converting PDF to JPG via pdftoppm (${dpi} DPI)"
181 pdftoppm -jpeg -r "${dpi}" "${pdf_path}" "${output_dir}/slide"
182
183 # Rename to zero-padded 3-digit format and optionally remap slide numbers
184 if [[ -n "${slide_numbers}" ]]; then
185 IFS=',' read -ra target_nums <<< "${slide_numbers}"
186 local idx=0
187 for file in $(find "${output_dir}" -maxdepth 1 -name 'slide-*.jpg' | sort); do
188 if (( idx < ${#target_nums[@]} )); then
189 local new_name
190 new_name=$(printf "slide-%03d.jpg" "${target_nums[idx]}")
191 local base_name
192 base_name="$(basename "${file}")"
193 if [[ "${base_name}" != "${new_name}" ]]; then
194 mv "${file}" "${output_dir}/${new_name}"
195 fi
196 (( idx++ )) || true
197 fi
198 done
199 else
200 for file in $(find "${output_dir}" -maxdepth 1 -name 'slide-*.jpg' | sort); do
201 local base_name
202 base_name="$(basename "${file}")"
203 if [[ "${base_name}" =~ ^slide-([0-9]+)\.jpg$ ]]; then
204 local num="${BASH_REMATCH[1]}"
205 local new_name
206 new_name=$(printf "slide-%03d.jpg" "$((10#${num}))")
207 if [[ "${base_name}" != "${new_name}" ]]; then
208 mv "${file}" "${output_dir}/${new_name}"
209 fi
210 fi
211 done
212 fi
213 else
214 echo "pdftoppm not found, falling back to PyMuPDF"
215 local python
216 python="$(get_venv_python_path)"
217 local render_script="${SCRIPT_DIR}/render_pdf_images.py"
218
219 local -a render_args=(
220 "${render_script}"
221 "--input" "${pdf_path}"
222 "--output-dir" "${output_dir}"
223 "--dpi" "${dpi}"
224 )
225 [[ -n "${slide_numbers}" ]] && render_args+=("--slide-numbers" "${slide_numbers}")
226
227 "${python}" "${render_args[@]}"
228 fi
229
230 local image_count
231 image_count="$(find "${output_dir}" -maxdepth 1 -name 'slide-*.jpg' | wc -l | tr -d ' ')"
232 echo "Exported ${image_count} slide image(s) to ${output_dir}"
233}
234
235invoke_validate_deck() {
236 local python
237 python="$(get_venv_python_path)"
238 local has_vision_prompt=false
239 [[ -n "${VALIDATION_PROMPT:-}" || -n "${VALIDATION_PROMPT_FILE:-}" ]] && has_vision_prompt=true
240
241 local total_steps=2
242 ${has_vision_prompt} && total_steps=3
243
244 # Default image output directory
245 if [[ -z "${IMAGE_OUTPUT_DIR:-}" ]]; then
246 IMAGE_OUTPUT_DIR="$(dirname "${INPUT_PATH}")/validation"
247 fi
248
249 # Step 1: Export slides to images
250 echo "Step 1/${total_steps}: Exporting slides to images..."
251 invoke_export_slides
252
253 # Step 2: Run PPTX property checks
254 echo "Step 2/${total_steps}: Running PPTX property checks..."
255 local pptx_script="${SCRIPT_DIR}/validate_deck.py"
256 local -a pptx_args=(
257 "${pptx_script}"
258 "--input" "${INPUT_PATH}"
259 )
260 [[ -n "${CONTENT_DIR:-}" ]] && pptx_args+=("--content-dir" "${CONTENT_DIR}")
261 [[ -n "${SLIDES:-}" ]] && pptx_args+=("--slides" "${SLIDES}")
262
263 local deck_output="${IMAGE_OUTPUT_DIR}/deck-validation-results.json"
264 pptx_args+=("--output" "${deck_output}")
265 local deck_report="${IMAGE_OUTPUT_DIR}/deck-validation-report.md"
266 pptx_args+=("--report" "${deck_report}")
267 pptx_args+=("--per-slide-dir" "${IMAGE_OUTPUT_DIR}")
268
269 local exit_code=0
270 "${python}" "${pptx_args[@]}" || exit_code=$?
271 if (( exit_code == 2 )); then
272 err "validate_deck.py encountered an error (exit code ${exit_code})."
273 fi
274 if (( exit_code == 1 )); then
275 echo "PPTX property checks found warnings — see ${deck_report}"
276 fi
277
278 # Step 3: Vision validation (when prompt provided)
279 if ${has_vision_prompt}; then
280 echo "Step 3/${total_steps}: Running Copilot SDK vision validation..."
281 local vision_script="${SCRIPT_DIR}/validate_slides.py"
282 local -a vision_args=(
283 "${vision_script}"
284 "--image-dir" "${IMAGE_OUTPUT_DIR}"
285 "--model" "${VALIDATION_MODEL}"
286 )
287 [[ -n "${VALIDATION_PROMPT:-}" ]] && vision_args+=("--prompt" "${VALIDATION_PROMPT}")
288 [[ -n "${VALIDATION_PROMPT_FILE:-}" ]] && vision_args+=("--prompt-file" "${VALIDATION_PROMPT_FILE}")
289
290 local vision_output="${IMAGE_OUTPUT_DIR}/validation-results.json"
291 vision_args+=("--output" "${vision_output}")
292 [[ -n "${SLIDES:-}" ]] && vision_args+=("--slides" "${SLIDES}")
293
294 "${python}" "${vision_args[@]}"
295 echo "Vision validation results: ${vision_output}"
296 fi
297}
298
299parse_args() {
300 while (( $# > 0 )); do
301 case "$1" in
302 --action) ACTION="$2"; shift 2 ;;
303 --content-dir) CONTENT_DIR="$2"; shift 2 ;;
304 --style) STYLE_PATH="$2"; shift 2 ;;
305 --output) OUTPUT_PATH="$2"; shift 2 ;;
306 --input) INPUT_PATH="$2"; shift 2 ;;
307 --output-dir) OUTPUT_DIR="$2"; shift 2 ;;
308 --template) TEMPLATE_PATH="$2"; shift 2 ;;
309 --source) SOURCE_PATH="$2"; shift 2 ;;
310 --slides) SLIDES="$2"; shift 2 ;;
311 --image-output-dir) IMAGE_OUTPUT_DIR="$2"; shift 2 ;;
312 --resolution) RESOLUTION="$2"; shift 2 ;;
313 --validation-prompt) VALIDATION_PROMPT="$2"; shift 2 ;;
314 --validation-prompt-file) VALIDATION_PROMPT_FILE="$2"; shift 2 ;;
315 --validation-model) VALIDATION_MODEL="$2"; shift 2 ;;
316 --skip-venv-setup) SKIP_VENV_SETUP=true; shift ;;
317 -h|--help) usage ;;
318 *) err "Unknown option: $1" ;;
319 esac
320 done
321}
322
323main() {
324 parse_args "$@"
325
326 [[ -z "${ACTION:-}" ]] && err "Action is required. Use --action <build|extract|validate|export>."
327
328 if [[ "${SKIP_VENV_SETUP}" == "false" ]]; then
329 test_uv_availability
330 initialize_python_environment
331 fi
332
333 case "${ACTION}" in
334 build)
335 assert_build_parameters
336 invoke_build_deck
337 ;;
338 extract)
339 assert_extract_parameters
340 invoke_extract_content
341 ;;
342 validate)
343 assert_validate_parameters
344 invoke_validate_deck
345 ;;
346 export)
347 assert_export_parameters
348 invoke_export_slides
349 ;;
350 *) err "Unknown action: ${ACTION}. Use build|extract|validate|export." ;;
351 esac
352}
353
354main "$@"
355