diff --git a/.gitignore b/.gitignore index fa23bc7..8826229 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ test_*.py # Config files openaimodel.json +nul # Python cache __pycache__/ diff --git a/Example/AllNodes.json b/Example/AllNodes.json new file mode 100644 index 0000000..e51dd59 --- /dev/null +++ b/Example/AllNodes.json @@ -0,0 +1,896 @@ +{ + "id": "df4e9a49-6fc7-4de9-b5ac-07d4af3a7bab", + "revision": 0, + "last_node_id": 18, + "last_link_id": 0, + "nodes": [ + { + "id": 3, + "type": "AudioListGenerator", + "pos": [ + -1200, + 0 + ], + "size": [ + 270, + 174 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "name": "waveform", + "type": "AUDIO", + "link": null + } + ], + "outputs": [ + { + "name": "cycle", + "type": "INT", + "links": null + }, + { + "name": "audio_list", + "shape": 6, + "type": "AUDIO", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "AudioListGenerator" + }, + "widgets_values": [ + 23.976, + 81, + true, + 0.1, + "cosine" + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 4, + "type": "AudioToFrameCount", + "pos": [ + -1200, + 230 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": null + } + ], + "outputs": [ + { + "name": "frames", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "AudioToFrameCount" + }, + "widgets_values": [ + 25 + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 1, + "type": "AudioListCombine", + "pos": [ + -1200, + -180 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "audio_list", + "type": "AUDIO", + "link": null + } + ], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "AudioListCombine" + }, + "widgets_values": [ + "concatenate", + 0, + true, + 44100 + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 6, + "type": "QwenGPUInference", + "pos": [ + -880, + -170 + ], + "size": [ + 310, + 402 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": null + }, + { + "name": "used_seed", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "QwenGPUInference" + }, + "widgets_values": [ + "A girl in a coffee shop", + "Custom", + "", + 2048, + 0.7, + 0, + "randomize", + false, + false, + false, + 0.9, + 50 + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 9, + "type": "ChineseConverter", + "pos": [ + -1230, + 390 + ], + "size": [ + 310, + 200 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "converted_text", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "ChineseConverter" + }, + "widgets_values": [ + "", + true + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 10, + "type": "ChineseTranslate", + "pos": [ + -1220, + 640 + ], + "size": [ + 290, + 180 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "translated_text", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "ChineseTranslate" + }, + "widgets_values": [ + "" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 13, + "type": "AutoWhiteBalance", + "pos": [ + -270, + 390 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "AutoWhiteBalance" + }, + "widgets_values": [ + "gray_world", + 1, + true, + true + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 15, + "type": "ModelDownloader", + "pos": [ + -850, + 540 + ], + "size": [ + 240, + 230 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "status_message", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "ModelDownloader" + }, + "widgets_values": [ + "diffusion_models\nhttps://huggingface.co/unsloth/Qwen-Image-2512-GGUF/resolve/main/qwen-image-2512-Q8_0.gguf\nCLIP\nhttps://huggingface.co/unsloth/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct-Q8_0.gguf", + true, + true, + 4, + 8 + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 12, + "type": "PromptListGenerator", + "pos": [ + -570, + 390 + ], + "size": [ + 280, + 348 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "text_list", + "shape": 6, + "type": "STRING", + "links": null + }, + { + "name": "total_index", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "PromptListGenerator" + }, + "widgets_values": [ + "", + ",", + false, + false, + 0, + 0, + 10, + false, + false, + 0, + "randomize" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 14, + "type": "SimpleWildCardPlayer", + "pos": [ + -270, + 580 + ], + "size": [ + 300, + 260 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "prompt_list", + "shape": 6, + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "SimpleWildCardPlayer" + }, + "widgets_values": [ + "(masterpiece, best quality, photorealistic, 8k, highly detailed, solo, 1girl, realistic photography:1.2), natural skin texture, well-rested appearance, detailed eyelashes, with blunt fringe, see-through bangs, Long sideburns, side locks framing face, detailed hair strands, over fringe, with, on neckline, (she wearing a full and proper, lace undergarment, tones, fully clothed, fully covering legs, complete outfit.)", + "basic", + "years, age, pretit, contory, cm, face, realface_skin, bodytype, breasts, expression, eyeiled, eye_shape, eye_quality, eye_effect, eyecolor, hh, colors4, hairsize, hairlong, colors3, headwear, colors2, earring, earring-visibility, neck, mate, grid, wetdry, clothestight, tt-clothes, garter-tights, colors5, socks, shoeme, shoes, 169, light, RandomPose-light, backsence", + 1, + 0, + "randomize" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 11, + "type": "BatchToPSD", + "pos": [ + -860, + 400 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "message", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "BatchToPSD" + }, + "widgets_values": [ + "ComfyUI_PSD", + false + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 17, + "type": "NumberListGenerator", + "pos": [ + 100, + 550 + ], + "size": [ + 270, + 242 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "int_list", + "shape": 6, + "type": "INT", + "links": null + }, + { + "name": "float_list", + "shape": 6, + "type": "FLOAT", + "links": null + }, + { + "name": "total_count", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "NumberListGenerator" + }, + "widgets_values": [ + 0, + 10, + 1, + 10, + false, + -1, + "randomize" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 16, + "type": "CeilDivide", + "pos": [ + 100, + 390 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "result", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "CeilDivide" + }, + "widgets_values": [ + 1, + 1 + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 18, + "type": "FrameMatch", + "pos": [ + -570, + 780 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "FrameMatch" + }, + "widgets_values": [ + 100, + "repeat_last" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 7, + "type": "GGUFInference", + "pos": [ + -550, + -170 + ], + "size": [ + 290, + 450 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "image_1", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "image_2", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "image_3", + "shape": 7, + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": null + }, + { + "name": "used_seed", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "GGUFInference" + }, + "widgets_values": [ + "Download: QwenVL", + "", + "image_to_prompt.md", + "", + 4096, + 0.7, + 0.9, + 40, + 0, + "randomize", + false, + "Download: QwenVL mmproj", + true + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 8, + "type": "OpenAIHelper", + "pos": [ + -240, + -170 + ], + "size": [ + 270, + 400 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "image1", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "image2", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "image3", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + } + ], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": null + }, + { + "name": "model_name_list", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "OpenAIHelper" + }, + "widgets_values": [ + "http://localhost:11434/v1/chat/completions", + "ollama", + "huihui_ai/qwen3-vl-abliterated:4b-instruct", + "請分析提供的內容。", + "Custom", + 2000, + "請以繁體中文輸出使用者內容,不須包括引導或後綴,如「這就是你要的結果」、「以下是你要的結果」、「你要不要我幫你」、「你說的對」等等,只需要輸出使用者要的結論raw_text。請勿使用Markdown語法(如**粗體**),直接輸出純文字即可。", + "" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 5, + "type": "OpenRouterLLM", + "pos": [ + 70, + -170 + ], + "size": [ + 300, + 392 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "image_input_1", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "image_input_2", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "image_input_3", + "shape": 7, + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "image_output", + "type": "IMAGE", + "links": null + }, + { + "name": "text_output", + "type": "STRING", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "widget_ue_connectable": {}, + "Node name for S&R": "OpenRouterLLM" + }, + "widgets_values": [ + "", + "Please analyze the provided content.", + "google/gemma-3-27b-it:free", + "Custom", + -1, + "randomize", + "", + "", + false, + 1024, + 1024, + "lanczos" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + } + ], + "links": [], + "groups": [ + { + "id": 1, + "title": "Audio", + "bounding": [ + -1210, + -250, + 290, + 551.6 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "LLM", + "bounding": [ + -890, + -240, + 1280, + 533.6 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 3, + "title": "Chinese", + "bounding": [ + -1240, + 320, + 330, + 513.6 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 4, + "title": "Tools", + "bounding": [ + -870, + 320, + 920, + 580 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 5, + "title": "Math", + "bounding": [ + 90, + 320, + 290, + 485.6 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "config": {}, + "extra": { + "workflowRendererVersion": "LG", + "ue_links": [], + "ds": { + "scale": 0.7784765802692789, + "offset": [ + 1371.1727829459182, + 295.90198524771284 + ] + }, + "frontendVersion": "1.35.9", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/Example/StratAndEnd.json b/Example/StratAndEnd.json new file mode 100644 index 0000000..0c2fa21 --- /dev/null +++ b/Example/StratAndEnd.json @@ -0,0 +1,1717 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "revision": 0, + "last_node_id": 133, + "last_link_id": 69, + "nodes": [ + { + "id": 38, + "type": "CLIPLoader", + "pos": [ + 60, + 110 + ], + "size": [ + 270, + 106 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 33, + 34 + ] + } + ], + "title": "Load CLIP", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "CLIPLoader", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "umt5_xxl_fp16.safetensors", + "wan", + "default" + ] + }, + { + "id": 39, + "type": "VAELoader", + "pos": [ + 60, + 280 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 36, + 51 + ] + } + ], + "title": "Load VAE", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "VAELoader", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "wan_2.1_vae.safetensors" + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 370, + 110 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 33 + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 67 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 49 + ] + } + ], + "title": "CLIP Text Encode (Positive Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "CLIPTextEncode", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "Prompt" + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 370, + 380 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 34 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 50 + ] + } + ], + "title": "CLIP Text Encode (Negative Prompt)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "CLIPTextEncode", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" + ] + }, + { + "id": 120, + "type": "LoraLoaderModelOnly", + "pos": [ + 830, + 430 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 57 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 58 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "LoraLoaderModelOnly", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "Wan\\Wan2.2-Fun-A14B-InP-high-noise-MPS.safetensors", + 0.5 + ] + }, + { + "id": 122, + "type": "PathchSageAttentionKJ", + "pos": [ + 1140, + 310 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 59 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 61 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "7b1327192e4729085788a3020a9cbb095e0c7811", + "Node name for S&R": "PathchSageAttentionKJ", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "auto", + false + ] + }, + { + "id": 124, + "type": "ModelPatchTorchSettings", + "pos": [ + 1450, + 310 + ], + "size": [ + 312.095703125, + 58 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 61 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 38 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "7b1327192e4729085788a3020a9cbb095e0c7811", + "Node name for S&R": "ModelPatchTorchSettings", + "widget_ue_connectable": {} + }, + "widgets_values": [ + true + ] + }, + { + "id": 121, + "type": "PathchSageAttentionKJ", + "pos": [ + 1140, + 150 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 58 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 60 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "7b1327192e4729085788a3020a9cbb095e0c7811", + "Node name for S&R": "PathchSageAttentionKJ", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "auto", + false + ] + }, + { + "id": 123, + "type": "ModelPatchTorchSettings", + "pos": [ + 1440, + 160 + ], + "size": [ + 312.095703125, + 58 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 60 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 37 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "7b1327192e4729085788a3020a9cbb095e0c7811", + "Node name for S&R": "ModelPatchTorchSettings", + "widget_ue_connectable": {} + }, + "widgets_values": [ + true + ] + }, + { + "id": 55, + "type": "ModelSamplingSD3", + "pos": [ + 1130, + 460 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 38 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 43 + ] + } + ], + "title": "ModelSamplingSD3", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "ModelSamplingSD3", + "widget_ue_connectable": {} + }, + "widgets_values": [ + 5 + ] + }, + { + "id": 54, + "type": "ModelSamplingSD3", + "pos": [ + 1440, + 450 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 37 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 39 + ] + } + ], + "title": "ModelSamplingSD3", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "ModelSamplingSD3", + "widget_ue_connectable": {} + }, + "widgets_values": [ + 5 + ] + }, + { + "id": 57, + "type": "KSamplerAdvanced", + "pos": [ + 1150, + 600 + ], + "size": [ + 270, + 334 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 39 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 40 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 41 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 42 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 46 + ] + } + ], + "title": "KSampler (Advanced)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "KSamplerAdvanced", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "enable", + 318075405732891, + "randomize", + 6, + 1, + "euler", + "simple", + 0, + 2, + "enable" + ] + }, + { + "id": 67, + "type": "WanFirstLastFrameToVideo", + "pos": [ + 830, + 120 + ], + "size": [ + 270, + 250 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "positive", + "type": "CONDITIONING", + "link": 49 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 50 + }, + { + "name": "vae", + "type": "VAE", + "link": 51 + }, + { + "name": "clip_vision_start_image", + "shape": 7, + "type": "CLIP_VISION_OUTPUT", + "link": null + }, + { + "name": "clip_vision_end_image", + "shape": 7, + "type": "CLIP_VISION_OUTPUT", + "link": null + }, + { + "name": "start_image", + "shape": 7, + "type": "IMAGE", + "link": 52 + }, + { + "name": "end_image", + "shape": 7, + "type": "IMAGE", + "link": 53 + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 47 + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 48 + } + ], + "outputs": [ + { + "name": "positive", + "type": "CONDITIONING", + "links": [ + 40, + 44 + ] + }, + { + "name": "negative", + "type": "CONDITIONING", + "links": [ + 41, + 45 + ] + }, + { + "name": "latent", + "type": "LATENT", + "links": [ + 42 + ] + } + ], + "title": "WanFirstLastFrameToVideo", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "WanFirstLastFrameToVideo", + "widget_ue_connectable": {} + }, + "widgets_values": [ + 832, + 480, + 101, + 1 + ] + }, + { + "id": 62, + "type": "LoadImage", + "pos": [ + 40, + 400 + ], + "size": [ + 270, + 314 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 52, + 62, + 65 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "title": "Load Image", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "LoadImage", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "cut_9.png", + "image" + ] + }, + { + "id": 68, + "type": "LoadImage", + "pos": [ + 30, + 770 + ], + "size": [ + 270, + 314 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 53, + 66 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "title": "Load Image", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "LoadImage", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "cut_10.png", + "image" + ] + }, + { + "id": 119, + "type": "LoraLoaderModelOnly", + "pos": [ + 830, + 690 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 56 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 59 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "LoraLoaderModelOnly", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "Wan\\Wan2.2-Fun-A14B-InP-low-noise-HPS2.1.safetensors", + 0.5 + ] + }, + { + "id": 116, + "type": "UNETLoader", + "pos": [ + 830, + 830 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 56 + ] + } + ], + "title": "Load Diffusion Model", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "UNETLoader", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "wan2.2_i2v_A14b_low_noise_scaled_fp8_e4m3_lightx2v_4step_comfyui.safetensors", + "default" + ] + }, + { + "id": 115, + "type": "UNETLoader", + "pos": [ + 830, + 960 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 55 + ] + } + ], + "title": "Load Diffusion Model", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "UNETLoader", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "wan2.2_i2v_A14b_high_noise_scaled_fp8_e4m3_lightx2v_4step_comfyui_1030.safetensors", + "default" + ] + }, + { + "id": 118, + "type": "LoraLoaderModelOnly", + "pos": [ + 830, + 560 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 55 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 57 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "LoraLoaderModelOnly", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "Wan\\lightx2v_I2V_14B_480p_cfg_step_distill_rank256_bf16.safetensors", + 2 + ] + }, + { + "id": 133, + "type": "PreviewAny", + "pos": [ + -810, + 630 + ], + "size": [ + 390, + 360 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 69 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PreviewAny", + "widget_ue_connectable": {} + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 132, + "type": "ModelDownloader", + "pos": [ + -820, + 120 + ], + "size": [ + 420, + 450 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "status_message", + "type": "STRING", + "links": [ + 69 + ] + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "Node name for S&R": "ModelDownloader", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "diffusion_models\nhttps://huggingface.co/lightx2v/Wan2.2-Distill-Models/resolve/main/wan2.2_i2v_A14b_high_noise_scaled_fp8_e4m3_lightx2v_4step_comfyui_1030.safetensors\nhttps://huggingface.co/lightx2v/Wan2.2-Distill-Models/resolve/main/wan2.2_i2v_A14b_low_noise_scaled_fp8_e4m3_lightx2v_4step_comfyui.safetensors\nloras\\Wan\nhttps://huggingface.co/Kijai/WanVideo_comfy/resolve/main/Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank256_bf16.safetensors\nhttps://huggingface.co/alibaba-pai/Wan2.2-Fun-Reward-LoRAs/resolve/main/Wan2.2-Fun-A14B-InP-low-noise-HPS2.1.safetensors\nhttps://huggingface.co/alibaba-pai/Wan2.2-Fun-Reward-LoRAs/resolve/main/Wan2.2-Fun-A14B-InP-high-noise-MPS.safetensors\nclip\nhttps://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp16.safetensors\nvae\nhttps://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors\nupscale_models\nhttps://huggingface.co/dseditor/UpscaleModels/resolve/main/2x-AnimeSharpV2_RPLKSR_Sharp.pth", + true, + true, + 20, + 8 + ] + }, + { + "id": 131, + "type": "PreviewAny", + "pos": [ + -380, + 640 + ], + "size": [ + 390, + 280 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 68 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "PreviewAny", + "widget_ue_connectable": {} + }, + "widgets_values": [ + null, + null, + null + ] + }, + { + "id": 128, + "type": "UpscaleModelLoader", + "pos": [ + 370, + 1030 + ], + "size": [ + 360, + 70 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "UPSCALE_MODEL", + "type": "UPSCALE_MODEL", + "links": [ + 63 + ] + } + ], + "title": "Load Upscale Model", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "UpscaleModelLoader", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "2x-AnimeSharpV2_RPLKSR_Sharp.pth" + ] + }, + { + "id": 125, + "type": "ImageResizeKJv2", + "pos": [ + 430, + 630 + ], + "size": [ + 240, + 348 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 62 + }, + { + "name": "mask", + "shape": 7, + "type": "MASK", + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + }, + { + "name": "width", + "type": "INT", + "links": [ + 47 + ] + }, + { + "name": "height", + "type": "INT", + "links": [ + 48 + ] + }, + { + "name": "mask", + "type": "MASK", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "7b1327192e4729085788a3020a9cbb095e0c7811", + "Node name for S&R": "ImageResizeKJv2", + "widget_ue_connectable": {} + }, + "widgets_values": [ + 640, + 640, + "bilinear", + "resize", + "0, 0, 0", + "center", + 2, + "cpu", + "Output: 1 x 640 x 360 | 2.64MB" + ] + }, + { + "id": 130, + "type": "GGUFInference", + "pos": [ + -380, + 120 + ], + "size": [ + 400, + 446 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "image_1", + "shape": 7, + "type": "IMAGE", + "link": 65 + }, + { + "name": "image_2", + "shape": 7, + "type": "IMAGE", + "link": 66 + }, + { + "name": "image_3", + "shape": 7, + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": [ + 67, + 68 + ] + }, + { + "name": "used_seed", + "type": "INT", + "links": null + } + ], + "properties": { + "cnr_id": "Listhelper", + "ver": "b8d4ae6b6a68b015522ae2a09a88179fa2231831", + "Node name for S&R": "GGUFInference", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "Download: QwenVL", + "描述魔法少女轉身擊倒怪物的過場", + "video_frame_transition.md", + "", + 3072, + 0.7, + 0.9, + 40, + 817102438267074, + "randomize", + false, + "Download: QwenVL mmproj", + true + ] + }, + { + "id": 58, + "type": "KSamplerAdvanced", + "pos": [ + 1440, + 590 + ], + "size": [ + 250, + 334 + ], + "flags": {}, + "order": 25, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 43 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 44 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 45 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 46 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 35 + ] + } + ], + "title": "KSampler (Advanced)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "KSamplerAdvanced", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "disable", + 439340679128295, + "randomize", + 6, + 1, + "euler", + "simple", + 2, + 10000, + "disable" + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1720, + 560 + ], + "size": [ + 140, + 46 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 35 + }, + { + "name": "vae", + "type": "VAE", + "link": 36 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 64 + ] + } + ], + "title": "VAE Decode", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "VAEDecode", + "widget_ue_connectable": {} + }, + "widgets_values": [] + }, + { + "id": 129, + "type": "ImageUpscaleWithModel", + "pos": [ + 1720, + 660 + ], + "size": [ + 233.5689453125, + 50 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "name": "upscale_model", + "type": "UPSCALE_MODEL", + "link": 63 + }, + { + "name": "image", + "type": "IMAGE", + "link": 64 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 54 + ] + } + ], + "title": "Upscale Image (using Model)", + "properties": { + "cnr_id": "comfy-core", + "ver": "0.6.0", + "Node name for S&R": "ImageUpscaleWithModel", + "widget_ue_connectable": {} + }, + "widgets_values": [] + }, + { + "id": 107, + "type": "VHS_VideoCombine", + "pos": [ + 1990, + 480 + ], + "size": [ + 220.7333984375, + 460.91253662109375 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 54 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "a7ce59e381934733bfae03b1be029756d6ce936d", + "Node name for S&R": "VHS_VideoCombine", + "widget_ue_connectable": {} + }, + "widgets_values": { + "frame_rate": 24, + "loop_count": 0, + "filename_prefix": "ComfyUI", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "ComfyUI_01771.mp4", + "subfolder": "", + "type": "output", + "format": "video/h264-mp4", + "frame_rate": 24, + "workflow": "ComfyUI_01771.png", + "fullpath": "F:\\CUI\\ComfyUI\\output\\ComfyUI_01771.mp4" + } + } + } + } + ], + "links": [ + [ + 33, + 38, + 0, + 6, + 0, + "CLIP" + ], + [ + 34, + 38, + 0, + 7, + 0, + "CLIP" + ], + [ + 35, + 58, + 0, + 8, + 0, + "LATENT" + ], + [ + 36, + 39, + 0, + 8, + 1, + "VAE" + ], + [ + 37, + 123, + 0, + 54, + 0, + "MODEL" + ], + [ + 38, + 124, + 0, + 55, + 0, + "MODEL" + ], + [ + 39, + 54, + 0, + 57, + 0, + "MODEL" + ], + [ + 40, + 67, + 0, + 57, + 1, + "CONDITIONING" + ], + [ + 41, + 67, + 1, + 57, + 2, + "CONDITIONING" + ], + [ + 42, + 67, + 2, + 57, + 3, + "LATENT" + ], + [ + 43, + 55, + 0, + 58, + 0, + "MODEL" + ], + [ + 44, + 67, + 0, + 58, + 1, + "CONDITIONING" + ], + [ + 45, + 67, + 1, + 58, + 2, + "CONDITIONING" + ], + [ + 46, + 57, + 0, + 58, + 3, + "LATENT" + ], + [ + 47, + 125, + 1, + 67, + 7, + "INT" + ], + [ + 48, + 125, + 2, + 67, + 8, + "INT" + ], + [ + 49, + 6, + 0, + 67, + 0, + "CONDITIONING" + ], + [ + 50, + 7, + 0, + 67, + 1, + "CONDITIONING" + ], + [ + 51, + 39, + 0, + 67, + 2, + "VAE" + ], + [ + 52, + 62, + 0, + 67, + 5, + "IMAGE" + ], + [ + 53, + 68, + 0, + 67, + 6, + "IMAGE" + ], + [ + 54, + 129, + 0, + 107, + 0, + "IMAGE" + ], + [ + 55, + 115, + 0, + 118, + 0, + "MODEL" + ], + [ + 56, + 116, + 0, + 119, + 0, + "MODEL" + ], + [ + 57, + 118, + 0, + 120, + 0, + "MODEL" + ], + [ + 58, + 120, + 0, + 121, + 0, + "MODEL" + ], + [ + 59, + 119, + 0, + 122, + 0, + "MODEL" + ], + [ + 60, + 121, + 0, + 123, + 0, + "MODEL" + ], + [ + 61, + 122, + 0, + 124, + 0, + "MODEL" + ], + [ + 62, + 62, + 0, + 125, + 0, + "IMAGE" + ], + [ + 63, + 128, + 0, + 129, + 0, + "UPSCALE_MODEL" + ], + [ + 64, + 8, + 0, + 129, + 1, + "IMAGE" + ], + [ + 65, + 62, + 0, + 130, + 0, + "IMAGE" + ], + [ + 66, + 68, + 0, + 130, + 1, + "IMAGE" + ], + [ + 67, + 130, + 0, + 6, + 1, + "STRING" + ], + [ + 68, + 130, + 0, + 131, + 0, + "STRING" + ], + [ + 69, + 132, + 0, + 133, + 0, + "STRING" + ] + ], + "groups": [ + { + "id": 1, + "title": "Group", + "bounding": [ + 1130, + 70, + 642.095703125, + 325.6 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "config": {}, + "extra": { + "ue_links": [], + "ds": { + "scale": 0.64336907460271, + "offset": [ + 1005.3276618431812, + 148.71831082012548 + ] + }, + "workflowRendererVersion": "LG", + "links_added_by_ue": [], + "frontendVersion": "1.35.9", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/Prompt/image_edit.md b/Prompt/image_edit.md deleted file mode 100644 index 9e15a05..0000000 --- a/Prompt/image_edit.md +++ /dev/null @@ -1,124 +0,0 @@ -# Image Analysis & Element Replacement Editor - -You are an advanced image analysis and prompt editing AI. Your task is to: -1. Analyze the provided image and generate a comprehensive English prompt -2. Understand the user's edit instruction -3. Intelligently modify the prompt to reflect the requested changes - -## Core Workflow: - -### Step 1: Image Analysis -Analyze the image thoroughly and create a detailed English prompt that captures: -- **Main subjects**: People, objects, animals -- **Appearance details**: Colors, textures, materials, styles -- **Actions & poses**: What subjects are doing -- **Environment**: Setting, background, location -- **Composition**: Layout, perspective, framing -- **Lighting**: Direction, quality, mood -- **Style**: Art style, photography type -- **Quality markers**: Resolution, detail level - -### Step 2: Understanding Edit Instructions -The user will provide editing instructions in natural language (English or Chinese), such as: -- "Change the dress to pink" / "把衣服換成粉紅色" -- "Make it nighttime" / "改成夜晚場景" -- "Add a cat in the background" / "背景加入一隻貓" -- "Change the hairstyle to short" / "把髮型改成短髮" -- "Replace the car with a bicycle" / "把車子換成腳踏車" - -### Step 3: Intelligent Prompt Modification -- **Identify** the specific element in the original prompt that corresponds to the user's instruction -- **Replace** that element with the new description while maintaining coherence -- **Preserve** all other elements that weren't mentioned in the edit instruction -- **Ensure** the modified prompt remains natural and grammatically correct -- **Maintain** the style and quality descriptors unless specifically changed - -## Element Replacement Rules: - -1. **Clothing Changes**: Replace only the clothing description, keep the pose, person characteristics, and other details - - Original: "woman wearing blue dress" - - Instruction: "change to red jacket" - - Modified: "woman wearing red jacket" - -2. **Color Changes**: Replace the specific color attribute - - Original: "red sports car" - - Instruction: "make it silver" - - Modified: "silver sports car" - -3. **Object Replacement**: Replace the entire object while maintaining its role in the composition - - Original: "man holding coffee cup" - - Instruction: "change coffee to wine glass" - - Modified: "man holding wine glass" - -4. **Environmental Changes**: Modify setting while keeping subjects intact - - Original: "beach at sunset" - - Instruction: "change to mountain landscape" - - Modified: "mountain landscape at sunset" - -5. **Addition Requests**: Integrate new elements naturally - - Original: "empty room with white walls" - - Instruction: "add a painting on the wall" - - Modified: "room with white walls and a framed painting" - -6. **Style Changes**: Replace style descriptors globally - - Original: "photorealistic portrait" - - Instruction: "make it anime style" - - Modified: "anime style portrait" - -## Output Format: - -**CRITICAL INSTRUCTION**: Your response must ONLY contain the final modified English prompt. Do NOT include: -- ❌ Any explanations or commentary (in Chinese or English) -- ❌ Phrases like "這是...", "以下是...", "根據您的要求..." -- ❌ Section headers like "Original Prompt:", "Modified Prompt:", etc. -- ❌ Analysis or reasoning about changes -- ❌ Any introductory or concluding statements - -**OUTPUT REQUIREMENTS**: -- ✅ Output ONLY the final modified English prompt -- ✅ The prompt should be ready for direct use in image generation -- ✅ Single paragraph format with comma-separated elements -- ✅ Professional, natural English language - -## Important Guidelines: - -- **Be precise**: Only change what the user explicitly asks to change -- **Maintain coherence**: Ensure the modified prompt makes logical sense -- **Preserve quality**: Keep technical quality descriptors (4k, detailed, etc.) -- **Natural language**: The output should read as a natural, cohesive prompt -- **Element relationships**: Consider how changes affect related elements -- **Cultural understanding**: Understand editing instructions in both English and Chinese -- **Context awareness**: Some changes may require adjusting multiple related elements -- **Direct output only**: NO explanations, NO comments, ONLY the modified prompt - -## Examples: - -### Example 1: -**Input Image**: [Photo of a young woman in blue dress standing in a garden] -**User Instruction**: "把衣服換成粉紅色洋裝" - -**CORRECT Output** (only this): -``` -A young woman with long brown hair, gentle smile, wearing elegant pink dress, standing in a sunlit garden, soft natural lighting, warm color palette, professional portrait photography, high resolution -``` - -**WRONG Output** (DO NOT do this): -``` -這是一個非常清楚的編輯請求,我將根據您的指示將女孩的衣物改為粉色洋裝。 -以下是符合您要求的修改後提示: - -A young woman with long brown hair, gentle smile, wearing elegant pink dress, standing in a sunlit garden, soft natural lighting, warm color palette, professional portrait photography, high resolution -``` - -### Example 2: -**Input Image**: [Beach scene with red car] -**User Instruction**: "change the car to blue" - -**CORRECT Output** (only this): -``` -Blue sports car parked on sandy beach, ocean waves in background, sunset lighting, golden hour, professional automotive photography, high resolution, detailed -``` - ---- - -Now, analyze the provided image and wait for the user's editing instruction. When you receive the instruction, output ONLY the modified English prompt with NO additional text. diff --git a/Prompt/qwen2512_en.md b/Prompt/qwen2512_en.md new file mode 100644 index 0000000..8b2bd05 --- /dev/null +++ b/Prompt/qwen2512_en.md @@ -0,0 +1,81 @@ +# Image Prompt Rewriting Expert +You are a world-class expert in crafting image prompts, fluent in both Chinese and English, with exceptional visual comprehension and descriptive abilities. +Your task is to automatically classify the user's original image description into one of three categories—**portrait**, **text-containing image**, or **general image**—and then rewrite it naturally, precisely, and aesthetically in English, strictly adhering to the following core requirements and category-specific guidelines. +--- +## Core Requirements (Apply to All Tasks) +1. **Use fluent, natural descriptive language** within a single continuous response block. + Strictly avoid formal Markdown lists (e.g., using • or *), numbered items, or headings. While the final output should be a single response, for structured content such as infographics or charts, you can use line breaks to separate logical sections. Within these sections, a hyphen (-) can introduce items in a list-like fashion, but these items should still be phrased as descriptive sentences or phrases that contribute to the overall narrative description of the image's content and layout. +2. **Enrich visual details appropriately**: + - Determine whether the image contains text. If not, do not add any extraneous textual elements. + - When the original description lacks sufficient detail, supplement logically consistent environmental, lighting, texture, or atmospheric elements to enhance visual appeal. When the description is already rich, make only necessary adjustments. When it is overly verbose or redundant, condense while preserving the original intent. + - All added content must align stylistically and logically with existing information; never alter original concepts or content. + - Exercise restraint in simple scenes to avoid unnecessary elaboration. +3. **Never modify proper nouns**: Names of people, brands, locations, IPs, movie/game titles, slogans in their original wording, URLs, phone numbers, etc., must be preserved exactly as given. +4. **Fully represent all textual content**: + - If the image contains visible text, **enclose every piece of displayed text in English double quotation marks (" ")** to distinguish it from other content. + - Accurately describe the text’s content, position, layout direction (horizontal/vertical/wrapped), font style, color, size, and presentation method (e.g., printed, embroidered, neon). + - If the prompt implies the presence of specific text or numbers (even indirectly), explicitly state the **exact textual/numeric content**, enclosed in double quotation marks. Avoid vague references like "a list" or "a roster"; instead, provide concrete examples without excessive length. + - If no text appears in the image, explicitly state: "The image contains no recognizable text." +5. **Clearly specify the overall artistic style**, such as realistic photography, anime illustration, movie poster, cyberpunk concept art, watercolor painting, 3D rendering, game CG, etc. +--- +## Subtask 1: Portrait Image Rewriting +When the image centers on a human subject, or if the prompt uses terms like 'portrait' or 'headshot' without a specified subject, you must describe a detailed human character and ensure the following: +1. **Define Subject's Identity and Physical Appearance**: + You must provide clear, specific, and unambiguous information for the subject, avoiding generalities. + - Identity: explicitly state the subject's ethnicity (e.g., East Asian, West African, Scandinavian, South American), gender (male, female), and a specific age or a narrow, descriptive age range (e.g., "a 25-year-old," "in her early 40s," "approximately 30 years old"). Avoid vague terms like "young" or "old." + - Facial Characteristics and Expression: describe the overall face shape (e.g., oval, square, heart-shaped) and distinct structural features (e.g., high cheekbones, a strong jawline). Detail the specific features like eyes (e.g., almond-shaped, deep-set; color like emerald green or deep brown), nose (e.g., aquiline, button), and mouth (e.g., full lips, defined cupid's bow). Conclude with a precise expression (e.g., a faint, knowing smile; a look of serene contemplation). + - Skin, Makeup, and Grooming: detail the skin with precision, defining its tone (e.g., porcelain, olive, tan, deep ebony) and texture or features (e.g., smooth with a dewy finish, matte with a light dusting of freckles, weathered laugh lines). If present, specify makeup application and style, covering elements such as **eyeshadow, eyeliner, eyelashes, eyebrow shape, lipstick, blush, and highlight**. For facial hair, describe its style and grooming (e.g., a neatly trimmed beard, a five o'clock shadow). +2. **Describe clothing, hairstyle, and accessories**: + - Clothing: specify all garments, including tops, bottoms, footwear, one-piece outfits, and outerwear. Note their type (e.g., silk blouse, denim jeans, leather boots, knit dress, wool overcoat) and fabric texture. + - Hairstyle: describe the hair color, length, texture, and style. For color, specify the shade (e.g., jet black, platinum blonde, auburn red). For style, describe the cut and arrangement (e.g., long and straight, curly with bangs, a center-parted bob). + - Accessories: list any additional items such as headwear, jewelry (earrings, necklaces, rings), glasses, etc. +3. **Capture Pose and Action**: Articulate the subject’s posture and movement with intention and narrative. + - Body Posture: describe the overall stance or position (e.g., leaning casually against a wall, sitting upright with perfect posture, in mid-stride while walking). + - Gaze & Head Position: specify the direction of the subject's gaze (e.g., looking directly into the camera, gazing off-frame to the left, looking down at an object) and the tilt of the head (e.g., tilted slightly, held high). + - Hand & Arm Gestures: detail the placement and action of the hands and arms (e.g., one hand gently resting on the chin, arms crossed confidently over the chest, hands tucked into pockets, gesturing mid-conversation). + - Ensure all poses and interactions adhere to anatomical correctness and physical plausibility. The resulting depiction must appear logical, natural, and contextually harmonious. +4. **Depict background and environment**: specific setting (e.g., café, street, interior), background objects, lighting (direction, intensity, color temperature), weather, and overall mood. +5. **Note other object details**: if non-human items are present (e.g., cups, books, pets), describe their quantity, color, material, position, and spatial or functional relationship to the person. +6. **Recommended Description Flow**: + To ensure clarity, a logical flow is recommended for portrait descriptions. A good starting point is the subject's overall identity (ethnicity, gender, age), followed by their prominent features like clothing, hairstyle, and facial details, and concluding with their pose and the surrounding environment. + However, always prioritize a natural narrative over this rigid structure; adapt the order as needed to create a more compelling and readable description. +7. **Maintain conciseness**: aim for a succinct description, ideally around 200 words, ensuring all critical details are included without excessive verbosity. +--- +## Subtask 2: Text-Containing Image Rewriting +When the image contains recognizable text, please ensure the following: +1. **Faithfully reproduce all text content**: + - Clearly specify the location of the text (e.g., on a sign, screen, clothing, packaging, poster, etc.). + - Accurately transcribe all visible text, including punctuation, capitalization, line breaks, and layout direction (e.g., horizontal, vertical, wrapped). + - Describe the font style (e.g., handwritten, serif, calligraphy, pixel art style, etc.), color, size, clarity, and whether it has any outlines/strokes or shadows. + - For non-English text (e.g., Chinese, Japanese, Korean, etc.), retain the original text and specify the language. +2. **Describe the relationship between the text and its carrier**: + - Presentation method (e.g., printed, on an LED screen, neon light, embroidered, graffiti, etc.). + - Compositional role (e.g., title, slogan, brand logo, decoration, etc.). + - Spatial relationship with people or other objects (e.g., held in hand, posted on a wall, projected, etc.). +3. **Supplement with environment and atmosphere details**: + - Scene type (e.g., indoor/outdoor, commercial street, exhibition hall, etc.). + - The effect of lighting on text readability (e.g., glare, backlighting, night illumination, etc.). + - Overall color tone and artistic style (e.g., retro, minimalist, cyberpunk, etc.). +4. **In infographic/knowledge-based scenarios, supplement text appropriately**: + - If the prompt's text information is incomplete but implies that text should be present, add the layout and specific, concise example text. You must state the exact text content. Do not use vague placeholders like "a list of names," "a chart", "such as", "possibly", or "with accompanying text"; instead, provide the detailed and exact words/characters/symbols/phrases/numbers/punctuations. Also, note that your added text must be concise and accurate, and its layout must be harmonious with the image. + - For example, instead of a vague description like "The panel shows object attributes," provide specific, concrete examples like: "The properties panel on the right is labeled 'Object Attributes' and lists the following values: 'Coordinates: X=150, Y=300', 'Rotation: 45°', and 'Material: Carbon Fiber'." + - If the user has already provided detailed text, strictly adhere to it without additions or changes. + - Ensure all described text, whether provided by the user or supplemented by you, logically aligns with the overall context of the prompt. Avoid inventing content that contradicts the user's core concept or the image's established style. +--- +## Subtask 3: General Image Rewriting +When the image lacks human subjects or text, or primarily features landscapes, still lifes, or abstract compositions, cover these elements: +1. **Core visual components**: + - Subject type, quantity, form, color, material, state (static/moving), and distinctive details. + - Spatial layering (foreground, midground, background) and relative positions/distances between objects. + - Lighting and color (light source direction, contrast, dominant hues, highlights/reflections/shadows). + - Surface textures (smooth, rough, metallic, fabric-like, transparent, frosted, etc.). +2. **Scene and atmosphere**: + - Setting type (natural landscape, urban architecture, interior space, staged still life, etc.). + - Time and weather (morning mist, midday sun, post-rain dampness, snowy night silence, golden-hour warmth, etc.). + - Emotional tone (cozy, lonely, mysterious, high-tech, vibrant, etc.). +3. **Visual relationships among multiple objects**: + - Functional connections (e.g., teapot and cup, utensils and food). + - Dynamic interactions (e.g., wind blowing curtains, water hitting rocks). + - Scale and proportion (e.g., towering skyscrapers, boulders vs. people, macro close-ups). +--- +Based on the user’s input, automatically determine the appropriate task category and output a single English image prompt that fully complies with the above specifications. Even if the input is this instruction itself, treat it as a description to be rewritten. **Do not explain, confirm, or add any extra responses—output only the rewritten prompt text.** \ No newline at end of file diff --git a/Prompt/video_frame_transition.md b/Prompt/video_frame_transition.md new file mode 100644 index 0000000..75c7854 --- /dev/null +++ b/Prompt/video_frame_transition.md @@ -0,0 +1,93 @@ +# 视频帧过渡分析器 + +你是一位专业的动作设计师和故事板艺术家。你的任务是观察两张图片(起始帧和结束帧),然后描述两帧之间应该发生的动作、行为和场景变化,以创造流畅自然的过渡。 + +## 核心原则: + +**你必须做的:** +- 描述角色/物体的具体动作和行为 +- 推理两帧之间合理的中间动作 +- 描述环境和场景的自然变化 +- 关注动作的连贯性和因果关系 + +**你绝对不能做的:** +- ❌ 使用"从第一帧到第二帧"这样的表述 +- ❌ 描述运镜技术(如"平移"、"推拉"、"跟随"等) +- ❌ 分析叙事手法或电影技巧 +- ❌ 过度解释视觉引导或构图理论 +- ❌ 使用学术化的影视分析语言 + +## 分析步骤: + +1. **观察起始状态** + - 角色在做什么? + - 处于什么位置和姿态? + - 周围环境是什么样的? + +2. **观察结束状态** + - 角色现在在做什么? + - 位置和姿态有何变化? + - 环境发生了什么变化? + +3. **推理中间动作** + - 角色需要做哪些动作才能从起始到达结束状态? + - 这些动作的先后顺序是什么? + - 动作应该快还是慢? + +4. **描述场景变化** + - 环境如何随着动作自然变化? + - 光线、色彩、氛围的变化? + +## 输出格式: + +用简体中文写一段自然流畅的描述,就像在讲述一个正在发生的故事。 + +**写作风格:** +- 使用现在进行时或一般现在时 +- 动词要具体、生动 +- 描述要像在看实际发生的动作 +- 语言简洁直接,避免冗长 + +**结构建议:** +1. 开场:简述初始状态和即将发生的动作 +2. 主体:详细描述动作过程和场景变化 +3. 结尾:描述最终状态和整体氛围 + +## 正确示例: + +### 示例1(机器人购物场景) +"银发机器人站在繁华的街道上,她的目光被右侧一个色彩斑斓的摊位吸引。她转过身,迈步走向摊位,同时伸出右手指向展示柜中的某件商品。随着她的移动,周围的高楼大厦和霓虹广告牌逐渐退到视野边缘,摊位上粉红色的招牌灯光和玻璃罩中的植物变得越来越清晰。她在摊位前停下,身体微微前倾,专注地观察着商品。街道的冷色调灯光渐渐被摊位温暖的局部照明取代,营造出亲切的购物氛围。" + +### 示例2(人物转身) +"女孩站在窗边,阳光从侧面照在她的脸上。她听到身后的声音,缓缓转过头,目光从窗外的风景移向房间内部。随着她的转身,发丝在空中轻轻飘动,面部的光影也随之变化——原本明亮的右脸逐渐进入阴影,而左脸开始接受来自室内的柔和灯光。她的表情从沉思转为好奇,嘴角微微上扬。" + +### 示例3(奔跑动作) +"少年蹲在起跑线上,肌肉紧绷,双眼紧盯前方。一声令下,他猛地蹬地起身,身体向前爆发,双臂有力摆动。他的步伐从最初的短促有力逐渐拉长,速度越来越快。背景的建筑物和树木开始向后飞速掠过,变成模糊的色块。他的呼吸急促,汗水从额头滑落,但眼神依然坚定。周围的光线因为速度而产生轻微的拖影效果,整个画面充满动感。" + +## 错误示例(避免这样写): + +❌ **错误1:技术性描述** +"镜头执行了一次流畅的平移运镜(Pan),具体为向右水平移动并伴随轻微拉远。画面中的核心主体保持在视觉中心位置..." + +✅ **正确改写** +"机器人转身走向右侧的摊位,她的身影在移动中始终清晰可见..." + +❌ **错误2:过度分析** +"这种叙事手法既强化了视觉引导性,也体现了角色的主动性与目的导向..." + +✅ **正确改写** +"她主动走向摊位,显然对那里的商品很感兴趣..." + +❌ **错误3:使用"帧"的概念** +"从第一帧到第二帧,角色完成了转身动作..." + +✅ **正确改写** +"角色转过身,面向新的方向..." + +## 特别提醒: + +你是在描述**动作本身**,不是在分析**如何拍摄这个动作**。 + +想象你在向动画师解释角色应该如何移动,或者向演员说明应该如何表演,而不是向摄影师解释如何操作摄影机。 + +现在,请观察提供的两张图片,用自然流畅的语言描述两帧之间发生的动作和场景变化。 diff --git a/__init__.py b/__init__.py index d721463..f4bebb8 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,14 @@ -from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS +from .nodes import NODE_CLASS_MAPPINGS as NODES_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as NODES_DISPLAY_MAPPINGS + +# Import Chinese tools nodes +try: + from .chinese_tools import NODE_CLASS_MAPPINGS as CHINESE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CHINESE_DISPLAY_MAPPINGS + # Merge the mappings + NODE_CLASS_MAPPINGS = {**NODES_MAPPINGS, **CHINESE_MAPPINGS} + NODE_DISPLAY_NAME_MAPPINGS = {**NODES_DISPLAY_MAPPINGS, **CHINESE_DISPLAY_MAPPINGS} +except Exception as e: + print(f"⚠️ Failed to load Chinese tools nodes: {e}") + NODE_CLASS_MAPPINGS = NODES_MAPPINGS + NODE_DISPLAY_NAME_MAPPINGS = NODES_DISPLAY_MAPPINGS __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/chinese_tools.py b/chinese_tools.py new file mode 100644 index 0000000..3328b59 --- /dev/null +++ b/chinese_tools.py @@ -0,0 +1,545 @@ +""" +Chinese Text Processing Tools for ComfyUI +Includes Chinese converter, translator, and auto white balance nodes +Dependencies are installed on first use to avoid import failures +""" + +import sys +import subprocess +import os + +# Lazy import flags +_opencc_available = None +_argostranslate_available = None + +def check_and_install_opencc(): + """Check and install opencc-python-reimplemented if needed""" + global _opencc_available + + if _opencc_available is not None: + return _opencc_available + + try: + import opencc + _opencc_available = True + print("✓ opencc is available") + return True + except ImportError: + print("⚠️ opencc not found. Installing opencc-python-reimplemented...") + try: + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "opencc-python-reimplemented" + ]) + print("✓ opencc-python-reimplemented installed successfully") + print("⚠️ Please restart ComfyUI to use Chinese Converter node") + _opencc_available = False # Needs restart + return False + except Exception as e: + print(f"❌ Failed to install opencc: {e}") + _opencc_available = False + return False + +def check_and_install_argostranslate(): + """Check and install argostranslate if needed""" + global _argostranslate_available + + if _argostranslate_available is not None: + return _argostranslate_available + + try: + import argostranslate.package + import argostranslate.translate + _argostranslate_available = True + print("✓ argostranslate is available") + return True + except ImportError: + print("⚠️ argostranslate not found. Installing...") + try: + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "argostranslate" + ]) + print("✓ argostranslate installed successfully") + print("⚠️ Please restart ComfyUI to use Chinese Translate node") + _argostranslate_available = False # Needs restart + return False + except Exception as e: + print(f"❌ Failed to install argostranslate: {e}") + _argostranslate_available = False + return False + + +class ChineseConverterNode: + """ + Chinese Simplified/Traditional Converter Node + Uses opencc library for high-quality conversion + Boolean switch: True=Simplified to Traditional, False=Traditional to Simplified + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_text": ("STRING", { + "multiline": True, + "default": "" + }), + "simp_to_trad": ("BOOLEAN", { + "default": True, + "label_on": "Simp→Trad", + "label_off": "Trad→Simp" + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("converted_text",) + FUNCTION = "convert_chinese" + CATEGORY = "ListHelper/Chinese" + + def __init__(self): + """Initialize converters""" + self.s2t_converter = None + self.t2s_converter = None + self.opencc_ready = check_and_install_opencc() + + if self.opencc_ready: + try: + import opencc + # Simplified to Traditional converter + self.s2t_converter = opencc.OpenCC('s2t') + # Traditional to Simplified converter + self.t2s_converter = opencc.OpenCC('t2s') + except Exception as e: + print(f"⚠️ opencc initialization failed: {e}") + self.s2t_converter = None + self.t2s_converter = None + + def convert_chinese(self, input_text, simp_to_trad): + """ + Convert Chinese text + + Args: + input_text: Input text + simp_to_trad: True=Simplified to Traditional, False=Traditional to Simplified + + Returns: + Converted text + """ + + if not input_text.strip(): + return ("",) + + # Check if opencc is ready + if not self.opencc_ready: + error_msg = "ERROR: opencc is not installed.\n\nPlease restart ComfyUI to complete installation,\nor manually install: pip install opencc-python-reimplemented" + print(error_msg) + return (error_msg,) + + try: + import opencc + + if simp_to_trad: + # Simplified to Traditional + if self.s2t_converter is None: + self.s2t_converter = opencc.OpenCC('s2t.json') + converted_text = self.s2t_converter.convert(input_text) + else: + # Traditional to Simplified + if self.t2s_converter is None: + self.t2s_converter = opencc.OpenCC('t2s.json') + converted_text = self.t2s_converter.convert(input_text) + + return (converted_text,) + + except Exception as e: + error_msg = f"Conversion failed: {str(e)}" + print(error_msg) + return (error_msg,) + + +class ArgosTranslateNode: + """ + Chinese to English Translation Node using Argos Translate + Supports Traditional and Simplified Chinese + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_text": ("STRING", { + "multiline": True, + "default": "" + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("translated_text",) + FUNCTION = "translate_text" + CATEGORY = "ListHelper/Chinese" + + def __init__(self): + """Initialize translator""" + self.translator_available = check_and_install_argostranslate() + + if self.translator_available: + try: + import argostranslate.package + # Update package index + argostranslate.package.update_package_index() + except Exception as e: + print(f"⚠️ Failed to update language package index: {e}") + + def install_language_package(self): + """Install Chinese to English language package""" + if not self.translator_available: + return False + + try: + import argostranslate.package + + # Get available packages + available_packages = argostranslate.package.get_available_packages() + + # Find Chinese to English package + target_package = None + for package in available_packages: + # Check for Chinese to English package + if (package.from_code == 'zh' and package.to_code == 'en') or \ + (package.from_code == 'zh-cn' and package.to_code == 'en') or \ + (package.from_code == 'zh-tw' and package.to_code == 'en'): + target_package = package + break + + if target_package is None: + print("❌ Chinese to English language package not found") + return False + + # Check if already installed + installed_packages = argostranslate.package.get_installed_packages() + for installed_package in installed_packages: + if (installed_package.from_code == target_package.from_code and + installed_package.to_code == target_package.to_code): + print(f"✓ Language package already installed: {target_package.from_code} → {target_package.to_code}") + return True + + # Download and install language package + print(f"⬇️ Downloading language package: {target_package.from_code} → {target_package.to_code}") + download_path = target_package.download() + print(f"📦 Installing language package...") + argostranslate.package.install_from_path(download_path) + print("✓ Language package installed successfully") + return True + + except Exception as e: + print(f"❌ Language package installation failed: {e}") + return False + + def translate_text(self, input_text): + """ + Translate text (auto-install language package) + + Args: + input_text: Input Chinese text (Traditional or Simplified) + + Returns: + Translated English text + """ + + if not input_text.strip(): + return ("",) + + # Check if argostranslate is available + if not self.translator_available: + error_msg = "ERROR: argostranslate is not installed.\n\nPlease restart ComfyUI to complete installation,\nor manually install: pip install argostranslate" + print(error_msg) + return (error_msg,) + + try: + import argostranslate.package + import argostranslate.translate + + # Auto-install language package + if not self.install_language_package(): + error_msg = "Language package installation failed. Please check your internet connection." + return (error_msg,) + + # Get installed packages + installed_packages = argostranslate.package.get_installed_packages() + + # Find Chinese to English translator + translator = None + for package in installed_packages: + if package.from_code in ['zh', 'zh-cn', 'zh-tw'] and package.to_code == 'en': + translator = package + break + + if translator is None: + error_msg = "Chinese to English translator not found. Language package may have failed to install." + print(error_msg) + return (error_msg,) + + # Perform translation + print(f"🌐 Using translator: {translator.from_code} → {translator.to_code}") + translated_text = argostranslate.translate.translate(input_text, translator.from_code, translator.to_code) + + return (translated_text,) + + except Exception as e: + error_msg = f"Translation failed: {str(e)}" + print(error_msg) + return (error_msg,) + + +# Standard imports for image processing +import torch +import numpy as np + +try: + import cv2 + cv2_available = True +except ImportError: + print("⚠️ OpenCV (cv2) not found. Auto White Balance node will not be available.") + print(" Install with: pip install opencv-python") + cv2_available = False + +try: + from PIL import Image + pil_available = True +except ImportError: + print("⚠️ PIL not found. Auto White Balance node will not be available.") + print(" Install with: pip install Pillow") + pil_available = False + + +class AutoWhiteBalanceNode: + """ + Auto White Balance Adjustment Node for ComfyUI + Supports multiple white balance algorithms to correct color cast + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "method": (["gray_world", "white_patch", "simple_avg", "histogram_stretch"], { + "default": "gray_world" + }), + "strength": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 2.0, + "step": 0.1, + "display": "slider" + }), + }, + "optional": { + "preserve_brightness": ("BOOLEAN", {"default": True}), + "clip_values": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "adjust_white_balance" + CATEGORY = "ListHelper/Tools" + + def tensor_to_cv2(self, tensor_image): + """Convert ComfyUI tensor format to OpenCV format""" + # tensor_image shape: [batch, height, width, channels] + if len(tensor_image.shape) == 4: + tensor_image = tensor_image[0] # Take first image + + # Convert to numpy array and scale to 0-255 range + image_np = tensor_image.cpu().numpy() + if image_np.max() <= 1.0: + image_np = (image_np * 255).astype(np.uint8) + else: + image_np = image_np.astype(np.uint8) + + # Convert to BGR format (OpenCV default) + if image_np.shape[2] == 3: # RGB + image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR) + else: # Assume already BGR + image_bgr = image_np + + return image_bgr + + def cv2_to_tensor(self, cv2_image): + """Convert OpenCV format to ComfyUI tensor format""" + # Convert back to RGB + if len(cv2_image.shape) == 3 and cv2_image.shape[2] == 3: + image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB) + else: + image_rgb = cv2_image + + # Normalize to 0-1 range + image_normalized = image_rgb.astype(np.float32) / 255.0 + + # Convert to tensor format [1, height, width, channels] + tensor_image = torch.from_numpy(image_normalized).unsqueeze(0) + + return tensor_image + + def gray_world_algorithm(self, image): + """Gray World Algorithm - assumes average color should be gray""" + # Calculate average for each channel + avg_b = np.mean(image[:, :, 0]) + avg_g = np.mean(image[:, :, 1]) + avg_r = np.mean(image[:, :, 2]) + + # Calculate overall average brightness + avg_gray = (avg_b + avg_g + avg_r) / 3 + + # Calculate adjustment coefficients + scale_b = avg_gray / avg_b if avg_b > 0 else 1.0 + scale_g = avg_gray / avg_g if avg_g > 0 else 1.0 + scale_r = avg_gray / avg_r if avg_r > 0 else 1.0 + + # Apply adjustment + result = image.astype(np.float32) + result[:, :, 0] *= scale_b + result[:, :, 1] *= scale_g + result[:, :, 2] *= scale_r + + return result + + def white_patch_algorithm(self, image): + """White Patch Algorithm - assumes brightest point should be white""" + # Find maximum value for each channel + max_b = np.max(image[:, :, 0]) + max_g = np.max(image[:, :, 1]) + max_r = np.max(image[:, :, 2]) + + # Calculate adjustment coefficients + scale_b = 255.0 / max_b if max_b > 0 else 1.0 + scale_g = 255.0 / max_g if max_g > 0 else 1.0 + scale_r = 255.0 / max_r if max_r > 0 else 1.0 + + # Apply adjustment + result = image.astype(np.float32) + result[:, :, 0] *= scale_b + result[:, :, 1] *= scale_g + result[:, :, 2] *= scale_r + + return result + + def simple_average_algorithm(self, image): + """Simple Average Algorithm - equalizes RGB channel averages""" + # Calculate average for each channel + avg_b = np.mean(image[:, :, 0]) + avg_g = np.mean(image[:, :, 1]) + avg_r = np.mean(image[:, :, 2]) + + # Use green channel as reference (human eye is most sensitive to green) + reference = avg_g + + # Calculate adjustment coefficients + scale_b = reference / avg_b if avg_b > 0 else 1.0 + scale_g = 1.0 # Green channel unchanged + scale_r = reference / avg_r if avg_r > 0 else 1.0 + + # Apply adjustment + result = image.astype(np.float32) + result[:, :, 0] *= scale_b + result[:, :, 1] *= scale_g + result[:, :, 2] *= scale_r + + return result + + def histogram_stretch_algorithm(self, image): + """Histogram Stretch Algorithm""" + result = image.astype(np.float32) + + for i in range(3): # For each color channel + channel = result[:, :, i] + + # Calculate 1% and 99% percentiles, ignoring extreme values + p1 = np.percentile(channel, 1) + p99 = np.percentile(channel, 99) + + # Stretch to 0-255 range + if p99 > p1: + channel = (channel - p1) * 255.0 / (p99 - p1) + result[:, :, i] = np.clip(channel, 0, 255) + + return result + + def preserve_image_brightness(self, original, adjusted): + """Preserve original image brightness""" + # Calculate original image brightness + original_lab = cv2.cvtColor(original.astype(np.uint8), cv2.COLOR_BGR2LAB) + original_brightness = np.mean(original_lab[:, :, 0]) + + # Calculate adjusted image brightness + adjusted_lab = cv2.cvtColor(adjusted.astype(np.uint8), cv2.COLOR_BGR2LAB) + adjusted_brightness = np.mean(adjusted_lab[:, :, 0]) + + # Adjust brightness + if adjusted_brightness > 0: + brightness_ratio = original_brightness / adjusted_brightness + adjusted_lab[:, :, 0] = np.clip(adjusted_lab[:, :, 0] * brightness_ratio, 0, 255) + + # Convert back to BGR + result = cv2.cvtColor(adjusted_lab, cv2.COLOR_LAB2BGR) + return result.astype(np.float32) + + return adjusted + + def adjust_white_balance(self, image, method="gray_world", strength=1.0, + preserve_brightness=True, clip_values=True): + """Main white balance adjustment function""" + + # Check dependencies + if not cv2_available or not pil_available: + error_msg = "ERROR: Required dependencies not installed.\n\nPlease install:\npip install opencv-python Pillow" + print(error_msg) + return (image,) # Return original image + + # Convert input format + cv2_image = self.tensor_to_cv2(image) + original_image = cv2_image.copy() + + # Select algorithm + if method == "gray_world": + adjusted = self.gray_world_algorithm(cv2_image) + elif method == "white_patch": + adjusted = self.white_patch_algorithm(cv2_image) + elif method == "simple_avg": + adjusted = self.simple_average_algorithm(cv2_image) + elif method == "histogram_stretch": + adjusted = self.histogram_stretch_algorithm(cv2_image) + else: + adjusted = cv2_image.astype(np.float32) + + # Apply strength adjustment + if strength != 1.0: + adjusted = original_image.astype(np.float32) * (1.0 - strength) + adjusted * strength + + # Preserve brightness + if preserve_brightness: + adjusted = self.preserve_image_brightness(original_image, adjusted) + + # Clip value range + if clip_values: + adjusted = np.clip(adjusted, 0, 255) + + # Convert back to tensor format + result_tensor = self.cv2_to_tensor(adjusted.astype(np.uint8)) + + return (result_tensor,) + + +# Node mappings +NODE_CLASS_MAPPINGS = { + "ChineseConverter": ChineseConverterNode, + "ChineseTranslate": ArgosTranslateNode, + "AutoWhiteBalance": AutoWhiteBalanceNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ChineseConverter": "Chinese Converter (Simp⇄Trad)", + "ChineseTranslate": "Chinese to English Translate", + "AutoWhiteBalance": "Auto White Balance", +} diff --git a/gguf_inference.py b/gguf_inference.py index 80412c2..13f727c 100644 --- a/gguf_inference.py +++ b/gguf_inference.py @@ -231,7 +231,7 @@ class GGUFInference: "default": "" }), "max_tokens": ("INT", { - "default": 4096, + "default": 3072, "min": 1, "max": 8192, "step": 1 @@ -263,10 +263,16 @@ class GGUFInference: }), "mmproj_file": (mmproj_names, { "default": default_mmproj, - "tooltip": "Vision model mmproj file (auto-enabled when image is provided and model is VL type)" + "tooltip": "Vision model mmproj file (auto-enabled when any image is provided and model is VL type)" }), - "image": ("IMAGE", { - "tooltip": "Input image for vision model (auto-enables vision mode for VL models)" + "image_1": ("IMAGE", { + "tooltip": "First input image for vision model (auto-enables vision mode for VL models)" + }), + "image_2": ("IMAGE", { + "tooltip": "Second input image for vision model (optional, for multi-image analysis)" + }), + "image_3": ("IMAGE", { + "tooltip": "Third input image for vision model (optional, for multi-image analysis)" }), "auto_install_llama_cpp": ("BOOLEAN", { "default": True, @@ -278,7 +284,7 @@ class GGUFInference: RETURN_TYPES = ("STRING", "INT",) RETURN_NAMES = ("text", "used_seed",) FUNCTION = "inference" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/LLM" def _free_memory(self): """Free GPU and system memory""" @@ -825,7 +831,7 @@ class GGUFInference: return (False, text) def _tensor_to_base64(self, image_tensor) -> str: - """Convert ComfyUI IMAGE tensor to base64 string""" + """Convert ComfyUI IMAGE tensor to base64 string (single image)""" try: from PIL import Image @@ -856,6 +862,53 @@ class GGUFInference: traceback.print_exc() return None + def _tensors_to_base64_list(self, image_tensor) -> List[str]: + """Convert ComfyUI IMAGE tensor(s) to list of base64 strings + + Args: + image_tensor: ComfyUI IMAGE tensor [B, H, W, C] or [H, W, C] + + Returns: + List of base64 encoded image URLs + """ + try: + from PIL import Image + + # Handle single image [H, W, C] + if len(image_tensor.shape) == 3: + image_tensor = image_tensor.unsqueeze(0) # Add batch dimension + + # ComfyUI IMAGE format: [B, H, W, C] with values in [0, 1] + batch_size = image_tensor.shape[0] + base64_list = [] + + for i in range(batch_size): + # Get single image from batch + single_image = image_tensor[i] + + # Convert from [0, 1] to [0, 255] + image_np = (single_image.cpu().numpy() * 255).astype(np.uint8) + + # Create PIL Image + pil_image = Image.fromarray(image_np) + + # Convert to JPEG bytes + buffered = io.BytesIO() + pil_image.save(buffered, format="JPEG", quality=95) + img_bytes = buffered.getvalue() + + # Encode to base64 + img_base64 = base64.b64encode(img_bytes).decode('utf-8') + base64_list.append(f"data:image/jpeg;base64,{img_base64}") + + return base64_list + + except Exception as e: + print(f"Error converting images to base64: {e}") + import traceback + traceback.print_exc() + return None + def inference( self, model: str, @@ -869,7 +922,9 @@ class GGUFInference: seed: int = 0, keep_model_loaded: bool = False, mmproj_file: str = "No mmproj files", - image = None, + image_1 = None, + image_2 = None, + image_3 = None, auto_install_llama_cpp: bool = True, ) -> Tuple[str, int]: """Execute GGUF model inference""" @@ -937,12 +992,22 @@ class GGUFInference: # Check if this is a vision model is_vision_model = self._is_vision_model(model_path) - # Auto-detect vision mode: enable if image is provided and model is VL type + # Collect all provided images into a list + images = [] + if image_1 is not None: + images.append(image_1) + if image_2 is not None: + images.append(image_2) + if image_3 is not None: + images.append(image_3) + + # Auto-detect vision mode: enable if any image is provided and model is VL type enable_vision = False - if image is not None and is_vision_model: + if len(images) > 0 and is_vision_model: enable_vision = True - elif image is not None and not is_vision_model: - print(f"⚠️ Image ignored (model is text-only)") + print(f"📸 Detected {len(images)} image(s) for vision analysis") + elif len(images) > 0 and not is_vision_model: + print(f"⚠️ {len(images)} image(s) ignored (model is text-only)") # Get mmproj path if vision is enabled mmproj_path = None @@ -1113,21 +1178,27 @@ class GGUFInference: if system_prompt and system_prompt.strip(): messages.append({"role": "system", "content": system_prompt}) - # Add image if vision is enabled and model supports it - if enable_vision and is_vision_model and image is not None: - # Convert image tensor to base64 - image_url = self._tensor_to_base64(image) - if image_url is None: - error_msg = "Error: Failed to convert image to base64 format." - print(error_msg) - return (error_msg, seed) - + # Add image(s) if vision is enabled and model supports it + if enable_vision and is_vision_model and len(images) > 0: + # Build content with text first + content = [{"type": "text", "text": prompt}] + + # Convert each image to base64 and add to content + for idx, img in enumerate(images, 1): + # Convert single image tensor to base64 + image_url = self._tensor_to_base64(img) + if image_url is None: + error_msg = f"Error: Failed to convert image_{idx} to base64 format." + print(error_msg) + return (error_msg, seed) + content.append({"type": "image_url", "image_url": image_url}) + + # Log number of images being processed + print(f"🎬 Processing {len(images)} image(s) with vision model") + messages.append({ "role": "user", - "content": [ - {"type": "text", "text": prompt}, - {"type": "image_url", "image_url": image_url} - ] + "content": content }) else: messages.append({"role": "user", "content": prompt}) @@ -1169,7 +1240,7 @@ class GGUFInference: print(f"✓ Done ({inference_time:.1f}s, {tokens_generated} tokens, {tokens_per_sec:.0f}t/s)") # Always free image-related memory after inference - if image is not None: + if len(images) > 0: self._free_image_memory() # Unload model if requested @@ -1184,7 +1255,7 @@ class GGUFInference: print(error_msg) # Clean up resources on error (silently) - if image is not None: + if len(images) > 0: self._free_image_memory() # If error suggests memory/loading issue, unload model completely diff --git a/locales/README.md b/locales/README.md new file mode 100644 index 0000000..9cde66a --- /dev/null +++ b/locales/README.md @@ -0,0 +1,112 @@ +# ComfyUI-ListHelper 國際化 (i18n) 語系檔案 + +本資料夾包含 ComfyUI-ListHelper 節點的多語言翻譯檔案。 + +## 資料夾結構 + +``` +locales/ +├── en/ # 英文 +│ ├── main.json +│ └── nodeDefs.json +├── zh/ # 簡體中文 +│ ├── main.json +│ └── nodeDefs.json +├── zh-TW/ # 繁體中文 +│ ├── main.json +│ └── nodeDefs.json +└── README.md # 本說明文件 +``` + +## 語系檔案說明 + +### main.json +定義節點分類的翻譯: +```json +{ + "nodeCategories": { + "ListHelper": "列表輔助工具" + } +} +``` + +### nodeDefs.json +定義各節點的顯示名稱、輸入輸出參數翻譯: +```json +{ + "AudioListGenerator": { + "display_name": "音訊分割為列表", + "inputs": { + "waveform": { "name": "波形" }, + ... + }, + "outputs": { + "0": { "name": "循環" }, + ... + } + }, + ... +} +``` + +## 支援的語系 + +| 語系代碼 | 語言 | 狀態 | +|---------|------|------| +| `en` | English (英文) | ✓ 完成 | +| `zh` | 简体中文 (簡體中文) | ✓ 完成 | +| `zh-TW` | 繁體中文 (繁體中文) | ✓ 完成 | + +## 如何更新翻譯 + +### 方法一:手動編輯 +直接編輯對應語系的 JSON 檔案。 + +### 方法二:使用轉換腳本(簡繁轉換) + +本專案提供 OpenCC 轉換腳本,可自動將繁體中文轉換為簡體中文: + +```bash +# 從專案根目錄執行 +python -m convert_zh_tw_to_zh +``` + +**注意**:此腳本會自動覆蓋 `locales/zh/` 下的檔案。 + +## 翻譯工作流程 + +1. 編輯 `locales/zh-TW/` 下的繁體中文翻譯 +2. 手動編輯 `locales/en/` 下的英文翻譯 +3. 執行 `convert_zh_tw_to_zh.py` 生成簡體中文翻譯 +4. 執行 `validate_json.py` 驗證所有 JSON 格式 + +## 驗證 JSON 格式 + +執行以下命令驗證所有語系檔案的 JSON 格式: + +```bash +python -m validate_json +``` + +## ComfyUI 語系支援 + +ComfyUI 會自動根據使用者的瀏覽器語言設定載入對應的語系檔案。語系檔案的結構遵循 ComfyUI 的標準格式。 + +## 貢獻翻譯 + +歡迎為 ComfyUI-ListHelper 貢獻新的語系翻譯!請參考現有語系的檔案結構創建新的翻譯。 + +## 技術細節 + +- 簡繁轉換使用 **OpenCC** (Open Chinese Convert) +- 轉換配置:`t2s` (Traditional to Simplified) +- 所有 JSON 檔案使用 UTF-8 編碼 +- 遵循 ComfyUI 官方 i18n 標準 + +## 維護者 + +ComfyUI-ListHelper 開發團隊 + +--- + +**最後更新**: 2026-01-03 diff --git a/locales/en/main.json b/locales/en/main.json new file mode 100644 index 0000000..4e59c93 --- /dev/null +++ b/locales/en/main.json @@ -0,0 +1,5 @@ +{ + "nodeCategories": { + "ListHelper": "List Helper" + } +} diff --git a/locales/en/nodeDefs.json b/locales/en/nodeDefs.json new file mode 100644 index 0000000..9c88941 --- /dev/null +++ b/locales/en/nodeDefs.json @@ -0,0 +1,517 @@ +{ + "AudioListGenerator": { + "display_name": "Audio Split to List", + "inputs": { + "waveform": { + "name": "Waveform" + }, + "videofps": { + "name": "Video FPS" + }, + "samplefps": { + "name": "Sample FPS" + }, + "pad_last_segment": { + "name": "Pad Last Segment" + }, + "crossfade_duration": { + "name": "Crossfade Duration" + }, + "crossfade_type": { + "name": "Crossfade Type" + } + }, + "outputs": { + "0": { + "name": "Cycle" + }, + "1": { + "name": "Audio List" + } + } + }, + "AudioToFrameCount": { + "display_name": "Audio to Frame Count", + "inputs": { + "audio": { + "name": "Audio" + }, + "fps": { + "name": "FPS" + } + }, + "outputs": { + "0": { + "name": "Frames" + } + } + }, + "PromptListGenerator": { + "display_name": "Prompt List Generator", + "inputs": { + "text": { + "name": "Text" + }, + "delimiter": { + "name": "Delimiter" + }, + "use_regex": { + "name": "Use Regex" + }, + "keep_delimiter": { + "name": "Keep Delimiter" + }, + "start_index": { + "name": "Start Index" + }, + "skip_every": { + "name": "Skip Every" + }, + "max_count": { + "name": "Max Count" + }, + "skip_first_index": { + "name": "Skip First Index" + }, + "random_order": { + "name": "Random Order" + }, + "seed": { + "name": "Seed" + } + }, + "outputs": { + "0": { + "name": "Text List" + }, + "1": { + "name": "Total Index" + } + } + }, + "NumberListGenerator": { + "display_name": "Number List Generator", + "inputs": { + "min_value": { + "name": "Min Value" + }, + "max_value": { + "name": "Max Value" + }, + "step": { + "name": "Step" + }, + "count": { + "name": "Count" + }, + "random": { + "name": "Random" + }, + "seed": { + "name": "Seed" + } + }, + "outputs": { + "0": { + "name": "Int List" + }, + "1": { + "name": "Float List" + }, + "2": { + "name": "Total Count" + } + } + }, + "AudioListCombine": { + "display_name": "Audio List Combine", + "inputs": { + "audio_list": { + "name": "Audio List" + }, + "combine_mode": { + "name": "Combine Mode" + }, + "fade_duration": { + "name": "Fade Duration" + }, + "normalize_output": { + "name": "Normalize Output" + }, + "target_sample_rate": { + "name": "Target Sample Rate" + } + }, + "outputs": { + "0": { + "name": "Audio" + } + } + }, + "CeilDivide": { + "display_name": "Ceil Divide", + "inputs": { + "a": { + "name": "A" + }, + "b": { + "name": "B" + } + }, + "outputs": { + "0": { + "name": "Result" + } + } + }, + "FrameMatch": { + "display_name": "Frame Match", + "inputs": { + "images": { + "name": "Images" + }, + "target_frames": { + "name": "Target Frames" + }, + "fill_mode": { + "name": "Fill Mode" + } + }, + "outputs": { + "0": { + "name": "Images" + } + } + }, + "SimpleWildCardPlayer": { + "display_name": "Simple WildCard Player", + "inputs": { + "basic_prompt": { + "name": "Basic Prompt" + }, + "wildcard_template": { + "name": "Wildcard Template" + }, + "wildcard_files": { + "name": "Wildcard Files" + }, + "batch_count": { + "name": "Batch Count" + }, + "seed": { + "name": "Seed" + } + }, + "outputs": { + "0": { + "name": "Prompt List" + } + } + }, + "QwenGPUInference": { + "display_name": "Qwen TE LLM", + "inputs": { + "user_prompt": { + "name": "User Prompt" + }, + "prompt_template": { + "name": "Prompt Template" + }, + "system_prompt": { + "name": "System Prompt" + }, + "max_new_tokens": { + "name": "Max New Tokens" + }, + "temperature": { + "name": "Temperature" + }, + "seed": { + "name": "Seed" + }, + "keep_model_loaded": { + "name": "Keep Model Loaded" + }, + "use_flash_attention": { + "name": "Use Flash Attention" + }, + "use_quantization": { + "name": "Use Quantization" + }, + "top_p": { + "name": "Top P" + }, + "top_k": { + "name": "Top K" + } + }, + "outputs": { + "0": { + "name": "Text" + }, + "1": { + "name": "Used Seed" + } + } + }, + "GGUFInference": { + "display_name": "GGUF LLM", + "inputs": { + "model": { + "name": "Model" + }, + "prompt": { + "name": "Prompt" + }, + "prompt_template": { + "name": "Prompt Template" + }, + "system_prompt": { + "name": "System Prompt" + }, + "max_tokens": { + "name": "Max Tokens" + }, + "temperature": { + "name": "Temperature" + }, + "top_p": { + "name": "Top P" + }, + "top_k": { + "name": "Top K" + }, + "seed": { + "name": "Seed" + }, + "keep_model_loaded": { + "name": "Keep Model Loaded" + }, + "mmproj_file": { + "name": "MMProj File" + }, + "image_1": { + "name": "Image 1" + }, + "image_2": { + "name": "Image 2" + }, + "image_3": { + "name": "Image 3" + }, + "auto_install_llama_cpp": { + "name": "Auto Install Llama CPP" + } + }, + "outputs": { + "0": { + "name": "Text" + }, + "1": { + "name": "Used Seed" + } + } + }, + "BatchToPSD": { + "display_name": "Batch to PSD", + "inputs": { + "images": { + "name": "Images" + }, + "filename_prefix": { + "name": "Filename Prefix" + }, + "reverse_layer_order": { + "name": "Reverse Layer Order" + } + }, + "outputs": { + "0": { + "name": "Message" + } + } + }, + "ModelDownloader": { + "display_name": "Model Downloader", + "inputs": { + "download_list": { + "name": "Download List" + }, + "use_s3c": { + "name": "Use S3C" + }, + "use_custom_path": { + "name": "Use Custom Path" + }, + "chunk_size_mb": { + "name": "Chunk Size (MB)" + }, + "max_workers": { + "name": "Max Workers" + } + }, + "outputs": { + "0": { + "name": "Status Message" + } + } + }, + "OpenAIHelper": { + "display_name": "OpenAI Helper", + "inputs": { + "endpoint": { + "name": "Endpoint" + }, + "api_key": { + "name": "API Key" + }, + "model_name": { + "name": "Model Name" + }, + "user_prompt": { + "name": "User Prompt" + }, + "prompt_template": { + "name": "Prompt Template" + }, + "max_tokens": { + "name": "Max Tokens" + }, + "system_prompt": { + "name": "System Prompt" + }, + "image1": { + "name": "Image 1" + }, + "image2": { + "name": "Image 2" + }, + "image3": { + "name": "Image 3" + }, + "audio": { + "name": "Audio" + }, + "file_path": { + "name": "File Path" + } + }, + "outputs": { + "0": { + "name": "Text" + }, + "1": { + "name": "Model Name List" + } + } + }, + "OpenRouterLLM": { + "display_name": "OpenRouter LLM", + "inputs": { + "api_key": { + "name": "API Key" + }, + "user_prompt": { + "name": "User Prompt" + }, + "text_model": { + "name": "Text Model" + }, + "prompt_template": { + "name": "Prompt Template" + }, + "seed": { + "name": "Seed" + }, + "custom_model": { + "name": "Custom Model" + }, + "system_prompt": { + "name": "System Prompt" + }, + "enable_resize": { + "name": "Enable Resize" + }, + "target_width": { + "name": "Target Width" + }, + "target_height": { + "name": "Target Height" + }, + "resize_method": { + "name": "Resize Method" + }, + "image_input_1": { + "name": "Image Input 1" + }, + "image_input_2": { + "name": "Image Input 2" + }, + "image_input_3": { + "name": "Image Input 3" + } + }, + "outputs": { + "0": { + "name": "Image Output" + }, + "1": { + "name": "Text Output" + } + } + }, + "ChineseConverter": { + "display_name": "Chinese Converter (Simp⇄Trad)", + "inputs": { + "input_text": { + "name": "Input Text" + }, + "simp_to_trad": { + "name": "Simplified to Traditional" + } + }, + "outputs": { + "0": { + "name": "Converted Text" + } + } + }, + "ChineseTranslate": { + "display_name": "Chinese to English Translate", + "inputs": { + "input_text": { + "name": "Input Text" + } + }, + "outputs": { + "0": { + "name": "Translated Text" + } + } + }, + "AutoWhiteBalance": { + "display_name": "Auto White Balance", + "inputs": { + "image": { + "name": "Image" + }, + "method": { + "name": "Method" + }, + "strength": { + "name": "Strength" + }, + "preserve_brightness": { + "name": "Preserve Brightness" + }, + "clip_values": { + "name": "Clip Values" + } + }, + "outputs": { + "0": { + "name": "Image" + } + } + } +} \ No newline at end of file diff --git a/locales/zh-TW/main.json b/locales/zh-TW/main.json new file mode 100644 index 0000000..3dbc1d2 --- /dev/null +++ b/locales/zh-TW/main.json @@ -0,0 +1,5 @@ +{ + "nodeCategories": { + "ListHelper": "列表輔助工具" + } +} diff --git a/locales/zh-TW/nodeDefs.json b/locales/zh-TW/nodeDefs.json new file mode 100644 index 0000000..ee536b9 --- /dev/null +++ b/locales/zh-TW/nodeDefs.json @@ -0,0 +1,517 @@ +{ + "AudioListGenerator": { + "display_name": "音訊分割為列表", + "inputs": { + "waveform": { + "name": "波形" + }, + "videofps": { + "name": "影片幀率" + }, + "samplefps": { + "name": "取樣幀率" + }, + "pad_last_segment": { + "name": "填充最後片段" + }, + "crossfade_duration": { + "name": "交叉淡化時長" + }, + "crossfade_type": { + "name": "交叉淡化類型" + } + }, + "outputs": { + "0": { + "name": "循環" + }, + "1": { + "name": "音訊列表" + } + } + }, + "AudioToFrameCount": { + "display_name": "音訊轉幀數", + "inputs": { + "audio": { + "name": "音訊" + }, + "fps": { + "name": "幀率" + } + }, + "outputs": { + "0": { + "name": "幀數" + } + } + }, + "PromptListGenerator": { + "display_name": "提示詞列表產生器", + "inputs": { + "text": { + "name": "文字" + }, + "delimiter": { + "name": "分隔符" + }, + "use_regex": { + "name": "使用正規表示式" + }, + "keep_delimiter": { + "name": "保留分隔符" + }, + "start_index": { + "name": "起始索引" + }, + "skip_every": { + "name": "跳過間隔" + }, + "max_count": { + "name": "最大數量" + }, + "skip_first_index": { + "name": "跳過第一個索引" + }, + "random_order": { + "name": "隨機排序" + }, + "seed": { + "name": "種子" + } + }, + "outputs": { + "0": { + "name": "文字列表" + }, + "1": { + "name": "總索引" + } + } + }, + "NumberListGenerator": { + "display_name": "數字列表產生器", + "inputs": { + "min_value": { + "name": "最小值" + }, + "max_value": { + "name": "最大值" + }, + "step": { + "name": "步長" + }, + "count": { + "name": "數量" + }, + "random": { + "name": "隨機" + }, + "seed": { + "name": "種子" + } + }, + "outputs": { + "0": { + "name": "整數列表" + }, + "1": { + "name": "浮點數列表" + }, + "2": { + "name": "總數量" + } + } + }, + "AudioListCombine": { + "display_name": "音訊列表合併", + "inputs": { + "audio_list": { + "name": "音訊列表" + }, + "combine_mode": { + "name": "合併模式" + }, + "fade_duration": { + "name": "淡化時長" + }, + "normalize_output": { + "name": "標準化輸出" + }, + "target_sample_rate": { + "name": "目標取樣率" + } + }, + "outputs": { + "0": { + "name": "音訊" + } + } + }, + "CeilDivide": { + "display_name": "無條件進位除法", + "inputs": { + "a": { + "name": "A" + }, + "b": { + "name": "B" + } + }, + "outputs": { + "0": { + "name": "結果" + } + } + }, + "FrameMatch": { + "display_name": "幀數匹配", + "inputs": { + "images": { + "name": "圖像" + }, + "target_frames": { + "name": "目標幀數" + }, + "fill_mode": { + "name": "填充模式" + } + }, + "outputs": { + "0": { + "name": "圖像" + } + } + }, + "SimpleWildCardPlayer": { + "display_name": "簡易萬用字元播放器", + "inputs": { + "basic_prompt": { + "name": "基礎提示詞" + }, + "wildcard_template": { + "name": "萬用字元範本" + }, + "wildcard_files": { + "name": "萬用字元檔案" + }, + "batch_count": { + "name": "批次數量" + }, + "seed": { + "name": "種子" + } + }, + "outputs": { + "0": { + "name": "提示詞列表" + } + } + }, + "QwenGPUInference": { + "display_name": "Qwen TE 語言模型", + "inputs": { + "user_prompt": { + "name": "使用者提示詞" + }, + "prompt_template": { + "name": "提示詞範本" + }, + "system_prompt": { + "name": "系統提示詞" + }, + "max_new_tokens": { + "name": "最大生成標記數" + }, + "temperature": { + "name": "溫度" + }, + "seed": { + "name": "種子" + }, + "keep_model_loaded": { + "name": "保持模型載入" + }, + "use_flash_attention": { + "name": "使用 Flash Attention" + }, + "use_quantization": { + "name": "使用量化" + }, + "top_p": { + "name": "Top P" + }, + "top_k": { + "name": "Top K" + } + }, + "outputs": { + "0": { + "name": "文字" + }, + "1": { + "name": "使用的種子" + } + } + }, + "GGUFInference": { + "display_name": "GGUF 語言模型", + "inputs": { + "model": { + "name": "模型" + }, + "prompt": { + "name": "提示詞" + }, + "prompt_template": { + "name": "提示詞範本" + }, + "system_prompt": { + "name": "系統提示詞" + }, + "max_tokens": { + "name": "最大標記數" + }, + "temperature": { + "name": "溫度" + }, + "top_p": { + "name": "Top P" + }, + "top_k": { + "name": "Top K" + }, + "seed": { + "name": "種子" + }, + "keep_model_loaded": { + "name": "保持模型載入" + }, + "mmproj_file": { + "name": "MMProj 檔案" + }, + "image_1": { + "name": "圖像 1" + }, + "image_2": { + "name": "圖像 2" + }, + "image_3": { + "name": "圖像 3" + }, + "auto_install_llama_cpp": { + "name": "自動安裝 Llama CPP" + } + }, + "outputs": { + "0": { + "name": "文字" + }, + "1": { + "name": "使用的種子" + } + } + }, + "BatchToPSD": { + "display_name": "批次轉 PSD", + "inputs": { + "images": { + "name": "圖像" + }, + "filename_prefix": { + "name": "檔案名稱前綴" + }, + "reverse_layer_order": { + "name": "反轉圖層順序" + } + }, + "outputs": { + "0": { + "name": "訊息" + } + } + }, + "ModelDownloader": { + "display_name": "模型下載器", + "inputs": { + "download_list": { + "name": "下載列表" + }, + "use_s3c": { + "name": "使用 S3C" + }, + "use_custom_path": { + "name": "使用自訂路徑" + }, + "chunk_size_mb": { + "name": "分塊大小 (MB)" + }, + "max_workers": { + "name": "最大執行緒數" + } + }, + "outputs": { + "0": { + "name": "狀態訊息" + } + } + }, + "OpenAIHelper": { + "display_name": "OpenAI 輔助工具", + "inputs": { + "endpoint": { + "name": "端點" + }, + "api_key": { + "name": "API 金鑰" + }, + "model_name": { + "name": "模型名稱" + }, + "user_prompt": { + "name": "使用者提示詞" + }, + "prompt_template": { + "name": "提示詞範本" + }, + "max_tokens": { + "name": "最大標記數" + }, + "system_prompt": { + "name": "系統提示詞" + }, + "image1": { + "name": "圖像 1" + }, + "image2": { + "name": "圖像 2" + }, + "image3": { + "name": "圖像 3" + }, + "audio": { + "name": "音訊" + }, + "file_path": { + "name": "檔案路徑" + } + }, + "outputs": { + "0": { + "name": "文字" + }, + "1": { + "name": "模型名稱列表" + } + } + }, + "OpenRouterLLM": { + "display_name": "OpenRouter 語言模型", + "inputs": { + "api_key": { + "name": "API 金鑰" + }, + "user_prompt": { + "name": "使用者提示詞" + }, + "text_model": { + "name": "文字模型" + }, + "prompt_template": { + "name": "提示詞範本" + }, + "seed": { + "name": "種子" + }, + "custom_model": { + "name": "自訂模型" + }, + "system_prompt": { + "name": "系統提示詞" + }, + "enable_resize": { + "name": "啟用調整大小" + }, + "target_width": { + "name": "目標寬度" + }, + "target_height": { + "name": "目標高度" + }, + "resize_method": { + "name": "縮放方法" + }, + "image_input_1": { + "name": "圖像輸入 1" + }, + "image_input_2": { + "name": "圖像輸入 2" + }, + "image_input_3": { + "name": "圖像輸入 3" + } + }, + "outputs": { + "0": { + "name": "圖像輸出" + }, + "1": { + "name": "文字輸出" + } + } + }, + "ChineseConverter": { + "display_name": "中文簡繁轉換", + "inputs": { + "input_text": { + "name": "輸入文字" + }, + "simp_to_trad": { + "name": "簡體轉繁體" + } + }, + "outputs": { + "0": { + "name": "轉換後文字" + } + } + }, + "ChineseTranslate": { + "display_name": "中文翻譯英文", + "inputs": { + "input_text": { + "name": "輸入文字" + } + }, + "outputs": { + "0": { + "name": "翻譯後文字" + } + } + }, + "AutoWhiteBalance": { + "display_name": "自動白平衡", + "inputs": { + "image": { + "name": "圖像" + }, + "method": { + "name": "方法" + }, + "strength": { + "name": "強度" + }, + "preserve_brightness": { + "name": "保持亮度" + }, + "clip_values": { + "name": "裁剪數值" + } + }, + "outputs": { + "0": { + "name": "圖像" + } + } + } +} \ No newline at end of file diff --git a/locales/zh/main.json b/locales/zh/main.json new file mode 100644 index 0000000..b915f83 --- /dev/null +++ b/locales/zh/main.json @@ -0,0 +1,5 @@ +{ + "nodeCategories": { + "ListHelper": "列表辅助工具" + } +} diff --git a/locales/zh/nodeDefs.json b/locales/zh/nodeDefs.json new file mode 100644 index 0000000..765c2d6 --- /dev/null +++ b/locales/zh/nodeDefs.json @@ -0,0 +1,517 @@ +{ + "AudioListGenerator": { + "display_name": "音讯分割为列表", + "inputs": { + "waveform": { + "name": "波形" + }, + "videofps": { + "name": "影片帧率" + }, + "samplefps": { + "name": "取样帧率" + }, + "pad_last_segment": { + "name": "填充最后片段" + }, + "crossfade_duration": { + "name": "交叉淡化时长" + }, + "crossfade_type": { + "name": "交叉淡化类型" + } + }, + "outputs": { + "0": { + "name": "循环" + }, + "1": { + "name": "音讯列表" + } + } + }, + "AudioToFrameCount": { + "display_name": "音讯转帧数", + "inputs": { + "audio": { + "name": "音讯" + }, + "fps": { + "name": "帧率" + } + }, + "outputs": { + "0": { + "name": "帧数" + } + } + }, + "PromptListGenerator": { + "display_name": "提示词列表产生器", + "inputs": { + "text": { + "name": "文字" + }, + "delimiter": { + "name": "分隔符" + }, + "use_regex": { + "name": "使用正规表示式" + }, + "keep_delimiter": { + "name": "保留分隔符" + }, + "start_index": { + "name": "起始索引" + }, + "skip_every": { + "name": "跳过间隔" + }, + "max_count": { + "name": "最大数量" + }, + "skip_first_index": { + "name": "跳过第一个索引" + }, + "random_order": { + "name": "随机排序" + }, + "seed": { + "name": "种子" + } + }, + "outputs": { + "0": { + "name": "文字列表" + }, + "1": { + "name": "总索引" + } + } + }, + "NumberListGenerator": { + "display_name": "数字列表产生器", + "inputs": { + "min_value": { + "name": "最小值" + }, + "max_value": { + "name": "最大值" + }, + "step": { + "name": "步长" + }, + "count": { + "name": "数量" + }, + "random": { + "name": "随机" + }, + "seed": { + "name": "种子" + } + }, + "outputs": { + "0": { + "name": "整数列表" + }, + "1": { + "name": "浮点数列表" + }, + "2": { + "name": "总数量" + } + } + }, + "AudioListCombine": { + "display_name": "音讯列表合并", + "inputs": { + "audio_list": { + "name": "音讯列表" + }, + "combine_mode": { + "name": "合并模式" + }, + "fade_duration": { + "name": "淡化时长" + }, + "normalize_output": { + "name": "标准化输出" + }, + "target_sample_rate": { + "name": "目标取样率" + } + }, + "outputs": { + "0": { + "name": "音讯" + } + } + }, + "CeilDivide": { + "display_name": "无条件进位除法", + "inputs": { + "a": { + "name": "A" + }, + "b": { + "name": "B" + } + }, + "outputs": { + "0": { + "name": "结果" + } + } + }, + "FrameMatch": { + "display_name": "帧数匹配", + "inputs": { + "images": { + "name": "图像" + }, + "target_frames": { + "name": "目标帧数" + }, + "fill_mode": { + "name": "填充模式" + } + }, + "outputs": { + "0": { + "name": "图像" + } + } + }, + "SimpleWildCardPlayer": { + "display_name": "简易万用字元播放器", + "inputs": { + "basic_prompt": { + "name": "基础提示词" + }, + "wildcard_template": { + "name": "万用字元范本" + }, + "wildcard_files": { + "name": "万用字元档案" + }, + "batch_count": { + "name": "批次数量" + }, + "seed": { + "name": "种子" + } + }, + "outputs": { + "0": { + "name": "提示词列表" + } + } + }, + "QwenGPUInference": { + "display_name": "Qwen TE 语言模型", + "inputs": { + "user_prompt": { + "name": "使用者提示词" + }, + "prompt_template": { + "name": "提示词范本" + }, + "system_prompt": { + "name": "系统提示词" + }, + "max_new_tokens": { + "name": "最大生成标记数" + }, + "temperature": { + "name": "温度" + }, + "seed": { + "name": "种子" + }, + "keep_model_loaded": { + "name": "保持模型载入" + }, + "use_flash_attention": { + "name": "使用 Flash Attention" + }, + "use_quantization": { + "name": "使用量化" + }, + "top_p": { + "name": "Top P" + }, + "top_k": { + "name": "Top K" + } + }, + "outputs": { + "0": { + "name": "文字" + }, + "1": { + "name": "使用的种子" + } + } + }, + "GGUFInference": { + "display_name": "GGUF 语言模型", + "inputs": { + "model": { + "name": "模型" + }, + "prompt": { + "name": "提示词" + }, + "prompt_template": { + "name": "提示词范本" + }, + "system_prompt": { + "name": "系统提示词" + }, + "max_tokens": { + "name": "最大标记数" + }, + "temperature": { + "name": "温度" + }, + "top_p": { + "name": "Top P" + }, + "top_k": { + "name": "Top K" + }, + "seed": { + "name": "种子" + }, + "keep_model_loaded": { + "name": "保持模型载入" + }, + "mmproj_file": { + "name": "MMProj 档案" + }, + "image_1": { + "name": "图像 1" + }, + "image_2": { + "name": "图像 2" + }, + "image_3": { + "name": "图像 3" + }, + "auto_install_llama_cpp": { + "name": "自动安装 Llama CPP" + } + }, + "outputs": { + "0": { + "name": "文字" + }, + "1": { + "name": "使用的种子" + } + } + }, + "BatchToPSD": { + "display_name": "批次转 PSD", + "inputs": { + "images": { + "name": "图像" + }, + "filename_prefix": { + "name": "档案名称前缀" + }, + "reverse_layer_order": { + "name": "反转图层顺序" + } + }, + "outputs": { + "0": { + "name": "讯息" + } + } + }, + "ModelDownloader": { + "display_name": "模型下载器", + "inputs": { + "download_list": { + "name": "下载列表" + }, + "use_s3c": { + "name": "使用 S3C" + }, + "use_custom_path": { + "name": "使用自订路径" + }, + "chunk_size_mb": { + "name": "分块大小 (MB)" + }, + "max_workers": { + "name": "最大执行绪数" + } + }, + "outputs": { + "0": { + "name": "状态讯息" + } + } + }, + "OpenAIHelper": { + "display_name": "OpenAI 辅助工具", + "inputs": { + "endpoint": { + "name": "端点" + }, + "api_key": { + "name": "API 金钥" + }, + "model_name": { + "name": "模型名称" + }, + "user_prompt": { + "name": "使用者提示词" + }, + "prompt_template": { + "name": "提示词范本" + }, + "max_tokens": { + "name": "最大标记数" + }, + "system_prompt": { + "name": "系统提示词" + }, + "image1": { + "name": "图像 1" + }, + "image2": { + "name": "图像 2" + }, + "image3": { + "name": "图像 3" + }, + "audio": { + "name": "音讯" + }, + "file_path": { + "name": "档案路径" + } + }, + "outputs": { + "0": { + "name": "文字" + }, + "1": { + "name": "模型名称列表" + } + } + }, + "OpenRouterLLM": { + "display_name": "OpenRouter 语言模型", + "inputs": { + "api_key": { + "name": "API 金钥" + }, + "user_prompt": { + "name": "使用者提示词" + }, + "text_model": { + "name": "文字模型" + }, + "prompt_template": { + "name": "提示词范本" + }, + "seed": { + "name": "种子" + }, + "custom_model": { + "name": "自订模型" + }, + "system_prompt": { + "name": "系统提示词" + }, + "enable_resize": { + "name": "启用调整大小" + }, + "target_width": { + "name": "目标宽度" + }, + "target_height": { + "name": "目标高度" + }, + "resize_method": { + "name": "缩放方法" + }, + "image_input_1": { + "name": "图像输入 1" + }, + "image_input_2": { + "name": "图像输入 2" + }, + "image_input_3": { + "name": "图像输入 3" + } + }, + "outputs": { + "0": { + "name": "图像输出" + }, + "1": { + "name": "文字输出" + } + } + }, + "ChineseConverter": { + "display_name": "中文简繁转换", + "inputs": { + "input_text": { + "name": "输入文字" + }, + "simp_to_trad": { + "name": "简体转繁体" + } + }, + "outputs": { + "0": { + "name": "转换后文字" + } + } + }, + "ChineseTranslate": { + "display_name": "中文翻译英文", + "inputs": { + "input_text": { + "name": "输入文字" + } + }, + "outputs": { + "0": { + "name": "翻译后文字" + } + } + }, + "AutoWhiteBalance": { + "display_name": "自动白平衡", + "inputs": { + "image": { + "name": "图像" + }, + "method": { + "name": "方法" + }, + "strength": { + "name": "强度" + }, + "preserve_brightness": { + "name": "保持亮度" + }, + "clip_values": { + "name": "裁剪数值" + } + }, + "outputs": { + "0": { + "name": "图像" + } + } + } +} \ No newline at end of file diff --git a/model_downloader.py b/model_downloader.py index afc6761..103e256 100644 --- a/model_downloader.py +++ b/model_downloader.py @@ -22,6 +22,14 @@ class ModelDownloader: "default": "diffusion_models\nhttps://huggingface.co/unsloth/Qwen-Image-2512-GGUF/resolve/main/qwen-image-2512-Q8_0.gguf\nCLIP\nhttps://huggingface.co/unsloth/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct-Q8_0.gguf", "tooltip": "Download list format:\nFolder name\nURL1\nURL2\n..." }), + "use_s3c": ("BOOLEAN", { + "default": True, + "tooltip": "使用 S3impleClient 進行加速下載(如停用則使用普通 HTTP 下載)" + }), + "use_custom_path": ("BOOLEAN", { + "default": True, + "tooltip": "使用自訂路徑(如停用則使用 HuggingFace Hub 原始快取路徑)" + }), "chunk_size_mb": ("INT", { "default": 4, "min": 1, @@ -43,65 +51,73 @@ class ModelDownloader: RETURN_NAMES = ("status_message",) FUNCTION = "download_models" OUTPUT_NODE = True - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Tools" - def download_models(self, download_list: str, chunk_size_mb: int, max_workers: int) -> Tuple[str]: + def download_models(self, download_list: str, use_s3c: bool, use_custom_path: bool, + chunk_size_mb: int, max_workers: int) -> Tuple[str]: """ Download models to ComfyUI/models folders Args: download_list: Download list text + use_s3c: Use S3impleClient for accelerated download + use_custom_path: Use custom path (if False, use HuggingFace Hub cache path) chunk_size_mb: Chunk size (MB) max_workers: Maximum parallel workers Returns: Status message """ - # Ensure S3impleClient is installed - s3c_installed = self._ensure_s3impleclient() - if not s3c_installed: - return ("Error: Failed to install or import S3impleClient. Please install manually: pip install s3impleclient",) + # Ensure S3impleClient is installed (only if use_s3c is enabled) + s3c_installed = False + if use_s3c: + s3c_installed = self._ensure_s3impleclient() + if not s3c_installed: + return ("錯誤:無法安裝或匯入 S3impleClient。請手動安裝:pip install s3impleclient",) # Parse download list download_map = self._parse_download_list(download_list) if not download_map: - return ("Error: Download list format is incorrect or empty",) + return ("錯誤:下載列表格式不正確或為空",) - # Get ComfyUI models root directory - models_root = self._get_models_root() - print(f"ModelDownloader: Models root directory = {models_root}") + # Get ComfyUI models root directory (only if using custom path) + models_root = None + if use_custom_path: + models_root = self._get_models_root() + print(f"ModelDownloader: Models root directory = {models_root}") - # Check for duplicate files before download + # Check for duplicate files before download (only if using custom path) skipped_files = [] - for folder_name, urls in download_map.items(): - target_folder = self._get_target_folder(models_root, folder_name) - for url in urls: - filename = self._extract_filename(url) - dest_path = os.path.join(target_folder, filename) - if os.path.exists(dest_path): - skipped_files.append(f"{folder_name}/{filename}") - print(f"ModelDownloader: File already exists, skipping: {dest_path}") - - # Remove URLs that point to existing files - if skipped_files: - filtered_download_map = {} + if use_custom_path: for folder_name, urls in download_map.items(): target_folder = self._get_target_folder(models_root, folder_name) - filtered_urls = [] for url in urls: filename = self._extract_filename(url) dest_path = os.path.join(target_folder, filename) - if not os.path.exists(dest_path): - filtered_urls.append(url) - if filtered_urls: - filtered_download_map[folder_name] = filtered_urls - download_map = filtered_download_map + if os.path.exists(dest_path): + skipped_files.append(f"{folder_name}/{filename}") + print(f"ModelDownloader: 檔案已存在,跳過:{dest_path}") - if not download_map: - skip_msg = f"All files already exist. Skipped:\n" + "\n".join(f" - {f}" for f in skipped_files) - print(f"ModelDownloader: {skip_msg}") - return (skip_msg,) + # Remove URLs that point to existing files + if skipped_files: + filtered_download_map = {} + for folder_name, urls in download_map.items(): + target_folder = self._get_target_folder(models_root, folder_name) + filtered_urls = [] + for url in urls: + filename = self._extract_filename(url) + dest_path = os.path.join(target_folder, filename) + if not os.path.exists(dest_path): + filtered_urls.append(url) + if filtered_urls: + filtered_download_map[folder_name] = filtered_urls + download_map = filtered_download_map + + if not download_map: + skip_msg = f"所有檔案已存在。已跳過:\n" + "\n".join(f" - {f}" for f in skipped_files) + print(f"ModelDownloader: {skip_msg}") + return (skip_msg,) # Auto-detect if should use HuggingFace patch all_hf_urls = all( @@ -110,23 +126,25 @@ class ModelDownloader: for url in urls ) - # Initialize downloader - try: - import s3impleclient as s3c + # Initialize downloader (only if using S3C) + s3c = None + if use_s3c: + try: + import s3impleclient as s3c - # Configure download settings - chunk_size_bytes = chunk_size_mb * 1024 * 1024 - s3c.configure_download(s3c.DownloadConfig( - chunk_size=chunk_size_bytes, - max_workers=max_workers, - timeout=300.0, - max_retries=5, - )) + # Configure download settings + chunk_size_bytes = chunk_size_mb * 1024 * 1024 + s3c.configure_download(s3c.DownloadConfig( + chunk_size=chunk_size_bytes, + max_workers=max_workers, + timeout=300.0, + max_retries=5, + )) - print(f"ModelDownloader: Configuration complete - chunk_size={chunk_size_mb}MB, workers={max_workers}") + print(f"ModelDownloader: S3C 設定完成 - chunk_size={chunk_size_mb}MB, workers={max_workers}") - except Exception as e: - return (f"Error: Failed to configure S3impleClient: {str(e)}",) + except Exception as e: + return (f"錯誤:無法設定 S3impleClient:{str(e)}",) # Auto-detect and apply HuggingFace patch if conditions are met hf_available = False @@ -136,17 +154,18 @@ class ModelDownloader: import huggingface_hub hf_available = True - # Try to apply patch, but continue with HTTP if it fails - try: - s3c.patch_huggingface_hub() - patch_applied = True - print("ModelDownloader: HuggingFace Hub acceleration enabled") - except Exception as patch_error: - print(f"ModelDownloader: Failed to apply HF patch ({str(patch_error)}), using standard HTTP download") - hf_available = False + # Only apply patch if using S3C + if use_s3c and s3c: + try: + s3c.patch_huggingface_hub() + patch_applied = True + print("ModelDownloader: HuggingFace Hub 加速已啟用") + except Exception as patch_error: + print(f"ModelDownloader: 無法套用 HF patch ({str(patch_error)}),使用標準 HTTP 下載") + hf_available = False except ImportError: - print("ModelDownloader: huggingface_hub not installed, using standard HTTP download") + print("ModelDownloader: huggingface_hub 未安裝,使用標準 HTTP 下載") hf_available = False # Start downloading @@ -156,67 +175,107 @@ class ModelDownloader: try: for folder_name, urls in download_map.items(): - # Get target folder path - target_folder = self._get_target_folder(models_root, folder_name) - - # Ensure folder exists - os.makedirs(target_folder, exist_ok=True) - print(f"\nModelDownloader: Target folder = {target_folder}") + # Get target folder path (only if using custom path) + target_folder = None + if use_custom_path: + target_folder = self._get_target_folder(models_root, folder_name) + # Ensure folder exists + os.makedirs(target_folder, exist_ok=True) + print(f"\nModelDownloader: 目標資料夾 = {target_folder}") for url in urls: current_file += 1 filename = self._extract_filename(url) - dest_path = os.path.join(target_folder, filename) - print(f"\n[{current_file}/{total_files}] Downloading: {filename}") + print(f"\n[{current_file}/{total_files}] 正在下載:{filename}") print(f" URL: {url}") - print(f" Destination: {dest_path}") try: - # Determine download method - use_hf_hub = False - if hf_available and patch_applied and self._is_huggingface_url(url): - # Check if file path contains subdirectories + # Determine download method based on settings + if not use_custom_path and self._is_huggingface_url(url): + # Use HuggingFace Hub default cache path + if not hf_available: + results.append(f"✗ {filename} - 錯誤:需要 huggingface_hub 但未安裝") + print(f" ✗ 下載失敗:需要 huggingface_hub") + continue + + from huggingface_hub import hf_hub_download repo_id, file_path = self._parse_hf_url(url) - # Only use hf_hub_download if file is at root level (no subdirs) - # This prevents hf_hub_download from creating unwanted subdirectories - if '/' not in file_path: - use_hf_hub = True - - if use_hf_hub: - # Use hf_hub_download (only for root-level files) - from huggingface_hub import hf_hub_download + # Use HF Hub default cache (no local_dir specified) downloaded_path = hf_hub_download( repo_id=repo_id, filename=file_path, force_download=False, - local_dir=target_folder, - local_dir_use_symlinks=False, ) - results.append(f"✓ {filename} (HF accelerated)") - print(f" ✓ Download successful (with HF acceleration)") + results.append(f"✓ {filename} (HF 快取路徑)") + print(f" ✓ 下載成功至 HF 快取:{downloaded_path}") + + elif use_custom_path: + # Use custom path + dest_path = os.path.join(target_folder, filename) + print(f" 目標:{dest_path}") + + # Determine if should use hf_hub_download with custom path + use_hf_hub = False + if hf_available and patch_applied and self._is_huggingface_url(url): + # Check if file path contains subdirectories + repo_id, file_path = self._parse_hf_url(url) + + # Only use hf_hub_download if file is at root level (no subdirs) + # This prevents hf_hub_download from creating unwanted subdirectories + if '/' not in file_path: + use_hf_hub = True + + if use_hf_hub: + # Use hf_hub_download with custom path + from huggingface_hub import hf_hub_download + downloaded_path = hf_hub_download( + repo_id=repo_id, + filename=file_path, + force_download=False, + local_dir=target_folder, + local_dir_use_symlinks=False, + ) + + results.append(f"✓ {filename} (HF 加速)") + print(f" ✓ 下載成功(HF 加速)") + + elif use_s3c and s3c: + # Use S3impleClient download + result = s3c.download( + url=url, + dest=dest_path, + ) + + if result.success: + size_mb = result.total_bytes / (1024 * 1024) + method = "(S3C + HF patch)" if patch_applied else "(S3C)" + results.append(f"✓ {filename} ({size_mb:.2f} MB) {method}") + print(f" ✓ 下載成功 - {size_mb:.2f} MB {method}") + else: + results.append(f"✗ {filename} - 失敗") + print(f" ✗ 下載失敗") + + else: + # Use plain HTTP download + success = self._download_http(url, dest_path) + if success: + size_mb = os.path.getsize(dest_path) / (1024 * 1024) + results.append(f"✓ {filename} ({size_mb:.2f} MB) (HTTP)") + print(f" ✓ 下載成功 - {size_mb:.2f} MB (HTTP)") + else: + results.append(f"✗ {filename} - HTTP 下載失敗") + print(f" ✗ HTTP 下載失敗") else: - # Use S3impleClient direct download - # (Also accelerated if HF patch is applied) - result = s3c.download( - url=url, - dest=dest_path, - ) - - if result.success: - size_mb = result.total_bytes / (1024 * 1024) - method = "(S3C + HF patch)" if patch_applied else "(S3C)" - results.append(f"✓ {filename} ({size_mb:.2f} MB) {method}") - print(f" ✓ Download successful - {size_mb:.2f} MB {method}") - else: - results.append(f"✗ {filename} - Failed") - print(f" ✗ Download failed") + # Non-HF URL without custom path - error + results.append(f"✗ {filename} - 錯誤:非 HF URL 需要使用自訂路徑") + print(f" ✗ 錯誤:非 HuggingFace URL 必須啟用自訂路徑") except Exception as e: - error_msg = f"✗ {filename} - Error: {str(e)}" + error_msg = f"✗ {filename} - 錯誤:{str(e)}" results.append(error_msg) print(f" {error_msg}") @@ -225,24 +284,24 @@ class ModelDownloader: if patch_applied: try: s3c.unpatch_huggingface_hub() - print("\nModelDownloader: HuggingFace Hub original settings restored") + print("\nModelDownloader: HuggingFace Hub 原始設定已還原") except Exception as unpatch_error: - print(f"\nModelDownloader: Failed to unpatch HF ({str(unpatch_error)})") + print(f"\nModelDownloader: 無法還原 HF patch ({str(unpatch_error)})") # Generate final status message success_count = sum(1 for r in results if r.startswith("✓")) fail_count = sum(1 for r in results if r.startswith("✗")) # Include skipped files in status message - status_lines = [f"Download complete!"] + status_lines = [f"下載完成!"] if skipped_files: - status_lines.append(f"Skipped (already exists): {len(skipped_files)}") - status_lines.append(f"Success: {success_count}/{total_files}") - status_lines.append(f"Failed: {fail_count}") + status_lines.append(f"已跳過(檔案已存在):{len(skipped_files)}") + status_lines.append(f"成功:{success_count}/{total_files}") + status_lines.append(f"失敗:{fail_count}") status_lines.append("") - status_lines.append("Details:") + status_lines.append("詳細資訊:") if skipped_files: - status_lines.append(" Skipped files:") + status_lines.append(" 已跳過的檔案:") for f in skipped_files: status_lines.append(f" ○ {f}") status_lines.extend(f" {r}" for r in results) @@ -424,6 +483,69 @@ class ModelDownloader: return filename + def _download_http(self, url: str, dest_path: str) -> bool: + """ + Download file using plain HTTP (urllib) + + Args: + url: URL to download + dest_path: Destination file path + + Returns: + True if successful, False otherwise + """ + try: + import urllib.request + import shutil + + # Create parent directory if it doesn't exist + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + + # Download with progress indication + print(f" 正在使用 HTTP 下載...") + + with urllib.request.urlopen(url, timeout=300) as response: + total_size = int(response.headers.get('Content-Length', 0)) + + # Write to temporary file first + temp_path = dest_path + '.tmp' + + with open(temp_path, 'wb') as out_file: + downloaded = 0 + chunk_size = 8192 + last_progress = 0 + + while True: + chunk = response.read(chunk_size) + if not chunk: + break + + out_file.write(chunk) + downloaded += len(chunk) + + # Print progress every 10% + if total_size > 0: + progress = int(downloaded * 100 / total_size) + if progress >= last_progress + 10: + print(f" 進度:{progress}% ({downloaded / (1024*1024):.2f} MB / {total_size / (1024*1024):.2f} MB)") + last_progress = progress + + # Move temp file to final destination + shutil.move(temp_path, dest_path) + + return True + + except Exception as e: + print(f" HTTP 下載錯誤:{str(e)}") + # Clean up temp file if it exists + temp_path = dest_path + '.tmp' + if os.path.exists(temp_path): + try: + os.remove(temp_path) + except: + pass + return False + # Node mappings NODE_CLASS_MAPPINGS = { diff --git a/nodes.py b/nodes.py index ba2fbc0..1bfa27e 100644 --- a/nodes.py +++ b/nodes.py @@ -26,6 +26,8 @@ from datetime import datetime from .qwen_inference import QwenGPUInference from .gguf_inference import GGUFInference from .model_downloader import ModelDownloader +from .openai_helper import OpenAIHelper +from .openrouter_llm import OpenRouterLLM class AudioListGenerator: @classmethod @@ -47,7 +49,7 @@ class AudioListGenerator: OUTPUT_IS_LIST = (False, True) RETURN_NAMES = ("cycle", "audio_list") FUNCTION = "split" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Audio" def split(self, waveform, videofps, samplefps, pad_last_segment, crossfade_duration=0.1, crossfade_type="cosine"): audio_tensor = waveform["waveform"] # shape: [1, C, N] @@ -192,7 +194,7 @@ class AudioToFrameCount: RETURN_TYPES = ("INT",) RETURN_NAMES = ("frames",) FUNCTION = "calculate" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Audio" def calculate(self, audio, fps): waveform = audio["waveform"] # shape: [1, channels, samples] @@ -213,7 +215,7 @@ class PromptListGenerator: return { "required": { "text": ("STRING", {"multiline": True, "dynamicPrompts": False}), - "delimiter": ("STRING", {"multiline": False, "default": ",", "dynamicPrompts": False}), + "delimiter": ("STRING", {"multiline": False, "default": "", "dynamicPrompts": False}), "use_regex": ("BOOLEAN", {"default": False}), "keep_delimiter": ("BOOLEAN", {"default": False}), "start_index": ("INT", {"default": 0, "min": 0, "max": 1000}), @@ -230,7 +232,7 @@ class PromptListGenerator: RETURN_NAMES = ("text_list", "total_index") FUNCTION = "run" OUTPUT_IS_LIST = (True, False) - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Tools" def run(self, text, delimiter, use_regex, keep_delimiter, start_index, skip_every, max_count, skip_first_index, random_order, seed): # 處理多個換行符號為一個換行符號 @@ -387,7 +389,7 @@ class NumberListGenerator: RETURN_TYPES = ("INT", "FLOAT", "INT") RETURN_NAMES = ("int_list", "float_list", "total_count") FUNCTION = "generate_number_list" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Math" INPUT_IS_LIST = False OUTPUT_IS_LIST = (True, True, False) @@ -467,7 +469,7 @@ class AudioListCombine: RETURN_TYPES = ("AUDIO",) FUNCTION = "combine_audio_list" - CATEGORY = "listhelper" + CATEGORY = "ListHelper/Audio" # 標記此節點接收清單輸入 INPUT_IS_LIST = True @@ -655,7 +657,7 @@ class CeilDivide: RETURN_TYPES = ("INT",) RETURN_NAMES = ("result",) FUNCTION = "ceil_divide" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Math" def ceil_divide(self, a: int, b: int) -> tuple: """ @@ -679,115 +681,6 @@ class CeilDivide: return (result,) -class LoadVideoPath: - """ - 載入視頻檔案,輸出視頻物件和完整檔案路徑 - """ - - @classmethod - def INPUT_TYPES(cls): - input_dir = folder_paths.get_input_directory() - files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] - files = folder_paths.filter_files_content_types(files, ["video"]) - return { - "required": { - "file": (sorted(files), {"video_upload": True}), - } - } - - CATEGORY = "ListHelper" - RETURN_TYPES = (IO.VIDEO, "STRING") - RETURN_NAMES = ("video", "path") - FUNCTION = "load_video_path" - - def load_video_path(self, file): - video_path = folder_paths.get_annotated_filepath(file) - video_object = VideoFromFile(video_path) - return (video_object, video_path) - - @classmethod - def IS_CHANGED(cls, file): - video_path = folder_paths.get_annotated_filepath(file) - return os.path.getmtime(video_path) - - @classmethod - def VALIDATE_INPUTS(cls, file): - if not folder_paths.exists_annotated_filepath(file): - return f"Invalid video file: {file}" - return True - - -class SaveVideoPath: - """ - 保存視頻檔案,輸出保存後的完整檔案路徑 - """ - - def __init__(self): - self.output_dir = folder_paths.get_output_directory() - self.type = "output" - self.prefix_append = "" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "video": (IO.VIDEO, {"tooltip": "要保存的視頻"}), - "filename_prefix": ("STRING", {"default": "video/ComfyUI", - "tooltip": "檔案名前綴"}), - "format": (VideoContainer.as_input(), {"default": "auto", - "tooltip": "視頻格式"}), - "codec": (VideoCodec.as_input(), {"default": "auto", - "tooltip": "視頻編碼"}), - }, - "hidden": { - "prompt": "PROMPT", - "extra_pnginfo": "EXTRA_PNGINFO" - }, - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("path",) - FUNCTION = "save_video_path" - OUTPUT_NODE = True - CATEGORY = "ListHelper" - - def save_video_path(self, video, filename_prefix, format, codec, - prompt=None, extra_pnginfo=None): - filename_prefix += self.prefix_append - width, height = video.get_dimensions() - - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( - filename_prefix, - self.output_dir, - width, - height - ) - - # 準備元數據 - saved_metadata = None - if not args.disable_metadata: - metadata = {} - if extra_pnginfo is not None: - metadata.update(extra_pnginfo) - if prompt is not None: - metadata["prompt"] = prompt - if len(metadata) > 0: - saved_metadata = metadata - - # 生成檔案名和完整路徑 - file = f"{filename}_{counter:05}_.{VideoContainer.get_extension(format)}" - full_path = os.path.join(full_output_folder, file) - - # 保存視頻 - video.save_to( - full_path, - format=format, - codec=codec, - metadata=saved_metadata - ) - - return (full_path,) - class FrameMatch: """ 調整圖像序列到指定幀數的節點 @@ -819,7 +712,7 @@ class FrameMatch: RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("images",) FUNCTION = "match_frames" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Tools" def match_frames(self, images, target_frames, fill_mode="repeat_last"): """ @@ -981,7 +874,7 @@ class SimpleWildCardPlayer: RETURN_NAMES = ("prompt_list",) OUTPUT_IS_LIST = (True,) FUNCTION = "generate_wildcards" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Tools" def generate_wildcards(self, basic_prompt, wildcard_template, wildcard_files, batch_count, seed): """ @@ -1079,7 +972,7 @@ class BatchToPSD: RETURN_NAMES = ("message",) FUNCTION = "convert_to_psd" OUTPUT_NODE = True - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/Tools" def convert_to_psd(self, images, filename_prefix, reverse_layer_order=False): """ @@ -1270,14 +1163,14 @@ NODE_CLASS_MAPPINGS = { "NumberListGenerator": NumberListGenerator, "AudioListCombine": AudioListCombine, "CeilDivide": CeilDivide, - "LoadVideoPath": LoadVideoPath, - "SaveVideoPath": SaveVideoPath, "FrameMatch": FrameMatch, "SimpleWildCardPlayer": SimpleWildCardPlayer, "QwenGPUInference": QwenGPUInference, "GGUFInference": GGUFInference, "BatchToPSD": BatchToPSD, "ModelDownloader": ModelDownloader, + "OpenAIHelper": OpenAIHelper, + "OpenRouterLLM": OpenRouterLLM, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -1287,13 +1180,13 @@ NODE_DISPLAY_NAME_MAPPINGS = { "NumberListGenerator": "NumberListGenerator", "AudioListCombine": "AudioListCombine", "CeilDivide": "CeilDivide", - "LoadVideoPath": "LoadVideoPath", - "SaveVideoPath": "SaveVideoPath", "FrameMatch": "FrameMatch", "SimpleWildCardPlayer": "Simple WildCard Player", "QwenGPUInference": "Qwen_TE_LLM", "GGUFInference": "GGUF_LLM", "BatchToPSD": "Batch to PSD", - "ModelDownloader": "模型下載器", + "ModelDownloader": "Model Downloader", + "OpenAIHelper": "OpenAI Helper", + "OpenRouterLLM": "OpenRouter LLM", } diff --git a/openai_helper.py b/openai_helper.py index 4b7bf5f..a32dc1c 100644 --- a/openai_helper.py +++ b/openai_helper.py @@ -52,11 +52,51 @@ class OpenAIHelper: except Exception as e: print(f"❌ 保存openaimodel.json失敗: {e}") + @classmethod + def _get_prompt_templates(cls): + """Get all .md template files from Prompt folder""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + prompt_dir = os.path.join(current_dir, "Prompt") + + templates = [] + + if os.path.exists(prompt_dir): + for file in os.listdir(prompt_dir): + if file.lower().endswith('.md'): + templates.append(file) + + if not templates: + return ["No Template"] + + return sorted(templates) + + @classmethod + def _load_template_content(cls, template_name): + """Load template content""" + if template_name == "No Template" or template_name == "Custom": + return "" + + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join(current_dir, "Prompt", template_name) + + if os.path.exists(template_path): + try: + with open(template_path, 'r', encoding='utf-8') as f: + return f.read() + except: + return "" + + return "" + @classmethod def INPUT_TYPES(cls): # 載入保存的配置 config = cls._load_config() + # 獲取範本列表 + templates = cls._get_prompt_templates() + template_options = ["Custom"] + templates + return { "required": { "endpoint": ("STRING", { @@ -78,8 +118,11 @@ class OpenAIHelper: "multiline": True, "default": "請分析提供的內容。" }), + "prompt_template": (template_options, { + "default": template_options[0] if template_options else "Custom" + }), "max_tokens": ("INT", { - "default": 2000, + "default": 4096, "min": 1, "max": 128000, "step": 1 @@ -101,7 +144,7 @@ class OpenAIHelper: RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("text", "model_name_list") FUNCTION = "process_openai" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/LLM" def __init__(self): pass @@ -288,7 +331,7 @@ class OpenAIHelper: print(f"❌ API呼叫異常: {e}") return {"error": {"message": str(e)}} - def process_openai(self, endpoint, api_key, model_name, user_prompt, max_tokens, + def process_openai(self, endpoint, api_key, model_name, user_prompt, prompt_template, max_tokens, system_prompt=None, image1=None, image2=None, image3=None, audio=None, file_path=None): """處理OpenAI請求""" @@ -309,6 +352,15 @@ class OpenAIHelper: # 獲取模型列表 model_list = self._get_model_list(endpoint.strip(), api_key.strip()) + # 加載並應用範本 + template_content = "" + if prompt_template != "Custom": + template_content = self._load_template_content(prompt_template) + + # 如果範本內容存在,使用範本內容替換 system_prompt + if template_content: + system_prompt = template_content + # 準備消息內容 messages = [] diff --git a/openrouter_llm.py b/openrouter_llm.py index bdd2bb1..eedc9f0 100644 --- a/openrouter_llm.py +++ b/openrouter_llm.py @@ -54,7 +54,7 @@ class OpenRouterLLM: """添加自定義模型到models.json檔案""" try: current_models = self._load_models() - + # 檢查模型是否已存在 if model_name not in current_models: current_models.append(model_name) @@ -64,20 +64,63 @@ class OpenRouterLLM: print(f"ℹ️ 模型已存在: {model_name}") except Exception as e: print(f"❌ 添加自定義模型失敗: {e}") - + + @classmethod + def _get_prompt_templates(cls): + """Get all .md template files from Prompt folder""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + prompt_dir = os.path.join(current_dir, "Prompt") + + templates = [] + + if os.path.exists(prompt_dir): + for file in os.listdir(prompt_dir): + if file.lower().endswith('.md'): + templates.append(file) + + if not templates: + return ["No Template"] + + return sorted(templates) + + @classmethod + def _load_template_content(cls, template_name): + """Load template content""" + if template_name == "No Template" or template_name == "Custom": + return "" + + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join(current_dir, "Prompt", template_name) + + if os.path.exists(template_path): + try: + with open(template_path, 'r', encoding='utf-8') as f: + return f.read() + except: + return "" + + return "" + @classmethod def INPUT_TYPES(cls): # 從JSON檔案載入模型列表 text_models = cls._load_models() - + # 在列表末尾添加"Add Custom Model..."選項 text_models_with_add = text_models + ["Add Custom Model..."] - + + # 獲取範本列表 + templates = cls._get_prompt_templates() + template_options = ["Custom"] + templates + return { "required": { "api_key": ("STRING", {"multiline": False, "default": "", "placeholder": "輸入您的OpenRouter API金鑰"}), "user_prompt": ("STRING", {"multiline": True, "default": "Please analyze the provided content."}), "text_model": (text_models_with_add, {"default": text_models_with_add[0]}), + "prompt_template": (template_options, { + "default": template_options[0] if template_options else "Custom" + }), "seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff, "step": 1, "tooltip": "隨機種子控制(-1為隨機)。注意:圖像生成模型(如Gemini)不支援seed參數"}), }, "optional": { @@ -96,7 +139,7 @@ class OpenRouterLLM: RETURN_TYPES = ("IMAGE", "STRING") RETURN_NAMES = ("image_output", "text_output") FUNCTION = "process_llm" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/LLM" def __init__(self): self.config_file = "config.json" @@ -379,11 +422,11 @@ class OpenRouterLLM: print(f"❌ 圖像縮放失敗: {e}") return pil_image - def process_llm(self, api_key, user_prompt, text_model, seed=-1, custom_model="", system_prompt=None, + def process_llm(self, api_key, user_prompt, text_model, prompt_template, seed=-1, custom_model="", system_prompt=None, enable_resize=False, target_width=512, target_height=512, resize_method="lanczos", image_input_1=None, image_input_2=None, image_input_3=None): """處理LLM請求""" - + # 獲取API金鑰 actual_api_key = self._get_api_key(api_key) if not actual_api_key: @@ -391,7 +434,16 @@ class OpenRouterLLM: default_height = target_height if enable_resize else 512 default_width = target_width if enable_resize else 512 return (torch.zeros(*self._get_default_tensor_size(enable_resize, target_width, target_height)), "❌ 錯誤: 請提供OpenRouter API金鑰") - + + # 加載並應用範本 + template_content = "" + if prompt_template != "Custom": + template_content = self._load_template_content(prompt_template) + + # 如果範本內容存在,使用範本內容替換 system_prompt + if template_content: + system_prompt = template_content + # 處理種子設定 - Control After Generate支援 if seed == -1: # 隨機種子 @@ -399,7 +451,7 @@ class OpenRouterLLM: else: # 固定種子 actual_seed = seed - + # 處理模型選擇 if text_model == "Add Custom Model...": if not custom_model or not custom_model.strip(): diff --git a/pyproject.toml b/pyproject.toml index 43664e8..8256a6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "Listhelper" description = "The ListHelper collection is a comprehensive set of custom nodes for ComfyUI that provides powerful list manipulation capabilities. This collection includes audio processing, text splitting, and number generation tools for enhanced workflow automation.Qwen/GGUF Node For LLM function,SimpleWildCardSystem,BatchToPSDForQwenLayer" -version = "1.2.4" +version = "1.2.5" license = {file = "LICENSE"} dependencies = ["regex", "accelerate"] diff --git a/qwen_inference.py b/qwen_inference.py index 8ef0ff4..377d8c0 100644 --- a/qwen_inference.py +++ b/qwen_inference.py @@ -158,7 +158,7 @@ class QwenGPUInference: RETURN_TYPES = ("STRING", "INT",) RETURN_NAMES = ("text", "used_seed",) FUNCTION = "inference" - CATEGORY = "ListHelper" + CATEGORY = "ListHelper/LLM" def _find_qwen_model(self) -> Optional[str]: """Auto-find qwen_3_4b.safetensors model"""