#requires -version 5.1 <# kie-image.ps1 - generate hero/OG images via kie.ai (z-image by default). Loads KIE_API_KEY from .env.local at repo root (or $env:KIE_API_KEY). Usage: powershell -File seo-audit-2026-05-27\scripts\kie-image.ps1 -Prompt "..." -OutPath "images/-hero.png" [-Model z-image] [-Aspect 16:9] [-Resolution 2K] Models: z-image (default, fast), nano-banana-2 (premium fallback), nano-banana-pro. Mechanism: POST /api/v1/jobs/createTask with callBackUrl -> ntfy.sh topic -> poll for result. Note: The old /api/v1/jobs/getTaskDetails polling endpoint no longer exists (API change 2026-05). Results are now delivered via callBackUrl (webhook). This script uses ntfy.sh as a free anonymous webhook receiver since no public server is required. #> param( [Parameter(Mandatory)][string]$Prompt, [Parameter(Mandatory)][string]$OutPath, [string]$Model = "z-image", [string]$Aspect = "16:9", [string]$Resolution = "2K", [string]$Format = "png", [int]$TimeoutSec = 300 ) $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) if(-not $env:KIE_API_KEY){ $envFile = Join-Path $repoRoot ".env.local" if(Test-Path $envFile){ $line = Get-Content $envFile -ErrorAction SilentlyContinue | Where-Object { $_ -match '^KIE_API_KEY=' } | Select-Object -First 1 if($line){ $env:KIE_API_KEY = ($line -replace '^KIE_API_KEY=','').Trim('"',"'") } } } if(-not $env:KIE_API_KEY){ throw "KIE_API_KEY not set (env var or .env.local)" } # Generate unique ntfy.sh topic for callback $ntfyTopic = "kie-img-$(Get-Date -Format 'yyyyMMddHHmmss')-$(Get-Random -Maximum 9999)" $callBackUrl = "https://ntfy.sh/$ntfyTopic" $headers = @{ Authorization = "Bearer $($env:KIE_API_KEY)"; 'Content-Type' = 'application/json' } $body = @{ model = $Model input = @{ prompt = $Prompt; aspect_ratio = $Aspect; resolution = $Resolution; output_format = $Format } callBackUrl = $callBackUrl } | ConvertTo-Json -Depth 6 Write-Host "[kie-image] submit model=$Model aspect=$Aspect res=$Resolution" Write-Host "[kie-image] callback=$callBackUrl" $create = Invoke-RestMethod -Uri 'https://api.kie.ai/api/v1/jobs/createTask' -Method POST -Headers $headers -Body $body if($create.code -ne 200){ throw "createTask failed: $($create | ConvertTo-Json -Depth 5 -Compress)" } $taskId = $create.data.taskId Write-Host "[kie-image] taskId=$taskId polling via ntfy.sh..." $deadline = (Get-Date).AddSeconds($TimeoutSec) $imageUrl = $null while((Get-Date) -lt $deadline){ Start-Sleep -Seconds 8 try { $r = Invoke-WebRequest -Uri "https://ntfy.sh/$ntfyTopic/json?poll=1" -UseBasicParsing -ErrorAction Stop $text = [System.Text.Encoding]::UTF8.GetString($r.RawContentStream.ToArray()) # Parse NDJSON lines for kie.ai callback $lines = $text -split '[\r\n]+' | Where-Object { $_ -match '"event":"message"' } foreach ($line in $lines) { try { $msg = $line | ConvertFrom-Json $payload = $msg.message | ConvertFrom-Json if ($payload.code -eq 200 -and $payload.data.taskId -eq $taskId) { $state = $payload.data.state Write-Host "[kie-image] state=$state" if ($state -eq 'success') { $rj = $payload.data.resultJson | ConvertFrom-Json $imageUrl = $rj.resultUrls[0] break } if ($state -eq 'failed' -or $state -eq 'error') { throw "task failed: $($payload | ConvertTo-Json -Depth 5 -Compress)" } } } catch { } } } catch { } if ($imageUrl) { break } } if(-not $imageUrl){ throw "timed out after $TimeoutSec s waiting for task $taskId" } $outFull = Join-Path $repoRoot $OutPath $outDir = Split-Path -Parent $outFull if($outDir -and -not (Test-Path $outDir)){ New-Item -ItemType Directory -Force -Path $outDir | Out-Null } Invoke-WebRequest -Uri $imageUrl -OutFile $outFull -UseBasicParsing Write-Host "[kie-image] saved: $outFull" Write-Output $outFull