Files
the-silent-gods-hearthfire/tools/package-image-catalog.ps1

289 lines
23 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
param(
[string]$ProjectRoot = (Split-Path -Parent $PSScriptRoot),
[string]$PackageDate = ([DateTimeOffset]::UtcNow.ToOffset([TimeSpan]::FromHours(8)).ToString('yyyy-MM-dd')),
[string]$PackageName = '',
[switch]$RefreshDescriptions
)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$ProjectRoot = [IO.Path]::GetFullPath($ProjectRoot)
if (-not $PackageName) { $PackageName = '素材网上传包_' + $PackageDate }
if ($PackageName -notmatch '^素材网上传包_[^/\\:]+$') { throw 'PackageName必须是素材网上传包_开头的单个目录名' }
$dest = Join-Path $ProjectRoot "06_AI动漫制作/07_交付与发行/$PackageName"
$existingNames=@{}
if (Test-Path -LiteralPath $dest) {
if (-not $RefreshDescriptions) { throw "目标已存在,为保留已有交付请另用PackageName:$dest" }
$prior=Get-Content -LiteralPath "$dest/素材清单.json" -Raw | ConvertFrom-Json
if ($prior.schema_version -ne 1 -or $prior.project -ne '炉火照彻诸神沉默的年代') {throw '不是本工具的交付包'}
foreach($item in @($prior.items)+@(Get-Content -LiteralPath "$dest/外部参考清单.json" -Raw | ConvertFrom-Json)) {$existingNames[$item.sha256]=$item.filename}
}
$notes = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'image_catalog_notes.json') -Raw | ConvertFrom-Json -AsHashtable
$episodeRoot = Join-Path $ProjectRoot '06_AI动漫制作/05_生产素材/正片/S01E01_H3_V01'
$manifest = Get-Content -LiteralPath "$episodeRoot/source_manifest.json" -Raw | ConvertFrom-Json
$shotMap = @{}; foreach ($shot in $manifest.shots) { $shotMap[$shot.id] = $shot }
$sceneNames = @{1='前世租屋';2='化工厂交班与更衣';3='事故与救人';4='婴儿与泉堂外景';5='孤儿院照料婴儿';6='泉堂祝福与沉默';7='孤儿院日常';8='大公府出发';9='大圣堂会面';10='勇者装备交付';12='少年与送货'}
$linked = @{}
$indexPath = Join-Path $ProjectRoot '06_AI动漫制作/02_视觉圣经/角色设定/角色参考图索引.md'
foreach ($m in [regex]::Matches([IO.File]::ReadAllText($indexPath),'\[([^\]]+)\]\(([^)]+\.png)\)')) {
$target = [IO.Path]::GetFullPath((Join-Path (Split-Path $indexPath) $m.Groups[2].Value))
$linked[$target] = $m.Groups[1].Value
}
$reviewNotes = @{}
$reviewDoc = Join-Path $ProjectRoot '06_AI动漫制作/02_视觉圣经/角色设定/2026-09-29_其余角色逐张生成_V08.md'
foreach ($line in Get-Content -LiteralPath $reviewDoc) {
if ($line -match '^\| \d+ \| ([^|]+) \| ([^|]+) \|') { $reviewNotes[$Matches[1].Trim()] = $Matches[2].Trim() }
}
Push-Location $ProjectRoot
try { $paths = @(rg --files --hidden --no-ignore -g '*.png' -g '*.jpg' -g '*.jpeg' -g '*.webp' -g '*.gif' -g '*.bmp' -g '!.git/**' -g '!06_AI动漫制作/07_交付与发行/**') } finally { Pop-Location }
$sources = [Collections.Generic.List[object]]::new()
foreach ($rel in $paths) {
$sources.Add([pscustomobject]@{Full=(Join-Path $ProjectRoot $rel);Rel=$rel.Replace('\','/');External=($rel -match '画风参考/|画风参考\\|用户提供');Origin='项目'})
}
$desktop = 'C:\Users\zhang\Desktop\AI短剧\角色管理'
if (Test-Path -LiteralPath $desktop) {
foreach ($f in Get-ChildItem -LiteralPath $desktop -File -Recurse | Where-Object Extension -in @('.png','.jpg','.jpeg','.webp')) {
$sources.Add([pscustomobject]@{Full=$f.FullName;Rel=('桌面角色管理/'+[IO.Path]::GetRelativePath($desktop,$f.FullName).Replace('\','/'));External=$true;Origin='桌面来源待核参考'})
}
}
foreach ($s in $sources) { $s | Add-Member Hash (Get-FileHash -LiteralPath $s.Full -Algorithm SHA256).Hash.ToLowerInvariant() }
$groups = @($sources | Group-Object Hash)
$generated = [Collections.Generic.List[object]]::new()
$external = [Collections.Generic.List[object]]::new()
$copyJobs = [Collections.Generic.List[object]]::new()
function CleanName([string]$value) {
$clean = [regex]::Replace($value,'[<>:"/\\|?*\x00-\x1f]','_')
$clean = [regex]::Replace($clean,'\s+','_').Trim('.',' ')
if ($clean.Length -gt 88) { $clean = $clean.Substring(0,88) }
return $clean
}
function ShortText([string]$value,[int]$max=1200) {
$value = ($value -replace '\r?\n',' ' -replace '\s+',' ').Trim()
if ($value.Length -gt $max) { return $value.Substring(0,$max)+'…' }
return $value
}
foreach ($group in $groups) {
$ordered = @($group.Group | Sort-Object @{Expression={$_.External}},@{Expression={$_.Rel -match '^99_|post_frames|review|rejected'}},Rel)
$s = $ordered[0]; $rel=$s.Rel; $f=Get-Item -LiteralPath $s.Full; $stem=$f.BaseName
$isExternal = @($ordered | Where-Object {-not $_.External}).Count -eq 0
$category='生成辅助图'; $status='生产素材待审'; $use='构图、画面连续性与图生视频输入参考;入正式资产库前审看。'
$prefix='02'; $title=$stem; $detail=''; $warn='图片存在不等于已通过艺术或连续性验收。'; $context=''; $shotId=''; $age='未在本图单独核定'
$evidence=[Collections.Generic.List[string]]::new(); $evidence.Add($rel)
$assetId=if ($stem -match '^(CHR|REF|BG|MAP)-') { $stem } else { '' }
$isArchived=$rel.StartsWith('99_')
if ($isExternal) {
$category='外部参考';$status='非本项目生成/来源授权待核';$prefix='90'
$title='外部参考|'+$stem
$detail=if ($rel -match '画风参考|用户提供') {'用户提供的既有动画画风截图,仅作线条、色彩和光影讨论。'} else {'桌面角色管理目录中的原有参考图;未找到足够证据将其认定为本项目新生成的原创素材。'}
$warn='不要按本项目原创生成图公开发布或出售;来源及授权待核实。';$use='私人参考索引,不默认上传。'
} elseif ($rel -match 'post_frames/') {
$category='视频逐帧中间图';$prefix='06';$status='后期中间图/非独立原画'
$title=([regex]::Match($rel,'E01-S\d+-C\d+').Value)+'|'+$stem
$detail='视频后期逐帧序列中的单帧,供屏幕内容合成、画面检查或后期追溯;相邻文件通常属于同一镜头,不应全部作为不同原创设定图。'
$warn='帧序号是文件编号,不直接推算时间码;原视频帧率与提取起点须另查。';$use='技术归档;默认预览隐藏,不建议逐张公开上架。'
} elseif ($stem -match '^(review|compare|waveform)|_sheet$|检查|抽帧|light_controlled_review' -or $rel -match '/scenes/') {
$category=if ($stem -eq 'waveform') {'音频波形图'} else {'检查拼图或抽帧'}
$prefix='05';$status='质检辅助/非独立原画'
$title=if ($rel -match 'E01-S\d+-C\d+') { $Matches[0]+'|'+$stem } elseif($stem -eq 'waveform') { (Split-Path (Split-Path $s.Full) -Leaf)+'|波形图' } else {$stem}
$detail='制作过程中生成的检查、比较或预览图片,用于画面节奏、构图或技术效果对照。'
$warn='可能含多帧、标注或技术图表;不能当作单帧设定图或完整成片。';$use='内部审看与制作过程记录。'
}
if (-not $isExternal -and ($rel -match '/角色设定/' -or $stem -match '^CHR-|^REF-' -or $isArchived)) {
$category='角色参考图';$prefix='01';$status='REVIEW/候选未定稿'
if ($linked.ContainsKey($s.Full)) { $title=$linked[$s.Full] }
elseif ($rel -match '/([^/]+)/(?:01_参考图|02_生成候选)/') { $title=$Matches[1]+'|'+$stem }
$detail='用于角色身份、服装、种族轮廓和年龄版本对照的生成候选。'
$warn='图中自发纹章、文字与装饰不自动成为小说设定;没有完整侧背、表情和跨镜测试。'
$parent=Split-Path (Split-Path $s.Full)
$design=Join-Path $parent '视觉设计方案.md'
if (-not (Test-Path -LiteralPath $design) -and $rel -match '(0[12]_转生[前后]角色/.*)/(01_参考图|02_生成候选)/') {
$design=Join-Path $ProjectRoot ('06_AI动漫制作/02_视觉圣经/角色设定/'+$Matches[1]+'/视觉设计方案.md')
}
if (Test-Path -LiteralPath $design) {
$bullets=@(Get-Content -LiteralPath $design | Where-Object {$_ -match '^- (性别|视觉年龄|骨相|体型|服装|道具|动作|不可漂移|种族|发色|瞳色)'})
$bullets=@($bullets | Where-Object {$_ -notmatch '\d+岁|真实年龄'})
$context='通用角色卡供对照,旧版服装条目以本图生成记录覆盖:'+(ShortText ($bullets -join ' ') 1700)
$evidence.Add([IO.Path]::GetRelativePath($ProjectRoot,$design).Replace('\','/'))
}
foreach ($k in @($reviewNotes.Keys | Sort-Object Length -Descending)) { if ($title -like "*$k*" -and $stem -match 'V08') { $detail += ' 生成记录的画面描述与初检:'+$reviewNotes[$k];$evidence.Add([IO.Path]::GetRelativePath($ProjectRoot,$reviewDoc).Replace('\','/'));break } }
}
foreach ($k in @($notes.Keys | Sort-Object Length -Descending)) {
if (-not $isExternal -and $stem.Contains($k)) { $title=$notes[$k][0]+'|'+$stem;$detail=$notes[$k][1];$warn=$notes[$k][2];break }
}
if (-not $isExternal -and $stem -match '^BG-') { $category='场景设定图';$prefix='02';$status='REVIEW/候选未定稿' }
if (-not $isExternal -and $stem -match '^MAP-') { $category='世界地图';$prefix='03';$status='REVIEW/布局草案';$use='地理布局、商路和行军关系讨论,不用于精确测距。' }
if ($stem -match '(?:-|_)A(\d+)(?:-|_)') { $age=$Matches[1]+'岁(文件所标版本;归档图不保证符合现行时间线)' }
if ($isArchived) {
$prefix='04';$status='ARCHIVED/历史版本'
$warn='已被后续方案替代或仍有问题,不作为当前生产底图。 '+$warn
$use='设计迭代追溯、旧版对比;上传时必须保留“历史版本”标签。'
if ($rel -match '服饰修订前') { $warn+=' 此批衣装与世界观时代、阶层要求不符,后续已经修订。' }
if ($rel -match '魔王比例修订前') { $warn+=' 魔王头身比例或双视图比例有问题;当前参照V07而不是此图。' }
if ($rel -match '魔王外貌微调前') { $warn+=' 此图早于V07头部缩小及衣装前片修订。' }
if ($rel -match '地图候选') { $warn+=' 北部直连关系不足,当前已改为V03。' }
if ($rel -match '角色细节修订前') { $warn+=' 这是V08B之前的局部问题版本;萨尔克靴/手、神父年龄或奈芙拉肢体读图需参照修订。' }
}
if ($rel -match '(E01-S\d+-C\d+)') {
$shotId=$Matches[1]
if ($shotMap.ContainsKey($shotId)) {
$sh=$shotMap[$shotId];$sceneName=$sceneNames[[int]$sh.scene]
$context="原镜头:$shotId;场景:$sceneName;景别机位:$($sh.framing);计划画面与表演:$($sh.action_performance);空间光线:$($sh.space_light);声音关系:$($sh.sound)。这是原制作说明,不保证每项均已在图片中正确实现。"
$evidence.Add('06_AI动漫制作/05_生产素材/正片/S01E01_H3_V01/source_manifest.json')
if ($category -eq '生成辅助图') { $title="$shotId|$sceneName|$stem";$detail="第一集对应镜头的生成首帧或返修底图。"+$sh.action_performance }
}
$readyPath=Join-Path (Split-Path $s.Full) 'ready.json'
if (Test-Path -LiteralPath $readyPath) {
$ready=Get-Content -LiteralPath $readyPath -Raw | ConvertFrom-Json
if ($ready.frame_qc.notes) { $warn+=' 原首帧审看记录:'+$ready.frame_qc.notes }
}
}
if ($rel -match 'rejected|first_v1') { $status='历史或返修前版本/复核';$warn+=' 文件位于返修或旧版路径,不按最新版自动使用。' }
if (-not $detail) { $detail='本项目制作图片。独立画面内容尚无充分文字记录,请结合原图核对后补充;不从文件名编造人物身份或事件。' }
if ($rel -match '视频测试' -and -not $shotId) {
if ($rel -match '男主收手机') { $context='前世男主在化工厂更衣柜前收手机的测试段;母亲中秋消息通过屏幕文字表现,不另配母亲旁白。' }
elseif ($rel -match '泉堂夜景|SC06_40秒') { $context='泉堂祝福测试段,855年的45岁马提亚斯、13岁阿黛尔与婴儿;冷窗光与暖烛光,神像保持静止与沉默。' }
elseif ($rel -match '开篇40秒') { $context='前世租屋开篇40秒柔光测试,风扇、水杯、电脑和29岁男主的日常。测试版不自动替代正片同名镜头。' }
$use+=' 测试素材与正式镜头身份分开登记。'
}
if ($rel -match '视频测试/.+/(?:frames/)?(C0[1-8])(?:R2)?(?:_|/|\.)' -and $rel -match 'SC06_40秒|开篇40秒') {
$testId=$Matches[1]
$testMatch=[regex]::Match($rel,'/(C0[1-8])(?:R2)?(?:_|/|\.)')
$testId=$testMatch.Groups[1].Value
if($rel -match 'SC06_40秒') {
$testText=@{C01='马提亚斯怀抱婴儿来到泉堂神像前,阿黛尔持烛在旁,左窗冷光与右侧暖室构成静默空间。';C02='45岁神父怀抱婴儿祷告的近景,银白反光打断话语,眼神抬向神像方向。';C03='灰石神像水纹与婴儿祝福印记的近景,光是细微回应,不是神像活化或女神开口。';C04='13岁阿黛尔双手持烛的近景,视线由烛火转向画外神父与神像,表现好奇和探寻。';C05='神父等待回应的表情近景,抱婴姿势稳定,期待逐渐转为接受沉默。';C06='神父将目光重新落回婴儿、少女跟随注视的双人场面,祝福余光退去,回到实际照料。';C07='泉堂仪式之后的延续场面,以仍在燃烧的烛火和人物实际照料维持日常,没有女神额外发言。';C08='祝福事件后的记录与报告场面,供边境发现向大圣堂上报的段落衔接。'}
} else {
$testText=@{C01='租屋窗边风扇、半杯水与薄荷盆栽近景,冷暖柔光表现普通生活。';C02='前世男主从左肩后看电脑分类结果,屏幕中的猫是静态图片,鼠标与错误识别服务轻微幽默。';C03='29岁男主看向右侧电脑的侧面近景,克制笑意与自然疲倦构成普通青年形象。';C04='双手与笔记本的俯拍近景,未完成计算、铅笔和纸页用于呈现广泛但仍在试错的爱好。';C05='男主听见闹钟后停下电脑操作的侧面中景,收手和肩部放松表示结束折腾、准备休息。';C06='男主在窗边查看稀疏的第三盆薄荷,杯与土壤形成浇水动作起点,植物不神奇生长。';C07='浇水后拉窗帘、准备走向床铺的租屋全景,窗、家具和人物动线保持连续。'}
}
if($testText.ContainsKey($testId)) {
if($category -eq '生成辅助图') {$detail=$testText[$testId]+' 这是该测试镜头的输入或辅助静图,动态完成度另查视频。';$title=$testId+'|'+$(if($rel -match 'SC06_40秒'){'泉堂祝福测试'}else{'前世租屋测试'})+'|'+$stem}
else {$detail+=' 对应镜头内容:'+$testText[$testId]}
}
}
$img=[Drawing.Image]::FromFile($s.Full)
try { $width=$img.Width;$height=$img.Height;$format=$f.Extension.ToLowerInvariant() } finally { $img.Dispose() }
$hash=$group.Name
$imageName=if($existingNames.ContainsKey($hash)) {$existingNames[$hash]} else {(CleanName ("$prefix"+'__'+$title))+'__'+$hash.Substring(0,10)+$format}
$folder=if ($isExternal) {'外部参考_不默认上传'} else {'图片'}
$outRel="$folder/$imageName"
$srcRels=@($ordered.Rel)
$description="《炉火照彻诸神沉默的年代》素材。$detail "+$(if($context){"设定/制作背景(非逐项视觉验收):$context "})+"状态:$status。用途:$use 注意:$warn"
$record=[pscustomobject][ordered]@{
asset_id=('IMG-'+$hash.Substring(0,16));original_asset_id=$assetId;filename=$imageName;image_path=$outRel;
title=$title;category=$category;status=$status;description=$description;content_description=$detail;
context_notes=$context;usage=$use;limitations=$warn;age_version=$age;shot_id=$shotId;
tags=@('炉火照彻诸神沉默的年代','AI动漫',$category,$status);width=$width;height=$height;bytes=$f.Length;
sha256=$hash;original_filename=$f.Name;source_paths=$srcRels;evidence_paths=@($evidence);
description_basis='依据项目索引、已有生成记录及制作数据整理;不是全图重新逐张验收';
public_release_approved=$false;rights_note='本包不作版权或商业授权保证;外部参考与生成素材分别标记';
default_gallery_visible=($prefix -in @('01','02','03'));duplicate_source_count=$srcRels.Count
}
$copyJobs.Add([pscustomobject]@{Source=$s.Full;Relative=$outRel;Record=$record})
if ($isExternal) { $external.Add($record) } else { $generated.Add($record) }
}
$generated=@($generated | Sort-Object category,title,asset_id)
$external=@($external | Sort-Object title)
foreach($folder in @("$dest/图片","$dest/外部参考_不默认上传")) {if(-not(Test-Path -LiteralPath $folder)){New-Item -ItemType Directory -Path $folder | Out-Null}}
$utf8=[Text.UTF8Encoding]::new($false)
foreach ($job in $copyJobs) {
$target=Join-Path $dest $job.Relative
if(Test-Path -LiteralPath $target){
if((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne $job.Record.sha256){throw "图片内容已改变,不覆盖:$target"}
}else{Copy-Item -LiteralPath $job.Source -Destination $target}
$r=$job.Record
$text=@"
标题:$($r.title)
素材编号:$($r.asset_id)
原资产编号:$($r.original_asset_id)
图片文件:$($r.filename)
分类:$($r.category)
状态:$($r.status)
年龄版本:$($r.age_version)
镜头编号:$($r.shot_id)
【内容说明】
$($r.content_description)
【设定与制作背景】
$($r.context_notes)
【建议用途】
$($r.usage)
【版本问题与使用限制】
$($r.limitations)
【尺寸与来源】
尺寸:$($r.width) × $($r.height) 像素
文件字节数:$($r.bytes)
SHA-256:$($r.sha256)
原文件名:$($r.original_filename)
全部来源:
$($r.source_paths -join [Environment]::NewLine)
说明依据:$($r.description_basis)
公开发布批准:尚未确认;本包仅供整理和选择上传
来源授权说明:$($r.rights_note)
【素材网站可用简介】
$($r.description)
标签:$($r.tags -join ',')
"@
[IO.File]::WriteAllText([IO.Path]::ChangeExtension($target,'.txt'),$text,$utf8)
}
$catalog=[pscustomobject]@{schema_version=1;project='炉火照彻诸神沉默的年代';packaged_date=$PackageDate;source_image_files=$sources.Count;project_source_files=$paths.Count;unique_project_images=$generated.Count;unique_external_references=$external.Count;duplicate_copies_removed=$sources.Count-$groups.Count;items=$generated}
[IO.File]::WriteAllText("$dest/素材清单.json",($catalog | ConvertTo-Json -Depth 12),$utf8)
[IO.File]::WriteAllText("$dest/外部参考清单.json",(@($external) | ConvertTo-Json -Depth 12),$utf8)
$csv=@($generated | ForEach-Object { [pscustomobject][ordered]@{素材编号=$_.asset_id;文件名=$_.filename;图片路径=$_.image_path;标题=$_.title;分类=$_.category;版本状态=$_.status;详细说明=$_.description;标签=($_.tags -join ';');宽度=$_.width;高度=$_.height;字节数=$_.bytes;原资产编号=$_.original_asset_id;年龄版本=$_.age_version;镜头编号=$_.shot_id;原路径=($_.source_paths -join ';');SHA256=$_.sha256;公开发布已批准='否';说明依据=$_.description_basis} })
$csv | Export-Csv -LiteralPath "$dest/素材清单.csv" -NoTypeInformation -Encoding utf8BOM
$generated | Where-Object default_gallery_visible | Select-Object asset_id,filename,title,category,status | Export-Csv -LiteralPath "$dest/优先审看清单.csv" -NoTypeInformation -Encoding utf8BOM
$sourceMap=foreach($r in @($generated)+@($external)) {foreach($p in $r.source_paths){[pscustomobject]@{原路径=$p;目标路径=$r.image_path;SHA256=$r.sha256;处理='原文件保留;内容相同仅输出一份'}}}
$sourceMap | Export-Csv -LiteralPath "$dest/原图到上传文件映射.csv" -NoTypeInformation -Encoding utf8BOM
$escapedJson=(@($generated) | ConvertTo-Json -Depth 10 -Compress).Replace('<','\u003c')
$template=[IO.File]::ReadAllText((Join-Path $PSScriptRoot 'image_catalog_gallery.html'))
[IO.File]::WriteAllText("$dest/图片预览目录.html",$template.Replace('__CATALOG_DATA__',$escapedJson),$utf8)
$counts=($generated | Group-Object category | ForEach-Object {"- $($_.Name):$($_.Count)张"}) -join [Environment]::NewLine
$readme=@"
# 素材网上传包
项目:《炉火照彻诸神沉默的年代》;整理日期:$PackageDate。
## 直接使用
- **图片/**:$($generated.Count)张内容去重后的本项目图片全部集中在这一个文件夹;每张旁边有同名TXT详细说明。
- **图片预览目录.html**:双击可离线打开,按标题/说明/分类搜索;默认只显示当前人物、地图与生产底图,勾选可显示历史和中间图。
- **素材清单.csv**:UTF-8 BOM,通用网站批量导入字段;用表格软件打开时选UTF-8。
- **素材清单.json**:包含图片、标题、完整说明、标签、尺寸、来源、版本、SHA-256。
- **优先审看清单.csv**:便于先审核候选原画,不代表已批准公开发布。
- **原图到上传文件映射.csv**:原路径到唯一图片的映射,不遗漏重复副本来源。
- **外部参考_不默认上传/**:$($external.Count)张外部截图或来源未核的桌面参考,另附说明,不混入生成图库。
## 盘点与范围
扫描仓库已有图片$($paths.Count)个,加已知桌面角色参考$($sources.Count-$paths.Count)个。以文件内容SHA-256去重;相同画面但字节不同的图片仍分别保留,不做相似图删除。
合计保留$($groups.Count)份唯一图片,其中本项目$($generated.Count)张、外部参考$($external.Count)张;去掉$($sources.Count-$groups.Count)个重复副本的额外拷贝,原文件全部不动。仅整理本地落盘项目文件,未声称扫描整台电脑、所有临时生成目录或服务器全部图片。
没有重画、裁切、压缩或放大图片,目标图片与来源字节一致;没有上传网站、提交或推送。
## 本项目图片分类
$counts
## 上传前注意
1. 当前人设仍为REVIEW,不因整理自动成为定稿。旧版图保留ARCHIVED;同名人物的年龄版不能混用。
2. 说明来自原生成记录、项目设定与镜头资料;没有声称重新逐张通过视觉质检。设定背景是目标,不保证画面正确画出所有细节。
3. 逐帧图、波形、拼图和检查图只宜技术归档,不能包装成独立原创设定图。网站可用分类过滤。
4. 外部动画截图和桌面来源待核参考不进入主导入表。生成素材的具体授权仍需按你的实际服务与用途确认,本包不宣称授权已经审核。
5. 不上传工作流、服务地址、凭据或服务器配置;图片保留原字节,PNG可能仍有原生元数据,公开发布前按素材站隐私要求检查。
6. CSV为通用字段,并非已适配某个站点接口;导入时映射“图片路径、标题、详细说明、标签”。TXT可直接复制文案。
"@
[IO.File]::WriteAllText("$dest/README.md",$readme,$utf8)
$failures=[Collections.Generic.List[string]]::new()
foreach($job in $copyJobs){
$target=Join-Path $dest $job.Relative
if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne $job.Record.sha256) {$failures.Add($job.Relative)}
if (-not (Test-Path -LiteralPath ([IO.Path]::ChangeExtension($target,'.txt')))) {$failures.Add('missing description '+$job.Relative)}
}
$validation=[pscustomobject]@{image_files=$groups.Count;main_images=$generated.Count;external_images=$external.Count;descriptions=$groups.Count;csv_rows=@(Import-Csv -LiteralPath "$dest/素材清单.csv").Count;source_mapping_rows=@($sourceMap).Count;hash_mismatches=@($failures);source_files_untouched=$true}
[IO.File]::WriteAllText("$dest/校验报告.json",($validation | ConvertTo-Json -Depth 5),$utf8)
[pscustomobject]@{destination=$dest;validation=$validation;categories=@($generated | Group-Object category | Select-Object Count,Name)} | ConvertTo-Json -Depth 6