diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..4f1e617affd49ef0ea41c561906f92bd1c5c221b --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "comfyui", + "runtimeExecutable": "D:\\SD\\ComfyUI_windows_portable\\python_embeded\\python.exe", + "runtimeArgs": [ + "-s", + "D:\\SD\\ComfyUI_windows_portable\\ComfyUI\\main.py", + "--windows-standalone-build", + "--enable-dynamic-vram", + "--use-sage-attention", + "--output-directory", + "D:\\sd\\output" + ], + "port": 8188 + } + ] +} diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..a617ac968d9e16e21cdb36587b0f52a3f7d58ac8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,42 @@ +{ + "permissions": { + "allow": [ + "Bash(curl -s -m * http://127.0.0.1:8188/*)", + "Bash(curl -s -m * \"http://127.0.0.1:8188/*)", + "Bash(curl -s http://127.0.0.1:8188*)", + "Bash(curl -s \"http://127.0.0.1:8188*)", + "Bash(curl -s * http://127.0.0.1:8188*)", + "Bash(curl -s * \"http://127.0.0.1:8188*)", + "Bash(nvidia-smi)", + "Bash(nvidia-smi --query-gpu=*)", + "Bash(nvidia-smi --query-compute-apps=*)", + "Bash(tasklist)", + "Bash(tasklist *)", + "Bash(wmic process get *)", + "Bash(where *)", + "Bash(git --no-pager log *)", + "Bash(git --no-pager diff *)", + "Bash(git --no-pager show *)", + "Bash(git --no-pager status *)", + "Bash(git add *)", + "Bash(git commit *)", + "PowerShell(Get-ChildItem *)", + "PowerShell(Get-Item *)", + "PowerShell(Get-Process *)", + "PowerShell(Get-CimInstance *)", + "PowerShell(Get-NetTCPConnection *)", + "PowerShell(Get-WinEvent *)", + "PowerShell(nvidia-smi --query-gpu=*)", + "PowerShell(nvidia-smi --query-compute-apps=*)", + "mcp__claude-in-chrome__tabs_context_mcp", + "mcp__Claude_Browser__read_page", + "mcp__Claude_Browser__get_page_text", + "mcp__Claude_Browser__read_console_messages", + "mcp__Claude_Browser__read_network_requests", + "mcp__Claude_Browser__find", + "mcp__Claude_Browser__tabs_context", + "mcp__Claude_Browser__preview_list", + "mcp__Claude_Browser__preview_logs" + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000000000000000000000000000000000..9cdfa2d57336b409d58be775a7f2e7ea27c6bf60 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,665 @@ +{ + "permissions": { + "allow": [ + "Bash(claude mcp *)", + "Bash(\"/c/Users/bjoo/.local/bin/claude.exe\" -p \"Call the list_saved_workflows tool from comfyui-cloud and print the raw result.\" --allowedTools \"mcp__comfyui-cloud__list_saved_workflows\")", + "Bash(curl -s -m 10 http://127.0.0.1:8188/system_stats)", + "Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); [print\\(g.get\\('name'\\), g.get\\('vram_free'\\)\\) for g in d.get\\('devices',[]\\)]\")", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/input/**)", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/models/checkpoints/**)", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/models/diffusion_models/**)", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/models/unet/**)", + "mcp__claude-in-chrome__tabs_context_mcp", + "Bash(curl -s -m 10 \"http://127.0.0.1:8188/object_info/UNETLoader\")", + "Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d['UNETLoader']['input'], indent=2\\)[:1500]\\)\")", + "Bash(curl -s -m 10 \"http://127.0.0.1:8188/object_info/ShowText|pysssss\")", + "Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); k=list\\(d.keys\\(\\)\\)[0]; print\\(json.dumps\\(d[k]['input'], indent=2\\)[:800]\\); print\\('OUTPUT_NODE:', d[k].get\\('output_node'\\)\\)\")", + "Bash(mkdir -p /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad)", + "Bash(curl -s -m 10 \"http://127.0.0.1:8188/object_info/DolphinVisionDirector\")", + "Bash(python - /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/dvd_info.json)", + "Read(//c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/**)", + "Bash(curl -sv -m 10 http://127.0.0.1:8188/object_info/DolphinVisionDirector -o /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/dvd_info.json)", + "Bash(curl -s -m 60 http://127.0.0.1:8188/object_info/DolphinVisionDirector -o /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/dvd_info.json)", + "Bash(curl -s -m 8 http://127.0.0.1:8188/system_stats -o /dev/null -w \"system_stats: %{http_code} in %{time_total}s\\\\n\")", + "Bash(curl -s -m 8 http://127.0.0.1:8188/queue -w \"\\\\nqueue: %{http_code} in %{time_total}s\\\\n\")", + "PowerShell(Get-Process -Id 31388 -ErrorAction SilentlyContinue)", + "PowerShell(Get-CimInstance Win32_Process -Filter \"ProcessId=31388\" -ErrorAction SilentlyContinue)", + "Bash(curl -s -m 8 http://127.0.0.1:8188/system_stats -o /dev/null -w \"now: %{http_code} in %{time_total}s\\\\n\")", + "Bash(curl -s -m 30 http://127.0.0.1:8188/object_info/DolphinVisionDirector -o /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/dvd_info.json)", + "Bash(curl -s -m 15 \"http://127.0.0.1:8188/history/f583e65d-e2d9-46fc-a4e5-a67122c3adc4\" -o \"/c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/history_result.json\")", + "Bash(PYTHONIOENCODING=utf-8 python - /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/history_result.json)", + "mcp__claude-in-chrome__navigate", + "mcp__claude-in-chrome__javascript_tool", + "mcp__claude-in-chrome__computer", + "mcp__claude-in-chrome__read_page", + "mcp__claude-in-chrome__tabs_create_mcp", + "Bash(python -)", + "Bash(python -m py_compile dolphin_vision_director.py)", + "PowerShell(Get-NetTCPConnection -LocalPort 8188 -ErrorAction SilentlyContinue)", + "Bash(curl -s -m 20 \"http://127.0.0.1:8188/object_info/DolphinVisionDirector\" -o /dev/null -w \"DolphinVisionDirector object_info: %{http_code}\\\\n\")", + "Bash(curl -s -m 20 http://127.0.0.1:8188/object_info/WanVideoNAG -o /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/nag_info.json)", + "Bash(PYTHONIOENCODING=utf-8 python - /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/7b5a824e-a1dd-42ed-be45-b5a9cccfc005/scratchpad/nag_info.json)", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/user/**)", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/**)", + "Bash(python3 -c ' *)", + "Bash(python -c ' *)", + "Read(//c/Users/bjoo/AppData/Local/ComfyUI-LoRA-Manager/cache/model/**)", + "Bash(python - /c/Users/bjoo/AppData/Local/ComfyUI-LoRA-Manager/cache/model/comfyui.sqlite)", + "Bash(PYTHONIOENCODING=utf-8 python -c ' *)", + "Bash(mkdir -p \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/5f5946c1-33a3-471e-8b49-6552febb9164/scratchpad\")", + "Bash(cd \"D:/SD/ComfyUI_windows_portable\" && ls)", + "PowerShell(Get-NetTCPConnection -LocalPort 8188 -State Listen -ErrorAction SilentlyContinue)", + "Bash(curl -s -m 8 http://127.0.0.1:8188/queue)", + "PowerShell(Get-CimInstance Win32_Process -Filter \"ProcessId=5452\" -ErrorAction SilentlyContinue)", + "Bash(cd \"D:/SD/ComfyUI_windows_portable/ComfyUI/models/loras\" 2>&1 && find . -iname \"*PENISLORA*\" 2>&1)", + "Bash(ls \"C:\\\\Users\\\\bjoo\\\\.claude\\\\projects\\\\D--SD-ComfyUI-windows-portable-ComfyUI\\\\memory\\\\\" 2>&1)", + "Skill(fewer-permission-prompts)", + "Skill(fewer-permission-prompts:*)", + "Bash(ls -la \"C:/Users/bjoo/.claude/projects/\" 2>&1 | head -50)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/5f5946c1-33a3-471e-8b49-6552febb9164/scratchpad/scan_perms2.py\")", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/5f5946c1-33a3-471e-8b49-6552febb9164/scratchpad/scan_curl.py\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:8188/ --max-time 3)", + "Bash(curl -s http://127.0.0.1:8188/history?max_items=5 --max-time 5)", + "Bash(curl -s http://127.0.0.1:8188/history?max_items=5 --max-time 5 -o C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/history.json)", + "Bash(curl -s http://127.0.0.1:8188/queue --max-time 5)", + "Bash(nvidia-smi --query-gpu=name,memory.used,memory.total,memory.free --format=csv)", + "PowerShell(Get-CimInstance Win32_OperatingSystem)", + "Read(//d/SD/ComfyUI_windows_portable/**)", + "PowerShell(Get-CimInstance Win32_Process -Filter \"Name = 'python.exe'\")", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/**)", + "Bash(grep -rniL \"^$\" --include=\"*.py\" -l \"unload_all_models\\\\|soft_empty_cache\\\\|free_memory\\\\|PurgeVRAM\\\\|UnloadAllModels\" .)", + "Bash(grep -rniE \"unload_all_models|soft_empty_cache\\\\\\(|PurgeVRAM|UnloadAllModels|Free Memory|FreeMemory|ClearVRAM|VRAM.?Debug\" --include=\"*.py\" -l .)", + "Bash(cd \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/comfyui_memory_cleanup\" && cat __init__.py 2>/dev/null | head -150)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/VRAMCleanup\" --max-time 5)", + "Bash(grep -rl \"class TripleKSamplerWan22LightningAdvanced\" --include=\"*.py\" .)", + "Bash(curl -s http://127.0.0.1:8188/object_info/TripleKSamplerWan22LightningAdvanced --max-time 5 -o C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/tks_info.json)", + "Read(//d/sd/models/**)", + "Bash(find \"D:/sd\" -maxdepth 2 -type d 2>/dev/null | head -20)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/TorchCompileModel\" --max-time 5)", + "mcp__claude-in-chrome__read_console_messages", + "mcp__claude-in-chrome__read_network_requests", + "Bash(curl -s -D - -o /dev/null \"http://127.0.0.1:8188/\" --max-time 5 -A \"Mozilla/5.0 \\(Windows NT 10.0; Win64; x64\\) AppleWebKit/537.36 \\(KHTML, like Gecko\\) Chrome/120.0.0.0 Safari/537.36\")", + "mcp__claude-in-chrome__list_connected_browsers", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/Fast%20Groups%20Bypasser%20\\(rgthree\\)\" --max-time 5)", + "Bash(curl -s http://127.0.0.1:8188/object_info --max-time 15 -o C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/all_object_info.json)", + "Bash(head -50 __init__.py)", + "Bash(python_embeded/python.exe -c ' *)", + "Bash(PYTHONIOENCODING=utf-8 python_embeded/python.exe -c ' *)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/DolphinVisionDirector\" --max-time 5)", + "Bash(cp 0705_vision2.json 0705_vision2.json.bak_20260703_before_numclips)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/VHS_VideoCombine\" --max-time 5)", + "PowerShell(Stop-Process -Id 2044 -Force)", + "PowerShell(Get-Process -Id 2044 -ErrorAction SilentlyContinue)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://127.0.0.1:8188/ --max-time 5)", + "Bash(curl -s \"http://127.0.0.1:8188/queue\" --max-time 5)", + "Bash(curl -s -o /dev/null -w \"VRAMCleanup: %{http_code}\\\\n\" \"http://127.0.0.1:8188/object_info/VRAMCleanup\")", + "Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('VRAMCleanup registered:', bool\\(d\\)\\)\")", + "Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('TorchCompileModel registered:', bool\\(d\\)\\)\")", + "Bash(curl -s http://127.0.0.1:8188/history?max_items=10 --max-time 5 -o C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/history2.json)", + "Bash(\"D:/SD/ComfyUI_windows_portable/python_embeded/python.exe\" -m pip show sageattention)", + "Bash(\"D:/SD/ComfyUI_windows_portable/python_embeded/python.exe\" -c \"import sageattention; print\\(sageattention.__file__\\); print\\(dir\\(sageattention\\)\\)\")", + "Bash(D:/SD/ComfyUI_windows_portable/python_embeded/python.exe -c ' *)", + "Bash(\"D:/SD/ComfyUI_windows_portable/python_embeded/python.exe\" -c \"import sageattn3\")", + "WebSearch", + "WebFetch(domain:github.com)", + "Bash(\"D:/SD/ComfyUI_windows_portable/python_embeded/python.exe\" --version)", + "Bash(where nvcc *)", + "Bash(nvcc --version)", + "Bash(where cl.exe)", + "Bash(\"C:/Program Files \\(x86\\)/Microsoft Visual Studio/Installer/vswhere.exe\" -latest -products '*' -property installationPath)", + "Bash(find \"C:/Program Files/Microsoft Visual Studio/2022\" -iname \"vcvarsall.bat\" 2>/dev/null)", + "Bash(cmd //c \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\fcfc6473-30fd-4447-95fe-3e3e03867e77\\\\scratchpad\\\\build_sage3.bat\")", + "Bash(cmd //c \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\fcfc6473-30fd-4447-95fe-3e3e03867e77\\\\scratchpad\\\\build_sage3_v2.bat\")", + "Bash(cmd //c \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\fcfc6473-30fd-4447-95fe-3e3e03867e77\\\\scratchpad\\\\build_sage3_v3.bat\")", + "Bash(cmd //c \"C:\\\\sage3build\\\\build_sage3.bat\")", + "Bash(git checkout *)", + "Bash(\"D:/SD/ComfyUI_windows_portable/python_embeded/python.exe\" -m pip show sageattn3)", + "Bash(cp 0705_vision2.json 0705_vision2.json.bak_20260703_before_lightningsteps)", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\inspect_wf.py\")", + "Bash(grep -n \"\" \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/tripleksampler/nodes.py\" | sed -n '1140,1260p')", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\inspect_vd.py\")", + "Bash(/c/Windows/py -c ' *)", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\trace_prompt_links.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\trace2.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\trace3.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\remove_settings_node.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\bump_steps_cfg.py\")", + "mcp__claude-in-chrome__find", + "mcp__claude-in-chrome__form_input", + "mcp__d88663d0-df79-4bf4-92bd-3d23932f7923__hub_repo_search", + "mcp__d88663d0-df79-4bf4-92bd-3d23932f7923__hub_repo_details", + "WebFetch(domain:huggingface.co)", + "WebFetch(domain:civitai.com)", + "mcp__d88663d0-df79-4bf4-92bd-3d23932f7923__hf_hub_query", + "mcp__d88663d0-df79-4bf4-92bd-3d23932f7923__hf_fs", + "Bash(powershell -NoProfile -Command \"Get-CimInstance Win32_Process -Filter \\\\\"Name='python.exe'\\\\\" | Select-Object ProcessId, CommandLine | Format-List\")", + "Bash(powershell -NoProfile -Command \"Stop-Process -Id 47672 -Force\")", + "Bash(powershell -NoProfile -Command \"Get-CimInstance Win32_Process -Filter \\\\\"Name='python.exe'\\\\\" | Select-Object ProcessId, CommandLine\")", + "Bash(powershell -NoProfile -Command \"Get-Process -Id 47672 -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, Responding\")", + "Bash(sleep 15 && ls -t \"D:/SD/ComfyUI_windows_portable\"/comfyui_restart_*.log 2>/dev/null | head -3)", + "Bash(powershell -NoProfile -Command \"Stop-Process -Id 2668 -Force\")", + "Bash(powershell -NoProfile -Command \"Stop-Process -Id 11268 -Force\")", + "Bash(powershell -NoProfile -Command \"Get-Process -Id 11268 -ErrorAction SilentlyContinue | Select-Object Id\")", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js\" /tmp/check.mjs)", + "Bash(node --check /tmp/check.mjs)", + "Bash(powershell -NoProfile -Command \"Stop-Process -Id 45940 -Force\")", + "Bash(powershell -NoProfile -Command \"Get-CimInstance Win32_Process -Filter \\\\\"Name='python.exe'\\\\\" | Select-Object ProcessId\")", + "Bash(powershell -NoProfile -Command \"Get-Process -Id 45940 -ErrorAction SilentlyContinue\")", + "Bash(curl -s -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://127.0.0.1:8188/extensions/dolphin_nodes/dolphin_lora_rows.js)", + "Bash(curl -s http://127.0.0.1:8188/extensions/dolphin_nodes/dolphin_lora_rows.js)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js\" /tmp/check2.mjs)", + "Bash(node --check /tmp/check2.mjs)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js\" /tmp/check3.mjs)", + "Bash(node --check /tmp/check3.mjs)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js\" /tmp/check4.mjs)", + "Bash(node --check /tmp/check4.mjs)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js\" /tmp/check5.mjs)", + "Bash(node --check /tmp/check5.mjs)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js\" /tmp/check6.mjs)", + "Bash(node --check /tmp/check6.mjs)", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\fix_link1455.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\fix_link1376.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\remove_lightx2v_only_mode.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\dump_widgets.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\reset_lora_widgets.py\")", + "Bash(curl -s http://127.0.0.1:8188/history?max_items=1)", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\check_history.py\")", + "Read(//d/sd/models/loras/**)", + "Bash(sort -k9)", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\set_native4step.py\")", + "Bash(python -m py_compile __init__.py arch_logic.py promp_logic_v28_stitch.py promp_logic_v30_story.py promp_logic_v32_vision.py)", + "Bash(powershell -NoProfile -Command \"Stop-Process -Id 23756 -Force\")", + "Bash(powershell -NoProfile -Command \"Get-Process -Id 23756 -ErrorAction Stop\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\check_gguf_history.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\diff_v30_vs_current.py\")", + "Bash(/c/Windows/py \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\57e6bdef-7658-487a-ba07-aea70100b0eb\\\\scratchpad\\\\apply_wan21_hybrid.py\")", + "Bash(powershell -Command \"\\(Get-Item '0705_vision2.json'\\).Length\")", + "Bash(stat -c%s \"0705_vision2.json\")", + "Bash(node -e ' *)", + "Bash(cp \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\0705_vision2.json\" \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\user\\\\default\\\\workflows\\\\0705_vision2.json\")", + "Bash(grep -n '^ \"links\"\\\\|^ \"links\"\\\\|\"links\": \\\\[\\\\s*$' \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\0705_vision2.json\")", + "Bash(python3 -c \"print\\('hello'\\)\")", + "Bash(echo \"exit:$?\")", + "Bash(python)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://127.0.0.1:8188 --max-time 5)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://localhost:8188 --max-time 5)", + "Bash(curl -s http://127.0.0.1:8188/object_info/MatchImageAndMaskSize_Dolphin)", + "Bash(\"D:/SD/ComfyUI_windows_portable/python_embeded/python.exe\" -c \"import color_matcher; print\\(color_matcher.__file__\\)\")", + "Bash(curl -s http://127.0.0.1:8188/object_info/MaskRegionColorMatch_Dolphin)", + "Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d['MaskRegionColorMatch_Dolphin']['input']['required']['ring_width']\\)\\)\")", + "Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\)['MaskRegionColorMatch_Dolphin']['input']['required']; print\\(list\\(d.keys\\(\\)\\)\\)\")", + "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d['hf_models'], indent=2\\)\\)\")", + "Bash(git config *)", + "Bash(git add *)", + "Bash(git commit -m ' *)", + "PowerShell(Get-CimInstance Win32_Process -Filter \"ProcessId=70880\" -ErrorAction SilentlyContinue)", + "Bash(curl -s -m 8 \"http://127.0.0.1:8188/object_info/DolphinVisionDirector\" -o /dev/null -w \"DolphinVisionDirector: %{http_code}\\\\n\")", + "Bash(curl -s -m 8 \"http://127.0.0.1:8188/object_info/DolphinPromptDirector\" -o /dev/null -w \"DolphinPromptDirector: %{http_code}\\\\n\")", + "Bash(curl -s -m 8 \"http://127.0.0.1:8188/object_info/DolphinPromptDirector\")", + "Bash(python -m py_compile dolphin_prompt_director.py)", + "Bash(curl -s -m 8 \"http://127.0.0.1:8188/object_info/DolphinPromptDirector\" -o \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/5f5946c1-33a3-471e-8b49-6552febb9164/scratchpad/ppd_info2.json\")", + "Bash(xargs -I{} sh -c 'grep -l \"getMostRecentWorkflow\" \"{}\" 2>/dev/null')", + "Bash(grep -o \"getMostRecentWorkflow[^}]\\\\{0,300\\\\}\" \"D:/SD/ComfyUI_windows_portable/python_embeded/Lib/site-packages/comfyui_frontend_package/static/assets/dialogService-CVq-DX47.js\")", + "Bash(curl -s -m 8 \"http://127.0.0.1:8188/api/extensions\")", + "Bash(ls \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\\" | head -20; echo \"---\"; ls \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\" 2>/dev/null | head -20)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/485e6e77-03ce-4325-8ff3-3895d87d3091/scratchpad/gen_v2v_workflow.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" -c \"print\\('hello'\\)\"; Write-Output \"EXIT=$LASTEXITCODE\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" -c \"import torch; print\\('torch OK', torch.__version__\\)\"; Write-Output \"EXIT=$LASTEXITCODE\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\d4bb3e78-b9b7-42fa-912c-5e8d393db7cb\\\\scratchpad\\\\smoke_test.py\")", + "Bash(rm -rf \"/c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d4bb3e78-b9b7-42fa-912c-5e8d393db7cb/scratchpad/dsb_test\")", + "Bash(./python.exe \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/build.py\" --help)", + "Read(//d/SD/**)", + "Read(//d//**)", + "Read(//d/sd/models/diffusion_models/**)", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\dataset_builder\\\\train\\\\setup_env.ps1\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\dataset_builder\\\\train\\\\setup_env.ps1\" -Yes)", + "Bash(./python.exe \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/train/download_checkpoints.py\")", + "Bash(./python.exe \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/train/download_checkpoints.py\" --yes)", + "Bash(./python.exe -c ' *)", + "Bash(powershell -Command \"\\(Get-Item 'D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\user\\\\default\\\\workflows\\\\10S_comb.json'\\).Length\")", + "Bash(find \"/c/Users/bjoo/.cache/huggingface/hub\" -maxdepth 1 -iname \"*Wan_2.2_ComfyUI*\" -o -iname \"*Wan2.1-I2V-14B-720P*\" 2>/dev/null *)", + "PowerShell(Get-Process -Id 89032)", + "PowerShell(Get-NetTCPConnection -OwningProcess 89032 -ErrorAction SilentlyContinue)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/485e6e77-03ce-4325-8ff3-3895d87d3091/scratchpad/patch_10s_comb.py\")", + "Bash(python -m py_compile wan_svipro_fixed.py __init__.py)", + "Bash(\"./venv/Scripts/python.exe\" -m pip install \"torch==2.9.1\" torchvision --index-url https://download.pytorch.org/whl/cu130)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/485e6e77-03ce-4325-8ff3-3895d87d3091/scratchpad/patch_svipro_fixed.py\")", + "Bash(\"./venv/Scripts/python.exe\" -c \"import torch; print\\(torch.__version__, torch.version.cuda\\); print\\(torch.cuda.get_device_capability\\(0\\)\\); print\\(torch.cuda.is_available\\(\\)\\)\")", + "Bash(PYTHONIOENCODING=utf-8 \"./venv/Scripts/python.exe\" \"./musubi_tuner/src/musubi_tuner/wan_train_network.py\" --help)", + "Bash(./python.exe D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/build.py --input __TRACKED_VAR__/raw --output __TRACKED_VAR__/out --trigger test_subject --captioner wd14)", + "Bash(python -m py_compile wan_svipro_fixed.py)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/485e6e77-03ce-4325-8ff3-3895d87d3091/scratchpad/patch_svipro_amplitude.py\")", + "Bash(git commit *)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/485e6e77-03ce-4325-8ff3-3895d87d3091/scratchpad/patch_continuity.py\")", + "Bash(mkdir -p C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d4bb3e78-b9b7-42fa-912c-5e8d393db7cb/scratchpad/smoke_ds2/raw)", + "Bash(wmic process *)", + "Bash(python -m py_compile wan_svipro_gray.py __init__.py)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/485e6e77-03ce-4325-8ff3-3895d87d3091/scratchpad/patch_svipro_gray.py\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://127.0.0.1:7861)", + "Bash(./python.exe \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/inspect_lora.py\" \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\models\\\\loras\\\\2255050_wan2.2pisswet_high_noise.safetensors\")", + "Bash(cmd //c \"set\")", + "Bash(./python.exe \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/inspect_lora.py\" \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\models\\\\loras\\\\PENISLORA_22_i2v_HIGH_e320.safetensors\")", + "Bash(ls -la ~/Downloads/ 2>/dev/null | grep -i yt-dlp)", + "Bash(where python *)", + "Bash(python -m py_compile yt-dlp-gui.py)", + "Bash(ls \"/c/Users/bjoo/AppData/Local/Programs/Python/Python312/\" | grep -i pythonw)", + "PowerShell(Get-NetAdapter | Where-Object {$_.InterfaceDescription -match 'VPN|TAP|WireGuard|Tunnel|PPP'} | Select-Object Name, InterfaceDescription, Status | Format-Table -AutoSize)", + "PowerShell(Get-Process | Where-Object {$_.ProcessName -match 'openvpn|wireguard|nordvpn|expressvpn|protonvpn|surfshark|tunnelbear|cisco|anyconnect|forticlient|globalprotect|zerotier|tailscale|mullvad|ivpn|hotspotshield|windscribe|hidemyass|privatevpn|purevpn'} | Select-Object ProcessName, Id | Format-Table -AutoSize)", + "Bash(timeout 3 python yt-dlp-gui.py)", + "Bash(echo \"EXIT_CODE:$?\")", + "Bash(grep -E '^[0-9]+$')", + "Bash(curl -s -o /dev/null -w \"http %{http_code}\\\\n\" http://127.0.0.1:7861)", + "Bash(rm -rf C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d4bb3e78-b9b7-42fa-912c-5e8d393db7cb/scratchpad/webui_test/train_out3)", + "Bash(echo \"launched pid $!\")", + "Bash(python -m py_compile wan_svipro_gray.py)", + "Bash(ls -la C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d4bb3e78-b9b7-42fa-912c-5e8d393db7cb/scratchpad/webui_test/*.log)", + "Bash(echo \"=== full run_train3.log ===\")", + "Bash(cat C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d4bb3e78-b9b7-42fa-912c-5e8d393db7cb/scratchpad/webui_test/run_train3.log)", + "Bash(curl -s -o /dev/null -w \"webui http %{http_code}\\\\n\" http://127.0.0.1:7861)", + "Bash(grep -v \"^$\")", + "Bash(rm -rf \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d4bb3e78-b9b7-42fa-912c-5e8d393db7cb/scratchpad/webui_test\")", + "Bash(rm -f \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d4bb3e78-b9b7-42fa-912c-5e8d393db7cb/scratchpad/webui.log\")", + "Bash(curl -s -o /dev/null -w \"webui still up: http %{http_code}\\\\n\" http://127.0.0.1:7861)", + "PowerShell(Stop-Process -Id 103496 -Force)", + "PowerShell(Get-Process -Id 103496 -ErrorAction SilentlyContinue)", + "Bash(cp 0705_vision2.json 0705_vision2.json.bak_20260703_before_previewonly)", + "PowerShell(Stop-Process -Id 31768 -Force)", + "PowerShell(Get-Process -Id 31768 -ErrorAction SilentlyContinue)", + "Bash(curl -s http://127.0.0.1:8188/object_info/DolphinVisionDirector --max-time 5 -o C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/dvd_objinfo.json)", + "Bash(cp 0705_vision2.json 0705_vision2.json.bak_20260703_before_trimwidgets)", + "Bash(curl -s \"http://127.0.0.1:8188/history?max_items=5\" --max-time 5)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/WanNativeResize_Dolphin\" --max-time 5)", + "Bash(./python.exe D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/build.py --input C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fa088da8-83db-496b-ae2c-8203c1b258d5/scratchpad/clips_test/raw --output C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fa088da8-83db-496b-ae2c-8203c1b258d5/scratchpad/clips_test/out --trigger test --captioner wd14 --clips-per-video 5)", + "Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('registered:', bool\\(d\\)\\)\")", + "Bash(cp 0705_vision2.json lora_test.json)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/LoraLoaderModelOnly\" --max-time 5)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/ShowText%7Cpysssss\" --max-time 5)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/WanNativeResize_Dolphin\" --max-time 5 -o /tmp_resize.json)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/CLIPTextEncode\" --max-time 5)", + "Bash(python -m py_compile dolphin_prompt_director.py __init__.py)", + "Bash(tasklist)", + "Bash(python -m py_compile disk_cache_provider.py __init__.py)", + "Bash(curl -s http://127.0.0.1:8188/queue)", + "Bash(python -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('running:', len\\(d['queue_running']\\), 'pending:', len\\(d['queue_pending']\\)\\)\")", + "Bash(sort -k5 -n -r)", + "Bash(python -m py_compile disk_cache_provider.py)", + "Bash(python -m py_compile dolphin_prompt_director.py dolphin_vision_director.py __init__.py)", + "Bash(python \"/c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/rewire_perclip.py\")", + "Bash(python \"/c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/rewire_perclip_fix.py\")", + "Bash(python \"/c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/rewire_perclip_step5.py\")", + "Bash(python \"/c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/rewire_perclip_layout.py\")", + "Bash(python \"/c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fcfc6473-30fd-4447-95fe-3e3e03867e77/scratchpad/fix_modelsampling_nag.py\")", + "Bash(python -m json.tool)", + "Bash(python -m py_compile arch_logic_v16.py)", + "Bash(python -c *)", + "Bash(../../python_embeded/python.exe -c ' *)", + "mcp__claude-in-chrome__get_page_text", + "WebFetch(domain:aidispo.xyz)", + "Bash(curl -sS -o /dev/null -w \"HTTP:%{http_code} TIME:%{time_total}s\\\\n\" --max-time 15 https://aidispo.xyz/)", + "Bash(nslookup aidispo.xyz)", + "Bash(nslookup aidispo.xyz 8.8.8.8)", + "Bash(nslookup aidispo.xyz 1.1.1.1)", + "Bash(nslookup -type=NS aidispo.xyz)", + "Bash(python -m py_compile lora_classifier.py)", + "Bash(python lora_classifier.py)", + "Bash(PYTHONIOENCODING=utf-8 python lora_classifier.py)", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/553f71b1-eee1-4b6b-b29d-ae7b91fee046/scratchpad/convert_lanpaint.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/553f71b1-eee1-4b6b-b29d-ae7b91fee046/scratchpad/convert_lanpaint_live.py\")", + "Bash(gh issue *)", + "Bash(py --version)", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\inspect_wf.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\inspect_wf2.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\inspect_wf3.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\build_slowmo_workflows.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\validate_wf.py\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://127.0.0.1:8188/ --max-time 3)", + "WebFetch(domain:www.apatero.com)", + "Bash(gh api *)", + "WebFetch(domain:api.github.com)", + "Bash(curl -sL \"https://raw.githubusercontent.com/vita-epfl/Stable-Video-Infinity/svi_wan22/comfyui_workflow/SVI-Wan22-1210-4-Clips.json\" -o \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a/scratchpad/svi_wan22_4clips.json\" -w \"\\\\nHTTP %{http_code} size=%{size_download}\\\\n\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\inspect_svi_wf.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\inspect_svi_wf2.py\")", + "WebFetch(domain:raw.githubusercontent.com)", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\build_svi_scenario_a.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\build_svi_scenario_a_2clip.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\swap_svipro_gray.py\")", + "Bash(python -m http.server 8799 --bind 127.0.0.1)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"http://127.0.0.1:8799/0705_vision3_svigray_test.json\")", + "Bash(netstat -ano)", + "Bash(taskkill //F //PID 41392)", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\cors_server.py\")", + "Bash(curl -s -D - -o /dev/null \"http://127.0.0.1:8799/0705_vision3_svigray_test.json\")", + "Bash(awk '{print $5}')", + "Bash(taskkill //F //PID 35940)", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/WanVideoNAG\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3cd796ee-7ccd-4afc-a4b7-afbcf94f5e7a\\\\scratchpad\\\\build_svi_simple_nag.py\")", + "Bash(curl -s \"http://127.0.0.1:8188/object_info/TripleKSamplerWan22LightningAdvanced\")", + "Bash(curl -sL -o i2v_native.json \"https://huggingface.co/lightx2v/Wan2.2-Lightning/resolve/main/Wan2.2-I2V-A14B-4steps-lora-rank64-Seko-V1/Wan2.2-I2V-A14B-4steps-lora-rank64-Seko-V1-NativeComfy.json\")", + "Bash(./python_embeded/python.exe \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/test_svipro_motion.py\")", + "Bash(PYTHONIOENCODING=utf-8 ./python_embeded/python.exe \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/test_svipro_motion.py\")", + "Bash(curl -s -m 5 http://127.0.0.1:8188/system_stats -o /dev/null -w \"%{http_code}\\\\n\")", + "Bash(curl -s -m 10 http://127.0.0.1:8188/object_info/WanImageToVideoSVIProMotion_Dolphin)", + "Bash(PYTHONIOENCODING=utf-8 ./python_embeded/python.exe -c ' *)", + "Bash(curl -s -m 15 http://127.0.0.1:8188/object_info/WanImageToVideoSVIProMotion_Dolphin)", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/make_svimotion_test.py\")", + "Bash(ls -d \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows\" 2>&1 && cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/0705_vision3_svimotion_test.json\" \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/__validate_svimotion.json\" && echo copied)", + "Bash(PYTHONIOENCODING=utf-8 python -)", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/patch_svi_test.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/wire_nag.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/fix_first_base_model.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/apply_all.py\")", + "Bash(curl -s -m 5 -X POST http://127.0.0.1:8188/manager/reboot -o /dev/null -w \"reboot endpoint: %{http_code}\\\\n\")", + "Bash(curl -s -m 10 http://127.0.0.1:8188/object_info/DolphinArchitectV16)", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/d23fdd5e-1ca3-4729-9900-2ec0a67708b1/scratchpad/migrate_architect_v16_widgets.py\")", + "Bash(command -v node)", + "Bash(node --check \"D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js\")", + "Bash(curl -s -m 10 http://127.0.0.1:8188/object_info/TripleKSamplerWan22LightningAdvanced)", + "Bash(curl -s -m 10 http://127.0.0.1:8188/object_info/ImageBatchExtendWithOverlap)", + "Bash(tasklist /FI \"IMAGENAME eq python.exe\" /FO CSV /V)", + "Bash(xargs -n1 basename)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0730.json\" \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0730.bak_20260730_1_before_seedcontroller.json\" && ls -la \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/\" | grep SVI_0730)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/validate_seed_controller.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/validate_seed_controller.py\")", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/make_draft_workflow.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/validate_independent.py\")", + "Bash(git status *)", + "Bash(git mv *)", + "Bash(mv lora_classifier.py _archive/nodes_unregistered/)", + "Bash(mv \"0705_vision2.json.bak_20260703_before_previewonly\" _archive/workflows/)", + "Bash(mv \"0705_vision2.json.bak_20260703_before_trimwidgets\" _archive/workflows/)", + "Bash(mv \"0705_vision2.json.bak_20260724_1316_before_perclip_refactor\" _archive/workflows/)", + "Bash(mv \"0705_vision2.json.bak_20260724_before_fixseed\" _archive/workflows/)", + "Bash(mv \"0705_vision3_svigray_test.json\" _archive/workflows/)", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/migrate_visiondirector_widgets.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/revert_visiondirector_widgets.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/make_0730C_3step.py\")", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0730B.json\" \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0730D.json\" *)", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/make_0730D_3sampler.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/validate_0730D.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/swap_extend_svipro_motion.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/validate_0730C_swap.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/add_colormatch.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/edcc9f60-df16-4a24-a1cc-2f19a6b49251/scratchpad/validate_colormatch.py\")", + "Bash(python lora_profile.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/models/loras\")", + "Bash(python -m py_compile dolphin_vision_director.py lora_profile.py)", + "Bash(python -m py_compile dolphin_vision_director.py dolphin_prompt_director.py arch_logic_v16.py lora_profile.py)", + "Bash(python test_weave.py)", + "Bash(python -m py_compile dolphin_vision_director.py dolphin_prompt_director.py arch_logic_v16.py)", + "PowerShell(nvidia-smi --query-gpu=name,driver_version,memory.total,memory.used,power.limit,power.max_limit,power.default_limit,temperature.gpu,pcie.link.gen.current,pcie.link.gen.max --format=csv)", + "PowerShell(Get-WinEvent -FilterHashtable @{LogName='System'; StartTime=\\(Get-Date\\).AddDays\\(-14\\)} -ErrorAction SilentlyContinue | Where-Object { $_.Id -in 4101,41,6008,17,18,19,1001 -or $_.ProviderName -match 'WHEA|Display|nvlddmkm' } | Select-Object -First 40 TimeCreated,Id,ProviderName,@{n='Msg';e={\\($_.Message -split \"`n\"\\)[0]}} | Format-Table -AutoSize -Wrap)", + "PowerShell(Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='nvlddmkm'; StartTime=\\(Get-Date\\).AddDays\\(-30\\)} -ErrorAction SilentlyContinue | Group-Object Id | Select-Object Name,Count | Format-Table -AutoSize; \"---- burst dates ----\"; Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='nvlddmkm'; StartTime=\\(Get-Date\\).AddDays\\(-30\\)} -ErrorAction SilentlyContinue | Group-Object {$_.TimeCreated.ToString\\('yyyy-MM-dd HH:mm'\\)} | Select-Object Name,Count | Format-Table -AutoSize)", + "PowerShell($e = Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='nvlddmkm'; StartTime=\\(Get-Date\\).AddDays\\(-30\\)} -ErrorAction SilentlyContinue | Select-Object -First 3; foreach \\($x in $e\\) { \"=== Id=$\\($x.Id\\) $\\($x.TimeCreated\\) ===\"; \"MSG: \" + $x.Message; $b=$x.Properties | ForEach-Object { $_.Value }; \"RAWCOUNT: \" + $b.Count })", + "PowerShell(\"--- TDR settings ---\"; Get-ItemProperty 'HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\GraphicsDrivers' -ErrorAction SilentlyContinue | Select-Object TdrDelay,TdrDdiDelay,TdrLevel,TdrLimitTime,TdrLimitCount | Format-List; \"--- Kernel-Power 41 history \\(30d\\) ---\"; Get-WinEvent -FilterHashtable @{LogName='System'; Id=41; StartTime=\\(Get-Date\\).AddDays\\(-30\\)} -ErrorAction SilentlyContinue | Select-Object TimeCreated | Format-Table -AutoSize; \"--- WHEA ---\"; Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-WHEA-Logger'; StartTime=\\(Get-Date\\).AddDays\\(-30\\)} -ErrorAction SilentlyContinue | Select-Object -First 10 TimeCreated,Id | Format-Table -AutoSize)", + "PowerShell($p='C:\\\\Program Files \\(x86\\)\\\\MSI Afterburner'; if\\(Test-Path $p\\){ Get-ChildItem \"$p\\\\Profiles\" -ErrorAction SilentlyContinue | Select-Object Name,LastWriteTime | Format-Table -AutoSize; \"=== MSIAfterburner.cfg 주요 항목 ===\"; Get-Content \"$p\\\\MSIAfterburner.cfg\" -ErrorAction SilentlyContinue | Select-String -Pattern 'PowerLimit|CoreClkBoost|CurveEditor|StartupProfile|ApplyOverclockingAtStartup|VFCurve|MemClkBoost|VoltageLimit|ThermalLimit' | Select-Object -First 30 } else { \"Afterburner 기본 경로에 없음\" })", + "Bash(python test_weaver.py)", + "Bash(python -m py_compile __init__.py dolphin_lora_prompt.py lora_profile.py dolphin_vision_director.py dolphin_prompt_director.py arch_logic_v16.py)", + "PowerShell($p='C:\\\\Program Files \\(x86\\)\\\\MSI Afterburner\\\\MSIAfterburner.cfg'; $c=Get-Content -LiteralPath $p; $i=[array]::IndexOf\\($c,'[Profile1]'\\); if\\($i -ge 0\\){ \"=== [Profile1] ===\"; $c[$i..\\($i+8\\)] | Where-Object { $_ -match '^\\(PowerLimit|CoreClkBoost|MemClkBoost|CoreVoltageBoost|FanMode\\)=' } }; \"=== 현재 GPU 상태 ===\"; nvidia-smi --query-gpu=power.limit,power.default_limit,clocks.max.memory,clocks.current.memory,clocks.max.graphics,power.draw,utilization.gpu --format=csv)", + "PowerShell($p='C:\\\\Program Files \\(x86\\)\\\\MSI Afterburner\\\\Profiles\\\\VEN_10DE&DEV_2B85&SUBSYS_176119DA&REV_A1&BUS_43&DEV_0&FN_0.cfg'; \"=== 최종 수정: \" + \\(Get-Item -LiteralPath $p\\).LastWriteTime + \" ===\"; Get-Content -LiteralPath $p | Select-String -Pattern '^\\\\[|^PowerLimit=|^MemClkBoost=|^CoreClkBoost=|^CoreVoltageBoost=' | Select-Object -First 40)", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\cmp_lightx.py\" \"$s\\\\lightx.json\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\deep.py\" \"$s\\\\deep.json\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\deep2.py\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\seko.py\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\gguf_keys.py\" \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\models\\\\unet\\\\Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf\" \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\models\\\\unet\\\\Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf\")", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/bafbbba1-90c6-473d-adca-987f2886303d/scratchpad/test_energy.py\")", + "Bash(PYTHONIOENCODING=utf-8 python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/bafbbba1-90c6-473d-adca-987f2886303d/scratchpad/test_energy.py\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\schedule.py\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\inspect_wf.py\" 2>&1 | Select-Object -First 130)", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\inspect_wf.py\" 2>&1 | Select-String -Pattern 'SUBGRAPH|TripleKSampler|NAG|EmptyHunyuan|INTConstant|LoraLoader|widgets|IN ' | Select-Object -First 90)", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\split_trace.py\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\verify_beats.py\")", + "PowerShell($s=\"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\3550f829-5fde-43b7-b6dd-cb0068774acd\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"$s\\\\lora_matrix.py\")", + "PowerShell(try { $r = Invoke-WebRequest -Uri \"http://127.0.0.1:8188/system_stats\" -UseBasicParsing -TimeoutSec 5; $r.Content } catch { \"NOT RUNNING: $\\($_.Exception.Message\\)\" })", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\inspect_svi_lora.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\inspect2.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\make_variants.py\")", + "Bash(cd /c/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/c0afbfdf-4086-4bd7-97c2-9a00be688031/scratchpad && ls -la /d/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/painter10s.json /d/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0801.json)", + "Bash(python dump.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/painter10s.json\")", + "Bash(python dump.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0801.json\")", + "Bash(PYTHONIOENCODING=utf-8 python dump.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/painter10s.json\")", + "Bash(PYTHONIOENCODING=utf-8 python dump.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0801.json\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\motion_metric2.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\ab2_metrics.py\")", + "Bash(PYTHONIOENCODING=utf-8 sed -n '150,240p' nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '440,600p' nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '836,880p' nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '880,900p' nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 python dumpsub.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0801.json\")", + "Bash(echo \"--- subgraph diff rc=$? ---\")", + "Bash(echo \"--- toplevel diff rc=$? ---\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\inspect_0801.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\inspect_0801b.py\")", + "Bash(PYTHONIOENCODING=utf-8 python make_0801A.py)", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\sigma_sched.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\inspect_0801c.py\")", + "Bash(PYTHONIOENCODING=utf-8 python make_0801B.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '527,560p' dolphin_vision_director.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"seed\" dolphin_vision_director.py)", + "Read(//d/SD/ComfyUI_windows_portable/ComfyUI/temp/**)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"CACHE_DIR\" /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/disk_cache_provider.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '405,412p' dolphin_vision_director.py)", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\ab3_metrics.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\find_setnode.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\apply_steps8.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\state_and_flicker.py\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b07deb9e-9dc0-45e2-baa2-db286b327e37\\\\scratchpad\\\\nag_chain.py\")", + "Bash(PYTHONIOENCODING=utf-8 grep -rln \"PainterI2V\" --include=*.py .)", + "Bash(PYTHONIOENCODING=utf-8 grep -rn \"PainterI2V\\\\w*\" --include=*.py . -o)", + "Bash(PYTHONIOENCODING=utf-8 grep -oh \"\\\\\"type\\\\\": \\\\\"Painter[^\\\\\"]*\\\\\"\" *.json)", + "Bash(PYTHONIOENCODING=utf-8 grep -rloh \"PainterI2V_S\\\\|painteri2v_s\\\\|I2V_S\" *.json)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '1,150p' /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/ComfyUI-PainterI2Vadvanced/nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -rn \"NODE_CLASS_MAPPINGS\" -A 12 /d/SD/ComfyUI_windows_portable/ComfyUI/comfy_extras/nodes_context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class \\\\|node_id=\\\\|NODE_CLASS\\\\|display_name\\\\|def define_schema\\\\|Input\\(\" /d/SD/ComfyUI_windows_portable/ComfyUI/comfy_extras/nodes_context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 python dump.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/PainterI2V_S.json\")", + "Bash(PYTHONIOENCODING=utf-8 sed -n '40,75p' dolphin_prompt_director.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"RETURN_TYPES\\\\|RETURN_NAMES\\\\|num_clips\\\\|clip_count\\\\|10s\\\\|15s\" dolphin_prompt_director.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class StringConcatenate\\\\|node_id=\\\\\"String\" /d/SD/ComfyUI_windows_portable/ComfyUI/comfy_extras/nodes_string.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '39,64p' /d/SD/ComfyUI_windows_portable/ComfyUI/comfy_extras/nodes_string.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class ContextSchedules\" -A 12 /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class ContextFuseMethods\" -A 14 /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 python make_ctx.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class Create Hook\\\\|node_id=\\\\|display_name=\" /d/SD/ComfyUI_windows_portable/ComfyUI/comfy_extras/nodes_hooks.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"start_percent\\\\|class HookKeyframe\\\\b\" /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/hooks.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"split_conds_to_windows\\\\|region_index\\\\|def.*split_cond\" /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"cond_retain_index_list\\\\|retain_index\\\\|c_concat\\\\|concat_latent\\\\|def get_resized_cond\\\\|sub_idxs\" /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '66,90p' /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '505,525p' /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '113,132p' /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '40,110p' /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/Comfyui-PainterFLF2V/nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 python restore_flf.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class PrimitiveBoolean\" -A 20 /d/SD/ComfyUI_windows_portable/ComfyUI/comfy_extras/nodes_primitive.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '129,200p' /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/ComfyUI-Crystools/nodes/switch.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"node_id=\\\\|class \" /d/SD/ComfyUI_windows_portable/ComfyUI/comfy_extras/nodes_primitive.py)", + "Bash(PYTHONIOENCODING=utf-8 python elegant_switch.py)", + "Bash(PYTHONIOENCODING=utf-8 python audit_unused.py)", + "Bash(PYTHONIOENCODING=utf-8 python cleanup.py)", + "Bash(PYTHONIOENCODING=utf-8 python diff_effective.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"def common_ksampler\" -A 12 /d/SD/ComfyUI_windows_portable/ComfyUI/nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"vibe\\\\|pacing\\\\|PACING\\\\|beat\" dolphin_vision_director.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '366,392p' dolphin_vision_director.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '695,712p' dolphin_vision_director.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '820,835p' dolphin_vision_director.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"inject_triggers\\\\|trigger\" dolphin_prompt_director.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"def create_windows_uniform_standard\" -A 32 /d/SD/ComfyUI_windows_portable/ComfyUI/comfy/context_windows.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -rn \"class ImageFromBatch\" -A 22 /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/ComfyUI_essentials/image.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -rln \"ImageFromBatch+\" /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/*/[a-z]*.py)", + "Bash(PYTHONIOENCODING=utf-8 python cmp_svi.py \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_OK.json\" \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/SVI_0801C.json\")", + "Bash(PYTHONIOENCODING=utf-8 python make_0802ok.py)", + "Bash(PYTHONIOENCODING=utf-8 python fix_base_weights.py)", + "Bash(PYTHONIOENCODING=utf-8 python deep_cmp.py SVI_OK)", + "Bash(PYTHONIOENCODING=utf-8 python deep_cmp.py SVI_0802_OK)", + "Bash(PYTHONIOENCODING=utf-8 grep -rn \"WanImageToVideoSVIProMotion_Dolphin\" __init__.py *.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"RETURN_NAMES\\\\|RETURN_TYPES\\\\|motion_amplitude\\\\|amplitude_mode\\\\|def encode\\\\|if motion_amplitude\\\\|latents_mean\\\\|process_out\\\\|return \\(\" wan_svipro_motion.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"_amplify\" -A 14 wan_svipro_motion.py)", + "Bash(PYTHONIOENCODING=utf-8 python parse_meta.py meta_ok.txt)", + "Bash(PYTHONIOENCODING=utf-8 python pin_prompt.py)", + "Bash(PYTHONIOENCODING=utf-8 python make_10s_smooth.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -rn \"class ImageBatchExtendWithOverlap\" -A 40 /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/*/*.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -rln \"ImageBatchExtendWithOverlap\" --include=*.py .)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class ImageBatchExtendWithOverlap\" -A 60 /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/comfyui-kjnodes/nodes/image_nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 sed -n '2156,2215p' /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/comfyui-kjnodes/nodes/image_nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 grep -n \"class ColorMatchV2\" -A 30 /d/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/comfyui-kjnodes/nodes/image_nodes.py)", + "Bash(PYTHONIOENCODING=utf-8 python smooth_v2.py)", + "Bash(mkdir -p C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fa088da8-83db-496b-ae2c-8203c1b258d5/scratchpad/dircheck/raw)", + "Bash(./python.exe D:/SD/ComfyUI_windows_portable/ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/build.py --input C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fa088da8-83db-496b-ae2c-8203c1b258d5/scratchpad/dircheck/raw --output C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/fa088da8-83db-496b-ae2c-8203c1b258d5/scratchpad/dircheck/out --trigger test --captioner wd14)", + "PowerShell(Get-CimInstance Win32_Process -Filter \"Name='python.exe' or Name='accelerate.exe'\")", + "Bash(nvidia-smi --query-gpu=memory.used,memory.total,utilization.gpu --format=csv)", + "PowerShell(Get-CimInstance Win32_Process -Filter \"Name='python.exe'\")", + "Bash(tasklist //FI \"PID eq 40764\")", + "PowerShell(Get-NetTCPConnection -LocalPort 7861 -State Listen -ErrorAction SilentlyContinue)", + "Bash(netstat -ano -p tcp)", + "Bash(taskkill //F //T //PID 48048)", + "Bash(taskkill //F //T //PID 47888)", + "Bash(tasklist //FI \"PID eq 47888\" //FI \"PID eq 49728\" //FI \"PID eq 21336\" //FI \"PID eq 30148\" //FI \"PID eq 32348\")", + "PowerShell(Get-ChildItem \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\output\" -Force | Where-Object { $_.LinkType } | Select-Object Name, LinkType, @{n='Target';e={$_.Target}} | Format-List)", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" -c \"import os;d=r'D:\\\\SD\\\\output';t=os.path.join\\(d,'I2V','RAW'\\);print\\('realpath dir :',os.path.realpath\\(d\\)\\);print\\('realpath targ:',os.path.realpath\\(t\\)\\);print\\('common:',os.path.commonpath\\(\\(os.path.realpath\\(d\\),os.path.realpath\\(t\\)\\)\\)\\)\")", + "PowerShell(git -C \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\" log -5 --format=\"%h %ad %s\" --date=short -- folder_paths.py)", + "PowerShell(git -C \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\" log -S \"def is_within_directory\" --format=\"%h %ad %s\" --date=short -- folder_paths.py)", + "PowerShell(git -C \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\" log -1 --format=\"%h %ad %s\" --date=iso)", + "PowerShell(\"=== when was is_within_directory introduced ===\")", + "PowerShell(\"=== HEAD ===\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" -s -c \"import sys; sys.path.insert\\(0, r'D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI'\\); import folder_paths; folder_paths.set_output_directory\\(r'D:\\\\sd\\\\output'\\); print\\(folder_paths.get_save_image_path\\('I2V/RAW/test', folder_paths.get_output_directory\\(\\), 1280, 720\\)\\)\")", + "PowerShell(\"=== dolphin_nodes tools dir? ===\")", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\tools\\\\reapply_symlink_output_patch.ps1\"; \"exit=$LASTEXITCODE\")", + "Bash(mkdir -p /d/sd/models/loras/Krea2)", + "Bash(curl -L --fail -o \"/d/sd/models/loras/Krea2/krea2_identity_edit_v1_2.safetensors.part\" \"https://huggingface.co/conradlocke/krea2-identity-edit/resolve/main/krea2_identity_edit_v1_2.safetensors\" -w \"\\\\nHTTP %{http_code} | %{size_download} bytes | %{speed_download} B/s\\\\n\" -s --show-error)", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_minimax.py\")", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_minimax.py\" 2>&1 | Select-String -Pattern '^\\(ok|FAIL|ALL|SOME|---|\\\\[|At |She |Yuna|dusk|fallback\\)')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_reg.py\" 2>&1 | Select-String -Pattern 'registered|widgets|missing|extra|RETURN|Minimax')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_minimax.py\" 2>&1 | Select-String -Pattern '^\\(ok |FAIL| got| want|ALL|SOME|20s fallback|⚠\\)')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_minimax.py\" 2>&1 | Select-String -Pattern '^\\(FAIL| got| want|ALL|SOME|20s fallback|fallback:\\)')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_reg.py\" 2>&1 | Select-String -Pattern 'registered|widgets|missing|extra|RETURN|Minimax 로드')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_e2e.py\" 2>&1 | Select-Object -Skip 2)", + "Bash(nvidia-smi --query-gpu=name,memory.total,memory.used,memory.reserved,utilization.gpu,power.draw,clocks.sm --format=csv)", + "Bash(nvidia-smi --query-compute-apps=pid,used_memory,name --format=csv)", + "PowerShell(Get-PhysicalDisk | Select-Object DeviceId,FriendlyName,MediaType,BusType,@{n='SizeGB';e={[math]::Round\\($_.Size/1GB\\)}} | Format-Table -AutoSize; Get-Partition | Where-Object DriveLetter -in 'C','D' | Select-Object DriveLetter,DiskNumber; Get-CimInstance Win32_PageFileUsage | Select-Object Name,AllocatedBaseSize,CurrentUsage)", + "PowerShell($os = Get-CimInstance Win32_OperatingSystem; $cs = Get-CimInstance Win32_ComputerSystem; [PSCustomObject]@{ AutoManagedPagefile=$cs.AutomaticManagedPagefile; TotalRAM_GB=[math]::Round\\($cs.TotalPhysicalMemory/1GB,1\\); FreeRAM_GB=[math]::Round\\($os.FreePhysicalMemory/1MB,1\\); CommitLimit_GB=[math]::Round\\($os.TotalVirtualMemorySize/1MB,1\\); CommitUsed_GB=[math]::Round\\(\\($os.TotalVirtualMemorySize-$os.FreeVirtualMemory\\)/1MB,1\\) } | Format-List; Get-CimInstance Win32_PageFileSetting | Select-Object Name,InitialSize,MaximumSize)", + "Bash(PYTHONIOENCODING=utf-8 ../python_embeded/python.exe -c ' *)", + "WebFetch(domain:docs.ltx.io)", + "Bash(/d/SD/ComfyUI_windows_portable/python_embeded/python.exe -X utf8 -c ' *)", + "Bash(grep -n \"relative_path\\\\|os.sep\\\\|replace\\(\\\\\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"\" /d/SD/ComfyUI_windows_portable/ComfyUI/folder_paths.py)", + "PowerShell(& \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b860b1fe-4dc7-4bed-a239-3b1ec6dd53e9\\\\scratchpad\\\\scan.ps1\")", + "PowerShell(& \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b860b1fe-4dc7-4bed-a239-3b1ec6dd53e9\\\\scratchpad\\\\scan2.ps1\")", + "PowerShell($p=\"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\.claude\"; if \\(Test-Path $p\\) { Get-ChildItem $p -Recurse -File | Select-Object FullName } else { \"no .claude dir\" }; \"--- user settings ---\"; if \\(Test-Path \"$env:USERPROFILE\\\\.claude\\\\settings.json\"\\) { Get-Content \"$env:USERPROFILE\\\\.claude\\\\settings.json\" -Raw } else { \"none\" })", + "Bash(awk '{print $1}')", + "Bash(git --no-pager log --stat -1)", + "Bash(git --no-pager log --oneline -3)", + "WebFetch(domain:kingy.ai)", + "WebFetch(domain:blog.comfy.org)", + "Bash(wmic ComputerSystem get TotalPhysicalMemory)", + "WebFetch(domain:www.kombitz.com)", + "WebFetch(domain:www.videotoprompt.app)", + "WebFetch(domain:mixio.studio)", + "WebFetch(domain:www.photogptai.com)", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; Set-Location \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" fix.py)", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_minimax.py\" 2>&1 | Select-String -Pattern '^\\(FAIL| got| want|ALL|SOME\\)')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_e2e.py\" 2>&1 | Select-Object -Skip 30)", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_reg.py\" 2>&1 | Select-String -Pattern 'registered|widgets|missing|extra|RETURN')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; Set-Location \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" patch2.py)", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_minimax.py\" 2>&1 | Select-String -Pattern '^\\(FAIL| got| want|ALL|SOME|Yuna, a woman\\)')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_e2e.py\" 2>&1 | Select-String -Pattern '^\\(\\\\[|Yuna|First|=====|>>>|⚠|AssertionError|Traceback\\)' -Context 0,1)", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_minimax.py\" 2>&1 | Select-String -Pattern '^\\(FAIL| got| want|ALL|SOME\\)'; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_e2e.py\" 2>&1 | Select-String -Pattern '^\\(>>>|AssertionError|For roughly|Yuna\\\\. For\\)')", + "PowerShell($env:PYTHONIOENCODING=\"utf-8\"; & \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\python_embeded\\\\python.exe\" \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\b67454ac-dcc9-436c-a752-89e8afa7fd38\\\\scratchpad\\\\test_reg.py\" 2>&1 | Select-String -Pattern 'registered|widgets|missing|extra')", + "Bash(mkdir -p D:/SD/ComfyUI_windows_portable/ComfyUI/models/loras)", + "Bash(curl -L -o minimax_h3_turbo_4step_ema_ckpt850_pruned_comfyui.safetensors.part \"https://huggingface.co/drbaph/MiniMax-H3-Turbo-Lora-ComfyUI/resolve/main/minimax_h3_turbo_4step_ema_ckpt850_pruned_comfyui.safetensors\" --progress-bar)", + "Bash(python_embeded/python.exe \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/bcff1cb2-b893-4caa-b23f-9593c85aa7f9/scratchpad/build_h3_extend.py\")", + "Bash(sed -i '22s/다섯 가지/SVI\\\\/COMP\\\\/ANI\\\\/COMB\\\\/GROK\\\\/LTX\\\\/H3/' rename.py)", + "Bash(command -v python3.exe)", + "Bash(\"/d/SD/ComfyUI_windows_portable/python_embeded/python.exe\" -m py_compile rename.py)", + "WebFetch(domain:sonusahani.com)", + "WebFetch(domain:x.com)", + "Bash(gh repo *)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/H3_I2V_turbo_2.json\" \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/H3_I2V_turbo_2.json.bak\" *)", + "WebFetch(domain:jo-nike.github.io)", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/4eaf8adb-112d-4cf0-973e-596238ffa388/scratchpad/integrate_motion_context.py\")", + "Bash(python \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/4eaf8adb-112d-4cf0-973e-596238ffa388/scratchpad/validate_workflow.py\")", + "Bash(curl -s \"https://api.github.com/search/code?q=video_wan_animate2+repo:Comfy-Org/workflow_templates\")", + "Bash(curl -s \"https://api.github.com/repos/Comfy-Org/workflow_templates/git/trees/main?recursive=1\")", + "Bash(curl -s \"https://raw.githubusercontent.com/Comfy-Org/workflow_templates/main/templates/video_wan_animate2.json\" -o \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/6375561a-517b-4f36-9a22-cda969f4d956/scratchpad/video_wan_animate2.json\")", + "Bash(py -3 -c \"print\\('ok'\\)\")", + "Bash(py -3 -c ' *)", + "Bash(./python_embeded/python.exe -s -m pip install -r ComfyUI/requirements.txt)", + "Bash(cp \"C:/Users/bjoo/AppData/Local/Temp/claude/D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes/6375561a-517b-4f36-9a22-cda969f4d956/scratchpad/video_wan_animate2.json\" \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/default/workflows/video_wan_animate2.json\" && echo copied)", + "Bash(curl -sI \"https://huggingface.co/Comfy-Org/Wan-Animate-2/resolve/main/diffusion_models/wan_animate_2_int8_convrot.safetensors\")", + "Bash(curl -sI \"https://huggingface.co/Comfy-Org/Wan-Animate-2/resolve/main/loras/lightx2v_I2V_14B_480p_cfg_step_distill_rank64_bf16.safetensors\")", + "Bash(curl -sIL \"https://huggingface.co/Comfy-Org/Wan-Animate-2/resolve/main/diffusion_models/wan_animate_2_int8_convrot.safetensors\")", + "Bash(curl -sIL \"https://huggingface.co/Comfy-Org/Wan-Animate-2/resolve/main/loras/lightx2v_I2V_14B_480p_cfg_step_distill_rank64_bf16.safetensors\")", + "Bash(curl -sL \"https://raw.githubusercontent.com/Comfy-Org/workflow_templates/refs/heads/main/input/pink_hair_mech_arms_ref.png\" -o pink_hair_mech_arms_ref.png)", + "Bash(curl -sL \"https://raw.githubusercontent.com/Comfy-Org/workflow_templates/refs/heads/main/input/street_dance_drive.mp4\" -o street_dance_drive.mp4)", + "Bash(curl -L \"https://huggingface.co/Comfy-Org/Wan-Animate-2/resolve/main/loras/lightx2v_I2V_14B_480p_cfg_step_distill_rank64_bf16.safetensors\" -o lightx2v_I2V_14B_480p_cfg_step_distill_rank64_bf16.safetensors --progress-bar)", + "Bash(curl -s http://127.0.0.1:8188/history?max_items=3 -o /tmp/history.json)", + "Bash(curl -sv http://127.0.0.1:8188/system_stats)", + "Bash(curl -s http://127.0.0.1:8188/history?max_items=5 -o 'C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\d26bed8e-b31a-41a3-8a79-051cab0eea70\\\\scratchpad/history.json')", + "Bash(curl -s http://127.0.0.1:8188/history?max_items=50 -o 'C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\d26bed8e-b31a-41a3-8a79-051cab0eea70\\\\scratchpad/history_full.json')", + "Bash(curl -s http://127.0.0.1:8188/queue -o 'C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\d26bed8e-b31a-41a3-8a79-051cab0eea70\\\\scratchpad/queue.json')", + "Bash(python -m py_compile dolphin_minimax_director.py)", + "Bash(python -m py_compile dolphin_vision_director.py dolphin_minimax_director.py dolphin_prompt_director.py)", + "Bash(python3)", + "Bash(cat)", + "Bash(python /tmp/test_split.py)", + "Bash(export PYTHONIOENCODING=utf-8)", + "Bash(find / -iname \"*nsfw*mmaudio*\" -o -iname \"*mmaudio*nsfw*\" 2>/dev/null | head -50)", + "Bash(find / -ipath \"*mmaudio*\" 2>/dev/null | grep -v \"custom_nodes/.*\\\\.py\" | head -100)", + "Bash(kill %1 %2)", + "Bash(jobs -l)", + "WebFetch(domain:comfyai.run)", + "Bash(npm --version)", + "WebFetch(domain:opencof.github.io)", + "WebFetch(domain:www.patreon.com)", + "Bash(gh auth *)", + "PowerShell(winget list --id GitHub.cli 2>&1)", + "PowerShell(winget install --id GitHub.cli -e --accept-source-agreements --accept-package-agreements)", + "Bash(curl -s -m 3 http://127.0.0.1:8188/ -o /dev/null -w \"%{http_code}\\\\n\")", + "Bash(\"/c/SD/ComfyUI_windows_portable/python_embeded/python.exe\" cm-cli.py --help)", + "Bash(\"/d/SD/ComfyUI_windows_portable/python_embeded/python.exe\" cm-cli.py --help)", + "Bash(\"/d/SD/ComfyUI_windows_portable/python_embeded/python.exe\" cm-cli.py save-snapshot)", + "Bash(python3 -c \"import json;d=json.load\\(open\\('2026-08-12_12-32-11_snapshot.json'\\)\\);print\\(list\\(d.keys\\(\\)\\)\\);print\\('git_custom_nodes count:',len\\(d.get\\('git_custom_nodes',{}\\)\\)\\)\")", + "Bash(\"/d/SD/ComfyUI_windows_portable/python_embeded/python.exe\" -c \"import json;d=json.load\\(open\\('2026-08-12_12-32-11_snapshot.json'\\)\\);print\\(list\\(d.keys\\(\\)\\)\\);print\\('git_custom_nodes count:',len\\(d.get\\('git_custom_nodes',{}\\)\\)\\)\")", + "Bash(/d/SD/ComfyUI_windows_portable/python_embeded/python.exe -c ' *)", + "Bash(mkdir -p cloud_env)", + "Bash(cp \"D:/SD/ComfyUI_windows_portable/ComfyUI/user/__manager/snapshots/2026-08-12_12-32-11_snapshot.json\" cloud_env/comfyui_env_snapshot.json)", + "Bash(git push *)", + "Bash(where ffmpeg *)", + "Bash(ffmpeg -version)", + "Bash(npm install *)", + "PowerShell(\\(Get-Content 'D:\\\\orga.py' -Raw\\) -replace [regex]::Escape\\(\" shutil.move\\(src, dst\\)`r`n return True, None\"\\), \" shutil.move\\(src, dst\\)`r`n return True, dst\" | Set-Content 'D:\\\\orga.py' -NoNewline)", + "Bash(python -m py_compile \"/d/orga.py\")", + "Bash(ls -la /d/ 2>&1 | head -5; echo \"---\"; ls -la \"D:/orga.py\" 2>&1)", + "Bash(python -m py_compile \"D:/orga.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga2.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga4.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga5.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga6.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga7.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga8.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga9.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga11.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga12.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga13.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga14.py\")", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga15.py\")", + "Bash(python3 -)", + "Bash(python -m py_compile \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\d2b174de-a935-400d-9712-b631f10d35b9\\\\scratchpad\\\\dedupe_folders_gui.py\")", + "Bash(python -m py_compile \"D:\\\\dedupe_folders.py\" \"D:\\\\dedupe_folders_gui.py\")", + "Bash(awk '{print $5\"MB\", $NF}')", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\tools\\\\reapply_symlink_output_patch.ps1\" -Verify)", + "PowerShell(& \"D:\\\\SD\\\\ComfyUI_windows_portable\\\\ComfyUI\\\\custom_nodes\\\\dolphin_nodes\\\\tools\\\\reapply_symlink_output_patch.ps1\")", + "Bash(./yt-dlp.exe --version)", + "Bash(./yt-dlp.exe -U)", + "Bash(./yt-dlp.exe -f bestaudio/best --no-playlist -o \"test_%\\(title\\)s.%\\(ext\\)s\" \"https://www.youtube.com/watch?v=bMhDJ0S0OBA\")", + "Bash(\"/c/Users/bjoo/anaconda3/pkgs/ffmpeg-6.1.1-hc79a5da_3/Library/bin/ffmpeg.exe\" -version)", + "PowerShell(& \"C:\\\\Users\\\\bjoo\\\\anaconda3\\\\pkgs\\\\ffmpeg-6.1.1-hc79a5da_3\\\\Library\\\\bin\\\\ffmpeg.exe\" -version 2>&1)", + "PowerShell(& \"C:\\\\Program Files\\\\Krita \\(x64\\)\\\\bin\\\\ffmpeg.exe\" -version 2>&1)", + "Bash(ls \"/c/Program Files/Krita \\(x64\\)/bin/\" | grep -i \"ffprobe\\\\|ffmpeg\")", + "Bash(rm -f \"ILLIT\"*.webm \"ILLIT\"*.mp3)", + "Bash(./yt-dlp.exe -o \"verify_test.%\\(ext\\)s\" --ffmpeg-location \"C:\\\\Program Files\\\\Krita \\(x64\\)\\\\bin\" -x --audio-format mp3 -f bestaudio/best --no-playlist \"https://www.youtube.com/watch?v=bMhDJ0S0OBA\")", + "Bash(./yt-dlp.exe -o \"%\\(title\\)s.%\\(ext\\)s\" --ffmpeg-location \"C:\\\\Program Files\\\\Krita \\(x64\\)\\\\bin\" -x --audio-format mp3 -f bestaudio/best --no-playlist --force-overwrites -v \"https://www.youtube.com/watch?v=bMhDJ0S0OBA\")", + "PowerShell([System.Text.Encoding]::Default.EncodingName; chcp)", + "Bash(sort -k6,7)", + "WebFetch(domain:openrouter.ai)", + "Bash(python -m py_compile dolphin_vision_director.py dolphin_minimax_director.py)", + "Bash(node --check web/dolphin_preset_sync.js)", + "Bash(python \"C:\\\\Users\\\\bjoo\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D--SD-ComfyUI-windows-portable-ComfyUI-custom-nodes-dolphin-nodes\\\\0a101ba9-4e7e-41e4-b61a-533bc9170100\\\\scratchpad\\\\patch_orga17.py\")" + ], + "defaultMode": "bypassPermissions" + } +} diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f53e6b63dbd483145dc89bb537089bc3e4a19d49 --- /dev/null +++ b/__init__.py @@ -0,0 +1,227 @@ +import traceback + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} +WEB_DIRECTORY = "./web" + + +def register_node(class_name, node_class, display_name): + NODE_CLASS_MAPPINGS[class_name] = node_class + NODE_DISPLAY_NAME_MAPPINGS[class_name] = display_name + + +# 1. Wan Native Resize (이미지 -> WAN 해상도 리사이즈, 비율 유지 + 정렬) +try: + from .wan_resizer import WanNativeResize_Dolphin + register_node("WanNativeResize_Dolphin", WanNativeResize_Dolphin, + "🐬 Wan Native Resize (Dolphin)") +except Exception: + print(f"\n❌ [Dolphin] Wan Resize 로드 실패:\n{traceback.format_exc()}") + +# 2. Story Splitter V32 (텍스트 서사 -> 4샷 프롬프트 분배) +try: + from .promp_logic_v32_story import DolphinMultiActionPromptNode_V32 + register_node("DolphinMultiActionPromptNode_V32", DolphinMultiActionPromptNode_V32, + "🐬 Dolphin Story Splitter V32") +except Exception: + print(f"\n❌ [Dolphin] V32 Story Splitter 로드 실패:\n{traceback.format_exc()}") + +# 3. Architect V16 (클립별 LoRA -> TripleKSampler, model-only) +try: + from .arch_logic_v16 import DolphinTripleLoraMatrix as DolphinArchitectV16 + register_node("DolphinArchitectV16", DolphinArchitectV16, + "🐬 Dolphin Architect V16") +except Exception: + print(f"\n❌ [Dolphin] Architect V16 로드 실패:\n{traceback.format_exc()}") + +# 4. Cinematic Director (올인원: 스토리 분배 + 클립별 로라 + 트리거 주입) +try: + from .dolphin_director import DolphinCinematicDirector + register_node("DolphinCinematicDirector", DolphinCinematicDirector, + "🎬 Dolphin Cinematic Director (All-in-One)") +except Exception: + print(f"\n❌ [Dolphin] Cinematic Director 로드 실패:\n{traceback.format_exc()}") + +# 5. Vision Director (이미지 인식 + 필수동작 + 클립당 로라 4개) +try: + from .dolphin_vision_director import DolphinVisionDirector + register_node("DolphinVisionDirector", DolphinVisionDirector, + "🎬👁 Dolphin Vision Director (Image→4Clips)") +except Exception: + print(f"\n❌ [Dolphin] Vision Director 로드 실패:\n{traceback.format_exc()}") + +# 6. Architect V15 (arch_logic_v16 이전 구버전, model_base/high/low + clip 개별 입력) +try: + from .arch_logic import DolphinArchitectV15 + register_node("DolphinArchitectV15", DolphinArchitectV15, + "🐬 Dolphin Architect V15 (legacy)") +except Exception: + print(f"\n❌ [Dolphin] Architect V15 로드 실패:\n{traceback.format_exc()}") + +# 7. Story Splitter V28 (V32 이전 구버전, stitch 방식) +try: + from .promp_logic_v28_stitch import DolphinMultiActionPromptNode_V28 + register_node("DolphinMultiActionPromptNode_V28", DolphinMultiActionPromptNode_V28, + "🐬 Dolphin Story Splitter V28 (legacy)") +except Exception: + print(f"\n❌ [Dolphin] V28 Story Splitter 로드 실패:\n{traceback.format_exc()}") + +# 8. Story Splitter V30 (V32 이전 구버전) +try: + from .promp_logic_v30_story import DolphinMultiActionPromptNode_V30 + register_node("DolphinMultiActionPromptNode_V30", DolphinMultiActionPromptNode_V30, + "🐬 Dolphin Story Splitter V30 (legacy)") +except Exception: + print(f"\n❌ [Dolphin] V30 Story Splitter 로드 실패:\n{traceback.format_exc()}") + +# 9. Prompt V32 Vision (promp_logic_v32_story.py의 DolphinMultiActionPromptNode_V32와 +# 클래스명이 동일해서 충돌 — 등록 키/클래스명을 _Vision으로 구분해서 둘 다 살림) +try: + from .promp_logic_v32_vision import DolphinMultiActionPromptNode_V32 as DolphinMultiActionPromptNode_V32_Vision + register_node("DolphinMultiActionPromptNode_V32_Vision", DolphinMultiActionPromptNode_V32_Vision, + "🐬 Dolphin Prompt V32 Vision (legacy)") +except Exception: + print(f"\n❌ [Dolphin] V32 Vision 로드 실패:\n{traceback.format_exc()}") + + +# 10. Match Image & Mask Size (인페인트 블렌드용: 이미지/마스크를 참조 이미지 실제 크기에 맞춤) +try: + from .match_size import MatchImageAndMaskSize_Dolphin + register_node("MatchImageAndMaskSize_Dolphin", MatchImageAndMaskSize_Dolphin, + "🐬 Match Image & Mask Size (Dolphin)") +except Exception: + print(f"\n❌ [Dolphin] Match Image & Mask Size 로드 실패:\n{traceback.format_exc()}") + + +# 11. Mask-Region Color Match (인페인트 마스크 영역만 국소적으로 색보정) +try: + from .mask_region_color_match import MaskRegionColorMatch_Dolphin + register_node("MaskRegionColorMatch_Dolphin", MaskRegionColorMatch_Dolphin, + "🐬 Mask-Region Color Match (Dolphin)") +except Exception: + print(f"\n❌ [Dolphin] Mask-Region Color Match 로드 실패:\n{traceback.format_exc()}") + + +# 12. Prompt Director (Vision Director와 동일한 비전+LLM 엔진, MODEL/LoRA 전부 제거 - +# 프롬프트 분배만. 로라/모델은 Architect V16 등 별도 노드에서 처리한다는 전제) +try: + from .dolphin_prompt_director import ( + DolphinPromptDirector, DolphinPromptDirectorSingle, DolphinActionSplitter, + ) + register_node("DolphinPromptDirector", DolphinPromptDirector, + "🎬📝 Dolphin Prompt Director (Image→N Prompts, no model/LoRA)") + register_node("DolphinPromptDirectorSingle", DolphinPromptDirectorSingle, + "🎬📝 Dolphin Prompt Director (Single, Image→1 Prompt)") + register_node("DolphinActionSplitter", DolphinActionSplitter, + "✂️ Dolphin Action Splitter (deterministic, no LLM)") +except Exception: + print(f"\n❌ [Dolphin] Prompt Director 로드 실패:\n{traceback.format_exc()}") + + +# 13. WanImageToVideoSVIPro mask 버그 수정판 (comfyui-kjnodes 원본은 motion_latent_count로 +# 이어붙인 모션 컨텍스트 프레임을 concat_mask에서 "known"으로 표시하지 않아서, 클립 +# 경계에서 몇 프레임 반복 후 전혀 다른 영상으로 튀는 문제가 있었음. anchor+motion 프레임 +# 전체를 known(0.0)으로 마스킹하도록 수정.) +try: + from .wan_svipro_fixed import WanImageToVideoSVIProFixed_Dolphin + register_node("WanImageToVideoSVIProFixed_Dolphin", WanImageToVideoSVIProFixed_Dolphin, + "🐬 Wan I2V SVIPro (motion-mask fix)") +except Exception: + print(f"\n❌ [Dolphin] SVIPro Fixed 로드 실패:\n{traceback.format_exc()}") + + +# 14. Wan I2V Continuity (코어 WanImageToVideo 방식 = known 프레임 여러 장을 회색 filler와 +# 함께 한 번에 통짜 VAE 인코딩 → latent 문맥 일관성 보장. SVIPro식 raw latent 스플라이싱의 +# 화질 튐/불연속 문제를 회피. PainterI2V의 motion_amplitude(슬로우모션 방지) 이식.) +try: + from .wan_i2v_continuity import WanI2VContinuity_Dolphin + register_node("WanI2VContinuity_Dolphin", WanI2VContinuity_Dolphin, + "🐬 Wan I2V Continuity (multi-frame + amplitude)") +except Exception: + print(f"\n❌ [Dolphin] Wan I2V Continuity 로드 실패:\n{traceback.format_exc()}") + + +# 15. SVIPro 드롭인 교체 (슬로우모션 수정). 원본은 filler를 latent 공간에서 +# Wan21().process_out(zeros) = latents_mean 상수로 채워서 "변화 없음" 신호를 주고 +# 분포 밖 입력이 됐음 -> 코어 WanImageToVideo/PainterI2V처럼 회색(0.5) 픽셀을 VAE +# 인코딩한 filler로 교체. 마스크 버그(anchor 1프레임만 known)도 함께 수정. +try: + from .wan_svipro_gray import WanImageToVideoSVIProGray_Dolphin + register_node("WanImageToVideoSVIProGray_Dolphin", WanImageToVideoSVIProGray_Dolphin, + "🐬 Wan I2V SVIPro (gray filler, slow-motion fix)") +except Exception: + print(f"\n❌ [Dolphin] SVIPro Gray 로드 실패:\n{traceback.format_exc()}") + + +# 16. SVIPro 드롭인 교체 (모션 증폭판). 15번(gray)과 달리 패딩·마스크는 상류 원본 그대로 두고 +# PainterI2V의 diff 증폭만 얹는다 — SVI Pro LoRA가 원본 규약 위에서 학습됐기 때문. +# high/low conditioning을 따로 뽑아서 모션 증폭은 high-noise 스테이지에만 먹인다. +# motion_amplitude=1.0이면 상류 원본과 출력이 동일(회귀 확인용). +try: + from .wan_svipro_motion import WanImageToVideoSVIProMotion_Dolphin + register_node("WanImageToVideoSVIProMotion_Dolphin", WanImageToVideoSVIProMotion_Dolphin, + "🐬 Wan I2V SVIPro (motion amplitude, hi/lo split)") +except Exception: + print(f"\n❌ [Dolphin] SVIPro Motion 로드 실패:\n{traceback.format_exc()}") + + +# 17. 디스크 노드 캐시 (노드가 아니라 ComfyUI cache provider 등록). +# 시드/프롬프트/이미지가 같으면 재시작 후에도 JoyCaption~샘플러 체인을 스킵하고 +# 디스크에서 결과 복원. RAM_PRESSURE 모드의 캐시 증발도 디스크에서 자동 복구. +try: + from . import disk_cache_provider + disk_cache_provider.register() +except Exception: + print(f"\n❌ [Dolphin] 디스크 노드 캐시 로드 실패 (구버전 ComfyUI엔 provider API 없음):\n{traceback.format_exc()}") + + +# 18. Batch Seed Controller (4클립 영상 샘플러 시드를 한 노드에서 스위칭: +# 고정/전체랜덤/특정클립만재생성). +try: + from .dolphin_seed_controller import DolphinBatchSeedController + register_node("DolphinBatchSeedController", DolphinBatchSeedController, + "🎲 Dolphin Batch Seed Controller (random-all / regen-from-clip)") +except Exception: + print(f"\n❌ [Dolphin] Batch Seed Controller 로드 실패:\n{traceback.format_exc()}") + + +# 19. LoRA Prompt Weaver (완성된 클립 프롬프트를 "그 클립에 걸리는 로라들이 잘 발화하도록" +# 다시 쓰는 후처리 노드. 로라 사이드카(.metadata.json)의 설명/태그/학습캡션을 LLM에 +# 먼저 주고, 트리거를 문장 안 해당 부위에 붙여서 재작성한다 - 예전처럼 이름 옆에 +# 콤마로 나열하면 bounce/swing 같은 범용 동사가 전신 모션으로 새기 때문. +# 기존 노드는 건드리지 않고 뒤에 끼워 넣는 방식이라 어떤 프롬프트 소스에도 붙는다.) +try: + from .dolphin_lora_prompt import DolphinLoraPromptWeaver + register_node("DolphinLoraPromptWeaver", DolphinLoraPromptWeaver, + "🧬 Dolphin LoRA Prompt Weaver (프롬프트→로라에 맞게 재작성)") +except Exception: + print(f"\n❌ [Dolphin] LoRA Prompt Weaver 로드 실패:\n{traceback.format_exc()}") + + +# 20. Minimax Director (MiniMax/Hailuo용 프롬프트 디렉터. Wan용 Prompt Director와 달리 +# 클립 하나가 6/10초라서, 그 안을 초 단위 비트로 쪼개고 각 구간에 동작 + MiniMax +# 공식 카메라 무브 태그를 박아준다.) +try: + from .dolphin_minimax_director import DolphinMinimaxDirector + register_node("DolphinMinimaxDirector", DolphinMinimaxDirector, + "🎬🅜 Dolphin Minimax Director (초 단위 타임라인 → Hailuo 프롬프트)") +except Exception: + print(f"\n❌ [Dolphin] Minimax Director 로드 실패:\n{traceback.format_exc()}") + + +# 21. Wan Refine H3 (MiniMax H3 출력 → Wan 2.2 LOW noise 저-denoise 리파인 패스 올인원). +# H3(24fps / 17k+5 프레임 그리드 / 768 short edge)와 Wan(16fps / 4n+1 / Wan2.1 VAE 16ch) +# 사이의 배선 함정을 한 노드에서 처리: 4n+1 정렬, VAE 오배선 차단, 소스 라텐트 인코딩, +# concat 컨디셔닝 재구성, sigma_shift 직접 패치, 색 드리프트 복원, 오디오 길이 정렬. +# 같이 등록되는 Frame Grid 노드는 두 그리드가 겹치는 length(5/73/141/209/277/345…)를 계산한다. +try: + from .wan_h3_refine import WanH3Refine_Dolphin, WanH3FrameGrid_Dolphin + register_node("WanH3Refine_Dolphin", WanH3Refine_Dolphin, + "🐬🅜 Wan Refine H3 (low-denoise pass)") + register_node("WanH3FrameGrid_Dolphin", WanH3FrameGrid_Dolphin, + "🐬🅜 H3↔Wan Frame Grid") +except Exception: + print(f"\n❌ [Dolphin] Wan Refine H3 로드 실패:\n{traceback.format_exc()}") + + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d41f9c531d00fec67fb657f188ea1edf639c7c3 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72139fc8622f955503d9890d9c2689c65e1e1a9a Binary files /dev/null and b/__pycache__/__init__.cpython-313.pyc differ diff --git a/__pycache__/arch_logic.cpython-312.pyc b/__pycache__/arch_logic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13a5071baed3fc9391dc9ebd79c9ba2c540555e3 Binary files /dev/null and b/__pycache__/arch_logic.cpython-312.pyc differ diff --git a/__pycache__/arch_logic.cpython-313.pyc b/__pycache__/arch_logic.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed56650b9727d8578aad28287e3e1f836768fbef Binary files /dev/null and b/__pycache__/arch_logic.cpython-313.pyc differ diff --git a/__pycache__/arch_logic_v16.cpython-312.pyc b/__pycache__/arch_logic_v16.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b851f76001c2269cf9318065357dd3df7780ec2a Binary files /dev/null and b/__pycache__/arch_logic_v16.cpython-312.pyc differ diff --git a/__pycache__/arch_logic_v16.cpython-313.pyc b/__pycache__/arch_logic_v16.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d81866f946e753c06adfd9ef558235492589cc4d Binary files /dev/null and b/__pycache__/arch_logic_v16.cpython-313.pyc differ diff --git a/__pycache__/dedupe_folders.cpython-312.pyc b/__pycache__/dedupe_folders.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2395d464a77c40167cfa9e4c351bf9c41a4be15a Binary files /dev/null and b/__pycache__/dedupe_folders.cpython-312.pyc differ diff --git a/__pycache__/dedupe_folders_gui.cpython-312.pyc b/__pycache__/dedupe_folders_gui.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0277dc5f972815b9a807728c916ea9bc7edd1e77 Binary files /dev/null and b/__pycache__/dedupe_folders_gui.cpython-312.pyc differ diff --git a/__pycache__/disk_cache_provider.cpython-312.pyc b/__pycache__/disk_cache_provider.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2df7a854aa4d05c937748d7933c0d722ba330716 Binary files /dev/null and b/__pycache__/disk_cache_provider.cpython-312.pyc differ diff --git a/__pycache__/disk_cache_provider.cpython-313.pyc b/__pycache__/disk_cache_provider.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..880f86f378cff82ed80487e3d14f276ca85fcdfd Binary files /dev/null and b/__pycache__/disk_cache_provider.cpython-313.pyc differ diff --git a/__pycache__/dolphin_director.cpython-313.pyc b/__pycache__/dolphin_director.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ba14151ab3acd6bc0447d934d672ae1462fdc0e Binary files /dev/null and b/__pycache__/dolphin_director.cpython-313.pyc differ diff --git a/__pycache__/dolphin_lora_prompt.cpython-312.pyc b/__pycache__/dolphin_lora_prompt.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4936e355ccf3dc3551a5687e1b1368781724e00f Binary files /dev/null and b/__pycache__/dolphin_lora_prompt.cpython-312.pyc differ diff --git a/__pycache__/dolphin_lora_prompt.cpython-313.pyc b/__pycache__/dolphin_lora_prompt.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ceb8bedc28a6cf3c310482a87f19a6ed408354fe Binary files /dev/null and b/__pycache__/dolphin_lora_prompt.cpython-313.pyc differ diff --git a/__pycache__/dolphin_minimax_director.cpython-312.pyc b/__pycache__/dolphin_minimax_director.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5e5e8aa70606878f30536ad0d199b5946ebc219 Binary files /dev/null and b/__pycache__/dolphin_minimax_director.cpython-312.pyc differ diff --git a/__pycache__/dolphin_minimax_director.cpython-313.pyc b/__pycache__/dolphin_minimax_director.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eeb3ded3abe07782bce95559c2b6e43ceca0e925 Binary files /dev/null and b/__pycache__/dolphin_minimax_director.cpython-313.pyc differ diff --git a/__pycache__/dolphin_prompt_director.cpython-312.pyc b/__pycache__/dolphin_prompt_director.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54bf8614c63fd3310b782c6da15ce93fe3873421 Binary files /dev/null and b/__pycache__/dolphin_prompt_director.cpython-312.pyc differ diff --git a/__pycache__/dolphin_prompt_director.cpython-313.pyc b/__pycache__/dolphin_prompt_director.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..acc8f055878aa3664174fcd93b3711ccc8c2ef7f Binary files /dev/null and b/__pycache__/dolphin_prompt_director.cpython-313.pyc differ diff --git a/__pycache__/dolphin_seed_controller.cpython-312.pyc b/__pycache__/dolphin_seed_controller.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a689f7f3f8388be64d5944db3fdee50ab0175e7b Binary files /dev/null and b/__pycache__/dolphin_seed_controller.cpython-312.pyc differ diff --git a/__pycache__/dolphin_seed_controller.cpython-313.pyc b/__pycache__/dolphin_seed_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..037be033dfc1c574638dd2d5eebe2a9f66177a69 Binary files /dev/null and b/__pycache__/dolphin_seed_controller.cpython-313.pyc differ diff --git a/__pycache__/dolphin_vision_director.cpython-312.pyc b/__pycache__/dolphin_vision_director.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f706a6061ea9a27cb0b979a74778f9fcb6275b32 Binary files /dev/null and b/__pycache__/dolphin_vision_director.cpython-312.pyc differ diff --git a/__pycache__/dolphin_vision_director.cpython-313.pyc b/__pycache__/dolphin_vision_director.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aab0040bfc4651b10f664c5e579a7cbc713e2697 Binary files /dev/null and b/__pycache__/dolphin_vision_director.cpython-313.pyc differ diff --git a/__pycache__/lora_classifier.cpython-312.pyc b/__pycache__/lora_classifier.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4828425a49ad88c9831f897eca1eb0c182d1256a Binary files /dev/null and b/__pycache__/lora_classifier.cpython-312.pyc differ diff --git a/__pycache__/lora_profile.cpython-312.pyc b/__pycache__/lora_profile.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35125f0da62e9ecfa618e193e7ed902b3f35fe4d Binary files /dev/null and b/__pycache__/lora_profile.cpython-312.pyc differ diff --git a/__pycache__/lora_profile.cpython-313.pyc b/__pycache__/lora_profile.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a91927de950629a8544b4a40b3fa5113a61de5e Binary files /dev/null and b/__pycache__/lora_profile.cpython-313.pyc differ diff --git a/__pycache__/mask_region_color_match.cpython-313.pyc b/__pycache__/mask_region_color_match.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a45b9b87a2b0f2a99f2211c76309af6419cac5c1 Binary files /dev/null and b/__pycache__/mask_region_color_match.cpython-313.pyc differ diff --git a/__pycache__/match_size.cpython-313.pyc b/__pycache__/match_size.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5596aba2ef2daed791ed6e69d9ca03cfb959861 Binary files /dev/null and b/__pycache__/match_size.cpython-313.pyc differ diff --git a/__pycache__/orga.cpython-312.pyc b/__pycache__/orga.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af645e7dd6751da7620b21c2098f029c8390695b Binary files /dev/null and b/__pycache__/orga.cpython-312.pyc differ diff --git a/__pycache__/promp_logic_v28_stitch.cpython-312.pyc b/__pycache__/promp_logic_v28_stitch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7126b21d38ecc5fd9520afaffb580f6ecf71679 Binary files /dev/null and b/__pycache__/promp_logic_v28_stitch.cpython-312.pyc differ diff --git a/__pycache__/promp_logic_v28_stitch.cpython-313.pyc b/__pycache__/promp_logic_v28_stitch.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6686db9f77fe3f5c24e535e051a36cd005568595 Binary files /dev/null and b/__pycache__/promp_logic_v28_stitch.cpython-313.pyc differ diff --git a/__pycache__/promp_logic_v30_story.cpython-312.pyc b/__pycache__/promp_logic_v30_story.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c4fa1311375c349438618645dfda712b0916169 Binary files /dev/null and b/__pycache__/promp_logic_v30_story.cpython-312.pyc differ diff --git a/__pycache__/promp_logic_v30_story.cpython-313.pyc b/__pycache__/promp_logic_v30_story.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62516ade31c1b87049de168887a68c9400fa35d4 Binary files /dev/null and b/__pycache__/promp_logic_v30_story.cpython-313.pyc differ diff --git a/__pycache__/promp_logic_v32_story.cpython-312.pyc b/__pycache__/promp_logic_v32_story.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd6e0f54bc730db6cc34727d0d7c61497b07e7c3 Binary files /dev/null and b/__pycache__/promp_logic_v32_story.cpython-312.pyc differ diff --git a/__pycache__/promp_logic_v32_story.cpython-313.pyc b/__pycache__/promp_logic_v32_story.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a2afb9d6373ba57653d1ec0c7e792f9e1400d79 Binary files /dev/null and b/__pycache__/promp_logic_v32_story.cpython-313.pyc differ diff --git a/__pycache__/promp_logic_v32_vision.cpython-312.pyc b/__pycache__/promp_logic_v32_vision.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66d58d5447d466944ffb1d13e81d453856cc1b04 Binary files /dev/null and b/__pycache__/promp_logic_v32_vision.cpython-312.pyc differ diff --git a/__pycache__/promp_logic_v32_vision.cpython-313.pyc b/__pycache__/promp_logic_v32_vision.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e0017710b47fe0e996e6c373d95c1438252c6a6 Binary files /dev/null and b/__pycache__/promp_logic_v32_vision.cpython-313.pyc differ diff --git a/__pycache__/wan_h3_refine.cpython-313.pyc b/__pycache__/wan_h3_refine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f98bfa9db32a7e2a07dbe5627fe4e8436528821 Binary files /dev/null and b/__pycache__/wan_h3_refine.cpython-313.pyc differ diff --git a/__pycache__/wan_i2v_continuity.cpython-312.pyc b/__pycache__/wan_i2v_continuity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39451ce970fde1ecc9294f739d41d1b2a9facb3b Binary files /dev/null and b/__pycache__/wan_i2v_continuity.cpython-312.pyc differ diff --git a/__pycache__/wan_i2v_continuity.cpython-313.pyc b/__pycache__/wan_i2v_continuity.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e28afdf55a2bb4efeb6aa7dcee3fcfd8d3f2564 Binary files /dev/null and b/__pycache__/wan_i2v_continuity.cpython-313.pyc differ diff --git a/__pycache__/wan_resizer.cpython-312.pyc b/__pycache__/wan_resizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fedfb6776de74f9ba1842e609968fbd9c349157f Binary files /dev/null and b/__pycache__/wan_resizer.cpython-312.pyc differ diff --git a/__pycache__/wan_resizer.cpython-313.pyc b/__pycache__/wan_resizer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17292746cc3500285809bca1aa78c93b1814f9c3 Binary files /dev/null and b/__pycache__/wan_resizer.cpython-313.pyc differ diff --git a/__pycache__/wan_svipro_fixed.cpython-312.pyc b/__pycache__/wan_svipro_fixed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70634542ece8a9c442bc2e32f64f2be68796b47a Binary files /dev/null and b/__pycache__/wan_svipro_fixed.cpython-312.pyc differ diff --git a/__pycache__/wan_svipro_fixed.cpython-313.pyc b/__pycache__/wan_svipro_fixed.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b018b3893bba3f504a5f8dc72886b9aad2d32831 Binary files /dev/null and b/__pycache__/wan_svipro_fixed.cpython-313.pyc differ diff --git a/__pycache__/wan_svipro_gray.cpython-312.pyc b/__pycache__/wan_svipro_gray.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bd9a976255c60a162d4ca5196fdb0fca5ceebf6 Binary files /dev/null and b/__pycache__/wan_svipro_gray.cpython-312.pyc differ diff --git a/__pycache__/wan_svipro_gray.cpython-313.pyc b/__pycache__/wan_svipro_gray.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6596d71d4d5e5e8e56e9c1586e81ea6826d5da82 Binary files /dev/null and b/__pycache__/wan_svipro_gray.cpython-313.pyc differ diff --git a/__pycache__/wan_svipro_motion.cpython-313.pyc b/__pycache__/wan_svipro_motion.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d19f954f227dfa9afa27251b1c0dcce0deb01613 Binary files /dev/null and b/__pycache__/wan_svipro_motion.cpython-313.pyc differ diff --git a/cloud_env/comfyui_env_snapshot.json b/cloud_env/comfyui_env_snapshot.json new file mode 100644 index 0000000000000000000000000000000000000000..28b79025f3c51aa85042076f25e16f14429dc258 --- /dev/null +++ b/cloud_env/comfyui_env_snapshot.json @@ -0,0 +1,598 @@ +{ + "comfyui": "fe4195f7f4275f2626cbafc703acc3ddde1e5490", + "git_custom_nodes": { + "https://github.com/giriss/comfy-image-saver": { + "hash": "c6223965d45871d1ff2931b6ac9518a13947c920", + "disabled": false + }, + "https://github.com/M1kep/ComfyLiterals": { + "hash": "bdddb08ca82d90d75d97b1d437a652e0284a32ac", + "disabled": false + }, + "https://github.com/evanspearman/ComfyMath": { + "hash": "c01177221c31b8e5fbc062778fc8254aeb541638", + "disabled": false + }, + "https://github.com/yolain/ComfyUI-Easy-Use": { + "hash": "595e0738a9e3f8d0d9c4d875461b2d2c9e7559c7", + "disabled": false + }, + "https://github.com/NikoDemon80/ComfyUI-H3-Motion-Context.git": { + "hash": "15fc6a7bf7b78efb27f33d7eef3818e7ed0e118a", + "disabled": false + }, + "https://github.com/facok/comfyui-krea2-controlnet": { + "hash": "79ebfd3bd80d2180b334dd7ce57f3c9ddaa0848f", + "disabled": false + }, + "https://github.com/ltdrdata/ComfyUI-Manager": { + "hash": "8c5e640f83a26f22b0c7f0e63e89f7bab7fa6084", + "disabled": false + }, + "https://github.com/princepainter/Comfyui-PainterFLF2V": { + "hash": "c81a68feceb90b28557a979d6de44eac59c89123", + "disabled": false + }, + "https://github.com/princepainter/ComfyUI-PainterI2V": { + "hash": "c46e480c35d9a54e70139e354b1fc2975d0b6805", + "disabled": false + }, + "https://github.com/princepainter/ComfyUI-PainterI2Vadvanced": { + "hash": "a044ac7b2a33565343ddc75283c94a41371b4352", + "disabled": false + }, + "https://github.com/princepainter/ComfyUI-PainterI2VforKJ": { + "hash": "56f6e8a849d24ede99f3000d2b0fa2c0bc733bd8", + "disabled": false + }, + "https://github.com/Saganaki22/ComfyUI-sol-attn.git": { + "hash": "bd28909e6a152ba5db4e3590d2d9df1c249943f2", + "disabled": false + }, + "https://github.com/un-seen/comfyui-tensorops": { + "hash": "d34488e3079ecd10db2fe867c3a7af568115faed", + "disabled": false + }, + "https://github.com/jamesWalker55/comfyui-various": { + "hash": "5bd85aaf7616878471469c4ec7e11bbd0cef3bf2", + "disabled": false + }, + "https://github.com/GACLove/ComfyUI-VFI": { + "hash": "6176a430f12cd16003f4664c1e3c6af8e96cc3c6", + "disabled": false + }, + "https://github.com/kijai/ComfyUI-WanVideoWrapper": { + "hash": "088128b224242e110d3906c6750e9a3a348a659b", + "disabled": false + }, + "https://github.com/TripleHeadedMonkey/ComfyUI-Zlycoris": { + "hash": "cc8ecb9952940aa642e71d9b123b2d0a768e84d8", + "disabled": false + }, + "https://github.com/bjoo46/dolphin_nodes.git": { + "hash": "d1dee42586cc0f2bf0ef986c92d836052629c02a", + "disabled": false + }, + "https://github.com/ClownsharkBatwing/RES4LYF": { + "hash": "26036f647ca15d3048a193daf99a40cecfc3820d", + "disabled": false + }, + "https://github.com/Ltamann/ComfyUI-TBG-ETUR": { + "hash": "39e3a5f1225a817eea634d0c7ebbd7635cccf588", + "disabled": false + } + }, + "cnr_custom_nodes": { + "10s-comfy-nodes": "1.9.7", + "cg-use-everywhere": "7.8.0", + "comfyui-af-find-nodes": "0.3.2", + "ComfyUI-Conditioning-Rebalance": "2.0.2", + "ComfyUI-Crystools": "1.27.4", + "comfyui-custom-scripts": "1.2.5", + "comfyui-easy-sam3": "1.0.4", + "comfyui-florence2": "1.1.0", + "ComfyUI-GGUF": "1.1.10", + "comfyui-image-saver": "1.24.0", + "comfyui-impact-pack": "8.28.3", + "comfyui-impact-subpack": "1.3.5", + "comfyui-inpaint-nodes": "1.4.3", + "comfyui-inpainteasy": "1.0.2", + "comfyui-jakeupgrade": "2.7.5", + "ComfyUI-JoyCaption": "2.0.2", + "comfyui-kjnodes": "1.5.0", + "comfyui-krea2-conditioning": "1.0.2", + "comfyui-krea2edit": "1.2.3", + "ComfyUI-Krea2T-Enhancer": "1.1.2", + "comfyui-lora-manager": "1.2.0", + "comfyui-minimax-h3-turbo": "1.2.3", + "comfyui-mmaudio": "1.0.3", + "comfyui-mxtoolkit": "0.9.92", + "ComfyUI-nhknodes": "1.1.0", + "comfyui-promptchain": "0.1.13", + "comfyui-rmbg": "3.1.0", + "comfyui-segment-anything-2": "1.0.2", + "comfyui-videohelpersuite": "1.7.9", + "comfyui-vrgamedevgirl": "9.1.1", + "comfyui-wd14-tagger": "1.0.1", + "ComfyUI-Woosh": "0.2.7", + "comfyui_controlnet_aux": "1.1.5", + "comfyui_essentials": "1.1.0", + "comfyui_layerstyle": "2.0.40", + "comfyui_memory_cleanup": "1.1.3", + "comfyui_nvidia_rtx_nodes": "0.1.3", + "comfyui_queue_manager": "0.0.17", + "comfyui_tinyterranodes": "2.0.11", + "controlaltai-nodes": "1.1.4", + "erosdiffusion-eulerflowmatchingdiscretescheduler": "1.0.8", + "LanPaint": "1.5.5", + "rgthree-comfy": "1.0.2607232129", + "seedvr2_videoupscaler": "2.5.24", + "stablediffusion-dpmpp_2m_alt-sampler": "1.0.0", + "tripleksampler": "0.10.5" + }, + "file_custom_nodes": [ + { + "filename": "websocket_image_save.py", + "disabled": false + } + ], + "pips": { + "absl-py==2.3.1": "", + "accelerate==1.12.0": "", + "addict==2.4.0": "", + "ai-edge-litert==2.1.4": "", + "aiofiles==24.1.0": "", + "aiohappyeyeballs": "file:///D:/a/ComfyUI/cu130_python_deps/aiohappyeyeballs-2.6.1-py3-none-any.whl#sha256=f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", + "aiohttp==3.13.3": "", + "aiohttp_socks==0.11.0": "", + "aiosignal": "file:///D:/a/ComfyUI/cu130_python_deps/aiosignal-1.4.0-py3-none-any.whl#sha256=053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "aiosqlite==0.22.1": "", + "albucore==0.0.24": "", + "albumentations==2.0.8": "", + "alembic": "file:///D:/a/ComfyUI/cu130_python_deps/alembic-1.17.2-py3-none-any.whl#sha256=f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", + "aliyun-python-sdk-core==2.16.0": "", + "aliyun-python-sdk-kms==2.16.5": "", + "annotated-doc==0.0.4": "", + "annotated-types": "file:///D:/a/ComfyUI/cu130_python_deps/annotated_types-0.7.0-py3-none-any.whl#sha256=1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "antlr4-python3-runtime==4.9.3": "", + "anyascii==0.3.3": "", + "anyio==4.12.1": "", + "argbind==0.3.9": "", + "arrow==1.4.0": "", + "asgiref==3.11.1": "", + "attrs": "file:///D:/a/ComfyUI/cu130_python_deps/attrs-25.4.0-py3-none-any.whl#sha256=adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", + "audioop-lts==0.2.2": "", + "audioread==3.1.0": "", + "av==17.0.1": "", + "backoff==2.2.1": "", + "backports.strenum==1.2.8": "", + "backports.zstd==1.3.0": "", + "beautifulsoup4==4.14.3": "", + "binaryornot==0.6.0": "", + "bitsandbytes==0.49.1": "", + "blake3==1.0.8": "", + "blend_modes==2.2.0": "", + "blind-watermark==0.4.4": "", + "boto3==1.43.40": "", + "botocore==1.43.40": "", + "braceexpand==0.1.7": "", + "brotli==1.2.0": "", + "cachetools==6.2.4": "", + "certifi": "file:///D:/a/ComfyUI/cu130_python_deps/certifi-2025.11.12-py3-none-any.whl#sha256=97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", + "cffi": "file:///D:/a/ComfyUI/cu130_python_deps/cffi-2.0.0-cp313-cp313-win_amd64.whl#sha256=19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", + "cfgv==3.5.0": "", + "chardet==5.2.0": "", + "charset-normalizer": "file:///D:/a/ComfyUI/cu130_python_deps/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl#sha256=b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "click==8.4.1": "", + "clip-interrogator==0.6.0": "", + "cloudpickle==3.1.2": "", + "cloudscraper==1.2.71": "", + "cmake==4.2.1": "", + "color-matcher==0.6.0": "", + "colorama": "file:///D:/a/ComfyUI/cu130_python_deps/colorama-0.4.6-py2.py3-none-any.whl#sha256=4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", + "coloredlogs==15.0.1": "", + "colorlog==6.10.1": "", + "colour-science==0.4.7": "", + "comfy-3d-viewers==0.2.44": "", + "comfy-aimdo==0.4.13": "", + "comfy-angle==0.1.0": "", + "comfy-cli==1.10.5": "", + "comfy-env==0.3.89": "", + "comfy-kitchen==0.2.28": "", + "comfy-test==0.0.70": "", + "comfyui-embedded-docs==0.5.9": "", + "comfyui-workflow-templates-core==0.3.302": "", + "comfyui-workflow-templates-json==0.1.37": "", + "comfyui-workflow-templates-media-api==0.3.84": "", + "comfyui-workflow-templates-media-assets-01==0.1.24": "", + "comfyui-workflow-templates-media-image==0.3.160": "", + "comfyui-workflow-templates-media-other==0.3.229": "", + "comfyui-workflow-templates-media-video==0.3.101": "", + "comfyui_frontend_package==1.48.7": "", + "comfyui_workflow_templates==0.11.37": "", + "configs==3.0.3": "", + "contourpy==1.3.3": "", + "contractions==0.1.73": "", + "cookiecutter==2.7.1": "", + "crcmod==1.7": "", + "cryptography==46.0.3": "", + "cstr": "git+https://github.com/ltdrdata/cstr@0520c29a18a7a869a6e5983861d6f7a4c86f8e9b", + "cycler==0.12.1": "", + "datasets==3.6.0": "", + "dds-cloudapi-sdk==0.5.3": "", + "ddt==1.7.2": "", + "decorator==5.2.1": "", + "decord==0.6.0": "", + "deepdiff==8.6.1": "", + "defusedxml==0.7.1": "", + "demucs==4.0.1": "", + "diffusers==0.36.0": "", + "dill==0.3.8": "", + "distlib==0.4.0": "", + "distro==1.9.0": "", + "docstring_parser==0.18.0": "", + "docutils==0.22.4": "", + "dora_search==0.1.12": "", + "dotenv==0.9.9": "", + "easydict==1.13": "", + "einops": "file:///D:/a/ComfyUI/cu130_python_deps/einops-0.8.1-py3-none-any.whl#sha256=919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", + "evalidate==2.1.4": "", + "exejs==0.0.7": "", + "facexlib==0.3.0": "", + "fairscale==0.4.13": "", + "fal_client==0.13.1": "", + "fastapi==0.138.0": "", + "ffmpeg-python==0.2.0": "", + "ffmpy": "git+https://github.com/ltdrdata/ffmpy.git@f000737698b387ffaeab7cd871b0e9185811230d", + "filelock==3.25.2": "", + "filterpy==1.4.5": "", + "flatbuffers==25.12.19": "", + "fonttools==4.61.1": "", + "frozenlist": "file:///D:/a/ComfyUI/cu130_python_deps/frozenlist-1.8.0-cp313-cp313-win_amd64.whl#sha256=878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", + "fsspec==2025.3.0": "", + "ftfy==6.1.1": "", + "funasr==1.4.1": "", + "future==1.0.0": "", + "fvcore==0.1.5.post20221221": "", + "gdown==5.2.0": "", + "gguf==0.17.1": "", + "gitdb==4.0.12": "", + "GitPython==3.1.46": "", + "glcontext==3.0.0": "", + "glfw==2.10.0": "", + "google-ai-generativelanguage==0.6.15": "", + "google-api-core==2.29.0": "", + "google-api-python-client==2.188.0": "", + "google-auth==2.47.0": "", + "google-auth-httplib2==0.3.0": "", + "google-genai==1.59.0": "", + "google-generativeai==0.8.6": "", + "googleapis-common-protos==1.72.0": "", + "gradio==6.19.0": "", + "gradio_client==2.5.0": "", + "greenlet": "file:///D:/a/ComfyUI/cu130_python_deps/greenlet-3.2.4-cp313-cp313-win_amd64.whl#sha256=554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", + "grog==0.93": "", + "groovy==0.1.2": "", + "groundingdino-py==0.4.0": "", + "grpcio==1.76.0": "", + "grpcio-status==1.71.2": "", + "h11==0.16.0": "", + "h2==4.3.0": "", + "hear21passt==0.0.26": "", + "hf-gradio==0.4.1": "", + "hf-xet==1.5.1": "", + "hf_transfer==0.1.9": "", + "hpack==4.1.0": "", + "httpcore==1.0.9": "", + "httplib2==0.31.1": "", + "httpx==0.28.1": "", + "httpx-sse==0.4.3": "", + "huggingface_hub==1.20.1": "", + "humanfriendly==10.0": "", + "hydra-core==1.3.2": "", + "hyperframe==6.1.0": "", + "identify==2.6.18": "", + "idna": "file:///D:/a/ComfyUI/cu130_python_deps/idna-3.11-py3-none-any.whl#sha256=771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "ImageIO==2.37.2": "", + "imageio-ffmpeg==0.6.0": "", + "img2texture": "git+https://github.com/ltdrdata/img2texture.git@d6159abea44a0b2cf77454d3d46962c8b21eb9d3", + "importlib_metadata==8.7.1": "", + "inflate64==1.0.4": "", + "inflect==7.5.0": "", + "iniconfig==2.3.0": "", + "insightface==1.0.1": "", + "iopath==0.1.10": "", + "jaconv==0.5.0": "", + "jamo==0.4.1": "", + "jh2==5.0.10": "", + "jieba==0.42.1": "", + "Jinja2": "file:///D:/a/ComfyUI/cu130_python_deps/jinja2-3.1.6-py3-none-any.whl#sha256=85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", + "jiter==0.12.0": "", + "jmespath==0.10.0": "", + "joblib==1.5.3": "", + "json-logic==0.7.0a0": "", + "jsonschema==4.25.1": "", + "jsonschema-specifications==2025.9.1": "", + "julius==0.2.7": "", + "kaldifst==1.8.0": "", + "kaldiio==2.18.1": "", + "kiwisolver==1.4.9": "", + "kornia": "file:///D:/a/ComfyUI/cu130_python_deps/kornia-0.8.2-py2.py3-none-any.whl#sha256=32dfe77c9c74a87a2de49395aa3c2c376a1b63c27611a298b394d02d13905819", + "kornia_rs": "file:///D:/a/ComfyUI/cu130_python_deps/kornia_rs-0.1.10-cp313-cp313-win_amd64.whl#sha256=c57d157bebe64c22e2e44c72455b1c7365eee4d767e0c187dc28f22d072ebaf7", + "lameenc==1.8.2": "", + "lark==1.3.1": "", + "lazy_loader==0.4": "", + "librosa==0.11.0": "", + "lightning-utilities==0.15.3": "", + "llvmlite==0.46.0": "", + "lmdb==2.2.0": "", + "loguru==0.7.3": "", + "lxml==6.0.2": "", + "lycoris==0.0.0": "", + "Mako": "file:///D:/a/ComfyUI/cu130_python_deps/mako-1.3.10-py3-none-any.whl#sha256=baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", + "manifold3d==3.3.2": "", + "mapbox_earcut==2.0.0": "", + "markdown-it-py==4.0.0": "", + "MarkupSafe": "file:///D:/a/ComfyUI/cu130_python_deps/markupsafe-3.0.3-cp313-cp313-win_amd64.whl#sha256=9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", + "matplotlib==3.10.8": "", + "matrix-nio==0.25.2": "", + "mdurl==0.1.2": "", + "mediapipe==0.10.31": "", + "meson==1.10.0": "", + "meson-python==0.19.0": "", + "mixpanel==5.1.0": "", + "ml_dtypes==0.5.4": "", + "mmcv": "https://archive1.piwheels.org/simple/mmcv/mmcv-2.1.0-py2.py3-none-any.whl#sha256=3b4e4836676290d1f15f0a36b770572e0463024e269c451e48a3686c63c211fb", + "mmdet==3.3.0": "", + "mmengine==0.10.7": "", + "modelscope==1.39.1": "", + "modelscope-hub==0.2.0": "", + "moderngl==5.12.0": "", + "more-itertools==11.0.2": "", + "mpmath": "file:///D:/a/ComfyUI/cu130_python_deps/mpmath-1.3.0-py3-none-any.whl#sha256=a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", + "msgpack==1.1.2": "", + "mss==10.1.0": "", + "multidict": "file:///D:/a/ComfyUI/cu130_python_deps/multidict-6.7.0-cp313-cp313-win_amd64.whl#sha256=30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", + "multiprocess==0.70.16": "", + "multivolumefile==0.2.3": "", + "munkres==1.1.4": "", + "natsort==8.4.0": "", + "networkx": "file:///D:/a/ComfyUI/cu130_python_deps/networkx-3.5-py3-none-any.whl#sha256=0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", + "ninja==1.13.0": "", + "niquests==3.16.1": "", + "nodeenv==1.10.0": "", + "numba==0.63.1": "", + "numpy==2.3.5": "", + "nvidia-ml-py==13.590.44": "", + "olefile==0.47": "", + "ollama==0.6.1": "", + "omegaconf==2.3.0": "", + "omnivoice==0.1.5": "", + "onnx==1.20.1": "", + "onnxruntime==1.23.2": "", + "onnxruntime-gpu==1.23.2": "", + "open_clip_torch==3.2.0": "", + "openai==2.15.0": "", + "openai-whisper==20250625": "", + "opencv-contrib-python==4.13.0.92": "", + "opencv-python==4.10.0.84": "", + "opencv-python-headless==4.13.0.92": "", + "opensimplex==0.4.5.1": "", + "openunmix==1.3.0": "", + "optimum==2.1.0": "", + "ordered-set==4.1.0": "", + "orderly-set==5.5.0": "", + "orjson==3.11.9": "", + "oss2==2.19.1": "", + "packaging": "file:///D:/a/ComfyUI/cu130_python_deps/packaging-25.0-py3-none-any.whl#sha256=29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "pandas==2.3.3": "", + "pathos==0.3.4": "", + "pathspec==1.1.1": "", + "pedalboard==0.9.23": "", + "peft==0.18.0": "", + "pi_heif==1.4.0": "", + "piexif==1.1.3": "", + "pilgram==2.0.0": "", + "pillow==12.1.0": "", + "pixeloe==0.1.4": "", + "platformdirs==4.5.1": "", + "pluggy==1.6.0": "", + "polars==1.37.1": "", + "polars-runtime-32==1.37.1": "", + "pooch==1.8.2": "", + "portalocker==3.2.0": "", + "posthog==7.19.2": "", + "pox==0.3.6": "", + "ppft==1.7.7": "", + "pre_commit==4.5.1": "", + "prompt_toolkit==3.0.52": "", + "propcache": "file:///D:/a/ComfyUI/cu130_python_deps/propcache-0.4.1-cp313-cp313-win_amd64.whl#sha256=381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", + "proto-plus==1.27.0": "", + "protobuf==5.29.5": "", + "psd-tools==1.12.1": "", + "psutil==7.2.1": "", + "py-cpuinfo==9.0.0": "", + "py7zr==1.1.2": "", + "pyahocorasick==2.3.1": "", + "pyarrow==25.0.0": "", + "pyasn1==0.6.2": "", + "pyasn1_modules==0.4.2": "", + "pybcj==1.0.7": "", + "pycocoevalcap==1.2": "", + "pycocotools==2.0.11": "", + "pycollada==0.9.2": "", + "pycparser": "file:///D:/a/ComfyUI/cu130_python_deps/pycparser-2.23-py3-none-any.whl#sha256=e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", + "pycryptodome==3.23.0": "", + "pycryptodomex==3.23.0": "", + "pydantic==2.12.5": "", + "pydantic-settings": "file:///D:/a/ComfyUI/cu130_python_deps/pydantic_settings-2.12.0-py3-none-any.whl#sha256=fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", + "pydantic_core": "file:///D:/a/ComfyUI/cu130_python_deps/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl#sha256=79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", + "pyDeprecate==0.5.0": "", + "pydub==0.25.1": "", + "pygit2==1.19.1": "", + "PyGithub==2.8.1": "", + "Pygments==2.19.2": "", + "PyJWT==2.8.0": "", + "pyloudnorm==0.2.0": "", + "PyMatting==1.1.14": "", + "PyNaCl==1.6.2": "", + "pynndescent==0.6.0": "", + "pynvml==13.0.1": "", + "PyOpenGL==3.1.10": "", + "pyparsing==3.3.1": "", + "pyppmd==1.3.1": "", + "pyproject-metadata==0.10.0": "", + "pyreadline3==3.5.4": "", + "PySocks==1.7.1": "", + "pytest==9.0.2": "", + "pytest-mock==3.15.1": "", + "python-dateutil==2.9.0.post0": "", + "python-discovery==1.2.2": "", + "python-dotenv": "file:///D:/a/ComfyUI/cu130_python_deps/python_dotenv-1.2.1-py3-none-any.whl#sha256=b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", + "python-multipart==0.0.32": "", + "python-slugify==8.0.4": "", + "python-socks==2.8.0": "", + "pytorch-lightning==2.6.5": "", + "pytz==2025.2": "", + "PyWavelets==1.9.0": "", + "pywin32==311": "", + "PyYAML": "file:///D:/a/ComfyUI/cu130_python_deps/pyyaml-6.0.3-cp313-cp313-win_amd64.whl#sha256=79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", + "pyzbar==0.1.9": "", + "qh3==1.5.6": "", + "qrcode==8.2": "", + "questionary==2.1.1": "", + "qwen-vl-utils==0.0.14": "", + "RapidFuzz==3.14.5": "", + "redis==7.3.0": "", + "referencing==0.37.0": "", + "regex": "file:///D:/a/ComfyUI/cu130_python_deps/regex-2025.11.3-cp313-cp313-win_amd64.whl#sha256=bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", + "rembg==2.0.72": "", + "replicate==1.0.7": "", + "requests": "file:///D:/a/ComfyUI/cu130_python_deps/requests-2.32.5-py3-none-any.whl#sha256=2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "requests-toolbelt==1.0.0": "", + "retrying==1.4.2": "", + "rich==14.2.0": "", + "rotary-embedding-torch==0.8.9": "", + "rpds-py==0.30.0": "", + "rsa==4.9.1": "", + "rtree==1.4.1": "", + "ruff==0.15.18": "", + "s3transfer==0.19.0": "", + "safehttpx==0.1.7": "", + "safetensors==0.7.0": "", + "sageattention": "https://github.com/woct0rdho/SageAttention/releases/download/v2.2.0-windows.post3/sageattention-2.2.0+cu130torch2.9.0.post3-cp39-abi3-win_amd64.whl#sha256=5d6b5799379e3ae66ffd9acd5dcce4a7a17f4e9114a9c24d4e09b69aead20068", + "SAM-2": "git+https://github.com/facebookresearch/sam2@2b90b9f5ceec907a1c18123530e92e794ad901a4", + "scikit-image==0.26.0": "", + "scikit-learn==1.8.0": "", + "scipy": "file:///D:/a/ComfyUI/cu130_python_deps/scipy-1.16.3-cp313-cp313-win_amd64.whl#sha256=062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", + "segment-anything==1.0": "", + "segmentation_models_pytorch==0.5.0": "", + "semantic-version==2.10.0": "", + "semver==3.0.4": "", + "sentencepiece": "file:///D:/a/ComfyUI/cu130_python_deps/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl#sha256=10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", + "sentry-sdk==2.55.0": "", + "setuptools": "file:///D:/a/ComfyUI/cu130_python_deps/setuptools-80.9.0-py3-none-any.whl#sha256=062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", + "shapely==2.1.2": "", + "shellingham==1.5.4": "", + "shtab==1.8.0": "", + "simpleeval==1.0.3": "", + "simplejson==4.1.1": "", + "simsimd==6.5.12": "", + "six==1.17.0": "", + "smmap==5.0.2": "", + "sniffio==1.3.1": "", + "sortedcontainers==2.4.0": "", + "sounddevice==0.5.3": "", + "soundfile==0.13.1": "", + "soupsieve==2.8.1": "", + "soxr==1.0.0": "", + "spaces==0.51.1": "", + "spandrel": "file:///D:/a/ComfyUI/cu130_python_deps/spandrel-0.4.1-py3-none-any.whl#sha256=49a39aa979769749a42203428355bc4840452854d6334ce0d465af46098dd448", + "SQLAlchemy==2.0.45": "", + "stable-ts==2.19.1": "", + "standard-aifc==3.13.0": "", + "standard-chunk==3.13.0": "", + "standard-sunau==3.13.0": "", + "starlette==1.3.1": "", + "stringzilla==4.6.0": "", + "submitit==1.5.4": "", + "supervision==0.6.0": "", + "surrealist==2.0.2": "", + "svg.path==7.0": "", + "sympy": "file:///D:/a/ComfyUI/cu130_python_deps/sympy-1.14.0-py3-none-any.whl#sha256=e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", + "tabulate==0.9.0": "", + "taichi==1.7.4": "", + "tenacity==9.1.2": "", + "tensorboardX==2.6.5": "", + "termcolor==3.3.0": "", + "terminaltables==3.1.10": "", + "text-unidecode==1.3": "", + "textsearch==0.0.24": "", + "texttable==1.7.0": "", + "threadpoolctl==3.6.0": "", + "tifffile==2025.12.20": "", + "tiktoken==0.12.0": "", + "timm==1.0.27": "", + "tokenizers==0.22.2": "", + "toml==0.10.2": "", + "tomli==2.4.1": "", + "tomli_w==1.2.0": "", + "tomlkit==0.14.0": "", + "torch": "file:///D:/a/ComfyUI/cu130_python_deps/torch-2.9.1%2Bcu130-cp313-cp313-win_amd64.whl#sha256=47cae56331fdec7f7082aad967b25868cc5f94b203fc273e40346a852aa009dd", + "torch-complex==0.4.4": "", + "torchaudio": "file:///D:/a/ComfyUI/cu130_python_deps/torchaudio-2.9.1%2Bcu130-cp313-cp313-win_amd64.whl#sha256=2e90a149a17a41676f5a2e4e95c3a46baf3a9389af205894c23b056c8ad06daf", + "torchcodec==0.9.1": "", + "torchdiffeq==0.2.5": "", + "torchlanc==1.1.1": "", + "torchmetrics==1.9.0": "", + "torchscale==0.3.0": "", + "torchsde": "file:///D:/a/ComfyUI/cu130_python_deps/torchsde-0.2.6-py3-none-any.whl#sha256=19bf7ff02eec7e8e46ba1cdb4aa0f9db1c51d492524a16975234b467f7fc463b", + "torchvision": "file:///D:/a/ComfyUI/cu130_python_deps/torchvision-0.24.1%2Bcu130-cp313-cp313-win_amd64.whl#sha256=d688426d623b8ba763087726faf5b34b1fe404f44741656978d855403dbc7444", + "tqdm": "file:///D:/a/ComfyUI/cu130_python_deps/tqdm-4.67.1-py3-none-any.whl#sha256=26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", + "trampoline": "file:///D:/a/ComfyUI/cu130_python_deps/trampoline-0.1.2-py3-none-any.whl#sha256=36cc9a4ff9811843d177fc0e0740efbd7da39eadfe6e50c9e2937cbc06d899d9", + "transformers==5.12.1": "", + "translators==6.0.2": "", + "transparent-background==1.3.4": "", + "treetable==0.2.6": "", + "trimesh==4.11.0": "", + "triton-windows==3.5.1.post24": "", + "typeguard==4.6.0": "", + "typer==0.21.1": "", + "typer-config==1.4.3": "", + "typing-inspection": "file:///D:/a/ComfyUI/cu130_python_deps/typing_inspection-0.4.2-py3-none-any.whl#sha256=4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", + "typing_extensions": "file:///D:/a/ComfyUI/cu130_python_deps/typing_extensions-4.15.0-py3-none-any.whl#sha256=f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", + "tyro==0.8.5": "", + "tzdata==2025.3": "", + "ultralytics==8.4.5": "", + "ultralytics-thop==2.0.18": "", + "umap-learn==0.5.12": "", + "unpaddedbase64==2.1.0": "", + "uritemplate==4.2.0": "", + "urllib3": "file:///D:/a/ComfyUI/cu130_python_deps/urllib3-2.5.0-py3-none-any.whl#sha256=e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", + "urllib3-future==2.15.901": "", + "uv==0.9.22": "", + "uvicorn==0.49.0": "", + "vhacdx==0.0.10": "", + "virtualenv==21.2.1": "", + "voxcpm==2.0.3": "", + "wandb==0.27.2": "", + "wassima==2.0.4": "", + "wcwidth==0.2.14": "", + "webdataset==1.0.2": "", + "websocket-client==1.9.0": "", + "websockets==15.0.1": "", + "wetext==0.1.6": "", + "wget==3.2": "", + "wheel==0.45.1": "", + "win32_setctime==1.2.0": "", + "word2number==1.1": "", + "wordcloud==1.9.6": "", + "xxhash==3.6.0": "", + "yacs==0.1.8": "", + "yapf==0.43.0": "", + "yarl": "file:///D:/a/ComfyUI/cu130_python_deps/yarl-1.22.0-cp313-cp313-win_amd64.whl#sha256=47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", + "yt-dlp==2026.3.13": "", + "zhipuai==2.1.5.20250825": "", + "zipp==3.23.0": "" + } +} \ No newline at end of file diff --git a/dataset_builder/README.md b/dataset_builder/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4788f9a718e41e3d3c26d040bb6a34089708641f --- /dev/null +++ b/dataset_builder/README.md @@ -0,0 +1,84 @@ +# Wan 2.2 LoRA Dataset Builder + +임의의 이미지/영상 폴더를 넣으면 피사체 중심으로 크롭하고 로컬 VLM으로 캡션을 달아, +LoRA 학습 스크립트(musubi-tuner 등)에 바로 넣을 수 있는 `images/`, `videos/`, +`.txt` 캡션, `dataset.toml` 초안을 생성하는 독립 CLI 도구. + +ComfyUI 노드가 아님 — `__init__.py`에서 import하지 않으므로 ComfyUI 노드 로딩에 +관여하지 않고, `python_embeded\python.exe`로 직접 실행한다. + +**사용 전제: 학습 대상 인물/캐릭터가 명백히 성인이어야 함.** 나이가 불명확하거나 +앳돼 보이는 인물의 성적 콘텐츠 데이터셋 구축에는 이 도구를 쓰지 말 것. + +## 사용법 + +``` +D:\SD\ComfyUI_windows_portable\python_embeded\python.exe ^ + dataset_builder\build.py ^ + --input "D:\raw_media" ^ + --output "D:\wan_lora_dataset" ^ + --trigger "ohwx man" ^ + --captioner joycaption +``` + +`--input` 폴더를 재귀적으로 스캔해 이미지(jpg/png/webp/bmp)와 영상(mp4/mov/mkv/avi/webm)을 +자동 분류하여 처리한다. + +## 파이프라인 + +1. **피사체 감지**: mediapipe 얼굴 검출 → (실패 시) OpenCV Haar cascade 얼굴 검출 → + (그래도 실패 시) ultralytics YOLOv8n 사람 검출 → (그래도 없으면) 전체 프레임 + 그대로 사용. 사람이 아닌 사물/장면 LoRA는 자연스럽게 "전체 프레임" 경로로 빠진다. +2. **버킷 매칭 + 크롭**: 감지된 bbox를 얼굴 2.2배 / 사람 1.3배로 패딩한 뒤, Wan + 480p/720p/1080p × (가로/세로/정사각) 버킷 중 종횡비가 가장 가까운 것을 골라 + 그 해상도로 크롭+리사이즈(16 배수 정렬). 원본 해상도보다 커지는(업스케일) 버킷은 + 자동으로 건너뛴다 — `--max-tier`로 상한만 지정. +3. **영상 씬 분할**: OpenCV 히스토그램 차분으로 컷 지점을 찾고, `--clip-min-sec` + ~ `--clip-max-sec` 범위로 클립을 나눈다. 클립마다 대표 프레임 몇 장의 bbox + 중앙값으로 크롭을 고정하고(프레임 흔들림 방지), `--video-fps`로 리샘플, 4n+1 + 프레임 수로 트리밍, optical flow 평균 크기로 "static/subtle/moderate/fast + motion" 태그를 캡션에 덧붙인다. +4. **캡셔닝** (`--captioner`): + - `joycaption` (기본) — `fancyfeast/llama-joycaption-beta-one-hf-llava`. + **주의**: HuggingFace 모델 카드에 "학습용 캡션" 전용 프롬프트 문자열이 공식 + 문서화되어 있지 않아, 여기 쓰인 지시문(`caption.py`의 `DEFAULT_PROMPT`)은 + 자체 작성한 기본값이다. 원하는 스타일이 있으면 `--caption-prompt`로 바꿔라. + - `florence2` — `microsoft/Florence-2-large`, 가볍고 빠름(~0.77B). + - `wd14` — `SmilingWolf/wd-vit-tagger-v3` (onnx), booru 태그 스타일. + - 공통: `--trigger`로 넘긴 트리거 워드를 캡션 맨 앞에 자동 삽입. + +## 출력 + +``` +output_dir/ + images/0001.jpg, 0001.txt, ... + videos/0001.mp4, 0001.txt, ... + dataset.toml # musubi-tuner 스타일 초안 — 실제 트레이너 버전 문서와 대조 후 사용 +``` + +## 모델 다운로드 / VRAM 참고 + +- 최초 실행 시 HuggingFace에서 캡셔너 가중치(JoyCaption ~16GB, Florence-2-large + ~1.5GB, WD14 ~수백MB)와 ultralytics `yolov8n.pt`(~6MB)를 자동 다운로드한다. + 네트워크/디스크 용량 확인 후 실행할 것. +- RTX 5090 32GB 기준, ComfyUI가 이미 VRAM을 점유 중이면 JoyCaption(8B, bf16 기준 + ~16GB)이 부족할 수 있다. 이 경우 `--load-in-4bit`(bitsandbytes)로 줄이거나, + ComfyUI 서버를 잠시 내려두고 실행할 것. + +## 알려진 환경 특이사항 + +- 이 embedded Python 환경은 `import torch`만 해도 인터프리터 종료 시 비정상 + 종료 코드(예: `-1073740791`)를 낸다. `dataset_builder`와 무관한 기존 환경 + 이슈이며, 실제 작업(파일 저장)은 그 전에 이미 끝난 뒤 발생하는 종료 시점 + 크래시라 결과물에는 영향 없음. 배치 스크립트에서 종료 코드로 성공/실패를 + 판단하지 말고 콘솔 로그의 `Done.` 줄로 확인할 것. + +## 검증된 것 / 안 된 것 + +- `crop_resize.py`(버킷 매칭 수학), `detect.py`(다단계 폴백), `scene_split.py` + (씬 컷 + 클립 추출 + 4n+1 트리밍 + 모션 태그)는 실제 샘플 이미지/영상으로 + 스모크 테스트 완료. +- `caption.py`(JoyCaption/Florence-2/WD14 실제 추론 호출)는 모델 가중치 다운로드가 + 커서 이번 세션에서 end-to-end로 돌려보지 않았다. 클래스/전처리는 각 모델의 + 공식 문서(HF 모델 카드, wd-tagger Space 소스)를 대조해 작성했지만, 처음 + 실행할 때 캡셔너 쪽 에러가 나면 그 부분부터 확인할 것. diff --git a/dataset_builder/__pycache__/build.cpython-313.pyc b/dataset_builder/__pycache__/build.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fb341dc1b1fc7e644f6ad0041ed0a2f713f67e2 Binary files /dev/null and b/dataset_builder/__pycache__/build.cpython-313.pyc differ diff --git a/dataset_builder/__pycache__/crop_resize.cpython-313.pyc b/dataset_builder/__pycache__/crop_resize.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53c9f2ad9ceb0830809323cfa3528b677430528f Binary files /dev/null and b/dataset_builder/__pycache__/crop_resize.cpython-313.pyc differ diff --git a/dataset_builder/build.py b/dataset_builder/build.py new file mode 100644 index 0000000000000000000000000000000000000000..14df6ed41dfce3954b9b22b85f6bd3447e1e46e5 --- /dev/null +++ b/dataset_builder/build.py @@ -0,0 +1,262 @@ +"""Wan 2.2 LoRA dataset builder. + +Feed it a folder of random images/videos; it detects the main subject, crops +to a Wan resolution bucket, captions locally, and writes an +images/ + videos/ + .txt caption dataset plus a starter dataset.toml. + +Usage: + python_embeded\\python.exe dataset_builder\\build.py --input ./raw --output ./dataset_out --trigger "ohwx man" + +Run from anywhere -- this script adds its own directory to sys.path so the +sibling modules (crop_resize, detect, caption, scene_split) import cleanly +whether invoked directly or via -m. +""" +import argparse +import os +import sys +import traceback + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import cv2 +import numpy as np +from PIL import Image + +import crop_resize +import scene_split +from caption import build_captioner, with_trigger +from detect import detect_subject_bbox, subdivide_bbox + +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} +VIDEO_EXTS = {".mp4", ".mov", ".mkv", ".avi", ".webm"} + + +def parse_args(): + p = argparse.ArgumentParser(description="Wan 2.2 LoRA dataset auto-builder") + p.add_argument("--input", required=True, help="Folder of raw images/videos (scanned recursively)") + p.add_argument("--output", required=True, help="Output dataset folder") + p.add_argument("--trigger", default="", help="Trigger word/phrase prepended to every caption") + + p.add_argument("--captioner", choices=["joycaption", "florence2", "wd14"], default="joycaption") + p.add_argument("--device", default="cuda") + p.add_argument("--load-in-4bit", action="store_true", help="4-bit load for JoyCaption (bitsandbytes)") + p.add_argument("--caption-prompt", default=None, help="Override JoyCaption instruction text") + p.add_argument("--wd14-general-thresh", type=float, default=0.35) + p.add_argument("--wd14-character-thresh", type=float, default=0.75) + + p.add_argument("--max-tier", choices=["480p", "720p", "1080p"], default="1080p", + help="Highest resolution bucket to consider; auto-picks smaller tiers to avoid upscaling") + p.add_argument("--face-pad", type=float, default=2.2) + p.add_argument("--person-pad", type=float, default=1.3) + p.add_argument("--person-region-pad", type=float, default=1.7, + help="padding for person_upper/person_lower crops (see --body-region)") + p.add_argument("--body-region", choices=["auto", "upper", "lower"], default="auto", + help="for person-kind detections, narrow the crop to the top or bottom " + "portion of the detected body instead of the whole figure -- use " + "when most source frames are already close-ups of one body region. " + "'auto' (default) keeps the full detected box. No effect on face-kind " + "detections.") + p.add_argument("--body-region-frac", type=float, default=0.45, + help="fraction of the person bbox's height kept when --body-region is upper/lower") + p.add_argument("--min-face-conf", type=float, default=0.5) + p.add_argument("--min-person-conf", type=float, default=0.35) + p.add_argument("--image-format", choices=["jpg", "png"], default="jpg") + + p.add_argument("--video-fps", type=float, default=16.0) + p.add_argument("--clip-min-sec", type=float, default=2.0) + p.add_argument("--clip-max-sec", type=float, default=6.0) + p.add_argument("--scene-diff-thresh", type=float, default=0.45) + p.add_argument("--clips-per-video", type=int, default=5, + help="cap clips extracted per source video, evenly sampled across its " + "timeline (e.g. a 5-minute video with many scene cuts won't dump " + "dozens of near-duplicate clips into the dataset); pass 0 for no cap") + + return p.parse_args() + + +def scan_inputs(input_dir): + images, videos = [], [] + for root, _dirs, files in os.walk(input_dir): + for name in files: + ext = os.path.splitext(name)[1].lower() + path = os.path.join(root, name) + if ext in IMAGE_EXTS: + images.append(path) + elif ext in VIDEO_EXTS: + videos.append(path) + return sorted(images), sorted(videos) + + +def process_image(path, idx, out_dir, args, captioner, pad_overrides): + pil_img = Image.open(path).convert("RGB") + bgr = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) + + bbox, kind = detect_subject_bbox(bgr, args.min_face_conf, args.min_person_conf) + if kind == "person" and args.body_region != "auto": + bbox = subdivide_bbox(bbox, args.body_region, args.body_region_frac) + kind = f"person_{args.body_region}" + crop_box, (tw, th), bucket_name, tier = crop_resize.compute_crop( + pil_img.width, pil_img.height, bbox, kind, pad_overrides, args.max_tier + ) + cropped = crop_resize.crop_resize_pil(pil_img, crop_box, tw, th) + + stem = f"{idx:04d}" + images_dir = os.path.join(out_dir, "images") + os.makedirs(images_dir, exist_ok=True) + img_path = os.path.join(images_dir, f"{stem}.{args.image_format}") + if args.image_format == "jpg": + cropped.save(img_path, quality=95) + else: + cropped.save(img_path) + + caption = captioner.caption([cropped]) + caption = with_trigger(caption, args.trigger) + with open(os.path.join(images_dir, f"{stem}.txt"), "w", encoding="utf-8") as f: + f.write(caption) + + print(f" [image] {os.path.basename(path)} -> {stem}.{args.image_format} " + f"({kind}, {bucket_name}/{tier}, {tw}x{th})") + + +def subsample_ranges(ranges, target): + """Evenly picks `target` ranges spread across the full list (by index, so + spread across the video's timeline) instead of just taking the first N -- + keeps whatever diversity of scenes exists rather than favoring the start.""" + if not target or len(ranges) <= target: + return ranges + if target <= 1: + return ranges[len(ranges) // 2 : len(ranges) // 2 + 1] + step = (len(ranges) - 1) / (target - 1) + picked_indices = sorted({round(i * step) for i in range(target)}) + return [ranges[i] for i in picked_indices] + + +def process_video(path, idx_start, out_dir, args, captioner, pad_overrides): + cuts = scene_split.find_scene_cuts(path, diff_thresh=args.scene_diff_thresh) + cap = cv2.VideoCapture(path) + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + src_fps = cap.get(cv2.CAP_PROP_FPS) or args.video_fps + cap.release() + + ranges = scene_split.build_clip_ranges( + total_frames, src_fps, cuts, args.clip_min_sec, args.clip_max_sec + ) + detected_count = len(ranges) + ranges = subsample_ranges(ranges, args.clips_per_video) + if args.clips_per_video and detected_count > len(ranges): + print(f" [video] {os.path.basename(path)}: {detected_count} scene(s) detected, " + f"sampling {len(ranges)} (--clips-per-video {args.clips_per_video})") + + idx = idx_start + for start, end in ranges: + result = scene_split.extract_clip( + path, start, end, args.video_fps, args.max_tier, pad_overrides, + args.body_region, args.body_region_frac, + ) + if result is None: + continue + frames, (tw, th), representative, motion_tag = result + + stem = f"{idx:04d}" + videos_dir = os.path.join(out_dir, "videos") + os.makedirs(videos_dir, exist_ok=True) + clip_path = os.path.join(videos_dir, f"{stem}.mp4") + scene_split.write_clip_mp4(frames, clip_path, args.video_fps) + + caption = captioner.caption([representative]) + caption = f"{caption.strip()}, {motion_tag}" if caption.strip() else motion_tag + caption = with_trigger(caption, args.trigger) + with open(os.path.join(videos_dir, f"{stem}.txt"), "w", encoding="utf-8") as f: + f.write(caption) + + print(f" [video] {os.path.basename(path)} [{start}-{end}] -> {stem}.mp4 " + f"({len(frames)}f @ {args.video_fps}fps, {tw}x{th}, {motion_tag})") + idx += 1 + return idx + + +def write_dataset_toml(out_dir, args, has_images, has_videos): + """musubi-tuner dataset config (schema verified against docs/dataset_config.md). + Items are already pre-cropped to one of a handful of Wan resolution buckets + by this tool, so enable_bucket lets musubi-tuner group them without forcing + a single resolution; `resolution` below is just the nominal/reference size.""" + ref_w, ref_h = crop_resize.bucket_set(args.max_tier)["landscape"] + + def p(*parts): + return os.path.join(out_dir, *parts).replace("\\", "/") + + lines = [ + "[general]", + f"resolution = [{ref_w}, {ref_h}]", + 'caption_extension = ".txt"', + "batch_size = 1", + "enable_bucket = true", + "bucket_no_upscale = true", + "", + ] + if has_images: + lines += [ + "[[datasets]]", + f'image_directory = "{p("images")}"', + f'cache_directory = "{p("images", "cache")}"', + "num_repeats = 1", + "", + ] + if has_videos: + max_frames = int(args.video_fps * args.clip_max_sec) + 1 + lines += [ + "[[datasets]]", + f'video_directory = "{p("videos")}"', + f'cache_directory = "{p("videos", "cache")}"', + 'frame_extraction = "full"', + f"max_frames = {max_frames}", + f"source_fps = {args.video_fps}", + "num_repeats = 1", + "", + ] + with open(os.path.join(out_dir, "dataset.toml"), "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + + +def main(): + args = parse_args() + os.makedirs(os.path.join(args.output, "images"), exist_ok=True) + os.makedirs(os.path.join(args.output, "videos"), exist_ok=True) + + images, videos = scan_inputs(args.input) + print(f"Found {len(images)} image(s), {len(videos)} video(s) under {args.input}") + if not images and not videos: + print("Nothing to do.") + return + + pad_overrides = { + "face": args.face_pad, "person": args.person_pad, + "person_upper": args.person_region_pad, "person_lower": args.person_region_pad, + } + + print(f"Loading captioner: {args.captioner} ...") + captioner = build_captioner(args) + + img_idx = 1 + for path in images: + try: + process_image(path, img_idx, args.output, args, captioner, pad_overrides) + img_idx += 1 + except Exception: + print(f" [image] FAILED {path}:") + traceback.print_exc() + + vid_idx = 1 + for path in videos: + try: + vid_idx = process_video(path, vid_idx, args.output, args, captioner, pad_overrides) + except Exception: + print(f" [video] FAILED {path}:") + traceback.print_exc() + + write_dataset_toml(args.output, args, bool(images), bool(videos)) + print(f"Done. {img_idx - 1} image(s), {vid_idx - 1} clip(s) -> {args.output}") + + +if __name__ == "__main__": + main() diff --git a/dataset_builder/caption.py b/dataset_builder/caption.py new file mode 100644 index 0000000000000000000000000000000000000000..ffa21ca1747f682fdc2ea1d56a5dfb79f1c7aeb1 --- /dev/null +++ b/dataset_builder/caption.py @@ -0,0 +1,209 @@ +"""Pluggable local captioners for LoRA dataset building. + +All captioners expose the same interface: ``caption(images: list[PIL.Image]) -> str``. +``images`` is a list so video callers can pass multiple sampled frames; single-image +captioners just use the first one. + +- JoyCaptionCaptioner: fancyfeast/llama-joycaption-beta-one-hf-llava (AutoProcessor + + LlavaForConditionalGeneration, confirmed against the model card). The exact prompt + wording for a dedicated "training caption" mode is not documented on the model card, + so the instruction text here is our own default and is overridable via --caption-prompt. +- Florence2Captioner: microsoft/Florence-2-large, task. +- WD14Captioner: SmilingWolf/wd-vit-tagger-v3 onnx tagger (booru-style tags), preprocessing + confirmed against the reference wd-tagger Space source (white-pad-to-square, RGB->BGR, + no normalization, category 9/4/0 = rating/character/general). +""" +import csv + +import numpy as np +import torch +from PIL import Image + + +class JoyCaptionCaptioner: + MODEL_ID = "fancyfeast/llama-joycaption-beta-one-hf-llava" + DEFAULT_PROMPT = ( + "Write a detailed but concise caption for this image, suitable for training " + "an image/video LoRA. Describe the subject's appearance, pose, clothing, " + "setting, and lighting in plain descriptive sentences. Do not guess names." + ) + + def __init__(self, device="cuda", load_in_4bit=False, prompt=None, system_prompt=None): + from transformers import AutoProcessor, LlavaForConditionalGeneration + + self.processor = AutoProcessor.from_pretrained(self.MODEL_ID) + load_kwargs = {} + if load_in_4bit: + from transformers import BitsAndBytesConfig + load_kwargs["quantization_config"] = BitsAndBytesConfig(load_in_4bit=True) + load_kwargs["device_map"] = "auto" + else: + load_kwargs["torch_dtype"] = torch.bfloat16 + load_kwargs["device_map"] = device + self.model = LlavaForConditionalGeneration.from_pretrained(self.MODEL_ID, **load_kwargs) + self.model.eval() + + self.system_prompt = system_prompt or "You are a helpful image captioner." + self.prompt = prompt or self.DEFAULT_PROMPT + + def caption(self, images): + image = images[0].convert("RGB") + convo = [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": self.prompt}, + ] + convo_string = self.processor.apply_chat_template( + convo, tokenize=False, add_generation_prompt=True + ) + inputs = self.processor(text=[convo_string], images=[image], return_tensors="pt") + inputs = {k: v.to(self.model.device) for k, v in inputs.items()} + if "pixel_values" in inputs: + inputs["pixel_values"] = inputs["pixel_values"].to(self.model.dtype) + + with torch.no_grad(): + generate_ids = self.model.generate( + **inputs, + max_new_tokens=300, + do_sample=True, + suppress_tokens=None, + use_cache=True, + temperature=0.6, + top_k=None, + top_p=0.9, + )[0] + + generate_ids = generate_ids[inputs["input_ids"].shape[1]:] + text = self.processor.tokenizer.decode( + generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False + ) + return text.strip() + + +class Florence2Captioner: + MODEL_ID = "microsoft/Florence-2-large" + TASK = "" + + def __init__(self, device="cuda"): + from transformers import AutoModelForCausalLM, AutoProcessor + + dtype = torch.float16 if device.startswith("cuda") else torch.float32 + self.device = device + self.model = ( + AutoModelForCausalLM.from_pretrained(self.MODEL_ID, trust_remote_code=True, torch_dtype=dtype) + .to(device) + .eval() + ) + self.processor = AutoProcessor.from_pretrained(self.MODEL_ID, trust_remote_code=True) + + def caption(self, images): + image = images[0].convert("RGB") + inputs = self.processor(text=self.TASK, images=image, return_tensors="pt") + inputs = {k: v.to(self.device, self.model.dtype) if v.is_floating_point() else v.to(self.device) + for k, v in inputs.items()} + + with torch.no_grad(): + generated_ids = self.model.generate( + input_ids=inputs["input_ids"], + pixel_values=inputs["pixel_values"], + max_new_tokens=200, + num_beams=3, + do_sample=False, + ) + text = self.processor.batch_decode(generated_ids, skip_special_tokens=False)[0] + parsed = self.processor.post_process_generation( + text, task=self.TASK, image_size=(image.width, image.height) + ) + return parsed[self.TASK].strip() + + +class WD14Captioner: + REPO_ID = "SmilingWolf/wd-vit-tagger-v3" + MODEL_FILE = "model.onnx" + TAGS_FILE = "selected_tags.csv" + + def __init__(self, general_thresh=0.35, character_thresh=0.75, use_gpu=True): + import onnxruntime as ort + from huggingface_hub import hf_hub_download + + model_path = hf_hub_download(self.REPO_ID, self.MODEL_FILE) + tags_path = hf_hub_download(self.REPO_ID, self.TAGS_FILE) + + providers = ["CPUExecutionProvider"] + if use_gpu: + providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] + self.session = ort.InferenceSession(model_path, providers=providers) + self.input_name = self.session.get_inputs()[0].name + self.output_name = self.session.get_outputs()[0].name + self.target_size = int(self.session.get_inputs()[0].shape[1]) + + self.general_thresh = general_thresh + self.character_thresh = character_thresh + self.tag_names, self.general_idx, self.character_idx = self._load_tags(tags_path) + + @staticmethod + def _load_tags(path): + names, general, character = [], [], [] + with open(path, newline="", encoding="utf-8") as f: + for i, row in enumerate(csv.DictReader(f)): + names.append(row["name"]) + cat = int(row["category"]) + if cat == 4: + character.append(i) + elif cat != 9: # skip rating (9); keep general (0) and any others + general.append(i) + return names, general, character + + def _preprocess(self, image: Image.Image): + image = image.convert("RGBA") + canvas = Image.new("RGBA", image.size, (255, 255, 255, 255)) + canvas.alpha_composite(image) + image = canvas.convert("RGB") + + w, h = image.size + size = max(w, h) + padded = Image.new("RGB", (size, size), (255, 255, 255)) + padded.paste(image, ((size - w) // 2, (size - h) // 2)) + padded = padded.resize((self.target_size, self.target_size), Image.BICUBIC) + + arr = np.asarray(padded, dtype=np.float32) + arr = arr[:, :, ::-1] # RGB -> BGR + return np.expand_dims(arr, axis=0) + + def caption(self, images): + image = images[0] + arr = self._preprocess(image) + preds = self.session.run([self.output_name], {self.input_name: arr})[0][0] + + general = [(self.tag_names[i], preds[i]) for i in self.general_idx if preds[i] >= self.general_thresh] + character = [(self.tag_names[i], preds[i]) for i in self.character_idx if preds[i] >= self.character_thresh] + general.sort(key=lambda t: -t[1]) + character.sort(key=lambda t: -t[1]) + + tags = [t.replace("_", " ") for t, _ in character] + [t.replace("_", " ") for t, _ in general] + return ", ".join(tags) + + +def build_captioner(args): + name = args.captioner + if name == "joycaption": + return JoyCaptionCaptioner( + device=args.device, + load_in_4bit=args.load_in_4bit, + prompt=args.caption_prompt, + ) + if name == "florence2": + return Florence2Captioner(device=args.device) + if name == "wd14": + return WD14Captioner( + general_thresh=args.wd14_general_thresh, + character_thresh=args.wd14_character_thresh, + use_gpu=args.device.startswith("cuda"), + ) + raise ValueError(f"Unknown captioner: {name}") + + +def with_trigger(caption, trigger): + caption = (caption or "").strip() + if not trigger: + return caption + return f"{trigger}, {caption}" if caption else trigger diff --git a/dataset_builder/crop_resize.py b/dataset_builder/crop_resize.py new file mode 100644 index 0000000000000000000000000000000000000000..56036fd6106e9d4fe6d83163fc4f0a50a8c93bdc --- /dev/null +++ b/dataset_builder/crop_resize.py @@ -0,0 +1,146 @@ +"""Wan 2.2 resolution-bucket matching + subject-centered crop math. + +Ports the area-preserving resize logic from ``wan_resizer.py`` +(``WanNativeResize_Dolphin``) into a torch/comfy-free form so it can run +outside ComfyUI in the standalone dataset builder CLI. +""" +import math + +import cv2 +import numpy as np +from PIL import Image + +TIER_AREA = { + "480p": 832 * 480, + "720p": 1280 * 720, + "1080p": 1920 * 1080, +} +TIER_ORDER = ["1080p", "720p", "480p"] + +ASPECTS = { + "landscape": 16 / 9, + "portrait": 9 / 16, + "square": 1.0, +} + +# Default bbox padding multiplier per detected subject kind (see detect.py). +# person_upper/person_lower are subdivide_bbox() regions -- padded more than +# plain "person" since they start from an already-narrowed sub-box. +DEFAULT_PAD = { + "face": 2.2, + "person": 1.3, + "person_upper": 1.7, + "person_lower": 1.7, + "full_frame": 1.0, +} + + +def _bucket_dims(area, aspect, alignment=16): + h = math.sqrt(area / aspect) + w = h * aspect + w = max(alignment, int(round(w / alignment)) * alignment) + h = max(alignment, int(round(h / alignment)) * alignment) + return w, h + + +def bucket_set(tier): + area = TIER_AREA[tier] + return {name: _bucket_dims(area, aspect) for name, aspect in ASPECTS.items()} + + +def nearest_bucket(crop_w, crop_h, tier): + """Pick the bucket of ``tier`` whose aspect ratio is closest to crop_w/crop_h.""" + crop_aspect = crop_w / crop_h + buckets = bucket_set(tier) + best_name = min( + buckets, + key=lambda n: abs(math.log(buckets[n][0] / buckets[n][1]) - math.log(crop_aspect)), + ) + return best_name, buckets[best_name] + + +def auto_tier(crop_w, crop_h, max_tier="1080p"): + """Pick the largest tier whose bucket area does not exceed the native crop's + pixel area, to avoid upscaling past source resolution. Falls back to the + smallest tier under max_tier if even that would upscale.""" + order = TIER_ORDER[TIER_ORDER.index(max_tier):] + native_area = crop_w * crop_h + for tier in order: + _, (bw, bh) = nearest_bucket(crop_w, crop_h, tier) + if bw * bh <= native_area: + return tier + return order[-1] + + +def pad_bbox(bbox, pad_mult): + x1, y1, x2, y2 = bbox + w, h = x2 - x1, y2 - y1 + cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + new_w, new_h = w * pad_mult, h * pad_mult + return (cx - new_w / 2, cy - new_h / 2, cx + new_w / 2, cy + new_h / 2) + + +def fit_aspect(bbox, img_w, img_h, target_aspect): + """Expand bbox to match target_aspect (w/h), centered on bbox, then clamp + to image bounds (shift first, hard-clamp as a last resort for oversized + crops).""" + x1, y1, x2, y2 = bbox + w, h = x2 - x1, y2 - y1 + cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 + cur_aspect = w / h if h else target_aspect + + if cur_aspect < target_aspect: + new_h, new_w = h, h * target_aspect + else: + new_w, new_h = w, w / target_aspect + + x1, x2 = cx - new_w / 2, cx + new_w / 2 + y1, y2 = cy - new_h / 2, cy + new_h / 2 + + if x1 < 0: + x2 -= x1 + x1 = 0 + if x2 > img_w: + x1 -= (x2 - img_w) + x2 = img_w + if y1 < 0: + y2 -= y1 + y1 = 0 + if y2 > img_h: + y1 -= (y2 - img_h) + y2 = img_h + + x1, y1 = max(0, x1), max(0, y1) + x2, y2 = min(img_w, x2), min(img_h, y2) + return (x1, y1, x2, y2) + + +def compute_crop(img_w, img_h, bbox, kind, pad_overrides=None, max_tier="1080p"): + """Given an image size and a detected subject bbox, return the crop box + (in original-image pixel space) and the resolution bucket to resize it to. + + Returns: (crop_box, (target_w, target_h), bucket_name, tier) + """ + pad = dict(DEFAULT_PAD) + if pad_overrides: + pad.update(pad_overrides) + + padded = pad_bbox(bbox, pad.get(kind, 1.0)) + pw, ph = padded[2] - padded[0], padded[3] - padded[1] + pw, ph = max(1.0, pw), max(1.0, ph) + + tier = auto_tier(pw, ph, max_tier) + bucket_name, (bw, bh) = nearest_bucket(pw, ph, tier) + crop_box = fit_aspect(padded, img_w, img_h, bw / bh) + return crop_box, (bw, bh), bucket_name, tier + + +def crop_resize_pil(image: Image.Image, crop_box, target_w, target_h): + x1, y1, x2, y2 = (int(round(v)) for v in crop_box) + return image.crop((x1, y1, x2, y2)).resize((target_w, target_h), Image.LANCZOS) + + +def crop_resize_np(frame_bgr: np.ndarray, crop_box, target_w, target_h): + x1, y1, x2, y2 = (int(round(v)) for v in crop_box) + cropped = frame_bgr[y1:y2, x1:x2] + return cv2.resize(cropped, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4) diff --git a/dataset_builder/detect.py b/dataset_builder/detect.py new file mode 100644 index 0000000000000000000000000000000000000000..0bebd66957097651da094fc867207092977bab44 --- /dev/null +++ b/dataset_builder/detect.py @@ -0,0 +1,140 @@ +"""Subject bbox detection for smart cropping. + +Priority: mediapipe face detection -> OpenCV Haar cascade face detection +(no model download required, always available with opencv-python) -> +ultralytics YOLOv8n person detection -> full frame (no crop). + +All bboxes are returned as (x1, y1, x2, y2) in pixel space, plus a "kind" +string ("face" | "person" | "full_frame") used by crop_resize.py to pick a +padding multiplier. +""" +import cv2 +import numpy as np + +_mp_detector = None +_mp_unavailable = False +_haar_cascade = None +_yolo_model = None + + +def _get_mediapipe_detector(min_conf): + global _mp_detector, _mp_unavailable + if _mp_unavailable: + return None + if _mp_detector is None: + try: + import mediapipe as mp + _mp_detector = mp.solutions.face_detection.FaceDetection( + model_selection=1, min_detection_confidence=min_conf + ) + except Exception: + _mp_unavailable = True + return None + return _mp_detector + + +def _get_haar_cascade(): + global _haar_cascade + if _haar_cascade is None: + path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml" + _haar_cascade = cv2.CascadeClassifier(path) + return _haar_cascade + + +def _get_yolo(): + global _yolo_model + if _yolo_model is None: + from ultralytics import YOLO + _yolo_model = YOLO("yolov8n.pt") + return _yolo_model + + +def _largest_bbox(candidates): + """candidates: list of (x1, y1, x2, y2). Returns the largest-area one.""" + if not candidates: + return None + return max(candidates, key=lambda b: max(0, b[2] - b[0]) * max(0, b[3] - b[1])) + + +def _detect_face_mediapipe(image_bgr, min_conf): + detector = _get_mediapipe_detector(min_conf) + if detector is None: + return None + h, w = image_bgr.shape[:2] + rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) + try: + results = detector.process(rgb) + except Exception: + return None + if not results.detections: + return None + boxes = [] + for det in results.detections: + rb = det.location_data.relative_bounding_box + x1, y1 = rb.xmin * w, rb.ymin * h + x2, y2 = x1 + rb.width * w, y1 + rb.height * h + boxes.append((x1, y1, x2, y2)) + return _largest_bbox(boxes) + + +def _detect_face_haar(image_bgr): + cascade = _get_haar_cascade() + gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY) + faces = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(40, 40)) + if len(faces) == 0: + return None + boxes = [(float(x), float(y), float(x + fw), float(y + fh)) for x, y, fw, fh in faces] + return _largest_bbox(boxes) + + +def _detect_person_yolo(image_bgr, min_conf): + try: + model = _get_yolo() + results = model.predict(image_bgr, classes=[0], conf=min_conf, verbose=False) + except Exception: + return None + boxes = [] + for r in results: + if r.boxes is None: + continue + for b in r.boxes: + x1, y1, x2, y2 = b.xyxy[0].tolist() + boxes.append((x1, y1, x2, y2)) + return _largest_bbox(boxes) + + +def detect_subject_bbox(image_bgr: np.ndarray, min_face_conf=0.5, min_person_conf=0.35): + """Returns (bbox, kind) where kind is 'face', 'person', or 'full_frame'.""" + h, w = image_bgr.shape[:2] + + bbox = _detect_face_mediapipe(image_bgr, min_face_conf) + if bbox: + return bbox, "face" + + bbox = _detect_face_haar(image_bgr) + if bbox: + return bbox, "face" + + bbox = _detect_person_yolo(image_bgr, min_person_conf) + if bbox: + return bbox, "person" + + return (0.0, 0.0, float(w), float(h)), "full_frame" + + +def subdivide_bbox(bbox, region, frac=0.45): + """Narrows a bbox to its top or bottom portion (by height), same width. + + Used to bias person-kind crops toward a specific body region (e.g. most + of the frame is a torso/lower-body shot rather than a full figure) without + needing a specialized body-part detector -- just a fraction of the + already-detected person bbox. `region` is "upper", "lower", or anything + else (returned unchanged, e.g. "auto"/"full"). + """ + x1, y1, x2, y2 = bbox + h = y2 - y1 + if region == "lower": + return (x1, y2 - h * frac, x2, y2) + if region == "upper": + return (x1, y1, x2, y1 + h * frac) + return bbox diff --git a/dataset_builder/inspect_lora.py b/dataset_builder/inspect_lora.py new file mode 100644 index 0000000000000000000000000000000000000000..6251fa2a5ca8fab3aac31a38984743e69301b6f1 --- /dev/null +++ b/dataset_builder/inspect_lora.py @@ -0,0 +1,69 @@ +"""Inspect a LoRA .safetensors file: embedded training metadata + inferred +rank/target modules from tensor shapes. Read-only, no GPU/torch needed -- +just parses the safetensors header, so it's fast and safe to run on any +LoRA file (including ones you didn't train yourself). + +Usage: + python_embeded\\python.exe dataset_builder\\inspect_lora.py path\\to\\lora.safetensors +""" +import argparse +import json +import re +from collections import Counter + +from safetensors import safe_open + + +def parse_args(): + p = argparse.ArgumentParser(description="Inspect a LoRA safetensors file's metadata") + p.add_argument("lora_path") + return p.parse_args() + + +def infer_rank(keys, shapes): + for k in keys: + if k.endswith("lora_A.weight") or k.endswith("lora_down.weight"): + return shapes[k][0] + return None + + +def summarize_target_modules(keys): + """Collapses per-block keys (blocks.0, blocks.1, ...) into module families + so you can see what parts of the model were trained without a 800-line dump.""" + families = Counter() + for k in keys: + base = re.sub(r"\.(lora_A|lora_B|lora_down|lora_up|alpha)(\.weight)?$", "", k) + base = re.sub(r"\.\d+\.", ".N.", base) + families[base] += 1 + return families + + +def main(): + args = parse_args() + with safe_open(args.lora_path, framework="pt") as f: + meta = f.metadata() or {} + keys = list(f.keys()) + shapes = {k: f.get_slice(k).get_shape() for k in keys} + + print(f"=== {args.lora_path} ===") + print(f"Tensor count: {len(keys)}") + print(f"Inferred rank (lora_A/lora_down first dim): {infer_rank(keys, shapes)}") + + print("\n--- Embedded metadata ---") + if not meta: + print("(none)") + for k, v in meta.items(): + try: + parsed = json.loads(v) + print(f"{k}:") + print(json.dumps(parsed, indent=2, ensure_ascii=False)[:3000]) + except (json.JSONDecodeError, TypeError): + print(f"{k}: {v}") + + print("\n--- Target module families (per-block keys collapsed) ---") + for base, count in summarize_target_modules(keys).most_common(30): + print(f" {base} x{count}") + + +if __name__ == "__main__": + main() diff --git a/dataset_builder/scene_split.py b/dataset_builder/scene_split.py new file mode 100644 index 0000000000000000000000000000000000000000..de689ace60a1743aaa82b3d1084eda0aaa185c2a --- /dev/null +++ b/dataset_builder/scene_split.py @@ -0,0 +1,190 @@ +"""Video scene-cut detection, clip extraction, and subject-centered crop/resize. + +Uses plain (non-relative) imports of sibling modules so this file works whether +invoked as ``python dataset_builder/build.py`` or ``python -m dataset_builder.build`` +(build.py inserts its own directory onto sys.path before importing). +""" +import math +import statistics + +import cv2 +import imageio +import numpy as np +from PIL import Image + +import crop_resize +from detect import detect_subject_bbox, subdivide_bbox + + +def _frame_hist(frame_bgr): + hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV) + hist = cv2.calcHist([hsv], [0, 1], None, [50, 60], [0, 180, 0, 256]) + cv2.normalize(hist, hist) + return hist + + +def find_scene_cuts(video_path, diff_thresh=0.45, sample_stride=1): + """Returns a sorted list of frame indices where a scene cut is detected, + via Bhattacharyya distance between consecutive HSV histograms.""" + cap = cv2.VideoCapture(video_path) + cuts = [] + prev_hist = None + idx = 0 + while True: + ok, frame = cap.read() + if not ok: + break + if idx % sample_stride == 0: + hist = _frame_hist(frame) + if prev_hist is not None: + score = cv2.compareHist(prev_hist, hist, cv2.HISTCMP_BHATTACHARYYA) + if score > diff_thresh: + cuts.append(idx) + prev_hist = hist + idx += 1 + cap.release() + return cuts + + +def build_clip_ranges(total_frames, fps, cuts, clip_min_sec=2.0, clip_max_sec=6.0): + """Turns scene-cut frame indices into (start, end) frame ranges honoring + clip_min/max seconds: too-short segments are merged forward, segments + longer than clip_max are chopped into roughly-equal sub-segments.""" + min_frames = max(1, int(clip_min_sec * fps)) + max_frames = max(min_frames, int(clip_max_sec * fps)) + + boundaries = [0] + sorted(set(c for c in cuts if 0 < c < total_frames)) + [total_frames] + raw_ranges = [] + start = boundaries[0] + for b in boundaries[1:]: + if b - start >= min_frames: + raw_ranges.append((start, b)) + start = b + if start < total_frames: + if raw_ranges and (total_frames - start) < min_frames: + s, _ = raw_ranges[-1] + raw_ranges[-1] = (s, total_frames) + else: + raw_ranges.append((start, total_frames)) + + ranges = [] + for s, e in raw_ranges: + length = e - s + if length <= max_frames: + ranges.append((s, e)) + continue + n_chunks = math.ceil(length / max_frames) + chunk_len = length // n_chunks + for i in range(n_chunks): + cs = s + i * chunk_len + ce = e if i == n_chunks - 1 else cs + chunk_len + ranges.append((cs, ce)) + + return [(s, e) for s, e in ranges if e - s >= max(1, min_frames // 2)] + + +def _sample_indices(start, end, n=3): + if end - start <= n: + return list(range(start, end)) + step = (end - start) / (n + 1) + return [int(start + step * (i + 1)) for i in range(n)] + + +def _median_bbox(bboxes): + return ( + statistics.median(b[0] for b in bboxes), + statistics.median(b[1] for b in bboxes), + statistics.median(b[2] for b in bboxes), + statistics.median(b[3] for b in bboxes), + ) + + +def _motion_tag(frames_gray): + if len(frames_gray) < 2: + return "static, minimal motion" + mags = [] + for a, b in zip(frames_gray, frames_gray[1:]): + flow = cv2.calcOpticalFlowFarneback(a, b, None, 0.5, 3, 15, 3, 5, 1.2, 0) + mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1]) + mags.append(float(np.mean(mag))) + avg = sum(mags) / len(mags) + if avg < 0.5: + return "static, minimal motion" + if avg < 2.0: + return "subtle motion" + if avg < 5.0: + return "moderate motion" + return "fast, dynamic motion" + + +def extract_clip(video_path, start, end, target_fps, max_tier="1080p", pad_overrides=None, + body_region="auto", body_region_frac=0.45): + """Reads frames [start, end) from video_path, computes one stable subject + crop for the whole clip (median bbox over sampled frames), resamples to + target_fps, trims to a 4n+1 frame count, and returns + (frames_bgr, (target_w, target_h), representative_pil_image, motion_tag), + or None if no frames could be read.""" + cap = cv2.VideoCapture(video_path) + src_fps = cap.get(cv2.CAP_PROP_FPS) or target_fps + img_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + img_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + bboxes, kinds = [], [] + for idx in _sample_indices(start, end, n=3): + cap.set(cv2.CAP_PROP_POS_FRAMES, idx) + ok, frame = cap.read() + if not ok: + continue + bbox, kind = detect_subject_bbox(frame) + bboxes.append(bbox) + kinds.append(kind) + + if not bboxes: + cap.release() + return None + + bbox = _median_bbox(bboxes) + kind = max(set(kinds), key=kinds.count) + if kind == "person" and body_region != "auto": + bbox = subdivide_bbox(bbox, body_region, body_region_frac) + kind = f"person_{body_region}" + crop_box, (tw, th), _bucket_name, _tier = crop_resize.compute_crop( + img_w, img_h, bbox, kind, pad_overrides, max_tier + ) + + step = src_fps / target_fps + raw_indices = [] + f = float(start) + while f < end: + raw_indices.append(int(round(f))) + f += step + if len(raw_indices) >= 5: + n = ((len(raw_indices) - 1) // 4) * 4 + 1 + raw_indices = raw_indices[:n] + + frames_out, frames_gray = [], [] + for idx in raw_indices: + cap.set(cv2.CAP_PROP_POS_FRAMES, idx) + ok, frame = cap.read() + if not ok: + continue + cropped = crop_resize.crop_resize_np(frame, crop_box, tw, th) + frames_out.append(cropped) + if len(frames_gray) < 6: + frames_gray.append(cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY)) + cap.release() + + if not frames_out: + return None + + motion_tag = _motion_tag(frames_gray) + mid = frames_out[len(frames_out) // 2] + representative = Image.fromarray(cv2.cvtColor(mid, cv2.COLOR_BGR2RGB)) + + return frames_out, (tw, th), representative, motion_tag + + +def write_clip_mp4(frames_bgr, out_path, fps): + with imageio.get_writer(str(out_path), fps=fps, codec="libx264", quality=8) as writer: + for frame in frames_bgr: + writer.append_data(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) diff --git a/dataset_builder/start_webui.bat b/dataset_builder/start_webui.bat new file mode 100644 index 0000000000000000000000000000000000000000..5a3c50c15c5d81f992a1dd97d59154d118e2a39b --- /dev/null +++ b/dataset_builder/start_webui.bat @@ -0,0 +1,21 @@ +@echo off +setlocal + +set "HERE=%~dp0" +set "PYTHON=%HERE%..\..\..\..\python_embeded\python.exe" + +if not exist "%PYTHON%" ( + echo Could not find python_embeded at: + echo %PYTHON% + echo This .bat expects to live at ComfyUI\custom_nodes\dolphin_nodes\dataset_builder\ + pause + exit /b 1 +) + +echo Starting Wan 2.2 LoRA web UI... +echo Once it says "Running on local URL", open http://127.0.0.1:7860 in your browser. +echo. + +"%PYTHON%" "%HERE%webui.py" %* + +pause diff --git a/dataset_builder/webui.py b/dataset_builder/webui.py new file mode 100644 index 0000000000000000000000000000000000000000..13d1dabcbee8721e321170a0aedd19e59eb10eaf --- /dev/null +++ b/dataset_builder/webui.py @@ -0,0 +1,402 @@ +"""Local web UI for the Wan 2.2 LoRA dataset builder + trainer. + +Thin Gradio layer over the already-verified CLI scripts (build.py, +train/train.py, train/download_checkpoints.py, train/setup_env.ps1) -- this +file does not reimplement any of their logic, it only shells out to them and +streams stdout live. Heavy ML models (captioners, torch) never load inside +this long-lived server process; each run is its own subprocess so a crash or +this environment's known torch-teardown quirk can't take the UI down with it. + +Run: + python_embeded\\python.exe dataset_builder\\webui.py [--port 7860] +""" +import argparse +import os +import subprocess +import sys + +import gradio as gr + +HERE = os.path.dirname(os.path.abspath(__file__)) +TRAIN_DIR = os.path.join(HERE, "train") +VENV_PYTHON = os.path.join(TRAIN_DIR, "venv", "Scripts", "python.exe") +PYTHON = sys.executable # python_embeded's own interpreter (has build.py's deps) + +sys.path.insert(0, TRAIN_DIR) +from download_checkpoints import FILES, COMMON_FILES # noqa: E402 + +# ComfyUI/custom_nodes/dolphin_nodes/dataset_builder/webui.py -> ComfyUI/models +MODELS_ROOT = os.path.normpath(os.path.join(HERE, "..", "..", "..", "models")) + + +# Any python_embeded process that imports torch on this machine can exit with +# this NTSTATUS during CUDA teardown *after* finishing its real work (confirmed +# for both dataset_builder/train/train.py and build.py itself, e.g. via +# caption.py's torch import) -- see dataset_builder/train/README.md's bug list. +# Reported as either the signed or unsigned 32-bit form depending on context. +BENIGN_TORCH_TEARDOWN_NTSTATUS = 0xC0000409 # STATUS_STACK_BUFFER_OVERRUN + + +EMBEDED_DIR = os.path.dirname(PYTHON) + + +def child_env(): + """Environment for subprocesses, scrubbed of this server's own Python. + + python_embeded is Python 3.13 and ships python313.dll in its own directory. + A child running the training venv's Python 3.10 will happily load that + 3.13 DLL if python_embeded is reachable via PATH or the inherited working + directory, and then every stdlib C extension fails with "Module use of + python313.dll conflicts with this version of Python" (confirmed twice + through this UI). Dropping python_embeded from PATH -- combined with the + fixed cwd below -- keeps the child's DLL search away from it. + """ + env = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"} + parts = env.get("PATH", "").split(os.pathsep) + embeded = os.path.normcase(EMBEDED_DIR) + env["PATH"] = os.pathsep.join( + p for p in parts if not os.path.normcase(p).startswith(embeded) + ) + return env + + +def stream_subprocess(cmd): + """Runs cmd, yielding the accumulated stdout text as new lines arrive.""" + proc = subprocess.Popen( + cmd, env=child_env(), cwd=HERE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, encoding="utf-8", errors="replace", bufsize=1, + ) + log = "" + for line in proc.stdout: + log += line + yield log + proc.wait() + if proc.returncode not in (0, None): + if (proc.returncode & 0xFFFFFFFF) == BENIGN_TORCH_TEARDOWN_NTSTATUS: + log += "\n(non-fatal: torch/CUDA teardown crash after finishing its work -- known environment quirk)" + else: + log += f"\n[exit code {proc.returncode}]" + yield log + + +# --------------------------------------------------------------------------- +# Checkpoint scanning (shared by Setup + Training tabs) +# --------------------------------------------------------------------------- + +def checkpoint_status_rows(): + rows = [] + for _repo, path, subdir, size in FILES["fp16"] + COMMON_FILES: + dest = os.path.join(MODELS_ROOT, subdir, os.path.basename(path)) + ok = os.path.exists(dest) + actual = f"{os.path.getsize(dest) / 1e9:.1f}GB" if ok else "-" + rows.append([os.path.basename(path), "있음" if ok else "없음", f"{size / 1e9:.1f}GB", actual]) + return rows + + +def venv_status_text(): + return "설치됨" if os.path.exists(VENV_PYTHON) else "설치 안 됨" + + +def list_checkpoints(subdir, name_filter=None, exclude=None): + d = os.path.join(MODELS_ROOT, subdir) + if not os.path.isdir(d): + return [] + files = os.listdir(d) + if name_filter: + files = [f for f in files if name_filter in f.lower()] + if exclude: + files = [f for f in files if exclude not in f.lower()] + return [os.path.join(d, f) for f in sorted(files)] + + +def list_dit_choices(): + # fp8_scaled DiT files can't be used for training (confirmed this session: + # musubi-tuner's loader rejects ComfyUI's pre-quantized scaled-fp8 format + # both with and without --fp8-scaled) -- excluded here so the dropdown + # can't steer anyone into that dead end. + return list_checkpoints("diffusion_models", name_filter="wan2.2_i2v", exclude="fp8_scaled") + + +def list_t5_choices(): + return list_checkpoints("text_encoders", name_filter="t5") + + +def list_vae_choices(): + return list_checkpoints("vae", name_filter="wan2.1_vae") + + +# --------------------------------------------------------------------------- +# Setup tab +# --------------------------------------------------------------------------- + +def run_setup_env(): + yield from stream_subprocess(["powershell.exe", "-File", os.path.join(TRAIN_DIR, "setup_env.ps1"), "-Yes"]) + + +def run_download_checkpoints(): + yield from stream_subprocess([ + PYTHON, os.path.join(TRAIN_DIR, "download_checkpoints.py"), + "--models-root", MODELS_ROOT, "--precision", "fp16", "--yes", + ]) + + +def build_setup_tab(): + with gr.Tab("Setup"): + gr.Markdown( + "학습에는 fp16 체크포인트가 필요합니다 (fp8_scaled는 ComfyUI 추론 전용 — " + "musubi-tuner 학습 로더가 못 읽는 것으로 확인됨). 아래에서 학습 환경/체크포인트 " + "상태를 확인하고 없는 것만 받으세요." + ) + with gr.Row(): + venv_box = gr.Textbox(label="학습 환경(venv)", value=venv_status_text(), interactive=False, scale=1) + setup_btn = gr.Button("학습 환경 설치 (venv + musubi-tuner)", scale=2) + setup_log = gr.Textbox(label="설치 로그", lines=10, max_lines=20, interactive=False) + setup_btn.click(run_setup_env, outputs=setup_log).then(venv_status_text, outputs=venv_box) + + gr.Markdown("### 체크포인트") + ckpt_table = gr.Dataframe( + headers=["파일", "상태", "예상 크기", "실제 크기"], + value=checkpoint_status_rows(), interactive=False, + ) + with gr.Row(): + refresh_btn = gr.Button("상태 새로고침") + confirm_dl = gr.Checkbox(label="위 목록 중 없는 fp16 체크포인트를 다운로드하겠습니다 (총 수십GB)") + dl_btn = gr.Button("다운로드", interactive=False) + dl_log = gr.Textbox(label="다운로드 로그", lines=10, max_lines=20, interactive=False) + + refresh_btn.click(checkpoint_status_rows, outputs=ckpt_table) + confirm_dl.change(lambda v: gr.update(interactive=v), inputs=confirm_dl, outputs=dl_btn) + dl_btn.click(run_download_checkpoints, outputs=dl_log).then(checkpoint_status_rows, outputs=ckpt_table) + + +# --------------------------------------------------------------------------- +# Dataset Builder tab +# --------------------------------------------------------------------------- + +def run_build(input_dir, output_dir, trigger, captioner, max_tier, face_pad, person_pad, + video_fps, clip_min_sec, clip_max_sec, image_format, clips_per_video, + body_region, body_region_frac): + if not input_dir or not output_dir: + yield "입력/출력 폴더를 모두 입력하세요." + return + cmd = [ + PYTHON, os.path.join(HERE, "build.py"), + "--input", input_dir, "--output", output_dir, "--trigger", trigger, + "--captioner", captioner, "--max-tier", max_tier, + "--face-pad", str(face_pad), "--person-pad", str(person_pad), + "--video-fps", str(video_fps), "--clip-min-sec", str(clip_min_sec), + "--clip-max-sec", str(clip_max_sec), "--image-format", image_format, + "--body-region", body_region, "--body-region-frac", str(body_region_frac), + "--clips-per-video", str(int(clips_per_video or 0)), + ] + yield from stream_subprocess(cmd) + + +def preview_gallery(output_dir): + img_dir = os.path.join(output_dir or "", "images") + if not os.path.isdir(img_dir): + return [] + files = sorted(f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png", ".webp"))) + return [os.path.join(img_dir, f) for f in files[:12]] + + +def build_dataset_tab(dataset_dir_handoff): + with gr.Tab("Dataset Builder"): + gr.Markdown("임의의 이미지/영상 폴더를 크롭+캡션 데이터셋으로 변환합니다.") + with gr.Row(): + input_dir = gr.Textbox(label="입력 폴더", placeholder=r"D:\raw_media") + output_dir = gr.Textbox(label="출력 폴더", placeholder=r"D:\wan_lora_dataset") + with gr.Row(): + trigger = gr.Textbox(label="트리거 워드", placeholder="ohwx man") + captioner = gr.Radio(["joycaption", "florence2", "wd14"], value="joycaption", label="캡셔너") + + with gr.Accordion("고급 설정", open=False): + with gr.Row(): + max_tier = gr.Radio(["480p", "720p", "1080p"], value="1080p", label="최대 해상도 티어") + image_format = gr.Radio(["jpg", "png"], value="jpg", label="이미지 저장 포맷") + with gr.Row(): + face_pad = gr.Number(value=2.2, label="얼굴 크롭 패딩") + person_pad = gr.Number(value=1.3, label="사람 크롭 패딩") + with gr.Row(): + body_region = gr.Radio( + ["auto", "upper", "lower"], value="auto", label="신체 부위 우선 크롭", + info="대부분 소스가 이미 특정 부위 클로즈업이면 upper/lower로 그쪽만 좁혀서 크롭", + ) + body_region_frac = gr.Number(value=0.45, label="부위 크롭 비율 (bbox 높이 중 남길 비율)") + with gr.Row(): + video_fps = gr.Number(value=16, label="영상 출력 FPS") + clip_min_sec = gr.Number(value=2.0, label="클립 최소 길이(초)") + clip_max_sec = gr.Number(value=6.0, label="클립 최대 길이(초)") + clips_per_video = gr.Number( + value=5, label="영상 1개당 클립 수 제한 (0이면 제한 없음)", + info="긴 영상(예: 5분)에서 씬 컷이 많이 잡혀 클립이 과하게 늘어날 때, " + "영상 전체 구간에 걸쳐 균등하게 N개만 뽑도록 제한합니다.", + ) + + build_btn = gr.Button("데이터셋 만들기", variant="primary") + build_log = gr.Textbox(label="로그", lines=12, max_lines=25, interactive=False) + gallery = gr.Gallery(label="결과 미리보기 (이미지)", columns=4, height=300) + send_btn = gr.Button("→ 학습 탭에 이 출력 폴더 채우기") + + build_btn.click( + run_build, + inputs=[input_dir, output_dir, trigger, captioner, max_tier, face_pad, person_pad, + video_fps, clip_min_sec, clip_max_sec, image_format, clips_per_video, + body_region, body_region_frac], + outputs=build_log, + ).then(preview_gallery, inputs=output_dir, outputs=gallery) + + send_btn.click(lambda d: d, inputs=output_dir, outputs=dataset_dir_handoff) + + +# --------------------------------------------------------------------------- +# Training tab +# --------------------------------------------------------------------------- + +def pick_default(choices, name_hint): + """Prefer a choice whose filename contains name_hint (e.g. 'low_noise'); + plain alphabetical order would otherwise default the low-noise dropdown + to the high_noise file since 'h' < 'l'.""" + for c in choices: + if name_hint in os.path.basename(c).lower(): + return c + return choices[0] if choices else None + + +def infer_mixed_precision(dit_low_path): + name = (dit_low_path or "").lower() + if "bf16" in name: + return "bf16" + return "fp16" + + +def run_train(dataset_dir, dit_low, dit_high, t5, vae, network_dim, learning_rate, + use_steps, max_train_steps, max_train_epochs, mixed_precision, + separate_passes, skip_cache, output_dir): + if not all([dataset_dir, dit_low, t5, vae, output_dir]): + yield "dataset-dir / DiT-low / T5 / VAE / output-dir는 필수입니다." + return + if not os.path.exists(VENV_PYTHON): + yield f"학습 환경(venv)이 없습니다: {VENV_PYTHON}\nSetup 탭에서 먼저 설치하세요." + return + cmd = [ + # train.py itself has no heavy deps, but running it with python_embeded + # (which on this machine is Python 3.13, sitting on a PATH with several + # other Python installs) leaked into the venv subprocess's inherited + # environment and broke stdlib C-extension loading in the venv (Python + # 3.10) with "Module use of python313.dll conflicts with this version + # of Python" -- confirmed via a real run through this UI. Running + # train.py with the venv's own interpreter keeps the whole chain + # self-consistent, matching every successful CLI run this session. + VENV_PYTHON, os.path.join(TRAIN_DIR, "train.py"), + "--dataset-dir", dataset_dir, "--dit-low", dit_low, "--t5", t5, "--vae", vae, + "--network-dim", str(int(network_dim)), "--learning-rate", str(learning_rate), + "--mixed-precision", mixed_precision, "--output-dir", output_dir, + ] + if dit_high: + cmd += ["--dit-high", dit_high] + if use_steps: + cmd += ["--max-train-steps", str(int(max_train_steps))] + else: + cmd += ["--max-train-epochs", str(int(max_train_epochs))] + if separate_passes: + cmd.append("--separate-passes") + if skip_cache: + cmd.append("--skip-cache") + yield from stream_subprocess(cmd) + + +def build_training_tab(dataset_dir_handoff): + with gr.Tab("Training"): + gr.Markdown( + "⚠️ ComfyUI를 켜 둔 채로 학습하면 VRAM이 부족할 수 있습니다 — 가능하면 " + "학습 중엔 ComfyUI 서버를 내려두세요." + ) + dataset_dir = gr.Textbox(label="데이터셋 폴더 (dataset.toml이 있는 곳)") + dataset_dir_handoff.change(lambda d: d, inputs=dataset_dir_handoff, outputs=dataset_dir) + + dit_choices = list_dit_choices() + t5_choices = list_t5_choices() + vae_choices = list_vae_choices() + with gr.Row(): + dit_low = gr.Dropdown(label="DiT (low-noise)", choices=dit_choices, + value=pick_default(dit_choices, "low_noise")) + dit_high = gr.Dropdown(label="DiT (high-noise, 선택)", choices=dit_choices, + value=pick_default(dit_choices, "high_noise")) + with gr.Row(): + t5 = gr.Dropdown(label="T5 텍스트 인코더", choices=t5_choices, + value=t5_choices[0] if t5_choices else None) + vae = gr.Dropdown(label="VAE", choices=vae_choices, + value=pick_default(vae_choices, "wan2.1_vae")) + refresh_ckpt_btn = gr.Button("체크포인트 목록 새로고침 (Setup 탭에서 다운로드한 뒤 눌러주세요)") + + with gr.Row(): + network_dim = gr.Number(value=32, label="LoRA rank (network_dim)") + learning_rate = gr.Number(value=2e-4, label="Learning rate") + mixed_precision = gr.Radio(["fp16", "bf16", "no"], value="fp16", label="Mixed precision") + + with gr.Row(): + use_steps = gr.Checkbox(label="스텝 수로 제한 (스모크 테스트용)", value=False) + max_train_steps = gr.Number(value=5, label="max_train_steps") + max_train_epochs = gr.Number(value=16, label="max_train_epochs") + + with gr.Row(): + separate_passes = gr.Checkbox(label="high/low 따로 학습 (파일 2개, 시간 2배)", value=False) + skip_cache = gr.Checkbox(label="캐싱 단계 건너뛰기 (이미 캐싱된 데이터셋)", value=False) + + output_dir = gr.Textbox(label="출력 폴더", placeholder=r"D:\wan_lora_output") + train_btn = gr.Button("학습 시작", variant="primary") + train_log = gr.Textbox(label="로그", lines=16, max_lines=30, interactive=False) + + def refresh_dit_low(): + c = list_dit_choices() + return gr.update(choices=c, value=pick_default(c, "low_noise")) + + def refresh_dit_high(): + c = list_dit_choices() + return gr.update(choices=c, value=pick_default(c, "high_noise")) + + def refresh_t5(): + c = list_t5_choices() + return gr.update(choices=c, value=c[0] if c else None) + + def refresh_vae(): + c = list_vae_choices() + return gr.update(choices=c, value=pick_default(c, "wan2.1_vae")) + + refresh_ckpt_btn.click(refresh_dit_low, outputs=dit_low) + refresh_ckpt_btn.click(refresh_dit_high, outputs=dit_high) + refresh_ckpt_btn.click(refresh_t5, outputs=t5) + refresh_ckpt_btn.click(refresh_vae, outputs=vae) + dit_low.change(infer_mixed_precision, inputs=dit_low, outputs=mixed_precision) + + train_btn.click( + run_train, + inputs=[dataset_dir, dit_low, dit_high, t5, vae, network_dim, learning_rate, + use_steps, max_train_steps, max_train_epochs, mixed_precision, + separate_passes, skip_cache, output_dir], + outputs=train_log, + ) + + +# --------------------------------------------------------------------------- + +def build_app(): + with gr.Blocks(title="Wan 2.2 LoRA Builder") as app: + gr.Markdown("# 🐬 Wan 2.2 LoRA Dataset + Training") + build_setup_tab() + dataset_dir_handoff = gr.Textbox(visible=False) + build_dataset_tab(dataset_dir_handoff) + build_training_tab(dataset_dir_handoff) + return app + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--port", type=int, default=7860) + return p.parse_args() + + +if __name__ == "__main__": + args = parse_args() + build_app().queue().launch(server_name="127.0.0.1", server_port=args.port, share=False) diff --git a/dolphin_tags_cache.json b/dolphin_tags_cache.json new file mode 100644 index 0000000000000000000000000000000000000000..b9963530381abfebfdb5091d164be2aba39e7634 --- /dev/null +++ b/dolphin_tags_cache.json @@ -0,0 +1,63 @@ +{ + "wan2.1-i2v-480p-penis-arousal-v1.0.safetensors::359305272::1754578081": [ + "penis becomes erect" + ], + "wan2.1-i2v-480p-flaccid-v1.0.safetensors::359305272::1753134483": [ + "penis", + "bounce", + "swing" + ], + "PENISLORA_22_i2v_HIGH_e320.safetensors::306807976::1759901296": [ + "PENISLORA" + ], + "PENISLORA_22_i2v_LOW_e496.safetensors::306807976::1759901312": [ + "PENISLORA" + ], + "wa22-4n4lfi5tin6-40epoc-low-k3nk.safetensors::306807976::1783607997": [ + "4n4lfi5tin6", + "4n4lfi5tin6, a close-up rear view of a woman engaging in anal fisting with her arm inside her anus." + ], + "wa22-4n4lfi5tin6-93epoc-high-k3nk.safetensors::306807976::1783607988": [ + "4n4lfi5tin6", + "4n4lfi5tin6, a close-up rear view of a person engaging in anal fisting." + ], + "DR34ML4Y_I2V_14B_HIGH_V2.safetensors::306807976::1767355819": [ + "m15510n4ry", + "bl0wj0b", + "c0wg1rl", + "d0gg1e", + "d0ubl3_bj" + ], + "DR34ML4Y_I2V_14B_LOW_V2.safetensors::306807976::1767355809": [ + "m15510n4ry", + "bl0wj0b", + "d0gg1e", + "c0wg1rl", + "d0ubl3_bj" + ], + "sfbehind_v2.1_high_noise.safetensors::306808416::1767009289": [ + "sfb3hind, sfbehind" + ], + "sfbehind_v2.1_low_noise.safetensors::306808416::1767009298": [ + "sfb3hind, sfbehind" + ], + "side-doggy-wan22-hn-40.safetensors::306807976::1779664874": [ + "side view doggystyle sex" + ], + "side-doggy-wan22-ln-57.safetensors::306807976::1779664861": [ + "side view doggystyle sex" + ], + "Pornmaster_wan 2.2_14b_I2V_Creampie_v1_high_noise.safetensors::306808560::1762906783": [ + "A woman, the camera quickly pulls back to reveal her vagina, with sperm on her vagina, and the sperm continues to drip down from her vagina," + ], + "creampie_wan22_e50_low.safetensors::306807976::1758341193": [ + "creampie" + ], + "erect_penis_epoch_80.safetensors::306807976::1744443760": [ + "penis" + ], + "masturbation_cumshot_v1.1_e310.safetensors::359257680::1747718457": [ + "which lands on her {thighs|pelvis|stomach|face|breasts}", + "She moves her hand rapidly up and down the shaft of her penis and she ejaculates white sticky liquid from her penis" + ] +} \ No newline at end of file diff --git a/promp_logic_v28_stitch.py b/promp_logic_v28_stitch.py new file mode 100644 index 0000000000000000000000000000000000000000..51c019b75ac242b009de78f0b8c5a2f34db803a0 --- /dev/null +++ b/promp_logic_v28_stitch.py @@ -0,0 +1,128 @@ +import os +import random +import re +import json +import urllib.request +import urllib.error + +class DolphinMultiActionPromptNode_V28: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mode": (["🤖 Auto LLM (Timeline)", "✍️ Manual Override (수동)"], {"default": "🤖 Auto LLM (Timeline)"}), + "character_name": ("STRING", {"multiline": False, "default": "AUTO"}), + "tagger_context": ("STRING", {"multiline": True, "default": ""}), + "artistic_vibe": ("STRING", {"multiline": True, "default": "cinematic lighting, dynamic motion"}), + + "action_part_1": ("STRING", {"multiline": True, "default": "문을 열고 뛰어든다."}), + "action_part_2": ("STRING", {"multiline": True, "default": "무릎을 꿇고 방어한다."}), + "action_part_3": ("STRING", {"multiline": True, "default": "파이프를 집어 던진다."}), + "action_part_4": ("STRING", {"multiline": True, "default": "쓰러진 적을 바라보며 숨을 고른다."}), + + "openrouter_api_key": ("STRING", {"multiline": False, "default": ""}), + "openrouter_model": ("STRING", {"multiline": False, "default": "meta-llama/llama-3.3-70b-instruct"}), + # 🔥 [추천] AI가 너무 앞서간다면 이 수치를 0.65 ~ 0.7 정도로 낮춰보세요. + "creativity": ("FLOAT", {"default": 0.85, "min": 0.1, "max": 1.5, "step": 0.05}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("prompt_1 (Clip 1: 0-5s)", "prompt_2 (Clip 2: 0-5s)", "prompt_3 (Clip 3: 0-5s)", "prompt_4 (Clip 4: 0-5s)") + FUNCTION = "generate_sequence" + CATEGORY = "Dolphin" + + def generate_sequence(self, mode, character_name, tagger_context, artistic_vibe, action_part_1, action_part_2, action_part_3, action_part_4, openrouter_api_key, openrouter_model, creativity, seed): + + user_defined_name = "" if character_name.upper() in ["AUTO", ""] else character_name.strip() + + # 🔥 [줄바꿈 로직] 윗줄 태그와 아랫줄 자연어 액션을 엔터(\n)로 깔끔하게 분리 + def build_final(action, raw_tags, master_scene, name): + tags_list = [] + if name: tags_list.append(name) + if raw_tags.strip(): tags_list.append(raw_tags.strip().strip(",. ")) + tag_block = ", ".join(tags_list) + + sentence_list = [] + if master_scene.strip(): sentence_list.append(master_scene.strip().strip(",. ")) + if action.strip(): sentence_list.append(action.strip()) + sentence_block = " ".join(sentence_list) + + if tag_block and sentence_block: + return f"{tag_block}\n{sentence_block}" + elif tag_block: + return tag_block + else: + return sentence_block + + if mode == "✍️ Manual Override (수동)": + return ( + build_final(action_part_1, tagger_context, "", user_defined_name), + build_final(action_part_2, tagger_context, "", user_defined_name), + build_final(action_part_3, tagger_context, "", user_defined_name), + build_final(action_part_4, tagger_context, "", user_defined_name) + ) + + random.seed(seed) + + # 🔥 [완급조절 & 안티-슬로우모션 융합] 미래 액션 스포일러 방지 및 역동성 강제 주입 + sys_prompt = ( + "You are an Elite Cinematic Action Choreographer.\n" + f"1. CHARACTER: If NAME is 'AUTO', assign a culturally appropriate name based on TAGS. If '{user_defined_name}', use it.\n" + "2. INITIAL POSTURE: Analyze TAGS for the character's starting physical state. State it in 1 short sentence.\n" + "3. MASTER SCENE: A vivid 1-sentence environment description based on TAGS and VIBE.\n" + "4. CHOREOGRAPHY & PACING (CRITICAL): You will output 4 Parts. \n" + " - You MUST strictly map [BASE ACTION 1] ONLY to PART 1, [BASE ACTION 2] ONLY to PART 2, etc.\n" + " - DO NOT anticipate, rush, or perform future actions in earlier parts. Keep the pacing deliberate.\n" + " - 🔥 ANTI-SLOW MOTION RULE: Video models default to slow-motion. You MUST inject aggressive kinetic adverbs (e.g., swiftly, rapidly, suddenly, explosively) into EVERY action description to force real-time, fast-paced movement.\n" + " - Each Part is an independent 5-second video clip. Timestamps MUST reset to [0-1s], [2-3s], [4-5s] for EACH Part.\n" + "Format EXACTLY:\nCHARACTER: [Name]\nINITIAL POSTURE: [State]\nMASTER SCENE: [Description]\n" + "PART 1: [0-1s] [Action A] [2-3s] [Action B] [4-5s] [Action C]\n" + "PART 2: [0-1s] [Action D] [2-3s] [Action E] [4-5s] [Action F]\n" + "PART 3: [0-1s] [Action G] [2-3s] [Action H] [4-5s] [Action I]\n" + "PART 4: [0-1s] [Action J] [2-3s] [Action K] [4-5s] [Action L]" + ) + + # 유저 입력을 BASE ACTION 으로 명확히 라벨링하여 전송 + usr_prompt = f"NAME: {character_name}\nTAGS: {tagger_context}\nVIBE: {artistic_vibe}\n[BASE ACTION 1]: {action_part_1}\n[BASE ACTION 2]: {action_part_2}\n[BASE ACTION 3]: {action_part_3}\n[BASE ACTION 4]: {action_part_4}" + + url = "https://openrouter.ai/api/v1/chat/completions" + payload = {"model": openrouter_model.strip(), "messages": [{"role": "system", "content": sys_prompt}, {"role": "user", "content": usr_prompt}], "temperature": creativity} + + try: + req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers={'Authorization': f'Bearer {openrouter_api_key}', 'Content-Type': 'application/json'}) + response = urllib.request.urlopen(req, timeout=120) + llm_prompt = json.loads(response.read().decode('utf-8'))['choices'][0]['message']['content'].strip() + print(f"\n✅ [Dolphin V28 - Stitching Clips]\n{llm_prompt}\n") + except Exception as e: + print(f"❌ [에러] {e}") + llm_prompt = "" + + final_char_name = user_defined_name + r_master, p1, p2, p3, p4 = "", action_part_1, action_part_2, action_part_3, action_part_4 + + if llm_prompt and "[removed]" not in llm_prompt: + cl = re.sub(r'[*#]', '', llm_prompt) + m_char = re.search(r'CHARACTER:\s*(.*?)(?=INITIAL POSTURE|$)', cl, re.I | re.S) + m_master = re.search(r'MASTER SCENE:\s*(.*?)(?=PART 1|$)', cl, re.I | re.S) + m1 = re.search(r'PART 1:\s*(.*?)(?=PART 2|$)', cl, re.I | re.S) + m2 = re.search(r'PART 2:\s*(.*?)(?=PART 3|$)', cl, re.I | re.S) + m3 = re.search(r'PART 3:\s*(.*?)(?=PART 4|$)', cl, re.I | re.S) + m4 = re.search(r'PART 4:\s*(.*)', cl, re.I | re.S) + + if not user_defined_name and m_char: + final_char_name = m_char.group(1).strip() + + r_master = m_master.group(1).strip() if m_master else "" + p1 = m1.group(1).strip() if m1 else action_part_1 + p2 = m2.group(1).strip() if m2 else action_part_2 + p3 = m3.group(1).strip() if m3 else action_part_3 + p4 = m4.group(1).strip() if m4 else action_part_4 + + return ( + build_final(p1, tagger_context, r_master, final_char_name), + build_final(p2, tagger_context, r_master, final_char_name), + build_final(p3, tagger_context, r_master, final_char_name), + build_final(p4, tagger_context, r_master, final_char_name) + ) \ No newline at end of file diff --git a/promp_logic_v30_story.py b/promp_logic_v30_story.py new file mode 100644 index 0000000000000000000000000000000000000000..e1b32c1de5f86a6957b2133b7db709d81731a2fb --- /dev/null +++ b/promp_logic_v30_story.py @@ -0,0 +1,176 @@ +import os +import random +import re +import json +import urllib.request +import urllib.error + +class DolphinMultiActionPromptNode_V30: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mode": (["🤖 Auto LLM (Story Splitter)", "✍️ Manual Override (수동)"], {"default": "🤖 Auto LLM (Story Splitter)"}), + "character_name": ("STRING", {"multiline": False, "default": "AUTO"}), + "tagger_context": ("STRING", {"multiline": True, "default": ""}), + "artistic_vibe": ("STRING", {"multiline": True, "default": "cinematic lighting, dynamic motion"}), + + "master_story": ("STRING", { + "multiline": True, + "default": "어두운 골목길. 갑자기 나타난 적들을 향해 돌진한다, 화려하게 검을 휘둘러 적을 쓰러뜨린다, 날아오는 총알을 튕겨낸다, 적에게 다가가 숨통을 끊는다." + }), + + "openrouter_api_key": ("STRING", {"multiline": False, "default": ""}), + "openrouter_model": ("STRING", {"multiline": False, "default": "meta-llama/llama-3.3-70b-instruct"}), + "creativity": ("FLOAT", {"default": 0.85, "min": 0.1, "max": 1.5, "step": 0.05}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("prompt_1 (Clip 1: 0-5s)", "prompt_2 (Clip 2: 0-5s)", "prompt_3 (Clip 3: 0-5s)", "prompt_4 (Clip 4: 0-5s)") + FUNCTION = "generate_sequence" + CATEGORY = "Dolphin" + + def generate_sequence(self, mode, character_name, tagger_context, artistic_vibe, master_story, openrouter_api_key, openrouter_model, creativity, seed): + + user_defined_name = "" if character_name.upper() in ["AUTO", ""] else character_name.strip() + + def build_final(action, raw_tags, master_scene, name): + tags_list = [] + if name: tags_list.append(name) + if raw_tags.strip(): tags_list.append(raw_tags.strip().strip(",. ")) + tag_block = ", ".join(tags_list) + + sentence_list = [] + if master_scene.strip(): sentence_list.append(master_scene.strip().strip(",. ")) + if action.strip(): sentence_list.append(action.strip()) + sentence_block = " ".join(sentence_list) + + if tag_block and sentence_block: + return f"{tag_block}\n{sentence_block}" + elif tag_block: + return tag_block + else: + return sentence_block + + if mode == "✍️ Manual Override (수동)": + return ( + build_final(master_story, tagger_context, "", user_defined_name), + build_final(master_story, tagger_context, "", user_defined_name), + build_final(master_story, tagger_context, "", user_defined_name), + build_final(master_story, tagger_context, "", user_defined_name) + ) + + random.seed(seed) + + # 1차: 마침표, 쉼표, 느낌표, 줄바꿈 기준으로 확실하게 쪼갬 + clean_story = re.sub(r'([.!?,\n])', r'\1|', master_story) + raw_sentences = clean_story.split('|') + sentences = [s.strip() for s in raw_sentences if len(s.strip()) > 1] + + # 🔥 [핵심 로직] 문장 개수에 따른 '시네마틱 호흡(Pacing)' 자동 설계 + + if len(sentences) == 1: + # 단 1개의 문장: 1~3번은 분위기 빌드업, 4번에 폭발! + chunk_1 = f"Atmospheric Build-up: Focus STRICTLY on the environment, lighting, and vibe. Character is mostly still. DO NOT execute the main action yet. (Upcoming target: {sentences[0]})" + chunk_2 = f"Rising Tension: Subtle character movements or camera panning. Anticipation builds. DO NOT execute the main action yet. (Upcoming target: {sentences[0]})" + chunk_3 = f"Pre-action Wind-up: Extreme tension right before the strike. Character prepares to move. DO NOT execute yet. (Upcoming target: {sentences[0]})" + chunk_4 = f"CLIMAX: Explosively and swiftly execute this exact action -> {sentences[0]}" + + elif len(sentences) == 2: + # 2개의 문장: 1번 빌드업 -> 2번 첫 액션 -> 3번 호흡 고르기 -> 4번 피니시 + chunk_1 = f"Atmospheric Build-up: Focus strictly on the environment and tension. DO NOT execute the action yet. (Upcoming target: {sentences[0]})" + chunk_2 = f"Phase 1 Action: Execute this action -> {sentences[0]}" + chunk_3 = f"Tension / Transition: Character prepares for the final blow. (Upcoming target: {sentences[1]})" + chunk_4 = f"CLIMAX Action: Explosively execute this action -> {sentences[1]}" + + elif len(sentences) == 3: + # 3개의 문장: 첫 문장을 10초(1,2번 파트)에 걸쳐 부드럽게 배분 + chunk_1 = f"Phase 1: Start this action slowly -> {sentences[0]}" + chunk_2 = f"Phase 2: Complete this action -> {sentences[0]}" + chunk_3 = sentences[1] + chunk_4 = sentences[2] + + else: + # 4개 이상: 기존의 수학적 4등분 배분 + k, m = divmod(len(sentences), 4) + chunks = [] + start = 0 + for i in range(4): + end = start + k + (1 if i < m else 0) + chunks.append(" ".join(sentences[start:end])) + start = end + chunk_1, chunk_2, chunk_3, chunk_4 = chunks + + sys_prompt = ( + "You are an Elite Cinematic Action Choreographer.\n" + f"1. CHARACTER: If NAME is 'AUTO', assign a culturally appropriate name based on TAGS. If '{user_defined_name}', use it.\n" + "2. INITIAL POSTURE: Analyze TAGS for the character's starting physical state. State it in 1 short sentence.\n" + "3. MASTER SCENE: A vivid 1-sentence environment description based on TAGS and VIBE.\n" + "4. STRICT CHOREOGRAPHY ISOLATION (CRITICAL):\n" + " - You will receive 4 explicit target events. You MUST translate them into English cinematic actions.\n" + " - FATAL ERROR PREVENTION: You MUST rigidly confine the actions. DO NOT animate Part 2, 3, or 4's event in Part 1. Keep them completely isolated in time.\n" + " - 🔥 OUTPUT RULE: DO NOT quote the original Korean text. ONLY output the English descriptions.\n" + " - 🔥 ANTI-SLOW MOTION RULE: Inject aggressive kinetic adverbs (swiftly, suddenly, explosively) into EVERY action description.\n" + " - Each Part MUST reset timestamps to [0-1s], [2-3s], [4-5s].\n" + "Format EXACTLY:\nCHARACTER: [Name]\nINITIAL POSTURE: [State]\nMASTER SCENE: [Description]\n" + "PART 1: [0-1s] [Action A] [2-3s] [Action B] [4-5s] [Action C]\n" + "PART 2: [0-1s] [Action D] [2-3s] [Action E] [4-5s] [Action F]\n" + "PART 3: [0-1s] [Action G] [2-3s] [Action H] [4-5s] [Action I]\n" + "PART 4: [0-1s] [Action J] [2-3s] [Action K] [4-5s] [Action L]" + ) + + usr_prompt = ( + f"NAME: {character_name}\nTAGS: {tagger_context}\nVIBE: {artistic_vibe}\n\n" + "=== ACTION ISOLATION SCRIPT ===\n" + f"▶ For PART 1 (0-5s), you MUST ONLY animate this specific event: \"{chunk_1}\"\n" + f"▶ For PART 2 (5-10s), you MUST ONLY animate this specific event: \"{chunk_2}\"\n" + f"▶ For PART 3 (10-15s), you MUST ONLY animate this specific event: \"{chunk_3}\"\n" + f"▶ For PART 4 (15-20s), you MUST ONLY animate this specific event: \"{chunk_4}\"\n" + "CRITICAL REMINDER: Do NOT allow any actions from Part 2, 3, or 4 to bleed into Part 1!" + ) + + url = "https://openrouter.ai/api/v1/chat/completions" + payload = {"model": openrouter_model.strip(), "messages": [{"role": "system", "content": sys_prompt}, {"role": "user", "content": usr_prompt}], "temperature": creativity} + + try: + req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers={'Authorization': f'Bearer {openrouter_api_key}', 'Content-Type': 'application/json'}) + response = urllib.request.urlopen(req, timeout=120) + llm_prompt = json.loads(response.read().decode('utf-8'))['choices'][0]['message']['content'].strip() + print(f"\n✅ [Dolphin V30 - Cinematic Pacing Applied]\n{llm_prompt}\n") + except Exception as e: + print(f"❌ [에러] {e}") + llm_prompt = "" + + final_char_name = user_defined_name + r_master, p1, p2, p3, p4 = "", "", "", "", "" + + if llm_prompt and "[removed]" not in llm_prompt: + cl = re.sub(r'[*#]', '', llm_prompt) + m_char = re.search(r'CHARACTER:\s*(.*?)(?=INITIAL POSTURE|$)', cl, re.I | re.S) + m_master = re.search(r'MASTER SCENE:\s*(.*?)(?=PART 1|$)', cl, re.I | re.S) + + m1 = re.search(r'PART 1:\s*(.*?)(?=PART 2|$)', cl, re.I | re.S) + m2 = re.search(r'PART 2:\s*(.*?)(?=PART 3|$)', cl, re.I | re.S) + m3 = re.search(r'PART 3:\s*(.*?)(?=PART 4|$)', cl, re.I | re.S) + m4 = re.search(r'PART 4:\s*(.*?)(?=\n\n|===|Note:|$)', cl, re.I | re.S) + + if not user_defined_name and m_char: + final_char_name = m_char.group(1).strip() + + r_master = m_master.group(1).strip() if m_master else "" + + p1 = m1.group(1).strip() if m1 else "⚠️ Formatting Error: Part 1" + p2 = m2.group(1).strip() if m2 else "⚠️ Formatting Error: Part 2" + p3 = m3.group(1).strip() if m3 else "⚠️ Formatting Error: Part 3" + p4 = m4.group(1).strip() if m4 else "⚠️ Formatting Error: Part 4" + else: + p1 = p2 = p3 = p4 = "⚠️ API Error: LLM request failed." + + return ( + build_final(p1, tagger_context, r_master, final_char_name), + build_final(p2, tagger_context, r_master, final_char_name), + build_final(p3, tagger_context, r_master, final_char_name), + build_final(p4, tagger_context, r_master, final_char_name) + ) \ No newline at end of file diff --git a/promp_logic_v32_story.py b/promp_logic_v32_story.py new file mode 100644 index 0000000000000000000000000000000000000000..c5d23cb4d7c4c7ed14524c7a43029f99323232a2 --- /dev/null +++ b/promp_logic_v32_story.py @@ -0,0 +1,275 @@ +import re +import json +import urllib.request +import urllib.error + + +# ====================================================================== +# OpenRouter 모델 프리셋 (NSFW/무삭제 대응) +# 라벨 -> 실제 슬러그. '⚙️ custom (아래 텍스트 사용)' 선택 시 텍스트 필드값 사용. +# 슬러그는 OpenRouter 기준으로 검증됨. 변경/신모델은 custom 으로 입력 가능. +# ====================================================================== +MODEL_PRESETS = { + "🔞 DeepSeek V3.2 (무삭제·JSON안정·추천)": "deepseek/deepseek-v3.2", + "🔞 DeepSeek V3 0324 (안정 버전)": "deepseek/deepseek-chat-v3-0324", + "🔞 Magnum v4 72B (표현 자연스러움)": "anthracite-org/magnum-v4-72b", + "🔞 Cydonia 24B v4.1 (경량·빠름·131K)": "thedrummer/cydonia-24b-v4.1", + # 2026-09-03: :free 슬러그가 404 됨 - 유료로 교체. + "🔞 Dolphin Mistral 24B Venice (무삭제)": "cognitivecomputations/dolphin-mistral-24b-venice-edition", + "🔞 Lumimaid 70B (RP 무삭제)": "neversleep/llama-3-lumimaid-70b", + # 2026-09-03 추가 (모델 페이지로만 확인, chat completions 스모크테스트 미완). + "🔞 Hermes 4 405B (스티어러블·포맷 엄격)": "nousresearch/hermes-4-405b", + # 2026-09-03: grok-4 / grok-4-fast deprecated(404) - grok-4.3으로 교체. + "🔞 Grok 4.3 (엣지·$1.25/$2.5)": "x-ai/grok-4.3", + "🔞 Euryale 70B L3.3 (감각적 산문·포맷 느슨)": "sao10k/l3.3-euryale-70b", + "SFW Llama 3.3 70B (정렬 강함)": "meta-llama/llama-3.3-70b-instruct", + "⚙️ custom (아래 텍스트 사용)": "__custom__", +} +MODEL_PRESET_LABELS = list(MODEL_PRESETS.keys()) + + +def resolve_model(preset_label, custom_text): + """프리셋 라벨과 커스텀 텍스트에서 실제 사용할 슬러그를 결정.""" + slug = MODEL_PRESETS.get(preset_label, "__custom__") + if slug == "__custom__": + return custom_text.strip() or "deepseek/deepseek-v3.2" + return slug + + +class DolphinMultiActionPromptNode_V32: + """ + 4개 연속 클립(각 5초, 총 20초)을 위한 시네마틱 프롬프트 분배 노드. + + 설계 원칙 (V30 대비 변경점): + - 서사 -> 4샷 분배를 LLM이 직접 판단한다. (문장 개수 하드코딩 pacing 분기 제거) + - 타임코드([0-1s] 등) 강제 삽입 제거. WAN I2V에서 노이즈만 됨. + - LLM은 JSON으로만 응답하게 강제 -> 파싱이 정규식보다 훨씬 견고. + - 파싱 실패 시 1회 자동 재시도, 그래도 실패하면 로컬 균등 분배로 폴백. + - seed를 실제 payload에 전달하여 재현성 확보. + """ + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mode": (["🤖 Auto LLM (Story Splitter)", "✍️ Manual Override (수동)"], + {"default": "🤖 Auto LLM (Story Splitter)"}), + "character_name": ("STRING", {"multiline": False, "default": "AUTO"}), + "tagger_context": ("STRING", {"multiline": True, "default": ""}), + "artistic_vibe": ("STRING", {"multiline": True, + "default": "cinematic lighting, dynamic motion"}), + "master_story": ("STRING", { + "multiline": True, + "default": ("어두운 골목길. 갑자기 나타난 적들을 향해 돌진한다, " + "화려하게 검을 휘둘러 적을 쓰러뜨린다, 날아오는 총알을 튕겨낸다, " + "적에게 다가가 숨통을 끊는다.") + }), + "openrouter_api_key": ("STRING", {"multiline": False, "default": ""}), + "model_preset": (MODEL_PRESET_LABELS, + {"default": "🔞 DeepSeek V3.2 (무삭제·JSON안정·추천)"}), + "openrouter_model": ("STRING", {"multiline": False, + "default": "deepseek/deepseek-v3.2"}), + "creativity": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 1.5, "step": 0.05}), + "pacing": (["auto", "slow-burn (1 beat)", "balanced (multi beat)", "rapid (4 distinct)"], + {"default": "auto"}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("prompt_1", "prompt_2", "prompt_3", "prompt_4", "debug_info") + FUNCTION = "generate_sequence" + CATEGORY = "Dolphin" + + # ------------------------------------------------------------------ helpers + + def _build_final(self, action, raw_tags, master_scene, name): + """태그 블록 + 문장 블록을 한 프롬프트로 합친다. (V30 build_final 계승)""" + tags = [] + if name: + tags.append(name) + if raw_tags.strip(): + tags.append(raw_tags.strip().strip(",. ")) + tag_block = ", ".join(tags) + + sents = [] + if master_scene.strip(): + sents.append(master_scene.strip().strip(",. ")) + if action.strip(): + sents.append(action.strip()) + sentence_block = " ".join(sents) + + if tag_block and sentence_block: + return f"{tag_block}\n{sentence_block}" + return tag_block or sentence_block + + def _local_split(self, story): + """LLM 폴백용: 의미 단위(마침표/느낌표/줄바꿈 우선, 쉼표는 보조)로 4등분.""" + primary = re.split(r'(?<=[.!?。])\s+|\n+', story) + primary = [p.strip() for p in primary if len(p.strip()) > 1] + if len(primary) < 4: + expanded = [] + for p in primary: + expanded.extend([c.strip() for c in p.split(',') if len(c.strip()) > 1]) + primary = expanded or [story.strip()] + + n = len(primary) + if n == 0: + return ["", "", "", ""] + k, m = divmod(n, 4) + chunks, idx = [], 0 + for i in range(4): + end = idx + k + (1 if i < m else 0) + chunks.append(" ".join(primary[idx:end]).strip()) + idx = end + if n < 4: + filled = [c for c in chunks if c] + target = filled[-1] if filled else story.strip() + build = [ + f"Atmospheric build-up before the action. (upcoming: {target})", + f"Rising tension, subtle movement. (upcoming: {target})", + ] + combined = build + filled + if len(combined) >= 4: + chunks = combined[-4:] + else: + chunks = combined + [target] * (4 - len(combined)) + return chunks[:4] + + def _pacing_hint(self, pacing): + return { + "auto": "Decide pacing yourself based on how many distinct beats the story contains.", + "slow-burn (1 beat)": ("The story is essentially ONE climactic beat. Spend clips 1-3 on " + "atmosphere, tension and wind-up; detonate the action only in clip 4."), + "balanced (multi beat)": ("Distribute the beats so early clips build and later clips " + "deliver, keeping motion continuous across the 20 seconds."), + "rapid (4 distinct)": ("Treat the story as 4 distinct consecutive actions, one per clip, " + "each kinetic and self-contained."), + }.get(pacing, "Decide pacing yourself.") + + def _call_llm(self, url, payload, api_key, timeout=120): + req = urllib.request.Request( + url, data=json.dumps(payload).encode('utf-8'), + headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}) + resp = urllib.request.urlopen(req, timeout=timeout) + data = json.loads(resp.read().decode('utf-8')) + return data['choices'][0]['message']['content'].strip() + + def _extract_json(self, text): + """LLM 응답에서 JSON 객체만 안전하게 추출.""" + if not text: + return None + text = re.sub(r'```(?:json)?', '', text).strip() + start = text.find('{') + end = text.rfind('}') + if start == -1 or end == -1 or end <= start: + return None + blob = text[start:end + 1] + try: + return json.loads(blob) + except json.JSONDecodeError: + blob2 = re.sub(r',\s*([}\]])', r'\1', blob) + try: + return json.loads(blob2) + except json.JSONDecodeError: + return None + + # ------------------------------------------------------------------ main + + def generate_sequence(self, mode, character_name, tagger_context, artistic_vibe, + master_story, openrouter_api_key, model_preset, openrouter_model, + creativity, pacing, seed): + + # 프리셋 라벨 -> 실제 슬러그 (custom 이면 텍스트 필드 사용) + openrouter_model = resolve_model(model_preset, openrouter_model) + + user_name = "" if character_name.strip().upper() in ["AUTO", ""] else character_name.strip() + + # ---- Manual override: 4칸 동일 출력 ---- + if mode == "✍️ Manual Override (수동)": + p = self._build_final(master_story, tagger_context, "", user_name) + return (p, p, p, p, "manual override") + + # ---- API 키 검증 ---- + api_key = openrouter_api_key.strip() + if not api_key: + fb = self._local_split(master_story) + outs = tuple(self._build_final(c, tagger_context, "", user_name) for c in fb) + return outs + ("⚠️ No API key. Used local fallback split.",) + + # ---- 프롬프트 구성 (JSON 강제) ---- + sys_prompt = ( + "You are an elite cinematic action choreographer and shot planner.\n" + "You will be given a short story (any language), character tags, and a visual vibe.\n" + "Your job: turn it into a 4-shot sequence (four consecutive 5-second video clips, 20s total).\n\n" + "RULES:\n" + "1. CHARACTER: if the name is 'AUTO', invent a culturally appropriate name from the tags; " + "otherwise use the given name.\n" + "2. MASTER_SCENE: one vivid English sentence describing the environment/lighting, " + "derived from tags + vibe. Stays constant across all 4 clips.\n" + "3. SHOT SPLIT: YOU decide how to distribute the narrative across the 4 clips. " + "Keep each clip's action temporally isolated - do NOT let a later clip's action appear early.\n" + "4. Never quote the original non-English text. Output English cinematic descriptions only.\n" + "5. Inject kinetic adverbs (swiftly, suddenly, explosively) - avoid unintended slow motion.\n" + "6. Do NOT add timestamps like [0-1s]; write natural continuous action prose per clip.\n\n" + "OUTPUT: respond with ONLY a JSON object, no prose, no code fences:\n" + '{"character": "...", "master_scene": "...", ' + '"clip_1": "...", "clip_2": "...", "clip_3": "...", "clip_4": "..."}' + ) + + usr_prompt = ( + f"NAME: {character_name}\n" + f"TAGS: {tagger_context}\n" + f"VIBE: {artistic_vibe}\n" + f"PACING DIRECTIVE: {self._pacing_hint(pacing)}\n\n" + f"STORY:\n{master_story}\n" + ) + + url = "https://openrouter.ai/api/v1/chat/completions" + payload = { + "model": openrouter_model.strip(), + "messages": [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": usr_prompt}, + ], + "temperature": creativity, + "response_format": {"type": "json_object"}, + } + if seed: + payload["seed"] = seed + + parsed, debug = None, "" + for attempt in (1, 2): + try: + raw = self._call_llm(url, payload, api_key) + parsed = self._extract_json(raw) + if parsed and all(parsed.get(k) for k in + ("clip_1", "clip_2", "clip_3", "clip_4")): + debug = f"✅ LLM ok (attempt {attempt})" + break + debug = f"⚠️ Parse failed (attempt {attempt}), retrying..." + except (urllib.error.URLError, urllib.error.HTTPError, + TimeoutError, KeyError, ValueError) as e: + debug = f"❌ API error (attempt {attempt}): {e}" + payload["temperature"] = min(creativity, 0.4) + + # ---- 성공: LLM 결과 조립 ---- + if parsed and all(parsed.get(k) for k in ("clip_1", "clip_2", "clip_3", "clip_4")): + final_name = user_name or str(parsed.get("character", "")).strip() + master = str(parsed.get("master_scene", "")).strip() + clips = [str(parsed[f"clip_{i}"]).strip() for i in range(1, 5)] + outs = tuple(self._build_final(c, tagger_context, master, final_name) for c in clips) + print(f"\n✅ [Dolphin V32] {debug}\nCHAR: {final_name}\nSCENE: {master}") + for i, c in enumerate(clips, 1): + print(f" CLIP {i}: {c}") + return outs + (f"{debug} | char={final_name}",) + + # ---- 실패: 로컬 폴백 ---- + fb = self._local_split(master_story) + outs = tuple(self._build_final(c, tagger_context, "", user_name) for c in fb) + print(f"\n⚠️ [Dolphin V32] LLM failed, local fallback. {debug}") + return outs + (f"⚠️ Fallback used. {debug}",) + + +NODE_CLASS_MAPPINGS = {"DolphinMultiActionPromptNode_V32": DolphinMultiActionPromptNode_V32} +NODE_DISPLAY_NAME_MAPPINGS = {"DolphinMultiActionPromptNode_V32": "🐬 Dolphin Story Splitter V32"} diff --git a/promp_logic_v32_vision.py b/promp_logic_v32_vision.py new file mode 100644 index 0000000000000000000000000000000000000000..aaab62c6719ee577ef0deb1b5f3487579473a14b --- /dev/null +++ b/promp_logic_v32_vision.py @@ -0,0 +1,247 @@ +import os +import random +import re +import json +import urllib.request +import urllib.error +import base64 +import io +import time +import numpy as np +from PIL import Image +import torch + + +class DolphinMultiActionPromptNode_V32: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "mode": (["🤖 Auto Vision+LLM", "✍️ Manual Override"], {"default": "🤖 Auto Vision+LLM"}), + "character_name": ("STRING", {"multiline": False, "default": "AUTO"}), + "artistic_vibe": ("STRING", {"multiline": True, "default": "cinematic lighting, high-speed action, dark fantasy"}), + + "master_story": ("STRING", { + "multiline": True, + "default": "어두운 골목길. 갑자기 나타난 적들을 향해 돌진한다, 화려하게 검을 휘둘러 적을 쓰러뜨린다, 날아오는 총알을 튕겨낸다, 적에게 다가가 숨통을 끊는다." + }), + + "openrouter_api_key": ("STRING", {"multiline": False, "default": ""}), + "openrouter_model": ("STRING", {"multiline": False, "default": "qwen/qwen-2-vl-72b-instruct"}), + "creativity": ("FLOAT", {"default": 0.85, "min": 0.1, "max": 1.5, "step": 0.05}), + "max_tokens": ("INT", {"default": 1500, "min": 256, "max": 8192, "step": 64}), + "retries": ("INT", {"default": 2, "min": 0, "max": 5}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ( + "prompt_1 (Clip 1: 0-5s)", + "prompt_2 (Clip 2: 0-5s)", + "prompt_3 (Clip 3: 0-5s)", + "prompt_4 (Clip 4: 0-5s)", + "raw_llm_output", + ) + FUNCTION = "generate_sequence" + CATEGORY = "Dolphin" + + # ----------------------------------------------------------------- + def _encode_image(self, image): + img_tensor = image[0] + i = 255. * img_tensor.cpu().numpy() + img_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + if img_pil.mode != "RGB": + img_pil = img_pil.convert("RGB") + buffered = io.BytesIO() + img_pil.save(buffered, format="JPEG", quality=90) + b64 = base64.b64encode(buffered.getvalue()).decode('utf-8') + return f"data:image/jpeg;base64,{b64}" + + def _split_sentences(self, master_story): + clean_story = re.sub(r'([.!?,\n])', r'\1|', master_story) + raw_sentences = clean_story.split('|') + return [s.strip() for s in raw_sentences if len(s.strip()) > 1] + + def _build_chunks(self, sentences): + n = len(sentences) + if n == 0: + base = "dynamic high-speed action" + return (base, base, base, base) + if n == 1: + s = sentences[0] + return ( + f"Phase 1: Rapid approach and high-speed dynamic movement. DO NOT stand still. (Target: {s})", + f"Phase 2: Swift, explosive execution of the action. (Target: {s})", + f"Phase 3: The climax at full 1x real-time speed. Lightning fast! (Target: {s})", + f"Phase 4: Fast-paced completion and quick recovery. (Target: {s})", + ) + if n == 2: + return ( + f"Phase 1: High-speed buildup and rapid preparation. (Target: {sentences[0]})", + f"Phase 2: Explosively execute -> {sentences[0]}", + f"Phase 3: Rapid transition, sprinting or moving quickly. (Target: {sentences[1]})", + f"Phase 4: Lightning-fast execution -> {sentences[1]}", + ) + if n == 3: + return ( + f"Phase 1: Start this action rapidly -> {sentences[0]}", + f"Phase 2: Explosively complete -> {sentences[0]}", + sentences[1], + sentences[2], + ) + # n >= 4: 균등 분배 + k, m = divmod(n, 4) + chunks = [] + start = 0 + for idx in range(4): + end = start + k + (1 if idx < m else 0) + chunks.append(" ".join(sentences[start:end])) + start = end + return tuple(chunks) + + def _call_llm(self, url, payload, api_key, retries, timeout=120): + last_err = None + for attempt in range(retries + 1): + try: + req = urllib.request.Request( + url, + data=json.dumps(payload).encode('utf-8'), + headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} + ) + response = urllib.request.urlopen(req, timeout=timeout) + body = json.loads(response.read().decode('utf-8')) + return body['choices'][0]['message']['content'].strip(), None + except Exception as e: + last_err = e + if attempt < retries: + time.sleep(1.5 * (attempt + 1)) + return None, last_err + + # ----------------------------------------------------------------- + def generate_sequence(self, image, mode, character_name, artistic_vibe, master_story, + openrouter_api_key, openrouter_model, creativity, max_tokens, retries, seed): + + user_defined_name = "" if character_name.upper() in ["AUTO", ""] else character_name.strip() + + def build_final(action, master_scene, name): + tags_list = [] + if name: + tags_list.append(name) + tag_block = ", ".join(tags_list) + + sentence_list = [] + if master_scene.strip(): + sentence_list.append(master_scene.strip().strip(",. ")) + if action.strip(): + sentence_list.append(action.strip()) + sentence_block = " ".join(sentence_list) + + if tag_block and sentence_block: + return f"{tag_block}\n{sentence_block}" + elif tag_block: + return tag_block + return sentence_block + + # ---- Manual Override ---- + if mode == "✍️ Manual Override": + fp = build_final(master_story, "", user_defined_name) + return (fp, fp, fp, fp, "[Manual Override]") + + random.seed(seed) + + base64_image = self._encode_image(image) + sentences = self._split_sentences(master_story) + chunk_1, chunk_2, chunk_3, chunk_4 = self._build_chunks(sentences) + + sys_prompt = ( + "You are an Elite Action Director prioritizing RAW SPEED and KINETIC ENERGY.\n" + f"1. CHARACTER: If NAME is 'AUTO', assign a name. If '{user_defined_name}', use it.\n" + "2. VISUAL ANALYSIS: You MUST base your descriptions EXACTLY on the character's clothing and weapons in the attached IMAGE.\n" + "3. MASTER SCENE: Write a 1-sentence environment description (lighting, weather).\n" + "4. SPEED-FOCUSED CHOREOGRAPHY (CRITICAL):\n" + " - 🚫 BAN SLOW-MOTION TRIGGERS: NEVER use words like 'micro-expressions', 'muscle tension', 'slowly turning', 'floating', or 'gradually'. These cause AI video models to render in slow-motion.\n" + " - ✅ FORCE 1x REAL-TIME SPEED: Describe large, sweeping, high-velocity movements. Use aggressive verbs (dashing, sprinting, whipping, snapping).\n" + " - ✅ KINETIC ADVERBS: Inject phrases like 'in a flash', 'at lightning speed', 'with explosive real-time velocity' into EVERY part.\n" + " - Example: 'suddenly dashes forward at full speed and delivers a lightning-fast horizontal strike, moving so quickly the rain splatters'.\n" + " - Strictly confine the actions. DO NOT animate future events early.\n" + " - 🔥 OUTPUT RULE: DO NOT quote the Korean text. Only output English.\n" + "Format EXACTLY:\nCHARACTER: [Name]\nMASTER SCENE: [Description]\n" + "PART 1: [0-1s] [Action A] [2-3s] [Action B] [4-5s] [Action C]\n" + "PART 2: [0-1s] [Action D] [2-3s] [Action E] [4-5s] [Action F]\n" + "PART 3: [0-1s] [Action G] [2-3s] [Action H] [4-5s] [Action I]\n" + "PART 4: [0-1s] [Action J] [2-3s] [Action K] [4-5s] [Action L]" + ) + + usr_text = ( + f"NAME: {character_name}\n" + f"VIBE: {artistic_vibe}\n\n" + "=== HIGH-SPEED ACTION SCRIPT ===\n" + f"▶ For PART 1 (0-5s), ONLY animate this: \"{chunk_1}\"\n" + f"▶ For PART 2 (5-10s), ONLY animate this: \"{chunk_2}\"\n" + f"▶ For PART 3 (10-15s), ONLY animate this: \"{chunk_3}\"\n" + f"▶ For PART 4 (15-20s), ONLY animate this: \"{chunk_4}\"\n" + "CRITICAL: Keep the action moving FAST. Avoid still poses or micro-details that look like slow-mo!" + ) + + url = "https://openrouter.ai/api/v1/chat/completions" + payload = { + "model": openrouter_model.strip(), + "messages": [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": [ + {"type": "text", "text": usr_text}, + {"type": "image_url", "image_url": {"url": base64_image}} + ]} + ], + "temperature": creativity, + "max_tokens": max_tokens, + } + + if not openrouter_api_key.strip(): + err_msg = "⚠️ API Error: OpenRouter API key is empty." + return (err_msg, err_msg, err_msg, err_msg, err_msg) + + llm_prompt, err = self._call_llm(url, payload, openrouter_api_key, retries) + if llm_prompt: + print(f"\n✅ [Dolphin V32 - Action Speed Optimized]\n{llm_prompt}\n") + else: + print(f"❌ [에러] API 호출 실패: {err}") + llm_prompt = "" + + final_char_name = user_defined_name + r_master = "" + p1 = p2 = p3 = p4 = "" + + if llm_prompt and "[removed]" not in llm_prompt: + cl = re.sub(r'[*#]', '', llm_prompt) + m_char = re.search(r'CHARACTER:\s*(.*?)(?=MASTER SCENE|$)', cl, re.I | re.S) + m_master = re.search(r'MASTER SCENE:\s*(.*?)(?=PART 1|$)', cl, re.I | re.S) + + m1 = re.search(r'PART 1:\s*(.*?)(?=PART 2|$)', cl, re.I | re.S) + m2 = re.search(r'PART 2:\s*(.*?)(?=PART 3|$)', cl, re.I | re.S) + m3 = re.search(r'PART 3:\s*(.*?)(?=PART 4|$)', cl, re.I | re.S) + m4 = re.search(r'PART 4:\s*(.*?)(?=\n\n|===|Note:|$)', cl, re.I | re.S) + + if not user_defined_name and m_char: + final_char_name = m_char.group(1).strip() + + r_master = m_master.group(1).strip() if m_master else "" + + # 파싱 실패 시 해당 청크(영문 지시문)를 폴백으로 사용해 비디오 프롬프트가 비지 않도록 함 + p1 = m1.group(1).strip() if m1 else chunk_1 + p2 = m2.group(1).strip() if m2 else chunk_2 + p3 = m3.group(1).strip() if m3 else chunk_3 + p4 = m4.group(1).strip() if m4 else chunk_4 + else: + # API 실패 시에도 스크립트 청크를 폴백으로 반환 (완전 실패보다 유용) + p1, p2, p3, p4 = chunk_1, chunk_2, chunk_3, chunk_4 + + return ( + build_final(p1, r_master, final_char_name), + build_final(p2, r_master, final_char_name), + build_final(p3, r_master, final_char_name), + build_final(p4, r_master, final_char_name), + llm_prompt if llm_prompt else "⚠️ API Error / empty response", + ) diff --git a/tools/reapply_symlink_output_patch.ps1 b/tools/reapply_symlink_output_patch.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..72c53bd084f35366454e3f8296c47068a8b8ad8e --- /dev/null +++ b/tools/reapply_symlink_output_patch.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + Re-applies the local revert of ComfyUI upstream commit 41a3e160d (#15216). + +.DESCRIPTION + #15216 changed get_save_image_path()'s containment check from a lexical + abspath comparison to is_within_directory(), which realpath()s both operands. + That resolves symlinks, so saving into a symlinked subfolder of the output + directory (e.g. D:\SD\output\I2V\RAW -> D:\iCloud\iCloudDrive\img\RAW) fails + with "Saving image outside the output folder is not allowed". + + The GHSA-779p-m5rp-r4h4 security fix (96e0e3585, 2026-07-02) is NOT touched -- + this only reverts the later refactor. ".." traversal stays blocked, and the + /view endpoint in server.py uses the same abspath check, so save and preview + remain consistent. + + Idempotent: safe to run any number of times. + + Update flow: folder_paths.py is tracked by ComfyUI's own git, so this patch + makes it dirty and `git pull` will refuse to overwrite it. Discard the patch, + pull, then re-apply: + + git -C checkout -- folder_paths.py + git -C pull + .\reapply_symlink_output_patch.ps1 + +.EXAMPLE + .\reapply_symlink_output_patch.ps1 + .\reapply_symlink_output_patch.ps1 -Verify +#> +[CmdletBinding()] +param( + # Path to ComfyUI's folder_paths.py. Defaults to the ComfyUI root three + # levels above this script (tools -> dolphin_nodes -> custom_nodes -> ComfyUI). + [string]$FolderPathsPy = (Join-Path $PSScriptRoot '..\..\..\folder_paths.py'), + + # Only report the current state; make no changes. + [switch]$Verify +) + +$ErrorActionPreference = 'Stop' + +$marker = '# DOLPHIN LOCAL PATCH: revert of upstream 41a3e160d' +$upstream = ' if not is_within_directory(output_dir, full_output_folder):' +$patched = @" + # DOLPHIN LOCAL PATCH: revert of upstream 41a3e160d (#15216), which swapped + # this lexical check for is_within_directory()'s realpath() one. That change + # breaks saving into a symlinked subfolder of the output dir (D:\SD\output\ + # I2V\RAW -> D:\iCloud\...). The /view endpoint in server.py still uses this + # same abspath-based check, so reverting keeps save and preview consistent. + # Re-apply after a ComfyUI update: + # custom_nodes/dolphin_nodes/tools/reapply_symlink_output_patch.ps1 + if os.path.commonpath((output_dir, os.path.abspath(full_output_folder))) != output_dir: +"@ + +if (-not (Test-Path $FolderPathsPy)) { + Write-Error "folder_paths.py not found at: $FolderPathsPy" + exit 1 +} +$FolderPathsPy = (Resolve-Path $FolderPathsPy).Path +$text = Get-Content $FolderPathsPy -Raw + +if ($text.Contains($marker)) { + Write-Host "[OK] Already patched: $FolderPathsPy" -ForegroundColor Green + exit 0 +} + +if (-not $text.Contains($upstream)) { + Write-Host "[FAIL] Neither the patch marker nor the expected upstream line was found." -ForegroundColor Red + Write-Host " Upstream probably rewrote this check. Inspect get_save_image_path() in:" + Write-Host " $FolderPathsPy" + exit 1 +} + +if ($Verify) { + Write-Host "[NEEDS PATCH] Upstream check is active; symlinked output subfolders will fail." -ForegroundColor Yellow + exit 2 +} + +$backup = "$FolderPathsPy.orig-$(Get-Date -Format yyyyMMdd-HHmmss)" +Copy-Item $FolderPathsPy $backup + +# Preserve the file's existing newline style rather than forcing CRLF. +$eol = if ($text -match "`r`n") { "`r`n" } else { "`n" } +$text = $text.Replace($upstream, ($patched -replace "`r?`n", $eol).TrimEnd($eol)) +Set-Content $FolderPathsPy $text -NoNewline + +Write-Host "[PATCHED] $FolderPathsPy" -ForegroundColor Green +Write-Host " backup: $backup" +Write-Host " Restart ComfyUI for the change to take effect." +exit 0 diff --git a/wan_h3_refine.py b/wan_h3_refine.py new file mode 100644 index 0000000000000000000000000000000000000000..708f24cb88e7ba9283a2c2ecff22dfb37bdc5707 --- /dev/null +++ b/wan_h3_refine.py @@ -0,0 +1,354 @@ +"""MiniMax H3 출력을 Wan 2.2 LOW noise expert로 저-denoise 리파인하는 올인원 노드. + +H3(24fps, 768 short edge, 17k+5 프레임 그리드)와 Wan(16fps, 4n+1 그리드, Wan2.1 VAE) +사이를 넘길 때 매번 손으로 맞춰야 하는 것들을 한 노드에 몰아넣는다: + + - 프레임 수를 4n+1로 정렬 (H3 기본 124프레임은 Wan에서 불법. 141/209/277/345는 합법) + - VAE 오배선 차단 (Wan2.2 VAE=48ch, H3 VAE=24ch를 꽂으면 여기서 막는다. 필요한 건 Wan2.1 VAE=16ch) + - 소스 영상을 인코딩해서 그대로 샘플링 라텐트로 사용 (WanImageToVideo가 뱉는 zeros 라텐트가 아님) + - I2V concat 컨디셔닝을 코어 WanImageToVideo와 동일한 규약으로 재구성 + - sigma_shift 패치를 샘플링 직전에 직접 적용 (그래프에 ModelSamplingSD3를 따로 놓는 방식은 + 상류에서 model_sampling을 통째로 교체하는 노드가 있으면 무시되므로 여기서 처리) + - VAE 왕복 + 저denoise 리페인트가 만드는 색 드리프트를 원본 기준으로 되돌림 + - H3가 같이 뽑아준 오디오를 정렬된 프레임 수에 맞춰 트림/패드해서 통과시킴 + +컨텍스트 윈도우(81프레임 초과 클립)는 이 노드가 아니라 상류의 코어 `WanContextWindowsManual` +노드가 담당한다 - MODEL을 패치하는 방식이라 이 노드는 아무것도 안 해도 자동으로 먹는다. +""" + +import torch +import torch.nn.functional as F + +import comfy.model_management +import comfy.model_sampling +import comfy.samplers +import comfy.utils +import node_helpers +import nodes + +# Wan 2.2 MoE의 high/low expert 경계 시그마. 시작 시그마가 이 값 아래면 LOW 단독으로 충분하다. +MOE_BOUNDARY_SIGMA = 0.9 + +# Wan2.1 VAE의 라텐트 채널 수. Wan2.2(5B) VAE는 48, MiniMax H3 VAE는 24라서 이걸로 구분된다. +WAN21_LATENT_CHANNELS = 16 + + +def _shifted_sigma(shift, t): + """flow matching의 time_snr_shift. comfy/model_sampling.py의 스케줄과 동일한 식.""" + if shift <= 0: + return t + return shift * t / (1.0 + (shift - 1.0) * t) + + +def _align_4n1(n, mode): + """Wan의 프레임 그리드(4n+1)로 맞춘 목표 프레임 수.""" + rem = (n - 1) % 4 + if rem == 0: + return n + if mode == "trim": + return n - rem + if mode == "pad": + return n + (4 - rem) + return n # off + + +def _color_match_per_frame(out, src, strength): + """프레임별/채널별 mean-std 매칭. 클립 전체 통계가 아니라 프레임별로 맞춰야 + H3 쪽 조명 변화(페이드, 그림자 통과 등)를 리파인이 눌러버리지 않는다.""" + if strength <= 0.0: + return out + eps = 1e-5 + o = out.float() + s = src.float() + o_mean = o.mean(dim=(1, 2), keepdim=True) + o_std = o.std(dim=(1, 2), keepdim=True) + s_mean = s.mean(dim=(1, 2), keepdim=True) + s_std = s.std(dim=(1, 2), keepdim=True) + matched = (o - o_mean) / (o_std + eps) * s_std + s_mean + return torch.lerp(o, matched, float(strength)).clamp(0.0, 1.0) + + +class WanH3Refine_Dolphin: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL", {"tooltip": "Wan 2.2 I2V A14B LOW noise + 캐릭터 LoRA까지 적용된 모델. " + "denoise가 낮으면 HIGH expert는 아예 안 쓰이므로 LOW만 로드하면 된다."}), + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + "vae": ("VAE", {"tooltip": "Wan2.1 VAE (16채널). Wan2.2 VAE나 H3 VAE를 꽂으면 에러로 막는다."}), + "images": ("IMAGE", {"tooltip": "H3가 뽑은 프레임들."}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 6, "min": 1, "max": 200, + "tooltip": "실제로 실행되는 스텝 수. denoise가 낮아도 이 횟수만큼 돈다 " + "(스케줄 총 길이가 steps/denoise로 늘어나고 그 꼬리에서 이 수만큼 밟는 구조). " + "리파인은 4~8이면 충분."}), + "cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1, + "tooltip": "증류(lightning/lightx2v) LoRA를 뺐다면 2.5~3.5, 걸었다면 1.0."}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler"}), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default": "simple"}), + "denoise": ("FLOAT", {"default": 0.30, "min": 0.01, "max": 1.0, "step": 0.01, + "tooltip": "0.2~0.35 권장. Wan은 16fps 기준으로 학습돼서 24fps 소스를 " + "'덜 움직이는 영상'으로 읽는다 - 높일수록 모션을 죽인다."}), + "sigma_shift": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 20.0, "step": 0.1, + "tooltip": "0이면 모델 기본값 유지. 생성용 5는 스텝을 고시그마 쪽으로 쏠리게 " + "하는데 리파인은 저시그마를 촘촘히 밟아야 하므로 2~3이 맞다."}), + "frame_align": (["trim", "pad", "off"], {"default": "trim", + "tooltip": "Wan의 4n+1 그리드로 정렬. trim=뒤를 자름, " + "pad=마지막 프레임 복제. H3 length를 141/209/277/345로 " + "뽑으면 애초에 정렬이 필요 없다."}), + "conditioning_mode": (["first_frame", "full_clip", "none"], {"default": "first_frame", + "tooltip": "first_frame=코어 WanImageToVideo와 동일(학습 분포 안). " + "full_clip=전 프레임을 known으로 주는 강한 소스 고정이지만 " + "I2V-A14B 학습 분포 밖이라 실험용. none=컨디셔닝 없음."}), + "upscale_by": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.05, + "tooltip": "리파인 전 확대 배율. 1.3을 넘기면 Wan 학습 해상도를 벗어나 " + "중복 패턴이 생긴다 - 큰 업스케일은 SeedVR2/Topaz로 따로."}), + "color_match": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05, + "tooltip": "VAE 왕복이 만드는 채도/밝기 드리프트를 원본 기준으로 되돌리는 강도."}), + "fps": ("INT", {"default": 24, "min": 1, "max": 120, + "tooltip": "소스 fps. 오디오 길이를 정렬된 프레임 수에 맞출 때만 쓴다. H3는 24."}), + }, + "optional": { + "audio": ("AUDIO", {"tooltip": "H3가 같이 뽑은 오디오. 정렬된 프레임 수에 맞춰 트림/패드해서 통과시킨다."}), + }, + } + + RETURN_TYPES = ("IMAGE", "AUDIO", "INT", "INT", "INT", "STRING") + RETURN_NAMES = ("IMAGE", "audio", "frames", "width", "height", "info") + FUNCTION = "refine" + CATEGORY = "Dolphin Node/Wan Video" + DESCRIPTION = "MiniMax H3 출력을 Wan 2.2 LOW noise로 저-denoise 리파인 (그리드 정렬 + VAE 검증 + 색보정 + 오디오 패스스루)" + + def refine(self, model, positive, negative, vae, images, seed, steps, cfg, + sampler_name, scheduler, denoise, sigma_shift, frame_align, + conditioning_mode, upscale_by, color_match, fps, audio=None): + + notes = [] + + # --- 1. VAE 검증 --------------------------------------------------- + latent_channels = getattr(vae, "latent_channels", None) + if latent_channels != WAN21_LATENT_CHANNELS: + hint = {24: "MiniMax H3 VAE(24ch)", 48: "Wan2.2 5B VAE(48ch)", 4: "SD VAE(4ch)"}.get( + latent_channels, f"라텐트 {latent_channels}채널 VAE") + raise ValueError( + f"[Dolphin] Wan 2.2 I2V A14B 리파인에는 Wan2.1 VAE(16채널)가 필요한데 {hint}가 연결됐다. " + f"models/vae의 Wan2_1_VAE_bf16.safetensors 계열로 바꿀 것." + ) + + # --- 2. 프레임 그리드 정렬 (Wan은 4n+1만 받는다) --------------------- + n_in = int(images.shape[0]) + if n_in < 5: + raise ValueError(f"[Dolphin] 프레임이 {n_in}장뿐이다. 최소 5장 필요.") + + n = _align_4n1(n_in, frame_align) + if n < 5: + raise ValueError(f"[Dolphin] 4n+1로 정렬하면 {n}프레임이 된다. 소스를 더 길게 뽑을 것.") + + if n < n_in: + images = images[:n] + notes.append(f"{n_in}→{n}프레임 트림") + elif n > n_in: + tail = images[-1:].repeat(n - n_in, 1, 1, 1) + images = torch.cat([images, tail], dim=0) + notes.append(f"{n_in}→{n}프레임 패드(마지막 프레임 복제)") + elif frame_align == "off" and (n_in - 1) % 4 != 0: + notes.append(f"⚠ {n_in}프레임은 4n+1이 아니다 (frame_align=off)") + + images = images[:, :, :, :3] + + # --- 3. 해상도 정렬 (16의 배수) -------------------------------------- + src_h, src_w = int(images.shape[1]), int(images.shape[2]) + tgt_w = max(16, int(round(src_w * upscale_by / 16.0)) * 16) + tgt_h = max(16, int(round(src_h * upscale_by / 16.0)) * 16) + + if (tgt_w, tgt_h) != (src_w, src_h): + images = comfy.utils.common_upscale( + images.movedim(-1, 1), tgt_w, tgt_h, "lanczos", "disabled" + ).movedim(1, -1).clamp(0.0, 1.0) + notes.append(f"{src_w}x{src_h}→{tgt_w}x{tgt_h}") + if upscale_by > 1.3: + notes.append("⚠ upscale_by>1.3은 Wan 학습 해상도 밖 (중복 패턴 위험)") + + # --- 4. 소스 인코딩: 이게 샘플링 시작 라텐트가 된다 -------------------- + source_latent = self._encode(vae, images) + + # --- 5. I2V concat 컨디셔닝 (코어 WanImageToVideo와 동일 규약) -------- + if conditioning_mode != "none": + known = images[:1] if conditioning_mode == "first_frame" else images + canvas = torch.ones((n, tgt_h, tgt_w, 3), + device=images.device, dtype=images.dtype) * 0.5 + canvas[:known.shape[0]] = known + concat_latent = self._encode(vae, canvas) + + mask = torch.ones((1, 1, source_latent.shape[2], + concat_latent.shape[-2], concat_latent.shape[-1]), + device=images.device, dtype=images.dtype) + mask[:, :, :((known.shape[0] - 1) // 4) + 1] = 0.0 + + values = {"concat_latent_image": concat_latent, "concat_mask": mask} + positive = node_helpers.conditioning_set_values(positive, values) + negative = node_helpers.conditioning_set_values(negative, values) + + if conditioning_mode == "full_clip": + notes.append("⚠ full_clip은 I2V-A14B 학습 분포 밖(첫 프레임만 known으로 학습됨)") + + # --- 6. sigma_shift 패치 -------------------------------------------- + effective_shift = self._model_shift(model) + if sigma_shift > 0: + model = self._patch_shift(model, sigma_shift) + effective_shift = sigma_shift + + # --- 7. 샘플링 ------------------------------------------------------- + out_latent, = nodes.common_ksampler( + model, seed, steps, cfg, sampler_name, scheduler, + positive, negative, {"samples": source_latent}, denoise=denoise, + ) + + # --- 8. 디코드 ------------------------------------------------------- + result = self._decode(vae, out_latent["samples"]) + if result.shape[0] > n: + result = result[:n] + elif result.shape[0] < n: + images = images[:result.shape[0]] + n = int(result.shape[0]) + + # --- 9. 색 드리프트 되돌리기 ----------------------------------------- + if result.shape[1:3] != images.shape[1:3]: + ref = F.interpolate(images.movedim(-1, 1), + size=(result.shape[1], result.shape[2]), + mode="bicubic", antialias=True).clamp(0, 1).movedim(1, -1) + else: + ref = images + result = _color_match_per_frame(result[:, :, :, :3], ref, color_match) + + # --- 10. 오디오를 프레임 수에 맞춰 정렬 ------------------------------- + audio_out = audio + if audio is not None and isinstance(audio, dict) and "waveform" in audio: + sr = int(audio.get("sample_rate", 48000)) + waveform = audio["waveform"] + target_samples = int(round(n / float(fps) * sr)) + cur = int(waveform.shape[-1]) + if cur > target_samples: + waveform = waveform[..., :target_samples] + notes.append(f"오디오 {cur}→{target_samples}샘플 트림") + elif cur < target_samples: + waveform = F.pad(waveform, (0, target_samples - cur)) + notes.append(f"오디오 {cur}→{target_samples}샘플 패드") + audio_out = {"waveform": waveform, "sample_rate": sr} + + # --- 11. 리포트 ------------------------------------------------------ + start_sigma = _shifted_sigma(effective_shift, denoise) + expert = "LOW 단독으로 충분" if start_sigma < MOE_BOUNDARY_SIGMA else "HIGH expert 구간 진입 (리파인이 아니라 재생성)" + info_lines = [ + f"frames {n_in} → {n} (4n+1, n={(n - 1) // 4}) {tgt_w}x{tgt_h} {n / float(fps):.2f}s @ {fps}fps", + f"denoise {denoise:.2f} / shift {effective_shift:.2f} → 시작 σ {start_sigma:.3f} (MoE 경계 {MOE_BOUNDARY_SIGMA}) → {expert}", + f"실행 스텝 {steps} (스케줄 총 길이 ~{int(round(steps / max(denoise, 1e-3)))})", + ] + if notes: + info_lines.append("· " + " / ".join(notes)) + info = "\n".join(info_lines) + print("[Dolphin] Wan H3 Refine\n " + "\n ".join(info_lines)) + + return (result, audio_out, n, tgt_w, tgt_h, info) + + # ---------------------------------------------------------------- helpers + + def _encode(self, vae, pixels): + try: + return vae.encode(pixels) + except comfy.model_management.OOM_EXCEPTION: + print("[Dolphin] VAE 인코딩 OOM → 타일 인코딩으로 폴백") + return vae.encode_tiled(pixels, tile_x=512, tile_y=512, + overlap=64, tile_t=32, overlap_t=8) + + def _decode(self, vae, samples): + try: + images = vae.decode(samples) + except comfy.model_management.OOM_EXCEPTION: + print("[Dolphin] VAE 디코딩 OOM → 타일 디코딩으로 폴백") + compression = vae.temporal_compression_decode() or 4 + images = vae.decode_tiled(samples, tile_x=512, tile_y=512, overlap=64, + tile_t=max(2, 32 // compression), + overlap_t=max(1, 8 // compression)) + if len(images.shape) == 5: # 배치 합치기 + images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) + return images.clamp(0.0, 1.0) + + def _model_shift(self, model): + try: + return float(getattr(model.get_model_object("model_sampling"), "shift", 1.0)) + except Exception: + return 1.0 + + def _patch_shift(self, model, shift): + """ModelSamplingSD3와 동일한 패치를 샘플링 직전에 직접 적용. + 그래프에 따로 놓는 ModelSamplingSD3 노드는 model_sampling을 통째로 교체하는 + 다른 노드(TripleKSampler 등)가 뒤에 있으면 무시되므로 여기서 하는 게 확실하다.""" + m = model.clone() + + class ModelSamplingAdvanced(comfy.model_sampling.ModelSamplingDiscreteFlow, + comfy.model_sampling.CONST): + pass + + original = m.get_model_object("model_sampling") + model_sampling = ModelSamplingAdvanced(model.model.model_config) + model_sampling.set_parameters(shift=shift, multiplier=1000) + if hasattr(original, "noise_scale"): + model_sampling.set_noise_scale(original.noise_scale) + m.add_object_patch("model_sampling", model_sampling) + return m + + +class WanH3FrameGrid_Dolphin: + """H3의 length를 Wan과 호환되는 값으로 미리 뽑아주는 계산기. + + H3 그리드는 n % 17 == 5, Wan 그리드는 n % 4 == 1이다. 둘이 겹치는 건 68프레임 주기 - + 5 / 73 / 141 / 209 / 277 / 345 ... H3 노드의 length를 여기서 나온 값으로 넣으면 + 리파인 단계에서 트림/패드가 아예 필요 없어진다.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "seconds": ("FLOAT", {"default": 5.9, "min": 0.2, "max": 60.0, "step": 0.1, + "tooltip": "원하는 길이(초). 24fps 기준으로 가장 가까운 호환 프레임 수를 찾는다."}), + "mode": (["wan_compatible", "h3_only"], {"default": "wan_compatible", + "tooltip": "wan_compatible=H3(17k+5)와 Wan(4n+1) 둘 다 만족하는 값. " + "h3_only=H3 그리드만 만족(리파인 안 할 때)."}), + } + } + + RETURN_TYPES = ("INT", "FLOAT", "STRING") + RETURN_NAMES = ("length", "seconds", "info") + FUNCTION = "compute" + CATEGORY = "Dolphin Node/Wan Video" + DESCRIPTION = "H3 length를 Wan 리파인과 호환되는 프레임 수(5/73/141/209/277/345…)로 계산" + + def compute(self, seconds, mode): + fps = 24 + want = max(5, int(round(seconds * fps))) + + # h3_only는 17k+5, wan_compatible은 17k+5 ∩ 4n+1 = 68m+5 (k가 4의 배수일 때). + period = 17 if mode == "h3_only" else 68 + below = 5 + period * max(0, (want - 5) // period) + above = below + period + n = below if (want - below) <= (above - want) else above + n = max(5, n) + + dur = n / float(fps) + wan_ok = (n - 1) % 4 == 0 + info = (f"length={n} ({dur:.2f}s @24fps) H3 그리드 OK" + f" / Wan 4n+1 {'OK (n=%d)' % ((n - 1) // 4) if wan_ok else '불가 - 리파인 시 트림 필요'}") + return (n, dur, info) + + +NODE_CLASS_MAPPINGS = { + "WanH3Refine_Dolphin": WanH3Refine_Dolphin, + "WanH3FrameGrid_Dolphin": WanH3FrameGrid_Dolphin, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "WanH3Refine_Dolphin": "🐬🅜 Wan Refine H3 (low-denoise pass)", + "WanH3FrameGrid_Dolphin": "🐬🅜 H3↔Wan Frame Grid", +} diff --git a/wan_i2v_continuity.py b/wan_i2v_continuity.py new file mode 100644 index 0000000000000000000000000000000000000000..37496c9043e25c6ac251ad01ac2df9c07750922c --- /dev/null +++ b/wan_i2v_continuity.py @@ -0,0 +1,108 @@ +import torch +import comfy.model_management +import comfy.utils +import node_helpers + + +class WanI2VContinuity_Dolphin: + """ + Wan2.2 I2V 노드 — 클립 이어붙이기(연속성)용. + + 기존 접근(WanImageToVideoSVIPro)의 문제: anchor 이미지를 따로 인코딩한 latent와, + 이전 클립 latent의 뒷부분을 그냥 잘라낸 조각(motion_latent)을 이어붙였다. WAN VAE는 + 인과적(causal) 시간 압축이라 각 latent 프레임이 "그 시점까지의 시퀀스 문맥"을 전제로 + 인코딩되는데, 서로 다른 문맥에서 나온 latent 조각을 이어붙이면 통계적으로 불연속이라 + 화질이 튀고 동작 연속성도 못 살렸다. + + 이 노드는 ComfyUI 코어 `WanImageToVideo`의 검증된 방식을 따른다: + 이전 클립에서 디코딩한 마지막 N프레임(픽셀)을 start_image로 받아, 회색(0.5) filler와 + 함께 하나의 시퀀스로 만들어 **한 번의 통짜 VAE 인코딩**을 한다. 그래서 known 구간과 + 생성 구간의 latent 문맥이 자연스럽게 이어진다. 마스크도 프레임수→latent수 변환을 + 정확히 해서 known 프레임 전체를 고정한다. + + 여기에 PainterI2V의 motion_amplitude(슬로우모션 방지)를 이식했다. 단 known 프레임은 + 건드리지 않고, 뒤에 붙는 filler(생성 대상) latent만 마지막 known latent 대비 차이를 + 증폭한다. motion_amplitude=1.0이면 코어 WanImageToVideo와 동일하게 동작한다. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + "vae": ("VAE",), + "width": ("INT", {"default": 832, "min": 16, "max": 4096, "step": 16}), + "height": ("INT", {"default": 480, "min": 16, "max": 4096, "step": 16}), + "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 4}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), + "motion_amplitude": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.05, + "tooltip": "1.0=코어 WanImageToVideo와 동일. >1.0이면 filler 구간에 슬로우모션 방지 증폭."}), + }, + "optional": { + "clip_vision_output": ("CLIP_VISION_OUTPUT",), + "start_image": ("IMAGE", {"tooltip": "known 프레임(들). 이전 클립의 디코딩된 마지막 N프레임을 넣으면 연속성이 생김. 1장이면 일반 I2V."}), + }, + } + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("positive", "negative", "latent") + FUNCTION = "encode" + CATEGORY = "Dolphin Node/Wan Video" + + def encode(self, positive, negative, vae, width, height, length, batch_size, + motion_amplitude=1.0, clip_vision_output=None, start_image=None): + latent = torch.zeros( + [batch_size, 16, ((length - 1) // 4) + 1, height // 8, width // 8], + device=comfy.model_management.intermediate_device(), + ) + + if start_image is not None: + start_image = comfy.utils.common_upscale( + start_image[:length].movedim(-1, 1), width, height, "bilinear", "center" + ).movedim(1, -1) + n_known_frames = start_image.shape[0] + + # known 프레임 뒤를 회색(0.5) filler로 채운 뒤 통짜로 인코딩 (코어 방식) + image = torch.ones( + (length, height, width, start_image.shape[-1]), + device=start_image.device, dtype=start_image.dtype, + ) * 0.5 + image[:n_known_frames] = start_image + + concat_latent_image = vae.encode(image[:, :, :, :3]) + + # 프레임수 -> latent수 (WAN VAE 4x 시간 압축) + n_known_latents = ((n_known_frames - 1) // 4) + 1 + + # motion_amplitude: filler(생성 대상) latent만 증폭. known 프레임은 그대로. + if motion_amplitude > 1.0 and concat_latent_image.shape[2] > n_known_latents: + base_latent = concat_latent_image[:, :, n_known_latents - 1:n_known_latents] + filler = concat_latent_image[:, :, n_known_latents:] + diff = filler - base_latent + diff_mean = diff.mean(dim=(1, 3, 4), keepdim=True) + diff_centered = diff - diff_mean + scaled = base_latent + diff_centered * motion_amplitude + diff_mean + scaled = torch.clamp(scaled, -6, 6) + concat_latent_image = torch.cat( + [concat_latent_image[:, :, :n_known_latents], scaled], dim=2 + ) + + mask = torch.ones( + (1, 1, latent.shape[2], concat_latent_image.shape[-2], concat_latent_image.shape[-1]), + device=start_image.device, dtype=start_image.dtype, + ) + mask[:, :, :n_known_latents] = 0.0 + + positive = node_helpers.conditioning_set_values( + positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask} + ) + negative = node_helpers.conditioning_set_values( + negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask} + ) + + if clip_vision_output is not None: + positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output}) + negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output}) + + return (positive, negative, {"samples": latent}) diff --git a/wan_resizer.py b/wan_resizer.py new file mode 100644 index 0000000000000000000000000000000000000000..1775814e25bdb7a0e50c757716fee1af7bf180a7 --- /dev/null +++ b/wan_resizer.py @@ -0,0 +1,56 @@ +import math +import torch +import comfy.utils + +class WanNativeResize_Dolphin: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "target_model": (["480P (832x480)", "720P (1280x720)", "1080P (1920x1080)"], {"default": "720P (1280x720)"}), + "upscale_method": (["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], {"default": "lanczos"}), + "alignment": ("INT", {"default": 16, "min": 8, "max": 64, "step": 8}), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "INT") + RETURN_NAMES = ("IMAGE", "width", "height") + FUNCTION = "resize_for_wan" + CATEGORY = "Dolphin Node/Wan Video" + + def resize_for_wan(self, image, target_model, upscale_method, alignment): + # 1. ComfyUI 이미지 텐서의 형태를 가져옵니다: (Batch/Frames, Height, Width, Channels) + b, h, w, c = image.shape + + # 2. 타겟 모델에 따른 총 픽셀 면적(Area) 세팅 + if "480P" in target_model: + target_area = 832 * 480 + elif "720P" in target_model: + target_area = 1280 * 720 + else: + target_area = 1920 * 1080 + + # 3. Byungjoo님의 수학 로직 (비율 유지 계산) + aspect_ratio = w / h + new_h = math.sqrt(target_area / aspect_ratio) + new_w = new_h * aspect_ratio + + # 4. [핵심] VAE가 좋아하는 배수(Alignment, 보통 16)로 반올림 처리 + new_w = int(round(new_w / alignment) * alignment) + new_h = int(round(new_h / alignment) * alignment) + + # 5. [최적화] 이미 목표 해상도와 일치한다면, 무거운 연산 없이 원본 통과 (패스스루) + if w == new_w and h == new_h: + return (image, new_w, new_h) + + # 6. [핵심] 텐서 차원 변경: ComfyUI (B, H, W, C) -> 리사이즈 엔진용 (B, C, H, W) + image = image.movedim(-1, 1) + + # 7. ComfyUI 내장 엔진을 사용해 초고화질 리사이즈 (Lanczos 지원) + resized_image = comfy.utils.common_upscale(image, new_w, new_h, upscale_method, "disabled") + + # 8. 텐서 차원 원상 복구: (B, C, H, W) -> ComfyUI (B, H, W, C) + resized_image = resized_image.movedim(1, -1) + + return (resized_image, new_w, new_h) \ No newline at end of file diff --git a/wan_svipro_fixed.py b/wan_svipro_fixed.py new file mode 100644 index 0000000000000000000000000000000000000000..a8e451be40448ef7852a6dab98d5913effa5c708 --- /dev/null +++ b/wan_svipro_fixed.py @@ -0,0 +1,95 @@ +import torch +import comfy.model_management as model_management +import comfy.latent_formats +import node_helpers + + +class WanImageToVideoSVIProFixed_Dolphin: + """ + WanImageToVideoSVIPro(comfyui-kjnodes)의 마스크 버그를 고친 버전. + + 원본은 anchor_samples + prev_samples(motion_latent_count 프레임)를 이어붙여 + concat_latent_image를 만들면서도, concat_mask는 mask[:, :, :1] = 0.0으로 + 앵커 프레임 1장만 "known"으로 표시한다. 그래서 실제로 이어붙인 모션 컨텍스트 + 프레임들이 "생성 대상(mask=1)"으로 취급돼 모델이 그 내용을 무시/이탈하고, + 클립이 몇 프레임 반복되다가 전혀 다른 영상으로 튀는 현상이 생긴다. + + 이 버전은 anchor + motion_latent 프레임 수 전체를 known(0.0)으로 마스킹해서 + 이어붙인 모션 컨텍스트가 실제로 고정된 참고 정보로 취급되게 한다. + + 추가로 PainterI2V/PainterFLF2V(ComfyUI-PainterI2V, Comfyui-PainterFLF2V)의 + "motion_amplitude" 슬로우모션 방지 트릭을 이식했다: anchor 뒤에 붙는 순수 + filler(생성 대상) 구간과 anchor의 latent 차이를 증폭해서, 4-step lightning + 계열 로라가 정지 이미지를 보고 동작을 과소평가하는 경향을 보정한다. 실제 + motion_latent(진짜 이전 프레임)는 건드리지 않고 filler 구간에만 적용한다. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 4}), + "anchor_samples": ("LATENT",), + "motion_latent_count": ("INT", {"default": 1, "min": 0, "max": 128, "step": 1, + "tooltip": "prev_samples 끝에서 몇 프레임을 모션 컨텍스트로 이어붙일지."}), + "motion_amplitude": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.05, + "tooltip": "1.0=비활성(원본과 동일). >1.0이면 PainterI2V식 슬로우모션 방지 증폭을 filler 구간에 적용."}), + }, + "optional": { + "prev_samples": ("LATENT", {"tooltip": "이전 클립의 (디코드 안 된) latent. 끝의 motion_latent_count 프레임만 사용."}), + }, + } + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("positive", "negative", "latent") + FUNCTION = "encode" + CATEGORY = "Dolphin Node/Wan Video" + + def encode(self, positive, negative, length, anchor_samples, motion_latent_count, + motion_amplitude=1.0, prev_samples=None): + anchor_latent = anchor_samples["samples"].clone() + B, C, T, H, W = anchor_latent.shape + + total_latents = ((length - 1) // 4) + 1 + empty_latent = torch.zeros([B, 16, total_latents, H, W], device=model_management.intermediate_device()) + device = anchor_latent.device + dtype = anchor_latent.dtype + + if prev_samples is None or motion_latent_count == 0: + known_frames = anchor_latent.shape[2] + image_cond_latent = anchor_latent + else: + motion_latent = prev_samples["samples"][:, :, -motion_latent_count:].clone() + known_frames = anchor_latent.shape[2] + motion_latent.shape[2] + image_cond_latent = torch.cat([anchor_latent, motion_latent], dim=2) + + padding_size = total_latents - known_frames + padding = torch.zeros(1, C, padding_size, H, W, dtype=dtype, device=device) + padding = comfy.latent_formats.Wan21().process_out(padding) + + # PainterI2V식 증폭: anchor 대비 filler(padding)의 차이를 키워서 + # "여기서부터는 크게 움직여야 한다"는 신호를 강하게 준다. 실제 + # motion_latent(진짜 이전 프레임)는 그대로 두고 filler에만 적용. + if motion_amplitude > 1.0 and padding_size > 0: + base_latent = anchor_latent[:, :, -1:].expand(-1, -1, padding_size, -1, -1) + diff = padding - base_latent + diff_mean = diff.mean(dim=(1, 3, 4), keepdim=True) + diff_centered = diff - diff_mean + padding = base_latent + diff_centered * motion_amplitude + diff_mean + padding = torch.clamp(padding, -6, 6) + + image_cond_latent = torch.cat([image_cond_latent, padding], dim=2) + + mask = torch.ones((1, 1, total_latents, H, W), device=device, dtype=dtype) + mask[:, :, :known_frames] = 0.0 # anchor + motion context 프레임 전부 known 처리 (버그 수정 지점) + + positive = node_helpers.conditioning_set_values( + positive, {"concat_latent_image": image_cond_latent, "concat_mask": mask} + ) + negative = node_helpers.conditioning_set_values( + negative, {"concat_latent_image": image_cond_latent, "concat_mask": mask} + ) + + return (positive, negative, {"samples": empty_latent}) diff --git a/wan_svipro_gray.py b/wan_svipro_gray.py new file mode 100644 index 0000000000000000000000000000000000000000..0f78d2a0df88ed162037b21aba0f2ed6d70914bd --- /dev/null +++ b/wan_svipro_gray.py @@ -0,0 +1,145 @@ +import torch +import comfy.model_management as model_management +import node_helpers + + +class WanImageToVideoSVIProGray_Dolphin: + """ + WanImageToVideoSVIPro(comfyui-kjnodes)의 드롭인 교체 노드 — 슬로우모션 원인 수정판. + + 원본 SVIPro는 아직 생성되지 않은 구간(filler)을 latent 공간에서 직접 만든다: + + padding = comfy.latent_formats.Wan21().process_out(torch.zeros(...)) + + `process_out(x) = x * std + mean` 이므로 `process_out(0)`은 **latents_mean 상수**다. + 즉 81프레임(=latent 21프레임) 기준으로 conditioning의 20/21이 시간축으로 전혀 + 변하지 않는 상수 블록이 되고, 모델은 "앞으로 아무 변화 없음" 신호를 받아 동작을 + 억제한다(슬로우모션). 게다가 이 값은 학습 때 그 자리에서 본 적 없는 분포 밖 입력이다. + + 코어 `WanImageToVideo`와 PainterI2V는 filler를 **픽셀 공간의 회색(0.5)** 으로 만든 뒤 + VAE 인코딩한다 — 모델이 학습 중 "unknown 구간"으로 실제 학습한 표현이다. 실측상 + PainterI2V의 슬로우모션 발생률이 ~0%인 반면 SVIPro는 ~70%였다. + + 이 노드는 SVIPro의 latent 기반 인터페이스(anchor_samples/prev_samples/ + motion_latent_count)를 그대로 유지하면서 filler를 회색 VAE 인코딩으로 바꾼다. + + v1(첫 시도)은 known latent와 회색 filler latent를 **따로따로** vae.encode()해서 + 이어붙였는데, 이게 "처음부터 회색 노이즈만 나옴" 버그로 이어졌다. WAN VAE는 인과적 + (causal) 압축이라 각 latent 프레임이 그 시점까지의 시퀀스 문맥을 전제로 인코딩되고, + WAN은 3D self-attention이라 시퀀스 전체에 걸쳐 정보가 섞인다 — 별도 인코딩된 두 + 구간을 이어붙이면 모델이 학습 때 한 번도 못 본 이음매가 생기고, 그게 anchor 프레임 + 까지 포함해서 시퀀스 전체를 오염시킨 것으로 보인다. + + 이번 버전은 PainterI2V/코어 WanImageToVideo와 동일하게, known 구간을 **픽셀로 + 디코딩**한 뒤 회색 filler 픽셀과 하나의 시퀀스로 합쳐서 **단 한 번**만 인코딩한다. + known 쪽에 디코드->재인코드 왕복이 한 번 더 생기지만(코드 리뷰로 이미 알려진 트레이드 + 오프), 이음매 불일치 문제는 구조적으로 사라진다. + + 추가로 원본의 마스크 버그도 고쳤다: 원본은 `mask[:, :, :1] = 0.0`으로 앵커 1프레임만 + known 처리해서 prev_samples로 이어붙인 모션 프레임이 생성 대상으로 취급됐다. + 여기서는 anchor + motion 프레임 전체를 known으로 마스킹한다. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + "vae": ("VAE", {"tooltip": "회색 filler를 인코딩하는 데 사용. 원본 SVIPro엔 없던 입력."}), + "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 4}), + "anchor_samples": ("LATENT",), + "motion_latent_count": ("INT", {"default": 1, "min": 0, "max": 128, "step": 1, + "tooltip": "prev_samples 끝에서 몇 latent 프레임을 모션 컨텍스트로 이어붙일지."}), + }, + "optional": { + "prev_samples": ("LATENT", {"tooltip": "이전 클립의 latent. 끝의 motion_latent_count 프레임만 사용."}), + }, + } + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("positive", "negative", "latent") + FUNCTION = "encode" + CATEGORY = "Dolphin Node/Wan Video" + + def encode(self, positive, negative, vae, length, anchor_samples, motion_latent_count, + prev_samples=None): + anchor_latent = anchor_samples["samples"].clone() + B, C, T, H, W = anchor_latent.shape + device, dtype = anchor_latent.device, anchor_latent.dtype + log = lambda *a: print("[SVIProGray]", *a) + log(f"anchor_latent shape={tuple(anchor_latent.shape)} dtype={dtype} " + f"min={anchor_latent.min().item():.4f} max={anchor_latent.max().item():.4f} " + f"mean={anchor_latent.mean().item():.4f}") + + total_latents = ((length - 1) // 4) + 1 + empty_latent = torch.zeros( + [B, 16, total_latents, H, W], device=model_management.intermediate_device() + ) + + if prev_samples is None or motion_latent_count == 0: + known = anchor_latent + else: + motion_latent = prev_samples["samples"][:, :, -motion_latent_count:].clone() + log(f"motion_latent shape={tuple(motion_latent.shape)} " + f"min={motion_latent.min().item():.4f} max={motion_latent.max().item():.4f}") + known = torch.cat([anchor_latent, motion_latent.to(device=device, dtype=dtype)], dim=2) + + known_frames = min(known.shape[2], total_latents) + known = known[:, :, :known_frames] + padding_size = total_latents - known_frames + log(f"total_latents={total_latents} known_frames={known_frames} padding_size={padding_size}") + + if padding_size > 0: + # known을 픽셀로 디코딩 -> 회색(0.5) filler 픽셀과 하나의 시퀀스로 합쳐서 + # 단 한 번만 인코딩 (PainterI2V/코어 WanImageToVideo와 동일한 방식). + # + # decode와 encode를 한 노드 안에서 연속 호출하면 decode가 남긴 중간 버퍼가 + # 안 비워진 채로 encode가 (최대 length 프레임 전체를 다시 인코딩하는) 큰 + # 버퍼를 새로 잡으려다 OOM이 날 수 있다. 그래서 decode 결과를 CPU로 옮겨 + # GPU 버퍼를 바로 비우고, encode 직전에 캐시를 명시적으로 정리한다. + known_pixels = vae.decode(known) + del known + log(f"known_pixels (raw decode) shape={tuple(known_pixels.shape)}") + if known_pixels.ndim == 5: + known_pixels = known_pixels[0] # [B,T,H,W,C] -> [T,H,W,C], B==1 가정 + known_pixels = known_pixels[:, :, :, :3].to("cpu") + log(f"known_pixels shape={tuple(known_pixels.shape)} " + f"min={known_pixels.min().item():.4f} max={known_pixels.max().item():.4f} " + f"mean={known_pixels.mean().item():.4f}") + model_management.soft_empty_cache() + + n_known_px = known_pixels.shape[0] + n_gray_px = max(length - n_known_px, 0) + log(f"n_known_px={n_known_px} n_gray_px={n_gray_px}") + if n_gray_px > 0: + gray = torch.ones( + (n_gray_px, known_pixels.shape[1], known_pixels.shape[2], 3), + dtype=known_pixels.dtype, + ) * 0.5 + combined = torch.cat([known_pixels, gray], dim=0) + else: + combined = known_pixels + del known_pixels + log(f"combined shape={tuple(combined.shape)}") + + image_cond_latent = vae.encode(combined).to(device=device, dtype=dtype) + del combined + model_management.soft_empty_cache() + log(f"image_cond_latent shape={tuple(image_cond_latent.shape)} " + f"min={image_cond_latent.min().item():.4f} max={image_cond_latent.max().item():.4f} " + f"mean={image_cond_latent.mean().item():.4f} std={image_cond_latent.std().item():.4f}") + else: + image_cond_latent = known + + mask = torch.ones((1, 1, total_latents, H, W), device=device, dtype=dtype) + mask[:, :, :known_frames] = 0.0 # anchor + motion 프레임 전부 known (원본 마스크 버그 수정) + + positive = node_helpers.conditioning_set_values( + positive, {"concat_latent_image": image_cond_latent, "concat_mask": mask} + ) + negative = node_helpers.conditioning_set_values( + negative, {"concat_latent_image": image_cond_latent, "concat_mask": mask} + ) + + return (positive, negative, {"samples": empty_latent}) diff --git a/wan_svipro_motion.py b/wan_svipro_motion.py new file mode 100644 index 0000000000000000000000000000000000000000..fb9d0322c705e08fa35dfd04cee8bd1d43355554 --- /dev/null +++ b/wan_svipro_motion.py @@ -0,0 +1,152 @@ +import torch +import comfy.latent_formats +import comfy.model_management as model_management +import node_helpers + + +class WanImageToVideoSVIProMotion_Dolphin: + """ + WanImageToVideoSVIPro(comfyui-kjnodes)의 드롭인 교체 노드 — filler 표현은 원본 그대로 두고 + **모션 증폭만** 얹은 버전. + + [wan_svipro_gray.py](wan_svipro_gray.py)와의 차이 — 그쪽은 filler를 회색(0.5) 픽셀 VAE + 인코딩으로 바꾸고 마스크도 anchor+motion 전체 known으로 바꿨다. 두 가지를 동시에 바꾸는 + 바람에 A/B가 안 깨끗했고, 무엇보다 SVI Pro LoRA는 **원본 규약**(process_out(zeros) 패딩 + + mask 첫 프레임만 known) 위에서 파인튜닝된 가중치라, 회색 filler는 base Wan에겐 + in-distribution이어도 SVI LoRA에겐 분포 밖 입력이 된다. 실제로 회색 버전은 Clip1부터 + 그레이 노이즈가 났다. + + 이 노드는 그래서 **패딩과 마스크를 원본과 비트 단위로 동일하게** 두고, PainterI2V가 실제로 + 슬로우모션을 잡는 그 연산 하나만 추가한다: + + diff = filler - base_latent # 마지막 known latent 기준 + diff_mean = diff.mean(dim=(1,3,4)) # 밝기 성분 분리 (증폭에서 제외) + scaled = base + (diff - diff_mean) * amplitude + diff_mean + + 코어 `WanImageToVideo`도 이미 회색 filler를 쓰는데 슬로우모션이 난다 — + [slowmo_ab_test_A_core_wanimagetovideo.json](slowmo_ab_test_A_core_wanimagetovideo.json)에서 + 확인됐다. 즉 filler를 뭘로 채우냐가 아니라 **앵커 대비 거리를 얼마나 벌려주냐**가 모션 + 진폭을 결정한다. 그 거리를 키우는 게 이 증폭이다. + + amplitude_mode: + - "constant": PainterI2V와 동일. filler 전 구간을 같은 배율로 증폭. + 원본 패딩은 시간축으로 상수라 증폭해도 상수 — "앵커에서 이만큼 멀어진다"는 + 거리 신호만 커진다. + - "ramp": 첫 filler 프레임 1.0 → 마지막 프레임 motion_amplitude로 선형 증가. + 상수 블록에 실제 시간 기울기를 만들어 "뒤로 갈수록 더 멀어진다"는 + 신호를 준다. 원본 패딩의 "미래에 변화 없음" 결함을 직접 겨냥한 변형. + + 출력이 conditioning 2쌍인 이유(PainterI2VAdvanced와 동일한 설계): 모션은 high-noise + 스테이지가 결정하고 low-noise는 디테일·색을 정리한다. low까지 증폭본을 먹이면 색 드리프트와 + 아티팩트만 늘어난다. high_* 는 증폭본, low_* 는 원본 그대로다. + + motion_amplitude=1.0이면 두 쌍 모두 상류 원본 SVIPro의 출력과 완전히 동일하다(회귀 확인용). + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 4}), + "anchor_samples": ("LATENT",), + "motion_latent_count": ("INT", {"default": 1, "min": 0, "max": 128, "step": 1, + "tooltip": "prev_samples 끝에서 몇 latent 프레임을 모션 컨텍스트로 이어붙일지."}), + "motion_amplitude": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.05, + "tooltip": "1.0=상류 SVIPro와 동일. 1.15부터 시작해서 1.3/1.5로 스윕."}), + "amplitude_mode": (["constant", "ramp"], {"default": "constant", + "tooltip": "constant=PainterI2V와 동일. ramp=뒤로 갈수록 증폭을 키워 시간 기울기를 만듦."}), + }, + "optional": { + "prev_samples": ("LATENT", {"tooltip": "이전 클립의 latent. 끝의 motion_latent_count 프레임만 사용."}), + }, + } + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("high_positive", "high_negative", "low_positive", "low_negative", "latent") + FUNCTION = "encode" + CATEGORY = "Dolphin Node/Wan Video" + + def encode(self, positive, negative, length, anchor_samples, motion_latent_count, + motion_amplitude, amplitude_mode, prev_samples=None): + log = lambda *a: print("[SVIProMotion]", *a) + + anchor_latent = anchor_samples["samples"].clone() + B, C, T, H, W = anchor_latent.shape + device, dtype = anchor_latent.device, anchor_latent.dtype + + total_latents = ((length - 1) // 4) + 1 + empty_latent = torch.zeros( + [B, 16, total_latents, H, W], device=model_management.intermediate_device() + ) + + # ---- 여기부터 상류 kjnodes WanImageToVideoSVIPro와 동일 ---- + if prev_samples is None or motion_latent_count == 0: + known_frames = anchor_latent.shape[2] + image_cond_latent = anchor_latent + else: + motion_latent = prev_samples["samples"][:, :, -motion_latent_count:].clone() + known_frames = anchor_latent.shape[2] + motion_latent.shape[2] + image_cond_latent = torch.cat( + [anchor_latent, motion_latent.to(device=device, dtype=dtype)], dim=2 + ) + + padding_size = total_latents - known_frames + log(f"total_latents={total_latents} known_frames={known_frames} padding_size={padding_size} " + f"amplitude={motion_amplitude} mode={amplitude_mode}") + + if padding_size > 0: + padding = torch.zeros(1, C, padding_size, H, W, dtype=dtype, device=device) + padding = comfy.latent_formats.Wan21().process_out(padding) + image_cond_latent = torch.cat([image_cond_latent, padding], dim=2) + elif padding_size < 0: + # known이 total보다 길면 상류는 그냥 크래시한다. 잘라서 진행. + log(f"known_frames > total_latents — {total_latents}프레임으로 자름") + image_cond_latent = image_cond_latent[:, :, :total_latents] + known_frames = total_latents + + mask = torch.ones((1, 1, total_latents, H, W), device=device, dtype=dtype) + mask[:, :, :1] = 0.0 + # ---- 상류와 동일한 부분 끝 ---- + + cond_low = image_cond_latent + cond_high = self._amplify(cond_low, known_frames, motion_amplitude, amplitude_mode, log) + + high_positive = node_helpers.conditioning_set_values( + positive, {"concat_latent_image": cond_high, "concat_mask": mask}) + high_negative = node_helpers.conditioning_set_values( + negative, {"concat_latent_image": cond_high, "concat_mask": mask}) + low_positive = node_helpers.conditioning_set_values( + positive, {"concat_latent_image": cond_low, "concat_mask": mask}) + low_negative = node_helpers.conditioning_set_values( + negative, {"concat_latent_image": cond_low, "concat_mask": mask}) + + return (high_positive, high_negative, low_positive, low_negative, + {"samples": empty_latent}) + + @staticmethod + def _amplify(cond, known_frames, amplitude, mode, log): + """filler 구간만 마지막 known latent 대비 차이를 증폭. known 프레임은 안 건드림.""" + if amplitude <= 1.0 or cond.shape[2] <= known_frames: + return cond + + base_latent = cond[:, :, known_frames - 1:known_frames] + filler = cond[:, :, known_frames:] + n_filler = filler.shape[2] + + diff = filler - base_latent + diff_mean = diff.mean(dim=(1, 3, 4), keepdim=True) # 밝기 성분 — 증폭 대상 아님 + diff_centered = diff - diff_mean + + if mode == "ramp" and n_filler > 1: + scale = torch.linspace(1.0, amplitude, n_filler, + device=cond.device, dtype=cond.dtype).view(1, 1, n_filler, 1, 1) + else: + scale = amplitude + + scaled = torch.clamp(base_latent + diff_centered * scale + diff_mean, -6, 6) + log(f"amplified filler[{known_frames}:{cond.shape[2]}] " + f"|diff| {diff.abs().mean().item():.4f} -> {(scaled - base_latent).abs().mean().item():.4f}") + + return torch.cat([cond[:, :, :known_frames], scaled], dim=2) diff --git a/web/dolphin_lora_rows.js b/web/dolphin_lora_rows.js new file mode 100644 index 0000000000000000000000000000000000000000..4141d382d01aec469ad05452a3b7c9dff5a5db0c --- /dev/null +++ b/web/dolphin_lora_rows.js @@ -0,0 +1,541 @@ +import { app } from "../../scripts/app.js"; + +// Compacts DolphinVisionDirector's 16 lora slots (each: combo + on/off + weight +// string = 3 separate stock widgets) into single power-lora-loader-style rows: +// [toggle] [lora name] [weight]. The 3 original widgets are kept exactly where +// they are in node.widgets (never reordered/removed) so saved-workflow +// widgets_values arrays keep working; they are only visually hidden (0 height). +// The compact rows are appended at the END of node.widgets via addCustomWidget, +// which never shifts the index of any pre-existing widget. + +const VISION_NODE_NAME = "DolphinVisionDirector"; +const ARCH_NODE_NAME = "DolphinArchitectV16"; +const NUM_CLIPS = 4; +const NUM_LORAS = 4; +const NUM_ARCH_LORAS = 6; +const ROW_HEIGHT = 24; +const HEADER_HEIGHT = 18; + +function fitText(ctx, str, maxWidth) { + if (!str) return ""; + const full = ctx.measureText(str).width; + if (full <= maxWidth) return str; + const ellipsis = "…"; + let lo = 0, hi = str.length; + while (lo < hi) { + const mid = Math.ceil((lo + hi) / 2); + if (ctx.measureText(str.slice(0, mid) + ellipsis).width <= maxWidth) lo = mid; + else hi = mid - 1; + } + return str.slice(0, lo) + ellipsis; +} + +function hideWidget(widget, suffix = "") { + if (!widget.__dolphinHidden) { + widget.__dolphinHidden = true; + widget.origType = widget.type; + widget.origComputeSize = widget.computeSize; + widget.origSerializeValue = widget.serializeValue; + // Zero, not negative: with 48 widgets hidden at once, the usual "-4" + // margin-collapse trick (fine for a single hidden widget) compounds to a + // large negative offset that throws off LiteGraph's remaining-space + // distribution to flexible multiline widgets (essential_actions, + // artistic_vibe), ballooning their height. + widget.computeSize = () => [0, 0]; + widget.type = "converted-widget" + suffix; + } + // This ComfyUI build renders stock combo/string/boolean widgets as real DOM + // elements (widget.element) absolutely positioned over the canvas, not pure + // canvas draws — computeSize/type alone only affects canvas layout and does + // NOT hide the floating DOM element. Same fix the bundled tripleksampler + // custom node's own UI extension uses for its dynamic widgets. + widget.hidden = true; + if (widget.options) widget.options.hidden = true; + if (widget.element) { + widget.element.style.display = "none"; + const label = widget.element.previousElementSibling; + if (label && label.textContent === widget.name + ":") { + label.style.display = "none"; + } + } +} + +// SERIALIZE_FALSE: every custom widget in this file is presentation-only — the real +// values live on the stock widgets it wraps. LGraphNode.serialize() in this build does +// NOT call serializeValue(); it reads `widget.value` directly and writes `value ?? null`, +// skipping a widget only when `widget.serialize === false` (note: the plain property, +// not `options.serialize`). Without this flag every custom widget appended a trailing +// null to widgets_values — the Architect node was saving 26 entries where 19 are real. +// The nulls were harmless (custom widgets are appended AFTER the stock ones, so no index +// shifted) but they grow every saved workflow, and old files with the extra trailing +// values still load correctly since only the tail is affected. +class DolphinSectionHeaderWidget { + constructor(label) { + this.type = "custom"; + this.name = "dolphin_header_" + label; + this.__dolphinHidden = true; // never touched by hideWidget, just marks "not a real value widget" + this.serialize = false; + this.label = label; + } + computeSize(width) { + return [width, HEADER_HEIGHT]; + } + draw(ctx, node, widgetWidth, posY, height) { + ctx.save(); + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.globalAlpha = 0.75; + ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + ctx.font = "bold 11px Arial"; + ctx.fillText(this.label, 15, posY + height * 0.5); + ctx.restore(); + } + mouse() { + return false; + } + serializeValue() { + return undefined; + } +} + +class DolphinLoraRowWidget { + constructor(name, comboW, onW, weightW) { + this.type = "custom"; + this.name = name; + this.serialize = false; // SERIALIZE_FALSE 참고 + this.comboW = comboW; + this.onW = onW; + this.weightW = weightW; + this.hitAreas = { + toggle: [0, 0], + lora: [0, 0], + weight: [0, 0], + }; + } + computeSize(width) { + return [width, ROW_HEIGHT]; + } + serializeValue() { + return undefined; // real values live on comboW/onW/weightW, not on this row + } + draw(ctx, node, widgetWidth, posY, height) { + const margin = 15; + const inner = 6; + const on = this.onW.value !== false; + ctx.save(); + + // row background + ctx.beginPath(); + ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR; + ctx.roundRect(margin, posY + 1, widgetWidth - margin * 2, height - 2, [height * 0.25]); + ctx.fill(); + ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR; + ctx.stroke(); + + const midY = posY + height * 0.5; + let x = margin + inner; + + // toggle switch + const toggleH = height - 10; + const toggleW = toggleH * 1.7; + ctx.beginPath(); + ctx.fillStyle = "#00000055"; + ctx.roundRect(x, posY + 5, toggleW, toggleH, [toggleH * 0.5]); + ctx.fill(); + ctx.beginPath(); + ctx.fillStyle = on ? "#8ab4ff" : "#777"; + const knobX = on ? x + toggleW - toggleH * 0.5 : x + toggleH * 0.5; + ctx.arc(knobX, posY + height * 0.5, toggleH * 0.36, 0, Math.PI * 2); + ctx.fill(); + this.hitAreas.toggle = [x, toggleW]; + x += toggleW + inner * 1.5; + + ctx.globalAlpha = on ? 1 : 0.45; + + // weight field (measure first, right-aligned) + ctx.font = "11px Arial"; + const weightText = (this.weightW.value ?? "").toString(); + const weightBoxW = Math.min(100, Math.max(46, ctx.measureText(weightText).width + 14)); + const weightX = widgetWidth - margin - inner - weightBoxW; + + // lora name (fills remaining space) + const nameX = x; + const nameW = Math.max(20, weightX - inner - nameX); + const hasLora = this.comboW.value && this.comboW.value !== "None"; + ctx.fillStyle = hasLora ? LiteGraph.WIDGET_TEXT_COLOR : LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText(fitText(ctx, hasLora ? this.comboW.value : "— pick lora —", nameW), nameX, midY); + this.hitAreas.lora = [nameX, nameW]; + + // weight box + ctx.beginPath(); + ctx.fillStyle = "#00000055"; + ctx.roundRect(weightX, posY + 4, weightBoxW, height - 8, [4]); + ctx.fill(); + ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + ctx.textAlign = "center"; + ctx.fillText(fitText(ctx, weightText, weightBoxW - 10), weightX + weightBoxW / 2, midY); + this.hitAreas.weight = [weightX, weightBoxW]; + + ctx.restore(); + } + _within(pos, bounds) { + return pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[1]; + } + mouse(event, pos, node) { + if (event.type !== "pointerdown") return false; + if (this._within(pos, this.hitAreas.toggle)) { + this.onW.value = this.onW.value === false ? true : false; + node.setDirtyCanvas(true, true); + return true; + } + if (this._within(pos, this.hitAreas.lora)) { + let values = this.comboW.options?.values ?? []; + if (typeof values === "function") values = values(this.comboW, node) ?? []; + const canvas = app.canvas; + // Plain string array + single top-level callback is what makes + // LiteGraph.ContextMenu's built-in search/filter box appear (same + // pattern rgthree's showLoraChooser and KJNodes use) — passing + // {content, callback} objects per item silently skips that filter. + new LiteGraph.ContextMenu([...values], { + event, + scale: Math.max(1, canvas.ds?.scale ?? 1), + className: "dark", + title: "Choose a lora", + callback: (value) => { + if (typeof value !== "string") return; + this.comboW.value = value; + if (typeof this.comboW.callback === "function") { + this.comboW.callback(value, canvas, node, pos, event); + } + node.setDirtyCanvas(true, true); + }, + }); + return true; + } + if (this._within(pos, this.hitAreas.weight)) { + app.canvas.prompt( + "Weight (base, high, low)", + this.weightW.value ?? "", + (v) => { + if (v !== null) { + this.weightW.value = v; + node.setDirtyCanvas(true, true); + } + }, + event, + ); + return true; + } + return false; + } +} + +class DolphinTextFieldWidget { + constructor(label, targetW, multiline = false) { + this.type = "custom"; + this.name = "dolphin_field_" + targetW.name; + this.serialize = false; // SERIALIZE_FALSE 참고 + this.label = label; + this.targetW = targetW; + this.multiline = multiline; + this.hitAreas = { field: [0, 0] }; + } + computeSize(width) { + return [width, ROW_HEIGHT]; + } + serializeValue() { + return undefined; // real value lives on targetW + } + draw(ctx, node, widgetWidth, posY, height) { + const margin = 15; + const inner = 8; + ctx.save(); + ctx.beginPath(); + ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR; + ctx.roundRect(margin, posY + 1, widgetWidth - margin * 2, height - 2, [height * 0.25]); + ctx.fill(); + ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR; + ctx.stroke(); + + const midY = posY + height * 0.5; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + ctx.fillText(this.label, margin + inner, midY); + const labelW = ctx.measureText(this.label).width; + + const valueX = margin + inner + labelW + inner; + const valueW = Math.max(20, widgetWidth - margin - inner - valueX); + const text = (this.targetW.value ?? "").toString().replace(/\n/g, " ⏎ "); + ctx.fillStyle = text ? LiteGraph.WIDGET_TEXT_COLOR : LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + ctx.fillText(fitText(ctx, text || "(empty, click to edit)", valueW), valueX, midY); + this.hitAreas.field = [margin, widgetWidth - margin * 2]; + ctx.restore(); + } + _within(pos, bounds) { + return pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[1]; + } + mouse(event, pos, node) { + if (event.type !== "pointerdown") return false; + if (this._within(pos, this.hitAreas.field)) { + app.canvas.prompt( + this.label, + this.targetW.value ?? "", + (v) => { + if (v !== null) { + this.targetW.value = v; + node.setDirtyCanvas(true, true); + } + }, + event, + this.multiline, + ); + return true; + } + return false; + } +} + +// 숨겨진 스톡 위젯은 .value만 바꿔도 직렬화(widgets_values)에는 제대로 반영되지만, +// 이 빌드는 combo/string/boolean을 실제 DOM 엘리먼트로도 들고 있어서 그쪽 값이 남아 +// 있으면 나중에 ComfyUI의 per-frame DOM 싱크가 옛 값을 되돌려 쓸 수 있다. 그래서 +// .value를 바꿀 때 엘리먼트도 같이 맞춰준다. +function syncWidgetElement(widget) { + const el = widget?.element; + if (!el) return; + try { + if (el.type === "checkbox") el.checked = widget.value !== false; + else if ("value" in el) el.value = widget.value ?? ""; + } catch (e) { + console.error("[Dolphin.LoraRows] element sync failed:", e); + } +} + +// 6개 슬롯을 Python 쪽 기본값(lora="None", on=True, weights="0.0, 0.0, 0.0")으로 +// 되돌리는 버튼. 되돌릴 수 없는 조작이라 ContextMenu로 한 번 확인을 받는다 - +// 파일 상단의 lora chooser와 같은 방식이라 별도 UI 의존성이 없다. +class DolphinResetButtonWidget { + constructor(label, slots, extraW) { + this.type = "custom"; + this.name = "dolphin_reset_button"; + this.serialize = false; // SERIALIZE_FALSE 참고 + this.label = label; + this.slots = slots; // [{comboW, onW, weightW}, ...] + this.extraW = extraW; // manual_triggers (없을 수도 있음) + this.hitAreas = { button: [0, 0] }; + } + computeSize(width) { + return [width, ROW_HEIGHT]; + } + serializeValue() { + return undefined; // 값이 없는 순수 액션 위젯 + } + draw(ctx, node, widgetWidth, posY, height) { + const margin = 15; + ctx.save(); + ctx.beginPath(); + ctx.fillStyle = "#00000055"; + ctx.roundRect(margin, posY + 1, widgetWidth - margin * 2, height - 2, [height * 0.25]); + ctx.fill(); + ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR; + ctx.stroke(); + + ctx.font = "11px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + ctx.fillText(this.label, widgetWidth * 0.5, posY + height * 0.5); + this.hitAreas.button = [margin, widgetWidth - margin * 2]; + ctx.restore(); + } + _within(pos, bounds) { + return pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[1]; + } + _reset(node) { + for (const { comboW, onW, weightW } of this.slots) { + comboW.value = "None"; + onW.value = true; + weightW.value = "0.0, 0.0, 0.0"; + syncWidgetElement(comboW); + syncWidgetElement(onW); + syncWidgetElement(weightW); + } + if (this.extraW) { + this.extraW.value = ""; + syncWidgetElement(this.extraW); + } + node.setDirtyCanvas(true, true); + } + mouse(event, pos, node) { + if (event.type !== "pointerdown") return false; + if (!this._within(pos, this.hitAreas.button)) return false; + const CONFIRM = `Reset all ${this.slots.length} slots`; + new LiteGraph.ContextMenu([CONFIRM, "Cancel"], { + event, + scale: Math.max(1, app.canvas.ds?.scale ?? 1), + className: "dark", + title: "Clear every lora, weight and trigger?", + callback: (value) => { + if (value === CONFIRM) this._reset(node); + }, + }); + return true; + } +} + +// buildRows(node) does the type-specific widget lookup + addCustomWidget calls +// and returns the array of stock widgets that got compacted into rows (so they +// can be hidden). Everything past this point (hiding, retry timers, size snap) +// is identical for every node type this gets installed on. +function installLoraRows(nodeType, buildRows) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const ret = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + const node = this; + + // Everything below is purely cosmetic (compact lora rows). If anything + // in here throws, it must NEVER propagate — an uncaught error here + // during graph.configure() can abort loading the rest of the workflow, + // leaving a blank canvas. Fail loud in the console, fail safe for the user. + try { + if (!node.__dolphinLoraRowsBuilt) { + node.__dolphinLoraRowsBuilt = true; + node.__dolphinHiddenWidgets = buildRows(node) || []; + } + + // widget.element (the real DOM ) may not exist yet at + // construction time, and ComfyUI's own per-frame DOM-sync can re-show + // it later — so (re-)apply hiding now and on a couple of deferred + // passes to make sure it sticks. This does NOT touch node.size, so it + // never fights a manual resize the user makes afterwards. + const applyHiding = () => { + try { + for (const w of node.__dolphinHiddenWidgets || []) hideWidget(w); + node.setDirtyCanvas(true, true); + } catch (e) { + console.error("[Dolphin.LoraRows] applyHiding failed:", e); + } + }; + applyHiding(); + setTimeout(applyHiding, 50); + setTimeout(applyHiding, 250); + + // Snap node.size to the freshly computed minimum ONCE on (re)build. + // Collapsing many stock widgets into compact rows frees up a lot of + // height; a stale saved node.size (from before this UI existed) would + // otherwise leave that freed space around, and flexible multiline + // widgets auto-grow to fill it. + setTimeout(() => { + try { + const computed = node.computeSize(); + node.size = node.size || [0, 0]; + node.size[1] = computed[1]; + node.setDirtyCanvas(true, true); + } catch (e) { + console.error("[Dolphin.LoraRows] size snap failed:", e); + } + }, 300); + } catch (e) { + console.error("[Dolphin.LoraRows] onNodeCreated hook failed:", e); + } + return ret; + }; +} + +function buildVisionDirectorRows(node) { + const hidden = []; + for (let c = 1; c <= NUM_CLIPS; c++) { + node.addCustomWidget(new DolphinSectionHeaderWidget(`── Clip ${c} Loras ──`)); + for (let n = 1; n <= NUM_LORAS; n++) { + const comboW = node.widgets.find((w) => w.name === `c${c}_lora_${n}`); + const onW = node.widgets.find((w) => w.name === `c${c}_lora_${n}_on`); + const weightW = node.widgets.find((w) => w.name === `c${c}_w_${n}`); + if (!comboW || !onW || !weightW) continue; + hidden.push(comboW, onW, weightW); + node.addCustomWidget(new DolphinLoraRowWidget(`c${c}_lora_${n}_row`, comboW, onW, weightW)); + } + } + const manualTriggersW = node.widgets.find((w) => w.name === "manual_triggers"); + if (manualTriggersW) { + hidden.push(manualTriggersW); + node.addCustomWidget(new DolphinTextFieldWidget("manual_triggers:", manualTriggersW, true)); + } + return hidden; +} + +// DolphinArchitectV16 (🐬 Dolphin Architect V16): one flat bank of lora_1..6 / +// lora_1_on..6_on / weights_1..6 (each already base+high+low as one comma +// string, e.g. "0.4, 0.7, 0") — no per-clip grouping, so no section headers. +// 파일명에 HIGH/LOW가 들어있으면 Wan2.2 분리 로라로 보고 그쪽 모델 슬롯만 켠다 +// (base,high,low). 둘 다 없으면 Wan2.1 등 범용 로라로 보고 셋 다 켠다. "high_noise"/ +// "low_noise" 같은 변형도 그냥 substring 매치라 자동으로 걸린다. +// HN/LN 약어(-HN-, _HN_, ...-HN.safetensors 식)는 앞뒤가 영숫자가 아닐 때만 매치하는 +// 토큰 경계 정규식을 쓴다 - 그냥 substring이면 "krea2DepthControlnet"(contro-L-N-et) +// 같은 무관한 파일명이 "ln"을 우연히 포함해서 Low Noise로 오인식된다. +const HN_TOKEN = /(?:^|[^a-z0-9])hn(?:[^a-z0-9]|$)/; +const LN_TOKEN = /(?:^|[^a-z0-9])ln(?:[^a-z0-9]|$)/; +// 한 글자 H/L 토큰(-H-, _H_, -L-, _L_, 그리고 확장자/이름 끝의 _H·-L 등). +// hn/ln보다 한 글자 짧아서 오탐 위험이 훨씬 크기 때문에 두 가지로 좁혔다: +// (1) 구분자를 '-'와 '_'로만 한정한다. hn/ln이 쓰는 일반 토큰 경계([^a-z0-9])까지 +// 허용하면 "flaccid-v1.0"의 점·공백·괄호 같은 문자도 경계가 되어 범위가 급격히 넓어진다. +// (2) 대문자만 매치한다(그래서 소문자화한 n이 아니라 원본 raw로 검사). HIGH/LOW 약어는 +// 관례적으로 대문자이고, 소문자까지 받으면 "..._v2_h_final" 같은 무관한 이름이 걸린다. +const H_TOKEN = /[-_]H(?=[-_]|\.[A-Za-z0-9]+$|$)/; +const L_TOKEN = /[-_]L(?=[-_]|\.[A-Za-z0-9]+$|$)/; +function guessLoraWeights(name) { + if (!name || name === "None") return null; + const raw = String(name); + const n = raw.toLowerCase(); + if (n.includes("high") || HN_TOKEN.test(n) || H_TOKEN.test(raw)) return "1, 1, 0"; + if (n.includes("low") || LN_TOKEN.test(n) || L_TOKEN.test(raw)) return "0, 0, 1"; + return "1, 1, 1"; +} + +// comboW.callback은 유저가 드롭다운/행-UI에서 실제로 선택했을 때만 불린다 - 워크플로우 +// 로드 시 configure()가 widgets_values를 복원하는 경로는 .value를 직접 대입할 뿐 이 +// callback을 거치지 않으므로, 저장된 수동 가중치를 자동으로 덮어쓸 위험이 없다. +function wireAutoWeight(comboW, weightW) { + const orig = comboW.callback; + comboW.callback = function (value, ...rest) { + const guess = guessLoraWeights(value); + if (guess !== null) weightW.value = guess; + return orig ? orig.apply(this, [value, ...rest]) : undefined; + }; +} + +function buildArchitectRows(node) { + const hidden = []; + const slots = []; + for (let n = 1; n <= NUM_ARCH_LORAS; n++) { + const comboW = node.widgets.find((w) => w.name === `lora_${n}`); + const onW = node.widgets.find((w) => w.name === `lora_${n}_on`); + const weightW = node.widgets.find((w) => w.name === `weights_${n}`); + if (!comboW || !onW || !weightW) continue; + wireAutoWeight(comboW, weightW); + hidden.push(comboW, onW, weightW); + slots.push({ comboW, onW, weightW }); + node.addCustomWidget(new DolphinLoraRowWidget(`lora_${n}_row`, comboW, onW, weightW)); + } + const manualTriggersW = node.widgets.find((w) => w.name === "manual_triggers"); + if (manualTriggersW) { + hidden.push(manualTriggersW); + node.addCustomWidget(new DolphinTextFieldWidget("manual_triggers:", manualTriggersW, true)); + } + if (slots.length) { + node.addCustomWidget(new DolphinResetButtonWidget("⟲ Reset all slots", slots, manualTriggersW)); + } + return hidden; +} + +app.registerExtension({ + name: "Dolphin.LoraRows", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name === VISION_NODE_NAME) { + installLoraRows(nodeType, buildVisionDirectorRows); + } else if (nodeData?.name === ARCH_NODE_NAME) { + installLoraRows(nodeType, buildArchitectRows); + } + }, +}); diff --git a/web/dolphin_preset_sync.js b/web/dolphin_preset_sync.js new file mode 100644 index 0000000000000000000000000000000000000000..bedcb43b35550bf79985dcff21fec31da5f46b68 --- /dev/null +++ b/web/dolphin_preset_sync.js @@ -0,0 +1,102 @@ +import { app } from "../../scripts/app.js"; + +// When the user picks a llm_preset/vlm_preset dropdown value, mirror the +// resolved slug into the paired llm_model/vlm_model text widget so what's +// displayed matches what dolphin_vision_director.py's resolve_slug() will +// actually call. Picking the "custom" sentinel leaves the text field alone +// (that's the whole point of custom mode). +// +// Keep these two tables in sync with LLM_PRESETS / VLM_PRESETS in +// dolphin_vision_director.py — they're duplicated here only because the +// backend doesn't expose the label->slug mapping over the object_info API, +// just the label list. + +// 2026-08-28: 파이썬 쪽이 한글 -> 영어 라벨로 옮겨간 뒤 이 테이블을 안 맞춰서 +// table[value]가 항상 undefined가 되고(=키가 하나도 안 맞음) 프리셋을 바꿔도 +// llm_model/vlm_model 텍스트 칸이 전혀 갱신 안 되는 버그가 있었다. 실제 추론에 +// 쓰이는 슬러그는 백엔드 resolve_slug()가 콤보 선택값을 직접 보고 매핑하므로 +// 이 버그와 무관하게 정상이었을 가능성이 높지만, 사용자가 확인할 유일한 화면 +// 요소가 이 텍스트 칸이라 "모델이 안 바뀐다"는 리포트로 이어졌다. 라벨을 여기 +// 하드코딩하는 구조 자체가 파이썬 쪽 변경 때마다 또 어긋날 수 있으니, 프리셋 +// 목록을 고칠 땐 항상 두 파일(dolphin_vision_director.py의 LLM_PRESETS/ +// VLM_PRESETS, 그리고 dolphin_minimax_director.py의 동일 복제본)과 이 파일을 +// 세 군데 다 맞출 것. +const LLM_PRESETS = { + "🔞 DeepSeek V3.2 (uncensored · stable JSON · recommended)": "deepseek/deepseek-v3.2", + "🔞 Magnum v4 72B (natural phrasing)": "anthracite-org/magnum-v4-72b", + "🔞 Cydonia 24B v4.1 (lightweight · fast)": "thedrummer/cydonia-24b-v4.1", + // 2026-09-03: :free 슬러그 404 - 유료로 교체. + "🔞 Dolphin Mistral Venice (uncensored)": "cognitivecomputations/dolphin-mistral-24b-venice-edition", + // 2026-09-03 추가 (모델 페이지로만 확인, chat completions 스모크테스트 미완). + "🔞 Hermes 4 405B (steerable · strict format)": "nousresearch/hermes-4-405b", + // 2026-09-03: grok-4 / grok-4-fast deprecated(404) - grok-4.3으로 교체. + "🔞 Grok 4.3 (edgy prose · $1.25/$2.5)": "x-ai/grok-4.3", + "🔞 Euryale 70B L3.3 (sensory prose · loose format)": "sao10k/l3.3-euryale-70b", +}; +// Valkyrie 49B V1은 추가했다가 뺐다 (2026-08-28, 실제 API 404 "No endpoints +// found") - dolphin_vision_director.py의 LLM_PRESETS 옆 주석 참고. + +const VLM_PRESETS = { + "👁 Qwen2.5-VL 72B (special cases · recommended)": "qwen/qwen2.5-vl-72b-instruct", + "👁 Qwen2.5-VL 32B (balanced)": "qwen/qwen2.5-vl-32b-instruct", + "👁 Qwen2.5-VL 32B (free)": "qwen/qwen2.5-vl-32b-instruct:free", + "👁 Mistral Small 3.1 24B (Pixtral)": "mistralai/mistral-small-3.1-24b-instruct", + "👁 Gemma 3 27B (lightweight)": "google/gemma-3-27b-it", +}; + +const NODE_NAMES = [ + "DolphinVisionDirector", + "DolphinPromptDirector", + "DolphinPromptDirectorSingle", + "DolphinMinimaxDirector", +]; +const SYNC_PAIRS = [ + { presetName: "llm_preset", modelName: "llm_model", table: LLM_PRESETS }, + { presetName: "vlm_preset", modelName: "vlm_model", table: VLM_PRESETS }, +]; + +function wireSync(node, presetName, modelName, table) { + const presetW = node.widgets?.find((w) => w.name === presetName); + const modelW = node.widgets?.find((w) => w.name === modelName); + if (!presetW || !modelW || presetW.__dolphinPresetSynced) return; + presetW.__dolphinPresetSynced = true; + + const origCallback = presetW.callback; + presetW.callback = function (value, ...rest) { + const ret = origCallback ? origCallback.apply(this, [value, ...rest]) : undefined; + try { + const slug = table[value]; + if (slug) { + modelW.value = slug; + if (typeof modelW.callback === "function") modelW.callback(slug); + node.setDirtyCanvas(true, true); + } + } catch (e) { + console.error("[Dolphin.PresetSync] sync failed:", e); + } + return ret; + }; +} + +app.registerExtension({ + name: "Dolphin.PresetSync", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (!NODE_NAMES.includes(nodeData?.name)) return; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const ret = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + // Never let this cosmetic hook break graph loading (see dolphin_lora_rows.js + // for why: an uncaught error here during graph.configure() can abort + // loading the rest of the workflow). + try { + for (const { presetName, modelName, table } of SYNC_PAIRS) { + wireSync(this, presetName, modelName, table); + } + } catch (e) { + console.error("[Dolphin.PresetSync] onNodeCreated hook failed:", e); + } + return ret; + }; + }, +});