From c73bfa7e2ae2e8c017d3811c933765fd19410688 Mon Sep 17 00:00:00 2001 From: zcr Date: Tue, 28 Apr 2026 17:03:04 +0800 Subject: [PATCH] =?UTF-8?q?3D=20=E6=89=93=E6=9D=BF=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api_route.py | 2 + app/api/api_sketch_to_garment.py | 104 +++++++++++++++++++++++ app/core/config.py | 68 +-------------- app/schemas/sketch_to_garment_schemas.py | 12 +++ app/service/sketch2garment/callback.py | 35 ++++++++ app/service/sketch2garment/celery_app.py | 46 ++++++++++ app/service/sketch2garment/server.py | 44 ++++++++++ app/service/sketch2garment/tasks.py | 57 +++++++++++++ 8 files changed, 303 insertions(+), 65 deletions(-) create mode 100644 app/api/api_sketch_to_garment.py create mode 100644 app/schemas/sketch_to_garment_schemas.py create mode 100644 app/service/sketch2garment/callback.py create mode 100644 app/service/sketch2garment/celery_app.py create mode 100644 app/service/sketch2garment/server.py create mode 100644 app/service/sketch2garment/tasks.py diff --git a/app/api/api_route.py b/app/api/api_route.py index 1af7b9f..c392e1a 100644 --- a/app/api/api_route.py +++ b/app/api/api_route.py @@ -11,6 +11,7 @@ from app.api import api_precompute from app.api import api_prompt_generation from app.api import api_recommendation from app.api import api_test +from app.api import api_sketch_to_garment router = APIRouter() @@ -26,6 +27,7 @@ router.include_router(api_precompute.router, tags=['api_precompute'], prefix="/a router.include_router(api_mannequins_edit.router, tags=['api_mannequins_edit'], prefix="/api") router.include_router(api_pose_transform.router, tags=['api_pose_transform'], prefix="/api") router.include_router(api_clothing_seg.router, tags=['api_clothing_seg'], prefix="/api") +router.include_router(api_sketch_to_garment.router, tags=['sketch_to_garment'], prefix="/api") """停用""" # from app.api import api_chat_robot diff --git a/app/api/api_sketch_to_garment.py b/app/api/api_sketch_to_garment.py new file mode 100644 index 0000000..332f1f1 --- /dev/null +++ b/app/api/api_sketch_to_garment.py @@ -0,0 +1,104 @@ +import json +import logging + +from fastapi import APIRouter, HTTPException + +from app.schemas.response_template import ResponseModel +from app.schemas.sketch_to_garment_schemas import SketchToGarmentModel +from app.service.sketch2garment.server import submit_sketch_to_garment_task + +logger = logging.getLogger() +router = APIRouter() + + +@router.post("/sketch_to_garment") +def sketch_to_garment_api(request_item: SketchToGarmentModel): + """ + ### 接口说明: + 将图片转换为3D模型(异步处理)。接口接收请求后立即返回任务ID,后台通过 Celery 处理,处理完成后结果会通过 RabbitMQ 发送。 + + ### 参数说明: + - **input_image_path**: 输入图片路径 + - **bucket_name**: bucket name + - **user_id**: 用户id + - **callback_url**: 回调url + - **task_id**: 任务id + - **model**: 转换模式 文本和图片 ,默认只有图片 + + ### 请求体示例: + **单张图片模式:** + ```json + { + "input_image_path": "test/53d38bd5-f77b-4034-ada2-45f1e2ebe00c.png", + "bucket_name": "test", + "user_id": "string-456", + "callback_url": "http://18.167.251.121:10015/api/image/webhook/img-to-3d", + "task_id": "string12", + "model": "picture" + } + ``` + + + ### 输出示例: + + ```json + { + "code": 200, + "msg": "OK!", + "data": { + "state": "success", + "task_id": "string12", + "message": "任务已成功提交,正在后台处理..." + } + } + ``` + ### 错误输出 + 参考文档: https://platform.tripo3d.ai/docs/error-handling + ```json + { + "code": 500, + "message": "You don’t have enough credit to create this task", + "data": { + "status": "fail", + "task_id": "123", + "message": "You don’t have enough credit to create this task", + "error": str(e) + } + } + ``` + + 回调请求参数例子: + ```json + { + "task_id": "string12", + "status": "success", + "result": { + "pattern": "test/string-456/pattern_making/now_string-456_pattern.png", + "texture": "test/string-456/pattern_making/now_string-456_texture.png", + "glb": "test/string-456/pattern_making/now_string-456_sim.glb", + "texture_fabric": "test/string-456/pattern_making/now_string-456_texture_fabric.png" + } + } + ``` + """ + try: + logger.info(f"sketch_to_garment request item is : @@@@@@:{json.dumps(request_item.model_dump(), indent=4)}") + result = submit_sketch_to_garment_task( + task_id=request_item.task_id, + callback_url=request_item.callback_url, + bucket_name=request_item.bucket_name, + input_image_path=request_item.input_image_path, + user_id=request_item.user_id, + model=request_item.model + ) + result = { + "state": "success", + "task_id": request_item.task_id, + "message": "任务已成功提交,正在后台处理...", + } + state_code = 200 + return ResponseModel(data=result, code=state_code) + + except Exception as e: + logger.warning(f"super_resolution Run Exception @@@@@@:{e}") + raise HTTPException(status_code=404, detail=str(e)) diff --git a/app/core/config.py b/app/core/config.py index 3c44ea2..3b9d178 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -87,6 +87,9 @@ class Settings(BaseSettings): A6000_SERVICE_HOST: str = Field(default='', description="") B_4_X_4090_SERVICE_HOST: str = Field(default='', description="") + # --- sketch to garment 模型url --- + SKETCH_TO_GARMENT_URL: str = Field(default='', description="") + # --- 其他配置信息 以下均为Docker容器内配置--- LOGS_PATH: str = Field(default="/logs/", description="") CATEGORY_PATH: str = Field(default="/app/service/attribute/config/descriptor/category/category_dis.csv", description="") @@ -97,71 +100,6 @@ class Settings(BaseSettings): settings = Settings() -# ====================== Nacos 配置管理 ====================== - -client_config = (ClientConfigBuilder() - .server_address(NACOS_SERVER_ADDRESSES) - .username(NACOS_USERNAME) - .password(NACOS_PASSWORD) - .namespace_id(NACOS_NAMESPACE) - .log_level('INFO') - .grpc_config(GRPCConfig(grpc_timeout=50000)) - .build()) - -# ====================== Nacos 配置管理 ====================== -nacos_config_data: Dict[str, Any] = {} - - -async def load_nacos_config() -> None: - """初始化 Nacos 配置并监听变化""" - global nacos_config_data, settings - global nacos_initialized_successfully - - try: - client = await NacosConfigService.create_config_service(client_config) - # 1. 第一次获取配置 - content = await client.get_config(ConfigParam(data_id=NACOS_DATA_ID, group=NACOS_GROUP)) - if content: - loaded = yaml.safe_load(content) or {} - nacos_config_data = loaded - # 用 Nacos 配置覆盖 settings - for key, value in loaded.items(): - if hasattr(settings, key): - setattr(settings, key, value) - logger.info(f"✅ Nacos 配置加载成功: {NACOS_DATA_ID} | 覆盖字段数量: {len(loaded)}") - else: - logger.warning("Nacos 返回配置为空,使用 .env + 默认值") - - # 2. 注册动态监听器(配置变更自动刷新) - async def listener(tenant: str, data_id: str, group: str, content: str): - global nacos_config_data, settings - try: - new_config = yaml.safe_load(content) if content else {} - nacos_config_data = new_config - # 实时覆盖 settings - for key, value in new_config.items(): - if hasattr(settings, key): - old_val = getattr(settings, key) - setattr(settings, key, value) - if old_val != value: - logger.info(f"🔄 配置更新 → {key}: {old_val} → {value}") - logger.info(f"【Nacos 动态更新】{NACOS_DATA_ID}") - except Exception as e: - logger.error(f"Nacos 配置解析失败: {e}") - - await client.add_listener(NACOS_DATA_ID, NACOS_GROUP, listener) - logger.info("✅ Nacos 配置监听器已注册(支持热更新)") - nacos_initialized_successfully = True - except Exception as e: - logger.error(f"❌ Nacos 初始化失败: {e}") - nacos_initialized_successfully = False - - -# 提供给 FastAPI 的依赖 -def get_settings() -> Settings: - return settings - - """Design 服务""" # 推荐服装类别映射 TABLE_CATEGORIES = { diff --git a/app/schemas/sketch_to_garment_schemas.py b/app/schemas/sketch_to_garment_schemas.py new file mode 100644 index 0000000..27c1c6d --- /dev/null +++ b/app/schemas/sketch_to_garment_schemas.py @@ -0,0 +1,12 @@ +from typing import List + +from pydantic import BaseModel, Field + + +class SketchToGarmentModel(BaseModel): + input_image_path: str = Field(..., description="输入图片路径列表") + bucket_name: str = Field(..., description="输入图片路径列表") + user_id: str = Field(..., description="用户id") + callback_url: str # 必填,客户端提供的回调地址 + task_id: str = Field() + model: str = Field(default="single", description="模型类型: single 或 multi") diff --git a/app/service/sketch2garment/callback.py b/app/service/sketch2garment/callback.py new file mode 100644 index 0000000..be3c32e --- /dev/null +++ b/app/service/sketch2garment/callback.py @@ -0,0 +1,35 @@ +import logging + +import httpx + +logger = logging.getLogger("app") + + +async def notify_callback(callback_url: str, task_id: str, status: str, result: dict, ): + """ + 调用客户端提供的回调接口 + """ + try: + payload = { + "task_id": task_id, + "status": status, + "result": result + } + logger.info(payload) + async with httpx.AsyncClient(timeout=30.0) as client: + resp = await client.post( + str(callback_url), + json=payload, + headers={"Content-Type": "application/json"} + ) + + if 200 <= resp.status_code < 300: + logger.info(f"回调成功 | task_id: {task_id} | status: {status} | url: {callback_url}") + return True + else: + logger.warning(f"回调返回非2xx状态码 | task_id: {task_id} | status: {resp.status_code} | url: {callback_url}") + return False + + except Exception as e: + logger.error(f"回调失败 | task_id: {task_id} | url: {callback_url} | error: {e}", exc_info=True) + return False diff --git a/app/service/sketch2garment/celery_app.py b/app/service/sketch2garment/celery_app.py new file mode 100644 index 0000000..0b3ed4d --- /dev/null +++ b/app/service/sketch2garment/celery_app.py @@ -0,0 +1,46 @@ +from celery import Celery +from kombu import Queue, Exchange + +from app.core.config import settings + +celery_app = Celery( + "sketch_to_garment", + broker=f"redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}/2", + backend=f"redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}", + include=["app.service.sketch2garment.tasks"] +) +print(f"redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}/3") +print(f"celery_app: {celery_app}") + +celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + timezone="Asia/Hong_Kong", + enable_utc=True, + task_track_started=True, + task_time_limit=300, # 单个任务最长 5 分钟 + task_soft_time_limit=280, + # 定义队列 + task_queues=( + Queue("sketch_to_garment_queue", + exchange=Exchange("sketch_to_garment_exchange", type="direct"), + durable=True), + + ), + + task_routes={ + 'app.service.sketch2garment.tasks.sketch_to_garment': + { + 'queue': 'sketch_to_garment_queue', + 'exchange': 'sketch_to_garment_exchange', # ← 修改这里 + }, + }, + task_default_queue="sketch_to_garment_queue", + + worker_concurrency=1, + worker_prefetch_multiplier=1, + worker_max_tasks_per_child=1, + task_acks_late=True, + task_reject_on_worker_lost=True, +) diff --git a/app/service/sketch2garment/server.py b/app/service/sketch2garment/server.py new file mode 100644 index 0000000..7adad23 --- /dev/null +++ b/app/service/sketch2garment/server.py @@ -0,0 +1,44 @@ +import logging + +from app.service.sketch2garment.tasks import sketch_to_garment + +logger = logging.getLogger(__name__) + + +def submit_sketch_to_garment_task(model: str = "single", task_id: str = "", callback_url: str = "", bucket_name: str = "test", user_id: str = "123", input_image_path: str = ""): + """提交 img_to_3D 任务(带队列长度限制)""" + queue_name = "img_to_3d_queue" + max_queue_length = 10 + + try: + # current_length = get_queue_length(queue_name) + + # if current_length >= max_queue_length: + # return { + # "state": "queue_full", + # "message": "当前 3D 生成请求较多,请稍后重试。", + # "queue_length": current_length, + # "max_length": max_queue_length + # } + + # 提交任务 + task = sketch_to_garment.apply_async( + args=(task_id, callback_url, bucket_name, input_image_path, user_id, model), + task_id=task_id, + queue="sketch_to_garment_queue") + + # logger.info(f"img_to_3d_task 已提交 | task_id: {task_id} | 当前队列长度: {current_length}") + + return { + "state": "success", + "task_id": task_id, + "message": "任务已成功提交,正在后台处理...", + } + + except Exception as e: + logger.error(f"提交 img_to_3d_task 失败: {e}", exc_info=True) + return { + "state": "fail", + "message": "提交失败,请稍后重试。", + "error": str(e) + } diff --git a/app/service/sketch2garment/tasks.py b/app/service/sketch2garment/tasks.py new file mode 100644 index 0000000..7700af6 --- /dev/null +++ b/app/service/sketch2garment/tasks.py @@ -0,0 +1,57 @@ +import asyncio +import logging + +from app.core.config import settings +from app.service.sketch2garment.callback import notify_callback +import httpx + +from app.service.sketch2garment.celery_app import celery_app + +logger = logging.getLogger(__name__) + + +@celery_app.task(bind=True, queue="sketch_to_garment_queue", max_retries=3, name='app.service.sketch2garment.tasks.sketch_to_garment') +def sketch_to_garment(self, task_id: str, callback_url: str, bucket_name: str, input_image_path: str, user_id: str, category: str = None): + payload = { + "bucket_name": bucket_name, + "category": category or settings.DEFAULT_CATEGORY, + "input_image_path": input_image_path, + "user_id": user_id + } + logger.info(f"payload: {payload}") + + try: + with httpx.Client(timeout=300.0) as client: # 注意这里用 AsyncClient 配合 Celery + # 如果你的 LitServe 是同步 endpoint,也可以用 httpx.Client() + response = client.post(settings.SKETCH_TO_GARMENT_URL, json=payload) + if response.status_code == 200: + result = response.json() + result_json = { + "pattern": result[1], + "texture": result[2], + "glb": result[3], + "texture_fabric": result[4] + } + asyncio.run( + notify_callback(callback_url=callback_url, task_id=task_id, result=result_json, status="success") + ) + else: + asyncio.run( + notify_callback( + callback_url=callback_url, + task_id=task_id, + result={ + "status": "fail", + "task_id": task_id, + "message": "fail", + "error": "fail" + }, + status="fail") + ) + except Exception as e: + return { + "status": "failed", + "task_id": task_id, + "input": payload, + "error": str(e) + }