Compare commits

..

181 Commits

Author SHA1 Message Date
Nathan 4892f59089 Add directories for Reference audio and translations in NewProject.bat 2026-06-11 10:24:25 -06:00
Nathan 912f1d07fa Add automatic git commit for cfgdeploy changes in NewProject.bat 2026-06-11 10:18:19 -06:00
Nathan 33262d74d3 nvenc 1mbps cq0
need to change the output dest & name
2026-05-15 17:50:02 -06:00
Nathan 697ef0c682 ignore specst
Specstory generates a `.cursorindexingignore`, which isn't even used by Cursor, not sure if it ever was.
2026-05-11 11:43:41 -06:00
Nathan 49c99c8bfd Stop tracking .specstory/ (local SpecStory data) 2026-04-07 11:55:09 -06:00
Raincloud 3870803d52 NewProject.bat read config.json dailyFormat to set project date prefix 2026-03-09 19:45:14 -06:00
Nathan ee89bf7aa9 reallusion CC binaries to LFS 2026-03-09 11:48:37 -06:00
Raincloud 3f12a290a4 proper 7z split archive handling 2026-03-05 14:23:28 -07:00
Raincloud 54d039644e make state_skipped harsher 2026-02-23 10:45:55 -07:00
Raincloud b173331155 integrate zipseglimit 2026-02-23 10:26:21 -07:00
Nathan 2672b7bb84 refactor ConfigLoader to CfgDeploy 2026-02-23 09:47:26 -07:00
Nathan fcdb7661fe heic to lfs 2026-02-20 13:22:24 -07:00
Nathan 80112adafd ignore Microsoft Office tmp 2026-02-19 12:57:17 -07:00
Nathan f9419a6649 updateseq: only create the _collisions folder if collisions are found. 2026-02-06 16:49:20 -07:00
Nathan f03fba1671 specst 2026-02-06 16:40:56 -07:00
Nathan 39ae955b9f corrupted dir needed underscore 2026-02-06 16:36:09 -07:00
Nathan 661da7cef5 cfgdeploy updateseq to Renders 2026-02-06 16:34:58 -07:00
Nathan 53684b7c8f better collision handling 2026-02-06 16:32:33 -07:00
Nathan f009c026b4 script recognize submodules 2026-02-06 16:25:33 -07:00
Nathan feb8f0e0c7 raw and lnk to LFS 2026-02-06 15:34:54 -07:00
Nathan 791462d7c9 fix false positive 2026-02-04 12:51:05 -07:00
Nathan 60546b3c51 integrate PNG compression, resolve #2 2026-02-02 17:19:54 -07:00
Nathan 9bbb0ca079 resolve #1 2026-02-02 16:40:18 -07:00
Nathan a56c1b0fa2 audition files to LFS 2026-01-28 09:43:35 -07:00
Nathan 1c498e2ff4 reconfigure targa behavior 2026-01-09 16:17:10 -07:00
Nathan abc668e1d3 specst 2026-01-09 15:33:43 -07:00
Nathan d77af346b7 fix powershell error 2026-01-09 15:26:23 -07:00
Nathan 47a05bf4f2 begin organize textures FlatColors merge 2026-01-09 12:05:08 -07:00
Raincloud 9b6b461c22 ignore BL_proxy 2026-01-04 20:37:29 -07:00
Nathan f94600e296 NewProject config workflow integration 2026-01-02 16:38:04 -07:00
Nathan 7a4e9b6662 versioned name handling more accurate 2025-12-30 15:53:09 -07:00
Nathan 60c03aadc0 db to lfs 2025-12-29 17:35:41 -07:00
Nathan 374e92ffb8 zipseq file type mix handling 2025-12-29 13:29:25 -07:00
Nathan e48c790d89 zipseq ignore Thumbs.db 2025-12-29 11:19:34 -07:00
Raincloud 45dc5956a2 ignore RnR universally 2025-12-26 18:12:53 -07:00
Nathan 9af9cac5b8 m4a and other audio to LFS 2025-12-22 17:23:43 -07:00
Nathan 6f46b3fce3 make paths relative 2025-12-19 17:24:53 -07:00
Nathan 4c8d6b20b3 step 3 actually working, can't believe it first try 2025-12-19 17:03:08 -07:00
Nathan 1bafe5fe85 remove debug 2025-12-19 15:40:25 -07:00
Nathan 7896059a86 reorganize FlatColors and remove empty folders 2025-12-19 15:34:07 -07:00
Nathan 5a7d1f66b9 remove duplicate files 2025-12-19 13:20:58 -07:00
Nathan 5232e7c2cc strip blendfile prefixes from common imgs 2025-12-19 13:17:03 -07:00
Nathan c0d625da13 separate behavior for inter- and intra-blendfile commonalities 2025-12-19 11:57:12 -07:00
Nathan 92e5e57f19 fix lint 2025-12-19 11:50:02 -07:00
Nathan 4d1b554f8f fix executionpolicy with pwsh 2025-12-19 11:33:04 -07:00
Nathan 658052cae4 remove example path 2025-12-19 11:31:47 -07:00
Nathan ee317463f1 pause 2025-12-19 11:30:55 -07:00
Nathan adafff58f0 begin reconfig orgtxt 2025-12-19 11:26:50 -07:00
Nathan 456d3b6411 cfgloader batch 2025-12-19 11:26:29 -07:00
Nathan 706c710dd2 append old organize_textures 2025-12-19 10:59:09 -07:00
Nathan d8afccd225 usdz to lfs 2025-12-17 17:28:58 -07:00
Nathan 06abb5c421 ignore footage and plates 2025-12-17 17:26:07 -07:00
Raincloud f9e1d39d53 pureref to LFS 2025-12-17 00:45:40 -07:00
Raincloud 11f286e87f ConfigLoader now changes config structDir when ran 2025-12-17 00:42:29 -07:00
Nathan 22e286a151 fix updateseq not working in non-submodule folder 2025-12-15 10:54:09 -07:00
Nathan 0f63199795 reorg houdini and bgeo/sc to LFS 2025-12-10 16:05:47 -07:00
Nathan df36d86ea8 hiplc to LFS 2025-12-10 12:18:22 -07:00
Nathan 19e4eb63aa Fix timestamp normalization to prevent false change detection after unzip 2025-12-10 12:06:02 -07:00
Nathan 0287cbd7ef reorg to adobe alternatives; add affinity (.af) to lfs 2025-12-09 16:32:42 -07:00
Nathan d0493fd855 xcf to LFS 2025-12-09 13:53:37 -07:00
Nathan b0669d9872 drp to lfs 2025-12-08 11:11:20 -07:00
Nathan 2b760bd598 and ffp3 flip fluids to lfs 2025-12-05 11:05:07 -07:00
Raincloud 95a19dceb3 npz to lfs 2025-11-27 13:01:31 -07:00
Raincloud e8f66cd06f add unitypackage 2025-11-26 21:45:57 -07:00
Raincloud c5a2463b91 specst 2025-11-26 18:43:40 -07:00
Nathan 1c86fa0c68 attr for flip fluids 2025-11-26 13:38:50 -07:00
Nathan b67c2e7779 deploy zip&unzipseqs 2025-11-26 13:36:00 -07:00
Nathan e1086312dd adjust gitattributes and ignore for physics and bakes 2025-11-26 13:00:35 -07:00
Nathan ba46dce3a3 Merge remote-tracking branch 'HOME/main' 2025-11-26 10:00:38 -07:00
Raincloud 2238021885 specst 2025-11-26 01:04:33 -07:00
Nathan c101fbb381 fix zipseq store not storing 2025-11-25 14:22:44 -07:00
Nathan 68330de0fb fix zipseq not clearing zips 2025-11-25 13:17:46 -07:00
Nathan 0cff26cb0d actual work config 2025-11-25 12:57:55 -07:00
Raincloud 75c2c1ed02 lfs webm webp 2025-11-18 21:43:50 -07:00
Raincloud 584f5135dd Merge branch 'main' into HOME 2025-11-18 21:37:45 -07:00
Nathan bffbff528e overhaul gitattributes and add seq to ignore again 2025-11-17 12:33:56 -07:00
Raincloud 756e8168fb Merge branch 'main' into HOME 2025-11-15 10:07:23 -07:00
Nathan fbc0f833dc swap back to LZMA for better multithreading 2025-11-13 14:22:16 -07:00
Nathan 06e8177f0b add USD to LFS 2025-11-13 10:56:28 -07:00
Raincloud 81ada88dc1 Merge remote-tracking branch 'origin/HOME' into HOME 2025-11-12 17:55:15 -07:00
Nathan 2fd5023816 7zinst fix bin-packing vs concurrent behavior for max CPU and RAM allocation 2025-11-12 10:10:31 -07:00
Nathan 6c20b2de52 work config 2025-11-12 09:49:40 -07:00
Raincloud b7ae8880e7 more 7zinst behavior modification 2025-11-12 09:01:32 -07:00
Raincloud 0da1dbd09a HOME config 2025-11-11 18:09:35 -07:00
Raincloud e1c1aacd0d HOME config 2025-11-11 17:57:28 -07:00
Nathan 6d4cdb0549 zipseq 7zinst of 0 bottleneck fix 2025-11-11 16:31:09 -07:00
Nathan 55a2074c62 fix zipseq ignoring zip metadata and not replacing with 7z 2025-11-11 12:45:44 -07:00
Nathan a152d17136 work config 2025-11-11 10:27:48 -07:00
Raincloud ff6d3f584f zipseq 7z instance tolerance for RAM overflow 2025-11-10 20:56:57 -07:00
Raincloud f795ea3848 fix gitattributes deployment 2025-11-10 20:41:48 -07:00
Raincloud f2e3a96bf2 zipseq ignore _CURRENT 2025-11-10 19:32:31 -07:00
Raincloud 23562de9cf deploy git attributes and ignore 2025-11-10 19:23:40 -07:00
Raincloud 5b409d771b Merge branch 'main' into HOME 2025-11-10 18:52:32 -07:00
Nathan 169b4f656d fix updateseq batch run behavior 2025-11-10 15:47:05 -07:00
Nathan a506c09a49 7z seq compression working 2025-11-10 11:35:25 -07:00
Nathan 83b5e62266 further deployment implementation to .config directory 2025-11-10 11:00:12 -07:00
Nathan 124ad9e3b6 support configloader deployment 2025-11-10 10:43:46 -07:00
Nathan dfc113ea38 reconfig zipseq 2025-11-10 10:36:28 -07:00
Nathan 5c5e22cfb7 change config bools 2025-11-10 10:24:55 -07:00
Nathan cef84c68b9 Merge remote-tracking branch 'HOME/HOME' 2025-11-10 10:19:06 -07:00
Raincloud db2ee821fb cont develop configloader 2025-11-10 10:16:23 -07:00
Raincloud e541aa1a12 begin develop configloader 2025-11-08 02:36:20 -07:00
Raincloud 78598f68db fix bad merge 2025-11-08 01:44:26 -07:00
Raincloud 6ab4f1e026 Merge remote-tracking branch 'origin/main' into HOME 2025-11-08 01:40:35 -07:00
Nathan b43660349f add .aegraphic and re-add .ma to LFS 2025-11-06 16:23:15 -07:00
Nathan eab3e22bbe updateseq fix rename? 2025-11-06 14:38:35 -07:00
Nathan ded87cdc68 zipseq ignore _archive 2025-11-06 12:35:07 -07:00
Nathan da70801c87 zipseq finalized 2025-11-06 11:02:59 -07:00
Nathan ce9f8da4b9 newproject working 2025-11-06 10:47:19 -07:00
Nathan 16fee83832 update attributes and ignore 2025-11-06 10:19:10 -07:00
Nathan a90acae5c5 append zipseqarchv script and batches 2025-11-06 10:10:29 -07:00
Raincloud 68ed642581 old specstories 2025-11-02 12:59:05 -07:00
Raincloud 7d12bd6d52 updateseq for both HOME and work 2025-11-02 12:57:39 -07:00
Nathan 699b053165 missed updateseq as well BRUH 2025-10-30 17:15:38 -06:00
Nathan becd0df1b7 reapply work config to date because forgor 2025-10-30 11:29:58 -06:00
Nathan e59fc1ad62 prproj gzip to LFS 2025-10-29 15:30:14 -06:00
Nathan 6849065df6 reapply work config 2025-10-27 16:03:08 -06:00
Raincloud 860087be4d update for HOME rules 2025-10-23 21:52:35 -06:00
Nathan e8fd60c666 Merge remote-tracking branch 'Nexus/main' into main 2025-10-23 10:35:14 -06:00
Nathan fc60c19bf7 add mp4s to rename step 2025-10-23 10:33:23 -06:00
Nathan 97dcd4edb7 lots of prompts 2025-10-08 16:24:22 -06:00
Raincloud 5d06604092 reconcile changes, they're all good apparently? 2025-09-17 18:43:42 -06:00
Nathan 6639a1970f Merge branch 'main' of http://10.1.10.3:30008/TalkingHeads/ProjectStructure 2025-09-17 13:57:28 -06:00
Nathan 0859f8baba skip uncompressed blends 2025-09-17 13:54:16 -06:00
Nathan 7693de5c48 bad script 2025-09-17 13:23:06 -06:00
Nathan ff7bb567f3 blend script working 2025-09-17 13:22:51 -06:00
Nathan d0885e105e move mocap to assets 2025-09-17 09:42:32 -06:00
Raincloud 5d5756a6d4 update home use again 2025-09-16 22:11:55 -06:00
Nathan 1ec937d260 add prin to LFS 2025-09-15 16:01:16 -06:00
Nathan 165c55a021 Resolve merge conflict in NewProject.bat - take most recent changes 2025-09-15 10:31:48 -06:00
Nathan eb58d45e03 find missing files slow operator 2025-09-10 16:40:39 -06:00
Nathan fc4a5b773f remove output txt hopefully 2025-09-10 11:43:23 -06:00
Nathan 241bd172c6 similarity detection and deletion of outputs 2025-09-10 11:41:11 -06:00
Nathan 8f3c8e5e2d fix CLI output and remove fake users from actions 2025-09-10 11:33:02 -06:00
Nathan 1e8dacddc8 fix orange and white 2025-09-09 15:47:16 -06:00
Nathan a8d8b2d2c6 append cel bsdf script 2025-09-09 13:46:25 -06:00
Nathan e08e015ff8 remove blend2, add ma 2025-09-05 10:14:35 -06:00
Nathan 5d50109cfc add wav to lfs 2025-09-05 09:57:14 -06:00
Nathan 1529544c37 remove _CURRENT folders from structure creation 2025-09-04 11:38:15 -06:00
Nathan 1bb5134421 add audio previews and beta 2025-09-03 12:35:00 -06:00
Nathan d6206d663b remove add safe directory 2025-09-03 10:27:48 -06:00
Raincloud 2fb151c6ca Merge branch 'main' of http://192.168.86.64:3000/Raincloud/ProjectStructure_HOME 2025-08-26 21:57:57 -06:00
Nathan 6bc313e64d merge work onto home 2025-08-26 16:30:45 -06:00
Nathan 146237466f fix execution errors 2025-08-26 16:14:56 -06:00
Nathan c086f4b031 upseqbatches updates all 2025-08-26 15:59:26 -06:00
Nathan 7021458963 Merge branch 'main' of http://10.1.10.3:30008/TalkingHeads/ProjectStructure 2025-08-26 15:58:12 -06:00
Nathan f412610a76 calls each with autocontinue 2025-08-26 15:56:42 -06:00
Nathan a957ade7ff blender crash handling 2025-08-26 15:56:14 -06:00
Nathan 7d86cae5b1 wipe destination in _CURRENT 2025-08-26 15:55:21 -06:00
Raincloud d89d8e0cf2 append components from work 2025-08-20 18:41:51 -06:00
Raincloud 053d3fc4cf remove ps1 from copy 2025-08-20 18:32:40 -06:00
Raincloud 0f7cb287d9 seqbatches to home paths too 2025-08-20 18:24:09 -06:00
Raincloud f7034b540c append changes from work to sync sequence file names 2025-08-20 18:23:14 -06:00
Nathan d4ad76ee97 forgot to append UpdateAllSeq 2025-08-20 16:22:48 -06:00
Nathan 39168558b0 mark safe directory 2025-08-20 14:46:06 -06:00
Nathan a690850b0b add initial commit 2025-08-20 14:40:49 -06:00
Nathan 244b99a39b updateseq rename seq based on folder name 2025-08-20 11:47:38 -06:00
Nathan 514670a204 aep to LFS 2025-08-20 11:07:10 -06:00
Nathan ecf46fdc44 restore and adapt upgrade batches 2025-08-19 12:11:03 -06:00
Nathan 95507cd44c remove pause 2025-08-19 12:08:03 -06:00
Nathan dbbf74d1e5 restore UpdateSequences 2025-08-19 12:06:51 -06:00
Nathan 12cb27afd6 cleanup 2025-08-19 11:54:56 -06:00
Nathan 0a819f7313 update config for prproj 2025-08-19 11:25:32 -06:00
Raincloud eacf44a6de append and adapt new updateseq from work 2025-08-18 19:31:09 -06:00
Nathan c569252fc6 update all seq 2025-08-18 17:10:05 -06:00
Nathan e3fea9d423 convert to ps1 script and debug 2025-08-18 17:04:19 -06:00
Nathan d276607178 ignore _archive folders 2025-08-18 15:12:24 -06:00
Raincloud 6af7a13f05 updateseq date format adapted 2025-08-17 14:31:05 -06:00
Raincloud cf81dd162c script paths relative 2025-08-17 14:27:16 -06:00
Raincloud d6e401c0b0 adapt date format 2025-08-17 14:22:09 -06:00
Raincloud 5d7778eb78 begin adaptation for home 2025-08-17 14:20:05 -06:00
Nathan 0cd8fd842f add glb and gltf 2025-08-15 12:45:24 -06:00
Nathan 46b061d36c NewDailies in VO and EL 2025-08-15 12:38:22 -06:00
Nathan 8cecc49f0f using relative paths 2025-08-15 12:34:52 -06:00
Nathan 51ce499390 make draggable 2025-08-15 12:21:45 -06:00
Nathan e79878e656 remove pause 2025-08-15 12:16:36 -06:00
Nathan fb9756bdb8 repath RebuildDailies 2025-08-15 12:14:56 -06:00
Nathan 86a8679553 restore old scripts 2025-08-15 12:07:33 -06:00
Raincloud 1d4fc7540a fix behavior with multiple submodules 2025-08-15 11:20:18 -06:00
Raincloud f6c30b4da7 add fbxkey 2025-08-11 14:44:13 -06:00
Raincloud 3d4dc664ed compress blend files 2025-08-11 13:03:09 -06:00
34 changed files with 5664 additions and 2298 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.pyo
.specstory/
.vscode/
-2
View File
@@ -1,2 +0,0 @@
# SpecStory explanation file
/.what-is-this.md
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
# ProjectStructure Configuration
The repository reads user preferences from `config.json` in the ProjectStructure
root (copied alongside helper batches when they are deployed to projects).
## Keys
| Key | Type | Meaning |
| --- | --- | --- |
| `dailyFormat` | bool | `true` → daily folders named `YYYY-MM-DD`; `false``daily_*` style. |
| `structDir` | string | Absolute or relative path to the canonical ProjectStructure directory. Blank values default to the folder containing `config.json`. |
| `projectsRoot` | string (optional) | Override for the root directory scanned by `UpgradeSeqBatches.ps1`. Defaults to the parent of `structDir`. |
| `zipper` | bool | `true` → use 7Zip (if available); `false` → use Pythons built-in zipfile module. |
| `compression` | int | Compression level `0-9` (0 = store only, 9 = max). Applies to both zipfile and 7Zip. |
## Notes
- `UpdateProjectBatches.ps1` copies `config.json` and `ConfigLoader.ps1` to the
target project so the helper `.bat` launchers can resolve script locations.
- When `zipper` is `true`, the tool searches for `7z`/`7za` on `PATH`. If neither
is found it logs a warning and falls back to `zipfile`.
- Leaving `structDir` empty is safe—the scripts fall back to the directory that
contains `config.json`.
- `compress_sequence_pngs.py` (used by CompressPNGs.bat) requires **Pillow** (PIL).
Install with: `pip install Pillow`
+22
View File
@@ -0,0 +1,22 @@
@echo off
setlocal EnableExtensions
set "script_dir=%~dp0"
set "ps1_path=%script_dir%CfgDeploy.ps1"
if not exist "%ps1_path%" (
echo [ERROR] CfgDeploy.ps1 not found at %ps1_path%
exit /b 1
)
echo Running CfgDeploy...
powershell -NoProfile -ExecutionPolicy Bypass -File "%ps1_path%"
set "rc=%errorlevel%"
if %rc% neq 0 (
echo.
echo Script exited with error code %rc%
pause
)
exit /b %rc%
+444
View File
@@ -0,0 +1,444 @@
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$ProjectPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$script:LoaderRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$script:ConfigPath = Join-Path -Path $script:LoaderRoot -ChildPath 'config.json'
$script:ConfigCache = $null
function Get-ProjectStructureConfig {
if ($null -ne $script:ConfigCache) {
return $script:ConfigCache
}
if (Test-Path -LiteralPath $script:ConfigPath) {
try {
$raw = Get-Content -LiteralPath $script:ConfigPath -Raw -ErrorAction Stop
if ($raw.Trim().Length -gt 0) {
$script:ConfigCache = $raw | ConvertFrom-Json
return $script:ConfigCache
}
}
catch {
Write-Warning "Failed to parse config.json: $($_.Exception.Message)"
}
}
$script:ConfigCache = [pscustomobject]@{}
return $script:ConfigCache
}
function Get-ConfigValue {
param(
[Parameter(Mandatory)] [string]$Name,
$Default = $null
)
$config = Get-ProjectStructureConfig
if ($config.PSObject.Properties.Name -contains $Name) {
$value = $config.$Name
if ($null -ne $value -and ($value -isnot [string] -or $value.Trim().Length -gt 0)) {
return $value
}
}
return $Default
}
function Get-StructDirectory {
# When script is run directly (not dot-sourced), always use script's directory for portability
if ($MyInvocation.InvocationName -ne '.') {
return $script:LoaderRoot
}
$value = Get-ConfigValue -Name 'structDir'
if ($null -eq $value -or [string]::IsNullOrWhiteSpace($value)) {
return $script:LoaderRoot
}
if ([System.IO.Path]::IsPathRooted($value)) {
$resolved = Resolve-Path -LiteralPath $value -ErrorAction SilentlyContinue
if ($null -ne $resolved) { return $resolved.Path }
return $value
}
$candidate = Join-Path -Path $script:LoaderRoot -ChildPath $value
$resolvedCandidate = Resolve-Path -LiteralPath $candidate -ErrorAction SilentlyContinue
if ($null -ne $resolvedCandidate) { return $resolvedCandidate.Path }
return $candidate
}
function Get-ProjectsRoot {
$value = Get-ConfigValue -Name 'projectsRoot'
if ($null -eq $value -or [string]::IsNullOrWhiteSpace($value)) {
$structDir = Get-StructDirectory
$parent = Split-Path -Parent $structDir
if ($null -eq $parent -or $parent.Length -eq 0 -or $parent -eq $structDir) {
return $structDir
}
return $parent
}
if ([System.IO.Path]::IsPathRooted($value)) {
$resolved = Resolve-Path -LiteralPath $value -ErrorAction SilentlyContinue
if ($null -ne $resolved) { return $resolved.Path }
return $value
}
$candidate = Join-Path -Path $script:LoaderRoot -ChildPath $value
$resolvedCandidate = Resolve-Path -LiteralPath $candidate -ErrorAction SilentlyContinue
if ($null -ne $resolvedCandidate) { return $resolvedCandidate.Path }
return $candidate
}
function Get-ProjectPathFromUser {
param(
[string]$Prompt = "Paste the project path to deploy in"
)
do {
$inputPath = Read-Host -Prompt $Prompt
if ([string]::IsNullOrWhiteSpace($inputPath)) {
Write-Warning "Path cannot be empty. Please try again."
continue
}
# Remove quotes if present
$inputPath = $inputPath.Trim('"', "'")
# Try to resolve the path
try {
if (Test-Path -LiteralPath $inputPath -PathType Container) {
$resolved = Resolve-Path -LiteralPath $inputPath -ErrorAction Stop
return $resolved.Path
}
else {
Write-Warning "Path does not exist or is not a directory: $inputPath"
$retry = Read-Host "Try again? (Y/N)"
if ($retry -notmatch '^[Yy]') {
return $null
}
}
}
catch {
Write-Warning "Invalid path: $($_.Exception.Message)"
$retry = Read-Host "Try again? (Y/N)"
if ($retry -notmatch '^[Yy]') {
return $null
}
}
} while ($true)
}
function Use-IsoDailyFormat {
$dailyFormat = Get-ConfigValue -Name 'dailyFormat' -Default $true
# Handle backward compatibility with boolean values
if ($dailyFormat -is [bool]) {
return $dailyFormat
}
# Handle string values
if ($dailyFormat -is [string]) {
$formatStr = $dailyFormat.Trim()
# ISO format contains YYYY-MM-DD pattern
if ($formatStr -match 'YYYY-MM-DD' -or $formatStr -match '\d{4}-\d{2}-\d{2}') {
return $true
}
# daily_YYMMDD or other formats are not ISO
return $false
}
# Default to false (not ISO format) for unknown types
return $false
}
function Use-7Zip {
$zipper = Get-ConfigValue -Name 'zipper' -Default $true
# Handle backward compatibility with boolean values
if ($zipper -is [bool]) {
return $zipper
}
# Handle string values
if ($zipper -is [string]) {
$zipperStr = $zipper.Trim().ToLower()
return ($zipperStr -eq '7z')
}
# Default to false (use zip) for unknown types
return $false
}
function Get-ZipCompressionLevel {
$value = Get-ConfigValue -Name 'compression' -Default 9
if ($value -is [string]) {
$parsed = 0
if ([int]::TryParse($value, [ref]$parsed)) {
$value = $parsed
}
}
if ($value -isnot [int]) {
return 9
}
return [Math]::Min(9, [Math]::Max(0, $value))
}
function Update-ConfigStructDir {
param(
[Parameter(Mandatory)] [string]$StructDir
)
$config = Get-ProjectStructureConfig
$config.structDir = $StructDir
try {
$json = $config | ConvertTo-Json -Depth 10
Set-Content -LiteralPath $script:ConfigPath -Value $json -NoNewline -ErrorAction Stop
$script:ConfigCache = $config
}
catch {
Write-Warning "Failed to update config.json: $($_.Exception.Message)"
}
}
# If script is run directly (not dot-sourced), prompt for project path and deploy
# When dot-sourced, InvocationName is '.'; when run directly, it's the script path or name
if ($MyInvocation.InvocationName -ne '.') {
# Update config.json to use script's directory as structDir for portability
Update-ConfigStructDir -StructDir $script:LoaderRoot
# Use provided ProjectPath parameter, or prompt user if not provided
if ([string]::IsNullOrWhiteSpace($ProjectPath)) {
$projectPath = Get-ProjectPathFromUser
}
else {
# Remove quotes if present and resolve the provided path
$ProjectPath = $ProjectPath.Trim('"', "'")
try {
if (Test-Path -LiteralPath $ProjectPath -PathType Container) {
$projectPath = (Resolve-Path -LiteralPath $ProjectPath -ErrorAction Stop).Path
}
else {
Write-Error "Project path does not exist or is not a directory: $ProjectPath"
exit 1
}
}
catch {
Write-Error "Unable to resolve project directory: $($_.Exception.Message)"
exit 1
}
}
if ($null -ne $projectPath) {
# Deploy batch files and config to the project
$structDir = Get-StructDirectory
if (-not (Test-Path -LiteralPath $structDir -PathType Container)) {
Write-Error "Configured structDir not found: $structDir"
exit 1
}
try {
$resolvedProject = (Resolve-Path -LiteralPath $projectPath -ErrorAction Stop).Path
}
catch {
Write-Error "Unable to resolve project directory: $($_.Exception.Message)"
exit 1
}
if (-not (Test-Path -LiteralPath $resolvedProject -PathType Container)) {
Write-Error "Project path is not a directory: $resolvedProject"
exit 1
}
Write-Host "`nDeploying to: $resolvedProject" -ForegroundColor Cyan
Write-Host "Struct directory: $structDir" -ForegroundColor Cyan
$specs = @(
@{ Name = 'UpdateSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateSequences.bat' },
@{ Name = 'UpdateAllSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateAllSequences.bat' },
@{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' },
@{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' },
@{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' },
@{ Name = '.gitattributes'; Source = Join-Path -Path $structDir -ChildPath 'components\gitattributes' },
@{ Name = '.gitignore'; Source = Join-Path -Path $structDir -ChildPath 'components\gitignore' }
)
# Config files to deploy to projectroot\.config\
$configAssets = @(
@{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' },
@{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' }
)
foreach ($spec in $specs) {
if (-not (Test-Path -LiteralPath $spec.Source -PathType Leaf)) {
Write-Error "Source file not found: $($spec.Source)"
exit 1
}
}
foreach ($asset in $configAssets) {
if (-not (Test-Path -LiteralPath $asset.Source -PathType Leaf)) {
Write-Error "Config asset not found: $($asset.Source)"
exit 1
}
}
# Ensure .config directory exists in project root and is hidden
$projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config'
if (-not (Test-Path -LiteralPath $projectConfigDir -PathType Container)) {
New-Item -Path $projectConfigDir -ItemType Directory -Force | Out-Null
Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan
}
# Set hidden attribute on .config directory
$folder = Get-Item -LiteralPath $projectConfigDir -Force
$folder.Attributes = $folder.Attributes -bor [System.IO.FileAttributes]::Hidden
$touchedDirs = @{}
$summary = @()
foreach ($spec in $specs) {
Write-Host "`n=== Updating $($spec.Name) ===" -ForegroundColor Magenta
$targets = Get-ChildItem -LiteralPath $resolvedProject -Recurse -Filter $spec.Name -File -Force -ErrorAction SilentlyContinue
$targets = $targets | Where-Object { $_.FullName -ne $spec.Source }
if (-not $targets) {
Write-Host "No targets found." -ForegroundColor Yellow
$summary += [pscustomobject]@{
Name = $spec.Name
Updated = 0
Failed = 0
Skipped = 0
Total = 0
}
continue
}
$updated = 0
$failed = 0
foreach ($target in $targets) {
try {
Copy-Item -Path $spec.Source -Destination $target.FullName -Force
# Set hidden attribute for .gitattributes and .gitignore files
if ($spec.Name -eq '.gitattributes' -or $spec.Name -eq '.gitignore') {
$file = Get-Item -LiteralPath $target.FullName -Force
$file.Attributes = $file.Attributes -bor [System.IO.FileAttributes]::Hidden
}
Write-Host "[OK] $($target.FullName)" -ForegroundColor Green
$updated++
$targetDir = $target.Directory.FullName
if (-not $touchedDirs.ContainsKey($targetDir)) {
$touchedDirs[$targetDir] = $true
}
}
catch {
Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
$failed++
}
}
$summary += [pscustomobject]@{
Name = $spec.Name
Updated = $updated
Failed = $failed
Skipped = 0
Total = @($targets).Count
}
}
Write-Host "`n=== Summary ===" -ForegroundColor Cyan
foreach ($item in $summary) {
Write-Host ("{0,-22} Updated: {1,3} Failed: {2,3} Total: {3,3}" -f $item.Name, $item.Updated, $item.Failed, $item.Total)
}
if (($summary | Measure-Object -Property Failed -Sum).Sum -gt 0) {
Write-Host "Completed with errors." -ForegroundColor Yellow
exit 1
}
Write-Host "All batch files refreshed successfully." -ForegroundColor Green
# Deploy UpdateSequences.bat, ZipSeqArchv.bat, UnzipSeqArchv.bat, CompressPNGs.bat to \Renders
Write-Host "`n=== Deploying Renders batch files to \Renders ===" -ForegroundColor Magenta
$rendersDir = Join-Path -Path $resolvedProject -ChildPath 'Renders'
if (-not (Test-Path -LiteralPath $rendersDir -PathType Container)) {
New-Item -Path $rendersDir -ItemType Directory -Force | Out-Null
Write-Host "Created Renders directory: $rendersDir" -ForegroundColor Cyan
}
$rendersBatches = @(
@{ Name = 'UpdateSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateSequences.bat' },
@{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' },
@{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' },
@{ Name = 'CompressPNGs.bat'; Source = Join-Path -Path $structDir -ChildPath 'CompressPNGs.bat' }
)
foreach ($bat in $rendersBatches) {
$targetPath = Join-Path -Path $rendersDir -ChildPath $bat.Name
try {
Copy-Item -Path $bat.Source -Destination $targetPath -Force
Write-Host "[OK] $targetPath" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] $targetPath" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
exit 1
}
}
# Deploy .gitattributes and .gitignore to project root if they don't exist
Write-Host "`n=== Deploying .gitattributes and .gitignore to project root ===" -ForegroundColor Magenta
$gitFiles = @(
@{ Name = '.gitattributes'; Source = Join-Path -Path $structDir -ChildPath 'components\gitattributes' },
@{ Name = '.gitignore'; Source = Join-Path -Path $structDir -ChildPath 'components\gitignore' }
)
foreach ($gitFile in $gitFiles) {
$targetPath = Join-Path -Path $resolvedProject -ChildPath $gitFile.Name
try {
Copy-Item -Path $gitFile.Source -Destination $targetPath -Force
# Set hidden attribute on the file
$file = Get-Item -LiteralPath $targetPath -Force
$file.Attributes = $file.Attributes -bor [System.IO.FileAttributes]::Hidden
Write-Host "[OK] $targetPath" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] $targetPath" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
exit 1
}
}
# Deploy config files to projectroot\.config\
Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta
foreach ($asset in $configAssets) {
$targetPath = Join-Path -Path $projectConfigDir -ChildPath $asset.Name
try {
Copy-Item -Path $asset.Source -Destination $targetPath -Force
Write-Host "[OK] $targetPath" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] $targetPath" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
exit 1
}
}
Write-Host "`nConfig files deployed successfully." -ForegroundColor Green
Write-Host "Deployment completed successfully." -ForegroundColor Green
exit 0
}
else {
Write-Host "No project path provided." -ForegroundColor Yellow
exit 1
}
}
+43
View File
@@ -0,0 +1,43 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "REN_DIR=%~dp0"
for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI"
set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run CfgDeploy.ps1 to deploy helper files.
exit /b 1
)
if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run CfgDeploy.ps1 to deploy helper files.
exit /b 1
)
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'compress_sequence_pngs.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"compress_sequence_pngs.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%~I"
if not defined PY_SCRIPT (
echo [ERROR] Unable to resolve compress_sequence_pngs.py path from config.
exit /b 1
)
pushd "%PROJ_ROOT%" >nul 2>&1
python "%PY_SCRIPT%" %*
set "ERR=!ERRORLEVEL!"
popd >nul 2>&1
if !ERR! NEQ 0 (
echo PNG compression failed ^(exit code !ERR!^).
exit /b !ERR!
)
exit /b 0
+37
View File
@@ -0,0 +1,37 @@
@echo off
setlocal EnableDelayedExpansion
if "%~1"=="" (
echo Drop one or more video files onto this script.
echo.
echo Output:
echo .mov/.mkv/etc. -^> same name .mp4 next to source
echo .mp4 -^> same name _nvenc.mp4 ^(avoids overwriting source^)
pause
exit /b 1
)
:loop
if "%~1"=="" goto done
set "IN=%~f1"
set "OUT=%~dp1%~n1.mp4"
if /i "%~x1"==".mp4" set "OUT=%~dp1%~n1_nvenc.mp4"
echo.
echo IN : "!IN!"
echo OUT: "!OUT!"
ffmpeg -hide_banner -y -hwaccel cuda -i "!IN!" -c:v h264_nvenc -preset p7 -tune hq -rc vbr -cq 0 -b:v 0 -maxrate 1000k -bufsize 2000k -pix_fmt yuv420p -c:a aac -b:a 96k -movflags +faststart "!OUT!"
if errorlevel 1 (
echo FAILED: %~nx1
) else (
echo OK: %~nx1
)
shift
goto loop
:done
echo.
echo All jobs finished.
pause
+68
View File
@@ -0,0 +1,68 @@
# Simple helper script to get structDir from project config.json
# Reads config.json from .config folder in project root
param(
[string]$ProjectRoot
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
# Try to determine project root from script location
if ($PSScriptRoot) {
$ProjectRoot = Split-Path -Parent $PSScriptRoot
}
elseif ($MyInvocation.MyCommand.Path) {
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
}
else {
Write-Error "Unable to determine project root. Please provide -ProjectRoot parameter."
exit 1
}
}
$configPath = Join-Path -Path $ProjectRoot -ChildPath '.config\config.json'
if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
Write-Error "config.json not found at: $configPath"
exit 1
}
try {
$config = Get-Content -LiteralPath $configPath -Raw -ErrorAction Stop | ConvertFrom-Json
if ($config.PSObject.Properties.Name -contains 'structDir') {
$structDir = $config.structDir
if ($null -ne $structDir -and ($structDir -isnot [string] -or $structDir.Trim().Length -gt 0)) {
# If it's an absolute path, resolve it
if ([System.IO.Path]::IsPathRooted($structDir)) {
$resolved = Resolve-Path -LiteralPath $structDir -ErrorAction SilentlyContinue
if ($null -ne $resolved) {
Write-Output $resolved.Path
exit 0
}
Write-Output $structDir
exit 0
}
# Relative path - resolve relative to config location
$candidate = Join-Path -Path (Split-Path -Parent $configPath) -ChildPath $structDir
$resolvedCandidate = Resolve-Path -LiteralPath $candidate -ErrorAction SilentlyContinue
if ($null -ne $resolvedCandidate) {
Write-Output $resolvedCandidate.Path
exit 0
}
Write-Output $candidate
exit 0
}
}
# Default: return the directory containing config.json (project root)
Write-Output $ProjectRoot
exit 0
}
catch {
Write-Error "Failed to read or parse config.json: $($_.Exception.Message)"
exit 1
}
+4 -4
View File
@@ -1,16 +1,16 @@
@echo off @echo off
setlocal setlocal
:: Get current date in YYMMDD format :: Get current date in YYYY-MM-DD format
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do ( for /f "tokens=2-4 delims=/ " %%a in ('date /t') do (
set mm=%%a set mm=%%a
set dd=%%b set dd=%%b
set yy=%%c set yy=%%c
) )
set yy=%yy:~-2% set yyyy=20%yy:~-2%
:: Create daily folder :: Create daily folder
mkdir "daily_%yy%%mm%%dd%" mkdir "%yyyy%-%mm%-%dd%"
echo Daily folder created: daily_%yy%%mm%%dd% echo Daily folder created: %yyyy%-%mm%-%dd%
pause pause
+73 -23
View File
@@ -1,31 +1,66 @@
@echo off @echo off
setlocal setlocal EnableExtensions
:: Get current date in YYMMDD format :: Search for .config directory in current working directory (including subdirectories) to find ProjectStructure location
set "STRUCT_DIR="
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Continue'; $searchDir = '%CD%'; try { $subDirs = Get-ChildItem -LiteralPath $searchDir -Directory -Force -ErrorAction Stop; $configDirs = @(); foreach ($subDir in $subDirs) { $configPath = Join-Path $subDir.FullName '.config\config.json'; if (Test-Path -LiteralPath $configPath -PathType Leaf) { $configDirs += $configPath } }; if ($configDirs.Count -gt 0) { $configPath = $configDirs[0]; $config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json; if ($config.structDir) { $structDir = $config.structDir.ToString().Trim(); if ([System.IO.Path]::IsPathRooted($structDir)) { try { $resolved = Resolve-Path -LiteralPath $structDir -ErrorAction Stop; Write-Output $resolved.Path } catch { Write-Output $structDir } } else { $candidate = Join-Path (Split-Path -Parent $configPath) $structDir; try { $resolvedCandidate = Resolve-Path -LiteralPath $candidate -ErrorAction Stop; Write-Output $resolvedCandidate.Path } catch { Write-Output $candidate } } } } } catch { }"`) do set "STRUCT_DIR=%%I"
if not defined STRUCT_DIR (
echo [ERROR] Unable to find .config directory or resolve ProjectStructure directory.
echo Please ensure you are running NewProject.bat from a directory containing projects with .config folders.
exit /b 1
)
:: Debug: Show discovered structDir
echo Found ProjectStructure directory: %STRUCT_DIR%
:: Locate CfgDeploy.ps1 in the ProjectStructure directory
set "CFG_DEPLOY=%STRUCT_DIR%\CfgDeploy.ps1"
if not exist "%CFG_DEPLOY%" (
echo [ERROR] CfgDeploy.ps1 not found at: %CFG_DEPLOY%
exit /b 1
)
:: Get dailyFormat and current date from config
set "DAILY_FORMAT=YYMMDD"
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'Stop'; try { $configPath = Join-Path '%STRUCT_DIR%' 'config.json'; if (Test-Path $configPath) { $config = Get-Content $configPath -Raw | ConvertFrom-Json; $fmt = $config.dailyFormat; if ($fmt) { Write-Output $fmt.ToString() } } } catch { } finally { if (-not $fmt) { Write-Output 'YYMMDD' } }"`) do set "DAILY_FORMAT=%%I"
:: Get current date components
for /f "tokens=2-4 delims=/ " %%a in ('date /t') do ( for /f "tokens=2-4 delims=/ " %%a in ('date /t') do (
set mm=%%a set mm=%%a
set dd=%%b set dd=%%b
set yy=%%c set yy=%%c
) )
set yy=%yy:~-2% set yyyy=20%yy:~-2%
:: Format date prefix based on dailyFormat
if "%DAILY_FORMAT%"=="YYYY-MM-DD" (
set datePrefix=%yyyy%-%mm%-%dd%
) else (
set datePrefix=%yy:~-2%%mm%%dd%
)
:: Ask for project name :: Ask for project name
set /p projectName="Enter project name (press Enter for default 'NewProject'): " set /p projectName="Enter project name (press Enter for default 'NewProject'): "
if "%projectName%"=="" set projectName=NewProject if "%projectName%"=="" set projectName=NewProject
set projectRoot=%yy%%mm%%dd%_%projectName% set projectRoot=%datePrefix%_%projectName%
:: Create main project directory :: Create main project directory
mkdir "%projectRoot%" mkdir "%projectRoot%"
:: Create Assets structure :: Create Assets structure
mkdir "%projectRoot%\Assets\ElevenLabs" mkdir "%projectRoot%\Assets\ElevenLabs"
if exist "%STRUCT_DIR%\NewDaily.bat" copy /Y "%STRUCT_DIR%\NewDaily.bat" "%projectRoot%\Assets\ElevenLabs\NewDaily.bat" >nul
mkdir "%projectRoot%\Assets\Blends" mkdir "%projectRoot%\Assets\Blends"
mkdir "%projectRoot%\Assets\Mocap"
mkdir "%projectRoot%\Assets\VO" mkdir "%projectRoot%\Assets\VO"
if exist "%~dp0NewDaily.bat" copy /Y "%~dp0NewDaily.bat" "%projectRoot%\Assets\VO\NewDaily.bat" >nul
:: Create Blends structure :: Create Blends structure
mkdir "%projectRoot%\Blends\animations\_CURRENT" mkdir "%projectRoot%\Blends\animations\"
mkdir "%projectRoot%\Blends\stills\_CURRENT"
mkdir "%projectRoot%\Blends\stills\img-BG" mkdir "%projectRoot%\Blends\stills\img-BG"
:: Create Deliverable structure :: Create Deliverable structure
@@ -34,33 +69,48 @@ mkdir "%projectRoot%\Deliverable\"
:: Create Pr structure :: Create Pr structure
mkdir "%projectRoot%\Pr\RnR\RIFE" mkdir "%projectRoot%\Pr\RnR\RIFE"
:: Create Reference structure
mkdir "%projectRoot%\Reference\Translation Scripts"
mkdir "%projectRoot%\Reference\VO clips"
:: Add project root additions :: Add project root additions
mkdir "%projectRoot%\Reference\audio"
mkdir "%projectRoot%\Reference\translations"
if not exist "%projectRoot%\Renders" mkdir "%projectRoot%\Renders" if not exist "%projectRoot%\Renders" mkdir "%projectRoot%\Renders"
:: Place helper scripts into Renders :: Place helper scripts into Renders (config lives in .config\, deployed by CfgDeploy below)
if exist "%CD%\NewDaily.bat" copy /Y "%CD%\NewDaily.bat" "%projectRoot%\Renders\NewDaily.bat" >nul set "templateRoot=%STRUCT_DIR%"
if exist "%CD%\UpdateSequences.bat" copy /Y "%CD%\UpdateSequences.bat" "%projectRoot%\Renders\UpdateSequences.bat" >nul for %%F in (UpdateSequences.bat ZipSeqArchv.bat UnzipSeqArchv.bat CompressPNGs.bat) do (
if exist "%templateRoot%\%%F" copy /Y "%templateRoot%\%%F" "%projectRoot%\Renders\%%F" >nul
:: Create Translations Google Drive shortcut )
echo [InternetShortcut] > "%projectRoot%\Reference\Translation Scripts\Translations Google Drive.url"
echo URL=https://drive.google.com/drive/folders/1lND5207vl-qf5RbTQ2eejeXJCc4r5rUo >> "%projectRoot%\Reference\Translation Scripts\Translations Google Drive.url"
:: Create Amazon folder shortcut
powershell -Command "$folder='%projectRoot%\Deliverable'; $WS = New-Object -ComObject WScript.Shell; $SC = $WS.CreateShortcut([System.IO.Path]::Combine($folder, 'Amazon - for Elizabeth.lnk')); $SC.TargetPath = 'D:\Amazon - for Elizabeth'; $SC.WorkingDirectory = 'D:\Amazon - for Elizabeth'; $SC.IconLocation = 'shell32.dll,3'; $SC.Save()"
:: Use repo-provided templates for git config files :: Use repo-provided templates for git config files
if exist "%CD%\components\gitignore" copy /Y "%CD%\components\gitignore" "%projectRoot%\.gitignore" >nul if exist "%~dp0components\gitignore" copy /Y "%~dp0components\gitignore" "%projectRoot%\.gitignore" >nul
if exist "%CD%\components\gitattributes" copy /Y "%CD%\components\gitattributes" "%projectRoot%\.gitattributes" >nul if exist "%~dp0components\gitattributes" copy /Y "%~dp0components\gitattributes" "%projectRoot%\.gitattributes" >nul
:: Initialize git and install Git LFS :: Initialize git and install Git LFS
pushd "%projectRoot%" >nul pushd "%projectRoot%" >nul
git init git init
git lfs install git lfs install
git add . -v
git commit -m "init"
popd >nul popd >nul
:: Deploy config workflow using CfgDeploy.ps1
echo.
echo Deploying config workflow...
for %%P in ("%projectRoot%") do set "PROJECT_ROOT_ABS=%%~fP"
powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%CFG_DEPLOY%' -ProjectPath '%PROJECT_ROOT_ABS%'"
if errorlevel 1 (
echo [WARNING] Config workflow deployment failed. You may need to run CfgDeploy.ps1 manually.
) else (
echo Config workflow deployed successfully.
pushd "%projectRoot%" >nul
git add .
git commit -m "cfgdeploy"
if errorlevel 1 (
echo [WARNING] No cfgdeploy changes to commit, or commit failed.
) else (
echo cfgdeploy changes committed.
)
popd >nul
)
echo Project structure created successfully in folder: %projectRoot% echo Project structure created successfully in folder: %projectRoot%
pause pause
+42
View File
@@ -0,0 +1,42 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "REN_DIR=%~dp0"
for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI"
set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run CfgDeploy.ps1 to deploy helper files.
exit /b 1
)
if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run CfgDeploy.ps1 to deploy helper files.
exit /b 1
)
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I"
if not defined PY_SCRIPT (
echo [ERROR] Unable to resolve zip_sequences.py path from config.
exit /b 1
)
pushd "%PROJ_ROOT%" >nul 2>&1
python "%PY_SCRIPT%" --mode expand --verbose %*
set "ERR=!ERRORLEVEL!"
if not "!ERR!"=="0" (
echo Failed to expand render sequence archives (exit code !ERR!).
)
popd >nul 2>&1
exit /b !ERR!
+58
View File
@@ -0,0 +1,58 @@
@echo off
setlocal enabledelayedexpansion
echo UpdateAllSequences - Running all UpdateSequences.bat files in submodule folders...
echo.
echo Searching for submodule folders and UpdateSequences.bat files...
echo.
REM Find all submodule folders and check for UpdateSequences.bat
set /a count=0
set /a found=0
echo Found the following submodule folders with UpdateSequences.bat:
for /d %%S in (*) do (
if exist "%%S\UpdateSequences.bat" (
echo - %%S
set /a found+=1
)
)
if !found!==0 (
echo No submodule folders with UpdateSequences.bat found!
echo.
echo Note: This script looks for UpdateSequences.bat files in immediate subdirectories.
echo Make sure you're running this from the project root where submodules are located.
pause
exit /b 1
)
echo.
echo Found !found! submodule folders with UpdateSequences.bat files.
echo.
echo Starting execution...
echo.
REM Execute each UpdateSequences.bat found in submodule folders
for /d %%S in (*) do (
if exist "%%S\UpdateSequences.bat" (
set /a count+=1
echo [!count!/!found!] Running UpdateSequences.bat in %%S...
pushd "%%S"
call "UpdateSequences.bat" < NUL
set "rc=!errorlevel!"
popd
if !rc!==0 (
echo Completed: %%S (SUCCESS)
) else (
echo Completed: %%S (FAILED - RC=!rc!)
)
echo.
)
)
echo.
echo Operation completed. Successfully executed !count! UpdateSequences.bat files.
echo.
echo Note: Check individual _CURRENT\_UpdateSequences.log files in each submodule for details.
pause
+164
View File
@@ -0,0 +1,164 @@
# Refresh helper batch scripts within a specific project directory.
param(
[string]$ProjectPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if (-not $PSScriptRoot) {
$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
}
$cfgDeploy = Join-Path -Path $PSScriptRoot -ChildPath 'CfgDeploy.ps1'
if (-not (Test-Path -LiteralPath $cfgDeploy)) {
throw "Missing CfgDeploy.ps1 in $PSScriptRoot"
}
. $cfgDeploy
$structDir = Get-StructDirectory
if (-not (Test-Path -LiteralPath $structDir -PathType Container)) {
throw "Configured structDir not found: $structDir"
}
if ([string]::IsNullOrWhiteSpace($ProjectPath)) {
$ProjectPath = Read-Host 'Enter the full path to the project directory'
}
if ([string]::IsNullOrWhiteSpace($ProjectPath)) {
Write-Error 'No project directory provided.'
exit 1
}
try {
$resolvedProject = (Resolve-Path -LiteralPath $ProjectPath -ErrorAction Stop).Path
}
catch {
Write-Error "Unable to resolve project directory: $($_.Exception.Message)"
exit 1
}
if (-not (Test-Path -LiteralPath $resolvedProject -PathType Container)) {
Write-Error "Project path is not a directory: $resolvedProject"
exit 1
}
Write-Host "Project directory: $resolvedProject" -ForegroundColor Cyan
Write-Host "Struct directory: $structDir" -ForegroundColor Cyan
$specs = @(
@{ Name = 'UpdateSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateSequences.bat' },
@{ Name = 'UpdateAllSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateAllSequences.bat' },
@{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' },
@{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' },
@{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' }
)
# Config files to deploy to projectroot\.config\
$configAssets = @(
@{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' },
@{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' }
)
foreach ($spec in $specs) {
if (-not (Test-Path -LiteralPath $spec.Source -PathType Leaf)) {
Write-Error "Source file not found: $($spec.Source)"
exit 1
}
}
foreach ($asset in $configAssets) {
if (-not (Test-Path -LiteralPath $asset.Source -PathType Leaf)) {
Write-Error "Config asset not found: $($asset.Source)"
exit 1
}
}
# Ensure .config directory exists in project root
$projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config'
if (-not (Test-Path -LiteralPath $projectConfigDir -PathType Container)) {
New-Item -Path $projectConfigDir -ItemType Directory -Force | Out-Null
Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan
}
$touchedDirs = @{}
$summary = @()
foreach ($spec in $specs) {
Write-Host "`n=== Updating $($spec.Name) ===" -ForegroundColor Magenta
$targets = Get-ChildItem -LiteralPath $resolvedProject -Recurse -Filter $spec.Name -File -ErrorAction SilentlyContinue
$targets = $targets | Where-Object { $_.FullName -ne $spec.Source }
if (-not $targets) {
Write-Host "No targets found." -ForegroundColor Yellow
$summary += [pscustomobject]@{
Name = $spec.Name
Updated = 0
Failed = 0
Skipped = 0
Total = 0
}
continue
}
$updated = 0
$failed = 0
foreach ($target in $targets) {
try {
Copy-Item -Path $spec.Source -Destination $target.FullName -Force
Write-Host "[OK] $($target.FullName)" -ForegroundColor Green
$updated++
$targetDir = $target.Directory.FullName
if (-not $touchedDirs.ContainsKey($targetDir)) {
$touchedDirs[$targetDir] = $true
}
}
catch {
Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
$failed++
}
}
$summary += [pscustomobject]@{
Name = $spec.Name
Updated = $updated
Failed = $failed
Skipped = 0
Total = @($targets).Count
}
}
Write-Host "`n=== Summary ===" -ForegroundColor Cyan
foreach ($item in $summary) {
Write-Host ("{0,-22} Updated: {1,3} Failed: {2,3} Total: {3,3}" -f $item.Name, $item.Updated, $item.Failed, $item.Total)
}
if (($summary | Measure-Object -Property Failed -Sum).Sum -gt 0) {
Write-Host "Completed with errors." -ForegroundColor Yellow
exit 1
}
Write-Host "All batch files refreshed successfully." -ForegroundColor Green
# Deploy config files to projectroot\.config\
Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta
foreach ($asset in $configAssets) {
$targetPath = Join-Path -Path $projectConfigDir -ChildPath $asset.Name
try {
Copy-Item -Path $asset.Source -Destination $targetPath -Force
Write-Host "[OK] $targetPath" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] $targetPath" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
exit 1
}
}
Write-Host "Config files deployed successfully." -ForegroundColor Green
+62 -67
View File
@@ -1,76 +1,71 @@
@echo off @echo off
setlocal EnableExtensions EnableDelayedExpansion setlocal EnableExtensions
set "rootDir=%CD%" set "script_dir=%~dp0"
set "currentDir=%rootDir%\_CURRENT" set "current_dir=%CD%"
if not exist "%currentDir%" ( set "PROJ_ROOT="
mkdir "%currentDir%" >nul 2>&1
if not exist "%currentDir%" ( REM First, check project level (one directory up from current) - most common case
echo ERROR: Cannot create "%currentDir%" permission denied? for %%I in ("%current_dir%..") do set "project_level=%%~fI"
echo Run the script with sufficient rights or choose a writable location. set "project_config=%project_level%\.config\config.json"
if exist "%project_config%" (
set "PROJ_ROOT=%project_level%"
)
REM If not found at project level, search up from current working directory
if not defined PROJ_ROOT (
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "Set-StrictMode -Version Latest; $dir = '%current_dir%'; $found = $null; while ($dir -and -not $found) { $configPath = Join-Path $dir '.config\config.json'; if (Test-Path -LiteralPath $configPath) { $found = $dir; break }; $parent = Split-Path -Parent $dir; if ($parent -eq $dir) { break }; $dir = $parent }; if ($found) { Write-Output $found } else { Write-Output '' }"`) do set "PROJ_ROOT=%%I"
)
REM If still not found, try searching from script location
if not defined PROJ_ROOT (
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "Set-StrictMode -Version Latest; $dir = '%script_dir%'; $found = $null; while ($dir -and -not $found) { $configPath = Join-Path $dir '.config\config.json'; if (Test-Path -LiteralPath $configPath) { $found = $dir; break }; $parent = Split-Path -Parent $dir; if ($parent -eq $dir) { break }; $dir = $parent }; if ($found) { Write-Output $found } else { Write-Output '' }"`) do set "PROJ_ROOT=%%I"
)
REM Fallback: try original logic (script in 3 ProjectStructure)
if not defined PROJ_ROOT (
for %%I in ("%script_dir%..\..") do set "PROJ_ROOT=%%~fI"
)
set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%CONFIG_DIR%\config.json"
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found at %CONFIG_PATH%
if defined project_config (
echo Also checked: %project_config%
)
echo Please run CfgDeploy.ps1 to deploy helper files.
exit /b 1 exit /b 1
)
) )
set "logFile=%currentDir%\_UpdateSequences.log" set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
echo [%date% %time%] === UpdateSequences run started in "%rootDir%" ===>> "%logFile%"
set /a dailiesScanned=0 if not exist "%GET_STRUCT_DIR%" (
set /a sequencesTotal=0 echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
set /a sequencesMirrored=0 echo Please run CfgDeploy.ps1 to deploy helper files.
set /a mirrorFailures=0 exit /b 1
for /d %%D in (daily_*) do (
if exist "%%D" (
set /a dailiesScanned+=1
set "hadSub=0"
rem Case 1: subfolders treated as sequences
for /d %%S in ("%%D\*") do (
set /a sequencesTotal+=1
set "hadSub=1"
call :MirrorOne "%%D\%%~nxS" "%%~nxS"
)
rem Case 2: no subfolders => mirror daily itself as a sequence
if "!hadSub!"=="0" (
set /a sequencesTotal+=1
call :MirrorOne "%%D" "%%~nxD"
)
)
) )
echo( Summary: dailies=%dailiesScanned% sequences=%sequencesTotal% ok=%sequencesMirrored% fail=%mirrorFailures% for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
echo [%date% %time%] === UpdateSequences run completed (d=%dailiesScanned% seq=%sequencesTotal% ok=%sequencesMirrored% fail=%mirrorFailures%) ===>> "%logFile%" "Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $ps1Path = Join-Path $structDir 'UpdateSequences.ps1'; if (-not (Test-Path -LiteralPath $ps1Path)) { throw \"UpdateSequences.ps1 not found at $ps1Path\" }; Write-Output $ps1Path"`) do set "ps1=%%I"
echo( Done. Press any key to exit.
if not defined ps1 (
echo [ERROR] Unable to resolve UpdateSequences.ps1 path from config.
exit /b 1
)
REM Run from batch directory (sequences next to batch, or submodule root)
set "WORK_DIR=%script_dir%"
if "%WORK_DIR:~-1%"=="\" set "WORK_DIR=%WORK_DIR:~0,-1%"
pushd "%WORK_DIR%" >nul 2>&1
echo Running PowerShell update script in %WORK_DIR%...
powershell -NoProfile -ExecutionPolicy Bypass -File "%ps1%"
set "rc=%errorlevel%"
popd >nul 2>&1
echo PowerShell exited with RC=%rc%
echo Done.
pause >nul pause >nul
exit /b 0 exit /b %rc%
:MirrorOne
setlocal EnableExtensions EnableDelayedExpansion
set "srcRel=%~1"
set "seqName=%~2"
set "srcAbs=%rootDir%\%srcRel%"
set "dstAbs=%currentDir%\%seqName%"
rem Only proceed if source is a directory (trailing \ check)
if not exist "%srcRel%\" (
echo Skip: missing folder "%srcRel%"
endlocal & exit /b 0
)
echo Mirror: "%srcAbs%" -^> "%dstAbs%"
echo [%date% %time%] Mirror: "%srcAbs%" -^> "%dstAbs%" >> "%logFile%"
robocopy "%srcAbs%" "%dstAbs%" /MIR /MT:8 /R:1 /W:1 /COPY:DAT /DCOPY:DAT /FFT /NFL /NDL /NP /NJH /NJS >> "%logFile%" 2>&1
set "rc=!errorlevel!"
if "!rc!"=="" set "rc=0"
if !rc! LSS 8 (
set /a sequencesMirrored+=1
echo( OK (rc=!rc!)
echo [%date% %time%] OK rc=!rc! >> "%logFile%"
) else (
set /a mirrorFailures+=1
echo( ERROR (rc=!rc!) - see log
echo [%date% %time%] ERROR rc=!rc! >> "%logFile%"
)
endlocal & exit /b 0
+374
View File
@@ -0,0 +1,374 @@
[CmdletBinding()]
param(
[switch]$DebugMode
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
$cfgDeploy = Join-Path -Path $ScriptDir -ChildPath 'CfgDeploy.ps1'
if (-not (Test-Path -LiteralPath $cfgDeploy)) {
throw "Missing CfgDeploy.ps1 in $ScriptDir"
}
. $cfgDeploy
$useIsoDailyFormat = Use-IsoDailyFormat
function Sync-SequenceFilenames {
param(
[Parameter(Mandatory)] [string]$SequenceFolderPath,
[Parameter(Mandatory)] [string]$SequenceName,
[string]$LogFile,
[string[]]$Extensions = @('.png','.jpg','.jpeg','.exr','.tif','.tiff','.bmp','.tga')
)
$renamed = 0
$collisions = 0
$errors = 0
$checked = 0
$minFrame = [int]::MaxValue
$maxFrame = -1
$frameCount = 0
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] RENAME CHECK in '$SequenceFolderPath' (seq='$SequenceName')" | Add-Content -LiteralPath $LogFile }
$files = Get-ChildItem -LiteralPath $SequenceFolderPath -File -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notlike '*\_archive\*' -and ($Extensions -contains $_.Extension.ToLower()) }
foreach ($f in $files) {
$checked++
$base = [System.IO.Path]::GetFileNameWithoutExtension($f.Name)
$ext = $f.Extension
$digits = $null
if ($base -match '_(\d{6})$') {
$digits = $Matches[1]
}
elseif ($base -match '(?<!_)\b(\d{6})$') {
$digits = $Matches[1]
}
elseif ($base -match '(\d{4})$') {
$digits = ('00' + $Matches[1])
}
else {
continue
}
try {
$n = [int]$digits
if ($n -lt $minFrame) { $minFrame = $n }
if ($n -gt $maxFrame) { $maxFrame = $n }
$frameCount++
} catch {}
$targetBase = "$SequenceName" + '_' + $digits
if ($base -eq $targetBase) { continue }
$newName = $targetBase + $ext
$newPath = Join-Path $SequenceFolderPath $newName
try {
if (Test-Path -LiteralPath $newPath) {
$existing = Get-Item -LiteralPath $newPath -ErrorAction Stop
$sameSize = ($existing.Length -eq $f.Length)
$sameTime = ([math]::Abs(($existing.LastWriteTimeUtc - $f.LastWriteTimeUtc).TotalSeconds) -le 1)
if ($sameSize -and $sameTime) {
Remove-Item -LiteralPath $f.FullName -Force -ErrorAction Stop
$collisions++
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] RENAME DROP duplicate: '$($f.Name)' (matches '$newName')" | Add-Content -LiteralPath $LogFile }
continue
}
$archiveDir = Join-Path $SequenceFolderPath '_archive'
if (-not (Test-Path -LiteralPath $archiveDir)) {
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
}
$archiveName = $existing.Name
$archivePath = Join-Path $archiveDir $archiveName
if (Test-Path -LiteralPath $archivePath) {
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($archiveName)
$archivePath = Join-Path $archiveDir ("{0}_{1}{2}" -f $baseName, $stamp, $existing.Extension)
}
Move-Item -LiteralPath $existing.FullName -Destination $archivePath -Force -ErrorAction Stop
$collisions++
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] RENAME ARCHIVE existing '$($existing.Name)' -> '$archivePath'" | Add-Content -LiteralPath $LogFile }
}
Rename-Item -LiteralPath $f.FullName -NewName $newName -ErrorAction Stop
$renamed++
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] RENAME: '$($f.Name)' -> '$newName'" | Add-Content -LiteralPath $LogFile }
}
catch {
$errors++
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] RENAME ERROR for '$($f.Name)': $($_.Exception.Message)" | Add-Content -LiteralPath $LogFile }
}
}
$minOut = $null
$maxOut = $null
if ($frameCount -gt 0) {
$minOut = $minFrame
$maxOut = $maxFrame
}
return [pscustomobject]@{
Renamed = $renamed
Collisions = $collisions
Errors = $errors
Checked = $checked
MinFrame = $minOut
MaxFrame = $maxOut
FrameCount = $frameCount
}
}
function Get-Mp4FrameRange {
param([string]$Name)
if ($Name -match '[-_](\d+)[-_](\d+)\.mp4$') {
return [pscustomobject]@{ Start = [int]$Matches[1]; End = [int]$Matches[2] }
}
return $null
}
function Rename-SequencePreviewMp4 {
param(
[Parameter(Mandatory)] [string]$SequenceFolderPath,
[Parameter(Mandatory)] [string]$SequenceName,
[Parameter(Mandatory)] [int]$StartFrame,
[Parameter(Mandatory)] [int]$EndFrame,
[Parameter(Mandatory)] [string]$RootPath,
[string]$LogFile
)
$renamed = 0
$collisions = 0
$errors = 0
$checked = 0
$targetName = "$SequenceName-$StartFrame-$EndFrame.mp4"
$targetPath = Join-Path $SequenceFolderPath $targetName
$collisionsDir = Join-Path $RootPath '_collisions'
$mp4s = @(Get-ChildItem -LiteralPath $SequenceFolderPath -File -Filter '*.mp4' -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notlike '*\_archive\*' })
if ($mp4s.Count -eq 0) { return [pscustomobject]@{ Renamed = 0; Collisions = 0; Errors = 0; Checked = 0 } }
$checked = $mp4s.Count
$candidates = @($mp4s | ForEach-Object {
$fr = Get-Mp4FrameRange -Name $_.Name
$frameMatch = ($fr -and $fr.Start -eq $StartFrame -and $fr.End -eq $EndFrame)
[pscustomobject]@{
File = $_
FrameMatch = $frameMatch
LastWrite = $_.LastWriteTimeUtc
}
} | Sort-Object -Property @{ Expression = { -not $_.FrameMatch }; Ascending = $true }, @{ Expression = 'LastWrite'; Descending = $true })
$best = $candidates[0].File
$moveToCollisions = @(if ($candidates.Count -gt 1) { $candidates[1..($candidates.Count - 1)] } else { @() })
$hasCollisions = ($moveToCollisions.Count -gt 0) -or ($best.Name -ne $targetName -and (Test-Path -LiteralPath $targetPath))
if (-not $hasCollisions -and $best.Name -eq $targetName) {
return [pscustomobject]@{ Renamed = 0; Collisions = 0; Errors = 0; Checked = $checked }
}
try {
if ($hasCollisions -and -not (Test-Path -LiteralPath $collisionsDir)) {
New-Item -ItemType Directory -Path $collisionsDir -Force | Out-Null
}
foreach ($c in $moveToCollisions) {
$f = $c.File
$destName = $f.Name
$destPath = Join-Path $collisionsDir $destName
if (Test-Path -LiteralPath $destPath) {
$base = [System.IO.Path]::GetFileNameWithoutExtension($destName)
$destName = "{0}_{1}.mp4" -f $base, (Get-Date -Format 'yyyyMMdd_HHmmss')
$destPath = Join-Path $collisionsDir $destName
}
Move-Item -LiteralPath $f.FullName -Destination $destPath -Force -ErrorAction Stop
$collisions++
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] MP4 COLLISION moved: '$($f.Name)' -> '$destPath'" | Add-Content -LiteralPath $LogFile }
}
if ($best.Name -ne $targetName) {
if (Test-Path -LiteralPath $targetPath) {
$destPath = Join-Path $collisionsDir $targetName
if (Test-Path -LiteralPath $destPath) {
$base = [System.IO.Path]::GetFileNameWithoutExtension($targetName)
$destPath = Join-Path $collisionsDir ("{0}_{1}.mp4" -f $base, (Get-Date -Format 'yyyyMMdd_HHmmss'))
}
Move-Item -LiteralPath $targetPath -Destination $destPath -Force -ErrorAction Stop
$collisions++
}
Rename-Item -LiteralPath $best.FullName -NewName $targetName -ErrorAction Stop
$renamed++
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] MP4 RENAME: '$($best.Name)' -> '$targetName'" | Add-Content -LiteralPath $LogFile }
}
} catch {
$errors++
if ($LogFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] MP4 ERROR: $($_.Exception.Message)" | Add-Content -LiteralPath $LogFile }
}
return [pscustomobject]@{
Renamed = $renamed
Collisions = $collisions
Errors = $errors
Checked = $checked
}
}
function Resolve-SequenceName {
param(
[Parameter(Mandatory)] [System.IO.DirectoryInfo]$Directory
)
if ($Directory.Name -eq '_CURRENT' -and $Directory.Parent) {
return $Directory.Parent.Name
}
return $Directory.Name
}
function Add-SequenceFolder {
param(
[Parameter(Mandatory)] [System.IO.DirectoryInfo]$Directory,
[Parameter(Mandatory)] [hashtable]$Map
)
if ($Directory.Name -like '_*') { return }
$fullPath = $Directory.FullName
if ($Map.ContainsKey($fullPath)) { return }
# Check if directory contains sequence files directly (not just subdirectories)
$extensions = @('.png','.jpg','.jpeg','.exr','.tif','.tiff','.bmp','.tga')
$hasSequenceFiles = Get-ChildItem -LiteralPath $fullPath -File -ErrorAction SilentlyContinue |
Where-Object { $extensions -contains $_.Extension.ToLower() } |
Select-Object -First 1
# If it only has subdirectories and no sequence files, skip it
$hasSubdirs = Get-ChildItem -LiteralPath $fullPath -Directory -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($null -eq $hasSequenceFiles -and $null -ne $hasSubdirs) {
return
}
$Map[$fullPath] = Resolve-SequenceName -Directory $Directory
}
try {
$root = (Get-Location).ProviderPath
$logFile = $null
if ($DebugMode) {
$logFile = Join-Path $root ("UpdateSequences_{0}.log" -f (Get-Date -Format 'yyyyMMdd_HHmmss'))
"[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] === UpdateSequences started in '$root' ===" | Out-File -LiteralPath $logFile -Encoding UTF8
}
$sequenceMap = @{}
$primaryPattern = if ($useIsoDailyFormat) { '????-??-??' } else { 'daily_*' }
$secondaryPattern = if ($useIsoDailyFormat) { 'daily_*' } else { '????-??-??' }
$primaryDirs = Get-ChildItem -LiteralPath $root -Directory -Filter $primaryPattern -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notlike '_*' }
foreach ($d in $primaryDirs) {
$seqDirs = @(Get-ChildItem -LiteralPath $d.FullName -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '_*' })
if ($seqDirs.Count -eq 0) {
Add-SequenceFolder -Directory $d -Map $sequenceMap
} else {
foreach ($s in $seqDirs) {
Add-SequenceFolder -Directory $s -Map $sequenceMap
}
}
}
$secondaryDirs = Get-ChildItem -LiteralPath $root -Directory -Filter $secondaryPattern -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notlike '_*' }
foreach ($d in $secondaryDirs) {
$seqDirs = @(Get-ChildItem -LiteralPath $d.FullName -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '_*' })
if ($seqDirs.Count -eq 0) {
Add-SequenceFolder -Directory $d -Map $sequenceMap
} else {
foreach ($s in $seqDirs) {
Add-SequenceFolder -Directory $s -Map $sequenceMap
}
}
}
$directSeqs = Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue |
Where-Object {
$_.Name -notlike '_*' -and
$_.Name -notlike 'daily_*' -and
$_.Name -notmatch '^\d{4}-\d{2}-\d{2}$'
}
foreach ($seq in $directSeqs) {
$seqDirs = @(Get-ChildItem -LiteralPath $seq.FullName -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '_*' })
if ($seqDirs.Count -eq 0) {
Add-SequenceFolder -Directory $seq -Map $sequenceMap
} else {
foreach ($s in $seqDirs) {
Add-SequenceFolder -Directory $s -Map $sequenceMap
}
}
}
$sequenceFolders = $sequenceMap.GetEnumerator() | ForEach-Object {
[pscustomobject]@{
Path = $_.Key
Name = $_.Value
}
} | Sort-Object -Property Path
if (-not $sequenceFolders) {
Write-Host "No sequence folders found." -ForegroundColor Yellow
if ($logFile) { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] No sequence folders found." | Add-Content -LiteralPath $logFile }
exit 0
}
$totalSequences = 0
$filesRenamedTotal = 0
$renameCollisions = 0
$renameErrors = 0
$mp4RenamedTotal = 0
$mp4Collisions = 0
$mp4Errors = 0
foreach ($seq in $sequenceFolders) {
$totalSequences++
$renameResult = Sync-SequenceFilenames -SequenceFolderPath $seq.Path -SequenceName $seq.Name -LogFile $logFile
if ($DebugMode -or $renameResult.Renamed -gt 0 -or $renameResult.Collisions -gt 0 -or $renameResult.Errors -gt 0) {
Write-Host "[RENAME]|$($seq.Path)|$($seq.Name)|checked=$($renameResult.Checked)|renamed=$($renameResult.Renamed)|collisions=$($renameResult.Collisions)|errors=$($renameResult.Errors)" -ForegroundColor Cyan
}
$filesRenamedTotal += $renameResult.Renamed
$renameCollisions += $renameResult.Collisions
$renameErrors += $renameResult.Errors
if ($renameResult.FrameCount -gt 0 -and $null -ne $renameResult.MinFrame -and $null -ne $renameResult.MaxFrame) {
$mp4Result = Rename-SequencePreviewMp4 -SequenceFolderPath $seq.Path -SequenceName $seq.Name -StartFrame $renameResult.MinFrame -EndFrame $renameResult.MaxFrame -RootPath $root -LogFile $logFile
if ($DebugMode -or $mp4Result.Renamed -gt 0 -or $mp4Result.Collisions -gt 0 -or $mp4Result.Errors -gt 0) {
Write-Host "[MP4]|$($seq.Path)|$($seq.Name)|renamed=$($mp4Result.Renamed)|collisions=$($mp4Result.Collisions)|errors=$($mp4Result.Errors)" -ForegroundColor Cyan
}
$mp4RenamedTotal += $mp4Result.Renamed
$mp4Collisions += $mp4Result.Collisions
$mp4Errors += $mp4Result.Errors
}
}
Write-Host "=== SUMMARY REPORT ===" -ForegroundColor Magenta
Write-Host "Sequences scanned: $totalSequences" -ForegroundColor White
Write-Host "Files renamed: $filesRenamedTotal (collisions: $renameCollisions, errors: $renameErrors)" -ForegroundColor White
Write-Host "Preview MP4s renamed: $mp4RenamedTotal (collisions: $mp4Collisions, errors: $mp4Errors)" -ForegroundColor White
Write-Host "=====================" -ForegroundColor Magenta
if ($logFile) {
"[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] === UpdateSequences completed (seq=$totalSequences renamed=$filesRenamedTotal mp4=$mp4RenamedTotal) ===" | Add-Content -LiteralPath $logFile
}
exit 0
}
catch {
if ($logFile) {
"[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] ERROR: $($_.Exception.Message)" | Add-Content -LiteralPath $logFile
}
Write-Host "ERROR: $_" -ForegroundColor Red
exit 1
}
+8
View File
@@ -0,0 +1,8 @@
# Deprecated helper retained for compatibility.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Write-Warning 'UpgradeSeqBatches.ps1 has been deprecated.'
Write-Warning 'Please run UpdateProjectBatches.ps1 and choose the project that needs refreshed batch files.'
exit 1
+28 -2
View File
@@ -44,7 +44,19 @@ if not exist "%rendersDir%" (
) )
) )
for %%F in (NewDaily.bat UpdateSequences.bat) do ( for %%F in (UpdateSequences.bat ZipSeqArchv.bat UnzipSeqArchv.bat) do (
if exist "%scriptDir%%%F" (
if "%DRY%"=="1" (
echo [DRY] copy "%scriptDir%%%F" "%rendersDir%\%%F"
) else (
copy /Y "%scriptDir%%%F" "%rendersDir%\%%F" >nul
)
) else (
echo [WARN] Missing template: "%scriptDir%%%F"
)
)
for %%F in (CfgDeploy.ps1 config.json) do (
if exist "%scriptDir%%%F" ( if exist "%scriptDir%%%F" (
if "%DRY%"=="1" ( if "%DRY%"=="1" (
echo [DRY] copy "%scriptDir%%%F" "%rendersDir%\%%F" echo [DRY] copy "%scriptDir%%%F" "%rendersDir%\%%F"
@@ -118,9 +130,23 @@ if exist "%animDir%" (
set "prefix=!name:~0,6!" set "prefix=!name:~0,6!"
if /I not "!prefix!"=="daily_" ( if /I not "!prefix!"=="daily_" (
set "submodName=%%~nS" set "submodName=%%~nS"
set "_subdir=%rendersDir%\!submodName!"
rem Ensure submodule dir exists and place helper scripts there
if "%DRY%"=="1" (
if not exist "!_subdir!" echo [DRY] mkdir "!_subdir!"
if exist "%scriptDir%UpdateSequences.bat" echo [DRY] copy "%scriptDir%UpdateSequences.bat" "!_subdir!\UpdateSequences.bat"
if exist "%scriptDir%ZipSeqArchv.bat" echo [DRY] copy "%scriptDir%ZipSeqArchv.bat" "!_subdir!\ZipSeqArchv.bat"
if exist "%scriptDir%UnzipSeqArchv.bat" echo [DRY] copy "%scriptDir%UnzipSeqArchv.bat" "!_subdir!\UnzipSeqArchv.bat"
) else (
if not exist "!_subdir!" mkdir "!_subdir!" >nul 2>&1
if exist "%scriptDir%UpdateSequences.bat" copy /Y "%scriptDir%UpdateSequences.bat" "!_subdir!\UpdateSequences.bat" >nul
if exist "%scriptDir%ZipSeqArchv.bat" copy /Y "%scriptDir%ZipSeqArchv.bat" "!_subdir!\ZipSeqArchv.bat" >nul
if exist "%scriptDir%UnzipSeqArchv.bat" copy /Y "%scriptDir%UnzipSeqArchv.bat" "!_subdir!\UnzipSeqArchv.bat" >nul
)
for /d %%D in ("%%S\daily_*") do ( for /d %%D in ("%%S\daily_*") do (
set "dailyName=%%~nD"
set "_src=%%D\seq" set "_src=%%D\seq"
set "_dst=%rendersDir%\!submodName!" set "_dst=%rendersDir%\!submodName!\!dailyName!"
set /a debugTotal+=1 set /a debugTotal+=1
if "%DRY%"=="1" ( if "%DRY%"=="1" (
if exist "!_src!" ( if exist "!_src!" (
+44
View File
@@ -0,0 +1,44 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "REN_DIR=%~dp0"
for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI"
set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run CfgDeploy.ps1 to deploy helper files.
exit /b 1
)
if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run CfgDeploy.ps1 to deploy helper files.
exit /b 1
)
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%~I"
if not defined PY_SCRIPT (
echo [ERROR] Unable to resolve zip_sequences.py path from config.
exit /b 1
)
pushd "%PROJ_ROOT%" >nul 2>&1
python "%PY_SCRIPT%" --verbose %*
set "ERR=!ERRORLEVEL!"
popd >nul 2>&1
if !ERR! NEQ 0 (
echo Failed to update render sequence archives ^(exit code !ERR!^).
exit /b !ERR!
)
exit /b 0
+137 -34
View File
@@ -1,40 +1,143 @@
*.mp4 filter=lfs diff=lfs merge=lfs -text # Git LFS attributes configuration
*.png filter=lfs diff=lfs merge=lfs -text # All listed file types will be tracked by Git LFS
*.jpg filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text # Video files
*.hdr filter=lfs diff=lfs merge=lfs -text *.avi filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.7z filter=lfs diff=lfs merge=lfs -text
*.abc filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
*.mkv filter=lfs diff=lfs merge=lfs -text *.mkv filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.max filter=lfs diff=lfs merge=lfs -text
*.ma filter=lfs diff=lfs merge=lfs -text
*.bvh filter=lfs diff=lfs merge=lfs -text
*.h5 filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.c4d filter=lfs diff=lfs merge=lfs -text
*.mov filter=lfs diff=lfs merge=lfs -text *.mov filter=lfs diff=lfs merge=lfs -text
*.blend1 filter=lfs diff=lfs merge=lfs -text *.MOV filter=lfs diff=lfs merge=lfs -text
*.3ds filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text
*.hdf5 filter=lfs diff=lfs merge=lfs -text *.webm filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text # Image files
*.fbx filter=lfs diff=lfs merge=lfs -text *.bmp filter=lfs diff=lfs merge=lfs -text
*.blend2 filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text
*.mb filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text *.hdr filter=lfs diff=lfs merge=lfs -text
*.ai filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text *.tga filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text *.tif filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text *.tiff filter=lfs diff=lfs merge=lfs -text
*.bmp filter=lfs diff=lfs merge=lfs -text *.webp filter=lfs diff=lfs merge=lfs -text
*.eps filter=lfs diff=lfs merge=lfs -text *.heic filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text *.HEIC filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.avi filter=lfs diff=lfs merge=lfs -text # 3D/CG files
*.3ds filter=lfs diff=lfs merge=lfs -text
*.abc filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.blend1 filter=lfs diff=lfs merge=lfs -text
*.blend2 filter=lfs diff=lfs merge=lfs -text
*.bvh filter=lfs diff=lfs merge=lfs -text
*.bvh.gz filter=lfs diff=lfs merge=lfs -text *.bvh.gz filter=lfs diff=lfs merge=lfs -text
*.c4d filter=lfs diff=lfs merge=lfs -text
*.dae filter=lfs diff=lfs merge=lfs -text *.dae filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text *.fbx filter=lfs diff=lfs merge=lfs -text
*.fbxkey filter=lfs diff=lfs merge=lfs -text
*.ma filter=lfs diff=lfs merge=lfs -text
*.max filter=lfs diff=lfs merge=lfs -text
*.mb filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.usd filter=lfs diff=lfs merge=lfs -text
*.usdz filter=lfs diff=lfs merge=lfs -text
*.vdb filter=lfs diff=lfs merge=lfs -text
*.bphys filter=lfs diff=lfs merge=lfs -text
*.bobj filter=lfs diff=lfs merge=lfs -text
*.bvel filter=lfs diff=lfs merge=lfs -text
*.bpointcache filter=lfs diff=lfs merge=lfs -text
*.blob filter=lfs diff=lfs merge=lfs -text
*.unitypackage filter=lfs diff=lfs merge=lfs -text
# Character Creator files (Reallusion)
*.ccAvatar filter=lfs diff=lfs merge=lfs -text
*.ccProject filter=lfs diff=lfs merge=lfs -text
*.ccCloth filter=lfs diff=lfs merge=lfs -text
*.ccProp filter=lfs diff=lfs merge=lfs -text
*.ccScene filter=lfs diff=lfs merge=lfs -text
*.ccHair filter=lfs diff=lfs merge=lfs -text
*.rlMotion filter=lfs diff=lfs merge=lfs -text
*.rlPose filter=lfs diff=lfs merge=lfs -text
*.iAvatar filter=lfs diff=lfs merge=lfs -text
*.iMotion filter=lfs diff=lfs merge=lfs -text
*.iProp filter=lfs diff=lfs merge=lfs -text
*.iMaterial filter=lfs diff=lfs merge=lfs -text
*.rlHead filter=lfs diff=lfs merge=lfs -text
*.rlHair filter=lfs diff=lfs merge=lfs -text
*.rlScene filter=lfs diff=lfs merge=lfs -text
*.rlSkintex filter=lfs diff=lfs merge=lfs -text
# Houdini files
*.hiplc filter=lfs diff=lfs merge=lfs -text
*.bgeo filter=lfs diff=lfs merge=lfs -text
*.bgeo.sc filter=lfs diff=lfs merge=lfs -text
# Flip Fluids cache files
*.data filter=lfs diff=lfs merge=lfs -text
*.sqlite3 filter=lfs diff=lfs merge=lfs -text
*.ffp3 filter=lfs diff=lfs merge=lfs -text
# Substance files
*.sbs filter=lfs diff=lfs merge=lfs -text
*.sbsar filter=lfs diff=lfs merge=lfs -text
*.spp filter=lfs diff=lfs merge=lfs -text
# Audio files
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.m4a filter=lfs diff=lfs merge=lfs -text
*.aac filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.flac filter=lfs diff=lfs merge=lfs -text
# Archive/Compression files
*.7z filter=lfs diff=lfs merge=lfs -text
*.7z.[0-9][0-9][0-9] filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
# Documents
*.docx filter=lfs diff=lfs merge=lfs -text
*.xlsx filter=lfs diff=lfs merge=lfs -text
*.eps filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
# Adobe files
*.aegraphic filter=lfs diff=lfs merge=lfs -text
*.aep filter=lfs diff=lfs merge=lfs -text
*.prel filter=lfs diff=lfs merge=lfs -text
*.prin filter=lfs diff=lfs merge=lfs -text
*.prmf filter=lfs diff=lfs merge=lfs -text
*.prproj filter=lfs diff=lfs merge=lfs -text
*.ai filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.pk filter=lfs diff=lfs merge=lfs -text
*.pkf filter=lfs diff=lfs merge=lfs -text
# Davinci Resolve files
*.dpx filter=lfs diff=lfs merge=lfs -text
*.drp filter=lfs diff=lfs merge=lfs -text
# Data files
*.h5 filter=lfs diff=lfs merge=lfs -text
*.hdf5 filter=lfs diff=lfs merge=lfs -text
# Other
*.uni filter=lfs diff=lfs merge=lfs -text
*.pdn filter=lfs diff=lfs merge=lfs -text
*.pur filter=lfs diff=lfs merge=lfs -text
*.db filter=lfs diff=lfs merge=lfs -text
*.raw filter=lfs diff=lfs merge=lfs -text
*.lnk filter=lfs diff=lfs merge=lfs -text
# Python files
*.npz filter=lfs diff=lfs merge=lfs -text
# Adobe Alternatives
*.xcf filter=lfs diff=lfs merge=lfs -text
*.af filter=lfs diff=lfs merge=lfs -text
+20 -16
View File
@@ -40,13 +40,7 @@
*~ *~
*.tmp *.tmp
*.temp *.temp
BL_proxy/
# Blender internal cache files
*.bpy
*.bmesh
*.bvh
*.bobj
*.bphys
# OS generated files # OS generated files
.DS_Store .DS_Store
@@ -64,6 +58,8 @@ desktop.ini
*.swp *.swp
*.swo *.swo
*~ *~
.specstory/
.cursorindexingignore
# Large media files (uncomment if you want to exclude rendered outputs) # Large media files (uncomment if you want to exclude rendered outputs)
# *.mp4 # *.mp4
@@ -83,12 +79,6 @@ desktop.ini
# *.aac # *.aac
# *.ogg # *.ogg
# Archive files
*.zip
*.rar
*.7z
*.tar.gz
# Log files # Log files
*.log *.log
@@ -119,10 +109,24 @@ temp/
tmp/ tmp/
cache/ cache/
# image sequences Renders/**
!Renders/_zipped/
!Renders/_zipped/**
!Renders/**/
!Renders/**/*.bat
!Renders/**/*.log
seq/ seq/
Renders/
# Premiere # Premiere
*.prlock *.prlock
Adobe Premiere Pro Video Previews/ Adobe Premiere Pro Video Previews/
Adobe Premiere Pro Audio Previews/
Adobe Premiere Pro (Beta) Video Previews/
Adobe Premiere Pro (Beta) Audio Previews/
footage/
plates/
RnR/
# Microsoft Office temporary files
~$*
+173
View File
@@ -0,0 +1,173 @@
@echo off
setlocal enabledelayedexpansion
echo Starting Blender file compression...
REM Check if files were dragged onto the batch file
if "%~1"=="" (
echo.
echo Usage: Drag .blend files onto this batch file to compress them.
echo Output will be saved to the \output folder.
echo.
pause
exit /b 1
)
REM Create output directory if it doesn't exist
if not exist "output" mkdir "output"
REM Initialize counters and tracking
set "processed=0"
set "crashed=0"
set "skipped=0"
set "deleted_similar=0"
set "threshold_percent=10"
REM Create a temporary Python script for compression
set "TEMP_SCRIPT=compress_blend_temp.py"
echo Creating Python script: !TEMP_SCRIPT!
(
echo import bpy
echo import os
echo import sys
echo.
echo # Enable compression globally
echo bpy.context.preferences.filepaths.save_version = 0
echo bpy.context.preferences.filepaths.use_file_compression = True
echo.
echo # Create output directory if it doesn't exist
echo if not os.path.exists^("output"^):
echo os.makedirs^("output"^)
echo.
echo # Get command line arguments ^(the dragged files^)
echo blend_files = sys.argv[sys.argv.index^("--"^) + 1:] if "--" in sys.argv else []
echo.
echo print^("Found " + str^(len^(blend_files^)^) + " .blend files to compress"^)
echo.
echo def is_compressed_file^(path^):
echo try:
echo with open^(path, 'rb'^) as f:
echo header = f.read^(4^)
echo if header[:2] == b'\x1f\x8b': # gzip
echo return True
echo if header == b'\x28\xb5\x2f\xfd': # zstd
echo return True
echo return False
echo except Exception:
echo return False
echo.
echo for blend_file in blend_files:
echo if blend_file.lower^(^).endswith^(".blend"^):
echo try:
echo # Create output path in output folder
echo filename = os.path.basename^(blend_file^)
echo output_file = os.path.join^("output", filename^)
echo # Check if output file already exists
echo if os.path.exists^(output_file^):
echo print^("SKIP_EXISTING:" + blend_file^)
echo continue
echo # Skip if source is already compressed
echo if is_compressed_file^(blend_file^):
echo print^("SKIP_ALREADY_COMPRESSED:" + blend_file^)
echo continue
echo print^("PROCESSING:" + blend_file^)
echo # Load the blend file
echo bpy.ops.wm.open_mainfile^(filepath=blend_file^)
echo # Save with compression to output folder
echo bpy.ops.wm.save_mainfile^(filepath=output_file, compress=True^)
echo print^("SUCCESS:" + blend_file + ":" + output_file^)
echo except Exception as e:
echo print^("CRASH:" + blend_file + ":" + str^(e^)^)
echo else:
echo print^("SKIP_NOT_BLEND:" + blend_file^)
echo.
echo print^("COMPRESSION_COMPLETE"^)
echo bpy.ops.wm.quit_blender^(^)
) > "!TEMP_SCRIPT!"
REM Check if script was created successfully
if exist "!TEMP_SCRIPT!" (
echo Script created successfully: !TEMP_SCRIPT!
) else (
echo ERROR: Failed to create script file!
pause
exit /b 1
)
REM Run Blender to process the dragged files
echo Processing dragged .blend files...
echo Using script: !TEMP_SCRIPT!
del /f /q "blender_output.txt" 2>nul
blender --background --factory-startup --python "!TEMP_SCRIPT!" -- %* 2^>^&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath 'blender_output.txt'"
REM Clean up temporary script
echo Cleaning up temporary script...
del "!TEMP_SCRIPT!" 2>nul
REM Parse output and generate report
echo.
echo ========================================
echo COMPRESSION REPORT
echo ========================================
echo.
REM Count processed files and get file info
for /f "tokens=1,2,3 delims=:" %%a in (blender_output.txt) do (
if "%%a"=="SUCCESS" (
set /a processed+=1
set "original_file=%%b"
set "compressed_file=%%c"
REM Get file sizes
for %%f in ("!original_file!") do set "original_size=%%~zf"
for %%f in ("!compressed_file!") do set "compressed_size=%%~zf"
echo [!processed!] !original_file!
echo Original: !original_size! bytes
echo Compressed: !compressed_size! bytes
set /a "savings=!original_size! - !compressed_size!"
if !original_size! gtr 0 (set /a percent=(savings*100)/original_size) else (set percent=0)
echo Savings: !savings! bytes
echo Reduction: !percent!%%
if !percent! lss !threshold_percent! (
del "!compressed_file!" 2>nul
set /a deleted_similar+=1
echo Deleted: too similar to original
)
echo.
) else if "%%a"=="CRASH" (
set /a crashed+=1
echo [!crashed!] CRASHED: %%b
echo Error: %%c
echo.
) else if "%%a"=="SKIP_EXISTING" (
set /a skipped+=1
echo [!skipped!] SKIPPED ^(already exists^): %%b
echo.
) else if "%%a"=="SKIP_NOT_BLEND" (
set /a skipped+=1
echo [!skipped!] SKIPPED ^(not .blend^): %%b
echo.
) else if "%%a"=="SKIP_ALREADY_COMPRESSED" (
set /a skipped+=1
echo [!skipped!] SKIPPED ^(already compressed^): %%b
echo.
)
)
REM Clean up output file
del /f /q "blender_output.txt" 2>nul
echo ========================================
echo SUMMARY
echo ========================================
echo Processed: !processed! files
echo Crashed: !crashed! files
echo Skipped: !skipped! files
echo Deleted ^(too similar^): !deleted_similar! files
echo Total: %* files
echo ========================================
echo.
echo Done!
pause
+465
View File
@@ -0,0 +1,465 @@
#!/usr/bin/env python3
"""Compress PNGs in unchanged render sequences. Writes to staging, prompts for Y/N, then overwrites originals."""
from __future__ import annotations
import argparse
import json
import multiprocessing
import os
import platform
import shutil
import sys
import time
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
try:
from PIL import Image
except ImportError:
print("Error: Pillow (PIL) is required. Install with: pip install Pillow", file=sys.stderr)
sys.exit(1)
try:
import unlock_processpool
unlock_processpool.please()
UNLOCKED = True
except ImportError:
UNLOCKED = False
RENDER_ROOT = Path("Renders")
ARCHIVE_ROOT = RENDER_ROOT / "_zipped"
STAGING_DIR = "_compressed_staging"
CORRUPTED_DIR = "_corrupted"
STATE_SUFFIX = ".meta.json"
PNG_EXT = ".png"
def is_archive_path(path: Path) -> bool:
return any(part in ("_archive", "_CURRENT", "_zipped", STAGING_DIR, CORRUPTED_DIR) for part in path.parts)
def find_sequence_dirs(root: Path) -> list[Path]:
seen = set()
out = []
for dirpath, dirnames, filenames in os.walk(root):
path = Path(dirpath)
dirnames[:] = [d for d in dirnames if d not in ("_archive", "_CURRENT", STAGING_DIR, CORRUPTED_DIR, "_zipped")]
if is_archive_path(path):
continue
if any(Path(dirpath, f).suffix.lower() == PNG_EXT for f in filenames):
resolved = path.resolve()
if resolved not in seen:
seen.add(resolved)
out.append(path)
return out
def iter_png_files(seq_dir: Path):
for dirpath, dirnames, filenames in os.walk(seq_dir):
path = Path(dirpath)
dirnames[:] = [d for d in dirnames if d not in ("_archive", "_CURRENT", "_zipped", STAGING_DIR, CORRUPTED_DIR)]
if is_archive_path(path):
continue
for f in filenames:
if Path(f).suffix.lower() == PNG_EXT:
yield path / f
def load_state(state_path: Path) -> dict | None:
if not state_path.exists():
return None
try:
state = json.loads(state_path.read_text())
if "files" in state:
state["files"] = [e for e in state.get("files", []) if Path(e.get("path", "")).name.lower() != "thumbs.db"]
if platform.system() == "Windows" and "files" in state:
for e in state.get("files", []):
if "mtime_ns" in e:
e["mtime_ns"] = (e["mtime_ns"] // 100) * 100
return state
except json.JSONDecodeError:
return None
def _canonical_path(p: str) -> str:
"""Normalize path for comparison (case-insensitive, consistent separators)."""
return Path(p).as_posix().lower()
def state_changed(seq_state: dict, stored_state: dict | None, *, verbose: bool = False) -> bool:
if stored_state is None:
return True
def png_only(files):
return [e for e in files if Path(e.get("path", "")).suffix.lower() == ".png"]
def norm(s):
out = []
for e in png_only(s.get("files", [])):
if Path(e.get("path", "")).name.lower() == "thumbs.db":
continue
out.append({"path": _canonical_path(e["path"]), "size": e["size"]})
out.sort(key=lambda x: x["path"])
return {"files": out}
n_seq = norm(seq_state)
n_stored = norm(stored_state)
if n_seq == n_stored:
return False
if verbose:
seq_map = {f["path"]: f for f in n_seq["files"]}
stored_map = {f["path"]: f for f in n_stored["files"]}
seq_paths = set(seq_map.keys())
stored_paths = set(stored_map.keys())
if seq_paths != stored_paths:
missing = stored_paths - seq_paths
extra = seq_paths - stored_paths
if missing:
print(f" State diff: in archive but not on disk: {sorted(missing)[:3]}{'...' if len(missing) > 3 else ''}")
if extra:
print(f" State diff: on disk but not in archive: {sorted(extra)[:3]}{'...' if len(extra) > 3 else ''}")
else:
for p in sorted(seq_paths)[:3]:
a, b = seq_map[p], stored_map[p]
if a != b:
if a["size"] != b["size"]:
print(f" State diff: {p}: size {a['size']} vs {b['size']}")
return True
def compute_state(seq_dir: Path) -> dict:
entries = []
is_windows = platform.system() == "Windows"
for p in sorted(iter_png_files(seq_dir), key=lambda x: x.relative_to(seq_dir).as_posix().lower()):
stat = p.stat()
mtime_ns = stat.st_mtime_ns
if is_windows:
mtime_ns = (mtime_ns // 100) * 100
entries.append({"path": p.relative_to(seq_dir).as_posix(), "size": stat.st_size, "mtime_ns": mtime_ns})
return {"files": entries}
def find_archive_and_state(seq_dir: Path) -> tuple[Path | None, Path | None]:
"""Return (archive_path, state_path) if sequence is archived and unchanged; else (None, None)."""
try:
rel = seq_dir.relative_to(RENDER_ROOT)
except ValueError:
return None, None
for suffix in (".7z", ".zip"):
archive = ARCHIVE_ROOT / f"{rel}{suffix}"
if archive.exists():
state = archive.with_suffix(archive.suffix + STATE_SUFFIX)
return archive, state
return None, None
def compress_png(input_path: Path, output_path: Path, force_bitdepth: str | None) -> tuple[str, bool, str | None, int, int, float, bool, bool]:
"""Returns (path, success, error, orig_size, new_size, savings_pct, was_skipped, is_corrupted)."""
try:
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
orig_size = input_path.stat().st_size
if orig_size == 0:
return (str(input_path), False, "CORRUPTED: File is 0 bytes (empty/placeholder)", 0, 0, 0, False, True)
try:
img = Image.open(input_path)
img.load()
except Exception as e:
return (str(input_path), False, f"CORRUPTED: Cannot open/load image - {e}", orig_size, 0, 0, False, True)
if img.width == 0 or img.height == 0:
return (str(input_path), False, f"CORRUPTED: Invalid dimensions ({img.width}x{img.height})", orig_size, 0, 0, False, True)
# All-black + suspiciously small (corruption indicator)
try:
sample_size = min(100, img.width * img.height)
pixels = list(img.getdata()[:sample_size])
if pixels:
all_black = True
for pixel in pixels:
if isinstance(pixel, (tuple, list)):
if any(p > 0 for p in pixel):
all_black = False
break
else:
if pixel > 0:
all_black = False
break
if all_black and orig_size < (img.width * img.height * 0.1):
return (str(input_path), False, "CORRUPTED: Image appears all black with suspiciously small file size", orig_size, 0, 0, False, True)
except Exception:
pass
# Determine target bit depth
if force_bitdepth == "8":
is_16bit = False
elif force_bitdepth == "16":
is_16bit = True
else:
is_16bit = img.mode == "I"
if not is_16bit and img.mode in ("RGB", "RGBA", "LA"):
try:
pixels = list(img.getdata())
if pixels:
if isinstance(pixels[0], (tuple, list)):
max_val = max(max(p) for p in pixels[:1000])
else:
max_val = max(pixels[:1000])
if max_val > 255:
is_16bit = True
except Exception:
pass
# Handle bit depth conversion (8-bit to 8-bit, 16-bit to 16-bit)
if is_16bit:
if force_bitdepth == "16":
if img.mode != "I":
img = img.convert("RGBA" if ("A" in img.mode or img.mode == "LA") else "RGB")
else:
if img.mode == "I":
pass
elif img.mode in ("RGB", "RGBA"):
pass
else:
img = img.convert("RGBA" if "A" in img.mode else "RGB")
else:
if force_bitdepth == "8":
if img.mode == "I":
img = img.convert("L")
elif img.mode == "RGBA":
pass
elif img.mode == "RGB":
pass
else:
img = img.convert("RGBA" if "A" in img.mode else "RGB")
else:
if img.mode == "RGBA":
pass
elif img.mode != "RGB":
img = img.convert("RGB")
img.save(str(output_path), "PNG", optimize=True, compress_level=9)
new_size = output_path.stat().st_size
savings = (orig_size - new_size) / orig_size * 100 if orig_size > 0 else 0
return (str(input_path), True, None, orig_size, new_size, savings, False, False)
except Exception as e:
corrupt = "truncated" in str(e).lower() or "cannot identify" in str(e).lower() or "corrupt" in str(e).lower()
return (str(input_path), False, str(e), 0, 0, 0, False, corrupt)
def move_to_corrupted(src: Path, base: Path, corrupted_root: Path) -> bool:
try:
rel = src.relative_to(base)
except ValueError:
rel = src.name
dest = corrupted_root / rel
dest.parent.mkdir(parents=True, exist_ok=True)
src.rename(dest)
return True
def format_size(size_bytes: float) -> str:
for unit in ("B", "KB", "MB", "GB"):
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} TB"
def format_time(seconds: float) -> str:
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
centiseconds = int((seconds % 1) * 100)
return f"{hours:02d}:{minutes:02d}:{secs:02d}:{centiseconds:02d}"
def main() -> int:
parser = argparse.ArgumentParser(description="Compress PNGs in unchanged sequences; staging + Y/N overwrite.")
parser.add_argument("--8bit", "-8", action="store_true", dest="force_8bit")
parser.add_argument("--16bit", "-16", action="store_true", dest="force_16bit")
args = parser.parse_args()
force_bitdepth = "8" if args.force_8bit else ("16" if args.force_16bit else None)
if args.force_8bit and args.force_16bit:
print("Error: Cannot specify both --8bit and --16bit. Choose one.")
return 1
if force_bitdepth == "8":
print("Mode: Forcing 8-bit color depth")
elif force_bitdepth == "16":
print("Mode: Forcing 16-bit color depth")
else:
print("Mode: Auto-detect bit depth (preserve 16-bit if present)")
if not RENDER_ROOT.exists():
print("[ERROR] Renders directory not found.")
return 1
staging_root = RENDER_ROOT / STAGING_DIR
corrupted_root = RENDER_ROOT / CORRUPTED_DIR
# Find changed sequences (no archive, or state differs from stored)
changed: list[Path] = []
for seq_dir in find_sequence_dirs(RENDER_ROOT):
current = compute_state(seq_dir)
if not current["files"]:
continue
_, state_path = find_archive_and_state(seq_dir)
if state_path is None or not state_path.exists():
changed.append(seq_dir)
rel = seq_dir.relative_to(RENDER_ROOT)
print(f" Changed (no archive): {rel}")
continue
stored = load_state(state_path)
if state_changed(current, stored, verbose=True):
changed.append(seq_dir)
rel = seq_dir.relative_to(RENDER_ROOT)
print(f" Changed: {rel}")
if not changed:
print("No changed sequences with PNGs found. All sequences are up to date.")
return 0
print(f"Found {len(changed)} changed sequence(s) to compress.")
staging_root.mkdir(parents=True, exist_ok=True)
corrupted_root.mkdir(parents=True, exist_ok=True)
# Collect PNGs to compress
work: list[tuple[Path, Path]] = []
for seq_dir in changed:
rel = seq_dir.relative_to(RENDER_ROOT)
staging_seq = staging_root / rel
for png in iter_png_files(seq_dir):
out = staging_seq / png.relative_to(seq_dir)
work.append((png, out))
if not work:
print("No PNG files in changed sequences.")
return 0
max_workers = multiprocessing.cpu_count()
if platform.system() == "Windows" and not UNLOCKED:
max_workers = min(max_workers, 61)
if max_workers < multiprocessing.cpu_count():
print(f"Detected {multiprocessing.cpu_count()} CPU threads, but Windows limits ProcessPoolExecutor to 61 workers.")
print("Install 'unlock-processpool-win' package to use all cores: pip install unlock-processpool-win")
else:
if UNLOCKED:
print("Using unlock-processpool-win to bypass Windows 61-worker limit")
print(f"Compressing {len(work)} PNGs with {max_workers} workers...")
print("-" * 80)
compressed = 0
corrupted_count = 0
corrupted_paths: list[tuple[str, str]] = []
total_orig = 0
total_new = 0
failed = 0
start_time = time.time()
last_update_time = start_time
with ProcessPoolExecutor(max_workers=max_workers) as ex:
futures = {ex.submit(compress_png, inp, out, force_bitdepth): (inp, out) for inp, out in work}
for future in as_completed(futures):
inp, out = futures[future]
path, ok, err, o_sz, n_sz, _, _, is_corrupt = future.result()
if ok:
compressed += 1
total_orig += o_sz
total_new += n_sz
elif is_corrupt:
corrupted_count += 1
try:
if move_to_corrupted(Path(path), RENDER_ROOT, corrupted_root):
corrupted_paths.append((path, f"Moved to {corrupted_root / Path(path).relative_to(RENDER_ROOT)}"))
else:
corrupted_paths.append((path, str(err)))
except Exception as e:
corrupted_paths.append((path, str(e)))
print(f"\n[CORRUPTED] {path} -> {err}")
else:
failed += 1
print(f"\n[ERROR] {path}: {err}")
# Live progress (update every 0.5s or on first completion)
current_time = time.time()
time_since_update = current_time - last_update_time
processed = compressed + corrupted_count + failed
if processed == 1 or time_since_update >= 0.5:
elapsed = current_time - start_time
rate = processed / elapsed if elapsed > 0 else 0
remaining = len(work) - processed
eta_seconds = remaining / rate if rate > 0 and remaining > 0 else 0
total_savings = total_orig - total_new
total_savings_pct = (total_savings / total_orig * 100) if total_orig > 0 else 0
eta_str = format_time(eta_seconds) if eta_seconds > 0 else "calculating..."
elapsed_str = format_time(elapsed)
print(
f"[{processed:5d}/{len(work)}] "
f"Compressed: {compressed} | Corrupted: {corrupted_count} | Failed: {failed} | "
f"Speed: {rate:.1f} files/sec | "
f"Elapsed: {elapsed_str} | ETA: {eta_str} | "
f"Saved: {format_size(total_savings)} ({total_savings_pct:.1f}%)",
end="\r",
flush=True,
)
last_update_time = current_time
print() # Clear progress line
total_time = time.time() - start_time
total_savings = total_orig - total_new
total_savings_pct = (total_savings / total_orig * 100) if total_orig > 0 else 0
avg_rate = (compressed + corrupted_count + failed) / total_time if total_time > 0 else 0
print("=" * 80)
print("Compression complete!")
print(f"Successfully compressed: {compressed} files")
if corrupted_count > 0:
print(f"Corrupted (bad PNGs): {corrupted_count} files")
if failed > 0:
print(f"Failed: {failed} files")
print(f"Total time: {format_time(total_time)}")
print(f"Average speed: {avg_rate:.2f} files/second")
print(f"Original size: {format_size(total_orig)}")
print(f"Compressed size: {format_size(total_new)}")
print(f"Total savings: {format_size(total_savings)} ({total_savings_pct:.1f}%)")
if corrupted_paths:
print(f"\nCorrupted files moved to {corrupted_root}")
print("\n" + "=" * 80)
print("CORRUPTED FILES LIST:")
print("=" * 80)
for item in corrupted_paths:
file_path, move_status = item
print(f" {file_path}")
print(f" Status: {move_status}")
check_dirs = [f"Renders\\{STAGING_DIR}"]
if corrupted_count > 0:
check_dirs.append(f"Renders\\{CORRUPTED_DIR}")
resp = input(f"\nCheck files in {' and '.join(check_dirs)}. Ready to overwrite originals? (Y/N): ").strip().upper()
if resp != "Y":
print("Exiting. Staging and corrupted folders left for inspection.")
return 0
# Overwrite originals with staged files
for seq_dir in changed:
rel = seq_dir.relative_to(RENDER_ROOT)
staging_seq = staging_root / rel
if not staging_seq.exists():
continue
for staged in staging_seq.rglob("*.png"):
rel_file = staged.relative_to(staging_seq)
orig = seq_dir / rel_file
if orig.exists():
shutil.copy2(staged, orig)
# Cleanup staging and corrupted
shutil.rmtree(staging_root, ignore_errors=True)
if corrupted_root.exists():
shutil.rmtree(corrupted_root, ignore_errors=True)
print("Overwrite complete. Staging and corrupted folders cleaned up.")
return 0
if __name__ == "__main__":
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
{
"dailyFormat": "YYYY-MM-DD",
"structDir": "D:\\0 ProjectStructure",
"zipper": "7z",
"compression": 0,
"Max7zInst": 0,
"zipsegLimit": "2G"
}
+23
View File
@@ -0,0 +1,23 @@
@echo off
setlocal EnableExtensions
set "script_dir=%~dp0"
set "ps1_path=%script_dir%organize_textures.ps1"
if not exist "%ps1_path%" (
echo [ERROR] organize_textures.ps1 not found at %ps1_path%
exit /b 1
)
echo Running texture organizer...
pwsh -NoProfile -ExecutionPolicy Bypass -File "%ps1_path%"
set "rc=%errorlevel%"
if %rc% neq 0 (
echo.
echo Script exited with error code %rc%
)
echo.
pause
exit /b %rc%
+980
View File
@@ -0,0 +1,980 @@
# Script to organize texture files by checksum with two-level duplicate detection
# Pass 1: Intra-blendfile duplicates → [blendfile]\common
# Pass 2: Inter-blendfile duplicates → \textures\common
# Usage: .\organize_textures.ps1
# Prompt user for texture folder path
$textureFolderPath = Read-Host "Enter texture folder path"
# Validate the input path
if ([string]::IsNullOrWhiteSpace($textureFolderPath)) {
Write-Host "Error: No path provided." -ForegroundColor Red
exit
}
if (-not (Test-Path -Path $textureFolderPath -PathType Container)) {
Write-Host "Error: Path does not exist or is not a directory: $textureFolderPath" -ForegroundColor Red
exit
}
# Resolve the full path
$textureFolderPath = (Resolve-Path $textureFolderPath).ProviderPath
Write-Host "Processing texture folder: $textureFolderPath" -ForegroundColor Cyan
# Add required .NET assemblies for image processing (for FlatColors standardization)
Add-Type -AssemblyName System.Drawing
# Function to calculate checksums for files
function Get-FilesWithChecksums {
param(
[array]$Files
)
$throttleLimit = [Math]::Max(1, [Environment]::ProcessorCount)
$parallelScriptBlock = {
try {
$hash = Get-FileHash -Path $_.FullName -Algorithm SHA256
[PSCustomObject]@{
File = $_
Hash = $hash.Hash
}
} catch {
Write-Warning "Failed to calculate checksum for: $($_.FullName) - $($_.Exception.Message)"
$null
}
}
return $Files | ForEach-Object -Parallel $parallelScriptBlock -ThrottleLimit $throttleLimit | Where-Object { $null -ne $_ }
}
# Function to move files to common folder
function Move-FilesToCommon {
param(
[array]$Files,
[string]$CommonPath,
[string]$DuplicatesPath,
[hashtable]$FilesInCommon
)
$movedCount = 0
$duplicateCount = 0
foreach ($fileObj in $Files) {
$fileName = $fileObj.Name
$destinationPath = Join-Path -Path $CommonPath -ChildPath $fileName
# Handle name conflicts
if ($FilesInCommon.ContainsKey($fileName)) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
$extension = [System.IO.Path]::GetExtension($fileName)
$counter = 1
do {
$newFileName = "${baseName}_${counter}${extension}"
$destinationPath = Join-Path -Path $CommonPath -ChildPath $newFileName
$counter++
} while ($FilesInCommon.ContainsKey($newFileName) -or (Test-Path -Path $destinationPath))
$fileName = $newFileName
}
try {
Move-Item -Path $fileObj.FullName -Destination $destinationPath -Force
$FilesInCommon[$fileName] = $true
$movedCount++
} catch {
Write-Warning "Failed to move file: $($fileObj.FullName) - $($_.Exception.Message)"
}
}
return @{
MovedCount = $movedCount
DuplicateCount = $duplicateCount
}
}
# Function to extract suffix after blendfile prefix (e.g., "Demarco_Std_Teeth_ao.jpg" -> "Std_Teeth_ao.jpg")
# Only strips prefixes that are in the $ValidPrefixes list
function Get-FileNameWithoutPrefix {
param(
[string]$FileName,
[string[]]$ValidPrefixes
)
# Validate input
if ([string]::IsNullOrWhiteSpace($FileName)) {
return $FileName
}
# If no valid prefixes provided, return original filename
if ($null -eq $ValidPrefixes -or $ValidPrefixes.Count -eq 0) {
return $FileName
}
# Use Split instead of regex for robustness (avoids $matches variable issues in loops)
# Split by the first underscore only (limit to 2 parts)
$parts = $FileName.Split('_', 2)
if ($parts.Length -eq 2) {
$prefix = $parts[0]
$suffix = $parts[1]
# Only strip if the prefix is in the valid prefixes list
if ($ValidPrefixes -contains $prefix -and -not [string]::IsNullOrWhiteSpace($suffix)) {
return $suffix
}
}
# No valid prefix found or suffix is empty, return original filename
return $FileName
}
# Function to process duplicate group
function Process-DuplicateGroup {
param(
[array]$Files,
[string]$CommonPath,
[hashtable]$FilesInCommon,
[switch]$StripPrefix,
[string[]]$ValidPrefixes = @(),
[System.Collections.ArrayList]$MoveLog = $null
)
$movedCount = 0
$duplicateCount = 0
if ($Files.Count -eq 1) {
# Single file - leave in place (will be processed in Pass 2)
return @{
MovedCount = 0
DuplicateCount = 0
}
}
# Validate files array
if ($null -eq $Files -or $Files.Count -eq 0) {
return @{
MovedCount = 0
DuplicateCount = 0
}
}
# Multiple files with same checksum (duplicates)
# Determine the filename to use
$firstFile = $Files[0].File
if ($null -eq $firstFile -or [string]::IsNullOrWhiteSpace($firstFile.Name)) {
Write-Warning "Invalid file object in duplicate group"
return @{
MovedCount = 0
DuplicateCount = 0
}
}
$fileName = $firstFile.Name
# If StripPrefix is enabled, always strip prefix from the filename
if ($StripPrefix) {
# Always try to strip prefix - if file has a prefix, it will be removed
$strippedName = Get-FileNameWithoutPrefix -FileName $firstFile.Name -ValidPrefixes $ValidPrefixes
# Use stripped name if it's different from original (prefix was removed)
# Otherwise keep original (file had no prefix to strip)
if ($strippedName -ne $firstFile.Name) {
$fileName = $strippedName
}
# If we have multiple files with same checksum, try to find common suffix
# This handles cases where files from different blendfiles share the same texture
if ($Files.Count -gt 1) {
$allSuffixes = @()
foreach ($fileObj in $Files) {
# $fileObj is a PSCustomObject with .File property, so access .File.Name
$suffix = Get-FileNameWithoutPrefix -FileName $fileObj.File.Name -ValidPrefixes $ValidPrefixes
$allSuffixes += $suffix
}
# If all files strip to the same suffix, use that
$uniqueSuffixes = @($allSuffixes | Select-Object -Unique)
if ($uniqueSuffixes.Count -eq 1 -and $uniqueSuffixes[0] -ne $firstFile.Name) {
$fileName = [string]$uniqueSuffixes[0]
}
}
}
# Ensure fileName is never null or empty
if ([string]::IsNullOrWhiteSpace($fileName)) {
$fileName = $firstFile.Name
}
$destinationPath = Join-Path -Path $CommonPath -ChildPath $fileName
# Handle name conflicts for the first file
if ($FilesInCommon.ContainsKey($fileName)) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
$extension = [System.IO.Path]::GetExtension($fileName)
$counter = 1
do {
$newFileName = "${baseName}_${counter}${extension}"
$destinationPath = Join-Path -Path $CommonPath -ChildPath $newFileName
$counter++
} while ($FilesInCommon.ContainsKey($newFileName) -or (Test-Path -Path $destinationPath))
$fileName = $newFileName
}
try {
Move-Item -Path $firstFile.FullName -Destination $destinationPath -Force
$FilesInCommon[$fileName] = $true
$movedCount++
# Log the move
if ($null -ne $MoveLog) {
$normalizedOriginal = [System.IO.Path]::GetFullPath($firstFile.FullName)
$normalizedNew = [System.IO.Path]::GetFullPath($destinationPath)
$null = $MoveLog.Add([PSCustomObject]@{
OriginalPath = $normalizedOriginal
NewPath = $normalizedNew
Type = "moved"
})
}
} catch {
Write-Warning "Failed to move first duplicate file: $($firstFile.FullName) - $($_.Exception.Message)"
}
# Delete remaining duplicate files (they're replaced by the common file)
for ($i = 1; $i -lt $Files.Count; $i++) {
$fileObj = $Files[$i].File
try {
$originalPath = $fileObj.FullName
Remove-Item -Path $originalPath -Force
$duplicateCount++
# Log the deletion (replaced by the moved file)
if ($null -ne $MoveLog) {
$normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath)
$normalizedReplacement = [System.IO.Path]::GetFullPath($destinationPath)
$null = $MoveLog.Add([PSCustomObject]@{
OriginalPath = $normalizedOriginal
ReplacedBy = $normalizedReplacement
Type = "deleted"
})
}
} catch {
Write-Warning "Failed to delete duplicate file: $($fileObj.FullName) - $($_.Exception.Message)"
}
}
return @{
MovedCount = $movedCount
DuplicateCount = $duplicateCount
}
}
# Function to extract and normalize hex color code from filename (for FlatColors)
# For TGA files, preserves 8-digit codes (RGBA). For others, uses 6-digit codes (RGB).
function Get-NormalizedColorCode {
param(
[string]$FileName,
[string]$Extension = ""
)
# Remove extension if not provided
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($FileName)
if ([string]::IsNullOrWhiteSpace($Extension)) {
$Extension = [System.IO.Path]::GetExtension($FileName)
}
# For TGA files, preserve 8-digit codes (RGBA with alpha channel)
if ($Extension -eq ".tga") {
# Match 8-digit hex: #RRGGBBAA
if ($baseName -match '^(#?)([0-9A-Fa-f]{8})') {
$hexCode = $Matches[2].ToUpper()
return "#$hexCode"
}
# Fallback to 6-digit if 8-digit not found
if ($baseName -match '^(#?)([0-9A-Fa-f]{6})') {
$hexCode = $Matches[2].ToUpper()
return "#$hexCode"
}
} else {
# For non-TGA files, use 6-digit codes (RGB)
if ($baseName -match '^(#?)([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?') {
$hexCode = $Matches[2].ToUpper()
# Return normalized format: #RRGGBB (always with #, always uppercase, no suffix)
return "#$hexCode"
}
}
return $null
}
# Function to convert and resize image to 16x16 JPG (for FlatColors)
function Convert-To16x16Jpg {
param(
[string]$SourcePath,
[string]$DestinationPath
)
try {
$image = [System.Drawing.Image]::FromFile($SourcePath)
# Create 16x16 bitmap
$bitmap = New-Object System.Drawing.Bitmap(16, 16)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
# Draw resized image
$graphics.DrawImage($image, 0, 0, 16, 16)
# Get JPEG codec
$jpegCodec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq "image/jpeg" }
$encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
$encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, 95)
# Save as JPG
$bitmap.Save($DestinationPath, $jpegCodec, $encoderParams)
# Cleanup
$graphics.Dispose()
$bitmap.Dispose()
$image.Dispose()
$encoderParams.Dispose()
return $true
} catch {
Write-Warning "Failed to convert image: $SourcePath - $($_.Exception.Message)"
return $false
}
}
# Function to check if image is 16x16 (for FlatColors)
function Test-ImageSize {
param([string]$ImagePath)
try {
$image = [System.Drawing.Image]::FromFile($ImagePath)
$is16x16 = ($image.Width -eq 16 -and $image.Height -eq 16)
$image.Dispose()
return $is16x16
} catch {
return $false
}
}
# ============================================================================
# PASS 1: Intra-Blendfile Processing
# ============================================================================
Write-Host ""
Write-Host "=== PASS 1: Intra-Blendfile Processing ===" -ForegroundColor Cyan
# Get all direct subdirectories of texture folder (blendfile folders)
$blendfileFolders = Get-ChildItem -Path $textureFolderPath -Directory | Where-Object { $_.Name -ne "common" }
if ($null -eq $blendfileFolders -or $blendfileFolders.Count -eq 0) {
Write-Host "No blendfile folders found. Skipping Pass 1." -ForegroundColor Yellow
} else {
Write-Host "Found $($blendfileFolders.Count) blendfile folder(s) to process." -ForegroundColor Green
$totalPass1Moved = 0
$totalPass1Duplicates = 0
$pass1MoveLog = [System.Collections.ArrayList]::new()
foreach ($blendfileFolder in $blendfileFolders) {
Write-Host ""
Write-Host "Processing blendfile: $($blendfileFolder.Name)" -ForegroundColor Yellow
# Get all files in this blendfile folder, excluding \common folders
$blendfileFiles = Get-ChildItem -Path $blendfileFolder.FullName -Recurse -File | Where-Object { $_.FullName -notlike "*\common\*" }
if ($null -eq $blendfileFiles -or $blendfileFiles.Count -eq 0) {
Write-Host " No files found in this blendfile folder." -ForegroundColor Gray
continue
}
Write-Host " Found $($blendfileFiles.Count) files." -ForegroundColor Gray
# Calculate checksums
Write-Host " Calculating checksums..." -ForegroundColor Gray
$filesWithChecksums = Get-FilesWithChecksums -Files $blendfileFiles
# Group by checksum
$groupedByChecksum = $filesWithChecksums | Group-Object -Property Hash
Write-Host " Found $($groupedByChecksum.Count) unique checksums." -ForegroundColor Gray
# Create [blendfile]\common directory
$blendfileCommonPath = Join-Path -Path $blendfileFolder.FullName -ChildPath "common"
if (-not (Test-Path -Path $blendfileCommonPath -PathType Container)) {
New-Item -ItemType Directory -Path $blendfileCommonPath | Out-Null
}
# Track filenames already in [blendfile]\common
$filesInBlendfileCommon = @{}
# Process each checksum group
$blendfileMoved = 0
$blendfileDuplicates = 0
foreach ($group in $groupedByChecksum) {
$result = Process-DuplicateGroup -Files $group.Group -CommonPath $blendfileCommonPath -FilesInCommon $filesInBlendfileCommon -MoveLog $pass1MoveLog
$blendfileMoved += $result.MovedCount
$blendfileDuplicates += $result.DuplicateCount
}
Write-Host " Moved $blendfileMoved file(s) to \common, deleted $blendfileDuplicates duplicate(s)" -ForegroundColor Green
$totalPass1Moved += $blendfileMoved
$totalPass1Duplicates += $blendfileDuplicates
}
Write-Host ""
Write-Host "Pass 1 complete: $totalPass1Moved file(s) moved, $totalPass1Duplicates duplicate(s) deleted" -ForegroundColor Green
}
# ============================================================================
# PASS 2: Inter-Blendfile Processing
# ============================================================================
Write-Host ""
Write-Host "=== PASS 2: Inter-Blendfile Processing ===" -ForegroundColor Cyan
# Build list of valid blendfile prefixes from folder names
$validBlendfilePrefixes = @()
$blendfileFoldersForPrefixes = Get-ChildItem -Path $textureFolderPath -Directory | Where-Object { $_.Name -ne "common" }
if ($null -ne $blendfileFoldersForPrefixes) {
$validBlendfilePrefixes = $blendfileFoldersForPrefixes | ForEach-Object { $_.Name }
Write-Host "Valid blendfile prefixes: $($validBlendfilePrefixes -join ', ')" -ForegroundColor Gray
}
# Collect FlatColors files separately (including those in \common\FlatColors folders, but not from root \common\FlatColors)
Write-Host "Collecting FlatColors files..." -ForegroundColor Yellow
$rootCommonFlatColorsPath = Join-Path -Path $textureFolderPath -ChildPath "common\FlatColors"
$allFlatColorsFiles = Get-ChildItem -Path $textureFolderPath -Recurse -File | Where-Object {
($_.FullName -like "*\FlatColors\*" -or $_.Name -like "*FlatColors*") -and
$_.FullName -notlike "$([regex]::Escape($rootCommonFlatColorsPath))\*"
}
# Get all remaining files (excluding all \common folders, but we'll handle FlatColors separately)
Write-Host "Collecting remaining files..." -ForegroundColor Yellow
$remainingFiles = Get-ChildItem -Path $textureFolderPath -Recurse -File | Where-Object { $_.FullName -notlike "*\common\*" }
if ($null -eq $remainingFiles -or $remainingFiles.Count -eq 0) {
Write-Host "No remaining files found to process." -ForegroundColor Yellow
} else {
Write-Host "Found $($remainingFiles.Count) remaining files to process." -ForegroundColor Green
# Calculate checksums
Write-Host "Calculating checksums using parallel processing (this may take a while)..." -ForegroundColor Yellow
$filesWithChecksums = Get-FilesWithChecksums -Files $remainingFiles
Write-Host "Checksum calculation complete." -ForegroundColor Green
# Separate FlatColors files from other files before processing
Write-Host "Identifying FlatColors files..." -ForegroundColor Yellow
$flatColorsFiles = $allFlatColorsFiles | Where-Object { (Test-Path -Path $_.FullName) }
$nonFlatColorsFiles = $remainingFiles | Where-Object { $_.Name -notlike "*FlatColors*" }
Write-Host "Found $($flatColorsFiles.Count) FlatColors file(s) (including from \common\FlatColors folders), $($nonFlatColorsFiles.Count) other file(s)" -ForegroundColor Gray
# Group non-FlatColors files by checksum
Write-Host "Grouping files by checksum..." -ForegroundColor Yellow
$nonFlatColorsWithChecksums = $filesWithChecksums | Where-Object { $_.File.Name -notlike "*FlatColors*" }
$groupedByChecksum = $nonFlatColorsWithChecksums | Group-Object -Property Hash
Write-Host "Found $($groupedByChecksum.Count) unique checksums." -ForegroundColor Green
# Create \textures\common directory
$rootCommonPath = Join-Path -Path $textureFolderPath -ChildPath "common"
if (-not (Test-Path -Path $rootCommonPath -PathType Container)) {
New-Item -ItemType Directory -Path $rootCommonPath | Out-Null
Write-Host "Created directory: $rootCommonPath" -ForegroundColor Green
}
# Track filenames already in \textures\common
$filesInRootCommon = @{}
# Process each checksum group (excluding FlatColors)
Write-Host "Moving files to \common and deleting duplicates..." -ForegroundColor Yellow
$pass2Moved = 0
$pass2Duplicates = 0
$pass2MoveLog = [System.Collections.ArrayList]::new()
foreach ($group in $groupedByChecksum) {
$files = $group.Group
if ($files.Count -eq 1) {
# Single file - leave in place (unique file)
continue
}
# Multiple files with same checksum (duplicates across blendfiles)
# Use StripPrefix to remove blendfile prefixes (e.g., "Demarco_", "Chan_") when all files share the same suffix
$result = Process-DuplicateGroup -Files $files -CommonPath $rootCommonPath -FilesInCommon $filesInRootCommon -StripPrefix -ValidPrefixes $validBlendfilePrefixes -MoveLog $pass2MoveLog
$pass2Moved += $result.MovedCount
$pass2Duplicates += $result.DuplicateCount
}
# Process FlatColors files: standardize to 16x16 JPG (except TGA), merge duplicates by color code
Write-Host "Processing FlatColors files (standardizing and merging duplicates)..." -ForegroundColor Yellow
if ($null -ne $flatColorsFiles -and $flatColorsFiles.Count -gt 0) {
Write-Host "Found $($flatColorsFiles.Count) FlatColors file(s) to process" -ForegroundColor Gray
# Group files by normalized color code (instead of checksum)
Write-Host "Grouping FlatColors files by color code..." -ForegroundColor Yellow
$filesByColor = @{}
foreach ($file in $flatColorsFiles) {
if (-not (Test-Path -Path $file.FullName)) {
continue
}
# Use composite key: color code + extension to separate TGA (8-digit) from JPG (6-digit)
$colorCode = Get-NormalizedColorCode -FileName $file.Name -Extension $file.Extension
if ($null -eq $colorCode) {
Write-Warning "Could not extract color code from: $($file.Name)"
continue
}
# Create composite key: colorCode_extension to keep TGA and JPG separate
$groupKey = "$colorCode$($file.Extension)"
if (-not $filesByColor.ContainsKey($groupKey)) {
$filesByColor[$groupKey] = @()
}
$filesByColor[$groupKey] += $file
}
Write-Host "Found $($filesByColor.Count) unique color code(s)" -ForegroundColor Gray
# Create \textures\common\FlatColors directory
$flatColorsPath = Join-Path -Path $rootCommonPath -ChildPath "FlatColors"
if (-not (Test-Path -Path $flatColorsPath -PathType Container)) {
New-Item -ItemType Directory -Path $flatColorsPath | Out-Null
Write-Host "Created directory: $flatColorsPath" -ForegroundColor Green
}
# Track filenames already in \common\FlatColors
$filesInFlatColors = @{}
$flatColorsMoved = 0
$flatColorsDuplicates = 0
$flatColorsConverted = 0
$flatColorsResized = 0
$flatColorsMoveLog = [System.Collections.ArrayList]::new()
foreach ($groupKey in $filesByColor.Keys) {
$files = $filesByColor[$groupKey]
# Extract color code and extension from group key
# Group key format: #RRGGBB.ext or #RRGGBBAA.ext
if ($groupKey -match '^(#[0-9A-F]{6,8})(\.[^.]+)$') {
$colorCode = $Matches[1]
$targetExtension = $Matches[2]
} else {
Write-Warning "Invalid group key format: $groupKey"
continue
}
$targetFileName = "$colorCode$targetExtension"
# Find the best file to keep
$fileToKeep = $null
# First, check if there's already a correct file in target location
$existingTarget = Join-Path -Path $flatColorsPath -ChildPath $targetFileName
if (Test-Path -Path $existingTarget) {
$existingFile = Get-Item -Path $existingTarget
# Check if it's already correct
if ($existingFile.Extension -eq ".tga") {
$fileToKeep = $existingFile
} elseif ($existingFile.Extension -eq ".jpg" -and (Test-ImageSize -ImagePath $existingFile.FullName)) {
$fileToKeep = $existingFile
}
}
# If no existing correct file, find best source file
if ($null -eq $fileToKeep) {
# Prefer TGA file first (preserves transparency)
$tgaFile = $files | Where-Object {
$_.Extension -eq ".tga" -and
(Test-Path -Path $_.FullName)
} | Select-Object -First 1
if ($tgaFile) {
$fileToKeep = $tgaFile
} else {
# If no TGA, prefer existing JPG file
$jpgFile = $files | Where-Object {
($_.Extension -eq ".jpg" -or $_.Extension -eq ".jpeg") -and
(Test-Path -Path $_.FullName)
} | Select-Object -First 1
if ($jpgFile) {
$fileToKeep = $jpgFile
} else {
# Otherwise, use the first available file
$fileToKeep = $files | Where-Object { Test-Path -Path $_.FullName } | Select-Object -First 1
}
}
}
if ($null -eq $fileToKeep) {
Write-Warning " ${colorCode}: No valid file found to process"
continue
}
$targetPath = Join-Path -Path $flatColorsPath -ChildPath $targetFileName
# If target is TGA, preserve as-is (don't convert)
if ($targetExtension -eq ".tga") {
if ($fileToKeep.FullName -ne $targetPath) {
# Move TGA to target location
try {
if (Test-Path -Path $targetPath) {
Remove-Item -Path $targetPath -Force
}
$originalPath = $fileToKeep.FullName
Move-Item -Path $originalPath -Destination $targetPath -Force
$filesInFlatColors[$targetFileName] = $true
$flatColorsMoved++
# Log the move
$normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath)
$normalizedNew = [System.IO.Path]::GetFullPath($targetPath)
$null = $flatColorsMoveLog.Add([PSCustomObject]@{
OriginalPath = $normalizedOriginal
NewPath = $normalizedNew
Type = "moved"
})
} catch {
Write-Warning " ${colorCode}: Failed to move TGA: $($_.Exception.Message)"
}
}
} else {
# Target is JPG - convert/resize if needed
$needsConversion = $false
$needsResize = $false
if ($fileToKeep.Extension -ne ".jpg" -and $fileToKeep.Extension -ne ".jpeg") {
$needsConversion = $true
}
if (-not (Test-ImageSize -ImagePath $fileToKeep.FullName)) {
$needsResize = $true
}
# Check if target already exists and is correct
if ((Test-Path -Path $targetPath) -and
(Get-Item -Path $targetPath).Extension -eq ".jpg" -and
(Test-ImageSize -ImagePath $targetPath)) {
# Target already exists and is correct, skip
} elseif ($needsConversion -or $needsResize -or $fileToKeep.FullName -ne $targetPath) {
# Convert/resize to target
if (Convert-To16x16Jpg -SourcePath $fileToKeep.FullName -DestinationPath $targetPath) {
if ($needsConversion -and $needsResize) {
$flatColorsConverted++
$flatColorsResized++
} elseif ($needsConversion) {
$flatColorsConverted++
} elseif ($needsResize) {
$flatColorsResized++
} else {
$flatColorsMoved++
}
$filesInFlatColors[$targetFileName] = $true
# Log the conversion/move
$normalizedOriginal = [System.IO.Path]::GetFullPath($fileToKeep.FullName)
$normalizedNew = [System.IO.Path]::GetFullPath($targetPath)
$null = $flatColorsMoveLog.Add([PSCustomObject]@{
OriginalPath = $normalizedOriginal
NewPath = $normalizedNew
Type = "moved"
})
} else {
Write-Warning " ${colorCode}: Failed to convert/resize"
continue
}
} else {
# File is already correct format and size
if ($fileToKeep.FullName -ne $targetPath) {
try {
if (Test-Path -Path $targetPath) {
Remove-Item -Path $targetPath -Force
}
$originalPath = $fileToKeep.FullName
Move-Item -Path $originalPath -Destination $targetPath -Force
$filesInFlatColors[$targetFileName] = $true
$flatColorsMoved++
# Log the move
$normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath)
$normalizedNew = [System.IO.Path]::GetFullPath($targetPath)
$null = $flatColorsMoveLog.Add([PSCustomObject]@{
OriginalPath = $normalizedOriginal
NewPath = $normalizedNew
Type = "moved"
})
} catch {
Write-Warning " ${colorCode}: Failed to move: $($_.Exception.Message)"
}
}
}
}
# Delete duplicate files (all files in the group except the target)
foreach ($file in $files) {
if ($file.FullName -ne $targetPath -and (Test-Path -Path $file.FullName)) {
try {
$originalPath = $file.FullName
Remove-Item -Path $originalPath -Force
$flatColorsDuplicates++
# Log the deletion
$normalizedOriginal = [System.IO.Path]::GetFullPath($originalPath)
$normalizedReplacement = [System.IO.Path]::GetFullPath($targetPath)
$null = $flatColorsMoveLog.Add([PSCustomObject]@{
OriginalPath = $normalizedOriginal
ReplacedBy = $normalizedReplacement
Type = "deleted"
})
} catch {
Write-Warning " Failed to delete duplicate: $($file.FullName) - $($_.Exception.Message)"
}
}
}
}
Write-Host "Processed FlatColors: $flatColorsMoved moved, $flatColorsConverted converted, $flatColorsResized resized, $flatColorsDuplicates duplicate(s) deleted" -ForegroundColor Green
} else {
Write-Host "No FlatColors files found to process." -ForegroundColor Gray
}
Write-Host ""
Write-Host "Pass 2 complete: $pass2Moved file(s) moved to \common, $pass2Duplicates duplicate(s) deleted" -ForegroundColor Green
}
# ============================================================================
# Save Move Log and Find Blend Files
# ============================================================================
Write-Host ""
Write-Host "Saving move log and finding blend files..." -ForegroundColor Yellow
# Combine all move logs
$allMoves = @()
if ($null -ne $pass1MoveLog -and $pass1MoveLog.Count -gt 0) {
$allMoves += $pass1MoveLog
}
if ($null -ne $pass2MoveLog -and $pass2MoveLog.Count -gt 0) {
$allMoves += $pass2MoveLog
}
if ($null -ne $flatColorsMoveLog -and $flatColorsMoveLog.Count -gt 0) {
$allMoves += $flatColorsMoveLog
}
# Get parent directory of texture folder
$blendFileParentDir = Split-Path -Path $textureFolderPath -Parent
# Find matching blend files
# Match logic: blendfile folder name (e.g., "Beth") should appear in blend file name (e.g., "AM_Beth_v3.2.blend")
$blendFileMappings = @()
if ($null -ne $blendfileFolders -and $blendfileFolders.Count -gt 0) {
# Get blendfile folder names
$blendfileFolderNames = $blendfileFolders | ForEach-Object { $_.Name }
# Find all .blend files in parent directory
$blendFiles = Get-ChildItem -Path $blendFileParentDir -Filter "*.blend" -File -ErrorAction SilentlyContinue
if ($null -ne $blendFiles -and $blendFiles.Count -gt 0) {
foreach ($blendFile in $blendFiles) {
$blendFileName = $blendFile.BaseName
# Check if any blendfile folder name appears in the blend file name
foreach ($folderName in $blendfileFolderNames) {
# Match folder name when surrounded by underscores, dots, hyphens, or at start/end
# This avoids partial matches (e.g., "Beth" in "Bethany") while allowing "Beth" in "AM_Beth_v3.2"
$escapedFolderName = [regex]::Escape($folderName)
if ($blendFileName -match "(^|[._-])$escapedFolderName([._-]|$)") {
$blendFileMappings += [PSCustomObject]@{
BlendFile = [System.IO.Path]::GetFullPath($blendFile.FullName)
BlendfileFolder = $folderName
}
Write-Host "Found matching blend file: $($blendFile.Name) -> $folderName" -ForegroundColor Gray
break # Only match once per blend file
}
}
}
}
}
# Create move log object
$moveLogData = [PSCustomObject]@{
TextureFolderPath = $textureFolderPath
Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
TotalMoves = ($allMoves | Where-Object { $_.Type -eq "moved" }).Count
TotalDeletes = ($allMoves | Where-Object { $_.Type -eq "deleted" }).Count
Moves = $allMoves
BlendFileMappings = $blendFileMappings
}
# Save to JSON file
$moveLogPath = Join-Path -Path $blendFileParentDir -ChildPath "texture_moves.json"
try {
$moveLogData | ConvertTo-Json -Depth 10 | Set-Content -Path $moveLogPath -Encoding UTF8
Write-Host "Move log saved to: $moveLogPath" -ForegroundColor Green
Write-Host " Total moves: $($moveLogData.TotalMoves), Total deletes: $($moveLogData.TotalDeletes)" -ForegroundColor Gray
Write-Host " Blend files found: $($blendFileMappings.Count)" -ForegroundColor Gray
} catch {
Write-Warning "Failed to save move log: $($_.Exception.Message)"
}
# ============================================================================
# Remap Texture Paths in Blend Files
# ============================================================================
if ($blendFileMappings.Count -gt 0 -and (Test-Path -Path $moveLogPath)) {
Write-Host ""
Write-Host "Remapping texture paths in blend files..." -ForegroundColor Yellow
# Find Blender executable from PATH
$blenderExe = $null
$blenderInPath = Get-Command blender -ErrorAction SilentlyContinue
if ($null -ne $blenderInPath) {
$blenderExe = $blenderInPath.Source
}
if ($null -eq $blenderExe) {
Write-Warning "Blender executable not found. Skipping texture path remapping."
Write-Host " Please install Blender or add it to your PATH." -ForegroundColor Yellow
} else {
Write-Host "Found Blender: $blenderExe" -ForegroundColor Gray
# Get the remap script path (should be in the same directory as this script)
$scriptDir = Split-Path -Path $MyInvocation.MyCommand.Path -Parent
$remapScriptPath = Join-Path -Path $scriptDir -ChildPath "remap_texture_paths.py"
if (-not (Test-Path -Path $remapScriptPath)) {
Write-Warning "Remap script not found: $remapScriptPath"
Write-Warning "Skipping texture path remapping."
} else {
$processedCount = 0
$failedCount = 0
Write-Host "Processing $($blendFileMappings.Count) blend file(s)..." -ForegroundColor Gray
foreach ($mapping in $blendFileMappings) {
$blendFilePath = $mapping.BlendFile
$blendFileName = Split-Path -Path $blendFilePath -Leaf
Write-Host " Processing: $blendFileName" -ForegroundColor Cyan
# Check if blend file exists
if (-not (Test-Path -Path $blendFilePath)) {
Write-Warning " Blend file not found: $blendFilePath"
$failedCount++
continue
}
# Run Blender with the remapping script (one file at a time)
# Delete old output file
Remove-Item -Path "blender_output.txt" -ErrorAction SilentlyContinue
try {
# Run Blender and capture output while displaying it (like compress_blend_files.bat)
# Build command with proper argument array
$blenderArgsArray = @(
"--background",
"--factory-startup",
"--python", $remapScriptPath,
"--",
$blendFilePath,
$moveLogPath
)
# Execute and pipe output through Tee-Object to display and save
& $blenderExe $blenderArgsArray 2>&1 | Tee-Object -FilePath "blender_output.txt" | ForEach-Object {
Write-Host " $_"
}
# Check exit code
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host " Successfully remapped texture paths" -ForegroundColor Green
$processedCount++
} else {
Write-Warning " Failed to remap texture paths (exit code: $exitCode)"
$failedCount++
}
} catch {
Write-Warning " Exception while processing blend file: $($_.Exception.Message)"
$failedCount++
}
}
# Clean up temporary output files
Remove-Item -Path "blender_output.txt" -ErrorAction SilentlyContinue
Remove-Item -Path "blender_error.txt" -ErrorAction SilentlyContinue
Write-Host ""
if ($processedCount -gt 0 -or $failedCount -gt 0) {
Write-Host "Blend file remapping complete: $processedCount succeeded, $failedCount failed" -ForegroundColor $(if ($failedCount -eq 0) { "Green" } else { "Yellow" })
}
}
}
} else {
if ($blendFileMappings.Count -eq 0) {
Write-Host ""
Write-Host "No matching blend files found. Skipping texture path remapping." -ForegroundColor Gray
}
}
# Function to remove empty folders recursively
function Remove-EmptyFolders {
param(
[string]$Path,
[string]$RootPath
)
$removedCount = 0
# Get all subdirectories
$subdirs = Get-ChildItem -Path $Path -Directory -ErrorAction SilentlyContinue
if ($null -ne $subdirs) {
foreach ($subdir in $subdirs) {
# Recursively process subdirectories first
$removedCount += Remove-EmptyFolders -Path $subdir.FullName -RootPath $RootPath
# Check if this directory is now empty (after processing subdirectories)
$items = Get-ChildItem -Path $subdir.FullName -ErrorAction SilentlyContinue
if ($null -eq $items -or $items.Count -eq 0) {
# Don't remove the root path
if ($subdir.FullName -ne $RootPath) {
try {
Remove-Item -Path $subdir.FullName -Force -ErrorAction Stop
$removedCount++
} catch {
Write-Warning "Failed to remove empty folder: $($subdir.FullName) - $($_.Exception.Message)"
}
}
}
}
}
return $removedCount
}
# Remove empty folders
Write-Host ""
Write-Host "Removing empty folders..." -ForegroundColor Yellow
$emptyFoldersRemoved = Remove-EmptyFolders -Path $textureFolderPath -RootPath $textureFolderPath
if ($emptyFoldersRemoved -gt 0) {
Write-Host "Removed $emptyFoldersRemoved empty folder(s)" -ForegroundColor Green
} else {
Write-Host "No empty folders found to remove." -ForegroundColor Gray
}
Write-Host ""
Write-Host "File organization complete!" -ForegroundColor Green
-8
View File
@@ -1,8 +0,0 @@
new script named UpgradeToGitProj. this is to be ran in a project structure that was pre-git.
1. appends gitignore and gitattributes, initializes git, and installs git lfs
- If already initialized, will this just error but continue?
2. Manages Renders folder:
a. creates folder, adding NewDaily and UpdateSeq scripts
b. scans the structure within blends\animations and creates a folder for each submodule, e.g. Horizontal, Shorts, Vertical, etc. If there are no submodules, it just grabs the daily_* folders.
c. For each daily_* folder, it copies the contents of each daily_*\seq\ folder into the Renders\submodule\ folder. If it's taking daily_* folders from the root, it just copies the contents of the daily_*\seq\ folder into the Renders\ folder.
+275
View File
@@ -0,0 +1,275 @@
"""
Blender script to remap texture paths after texture organization.
Usage: blender --background --factory-startup --python remap_texture_paths.py -- [blend_file_path] [texture_moves_json_path]
"""
import bpy
import json
import os
import sys
def normalize_path(path):
"""Normalize a path for comparison (handle case, separators, etc.)"""
if not path:
return ""
# Convert to absolute path and normalize
abs_path = os.path.abspath(path)
# Normalize separators (Windows uses backslash, but we'll compare case-insensitively)
normalized = os.path.normpath(abs_path).replace('\\', '/')
return normalized.lower()
def load_move_log(json_path):
"""Load the move log JSON file and build lookup dictionaries"""
try:
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Debug: Check what keys are in the data
print(f" JSON keys: {list(data.keys())}")
# PowerShell ConvertTo-Json uses PascalCase by default, so check both cases
moves = data.get('moves', []) or data.get('Moves', [])
print(f" Found {len(moves)} move entries in JSON")
if len(moves) > 0:
# Debug: Show first move entry structure
print(f" First move entry keys: {list(moves[0].keys())}")
print(f" First move entry: {moves[0]}")
# Build lookup dictionaries: normalized original path -> actual new path
original_to_new = {}
original_to_replacement = {}
for move in moves:
# Try both camelCase and PascalCase property names
move_type = move.get('type', '') or move.get('Type', '')
original = move.get('originalPath', '') or move.get('OriginalPath', '')
if not original:
continue
orig_norm = normalize_path(original)
if move_type == 'moved' or move_type == 'Moved':
new_path = move.get('newPath', '') or move.get('NewPath', '')
if new_path:
original_to_new[orig_norm] = new_path
elif move_type == 'deleted' or move_type == 'Deleted':
replacement = move.get('replacedBy', '') or move.get('ReplacedBy', '')
if replacement:
original_to_replacement[orig_norm] = replacement
print(f" Built {len(original_to_new)} move mappings and {len(original_to_replacement)} replacement mappings")
return original_to_new, original_to_replacement, data.get('textureFolderPath', '') or data.get('TextureFolderPath', '')
except Exception as e:
print(f"ERROR: Failed to load move log: {e}")
import traceback
traceback.print_exc()
return {}, {}, ""
def remap_texture_paths(blend_file_path, move_log_path):
"""Remap all texture paths in the blend file"""
print(f"\n=== REMAPPING TEXTURE PATHS ===")
print(f"Blend file: {blend_file_path}")
print(f"Move log: {move_log_path}")
# Load move log
original_to_new, original_to_replacement, texture_folder_path = load_move_log(move_log_path)
if not original_to_new and not original_to_replacement:
print("WARNING: No moves found in move log. Nothing to remap.")
return 0
print(f"Loaded {len(original_to_new)} move mappings and {len(original_to_replacement)} replacement mappings")
# Get blend file directory for relative path conversion
blend_file_dir = os.path.dirname(os.path.abspath(blend_file_path))
remapped_count = 0
not_found_count = 0
# Remap paths in image datablocks
print("\nRemapping image datablock paths...")
for image in bpy.data.images:
if not image.filepath:
continue
# Convert to absolute path for comparison
abs_path = bpy.path.abspath(image.filepath)
if not abs_path:
continue
abs_norm = normalize_path(abs_path)
was_relative = not os.path.isabs(image.filepath)
# Check if this path needs remapping
new_path = None
# First check moved files
if abs_norm in original_to_new:
new_path = original_to_new[abs_norm]
# Then check deleted files (replaced by)
elif abs_norm in original_to_replacement:
new_path = original_to_replacement[abs_norm]
if new_path:
# Convert to relative path if original was relative
if was_relative:
try:
rel_path = bpy.path.relpath(new_path, blend_file_dir)
image.filepath = rel_path
except:
# Fallback to absolute if relative conversion fails
image.filepath = new_path
else:
image.filepath = new_path
print(f" Remapped: {os.path.basename(abs_path)} -> {os.path.basename(new_path)}")
remapped_count += 1
else:
# Check if file exists - if not, it might be a broken reference
if not os.path.exists(abs_path):
not_found_count += 1
# Also check material node trees for image texture nodes
# Note: Most image texture nodes reference the image datablock (which we already remapped above),
# but we check node.filepath for completeness in case any nodes have direct file paths
print("\nRemapping material node texture paths...")
node_remapped_count = 0
for material in bpy.data.materials:
if not material.use_nodes or not material.node_tree:
continue
for node in material.node_tree.nodes:
if node.type == 'TEX_IMAGE' and hasattr(node, 'filepath') and node.filepath:
abs_path = bpy.path.abspath(node.filepath)
if abs_path:
abs_norm = normalize_path(abs_path)
new_path = None
if abs_norm in original_to_new:
new_path = original_to_new[abs_norm]
elif abs_norm in original_to_replacement:
new_path = original_to_replacement[abs_norm]
if new_path:
node.filepath = new_path
print(f" Remapped node texture: {os.path.basename(abs_path)} -> {os.path.basename(new_path)}")
node_remapped_count += 1
if node_remapped_count > 0:
remapped_count += node_remapped_count
# Convert any remaining absolute paths in image datablocks to relative
# (ignoring images from linked files)
print("\nConverting absolute image paths to relative...")
abs_to_rel_count = 0
for image in bpy.data.images:
# Skip images from linked files (library datablocks)
if image.library:
continue
if not image.filepath:
continue
# Check if path is absolute
if os.path.isabs(image.filepath):
try:
# Convert to relative path
rel_path = bpy.path.relpath(image.filepath, blend_file_dir)
if rel_path != image.filepath:
image.filepath = rel_path
print(f" Converted to relative: {os.path.basename(image.filepath)}")
abs_to_rel_count += 1
except Exception as e:
# If conversion fails, skip it (might be a network path or other issue)
pass
if abs_to_rel_count > 0:
remapped_count += abs_to_rel_count
print(f"\n=== REMAPPING SUMMARY ===")
print(f"Paths remapped: {remapped_count}")
print(f"Broken references (not found): {not_found_count}")
# Make all paths relative after remapping (this handles other datablock types)
try:
print(f"\nMaking paths relative...")
bpy.ops.file.make_paths_relative()
print(f" Converted absolute paths to relative paths")
except Exception as e:
print(f" Warning: Failed to make paths relative: {e}")
# Save the blend file
try:
print(f"\nSaving blend file...")
# Ensure we use an absolute path
abs_blend_path = os.path.abspath(blend_file_path)
print(f" Saving to: {abs_blend_path}")
bpy.ops.wm.save_mainfile(filepath=abs_blend_path)
# Verify the file was actually saved
if os.path.exists(abs_blend_path):
file_time = os.path.getmtime(abs_blend_path)
print(f"Successfully saved: {abs_blend_path}")
print(f" File modified time: {file_time}")
return remapped_count
else:
print(f"ERROR: File was not created at {abs_blend_path}")
return -1
except Exception as e:
print(f"ERROR: Failed to save blend file: {e}")
import traceback
traceback.print_exc()
return -1
def main():
# Get command line arguments (after --)
if '--' in sys.argv:
args = sys.argv[sys.argv.index('--') + 1:]
else:
args = sys.argv[1:]
if len(args) < 2:
print("ERROR: Usage: blender --background --factory-startup --python remap_texture_paths.py -- [blend_file_path] [texture_moves_json_path]")
return 1
blend_file_path = args[0]
move_log_path = args[1]
# Validate paths
if not os.path.exists(blend_file_path):
print(f"ERROR: Blend file not found: {blend_file_path}")
return 1
if not os.path.exists(move_log_path):
print(f"ERROR: Move log file not found: {move_log_path}")
return 1
# Load the blend file
try:
print(f"Loading blend file: {blend_file_path}")
bpy.ops.wm.open_mainfile(filepath=blend_file_path)
except Exception as e:
print(f"ERROR: Failed to load blend file: {e}")
return 1
# Remap texture paths
result = remap_texture_paths(blend_file_path, move_log_path)
if result >= 0:
print("\nTexture path remapping completed successfully!")
return 0
else:
print("\nTexture path remapping failed!")
return 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)
+161
View File
@@ -0,0 +1,161 @@
@echo off
setlocal enabledelayedexpansion
echo Starting fake user removal for actions...
REM Check if files were dragged onto the batch file
if "%~1"=="" (
echo.
echo Usage: Drag .blend files onto this batch file to remove fake users from actions.
echo Output will be saved to the \output folder.
echo.
pause
exit /b 1
)
REM Create output directory if it doesn't exist
if not exist "output" mkdir "output"
REM Initialize counters and tracking
set "processed=0"
set "crashed=0"
set "skipped=0"
set "deleted_similar=0"
set "threshold_percent=10"
set "updated_total=0"
REM Create a temporary Python script for processing
set "TEMP_SCRIPT=remove_action_fake_users_temp.py"
echo Creating Python script: !TEMP_SCRIPT!
(
echo import bpy
echo import os
echo import sys
echo.
echo # Create output directory if it doesn't exist
echo if not os.path.exists^("output"^):
echo os.makedirs^("output"^)
echo.
echo # Get command line arguments ^(the dragged files^)
echo blend_files = sys.argv[sys.argv.index^("--"^) + 1:] if "--" in sys.argv else []
echo.
echo print^("Found " + str^(len^(blend_files^)^) + " .blend files to process"^)
echo.
echo for blend_file in blend_files:
echo ^ if blend_file.lower^(^).endswith^(".blend"^):
echo ^ try:
echo ^ # Create output path in output folder
echo ^ filename = os.path.basename^(blend_file^)
echo ^ output_file = os.path.join^("output", filename^)
echo ^ # Check if output file already exists
echo ^ if os.path.exists^(output_file^):
echo ^ print^("SKIP_EXISTING:" + blend_file^)
echo ^ continue
echo ^ print^("PROCESSING:" + blend_file^)
echo ^ # Load the blend file
echo ^ bpy.ops.wm.open_mainfile^(filepath=blend_file^)
echo ^ # Remove fake users from all actions
echo ^ changed = 0
echo ^ for action in bpy.data.actions:
echo ^ if getattr^(action, "use_fake_user", False^):
echo ^ action.use_fake_user = False
echo ^ changed += 1
echo ^ print^("UPDATED:" + blend_file + ":" + str^(changed^)^)
echo ^ # Save to output folder
echo ^ bpy.ops.wm.save_mainfile^(filepath=output_file, compress=True^)
echo ^ print^("SUCCESS:" + blend_file + ":" + output_file^)
echo ^ except Exception as e:
echo ^ print^("CRASH:" + blend_file + ":" + str^(e^)^)
echo ^ else:
echo ^ print^("SKIP_NOT_BLEND:" + blend_file^)
echo.
echo print^("FAKE_USER_REMOVAL_COMPLETE"^)
echo bpy.ops.wm.quit_blender^(^)
) > "!TEMP_SCRIPT!"
REM Check if script was created successfully
if exist "!TEMP_SCRIPT!" (
echo Script created successfully: !TEMP_SCRIPT!
) else (
echo ERROR: Failed to create script file!
pause
exit /b 1
)
REM Run Blender to process the dragged files
echo Processing dragged .blend files...
echo Using script: !TEMP_SCRIPT!
del /f /q "blender_output.txt" 2>nul
blender --background --factory-startup --python "!TEMP_SCRIPT!" -- %* 2^>^&1 | powershell -NoProfile -Command "$input | Tee-Object -FilePath 'blender_output.txt'"
REM Clean up temporary script
echo Cleaning up temporary script...
del "!TEMP_SCRIPT!" 2>nul
REM Parse output and generate report
echo.
echo ========================================
echo ACTION FAKE USER REMOVAL REPORT
echo ========================================
echo.
REM Track updated counts per file during parse
for /f "tokens=1,2,3 delims=:" %%a in (blender_output.txt) do (
if "%%a"=="SUCCESS" (
set /a processed+=1
echo [!processed!] SUCCESS: %%b
echo Saved to: %%c
REM Compare sizes and delete if reduction < threshold
for %%f in ("%%b") do set "original_size=%%~zf"
for %%f in ("%%c") do set "compressed_size=%%~zf"
set /a savings=original_size-compressed_size
if !original_size! gtr 0 (set /a percent=(savings*100)/original_size) else (set percent=0)
echo Original: !original_size! bytes
echo Output: !compressed_size! bytes
echo Reduction: !percent!%%
if !percent! lss !threshold_percent! (
del "%%c" 2>nul
set /a deleted_similar+=1
echo Deleted: too similar to original
)
echo.
) else if "%%a"=="CRASH" (
set /a crashed+=1
echo [!crashed!] CRASHED: %%b
echo Error: %%c
echo.
) else if "%%a"=="SKIP_EXISTING" (
set /a skipped+=1
echo [!skipped!] SKIPPED ^(already exists^): %%b
echo.
) else if "%%a"=="SKIP_NOT_BLEND" (
set /a skipped+=1
echo [!skipped!] SKIPPED ^(not .blend^): %%b
echo.
) else if "%%a"=="UPDATED" (
REM Accumulate total updated actions
for /f "delims= tokens=1" %%x in ("%%c") do set "updated_in_file=%%x"
set /a updated_total+=updated_in_file
echo Updated actions: !updated_in_file!
)
)
REM Clean up output file
del /f /q "blender_output.txt" 2>nul
echo ========================================
echo SUMMARY
echo ========================================
echo Processed: !processed! files
echo Crashed: !crashed! files
echo Skipped: !skipped! files
echo Deleted ^(too similar^): !deleted_similar! files
echo Total actions updated: !updated_total!
echo Total: %* files
echo ========================================
echo.
echo Done!
pause
+375
View File
@@ -0,0 +1,375 @@
import bpy
import re
import os
def link_bsdf_materials():
"""Link all materials from the BSDF library file"""
library_path = r"R:\Creative\artsy\maya\0 ProjectStructure\1 BlenderAssets\Amazon\MATERIALS_BSDF_pallette_v1.0.blend"
if not os.path.exists(library_path):
print(f"Warning: Library file not found at {library_path}")
return []
print(f"Linking materials from: {library_path}")
# Get list of materials before linking
materials_before = set(bpy.data.materials.keys())
# Link all materials from the library file
with bpy.data.libraries.load(library_path, link=True) as (data_from, data_to):
# Link all materials
data_to.materials = data_from.materials
# Get list of newly linked materials
materials_after = set(bpy.data.materials.keys())
newly_linked = materials_after - materials_before
print(f"Linked {len(newly_linked)} materials from library")
for mat_name in sorted(newly_linked):
print(f" - {mat_name}")
return list(newly_linked)
def remap_appended_to_linked():
"""Remap any appended BSDF materials to their linked counterparts"""
print("\nChecking for appended BSDF materials to remap to linked versions...")
materials = bpy.data.materials
remapping_count = 0
# Group materials by base name (without library suffix)
material_groups = {}
for mat in materials:
# Check if it's a BSDF material (from any source)
if mat.name.startswith("BSDF_") or "BSDF_" in mat.name:
# Extract base name (remove library reference if present)
base_name = mat.name.split(".blend")[0] if ".blend" in mat.name else mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
if base_name not in material_groups:
material_groups[base_name] = []
material_groups[base_name].append(mat)
# For each group, prefer linked materials over appended ones
for base_name, mats in material_groups.items():
if len(mats) > 1:
# Sort to prefer linked materials (they have library references)
linked_mats = [m for m in mats if m.library is not None]
appended_mats = [m for m in mats if m.library is None]
if linked_mats and appended_mats:
# Use the first linked material as target
target_material = linked_mats[0]
# Remap all appended materials to the linked one
for appended_mat in appended_mats:
if appended_mat.users > 0:
print(f"Remapping appended {appended_mat.name} ({appended_mat.users} users) to linked {target_material.name}")
appended_mat.user_remap(target_material)
remapping_count += 1
# Remove the unused appended material
if appended_mat.users == 0:
print(f"Removing unused appended material: {appended_mat.name}")
bpy.data.materials.remove(appended_mat)
# Also check for any BSDF materials that might be from old paths or different files
# and try to find matching linked materials
for mat in materials:
if mat.library is None and (mat.name.startswith("BSDF_") or "BSDF_" in mat.name):
# This is an appended BSDF material - look for a linked version
base_name = mat.name.split(".blend")[0] if ".blend" in mat.name else mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
# Look for any linked material with the same base name
for linked_mat in materials:
if (linked_mat.library is not None and
linked_mat.name.startswith("BSDF_") and
(linked_mat.name == base_name or
linked_mat.name.startswith(base_name + ".") or
linked_mat.name == mat.name)):
if mat.users > 0:
print(f"Remapping old BSDF {mat.name} ({mat.users} users) to linked {linked_mat.name}")
mat.user_remap(linked_mat)
remapping_count += 1
# Remove the unused material
if mat.users == 0:
print(f"Removing unused old BSDF material: {mat.name}")
bpy.data.materials.remove(mat)
break
print(f"Remapped {remapping_count} appended/old BSDF materials to linked versions")
return remapping_count
def remap_missing_datablocks():
"""Remap materials that have missing/broken library links"""
print("\nChecking for missing datablocks to remap...")
materials = bpy.data.materials
remapping_count = 0
# Find materials with missing library links
missing_materials = []
for mat in materials:
if mat.library is not None and mat.library.filepath and not os.path.exists(bpy.path.abspath(mat.library.filepath)):
missing_materials.append(mat)
print(f"Found missing datablock: {mat.name} (from {mat.library.filepath})")
if not missing_materials:
print("No missing datablocks found.")
return 0
# For each missing material, try to find a replacement
for missing_mat in missing_materials:
base_name = missing_mat.name.split(".blend")[0] if ".blend" in missing_mat.name else missing_mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
# Look for a replacement material
replacement_found = False
# First, try to find a linked material with the same name from the current library
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name == missing_mat.name):
if missing_mat.users > 0:
print(f"Remapping missing {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
# If no exact match, try to find a BSDF material with similar name
if not replacement_found and (missing_mat.name.startswith("BSDF_") or "BSDF_" in missing_mat.name):
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name.startswith("BSDF_") and
(mat.name == base_name or
mat.name.startswith(base_name + ".") or
base_name in mat.name)):
if missing_mat.users > 0:
print(f"Remapping missing BSDF {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
# If still no replacement, try to find any valid linked material with the same base name
if not replacement_found:
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name == base_name):
if missing_mat.users > 0:
print(f"Remapping missing {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
if not replacement_found:
print(f"Warning: No replacement found for missing material {missing_mat.name}")
print(f"Remapped {remapping_count} missing datablocks to valid linked materials")
return remapping_count
def replace_cel_materials():
"""Replace all CEL materials with their BSDF counterparts using Blender's user remapping"""
# First, link BSDF materials from library
linked_materials = link_bsdf_materials()
# Then, remap any missing datablocks
missing_remaps = remap_missing_datablocks()
# Then, remap any appended BSDF materials to linked versions
appended_remaps = remap_appended_to_linked()
print(f"\n=== STARTING MATERIAL REPLACEMENT ===")
# Custom material mappings (source -> target)
custom_mappings = {
"bag BLACK (squid ink)": "BSDF_black_SQUID-INK",
"bag WHITE": "BSDF_WHITE",
"Wheel-White": "BSDF_WHITE",
"Bag Colors": "BSDF_Bag Colors",
"cardboard": "Package_Cardboard",
"blue (triton)": "BSDF_blue-2_TRITON",
"gray (snow)": "BSDF_gray-6_SNOW",
"gray (storm)": "BSDF_gray-2_STORM",
"gray (summit)": "BSDF_gray-5_SUMMIT",
"light blue (prime)": "BSDF_blue-4_PRIME",
"yellow (summer)": "BSDF_orange-5_SUMMER",
"Accessory_CEL_gray-6_SNOW": "BSDF_gray-6_SNOW",
"Accessory_CEL_SquidInk": "BSDF_black_SQUID-INK",
"FingerScanner": "BSDF_black_SQUID-INK",
"cel BLACK (squid ink)": "BSDF_black_SQUID-INK",
"cel WHITE": "BSDF_WHITE",
"gray (stone)": "BSDF_gray-3_STONE",
"green (oxygen)": "BSDF_green-3_OXYGEN",
"orange (smile)": "BSDF_orange-3_SMILE",
"orange (blaze)": "BSDF_orange-1_BLAZE"
}
# Get all materials in the scene
materials = bpy.data.materials
# Dictionary to store source -> target material mapping
material_mapping = {}
# Replace all CEL materials with their BSDF counterparts, ignoring numeric suffixes
cel_pattern = re.compile(r"^(CEL_.+?)(\.\d{3})?$")
bsdf_pattern = re.compile(r"^(BSDF_.+?)(\.\d{3})?$")
# Build a mapping from base BSDF name to BSDF material (without suffix)
bsdf_base_map = {bsdf_pattern.match(mat.name).group(1): mat for mat in materials if bsdf_pattern.match(mat.name)}
# Build a mapping from exact material names to materials
exact_material_map = {mat.name: mat for mat in materials}
# Helpers to normalize names (case-insensitive, ignore numeric suffixes and library suffix)
def normalize_base(name):
base_name = name.split(".blend")[0] if ".blend" in name else name
match = re.match(r"^(.*?)(\.\d{3})?$", base_name)
base_name = match.group(1) if match else base_name
return base_name.strip().casefold()
# Map normalized base name -> list of materials
materials_by_base = {}
for mat in materials:
base = normalize_base(mat.name)
materials_by_base.setdefault(base, []).append(mat)
# Normalize BSDF base name map for robust target lookups
bsdf_base_map_normalized = {normalize_base(base): mat for base, mat in bsdf_base_map.items()}
replacements_made = 0
missing_targets = []
# Process custom mappings first (case/suffix-insensitive)
for source_name, target_name in custom_mappings.items():
# Gather source candidates by exact name or base name (handles .001 etc.)
if source_name in exact_material_map:
source_candidates = [exact_material_map[source_name]]
else:
source_candidates = materials_by_base.get(normalize_base(source_name), [])
if not source_candidates:
print(f"Warning: Source material '{source_name}' not found")
continue
# Resolve target BSDF by exact or base name
target_material = exact_material_map.get(target_name)
if target_material is None:
target_material = bsdf_base_map_normalized.get(normalize_base(target_name))
if target_material is None:
# Final fallback: any BSDF whose base equals target base
target_base_norm = normalize_base(target_name)
for mat in materials:
m = bsdf_pattern.match(mat.name)
if m and normalize_base(m.group(1)) == target_base_norm:
target_material = mat
break
if target_material is None:
missing_targets.append(f"{source_name} -> {target_name}")
print(f"Warning: Target material '{target_name}' not found for custom mapping '{source_name}'")
continue
for src_mat in source_candidates:
material_mapping[src_mat] = target_material
print(f"Found custom mapping: {src_mat.name} -> {target_material.name}")
# Find all CEL materials and their BSDF counterparts
for mat in materials:
cel_match = cel_pattern.match(mat.name)
if cel_match:
base_cel = cel_match.group(1)
base_bsdf = base_cel.replace("CEL_", "BSDF_", 1)
if base_bsdf in bsdf_base_map:
material_mapping[mat] = bsdf_base_map[base_bsdf]
print(f"Found CEL mapping: {mat.name} -> {bsdf_base_map[base_bsdf].name}")
else:
missing_targets.append(f"{mat.name} -> {base_bsdf}")
print(f"Warning: No BSDF counterpart found for {mat.name}")
# Use Blender's built-in user remapping to replace ALL users
for source_material, target_material in material_mapping.items():
print(f"Remapping all users of {source_material.name} to {target_material.name}")
# Get user count before remapping
users_before = source_material.users
# Remap all users of the source material to the target material
# This catches ALL users including geometry node instances, drivers, etc.
source_material.user_remap(target_material)
# Get user count after remapping
users_after = source_material.users
replacements_this_material = users_before - users_after
replacements_made += replacements_this_material
print(f" Users before: {users_before}, after: {users_after}")
print(f" Remapped {replacements_this_material} users")
# Optional: Remove unused source materials after remapping
print(f"\nCleaning up unused source materials...")
removed_materials = 0
for source_material in material_mapping.keys():
if source_material.users == 0:
print(f"Removing unused material: {source_material.name}")
bpy.data.materials.remove(source_material)
removed_materials += 1
else:
print(f"Warning: {source_material.name} still has {source_material.users} users after remapping")
# Summary
print(f"\n=== REPLACEMENT SUMMARY ===")
print(f"Materials linked from library: {len(linked_materials)}")
print(f"Missing datablocks remapped: {missing_remaps}")
print(f"Appended->Linked remappings: {appended_remaps}")
print(f"Total materials mapped: {len(material_mapping)}")
print(f"Successful mappings: {len(material_mapping)}")
print(f"Total user remappings: {replacements_made}")
print(f"Source materials removed: {removed_materials}")
if missing_targets:
print(f"\nMissing target materials for:")
for mapping in missing_targets:
print(f" - {mapping}")
return len(material_mapping), replacements_made
# Run the replacement
if __name__ == "__main__":
replace_cel_materials()
print("\nRemaining CEL materials in file:")
cel_count = 0
for mat in bpy.data.materials:
if mat.name.startswith("CEL_"):
print(f" {mat.name} ({mat.users} users)")
cel_count += 1
if cel_count == 0:
print(" None - all CEL materials have been replaced!")
print("\nBSDF materials in file:")
for mat in bpy.data.materials:
if mat.name.startswith("BSDF_"):
print(f" {mat.name} ({mat.users} users)")
+62
View File
@@ -0,0 +1,62 @@
@echo off
setlocal enabledelayedexpansion
rem Get current directory
set "srcDir=%CD%"
echo Current directory: %srcDir%
rem Get parent directory
for %%I in ("%srcDir%\..") do set "parentDir=%%~fI"
echo Parent directory: %parentDir%
rem Set the _CURRENT directory
set "currentDir=%parentDir%\_CURRENT"
echo Target directory: %currentDir%
rem Create _CURRENT directory if it doesn't exist
if not exist "%currentDir%" (
echo Creating _CURRENT directory...
mkdir "%currentDir%"
)
echo.
echo Looking for files in: %srcDir%
echo.
set "fileFound=false"
for %%F in ("%srcDir%\*.*") do (
rem Skip directories and the batch file itself
if not "%%~dpnxF"=="%~f0" if not "%%~aF:~0,1"=="d" (
set "fileFound=true"
echo Found file: %%~nxF
if not exist "%currentDir%\%%~nxF" (
echo [NEW] Copying to _CURRENT...
copy "%%F" "%currentDir%\"
) else (
echo [EXISTS] Comparing dates...
for %%A in ("%%F") do set "sourceDate=%%~tA"
for %%B in ("%currentDir%\%%~nxF") do set "targetDate=%%~tB"
echo Source date: !sourceDate!
echo Target date: !targetDate!
rem Use PowerShell to properly compare file timestamps
powershell -Command "if ((Get-Item '%%F').LastWriteTime -gt (Get-Item '%currentDir%\%%~nxF').LastWriteTime) { exit 1 } else { exit 0 }"
if !errorlevel! equ 1 (
echo [UPDATED] Source is newer, copying...
copy "%%F" "%currentDir%\"
) else (
echo [SKIP] Target is up to date or newer.
)
)
echo.
)
)
if "%fileFound%"=="false" (
echo No files found in %srcDir% to process.
echo Make sure you're running this from the correct directory.
)
echo.
+48
View File
@@ -0,0 +1,48 @@
@echo off
setlocal enabledelayedexpansion
echo PushAllToCurrent - Running all 0MoveToCurrent.bat files in daily_ folders...
echo.
echo Searching for daily_ folders and 0MoveToCurrent.bat files...
echo.
REM Find all daily_ folders and check for 0MoveToCurrent.bat
set /a count=0
set /a found=0
echo Found the following daily_ folders with 0MoveToCurrent.bat:
for /d %%D in (daily_*) do (
if exist "%%D\0MoveToCurrent.bat" (
echo - %%D
set /a found+=1
)
)
if !found!==0 (
echo No daily_ folders with 0MoveToCurrent.bat found!
pause
exit /b 1
)
echo.
echo Found !found! folders with 0MoveToCurrent.bat files.
echo.
echo Starting execution...
echo.
REM Execute each 0MoveToCurrent.bat found in daily_ folders
for /d %%D in (daily_*) do (
if exist "%%D\0MoveToCurrent.bat" (
set /a count+=1
echo [!count!/!found!] Running 0MoveToCurrent.bat in %%D...
pushd "%%D"
call "0MoveToCurrent.bat"
popd
echo Completed: %%D
echo.
)
)
echo.
echo Operation completed. Successfully executed !count! batch files.
pause
+39
View File
@@ -0,0 +1,39 @@
@echo off
echo RebuildDailies - Copying 0MoveToCurrent.bat to all daily_* folders...
echo.
REM Check if source file exists
if not exist "A:\1 Amazon_Active_Projects\3 ProjectStructure\scripts_old\0MoveToCurrent.bat" (
echo ERROR: Source file "A:\1 Amazon_Active_Projects\3 ProjectStructure\scripts_old\0MoveToCurrent.bat" not found!
pause
exit /b 1
)
REM Search for daily_* folders recursively
echo Searching for daily_* folders in all subfolders...
echo.
REM Show which folders will be updated
echo Found the following daily_* folders:
for /f "delims=" %%D in ('dir /s /b /ad "*daily_*" 2^>nul') do echo - %%D
echo.
echo Copying 0MoveToCurrent.bat to each folder...
echo.
REM Copy the file to each daily_* folder found recursively
set /a count=0
for /f "delims=" %%D in ('dir /s /b /ad "*daily_*" 2^>nul') do (
echo Copying to %%D...
copy /Y "A:\1 Amazon_Active_Projects\3 ProjectStructure\scripts_old\0MoveToCurrent.bat" "%%D\" >nul
if errorlevel 1 (
echo ERROR: Failed to copy to %%D
) else (
echo SUCCESS: Copied to %%D
set /a count+=1
)
)
echo.
echo Operation completed. Successfully copied to %count% folders.
pause
+1396
View File
File diff suppressed because it is too large Load Diff