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-PptxPipeline.ps1

662lines · modecode

1#!/usr/bin/env pwsh
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4#Requires -Version 7.0
5
6<#
7.SYNOPSIS
8 Orchestrates PowerPoint slide deck operations via Python scripts.
9
10.DESCRIPTION
11 Manages the Python virtual environment and dispatches to the correct Python
12 script for building, extracting, or validating PowerPoint slide decks. Sets
13 up a venv with required dependencies on first run.
14
15.PARAMETER Action
16 The operation to perform: Build, Extract, or Validate.
17
18.PARAMETER ContentDir
19 Path to the content/ directory containing slide folders and global style.
20 Required for Build; optional for Validate.
21
22.PARAMETER StylePath
23 Path to the global style.yaml file. Required for Build.
24
25.PARAMETER OutputPath
26 Output PPTX file path. Required for Build.
27
28.PARAMETER InputPath
29 Input PPTX file path. Required for Extract and Validate.
30
31.PARAMETER OutputDir
32 Output directory for extracted content. Required for Extract.
33
34.PARAMETER SourcePath
35 Source PPTX for partial rebuilds. Optional for Build.
36
37.PARAMETER TemplatePath
38 Template PPTX file path for themed builds. Optional for Build.
39
40.PARAMETER Slides
41 Comma-separated slide numbers to rebuild. Requires SourcePath. Optional for Build.
42
43.PARAMETER SkipVenvSetup
44 Skip virtual environment creation and dependency installation.
45
46.PARAMETER ImageOutputDir
47 Output directory for exported slide images. Required for Export.
48
49.PARAMETER Resolution
50 DPI resolution for exported slide images. Defaults to 150. Optional for Export.
51
52.EXAMPLE
53 ./Invoke-PptxPipeline.ps1 -Action Build -ContentDir content/ -StylePath content/global/style.yaml -OutputPath slide-deck/presentation.pptx
54
55.EXAMPLE
56 ./Invoke-PptxPipeline.ps1 -Action Extract -InputPath existing-deck.pptx -OutputDir content/
57
58.EXAMPLE
59 ./Invoke-PptxPipeline.ps1 -Action Validate -InputPath slide-deck/presentation.pptx -ContentDir content/
60
61.EXAMPLE
62 ./Invoke-PptxPipeline.ps1 -Action Build -ContentDir content/ -StylePath content/global/style.yaml -OutputPath slide-deck/presentation.pptx -TemplatePath template.pptx
63
64.EXAMPLE
65 ./Invoke-PptxPipeline.ps1 -Action Build -ContentDir content/ -StylePath content/global/style.yaml -OutputPath slide-deck/presentation.pptx -SourcePath slide-deck/presentation.pptx -Slides "3,7,15"
66
67.EXAMPLE
68 ./Invoke-PptxPipeline.ps1 -Action Export -InputPath slide-deck/presentation.pptx -ImageOutputDir slide-deck/validation/ -Slides "1,3,5" -Resolution 150
69#>
70
71# Invoke-* functions consume these script-level parameters through dynamic scoping
72[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TemplatePath',
73 Justification = 'Consumed by Invoke-BuildDeck through dynamic scoping')]
74[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Resolution',
75 Justification = 'Consumed by Invoke-ExportSlides through dynamic scoping')]
76[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ValidationPrompt',
77 Justification = 'Consumed by Invoke-ValidateDeck through dynamic scoping')]
78[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ValidationPromptFile',
79 Justification = 'Consumed by Invoke-ValidateDeck through dynamic scoping')]
80[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ValidationModel',
81 Justification = 'Consumed by Invoke-ValidateDeck through dynamic scoping')]
82[CmdletBinding()]
83param(
84 [Parameter(Mandatory = $true)]
85 [ValidateSet('Build', 'Extract', 'Validate', 'Export')]
86 [string]$Action,
87
88 [Parameter()]
89 [string]$ContentDir,
90
91 [Parameter()]
92 [string]$StylePath,
93
94 [Parameter()]
95 [string]$OutputPath,
96
97 [Parameter()]
98 [string]$InputPath,
99
100 [Parameter()]
101 [string]$OutputDir,
102
103 [Parameter()]
104 [string]$TemplatePath,
105
106 [Parameter()]
107 [string]$SourcePath,
108
109 [Parameter()]
110 [string]$Slides,
111
112 [Parameter()]
113 [string]$ImageOutputDir,
114
115 [Parameter()]
116 [int]$Resolution = 150,
117
118 [Parameter()]
119 [string]$ValidationPrompt,
120
121 [Parameter()]
122 [string]$ValidationPromptFile,
123
124 [Parameter()]
125 [string]$ValidationModel = 'claude-haiku-4.5',
126
127 [Parameter()]
128 [switch]$SkipVenvSetup
129)
130
131$ErrorActionPreference = 'Stop'
132
133$ScriptDir = $PSScriptRoot
134$SkillRoot = Split-Path $ScriptDir
135$VenvDir = Join-Path $SkillRoot '.venv'
136
137#region Environment Setup
138
139function Test-UvAvailability {
140 <#
141 .SYNOPSIS
142 Verifies uv is available on PATH.
143 .OUTPUTS
144 [string] The resolved uv command path.
145 #>
146 [CmdletBinding()]
147 [OutputType([string])]
148 param()
149
150 $resolved = Get-Command 'uv' -ErrorAction SilentlyContinue
151 if ($resolved) {
152 return $resolved.Source
153 }
154 throw 'uv is required but was not found on PATH. Install with: curl -LsSf https://astral.sh/uv/install.sh | sh'
155}
156
157function Initialize-PythonEnvironment {
158 <#
159 .SYNOPSIS
160 Syncs the Python virtual environment and dependencies via uv.
161 .DESCRIPTION
162 Runs uv sync from the skill root directory. Creates the virtual
163 environment and installs all dependencies declared in pyproject.toml.
164 #>
165 [CmdletBinding()]
166 [OutputType([void])]
167 param()
168
169 Write-Host 'Syncing Python environment via uv...'
170 & uv sync --directory $SkillRoot
171 if ($LASTEXITCODE -ne 0) {
172 throw 'Failed to sync Python environment via uv.'
173 }
174 Write-Host 'Environment synchronized.'
175}
176
177function Get-VenvPythonPath {
178 <#
179 .SYNOPSIS
180 Returns the path to the venv Python executable.
181 .OUTPUTS
182 [string] Absolute path to the venv python binary.
183 #>
184 [CmdletBinding()]
185 [OutputType([string])]
186 param()
187
188 if ($IsWindows) {
189 return Join-Path $VenvDir 'Scripts/python.exe'
190 }
191 return Join-Path $VenvDir 'bin/python'
192}
193
194#endregion
195
196#region Parameter Validation
197
198function Assert-BuildParameters {
199 <#
200 .SYNOPSIS
201 Validates that required parameters for Build action are present.
202 #>
203 [CmdletBinding()]
204 [OutputType([void])]
205 param(
206 [Parameter()]
207 [string]$ContentDir,
208
209 [Parameter()]
210 [string]$StylePath,
211
212 [Parameter()]
213 [string]$OutputPath,
214
215 [Parameter()]
216 [string]$Slides,
217
218 [Parameter()]
219 [string]$SourcePath
220 )
221
222 if (-not $ContentDir) {
223 throw 'Build action requires -ContentDir.'
224 }
225 if (-not $StylePath) {
226 throw 'Build action requires -StylePath.'
227 }
228 if (-not $OutputPath) {
229 throw 'Build action requires -OutputPath.'
230 }
231 if ($Slides -and -not $SourcePath) {
232 throw '-Slides requires -SourcePath for partial rebuilds.'
233 }
234}
235
236function Assert-ExtractParameters {
237 <#
238 .SYNOPSIS
239 Validates that required parameters for Extract action are present.
240 #>
241 [CmdletBinding()]
242 [OutputType([void])]
243 param(
244 [Parameter()]
245 [string]$InputPath,
246
247 [Parameter()]
248 [string]$OutputDir
249 )
250
251 if (-not $InputPath) {
252 throw 'Extract action requires -InputPath.'
253 }
254 if (-not $OutputDir) {
255 throw 'Extract action requires -OutputDir.'
256 }
257}
258
259function Assert-ValidateParameters {
260 <#
261 .SYNOPSIS
262 Validates that required parameters for Validate action are present.
263 #>
264 [CmdletBinding()]
265 [OutputType([void])]
266 param(
267 [Parameter()]
268 [string]$InputPath
269 )
270
271 if (-not $InputPath) {
272 throw 'Validate action requires -InputPath.'
273 }
274}
275
276function Assert-ExportParameters {
277 <#
278 .SYNOPSIS
279 Validates that required parameters for Export action are present.
280 #>
281 [CmdletBinding()]
282 [OutputType([void])]
283 param(
284 [Parameter()]
285 [string]$InputPath,
286
287 [Parameter()]
288 [string]$ImageOutputDir
289 )
290
291 if (-not $InputPath) {
292 throw 'Export action requires -InputPath.'
293 }
294 if (-not $ImageOutputDir) {
295 throw 'Export action requires -ImageOutputDir.'
296 }
297}
298
299#endregion
300
301#region Script Execution
302
303function Invoke-BuildDeck {
304 <#
305 .SYNOPSIS
306 Runs build_deck.py with the provided parameters.
307 #>
308 [CmdletBinding()]
309 [OutputType([void])]
310 param()
311
312 $python = Get-VenvPythonPath
313 $script = Join-Path $ScriptDir 'build_deck.py'
314
315 $arguments = @(
316 $script,
317 '--content-dir', $ContentDir,
318 '--style', $StylePath,
319 '--output', $OutputPath
320 )
321
322 if ($TemplatePath) {
323 $arguments += '--template'
324 $arguments += $TemplatePath
325 }
326 if ($SourcePath) {
327 $arguments += '--source'
328 $arguments += $SourcePath
329 }
330 if ($Slides) {
331 $arguments += '--slides'
332 $arguments += $Slides
333 }
334
335 Write-Host "Building deck from $ContentDir -> $OutputPath"
336 & $python @arguments
337 if ($LASTEXITCODE -ne 0) {
338 throw "build_deck.py failed with exit code $LASTEXITCODE."
339 }
340}
341
342function Invoke-ExtractContent {
343 <#
344 .SYNOPSIS
345 Runs extract_content.py with the provided parameters.
346 #>
347 [CmdletBinding()]
348 [OutputType([void])]
349 param()
350
351 $python = Get-VenvPythonPath
352 $script = Join-Path $ScriptDir 'extract_content.py'
353
354 $arguments = @(
355 $script,
356 '--input', $InputPath,
357 '--output-dir', $OutputDir
358 )
359
360 if ($Slides) {
361 $arguments += '--slides'
362 $arguments += $Slides
363 }
364
365 Write-Host "Extracting content from $InputPath -> $OutputDir"
366 & $python @arguments
367 if ($LASTEXITCODE -ne 0) {
368 throw "extract_content.py failed with exit code $LASTEXITCODE."
369 }
370}
371
372function Invoke-ValidateDeck {
373 <#
374 .SYNOPSIS
375 Exports slides to images, runs PPTX property checks, and optionally runs
376 Copilot SDK vision validation.
377 .DESCRIPTION
378 Chains Export (PPTX to JPG images), validate_deck.py (speaker notes,
379 slide count), and validate_slides.py (vision-based quality checks via
380 Copilot SDK). The vision step runs when ValidationPrompt or
381 ValidationPromptFile is provided.
382 #>
383 [CmdletBinding()]
384 [OutputType([void])]
385 param()
386
387 $python = Get-VenvPythonPath
388 $hasVisionPrompt = $ValidationPrompt -or $ValidationPromptFile
389 $totalSteps = if ($hasVisionPrompt) { 3 } else { 2 }
390
391 # Default image output directory when not specified
392 if (-not $ImageOutputDir) {
393 $ImageOutputDir = Join-Path (Split-Path $InputPath) 'validation'
394 }
395
396 # Step 1: Export slides to images
397 Write-Host "Step 1/$totalSteps`: Exporting slides to images..."
398 Invoke-ExportSlides
399
400 # Step 2: Run PPTX-only checks (speaker notes, slide count)
401 Write-Host "Step 2/$totalSteps`: Running PPTX property checks..."
402 $pptxScript = Join-Path $ScriptDir 'validate_deck.py'
403 $pptxArgs = @(
404 $pptxScript,
405 '--input', $InputPath
406 )
407 if ($ContentDir) {
408 $pptxArgs += '--content-dir'
409 $pptxArgs += $ContentDir
410 }
411 if ($Slides) {
412 $pptxArgs += '--slides'
413 $pptxArgs += $Slides
414 }
415 $deckOutputPath = Join-Path $ImageOutputDir 'deck-validation-results.json'
416 $pptxArgs += '--output'
417 $pptxArgs += $deckOutputPath
418 $deckReportPath = Join-Path $ImageOutputDir 'deck-validation-report.md'
419 $pptxArgs += '--report'
420 $pptxArgs += $deckReportPath
421 $pptxArgs += '--per-slide-dir'
422 $pptxArgs += $ImageOutputDir
423
424 & $python @pptxArgs
425 if ($LASTEXITCODE -eq 2) {
426 throw "validate_deck.py encountered an error (exit code $LASTEXITCODE)."
427 }
428 if ($LASTEXITCODE -eq 1) {
429 Write-Host "PPTX property checks found warnings — see $deckReportPath"
430 }
431
432 # Step 3: Run Copilot SDK vision validation (when prompt provided)
433 if ($hasVisionPrompt) {
434 Write-Host "Step 3/$totalSteps`: Running Copilot SDK vision validation..."
435 $visionScript = Join-Path $ScriptDir 'validate_slides.py'
436 $visionArgs = @(
437 $visionScript,
438 '--image-dir', $ImageOutputDir,
439 '--model', $ValidationModel
440 )
441 if ($ValidationPrompt) {
442 $visionArgs += '--prompt'
443 $visionArgs += $ValidationPrompt
444 }
445 if ($ValidationPromptFile) {
446 $visionArgs += '--prompt-file'
447 $visionArgs += $ValidationPromptFile
448 }
449 $visionOutputPath = Join-Path $ImageOutputDir 'validation-results.json'
450 $visionArgs += '--output'
451 $visionArgs += $visionOutputPath
452 if ($Slides) {
453 $visionArgs += '--slides'
454 $visionArgs += $Slides
455 }
456
457 & $python @visionArgs
458 if ($LASTEXITCODE -ne 0) {
459 throw "validate_slides.py failed with exit code $LASTEXITCODE."
460 }
461 Write-Host "Vision validation results: $visionOutputPath"
462 }
463}
464
465function Invoke-ExportSlides {
466 <#
467 .SYNOPSIS
468 Exports PPTX slides to PDF then converts to JPG images.
469 .DESCRIPTION
470 Calls export_slides.py to convert PPTX to PDF, then uses pdftoppm
471 (from poppler) or a PyMuPDF fallback to render PDF pages as JPGs.
472 #>
473 [CmdletBinding()]
474 [OutputType([void])]
475 param()
476
477 $python = Get-VenvPythonPath
478 $exportScript = Join-Path $ScriptDir 'export_slides.py'
479
480 # Pre-flight: verify LibreOffice is available (required for PPTX-to-PDF)
481 $libreoffice = Get-Command 'libreoffice' -ErrorAction SilentlyContinue
482 if (-not $libreoffice) {
483 $libreoffice = Get-Command 'soffice' -ErrorAction SilentlyContinue
484 }
485 if (-not $libreoffice) {
486 $installHint = if ($IsMacOS) { 'brew install --cask libreoffice' }
487 elseif ($IsWindows) { 'winget install TheDocumentFoundation.LibreOffice' }
488 else { 'sudo apt-get install libreoffice' }
489 throw "LibreOffice is required for PPTX-to-PDF export but was not found on PATH. Install with: $installHint"
490 }
491
492 # Ensure output directory exists
493 if (-not (Test-Path $ImageOutputDir)) {
494 New-Item -ItemType Directory -Path $ImageOutputDir -Force | Out-Null
495 }
496
497 # Clear stale slide images from prior runs to prevent validate_slides.py
498 # from picking up outdated images that no longer represent the current deck.
499 $staleImages = Get-ChildItem -Path $ImageOutputDir -Filter 'slide-*.jpg' -ErrorAction SilentlyContinue
500 if ($staleImages) {
501 $staleImages | Remove-Item -Force
502 Write-Host "Cleared $($staleImages.Count) stale slide image(s) from $ImageOutputDir"
503 }
504
505 $pdfOutput = Join-Path $ImageOutputDir 'slides.pdf'
506
507 # Build arguments for export_slides.py
508 $arguments = @(
509 $exportScript,
510 '--input', $InputPath,
511 '--output', $pdfOutput
512 )
513 if ($Slides) {
514 $arguments += '--slides'
515 $arguments += $Slides
516 }
517
518 Write-Host "Exporting slides from $InputPath to PDF"
519 & $python @arguments
520 if ($LASTEXITCODE -ne 0) {
521 throw "export_slides.py failed with exit code $LASTEXITCODE."
522 }
523
524 # Convert PDF to JPG images
525 ConvertTo-SlideImages -PdfPath $pdfOutput -OutputDir $ImageOutputDir -Dpi $Resolution -SlideNumbers $Slides
526
527 # Clean up intermediate PDF
528 if (Test-Path $pdfOutput) {
529 Remove-Item $pdfOutput -Force
530 Write-Host 'Cleaned up intermediate PDF.'
531 }
532}
533
534function ConvertTo-SlideImages {
535 <#
536 .SYNOPSIS
537 Converts PDF pages to JPG images using pdftoppm or PyMuPDF fallback.
538 .DESCRIPTION
539 When SlideNumbers is provided, output images are named to match the
540 original slide numbers (e.g. slide-023.jpg) instead of sequential
541 numbering (slide-001.jpg). This ensures validate_slides.py can find
542 images by their actual slide number after filtered exports.
543 .PARAMETER PdfPath
544 Path to the PDF file to convert.
545 .PARAMETER OutputDir
546 Directory where JPG files will be saved.
547 .PARAMETER Dpi
548 Resolution in DPI for the rendered images.
549 .PARAMETER SlideNumbers
550 Comma-separated original slide numbers for output naming. When the
551 PDF contains a filtered subset of slides, this maps each sequential
552 PDF page to the correct original slide number in the filename.
553 #>
554 [CmdletBinding()]
555 [OutputType([void])]
556 param(
557 [Parameter(Mandatory = $true)]
558 [string]$PdfPath,
559
560 [Parameter(Mandatory = $true)]
561 [string]$OutputDir,
562
563 [Parameter()]
564 [int]$Dpi = 150,
565
566 [Parameter()]
567 [string]$SlideNumbers
568 )
569
570 $pdftoppm = Get-Command 'pdftoppm' -ErrorAction SilentlyContinue
571 if ($pdftoppm) {
572 Write-Host "Converting PDF to JPG via pdftoppm (${Dpi} DPI)"
573 $prefix = Join-Path $OutputDir 'slide'
574 & pdftoppm -jpeg -r $Dpi $PdfPath $prefix
575 if ($LASTEXITCODE -ne 0) {
576 throw "pdftoppm failed with exit code $LASTEXITCODE."
577 }
578
579 # Collect sequentially-numbered output files sorted by number
580 $seqFiles = Get-ChildItem -Path $OutputDir -Filter 'slide-*.jpg' |
581 Where-Object { $_.Name -match '^slide-(\d+)\.jpg$' } |
582 Sort-Object { [int]($_.Name -replace '^slide-(\d+)\.jpg$', '$1') }
583
584 if ($SlideNumbers) {
585 # Rename from sequential numbers to original slide numbers
586 $targetNums = $SlideNumbers -split ',' | ForEach-Object { [int]$_.Trim() }
587 $idx = 0
588 foreach ($file in $seqFiles) {
589 if ($idx -lt $targetNums.Count) {
590 $newName = 'slide-{0:D3}.jpg' -f $targetNums[$idx]
591 if ($file.Name -ne $newName) {
592 Rename-Item -Path $file.FullName -NewName $newName
593 }
594 $idx++
595 }
596 }
597 }
598 else {
599 # Zero-pad to 3 digits for consistency (slide-1.jpg -> slide-001.jpg)
600 foreach ($file in $seqFiles) {
601 if ($file.Name -match '^slide-(\d+)\.jpg$') {
602 $num = [int]$Matches[1]
603 $newName = 'slide-{0:D3}.jpg' -f $num
604 if ($file.Name -ne $newName) {
605 Rename-Item -Path $file.FullName -NewName $newName
606 }
607 }
608 }
609 }
610 }
611 else {
612 Write-Host 'pdftoppm not found, falling back to PyMuPDF'
613 $python = Get-VenvPythonPath
614 $renderScript = Join-Path $ScriptDir 'render_pdf_images.py'
615
616 $renderArgs = @($renderScript, '--input', $PdfPath, '--output-dir', $OutputDir, '--dpi', $Dpi)
617 if ($SlideNumbers) {
618 $renderArgs += '--slide-numbers'
619 $renderArgs += $SlideNumbers
620 }
621
622 & $python @renderArgs
623 if ($LASTEXITCODE -ne 0) {
624 throw "render_pdf_images.py failed with exit code $LASTEXITCODE."
625 }
626 }
627
628 $imageCount = (Get-ChildItem -Path $OutputDir -Filter 'slide-*.jpg').Count
629 Write-Host "Exported $imageCount slide image(s) to $OutputDir"
630}
631
632#endregion
633
634#region Main
635
636if ($MyInvocation.InvocationName -ne '.') {
637 if (-not $SkipVenvSetup) {
638 Test-UvAvailability | Out-Null
639 Initialize-PythonEnvironment
640 }
641
642 switch ($Action) {
643 'Build' {
644 Assert-BuildParameters -ContentDir $ContentDir -StylePath $StylePath -OutputPath $OutputPath -Slides $Slides -SourcePath $SourcePath
645 Invoke-BuildDeck
646 }
647 'Extract' {
648 Assert-ExtractParameters -InputPath $InputPath -OutputDir $OutputDir
649 Invoke-ExtractContent
650 }
651 'Validate' {
652 Assert-ValidateParameters -InputPath $InputPath
653 Invoke-ValidateDeck
654 }
655 'Export' {
656 Assert-ExportParameters -InputPath $InputPath -ImageOutputDir $ImageOutputDir
657 Invoke-ExportSlides
658 }
659 }
660}
661
662#endregion
663