diff --git a/diffusers_helper/lora_utils.py b/diffusers_helper/lora_utils.py index e3b14332..9d821c78 100644 --- a/diffusers_helper/lora_utils.py +++ b/diffusers_helper/lora_utils.py @@ -1,4 +1,4 @@ -from pathlib import Path +from pathlib import Path, PurePath from typing import Dict, List, Optional, Union from diffusers.loaders.lora_pipeline import _fetch_state_dict from diffusers.loaders.lora_conversion_utils import _convert_hunyuan_video_lora_to_diffusers @@ -33,7 +33,17 @@ def load_lora(transformer, lora_path: Path, weight_name: Optional[str] = "pytorc state_dict = _convert_hunyuan_video_lora_to_diffusers(state_dict) - adapter_name = weight_name.split(".")[0] + # should weight_name even be Optional[str] or just str? + # For now, we assume it is never None + # The module name in the state_dict must not include a . in the name + # See https://github.com/pytorch/pytorch/pull/6639/files#diff-4be56271f7bfe650e3521c81fd363da58f109cd23ee80d243156d2d6ccda6263R133-R134 + adapter_name = PurePath(str(weight_name).replace('_DOT_', '.')).stem.replace('.', '_DOT_') + if '_DOT_' in adapter_name: + print( + f"LoRA file '{weight_name}' contains a '.' in the name. " + + 'This may cause issues. Consider renaming the file.' + + f" Using '{adapter_name}' as the adapter name to be safe." + ) # Check if adapter already exists and delete it if it does if hasattr(transformer, 'peft_config') and adapter_name in transformer.peft_config: diff --git a/modules/interface.py b/modules/interface.py index bfa240dc..0513187f 100644 --- a/modules/interface.py +++ b/modules/interface.py @@ -2,6 +2,7 @@ import time import datetime import random +import json import os from typing import List, Dict, Any, Optional from PIL import Image @@ -125,7 +126,8 @@ def create_interface( height=420, elem_classes="contain-image" ) - + + with gr.Accordion("Latent Image Options", open=False): latent_type = gr.Dropdown( ["Black", "White", "Noise", "Green Screen"], label="Latent Image", value="Black", info="Used as a starting point if no image is provided" @@ -142,11 +144,21 @@ def create_interface( with gr.Row(): steps = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1) total_second_length = gr.Slider(label="Video Length (Seconds)", minimum=1, maximum=120, value=6, step=0.1) - with gr.Row(): - resolution = gr.Slider( - label="Output Resolution (Width)", minimum=128, maximum=768, value=640, - step=32, info="Nearest valid bucket size will be used. Height will be adjusted automatically." + with gr.Row("Resolution"): + resolutionW = gr.Slider( + label="Width", minimum=128, maximum=768, value=640, step=32, + info="Nearest valid width will be used." + ) + resolutionH = gr.Slider( + label="Height", minimum=128, maximum=768, value=640, step=32, + info="Nearest valid height will be used." ) + def on_input_image_change(img): + if img is not None: + return gr.update(info="Nearest valid bucket size will be used. Height will be adjusted automatically."), gr.update(visible=False) + else: + return gr.update(info="Nearest valid width will be used."), gr.update(visible=True) + input_image.change(fn=on_input_image_change, inputs=[input_image], outputs=[resolutionW, resolutionH]) with gr.Row("LoRAs"): lora_selector = gr.Dropdown( choices=lora_names, @@ -155,7 +167,7 @@ def create_interface( value=[], info="Select one or more LoRAs to use for this job" ) - + lora_names_states = gr.State(lora_names) lora_sliders = {} for lora in lora_names: lora_sliders[lora] = gr.Slider( @@ -173,20 +185,18 @@ def create_interface( save_metadata = gr.Checkbox(label="Save Metadata", value=True, info="Save to JSON file") with gr.Row("TeaCache"): use_teacache = gr.Checkbox(label='Use TeaCache', value=True, info='Faster speed, but often makes hands and fingers slightly worse.') - n_prompt = gr.Textbox(label="Negative Prompt", value="", visible=False) # Not used with gr.Row(): seed = gr.Number(label="Seed", value=31337, precision=0) randomize_seed = gr.Checkbox(label="Randomize", value=False, info="Generate a new random seed for each job") - with gr.Accordion("Advanced Parameters", open=False): latent_window_size = gr.Slider(label="Latent Window Size", minimum=1, maximum=33, value=9, step=1, visible=True, info='Change at your own risk, very experimental') # Should not change cfg = gr.Slider(label="CFG Scale", minimum=1.0, maximum=32.0, value=1.0, step=0.01, visible=False) # Should not change gs = gr.Slider(label="Distilled CFG Scale", minimum=1.0, maximum=32.0, value=10.0, step=0.01) rs = gr.Slider(label="CFG Re-Scale", minimum=0.0, maximum=1.0, value=0.0, step=0.01, visible=False) # Should not change - gpu_memory_preservation = gr.Slider(label="GPU Inference Preserved Memory (GB) (larger means slower)", minimum=6, maximum=128, value=6, step=0.1, info="Set this number to a larger value if you encounter OOM. Larger value causes slower speed.") + gpu_memory_preservation = gr.Slider(label="GPU Inference Preserved Memory (GB) (larger means slower)", minimum=1, maximum=128, value=6, step=0.1, info="Set this number to a larger value if you encounter OOM. Larger value causes slower speed.") with gr.Accordion("Output Parameters", open=False): mp4_crf = gr.Slider(label="MP4 Compression", minimum=0, maximum=100, value=16, step=1, info="Lower means better quality. 0 is uncompressed. Change to 16 if you get black outputs. ") clean_up_videos = gr.Checkbox( @@ -217,6 +227,7 @@ def create_interface( elem_classes="contain-image" ) + with gr.Accordion("Latent Image Options", open=False): f1_latent_type = gr.Dropdown( ["Black", "White", "Noise", "Green Screen"], label="Latent Image", value="Black", info="Used as a starting point if no image is provided" @@ -233,11 +244,21 @@ def create_interface( with gr.Row(): f1_steps = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1) f1_total_second_length = gr.Slider(label="Video Length (Seconds)", minimum=1, maximum=120, value=5, step=0.1) - with gr.Row(): - f1_resolution = gr.Slider( - label="Output Resolution (Width)", minimum=128, maximum=768, value=640, - step=32, info="Nearest valid bucket size will be used. Height will be adjusted automatically." + with gr.Row("Resolution"): + f1_resolutionW = gr.Slider( + label="Width", minimum=128, maximum=768, value=640, step=32, + info="Nearest valid width will be used." + ) + f1_resolutionH = gr.Slider( + label="Height", minimum=128, maximum=768, value=640, step=32, + info="Nearest valid height will be used." ) + def f1_on_input_image_change(img): + if img is not None: + return gr.update(info="Nearest valid bucket size will be used. Height will be adjusted automatically."), gr.update(visible=False) + else: + return gr.update(info="Nearest valid width will be used."), gr.update(visible=True) + f1_input_image.change(fn=f1_on_input_image_change, inputs=[f1_input_image], outputs=[f1_resolutionW, f1_resolutionH]) with gr.Row("LoRAs"): f1_lora_selector = gr.Dropdown( choices=lora_names, @@ -246,7 +267,7 @@ def create_interface( value=[], info="Select one or more LoRAs to use for this job" ) - + f1_lora_names_states = gr.State(lora_names) f1_lora_sliders = {} for lora in lora_names: f1_lora_sliders[lora] = gr.Slider( @@ -275,7 +296,7 @@ def create_interface( f1_cfg = gr.Slider(label="CFG Scale", minimum=1.0, maximum=32.0, value=1.0, step=0.01, visible=False) f1_gs = gr.Slider(label="Distilled CFG Scale", minimum=1.0, maximum=32.0, value=10.0, step=0.01) f1_rs = gr.Slider(label="CFG Re-Scale", minimum=0.0, maximum=1.0, value=0.0, step=0.01, visible=False) - f1_gpu_memory_preservation = gr.Slider(label="GPU Inference Preserved Memory (GB) (larger means slower)", minimum=6, maximum=128, value=6, step=0.1, info="Set this number to a larger value if you encounter OOM. Larger value causes slower speed.") + f1_gpu_memory_preservation = gr.Slider(label="GPU Inference Preserved Memory (GB) (larger means slower)", minimum=1, maximum=128, value=6, step=0.1, info="Set this number to a larger value if you encounter OOM. Larger value causes slower speed.") with gr.Accordion("Output Parameters", open=False): f1_mp4_crf = gr.Slider(label="MP4 Compression", minimum=0, maximum=100, value=16, step=1, info="Lower means better quality. 0 is uncompressed. Change to 16 if you get black outputs. ") f1_clean_up_videos = gr.Checkbox( @@ -339,6 +360,67 @@ def create_interface( object-fit: cover; } """ + # with gr.TabItem("Outputs"): + # outputDirectory = settings.get("output_dir", settings.default_settings['output_dir']) + # def get_gallery_items(): + # items = [] + # for f in os.listdir(outputDirectory): + # if f.endswith(".png"): + # prefix = os.path.splitext(f)[0] + # latest_video = get_latest_video_version(prefix) + # if latest_video: + # video_path = os.path.join(outputDirectory, latest_video) + # mtime = os.path.getmtime(video_path) + # preview_path = os.path.join(outputDirectory, f) + # items.append((preview_path, prefix, mtime)) + # items.sort(key=lambda x: x[2], reverse=True) + # return [(i[0], i[1]) for i in items] + # def get_latest_video_version(prefix): + # max_number = -1 + # selected_file = None + # for f in os.listdir(outputDirectory): + # if f.startswith(prefix + "_") and f.endswith(".mp4"): + # num = int(f.replace(prefix + "_", '').replace(".mp4", '')) + # if num > max_number: + # max_number = num + # selected_file = f + # return selected_file + # def load_video_and_info_from_prefix(prefix): + # video_file = get_latest_video_version(prefix) + # if not video_file: + # return None, "JSON not found." + # video_path = os.path.join(outputDirectory, video_file) + # json_path = os.path.join(outputDirectory, prefix) + ".json" + # info = {"description": "no info"} + # if os.path.exists(json_path): + # with open(json_path, "r", encoding="utf-8") as f: + # info = json.load(f) + # return video_path, json.dumps(info, indent=2, ensure_ascii=False) + # gallery_items_state = gr.State(get_gallery_items()) + # with gr.Row(): + # with gr.Column(scale=2): + # thumbs = gr.Gallery( + # # value=[i[0] for i in get_gallery_items()], + # columns=[4], + # allow_preview=False, + # object_fit="cover", + # height="auto" + # ) + # refresh_button = gr.Button("Update") + # with gr.Column(scale=5): + # video_out = gr.Video(sources=[], autoplay=True, loop=True, visible=False) + # with gr.Column(scale=1): + # info_out = gr.Textbox(label="Generation info", visible=False) + # def refresh_gallery(): + # new_items = get_gallery_items() + # return gr.update(value=[i[0] for i in new_items]), new_items + # refresh_button.click(fn=refresh_gallery, outputs=[thumbs, gallery_items_state]) + + # def on_select(evt: gr.SelectData, gallery_items): + # prefix = gallery_items[evt.index][1] + # video, info = load_video_and_info_from_prefix(prefix) + # return gr.update(value=video, visible=True), gr.update(value=info, visible=True) + # thumbs.select(fn=on_select, inputs=[gallery_items_state], outputs=[video_out, info_out]) with gr.Tab("Settings"): with gr.Row(): @@ -430,7 +512,7 @@ def cleanup_temp_files(): # Connect the main process function (wrapper for adding to queue) def process_with_queue_update(model_type, *args): # Extract all arguments (ensure order matches inputs lists) - input_image, prompt_text, n_prompt, seed_value, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, mp4_crf, randomize_seed_checked, save_metadata_checked, blend_sections, latent_type, clean_up_videos, selected_loras, resolution, *lora_args = args + input_image, prompt_text, n_prompt, seed_value, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, mp4_crf, randomize_seed_checked, save_metadata_checked, blend_sections, latent_type, clean_up_videos, selected_loras, resolutionW, resolutionH, *lora_args = args # DO NOT parse the prompt here. Parsing happens once in the worker. @@ -439,7 +521,7 @@ def process_with_queue_update(model_type, *args): # Pass the model_type and the ORIGINAL prompt_text string to the backend process function result = process_fn(model_type, input_image, prompt_text, n_prompt, seed_value, total_second_length, # Pass original prompt_text string latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, - use_teacache, mp4_crf, save_metadata_checked, blend_sections, latent_type, clean_up_videos, selected_loras, resolution, *lora_args) + use_teacache, mp4_crf, save_metadata_checked, blend_sections, latent_type, clean_up_videos, selected_loras, resolutionW, resolutionH, *lora_args) # If randomize_seed is checked, generate a new random seed for the next job new_seed_value = None @@ -492,7 +574,9 @@ def end_process_with_update(): latent_type, clean_up_videos, lora_selector, - resolution + resolutionW, + resolutionH, + lora_names_states ] # Add LoRA sliders to the input list ips.extend([lora_sliders[lora] for lora in lora_names]) @@ -518,7 +602,9 @@ def end_process_with_update(): f1_latent_type, f1_clean_up_videos, f1_lora_selector, - f1_resolution + f1_resolutionW, + f1_resolutionH, + f1_lora_names_states ] # Add F1 LoRA sliders to the input list f1_ips.extend([f1_lora_sliders[lora] for lora in lora_names]) @@ -626,8 +712,6 @@ def load_metadata_from_json(json_path): return [gr.update()] * (2 + num_orig_sliders) try: - import json - with open(json_path, 'r') as f: metadata = json.load(f) diff --git a/modules/interface.py.new b/modules/interface.py.new deleted file mode 100644 index 3c45e843..00000000 --- a/modules/interface.py.new +++ /dev/null @@ -1,332 +0,0 @@ -import gradio as gr -import time -import datetime -import random -import os -from typing import List, Dict, Any, Optional - -from modules.video_queue import JobStatus -from modules.prompt_handler import get_section_boundaries, get_quick_prompts -from diffusers_helper.gradio.progress_bar import make_progress_bar_css, make_progress_bar_html - - -def create_interface( - process_fn, - monitor_fn, - end_process_fn, - update_queue_status_fn, - load_lora_file_fn, - job_queue, - settings, - default_prompt: str = '"[1s: The person waves hello] [3s: The person jumps up and down] [5s: The person does a dance]', - lora_names: list = [], - lora_values: list = [] -): - """ - Create the Gradio interface for the video generation application - - Args: - process_fn: Function to process a new job - monitor_fn: Function to monitor an existing job - end_process_fn: Function to cancel the current job - update_queue_status_fn: Function to update the queue status display - default_prompt: Default prompt text - lora_names: List of loaded LoRA names - - Returns: - Gradio Blocks interface - """ - # Get section boundaries and quick prompts - section_boundaries = get_section_boundaries() - quick_prompts = get_quick_prompts() - - # Create the interface - css = make_progress_bar_css() - css += """ - .contain-image img { - object-fit: contain !important; - width: 100% !important; - height: 100% !important; - background: #222; - } - """ - - css += """ - #fixed-toolbar { - position: fixed; - top: 0; - left: 0; - width: 100vw; - z-index: 1000; - background: rgb(11, 15, 25); - color: #fff; - padding: 10px 20px; - display: flex; - align-items: center; - gap: 16px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - border-bottom: 1px solid #4f46e5; - } - #toolbar-add-to-queue-btn button { - font-size: 14px !important; - padding: 4px 16px !important; - height: 32px !important; - min-width: 80px !important; - } - - .gr-button-primary{ - color:white; - } - body, .gradio-container { - padding-top: 60px !important; /* Adjust if your toolbar is taller */ - } - """ - - block = gr.Blocks(css=css, title="FramePack Studio", theme="soft").queue() - - with block: - with gr.Row(elem_id="fixed-toolbar"): - gr.Markdown("
Queue: 0 | Completed: 0
") - refresh_stats_btn = gr.Button("Refresh", elem_id="refresh-stats-btn") # Add a refresh icon/button - start_button = gr.Button(value="Add to Queue", elem_id="toolbar-add-to-queue-btn") - - with gr.Tabs(): - with gr.TabItem("Generate"): - with gr.Row(): - with gr.Column(): - input_image = gr.Image( - sources='upload', - type="numpy", - label="Image (optional)", - height=420, - elem_classes="contain-image" - ) - - with gr.Accordion("Latent Image Options", open=False): - latent_type = gr.Dropdown( - ["Black", "White", "Noise", "Green Screen"], - label="Latent Image", - value="Black", - info="Used as a starting point if no image is provided" - ) - - prompt = gr.Textbox(label="Prompt", value=default_prompt) - - with gr.Accordion("Prompt Parameters", open=False): - blend_sections = gr.Slider( - minimum=0, maximum=10, value=4, step=1, - label="Number of sections to blend between prompts" - ) - with gr.Accordion("Generation Parameters", open=True): - with gr.Row(): - steps = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1) - total_second_length = gr.Slider(label="Video Length (Seconds)", minimum=1, maximum=120, value=5, step=0.1) - with gr.Row("LoRAs"): - lora_selector = gr.Dropdown( - choices=lora_names, - label="Select LoRAs to Load", - multiselect=True, - value=[], - info="Select one or more LoRAs to use for this job" - ) - - lora_sliders = {} - for lora in lora_names: - lora_sliders[lora] = gr.Slider( - minimum=0.0, maximum=2.0, value=1.0, step=0.01, - label=f"{lora} Weight", visible=False, interactive=True - ) - - with gr.Row("Metadata"): - json_upload = gr.File( - label="Upload Metadata JSON (optional)", - file_types=[".json"], - type="filepath", - height=100, - ) - save_metadata = gr.Checkbox(label="Save Metadata", value=True, info="Save to JSON file") - with gr.Row("TeaCache"): - use_teacache = gr.Checkbox(label='Use TeaCache', value=True, info='Faster speed, but often makes hands and fingers slightly worse.') - n_prompt = gr.Textbox(label="Negative Prompt", value="", visible=False) # Not used - - with gr.Row(): - seed = gr.Number(label="Seed", value=31337, precision=0) - randomize_seed = gr.Checkbox(label="Randomize", value=False, info="Generate a new random seed for each job") - - with gr.Accordion("Advanced Parameters", open=False): - latent_window_size = gr.Slider(label="Latent Window Size", minimum=1, maximum=33, value=9, step=1, visible=True, info='Change at your own risk, very experimental') # Should not change - cfg = gr.Slider(label="CFG Scale", minimum=1.0, maximum=32.0, value=1.0, step=0.01, visible=False) # Should not change - gs = gr.Slider(label="Distilled CFG Scale", minimum=1.0, maximum=32.0, value=10.0, step=0.01) - rs = gr.Slider(label="CFG Re-Scale", minimum=0.0, maximum=1.0, value=0.0, step=0.01, visible=False) # Should not change - gpu_memory_preservation = gr.Slider(label="GPU Inference Preserved Memory (GB) (larger means slower)", minimum=6, maximum=128, value=6, step=0.1, info="Set this number to a larger value if you encounter OOM. Larger value causes slower speed.") - with gr.Accordion("Output Parameters", open=False): - mp4_crf = gr.Slider(label="MP4 Compression", minimum=0, maximum=100, value=16, step=1, info="Lower means better quality. 0 is uncompressed. Change to 16 if you get black outputs. ") - clean_up_videos = gr.Checkbox( - label="Clean up video files", - value=True, - info="If checked, only the final video will be kept after generation." - ) - - with gr.Column(): - preview_image = gr.Image(label="Next Latents", height=150, visible=True, type="numpy") - result_video = gr.Video(label="Finished Frames", autoplay=True, show_share_button=False, height=256, loop=True) - progress_desc = gr.Markdown('', elem_classes='no-generating-animation') - progress_bar = gr.HTML('', elem_classes='no-generating-animation') - - with gr.Row(): - current_job_id = gr.Textbox(label="Current Job ID", visible=True, interactive=True) - end_button = gr.Button(value="Cancel Current Job", interactive=True) - with gr.Row(): - queue_status = gr.DataFrame( - headers=["Job ID", "Status", "Created", "Started", "Completed", "Elapsed"], - datatype=["str", "str", "str", "str", "str", "str"], - label="Job Queue" - ) - - with gr.TabItem("Settings"): - with gr.Row(): - with gr.Column(): - output_dir = gr.Textbox( - label="Output Directory", - value=settings.get("output_dir"), - placeholder="Path to save generated videos" - ) - metadata_dir = gr.Textbox( - label="Metadata Directory", - value=settings.get("metadata_dir"), - placeholder="Path to save metadata files" - ) - lora_dir = gr.Textbox( - label="LoRA Directory", - value=settings.get("lora_dir"), - placeholder="Path to LoRA models" - ) - auto_save = gr.Checkbox( - label="Auto-save settings", - value=settings.get("auto_save_settings", True) - ) - save_btn = gr.Button("Save Settings") - status = gr.HTML("") - - # Add a refresh timer that updates the queue status every 2 seconds - refresh_timer = gr.Number(value=0, visible=False) - - def refresh_timer_fn(): - """Updates the timer value periodically to trigger queue refresh""" - return int(time.time()) - - def get_queue_stats(): - jobs = job_queue.get_all_jobs() - in_queue = sum(1 for job in jobs if job.status in [JobStatus.PENDING, JobStatus.RUNNING]) - completed = sum(1 for job in jobs if job.status == JobStatus.COMPLETED) - return f"Queue: {in_queue} | Completed: {completed}
" - - def save_settings(output_dir, metadata_dir, lora_dir, auto_save): - try: - settings.update({ - "output_dir": output_dir, - "metadata_dir": metadata_dir, - "lora_dir": lora_dir, - "auto_save_settings": auto_save - }) - return "Settings saved successfully!
" - except Exception as e: - return f"Error saving settings: {str(e)}
" - - # Connect the buttons to their respective functions - start_button.click( - fn=process_fn, - inputs=[ - input_image, prompt, n_prompt, seed, total_second_length, - latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, - use_teacache, mp4_crf, save_metadata, blend_sections, latent_type, - clean_up_videos, lora_selector - ] + [lora_sliders[lora] for lora in lora_names], - outputs=[result_video, current_job_id, preview_image, progress_desc, progress_bar, start_button, end_button] - ) - - # Connect the end button to cancel the current job and update the queue - end_button.click( - fn=end_process_fn, - outputs=[queue_status] - ) - - # Auto-monitor the current job when job_id changes - current_job_id.change( - fn=monitor_fn, - inputs=[current_job_id], - outputs=[result_video, current_job_id, preview_image, progress_desc, progress_bar, start_button, end_button] - ) - - refresh_stats_btn.click( - fn=lambda: (get_queue_stats(), update_queue_status_fn()), - inputs=None, - outputs=[queue_stats_display, queue_status] - ) - - # Connect JSON metadata loader - json_upload.change( - fn=load_lora_file_fn, - inputs=[json_upload], - outputs=[prompt, seed] + lora_values - ) - - # Function to update slider visibility based on selection - def update_lora_sliders(selected_loras): - updates = [] - for lora in lora_names: - updates.append(gr.update(visible=(lora in selected_loras))) - return updates - - # Connect the dropdown to the sliders - lora_selector.change( - fn=update_lora_sliders, - inputs=[lora_selector], - outputs=[lora_sliders[lora] for lora in lora_names] - ) - - # Connect settings save button - save_btn.click( - fn=save_settings, - inputs=[output_dir, metadata_dir, lora_dir, auto_save], - outputs=[status] - ) - - return block - - -def format_queue_status(jobs): - """Format job data for display in the queue status table""" - rows = [] - for job in jobs: - created = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(job.created_at)) if job.created_at else "" - started = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(job.started_at)) if job.started_at else "" - completed = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(job.completed_at)) if job.completed_at else "" - - # Calculate elapsed time - elapsed_time = "" - if job.started_at: - if job.completed_at: - start_datetime = datetime.datetime.fromtimestamp(job.started_at) - complete_datetime = datetime.datetime.fromtimestamp(job.completed_at) - elapsed_seconds = (complete_datetime - start_datetime).total_seconds() - elapsed_time = f"{elapsed_seconds:.2f}s" - else: - # For running jobs, calculate elapsed time from now - start_datetime = datetime.datetime.fromtimestamp(job.started_at) - current_datetime = datetime.datetime.now() - elapsed_seconds = (current_datetime - start_datetime).total_seconds() - elapsed_time = f"{elapsed_seconds:.2f}s (running)" - - position = job.queue_position if hasattr(job, 'queue_position') else "" - - rows.append([ - job.id[:6] + '...', - job.status.value, - created, - started, - completed, - elapsed_time - ]) - return rows \ No newline at end of file diff --git a/studio.py b/studio.py index 1b4f781e..96b65f6d 100644 --- a/studio.py +++ b/studio.py @@ -2,6 +2,7 @@ import json import os +from pathlib import PurePath import time import argparse import traceback @@ -152,14 +153,15 @@ def verify_lora_state(transformer, label=""): settings = Settings() # --- Populate LoRA names AFTER settings are loaded --- -lora_folder_from_settings = settings.get("lora_dir", default_lora_folder) # Use setting, fallback to default +lora_folder_from_settings: str = settings.get("lora_dir", default_lora_folder) # Use setting, fallback to default print(f"Scanning for LoRAs in: {lora_folder_from_settings}") if os.path.isdir(lora_folder_from_settings): try: lora_files = [f for f in os.listdir(lora_folder_from_settings) if f.endswith('.safetensors') or f.endswith('.pt')] for lora_file in lora_files: - lora_names.append(lora_file.split('.')[0]) # Get name without extension + lora_name = PurePath(lora_file).stem + lora_names.append(lora_name) # Get name without extension print(f"Found LoRAs: {lora_names}") except Exception as e: print(f"Error scanning LoRA directory '{lora_folder_from_settings}': {e}") @@ -222,16 +224,17 @@ def move_lora_adapters_to_device(model, target_device): # Function to load a LoRA file -def load_lora_file(lora_file): +def load_lora_file(lora_file: str | PurePath): if not lora_file: return None, "No file selected" try: # Get the filename from the path - _, lora_name = os.path.split(lora_file) + lora_path = PurePath(lora_file) + lora_name = lora_path.name # Copy the file to the lora directory - lora_dest = os.path.join(lora_dir, lora_name) + lora_dest = PurePath(lora_dir, lora_path) import shutil shutil.copy(lora_file, lora_dest) @@ -246,7 +249,7 @@ def load_lora_file(lora_file): current_transformer = lora_utils.load_lora(current_transformer, lora_dir, lora_name) # Add to lora_names if not already there - lora_base_name = lora_name.split('.')[0] + lora_base_name = lora_path.stem if lora_base_name not in lora_names: lora_names.append(lora_base_name) @@ -291,7 +294,9 @@ def worker( job_stream=None, output_dir=None, metadata_dir=None, - resolution=640 # Add resolution parameter with default value + resolutionW=640, # Add resolution parameter with default value + resolutionH=640, + lora_loaded_names=[] ): global transformer_original, transformer_f1, current_transformer, high_vram @@ -425,7 +430,7 @@ def worker( stream_to_use.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Image processing ...')))) H, W, C = input_image.shape - height, width = find_nearest_bucket(H, W, resolution=resolution) + height, width = find_nearest_bucket(H, W, resolution=resolutionW) input_image_np = resize_and_center_crop(input_image, target_width=width, target_height=height) if save_metadata: @@ -448,24 +453,36 @@ def worker( "latent_window_size": latent_window_size, "mp4_crf": mp4_crf, "timestamp": time.time(), - "resolution": resolution, # Add resolution to metadata + "resolutionW": resolutionW, # Add resolution to metadata + "resolutionH": resolutionH, "model_type": model_type # Add model type to metadata } - # # Add LoRA information to metadata if LoRAs are used - # if selected_loras and len(selected_loras) > 0: - # lora_data = {} - # for i, lora_name in enumerate(selected_loras): - # # Get the corresponding weight if available - # weight = lora_values[i] if lora_values and i < len(lora_values) else 1.0 - # # Handle case where weight might be a list - # if isinstance(weight, list): - # # If it's a list, use the first element or default to 1.0 - # weight_value = weight[0] if weight and len(weight) > 0 else 1.0 - # else: - # weight_value = weight - # lora_data[lora_name] = float(weight_value) - - # metadata_dict["loras"] = lora_data + # Add LoRA information to metadata if LoRAs are used + def ensure_list(x): + if isinstance(x, list): + return x + elif x is None: + return [] + else: + return [x] + + selected_loras = ensure_list(selected_loras) + lora_values = ensure_list(lora_values) + + if selected_loras and len(selected_loras) > 0: + lora_data = {} + for lora_name in selected_loras: + try: + idx = lora_loaded_names.index(lora_name) + weight = lora_values[idx] if lora_values and idx < len(lora_values) else 1.0 + if isinstance(weight, list): + weight_value = weight[0] if weight and len(weight) > 0 else 1.0 + else: + weight_value = weight + lora_data[lora_name] = float(weight_value) + except ValueError: + lora_data[lora_name] = 1.0 + metadata_dict["loras"] = lora_data with open(os.path.join(metadata_dir, f'{job_id}.json'), 'w') as f: json.dump(metadata_dict, f, indent=2) @@ -539,7 +556,8 @@ def worker( # --- LoRA loading and scaling --- if selected_loras: - for idx, lora_name in enumerate(selected_loras): + for lora_name in selected_loras: + idx = lora_loaded_names.index(lora_name) lora_file = None for ext in [".safetensors", ".pt"]: # Find any file that starts with the lora_name and ends with the extension @@ -910,14 +928,16 @@ def process( latent_type, clean_up_videos, selected_loras, - resolution, - *lora_values + resolutionW, + resolutionH, + lora_loaded_names, + *lora_values ): # Create a blank black image if no # Create a default image based on the selected latent_type if input_image is None: - default_height, default_width = 640, 640 + default_height, default_width = resolutionH, resolutionW if latent_type == "White": # Create a white image input_image = np.ones((default_height, default_width, 3), dtype=np.uint8) * 255 @@ -965,7 +985,9 @@ def process( 'clean_up_videos': clean_up_videos, 'output_dir': settings.get("output_dir"), 'metadata_dir': settings.get("metadata_dir"), - 'resolution': resolution # Add resolution parameter + 'resolutionW': resolutionW, # Add resolution parameter + 'resolutionH': resolutionH, + 'lora_loaded_names': lora_loaded_names } # Add LoRA values if provided - extract them from the tuple