Compare commits

...
Author SHA1 Message Date
Meisoftcoltd ecdda64169 ⚡ optimize: merge sequential loops in prompt iteration
Merged two sequential loops over `prompt.items()` in `SequentialLoopTrigger.trigger_next` into a single pass.
The first pass now simultaneously finds the `estimated_chunk` from `AutoLoopCalculator` and performs mutations for seeds and loop indices.
`MasterSwitch` mutations are deferred to a small secondary pass after `is_next_final` is calculated using the gathered chunk size.

This change reduces the overhead of iterating over large prompt dictionaries and yielded a ~26.8% performance improvement in synthetic benchmarks.
2026-05-02 20:45:27 +00:00
Meisoft 8fc697a8c3 Merge pull request #100 from Meisoftcoltd/testing-format-bytes-utility-10775869003227782014
🧪 Unit tests for format_bytes utility function
2026-04-30 17:16:46 +02:00
Meisoftcoltd 75ab5d9676 🧪 Add unit tests for format_bytes utility function
- Created tests/test_vram_node.py
- Mocked dependencies (torch, comfy) to allow testing in isolation
- Implemented test cases for zero values, exact GB, fractional values, small values (rounding check), and large values.
- Verified test robustness by confirming failure when logic was intentionally altered.
2026-04-30 15:16:06 +00:00
Meisoft cba02bf08f Merge pull request #99 from Meisoftcoltd/security-fix-command-injection-ffmpeg-3363411280039704601
🔒 [security] Fix command injection in FFmpeg installation
2026-04-30 17:14:42 +02:00
Meisoftcoltd 8d8e3deb1e fix: 🔒 prevent command injection in FFmpeg installation
Replaces the shell-based subprocess call with list-based arguments and
separate invocations for 'apt-get update' and 'apt-get install'.
This eliminates the risk of command injection by avoiding shell
interpretation and properly handles the DEBIAN_FRONTEND environment
variable via sudo.
2026-04-30 14:59:46 +00:00
Meisoft a875f536da Merge pull request #98 from Meisoftcoltd/perf-concurrent-image-loading-13430517007901480050
⚡ [Performance] Concurrent image loading in Video Stitcher
2026-04-30 16:56:44 +02:00
Meisoftcoltd 12a55c7fd6 ⚡ [Performance] Concurrent image loading in Video Stitcher
Introduced concurrent image loading and processing in the Video Stitcher node
using ThreadPoolExecutor. This optimizes the transition from disk-cached PNGs
to the final video tensor by parallelizing I/O and color conversion.

- Replaced sequential loop with ThreadPoolExecutor.map
- Wrapped executor.map in list() to ensure exception propagation
- Verified ~85% speedup in mocked I/O-bound benchmarks
2026-04-30 14:55:12 +00:00
Meisoft 119626a20f Merge pull request #97 from Meisoftcoltd/add-tts-calculator-and-audio-stitcher-15540597175344943849
Support text-driven sequential looping and audio stitching
2026-04-28 19:29:57 +02:00
Meisoftcoltd b854a90bf9 feat: add AutoLoopCalculatorTTS and update IncrementalVideoStitcher for audio caching
- Registered `AutoLoopCalculatorTTS` in `NODE_DISPLAY_NAME_MAPPINGS`.
- Added `AutoLoopCalculatorTTS` class to `video.py` to allow looping through text paragraphs instead of video frames.
- Replaced the implementation of `IncrementalVideoStitcher` to cache audio tensors as `.pt` files and concatenate them along the time axis on the final cycle.
2026-04-28 17:26:50 +00:00
Meisoft e1181c468c Merge pull request #96 from Meisoftcoltd/ffmpeg-audio-standardizer-5748644544721797280
Fix VHS audio bug by physically cloning video with standardized limits
2026-04-25 21:12:26 +02:00
Meisoftcoltd e4f53ca75a feat(video): add conditional FFmpeg audio standardizer for VHS safety
- Introduced `standardize_video_file` helper in `video.py`.
- The helper creates a physical video clone via FFmpeg, copying the video stream exactly to retain quality while limiting the audio stream.
- Audio limits are conditionally applied using `torchaudio.info` (<=44100Hz and <=2 Channels) to avoid unnecessary processing.
- Implemented a "Fail-Fast" approach with `raise Exception` if the FFmpeg cloning process fails, preventing silent downstream crashes.
- Injected the helper directly into `VideoAnalyzerFaceDetector` and `VideoAnalyzerSceneDetector` to secure the path passed down to VHS.
2026-04-25 18:50:46 +00:00
Meisoft 16604f101c Merge pull request #95 from Meisoftcoltd/refactor/standardize-audio-extraction-7105937645850785286
Standardize audio to 44100Hz/Stereo and cleanup residual files
2026-04-25 15:57:42 +02:00
Meisoftcoltd de0797ddb9 Refactor audio extraction and clean up residual files
- Created `extract_and_standardize_audio` helper in `video.py` to standardize audio to 44100Hz and Stereo format.
- Refactored `VideoAnalyzerFaceDetector` and `VideoAnalyzerSceneDetector` to use the new helper function.
- Permanently deleted `modify_facedetector.py`, `modify_facedetector2.py`, and `modify_facedetector3.py` residual scripts.
2026-04-25 13:46:36 +00:00
Meisoft 771f4bf709 Merge pull request #94 from Meisoftcoltd/feat/scene-cuts-priority-1402347662369726826
Refactor scene cuts priority in loop calculators
2026-04-25 14:23:15 +02:00
Meisoftcoltd d660a6b41b Refactor loop calculators to prioritize scene cuts over face cuts\n\n* Added SCENE_CUTS type to VideoAnalyzerSceneDetector.\n* Replaced "safe_faces_list" usage in loop calculators with "scene_cuts_list" as priority.\n* Removed dead code/scripts: update_video.py, fix_newlines.py, fix_tqdm.py, add_scenedetector.py. 2026-04-25 12:20:48 +00:00
Meisoft 0805acb114 Merge pull request #93 from Meisoftcoltd/video-scene-detector-2048273577937433579
feat(video): add scene detector node and caching for sequential batcher
2026-04-25 08:39:27 +02:00
Meisoftcoltd 5983ff65b4 feat: rename video analyzer to face detector, add global cache, and introduce scene detector node
- Renamed `VideoAnalyzerWithAudio` to `VideoAnalyzerFaceDetector`.
- Introduced `VIDEO_ANALYSIS_CACHE` and `current_loop_index` for cycle caching.
- Created `VideoAnalyzerSceneDetector` to detect scene changes via pixel difference.
- Added dynamic reference frame preview functionality outside the caching logic.
- Registered nodes with proper display names in `__init__.py`.
2026-04-24 21:29:55 +00:00
Meisoft d4e507af7c Merge pull request #91 from Meisoftcoltd/fix-lazy-session-cache-8902851697963122458
Fix aggressive Lazy Evaluation in LazySessionCache
2026-04-22 19:34:37 +02:00
Meisoftcoltd 863154901b fix(switch_node): implement aggressive lazy evaluation for LazySessionCache
- Moved `value_in` to the `optional` group to allow ComfyUI to defer its evaluation.
- Updated `check_lazy_status` to return an empty list `[]` for loops > 0, completely cutting off upstream execution.
- Retained the `GLOBAL_SESSION_CACHE` logic scoped by `unique_id`.
- Removed `@register_node` decorator to prevent duplicate registry issues since mapping is handled in `__init__.py`.
2026-04-22 17:34:09 +00:00
Meisoft 6479883f6d Merge pull request #90 from Meisoftcoltd/feat-primitive-delay-12488753207120426735
Add PrimitiveDelay Tool Node
2026-04-21 23:04:10 +02:00
Meisoftcoltd 89a5f767d7 feat: add PrimitiveDelay tool node for timeline control
Adds a PrimitiveDelay custom node to tools.py that blocks execution for a user-configurable number of seconds, acting as a passthrough using the global `AnyType` wildcard connection. This allows users to throttle rapid API requests, wait for IO tasks to complete, or generally space out ComfyUI execution times in sequential batcher workflows. Re-uses the existing AnyType instance from switch_node.py to adhere to DRY principles.
2026-04-21 21:01:56 +00:00
Meisoft fb35df1526 Merge pull request #89 from Meisoftcoltd/feature/lazy-session-cache-8805062770877095561
Add LazySessionCache node with lazy evaluation
2026-04-21 08:59:51 +02:00
Meisoftcoltd 2551cd5832 feat: add LazySessionCache to prevent redundant input node executions
Adds the LazySessionCache node to `switch_node.py` with lazy evaluation.
It intercepts and stores the input value in cycle 0 and bypasses upstream
evaluation in subsequent loops. Updates `__init__.py` to map the class
and register its display name cleanly.
2026-04-21 06:25:30 +00:00
Meisoft 3eb5dcb7ae Merge pull request #88 from Meisoftcoltd/feat/add-logs-and-rename-debug-9893747542349634290
feat: add log string outputs to all nodes and rename debug prefix
2026-04-19 15:35:51 +02:00
Meisoftcoltd ce6041da4a feat: rename [DEBUG] to [Secuencial Batcher] and add STRING log outputs to all nodes
This commit addresses two primary requests:
1. Replaces all console log prefixes `[DEBUG]` (and variations) with `[Secuencial Batcher]`.
2. Extends the `RETURN_TYPES` and `RETURN_NAMES` of all nodes to output a `log` string containing exactly the text emitted via `print()`. This string is constructed by intercepting `print()` calls inside node methods using a local wrapper and appending the texts to a list, which is then returned joined by newlines.
2026-04-19 13:33:52 +00:00
Meisoft 7d266f818c Merge pull request #87 from Meisoftcoltd/feat/vhs-path-selector-dual-output-1840554855603352002
feat: update VHS_Path_Selector outputs and category
2026-04-18 12:40:45 +02:00
Meisoftcoltd 798d791593 feat(tools): enhance VHS_Path_Selector with dual outputs and reordering
- Added `VHS_FILENAMES` to output alongside `STRING` to allow better integration with downstream nodes (like Telegram).
- Implemented list reordering to place the selected file at index 0, tricking default downstream behavior.
- Moved `VHS_Path_Selector` category to "🔁 Sequential Batcher/Tools" for better organization.
- Added `__pycache__/` to `.gitignore` to prevent tracking compiled binaries.
2026-04-18 09:45:16 +00:00
Meisoft 41d752577c Merge pull request #86 from Meisoftcoltd/add-vhs-path-selector-5637212873751470347
Add VHS Path Selector node
2026-04-17 23:08:36 +02:00
Meisoftcoltd 5be95e32ab Add robust VHS_Path_Selector custom node to tools.py
Added a new versatile custom node, VHS_Path_Selector, that intelligently processes the output of the VHS Video Combine node (VHS_FILENAMES). It features a robust "armored" mode to gracefully handle unexpected inputs (None, missing lists, or invalid types) without crashing the workflow. It allows users to select a specific path (video with audio, muted video, or png thumbnail) via a dropdown, with smart fallbacks to ensure downstream nodes like Telegram bots always receive a valid file path. Debug logs and print statements are maintained in Spanish for consistency.
2026-04-17 20:12:03 +00:00
Meisoft 3cf6c13d17 Merge pull request #85 from Meisoftcoltd/feat/wanvideo-4n-plus-1-rule-2198541111553782481
feat: enforce 4n+1 frame rule for WanVideo VAE padding and calculations
2026-04-12 21:12:33 +02:00
Meisoftcoltd 7187f783b4 feat: enforce 4n+1 frame rule for WanVideo VAE padding and calculations
Replaces the previous "multiple of 4" rule with the `4n + 1` logic across
the AutoLoopCalculatorWan and VAESafeFramePadder nodes. This prevents frame
loss / truncation inside the VAE and ensures absolute consistency for
long generation runs and video reconstruction. Mention of "múltiplo de 4"
in the primary README has also been updated to "regla de 4n + 1".
2026-04-12 19:02:40 +00:00
Meisoft 10e0affe28 Merge pull request #84 from Meisoftcoltd/fix-vae-frame-loss-and-png-cache-15856296271751515989
Fix VAE Frame Loss and Optimize RAM via PNG Cache
2026-04-12 18:16:03 +02:00
Meisoftcoltd 444b6c6513 Fix VAE frame loss by forcing timeline completion on final chunk and optimize stitcher RAM via lightweight PNG caching. 2026-04-12 16:10:56 +00:00
Meisoft 490e3322d7 Merge pull request #83 from Meisoftcoltd/fix/incremental-video-stitcher-overwrite-13565695973057979585
fix(video): blindar el IncrementalVideoStitcher contra bugs de caché y sobreescritura
2026-04-12 16:30:00 +02:00
Meisoftcoltd 8dc273ff16 fix: blindar IncrementalVideoStitcher para evitar sobreescritura de frames
- Se introduce la subcarpeta `meisoft_video_cache`.
- Se guardan los lotes con un timestamp en milisegundos para evitar sobreescrituras en caso de que falle el `current_loop_index`.
- Se incorpora `@classmethod def IS_CHANGED` con `time.time()` para obligar a ComfyUI a ejecutar siempre el nodo e ignorar el cache.
- El directorio temporal se limpia correctamente en el ciclo 0 y se destruye tras ensamblar el vídeo completo.
2026-04-12 14:26:43 +00:00
Meisoft 4c5d15567b Merge pull request #82 from Meisoftcoltd/fix-vram-allocation-and-sync-3903827223534401494
Optimize VRAM Defragmenter execution order and eliminate allocation conflicts
2026-04-12 14:46:51 +02:00
Meisoftcoltd 242ff772a5 fix: optimize VRAM purge order and remove conflicting environment variable
- Removed `PYTORCH_CUDA_ALLOC_CONF` from `__init__.py` to allow elastic VRAM expansion via `expandable_segments:True`.
- Swapped `torch.cuda.empty_cache()` and `torch.cuda.synchronize()` order in `vram_node.py` to ensure pending operations finish before purging cache.
- Added critical warnings in `README.md` and `README_EN.md` discouraging the use of `--highvram` flag when starting ComfyUI to prevent VRAM Defragmenter sabotage.
2026-04-12 12:46:33 +00:00
Meisoft e49056c835 Merge pull request #81 from Meisoftcoltd/fix/timeline-overflow-padding-7170942151912358163
fix: decouple timeline from frame padding to prevent EOF crashes
2026-04-12 12:53:57 +02:00
Meisoftcoltd 0d794fc10e fix: decouple timeline from frame padding to prevent EOF crashes
Replaced artificial safe limit in AutoLoopCalculatorWan (video.py) and
AutoLoopCalculatorLTX (loop.py) with actual physical source frame limit.
This prevents trigger nodes from falsely requesting frames past the EOF
which resulted in "RuntimeError: No frames generated".
2026-04-12 10:26:08 +00:00
Meisoft 55cab6c11b Merge pull request #80 from Meisoftcoltd/update-readme-v1-6-0-8587778050238850121
docs: Update READMEs and bump version to 1.6.0
2026-04-12 12:00:40 +02:00
Meisoftcoltd 74238937dc docs: update README to version 1.6.0 with English translation
Updated README.md with the newly provided v1.6.0 presentation text.
Translated the updated text and updated README_EN.md accordingly.
Bumped __version__ in __init__.py to "1.6.0".
2026-04-12 02:38:05 +00:00
Meisoft f26a7ef983 Merge pull request #79 from Meisoftcoltd/vae-safe-frame-padder-6798265915743811604
Add VAESafeFramePadder for Hold Last Frame expansion
2026-04-12 03:48:39 +02:00
Meisoftcoltd 1b75c40e3e feat: add VAESafeFramePadder and update loop calculators math
- Added VAESafeFramePadder to `tools.py` which clones the last frame
  of an image tensor to satisfy mathematical rules (4n for WanVideo,
  8n+1 for LTX) avoiding audio clipping and VAE crashes.
- Registered the new tool in `__init__.py` under NODE_DISPLAY_NAME_MAPPINGS.
- Updated `AutoLoopCalculatorWan` in `video.py` to pad and round up to a
  multiple of 4.
- Updated `AutoLoopCalculatorLTX` in `loop.py` to pad and round up to an
  8n+1 multiple.
2026-04-12 01:33:46 +00:00
Meisoft 5b70ccaf18 Merge pull request #78 from Meisoftcoltd/jules-fix-frame-absorption-bypass-final-loop-race-condition-18010398085203789983
Fix final batch race condition by delegating global counter reset
2026-04-11 19:30:41 +02:00
Meisoftcoltd 9e893334a1 Fix final batch race condition by delegating global counter reset
Removes the zeroing of `global_loop_index` and `global_accumulated_frames` from the `SequentialLoopTrigger` node.
This architectural shift solves a race condition where the stitcher would process the final chunk based on freshly reset counters (0) instead of the actual completed value, resulting in empty frames or structural bypassing. State cleanup is now exclusively handled by the entry start node.
2026-04-11 17:16:12 +00:00
Meisoft dea765dbcd Merge pull request #77 from Meisoftcoltd/jules-fix-frame-absorption-bypass-2060959048677752393
fix: implement final batch bypass and aggressive frame absorption
2026-04-11 15:06:46 +02:00
Meisoftcoltd d06e88a9b2 fix: implement final batch bypass and aggressive frame absorption
- In `image.py`, added a final batch bypass (`advance_check`) to disable face detection and cropping when the current batch completes the remaining video sequence. This prevents the generation of single-frame dummy batches at the end.
- In `video.py` and `loop.py`, updated `AutoLoopCalculator`, `AutoLoopCalculatorWan`, and `AutoLoopCalculatorLTX` to use an `absorption_threshold = equitable_target * 1.5`. This forces the calculator to absorb up to 50% extra remaining frames in a single batch, eliminating micro-cycles.
2026-04-11 12:24:50 +00:00
Meisoft cdc10e72c9 Merge pull request #76 from Meisoftcoltd/vram-defragmenter-upgrade-7373945555480505137
Upgrade MeisoftVRAMDefragmenter with deep cleanup and visual stats
2026-04-11 04:15:27 +02:00
Meisoftcoltd 7ec112a2cd feat: add multi-platform support to system RAM purging
- Detected OS platform dynamically
- Added `ctypes.windll.kernel32.SetProcessWorkingSetSize` for deep RAM release on Windows
- Retained `malloc_trim` via `libc.so.6` for Linux environments
2026-04-11 02:13:43 +00:00
Meisoftcoltd c214b71b76 feat: upgrade VRAM Defragmenter node for deep memory cleaning
- Implemented dynamic allocator configuration (`expandable_segments:True`)
- Added native ComfyUI internal reference cleanup
- Improved python GC and system RAM purging (`malloc_trim` on Linux)
- Added clear visual debug output displaying VRAM recovered
2026-04-11 02:06:38 +00:00
Meisoft bfa0125a21 Merge pull request #75 from Meisoftcoltd/fix-video-analyzer-cache-3265285744156689374
fix: correct VideoAnalyzerWithAudio caching logic
2026-04-10 10:31:22 +02:00
Meisoftcoltd 2731ab0779 feat(video): add native ComfyUI progress bar to VideoAnalyzerWithAudio
Inject `comfy.utils.ProgressBar` into the frame scanning loop in `VideoAnalyzerWithAudio`. This provides a visual native green progress bar directly on the node in the ComfyUI interface, giving real-time feedback during long video analyses without requiring the user to monitor the terminal output.
2026-04-10 08:30:56 +00:00
Meisoftcoltd 35975007e7 fix(video): fix cache invalidation in VideoAnalyzerWithAudio
Update `IS_CHANGED` in `VideoAnalyzerWithAudio` to rely on the video path
and node parameters instead of `os.path.getmtime` and `time.time()`. This fixes
an issue where the node would constantly invalidate the cache and re-run on
every execution, rather than reusing cached results when inputs have not changed.
2026-04-10 08:19:59 +00:00
Meisoft 7cc86e6b94 Merge pull request #74 from Meisoftcoltd/fix/micro-cycle-death-loop-3346897521993443470
Fix micro-cycle death loops in mathematical frame calculation
2026-04-08 23:28:51 +02:00
Meisoftcoltd 63e84c2aa1 Fix Micro-Cycle Death Loops in frame calculation
This commit patches the `SessionImageSender`, `AutoLoopCalculatorWan`,
and `AutoLoopCalculatorLTX` nodes to prevent infinite loops caused by
extreme truncation at the end of video sequences.

- `image.py`: Ensures `SessionImageSender` accepts the entire batch if no
  faces are detected, rather than defaulting to frame 0, which previously
  trapped the workflow in a 1-frame micro-cycle loop.
- `video.py`: Updates `AutoLoopCalculatorWan` to always round the final
  chunk size up to the nearest multiple of 4, removing the strict
  cropping logic that broke the 3D VAE rule.
- `loop.py`: Updates `AutoLoopCalculatorLTX` to always round the final
  chunk size up to the nearest `8n + 1` value, removing the strict
  cropping logic that broke the DiT rule.
2026-04-08 21:22:08 +00:00
Meisoft df18b573d0 Merge pull request #73 from Meisoftcoltd/vram-defragmentation-wanvideo-13266185802220085488
feat: Implement Deep VRAM Defragmentation for WanVideo
2026-04-08 20:01:44 +02:00
Meisoftcoltd ecd973b8e5 feat: implement deep VRAM defragmentation and update docs
- Enforced a strict memory cleanup sequence in `MeisoftVRAMDefragmenter` (including IPC cleanup and CUDA synchronization).
- Injected `PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128"` in `__init__.py` to prevent VRAM micro-fragmentation.
- Added `unload_detector` option to `VideoAnalyzerWithAudio` to eagerly destroy the face detector model and free VRAM.
- Updated README.md with detailed explanations of the new features.
2026-04-08 12:04:56 +00:00
Meisoftcoltd 1c90c848e2 feat: implement deep VRAM defragmentation and PyTorch memory allocator fix
- Enforced a strict memory cleanup sequence in `MeisoftVRAMDefragmenter` (including IPC cleanup and CUDA synchronization).
- Injected `PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128"` in `__init__.py` to prevent VRAM micro-fragmentation.
- Added `unload_detector` option to `VideoAnalyzerWithAudio` to eagerly destroy the face detector model and free VRAM.
2026-04-08 11:56:02 +00:00
Meisoft 444dc2c350 Merge pull request #72 from Meisoftcoltd/vram-defragmenter-1390025307932295400
Implement VRAM Defragmentation Tactic
2026-04-08 09:56:13 +02:00
Meisoftcoltd 1e36f9e915 feat: implement VRAM defragmentation strategy and optimization
- Created `MeisoftVRAMDefragmenter` in `vram_node.py` as a passthrough to force model unloading, Python garbage collection, and CUDA cache defragmentation.
- Registered the new node in `__init__.py`.
- Added light memory cleanup on intermediate batch loops in `loop.py`, alongside a heavy cleanup (including ctypes `malloc_trim`) at the end.
- Added `unload_detector_after_analysis` boolean flag to `VideoAnalyzerWithAudio` in `video.py` to allow releasing the BBOX detector model immediately after use.
2026-04-08 07:48:48 +00:00
Meisoft 1701eeb218 Merge pull request #71 from Meisoftcoltd/feat/master-switch-lazy-evaluation-17740267901861549413
feat: Implement MasterSwitch for intelligent lazy branch evaluation
2026-04-07 22:12:11 +02:00
Meisoftcoltd 3f9c7cbee1 docs: update READMEs with MasterSwitch documentation
- Add Spanish and English descriptions for the new MasterSwitch node.
- Highlight Native Lazy Evaluation and Hot Graph Surgery features.
2026-04-07 20:11:40 +00:00
Meisoftcoltd ff5643b098 feat: add MasterSwitch node with lazy evaluation and graph surgery
- Implement `switch_node.py` with `MasterSwitch` handling native lazy evaluation.
- Register `MasterSwitch` in `__init__.py` using clean names.
- Intercept and apply Hot Graph Surgery in `loop.py` to strip out unneeded nodes in auto loop cycles for reduced VRAM usage.
2026-04-07 19:23:01 +00:00
Meisoft e647ac0728 Merge pull request #69 from Meisoftcoltd/feat/conditional-audio-router-14058830236118345976
feat: add ConditionalAudioRouter for bypassing audio processes in intermediate cycles
2026-04-06 10:31:34 +02:00
Meisoftcoltd 6080f61c27 feat: Add ConditionalAudioRouter node for Lip Sync bypass
Added `ConditionalAudioRouter` to `tools.py` with `torch` import to generate an empty audio tensor for bypassing a LipSync node during intermediate loops without breaking it. Registered node to `__init__.py`.
2026-04-06 08:08:48 +00:00
Meisoft 9cc66a4c80 Merge pull request #68 from Meisoftcoltd/fix/sequential-batcher-final-cycle-15656548652125589395
Fix Sequential Batcher intermediate behaviors on final cycle
2026-04-05 23:36:11 +02:00
Meisoftcoltd bd6016f559 fix: bypass keyframe save and output full tensor on final cycle
- Updated `SessionImageSender` to conditionally save keyframe only when not in the final cycle.
- Updated `IncrementalVideoStitcher` to return full concatenated tensor when in the final cycle, avoiding dummy frame output and blocking downstream nodes.
- Expose `IS_FINAL_CYCLE` boolean from `IncrementalVideoStitcher` to UI for downstream conditionals.
2026-04-05 21:32:14 +00:00
Meisoft 8a0ceca136 Merge pull request #67 from Meisoftcoltd/fix-silence-and-transparency-11065722630316388106
fix: Silence YOLO outputs and improve transparency in calculators
2026-04-05 19:07:58 +02:00
Meisoftcoltd 608be49841 fix: Silence YOLO outputs and improve transparency in calculators 2026-04-05 17:03:15 +00:00
Meisoft c7c8c93c9c Merge pull request #66 from Meisoftcoltd/fix/video-detector-tqdm-18028740387197632355
Fix bbox detector tuple error, silence YOLO spam, and add tqdm to video scan
2026-04-05 13:53:50 +02:00
Meisoftcoltd a6d8ea88dc docs: add documentation for GPU-accelerated face scanning
- Added Spanish documentation block to `README.md` explaining how to use the new `bbox_detector` port with Impact Pack.
- Added English documentation block to `README_EN.md` with the same instructions.
2026-04-05 11:33:59 +00:00
Meisoftcoltd 93d479fb6c fix: correct ImpactPack tuple unpacking and add tqdm to video frame scan
- Fixed tuple index extraction (`res[0]`) for `SEGS` object in bbox_detector.
- Silenced Ultralytics/YOLO console spam by wrapping detection logic in `redirect_stdout`.
- Upgraded the `while True:` loop to a `for` loop with `tqdm` for a clean progress bar, while preserving the RGB frame conversion logic for accuracy.
2026-04-05 11:26:17 +00:00
Meisoft 30670f0602 Merge pull request #65 from Meisoftcoltd/patch-loop-calculators-6028840695815220919
Implement proportional adjustment in Loop Calculators
2026-04-05 11:52:57 +02:00
13 changed files with 1500 additions and 361 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__/
+55 -39
View File
@@ -1,19 +1,25 @@
# ♾️ ComfyUI Sequential Batcher
**v1.5.2** | **VRAM-Optimized Generation** | **Recursive Self-Queuing**
**v1.6.0** | **VRAM-Optimized Generation** | **Recursive Self-Queuing** | **Perfect Audio Sync**
Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada para sortear los límites de VRAM en la generación de vídeo pesado (WanVideo, Hunyuan, etc.) mediante **Auto-Encolado Recursivo** y procesamiento autónomo lote por lote.
Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada para sortear los límites extremos de VRAM en la generación de vídeo pesado (WanVideo, Hunyuan, LTX) mediante **Auto-Encolado Recursivo**, procesamiento autónomo lote por lote y gestión de memoria forense.
> 🌍 **Read in English:** [README_EN.md](README_EN.md)
---
## 🌟 Lo Nuevo en v1.6.0 (Perfect Sync Update)
* **🛡️ VAE Safe Frame Padder (Hold Last Frame):** El motor ahora expande dinámicamente los tensores incompletos al final del vídeo. Si faltan frames para cumplir los estrictos requisitos de WanVideo (regla de `4n + 1`) o LTX (8n+1), clona el último fotograma de forma imperceptible. **Resultado: Cero cuelgues del VAE y 100% de sincronización de audio sin micro-cortes.**
* **📈 Inversión Matemática a "Expansión":** Los calculadores `AutoLoopCalculatorWan` y `AutoLoopCalculatorLTX` ahora redondean el timeline de forma segura hacia arriba, garantizando fluidez total en ciclos intermedios.
---
## ✨ Características Clave
* **Orquestación Autónoma:** Convierte tu ComfyUI en un motor de renderizado continuo. Pulsa "Queue" una sola vez y el flujo se retroalimentará hasta terminar todo el vídeo.
* **Cortes Inteligentes (Smart Chunking):** Analiza el vídeo base y realiza cortes matemáticos evitando separar frames donde los rostros son más nítidos, manteniendo la coherencia de identidad.
* **Extracción de Audio Nativa:** Extrae la pista de audio original directamente desde el nodo inicial y la inyecta de vuelta en el ensamblaje final.
* **Dual Input Engine:** El nodo explorador acepta tanto vídeos subidos manualmente como rutas inyectadas por cable desde descargadores (ej. YTDLP) comportándose exactamente como VideoHelperSuite.
* **Soporte Nativo LTX 2.3:** Se han añadido calculadores matemáticos específicos (`AutoLoopCalculatorLTX` y `ResTool64xLTX`) para garantizar que los lotes de vídeo cumplan con las estrictas reglas de descompresión de arquitectura DiT y el factor de escala latente (resoluciones divisibles por 64 y particiones de fotogramas de `8n + 1`).
* **Orquestación Autónoma:** Convierte tu ComfyUI en un motor de renderizado continuo. Pulsa "Queue Prompt" una sola vez y el flujo se retroalimentará modificando semillas e índices hasta terminar todo el vídeo.
* **Cortes Inteligentes (Smart Chunking):** Analiza el vídeo base y realiza cortes matemáticos protegiendo los frames donde los rostros son más nítidos, manteniendo la coherencia de identidad (Face Cuts).
* **Extracción de Audio Nativa:** Extrae la pista original directamente desde el nodo inicial y la inyecta de vuelta en el ensamblaje final.
* **Soporte Arquitecturas DiT (WanVideo & LTX 2.3):** Calculadores matemáticos específicos aseguran que los lotes de vídeo cumplan con las estrictas reglas de descompresión (regla de `4n + 1` para WanVideo, o regla `8n + 1` para LTX).
* **Resoluciones Protegidas (Megapixel Shield):** Herramientas matemáticas (`ResTool`) que escalan las dimensiones automáticamente protegiendo el *training floor* de cada modelo base.
---
@@ -22,46 +28,56 @@ Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada p
Para que el nodo `VideoAnalyzerWithAudio` pueda desempaquetar contenedores `.mp4` y extraer el audio con `torchaudio`, **FFmpeg es absolutamente obligatorio**.
* **🐧 Ubuntu / Linux / WSL2:**
El script de ComfyUI Manager intentará instalarlo por ti. Si falla, ejecuta:
El script intentará instalarlo por ti. Si falla, ejecuta manualmente:
```bash
sudo apt update && sudo apt install ffmpeg -y
```
* **🪟 Windows:**
Descarga los binarios precompilados de FFmpeg y añade la carpeta bin a tus variables de entorno (PATH).
Descarga los binarios precompilados de FFmpeg y añade la carpeta `bin` a tus variables de entorno (PATH).
## 🧠 La Arquitectura Híbrida (Cómo funciona)
El procesamiento de vídeo se divide en roles altamente especializados:
### 🚨 Advertencia Crítica sobre el Uso de la VRAM
Se **desrecomienda encarecidamente** el uso del argumento `--highvram` al arrancar ComfyUI.
* **🕵️ Video Analyzer Face detector + Audio (El Explorador):** Escanea el vídeo vía OpenCV o mediante un modelo YOLO/ONNX acelerado por GPU (opcional) para detección precisa de rostros, extrae el audio y escupe un Frame de Referencia visual. Actúa como la puerta de entrada principal.
* **📊 Auto Loop Calculator (El Cerebro):** Recibe la biometría del Explorador y calcula las coordenadas de corte asimétricas (chunk_frames, skip_frames) con un margen dinámico del ±10% para evitar minilotes residuales.
* **📊 Auto Loop Calculator (WanVideo 3dVAE):** Alternativa para WanVideo que asegura que los chunks de frames sean estrictamente múltiplos de 4, protegiendo al VAE 3D de colapsar.
* **🛠️ El Obrero (VHS_LoadVideo):** Liberado de tareas de análisis, este nodo estándar de ComfyUI se limita a extraer los tensores exactos que el Cerebro le ordena.
* **🎞️ Incremental Auto-Stitcher (El Ensamblador):** Recoge los lotes renderizados y la pista de audio original, cosiendo el vídeo final de forma progresiva.
* **📥 Session Image Receiver** y **📤 Session Image Sender:** Retienen y transfieren el último keyframe válido (frame de referencia) a través de los ciclos para mantener la coherencia temporal de la identidad.
* **⏱️ Auto FPS Limiter (El Sincronizador):** Previene errores de VRAM en vídeos de alta tasa de fotogramas (ej. 60 FPS) calculando automáticamente el salto de frames necesario (`select_every_nth`) y ajustando los FPS finales para garantizar que el audio y el movimiento mantengan una sincronización perfecta.
* **Resoluciones Protegidas (Megapixel Shield):** Herramientas de resolución matemáticas que escalan las dimensiones para evitar artefactos protegiendo el "training floor" de cada modelo:
* **📐 ResTool 8x (SD1.5)**
* **📏 ResTool 16x (SDXL)**
* **🎞️ ResTool 32x (WanVideo)**
* **🎬 ResTool 64x (Hunyuan)**
El argumento `--highvram` le da la orden estricta a ComfyUI de mantener los modelos bloqueados en la memoria de video. Esto sabotea directamente el funcionamiento de nuestro `VRAM Defragmenter`, ya que bloquea el comando `mm.unload_all_models()` impidiendo que la gráfica se vacíe entre ciclos. Para que esta suite funcione correctamente con modelos masivos como WanVideo y evite errores de OOM (Out Of Memory), los usuarios deben usar el arranque estándar (o `--normalvram`), permitiendo que el sistema libere dinámicamente la memoria hasta llegar a 0 GB entre lotes.
## 🔌 Guía de Cableado (Workflow Setup)
Para montar el flujo secuencial perfecto, sigue estos pasos:
---
1. **Setup Inicial:** Coloca el 🕵️ Video Analyzer Face detector + Audio al principio. Sube un vídeo o conéctale un cable STRING desde tu descargador favorito. Opcionalmente, conecta un nodo 'ONNX Detection Model Loader' a su entrada `bbox_detector` para escaneo de rostros por GPU.
2. **Matemáticas:** Conecta total_frames y safe_faces_list del Explorador hacia el 📊 Auto Loop Calculator (o 📊 Auto Loop Calculator (WanVideo 3dVAE) si usas WanVideo).
3. **Extracción:** Añade un nodo VHS_LoadVideo. Haz clic derecho en él -> Convert Widget to Input -> video. Conecta el video_name del Explorador a esta nueva entrada. Alimenta los parámetros de corte desde el Cerebro.
4. **Passthrough de Audio:** Lleva el cable source_audio desde el Explorador hasta el puerto audio de tu 🎞️ Incremental Auto-Stitcher (al final del flujo).
5. **El Gatillo:** Conecta la salida de tu Stitcher a la entrada del 🚀 Loop Trigger (Auto-Queue).
6. **Ejecución:** Conecta el 🏁 Loop Start (Index) al Cerebro y al Stitcher. Dale a "Queue Prompt" UNA SOLA VEZ (sin marcar Auto Queue en la UI). ¡Disfruta de la magia autónoma!
## 🧠 La Arquitectura Híbrida (Desglose de Nodos)
El procesamiento se divide en roles altamente especializados:
## 🧹 Auto-Limpieza de VRAM (NUEVO)
El nodo `SequentialLoopTrigger` ahora actúa como un gestor de limpieza inteligente. Una vez que detecta que se han procesado todos los frames y la generación del video ha finalizado, automáticamente:
1. Obliga a ComfyUI a soltar de memoria todos los modelos pesados (WanVideo, VAE, etc.).
2. Fuerza al recolector de basura de Python para eliminar memoria residual en la RAM.
3. Vacía profundamente las cachés de PyTorch, soportando múltiples plataformas (CUDA/ROCm para NVIDIA/AMD, y MPS para Apple Silicon).
* **🕵️ Video Analyzer Face detector + Audio:** Escanea el vídeo vía OpenCV o mediante un modelo YOLO acelerado por GPU. Extrae rostros, el audio y el Frame de Referencia.
* **📊 Auto Loop Calculator (Base / WanVideo / LTX):** Recibe la biometría y calcula las coordenadas de corte (chunk_frames, skip_frames).
* **🛡️ VAE Safe Frame Padder:** Intercepta el tensor final de VHS y lo rellena ("acolcha") clonando el último frame si falta material, salvando al VAE de un colapso.
* **🎞️ Incremental Auto-Stitcher:** Recoge los lotes renderizados y la pista de audio, cosiendo el vídeo final de forma progresiva en la carpeta temporal.
* **📥 Receiver & 📤 Sender:** Retienen y transfieren el último keyframe válido a través de los ciclos para mantener la coherencia temporal.
* **⏱️ Auto FPS Limiter:** Reduce los FPS inteligentemente garantizando que el audio y el movimiento mantengan una sincronización perfecta sin romper la VRAM.
* **🔀 Master Switch (Evaluación Perezosa):** Amputa físicamente los cables de rutas inactivas en el JSON enviado a ComfyUI. Los nodos pesados que no se necesitan en un ciclo específico ni siquiera se cargan en memoria.
* **🧹 VRAM Defragmenter:** Purga forense de memoria (Secuencia Sagrada) que limpia la caché de CUDA/MPS y fuerza al Garbage Collector entre ciclos pesados.
¡Esto asegura que tu gráfica vuelva a 0GB de uso al terminar, previniendo errores OOM en tus siguientes generaciones!
---
**Changelog v1.5.3:** Se ha introducido un sistema de limpieza profunda automática de VRAM en el nodo `SequentialLoopTrigger`, garantizando que la memoria de la GPU (y RAM) se libere completamente tras la generación de videos pesados en arquitecturas CUDA, ROCm y Mac MPS.
**Changelog v1.5.2:** Instalación automatizada de FFmpeg, mejora crítica en el bypass del pre-flight check de ComfyUI para inputs dinámicos en el Analyzer, y exposición detallada de excepciones de torchaudio.
## 🔌 Guía de Cableado Rápido (Workflow Setup)
1. **El Explorador:** Coloca el `Video Analyzer` al principio. Sube un vídeo (o usa una ruta de YTDLP).
2. **Matemáticas:** Conecta `total_frames` y `safe_faces_list` hacia tu `Auto Loop Calculator` elegido.
3. **Extracción Segura:** Añade el nodo nativo `VHS_LoadVideo`. Conviértele la entrada a *video_name* y conéctala al Analyzer. Conecta su salida `IMAGE` al nuevo nodo `VAE Safe Frame Padder`.
4. **Passthrough de Audio:** Lleva el cable `source_audio` desde el Explorador directamente hasta el puerto `audio` de tu `Incremental Auto-Stitcher` (al final del flujo).
5. **El Gatillo:** Conecta la salida de tu Stitcher al nodo `Loop Trigger (Auto-Queue)`.
6. **Ejecución:** Conecta el `Loop Start (Index)` al Cerebro y al Stitcher.
7. **Dispara:** Haz clic en "Queue Prompt" **UNA SOLA VEZ**. ¡Disfruta de la magia autónoma!
---
## 🚀 Aceleración por GPU (Escaneo de Rostros)
El `VideoAnalyzerWithAudio` tiene un puerto `bbox_detector` para delegar el escaneo facial a la tarjeta gráfica:
1. Instala el *Impact Pack*.
2. Añade el nodo `UltralyticsDetectorProvider` y selecciona un modelo de rostros (ej. `face_yolov8m.pt`).
3. Conecta su salida al puerto del Analyzer.
*(El nodo cuenta con auto-descarga: destruirá este modelo de la VRAM al terminar para hacer hueco a la generación).*
---
## 📝 Changelog Reciente
* **v1.6.0:** Implementación del `VAESafeFramePadder` (Hold Last Frame) y rediseño matemático a modo Expansión. Adiós a los recortes de audio y a los cuelgues en el ciclo final de WanVideo y LTX.
* **v1.5.4:** Inyección de la variable de entorno de PyTorch `max_split_size_mb:128` para evitar la micro-fragmentación de VRAM con kernels de Triton (SageAttention). Auto-descarga temprana de modelos YOLO.
* **v1.5.3:** "Secuencia Sagrada" en el VRAM Defragmenter (Romper ciclos -> Evacuación Suave -> Limpieza IPC -> Sincronización de hilos).
+57 -41
View File
@@ -1,67 +1,83 @@
# ♾️ ComfyUI Sequential Batcher
**v1.5.2** | **VRAM-Optimized Generation** | **Recursive Self-Queuing**
**v1.6.0** | **VRAM-Optimized Generation** | **Recursive Self-Queuing** | **Perfect Audio Sync**
A professional-grade suite of custom nodes for ComfyUI. Designed to bypass VRAM limitations in heavy video generation (WanVideo, Hunyuan, etc.) through **Recursive Self-Queuing** and autonomous, batch-by-batch processing.
A professional-grade suite of custom nodes for ComfyUI. Designed to bypass extreme VRAM limits in heavy video generation (WanVideo, Hunyuan, LTX) through **Recursive Auto-Queuing**, autonomous batch-by-batch processing, and forensic memory management.
> 🌍 **Leer en Español:** [README.md](README.md)
> 🇪🇸 **Leer en Español:** [README.md](README.md)
---
## 🌟 What's New in v1.6.0 (Perfect Sync Update)
* **🛡️ VAE Safe Frame Padder (Hold Last Frame):** The engine now dynamically expands incomplete tensors at the end of the video. If frames are missing to meet the strict requirements of WanVideo (multiples of 4) or LTX (8n+1), it clones the last frame imperceptibly. **Result: Zero VAE crashes and 100% audio sync with no micro-stutters.**
* **📈 Mathematical Inversion to "Expansion":** The `AutoLoopCalculatorWan` and `AutoLoopCalculatorLTX` calculators now safely round the timeline upwards, guaranteeing total fluidity in intermediate cycles.
---
## ✨ Key Features
* **Autonomous Orchestration:** Turn your ComfyUI into a continuous rendering engine. Press "Queue" once and the workflow will feed itself until the entire video is finished.
* **Smart Chunking:** Analyzes the base video and performs mathematical cuts avoiding the separation of frames where faces are sharpest, thus maintaining identity coherence.
* **Native Audio Extraction:** Extracts the original audio track straight from the entry node and seamlessly injects it back into the final stitch.
* **Dual Input Engine:** The explorer node accepts both manually uploaded videos and dynamically injected string paths from downloaders (e.g., YTDLP), behaving exactly like VideoHelperSuite.
* **Native LTX 2.3 Support:** Specific mathematical calculators (`AutoLoopCalculatorLTX` and `ResTool64xLTX`) have been added to ensure video batches comply with the strict DiT architecture decompression rules and latent downscale factors (resolutions strictly divisible by 64 and frame partitions of `8n + 1`).
* **Autonomous Orchestration:** Turn your ComfyUI into a continuous rendering engine. Press "Queue Prompt" just once and the flow will feed itself by modifying seeds and indices until the entire video is finished.
* **Smart Chunking:** Analyzes the base video and makes mathematical cuts protecting the frames where faces are sharpest, maintaining identity coherence (Face Cuts).
* **Native Audio Extraction:** Extracts the original track directly from the initial node and injects it back into the final assembly.
* **DiT Architectures Support (WanVideo & LTX 2.3):** Specific mathematical calculators ensure video batches comply with strict decompression rules (multiples of 4 for WanVideo, or the `8n + 1` rule for LTX).
* **Protected Resolutions (Megapixel Shield):** Mathematical tools (`ResTool`) that automatically scale dimensions protecting the *training floor* of each base model.
---
## ⚠️ System Requirements (Critical)
For the `VideoAnalyzerWithAudio` node to unpack `.mp4` containers and extract audio via `torchaudio`, **FFmpeg is absolutely mandatory**.
For the `VideoAnalyzerWithAudio` node to unpack `.mp4` containers and extract audio with `torchaudio`, **FFmpeg is absolutely mandatory**.
* **🐧 Ubuntu / Linux / WSL2:**
The ComfyUI Manager script will attempt to install it for you. If it fails, run:
The script will attempt to install it for you. If it fails, run manually:
```bash
sudo apt update && sudo apt install ffmpeg -y
```
* **🪟 Windows:**
Download the pre-compiled binaries from FFmpeg and add the bin folder to your environment variables (PATH).
Download the precompiled FFmpeg binaries and add the `bin` folder to your environment variables (PATH).
## 🧠 The Hybrid Architecture (How it works)
Video processing is divided into highly specialized roles:
### 🚨 Critical Warning Regarding VRAM Usage
The use of the `--highvram` argument when starting ComfyUI is **strongly discouraged**.
* **🕵️ Video Analyzer Face detector + Audio (The Explorer):** Scans the video via OpenCV or through an optional GPU-accelerated YOLO/ONNX model for precise face detection, extracts the audio and outputs a visual Reference Frame. Acts as the main gateway.
* **📊 Auto Loop Calculator (The Brain):** Receives biometrics from the Explorer and calculates asymmetric cut coordinates (chunk_frames, skip_frames) featuring a ±10% dynamic margin to avoid residual micro-batches.
* **📊 Auto Loop Calculator (WanVideo 3dVAE):** An alternative for WanVideo that ensures frame chunks are strictly multiples of 4, protecting the 3D VAE from crashing.
* **🛠️ The Worker (VHS_LoadVideo):** Freed from analytical tasks, this standard ComfyUI node simply extracts the exact tensors the Brain commands.
* **🎞️ Incremental Auto-Stitcher (The Assembler):** Collects the rendered batches and the pristine original audio track, progressively stitching the final video.
* **📥 Session Image Receiver** and **📤 Session Image Sender:** Hold and pass the last valid keyframe (reference frame) across iterations to maintain temporal identity coherence.
* **⏱️ Auto FPS Limiter (The Synchronizer):** Prevents VRAM OOM errors on high framerate videos (e.g., 60 FPS) by automatically calculating the required frame skip (`select_every_nth`) and adjusting the final FPS to ensure audio and motion maintain perfect synchronization.
* **Protected Resolutions (Megapixel Shield):** Mathematical resolution tools that scale dimensions to avoid artifacts by protecting the "training floor" of each model:
* **📐 ResTool 8x (SD1.5)**
* **📏 ResTool 16x (SDXL)**
* **🎞️ ResTool 32x (WanVideo)**
* **🎬 ResTool 64x (Hunyuan)**
The `--highvram` argument gives ComfyUI the strict command to keep models locked in video memory. This directly sabotages the operation of our `VRAM Defragmenter`, as it blocks the `mm.unload_all_models()` command, preventing the graphics card from emptying between cycles. For this suite to work correctly with massive models like WanVideo and avoid OOM (Out Of Memory) errors, users must use the standard startup (or `--normalvram`), allowing the system to dynamically release memory down to 0 GB between batches.
## 🔌 Wiring Guide (Workflow Setup)
To build the perfect sequential workflow, follow these steps:
---
1. **Initial Setup:** Place the 🕵️ Video Analyzer Face detector + Audio at the beginning. Upload a video or connect a STRING cable to it from your favorite downloader. Optionally, connect an 'ONNX Detection Model Loader' node to its `bbox_detector` input for GPU face scanning.
2. **The Math:** Route total_frames and safe_faces_list from the Explorer into the 📊 Auto Loop Calculator (or 📊 Auto Loop Calculator (WanVideo 3dVAE) if using WanVideo).
3. **Extraction:** Add a VHS_LoadVideo node. Right-click on it -> Convert Widget to Input -> video. Connect the video_name from the Explorer to this new input. Feed the cutting parameters from the Brain.
4. **Audio Passthrough:** Route the source_audio cable from the Explorer all the way to the audio port of your 🎞️ Incremental Auto-Stitcher (at the end of your workflow).
5. **The Trigger:** Connect your Stitcher's output to the 🚀 Loop Trigger (Auto-Queue) input.
6. **Execution:** Wire the 🏁 Loop Start (Index) to the Brain and the Stitcher. Press "Queue Prompt" ONLY ONCE (do not check Auto Queue in the UI). Sit back and enjoy the autonomous magic!
## 🧠 The Hybrid Architecture (Node Breakdown)
Processing is divided into highly specialized roles:
## 🧹 Auto VRAM Cleanup (NEW)
The `SequentialLoopTrigger` node now acts as an intelligent cleanup manager. Once it detects that all frames have been processed and video generation has finished, it automatically:
1. Forces ComfyUI to release all heavy models from memory (WanVideo, VAE, etc.).
2. Triggers Python's garbage collector to eliminate residual memory in RAM.
3. Deeply empties PyTorch caches, supporting multiple platforms (CUDA/ROCm for NVIDIA/AMD, and MPS for Apple Silicon).
* **🕵️ Video Analyzer Face detector + Audio:** Scans the video via OpenCV or using a GPU-accelerated YOLO model. Extracts faces, audio, and the Reference Frame.
* **📊 Auto Loop Calculator (Base / WanVideo / LTX):** Receives the biometrics and calculates cut coordinates (chunk_frames, skip_frames).
* **🛡️ VAE Safe Frame Padder:** Intercepts the final VHS tensor and pads it by cloning the last frame if material is missing, saving the VAE from a collapse.
* **🎞️ Incremental Auto-Stitcher:** Collects rendered batches and the audio track, progressively stitching the final video in the temporary folder.
* **📥 Receiver & 📤 Sender:** Retain and transfer the last valid keyframe across cycles to maintain temporal coherence.
* **⏱️ Auto FPS Limiter:** Smartly reduces FPS guaranteeing audio and motion maintain perfect synchronization without breaking VRAM.
* **🔀 Master Switch (Lazy Evaluation):** Physically amputates wires from inactive routes in the JSON sent to ComfyUI. Heavy nodes not needed in a specific cycle aren't even loaded into memory.
* **🧹 VRAM Defragmenter:** Forensic memory purge (Sacred Sequence) that clears CUDA/MPS cache and forces the Garbage Collector between heavy cycles.
This ensures your GPU returns to 0GB usage upon completion, preventing OOM errors in your subsequent generations!
---
**Changelog v1.5.3:** Introduced an automatic deep VRAM cleanup system within the `SequentialLoopTrigger` node, ensuring complete release of GPU (and RAM) memory after generating heavy videos across CUDA, ROCm, and Mac MPS architectures.
**Changelog v1.5.2:** Automated FFmpeg installation, critical enhancement on ComfyUI pre-flight check bypass for dynamic inputs in the Analyzer, and detailed exposure of torchaudio exceptions.
## 🔌 Quick Wiring Guide (Workflow Setup)
1. **The Explorer:** Place the `Video Analyzer` at the beginning. Upload a video (or use a YTDLP path).
2. **Math:** Connect `total_frames` and `safe_faces_list` to your chosen `Auto Loop Calculator`.
3. **Safe Extraction:** Add the native `VHS_LoadVideo` node. Convert its input to *video_name* and connect it to the Analyzer. Connect its `IMAGE` output to the new `VAE Safe Frame Padder` node.
4. **Audio Passthrough:** Route the `source_audio` wire from the Explorer directly to the `audio` port of your `Incremental Auto-Stitcher` (at the end of the flow).
5. **The Trigger:** Connect the output of your Stitcher to the `Loop Trigger (Auto-Queue)` node.
6. **Execution:** Connect the `Loop Start (Index)` to the Brain and the Stitcher.
7. **Fire:** Click "Queue Prompt" **JUST ONCE**. Enjoy the autonomous magic!
---
## 🚀 GPU Acceleration (Face Scanning)
The `VideoAnalyzerWithAudio` has a `bbox_detector` port to delegate facial scanning to the graphics card:
1. Install the *Impact Pack*.
2. Add the `UltralyticsDetectorProvider` node and select a face model (e.g. `face_yolov8m.pt`).
3. Connect its output to the Analyzer's port.
*(The node features auto-unload: it will destroy this model from VRAM upon completion to make room for generation).*
---
## 📝 Recent Changelog
* **v1.6.0:** Implementation of `VAESafeFramePadder` (Hold Last Frame) and mathematical redesign to Expansion mode. Goodbye to audio cuts and crashes in the final cycle of WanVideo and LTX.
* **v1.5.4:** Injection of the PyTorch environment variable `max_split_size_mb:128` to avoid VRAM micro-fragmentation with Triton kernels (SageAttention). Early YOLO model auto-unload.
* **v1.5.3:** "Sacred Sequence" in the VRAM Defragmenter (Break cycles -> Soft Evacuation -> IPC Cleanup -> Thread Synchronization).
+18 -3
View File
@@ -1,4 +1,6 @@
__version__ = "1.5.1"
import os
__version__ = "1.6.0"
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -9,25 +11,38 @@ def register_node(c, display_name=None):
return c
# Importar tools
from . import image, loop, video, tools
from . import image, loop, video, tools, vram_node
from .switch_node import MasterSwitch, LazySessionCache
NODE_CLASS_MAPPINGS.update({
"MasterSwitch": MasterSwitch,
"LazySessionCache": LazySessionCache,
})
NODE_DISPLAY_NAME_MAPPINGS.update({
"MasterSwitch": "🔀 Master Switch",
"LazySessionCache": "🗄️ Lazy Session Cache",
"MeisoftVRAMDefragmenter": "🧹 VRAM Defragmenter",
"SessionImageReceiver": "📥 Session Image Receiver",
"SessionImageSender": "📤 Session Image Sender",
"IncrementalVideoStitcher": "🎞️ Incremental Auto-Stitcher",
"VideoAnalyzerWithAudio": "🕵️ Video Analyzer Face detector + Audio",
"VideoAnalyzerFaceDetector": "🕵️ Video Analyzer Face detector + Audio",
"VideoAnalyzerSceneDetector": "🎬 Video Analyzer Scene detector",
"AutoLoopCalculator": "📊 Auto Loop Calculator",
"AutoLoopCalculatorWan": "📊 Auto Loop Calculator (WanVideo 3dVAE)",
"AutoLoopCalculatorLTX": "📊 Auto Loop Calculator (LTX 2.3)",
"AutoLoopCalculatorTTS": "📊 Auto Loop Calculator (TTS/Texto)",
"SequentialLoopStart": "🏁 Loop Start (Index)",
"SequentialLoopTrigger": "🚀 Loop Trigger (Auto-Queue)",
# Tools
"AutoFPSLimiter": "⏱️ Auto FPS Limiter",
"VAESafeFramePadder": "🛡️ VAE Safe Frame Padder",
"ResTool8x": "📐 ResTool 8x (SD1.5)",
"ResTool16x": "📏 ResTool 16x (SDXL)",
"ResTool32x": "🎞️ ResTool 32x (WanVideo)",
"ResTool64x": "🎬 ResTool 64x (Hunyuan)",
"ResTool64xLTX": "🌌 ResTool 64x (LTX 2.3)",
"ConditionalAudioRouter": "🎛️ Conditional Audio Router (Bypass)",
})
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+68 -34
View File
@@ -29,8 +29,8 @@ class SessionImageReceiver:
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("current_image",)
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("current_image", "log")
OUTPUT_NODE = True
FUNCTION = "get_image"
CATEGORY = "🔁 Sequential Batcher/Image"
@@ -40,6 +40,10 @@ class SessionImageReceiver:
return time.time()
def get_image(self, initial_image, current_loop_index):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
from . import loop
global global_session_image
@@ -50,23 +54,23 @@ class SessionImageReceiver:
accumulated = getattr(loop, 'global_accumulated_frames', 0)
source_total = getattr(loop, 'global_source_frame_count', 1)
print(f"\n{'='*50}")
print(f"📥 [DEBUG] NODO: Image Receiver")
print(f" -> Ciclo actual: {loop_idx} | Progreso global: {accumulated} / {source_total} frames")
_log(f"\n{'='*50}")
_log(f"📥 [Secuencial Batcher] NODO: Image Receiver")
_log(f" -> Ciclo actual: {loop_idx} | Progreso global: {accumulated} / {source_total} frames")
if is_first or global_session_image is None:
global_session_image = initial_image.clone().cpu()
print(f" -> 🆕 Iniciando sesión con la imagen ORIGINAL.")
_log(f" -> 🆕 Iniciando sesión con la imagen ORIGINAL.")
selected = initial_image
else:
print(f" -> ♻️ Usando el Keyframe validado y rescatado de la RAM.")
_log(f" -> ♻️ Usando el Keyframe validado y rescatado de la RAM.")
selected = global_session_image
print(f" -> 🖼️ Tensor shape: {selected.shape}")
print(f"{'='*50}\n")
_log(f" -> 🖼️ Tensor shape: {selected.shape}")
_log(f"{'='*50}\n")
ui_image = tensor_to_temp_image(selected, "receiver")
return {"ui": {"images": [ui_image]}, "result": (selected, )}
return {"ui": {"images": [ui_image]}, "result": (selected, "\n".join(log_output))}
@register_node
class SessionImageSender:
@@ -80,8 +84,8 @@ class SessionImageSender:
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("VALIDATED_IMAGES",)
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("VALIDATED_IMAGES", "log")
OUTPUT_NODE = True
FUNCTION = "set_image"
CATEGORY = "🔁 Sequential Batcher/Image"
@@ -91,6 +95,10 @@ class SessionImageSender:
return time.time()
def set_image(self, generated_images, current_loop_index, detect_faces):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
from . import loop
import cv2
import numpy as np
@@ -105,16 +113,27 @@ class SessionImageSender:
batch_size = generated_images.shape[0]
best_idx = batch_size - 1
print(f"\n{'='*50}")
print(f"📤 [DEBUG] NODO: Image Sender (Filtro Dinámico)")
print(f" -> Frames recibidos de la IA: {batch_size}")
accumulated = getattr(loop, 'global_accumulated_frames', 0)
source_total = getattr(loop, 'global_source_frame_count', 1)
stride = getattr(loop, 'global_select_every_nth', 1)
ltx_mode_active = getattr(loop, 'global_ltx_mode', False)
_log(f"\n{'='*50}")
_log(f"📤 [Secuencial Batcher] NODO: Image Sender (Filtro Dinámico)")
_log(f" -> Frames recibidos de la IA: {batch_size}")
# 🧠 BYPASS IMPLACABLE: Si al sumar este lote llegamos al final del video, NO RECORTAMOS
advance_check = (batch_size - 1) * stride if ltx_mode_active else batch_size * stride
if (accumulated + advance_check) >= source_total:
_log(f" -> 🏁 LOTE FINAL ABSOLUTO DETECTADO. Desactivando recorte de rostros para forzar la salida de TODOS los frames.")
detect_faces = False
if detect_faces:
try:
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(cascade_path)
print(f" -> 🕵️ Buscando el último rostro frontal en reversa (Desde frame {batch_size - 1} hasta 0)...")
_log(f" -> 🕵️ Buscando el último rostro frontal en reversa (Desde frame {batch_size - 1} hasta 0)...")
found = False
for i in range(batch_size - 1, -1, -1):
@@ -125,16 +144,16 @@ class SessionImageSender:
if len(faces) > 0:
best_idx = i
found = True
print(f" -> ✅ Rostro detectado en el frame [{i}].")
_log(f" -> ✅ Rostro detectado en el frame [{i}].")
break
if not found:
print(f" -> ⚠️ ALERTA: No se detectó rostro frontal en NINGÚN frame. Salvando el ciclo usando el frame 0 por defecto.")
best_idx = 0
_log(f" -> ⚠️ ALERTA: No se detectó rostro frontal en NINGÚN frame. Aceptando todo el lote para evitar bucles infinitos.")
best_idx = batch_size - 1
except Exception as e:
print(f" -> ⚠️ Error en OpenCV al validar: {e}")
_log(f" -> ⚠️ Error en OpenCV al validar: {e}")
else:
print(f" -> ⏩ Detección de rostros desactivada. Usando el último frame absoluto del lote ({batch_size - 1}).")
_log(f" -> ⏩ Detección de rostros desactivada. Usando el último frame absoluto del lote ({batch_size - 1}).")
best_idx = batch_size - 1
# 1. TRUNCAR EL TENSOR
@@ -156,25 +175,40 @@ class SessionImageSender:
# since it will be reused as the start of the next chunk.
advanced_original_frames = max(1, (frames_accepted - 1) * stride)
loop.global_accumulated_frames += advanced_original_frames
source_total = getattr(loop, 'global_source_frame_count', 1)
print(f" -> ✂️ Tensor truncado a {frames_accepted} frames válidos.")
print(f" -> 📈 Timeline avanzado a {loop.global_accumulated_frames} / {source_total}")
is_final_chunk = getattr(loop, 'global_is_final_chunk', False)
# 🚀 FIX: Compensar los frames destruidos por el VAE en el ciclo final
if is_final_chunk:
_log(f" -> 🏁 LOTE FINAL ABSOLUTO DETECTADO. Compensando mermas del VAE (Timeline forzado al 100%).")
loop.global_accumulated_frames = source_total
else:
loop.global_accumulated_frames += advanced_original_frames
# 🚀 NUEVO: Detectar si es el ciclo final o un ciclo único
is_final_cycle = loop.global_accumulated_frames >= source_total
_log(f" -> ✂️ Tensor truncado a {frames_accepted} frames válidos.")
_log(f" -> 📈 Timeline avanzado a {loop.global_accumulated_frames} / {source_total}")
last_frame = valid_images[-1:].clone().cpu()
global_session_image = last_frame
out_dir = folder_paths.get_output_directory()
filename = f"keyframe_{loop_idx:03d}.png"
filepath = os.path.join(out_dir, filename)
# 🛡️ Solo guardamos el keyframe si habrá un próximo ciclo
if not is_final_cycle:
out_dir = folder_paths.get_output_directory()
filename = f"keyframe_{loop_idx:03d}.png"
filepath = os.path.join(out_dir, filename)
img_array = 255. * last_frame[0].numpy()
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
img.save(filepath)
img_array = 255. * last_frame[0].numpy()
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
img.save(filepath)
print(f" -> 💾 Keyframe seguro guardado: {filename}")
print(f"{'='*50}\n")
_log(f" -> 💾 Keyframe seguro guardado: {filename}")
else:
_log(f" -> 🚀 Ciclo final/único detectado. Omitiendo guardado de keyframe.")
_log(f"{'='*50}\n")
ui_image = tensor_to_temp_image(last_frame, "sender")
return {"ui": {"images": [ui_image]}, "result": (valid_images, )}
return {"ui": {"images": [ui_image]}, "result": (valid_images, "\n".join(log_output))}
+2 -1
View File
@@ -21,7 +21,8 @@ def check_and_install_ffmpeg():
if sys.platform.startswith("linux"):
try:
print("⚙️ Intentando instalar FFmpeg automáticamente (Linux/WSL)...")
subprocess.check_call("sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg", shell=True)
subprocess.check_call(["sudo", "apt-get", "update"])
subprocess.check_call(["sudo", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "ffmpeg"])
print("✅ FFmpeg instalado correctamente.")
except Exception as e:
print(f"❌ Falló la instalación automática de FFmpeg: {e}")
+109 -31
View File
@@ -84,6 +84,7 @@ class AutoLoopCalculatorLTX:
},
"optional": {
"safe_faces_list": ("FACE_CUTS", {"forceInput": True}),
"scene_cuts_list": ("SCENE_CUTS", {"forceInput": True}),
}
}
@@ -92,35 +93,40 @@ class AutoLoopCalculatorLTX:
FUNCTION = "calculate"
CATEGORY = "🔁 Sequential Batcher/Loop"
def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None):
def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None, scene_cuts_list=None):
# --- Ajuste Proporcional LTX (Regla DiT 8n + 1) ---
import math
from . import loop
total_effective_frames = source_frame_count // select_every_nth
potential_effective_frames = source_frame_count // select_every_nth
# Truncar al (8n + 1) más cercano hacia abajo
if total_effective_frames < 9:
safe_effective_frames = total_effective_frames # Too short, bypass
if potential_effective_frames < 9:
safe_effective_frames = 9
else:
safe_effective_frames = ((total_effective_frames - 1) // 8) * 8 + 1
safe_effective_frames = ((potential_effective_frames + 6) // 8) * 8 + 1
safe_source_frame_count = safe_effective_frames * select_every_nth
loop.global_source_frame_count = safe_source_frame_count
# 🚀 FIX: Conservamos el límite FÍSICO real del vídeo para el timeline global
physical_source_frame_count = potential_effective_frames * select_every_nth
loop.global_source_frame_count = physical_source_frame_count
effective_padding = safe_effective_frames - potential_effective_frames
# Ajuste proporcional forzando regla 8n + 1
# Ajuste proporcional
estimated_loops = math.ceil(safe_effective_frames / target_frames_per_loop)
if estimated_loops > 0:
optimal_target = math.ceil(safe_effective_frames / estimated_loops)
# Encontrar el 8n + 1 más cercano hacia arriba para cubrir el total
adjusted_target = ((optimal_target - 1 + 7) // 8) * 8 + 1
target_frames_per_loop = adjusted_target
print(f" -> ⚖️ Ajuste Proporcional LTX: Target recalculado a {adjusted_target} frames por ciclo.")
# -------------------------------------------------------------
print(f" -> 🎞️ Capacidad del video original: {potential_effective_frames} frames (Nth: {select_every_nth})")
if effective_padding > 0:
print(f" -> 🛡️ Ajuste VAE: Se pedirán {safe_effective_frames} frames (Acolchado técnico: Se rellenarán {effective_padding} frames)")
else:
print(f" -> ✅ Ajuste VAE: Perfecto. Regla 8n+1 detectada.")
print(f" -> 📊 Timeline final: 0 a {physical_source_frame_count} (Límite Físico Real)")
loop.global_select_every_nth = select_every_nth
# Indicador global para el Stitcher y Sender (LTX mode activo)
global global_ltx_mode
global_ltx_mode = True
@@ -128,38 +134,58 @@ class AutoLoopCalculatorLTX:
print(f"\n{'='*50}")
print(f"📊 [DEBUG] NODO: Auto Loop Calculator (LTX 2.3)")
print(f" -> Timeline seguro ajustado a LTX: {current_pos} / {safe_source_frame_count} (Original: {source_frame_count})")
print(f" -> Timeline físico ajustado a LTX: {current_pos} / {physical_source_frame_count} (Original: {source_frame_count})")
if current_pos >= safe_source_frame_count:
if current_pos >= physical_source_frame_count:
return (1, current_pos, select_every_nth)
frames_left = safe_source_frame_count - current_pos
frames_left = physical_source_frame_count - current_pos
equitable_target = target_frames_per_loop * select_every_nth
ideal_cut = current_pos + equitable_target
if frames_left <= equitable_target:
best_cut = safe_source_frame_count
best_cut = physical_source_frame_count
print(f" -> 🧮 Absorbiendo resto final: meta fijada en frame {best_cut}")
else:
best_cut = ideal_cut
if safe_faces_list and len(safe_faces_list) > 0:
if scene_cuts_list and len(scene_cuts_list) > 0:
future_cuts = [c for c in scene_cuts_list if c > current_pos]
if future_cuts:
next_cut = min(future_cuts)
if next_cut <= ideal_cut:
chunk_from_scene = math.ceil((next_cut - current_pos) / select_every_nth)
safe_chunk_from_scene = ((chunk_from_scene + 6) // 8) * 8 + 1
if safe_chunk_from_scene < 9: safe_chunk_from_scene = 9
best_cut = current_pos + (safe_chunk_from_scene * select_every_nth)
print(f" -> 🎬 Bucle aislado por escena (Ajustado x8). Cortando en el frame: {best_cut} (Corte real: {next_cut})")
else:
best_cut = ideal_cut
print(f" -> ⚠️ Toma demasiado larga para la VRAM. Cortando por límite técnico en: {best_cut} (El plano real acaba en {next_cut})")
else:
best_cut = ideal_cut
print(f" -> 🎬 No quedan cortes de cámara por delante. Forzando corte final por VRAM en: {ideal_cut}")
elif safe_faces_list and len(safe_faces_list) > 0:
closest_face = min(safe_faces_list, key=lambda x: abs(x - ideal_cut))
best_cut = closest_face
print(f" -> ✂️ Corte Inteligente proyectado: {best_cut} (Meta equitativa: {ideal_cut})")
chunk_from_face = math.ceil((closest_face - current_pos) / select_every_nth)
safe_chunk_from_face = ((chunk_from_face + 6) // 8) * 8 + 1
if safe_chunk_from_face < 9: safe_chunk_from_face = 9
best_cut = current_pos + (safe_chunk_from_face * select_every_nth)
print(f" -> ✂️ Corte Facial Inteligente (Ajustado x8): {best_cut} (Meta equitativa: {ideal_cut})")
else:
print(f" -> ⚖️ Sin caras detectadas. Forzando corte equitativo: {ideal_cut}")
best_cut = ideal_cut
print(f" -> ⚖️ Sin detectores conectados. Forzando corte equitativo x8: {ideal_cut}")
effective_chunk_frames = math.ceil((best_cut - current_pos) / select_every_nth)
# Forzar a 8n + 1
effective_chunk_frames = ((effective_chunk_frames - 1) // 8) * 8 + 1
effective_chunk_frames = ((effective_chunk_frames + 6) // 8) * 8 + 1
if effective_chunk_frames < 9:
effective_chunk_frames = 9 # Mínimo vital LTX
effective_chunk_frames = 9
if current_pos + (effective_chunk_frames * select_every_nth) > safe_source_frame_count:
effective_chunk_frames = (safe_source_frame_count - current_pos) // select_every_nth
if current_pos + (effective_chunk_frames * select_every_nth) >= physical_source_frame_count:
print(f" -> 🏁 Chunk final LTX detectado. Ajustando a {effective_chunk_frames} frames para mantener regla 8n+1.")
loop.global_is_final_chunk = True
else:
loop.global_is_final_chunk = False
skip_frames = current_pos
@@ -205,16 +231,51 @@ class SequentialLoopTrigger:
if not is_final:
print(f" -> ⚙️ Preparando Ciclo {next_loop}...")
if prompt is not None:
# 🧠 HACKER MODE: Estimamos si el ciclo que vamos a encolar es el final
estimated_chunk = 50
found_calculator = False
m_seeds = 0
master_switches = []
for node_id, node_data in prompt.items():
class_type = node_data.get("class_type", "")
inputs = node_data.get("inputs", {})
if not found_calculator and "AutoLoopCalculator" in class_type:
estimated_chunk = inputs.get("target_frames_per_loop", 50)
found_calculator = True
# Mutación de semillas
for key in ["seed", "noise_seed"]:
if key in inputs and isinstance(inputs[key], (int, float)):
inputs[key] = random.randint(1, 0xffffffff)
m_seeds += 1
if node_data.get("class_type") == "SequentialLoopStart":
# Mutación de índice
if class_type == "SequentialLoopStart":
inputs["loop_idx"] = next_loop
inputs["reset_loop"] = False
# 🔪 Recolectamos Master Switch para cirugía diferida
if class_type == "MasterSwitch":
master_switches.append(inputs)
is_next_final = (global_accumulated_frames + estimated_chunk) >= global_source_frame_count
for inputs in master_switches:
print(f" -> 🔀 [Cirugía de Grafo] Mutando Master Switch para Ciclo {next_loop}...")
# Sobrescribimos el cable que viene del Stitcher por un booleano estático
inputs["is_final_cycle"] = is_next_final
if is_next_final:
if "on_false" in inputs:
del inputs["on_false"]
print(f" ✂️ Cable 'on_false' eliminado (Ahorro de VRAM en ruta inactiva).")
else:
if "on_true" in inputs:
del inputs["on_true"]
print(f" ✂️ Cable 'on_true' eliminado (Ruta pesada desconectada).")
print(f" -> 🎲 Semillas mutadas: {m_seeds}")
p = {"prompt": prompt}
@@ -227,13 +288,23 @@ class SequentialLoopTrigger:
print(f" -> ✅ Ciclo {next_loop} inyectado en la cola.")
except Exception as e:
print(f" -> ❌ Error HTTP: {e}")
# Limpieza ligera por ciclo para evitar fragmentación
import gc
import torch
import comfy.model_management as mm
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
else:
print(f" -> 🏁 ¡Generación Finalizada! Todos los frames ensamblados.")
global_loop_index = 0
global_accumulated_frames = 0
# --- LIMPIEZA EXTREMA DE VRAM AUTOMÁTICA (Multi-Plataforma) ---
print(f" -> 🧹 Iniciando vaciado automático de VRAM...")
import gc
import torch
import comfy.model_management as mm
try:
# 1. Obligamos al motor interno de ComfyUI a soltar los modelos
mm.unload_all_models()
@@ -248,6 +319,13 @@ class SequentialLoopTrigger:
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
# HACK: Forzar la desfragmentación de la memoria caché de PyTorch
import ctypes
try:
ctypes.CDLL('libc.so.6').malloc_trim(0)
except Exception:
pass
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
torch.mps.empty_cache()
+108
View File
@@ -0,0 +1,108 @@
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
class MasterSwitch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"is_final_cycle": ("BOOLEAN", {"default": False, "forceInput": False}),
},
"optional": {
"on_true": (any_type,),
"on_false": (any_type,),
}
}
RETURN_TYPES = (any_type, "STRING")
RETURN_NAMES = ("output", "log")
FUNCTION = "route"
CATEGORY = "🔁 Sequential Batcher/Logic"
# 🔥 Evaluación Perezosa Nativa (Protege el Ciclo 0)
def check_lazy_status(self, is_final_cycle, on_true=None, on_false=None):
if is_final_cycle:
return ["on_true"]
else:
return ["on_false"]
def route(self, is_final_cycle, on_true=None, on_false=None):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
if is_final_cycle:
return (on_true, "\n".join(log_output))
else:
return (on_false, "\n".join(log_output))
# Diccionario global para persistencia entre ciclos de ejecución
GLOBAL_SESSION_CACHE = {}
class LazySessionCache:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
# Mantenemos el índice como obligatorio para saber en qué ciclo estamos
"current_loop_index": ("INT", {"default": 0, "forceInput": True}),
},
"optional": {
# 'value_in' DEBE ser opcional para que la evaluación perezosa funcione
"value_in": (any_type, {"forceInput": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID"
}
}
RETURN_TYPES = (any_type, "STRING")
RETURN_NAMES = ("value_out", "log")
FUNCTION = "execute"
CATEGORY = "🔁 Sequential Batcher/Logic"
# 🔥 LA CLAVE: Aquí es donde ComfyUI decide si "tira del cable" o no
def check_lazy_status(self, current_loop_index, **kwargs):
# El índice llega como una lista o un int dependiendo del nodo anterior
idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index
if idx == 0:
# Ciclo 0: Obligamos a ComfyUI a ejecutar todo lo que haya atrás
return ["value_in"]
else:
# Ciclos > 0: NO pedimos 'value_in'. ComfyUI NO ejecutará los nodos anteriores.
return []
def execute(self, current_loop_index, value_in=None, unique_id="0"):
global GLOBAL_SESSION_CACHE
log_output = []
def _log(msg):
print(msg); log_output.append(str(msg))
idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index
_log(f"🧠 [Cache] Ciclo {idx} | Nodo ID: {unique_id}")
# --- LÓGICA DE CAPTURA (Ciclo 0) ---
if idx == 0:
if value_in is not None:
GLOBAL_SESSION_CACHE[unique_id] = value_in
_log(" -> 💾 Valor capturado y guardado en RAM.")
return (value_in, "\n".join(log_output))
else:
# Si es el ciclo 0 y no hay nada, algo va mal en el flujo
raise ValueError(f"❌ Error en LazySessionCache [{unique_id}]: No se recibió valor en el Ciclo 0.")
# --- LÓGICA DE RECUPERACIÓN (Ciclos > 0) ---
else:
if unique_id in GLOBAL_SESSION_CACHE:
_log(" -> ♻️ Recuperando valor de RAM (Upstream bloqueado con éxito).")
return (GLOBAL_SESSION_CACHE[unique_id], "\n".join(log_output))
else:
# Fallback de seguridad por si el caché se limpió o se saltó el ciclo 0
_log(" -> ⚠️ No hay caché. Intentando usar entrada directa.")
return (value_in, "\n".join(log_output))
+34
View File
@@ -0,0 +1,34 @@
import unittest
from unittest.mock import patch, MagicMock
import sys
import os
# Mock sys.platform to linux for testing check_and_install_ffmpeg
@patch("sys.platform", "linux")
@patch("shutil.which", return_value=None)
class TestInstall(unittest.TestCase):
@patch("subprocess.check_call")
def test_check_and_install_ffmpeg_linux(self, mock_check_call, mock_which):
from install import check_and_install_ffmpeg
check_and_install_ffmpeg()
# Verify that subprocess.check_call was called twice with the correct lists
self.assertEqual(mock_check_call.call_count, 2)
expected_calls = [
unittest.mock.call(["sudo", "apt-get", "update"]),
unittest.mock.call(["sudo", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "ffmpeg"])
]
mock_check_call.assert_has_calls(expected_calls)
# Verify shell=True was NOT used
for call in mock_check_call.call_args_list:
args, kwargs = call
if "shell" in kwargs:
self.assertFalse(kwargs["shell"], "shell should be False if explicitly provided")
# If not in kwargs, it defaults to False for subprocess.check_call
if __name__ == "__main__":
unittest.main()
+55
View File
@@ -0,0 +1,55 @@
import sys
import types
import pytest
from unittest.mock import MagicMock
import os
# Mock dependencies before importing anything that might depend on them
mock_torch = types.ModuleType("torch")
mock_torch.cuda = MagicMock()
mock_torch.cuda.is_available.return_value = False
sys.modules["torch"] = mock_torch
sys.modules["comfy"] = types.ModuleType("comfy")
sys.modules["comfy.model_management"] = types.ModuleType("comfy.model_management")
def get_format_bytes():
# Read the file
vram_node_path = os.path.join(os.path.dirname(__file__), "..", "vram_node.py")
with open(vram_node_path, 'r') as f:
code = f.read()
# Mock 'from . import register_node' by replacing it
code = code.replace("from . import register_node", "def register_node(c, *args, **kwargs): return c")
vram_module = types.ModuleType("vram_node")
exec(code, vram_module.__dict__)
return vram_module.format_bytes
@pytest.fixture
def format_bytes():
return get_format_bytes()
def test_format_bytes_zero(format_bytes):
assert format_bytes(0) == "0.00 GB"
def test_format_bytes_gigabyte(format_bytes):
assert format_bytes(1024**3) == "1.00 GB"
def test_format_bytes_fractional(format_bytes):
assert format_bytes(1024**3 * 0.5) == "0.50 GB"
assert format_bytes(1024**3 * 2.5) == "2.50 GB"
def test_format_bytes_small_values(format_bytes):
# 1 MB is 1/1024 GB ≈ 0.000976 GB -> 0.00 GB
assert format_bytes(1024**2) == "0.00 GB"
# 10 MB is 10/1024 GB ≈ 0.009765 GB -> 0.01 GB (due to rounding .2f)
assert format_bytes(1024**2 * 10) == "0.01 GB"
def test_format_bytes_large_values(format_bytes):
assert format_bytes(1024**3 * 1024) == "1024.00 GB"
assert format_bytes(1024**3 * 123.456) == "123.46 GB"
if __name__ == "__main__":
pytest.main([__file__])
+220 -20
View File
@@ -1,5 +1,43 @@
import math
import time
import torch
from . import register_node
from .switch_node import any_type
@register_node
class PrimitiveDelay:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"delay_seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3600.0, "step": 0.1, "tooltip": "Tiempo de pausa en segundos"}),
},
"optional": {
"any_in": (any_type, {"tooltip": "Conecta aquí el cable que quieres retrasar"}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("* (passthrough)",)
FUNCTION = "execute_delay"
CATEGORY = "🔁 Sequential Batcher/Tools"
def execute_delay(self, delay_seconds, any_in=None):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
_log(f"\n{'='*50}")
_log(f"⏱️ [Secuencial Batcher] NODO: Primitive Delay")
_log(f" -> Congelando la ejecución durante {delay_seconds} segundos...")
time.sleep(delay_seconds)
_log(f" -> ✅ Pausa terminada. Reanudando flujo.")
_log(f"{'='*50}\n")
return (any_in,)
# 🧠 Clase Base Invisible (El molde matemático por Megapíxeles)
class BaseResolutionTool:
@@ -13,12 +51,16 @@ class BaseResolutionTool:
}
}
RETURN_TYPES = ("INT", "INT", "STRING")
RETURN_NAMES = ("width", "height", "debug_info")
RETURN_TYPES = ("INT", "INT", "STRING", "STRING")
RETURN_NAMES = ("width", "height", "debug_info", "log")
FUNCTION = "get_resolution"
CATEGORY = "🔁 Sequential Batcher/Tools"
def get_resolution(self, aspect_ratio, base_resolution):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
div = self.DIVISOR
min_pixels = getattr(self, "MIN_PIXELS", 0)
longest_side = int(base_resolution) # Mantenemos el casting por seguridad
@@ -28,9 +70,9 @@ class BaseResolutionTool:
h_ratio = float(h_str)
ratio = w_ratio / h_ratio
print(f"\n{'='*50}")
print(f"🛠️ [DEBUG] NODO: Resolution Tool {div}x")
print(f" -> Petición Original: Lado mayor {longest_side}px | Ratio {aspect_ratio}")
_log(f"\n{'='*50}")
_log(f"🛠️ [Secuencial Batcher] NODO: Resolution Tool {div}x")
_log(f" -> Petición Original: Lado mayor {longest_side}px | Ratio {aspect_ratio}")
# 1. Calcular resolución inicial teórica
if w_ratio >= h_ratio:
@@ -44,8 +86,8 @@ class BaseResolutionTool:
# 2. 🛡️ Protección de Suelo por ÁREA TOTAL (Training Floor)
if current_pixels < min_pixels:
print(f" -> ⚠️ ALERTA: La resolución pedida ({int(current_pixels)} píxeles) es inferior al mínimo vital del modelo ({min_pixels} píxeles).")
print(f" -> 🛡️ Escalando proporcionalmente para evitar artefactos...")
_log(f" -> ⚠️ ALERTA: La resolución pedida ({int(current_pixels)} píxeles) es inferior al mínimo vital del modelo ({min_pixels} píxeles).")
_log(f" -> 🛡️ Escalando proporcionalmente para evitar artefactos...")
# Factor de escala basado en área
scale_factor = math.sqrt(min_pixels / current_pixels)
ideal_w *= scale_factor
@@ -58,10 +100,10 @@ class BaseResolutionTool:
debug_msg = f"{width}x{height} (Div {div})"
print(f" -> 🎯 Resultado Final Seguro: Ancho {width} | Alto {height} | Píxeles totales: {width*height}")
print(f"{'='*50}\n")
_log(f" -> 🎯 Resultado Final Seguro: Ancho {width} | Alto {height} | Píxeles totales: {width*height}")
_log(f"{'='*50}\n")
return (width, height, debug_msg)
return (width, height, debug_msg, "\n".join(log_output))
# 📦 Nodos Específicos Blindados por Píxeles
@register_node
@@ -101,28 +143,186 @@ class AutoFPSLimiter:
}
}
RETURN_TYPES = ("INT", "FLOAT")
RETURN_NAMES = ("select_every_nth", "new_fps")
RETURN_TYPES = ("INT", "FLOAT", "STRING")
RETURN_NAMES = ("select_every_nth", "new_fps", "log")
FUNCTION = "calculate_fps"
CATEGORY = "🔁 Sequential Batcher/Tools"
def calculate_fps(self, source_fps, target_max_fps):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
import math
print(f"\n{'='*50}")
print(f"⏱️ [DEBUG] NODO: Auto FPS Limiter")
print(f" -> FPS Originales: {source_fps:.2f} | Meta Máxima: {target_max_fps:.2f}")
_log(f"\n{'='*50}")
_log(f"⏱️ [Secuencial Batcher] NODO: Auto FPS Limiter")
_log(f" -> FPS Originales: {source_fps:.2f} | Meta Máxima: {target_max_fps:.2f}")
if source_fps <= target_max_fps:
nth = 1
new_fps = source_fps
print(f" -> ✅ Los FPS originales ya están dentro del límite.")
_log(f" -> ✅ Los FPS originales ya están dentro del límite.")
else:
# Calculamos el salto matemático (hacia arriba) para garantizar no superar la meta
nth = math.ceil(source_fps / target_max_fps)
new_fps = source_fps / nth
print(f" -> ✂️ Reducción necesaria. Procesando 1 de cada {nth} frames.")
_log(f" -> ✂️ Reducción necesaria. Procesando 1 de cada {nth} frames.")
print(f" -> 🎯 Resultado: {new_fps:.2f} FPS finales (Nth: {nth})")
print(f"{'='*50}\n")
_log(f" -> 🎯 Resultado: {new_fps:.2f} FPS finales (Nth: {nth})")
_log(f"{'='*50}\n")
return (nth, new_fps)
return (nth, new_fps, "\n".join(log_output))
@register_node
class ConditionalAudioRouter:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"audio": ("AUDIO",),
"is_final_cycle": ("BOOLEAN", {"forceInput": True}),
}
}
RETURN_TYPES = ("AUDIO", "STRING")
RETURN_NAMES = ("routed_audio", "log")
FUNCTION = "route"
CATEGORY = "🔁 Sequential Batcher/Tools"
def route(self, audio, is_final_cycle):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
if is_final_cycle:
_log(" -> 🔊 [Audio Router] Ciclo Final: Enviando pista de audio completa al Lip Sync.")
return (audio, "\n".join(log_output))
else:
_log(" -> 🔇 [Audio Router] Ciclo Intermedio: Generando micro-silencio para bypasear el Lip Sync...")
# Detectar el sample rate del audio original (estándar de ComfyUI es un dict)
sample_rate = 44100
if isinstance(audio, dict) and "sample_rate" in audio:
sample_rate = audio["sample_rate"]
# Creamos 0.1 segundos de silencio absoluto (1 canal, mínimos samples)
samples = int(sample_rate * 0.1)
dummy_waveform = torch.zeros((1, 1, samples), dtype=torch.float32)
dummy_audio = {"waveform": dummy_waveform, "sample_rate": sample_rate}
return (dummy_audio, "\n".join(log_output))
@register_node
class VAESafeFramePadder:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"rule": (["WanVideo (Regla 4n+1)", "LTX (Regla 8n+1)"],),
}
}
RETURN_TYPES = ("IMAGE", "INT", "STRING")
RETURN_NAMES = ("PADDED_IMAGES", "frames_added", "log")
FUNCTION = "pad"
CATEGORY = "🔁 Sequential Batcher/Tools"
def pad(self, images, rule):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
n_frames = images.shape[0]
target_frames = n_frames
if "WanVideo" in rule:
# 🚀 FIX: Regla 4n+1 para WanVideo (Evita la pérdida de 3 frames en el VAE)
if n_frames < 5:
target_frames = 5
else:
target_frames = ((n_frames + 2) // 4) * 4 + 1
elif "LTX" in rule:
if n_frames < 9:
target_frames = 9
else:
target_frames = ((n_frames + 6) // 8) * 8 + 1
diff = target_frames - n_frames
_log(f"\n{'='*50}")
_log(f"🛡️ [Secuencial Batcher] NODO: VAE Safe Frame Padder")
_log(f" -> Frames recibidos: {n_frames}")
_log(f" -> Regla aplicada: {rule}")
if diff > 0:
_log(f" -> 🧱 Tensor incompleto. Clonando el último frame {diff} vez/veces...")
last_frame = images[-1:] # Extraemos el último frame
padding = last_frame.repeat(diff, 1, 1, 1) # Lo multiplicamos
images = torch.cat([images, padding], dim=0) # Lo fusionamos al final
_log(f" -> ✅ Tensor final acolchado a: {target_frames} frames.")
else:
_log(f" -> ✅ Tensor perfecto. No requiere acolchado.")
_log(f"{'='*50}\n")
return (images, diff, "\n".join(log_output))
@register_node
class VHS_Path_Selector:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"vhs_filenames": ("VHS_FILENAMES",),
"select_file": (["video_with_audio", "video_muted", "png_thumbnail"],),
}
}
# 🔄 Ahora devuelve la cadena y el objeto VHS reordenado
RETURN_TYPES = ("STRING", "VHS_FILENAMES", "STRING")
RETURN_NAMES = ("file_path", "vhs_filenames", "log")
FUNCTION = "select"
CATEGORY = "🔁 Sequential Batcher/Tools" # 📍 Ubicación corregida
def select(self, vhs_filenames, select_file):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
try:
if vhs_filenames is None or not isinstance(vhs_filenames, (list, tuple)) or len(vhs_filenames) < 2:
_log("⚠️ [Path Selector] Entrada vhs_filenames vacía o inválida.")
return ("", vhs_filenames, "\n".join(log_output))
paths = vhs_filenames[1]
if not isinstance(paths, list) or not paths:
return ("", vhs_filenames, "\n".join(log_output))
# Identificación de archivos
audio_path = next((s for s in paths if isinstance(s, str) and s.endswith("-audio.mp4")), None)
muted_path = next((s for s in paths if isinstance(s, str) and s.endswith(".mp4") and not s.endswith("-audio.mp4")), None)
png_path = next((s for s in paths if isinstance(s, str) and s.endswith(".png")), None)
# Selección del archivo principal (result)
if select_file == "video_with_audio":
result = audio_path if audio_path else (muted_path if muted_path else paths[0])
elif select_file == "video_muted":
result = muted_path if muted_path else (audio_path if audio_path else paths[0])
else: # png_thumbnail
result = png_path if png_path else paths[0]
# 🪄 REORDENADO: Ponemos el elegido en la posición 0
new_paths = list(paths)
if result in new_paths:
new_paths.remove(result)
new_paths.insert(0, result)
# Creamos el nuevo paquete VHS [bool, [lista_reordenada]]
reordered_vhs = [vhs_filenames[0], new_paths]
_log(f"📂 [Path Selector] Selección: {select_file} -> {result}")
return (str(result), reordered_vhs, "\n".join(log_output))
except Exception as e:
_log(f"❌ [Path Selector] Error crítico: {e}")
return ("", vhs_filenames, "\n".join(log_output))
+654 -192
View File
File diff suppressed because it is too large Load Diff
+119
View File
@@ -0,0 +1,119 @@
import torch
import gc
import comfy.model_management as mm
import os
from . import register_node
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
def format_bytes(bytes_val):
return f"{bytes_val / (1024 ** 3):.2f} GB"
@register_node
class MeisoftVRAMDefragmenter:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"anything": (any_type,),
"force_model_unload": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = (any_type, "STRING")
RETURN_NAMES = ("anything", "log")
FUNCTION = "defragment"
CATEGORY = "🔁 Sequential Batcher/Tools"
def defragment(self, anything, force_model_unload):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
_log(f"\n{'='*65}")
_log(f"🧹 [Secuencial Batcher] NODO: VRAM Defragmenter (Modo Arquitectura Profunda)")
# --- ESTADÍSTICAS ANTES ---
alloc_before = 0
res_before = 0
if torch.cuda.is_available():
alloc_before = torch.cuda.memory_allocated()
res_before = torch.cuda.memory_reserved()
_log(f" -> 📊 Estado Inicial | Asignada: {format_bytes(alloc_before)} | Reservada (Caché): {format_bytes(res_before)}")
# --- FASE 1: CONFIGURACIÓN DINÁMICA DEL ALLOCATOR ---
try:
# Permite que la reserva de VRAM crezca en bloque elástico
torch.cuda.memory._set_allocator_settings("expandable_segments:True")
_log(" -> ⚙️ PyTorch Allocator ajustado a expandable_segments:True")
except Exception as e:
_log(f" -> ⚠️ Aviso: No se pudo ajustar expandable_segments: {e}")
# --- FASE 2: EVACUACIÓN (Opcional) ---
if force_model_unload:
_log(" -> 📦 Trasladando modelos inactivos a la RAM (unload_all_models)...")
mm.unload_all_models()
# --- FASE 3: LIMPIEZA INTERNA DE COMFYUI (LA CLAVE) ---
_log(" -> 🧹 Destruyendo referencias ocultas en el gestor de ComfyUI...")
if hasattr(mm, "cleanup_models"):
mm.cleanup_models()
if hasattr(mm, "cleanup_models_gc"):
mm.cleanup_models_gc()
# --- FASE 4: ROMPER REFERENCIAS PYTHON ---
_log(" -> 🗑️ Forzando Garbage Collector de Python (Doble pasada)...")
gc.collect()
gc.collect() # Doble pasada para romper referencias circulares rebeldes
# --- FASE 5: EVACUACIÓN SUAVE ---
mm.soft_empty_cache()
# --- FASE 6: PURGADO PROFUNDO DE PYTORCH ---
if torch.cuda.is_available():
_log(" -> 🧱 Desfragmentando Caching Allocator de CUDA...")
torch.cuda.ipc_collect()
torch.cuda.synchronize() # 🚀 OPTIMIZACIÓN: Esperar a que terminen los kernels pendientes PRIMERO
torch.cuda.empty_cache() # 🚀 Luego vaciamos la caché de forma segura
# --- FASE 7: LIBERACIÓN DE MEMORIA DEL SISTEMA (Multi-Plataforma) ---
import platform
try:
import ctypes
if platform.system() == "Windows":
# API de Windows: Reduce el 'Working Set' del proceso actual al mínimo
kernel32 = ctypes.windll.kernel32
current_process = kernel32.GetCurrentProcess()
kernel32.SetProcessWorkingSetSize(current_process, -1, -1)
_log(" -> 💻 Working Set reducido (RAM del sistema purgada en Windows).")
else:
# API de Linux/WSL: Libera la memoria de la librería C
ctypes.CDLL('libc.so.6').malloc_trim(0)
_log(" -> 💻 Malloc Trim ejecutado (RAM del sistema purgada en Linux/WSL).")
except Exception as e:
# Si algo falla a nivel de SO, fallamos silenciosamente sin romper el nodo
pass
# --- ESTADÍSTICAS DESPUÉS ---
if torch.cuda.is_available():
alloc_after = torch.cuda.memory_allocated()
res_after = torch.cuda.memory_reserved()
# Cálculos de liberación
freed_alloc = alloc_before - alloc_after
freed_res = res_before - res_after
sign_alloc = "+" if freed_alloc < 0 else "-"
sign_res = "+" if freed_res < 0 else "-"
_log(f" -> 📈 Estado Final | Asignada: {format_bytes(alloc_after)} | Reservada (Caché): {format_bytes(res_after)}")
_log(f" -> ✨ TOTAL LIBERADO | {sign_alloc}{abs(freed_alloc)/(1024**3):.2f} GB (Asignada) | {sign_res}{abs(freed_res)/(1024**3):.2f} GB (Reservada purgada)")
else:
_log(" -> ✨ Limpieza completada (Modo CPU/Otra arquitectura).")
_log(f"{'='*65}\n")
return (anything, "\n".join(log_output))