diff --git a/.gitignore b/.gitignore
index e83dd2c0..e0ac27bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,178 +1,192 @@
-hf_download/
-outputs/
-repo/
-
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# UV
-# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-#uv.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-.idea/
-
-# Ruff stuff:
-.ruff_cache/
-
-# PyPI configuration file
-.pypirc
+hf_download/
+outputs/
+repo/
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+.venv_FramePack_svc
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Visual Studio cache
+.vs/
+
+# Local helpers
+runsvc.sh
+tmp/
+
+# Created at runtime
+framepack_svc_queue.zip
+hf_download # to ensure symlink is caught correctly
+hf_download/
+outputs_svc/
\ No newline at end of file
diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json
new file mode 100644
index 00000000..6b611411
--- /dev/null
+++ b/.vs/VSWorkspaceState.json
@@ -0,0 +1,6 @@
+{
+ "ExpandedNodes": [
+ ""
+ ],
+ "PreviewInSolutionExplorer": false
+}
\ No newline at end of file
diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite
new file mode 100644
index 00000000..e69de29b
diff --git a/demo_gradio_svc.py b/demo_gradio_svc.py
new file mode 100644
index 00000000..9c53b282
--- /dev/null
+++ b/demo_gradio_svc.py
@@ -0,0 +1,742 @@
+# Stub temporary callout for many thanks to @Tophness PR-150 for great work on the queueing system integrated into this code.
+
+from diffusers_helper.hf_login import login
+
+import os
+os.environ['HF_HOME'] = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(__file__), './hf_download')))
+
+import gradio as gr
+from gradio_modal import Modal
+import torch
+import traceback
+import numpy as np
+import argparse
+import time
+import json
+import base64
+import io
+import zipfile
+import tempfile
+import atexit
+from pathlib import Path
+import threading
+import tkinter as tk
+from tkinter import filedialog
+from PIL import Image
+
+from diffusers import AutoencoderKLHunyuanVideo
+from transformers import LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer
+from transformers import SiglipImageProcessor, SiglipVisionModel
+
+from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked
+from diffusers_helper.memory import cpu, gpu, get_cuda_free_memory_gb, DynamicSwapInstaller
+from diffusers_helper.thread_utils import AsyncStream, async_run
+from diffusers_helper.gradio.progress_bar import make_progress_bar_css
+
+from generation_core import worker
+
+# --- Globals and Configuration ---
+parser = argparse.ArgumentParser()
+parser.add_argument('--share', action='store_true', default=False, help="Enable Gradio sharing link.")
+parser.add_argument("--server", type=str, default='127.0.0.1', help="Server name to bind to.")
+parser.add_argument("--port", type=int, required=False, help="Port to run the server on.")
+parser.add_argument("--inbrowser", action='store_true', default=False, help="Launch in browser automatically.")
+args = parser.parse_args()
+
+abort_event = threading.Event()
+queue_lock = threading.Lock()
+outputs_folder = './outputs_svc/'
+os.makedirs(outputs_folder, exist_ok=True)
+
+SETTINGS_FILENAME = "framepack_svc_settings.json" # The file for default workspace settings.
+AUTOSAVE_FILENAME = "framepack_svc_queue.zip"
+
+# Creative "Recipe" Parameters (for portable PNG metadata and task editing)
+CREATIVE_PARAM_KEYS = [
+ 'prompt', 'n_prompt', 'total_second_length', 'seed', 'preview_frequency_ui',
+ 'segments_to_decode_csv', 'gs_ui', 'gs_schedule_shape_ui', 'gs_final_ui', 'steps', 'cfg', 'rs'
+]
+
+# Environment/Debug Parameters (for the full workspace, machine/session-specific)
+ENVIRONMENT_PARAM_KEYS = [
+ 'use_teacache', 'use_fp32_transformer_output_ui', 'gpu_memory_preservation',
+ 'mp4_crf', 'output_folder_ui', 'latent_window_size'
+]
+
+# A comprehensive list of all UI components that define a task.
+ALL_TASK_UI_KEYS = CREATIVE_PARAM_KEYS + ENVIRONMENT_PARAM_KEYS
+
+# This maps UI key names to the names expected by the 'worker' function.
+# It acts as a bridge between the Gradio UI and the backend processing logic.
+UI_TO_WORKER_PARAM_MAP = {
+ 'prompt': 'prompt', 'n_prompt': 'n_prompt', 'total_second_length': 'total_second_length',
+ 'seed': 'seed', 'use_teacache': 'use_teacache', 'preview_frequency_ui': 'preview_frequency',
+ 'segments_to_decode_csv': 'segments_to_decode_csv', 'gs_ui': 'gs',
+ 'gs_schedule_shape_ui': 'gs_schedule_active', 'gs_final_ui': 'gs_final', 'steps': 'steps', 'cfg': 'cfg',
+ 'latent_window_size': 'latent_window_size', 'gpu_memory_preservation': 'gpu_memory_preservation',
+ 'use_fp32_transformer_output_ui': 'use_fp32_transformer_output', 'rs': 'rs',
+ 'mp4_crf': 'mp4_crf', 'output_folder_ui': 'output_folder'
+}
+print(f"FramePack SVC launching with args: {args}")
+
+# --- Model Loading ---
+free_mem_gb = get_cuda_free_memory_gb(gpu)
+high_vram = free_mem_gb > 60
+print(f'Free VRAM {free_mem_gb} GB')
+print(f'High-VRAM Mode: {high_vram}')
+print("Initializing models...")
+text_encoder = LlamaModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder', torch_dtype=torch.float16).cpu()
+text_encoder_2 = CLIPTextModel.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='text_encoder_2', torch_dtype=torch.float16).cpu()
+tokenizer = LlamaTokenizerFast.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer')
+tokenizer_2 = CLIPTokenizer.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='tokenizer_2')
+vae = AutoencoderKLHunyuanVideo.from_pretrained("hunyuanvideo-community/HunyuanVideo", subfolder='vae', torch_dtype=torch.float16).cpu()
+feature_extractor = SiglipImageProcessor.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='feature_extractor')
+image_encoder = SiglipVisionModel.from_pretrained("lllyasviel/flux_redux_bfl", subfolder='image_encoder', torch_dtype=torch.float16).cpu()
+transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained('lllyasviel/FramePackI2V_HY', torch_dtype=torch.bfloat16).cpu()
+print("Models loaded to CPU. Configuring...")
+vae.eval(); text_encoder.eval(); text_encoder_2.eval(); image_encoder.eval(); transformer.eval()
+if not high_vram: vae.enable_slicing(); vae.enable_tiling()
+transformer.high_quality_fp32_output_for_inference = False
+transformer.to(dtype=torch.bfloat16); vae.to(dtype=torch.float16); image_encoder.to(dtype=torch.float16); text_encoder.to(dtype=torch.float16); text_encoder_2.to(dtype=torch.float16)
+vae.requires_grad_(False); text_encoder.requires_grad_(False); text_encoder_2.requires_grad_(False); image_encoder.requires_grad_(False); transformer.requires_grad_(False)
+if not high_vram:
+ print("Low VRAM mode: Installing DynamicSwap for transformer and text_encoder.")
+ DynamicSwapInstaller.install_model(transformer, device=gpu); DynamicSwapInstaller.install_model(text_encoder, device=gpu)
+else:
+ print("High VRAM mode: Moving all models to GPU.")
+ text_encoder.to(gpu); text_encoder_2.to(gpu); image_encoder.to(gpu); vae.to(gpu); transformer.to(gpu)
+print("Model configuration and placement complete.")
+
+# --- Helper Functions ---
+def patched_video_is_playable(video_filepath): return True
+gr.processing_utils.video_is_playable = patched_video_is_playable
+
+def save_settings_to_file(filepath, *ui_values_tuple):
+ settings_to_save = dict(zip(ALL_TASK_UI_KEYS, ui_values_tuple))
+ try:
+ with open(filepath, 'w', encoding='utf-8') as f:
+ json.dump(settings_to_save, f, indent=4)
+ gr.Info(f"Workspace saved to {filepath}")
+ print(f"Workspace saved to {filepath}")
+ except Exception as e:
+ gr.Warning(f"Error saving workspace: {e}")
+ traceback.print_exc()
+
+def save_workspace(*ui_values_tuple):
+ root = tk.Tk(); root.withdraw()
+ file_path = filedialog.asksaveasfilename(
+ title="Save Full Workspace As",
+ defaultextension=".json",
+ initialfile="framepack_workspace.json",
+ filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
+ )
+ root.destroy()
+ if file_path:
+ save_settings_to_file(file_path, *ui_values_tuple)
+ else:
+ gr.Warning("Save cancelled by user.")
+
+def save_as_default_workspace(*ui_values_tuple):
+ gr.Info(f"Saving current settings as default to {SETTINGS_FILENAME}")
+ save_settings_to_file(SETTINGS_FILENAME, *ui_values_tuple)
+
+def get_default_values_map():
+ return {
+ 'prompt': '', 'n_prompt': '', 'total_second_length': 5.0, 'seed': -1,
+ 'use_teacache': True, 'preview_frequency_ui': 5, 'segments_to_decode_csv': '',
+ 'gs_ui': 10.0, 'gs_schedule_shape_ui': 'Off', 'gs_final_ui': 10.0, 'steps': 25,
+ 'cfg': 1.0, 'latent_window_size': 9, 'gpu_memory_preservation': 6.0,
+ 'use_fp32_transformer_output_ui': False, 'rs': 0.0, 'mp4_crf': 18,
+ 'output_folder_ui': outputs_folder,
+ }
+
+def load_settings_from_file(filepath, return_updates=True):
+ default_values_map = get_default_values_map()
+ try:
+ with open(filepath, 'r', encoding='utf-8') as f:
+ loaded_settings = json.load(f)
+ gr.Info(f"Loaded workspace from {filepath}")
+ except Exception as e:
+ gr.Warning(f"Could not load workspace from {filepath}: {e}")
+ loaded_settings = {}
+
+ final_settings = default_values_map.copy()
+ final_settings.update(loaded_settings)
+ output_values = []
+ for key in ALL_TASK_UI_KEYS:
+ raw_value = final_settings.get(key)
+ new_val = raw_value
+ try:
+ if key in ['seed', 'latent_window_size', 'steps', 'mp4_crf', 'preview_frequency_ui']:
+ new_val = int(raw_value)
+ elif key in ['total_second_length', 'cfg', 'gs_ui', 'rs', 'gpu_memory_preservation', 'gs_final_ui']:
+ new_val = float(raw_value)
+ elif key in ['use_teacache', 'use_fp32_transformer_output_ui']:
+ if isinstance(raw_value, str):
+ new_val = raw_value.lower() == 'true'
+ elif not isinstance(raw_value, bool):
+ new_val = default_values_map.get(key)
+ except (ValueError, TypeError):
+ print(f"Settings Warning: Could not convert '{raw_value}' for '{key}'. Using default.")
+ new_val = default_values_map.get(key)
+ output_values.append(new_val)
+ if return_updates:
+ return [gr.update(value=val) for val in output_values]
+ else:
+ return output_values
+
+def load_workspace():
+ root = tk.Tk(); root.withdraw()
+ file_path = filedialog.askopenfilename(
+ title="Select Workspace JSON File",
+ filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
+ )
+ root.destroy()
+ if file_path:
+ return load_settings_from_file(file_path, return_updates=True)
+ return [gr.update()] * len(ALL_TASK_UI_KEYS)
+
+def load_default_workspace_on_start():
+ if os.path.exists(SETTINGS_FILENAME):
+ print(f"Found and loading default workspace from {SETTINGS_FILENAME}")
+ return load_settings_from_file(SETTINGS_FILENAME)
+ print("No default workspace file found. Using default values.")
+ default_vals = get_default_values_map()
+ return [default_vals[key] for key in ALL_TASK_UI_KEYS]
+
+def np_to_base64_uri(np_array_or_tuple, format="png"):
+ if np_array_or_tuple is None: return None
+ try:
+ np_array = np_array_or_tuple[0] if isinstance(np_array_or_tuple, tuple) and len(np_array_or_tuple) > 0 and isinstance(np_array_or_tuple[0], np.ndarray) else np_array_or_tuple if isinstance(np_array_or_tuple, np.ndarray) else None
+ if np_array is None: return None
+ pil_image = Image.fromarray(np_array.astype(np.uint8))
+ if format.lower() == "jpeg" and pil_image.mode == "RGBA": pil_image = pil_image.convert("RGB")
+ buffer = io.BytesIO(); pil_image.save(buffer, format=format.upper()); img_bytes = buffer.getvalue()
+ return f"data:image/{format.lower()};base64,{base64.b64encode(img_bytes).decode('utf-8')}"
+ except Exception as e: print(f"Error converting NumPy to base64: {e}"); return None
+
+def get_queue_state(state_dict_gr_state):
+ if "queue_state" not in state_dict_gr_state: state_dict_gr_state["queue_state"] = {"queue": [], "next_id": 1, "processing": False, "editing_task_id": None}
+ return state_dict_gr_state["queue_state"]
+
+def update_queue_df_display(queue_state):
+ queue = queue_state.get("queue", []); data = []; processing = queue_state.get("processing", False); editing_task_id = queue_state.get("editing_task_id", None)
+ for i, task in enumerate(queue):
+ params = task['params']; task_id = task['id']; prompt_display = (params['prompt'][:77] + '...') if len(params['prompt']) > 80 else params['prompt']; prompt_title = params['prompt'].replace('"', '"'); prompt_cell = f'{prompt_display}'; img_uri = np_to_base64_uri(params.get('input_image'), format="png"); thumbnail_size = "50px"; img_md = f'
' if img_uri else ""; is_processing_current_task = processing and i == 0; is_editing_current_task = editing_task_id == task_id; task_status_val = task.get("status", "pending");
+ if is_processing_current_task: status_display = "⏳ Processing"
+ elif is_editing_current_task: status_display = "✏️ Editing"
+ elif task_status_val == "done": status_display = "✅ Done"
+ elif task_status_val == "error": status_display = f"❌ Error: {task.get('error_message', 'Unknown')}"
+ elif task_status_val == "aborted": status_display = "⏹️ Aborted"
+ elif task_status_val == "pending": status_display = "⏸️ Pending"
+ data.append([task_id, status_display, prompt_cell, f"{params.get('total_second_length', 0):.1f}s", params.get('steps', 0), img_md, "↑", "↓", "✖", "✎"])
+ return gr.DataFrame(value=data, visible=len(data) > 0)
+
+def add_or_update_task_in_queue(state_dict_gr_state, *args_from_ui_controls_tuple):
+ queue_state = get_queue_state(state_dict_gr_state); editing_task_id = queue_state.get("editing_task_id", None)
+
+ # The first argument is the image gallery, the rest are the parameter controls.
+ input_images_pil_list = args_from_ui_controls_tuple[0]
+ all_ui_values_tuple = args_from_ui_controls_tuple[1:]
+ if not input_images_pil_list:
+ gr.Warning("Input image(s) are required!")
+ return state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value="Add Task to Queue" if editing_task_id is None else "Update Task"), gr.update(visible=editing_task_id is not None)
+
+ # Create a dictionary of UI parameters from the new ALL_TASK_UI_KEYS list.
+ temp_params_from_ui = dict(zip(ALL_TASK_UI_KEYS, all_ui_values_tuple))
+
+ # Build the dictionary of parameters that the backend worker function expects.
+ base_params_for_worker_dict = {}
+ for ui_key, worker_key in UI_TO_WORKER_PARAM_MAP.items():
+ if ui_key == 'gs_schedule_shape_ui':
+ # Special handling to convert the UI radio choice to a boolean for the worker.
+ base_params_for_worker_dict[worker_key] = temp_params_from_ui.get(ui_key) != 'Off'
+ else:
+ base_params_for_worker_dict[worker_key] = temp_params_from_ui.get(ui_key)
+
+ if editing_task_id is not None:
+ if len(input_images_pil_list) > 1: gr.Warning("Cannot update task with multiple images. Cancel edit."); return state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value="Update Task"), gr.update(visible=True)
+ pil_img_for_update = input_images_pil_list[0][0] if isinstance(input_images_pil_list[0], tuple) else input_images_pil_list[0]
+ if not isinstance(pil_img_for_update, Image.Image): gr.Warning("Invalid image format for update."); return state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value="Update Task"), gr.update(visible=True)
+ img_np_for_update = np.array(pil_img_for_update)
+ with queue_lock:
+ task_found = False
+ for task in queue_state["queue"]:
+ if task["id"] == editing_task_id:
+ task["params"] = {**base_params_for_worker_dict, 'input_image': img_np_for_update}
+ task["status"] = "pending"
+ task_found = True
+ break
+ if not task_found: gr.Warning(f"Task {editing_task_id} not found for update.")
+ else: gr.Info(f"Task {editing_task_id} updated.")
+ queue_state["editing_task_id"] = None
+ else:
+ tasks_added_count = 0; first_new_task_id = -1
+ with queue_lock:
+ for img_obj in input_images_pil_list:
+ pil_image = img_obj[0] if isinstance(img_obj, tuple) else img_obj
+ if not isinstance(pil_image, Image.Image): gr.Warning("Skipping invalid image input."); continue
+ img_np_data = np.array(pil_image)
+ next_id = queue_state["next_id"]
+ if first_new_task_id == -1: first_new_task_id = next_id
+ task = {"id": next_id, "params": {**base_params_for_worker_dict, 'input_image': img_np_data}, "status": "pending"}
+ queue_state["queue"].append(task); queue_state["next_id"] += 1; tasks_added_count += 1
+ if tasks_added_count > 0: gr.Info(f"Added {tasks_added_count} task(s) (start ID: {first_new_task_id}).")
+ else: gr.Warning("No valid tasks added.")
+
+ return state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value="Add Task(s) to Queue", variant="secondary"), gr.update(visible=False)
+
+
+def cancel_edit_mode_action(state_dict_gr_state):
+ queue_state = get_queue_state(state_dict_gr_state)
+ if queue_state.get("editing_task_id") is not None: gr.Info("Edit cancelled."); queue_state["editing_task_id"] = None
+ return state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value="Add Task(s) to Queue", variant="secondary"), gr.update(visible=False)
+
+def move_task_in_queue(state_dict_gr_state, direction: str, selected_indices_list: list):
+ if not selected_indices_list or not selected_indices_list[0]: return state_dict_gr_state, update_queue_df_display(get_queue_state(state_dict_gr_state))
+ idx = int(selected_indices_list[0][0]); queue_state = get_queue_state(state_dict_gr_state); queue = queue_state["queue"]
+ with queue_lock:
+ if direction == 'up' and idx > 0: queue[idx], queue[idx-1] = queue[idx-1], queue[idx]
+ elif direction == 'down' and idx < len(queue) - 1: queue[idx], queue[idx+1] = queue[idx+1], queue[idx]
+ return state_dict_gr_state, update_queue_df_display(queue_state)
+
+def remove_task_from_queue(state_dict_gr_state, selected_indices_list: list):
+ removed_task_id = None
+ if not selected_indices_list or not selected_indices_list[0]: return state_dict_gr_state, update_queue_df_display(get_queue_state(state_dict_gr_state)), removed_task_id
+ idx = int(selected_indices_list[0][0]); queue_state = get_queue_state(state_dict_gr_state); queue = queue_state["queue"]
+ with queue_lock:
+ if 0 <= idx < len(queue): removed_task = queue.pop(idx); removed_task_id = removed_task['id']; gr.Info(f"Removed task {removed_task_id}.")
+ else: gr.Warning("Invalid index for removal.")
+ return state_dict_gr_state, update_queue_df_display(queue_state), removed_task_id
+
+def handle_queue_action_on_select(evt: gr.SelectData, state_dict_gr_state, *ui_param_controls_tuple):
+ if evt.index is None or evt.value not in ["↑", "↓", "✖", "✎"]:
+ return [state_dict_gr_state, update_queue_df_display(get_queue_state(state_dict_gr_state))] + [gr.update()] * (len(ALL_TASK_UI_KEYS) + 4)
+
+ row_index, col_index = evt.index; button_clicked = evt.value; queue_state = get_queue_state(state_dict_gr_state); queue = queue_state["queue"]; processing_flag = queue_state.get("processing", False)
+
+ outputs_list = [state_dict_gr_state, update_queue_df_display(queue_state)] + [gr.update()] * (len(ALL_TASK_UI_KEYS) + 4)
+
+ if button_clicked == "↑":
+ if processing_flag and row_index == 0: gr.Warning("Cannot move processing task."); return outputs_list
+ new_state, new_df = move_task_in_queue(state_dict_gr_state, 'up', [[row_index, col_index]]); outputs_list[0], outputs_list[1] = new_state, new_df
+ elif button_clicked == "↓":
+ if processing_flag and row_index == 0: gr.Warning("Cannot move processing task."); return outputs_list
+ if processing_flag and row_index == 1: gr.Warning("Cannot move below processing task."); return outputs_list
+ new_state, new_df = move_task_in_queue(state_dict_gr_state, 'down', [[row_index, col_index]]); outputs_list[0], outputs_list[1] = new_state, new_df
+ elif button_clicked == "✖":
+ if processing_flag and row_index == 0: gr.Warning("Cannot remove processing task."); return outputs_list
+ new_state, new_df, removed_id = remove_task_from_queue(state_dict_gr_state, [[row_index, col_index]]); outputs_list[0], outputs_list[1] = new_state, new_df
+ # If the removed task was being edited, cancel the edit mode.
+ if removed_id is not None and queue_state.get("editing_task_id", None) == removed_id:
+ queue_state["editing_task_id"] = None
+ outputs_list[2 + 1 + len(ALL_TASK_UI_KEYS)] = gr.update(value="Add Task(s) to Queue", variant="secondary") # add_task_button
+ outputs_list[2 + 1 + len(ALL_TASK_UI_KEYS) + 1] = gr.update(visible=False) # cancel_edit_task_button
+ elif button_clicked == "✎":
+ if processing_flag and row_index == 0: gr.Warning("Cannot edit processing task."); return outputs_list
+ if 0 <= row_index < len(queue):
+ task_to_edit = queue[row_index]; task_id_to_edit = task_to_edit['id']; params_to_load_to_ui = task_to_edit['params']
+ queue_state["editing_task_id"] = task_id_to_edit; gr.Info(f"Editing Task {task_id_to_edit}.")
+
+ img_np_from_task = params_to_load_to_ui.get('input_image')
+ if isinstance(img_np_from_task, np.ndarray):
+ pil_image_for_gallery = Image.fromarray(img_np_from_task)
+ # The gallery expects a list of tuples or list of images
+ outputs_list[2] = gr.update(value=[(pil_image_for_gallery, "loaded_image")])
+ else:
+ outputs_list[2] = gr.update(value=None)
+
+ for i, ui_key in enumerate(ALL_TASK_UI_KEYS):
+ worker_key = UI_TO_WORKER_PARAM_MAP.get(ui_key)
+ if worker_key in params_to_load_to_ui:
+ value_from_task = params_to_load_to_ui[worker_key]
+ if ui_key == 'gs_schedule_shape_ui':
+ outputs_list[3 + i] = gr.update(value="Linear" if value_from_task else "Off")
+ else:
+ outputs_list[3 + i] = gr.update(value=value_from_task)
+
+ outputs_list[2 + 1 + len(ALL_TASK_UI_KEYS)] = gr.update(value="Update Task", variant="secondary")
+ outputs_list[2 + 1 + len(ALL_TASK_UI_KEYS) + 1] = gr.update(visible=True)
+ else: gr.Warning("Invalid index for edit.")
+ return outputs_list
+
+
+def clear_task_queue_action(state_dict_gr_state):
+ queue_state = get_queue_state(state_dict_gr_state); queue = queue_state["queue"]; processing = queue_state["processing"]; cleared_count = 0
+ with queue_lock:
+ if processing:
+ if len(queue) > 1: cleared_count = len(queue) - 1; queue_state["queue"] = [queue[0]]; gr.Info(f"Cleared {cleared_count} pending tasks.")
+ else: gr.Info("Only processing task in queue.")
+ elif queue: cleared_count = len(queue); queue.clear(); gr.Info(f"Cleared {cleared_count} tasks.")
+ else: gr.Info("Queue empty.")
+ if not processing and cleared_count > 0 and os.path.isfile(AUTOSAVE_FILENAME):
+ try: os.remove(AUTOSAVE_FILENAME); print(f"Cleared autosave: {AUTOSAVE_FILENAME}.")
+ except OSError as e: print(f"Error deleting autosave: {e}")
+ return state_dict_gr_state, update_queue_df_display(queue_state)
+
+def save_queue_to_zip(state_dict_gr_state):
+ queue_state = get_queue_state(state_dict_gr_state); queue = queue_state.get("queue", [])
+ if not queue: gr.Info("Queue is empty. Nothing to save."); return state_dict_gr_state, ""
+ zip_buffer = io.BytesIO(); saved_files_count = 0
+ try:
+ with tempfile.TemporaryDirectory() as tmpdir:
+ queue_manifest = []; image_paths_in_zip = {}
+ for task in queue:
+ params_copy = task['params'].copy(); task_id_s = task['id']; input_image_np_data = params_copy.pop('input_image', None)
+ manifest_entry = {"id": task_id_s, "params": params_copy, "status": task.get("status", "pending")}
+ if input_image_np_data is not None:
+ img_hash = hash(input_image_np_data.tobytes()); img_filename_in_zip = f"task_{task_id_s}_input.png"; manifest_entry['image_ref'] = img_filename_in_zip
+ if img_hash not in image_paths_in_zip:
+ img_save_path = os.path.join(tmpdir, img_filename_in_zip)
+ try: Image.fromarray(input_image_np_data).save(img_save_path, "PNG"); image_paths_in_zip[img_hash] = img_filename_in_zip; saved_files_count +=1
+ except Exception as e: print(f"Error saving image for task {task_id_s} in zip: {e}")
+ queue_manifest.append(manifest_entry)
+ manifest_path = os.path.join(tmpdir, "queue_manifest.json");
+ with open(manifest_path, 'w', encoding='utf-8') as f: json.dump(queue_manifest, f, indent=4)
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
+ zf.write(manifest_path, arcname="queue_manifest.json")
+ for img_hash, img_filename_rel in image_paths_in_zip.items(): zf.write(os.path.join(tmpdir, img_filename_rel), arcname=img_filename_rel)
+ zip_buffer.seek(0); zip_base64 = base64.b64encode(zip_buffer.getvalue()).decode('utf-8')
+ gr.Info(f"Queue with {len(queue)} tasks ({saved_files_count} images) prepared for download.")
+ return state_dict_gr_state, zip_base64
+ except Exception as e: print(f"Error creating zip for queue: {e}"); traceback.print_exc(); gr.Warning("Failed to create zip data."); return state_dict_gr_state, ""
+ finally: zip_buffer.close()
+
+def load_queue_from_zip(state_dict_gr_state, uploaded_zip_file_obj):
+ if not uploaded_zip_file_obj or not hasattr(uploaded_zip_file_obj, 'name') or not Path(uploaded_zip_file_obj.name).is_file(): gr.Warning("No valid file selected."); return state_dict_gr_state, update_queue_df_display(get_queue_state(state_dict_gr_state))
+ queue_state = get_queue_state(state_dict_gr_state); newly_loaded_queue = []; max_id_in_file = 0; loaded_image_count = 0; error_messages = []
+ try:
+ with tempfile.TemporaryDirectory() as tmpdir_extract:
+ with zipfile.ZipFile(uploaded_zip_file_obj.name, 'r') as zf:
+ if "queue_manifest.json" not in zf.namelist(): raise ValueError("queue_manifest.json not found in zip")
+ zf.extractall(tmpdir_extract)
+ manifest_path = os.path.join(tmpdir_extract, "queue_manifest.json")
+ with open(manifest_path, 'r', encoding='utf-8') as f: loaded_manifest = json.load(f)
+
+ for task_data in loaded_manifest:
+ params_from_manifest = task_data.get('params', {}); task_id_loaded = task_data.get('id', 0); max_id_in_file = max(max_id_in_file, task_id_loaded)
+ image_ref_from_manifest = task_data.get('image_ref'); input_image_np_data = None
+ if image_ref_from_manifest:
+ img_path_in_extract = os.path.join(tmpdir_extract, image_ref_from_manifest)
+ if os.path.exists(img_path_in_extract):
+ try:
+ with Image.open(img_path_in_extract) as img_pil:
+ input_image_np_data = np.array(img_pil)
+ loaded_image_count +=1
+ except Exception as img_e: error_messages.append(f"Err loading img for task {task_id_loaded}: {img_e}")
+ else: error_messages.append(f"Missing img file for task {task_id_loaded}: {image_ref_from_manifest}")
+ runtime_task = {"id": task_id_loaded, "params": {**params_from_manifest, 'input_image': input_image_np_data}, "status": "pending"}
+ newly_loaded_queue.append(runtime_task)
+ with queue_lock: queue_state["queue"] = newly_loaded_queue; queue_state["next_id"] = max(max_id_in_file + 1, queue_state.get("next_id", 1))
+ gr.Info(f"Loaded {len(newly_loaded_queue)} tasks ({loaded_image_count} images).")
+ if error_messages: gr.Warning(" ".join(error_messages))
+ except Exception as e: print(f"Error loading queue: {e}"); traceback.print_exc(); gr.Warning(f"Failed to load queue: {str(e)[:200]}")
+ finally:
+ if uploaded_zip_file_obj and hasattr(uploaded_zip_file_obj, 'name') and uploaded_zip_file_obj.name and tempfile.gettempdir() in os.path.abspath(uploaded_zip_file_obj.name):
+ try: os.remove(uploaded_zip_file_obj.name)
+ except OSError: pass
+ return state_dict_gr_state, update_queue_df_display(queue_state)
+
+global_state_for_autosave_dict = {}
+def autosave_queue_on_exit_action(state_dict_gr_state_ref):
+ print("Attempting to autosave queue on exit...")
+ queue_state = get_queue_state(state_dict_gr_state_ref)
+ if not queue_state.get("queue"): print("Autosave: Queue is empty."); return
+ try:
+ _dummy_state_ignored, zip_b64_for_save = save_queue_to_zip(state_dict_gr_state_ref)
+ if zip_b64_for_save:
+ with open(AUTOSAVE_FILENAME, "wb") as f: f.write(base64.b64decode(zip_b64_for_save))
+ print(f"Autosave successful: Queue saved to {AUTOSAVE_FILENAME}")
+ else: print("Autosave failed: Could not generate zip data.")
+ except Exception as e: print(f"Error during autosave: {e}"); traceback.print_exc()
+atexit.register(autosave_queue_on_exit_action, global_state_for_autosave_dict)
+
+def autoload_queue_on_start_action(state_dict_gr_state):
+ queue_state = get_queue_state(state_dict_gr_state); df_update = update_queue_df_display(queue_state)
+ if not queue_state["queue"] and Path(AUTOSAVE_FILENAME).is_file():
+ print(f"Autoloading queue from {AUTOSAVE_FILENAME}...")
+ class MockFilepath:
+ def __init__(self, name): self.name = name
+ temp_state_for_load = {"queue_state": queue_state.copy()}
+ loaded_state_result, df_update_after_load = load_queue_from_zip(temp_state_for_load, MockFilepath(AUTOSAVE_FILENAME))
+ if loaded_state_result["queue_state"]["queue"]:
+ queue_state.update(loaded_state_result["queue_state"]); df_update = df_update_after_load
+ print(f"Autoload successful. Loaded {len(queue_state['queue'])} tasks.")
+ try: os.remove(AUTOSAVE_FILENAME); print(f"Removed autosave file: {AUTOSAVE_FILENAME}")
+ except OSError as e: print(f"Error removing autosave file '{AUTOSAVE_FILENAME}': {e}")
+ else:
+ print("Autoload: File existed but queue remains empty."); queue_state["queue"] = []; queue_state["next_id"] = 1; df_update = update_queue_df_display(queue_state)
+ return state_dict_gr_state, df_update
+
+def extract_metadata_from_pil_image(pil_image: Image.Image) -> dict:
+ if pil_image is None: return {}
+ pnginfo_data = getattr(pil_image, 'text', None)
+ if not isinstance(pnginfo_data, dict): return {}
+ params_json_str = pnginfo_data.get('parameters')
+ if not params_json_str:
+ print("No 'parameters' JSON key found in image metadata.")
+ return {}
+ try:
+ extracted_params = json.loads(params_json_str)
+ return extracted_params if isinstance(extracted_params, dict) else {}
+ except json.JSONDecodeError as e:
+ print(f"Error decoding metadata JSON: {e}")
+ return {}
+
+def handle_image_upload_for_metadata(gallery_pil_list):
+ if not gallery_pil_list or not isinstance(gallery_pil_list, list):
+ return gr.update(visible=False)
+
+ first_image_obj = gallery_pil_list[0]
+ # The gallery component might wrap the image in a tuple
+ pil_image = first_image_obj[0] if isinstance(first_image_obj, tuple) else first_image_obj
+
+ if not isinstance(pil_image, Image.Image):
+ return gr.update(visible=False)
+ try:
+ metadata = extract_metadata_from_pil_image(pil_image)
+ # MERGE: Only show the modal if creative parameters are found in the metadata.
+ if metadata and any(key in metadata for key in CREATIVE_PARAM_KEYS):
+ return gr.update(visible=True) # Show the modal
+ except Exception as e:
+ print(f"Error handling image upload for metadata: {e}")
+
+ return gr.update(visible=False)
+
+def ui_load_params_from_image_metadata(gallery_data_list):
+ # Loads ONLY creative parameters from image metadata and returns UI updates.
+ updates_for_ui = [gr.update()] * len(CREATIVE_PARAM_KEYS)
+
+ try:
+ first_image_obj = gallery_data_list[0]
+ pil_image = first_image_obj[0] if isinstance(first_image_obj, tuple) else first_image_obj
+ extracted_metadata = extract_metadata_from_pil_image(pil_image)
+ except Exception:
+ return updates_for_ui
+
+ if not extracted_metadata:
+ gr.Info("No relevant parameters found in image metadata.")
+ return updates_for_ui
+
+ gr.Info(f"Found metadata, applying to creative settings...")
+ num_applied = 0
+ default_values_map = get_default_values_map()
+ params_to_apply = {}
+
+ # Special handling for gs_schedule_active_ui, mapping it back to gs_schedule_shape_ui
+ if 'gs_schedule_active_ui' in extracted_metadata:
+ params_to_apply['gs_schedule_shape_ui'] = "Linear" if str(extracted_metadata['gs_schedule_active_ui']).lower() == 'true' else "Off"
+
+ # Iterate through only the creative keys to build the update list.
+ for i, key in enumerate(CREATIVE_PARAM_KEYS):
+ if key in extracted_metadata or key in params_to_apply:
+ raw_value = params_to_apply.get(key, extracted_metadata.get(key))
+ new_val = raw_value
+ try:
+ # Apply the same robust type checking as the main settings loader
+ if key in ['seed', 'steps', 'preview_frequency_ui']: new_val = int(raw_value)
+ elif key in ['total_second_length', 'cfg', 'gs_ui', 'rs', 'gs_final_ui']: new_val = float(raw_value)
+ updates_for_ui[i] = gr.update(value=new_val); num_applied += 1
+ except (ValueError, TypeError) as ve:
+ print(f"Metadata Warning: Could not convert '{raw_value}' for '{key}': {ve}")
+
+ if num_applied > 0: gr.Info(f"Applied {num_applied} parameter(s) from image metadata.")
+ return updates_for_ui
+
+def apply_and_hide_modal(gallery_data_list):
+ updates = ui_load_params_from_image_metadata(gallery_data_list)
+ return [gr.update(visible=False)] + updates
+
+def ui_update_total_segments(total_seconds_ui, latent_window_size_ui):
+ if not isinstance(total_seconds_ui, (int, float)) or not isinstance(latent_window_size_ui, (int, float)): return "Segments: Invalid input"
+ if latent_window_size_ui <= 0: return "Segments: Invalid window size"
+ total_vid_frames = total_seconds_ui * 30; calculated_sections = total_vid_frames / (latent_window_size_ui * 4)
+ total_segments = int(max(round(calculated_sections), 1)); return f"Calculated Total Segments: {total_segments}"
+
+def process_task_queue_main_loop(state_dict_gr_state):
+ queue_state = get_queue_state(state_dict_gr_state)
+ abort_event.clear()
+ if queue_state["processing"]: gr.Info("Queue is already processing."); yield state_dict_gr_state, update_queue_df_display(queue_state), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(interactive=False), gr.update(interactive=True); return
+ if not queue_state["queue"]: gr.Info("Queue is empty."); yield state_dict_gr_state, update_queue_df_display(queue_state), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(interactive=True), gr.update(interactive=False); return
+ queue_state["processing"] = True
+ yield (state_dict_gr_state, update_queue_df_display(queue_state), gr.update(), gr.update(visible=False), gr.update(value="Queue processing started..."), gr.update(value=""), gr.update(interactive=False), gr.update(interactive=True))
+ output_stream_for_ui = AsyncStream()
+
+ while queue_state["queue"] and not abort_event.is_set():
+ with queue_lock:
+ if not queue_state["queue"]: break
+ current_task_obj = queue_state["queue"][0]
+ task_parameters_for_worker = current_task_obj["params"]
+ current_task_id = current_task_obj["id"]
+
+ if task_parameters_for_worker.get('input_image') is None:
+ print(f"Skipping task {current_task_id}: Missing input image data.")
+ gr.Warning(f"Task {current_task_id} skipped: Input image is missing. Please edit the task to add an image.")
+ with queue_lock:
+ current_task_obj["status"] = "error"
+ current_task_obj["error_message"] = "Missing Image"
+ yield (state_dict_gr_state, update_queue_df_display(queue_state), gr.update(), gr.update(visible=False), gr.update(), gr.update(), gr.update(interactive=False), gr.update(interactive=True))
+ gr.Info("Queue processing halted due to task with missing image. Please remove or fix the task.")
+ break
+ if task_parameters_for_worker.get('seed') == -1:
+ task_parameters_for_worker['seed'] = np.random.randint(0, 2**32 - 1)
+ print(f"Task {current_task_id}: Using random seed {task_parameters_for_worker['seed']}")
+
+ print(f"Starting task {current_task_id} (Prompt: {task_parameters_for_worker.get('prompt', '')[:30]}...)."); current_task_obj["status"] = "processing"
+ yield (state_dict_gr_state, update_queue_df_display(queue_state), gr.update(), gr.update(visible=False), gr.update(value=f"Processing Task {current_task_id}..."), "", gr.update(interactive=False), gr.update(interactive=True))
+ worker_args = {**task_parameters_for_worker, 'task_id': current_task_id, 'output_queue_ref': output_stream_for_ui.output_queue, 'abort_event': abort_event, 'text_encoder': text_encoder, 'text_encoder_2': text_encoder_2, 'tokenizer': tokenizer, 'tokenizer_2': tokenizer_2, 'vae': vae, 'feature_extractor': feature_extractor, 'image_encoder': image_encoder, 'transformer': transformer, 'high_vram_flag': high_vram}
+ async_run(worker, **worker_args)
+ last_known_output_filename = None; task_completed_successfully = False
+ while True:
+ flag, data_from_worker = output_stream_for_ui.output_queue.next()
+ if flag == 'progress':
+ msg_task_id, preview_np_array, desc_str, html_str = data_from_worker
+ if msg_task_id == current_task_id: yield (state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value=last_known_output_filename), gr.update(visible=(preview_np_array is not None), value=preview_np_array), desc_str, html_str, gr.update(interactive=False), gr.update(interactive=True))
+ elif flag == 'file':
+ msg_task_id, segment_file_path, segment_info = data_from_worker
+ if msg_task_id == current_task_id: last_known_output_filename = segment_file_path; gr.Info(f"Task {current_task_id}: {segment_info}"); yield (state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value=last_known_output_filename), gr.update(), gr.update(), gr.update(), gr.update(interactive=False), gr.update(interactive=True))
+ elif flag == 'aborted':
+ msg_task_id = data_from_worker; print(f"Task {current_task_id} confirmed aborted by worker."); current_task_obj["status"] = "aborted"; task_completed_successfully = False; break
+ elif flag == 'error':
+ msg_task_id, error_message_str = data_from_worker; print(f"Task {current_task_id} failed: {error_message_str}"); gr.Warning(f"Task {current_task_id} Error: {error_message_str}"); current_task_obj["status"] = "error"; current_task_obj["error_message"] = str(error_message_str)[:100]; task_completed_successfully = False; break
+ elif flag == 'end':
+ msg_task_id, success_bool, final_video_path = data_from_worker; task_completed_successfully = success_bool; last_known_output_filename = final_video_path if success_bool else last_known_output_filename; current_task_obj["status"] = "done" if success_bool else "error"; print(f"Task {current_task_id} ended. Success: {success_bool}. Output: {last_known_output_filename}"); break
+
+ with queue_lock:
+ if queue_state["queue"] and queue_state["queue"][0]["id"] == current_task_id:
+ queue_state["queue"].pop(0)
+ print(f"Task {current_task_id} popped from queue.")
+ else:
+ print(f"Warning: Task {current_task_id} not at queue head after processing. It might have been removed already.")
+ final_desc = f"Task {current_task_id} {'completed' if task_completed_successfully else 'finished with issues'}."
+ yield (state_dict_gr_state, update_queue_df_display(queue_state), gr.update(value=last_known_output_filename), gr.update(visible=False), gr.update(value=final_desc), gr.update(value=""), gr.update(interactive=False), gr.update(interactive=True))
+
+ if abort_event.is_set(): gr.Info("Queue processing halted by user."); break
+
+ queue_state["processing"] = False;
+ print("Queue processing loop finished.")
+ final_status_msg = "All tasks processed." if not abort_event.is_set() else "Queue processing aborted."
+ yield (state_dict_gr_state, update_queue_df_display(queue_state), gr.update(), gr.update(visible=False), gr.update(value=final_status_msg), gr.update(value=""), gr.update(interactive=True), gr.update(interactive=False))
+
+def abort_current_task_processing_action(state_dict_gr_state):
+ queue_state = get_queue_state(state_dict_gr_state);
+ if queue_state["processing"]: gr.Info("Abort signal sent. Current task will attempt to stop shortly."); abort_event.set()
+ else: gr.Info("Nothing is currently processing.")
+ return state_dict_gr_state, gr.update(interactive=not queue_state["processing"])
+
+# --- Gradio UI Definition ---
+css_theme = make_progress_bar_css() + """ #queue_df { font-size: 0.85rem; } #queue_df th:nth-child(1), #queue_df td:nth-child(1) { width: 5%; } #queue_df th:nth-child(2), #queue_df td:nth-child(2) { width: 10%; } #queue_df th:nth-child(3), #queue_df td:nth-child(3) { width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;} #queue_df th:nth-child(4), #queue_df td:nth-child(4) { width: 8%; } #queue_df th:nth-child(5), #queue_df td:nth-child(5) { width: 8%; } #queue_df th:nth-child(6), #queue_df td:nth-child(6) { width: 10%; text-align: center; } #queue_df th:nth-child(7), #queue_df td:nth-child(7), #queue_df th:nth-child(8), #queue_df td:nth-child(8), #queue_df th:nth-child(9), #queue_df td:nth-child(9), #queue_df th:nth-child(10), #queue_df td:nth-child(10) { width: 4%; cursor: pointer; text-align: center; } #queue_df td:hover { background-color: #f0f0f0; } .gradio-container { max-width: 95% !important; margin: auto !important; } """
+block = gr.Blocks(css=css_theme, title="FramePack SVC").queue()
+with block:
+ app_state = gr.State({"queue_state": {"queue": [], "next_id": 1, "processing": False, "editing_task_id": None}})
+ gr.Markdown('# FramePack SVC (Stable Video Creation)')
+
+ with Modal(visible=False) as metadata_modal:
+ gr.Markdown("Image has saved parameters. Overwrite current creative settings?")
+ with gr.Row():
+ cancel_metadata_btn = gr.Button("No, Keep Current")
+ confirm_metadata_btn = gr.Button("Yes, Apply Settings", variant="primary")
+
+ with gr.Row():
+ with gr.Column(scale=1):
+ input_image_gallery_ui = gr.Gallery(type="pil", label="Input Image(s)", height=320, preview=True, allow_preview=True)
+
+ # --- Creative UI Components ---
+ prompt_ui = gr.Textbox(label="Prompt", lines=3, placeholder="A detailed description of the motion to generate.")
+ example_quick_prompts_ui = gr.Dataset(visible=True, components=[prompt_ui])
+ n_prompt_ui = gr.Textbox(label="Negative Prompt", lines=2, placeholder="Concepts to avoid.")
+ with gr.Row():
+ total_second_length_ui = gr.Slider(label="Total Video Length (Seconds)", minimum=0.1, maximum=120, value=5.0, step=0.1)
+ seed_ui = gr.Number(label="Seed", value=-1, precision=0)
+
+ with gr.Accordion("Advanced Settings", open=False):
+ total_segments_display_ui = gr.Markdown("Calculated Total Segments: N/A")
+ preview_frequency_ui = gr.Slider(label="Preview Frequency", minimum=0, maximum=100, value=5, step=1, info="Produce mp4 preview every N steps (0=off).")
+ segments_to_decode_csv_ui = gr.Textbox(label="Preview Segments", placeholder="e.g., 1,5,10. Always saves first & final mp4.", value="")
+ with gr.Row():
+ gs_ui = gr.Slider(label="Distilled CFG Start", minimum=1.0, maximum=32.0, value=10.0, step=0.01)
+ gs_schedule_shape_ui = gr.Radio(["Off", "Linear"], label="Variable CFG", value="Off")
+ gs_final_ui = gr.Slider(label="Distilled CFG End", minimum=1.0, maximum=32.0, value=10.0, step=0.01, interactive=False)
+ cfg_ui = gr.Slider(label="CFG (Real)", minimum=1.0, maximum=32.0, value=1.0, step=0.01)
+ rs_ui = gr.Slider(label="RS", minimum=0.0, maximum=32.0, value=0.0, step=0.01)
+ steps_ui = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1)
+
+ # --- Environment & Debug UI Components ---
+ with gr.Accordion("Debug Settings", open=False):
+ use_teacache_ui = gr.Checkbox(label='Use TeaCache (Optimize Speed)', value=True)
+ use_fp32_transformer_output_checkbox_ui = gr.Checkbox(label="Use FP32 Transformer Output", value=False)
+ gpu_memory_preservation_ui = gr.Slider(label="GPU Preserved Memory (GB)", minimum=4, maximum=128, value=6.0, step=0.1)
+ mp4_crf_ui = gr.Slider(label="MP4 Compression (CRF)", minimum=0, maximum=51, value=18, step=1)
+ latent_window_size_ui = gr.Slider(label="Latent Window Size", minimum=1, maximum=33, value=9, step=1, visible=True)
+ output_folder_ui_ctrl = gr.Textbox(label="Output Folder", value=outputs_folder)
+ save_as_default_button = gr.Button(value="Save Current as Default", variant="secondary")
+
+ # --- Main Workspace Save/Load Buttons ---
+ save_workspace_button = gr.Button(value="Save Workspace", variant="secondary") # Renamed
+ load_workspace_button = gr.Button(value="Load Workspace", variant="secondary") # Renamed
+
+ with gr.Column(scale=2):
+ gr.Markdown("## Task Queue")
+ queue_df_display_ui = gr.DataFrame(headers=["ID", "Status", "Prompt", "Length", "Steps", "Input", "↑", "↓", "✖", "✎"], datatype=["number","markdown","markdown","str","number","markdown","markdown","markdown","markdown","markdown"], col_count=(10,"fixed"), value=[], interactive=False, visible=False, elem_id="queue_df", wrap=True)
+ with gr.Row():
+ add_task_button = gr.Button(value="Add to Queue", variant="secondary")
+ process_queue_button = gr.Button("▶️ Process Queue", variant="primary")
+ abort_task_button = gr.Button("⏹️ Abort", variant="stop", interactive=False)
+ cancel_edit_task_button = gr.Button("Cancel Edit", visible=False, variant="secondary")
+ with gr.Row():
+ save_queue_zip_b64_output = gr.Text(visible=False); save_queue_button_ui = gr.DownloadButton("Save Queue", size="sm"); load_queue_button_ui = gr.UploadButton("Load Queue", file_types=[".zip"], size="sm"); clear_queue_button_ui = gr.Button("Clear Pending", size="sm", variant="stop")
+ gr.Markdown("## Live Preview")
+ current_task_preview_image_ui = gr.Image(interactive=False, visible=False)
+ current_task_progress_desc_ui = gr.Markdown('')
+ current_task_progress_bar_ui = gr.HTML('')
+ gr.Markdown("## Output Video")
+ last_finished_video_ui = gr.Video(interactive=True, autoplay=False)
+
+ # --- Define explicit lists of UI components for wiring up events ---
+ creative_ui_components = [
+ prompt_ui, n_prompt_ui, total_second_length_ui, seed_ui, preview_frequency_ui,
+ segments_to_decode_csv_ui, gs_ui, gs_schedule_shape_ui, gs_final_ui, steps_ui, cfg_ui, rs_ui
+ ]
+ environment_ui_components = [
+ use_teacache_ui, use_fp32_transformer_output_checkbox_ui, gpu_memory_preservation_ui,
+ mp4_crf_ui, output_folder_ui_ctrl, latent_window_size_ui
+ ]
+ full_workspace_ui_components = creative_ui_components + environment_ui_components
+
+ task_defining_ui_inputs = [input_image_gallery_ui] + full_workspace_ui_components
+
+ process_queue_outputs_list = [app_state, queue_df_display_ui, last_finished_video_ui, current_task_preview_image_ui, current_task_progress_desc_ui, current_task_progress_bar_ui, process_queue_button, abort_task_button]
+
+ # This list defines all the UI elements that can be updated when a task is selected for editing.
+ queue_df_select_outputs_list = [app_state, queue_df_display_ui, input_image_gallery_ui] + full_workspace_ui_components + [add_task_button, cancel_edit_task_button, last_finished_video_ui]
+
+ # Workspace Settings (.json file) Handlers
+ save_workspace_button.click(fn=save_workspace, inputs=full_workspace_ui_components, outputs=[])
+ load_workspace_button.click(fn=load_workspace, inputs=[], outputs=full_workspace_ui_components)
+ save_as_default_button.click(fn=save_as_default_workspace, inputs=full_workspace_ui_components, outputs=[])
+
+ # PNG Metadata (Creative Recipe) Handlers
+ input_image_gallery_ui.upload(fn=handle_image_upload_for_metadata, inputs=[input_image_gallery_ui], outputs=[metadata_modal])
+ confirm_metadata_outputs = [metadata_modal] + creative_ui_components
+ confirm_metadata_btn.click(fn=apply_and_hide_modal, inputs=[input_image_gallery_ui], outputs=confirm_metadata_outputs)
+ cancel_metadata_btn.click(lambda: gr.update(visible=False), None, metadata_modal)
+
+ # Queue and Processing Logic
+ add_task_button.click(fn=add_or_update_task_in_queue, inputs=[app_state] + task_defining_ui_inputs, outputs=[app_state, queue_df_display_ui, add_task_button, cancel_edit_task_button])
+ process_queue_button.click(fn=process_task_queue_main_loop, inputs=[app_state], outputs=process_queue_outputs_list)
+ cancel_edit_task_button.click(fn=cancel_edit_mode_action, inputs=[app_state], outputs=[app_state, queue_df_display_ui, add_task_button, cancel_edit_task_button])
+ abort_task_button.click(fn=abort_current_task_processing_action, inputs=[app_state], outputs=[app_state, abort_task_button])
+ clear_queue_button_ui.click(fn=clear_task_queue_action, inputs=[app_state], outputs=[app_state, queue_df_display_ui])
+ save_queue_button_ui.click(fn=save_queue_to_zip, inputs=[app_state], outputs=[app_state, save_queue_zip_b64_output]).then(fn=None, inputs=[save_queue_zip_b64_output], outputs=None, js="""(b64) => { if(!b64) return; const blob = new Blob([Uint8Array.from(atob(b64), c => c.charCodeAt(0))], {type: 'application/zip'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='framepack_svc_queue.zip'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }""")
+ load_queue_button_ui.upload(fn=load_queue_from_zip, inputs=[app_state, load_queue_button_ui], outputs=[app_state, queue_df_display_ui])
+ queue_df_display_ui.select(fn=handle_queue_action_on_select, inputs=[app_state] + task_defining_ui_inputs, outputs=queue_df_select_outputs_list)
+ def toggle_gs_final(gs_schedule_choice): return gr.update(interactive=(gs_schedule_choice != "Off"))
+ gs_schedule_shape_ui.change(fn=toggle_gs_final, inputs=[gs_schedule_shape_ui], outputs=[gs_final_ui])
+ for ctrl in [total_second_length_ui, latent_window_size_ui]: ctrl.change(fn=ui_update_total_segments, inputs=[total_second_length_ui, latent_window_size_ui], outputs=[total_segments_display_ui])
+
+ block.load(fn=load_default_workspace_on_start, inputs=[], outputs=full_workspace_ui_components).then(fn=autoload_queue_on_start_action, inputs=[app_state], outputs=[app_state, queue_df_display_ui]).then(lambda s_val: global_state_for_autosave_dict.update(s_val), inputs=[app_state], outputs=None).then(fn=ui_update_total_segments, inputs=[total_second_length_ui, latent_window_size_ui], outputs=[total_segments_display_ui])
+
+expanded_outputs_folder = os.path.abspath(os.path.expanduser(outputs_folder))
+if __name__ == "__main__":
+ print("Starting FramePack SVC application...")
+ block.launch(server_name=args.server, server_port=args.port, share=args.share, inbrowser=args.inbrowser, allowed_paths=[expanded_outputs_folder])
\ No newline at end of file
diff --git a/generation_core.py b/generation_core.py
new file mode 100644
index 00000000..d2d91225
--- /dev/null
+++ b/generation_core.py
@@ -0,0 +1,266 @@
+import torch
+import traceback
+import einops
+import numpy as np
+import os
+import threading
+import json
+from PIL import Image
+from PIL.PngImagePlugin import PngInfo
+
+from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake
+from diffusers_helper.utils import (
+ save_bcthw_as_mp4,
+ crop_or_pad_yield_mask,
+ soft_append_bcthw,
+ resize_and_center_crop,
+ generate_timestamp
+)
+from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan
+from diffusers_helper.memory import (
+ unload_complete_models,
+ load_model_as_complete,
+ move_model_to_device_with_memory_preservation,
+ offload_model_from_device_for_memory_preservation,
+ fake_diffusers_current_device,
+ gpu
+)
+from diffusers_helper.clip_vision import hf_clip_vision_encode
+from diffusers_helper.bucket_tools import find_nearest_bucket
+from diffusers_helper.gradio.progress_bar import make_progress_bar_html
+
+
+@torch.no_grad()
+def worker(
+ # --- Task I/O & Identity ---
+ task_id,
+ input_image,
+ output_folder,
+ output_queue_ref,
+
+ # --- Creative Parameters (The "Recipe") ---
+ prompt,
+ n_prompt,
+ seed,
+ total_second_length,
+ steps,
+ cfg,
+ gs,
+ gs_final,
+ gs_schedule_active,
+ rs,
+ preview_frequency,
+ segments_to_decode_csv,
+
+ # --- Environment & Debug Parameters ---
+ latent_window_size,
+ gpu_memory_preservation,
+ use_teacache,
+ use_fp32_transformer_output,
+ mp4_crf,
+
+ # --- Model & System Objects (Passed from main app) ---
+ text_encoder,
+ text_encoder_2,
+ tokenizer,
+ tokenizer_2,
+ vae,
+ feature_extractor,
+ image_encoder,
+ transformer,
+ high_vram_flag,
+
+ # --- Control Flow ---
+ abort_event: threading.Event = None
+):
+ outputs_folder = os.path.expanduser(output_folder) if output_folder else './outputs/'
+ os.makedirs(outputs_folder, exist_ok=True)
+
+ total_latent_sections = (total_second_length * 30) / (latent_window_size * 4)
+ total_latent_sections = int(max(round(total_latent_sections), 1))
+
+ job_id = f"{generate_timestamp()}_task{task_id}"
+ output_queue_ref.push(('progress', (task_id, None, f'Total Segments: {total_latent_sections}', make_progress_bar_html(0, 'Starting ...'))))
+
+ parsed_segments_to_decode_set = set()
+ if segments_to_decode_csv:
+ try:
+ parsed_segments_to_decode_set = {int(s.strip()) for s in segments_to_decode_csv.split(',') if s.strip()}
+ except ValueError:
+ print(f"Task {task_id}: Warning - Could not parse 'Segments to Decode CSV': \"{segments_to_decode_csv}\".")
+ final_output_filename = None; success = False
+ initial_gs_from_ui = gs
+ gs_final_value_for_schedule = gs_final if gs_final is not None else initial_gs_from_ui
+ original_fp32_setting = transformer.high_quality_fp32_output_for_inference
+ transformer.high_quality_fp32_output_for_inference = use_fp32_transformer_output
+ print(f"Task {task_id}: transformer.high_quality_fp32_output_for_inference set to {use_fp32_transformer_output}")
+
+ try:
+ if not isinstance(input_image, np.ndarray): raise ValueError(f"Task {task_id}: input_image is not a NumPy array.")
+
+ output_queue_ref.push(('progress', (task_id, None, f'Total Segments: {total_latent_sections}', make_progress_bar_html(0, 'Image processing ...'))))
+ if input_image.shape[-1] == 4:
+ pil_img = Image.fromarray(input_image)
+ input_image = np.array(pil_img.convert("RGB"))
+ H, W, C = input_image.shape
+ if C != 3: raise ValueError(f"Task {task_id}: Input image must be RGB, found {C} channels.")
+ height, width = find_nearest_bucket(H, W, resolution=640)
+ input_image_np = resize_and_center_crop(input_image, target_width=width, target_height=height)
+
+ metadata_obj = PngInfo()
+ params_to_save_in_metadata = {
+ "prompt": prompt,
+ "n_prompt": n_prompt,
+ "seed": seed,
+ "total_second_length": total_second_length,
+ "steps": steps,
+ "cfg": cfg,
+ "gs": gs,
+ "gs_final": gs_final,
+ "gs_schedule_active": gs_schedule_active,
+ "rs": rs,
+ "preview_frequency": preview_frequency,
+ "segments_to_decode_csv": segments_to_decode_csv
+ }
+ metadata_obj.add_text("parameters", json.dumps(params_to_save_in_metadata))
+ initial_image_with_params_path = os.path.join(outputs_folder, f'{job_id}_initial_image_with_params.png')
+ try: Image.fromarray(input_image_np).save(initial_image_with_params_path, pnginfo=metadata_obj)
+ except Exception as e_png: print(f"Task {task_id}: WARNING - Failed to save initial image with parameters: {e_png}")
+
+ if not high_vram_flag: unload_complete_models(text_encoder, text_encoder_2, image_encoder, vae, transformer)
+ output_queue_ref.push(('progress', (task_id, None, f'Total Segments: {total_latent_sections}', make_progress_bar_html(0, 'Text encoding ...'))))
+ if not high_vram_flag: fake_diffusers_current_device(text_encoder, gpu); load_model_as_complete(text_encoder_2, target_device=gpu)
+ llama_vec, clip_l_pooler = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2)
+ if cfg == 1: llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler)
+ else: llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2)
+ llama_vec, llama_attention_mask = crop_or_pad_yield_mask(llama_vec, length=512); llama_vec_n, llama_attention_mask_n = crop_or_pad_yield_mask(llama_vec_n, length=512)
+ input_image_pt = torch.from_numpy(input_image_np).float().permute(2,0,1).unsqueeze(0) / 127.5 - 1.0; input_image_pt = input_image_pt[:,:,None,:,:]
+ output_queue_ref.push(('progress', (task_id, None, f'Total Segments: {total_latent_sections}', make_progress_bar_html(0, 'VAE encoding ...'))))
+ if not high_vram_flag: load_model_as_complete(vae, target_device=gpu)
+ start_latent = vae_encode(input_image_pt, vae)
+ output_queue_ref.push(('progress', (task_id, None, f'Total Segments: {total_latent_sections}', make_progress_bar_html(0, 'CLIP Vision encoding ...'))))
+ if not high_vram_flag: load_model_as_complete(image_encoder, target_device=gpu)
+ image_encoder_output = hf_clip_vision_encode(input_image_np, feature_extractor, image_encoder)
+ image_encoder_last_hidden_state = image_encoder_output.last_hidden_state
+ llama_vec, llama_vec_n, clip_l_pooler, clip_l_pooler_n, image_encoder_last_hidden_state = [t.to(transformer.dtype) for t in [llama_vec, llama_vec_n, clip_l_pooler, clip_l_pooler_n, image_encoder_last_hidden_state]]
+
+ output_queue_ref.push(('progress', (task_id, None, f'Total Segments: {total_latent_sections}', make_progress_bar_html(0, 'Start sampling ...'))))
+ rnd = torch.Generator(device="cpu").manual_seed(int(seed))
+ num_frames = latent_window_size * 4 - 3
+ #overlapped_frames = num_frames
+
+ history_latents = torch.zeros(size=(1, 16, 1 + 2 + 16, height // 8, width // 8), dtype=torch.float32, device="cpu"); history_pixels = None
+ total_generated_latent_frames = 0
+ latent_paddings = list(reversed(range(total_latent_sections)))
+ if total_latent_sections > 4: latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0]
+
+ for latent_padding_iteration, latent_padding in enumerate(latent_paddings):
+ if abort_event and abort_event.is_set(): raise KeyboardInterrupt("Abort signal received.")
+ is_last_section = (latent_padding == 0)
+ latent_padding_size = latent_padding * latent_window_size
+ print(f'Task {task_id}: Seg {latent_padding_iteration + 1}/{total_latent_sections} (lp_val={latent_padding}), last_loop_seg={is_last_section}')
+
+ indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16]), device="cpu").unsqueeze(0)
+ clean_latent_indices_pre, _, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1)
+ clean_latents_pre = start_latent.to(history_latents.device, dtype=history_latents.dtype)
+ clean_latent_indices = torch.cat([clean_latent_indices_pre, clean_latent_indices_post], dim=1)
+ #current_history_depth_for_clean_split = history_latents.shape[2]; needed_depth_for_clean_split = 1 + 2 + 16
+ #history_latents_for_clean_split = history_latents
+ #if current_history_depth_for_clean_split < needed_depth_for_clean_split:
+ # padding_needed = needed_depth_for_clean_split - current_history_depth_for_clean_split
+ # pad_tensor = torch.zeros(history_latents.shape[0], history_latents.shape[1], padding_needed, history_latents.shape[3], history_latents.shape[4], dtype=history_latents.dtype, device=history_latents.device)
+ # history_latents_for_clean_split = torch.cat((history_latents, pad_tensor), dim=2)
+ clean_latents_post, clean_latents_2x, clean_latents_4x = history_latents[:, :, :1 + 2 + 16, :, :].split([1, 2, 16], dim=2)
+ clean_latents = torch.cat([clean_latents_pre, clean_latents_post], dim=2)
+
+ if not high_vram_flag: unload_complete_models(); move_model_to_device_with_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=gpu_memory_preservation)
+ transformer.initialize_teacache(enable_teacache=use_teacache, num_steps=steps)
+
+ def callback_diffusion_step(d):
+ if abort_event and abort_event.is_set(): raise KeyboardInterrupt("Abort signal received during sampling.")
+ current_diffusion_step = d['i'] + 1
+ is_first_step = current_diffusion_step == 1
+ is_last_step = current_diffusion_step == steps
+ is_preview_step = preview_frequency > 0 and (current_diffusion_step % preview_frequency == 0)
+ if not (is_first_step or is_last_step or is_preview_step):
+ return
+ preview_latent = d['denoised']; preview_img_np = vae_decode_fake(preview_latent)
+ preview_img_np = (preview_img_np * 255.0).detach().cpu().numpy().clip(0, 255).astype(np.uint8)
+ preview_img_np = einops.rearrange(preview_img_np, 'b c t h w -> (b h) (t w) c')
+ percentage = int(100.0 * current_diffusion_step / steps)
+ hint = f'Segment {latent_padding_iteration + 1}, Sampling {current_diffusion_step}/{steps}'
+ current_video_frames_count = history_pixels.shape[2] if history_pixels is not None else 0
+ desc = f'Task {task_id}: Vid Frames: {current_video_frames_count}, Len: {current_video_frames_count / 30 :.2f}s. Seg {latent_padding_iteration + 1}/{total_latent_sections}. Extending...'
+ output_queue_ref.push(('progress', (task_id, preview_img_np, desc, make_progress_bar_html(percentage, hint))))
+
+ current_segment_gs_to_use = initial_gs_from_ui
+ if gs_schedule_active and total_latent_sections > 1:
+ progress_for_gs = latent_padding_iteration / (total_latent_sections - 1) if total_latent_sections > 1 else 0
+ current_segment_gs_to_use = initial_gs_from_ui + (gs_final_value_for_schedule - initial_gs_from_ui) * progress_for_gs
+
+ generated_latents = sample_hunyuan(
+ transformer=transformer, sampler='unipc', width=width, height=height, frames=num_frames,
+ real_guidance_scale=cfg, distilled_guidance_scale=current_segment_gs_to_use, guidance_rescale=rs,
+ num_inference_steps=steps, generator=rnd, prompt_embeds=llama_vec.to(transformer.device),
+ prompt_embeds_mask=llama_attention_mask.to(transformer.device), prompt_poolers=clip_l_pooler.to(transformer.device),
+ negative_prompt_embeds=llama_vec_n.to(transformer.device), negative_prompt_embeds_mask=llama_attention_mask_n.to(transformer.device),
+ negative_prompt_poolers=clip_l_pooler_n.to(transformer.device), device=transformer.device, dtype=transformer.dtype,
+ image_embeddings=image_encoder_last_hidden_state.to(transformer.device), latent_indices=latent_indices.to(transformer.device),
+ clean_latents=clean_latents.to(transformer.device, dtype=transformer.dtype), clean_latent_indices=clean_latent_indices.to(transformer.device),
+ clean_latents_2x=clean_latents_2x.to(transformer.device, dtype=transformer.dtype), clean_latent_2x_indices=clean_latent_2x_indices.to(transformer.device),
+ clean_latents_4x=clean_latents_4x.to(transformer.device, dtype=transformer.dtype), clean_latent_4x_indices=clean_latent_4x_indices.to(transformer.device),
+ callback=callback_diffusion_step
+ )
+
+ if is_last_section:
+ generated_latents = torch.cat([start_latent.to(generated_latents), generated_latents], dim=2)
+
+ total_generated_latent_frames += int(generated_latents.shape[2])
+ history_latents = torch.cat([generated_latents.to(history_latents), history_latents], dim=2)
+
+ if not high_vram_flag:
+ offload_model_from_device_for_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=8)
+ load_model_as_complete(vae, target_device=gpu)
+
+ real_history_latents = history_latents[:, :, :total_generated_latent_frames, :, :]
+
+ if history_pixels is None:
+ history_pixels = vae_decode(real_history_latents, vae).cpu()
+ else:
+ section_latent_frames = (latent_window_size * 2 + 1) if is_last_section else (latent_window_size * 2)
+ overlapped_frames = latent_window_size * 4 - 3
+ current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames], vae).cpu()
+ history_pixels = soft_append_bcthw(current_pixels, history_pixels, overlapped_frames)
+
+ if not high_vram_flag: unload_complete_models()
+
+ current_video_frame_count = history_pixels.shape[2]
+
+ # Skip writing preview mp4 for this segment logic
+ should_save_mp4_this_iteration = False
+ current_segment_1_indexed = latent_padding_iteration # + 1
+ if (latent_padding_iteration == 0) or is_last_section or (parsed_segments_to_decode_set and current_segment_1_indexed in parsed_segments_to_decode_set):
+ should_save_mp4_this_iteration = True
+ if should_save_mp4_this_iteration:
+ segment_mp4_filename = os.path.join(outputs_folder, f'{job_id}_segment_{latent_padding_iteration}_frames_{current_video_frame_count}.mp4')
+ save_bcthw_as_mp4(history_pixels, segment_mp4_filename, fps=30, crf=mp4_crf)
+ final_output_filename = segment_mp4_filename
+ print(f"Task {task_id}: SAVED MP4 for segment {latent_padding_iteration} to {segment_mp4_filename}. Total video frames: {current_video_frame_count}")
+ output_queue_ref.push(('file', (task_id, segment_mp4_filename, f"Segment {latent_padding_iteration} MP4 saved ({current_video_frame_count} frames)")))
+ else:
+ print(f"Task {task_id}: SKIPPED MP4 save for intermediate segment {current_segment_1_indexed}.")
+
+ if is_last_section: success = True; break
+
+ except KeyboardInterrupt:
+ print(f"Worker task {task_id} caught KeyboardInterrupt (likely abort signal).")
+ output_queue_ref.push(('aborted', task_id)); success = False
+ except Exception as e:
+ print(f"Error in worker task {task_id}: {e}"); traceback.print_exc(); output_queue_ref.push(('error', (task_id, str(e)))); success = False
+ finally:
+ transformer.high_quality_fp32_output_for_inference = original_fp32_setting
+ print(f"Task {task_id}: Restored transformer.high_quality_fp32_output_for_inference to {original_fp32_setting}")
+ if not high_vram_flag: unload_complete_models(text_encoder, text_encoder_2, image_encoder, vae, transformer)
+ if final_output_filename and not os.path.dirname(final_output_filename) == os.path.abspath(outputs_folder):
+ final_output_filename = os.path.join(outputs_folder, os.path.basename(final_output_filename))
+ output_queue_ref.push(('end', (task_id, success, final_output_filename)))
\ No newline at end of file
diff --git a/requirements_svc.txt b/requirements_svc.txt
new file mode 100644
index 00000000..3df6b6de
--- /dev/null
+++ b/requirements_svc.txt
@@ -0,0 +1,22 @@
+accelerate==1.6.0
+diffusers==0.33.1
+transformers==4.46.2
+gradio==5.23.0
+sentencepiece==0.2.0
+pillow==11.1.0
+av==12.1.0
+numpy==1.26.2
+scipy==1.12.0
+requests==2.31.0
+torchsde==0.2.6
+
+einops
+opencv-contrib-python
+safetensors
+
+# --- FramePack_svc requirement ---
+gradio-modal
+
+# --- OK if one fails but less so if they both do ---
+xformers
+sageattention
\ No newline at end of file