Files
justvitamin/scripts/generate-draft-pdp-assets.ps1
T
Omair Saleh 056c47581f feat: editorial review dashboard + elite-grade pilot batch (5 SKUs)
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that
presents the 4-section PDP pilot batch back to Umar, compliance, and the board
in an editorial format. Adds the full data layer that drives it: 5 source-backed
per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed
evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated
product FAQs, and the Matrixify-ready CSV exports for Lewis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:50:09 +08:00

207 lines
9.1 KiB
PowerShell

param(
[switch]$Force
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Drawing
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$AssetRoot = if ($env:ASSET_ROOT) { $env:ASSET_ROOT } else { (Resolve-Path (Join-Path $Root "..\..\jv-assets")).Path }
$OutRoot = Join-Path $Root "content_population_exports"
$ProductsRoot = Join-Path $Root "data\content\products"
$IntelRoot = Join-Path $Root "data\intelligence"
$GeneratedAt = (Get-Date).ToUniversalTime().ToString("o")
New-Item -ItemType Directory -Force -Path $AssetRoot | Out-Null
function Read-JsonFile($Path, $Fallback = $null) {
if (-not (Test-Path -LiteralPath $Path)) { return $Fallback }
return (Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json)
}
function ConvertTo-PlainText($Value) {
if ($null -eq $Value) { return "" }
if ($Value -is [array]) { return (($Value | ForEach-Object { ConvertTo-PlainText $_ }) -join " · ") }
$text = [string]$Value
$text = $text -replace "[\u2013\u2014]", "-"
$text = $text -replace "[^\u0009\u000A\u000D\u0020-\u007E]", ""
$text = $text -replace "\s+", " "
return $text.Trim()
}
function Wrap-Text([System.Drawing.Graphics]$Graphics, [string]$Text, [System.Drawing.Font]$Font, [int]$MaxWidth, [int]$MaxLines = 4) {
$words = (ConvertTo-PlainText $Text) -split "\s+"
$lines = New-Object System.Collections.Generic.List[string]
$line = ""
foreach ($word in $words) {
$candidate = if ($line) { "$line $word" } else { $word }
if ($Graphics.MeasureString($candidate, $Font).Width -le $MaxWidth) {
$line = $candidate
} else {
if ($line) { $lines.Add($line) }
$line = $word
if ($lines.Count -ge $MaxLines) { break }
}
}
if ($line -and $lines.Count -lt $MaxLines) { $lines.Add($line) }
if ($lines.Count -eq $MaxLines -and $words.Count -gt 0) {
$last = $lines[$lines.Count - 1]
if ($Graphics.MeasureString("$last ...", $Font).Width -le $MaxWidth) { $lines[$lines.Count - 1] = "$last ..." }
}
return $lines.ToArray()
}
function Get-ProductThemes($Product, $Sku) {
$themes = New-Object System.Collections.Generic.List[string]
foreach ($benefit in @($Product.metafields.pdpProductBenefits)) {
$t = ConvertTo-PlainText $benefit
if ($t) { $themes.Add($t) }
}
$aspects = Read-JsonFile (Join-Path $IntelRoot "$Sku\review-aspects.json")
foreach ($aspect in @($aspects.aspects | Select-Object -First 3)) {
$t = ConvertTo-PlainText $aspect.aspect
if ($t -and -not $themes.Contains($t)) { $themes.Add($t) }
}
if ($themes.Count -eq 0) {
$themes.Add("Customer-language extraction pending")
$themes.Add("Evidence review required before approval")
$themes.Add("Draft visual asset for staging QA")
}
return @($themes | Select-Object -First 4)
}
function Draw-DraftAsset($TargetPath, $Product, [string]$Field) {
$isMobile = $Field -eq "pdpResultsMobile"
$isCompare = $Field -eq "pdpHowWeCompare"
$width = if ($isMobile) { 1080 } else { 1600 }
$height = if ($isMobile) { 1350 } else { 1000 }
$bitmap = New-Object System.Drawing.Bitmap($width, $height)
$g = [System.Drawing.Graphics]::FromImage($bitmap)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
$bg = [System.Drawing.Color]::FromArgb(247, 248, 255)
$brand = [System.Drawing.Color]::FromArgb(50, 50, 125)
$accent = [System.Drawing.Color]::FromArgb(245, 158, 11)
$muted = [System.Drawing.Color]::FromArgb(91, 97, 120)
$border = [System.Drawing.Color]::FromArgb(220, 225, 245)
$white = [System.Drawing.Color]::White
$g.Clear($bg)
$brandBrush = New-Object System.Drawing.SolidBrush($brand)
$accentBrush = New-Object System.Drawing.SolidBrush($accent)
$mutedBrush = New-Object System.Drawing.SolidBrush($muted)
$whiteBrush = New-Object System.Drawing.SolidBrush($white)
$borderPen = New-Object System.Drawing.Pen($border, 4)
$brandPen = New-Object System.Drawing.Pen($brand, 8)
$titleFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 52 } else { 58 }), [System.Drawing.FontStyle]::Bold)
$subFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 30 } else { 32 }), [System.Drawing.FontStyle]::Regular)
$bodyFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 28 } else { 30 }), [System.Drawing.FontStyle]::Regular)
$smallFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 22 } else { 22 }), [System.Drawing.FontStyle]::Bold)
$draftFont = New-Object System.Drawing.Font("Arial", $(if ($isMobile) { 24 } else { 24 }), [System.Drawing.FontStyle]::Bold)
$margin = if ($isMobile) { 72 } else { 96 }
$contentWidth = $width - (2 * $margin)
$g.FillRectangle($brandBrush, 0, 0, $width, $(if ($isMobile) { 118 } else { 110 }))
$g.DrawString("JUST VITAMINS", $smallFont, $whiteBrush, $margin, 36)
$g.DrawString("DRAFT ASSET - NEEDS HUMAN IMAGE APPROVAL", $draftFont, $accentBrush, $width - $margin - 520, 36)
$y = if ($isMobile) { 170 } else { 160 }
$assetLabel = if ($Field -eq "pdpResults") { "Results / benefit visual" } elseif ($Field -eq "pdpResultsMobile") { "Mobile results visual" } else { "How we compare visual" }
$g.DrawString($assetLabel.ToUpperInvariant(), $smallFont, $accentBrush, $margin, $y)
$y += 48
foreach ($line in (Wrap-Text $g (ConvertTo-PlainText $Product.name) $titleFont $contentWidth 2)) {
$g.DrawString($line, $titleFont, $brandBrush, $margin, $y)
$y += [int]($titleFont.GetHeight($g) + 6)
}
$y += 24
$boxHeight = if ($isMobile) { 560 } else { 410 }
$rect = New-Object System.Drawing.Rectangle($margin, $y, $contentWidth, $boxHeight)
$g.FillRectangle($whiteBrush, $rect)
$g.DrawRectangle($borderPen, $rect)
$innerX = $margin + 44
$innerY = $y + 42
$headline = if ($isCompare) { "Comparison content is queued for review" } else { "Customer-led message scaffold" }
$g.DrawString($headline, $subFont, $brandBrush, $innerX, $innerY)
$innerY += 72
$themes = Get-ProductThemes $Product $Product.sku
$idx = 1
foreach ($theme in $themes) {
$g.FillEllipse($accentBrush, $innerX, $innerY + 8, 30, 30)
$g.DrawString([string]$idx, $smallFont, $whiteBrush, $innerX + 8, $innerY + 6)
foreach ($line in (Wrap-Text $g $theme $bodyFont ($contentWidth - 130) 2)) {
$g.DrawString($line, $bodyFont, $mutedBrush, $innerX + 54, $innerY)
$innerY += [int]($bodyFont.GetHeight($g) + 3)
}
$innerY += 20
$idx += 1
}
$footerY = $height - 120
$g.DrawLine($borderPen, $margin, $footerY - 26, $width - $margin, $footerY - 26)
$footer = "Generated from dashboard evidence only. Not approved. Replace with final creative after quote-level, compliance, and image QA."
foreach ($line in (Wrap-Text $g $footer $smallFont $contentWidth 2)) {
$g.DrawString($line, $smallFont, $mutedBrush, $margin, $footerY)
$footerY += [int]($smallFont.GetHeight($g) + 4)
}
$bitmap.Save($TargetPath, [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose()
$bitmap.Dispose()
foreach ($obj in @($brandBrush, $accentBrush, $mutedBrush, $whiteBrush, $borderPen, $brandPen, $titleFont, $subFont, $bodyFont, $smallFont, $draftFont)) { $obj.Dispose() }
}
$rows = New-Object System.Collections.Generic.List[object]
$products = Get-ChildItem -LiteralPath $ProductsRoot -Filter "*.json" | ForEach-Object { Read-JsonFile $_.FullName }
foreach ($product in $products) {
foreach ($field in @("pdpResults", "pdpResultsMobile", "pdpHowWeCompare")) {
$filename = ConvertTo-PlainText $product.metafields.$field
if (-not $filename) { continue }
$target = Join-Path $AssetRoot $filename
$exists = Test-Path -LiteralPath $target
if ($exists -and -not $Force) {
$status = "preserved_existing"
} else {
Draw-DraftAsset $target $product $field
$status = if ($exists) { "regenerated_draft_png" } else { "generated_draft_png" }
}
$rows.Add([pscustomobject]@{
sku = $product.sku
field = $field
filename = $filename
path = $target
status = $status
approvalStatus = "draft_needs_human_image_approval"
})
}
}
$json = [pscustomobject]@{
generatedAt = $GeneratedAt
assetRoot = $AssetRoot
totalProductPdpAssets = $rows.Count
generated = @($rows | Where-Object { $_.status -like "generated*" }).Count
regenerated = @($rows | Where-Object { $_.status -like "regenerated*" }).Count
preserved = @($rows | Where-Object { $_.status -eq "preserved_existing" }).Count
caveat = "Draft PNGs are staging/review assets generated from current dashboard evidence. They are not human-approved final creative."
rows = $rows
}
$json | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $OutRoot "draft_pdp_asset_generation.json") -Encoding UTF8
$rows | Export-Csv -LiteralPath (Join-Path $OutRoot "draft_pdp_asset_generation.csv") -NoTypeInformation -Encoding UTF8
Write-Host "Draft PDP assets represented: $($rows.Count)"
Write-Host "Generated: $(@($rows | Where-Object { $_.status -eq 'generated_draft_png' }).Count)"
Write-Host "Regenerated: $(@($rows | Where-Object { $_.status -eq 'regenerated_draft_png' }).Count)"
Write-Host "Preserved existing: $(@($rows | Where-Object { $_.status -eq 'preserved_existing' }).Count)"