From 6ba51fe59f2fc60b536f5076172612dbf6b850b4 Mon Sep 17 00:00:00 2001 From: "kyj@bowong.ai" Date: Fri, 25 Jul 2025 11:14:47 +0800 Subject: [PATCH] FIX --- ext/comfyui_modal_deploy.py | 102 +++++++++++++++--------- ext/modal_downloader_deploy.py | 141 +++++++++++++++++++++++++++++++++ nodes/image_gesture_nodes.py | 18 +++-- nodes/image_modal_nodes.py | 2 +- 4 files changed, 215 insertions(+), 48 deletions(-) create mode 100644 ext/modal_downloader_deploy.py diff --git a/ext/comfyui_modal_deploy.py b/ext/comfyui_modal_deploy.py index c568519..154290b 100644 --- a/ext/comfyui_modal_deploy.py +++ b/ext/comfyui_modal_deploy.py @@ -14,55 +14,79 @@ image = ( .run_commands( "comfy --skip-prompt install --fast-deps --nvidia --version 0.3.40" ) - .pip_install_from_pyproject(os.path.join(os.path.dirname(__file__),"pyproject.toml")) - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-CustomNode.git", force_build=True) + .pip_install_from_pyproject(os.path.join(os.path.dirname(__file__), "pyproject.toml")) + .run_commands("comfy node install https://gitea.bowongai.com/Polaris/ComfyUI-CustomNode.git") .run_commands("comfy node install https://github.com/yolain/ComfyUI-Easy-Use.git") - .run_commands("cp -f /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/ext/nodes_bfl.py /root/comfy/ComfyUI/comfy_api_nodes/nodes_bfl.py") + .run_commands("comfy node install https://github.com/kijai/ComfyUI-WanVideoWrapper.git") + .run_commands("comfy node install https://github.com/christian-byrne/audio-separation-nodes-comfyui.git") + .run_commands("comfy node install https://github.com/crystian/ComfyUI-Crystools.git") + .run_commands("comfy node install https://github.com/pythongosssss/ComfyUI-Custom-Scripts.git") + .run_commands("comfy node install https://github.com/kijai/ComfyUI-KJNodes.git") + .run_commands("comfy node install https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite.git") + .run_commands("comfy node install https://github.com/WASasquatch/was-node-suite-comfyui.git") + .run_commands("comfy node install https://github.com/cubiq/ComfyUI_essentials.git") + .run_commands("comfy node install https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes.git") + .run_commands("comfy node install https://github.com/jamesWalker55/comfyui-various.git") + .pip_install("sageattention") + .run_commands("rm -rf /root/comfy/ComfyUI/models&&ln -s /models /root/comfy/ComfyUI/models") + .run_commands("rm -rf /root/comfy/ComfyUI/input&&ln -s /models/input /root/comfy/ComfyUI/input") + .run_commands("rm -rf /root/comfy/ComfyUI/outputs&&ln -s /models/output /root/comfy/ComfyUI/output") ) app = modal.App(image=image) custom_secret = modal.Secret.from_name("comfyui-custom-secret", environment_name="dev") +vol = modal.Volume.from_name("comfy_model", environment_name="dev", create_if_missing=True) @app.function( + cpu=(4, 64), + memory=(2048, 131072), + min_containers=0, + buffer_containers=0, max_containers=1, - secrets=[custom_secret] + gpu="L40S", + scaledown_window=600, + secrets=[custom_secret], + region="us", + volumes={ + "/models": vol + } ) @modal.concurrent( - max_inputs=10 + max_inputs=20 ) -@modal.web_server(8000, startup_timeout=60) +@modal.web_server(8000, startup_timeout=120) def ui_1(): - subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True) + subprocess.Popen("comfy launch -- --listen 0.0.0.0 --port 8000", shell=True) -@app.function( - max_containers=1, - secrets=[custom_secret] -) -@modal.concurrent( - max_inputs=10 -) -@modal.web_server(8000, startup_timeout=60) -def ui_2(): - subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True) - -@app.function( - max_containers=1, - secrets=[custom_secret] -) -@modal.concurrent( - max_inputs=10 -) -@modal.web_server(8000, startup_timeout=60) -def ui_3(): - subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True) - -@app.function( - max_containers=1, - secrets=[custom_secret] -) -@modal.concurrent( - max_inputs=10 -) -@modal.web_server(8000, startup_timeout=60) -def ui_4(): - subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True) +# @app.function( +# max_containers=1, +# secrets=[custom_secret] +# ) +# @modal.concurrent( +# max_inputs=10 +# ) +# @modal.web_server(8000, startup_timeout=60) +# def ui_2(): +# subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True) +# +# @app.function( +# max_containers=1, +# secrets=[custom_secret] +# ) +# @modal.concurrent( +# max_inputs=10 +# ) +# @modal.web_server(8000, startup_timeout=60) +# def ui_3(): +# subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True) +# +# @app.function( +# max_containers=1, +# secrets=[custom_secret] +# ) +# @modal.concurrent( +# max_inputs=10 +# ) +# @modal.web_server(8000, startup_timeout=60) +# def ui_4(): +# subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True) diff --git a/ext/modal_downloader_deploy.py b/ext/modal_downloader_deploy.py new file mode 100644 index 0000000..e967933 --- /dev/null +++ b/ext/modal_downloader_deploy.py @@ -0,0 +1,141 @@ +import os +import shutil +import tempfile +from pathlib import Path + +import aiofiles +import httpx +import modal +from fastapi import FastAPI, HTTPException, UploadFile +from pydantic import BaseModel, HttpUrl + +image = ( + modal.Image.debian_slim( + python_version="3.10" + ).pip_install( + ["fastapi[standard]", "httpx", "aiofiles"] + ) +) +app = modal.App(image=image) +vol = modal.Volume.from_name("comfy_model", create_if_missing=True) +DOWNLOAD_BASE_DIR = Path("/models") + + +@app.function( + cpu=(0.125, 8), + memory=(128, 4096), + scaledown_window=360, + timeout=600, + max_containers=500, + min_containers=0, + region="ap", + volumes={ + "/models": vol + } +) +@modal.concurrent(max_inputs=20) +@modal.asgi_app() +def fastapi_webapp(): + fastapi_app = FastAPI( + title="文件下载服务", + description="一个通过URL下载文件到指定目录的API端点", + ) + + # --- Pydantic 模型 --- + # 定义请求体的数据结构和验证规则 + class DownloadRequest(BaseModel): + url: HttpUrl # Pydantic 会自动验证这是否是一个有效的URL + save_path: str # 用户指定的相对保存路径(例如 "videos/my_video.mp4" 或 "my_document.pdf") + + # --- FastAPI 端点 --- + @fastapi_app.post("/download-file/", summary="从URL下载模型") + async def download_file_from_url(request: DownloadRequest): + if request.save_path.endswith("/"): + request.save_path += str(request.url).split("/")[-1].split("?")[0] + fn_call = await do_download.spawn.aio(request.url, request.save_path) + return {"task_id": fn_call.object_id} + + @fastapi_app.post("/upload-file/", summary="上传模型") + async def upload_file(file: UploadFile, save_path: str): + if save_path.endswith("/"): + save_path += str(file.filename) + destination_path = DOWNLOAD_BASE_DIR.joinpath(save_path).resolve() + if os.path.exists(destination_path): + os.remove(destination_path) + print("删除成功") + with open(destination_path, "wb") as f: + f.write(await file.read()) + return {"msg": "上传成功"} + + return fastapi_app + + +@app.function( + cpu=(0.125, 8), + memory=(128, 4096), + scaledown_window=300, + timeout=3600, + max_containers=500, + min_containers=0, + region="ap", + volumes={ + "/models": vol + } +) +async def do_download(url, save_path): + print(f"Downloading {url} to {save_path}") + file_name = os.path.basename(save_path) + if not file_name: + raise HTTPException( + status_code=400, + detail="无效的保存路径:无法提取文件名。" + ) + + # --- 安全性检查 --- + # 构建绝对目标路径 + temp_path = "/tmp/" + str(file_name) + destination_path = DOWNLOAD_BASE_DIR.joinpath(save_path).resolve() + + # 确保解析后的路径仍然在我们的基础下载目录内,防止目录遍历攻击 + if not destination_path.is_relative_to(DOWNLOAD_BASE_DIR.resolve()): + raise HTTPException( + status_code=400, + detail="不安全的路径:禁止在指定的下载目录之外写入文件。" + ) + # --- 文件下载与保存 --- + try: + # 创建目标文件夹(如果不存在) + destination_path.parent.mkdir(parents=True, exist_ok=True) + + # 使用 httpx 进行异步网络请求 + async with httpx.AsyncClient(follow_redirects=True) as client: + # 使用 stream=True 进行流式下载,适合大文件 + async with client.stream("GET", str(url)) as response: + # 检查请求是否成功 + response.raise_for_status() + + # 使用 aiofiles 进行异步文件写入 + async with aiofiles.open(temp_path, 'wb') as f: + async for chunk in response.aiter_bytes(): + await f.write(chunk) + if os.path.exists(destination_path): + os.remove(destination_path) + print("删除成功") + os.system("cp -f {} {}".format(temp_path, destination_path)) + print("移动成功") + except httpx.RequestError as e: + raise HTTPException( + status_code=502, + detail=f"下载文件时网络请求失败: {e}" + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"处理文件时发生内部错误: {e}" + ) + + print({ + "message": "文件下载成功", + "url": url, + "saved_at": str(destination_path) + }) diff --git a/nodes/image_gesture_nodes.py b/nodes/image_gesture_nodes.py index 9a70805..cb03802 100644 --- a/nodes/image_gesture_nodes.py +++ b/nodes/image_gesture_nodes.py @@ -40,7 +40,7 @@ class JMUtils: self.cos_secret_key = yaml_config["cos_secret_key"] self.cos_bucket_name = yaml_config["cos_sucai_bucket_name"] - def submit_task(self, prompt: str, img_url: str, duration: str = "10"): + def submit_task(self, prompt: str, img_url: str, duration: str = "10", resolution:str="720p"): try: headers = { "Content-Type": "application/json", @@ -52,7 +52,7 @@ class JMUtils: "content": [ { "type": "text", - "text": f"{prompt} --resolution 1080p --dur {duration} --camerafixed false", + "text": f"{prompt} --resolution {resolution} --dur {duration} --camerafixed false", }, { "type": "image_url", @@ -253,7 +253,7 @@ class JMUtils: """ try: # 下载视频 - video_path = self.download_video(video_url) + video_path, _ = self.download_video(video_url) # 获取视频总帧数 cmd_frames = [ @@ -334,7 +334,8 @@ class JMGestureCorrect: def INPUT_TYPES(s): return { "required": { - "image": ("IMAGE",) + "image": ("IMAGE",), + "resolution":(["720p","1080p"]) } } @@ -343,7 +344,7 @@ class JMGestureCorrect: FUNCTION = "gen" CATEGORY = "不忘科技-自定义节点🚩/图片/姿态" - def gen(self, image: torch.Tensor): + def gen(self, image: torch.Tensor, resolution: str): wait_time = 240 interval = 2 client = JMUtils() @@ -354,7 +355,7 @@ class JMGestureCorrect: else: raise Exception("上传失败") prompt = "Stand straight ahead, facing the camera, showing your full body, maintaining a proper posture, keeping the camera still, and ensuring that your head and feet are all within the frame" - submit_data = client.submit_task(prompt, image_url) + submit_data = client.submit_task(prompt, image_url, duration="5", resolution=resolution) if submit_data["status"]: job_id = submit_data["data"] else: @@ -385,6 +386,7 @@ class JMCustom: "default": "Stand straight ahead, facing the camera, showing your full body, maintaining a proper posture, keeping the camera still, and ensuring that your head and feet are all within the frame", "multiline": True}), "duration": ("INT", {"default": 5, "min": 2, "max": 10}), + "resolution": (["720p", "1080p"]), "wait_time": ("INT", {"default": 180, "min": 60, "max": 600}), } } @@ -394,7 +396,7 @@ class JMCustom: FUNCTION = "gen" CATEGORY = "不忘科技-自定义节点🚩/视频/即梦" - def gen(self, image: torch.Tensor, prompt: str, duration: int, wait_time: int): + def gen(self, image: torch.Tensor, prompt: str, duration: int, resolution: str, wait_time: int): interval = 2 client = JMUtils() image_io = client.tensor_to_io(image) @@ -403,7 +405,7 @@ class JMCustom: image_url = upload_data["data"] else: raise Exception("上传失败") - submit_data = client.submit_task(prompt, image_url, str(duration)) + submit_data = client.submit_task(prompt, image_url, str(duration), resolution=resolution) if submit_data["status"]: job_id = submit_data["data"] else: diff --git a/nodes/image_modal_nodes.py b/nodes/image_modal_nodes.py index 4c08891..6753f43 100644 --- a/nodes/image_modal_nodes.py +++ b/nodes/image_modal_nodes.py @@ -278,7 +278,7 @@ class ModalMidJourneyDescribeImage: }, } - RETURN_TYPES = ("TEXT",) + RETURN_TYPES = ("STRING",) RETURN_NAMES = ("描述内容",) FUNCTION = "process" OUTPUT_NODE = False