param( [string]$CandidatesPath = "artifacts\music-template-extraction\candidates.json", [string]$OutputDir = "artifacts\music-template-extraction\review" ) $ErrorActionPreference = "Stop" $data = Get-Content -Raw -Encoding UTF8 -LiteralPath $CandidatesPath | ConvertFrom-Json $probe = $null try { $probe = New-Object -ComObject KWPP.Application try { $probe.Quit() } catch {} } catch { $fallback = Join-Path $PSScriptRoot "render_music_template_candidates.py" $python = Get-Command python -ErrorAction Stop & $python.Source $fallback $CandidatesPath $OutputDir exit $LASTEXITCODE } finally { if ($probe) { [Runtime.InteropServices.Marshal]::ReleaseComObject($probe) | Out-Null } } $quotas = @{ child = 7; general = 12; traditional = 7; activity = 7; stage = 7; theory = 7 } $seenNames = @{} $seenSignatures = @{} $selected = [System.Collections.Generic.List[object]]::new() foreach ($scene in $quotas.Keys) { $count = 0 $items = $data.items | Where-Object { $_.scene -eq $scene -and $_.score -ge 78 -and $_.slides -ge 10 -and $_.slides -le 60 -and $_.size_mb -le 250 -and $_.median_text -le 120 } | Sort-Object score -Descending foreach ($item in $items) { if ($count -ge $quotas[$scene]) { break } if ($seenNames.ContainsKey($item.name_key) -or $seenSignatures.ContainsKey($item.signature)) { continue } $seenNames[$item.name_key] = $true $seenSignatures[$item.signature] = $true $selected.Add($item) $count += 1 } } New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null $OutputDir = (Resolve-Path -LiteralPath $OutputDir).Path $rendered = [System.Collections.Generic.List[object]]::new() for ($candidateIndex = 0; $candidateIndex -lt $selected.Count; $candidateIndex += 1) { $item = $selected[$candidateIndex] $id = "candidate-{0:d2}" -f ($candidateIndex + 1) $candidateDir = Join-Path $OutputDir $id New-Item -ItemType Directory -Force -Path $candidateDir | Out-Null $app = $null $deck = $null try { $app = New-Object -ComObject KWPP.Application try { $app.Visible = $false } catch {} $deck = $app.Presentations.Open($item.path, $true, $true, $false) $slideCount = $deck.Slides.Count $indexes = @(1, [int][math]::Max(2, [math]::Round($slideCount * 0.25)), [int][math]::Max(2, [math]::Round($slideCount * 0.5)), [int][math]::Max(2, [math]::Round($slideCount * 0.75)), [int]$slideCount) | Sort-Object -Unique foreach ($slideIndex in $indexes) { $slideIndex = [int]$slideIndex $target = Join-Path $candidateDir ("slide-{0:d2}.jpg" -f $slideIndex) $deck.Slides.Item($slideIndex).Export($target, "JPG", 640, 360) } $rendered.Add([pscustomobject]@{ id = $id name = $item.name scene = $item.scene score = $item.score source = $item.path slides = $slideCount samples = @($indexes | ForEach-Object { "slide-{0:d2}.jpg" -f $_ }) }) Write-Host "$id`t$($item.scene)`t$($item.name)" } catch { Write-Warning "$id render failed: $($_.Exception.Message)" } finally { if ($null -ne $deck) { try { $deck.Close() } catch {} } if ($null -ne $app) { try { $app.Quit() } catch {} } if ($null -ne $deck) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($deck) | Out-Null } if ($null -ne $app) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | Out-Null } [gc]::Collect() [gc]::WaitForPendingFinalizers() } } @{ items = $rendered } | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $OutputDir "manifest.json") -Encoding UTF8