From 385ff2d4aa7486c0cce9654b467d4232c5bd3b98 Mon Sep 17 00:00:00 2001 From: zhh Date: Thu, 16 Oct 2025 14:35:47 +0800 Subject: [PATCH 01/13] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ebf746..023ed87 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,4 @@ Docker 部署 3. 查看日志 - $ docker-compose logs -f \ No newline at end of file + $ docker-compose logs -f \ No newline at end of file From 7459583377032d0f2ae821e2b7f28d1bb4bd7dcd Mon Sep 17 00:00:00 2001 From: zhh Date: Mon, 3 Nov 2025 11:52:39 +0800 Subject: [PATCH 02/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20=20=E6=96=B0=E5=A2=9Ewan2.2=20pose-transform?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=8E=A5=E5=8F=A3=EF=BC=8Ccomfyui-api?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89?= =?UTF-8?q?:=20docs=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:?= =?UTF-8?q?=20refactor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B5=8B=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api_pose_transform.py | 49 +++ app/core/config.py | 3 + app/service/comfyui_I2V/server.py | 703 ++++++++++++++++++++++++++++++ 3 files changed, 755 insertions(+) create mode 100644 app/service/comfyui_I2V/server.py diff --git a/app/api/api_pose_transform.py b/app/api/api_pose_transform.py index fe5fc5a..bc8dd33 100644 --- a/app/api/api_pose_transform.py +++ b/app/api/api_pose_transform.py @@ -1,10 +1,13 @@ import json import logging +import requests from fastapi import APIRouter, BackgroundTasks, HTTPException +from app.core.config import COMFYUI_SERVER_ADDRESS from app.schemas.pose_transform import PoseTransformModel from app.schemas.response_template import ResponseModel +from app.service.comfyui_I2V.server import ComfyUIServerI2V from app.service.generate_image.service_pose_transform import PoseTransformService, infer_cancel as pose_transform_infer_cancel router = APIRouter() @@ -47,3 +50,49 @@ def pose_transform_cancel(tasks_id: str): logger.warning(f"pose_transform_cancel Run Exception @@@@@@:{e}") raise HTTPException(status_code=404, detail=str(e)) return ResponseModel(data=data['data']) + + +@router.post("/comfyui_pose_transform") +def comfyui_pose_transform(request_item: PoseTransformModel, background_tasks: BackgroundTasks): + """ + 创建一个具有以下参数的请求体: + - **tasks_id**: 任务id 用于取消生成任务和获取生成结果 + - **image_url**: 被生成图片的S3或minio url地址 + - **pose_id**: 1 + + + 示例参数: + { + "tasks_id": "123-89", + "image_url": "aida-results/result_0000b606-1902-11ef-9424-0242ac180002.png", + "pose_id": "1" + } + """ + try: + logger.info(f"pose_transform request item is : @@@@@@:{json.dumps(request_item.dict())}") + service = ComfyUIServerI2V(request_item) + background_tasks.add_task(service.get_result) + except Exception as e: + logger.warning(f"pose_transform Run Exception @@@@@@:{e}") + raise HTTPException(status_code=404, detail=str(e)) + return ResponseModel() + + +@router.get("/comfyui_pose_transform_cancel/{tasks_id}") +def pose_transform_cancel(tasks_id: str): + try: + logger.info(f"pose_transform_cancel request item is : @@@@@@:{tasks_id}") + response = requests.post( + f"http://{COMFYUI_SERVER_ADDRESS}/interrupt", + json={"prompt_id": tasks_id} + ) + data = {} + if response.status_code == 200: + data['data']['message'] = "任务已成功中断" + else: + data['data']['message'] = f"中断失败:{response.text}" + logger.info(f"pose_transform_cancel response @@@@@@:{data}") + except Exception as e: + logger.warning(f"pose_transform_cancel Run Exception @@@@@@:{e}") + raise HTTPException(status_code=404, detail=str(e)) + return ResponseModel(data=data['data']) diff --git a/app/core/config.py b/app/core/config.py index f0a2c35..e6c7283 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -230,3 +230,6 @@ TABLE_CATEGORIES = { "male_bottoms": "male/bottoms", "male_outwear": "male/outwear" } + +# --- ComfyUI 配置信息 --- +COMFYUI_SERVER_ADDRESS = "10.1.2.227:8080" # 替换为您的 ComfyUI 服务器地址 diff --git a/app/service/comfyui_I2V/server.py b/app/service/comfyui_I2V/server.py new file mode 100644 index 0000000..1ea7e29 --- /dev/null +++ b/app/service/comfyui_I2V/server.py @@ -0,0 +1,703 @@ +import io +import json +import logging +import os +import random +import tempfile +import time +import uuid + +import redis +import requests +from PIL import Image +from minio import Minio, S3Error +from moviepy.video.io.VideoFileClip import VideoFileClip + +from app.core.config import REDIS_HOST, REDIS_PORT, REDIS_DB, MINIO_URL, MINIO_ACCESS, MINIO_SECRET, MINIO_SECURE, COMFYUI_SERVER_ADDRESS, PS_RABBITMQ_QUEUES, DEBUG +from app.schemas.pose_transform import PoseTransformModel + +logger = logging.getLogger() + +workflow_json = { + "162": { + "inputs": { + "text": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走", + "clip": [ + "167", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Negative Prompt)" + } + }, + "163": { + "inputs": { + "fps": 24, + "images": [ + "164", + 0 + ] + }, + "class_type": "CreateVideo", + "_meta": { + "title": "创建视频" + } + }, + "164": { + "inputs": { + "samples": [ + "175", + 0 + ], + "vae": [ + "168", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "165": { + "inputs": { + "unet_name": "wan2.2_fun_control_high_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "166": { + "inputs": { + "unet_name": "wan2.2_fun_control_low_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "167": { + "inputs": { + "clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "type": "wan", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "168": { + "inputs": { + "vae_name": "wan_2.1_vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "加载VAE" + } + }, + "169": { + "inputs": { + "add_noise": "enable", + "noise_seed": 8860422635573, + "steps": 4, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "start_at_step": 0, + "end_at_step": 2, + "return_with_leftover_noise": "enable", + "model": [ + "176", + 0 + ], + "positive": [ + "180", + 0 + ], + "negative": [ + "180", + 1 + ], + "latent_image": [ + "180", + 2 + ] + }, + "class_type": "KSamplerAdvanced", + "_meta": { + "title": "K采样器(高级)" + } + }, + "170": { + "inputs": { + "filename_prefix": "video/wan2.2_fun_control", + "format": "auto", + "codec": "auto", + "video-preview": "", + "video": [ + "163", + 0 + ] + }, + "class_type": "SaveVideo", + "_meta": { + "title": "保存视频" + } + }, + "171": { + "inputs": { + "video": [ + "174", + 0 + ] + }, + "class_type": "GetVideoComponents", + "_meta": { + "title": "获取视频组件" + } + }, + "174": { + "inputs": { + "file": "skeleton_3.mp4" + }, + "class_type": "LoadVideo", + "_meta": { + "title": "加载视频" + } + }, + "175": { + "inputs": { + "add_noise": "disable", + "noise_seed": 0, + "steps": 4, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "start_at_step": 2, + "end_at_step": 4, + "return_with_leftover_noise": "disable", + "model": [ + "177", + 0 + ], + "positive": [ + "180", + 0 + ], + "negative": [ + "180", + 1 + ], + "latent_image": [ + "169", + 0 + ] + }, + "class_type": "KSamplerAdvanced", + "_meta": { + "title": "K采样器(高级)" + } + }, + "176": { + "inputs": { + "shift": 8.000000000000002, + "model": [ + "181", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "采样算法(SD3)" + } + }, + "177": { + "inputs": { + "shift": 8.000000000000002, + "model": [ + "182", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "采样算法(SD3)" + } + }, + "178": { + "inputs": { + "image": "296f5fd6-c5e4-4003-9798-f378a4f08411-0-89.png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "179": { + "inputs": { + "text": "On a sunny summer day, there are marshmallow - like clouds, and the sunlight is bright and warm. A girl with white curly double - ponytails is wearing unique sunglasses, distinctive clothes and shoes. Her posture is natural and full of dynamic tension. The background is the scene of the Leaning Tower of Pisa in Italy, emphasizing the realistic contrast of details in reality. The whole picture is in a realistic 3D style, rich in details and with a relaxed atmosphere. She is dancing slowly, waving her hands.", + "clip": [ + "167", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Positive Prompt)" + } + }, + "180": { + "inputs": { + "width": 512, + "height": 768, + "length": 121, + "batch_size": 1, + "positive": [ + "179", + 0 + ], + "negative": [ + "162", + 0 + ], + "vae": [ + "168", + 0 + ], + "ref_image": [ + "178", + 0 + ], + "control_video": [ + "171", + 0 + ] + }, + "class_type": "Wan22FunControlToVideo", + "_meta": { + "title": "Wan22FunControlToVideo" + } + }, + "181": { + "inputs": { + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", + "strength_model": 1, + "model": [ + "165", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + }, + "182": { + "inputs": { + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", + "strength_model": 1, + "model": [ + "166", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + } +} + +pose_video_map = { + "1": "input_pose_video/1.mp4", + "2": "input_pose_video/2.mp4", + "3": "input_pose_video/3.mp4", + "4": "input_pose_video/4.mp4", + "5": "input_pose_video/5.mp4", + "6": "input_pose_video/6.mp4" +} + + +class ComfyUIServerI2V: + def __init__(self, request_data): + self.image_url = request_data.image_url + self.pose_num = request_data.pose_id + self.tasks_id = request_data.tasks_id + self.user_id = self.tasks_id[self.tasks_id.rfind('-') + 1:] + self.redis_client = redis.StrictRedis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, decode_responses=True) + self.pose_transform_data = {'tasks_id': self.tasks_id, 'status': 'PENDING', 'message': "pending", 'gif_url': '', + 'video_url': '', 'image_url': ''} + self.redis_client.set(self.tasks_id, json.dumps(self.pose_transform_data)) + self.redis_client.expire(self.tasks_id, 600) + self.minio_client = Minio(MINIO_URL, access_key=MINIO_ACCESS, secret_key=MINIO_SECRET, secure=MINIO_SECURE) + + def get_result(self): + logger.info("11111111111111") + workflow_json['174']['inputs']['file'] = pose_video_map[self.pose_num ] + workflow_json['169']['inputs']['noise_seed'] = random.randint(0, 10 ** 18) + + # 下载图片 上传 comfyui server + in_memory_file, object_name = self.download_from_minio_in_memory() + if in_memory_file and object_name: + uploaded_filename = self.upload_in_memory_file_to_comfyui(in_memory_file, object_name) + + # 1. 提交任务 + prompt_response = self.queue_prompt(workflow_json, self.tasks_id) + if not prompt_response: + return + + prompt_id = prompt_response.get("prompt_id") + print(f" 任务已提交,Prompt ID: {prompt_id}") + + outputs = self.poll_history(prompt_id) + file_list = {} + for node_id, node_output in outputs.items(): + # 检查当前节点输出中是否包含 'images' 列表 + if 'images' in node_output and isinstance(node_output['images'], list): + + # 'images' 列表中的每个元素都是一个文件对象 + for file_info in node_output['images']: + # 确保关键字段存在 + if all(key in file_info for key in ['filename', 'subfolder', 'type']): + file_list = { + 'filename': file_info['filename'], + 'subfolder': file_info['subfolder'], + 'type': file_info['type'] + } + print(file_list) + return self.process_and_upload_comfyui_video(filename=file_list['filename'], subfolder=file_list['subfolder'], prompt_id=prompt_response['prompt_id']), prompt_id + + def read_tasks_status(self): + status_data = self.redis_client.get(self.tasks_id) + return json.loads(status_data), status_data + + def download_from_minio_in_memory(self): + bucket = self.image_url.split('/')[0] + object_name = self.image_url[self.image_url.find('/') + 1:] + print("🚀 正在连接 MinIO 客户端...") + + try: + # get_object 返回一个 ResponseStream 对象 + response_stream = self.minio_client.get_object( + bucket, + object_name, + ) + + # 读取整个流到内存 (BytesIO),避免写入本地文件 + image_bytes = response_stream.read() + + response_stream.close() + response_stream.release_conn() + + in_memory_file = io.BytesIO(image_bytes) + + print(f"✅ 图片已下载到内存 ({len(image_bytes)} 字节)。") + return in_memory_file, object_name.rsplit('/')[-1] + + except S3Error as e: + print(f"❌ MinIO S3 错误 (例如,对象不存在): {e}") + return None, None + except Exception as e: + print(f"❌ MinIO 下载过程中发生未知错误: {e}") + return None, None + + def upload_video_to_minio(self, BUCKET_NAME, OBJECT_NAME, LOCAL_FILE_PATH): + """使用 fput_object 从本地路径上传 MP4 文件""" + try: + # 使用 fput_object 上传文件 + # content_type 对于视频流播放非常重要,MP4 文件应使用 'video/mp4' + result = self.minio_client.fput_object( + bucket_name=BUCKET_NAME, + object_name=OBJECT_NAME, + file_path=LOCAL_FILE_PATH, + content_type="video/mp4" # 设置正确的内容类型 + ) + + print(f"✅ 文件 '{LOCAL_FILE_PATH}' 已成功上传至 MinIO:") + print(f" 对象名: {result.object_name}") + print(f" Etag: {result.etag}") + + except S3Error as e: + print(f"❌ MinIO 操作失败: {e}") + except FileNotFoundError: + print(f"❌ 找不到本地文件: {LOCAL_FILE_PATH}") + except Exception as e: + print(f"❌ 发生未知错误: {e}") + + def upload_gif_to_minio(self, BUCKET_NAME, OBJECT_NAME, LOCAL_FILE_PATH): + """使用 fput_object 从本地路径上传 MP4 文件""" + try: + # 使用 fput_object 上传文件 + # content_type 对于视频流播放非常重要,MP4 文件应使用 'video/mp4' + result = self.minio_client.fput_object( + bucket_name=BUCKET_NAME, + object_name=OBJECT_NAME, + file_path=LOCAL_FILE_PATH, + content_type="video/mp4" # 设置正确的内容类型 + ) + + print(f"✅ 文件 '{LOCAL_FILE_PATH}' 已成功上传至 MinIO:") + print(f" 对象名: {result.object_name}") + print(f" Etag: {result.etag}") + + except S3Error as e: + print(f"❌ MinIO 操作失败: {e}") + except FileNotFoundError: + print(f"❌ 找不到本地文件: {LOCAL_FILE_PATH}") + except Exception as e: + print(f"❌ 发生未知错误: {e}") + + def upload_in_memory_file_to_comfyui(self, in_memory_file, filename): + upload_url = f"http://{COMFYUI_SERVER_ADDRESS}/upload/image" + + data = { + "overwrite": "true", + "type": "input" + } + + # 构建 multipart/form-data: (文件名, 内存文件对象, MIME 类型) + # MIME 类型可以根据实际图片类型修改,这里使用常见的 png/jpeg + mime_type = 'image/png' if filename.lower().endswith('.png') else 'image/jpeg' + + files = { + 'image': (filename, in_memory_file, mime_type) + } + + print(f"⬆️ 正在上传图片 ({filename}) 到 ComfyUI...") + try: + comfyui_response = requests.post(upload_url, data=data, files=files) + comfyui_response.raise_for_status() + + result = comfyui_response.json() + uploaded_name = result.get('name') + + print(f"🎉 ComfyUI 上传成功! 服务器文件名: {uploaded_name}") + return uploaded_name + + except requests.exceptions.RequestException as e: + print(f"❌ ComfyUI 上传失败: {e}") + print(f" 响应内容: {comfyui_response.text}") + return None + + def process_and_upload_comfyui_video( + self, + filename: str, + subfolder: str, + prompt_id: str, + ): + """ + 完整的自动化流程:获取 ComfyUI 视频 -> 转换 GIF 并提取帧 -> 上传所有结果到 MinIO。 + """ + # 1. 从 ComfyUI 获取视频二进制数据 + mp4_bytes = self.get_comfyui_video_bytes(filename, subfolder) + if not mp4_bytes: + return + + # 2. 准备进行视频处理 + # moviepy 不支持直接使用 bytes,需要将 bytes 写入一个 BytesIO 或临时文件 + # 为了避免写磁盘,我们将使用 BytesIO,但 MoviePy 内部依赖 FFmpeg,有时需要一个可寻址的本地文件路径。 + # 最可靠且避免写本地的方案是在内存中操作,然后将结果上传。 + + # ⚠️ 关键点:将 mp4_bytes 写入 BytesIO 以模拟文件,供 moviepy 读取 + + # 定义输出对象名 + + output_base_name = uuid.uuid4().hex + MP4_OBJECT = f"{self.user_id}/pose_transform_video/{prompt_id}/{output_base_name}.mp4" + GIF_OBJECT = f"{self.user_id}/pose_transform_gif/{prompt_id}/{output_base_name}.gif" + FRAME_OBJECT = f"{self.user_id}/pose_transform_first_img/{prompt_id}/{output_base_name}_frame.jpg" + + # --- 视频处理和帧提取 --- + try: + # 1. 创建一个临时的 MP4 文件路径 + # delete=False 确保文件在关闭后仍然存在,直到我们手动删除 + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_file: + tmp_file.write(mp4_bytes) # 将内存数据写入磁盘 + temp_mp4_path = tmp_file.name # 记录文件路径 + + print(f"临时文件已写入: {temp_mp4_path}") + + # 2. 使用 moviepy 打开临时文件 (传入文件路径字符串) + clip = VideoFileClip(temp_mp4_path) + + # --- 在这里进行所有的视频处理和提取操作 --- + + # 提取第一帧 (保持原尺寸) + frame_array = clip.get_frame(t=0.0) + image = Image.fromarray(frame_array) + + frame_stream = io.BytesIO() + image.save(frame_stream, 'JPEG') + frame_bytes = frame_stream.getvalue() + + print("✅ 成功提取第一帧图片。") + + # 视频转 GIF (使用另一个临时文件来保存 GIF) + temp_gif_path = "" + with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmp_file: + temp_gif_path = tmp_file.name + + target_fps = int(round(clip.fps)) if clip.fps else 24 + clip.write_gif(temp_gif_path, fps=target_fps) + + with open(temp_gif_path, 'rb') as f: + gif_bytes = f.read() + + print("✅ 成功生成 GIF。") + + # 返回结果 (例如: 上传到 MinIO) + # return mp4_bytes, gif_bytes, frame_bytes + + # ----------------------------------------------- + + except Exception as e: + print(f"❌ 视频处理或文件操作失败: {e}") + # 在失败时,也尝试清理文件 + + finally: + # 3. 清理临时文件 (非常重要!) + if os.path.exists(temp_mp4_path): + os.remove(temp_mp4_path) + print(f"🗑️ 已删除临时 MP4 文件: {temp_mp4_path}") + + if 'temp_gif_path' in locals() and os.path.exists(temp_gif_path): + os.remove(temp_gif_path) + print(f"🗑️ 已删除临时 GIF 文件: {temp_gif_path}") + + # 3. 上传所有结果到 MinIO + + # 上传原始 MP4 + self.upload_stream_to_minio(mp4_bytes, MP4_OBJECT, "video/mp4") + + # 上传生成的 GIF + self.upload_stream_to_minio(gif_bytes, GIF_OBJECT, "image/gif") + + # 上传第一帧图片 + self.upload_stream_to_minio(frame_bytes, FRAME_OBJECT, "image/jpeg") + + return "\n🎉 所有任务完成!" + + # --- 辅助函数:提交任务到队列 --- + def queue_prompt(self, prompt, client_id): + """向 ComfyUI 提交工作流提示。""" + p = {"prompt": prompt, "client_id": client_id, "prompt_id": client_id} + data = json.dumps(p).encode('utf-8') + + # 提交任务到 /prompt 端点 + response = requests.post(f"http://{COMFYUI_SERVER_ADDRESS}/prompt", data=data) + print(f"-------------{response.text}") + print(f"------------{client_id}") + + if response.status_code == 200: + return response.json() + else: + print(f"提交任务失败,状态码: {response.status_code}") + print(response.text) + return None + + def get_history(self, prompt_id): + """通过 prompt_id 获取任务的历史记录和输出。""" + # 查询 /history/{prompt_id} 端点 + response = requests.get(f"http://{COMFYUI_SERVER_ADDRESS}/history/{prompt_id}") + + if response.status_code == 200: + return response.json() + else: + print(f"获取历史记录失败,状态码: {response.status_code}") + return None + + def poll_history(self, prompt_id, interval_seconds=5): + """步骤 2: 轮询 /history/{prompt_id} 检查任务是否完成""" + url = f"http://{COMFYUI_SERVER_ADDRESS}/history/{prompt_id}" + + print(f"⏳ 开始轮询状态 (间隔 {interval_seconds} 秒)...") + + while True: + time.sleep(interval_seconds) + + try: + response = requests.get(url) + # 任务未完成时,ComfyUI可能会返回404或空响应,我们只关注成功响应 + if response.status_code == 200: + history_data = response.json() + + # ComfyUI 返回的历史记录结构是 {prompt_id: {outputs: ...}} + if prompt_id in history_data: + print("🎉 任务已完成!") + return history_data[prompt_id]['outputs'] + + print("⏳ 任务仍在执行或等待中...") + + except requests.exceptions.RequestException as e: + # 处理可能的连接错误,但通常不会在内部轮询中发生 + print(f"⚠️ 轮询时发生错误: {e}") + pass + + def get_comfyui_video_bytes(self, filename: str, subfolder: str, file_type: str = "output"): + """ + 从 ComfyUI 的 /view 端点获取视频文件的二进制数据。 + + 参数: + - filename: 视频文件名 (例如: 'ComfyUI_00002_.mp4') + - subfolder: 存储子文件夹 (例如: 'ComfyUI_2025-10-31') + - file_type: 文件类型 (通常是 'output') + + 返回: + - 视频文件的二进制内容 (bytes) 或 None。 + """ + url = f"http://{COMFYUI_SERVER_ADDRESS}/view" + params = { + "filename": filename, + "subfolder": subfolder, + "type": file_type + } + + print(f"📡 正在从 ComfyUI 下载视频: {filename}") + try: + # 使用 requests.get 下载文件 + response = requests.get(url, params=params, stream=True) + response.raise_for_status() # 检查 HTTP 错误 + + # 返回文件的完整二进制内容 + return response.content + + except requests.exceptions.RequestException as e: + print(f"❌ 从 ComfyUI 获取视频失败: {e}") + return None + + def upload_stream_to_minio(self, video_bytes: bytes, object_name: str, content_type: str): + """从内存流上传数据到 MinIO。""" + print(f"☁️ 正在上传对象到 MinIO: {object_name}") + try: + + data_stream = io.BytesIO(video_bytes) + + result = self.minio_client.put_object( + bucket_name='aida-users', + object_name=object_name, + data=data_stream, + length=len(video_bytes), + content_type=content_type + ) + print(f"✅ MinIO 上传成功: {result.object_name}") + return True + except S3Error as e: + print(f"❌ MinIO 上传失败: {e}") + return False + + +if __name__ == '__main__': + request_data = PoseTransformModel( + tasks_id="1515151123-89111", + image_url="aida-results/result_0000b606-1902-11ef-9424-0242ac180002.png", + pose_id="6" + ) + + server = ComfyUIServerI2V(request_data) + print(server.get_result()) From a9d9bdcb7137649bfee5d290a1a480521f1c67e2 Mon Sep 17 00:00:00 2001 From: zhh Date: Mon, 3 Nov 2025 16:37:41 +0800 Subject: [PATCH 03/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20=20=E6=96=B0=E5=A2=9Ewan2.2=20pose-transform?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=8E=A5=E5=8F=A3=EF=BC=8Ccomfyui-api?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89?= =?UTF-8?q?:=20docs=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:?= =?UTF-8?q?=20refactor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B5=8B=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/comfyui_I2V/server.py | 134 +++++++++++++++--------------- 1 file changed, 65 insertions(+), 69 deletions(-) diff --git a/app/service/comfyui_I2V/server.py b/app/service/comfyui_I2V/server.py index 1ea7e29..7f98038 100644 --- a/app/service/comfyui_I2V/server.py +++ b/app/service/comfyui_I2V/server.py @@ -15,6 +15,7 @@ from moviepy.video.io.VideoFileClip import VideoFileClip from app.core.config import REDIS_HOST, REDIS_PORT, REDIS_DB, MINIO_URL, MINIO_ACCESS, MINIO_SECRET, MINIO_SECURE, COMFYUI_SERVER_ADDRESS, PS_RABBITMQ_QUEUES, DEBUG from app.schemas.pose_transform import PoseTransformModel +from app.service.generate_image.utils.mq import publish_status logger = logging.getLogger() @@ -331,15 +332,13 @@ class ComfyUIServerI2V: self.tasks_id = request_data.tasks_id self.user_id = self.tasks_id[self.tasks_id.rfind('-') + 1:] self.redis_client = redis.StrictRedis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, decode_responses=True) - self.pose_transform_data = {'tasks_id': self.tasks_id, 'status': 'PENDING', 'message': "pending", 'gif_url': '', - 'video_url': '', 'image_url': ''} + self.pose_transform_data = {'tasks_id': self.tasks_id, 'status': 'PENDING', 'message': "pending", 'gif_url': '', 'video_url': '', 'image_url': ''} self.redis_client.set(self.tasks_id, json.dumps(self.pose_transform_data)) self.redis_client.expire(self.tasks_id, 600) self.minio_client = Minio(MINIO_URL, access_key=MINIO_ACCESS, secret_key=MINIO_SECRET, secure=MINIO_SECURE) def get_result(self): - logger.info("11111111111111") - workflow_json['174']['inputs']['file'] = pose_video_map[self.pose_num ] + workflow_json['174']['inputs']['file'] = pose_video_map[self.pose_num] workflow_json['169']['inputs']['noise_seed'] = random.randint(0, 10 ** 18) # 下载图片 上传 comfyui server @@ -353,7 +352,7 @@ class ComfyUIServerI2V: return prompt_id = prompt_response.get("prompt_id") - print(f" 任务已提交,Prompt ID: {prompt_id}") + logger.info(f" 任务已提交,Prompt ID: {prompt_id}") outputs = self.poll_history(prompt_id) file_list = {} @@ -370,7 +369,7 @@ class ComfyUIServerI2V: 'subfolder': file_info['subfolder'], 'type': file_info['type'] } - print(file_list) + logger.info(file_list) return self.process_and_upload_comfyui_video(filename=file_list['filename'], subfolder=file_list['subfolder'], prompt_id=prompt_response['prompt_id']), prompt_id def read_tasks_status(self): @@ -380,7 +379,7 @@ class ComfyUIServerI2V: def download_from_minio_in_memory(self): bucket = self.image_url.split('/')[0] object_name = self.image_url[self.image_url.find('/') + 1:] - print("🚀 正在连接 MinIO 客户端...") + # print("🚀 正在连接 MinIO 客户端...") try: # get_object 返回一个 ResponseStream 对象 @@ -397,14 +396,14 @@ class ComfyUIServerI2V: in_memory_file = io.BytesIO(image_bytes) - print(f"✅ 图片已下载到内存 ({len(image_bytes)} 字节)。") + # print(f"✅ 图片已下载到内存 ({len(image_bytes)} 字节)。") return in_memory_file, object_name.rsplit('/')[-1] except S3Error as e: - print(f"❌ MinIO S3 错误 (例如,对象不存在): {e}") + logger.error(f"❌ MinIO S3 错误 (例如,对象不存在): {e}") return None, None except Exception as e: - print(f"❌ MinIO 下载过程中发生未知错误: {e}") + logger.error(f"❌ MinIO 下载过程中发生未知错误: {e}") return None, None def upload_video_to_minio(self, BUCKET_NAME, OBJECT_NAME, LOCAL_FILE_PATH): @@ -419,16 +418,16 @@ class ComfyUIServerI2V: content_type="video/mp4" # 设置正确的内容类型 ) - print(f"✅ 文件 '{LOCAL_FILE_PATH}' 已成功上传至 MinIO:") - print(f" 对象名: {result.object_name}") - print(f" Etag: {result.etag}") + # print(f"✅ 文件 '{LOCAL_FILE_PATH}' 已成功上传至 MinIO:") + # print(f" 对象名: {result.object_name}") + # print(f" Etag: {result.etag}") except S3Error as e: - print(f"❌ MinIO 操作失败: {e}") + logger.error(f"❌ MinIO 操作失败: {e}") except FileNotFoundError: - print(f"❌ 找不到本地文件: {LOCAL_FILE_PATH}") + logger.error(f"❌ 找不到本地文件: {LOCAL_FILE_PATH}") except Exception as e: - print(f"❌ 发生未知错误: {e}") + logger.error(f"❌ 发生未知错误: {e}") def upload_gif_to_minio(self, BUCKET_NAME, OBJECT_NAME, LOCAL_FILE_PATH): """使用 fput_object 从本地路径上传 MP4 文件""" @@ -442,16 +441,16 @@ class ComfyUIServerI2V: content_type="video/mp4" # 设置正确的内容类型 ) - print(f"✅ 文件 '{LOCAL_FILE_PATH}' 已成功上传至 MinIO:") - print(f" 对象名: {result.object_name}") - print(f" Etag: {result.etag}") + # print(f"✅ 文件 '{LOCAL_FILE_PATH}' 已成功上传至 MinIO:") + # print(f" 对象名: {result.object_name}") + # print(f" Etag: {result.etag}") except S3Error as e: - print(f"❌ MinIO 操作失败: {e}") + logger.error(f"❌ MinIO 操作失败: {e}") except FileNotFoundError: - print(f"❌ 找不到本地文件: {LOCAL_FILE_PATH}") + logger.error(f"❌ 找不到本地文件: {LOCAL_FILE_PATH}") except Exception as e: - print(f"❌ 发生未知错误: {e}") + logger.error(f"❌ 发生未知错误: {e}") def upload_in_memory_file_to_comfyui(self, in_memory_file, filename): upload_url = f"http://{COMFYUI_SERVER_ADDRESS}/upload/image" @@ -469,7 +468,7 @@ class ComfyUIServerI2V: 'image': (filename, in_memory_file, mime_type) } - print(f"⬆️ 正在上传图片 ({filename}) 到 ComfyUI...") + # print(f"⬆️ 正在上传图片 ({filename}) 到 ComfyUI...") try: comfyui_response = requests.post(upload_url, data=data, files=files) comfyui_response.raise_for_status() @@ -477,20 +476,15 @@ class ComfyUIServerI2V: result = comfyui_response.json() uploaded_name = result.get('name') - print(f"🎉 ComfyUI 上传成功! 服务器文件名: {uploaded_name}") + # print(f"🎉 ComfyUI 上传成功! 服务器文件名: {uploaded_name}") return uploaded_name except requests.exceptions.RequestException as e: - print(f"❌ ComfyUI 上传失败: {e}") - print(f" 响应内容: {comfyui_response.text}") + logger.error(f"❌ ComfyUI 上传失败: {e}") + logger.error(f" 响应内容: {comfyui_response.text}") return None - def process_and_upload_comfyui_video( - self, - filename: str, - subfolder: str, - prompt_id: str, - ): + def process_and_upload_comfyui_video(self, filename: str, subfolder: str, prompt_id: str, ): """ 完整的自动化流程:获取 ComfyUI 视频 -> 转换 GIF 并提取帧 -> 上传所有结果到 MinIO。 """ @@ -521,7 +515,7 @@ class ComfyUIServerI2V: tmp_file.write(mp4_bytes) # 将内存数据写入磁盘 temp_mp4_path = tmp_file.name # 记录文件路径 - print(f"临时文件已写入: {temp_mp4_path}") + # print(f"临时文件已写入: {temp_mp4_path}") # 2. 使用 moviepy 打开临时文件 (传入文件路径字符串) clip = VideoFileClip(temp_mp4_path) @@ -536,7 +530,7 @@ class ComfyUIServerI2V: image.save(frame_stream, 'JPEG') frame_bytes = frame_stream.getvalue() - print("✅ 成功提取第一帧图片。") + logger.info("✅ 成功提取第一帧图片。") # 视频转 GIF (使用另一个临时文件来保存 GIF) temp_gif_path = "" @@ -549,7 +543,7 @@ class ComfyUIServerI2V: with open(temp_gif_path, 'rb') as f: gif_bytes = f.read() - print("✅ 成功生成 GIF。") + logger.info("✅ 成功生成 GIF。") # 返回结果 (例如: 上传到 MinIO) # return mp4_bytes, gif_bytes, frame_bytes @@ -557,31 +551,44 @@ class ComfyUIServerI2V: # ----------------------------------------------- except Exception as e: - print(f"❌ 视频处理或文件操作失败: {e}") + logger.error(f"❌ 视频处理或文件操作失败: {e}") # 在失败时,也尝试清理文件 finally: # 3. 清理临时文件 (非常重要!) if os.path.exists(temp_mp4_path): os.remove(temp_mp4_path) - print(f"🗑️ 已删除临时 MP4 文件: {temp_mp4_path}") + logger.info(f"🗑️ 已删除临时 MP4 文件: {temp_mp4_path}") if 'temp_gif_path' in locals() and os.path.exists(temp_gif_path): os.remove(temp_gif_path) - print(f"🗑️ 已删除临时 GIF 文件: {temp_gif_path}") + logger.info(f"🗑️ 已删除临时 GIF 文件: {temp_gif_path}") # 3. 上传所有结果到 MinIO - # 上传原始 MP4 - self.upload_stream_to_minio(mp4_bytes, MP4_OBJECT, "video/mp4") + try: + # 上传原始 MP4 + self.upload_stream_to_minio(mp4_bytes, MP4_OBJECT, "video/mp4") - # 上传生成的 GIF - self.upload_stream_to_minio(gif_bytes, GIF_OBJECT, "image/gif") + # 上传生成的 GIF + self.upload_stream_to_minio(gif_bytes, GIF_OBJECT, "image/gif") - # 上传第一帧图片 - self.upload_stream_to_minio(frame_bytes, FRAME_OBJECT, "image/jpeg") + # 上传第一帧图片 + self.upload_stream_to_minio(frame_bytes, FRAME_OBJECT, "image/jpeg") - return "\n🎉 所有任务完成!" + self.pose_transform_data = {'tasks_id': self.tasks_id, 'status': 'SUCCESS', 'message': "success", 'gif_url': f'aida-users/{GIF_OBJECT}', 'video_url': f'aida-users/{MP4_OBJECT}', 'image_url': f'aida-users/{FRAME_OBJECT}'} + + # 推送消息 + # if not DEBUG: + publish_status(json.dumps(self.pose_transform_data), PS_RABBITMQ_QUEUES) + logger.info( + f" [x] Sent to: {PS_RABBITMQ_QUEUES} data:@@@@ {json.dumps(self.pose_transform_data, indent=4)}") + + return "\n🎉 所有任务完成!" + + except Exception as e: + logger.error(e) + return None # --- 辅助函数:提交任务到队列 --- def queue_prompt(self, prompt, client_id): @@ -591,32 +598,21 @@ class ComfyUIServerI2V: # 提交任务到 /prompt 端点 response = requests.post(f"http://{COMFYUI_SERVER_ADDRESS}/prompt", data=data) - print(f"-------------{response.text}") - print(f"------------{client_id}") + # print(f"-------------{response.text}") + # print(f"------------{client_id}") if response.status_code == 200: return response.json() else: - print(f"提交任务失败,状态码: {response.status_code}") - print(response.text) - return None - - def get_history(self, prompt_id): - """通过 prompt_id 获取任务的历史记录和输出。""" - # 查询 /history/{prompt_id} 端点 - response = requests.get(f"http://{COMFYUI_SERVER_ADDRESS}/history/{prompt_id}") - - if response.status_code == 200: - return response.json() - else: - print(f"获取历史记录失败,状态码: {response.status_code}") + logger.warning(f"提交任务失败,状态码: {response.status_code}") + logger.warning(response.text) return None def poll_history(self, prompt_id, interval_seconds=5): """步骤 2: 轮询 /history/{prompt_id} 检查任务是否完成""" url = f"http://{COMFYUI_SERVER_ADDRESS}/history/{prompt_id}" - print(f"⏳ 开始轮询状态 (间隔 {interval_seconds} 秒)...") + logger.info(f"⏳ 开始轮询状态 (间隔 {interval_seconds} 秒)...") while True: time.sleep(interval_seconds) @@ -629,14 +625,14 @@ class ComfyUIServerI2V: # ComfyUI 返回的历史记录结构是 {prompt_id: {outputs: ...}} if prompt_id in history_data: - print("🎉 任务已完成!") + logger.info("🎉 任务已完成!") return history_data[prompt_id]['outputs'] - print("⏳ 任务仍在执行或等待中...") + logger.info("⏳ 任务仍在执行或等待中...") except requests.exceptions.RequestException as e: # 处理可能的连接错误,但通常不会在内部轮询中发生 - print(f"⚠️ 轮询时发生错误: {e}") + logger.info(f"⚠️ 轮询时发生错误: {e}") pass def get_comfyui_video_bytes(self, filename: str, subfolder: str, file_type: str = "output"): @@ -658,7 +654,7 @@ class ComfyUIServerI2V: "type": file_type } - print(f"📡 正在从 ComfyUI 下载视频: {filename}") + logger.info(f"📡 正在从 ComfyUI 下载视频: {filename}") try: # 使用 requests.get 下载文件 response = requests.get(url, params=params, stream=True) @@ -668,12 +664,12 @@ class ComfyUIServerI2V: return response.content except requests.exceptions.RequestException as e: - print(f"❌ 从 ComfyUI 获取视频失败: {e}") + logger.error(f"❌ 从 ComfyUI 获取视频失败: {e}") return None def upload_stream_to_minio(self, video_bytes: bytes, object_name: str, content_type: str): """从内存流上传数据到 MinIO。""" - print(f"☁️ 正在上传对象到 MinIO: {object_name}") + logger.info(f"☁️ 正在上传对象到 MinIO: {object_name}") try: data_stream = io.BytesIO(video_bytes) @@ -685,10 +681,10 @@ class ComfyUIServerI2V: length=len(video_bytes), content_type=content_type ) - print(f"✅ MinIO 上传成功: {result.object_name}") + logger.info(f"✅ MinIO 上传成功: {result.object_name}") return True except S3Error as e: - print(f"❌ MinIO 上传失败: {e}") + logger.error(f"❌ MinIO 上传失败: {e}") return False From 98468ea7aa9b3b5f19c8acc970f6744cd871c635 Mon Sep 17 00:00:00 2001 From: zhh Date: Mon, 3 Nov 2025 16:39:42 +0800 Subject: [PATCH 04/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20=20=E6=96=B0=E5=A2=9Ewan2.2=20pose-transform?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=8E=A5=E5=8F=A3=EF=BC=8Ccomfyui-api?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89?= =?UTF-8?q?:=20docs=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:?= =?UTF-8?q?=20refactor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B5=8B=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/comfyui_I2V/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/service/comfyui_I2V/server.py b/app/service/comfyui_I2V/server.py index 7f98038..d2a9e72 100644 --- a/app/service/comfyui_I2V/server.py +++ b/app/service/comfyui_I2V/server.py @@ -579,8 +579,8 @@ class ComfyUIServerI2V: self.pose_transform_data = {'tasks_id': self.tasks_id, 'status': 'SUCCESS', 'message': "success", 'gif_url': f'aida-users/{GIF_OBJECT}', 'video_url': f'aida-users/{MP4_OBJECT}', 'image_url': f'aida-users/{FRAME_OBJECT}'} # 推送消息 - # if not DEBUG: - publish_status(json.dumps(self.pose_transform_data), PS_RABBITMQ_QUEUES) + if not DEBUG: + publish_status(json.dumps(self.pose_transform_data), PS_RABBITMQ_QUEUES) logger.info( f" [x] Sent to: {PS_RABBITMQ_QUEUES} data:@@@@ {json.dumps(self.pose_transform_data, indent=4)}") From 8ccbbe41b1a426753725bca04d8ff384ba79d554 Mon Sep 17 00:00:00 2001 From: zhh Date: Mon, 3 Nov 2025 17:37:33 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20=20=E6=96=B0=E5=A2=9Ewan2.2=20pose-transform?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E6=8E=A5=E5=8F=A3=EF=BC=8Ccomfyui-api?= =?UTF-8?q?=E5=BD=A2=E5=BC=8F=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89?= =?UTF-8?q?:=20docs=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:?= =?UTF-8?q?=20refactor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B5=8B=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/comfyui_I2V/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/service/comfyui_I2V/server.py b/app/service/comfyui_I2V/server.py index d2a9e72..58d4d1b 100644 --- a/app/service/comfyui_I2V/server.py +++ b/app/service/comfyui_I2V/server.py @@ -345,7 +345,7 @@ class ComfyUIServerI2V: in_memory_file, object_name = self.download_from_minio_in_memory() if in_memory_file and object_name: uploaded_filename = self.upload_in_memory_file_to_comfyui(in_memory_file, object_name) - + workflow_json['178']['inputs']['image'] = uploaded_filename # 1. 提交任务 prompt_response = self.queue_prompt(workflow_json, self.tasks_id) if not prompt_response: @@ -690,8 +690,8 @@ class ComfyUIServerI2V: if __name__ == '__main__': request_data = PoseTransformModel( - tasks_id="1515151123-89111", - image_url="aida-results/result_0000b606-1902-11ef-9424-0242ac180002.png", + tasks_id="12222515151123-89111", + image_url="aida-users/89/product_image/a6949500-2393-42ac-8723-440b5d5da2b2-0-89.png", pose_id="6" ) From 0e7ef80eedfcf26d880332633f82152789f87e1b Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:53:27 +0800 Subject: [PATCH 06/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20=20?= =?UTF-8?q?=E5=8D=B0=E8=8A=B1=E9=83=A8=E5=88=86=EF=BC=8C=E4=BF=AE=E6=94=B9?= =?UTF-8?q?no=5Fseg=5Fsketch=5Fprint=20=E4=B8=A2=E5=A4=B1overall=20?= =?UTF-8?q?=E5=8D=B0=E8=8A=B1=E9=97=AE=E9=A2=98=20docs=EF=BC=88=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:=20refactor=EF=BC=88?= =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=89:=20test(=E5=A2=9E=E5=8A=A0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/design_fast/pipeline/no_seg_print_painting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/service/design_fast/pipeline/no_seg_print_painting.py b/app/service/design_fast/pipeline/no_seg_print_painting.py index 029d580..dcceaba 100644 --- a/app/service/design_fast/pipeline/no_seg_print_painting.py +++ b/app/service/design_fast/pipeline/no_seg_print_painting.py @@ -142,7 +142,7 @@ class NoSegPrintPainting: print_mask = cv2.bitwise_and(result['mask'], cv2.cvtColor(mask_background, cv2.COLOR_BGR2GRAY)) img_fg = cv2.bitwise_or(print_background, print_background, mask=print_mask) three_channel_image = cv2.merge([cv2.bitwise_not(print_mask), cv2.bitwise_not(print_mask), cv2.bitwise_not(print_mask)]) - img_bg = cv2.bitwise_and(result['final_image'], three_channel_image) + img_bg = cv2.bitwise_and(result['no_seg_sketch_print'], three_channel_image) result['final_image'] = cv2.add(img_bg, img_fg) canvas = np.full_like(result['final_image'], 255) temp_bg = np.expand_dims(cv2.bitwise_not(result['mask']), axis=2).repeat(3, axis=2) From 85c486c3dcf884de93bd40bc03ad4eec7f718e8f Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:08:40 +0800 Subject: [PATCH 07/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20=20=E6=96=B0=E5=A2=9E=20=E9=AA=A8=E6=9E=B6/?= =?UTF-8?q?=E5=9B=BE=E7=89=87+prompt/=E9=A6=96=E5=B0=BE=E5=B8=A7+prompt=20?= =?UTF-8?q?->=20=E8=A7=86=E9=A2=91=E6=8E=A5=E5=8F=A3=20fix=EF=BC=88?= =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20docs=EF=BC=88=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:=20refactor=EF=BC=88?= =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=89:=20test(=E5=A2=9E=E5=8A=A0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api_pose_transform.py | 92 ++- app/schemas/comfyui_i2v.py | 23 + app/service/comfyui_I2V/flf2v_server.py | 639 ++++++++++++++++++ app/service/comfyui_I2V/i2v_server.py | 616 +++++++++++++++++ .../{server.py => pose2v_server.py} | 15 +- 5 files changed, 1367 insertions(+), 18 deletions(-) create mode 100644 app/schemas/comfyui_i2v.py create mode 100644 app/service/comfyui_I2V/flf2v_server.py create mode 100644 app/service/comfyui_I2V/i2v_server.py rename app/service/comfyui_I2V/{server.py => pose2v_server.py} (98%) diff --git a/app/api/api_pose_transform.py b/app/api/api_pose_transform.py index bc8dd33..534e296 100644 --- a/app/api/api_pose_transform.py +++ b/app/api/api_pose_transform.py @@ -5,9 +5,12 @@ import requests from fastapi import APIRouter, BackgroundTasks, HTTPException from app.core.config import COMFYUI_SERVER_ADDRESS +from app.schemas.comfyui_i2v import ComfyuiI2VModel, ComfyuiFLF2VModel from app.schemas.pose_transform import PoseTransformModel from app.schemas.response_template import ResponseModel -from app.service.comfyui_I2V.server import ComfyUIServerI2V +from app.service.comfyui_I2V.flf2v_server import ComfyUIServerFLF2V +from app.service.comfyui_I2V.i2v_server import ComfyUIServerI2V +from app.service.comfyui_I2V.pose2v_server import ComfyUIServerPose2V from app.service.generate_image.service_pose_transform import PoseTransformService, infer_cancel as pose_transform_infer_cancel router = APIRouter() @@ -52,8 +55,13 @@ def pose_transform_cancel(tasks_id: str): return ResponseModel(data=data['data']) -@router.post("/comfyui_pose_transform") -def comfyui_pose_transform(request_item: PoseTransformModel, background_tasks: BackgroundTasks): +""" +骨架 + 产品图 => 视频 +""" + + +@router.post("/comfyui_image_pose_2_video") +def comfyui_image_pose_2_video(request_item: PoseTransformModel, background_tasks: BackgroundTasks): """ 创建一个具有以下参数的请求体: - **tasks_id**: 任务id 用于取消生成任务和获取生成结果 @@ -69,19 +77,81 @@ def comfyui_pose_transform(request_item: PoseTransformModel, background_tasks: B } """ try: - logger.info(f"pose_transform request item is : @@@@@@:{json.dumps(request_item.dict())}") - service = ComfyUIServerI2V(request_item) + logger.info(f"image_pose_2_video request item is : @@@@@@:{json.dumps(request_item.dict())}") + service = ComfyUIServerPose2V(request_item) background_tasks.add_task(service.get_result) except Exception as e: - logger.warning(f"pose_transform Run Exception @@@@@@:{e}") + logger.warning(f"image_pose_2_video Run Exception @@@@@@:{e}") raise HTTPException(status_code=404, detail=str(e)) return ResponseModel() -@router.get("/comfyui_pose_transform_cancel/{tasks_id}") -def pose_transform_cancel(tasks_id: str): +""" +产品图 + 文 => 视频 +""" + + +@router.post("/comfyui_image_2_video") +def comfyui_image_2_video(request_item: ComfyuiI2VModel, background_tasks: BackgroundTasks): + """ + 创建一个具有以下参数的请求体: + - **tasks_id**: 任务id 用于取消生成任务和获取生成结果 + - **image_url**: 被生成图片的S3或minio url地址 + - **prompt**: 动作表述 + + 示例参数: + { + "tasks_id": "12222515151123-89111", + "image_url": "aida-users/89/product_image/a6949500-2393-42ac-8723-440b5d5da2b2-0-89.png", + "prompt": "Model executing a series of poses, dynamic camera movement alternating between detailed close-ups and full shots." + } + """ try: - logger.info(f"pose_transform_cancel request item is : @@@@@@:{tasks_id}") + logger.info(f"image_2_video request item is : @@@@@@:{json.dumps(request_item.dict())}") + service = ComfyUIServerI2V(request_item) + background_tasks.add_task(service.get_result) + except Exception as e: + logger.warning(f"image_2_video Run Exception @@@@@@:{e}") + raise HTTPException(status_code=404, detail=str(e)) + return ResponseModel() + + +""" +首尾帧 + 文 => 视频 +""" + + +@router.post("/comfyui_flf_2_video") +def comfyui_flf_2_video(request_item: ComfyuiFLF2VModel, background_tasks: BackgroundTasks): + """ + 创建一个具有以下参数的请求体: + - **tasks_id**: 任务id 用于取消生成任务和获取生成结果 + - **start_image_url**: 首帧 + - **end_image_url**: 尾帧 + - **prompt**: 动作描述 + + 示例参数: + { + "tasks_id": "202511051619-89111", + "start_image_url": "test/start.png", + "end_image_url": "test/end.png", + "prompt": "Model executing a series of poses, dynamic camera movement alternating between detailed close-ups and full shots." + } + """ + try: + logger.info(f"flf_2_video request item is : @@@@@@:{json.dumps(request_item.dict())}") + service = ComfyUIServerFLF2V(request_item) + background_tasks.add_task(service.get_result) + except Exception as e: + logger.warning(f"flf_2_video Run Exception @@@@@@:{e}") + raise HTTPException(status_code=404, detail=str(e)) + return ResponseModel() + + +@router.get("/comfyui_i_2_video_cancel/{tasks_id}") +def comfyui_i_2_video_cancel(tasks_id: str): + try: + logger.info(f"comfyui_i_2_video_cancel request item is : @@@@@@:{tasks_id}") response = requests.post( f"http://{COMFYUI_SERVER_ADDRESS}/interrupt", json={"prompt_id": tasks_id} @@ -91,8 +161,8 @@ def pose_transform_cancel(tasks_id: str): data['data']['message'] = "任务已成功中断" else: data['data']['message'] = f"中断失败:{response.text}" - logger.info(f"pose_transform_cancel response @@@@@@:{data}") + logger.info(f"comfyui_i_2_video_cancel response @@@@@@:{data}") except Exception as e: - logger.warning(f"pose_transform_cancel Run Exception @@@@@@:{e}") + logger.warning(f"comfyui_i_2_video_cancel Run Exception @@@@@@:{e}") raise HTTPException(status_code=404, detail=str(e)) return ResponseModel(data=data['data']) diff --git a/app/schemas/comfyui_i2v.py b/app/schemas/comfyui_i2v.py new file mode 100644 index 0000000..4d96a8f --- /dev/null +++ b/app/schemas/comfyui_i2v.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel + + +class ComfyuiPose2VModel(BaseModel): + # 骨架生成视频 + image_url: str + tasks_id: str + pose_id: str + + +class ComfyuiI2VModel(BaseModel): + # 图生视频 + image_url: str + prompt: str + tasks_id: str + + +class ComfyuiFLF2VModel(BaseModel): + # 首尾帧生视频 + start_image_url: str + end_image_url: str + prompt: str + tasks_id: str diff --git a/app/service/comfyui_I2V/flf2v_server.py b/app/service/comfyui_I2V/flf2v_server.py new file mode 100644 index 0000000..448f9ad --- /dev/null +++ b/app/service/comfyui_I2V/flf2v_server.py @@ -0,0 +1,639 @@ +import io +import json +import logging +import os +import random +import tempfile +import time +import uuid + +import requests +from PIL import Image +from minio import Minio, S3Error +from moviepy.video.io.VideoFileClip import VideoFileClip + +from app.core.config import MINIO_URL, MINIO_ACCESS, MINIO_SECRET, MINIO_SECURE, COMFYUI_SERVER_ADDRESS, PS_RABBITMQ_QUEUES, DEBUG +from app.schemas.comfyui_i2v import ComfyuiFLF2VModel +from app.service.generate_image.utils.mq import publish_status + +logger = logging.getLogger() + +# 首尾帧 + 文字 = 视频 工作流 +workflow_json = { + "6": { + "inputs": { + "text": "A bearded man with red facial hair wearing a yellow straw hat and dark coat in Van Gogh's self-portrait style, slowly and continuously transforms into a space astronaut. The transformation flows like liquid paint - his beard fades away strand by strand, the yellow hat melts and reforms smoothly into a silver space helmet, dark coat gradually lightens and restructures into a white spacesuit. The background swirling brushstrokes slowly organize and clarify into realistic stars and space, with Earth appearing gradually in the distance. Every change happens in seamless waves, maintaining visual continuity throughout the metamorphosis.\n\nConsistent soft lighting throughout, medium close-up maintaining same framing, central composition stays fixed, gentle color temperature shift from warm to cool, gradual contrast increase, smooth style transition from painterly to photorealistic. Static camera with subtle slow zoom, emphasizing the flowing transformation process without abrupt changes.", + "clip": [ + "38", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Positive Prompt)" + } + }, + "7": { + "inputs": { + "text": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走", + "clip": [ + "38", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Negative Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "58", + 0 + ], + "vae": [ + "39", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "37": { + "inputs": { + "unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "38": { + "inputs": { + "clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "type": "wan", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "39": { + "inputs": { + "vae_name": "wan_2.1_vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "加载VAE" + } + }, + "54": { + "inputs": { + "shift": 5, + "model": [ + "91", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "采样算法(SD3)" + } + }, + "55": { + "inputs": { + "shift": 5, + "model": [ + "92", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "采样算法(SD3)" + } + }, + "56": { + "inputs": { + "unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "57": { + "inputs": { + "add_noise": "enable", + "noise_seed": 984937593540091, + "steps": 4, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "start_at_step": 0, + "end_at_step": 2, + "return_with_leftover_noise": "enable", + "model": [ + "54", + 0 + ], + "positive": [ + "67", + 0 + ], + "negative": [ + "67", + 1 + ], + "latent_image": [ + "67", + 2 + ] + }, + "class_type": "KSamplerAdvanced", + "_meta": { + "title": "K采样器(高级)" + } + }, + "58": { + "inputs": { + "add_noise": "disable", + "noise_seed": 0, + "steps": 4, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "start_at_step": 2, + "end_at_step": 10000, + "return_with_leftover_noise": "disable", + "model": [ + "55", + 0 + ], + "positive": [ + "67", + 0 + ], + "negative": [ + "67", + 1 + ], + "latent_image": [ + "57", + 0 + ] + }, + "class_type": "KSamplerAdvanced", + "_meta": { + "title": "K采样器(高级)" + } + }, + "60": { + "inputs": { + "fps": 16, + "images": [ + "8", + 0 + ] + }, + "class_type": "CreateVideo", + "_meta": { + "title": "创建视频" + } + }, + "61": { + "inputs": { + "filename_prefix": "video/ComfyUI", + "format": "auto", + "codec": "auto", + "video": [ + "60", + 0 + ] + }, + "class_type": "SaveVideo", + "_meta": { + "title": "保存视频" + } + }, + "62": { + "inputs": { + "image": "video_wan2_2_14B_flf2v_start_image.png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载end图像" + } + }, + "67": { + "inputs": { + "width": 640, + "height": 640, + "length": 81, + "batch_size": 1, + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "vae": [ + "39", + 0 + ], + "start_image": [ + "68", + 0 + ], + "end_image": [ + "62", + 0 + ] + }, + "class_type": "WanFirstLastFrameToVideo", + "_meta": { + "title": "WanFirstLastFrameToVideo" + } + }, + "68": { + "inputs": { + "image": "video_wan2_2_14B_flf2v_end_image.png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载start图像" + } + }, + "91": { + "inputs": { + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", + "strength_model": 1, + "model": [ + "37", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + }, + "92": { + "inputs": { + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", + "strength_model": 1, + "model": [ + "56", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + } +} + + +class ComfyUIServerFLF2V: + def __init__(self, request_data): + self.start_image_url = request_data.start_image_url + self.end_image_url = request_data.end_image_url + self.prompt = request_data.prompt + self.tasks_id = request_data.tasks_id + self.user_id = self.tasks_id[self.tasks_id.rfind('-') + 1:] + self.server_status_data = {'tasks_id': self.tasks_id, 'status': 'PENDING', 'message': "pending", 'gif_url': '', 'video_url': '', 'image_url': ''} + self.minio_client = Minio(MINIO_URL, access_key=MINIO_ACCESS, secret_key=MINIO_SECRET, secure=MINIO_SECURE) + + def get_result(self): + workflow_json['6']['inputs']['text'] = self.prompt + workflow_json['57']['inputs']["noise_seed"] = random.randint(0, 10 ** 18) + + if self.start_image_url: + # 下载图片 上传 comfyui server + # TODO 设置视频宽度为480,高度自适应 + workflow_json['67']['inputs']["width"] = 480 + workflow_json['67']['inputs']["height"] = 848 + if self.start_image_url: + start_in_memory_file, start_object_name = self.download_from_minio_in_memory(self.start_image_url) + # 上传图片到comfyui server + filename = self.upload_in_memory_file_to_comfyui(start_in_memory_file, start_object_name) + workflow_json['68']['inputs']['image'] = filename + else: + assert "start_image_url is None" + + if self.end_image_url: + end_in_memory_file, end_object_name = self.download_from_minio_in_memory(self.end_image_url) + # 上传图片到comfyui server + filename = self.upload_in_memory_file_to_comfyui(end_in_memory_file, end_object_name) + workflow_json['62']['inputs']['image'] = filename + else: + assert "end_image_url is None" + + # 1. 提交任务 + prompt_response = self.queue_prompt(workflow_json, self.tasks_id) + if not prompt_response: + return + + prompt_id = prompt_response.get("prompt_id") + logger.info(f" 任务已提交,Prompt ID: {prompt_id}") + outputs = self.poll_history(prompt_id) + file_list = {} + for node_id, node_output in outputs.items(): + # 检查当前节点输出中是否包含 'images' 列表 + if 'images' in node_output and isinstance(node_output['images'], list): + # 'images' 列表中的每个元素都是一个文件对象 + for file_info in node_output['images']: + # 确保关键字段存在 + if all(key in file_info for key in ['filename', 'subfolder', 'type']): + file_list = { + 'filename': file_info['filename'], + 'subfolder': file_info['subfolder'], + 'type': file_info['type'] + } + logger.info(file_list) + return self.process_and_upload_comfyui_video(filename=file_list['filename'], subfolder=file_list['subfolder'], prompt_id=prompt_response['prompt_id']), prompt_id + + def download_from_minio_in_memory(self, image_url): + bucket = image_url.split('/')[0] + object_name = image_url[image_url.find('/') + 1:] + + try: + # get_object 返回一个 ResponseStream 对象 + response_stream = self.minio_client.get_object( + bucket, + object_name, + ) + + # 读取整个流到内存 (BytesIO),避免写入本地文件 + image_bytes = response_stream.read() + + response_stream.close() + response_stream.release_conn() + + in_memory_file = io.BytesIO(image_bytes) + + # print(f"✅ 图片已下载到内存 ({len(image_bytes)} 字节)。") + return in_memory_file, object_name.rsplit('/')[-1] + + except S3Error as e: + logger.error(f"❌ MinIO S3 错误 (例如,对象不存在): {e}") + return None, None + except Exception as e: + logger.error(f"❌ MinIO 下载过程中发生未知错误: {e}") + return None, None + + def upload_in_memory_file_to_comfyui(self, in_memory_file, filename): + upload_url = f"http://{COMFYUI_SERVER_ADDRESS}/upload/image" + + data = { + "overwrite": "true", + "type": "input" + } + + # 构建 multipart/form-data: (文件名, 内存文件对象, MIME 类型) + # MIME 类型可以根据实际图片类型修改,这里使用常见的 png/jpeg + mime_type = 'image/png' if filename.lower().endswith('.png') else 'image/jpeg' + + files = { + 'image': (filename, in_memory_file, mime_type) + } + + # print(f"⬆️ 正在上传图片 ({filename}) 到 ComfyUI...") + try: + comfyui_response = requests.post(upload_url, data=data, files=files) + comfyui_response.raise_for_status() + + result = comfyui_response.json() + uploaded_name = result.get('name') + + # print(f"🎉 ComfyUI 上传成功! 服务器文件名: {uploaded_name}") + return uploaded_name + + except requests.exceptions.RequestException as e: + logger.error(f"❌ ComfyUI 上传失败: {e}") + logger.error(f" 响应内容: {comfyui_response.text}") + return None + + def process_and_upload_comfyui_video(self, filename: str, subfolder: str, prompt_id: str, ): + """ + 完整的自动化流程:获取 ComfyUI 视频 -> 转换 GIF 并提取帧 -> 上传所有结果到 MinIO。 + """ + # 1. 从 ComfyUI 获取视频二进制数据 + mp4_bytes = self.get_comfyui_video_bytes(filename, subfolder) + if not mp4_bytes: + return + + # 2. 准备进行视频处理 + # moviepy 不支持直接使用 bytes,需要将 bytes 写入一个 BytesIO 或临时文件 + # 为了避免写磁盘,我们将使用 BytesIO,但 MoviePy 内部依赖 FFmpeg,有时需要一个可寻址的本地文件路径。 + # 最可靠且避免写本地的方案是在内存中操作,然后将结果上传。 + + # ⚠️ 关键点:将 mp4_bytes 写入 BytesIO 以模拟文件,供 moviepy 读取 + + # 定义输出对象名 + + output_base_name = uuid.uuid4().hex + MP4_OBJECT = f"{self.user_id}/pose_transform_video/{prompt_id}/{output_base_name}.mp4" + GIF_OBJECT = f"{self.user_id}/pose_transform_gif/{prompt_id}/{output_base_name}.gif" + FRAME_OBJECT = f"{self.user_id}/pose_transform_first_img/{prompt_id}/{output_base_name}_frame.jpg" + + # --- 视频处理和帧提取 --- + try: + # 1. 创建一个临时的 MP4 文件路径 + # delete=False 确保文件在关闭后仍然存在,直到我们手动删除 + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_file: + tmp_file.write(mp4_bytes) # 将内存数据写入磁盘 + temp_mp4_path = tmp_file.name # 记录文件路径 + + # print(f"临时文件已写入: {temp_mp4_path}") + + # 2. 使用 moviepy 打开临时文件 (传入文件路径字符串) + clip = VideoFileClip(temp_mp4_path) + + # --- 在这里进行所有的视频处理和提取操作 --- + + # 提取第一帧 (保持原尺寸) + frame_array = clip.get_frame(t=0.0) + image = Image.fromarray(frame_array) + + frame_stream = io.BytesIO() + image.save(frame_stream, 'JPEG') + frame_bytes = frame_stream.getvalue() + + logger.info("✅ 成功提取第一帧图片。") + + # 视频转 GIF (使用另一个临时文件来保存 GIF) + temp_gif_path = "" + with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmp_file: + temp_gif_path = tmp_file.name + + target_fps = int(round(clip.fps)) if clip.fps else 24 + clip.write_gif(temp_gif_path, fps=target_fps) + + with open(temp_gif_path, 'rb') as f: + gif_bytes = f.read() + + logger.info("✅ 成功生成 GIF。") + + # 返回结果 (例如: 上传到 MinIO) + # return mp4_bytes, gif_bytes, frame_bytes + + # ----------------------------------------------- + + except Exception as e: + logger.error(f"❌ 视频处理或文件操作失败: {e}") + # 在失败时,也尝试清理文件 + + finally: + # 3. 清理临时文件 (非常重要!) + if os.path.exists(temp_mp4_path): + os.remove(temp_mp4_path) + logger.info(f"🗑️ 已删除临时 MP4 文件: {temp_mp4_path}") + + if 'temp_gif_path' in locals() and os.path.exists(temp_gif_path): + os.remove(temp_gif_path) + logger.info(f"🗑️ 已删除临时 GIF 文件: {temp_gif_path}") + + # 3. 上传所有结果到 MinIO + + try: + # 上传原始 MP4 + self.upload_stream_to_minio(mp4_bytes, MP4_OBJECT, "video/mp4") + + # 上传生成的 GIF + self.upload_stream_to_minio(gif_bytes, GIF_OBJECT, "image/gif") + + # 上传第一帧图片 + self.upload_stream_to_minio(frame_bytes, FRAME_OBJECT, "image/jpeg") + + self.pose_transform_data = {'tasks_id': self.tasks_id, 'status': 'SUCCESS', 'message': "success", 'gif_url': f'aida-users/{GIF_OBJECT}', 'video_url': f'aida-users/{MP4_OBJECT}', 'image_url': f'aida-users/{FRAME_OBJECT}'} + + # 推送消息 + if not DEBUG: + publish_status(json.dumps(self.pose_transform_data), PS_RABBITMQ_QUEUES) + logger.info( + f" [x] Sent to: {PS_RABBITMQ_QUEUES} data:@@@@ {json.dumps(self.pose_transform_data, indent=4)}") + + return "\n🎉 所有任务完成!" + + except Exception as e: + logger.error(e) + return None + + # --- 辅助函数:提交任务到队列 --- + def queue_prompt(self, prompt, client_id): + """向 ComfyUI 提交工作流提示。""" + p = {"prompt": prompt, "client_id": client_id, "prompt_id": client_id} + data = json.dumps(p).encode('utf-8') + + # 提交任务到 /prompt 端点 + response = requests.post(f"http://{COMFYUI_SERVER_ADDRESS}/prompt", data=data) + # print(f"-------------{response.text}") + # print(f"------------{client_id}") + + if response.status_code == 200: + return response.json() + else: + logger.warning(f"提交任务失败,状态码: {response.status_code}") + logger.warning(response.text) + return None + + def poll_history(self, prompt_id, interval_seconds=5): + """步骤 2: 轮询 /history/{prompt_id} 检查任务是否完成""" + url = f"http://{COMFYUI_SERVER_ADDRESS}/history/{prompt_id}" + + logger.info(f"⏳ 开始轮询状态 (间隔 {interval_seconds} 秒)...") + + while True: + time.sleep(interval_seconds) + + try: + response = requests.get(url) + # 任务未完成时,ComfyUI可能会返回404或空响应,我们只关注成功响应 + if response.status_code == 200: + history_data = response.json() + + # ComfyUI 返回的历史记录结构是 {prompt_id: {outputs: ...}} + if prompt_id in history_data: + logger.info("🎉 任务已完成!") + return history_data[prompt_id]['outputs'] + + logger.info("⏳ 任务仍在执行或等待中...") + + except requests.exceptions.RequestException as e: + # 处理可能的连接错误,但通常不会在内部轮询中发生 + logger.info(f"⚠️ 轮询时发生错误: {e}") + pass + + def get_comfyui_video_bytes(self, filename: str, subfolder: str, file_type: str = "output"): + """ + 从 ComfyUI 的 /view 端点获取视频文件的二进制数据。 + + 参数: + - filename: 视频文件名 (例如: 'ComfyUI_00002_.mp4') + - subfolder: 存储子文件夹 (例如: 'ComfyUI_2025-10-31') + - file_type: 文件类型 (通常是 'output') + + 返回: + - 视频文件的二进制内容 (bytes) 或 None。 + """ + url = f"http://{COMFYUI_SERVER_ADDRESS}/view" + params = { + "filename": filename, + "subfolder": subfolder, + "type": file_type + } + + logger.info(f"📡 正在从 ComfyUI 下载视频: {filename}") + try: + # 使用 requests.get 下载文件 + response = requests.get(url, params=params, stream=True) + response.raise_for_status() # 检查 HTTP 错误 + + # 返回文件的完整二进制内容 + return response.content + + except requests.exceptions.RequestException as e: + logger.error(f"❌ 从 ComfyUI 获取视频失败: {e}") + return None + + def upload_stream_to_minio(self, video_bytes: bytes, object_name: str, content_type: str): + """从内存流上传数据到 MinIO。""" + logger.info(f"☁️ 正在上传对象到 MinIO: {object_name}") + try: + + data_stream = io.BytesIO(video_bytes) + + result = self.minio_client.put_object( + bucket_name='aida-users', + object_name=object_name, + data=data_stream, + length=len(video_bytes), + content_type=content_type + ) + logger.info(f"✅ MinIO 上传成功: {result.object_name}") + return True + except S3Error as e: + logger.error(f"❌ MinIO 上传失败: {e}") + return False + + +if __name__ == '__main__': + request_data = ComfyuiFLF2VModel( + tasks_id="202511051619-89111", + start_image_url="test/start.png", + end_image_url="test/end.png", + prompt="Model executing a series of poses, dynamic camera movement alternating between detailed close-ups and full shots." + ) + + server = ComfyUIServerFLF2V(request_data) + print(server.get_result()) diff --git a/app/service/comfyui_I2V/i2v_server.py b/app/service/comfyui_I2V/i2v_server.py new file mode 100644 index 0000000..bcb34c4 --- /dev/null +++ b/app/service/comfyui_I2V/i2v_server.py @@ -0,0 +1,616 @@ +import io +import json +import logging +import os +import random +import tempfile +import time +import uuid + +import requests +from PIL import Image +from minio import Minio, S3Error +from moviepy.video.io.VideoFileClip import VideoFileClip + +from app.core.config import MINIO_URL, MINIO_ACCESS, MINIO_SECRET, MINIO_SECURE, COMFYUI_SERVER_ADDRESS, PS_RABBITMQ_QUEUES, DEBUG +from app.schemas.comfyui_i2v import ComfyuiPose2VModel, ComfyuiI2VModel +from app.service.generate_image.utils.mq import publish_status + +logger = logging.getLogger() + +# 图 + 文字 = 视频 工作流 +workflow_json = { + "84": { + "inputs": { + "clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "type": "wan", + "device": "default" + }, + "class_type": "CLIPLoader", + "_meta": { + "title": "加载CLIP" + } + }, + "85": { + "inputs": { + "add_noise": "disable", + "noise_seed": 0, + "steps": 4, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "start_at_step": 2, + "end_at_step": 4, + "return_with_leftover_noise": "disable", + "model": [ + "103", + 0 + ], + "positive": [ + "98", + 0 + ], + "negative": [ + "98", + 1 + ], + "latent_image": [ + "86", + 0 + ] + }, + "class_type": "KSamplerAdvanced", + "_meta": { + "title": "K采样器(高级)" + } + }, + "86": { + "inputs": { + "add_noise": "enable", + "noise_seed": 823962998672127, + "steps": 4, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "start_at_step": 0, + "end_at_step": 2, + "return_with_leftover_noise": "enable", + "model": [ + "104", + 0 + ], + "positive": [ + "98", + 0 + ], + "negative": [ + "98", + 1 + ], + "latent_image": [ + "98", + 2 + ] + }, + "class_type": "KSamplerAdvanced", + "_meta": { + "title": "K采样器(高级)" + } + }, + "87": { + "inputs": { + "samples": [ + "85", + 0 + ], + "vae": [ + "90", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE解码" + } + }, + "89": { + "inputs": { + "text": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走", + "clip": [ + "84", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Negative Prompt)" + } + }, + "90": { + "inputs": { + "vae_name": "wan_2.1_vae.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "加载VAE" + } + }, + "93": { + "inputs": { + "text": "Model executing a series of poses, dynamic camera movement alternating between detailed close-ups and full shots.", + "clip": [ + "84", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Positive Prompt)" + } + }, + "94": { + "inputs": { + "fps": 16, + "images": [ + "87", + 0 + ] + }, + "class_type": "CreateVideo", + "_meta": { + "title": "创建视频" + } + }, + "95": { + "inputs": { + "unet_name": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "96": { + "inputs": { + "unet_name": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "UNet加载器" + } + }, + "97": { + "inputs": { + "image": "start (1).png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "加载图像" + } + }, + "98": { + "inputs": { + "width": 480, + "height": 848, + "length": 81, + "batch_size": 1, + "positive": [ + "93", + 0 + ], + "negative": [ + "89", + 0 + ], + "vae": [ + "90", + 0 + ], + "start_image": [ + "97", + 0 + ] + }, + "class_type": "WanImageToVideo", + "_meta": { + "title": "Wan图像到视频" + } + }, + "101": { + "inputs": { + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", + "strength_model": 1.0000000000000002, + "model": [ + "95", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + }, + "102": { + "inputs": { + "lora_name": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", + "strength_model": 1.0000000000000002, + "model": [ + "96", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "LoRA加载器(仅模型)" + } + }, + "103": { + "inputs": { + "shift": 5.000000000000001, + "model": [ + "102", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "采样算法(SD3)" + } + }, + "104": { + "inputs": { + "shift": 5.000000000000001, + "model": [ + "101", + 0 + ] + }, + "class_type": "ModelSamplingSD3", + "_meta": { + "title": "采样算法(SD3)" + } + }, + "108": { + "inputs": { + "filename_prefix": "video/ComfyUI", + "format": "auto", + "codec": "auto", + "video-preview": "", + "video": [ + "94", + 0 + ] + }, + "class_type": "SaveVideo", + "_meta": { + "title": "保存视频" + } + } +} + + +class ComfyUIServerI2V: + def __init__(self, request_data): + self.image_url = request_data.image_url + self.prompt = request_data.prompt + + self.tasks_id = request_data.tasks_id + self.user_id = self.tasks_id[self.tasks_id.rfind('-') + 1:] + self.server_status_data = {'tasks_id': self.tasks_id, 'status': 'PENDING', 'message': "pending", 'gif_url': '', 'video_url': '', 'image_url': ''} + self.minio_client = Minio(MINIO_URL, access_key=MINIO_ACCESS, secret_key=MINIO_SECRET, secure=MINIO_SECURE) + + def get_result(self): + workflow_json['93']['inputs']['text'] = self.prompt + workflow_json['86']['inputs']["noise_seed"] = random.randint(0, 10 ** 18) + + if self.image_url: + # 下载图片 上传 comfyui server + in_memory_file, object_name = self.download_from_minio_in_memory(self.image_url) + # TODO 设置视频宽度为480,高度自适应 + workflow_json['98']['inputs']["width"] = 480 + workflow_json['98']['inputs']["height"] = 848 + if in_memory_file and object_name: + # 上传图片到comfyui server + filename = self.upload_in_memory_file_to_comfyui(in_memory_file, object_name) + workflow_json['97']['inputs']['image'] = filename + + # 1. 提交任务 + prompt_response = self.queue_prompt(workflow_json, self.tasks_id) + if not prompt_response: + return + prompt_id = prompt_response.get("prompt_id") + logger.info(f" 任务已提交,Prompt ID: {prompt_id}") + outputs = self.poll_history(prompt_id) + file_list = {} + for node_id, node_output in outputs.items(): + # 检查当前节点输出中是否包含 'images' 列表 + if 'images' in node_output and isinstance(node_output['images'], list): + + # 'images' 列表中的每个元素都是一个文件对象 + for file_info in node_output['images']: + # 确保关键字段存在 + if all(key in file_info for key in ['filename', 'subfolder', 'type']): + file_list = { + 'filename': file_info['filename'], + 'subfolder': file_info['subfolder'], + 'type': file_info['type'] + } + logger.info(file_list) + return self.process_and_upload_comfyui_video(filename=file_list['filename'], subfolder=file_list['subfolder'], prompt_id=prompt_response['prompt_id']), prompt_id + + def download_from_minio_in_memory(self, image_url): + bucket = image_url.split('/')[0] + object_name = image_url[image_url.find('/') + 1:] + + try: + # get_object 返回一个 ResponseStream 对象 + response_stream = self.minio_client.get_object( + bucket, + object_name, + ) + + # 读取整个流到内存 (BytesIO),避免写入本地文件 + image_bytes = response_stream.read() + + response_stream.close() + response_stream.release_conn() + + in_memory_file = io.BytesIO(image_bytes) + + # print(f"✅ 图片已下载到内存 ({len(image_bytes)} 字节)。") + return in_memory_file, object_name.rsplit('/')[-1] + + except S3Error as e: + logger.error(f"❌ MinIO S3 错误 (例如,对象不存在): {e}") + return None, None + except Exception as e: + logger.error(f"❌ MinIO 下载过程中发生未知错误: {e}") + return None, None + + def upload_in_memory_file_to_comfyui(self, in_memory_file, filename): + upload_url = f"http://{COMFYUI_SERVER_ADDRESS}/upload/image" + + data = { + "overwrite": "true", + "type": "input" + } + + # 构建 multipart/form-data: (文件名, 内存文件对象, MIME 类型) + # MIME 类型可以根据实际图片类型修改,这里使用常见的 png/jpeg + mime_type = 'image/png' if filename.lower().endswith('.png') else 'image/jpeg' + + files = { + 'image': (filename, in_memory_file, mime_type) + } + + # print(f"⬆️ 正在上传图片 ({filename}) 到 ComfyUI...") + try: + comfyui_response = requests.post(upload_url, data=data, files=files) + comfyui_response.raise_for_status() + + result = comfyui_response.json() + uploaded_name = result.get('name') + + # print(f"🎉 ComfyUI 上传成功! 服务器文件名: {uploaded_name}") + return uploaded_name + + except requests.exceptions.RequestException as e: + logger.error(f"❌ ComfyUI 上传失败: {e}") + logger.error(f" 响应内容: {comfyui_response.text}") + return None + + def process_and_upload_comfyui_video(self, filename: str, subfolder: str, prompt_id: str, ): + """ + 完整的自动化流程:获取 ComfyUI 视频 -> 转换 GIF 并提取帧 -> 上传所有结果到 MinIO。 + """ + # 1. 从 ComfyUI 获取视频二进制数据 + mp4_bytes = self.get_comfyui_video_bytes(filename, subfolder) + if not mp4_bytes: + return + + # 2. 准备进行视频处理 + # moviepy 不支持直接使用 bytes,需要将 bytes 写入一个 BytesIO 或临时文件 + # 为了避免写磁盘,我们将使用 BytesIO,但 MoviePy 内部依赖 FFmpeg,有时需要一个可寻址的本地文件路径。 + # 最可靠且避免写本地的方案是在内存中操作,然后将结果上传。 + + # ⚠️ 关键点:将 mp4_bytes 写入 BytesIO 以模拟文件,供 moviepy 读取 + + # 定义输出对象名 + + output_base_name = uuid.uuid4().hex + MP4_OBJECT = f"{self.user_id}/pose_transform_video/{prompt_id}/{output_base_name}.mp4" + GIF_OBJECT = f"{self.user_id}/pose_transform_gif/{prompt_id}/{output_base_name}.gif" + FRAME_OBJECT = f"{self.user_id}/pose_transform_first_img/{prompt_id}/{output_base_name}_frame.jpg" + + # --- 视频处理和帧提取 --- + try: + # 1. 创建一个临时的 MP4 文件路径 + # delete=False 确保文件在关闭后仍然存在,直到我们手动删除 + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_file: + tmp_file.write(mp4_bytes) # 将内存数据写入磁盘 + temp_mp4_path = tmp_file.name # 记录文件路径 + + # print(f"临时文件已写入: {temp_mp4_path}") + + # 2. 使用 moviepy 打开临时文件 (传入文件路径字符串) + clip = VideoFileClip(temp_mp4_path) + + # --- 在这里进行所有的视频处理和提取操作 --- + + # 提取第一帧 (保持原尺寸) + frame_array = clip.get_frame(t=0.0) + image = Image.fromarray(frame_array) + + frame_stream = io.BytesIO() + image.save(frame_stream, 'JPEG') + frame_bytes = frame_stream.getvalue() + + logger.info("✅ 成功提取第一帧图片。") + + # 视频转 GIF (使用另一个临时文件来保存 GIF) + temp_gif_path = "" + with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as tmp_file: + temp_gif_path = tmp_file.name + + target_fps = int(round(clip.fps)) if clip.fps else 24 + clip.write_gif(temp_gif_path, fps=target_fps) + + with open(temp_gif_path, 'rb') as f: + gif_bytes = f.read() + + logger.info("✅ 成功生成 GIF。") + + # 返回结果 (例如: 上传到 MinIO) + # return mp4_bytes, gif_bytes, frame_bytes + + # ----------------------------------------------- + + except Exception as e: + logger.error(f"❌ 视频处理或文件操作失败: {e}") + # 在失败时,也尝试清理文件 + + finally: + # 3. 清理临时文件 (非常重要!) + if os.path.exists(temp_mp4_path): + os.remove(temp_mp4_path) + logger.info(f"🗑️ 已删除临时 MP4 文件: {temp_mp4_path}") + + if 'temp_gif_path' in locals() and os.path.exists(temp_gif_path): + os.remove(temp_gif_path) + logger.info(f"🗑️ 已删除临时 GIF 文件: {temp_gif_path}") + + # 3. 上传所有结果到 MinIO + + try: + # 上传原始 MP4 + self.upload_stream_to_minio(mp4_bytes, MP4_OBJECT, "video/mp4") + + # 上传生成的 GIF + self.upload_stream_to_minio(gif_bytes, GIF_OBJECT, "image/gif") + + # 上传第一帧图片 + self.upload_stream_to_minio(frame_bytes, FRAME_OBJECT, "image/jpeg") + + self.pose_transform_data = {'tasks_id': self.tasks_id, 'status': 'SUCCESS', 'message': "success", 'gif_url': f'aida-users/{GIF_OBJECT}', 'video_url': f'aida-users/{MP4_OBJECT}', 'image_url': f'aida-users/{FRAME_OBJECT}'} + + # 推送消息 + if not DEBUG: + publish_status(json.dumps(self.pose_transform_data), PS_RABBITMQ_QUEUES) + logger.info( + f" [x] Sent to: {PS_RABBITMQ_QUEUES} data:@@@@ {json.dumps(self.pose_transform_data, indent=4)}") + + return "\n🎉 所有任务完成!" + + except Exception as e: + logger.error(e) + return None + + # --- 辅助函数:提交任务到队列 --- + def queue_prompt(self, prompt, client_id): + """向 ComfyUI 提交工作流提示。""" + p = {"prompt": prompt, "client_id": client_id, "prompt_id": client_id} + data = json.dumps(p).encode('utf-8') + + # 提交任务到 /prompt 端点 + response = requests.post(f"http://{COMFYUI_SERVER_ADDRESS}/prompt", data=data) + # print(f"-------------{response.text}") + # print(f"------------{client_id}") + + if response.status_code == 200: + return response.json() + else: + logger.warning(f"提交任务失败,状态码: {response.status_code}") + logger.warning(response.text) + return None + + def poll_history(self, prompt_id, interval_seconds=5): + """步骤 2: 轮询 /history/{prompt_id} 检查任务是否完成""" + url = f"http://{COMFYUI_SERVER_ADDRESS}/history/{prompt_id}" + + logger.info(f"⏳ 开始轮询状态 (间隔 {interval_seconds} 秒)...") + + while True: + time.sleep(interval_seconds) + + try: + response = requests.get(url) + # 任务未完成时,ComfyUI可能会返回404或空响应,我们只关注成功响应 + if response.status_code == 200: + history_data = response.json() + + # ComfyUI 返回的历史记录结构是 {prompt_id: {outputs: ...}} + if prompt_id in history_data: + logger.info("🎉 任务已完成!") + return history_data[prompt_id]['outputs'] + + logger.info("⏳ 任务仍在执行或等待中...") + + except requests.exceptions.RequestException as e: + # 处理可能的连接错误,但通常不会在内部轮询中发生 + logger.info(f"⚠️ 轮询时发生错误: {e}") + pass + + def get_comfyui_video_bytes(self, filename: str, subfolder: str, file_type: str = "output"): + """ + 从 ComfyUI 的 /view 端点获取视频文件的二进制数据。 + + 参数: + - filename: 视频文件名 (例如: 'ComfyUI_00002_.mp4') + - subfolder: 存储子文件夹 (例如: 'ComfyUI_2025-10-31') + - file_type: 文件类型 (通常是 'output') + + 返回: + - 视频文件的二进制内容 (bytes) 或 None。 + """ + url = f"http://{COMFYUI_SERVER_ADDRESS}/view" + params = { + "filename": filename, + "subfolder": subfolder, + "type": file_type + } + + logger.info(f"📡 正在从 ComfyUI 下载视频: {filename}") + try: + # 使用 requests.get 下载文件 + response = requests.get(url, params=params, stream=True) + response.raise_for_status() # 检查 HTTP 错误 + + # 返回文件的完整二进制内容 + return response.content + + except requests.exceptions.RequestException as e: + logger.error(f"❌ 从 ComfyUI 获取视频失败: {e}") + return None + + def upload_stream_to_minio(self, video_bytes: bytes, object_name: str, content_type: str): + """从内存流上传数据到 MinIO。""" + logger.info(f"☁️ 正在上传对象到 MinIO: {object_name}") + try: + + data_stream = io.BytesIO(video_bytes) + + result = self.minio_client.put_object( + bucket_name='aida-users', + object_name=object_name, + data=data_stream, + length=len(video_bytes), + content_type=content_type + ) + logger.info(f"✅ MinIO 上传成功: {result.object_name}") + return True + except S3Error as e: + logger.error(f"❌ MinIO 上传失败: {e}") + return False + + +if __name__ == '__main__': + request_data = ComfyuiI2VModel( + tasks_id="12222515151123-89111", + image_url="aida-users/89/product_image/a6949500-2393-42ac-8723-440b5d5da2b2-0-89.png", + prompt="Model executing a series of poses, dynamic camera movement alternating between detailed close-ups and full shots." + ) + + server = ComfyUIServerI2V(request_data) + print(server.get_result()) diff --git a/app/service/comfyui_I2V/server.py b/app/service/comfyui_I2V/pose2v_server.py similarity index 98% rename from app/service/comfyui_I2V/server.py rename to app/service/comfyui_I2V/pose2v_server.py index 58d4d1b..e9927b2 100644 --- a/app/service/comfyui_I2V/server.py +++ b/app/service/comfyui_I2V/pose2v_server.py @@ -14,11 +14,12 @@ from minio import Minio, S3Error from moviepy.video.io.VideoFileClip import VideoFileClip from app.core.config import REDIS_HOST, REDIS_PORT, REDIS_DB, MINIO_URL, MINIO_ACCESS, MINIO_SECRET, MINIO_SECURE, COMFYUI_SERVER_ADDRESS, PS_RABBITMQ_QUEUES, DEBUG -from app.schemas.pose_transform import PoseTransformModel +from app.schemas.comfyui_i2v import ComfyuiPose2VModel from app.service.generate_image.utils.mq import publish_status logger = logging.getLogger() +# 图 + 骨架 = 视频 工作流 workflow_json = { "162": { "inputs": { @@ -314,8 +315,8 @@ workflow_json = { } } } - -pose_video_map = { +# 骨架映射 +video_map = { "1": "input_pose_video/1.mp4", "2": "input_pose_video/2.mp4", "3": "input_pose_video/3.mp4", @@ -325,7 +326,7 @@ pose_video_map = { } -class ComfyUIServerI2V: +class ComfyUIServerPose2V: def __init__(self, request_data): self.image_url = request_data.image_url self.pose_num = request_data.pose_id @@ -338,7 +339,7 @@ class ComfyUIServerI2V: self.minio_client = Minio(MINIO_URL, access_key=MINIO_ACCESS, secret_key=MINIO_SECRET, secure=MINIO_SECURE) def get_result(self): - workflow_json['174']['inputs']['file'] = pose_video_map[self.pose_num] + workflow_json['174']['inputs']['file'] = video_map[self.pose_num] workflow_json['169']['inputs']['noise_seed'] = random.randint(0, 10 ** 18) # 下载图片 上传 comfyui server @@ -689,11 +690,11 @@ class ComfyUIServerI2V: if __name__ == '__main__': - request_data = PoseTransformModel( + request_data = ComfyuiPose2VModel( tasks_id="12222515151123-89111", image_url="aida-users/89/product_image/a6949500-2393-42ac-8723-440b5d5da2b2-0-89.png", pose_id="6" ) - server = ComfyUIServerI2V(request_data) + server = ComfyUIServerPose2V(request_data) print(server.get_result()) From fee9334b1f64b19f99cd84d078bdcc7f92acd6e2 Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:59:04 +0800 Subject: [PATCH 08/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=AA=A8=E6=9E=B6=E7=94=9F=E6=88=90=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=89=8D=E4=B8=A4=E5=B8=A7=E8=BF=87=E6=9B=9D=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20docs=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=EF=BC=89:=20refactor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20te?= =?UTF-8?q?st(=E5=A2=9E=E5=8A=A0=E6=B5=8B=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/comfyui_I2V/pose2v_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/service/comfyui_I2V/pose2v_server.py b/app/service/comfyui_I2V/pose2v_server.py index e9927b2..7b33ded 100644 --- a/app/service/comfyui_I2V/pose2v_server.py +++ b/app/service/comfyui_I2V/pose2v_server.py @@ -36,7 +36,7 @@ workflow_json = { }, "163": { "inputs": { - "fps": 24, + "fps": 16, "images": [ "164", 0 @@ -243,7 +243,7 @@ workflow_json = { }, "179": { "inputs": { - "text": "On a sunny summer day, there are marshmallow - like clouds, and the sunlight is bright and warm. A girl with white curly double - ponytails is wearing unique sunglasses, distinctive clothes and shoes. Her posture is natural and full of dynamic tension. The background is the scene of the Leaning Tower of Pisa in Italy, emphasizing the realistic contrast of details in reality. The whole picture is in a realistic 3D style, rich in details and with a relaxed atmosphere. She is dancing slowly, waving her hands.", + "text": "The model is catwalking at the fashion show.", "clip": [ "167", 0 @@ -258,7 +258,7 @@ workflow_json = { "inputs": { "width": 512, "height": 768, - "length": 121, + "length": 81, "batch_size": 1, "positive": [ "179", @@ -691,7 +691,7 @@ class ComfyUIServerPose2V: if __name__ == '__main__': request_data = ComfyuiPose2VModel( - tasks_id="12222515151123-89111", + tasks_id="122225115151123-89111", image_url="aida-users/89/product_image/a6949500-2393-42ac-8723-440b5d5da2b2-0-89.png", pose_id="6" ) From 7d2149dcaf2d74259470b7bdda621c72f67f05ed Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:59:57 +0800 Subject: [PATCH 09/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20docs?= =?UTF-8?q?=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:=20refac?= =?UTF-8?q?tor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=B5=8B=E8=AF=95):=20=E5=A4=A9=E7=A5=A5design?= =?UTF-8?q?=E5=9B=9E=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/design_fast/design_generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/service/design_fast/design_generate.py b/app/service/design_fast/design_generate.py index 1ef5a3b..d0b3e79 100644 --- a/app/service/design_fast/design_generate.py +++ b/app/service/design_fast/design_generate.py @@ -213,7 +213,7 @@ def design_generate_v2(request_data): # 发送结果给java端 url = JAVA_STREAM_API_URL xu_pei_test_url = "https://137f6b5c3490.ngrok-free.app/api/third/party/receiveDesignResults" - tianxaing_test_url = "https://c2ae520723c9.ngrok-free.app/api/third/party/receiveDesignResults" + tianxaing_test_url = "https://bd88cece10dc.ngrok-free.app/api/third/party/receiveDesignResults" logger.info(f"java 回调 -> {url}") logger.info(f"xupei java 回调 -> {xu_pei_test_url}") logger.info(f"tianxiang java 回调 -> {tianxaing_test_url}") From e8d8b715aeaa6820ae762d4c4a31acb1d5675cbe Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:56:34 +0800 Subject: [PATCH 10/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20=20?= =?UTF-8?q?=20accessories=20=E6=9B=BF=E6=8D=A2=E4=B8=BA=20others=20docs?= =?UTF-8?q?=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:=20refac?= =?UTF-8?q?tor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=B5=8B=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/design/items/__init__.py | 2 +- .../design/items/{accessories.py => others.py} | 0 app/service/design_batch/design_batch_celery.py | 12 ++++++------ app/service/design_batch/item.py | 6 +++--- app/service/design_batch/pipeline/loading.py | 4 ++-- app/service/design_batch/pipeline/scale.py | 2 +- app/service/design_batch/pipeline/split.py | 2 +- app/service/design_batch/utils/organize.py | 2 +- app/service/design_fast/design_generate.py | 12 ++++++------ app/service/design_fast/item.py | 6 +++--- app/service/design_fast/pipeline/loading.py | 4 ++-- app/service/design_fast/pipeline/scale.py | 2 +- app/service/design_fast/pipeline/split.py | 2 +- app/service/design_fast/utils/organize.py | 6 +++--- app/service/design_fast/utils/synthesis_item.py | 10 +++++----- 15 files changed, 36 insertions(+), 36 deletions(-) rename app/service/design/items/{accessories.py => others.py} (100%) diff --git a/app/service/design/items/__init__.py b/app/service/design/items/__init__.py index 23f35bf..e3e6bd5 100644 --- a/app/service/design/items/__init__.py +++ b/app/service/design/items/__init__.py @@ -5,7 +5,7 @@ from .top import Top, Blouse, Outwear, Dress from .bottom import Bottom, Trousers, Skirt from .shoes import Shoes from .bag import Bag -from .accessories import Hairstyle, Earring +from .others import Hairstyle, Earring __all__ = [ 'ITEMS', 'build_item', diff --git a/app/service/design/items/accessories.py b/app/service/design/items/others.py similarity index 100% rename from app/service/design/items/accessories.py rename to app/service/design/items/others.py diff --git a/app/service/design_batch/design_batch_celery.py b/app/service/design_batch/design_batch_celery.py index 3f5eed7..f7be62e 100644 --- a/app/service/design_batch/design_batch_celery.py +++ b/app/service/design_batch/design_batch_celery.py @@ -5,9 +5,9 @@ from celery import Celery from minio import Minio from app.core.config import * -from app.service.design_batch.item import BodyItem, TopItem, BottomItem, AccessoriesItem +from app.service.design_batch.item import BodyItem, TopItem, BottomItem, OthersItem from app.service.design_batch.utils.MQ import publish_status -from app.service.design_batch.utils.organize import organize_body, organize_clothing, organize_accessories +from app.service.design_batch.utils.organize import organize_body, organize_clothing, organize_others from app.service.design_batch.utils.save_json import oss_upload_json from app.service.design_batch.utils.synthesis_item import update_base_size_priority, synthesis, synthesis_single @@ -33,8 +33,8 @@ def process_item(item, basic): elif item['type'].lower() in ['skirt', 'trousers', 'bottoms']: bottom_server = BottomItem(data=item, basic=basic, minio_client=minio_client) item_data = bottom_server.process() - elif item['type'].lower() in ['accessories']: - bottom_server = AccessoriesItem(data=item, basic=basic, minio_client=minio_client) + elif item['type'].lower() in ['others']: + bottom_server = OthersItem(data=item, basic=basic, minio_client=minio_client) item_data = bottom_server.process() else: raise NotImplementedError(f"Item type {item['type']} not implemented") @@ -47,8 +47,8 @@ def process_layer(item, layers): body_layer = organize_body(item) layers.append(body_layer) return item['body_image'].size - elif item['name'] == 'accessories': - front_layer, back_layer = organize_accessories(item) + elif item['name'] == 'others': + front_layer, back_layer = organize_others(item) layers.append(front_layer) layers.append(back_layer) else: diff --git a/app/service/design_batch/item.py b/app/service/design_batch/item.py index 5ddfdc7..ad50ff2 100644 --- a/app/service/design_batch/item.py +++ b/app/service/design_batch/item.py @@ -9,10 +9,10 @@ class BaseItem: self.result.update(basic) -class AccessoriesItem(BaseItem): +class OthersItem(BaseItem): def __init__(self, data, basic, minio_client): super().__init__(data, basic) - self.Accessories_pipeline = [ + self.Others_pipeline = [ LoadImage(minio_client), # KeyPoint(), ContourDetection(), @@ -25,7 +25,7 @@ class AccessoriesItem(BaseItem): ] def process(self): - for item in self.Accessories_pipeline: + for item in self.Others_pipeline: self.result = item(self.result) return self.result diff --git a/app/service/design_batch/pipeline/loading.py b/app/service/design_batch/pipeline/loading.py index 5a55d9d..332f29a 100644 --- a/app/service/design_batch/pipeline/loading.py +++ b/app/service/design_batch/pipeline/loading.py @@ -74,8 +74,8 @@ class LoadImage: keypoint = 'head_point' elif name == 'earring': keypoint = 'ear_point' - elif name == 'accessories': - keypoint = "accessories" + elif name == 'others': + keypoint = "others" else: raise KeyError(f"{name} does not belong to item category list: blouse, outwear, dress, trousers, skirt, " f"bag, shoes, hairstyle, earring.") diff --git a/app/service/design_batch/pipeline/scale.py b/app/service/design_batch/pipeline/scale.py index d1c7a36..daf2c65 100644 --- a/app/service/design_batch/pipeline/scale.py +++ b/app/service/design_batch/pipeline/scale.py @@ -46,7 +46,7 @@ class Scaling: result['scale'] = result['scale_bag'] elif result['keypoint'] == 'ear_point': result['scale'] = result['scale_earrings'] - elif result['keypoint'] == 'accessories': + elif result['keypoint'] == 'others': # 由于没有识别配饰keypoint的模型 所以统一将配饰的两个关键点设定为 (0,0) (0,img.width) # 模特的关键点设定为(0,0) (0,320/2) 距离比例简写为 160 / img.width distance_clo = result['img_shape'][1] diff --git a/app/service/design_batch/pipeline/split.py b/app/service/design_batch/pipeline/split.py index 88e8e75..288381a 100644 --- a/app/service/design_batch/pipeline/split.py +++ b/app/service/design_batch/pipeline/split.py @@ -21,7 +21,7 @@ class Split(object): def __call__(self, result): try: - if result['name'] in ('outwear', 'dress', 'blouse', 'skirt', 'trousers', 'tops', 'bottoms', 'accessories'): + if result['name'] in ('outwear', 'dress', 'blouse', 'skirt', 'trousers', 'tops', 'bottoms', 'others'): if result['resize_scale'][0] == 1.0 and result['resize_scale'][1] == 1.0: front_mask = result['front_mask'] diff --git a/app/service/design_batch/utils/organize.py b/app/service/design_batch/utils/organize.py index 33edc4f..0550419 100644 --- a/app/service/design_batch/utils/organize.py +++ b/app/service/design_batch/utils/organize.py @@ -55,7 +55,7 @@ def organize_clothing(layer): return front_layer, back_layer -def organize_accessories(layer): +def organize_others(layer): # 起始坐标 start_point = (0, 0) # 前片数据 diff --git a/app/service/design_fast/design_generate.py b/app/service/design_fast/design_generate.py index d0b3e79..63aeaff 100644 --- a/app/service/design_fast/design_generate.py +++ b/app/service/design_fast/design_generate.py @@ -6,8 +6,8 @@ import requests from minio import Minio from app.core.config import * -from app.service.design_fast.item import BodyItem, TopItem, BottomItem, AccessoriesItem -from app.service.design_fast.utils.organize import organize_body, organize_clothing, organize_accessories +from app.service.design_fast.item import BodyItem, TopItem, BottomItem, OthersItem +from app.service.design_fast.utils.organize import organize_body, organize_clothing, organize_others from app.service.design_fast.utils.progress import final_progress, update_progress from app.service.design_fast.utils.synthesis_item import synthesis, synthesis_single, update_base_size_priority from app.service.utils.decorator import RunTime @@ -30,8 +30,8 @@ def process_item(item, basic): elif item['type'].lower() in ['skirt', 'trousers', 'bottoms']: bottom_server = BottomItem(data=item, basic=basic, minio_client=minio_client) item_data = bottom_server.process() - elif item['type'].lower() in ['accessories']: - bottom_server = AccessoriesItem(data=item, basic=basic, minio_client=minio_client) + elif item['type'].lower() in ['others']: + bottom_server = OthersItem(data=item, basic=basic, minio_client=minio_client) item_data = bottom_server.process() else: raise NotImplementedError(f"Item type {item['type']} not implemented") @@ -44,8 +44,8 @@ def process_layer(item, layers): body_layer = organize_body(item) layers.append(body_layer) return item['body_image'].size - elif item['name'] == 'accessories': - front_layer, back_layer = organize_accessories(item) + elif item['name'] == 'others': + front_layer, back_layer = organize_others(item) layers.append(front_layer) layers.append(back_layer) else: diff --git a/app/service/design_fast/item.py b/app/service/design_fast/item.py index 74ca0c4..dcad5f0 100644 --- a/app/service/design_fast/item.py +++ b/app/service/design_fast/item.py @@ -9,10 +9,10 @@ class BaseItem: self.result.update(basic) -class AccessoriesItem(BaseItem): +class OthersItem(BaseItem): def __init__(self, data, basic, minio_client): super().__init__(data, basic) - self.Accessories_pipeline = [ + self.Others_pipeline = [ LoadImage(minio_client), # KeyPoint(), # ContourDetection(), @@ -26,7 +26,7 @@ class AccessoriesItem(BaseItem): ] def process(self): - for item in self.Accessories_pipeline: + for item in self.Others_pipeline: self.result = item(self.result) return self.result diff --git a/app/service/design_fast/pipeline/loading.py b/app/service/design_fast/pipeline/loading.py index 329e14b..7cc4296 100644 --- a/app/service/design_fast/pipeline/loading.py +++ b/app/service/design_fast/pipeline/loading.py @@ -105,8 +105,8 @@ class LoadImage: keypoint = 'head_point' elif name == 'earring': keypoint = 'ear_point' - elif name == 'accessories': - keypoint = "accessories" + elif name == 'others': + keypoint = "others" else: raise KeyError(f"{name} does not belong to item category list: blouse, outwear, dress, trousers, skirt, " f"bag, shoes, hairstyle, earring.") diff --git a/app/service/design_fast/pipeline/scale.py b/app/service/design_fast/pipeline/scale.py index d1c7a36..daf2c65 100644 --- a/app/service/design_fast/pipeline/scale.py +++ b/app/service/design_fast/pipeline/scale.py @@ -46,7 +46,7 @@ class Scaling: result['scale'] = result['scale_bag'] elif result['keypoint'] == 'ear_point': result['scale'] = result['scale_earrings'] - elif result['keypoint'] == 'accessories': + elif result['keypoint'] == 'others': # 由于没有识别配饰keypoint的模型 所以统一将配饰的两个关键点设定为 (0,0) (0,img.width) # 模特的关键点设定为(0,0) (0,320/2) 距离比例简写为 160 / img.width distance_clo = result['img_shape'][1] diff --git a/app/service/design_fast/pipeline/split.py b/app/service/design_fast/pipeline/split.py index 5c62899..906fc93 100644 --- a/app/service/design_fast/pipeline/split.py +++ b/app/service/design_fast/pipeline/split.py @@ -20,7 +20,7 @@ class Split(object): def __call__(self, result): try: - if result['name'] in ('outwear', 'dress', 'blouse', 'skirt', 'trousers', 'tops', 'bottoms', 'accessories'): + if result['name'] in ('outwear', 'dress', 'blouse', 'skirt', 'trousers', 'tops', 'bottoms', 'others'): ori_front_mask = result['front_mask'].copy() ori_back_mask = result['back_mask'].copy() diff --git a/app/service/design_fast/utils/organize.py b/app/service/design_fast/utils/organize.py index f5c8144..ce4a961 100644 --- a/app/service/design_fast/utils/organize.py +++ b/app/service/design_fast/utils/organize.py @@ -58,14 +58,14 @@ def organize_clothing(layer): return front_layer, back_layer -def organize_accessories(layer): +def organize_others(layer): # 起始坐标 start_point = (0, 0) layer['clothes_keypoint'] = { - 'accessories_left': [0, 0] + 'others_left': [0, 0] } layer['body_point_test'] = { - 'accessories_left': [0, 0] + 'others_left': [0, 0] } start_point = calculate_start_point(layer['keypoint'], layer['scale'], layer['clothes_keypoint'], layer['body_point_test'], layer["offset"], layer["resize_scale"]) diff --git a/app/service/design_fast/utils/synthesis_item.py b/app/service/design_fast/utils/synthesis_item.py index d46795e..a8ce1b4 100644 --- a/app/service/design_fast/utils/synthesis_item.py +++ b/app/service/design_fast/utils/synthesis_item.py @@ -79,11 +79,11 @@ def synthesis(data, size, basic_info): _, binary_body_mask = cv2.threshold(body_mask, 127, 255, cv2.THRESH_BINARY) top_outer_mask = np.array(binary_body_mask) bottom_outer_mask = np.array(binary_body_mask) - accessories_outer_mask = np.array(binary_body_mask) + others_outer_mask = np.array(binary_body_mask) top = True bottom = True - accessories = True + others = True i = len(data) while i: i -= 1 @@ -111,7 +111,7 @@ def synthesis(data, size, basic_info): background = np.zeros_like(top_outer_mask) background[all_y_start:all_y_end, all_x_start:all_x_end] = sketch_mask[mask_y_start:mask_y_end, mask_x_start:mask_x_end] bottom_outer_mask = background + bottom_outer_mask - elif accessories and data[i]['name'] in ['accessories_front']: + elif others and data[i]['name'] in ['others_front']: mask_shape = data[i]['mask'].shape y_offset, x_offset = data[i]['adaptive_position'] # 初始化叠加区域的起始和结束位置 @@ -121,13 +121,13 @@ def synthesis(data, size, basic_info): _, sketch_mask = cv2.threshold(data[i]['mask'], 127, 255, cv2.THRESH_BINARY) background = np.zeros_like(top_outer_mask) background[all_y_start:all_y_end, all_x_start:all_x_end] = sketch_mask[mask_y_start:mask_y_end, mask_x_start:mask_x_end] - accessories_outer_mask = background + accessories_outer_mask + others_outer_mask = background + others_outer_mask pass elif bottom is False and top is False: break all_mask = cv2.bitwise_or(top_outer_mask, bottom_outer_mask) - all_mask = cv2.bitwise_or(all_mask, accessories_outer_mask) + all_mask = cv2.bitwise_or(all_mask, others_outer_mask) for layer in data: if layer['image'] is not None: From 30bfd22e3e9a5d7420a63d28aacfefeb5037431d Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:44:19 +0800 Subject: [PATCH 11/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20=20?= =?UTF-8?q?=20=E9=AA=A8=E6=9E=B6=E7=94=9F=E6=88=90=E8=A7=86=E9=A2=91=20?= =?UTF-8?q?=E9=A6=96=E5=B8=A7=E6=9B=9D=E5=85=89=E9=97=AE=E9=A2=98=20docs?= =?UTF-8?q?=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:=20refac?= =?UTF-8?q?tor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=B5=8B=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/comfyui_I2V/pose2v_server.py | 51 +++++++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/app/service/comfyui_I2V/pose2v_server.py b/app/service/comfyui_I2V/pose2v_server.py index 7b33ded..d1db7bc 100644 --- a/app/service/comfyui_I2V/pose2v_server.py +++ b/app/service/comfyui_I2V/pose2v_server.py @@ -36,9 +36,9 @@ workflow_json = { }, "163": { "inputs": { - "fps": 16, + "fps": 24, "images": [ - "164", + "192", 0 ] }, @@ -256,9 +256,9 @@ workflow_json = { }, "180": { "inputs": { - "width": 512, - "height": 768, - "length": 81, + "width": 480, + "height": 720, + "length": 121, "batch_size": 1, "positive": [ "179", @@ -313,8 +313,47 @@ workflow_json = { "_meta": { "title": "LoRA加载器(仅模型)" } + }, + "189": { + "inputs": { + "images": [ + "171", + 0 + ] + }, + "class_type": "PreviewImage", + "_meta": { + "title": "预览图像" + } + }, + "190": { + "inputs": { + "images": [ + "192", + 0 + ] + }, + "class_type": "PreviewImage", + "_meta": { + "title": "预览图像" + } + }, + "192": { + "inputs": { + "batch_index": 4, + "length": 117, + "image": [ + "164", + 0 + ] + }, + "class_type": "ImageFromBatch", + "_meta": { + "title": "从批次获取图像" + } } } + # 骨架映射 video_map = { "1": "input_pose_video/1.mp4", @@ -691,7 +730,7 @@ class ComfyUIServerPose2V: if __name__ == '__main__': request_data = ComfyuiPose2VModel( - tasks_id="122225115151123-89111", + tasks_id="122522251123-89111", image_url="aida-users/89/product_image/a6949500-2393-42ac-8723-440b5d5da2b2-0-89.png", pose_id="6" ) From 322fb9c46b51f3f905457397bf97e8f00b2cf790 Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:54:04 +0800 Subject: [PATCH 12/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20fix=EF=BC=88=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20docs?= =?UTF-8?q?=EF=BC=88=E6=96=87=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:=20refac?= =?UTF-8?q?tor=EF=BC=88=E9=87=8D=E6=9E=84=EF=BC=89:=20test(=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=B5=8B=E8=AF=95):=20=E6=96=B0=E5=A2=9E=E5=A4=A9?= =?UTF-8?q?=E7=A5=A5=E5=9B=9E=E8=B0=83url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/design_fast/design_generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/service/design_fast/design_generate.py b/app/service/design_fast/design_generate.py index 63aeaff..10b5e9e 100644 --- a/app/service/design_fast/design_generate.py +++ b/app/service/design_fast/design_generate.py @@ -213,7 +213,7 @@ def design_generate_v2(request_data): # 发送结果给java端 url = JAVA_STREAM_API_URL xu_pei_test_url = "https://137f6b5c3490.ngrok-free.app/api/third/party/receiveDesignResults" - tianxaing_test_url = "https://bd88cece10dc.ngrok-free.app/api/third/party/receiveDesignResults" + tianxaing_test_url = "https://d7436c137fb9.ngrok-free.app/api/third/party/receiveDesignResults" logger.info(f"java 回调 -> {url}") logger.info(f"xupei java 回调 -> {xu_pei_test_url}") logger.info(f"tianxiang java 回调 -> {tianxaing_test_url}") From 6e06c8b5162efac063599e1927ba464d2a32f3ce Mon Sep 17 00:00:00 2001 From: zchengrong <124802516+zchengrong@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:14:22 +0800 Subject: [PATCH 13/13] =?UTF-8?q?feat=EF=BC=88=E6=96=B0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=89:=20=20=E6=96=B0=E5=A2=9E=E5=9B=BE=E5=B1=82=E8=B6=8A?= =?UTF-8?q?=E7=95=8C=E6=8E=A7=E5=88=B6=E5=8F=82=E6=95=B0=20fix=EF=BC=88?= =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug=EF=BC=89:=20docs=EF=BC=88=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=8F=98=E6=9B=B4=EF=BC=89:=20refactor=EF=BC=88?= =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=89:=20test(=E5=A2=9E=E5=8A=A0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/service/design_fast/utils/synthesis_item.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/service/design_fast/utils/synthesis_item.py b/app/service/design_fast/utils/synthesis_item.py index a8ce1b4..606b1b5 100644 --- a/app/service/design_fast/utils/synthesis_item.py +++ b/app/service/design_fast/utils/synthesis_item.py @@ -60,6 +60,18 @@ def positioning(all_mask_shape, mask_shape, offset): # @RunTime def synthesis(data, size, basic_info): + """ + + Args: + data: + size: + basic_info: + + Returns: + + """ + # out_of_bounds_control: 是否允许服装越界 True 允许 False 不允许 默认情况允许 + out_of_bounds_control = basic_info.get('out_of_bounds_control', True) # 创建底图 base_image = Image.new('RGBA', size, (0, 0, 0, 0)) try: @@ -88,7 +100,10 @@ def synthesis(data, size, basic_info): while i: i -= 1 if top and data[i]['name'] in ["blouse_front", "outwear_front", "dress_front", "tops_front"]: - top = False + if out_of_bounds_control: + top = True + else: + top = False mask_shape = data[i]['mask'].shape y_offset, x_offset = data[i]['adaptive_position'] # 初始化叠加区域的起始和结束位置