diff --git a/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md b/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md
index 0fa1de4..aefff28 100644
--- a/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md
+++ b/.specstory/history/2025-11-25_20-03Z-fix-archive-removal-issue-in-zipseqarchv.md
@@ -1804,7 +1804,6 @@ _**Agent (model default, mode Agent)**_
---
-
The fix is working as expected. The script now:
1. **Always runs cleanup** — even when no sequences need zipping
@@ -1816,3 +1815,1013 @@ The cleanup function now runs in all cases, not just when sequences are queued f
---
+_**User (2025-11-25 21:04Z)**_
+
+another issue: it's ignoring the sequence within this folder:
+
+P:\251119_Tatt2Away_How-it-Works\Renders\scab_v2.1\
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+
+
+---
+
+
+Investigating why that sequence is being ignored. Checking how sequences are detected:
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+ Tool use: **codebase_search** • Searched codebase "How does find_sequence_dirs detect sequence directories and what conditions might cause a directory to be skipped?" • **14** results
+
+| File | Lines |
+|------|-------|
+| `zip_sequences.py` | L471-496 |
+| `zip_sequences.py` | L783-940 |
+| `zip_sequences.py` | L478-514 |
+| `UpdateSequences.ps1` | L194-235 |
+| `zip_sequences.py` | L785-940 |
+| `UpdateSequences.ps1` | L232-276 |
+| `zip_sequences.py` | L943-1070 |
+| `UpdateSequences.ps1` | L60 |
+| `zip_sequences.py` | L553-780 |
+| `UpdateSequences.ps1` | L143-200 |
+| `zip_sequences.py` | L102 |
+| `zip_sequences.py` | L546-724 |
+| `zip_sequences.py` | L987-1091 |
+| `zip_sequences.py` | L788-1070 |
+
+
+
+
+---
+
+
+
+---
+
+
+Checking the sequence detection logic. `find_sequence_dirs` only detects directories that contain image files directly. If `scab_v2.1` has images only in subdirectories, it may be missed. Verifying and improving the detection:
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+---
+
+
+Adding debug logging to see why `scab_v2.1` is being skipped. Improving the detection logic to handle edge cases:
+
+
+---
+
+
+
+Tool use: **code_edit** • Edit file: zip_sequences.py
+
+**Chunk 1**
+Lines added: 17, lines removed: undefined
+
+```diff
+@@ -486,4 +486,21 @@
+ if has_frames:
+ yield path
++ # Also check subdirectories for sequence files (in case the directory itself has no files)
++ # but only if the directory is not too deep (to avoid scanning archive directories)
++ elif not any(part in ("_archive", "_CURRENT", "_zipped") for part in path.parts):
++ # Check if any immediate subdirectory has sequence files
++ for subdir in dirnames:
++ subdir_path = path / subdir
++ if subdir_path.is_dir():
++ try:
++ subdir_files = list(subdir_path.iterdir())
++ if any(f.is_file() and f.suffix.lower() in SEQUENCE_EXTENSIONS for f in subdir_files):
++ # This directory contains a subdirectory with sequence files
++ # Treat the parent as a sequence directory
++ yield path
++ break
++ except (OSError, PermissionError):
++ # Skip if we can't access the subdirectory
++ continue
+
+
+```
+
+
+
+
+
+---
+
+
+
+---
+
+
+Reverting that change and adding debug logging to see why it's being skipped:
+
+
+---
+
+
+
+Tool use: **code_edit** • Edit file: zip_sequences.py
+
+**Chunk 1**
+Lines added: 11, lines removed: 18
+
+```diff
+@@ -477,30 +477,23 @@
+
+
+- def find_sequence_dirs(root: Path) -> Iterator[Path]:
++ def find_sequence_dirs(root: Path, *, verbose: bool = False) -> Iterator[Path]:
+ for dirpath, dirnames, filenames in os.walk(root):
+ path = Path(dirpath)
+ dirnames[:] = [d for d in dirnames if d not in ("_archive", "_CURRENT")]
+ if is_archive_path(path):
++ if verbose:
++ rel = path.relative_to(root) if path.is_relative_to(root) else path
++ log("scan", f"Skipping archive path: {rel}", verbose_only=True, verbose=verbose)
+ continue
+ has_frames = any(Path(dirpath, f).suffix.lower() in SEQUENCE_EXTENSIONS for f in filenames)
+ if has_frames:
+ yield path
+- # Also check subdirectories for sequence files (in case the directory itself has no files)
+- # but only if the directory is not too deep (to avoid scanning archive directories)
+- elif not any(part in ("_archive", "_CURRENT", "_zipped") for part in path.parts):
+- # Check if any immediate subdirectory has sequence files
+- for subdir in dirnames:
+- subdir_path = path / subdir
+- if subdir_path.is_dir():
+- try:
+- subdir_files = list(subdir_path.iterdir())
+- if any(f.is_file() and f.suffix.lower() in SEQUENCE_EXTENSIONS for f in subdir_files):
+- # This directory contains a subdirectory with sequence files
+- # Treat the parent as a sequence directory
+- yield path
+- break
+- except (OSError, PermissionError):
+- # Skip if we can't access the subdirectory
+- continue
++ elif verbose:
++ # Log directories that don't have sequence files for debugging
++ rel = path.relative_to(root) if path.is_relative_to(root) else path
++ if "scab" in path.name.lower() or "v2" in path.name.lower():
++ # Special logging for directories that might be sequences
++ frame_extensions = [f for f in filenames if Path(f).suffix.lower() in SEQUENCE_EXTENSIONS]
++ log("scan", f"Directory {rel} has no sequence files (found {len(filenames)} files, {len(frame_extensions)} with sequence extensions)", verbose_only=True, verbose=verbose)
+
+
+```
+
+
+
+
+
+---
+
+
+
+---
+
+
+Updating the call to `find_sequence_dirs` to pass the verbose flag:
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+Tool use: **code_edit** • Edit file: zip_sequences.py
+
+**Chunk 1**
+Lines added: 1, lines removed: 1
+
+```diff
+@@ -807,5 +807,5 @@
+
+ if RENDER_ROOT.exists():
+- for seq_dir in find_sequence_dirs(RENDER_ROOT):
++ for seq_dir in find_sequence_dirs(RENDER_ROOT, verbose=verbose):
+ total_scanned += 1
+ rel = seq_dir.relative_to(RENDER_ROOT)
+```
+
+
+
+
+
+---
+
+
+
+ Tool use: **read_lints** • Read lints for 1 file
+
+Lint paths:
+
+- `zip_sequences.py`
+
+Lint results
+
+
+**No lint errors found**
+
+
+
+---
+
+
+
+---
+
+
+Improving detection to handle sequences where images are in subdirectories. Updating the logic:
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+Tool use: **code_edit** • Edit file: zip_sequences.py
+
+**Chunk 1**
+Lines added: 1, lines removed: undefined
+
+```diff
+@@ -478,4 +478,5 @@
+
+ def find_sequence_dirs(root: Path, *, verbose: bool = False) -> Iterator[Path]:
++ seen_dirs = set() # Track directories we've already yielded to avoid duplicates
+ for dirpath, dirnames, filenames in os.walk(root):
+ path = Path(dirpath)
+```
+
+**Chunk 2**
+Lines added: 19, lines removed: 3
+
+```diff
+@@ -486,14 +487,30 @@
+ log("scan", f"Skipping archive path: {rel}", verbose_only=True, verbose=verbose)
+ continue
++
++ # Check if this directory has sequence files directly
+ has_frames = any(Path(dirpath, f).suffix.lower() in SEQUENCE_EXTENSIONS for f in filenames)
+ if has_frames:
+- yield path
++ path_resolved = path.resolve()
++ if path_resolved not in seen_dirs:
++ seen_dirs.add(path_resolved)
++ yield path
+ elif verbose:
+ # Log directories that don't have sequence files for debugging
+ rel = path.relative_to(root) if path.is_relative_to(root) else path
+- if "scab" in path.name.lower() or "v2" in path.name.lower():
++ if "scab" in path.name.lower():
+ # Special logging for directories that might be sequences
+ frame_extensions = [f for f in filenames if Path(f).suffix.lower() in SEQUENCE_EXTENSIONS]
+- log("scan", f"Directory {rel} has no sequence files (found {len(filenames)} files, {len(frame_extensions)} with sequence extensions)", verbose_only=True, verbose=verbose)
++ all_files = list(path.iterdir()) if path.exists() else []
++ log("scan", f"Directory {rel}: {len(filenames)} files in dir, {len(frame_extensions)} with sequence extensions, {len(all_files)} total items", verbose_only=True, verbose=verbose)
++ # Check subdirectories
++ for subdir in dirnames[:5]: # Check first 5 subdirs
++ subdir_path = path / subdir
++ try:
++ subdir_files = list(subdir_path.iterdir()) if subdir_path.exists() else []
++ subdir_frame_files = [f for f in subdir_files if f.is_file() and f.suffix.lower() in SEQUENCE_EXTENSIONS]
++ if subdir_frame_files:
++ log("scan", f" Subdirectory {subdir} has {len(subdir_frame_files)} sequence files", verbose_only=True, verbose=verbose)
++ except (OSError, PermissionError):
++ pass
+
+
+```
+
+
+
+
+
+---
+
+
+
+---
+
+
+Run `ZipSeqArchv.bat` with `--verbose` to see why `scab_v2.1` is being skipped. The logging will show:
+1. Whether the directory is being scanned
+2. How many files it contains
+3. Whether any subdirectories have sequence files
+
+The current logic only detects directories that have sequence files directly in them. If `scab_v2.1` has images only in subdirectories, those subdirectories are detected as separate sequences, not `scab_v2.1` itself.
+
+If you want `scab_v2.1` to be treated as a single sequence even when images are in subdirectories, we can update the detection logic. Share the verbose output and we can adjust accordingly.
+
+---
+
+_**User (2025-11-25 21:04Z)**_
+
+PS P:\251119_Tatt2Away_How-it-Works\Renders> P:\251119_Tatt2Away_How-it-Works\Renders\ZipSeqArchv.bat
+
+[init] Loading configuration sources...
+
+[init] Reading project config at P:\251119_Tatt2Away_How-it-Works\.config\config.json
+
+[init] Configuration loaded from project
+
+[init] zip_sequences starting up...
+
+[init] Working directory: P:\251119_Tatt2Away_How-it-Works
+
+[init] Mode: zip, zipper: 7z, jobs arg: auto
+
+[init] 7z executable: C:\ProgramData\chocolatey\bin\7z.EXE, Max7zInst: auto, method: LZMA2, level: 5
+
+[init] Scanning sequences under \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders
+
+[scan] [1] Inspecting 231016_audio_skin-top_2_CH
+
+[scan] Skipping 231016_audio_skin-top_2_CH (unchanged since archive)
+
+[scan] [2] Inspecting scab_v2.1
+
+[scan] Queued scab_v2.1 for compression (~1.31GB) [1 total]
+
+[scan] [3] Inspecting tatt2away_process_v3
+
+[scan] Queued tatt2away_process_v3 for compression (~0.67GB) [2 total]
+
+[scan] [4] Inspecting Tatt2Away_stages_v2_CM
+
+[scan] Skipping Tatt2Away_stages_v2_CM (unchanged since archive)
+
+[zip] RAM: 102.2GB available (80% of total), 97.1GB for compression (95%)
+
+[zip] Estimated RAM per sequence: 6.0GB total across 2 sequences
+
+[zip] All sequences fit in RAM → using 2 workers (one per sequence)
+
+[zip] Final worker count: 2 (requested: None)
+
+[init] Preparing to compress 2 sequence(s) with 2 worker(s)
+
+[zip] Renders\scab_v2.1 -> Renders\_zipped\scab_v2.7z
+
+[zip] Renders\tatt2away_process_v3 -> Renders\_zipped\tatt2away_process_v3.7z
+
+[zip] 7z command: C:\ProgramData\chocolatey\bin\7z.EXE a -y -bb0 -mx=5 -t7z -m0=LZMA2 -md=1024m -mmt=on \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders\_zipped\tmpaxb_br7j.7z @C:\Users\Nathan\AppData\Local\Temp\tmpsim_da0y.lst
+
+[zip] 7z command: C:\ProgramData\chocolatey\bin\7z.EXE a -y -bb0 -mx=5 -t7z -m0=LZMA2 -md=1024m -mmt=on \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders\_zipped\tmp2vv7izgy.7z @C:\Users\Nathan\AppData\Local\Temp\tmpv25oxn65.lst
+
+[zip] 1/2 tatt2away_process_v3
+
+[zip] 2/2 scab_v2.1
+
+[zip] Updated 2 sequence archive(s).
+
+[zip] Archives updated. Stage manually with `git add Renders/_zipped`, if desired.
+
+[scan] Summary: scanned 4, quick-skipped 2, state-skipped 0, empty 0, queued 2
+
+[zip] Scanning for orphan archives in \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders\_zipped
+
+[zip] Archive 231016_audio_skin-top_2_CH.7z has matching sequence directory; keeping
+
+[zip] Removing orphan archive scab_v2.7z
+
+[zip] Deleted archive: scab_v2.7z
+
+[zip] Removed orphan metadata scab_v2.7z.meta.json
+
+[zip] Archive tatt2away_process_v3.7z has matching sequence directory; keeping
+
+[zip] Archive Tatt2Away_stages_v2_CM.7z has matching sequence directory; keeping
+
+[zip] Removed 1 orphan archive(s).
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+
+
+---
+
+
+Archive path is incorrect: `scab_v2.1` is being archived as `scab_v2.7z` instead of `scab_v2.1.7z`, so the `.1` is dropped. Checking `archive_path_for`:
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+---
+
+
+
+Tool use: **grep** • Grep for "def archive_path_for" in "zip_sequences.py" • 1 match
+
+Output mode: content
+
+
+| File | Content | Line |
+|------|------|------|
+| `zip_sequences.py` | `def archive_path_for(seq_dir: Path) -> Path:` | L566 |
+
+
+
+
+
+---
+
+
+
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+
+
+---
+
+
+
+---
+
+
+The issue is that `archive_path_for` uses `.with_suffix()`, which replaces the suffix. For `scab_v2.1`, `.1` is treated as a suffix and replaced with `.7z`, producing `scab_v2.7z` instead of `scab_v2.1.7z`. Fixing it to append the suffix instead:
+
+
+---
+
+
+
+Tool use: **code_edit** • Edit file: zip_sequences.py
+
+**Chunk 1**
+Lines added: 2, lines removed: 1
+
+```diff
+@@ -567,5 +567,6 @@
+ rel = seq_dir.relative_to(RENDER_ROOT)
+ suffix = ".7z" if ZIPPER_TYPE == "7z" else ".zip"
+- return (ARCHIVE_ROOT / rel).with_suffix(suffix)
++ # Append suffix instead of replacing, since directory names might have dots (e.g., "scab_v2.1")
++ return ARCHIVE_ROOT / f"{rel}{suffix}"
+
+
+```
+
+
+
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+---
+
+
+Fixing `sequence_dir_for` to handle this correctly:
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+Tool use: **code_edit** • Edit file: zip_sequences.py
+
+**Chunk 1**
+Lines added: 8, lines removed: 1
+
+```diff
+@@ -573,5 +573,12 @@
+ def sequence_dir_for(zip_path: Path) -> Path:
+ rel = zip_path.relative_to(ARCHIVE_ROOT)
+- return (RENDER_ROOT / rel).with_suffix("")
++ # Remove the archive suffix (.7z or .zip) from the end
++ # Handle both .7z and .zip extensions
++ rel_str = str(rel)
++ if rel_str.endswith(".7z"):
++ rel_str = rel_str[:-3]
++ elif rel_str.endswith(".zip"):
++ rel_str = rel_str[:-4]
++ return RENDER_ROOT / rel_str
+
+
+```
+
+
+
+
+
+---
+
+
+
+ Tool use: **read_lints** • Read lints for 1 file
+
+Lint paths:
+
+- `zip_sequences.py`
+
+Lint results
+
+
+**No lint errors found**
+
+
+
+---
+
+
+
+---
+
+
+Fixed the issue: `archive_path_for` used `.with_suffix()`, which replaced the suffix. For `scab_v2.1`, `.1` was treated as a suffix and replaced with `.7z`, producing `scab_v2.7z` instead of `scab_v2.1.7z`.
+
+**Changes made:**
+1. **`archive_path_for`**: Appends the suffix instead of replacing it, so `scab_v2.1` becomes `scab_v2.1.7z`
+2. **`sequence_dir_for`**: Removes the archive extension (`.7z` or `.zip`) from the end to correctly map back to the sequence directory
+
+When you run `ZipSeqArchv.bat` again:
+- `scab_v2.1` will be archived as `scab_v2.1.7z` (not `scab_v2.7z`)
+- The cleanup function will correctly match the archive to the sequence directory
+- It won't be removed as an orphan
+
+This also fixes the issue for any other directory names containing dots (e.g., `v2.1`, `test.1.2`, etc.).
+
+---
+
+_**User (2025-11-25 21:04Z)**_
+
+is the compression lvl 0 working on this? it doesn't seem to be going very fast
+
+PS P:\251119_Tatt2Away_How-it-Works\Renders> P:\251119_Tatt2Away_How-it-Works\Renders\ZipSeqArchv.bat
+
+[init] Loading configuration sources...
+
+[init] Reading project config at P:\251119_Tatt2Away_How-it-Works\.config\config.json
+
+[init] Configuration loaded from project
+
+[init] zip_sequences starting up...
+
+[init] Working directory: P:\251119_Tatt2Away_How-it-Works
+
+[init] Mode: zip, zipper: 7z, jobs arg: auto
+
+[init] 7z executable: C:\ProgramData\chocolatey\bin\7z.EXE, Max7zInst: auto, method: LZMA2, level: 0
+
+[init] Scanning sequences under \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders
+
+[scan] [1] Inspecting 231016_audio_skin-top_2_CH
+
+[scan] Skipping 231016_audio_skin-top_2_CH (unchanged since archive)
+
+[scan] [2] Inspecting scab_v2.1
+
+[scan] Queued scab_v2.1 for compression (~1.31GB) [1 total]
+
+[scan] [3] Inspecting tatt2away_process_v3
+
+[scan] Skipping tatt2away_process_v3 (unchanged since archive)
+
+[scan] [4] Inspecting Tatt2Away_stages_v2_CM
+
+[scan] Skipping Tatt2Away_stages_v2_CM (unchanged since archive)
+
+[zip] RAM: 102.2GB available (80% of total), 97.1GB for compression (95%)
+
+[zip] Estimated RAM per sequence: 3.0GB total across 1 sequences
+
+[zip] All sequences fit in RAM → using 1 workers (one per sequence)
+
+[zip] Final worker count: 1 (requested: None)
+
+[init] Preparing to compress 1 sequence(s) with 1 worker(s)
+
+[zip] Renders\scab_v2.1 -> Renders\_zipped\scab_v2.1.7z
+
+[zip] 7z command: C:\ProgramData\chocolatey\bin\7z.EXE a -y -bb0 -mx=0 -t7z -m0=LZMA2 -md=1024m -mmt=32 \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders\_zipped\tmpq3l68ie4.7z @C:\Users\Nathan\AppData\Local\Temp\tmp57ak8mgp.lst
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+
+
+---
+
+
+At compression level 0, the script still uses `-m0=LZMA2`, which compresses. At level 0, we should use `-m0=Copy` (store). Updating the code to use Copy when level is 0:
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+Tool use: **grep** • Grep for "COMPRESSION_LEVEL" in "zip_sequences.py" • 11 matches
+
+Output mode: content
+
+
+| File | Content | Line |
+|------|------|------|
+| `zip_sequences.py` | `COMPRESSION_LEVEL = CONFIG.get("compression", 9)` | L114 |
+| `zip_sequences.py` | `if isinstance(COMPRESSION_LEVEL, str):` | L115 |
+| `zip_sequences.py` | `COMPRESSION_LEVEL = int(COMPRESSION_LEVEL)` | L117 |
+| `zip_sequences.py` | `COMPRESSION_LEVEL = 9` | L119 |
+| `zip_sequences.py` | `if not isinstance(COMPRESSION_LEVEL, int):` | L120 |
+| `zip_sequences.py` | `COMPRESSION_LEVEL = 9` | L121 |
+| `zip_sequences.py` | `COMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL))` | L122 |
+| `zip_sequences.py` | `f"-mx=\{COMPRESSION_LEVEL\}",` | L645 |
+| `zip_sequences.py` | `if COMPRESSION_LEVEL <= 0:` | L743 |
+| `zip_sequences.py` | `zip_kwargs = \{"compresslevel": COMPRESSION_LEVEL\}` | L748 |
+| `zip_sequences.py` | `log("init", f"7z executable: \{exe\}, Max7zInst: \{max_inst\}, method: \{COMPRESSION_METHOD\}, level: \{COMPRESSION_LEVEL\}")` | L1117 |
+
+
+
+
+
+---
+
+
+
+---
+
+
+Tool use: **read_file** • Read file: zip_sequences.py
+
+
+
+
+---
+
+
+
+
+
+---
+
+
+
+---
+
+
+Updating the code to use Copy (store) when compression level is 0:
+
+
+---
+
+
+
+Tool use: **code_edit** • Edit file: zip_sequences.py
+
+**Chunk 1**
+Lines added: 42, lines removed: 35
+
+```diff
+@@ -648,40 +648,47 @@
+
+ # Set compression method and memory/dictionary size based on method
+- # PPMd uses memory parameter in method string, LZMA2 uses -md flag
++ # At compression level 0, use Copy (store) method for maximum speed
+ FIXED_DICT_SIZE_MB = 1024
+- if COMPRESSION_METHOD == "PPMd":
+- # PPMd: specify memory as part of method string
+- cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m")
+- elif COMPRESSION_METHOD == "LZMA2":
+- # LZMA2: use -md for dictionary size
+- cmd.append("-m0=LZMA2")
+- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
+- elif COMPRESSION_METHOD == "BZip2":
+- # BZip2: use -md for dictionary size (smaller max: 900KB)
+- max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB
+- cmd.append("-m0=BZip2")
+- cmd.append(f"-md={max_bzip2_dict}k")
+- elif COMPRESSION_METHOD == "Deflate":
+- # Deflate: doesn't use dictionary size parameter
+- cmd.append("-m0=Deflate")
+- else:
+- # Fallback: use LZMA2
+- cmd.append("-m0=LZMA2")
+- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
+-
+- # CPU thread allocation: when there's only 1 worker, use all CPU cores
+- # When there are multiple workers, use auto mode to let 7z decide
+- # Note: PPMd is single-threaded and won't benefit from -mmt
+- cpu_cores = os.cpu_count() or 1
+- if COMPRESSION_METHOD == "PPMd":
+- # PPMd is single-threaded, so -mmt won't help
+- # But we can still set it for consistency
+- cmd.append("-mmt=on")
+- elif worker_count == 1:
+- # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this)
+- cmd.append(f"-mmt={cpu_cores}")
+- else:
+- # Multiple workers: use auto mode (7z will manage threads)
+- cmd.append("-mmt=on")
++ if COMPRESSION_LEVEL == 0:
++ # Level 0 = no compression, just store files (fastest)
++ cmd.append("-m0=Copy")
++ # Copy method doesn't need threading, but enable it anyway for consistency
++ cmd.append("-mmt=on")
++ else:
++ # Compression levels 1-9: use configured compression method
++ if COMPRESSION_METHOD == "PPMd":
++ # PPMd: specify memory as part of method string
++ cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m")
++ elif COMPRESSION_METHOD == "LZMA2":
++ # LZMA2: use -md for dictionary size
++ cmd.append("-m0=LZMA2")
++ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
++ elif COMPRESSION_METHOD == "BZip2":
++ # BZip2: use -md for dictionary size (smaller max: 900KB)
++ max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB
++ cmd.append("-m0=BZip2")
++ cmd.append(f"-md={max_bzip2_dict}k")
++ elif COMPRESSION_METHOD == "Deflate":
++ # Deflate: doesn't use dictionary size parameter
++ cmd.append("-m0=Deflate")
++ else:
++ # Fallback: use LZMA2
++ cmd.append("-m0=LZMA2")
++ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
++
++ # CPU thread allocation: when there's only 1 worker, use all CPU cores
++ # When there are multiple workers, use auto mode to let 7z decide
++ # Note: PPMd is single-threaded and won't benefit from -mmt
++ cpu_cores = os.cpu_count() or 1
++ if COMPRESSION_METHOD == "PPMd":
++ # PPMd is single-threaded, so -mmt won't help
++ # But we can still set it for consistency
++ cmd.append("-mmt=on")
++ elif worker_count == 1:
++ # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this)
++ cmd.append(f"-mmt={cpu_cores}")
++ else:
++ # Multiple workers: use auto mode (7z will manage threads)
++ cmd.append("-mmt=on")
+
+ cmd.extend([
+```
+
+
+
+
+
+---
+
+
+
+ Tool use: **read_lints** • Read lints for 1 file
+
+Lint paths:
+
+- `zip_sequences.py`
+
+Lint results
+
+
+**No lint errors found**
+
+
+
+---
+
+
+
+---
+
+
+Fixed: at compression level 0, the script was still using LZMA2, which compresses. At level 0, it now uses `-m0=Copy` (store), which packages files without compression.
+
+**What changed:**
+- When `compression: 0` is set, the script now uses `-m0=Copy` instead of the configured compression method
+- Copy mode stores files without compression, so it's limited mainly by disk I/O speed
+- Compression levels 1-9 continue to use the configured method (LZMA2, PPMd, etc.)
+
+When you run `ZipSeqArchv.bat` with `compression: 0`, it should be much faster since it's just copying files into the archive without compression. The 7z command will now show `-m0=Copy` instead of `-m0=LZMA2` when compression level is 0.
+
+---
+
+_**User (2025-11-25 21:04Z)**_
+
+is good?
+
+PS P:\251119_Tatt2Away_How-it-Works\Renders> P:\251119_Tatt2Away_How-it-Works\Renders\ZipSeqArchv.bat
+
+[init] Loading configuration sources...
+
+[init] Reading project config at P:\251119_Tatt2Away_How-it-Works\.config\config.json
+
+[init] Configuration loaded from project
+
+[init] zip_sequences starting up...
+
+[init] Working directory: P:\251119_Tatt2Away_How-it-Works
+
+[init] Mode: zip, zipper: 7z, jobs arg: auto
+
+[init] 7z executable: C:\ProgramData\chocolatey\bin\7z.EXE, Max7zInst: auto, method: LZMA2, level: 0
+
+[init] Scanning sequences under \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders
+
+[scan] [1] Inspecting 231016_audio_skin-top_2_CH
+
+[scan] Skipping 231016_audio_skin-top_2_CH (unchanged since archive)
+
+[scan] [2] Inspecting scab_v2.1
+
+[scan] Queued scab_v2.1 for compression (~1.31GB) [1 total]
+
+[scan] [3] Inspecting tatt2away_process_v3
+
+[scan] Skipping tatt2away_process_v3 (unchanged since archive)
+
+[scan] [4] Inspecting Tatt2Away_stages_v2_CM
+
+[scan] Skipping Tatt2Away_stages_v2_CM (unchanged since archive)
+
+[zip] RAM: 102.2GB available (80% of total), 97.1GB for compression (95%)
+
+[zip] Estimated RAM per sequence: 3.0GB total across 1 sequences
+
+[zip] All sequences fit in RAM → using 1 workers (one per sequence)
+
+[zip] Final worker count: 1 (requested: None)
+
+[init] Preparing to compress 1 sequence(s) with 1 worker(s)
+
+[zip] Renders\scab_v2.1 -> Renders\_zipped\scab_v2.1.7z
+
+[zip] 7z command: C:\ProgramData\chocolatey\bin\7z.EXE a -y -bb0 -mx=0 -t7z -m0=Copy -mmt=on \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders\_zipped\tmpglnjsei8.7z @C:\Users\Nathan\AppData\Local\Temp\tmp_vsbg9m6.lst
+
+[zip] 1/1 scab_v2.1
+
+[zip] Updated 1 sequence archive(s).
+
+[zip] Archives updated. Stage manually with `git add Renders/_zipped`, if desired.
+
+[scan] Summary: scanned 4, quick-skipped 3, state-skipped 0, empty 0, queued 1
+
+[zip] Scanning for orphan archives in \\NEXUS\proj\251119_Tatt2Away_How-it-Works\Renders\_zipped
+
+[zip] Archive 231016_audio_skin-top_2_CH.7z has matching sequence directory; keeping
+
+[zip] Archive scab_v2.1.7z has matching sequence directory; keeping
+
+[zip] Archive tatt2away_process_v3.7z has matching sequence directory; keeping
+
+[zip] Archive Tatt2Away_stages_v2_CM.7z has matching sequence directory; keeping
+
+[zip] Removing orphan archive tmp4vygazhw.7z
+
+[zip] Deleted archive: tmp4vygazhw.7z
+
+[zip] Removing orphan archive tmp8caqu_bp.7z
+
+[zip] Deleted archive: tmp8caqu_bp.7z
+
+[zip] Removing orphan archive tmp8nuwz4x1.7z
+
+[zip] Deleted archive: tmp8nuwz4x1.7z
+
+[zip] Removing orphan archive tmpfs0howr_.7z
+
+[zip] Deleted archive: tmpfs0howr_.7z
+
+[zip] Removed 4 orphan archive(s).
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+
+
+---
+
+
+Yes, it's working correctly.
+
+1. Compression level 0 is working: the 7z command shows `-m0=Copy` instead of `-m0=LZMA2`, so it's using Copy (store) with no compression, which should be faster.
+
+2. Archive path is correct: `scab_v2.1.7z` is created correctly (not `scab_v2.7z`).
+
+3. Orphan cleanup is working: it found and removed 4 leftover temporary files (`tmp*.7z`) that should have been cleaned up.
+
+Everything looks good. With compression level 0, archiving should be much faster since it's just copying files into the archive without compression.
+
+---
+
diff --git a/config.json b/config.json
index 3c87098..20eb7fd 100644
--- a/config.json
+++ b/config.json
@@ -2,7 +2,7 @@
"dailyFormat": "daily_YYMMDD",
"structDir": "A:\\1 Amazon_Active_Projects\\3 ProjectStructure",
"zipper": "7z",
- "compression": 5,
+ "compression": 0,
"Max7zInst": 0
}
diff --git a/zip_sequences.py b/zip_sequences.py
index 8f0f8b9..56aecda 100644
--- a/zip_sequences.py
+++ b/zip_sequences.py
@@ -476,15 +476,42 @@ def is_archive_path(path: Path) -> bool:
return any(part in ("_archive", "_CURRENT") for part in path.parts)
-def find_sequence_dirs(root: Path) -> Iterator[Path]:
+def find_sequence_dirs(root: Path, *, verbose: bool = False) -> Iterator[Path]:
+ seen_dirs = set() # Track directories we've already yielded to avoid duplicates
for dirpath, dirnames, filenames in os.walk(root):
path = Path(dirpath)
dirnames[:] = [d for d in dirnames if d not in ("_archive", "_CURRENT")]
if is_archive_path(path):
+ if verbose:
+ rel = path.relative_to(root) if path.is_relative_to(root) else path
+ log("scan", f"Skipping archive path: {rel}", verbose_only=True, verbose=verbose)
continue
+
+ # Check if this directory has sequence files directly
has_frames = any(Path(dirpath, f).suffix.lower() in SEQUENCE_EXTENSIONS for f in filenames)
if has_frames:
- yield path
+ path_resolved = path.resolve()
+ if path_resolved not in seen_dirs:
+ seen_dirs.add(path_resolved)
+ yield path
+ elif verbose:
+ # Log directories that don't have sequence files for debugging
+ rel = path.relative_to(root) if path.is_relative_to(root) else path
+ if "scab" in path.name.lower():
+ # Special logging for directories that might be sequences
+ frame_extensions = [f for f in filenames if Path(f).suffix.lower() in SEQUENCE_EXTENSIONS]
+ all_files = list(path.iterdir()) if path.exists() else []
+ log("scan", f"Directory {rel}: {len(filenames)} files in dir, {len(frame_extensions)} with sequence extensions, {len(all_files)} total items", verbose_only=True, verbose=verbose)
+ # Check subdirectories
+ for subdir in dirnames[:5]: # Check first 5 subdirs
+ subdir_path = path / subdir
+ try:
+ subdir_files = list(subdir_path.iterdir()) if subdir_path.exists() else []
+ subdir_frame_files = [f for f in subdir_files if f.is_file() and f.suffix.lower() in SEQUENCE_EXTENSIONS]
+ if subdir_frame_files:
+ log("scan", f" Subdirectory {subdir} has {len(subdir_frame_files)} sequence files", verbose_only=True, verbose=verbose)
+ except (OSError, PermissionError):
+ pass
def iter_sequence_files(seq_dir: Path) -> Iterator[Path]:
@@ -539,12 +566,20 @@ def state_changed(seq_state: dict, stored_state: dict | None) -> bool:
def archive_path_for(seq_dir: Path) -> Path:
rel = seq_dir.relative_to(RENDER_ROOT)
suffix = ".7z" if ZIPPER_TYPE == "7z" else ".zip"
- return (ARCHIVE_ROOT / rel).with_suffix(suffix)
+ # Append suffix instead of replacing, since directory names might have dots (e.g., "scab_v2.1")
+ return ARCHIVE_ROOT / f"{rel}{suffix}"
def sequence_dir_for(zip_path: Path) -> Path:
rel = zip_path.relative_to(ARCHIVE_ROOT)
- return (RENDER_ROOT / rel).with_suffix("")
+ # Remove the archive suffix (.7z or .zip) from the end
+ # Handle both .7z and .zip extensions
+ rel_str = str(rel)
+ if rel_str.endswith(".7z"):
+ rel_str = rel_str[:-3]
+ elif rel_str.endswith(".zip"):
+ rel_str = rel_str[:-4]
+ return RENDER_ROOT / rel_str
def state_path_for(zip_path: Path) -> Path:
@@ -612,42 +647,49 @@ def zip_sequence(seq_dir: Path, zip_path: Path, per_job_memory_limit: int | None
]
# Set compression method and memory/dictionary size based on method
- # PPMd uses memory parameter in method string, LZMA2 uses -md flag
+ # At compression level 0, use Copy (store) method for maximum speed
FIXED_DICT_SIZE_MB = 1024
- if COMPRESSION_METHOD == "PPMd":
- # PPMd: specify memory as part of method string
- cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m")
- elif COMPRESSION_METHOD == "LZMA2":
- # LZMA2: use -md for dictionary size
- cmd.append("-m0=LZMA2")
- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
- elif COMPRESSION_METHOD == "BZip2":
- # BZip2: use -md for dictionary size (smaller max: 900KB)
- max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB
- cmd.append("-m0=BZip2")
- cmd.append(f"-md={max_bzip2_dict}k")
- elif COMPRESSION_METHOD == "Deflate":
- # Deflate: doesn't use dictionary size parameter
- cmd.append("-m0=Deflate")
- else:
- # Fallback: use LZMA2
- cmd.append("-m0=LZMA2")
- cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
-
- # CPU thread allocation: when there's only 1 worker, use all CPU cores
- # When there are multiple workers, use auto mode to let 7z decide
- # Note: PPMd is single-threaded and won't benefit from -mmt
- cpu_cores = os.cpu_count() or 1
- if COMPRESSION_METHOD == "PPMd":
- # PPMd is single-threaded, so -mmt won't help
- # But we can still set it for consistency
+ if COMPRESSION_LEVEL == 0:
+ # Level 0 = no compression, just store files (fastest)
+ cmd.append("-m0=Copy")
+ # Copy method doesn't need threading, but enable it anyway for consistency
cmd.append("-mmt=on")
- elif worker_count == 1:
- # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this)
- cmd.append(f"-mmt={cpu_cores}")
else:
- # Multiple workers: use auto mode (7z will manage threads)
- cmd.append("-mmt=on")
+ # Compression levels 1-9: use configured compression method
+ if COMPRESSION_METHOD == "PPMd":
+ # PPMd: specify memory as part of method string
+ cmd.append(f"-m0=PPMd:mem={FIXED_DICT_SIZE_MB}m")
+ elif COMPRESSION_METHOD == "LZMA2":
+ # LZMA2: use -md for dictionary size
+ cmd.append("-m0=LZMA2")
+ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
+ elif COMPRESSION_METHOD == "BZip2":
+ # BZip2: use -md for dictionary size (smaller max: 900KB)
+ max_bzip2_dict = min(FIXED_DICT_SIZE_MB, 900) # BZip2 max is 900KB
+ cmd.append("-m0=BZip2")
+ cmd.append(f"-md={max_bzip2_dict}k")
+ elif COMPRESSION_METHOD == "Deflate":
+ # Deflate: doesn't use dictionary size parameter
+ cmd.append("-m0=Deflate")
+ else:
+ # Fallback: use LZMA2
+ cmd.append("-m0=LZMA2")
+ cmd.append(f"-md={FIXED_DICT_SIZE_MB}m")
+
+ # CPU thread allocation: when there's only 1 worker, use all CPU cores
+ # When there are multiple workers, use auto mode to let 7z decide
+ # Note: PPMd is single-threaded and won't benefit from -mmt
+ cpu_cores = os.cpu_count() or 1
+ if COMPRESSION_METHOD == "PPMd":
+ # PPMd is single-threaded, so -mmt won't help
+ # But we can still set it for consistency
+ cmd.append("-mmt=on")
+ elif worker_count == 1:
+ # Single worker: use all CPU cores for maximum speed (LZMA2, BZip2, Deflate support this)
+ cmd.append(f"-mmt={cpu_cores}")
+ else:
+ # Multiple workers: use auto mode (7z will manage threads)
+ cmd.append("-mmt=on")
cmd.extend([
str(temp_zip_abs),
@@ -796,7 +838,7 @@ def run_zip(requested_workers: int | None, *, verbose: bool) -> int:
queued = 0
if RENDER_ROOT.exists():
- for seq_dir in find_sequence_dirs(RENDER_ROOT):
+ for seq_dir in find_sequence_dirs(RENDER_ROOT, verbose=verbose):
total_scanned += 1
rel = seq_dir.relative_to(RENDER_ROOT)
if total_scanned <= 5 or total_scanned % 10 == 0: