3D 打板部署
All checks were successful
git commit AiDA python develop 分支构建部署 / scheduled_deploy (push) Has been skipped
All checks were successful
git commit AiDA python develop 分支构建部署 / scheduled_deploy (push) Has been skipped
This commit is contained in:
@@ -11,6 +11,7 @@ from app.api import api_precompute
|
|||||||
from app.api import api_prompt_generation
|
from app.api import api_prompt_generation
|
||||||
from app.api import api_recommendation
|
from app.api import api_recommendation
|
||||||
from app.api import api_test
|
from app.api import api_test
|
||||||
|
from app.api import api_sketch_to_garment
|
||||||
|
|
||||||
router = APIRouter()
|
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_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_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_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
|
# from app.api import api_chat_robot
|
||||||
|
|||||||
104
app/api/api_sketch_to_garment.py
Normal file
104
app/api/api_sketch_to_garment.py
Normal file
@@ -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))
|
||||||
@@ -87,6 +87,9 @@ class Settings(BaseSettings):
|
|||||||
A6000_SERVICE_HOST: str = Field(default='', description="")
|
A6000_SERVICE_HOST: str = Field(default='', description="")
|
||||||
B_4_X_4090_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容器内配置---
|
# --- 其他配置信息 以下均为Docker容器内配置---
|
||||||
LOGS_PATH: str = Field(default="/logs/", description="")
|
LOGS_PATH: str = Field(default="/logs/", description="")
|
||||||
CATEGORY_PATH: str = Field(default="/app/service/attribute/config/descriptor/category/category_dis.csv", 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()
|
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 服务"""
|
"""Design 服务"""
|
||||||
# 推荐服装类别映射
|
# 推荐服装类别映射
|
||||||
TABLE_CATEGORIES = {
|
TABLE_CATEGORIES = {
|
||||||
|
|||||||
12
app/schemas/sketch_to_garment_schemas.py
Normal file
12
app/schemas/sketch_to_garment_schemas.py
Normal file
@@ -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")
|
||||||
35
app/service/sketch2garment/callback.py
Normal file
35
app/service/sketch2garment/callback.py
Normal file
@@ -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
|
||||||
46
app/service/sketch2garment/celery_app.py
Normal file
46
app/service/sketch2garment/celery_app.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
44
app/service/sketch2garment/server.py
Normal file
44
app/service/sketch2garment/server.py
Normal file
@@ -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)
|
||||||
|
}
|
||||||
57
app/service/sketch2garment/tasks.py
Normal file
57
app/service/sketch2garment/tasks.py
Normal file
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user