#@title 필요 파일 생성 # 이 파일은 셀 4에서 서브프로세스로 실행됩니다. import sys import os import time import glob import gc import torch import subprocess import random import argparse from typing import Sequence, Mapping, Any, Union import shutil # --- 0. 기본 설정 및 인수 파싱 --- def parse_args(): parser = argparse.ArgumentParser(description="ComfyUI Video Generation Script with All Controls from 1.py") parser.add_argument("--positive_prompt", type=str, required=True); parser.add_argument("--negative_prompt", type=str, required=True) parser.add_argument("--width", type=int, required=True); parser.add_argument("--height", type=int, required=True) parser.add_argument("--length", type=int, required=True); parser.add_argument("--upscale_ratio", type=float, required=True) parser.add_argument("--steps", type=int, default=4) parser.add_argument("--cfg_high", type=float, default=1.0) parser.add_argument("--cfg_low", type=float, default=1.0) parser.add_argument("--sampler_name_high", type=str, default="euler"); parser.add_argument("--scheduler_high", type=str, default="simple") parser.add_argument("--sampler_name_low", type=str, default="euler"); parser.add_argument("--scheduler_low", type=str, default="simple") parser.add_argument("--noise_seed", type=int, default=-1); parser.add_argument("--split_point_percent", type=float, default=50.0) parser.add_argument("--shift", type=float, default=8.0); parser.add_argument("--sageattention", type=str, default="on") parser.add_argument("--unet_high_name", type=str, required=True); parser.add_argument("--unet_low_name", type=str, required=True) parser.add_argument("--vae_name", type=str, required=True); parser.add_argument("--clip_name", type=str, required=True) parser.add_argument("--upscale_model_name", type=str, default="None") parser.add_argument("--upscale_model_scale", type=float, default=2.0) parser.add_argument("--upscale_chunk_size", type=int, default=30) parser.add_argument("--frame_rate", type=int, default=16); parser.add_argument("--interpolation", type=str, default="on") parser.add_argument("--rife_fast_mode", type=str, default="on"); parser.add_argument("--rife_ensemble", type=str, default="on") parser.add_argument("--rife_chunk_size", type=int, default=30) parser.add_argument("--connect_lora_clip", type=str, default="off") parser.add_argument("--video_encoder", type=str, default="GPU: HEVC (NVENC)"); parser.add_argument("--nvenc_cq", type=int, default=25); parser.add_argument("--nvenc_preset", type=str, default="p5"); parser.add_argument("--cpu_crf", type=int, default=19) # FFmpeg parser.add_argument("--lora_high_1_name", type=str, default="None"); parser.add_argument("--lora_high_1_strength_model", type=float, default=1.0); parser.add_argument("--lora_high_1_strength_clip", type=float, default=1.0) parser.add_argument("--lora_high_2_name", type=str, default="None"); parser.add_argument("--lora_high_2_strength_model", type=float, default=1.0); parser.add_argument("--lora_high_2_strength_clip", type=float, default=1.0) parser.add_argument("--lora_low_1_name", type=str, default="None"); parser.add_argument("--lora_low_1_strength_model", type=float, default=1.0); parser.add_argument("--lora_low_1_strength_clip", type=float, default=1.0) parser.add_argument("--lora_low_2_name", type=str, default="None"); parser.add_argument("--lora_low_2_strength_model", type=float, default=1.0); parser.add_argument("--lora_low_2_strength_clip", type=float, default=1.0) parser.add_argument("--input_resize_algo", type=str, default="bicubic") parser.add_argument("--output_resize_algo", type=str, default="bicubic") return parser.parse_args() def to_bool(s: str) -> bool: return s.lower() in ['true', '1', 't', 'y', 'yes', 'on'] def clear_memory(): if torch.cuda.is_available(): torch.cuda.empty_cache(); torch.cuda.ipc_collect() gc.collect() COMFYUI_BASE_PATH = '/content/ComfyUI' def get_value_at_index(obj: Union[Sequence, Mapping], index: int) -> Any: try: return obj[index] except (KeyError, TypeError): if isinstance(obj, dict) and "result" in obj: return obj["result"][index] raise def add_comfyui_directory_to_sys_path() -> None: if os.path.isdir(COMFYUI_BASE_PATH) and COMFYUI_BASE_PATH not in sys.path: sys.path.append(COMFYUI_BASE_PATH) def import_custom_nodes() -> None: try: import nest_asyncio nest_asyncio.apply() except ImportError: print("nest_asyncio not found, installing...") try: subprocess.run([sys.executable, "-m", "pip", "install", "-q", "nest_asyncio"], check=True) import nest_asyncio nest_asyncio.apply() print("nest_asyncio installed and applied.") except Exception as e: print(f"Failed to install or apply nest_asyncio: {e}") import asyncio, execution, server from nodes import init_extra_nodes try: loop = asyncio.get_event_loop() if loop.is_closed(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) server_instance = server.PromptServer(loop) execution.PromptQueue(server_instance) if not loop.is_running(): try: loop.run_until_complete(init_extra_nodes()) except RuntimeError as e: print(f"Note: Could not run init_extra_nodes synchronously, possibly due to existing loop state: {e}") try: asyncio.ensure_future(init_extra_nodes()) except Exception as fut_e: print(f"Error trying async init_extra_nodes: {fut_e}") else: try: asyncio.ensure_future(init_extra_nodes()) except Exception as fut_e: print(f"Error trying async init_extra_nodes on running loop: {fut_e}") def main(): args = parse_args() print("🚀 동영상 생성을 시작합니다 (Full Control Mode, VRAM Optimized)...\n") # 🚨🚨🚨 수정된 부분 시작: .mp4, .mkv, .webm 파일은 제외하고 output 폴더 정리 🚨🚨🚨 output_dir = os.path.join(COMFYUI_BASE_PATH, 'output') print(f" - 이전 출력물 정리 중... (Output: {output_dir})") deleted_count = 0 try: # output 폴더 내의 모든 파일과 폴더 목록을 가져옵니다. for item_name in os.listdir(output_dir): item_path = os.path.join(output_dir, item_name) # 💡 조건: 비디오 파일 확장자(.mp4, .mkv, .webm)는 삭제하지 않고 보존합니다. if item_name.lower().endswith(('.mp4', '.mkv', '.webm')): print(f" - 🗄️ 비디오 파일 '{item_name}'은 보존합니다.") continue # 파일 또는 링크인 경우 삭제 if os.path.isfile(item_path) or os.path.islink(item_path): os.unlink(item_path) deleted_count += 1 # 폴더인 경우 재귀적으로 삭제 elif os.path.isdir(item_path): shutil.rmtree(item_path) deleted_count += 1 print(f" ✅ 정리 완료. 보존된 비디오 외 {deleted_count}개의 항목이 삭제되었습니다.") except Exception as e: print(f" ❌ 출력 폴더 정리 중 오류 발생: {e}") # 🚨🚨🚨 수정된 부분 끝 🚨🚨🚨 # 임시 폴더 재생성 (정리 과정에서 삭제되었을 수 있음) os.makedirs(f"{COMFYUI_BASE_PATH}/output/temp", exist_ok=True); os.makedirs(f"{COMFYUI_BASE_PATH}/output/up", exist_ok=True) os.makedirs(f"{COMFYUI_BASE_PATH}/output/interpolated", exist_ok=True) add_comfyui_directory_to_sys_path() try: from utils.extra_config import load_extra_path_config except ImportError: print("⚠️ ComfyUI의 extra_model_paths.yaml 로딩 실패 (무시하고 진행)"); load_extra_path_config = lambda x: None extra_model_paths_file = os.path.join(COMFYUI_BASE_PATH, "extra_model_paths.yaml") if os.path.exists(extra_model_paths_file): load_extra_path_config(extra_model_paths_file) print("ComfyUI 커스텀 노드 초기화 중..."); import_custom_nodes(); from nodes import NODE_CLASS_MAPPINGS; print("커스텀 노드 초기화 완료.") if args.noise_seed == -1: final_seed = random.randint(1, 2**64); print(f" - 랜덤 시드 생성: {final_seed}") else: final_seed = args.noise_seed; print(f" - 고정 시드 사용: {final_seed}") split_step = max(0, int(args.steps * (args.split_point_percent / 100.0))); print(f" - 총 {args.steps} 스텝 중 {split_step} ( {args.split_point_percent}% )에서 분할") loras_in_use = not (args.lora_high_1_name == "None" and args.lora_high_2_name == "None" and args.lora_low_1_name == "None" and args.lora_low_2_name == "None") connect_clip_to_lora = to_bool(args.connect_lora_clip); should_keep_clip_loaded = loras_in_use and connect_clip_to_lora with torch.inference_mode(): loadimage=NODE_CLASS_MAPPINGS["LoadImage"](); upscalemodelloader=NODE_CLASS_MAPPINGS["UpscaleModelLoader"](); cliploader=NODE_CLASS_MAPPINGS["CLIPLoader"](); vaeloader=NODE_CLASS_MAPPINGS["VAELoader"](); cliptextencode=NODE_CLASS_MAPPINGS["CLIPTextEncode"](); unetloadergguf=NODE_CLASS_MAPPINGS["UnetLoaderGGUF"](); loraloader=NODE_CLASS_MAPPINGS["LoraLoader"](); imageresizekjv2=NODE_CLASS_MAPPINGS["ImageResizeKJv2"](); wanimagetovideo=NODE_CLASS_MAPPINGS["WanImageToVideo"](); modelsamplingsd3=NODE_CLASS_MAPPINGS["ModelSamplingSD3"](); ksampleradvanced=NODE_CLASS_MAPPINGS["KSamplerAdvanced"](); vaedecode=NODE_CLASS_MAPPINGS["VAEDecode"](); vhs_loadimagespath=NODE_CLASS_MAPPINGS["VHS_LoadImagesPath"](); imageupscalewithmodel=NODE_CLASS_MAPPINGS["ImageUpscaleWithModel"](); imagescaleby=NODE_CLASS_MAPPINGS["ImageScaleBy"](); rife_vfi=NODE_CLASS_MAPPINGS["RIFE VFI"](); vhs_videocombine=NODE_CLASS_MAPPINGS["VHS_VideoCombine"](); saveimage=NODE_CLASS_MAPPINGS["SaveImage"]() clipvisionloader=NODE_CLASS_MAPPINGS["CLIPVisionLoader"]() clipvisionencode=NODE_CLASS_MAPPINGS["CLIPVisionEncode"]() pathchsageattentionkj=NODE_CLASS_MAPPINGS["PathchSageAttentionKJ"]() # --- ✨ 1단계: CLIP Vision 로직 추가 및 메모리 해제 --- print("\n1단계: 데이터 로딩 및 초기 Latent 생성 중..."); print(f" - CLIP 로딩: {args.clip_name}"); cliploader_460 = cliploader.load_clip(clip_name=args.clip_name, type="wan", device="default"); cliptextencode_462 = cliptextencode.encode(text=args.positive_prompt, clip=get_value_at_index(cliploader_460, 0)); cliptextencode_463 = cliptextencode.encode(text=args.negative_prompt, clip=get_value_at_index(cliploader_460, 0)); loadimage_88 = loadimage.load_image(image="example.png"); imageresizekjv2_401 = imageresizekjv2.resize( width=args.width, height=args.height, upscale_method=args.input_resize_algo, image=get_value_at_index(loadimage_88, 0), keep_proportion="crop", pad_color="0, 0, 0", crop_position="center", divisible_by=2, unique_id=random.randint(1, 2**64) ); print(f" - CLIP Vision 로딩: clip_vision_h.safetensors"); clipvisionloader_cv = clipvisionloader.load_clip(clip_name="clip_vision_h.safetensors"); print(f" - CLIP Vision 인코딩 중..."); clipvisionencode_cv = clipvisionencode.encode( crop="none", clip_vision=get_value_at_index(clipvisionloader_cv, 0), image=get_value_at_index(imageresizekjv2_401, 0) ); clip_vision_output = get_value_at_index(clipvisionencode_cv, 0) print(f" - VAE 임시 로딩 (초기 Latent 생성용): {args.vae_name}"); vaeloader_temp = vaeloader.load_vae(vae_name=args.vae_name); wanimagetovideo_464 = wanimagetovideo.EXECUTE_NORMALIZED( width=get_value_at_index(imageresizekjv2_401, 1), height=get_value_at_index(imageresizekjv2_401, 2), length=args.length, batch_size=1, positive=get_value_at_index(cliptextencode_462, 0), negative=get_value_at_index(cliptextencode_463, 0), vae=get_value_at_index(vaeloader_temp, 0), clip_vision_output=clip_vision_output, start_image=get_value_at_index(imageresizekjv2_401, 0) ); if not should_keep_clip_loaded: print(" ✨ (최적화) 1단계 완료, CLIP 모델을 즉시 해제합니다."); del cliploader_460 else: print(" ⚠️ (설정) LoRA CLIP 연결 옵션이 활성화되어 3단계까지 CLIP 모델을 유지합니다.") print(" ✨ (최적화) 1단계 완료, 임시 VAE 및 CLIP Vision 모델을 해제합니다."); del vaeloader_temp, clipvisionloader_cv, clipvisionencode_cv, clip_vision_output; clear_memory(); print("1단계 완료."); # --- 1단계 수정 완료 --- print(f"\n2단계: High Noise 샘플링 시작..."); print(f" - UNet High 로딩: {args.unet_high_name}"); unetloadergguf_495 = unetloadergguf.load_unet(unet_name=args.unet_high_name); model = get_value_at_index(unetloadergguf_495, 0); clip = get_value_at_index(cliploader_460, 0) if should_keep_clip_loaded else None; model_for_patching = model; if to_bool(args.sageattention): print(" ✨ SageAttention 패치 적용 중 (High)..."); pathchsageattentionkj_124 = pathchsageattentionkj.patch(sage_attention="auto", model=model_for_patching); model_for_patching = get_value_at_index(pathchsageattentionkj_124, 0) if args.lora_high_1_name != "None": print(f" - H LoRA 1: {args.lora_high_1_name}"); model_for_patching, clip = loraloader.load_lora(lora_name=args.lora_high_1_name, strength_model=args.lora_high_1_strength_model, strength_clip=args.lora_high_1_strength_clip, model=model_for_patching, clip=clip) if args.lora_high_2_name != "None": print(f" - H LoRA 2: {args.lora_high_2_name}"); model_for_patching, clip = loraloader.load_lora(lora_name=args.lora_high_2_name, strength_model=args.lora_high_2_strength_model, strength_clip=args.lora_high_2_strength_clip, model=model_for_patching, clip=clip) shifted_model = get_value_at_index(modelsamplingsd3.patch(shift=args.shift, model=model_for_patching), 0); final_model = shifted_model; # 수정: cfg=args.cfg_high 사용 ksampleradvanced_466 = ksampleradvanced.sample(add_noise="enable", noise_seed=final_seed, steps=args.steps, cfg=args.cfg_high, sampler_name=args.sampler_name_high, scheduler=args.scheduler_high, start_at_step=0, end_at_step=split_step, return_with_leftover_noise="enable", model=final_model, positive=get_value_at_index(wanimagetovideo_464, 0), negative=get_value_at_index(wanimagetovideo_464, 1), latent_image=get_value_at_index(wanimagetovideo_464, 2)); if to_bool(args.sageattention): del pathchsageattentionkj_124 del unetloadergguf_495, model, clip, model_for_patching, shifted_model, final_model; clear_memory(); print("2단계 완료.") print(f"\n3단계: Low Noise 샘플링 시작..."); print(f" - UNet Low 로딩: {args.unet_low_name}"); unetloadergguf_496 = unetloadergguf.load_unet(unet_name=args.unet_low_name); model = get_value_at_index(unetloadergguf_496, 0); clip = get_value_at_index(cliploader_460, 0) if should_keep_clip_loaded else None; model_for_patching = model; if to_bool(args.sageattention): print(" ✨ SageAttention 패치 적용 중 (Low)..."); pathchsageattentionkj_129 = pathchsageattentionkj.patch(sage_attention="auto", model=model_for_patching); model_for_patching = get_value_at_index(pathchsageattentionkj_129, 0) if args.lora_low_1_name != "None": print(f" - L LoRA 1: {args.lora_low_1_name}"); model_for_patching, clip = loraloader.load_lora(lora_name=args.lora_low_1_name, strength_model=args.lora_low_1_strength_model, strength_clip=args.lora_low_1_strength_clip, model=model_for_patching, clip=clip) if args.lora_low_2_name != "None": print(f" - L LoRA 2: {args.lora_low_2_name}"); model_for_patching, clip = loraloader.load_lora(lora_name=args.lora_low_2_name, strength_model=args.lora_low_2_strength_model, strength_clip=args.lora_low_2_strength_clip, model=model_for_patching, clip=clip) shifted_model = get_value_at_index(modelsamplingsd3.patch(shift=args.shift, model=model_for_patching), 0); final_model = shifted_model; # 수정: cfg=args.cfg_low 사용 ksampleradvanced_465 = ksampleradvanced.sample(add_noise="disable", noise_seed=final_seed, steps=args.steps, cfg=args.cfg_low, sampler_name=args.sampler_name_low, scheduler=args.scheduler_low, start_at_step=split_step, end_at_step=10000, return_with_leftover_noise="disable", model=final_model, positive=get_value_at_index(wanimagetovideo_464, 0), negative=get_value_at_index(wanimagetovideo_464, 1), latent_image=get_value_at_index(ksampleradvanced_466, 0)); if to_bool(args.sageattention): del pathchsageattentionkj_129 if should_keep_clip_loaded: print(" ✨ (메모리) LoRA CLIP 연결 옵션 사용 완료, CLIP 모델을 해제합니다."); del cliploader_460 del unetloadergguf_496, model, clip, model_for_patching, shifted_model, final_model, ksampleradvanced_466, wanimagetovideo_464; clear_memory(); print("3단계 완료.") print(f"\n4단계: VAE 디코딩 및 임시 저장 중..."); print(f" - VAE 모델 로딩 (디코딩용): {args.vae_name}"); vaeloader_461 = vaeloader.load_vae(vae_name=args.vae_name); vaedecode_469 = vaedecode.decode(samples=get_value_at_index(ksampleradvanced_465, 0), vae=get_value_at_index(vaeloader_461, 0)); saveimage.save_images(filename_prefix="temp/example", images=get_value_at_index(vaedecode_469, 0)); del ksampleradvanced_465, vaeloader_461, vaedecode_469, loadimage_88, imageresizekjv2_401; clear_memory(); print("4단계 완료.") combine_input_dir_for_ffmpeg = f"{COMFYUI_BASE_PATH}/output/temp" if args.upscale_ratio > 1: if args.upscale_model_name == "None": print("\n5단계: 업스케일링 건너뜀 (모델이 선택되지 않음).") else: print(f"\n5단계: 프레임 업스케일링 중..."); print(f" - Upscale 모델 로딩: {args.upscale_model_name}"); upscalemodelloader_384 = upscalemodelloader.load_model(model_name=args.upscale_model_name); chunk_size = args.upscale_chunk_size; base_dir = f"{COMFYUI_BASE_PATH}/output/temp"; scale_by_ratio = args.upscale_ratio / args.upscale_model_scale; total_frames = 0 try: temp_files = [f for f in os.listdir(base_dir) if f.endswith(('.png', '.jpg', '.jpeg', '.webp'))] total_frames = len(temp_files) if total_frames == 0: raise FileNotFoundError("업스케일할 프레임이 'temp' 폴더에 없습니다.") except Exception as e: print(f" ❌ 업스케일 5단계 중단: 'temp' 폴더에서 프레임을 읽을 수 없습니다. (오류: {e})") if 'upscalemodelloader_384' in locals(): del upscalemodelloader_384 clear_memory() raise print(f" - 총 {total_frames}개의 프레임을 {chunk_size}개 단위로 분할하여 실행합니다...") for i in range(0, total_frames, chunk_size): print(f" - 배치 처리 중 (프레임 {i} ~ {min(i + chunk_size, total_frames) - 1})...") vhs_load_chunk = vhs_loadimagespath.load_images(directory=base_dir, skip_first_images=i, image_load_cap=chunk_size); loaded_images = get_value_at_index(vhs_load_chunk, 0); if loaded_images is None: print(" - (경고) 건너뛸 수 없는 이미지가 로드되었습니다, 이 배치를 건너뜁니다."); continue imageupscale_chunk = imageupscalewithmodel.upscale(upscale_model=get_value_at_index(upscalemodelloader_384, 0), image=loaded_images); imagescale_chunk = imagescaleby.upscale( upscale_method=args.output_resize_algo, scale_by=scale_by_ratio, image=get_value_at_index(imageupscale_chunk, 0) ); saveimage.save_images(filename_prefix="up/example", images=get_value_at_index(imagescale_chunk, 0)); del vhs_load_chunk, loaded_images, imageupscale_chunk, imagescale_chunk; clear_memory() del upscalemodelloader_384; clear_memory(); combine_input_dir_for_ffmpeg = f"{COMFYUI_BASE_PATH}/output/up"; print("5단계 완료.") else: print("\n5단계: 업스케일링 건너뜀 (비율 1.0).") # --- ✨ 6단계: RIFE 청크 로직 수정 (Overlap 적용) --- print("\n6단계: 비디오 결합 준비 중..."); final_frame_rate = float(args.frame_rate); ffmpeg_input_dir = combine_input_dir_for_ffmpeg if to_bool(args.interpolation): print(" - 프레임 보간 (RIFE)을 활성화합니다."); interpolated_dir = f"{COMFYUI_BASE_PATH}/output/interpolated"; source_dir = combine_input_dir_for_ffmpeg total_frames_rife = 0 try: temp_files = [f for f in os.listdir(source_dir) if f.endswith(('.png', '.jpg', '.jpeg', '.webp'))]; total_frames_rife = len(temp_files); if total_frames_rife == 0: raise FileNotFoundError(f"RIFE 보간할 프레임이 '{source_dir}' 폴더에 없습니다.") except Exception as e: print(f" ❌ RIFE 6단계 중단: '{source_dir}' 폴더에서 프레임을 읽을 수 없습니다. (오류: {e})"); raise chunk_size = args.rife_chunk_size; print(f" - 총 {total_frames_rife}개의 프레임을 RIFE 청크 {chunk_size}개 단위로 분할하여 실행합니다 (Overlap 적용)...") current_frame_idx = 0 is_first_chunk = True while current_frame_idx < total_frames_rife: load_from = current_frame_idx load_cap = chunk_size if not is_first_chunk: load_from -= 1 # 1프레임 겹치기 load_cap += 1 # 겹친 만큼 1프레임 더 로드 # 마지막 청크 경계 처리 if load_from + load_cap > total_frames_rife: load_cap = total_frames_rife - load_from # RIFE는 최소 2프레임이 필요함 if load_cap < 2: print(f" - (경고) RIFE 처리에 필요한 프레임(2개)이 부족하여 마지막 배치를 건너뜁니다.") break print(f" - RIFE 배치 처리 중 (원본 프레임 {load_from} ~ {load_from + load_cap - 1})...") vhs_load_chunk = vhs_loadimagespath.load_images(directory=source_dir, skip_first_images=load_from, image_load_cap=load_cap); loaded_images = get_value_at_index(vhs_load_chunk, 0); if loaded_images is None: print(" - (경고) 건너뛸 수 없는 이미지가 로드되었습니다, 이 배치를 건너뜁니다."); current_frame_idx += chunk_size is_first_chunk = False continue rife_chunk_result_tensor = get_value_at_index(rife_vfi.vfi( ckpt_name="rife49.pth", multiplier=2, fast_mode=to_bool(args.rife_fast_mode), ensemble=to_bool(args.rife_ensemble), frames=loaded_images ), 0) images_to_save = rife_chunk_result_tensor if not is_first_chunk: # 첫 번째가 아닌 모든 청크는 겹치는 첫 프레임을 제거 (텐서 슬라이싱) print(f" - (Overlap) 중복 프레임 1개 제거 후 저장") images_to_save = rife_chunk_result_tensor[1:] saveimage.save_images(filename_prefix="interpolated/example", images=images_to_save); del vhs_load_chunk, loaded_images, rife_chunk_result_tensor, images_to_save; clear_memory() current_frame_idx += chunk_size is_first_chunk = False ffmpeg_input_dir = interpolated_dir; final_frame_rate *= 2 else: print(" - 프레임 보간이 비활성화되었습니다."); # --- 6단계 수정 완료 --- print(f" - 최종 비디오를 FFmpeg ({args.video_encoder})로 결합합니다..."); print(f" - 입력 폴더: '{ffmpeg_input_dir}'") input_pattern = os.path.join(ffmpeg_input_dir, "example_%05d_.png") timestamp = time.strftime("%Y%m%d-%H%M%S"); output_filename = f"AnimateDiff_{timestamp}.mp4"; output_path = os.path.join(COMFYUI_BASE_PATH, "output", output_filename) ffmpeg_cmd = ["ffmpeg", "-framerate", str(final_frame_rate), "-i", input_pattern] encoder_choice = args.video_encoder if encoder_choice == "GPU: HEVC (NVENC)": ffmpeg_cmd.extend(["-c:v", "hevc_nvenc", "-cq", str(args.nvenc_cq), "-preset", args.nvenc_preset, "-tag:v", "hvc1"]) elif encoder_choice == "GPU: H.264 (NVENC)": ffmpeg_cmd.extend(["-c:v", "h264_nvenc", "-cq", str(args.nvenc_cq), "-preset", args.nvenc_preset]) else: ffmpeg_cmd.extend(["-c:v", "libx264", "-crf", str(args.cpu_crf), "-preset", "medium"]) ffmpeg_cmd.extend(["-pix_fmt", "yuv420p", "-y", output_path]) print(f" - 실행 명령어: {' '.join(ffmpeg_cmd)}") try: result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, check=True, encoding='utf-8') print(" - FFmpeg 실행 완료.") except FileNotFoundError: print(" ❌ 오류: 'ffmpeg' 명령어를 찾을 수 없습니다. 시스템에 설치되어 있는지 확인하세요."); raise except subprocess.CalledProcessError as e: print(f" ❌ 오류: FFmpeg 실행 실패 (Return code: {e.returncode})") if e.stdout: print(f" FFmpeg stdout:\n{e.stdout}") if e.stderr: print(f" FFmpeg stderr:\n{e.stderr}") raise except Exception as e: print(f" ❌ 오류: FFmpeg 실행 중 예상치 못한 오류 발생: {e}"); raise print("✅ 모든 단계 완료.") # --- ✨ UnboundLocalError 해결 및 복사 로직 (최종) --- latest_video = None if os.path.exists(output_path): latest_video = output_path print(f"LATEST_VIDEO_PATH:{latest_video}") else: output_dir = os.path.join(COMFYUI_BASE_PATH, "output"); video_files = glob.glob(os.path.join(output_dir, '**', '*.mp4'), recursive=True) + \ glob.glob(os.path.join(output_dir, '**', '*.mkv'), recursive=True) if not video_files: raise FileNotFoundError("생성된 동영상 파일을 찾을 수 없습니다!") latest_video = max(video_files, key=os.path.getctime) print(f"LATEST_VIDEO_PATH:{latest_video}") if latest_video is None: raise FileNotFoundError("최종 비디오 경로를 확정할 수 없습니다. 스크립트를 확인하세요.") base, ext = os.path.splitext(latest_video) original_copy_path = f"{base}_original{ext}" try: shutil.copy2(latest_video, original_copy_path) print(f"✅ 원본 복사본 생성 완료: {original_copy_path}") print(f"ORIGINAL_COPY_PATH:{original_copy_path}") except Exception as e: print(f"❌ 원본 복사본 생성 실패: {e}") # --- 수정 완료 -- if __name__ == "__main__": main()