Compare commits

...

171 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
32 changed files with 5424 additions and 2927 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
@@ -1,257 +0,0 @@
<!-- Generated by SpecStory -->
# Make file input draggable (2025-08-15 18:13Z)
_**User**_
can we make this draggable so the input are the file(s) dragged onto it, and the output still goes to \output ?
---
_**Assistant**_
I'll modify the batch file to make it draggable so you can drag .blend files onto it, and it will process those specific files while still outputting to the `\output` folder.
---
<details><summary>Edit file: compress_blend_files.bat</summary>
I'm modifying the batch file to accept dragged files as input instead of processing all files in an input folder, while keeping the output to the \output directory.
**Chunk 1**
Lines added: 11, lines removed: 2
```diff
@@ -4,6 +4,15 @@
echo Starting Blender file compression...
- REM Create input and output directories if they don't exist
- if not exist "input" mkdir "input"
+ 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"
```
**Chunk 2**
Lines added: 23, lines removed: 22
```diff
@@ -14,38 +23,39 @@
echo import bpy
echo import os
- echo import glob
+ 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 input and output directories if they don't exist
- echo if not os.path.exists^("input"^):
- echo os.makedirs^("input"^)
+ echo # Create output directory if it doesn't exist
echo if not os.path.exists^("output"^):
echo os.makedirs^("output"^)
echo.
- echo # Get all .blend files in input directory
- echo blend_files = glob.glob^("input/*.blend"^)
+ 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 for blend_file in blend_files:
- 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^("Skipping " + blend_file + " ^(already exists in output^)"^)
- 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^("Successfully compressed: " + blend_file + " -^> " + output_file^)
- echo except Exception as e:
- echo print^("Failed to compress " + blend_file + ": " + str^(e^)^)
+ 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^("Skipping " + blend_file + " ^(already exists in output^)"^)
+ 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^("Successfully compressed: " + blend_file + " -^> " + output_file^)
+ echo except Exception as e:
+ echo print^("Failed to compress " + blend_file + ": " + str^(e^)^)
+ echo else:
+ echo print^("Skipping " + blend_file + " ^(not a .blend file^)"^)
echo.
echo print^("Compression complete!"^)
```
**Chunk 3**
Lines added: 3, lines removed: 3
```diff
@@ -62,8 +72,8 @@
)
- REM Run Blender once to process all files
- echo Processing all .blend files in a single Blender instance...
+ REM Run Blender to process the dragged files
+ echo Processing dragged .blend files...
echo Using script: !TEMP_SCRIPT!
- blender --background --factory-startup --python "!TEMP_SCRIPT!"
+ blender --background --factory-startup --python "!TEMP_SCRIPT!" -- %*
REM Clean up temporary script
```
</details>
---
Perfect! I've modified the batch file to make it draggable. Here are the key changes:
1. **Drag-and-drop support**: The script now checks if files were dragged onto it using `%~1` (first command line argument)
2. **Usage instructions**: If no files are dragged, it shows helpful usage information
3. **Command line processing**: The Python script now reads the dragged files from `sys.argv` instead of scanning an input folder
4. **File validation**: Only processes files with `.blend` extension
5. **Output unchanged**: Still saves compressed files to the `\output` folder
Now you can:
- Drag one or multiple `.blend` files onto the batch file
- The script will process only those specific files
- Compressed output still goes to the `\output` folder
- If you run it without dragging files, it shows usage instructions
The script is much more convenient now - just drag your `.blend` files onto it and it will handle the compression automatically!
---
_**User**_
Starting Blender file compression...
Creating Python script: compress_blend_temp.py
import bpy
import os
import sys
# Enable compression globally
bpy.context.preferences.filepaths.save_version = 0
bpy.context.preferences.filepaths.use_file_compression = True
# Create output directory if it doesn't exist
if not os.path.exists("output"):
os.makedirs("output")
# Get command line arguments (the dragged files
blend_files = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
print("Found " + str(len(blend_files)) + " .blend files to compress")
for blend_file in blend_files:
if blend_file.lower().endswith(".blend"):
try:
# Create output path in output folder
filename = os.path.basename(blend_file)
output_file = os.path.join("output", filename)
# Check if output file already exists
if os.path.exists(output_file):
print("Skipping " + blend_file + " ^(already exists in output^)")
continue
print("Processing: " + blend_file)
# Load the blend file
bpy.ops.wm.open_mainfile(filepath=blend_file)
# Save with compression to output folder
bpy.ops.wm.save_mainfile(filepath=output_file, compress=True)
print("Successfully compressed: " + blend_file + " -^> " + output_file)
except Exception as e:
print("Failed to compress " + blend_file + ": " + str(e))
else:
print("Skipping " + blend_file + " ^(not a .blend file^)")
print("Compression complete")
bpy.ops.wm.quit_blender()
ERROR: Failed to create script file
Press any key to continue . . .
---
_**Assistant**_
I see the issue! The problem is with the escape characters in the batch file when creating the Python script. The `^` characters are being interpreted incorrectly. Let me fix this by properly escaping the Python code in the batch file.
---
<details><summary>Edit file: compress_blend_files.bat</summary>
I'm fixing the escape character issues in the Python script generation by properly handling the quotes and special characters in the batch file.
**Chunk 1**
Lines added: 1, lines removed: undefined
```diff
@@ -20,4 +20,5 @@
set "TEMP_SCRIPT=compress_blend_temp.py"
echo Creating Python script: !TEMP_SCRIPT!
+
(
echo import bpy
```
**Chunk 2**
Lines added: 1, lines removed: 1
```diff
@@ -33,5 +34,5 @@
echo os.makedirs^("output"^)
echo.
- echo # Get command line arguments (the dragged files)
+ echo # Get command line arguments ^(the dragged files^)
echo blend_files = sys.argv[sys.argv.index^("--"^) + 1:] if "--" in sys.argv else []
echo.
```
</details>
---
The issue was with the escape characters in the batch file. I've fixed the Python script generation by properly escaping the parentheses and quotes. The key changes:
1. **Fixed escape characters**: Changed `^(` to `^(` and `^)` to `^)` for proper escaping
2. **Added spacing**: Added a blank line before the Python script generation for better readability
3. **Consistent escaping**: Made sure all special characters are properly escaped
Now when you drag `.blend` files onto the batch file, it should:
1. Successfully create the temporary Python script
2. Process the dragged files
3. Save compressed versions to the `\output` folder
Try dragging some `.blend` files onto it now - it should work without the "Failed to create script file" error!
---
+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 -24
View File
@@ -1,32 +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 "A:\1 Amazon_Active_Projects\3 ProjectStructure\NewDaily.bat" copy /Y "A:\1 Amazon_Active_Projects\3 ProjectStructure\NewDaily.bat" "%projectRoot%\Assets\ElevenLabs\NewDaily.bat" >nul 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 "A:\1 Amazon_Active_Projects\3 ProjectStructure\NewDaily.bat" copy /Y "A:\1 Amazon_Active_Projects\3 ProjectStructure\NewDaily.bat" "%projectRoot%\Assets\VO\NewDaily.bat" >nul 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
@@ -35,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 "A:\1 Amazon_Active_Projects\3 ProjectStructure\NewDaily.bat" copy /Y "A:\1 Amazon_Active_Projects\3 ProjectStructure\NewDaily.bat" "%projectRoot%\Renders\NewDaily.bat" >nul set "templateRoot=%STRUCT_DIR%"
if exist "A:\1 Amazon_Active_Projects\3 ProjectStructure\UpdateSequences.bat" copy /Y "A:\1 Amazon_Active_Projects\3 ProjectStructure\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 "A:\1 Amazon_Active_Projects\3 ProjectStructure\components\gitignore" copy /Y "A:\1 Amazon_Active_Projects\3 ProjectStructure\components\gitignore" "%projectRoot%\.gitignore" >nul if exist "%~dp0components\gitignore" copy /Y "%~dp0components\gitignore" "%projectRoot%\.gitignore" >nul
if exist "A:\1 Amazon_Active_Projects\3 ProjectStructure\components\gitattributes" copy /Y "A:\1 Amazon_Active_Projects\3 ProjectStructure\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
+17 -3
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"
@@ -122,12 +134,14 @@ if exist "%animDir%" (
rem Ensure submodule dir exists and place helper scripts there rem Ensure submodule dir exists and place helper scripts there
if "%DRY%"=="1" ( if "%DRY%"=="1" (
if not exist "!_subdir!" echo [DRY] mkdir "!_subdir!" if not exist "!_subdir!" echo [DRY] mkdir "!_subdir!"
if exist "%scriptDir%NewDaily.bat" echo [DRY] copy "%scriptDir%NewDaily.bat" "!_subdir!\NewDaily.bat"
if exist "%scriptDir%UpdateSequences.bat" echo [DRY] copy "%scriptDir%UpdateSequences.bat" "!_subdir!\UpdateSequences.bat" 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 ( ) else (
if not exist "!_subdir!" mkdir "!_subdir!" >nul 2>&1 if not exist "!_subdir!" mkdir "!_subdir!" >nul 2>&1
if exist "%scriptDir%NewDaily.bat" copy /Y "%scriptDir%NewDaily.bat" "!_subdir!\NewDaily.bat" >nul
if exist "%scriptDir%UpdateSequences.bat" copy /Y "%scriptDir%UpdateSequences.bat" "!_subdir!\UpdateSequences.bat" >nul 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 "dailyName=%%~nD"
+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
+136 -36
View File
@@ -1,43 +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
*.fbxkey filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text
*.blend2 filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text
*.mb filter=lfs diff=lfs merge=lfs -text *.hdr filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text
*.ai filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text
*.gz 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
*.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 *.mp3 filter=lfs diff=lfs merge=lfs -text
*.glb filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text
*.gltf 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
+19 -15
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
~$*
+94 -7
View File
@@ -16,6 +16,13 @@ if "%~1"=="" (
REM Create output directory if it doesn't exist REM Create output directory if it doesn't exist
if not exist "output" mkdir "output" 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 REM Create a temporary Python script for compression
set "TEMP_SCRIPT=compress_blend_temp.py" set "TEMP_SCRIPT=compress_blend_temp.py"
echo Creating Python script: !TEMP_SCRIPT! echo Creating Python script: !TEMP_SCRIPT!
@@ -38,6 +45,18 @@ echo blend_files = sys.argv[sys.argv.index^("--"^) + 1:] if "--" in sys.argv els
echo. echo.
echo print^("Found " + str^(len^(blend_files^)^) + " .blend files to compress"^) echo print^("Found " + str^(len^(blend_files^)^) + " .blend files to compress"^)
echo. 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 for blend_file in blend_files:
echo if blend_file.lower^(^).endswith^(".blend"^): echo if blend_file.lower^(^).endswith^(".blend"^):
echo try: echo try:
@@ -46,20 +65,24 @@ echo filename = os.path.basename^(blend_file^)
echo output_file = os.path.join^("output", filename^) echo output_file = os.path.join^("output", filename^)
echo # Check if output file already exists echo # Check if output file already exists
echo if os.path.exists^(output_file^): echo if os.path.exists^(output_file^):
echo print^("Skipping " + blend_file + " ^(already exists in output^)"^) echo print^("SKIP_EXISTING:" + blend_file^)
echo continue echo continue
echo print^("Processing: " + blend_file^) 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 # Load the blend file
echo bpy.ops.wm.open_mainfile^(filepath=blend_file^) echo bpy.ops.wm.open_mainfile^(filepath=blend_file^)
echo # Save with compression to output folder echo # Save with compression to output folder
echo bpy.ops.wm.save_mainfile^(filepath=output_file, compress=True^) echo bpy.ops.wm.save_mainfile^(filepath=output_file, compress=True^)
echo print^("Successfully compressed: " + blend_file + " -^> " + output_file^) echo print^("SUCCESS:" + blend_file + ":" + output_file^)
echo except Exception as e: echo except Exception as e:
echo print^("Failed to compress " + blend_file + ": " + str^(e^)^) echo print^("CRASH:" + blend_file + ":" + str^(e^)^)
echo else: echo else:
echo print^("Skipping " + blend_file + " ^(not a .blend file^)"^) echo print^("SKIP_NOT_BLEND:" + blend_file^)
echo. echo.
echo print^("Compression complete!"^) echo print^("COMPRESSION_COMPLETE"^)
echo bpy.ops.wm.quit_blender^(^) echo bpy.ops.wm.quit_blender^(^)
) > "!TEMP_SCRIPT!" ) > "!TEMP_SCRIPT!"
@@ -75,12 +98,76 @@ if exist "!TEMP_SCRIPT!" (
REM Run Blender to process the dragged files REM Run Blender to process the dragged files
echo Processing dragged .blend files... echo Processing dragged .blend files...
echo Using script: !TEMP_SCRIPT! echo Using script: !TEMP_SCRIPT!
blender --background --factory-startup --python "!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 REM Clean up temporary script
echo Cleaning up temporary script... echo Cleaning up temporary script...
del "!TEMP_SCRIPT!" 2>nul 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.
echo Done! echo Done!
pause 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)")
+1396
View File
File diff suppressed because it is too large Load Diff