652 lines
24 KiB
Python
652 lines
24 KiB
Python
import asyncio
|
||
import io
|
||
import json
|
||
import logging
|
||
import mimetypes
|
||
import os
|
||
import time
|
||
from pathlib import Path
|
||
from typing import Any, Dict, Iterator, Tuple, List
|
||
from urllib.parse import urlparse
|
||
|
||
import httpx
|
||
import numpy as np
|
||
import requests
|
||
import trimesh
|
||
from minio import Minio, S3Error
|
||
|
||
from src.core.config import settings
|
||
from src.schemas.generate_3D import Tripo3dApiModel
|
||
from src.server.canvas_generate_3D.callback import notify_callback
|
||
|
||
logger = logging.getLogger(__name__)
|
||
minio_client = Minio(settings.MINIO_URL, access_key=settings.MINIO_ACCESS, secret_key=settings.MINIO_SECRET, secure=settings.MINIO_SECURE)
|
||
|
||
|
||
class TripoAPIError(RuntimeError):
|
||
pass
|
||
|
||
|
||
class Triop3dApiServer:
|
||
def __init__(self):
|
||
self.base_url = "https://api.tripo3d.ai/v2/openapi"
|
||
|
||
async def _get_client(self) -> httpx.AsyncClient:
|
||
"""获取或创建异步客户端(懒加载)"""
|
||
self.async_client = httpx.AsyncClient(
|
||
timeout=httpx.Timeout(120.0), # 可根据需要调整
|
||
headers={
|
||
"Authorization": f"Bearer {settings.TRIPO_API_KEY}",
|
||
"Accept": "application/json"
|
||
},
|
||
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10)
|
||
)
|
||
return self.async_client
|
||
|
||
async def request_json(self, method: str, endpoint: str, request_timeout: float, **kwargs) -> Dict[str, Any]:
|
||
"""异步请求核心方法 - 直接返回原始 resp(成功或失败都不抛异常)"""
|
||
url = f"{self.base_url}{endpoint}"
|
||
client = await self._get_client()
|
||
|
||
try:
|
||
resp = await client.request(method=method, url=url, timeout=request_timeout, **kwargs)
|
||
except httpx.RequestError as e:
|
||
# 网络层错误也包装成类似 API 的格式返回
|
||
return {
|
||
"code": -1,
|
||
"message": f"网络请求失败: {method} {url}",
|
||
"detail": str(e)
|
||
}
|
||
|
||
try:
|
||
return resp.json()
|
||
except Exception:
|
||
# 非 JSON 返回也包装返回
|
||
return {
|
||
"code": -2,
|
||
"message": f"响应不是合法 JSON: HTTP {resp.status_code}",
|
||
"raw": resp.text[:500] # 截取一部分避免过长
|
||
}
|
||
|
||
async def upload_image(self, image_path: str, request_timeout: float) -> str:
|
||
"""
|
||
从 MinIO 读取图片 → 直接上传到 Tripo3D
|
||
|
||
Args:
|
||
image_path: MinIO 中的完整路径,例如 "fida-public-bucket/furniture/sketches/xxx.png"
|
||
或 "user_123/images/test.png"
|
||
request_timeout: 请求超时时间
|
||
|
||
Returns:
|
||
str: Tripo3D 返回的 image_token
|
||
"""
|
||
try:
|
||
# 解析 bucket 和 object_name
|
||
bucket_name, object_name = image_path.split('/', 1)
|
||
print(f"从 MinIO 下载图片: {bucket_name}/{object_name}")
|
||
logger.info(f"从 MinIO 下载图片: {bucket_name}/{object_name}")
|
||
|
||
# 1. 从 MinIO 获取文件
|
||
response = minio_client.get_object(bucket_name=bucket_name, object_name=object_name)
|
||
# 2. 读取为 bytes(关键修复点)
|
||
data = response.read()
|
||
file_name = Path(object_name).name
|
||
content_type = get_mime_type(file_name)
|
||
# 3. 用 BytesIO 包装(httpx 处理更稳定)
|
||
file_obj = io.BytesIO(data)
|
||
|
||
files = {
|
||
"file": (
|
||
file_name, # 文件名
|
||
file_obj, # BytesIO 对象
|
||
content_type
|
||
)
|
||
}
|
||
|
||
# 4. 异步上传
|
||
payload = await self.request_json(
|
||
"POST",
|
||
"/upload",
|
||
request_timeout=request_timeout,
|
||
files=files
|
||
)
|
||
data = payload.get("data") or {}
|
||
file_token = data.get("image_token")
|
||
|
||
if not file_token:
|
||
raise TripoAPIError(f"上传成功但未返回 image_token: {json.dumps(payload, ensure_ascii=False)}")
|
||
|
||
print(f"✅ 图片上传成功 | image_token: {file_token} | 文件: {file_name}")
|
||
logger.info(f"✅ 图片上传成功 | image_token: {file_token} | 文件: {file_name}")
|
||
|
||
return file_token
|
||
except Exception as e:
|
||
logger.error(f"上传图片失败 {image_path}: {e}")
|
||
raise
|
||
|
||
# ====================== 异步上传多张图片 ======================
|
||
async def upload_images(self, image_paths: List[str], request_timeout: float) -> List[str]:
|
||
"""
|
||
批量从 MinIO 上传多张图片到 Tripo3D
|
||
|
||
Args:
|
||
image_paths: MinIO 对象路径列表
|
||
request_timeout: 请求超时时间
|
||
|
||
Returns:
|
||
List[str]: Tripo3D 返回的 image_token 列表
|
||
"""
|
||
file_tokens = []
|
||
|
||
for idx, image_path in enumerate(image_paths, 1):
|
||
print(f" - 上传第 {idx}/{len(image_paths)} 张图片: {image_path}")
|
||
logger.info(f" - 上传第 {idx}/{len(image_paths)} 张图片: {image_path}")
|
||
|
||
token = await self.upload_image(
|
||
image_path=image_path,
|
||
request_timeout=request_timeout
|
||
)
|
||
|
||
file_tokens.append(token)
|
||
|
||
print(f"✅ 所有图片上传完成,共 {len(file_tokens)} 张")
|
||
logger.info(f"✅ 所有图片上传完成,共 {len(file_tokens)} 张")
|
||
|
||
return file_tokens
|
||
|
||
async def create_task(self, payload: Dict[str, Any], request_timeout: float) -> Dict[str, Any]:
|
||
"""
|
||
创建任务
|
||
- 成功时返回原始响应(包含 code: 0 和 data.task_id)
|
||
- 失败时也返回原始响应,并确保错误码(code)被带上
|
||
"""
|
||
resp = await self.request_json("POST", "/task", request_timeout=request_timeout, json=payload)
|
||
|
||
# 如果是成功响应(通常 code == 0),直接返回
|
||
if isinstance(resp, dict) and resp.get("code") == 0:
|
||
return resp
|
||
|
||
# 失败情况:确保错误码存在,并返回完整响应(不抛异常)
|
||
if not isinstance(resp, dict):
|
||
resp = {"code": -3, "message": "未知错误", "raw_response": str(resp)}
|
||
|
||
# 如果响应中没有 code 字段,补充一个
|
||
if "code" not in resp:
|
||
resp["code"] = resp.get("error", {}).get("code") or -999
|
||
|
||
# 可选:统一加上一个更明显的错误标识(方便上层判断)
|
||
if resp.get("code") != 0:
|
||
resp.setdefault("success", False)
|
||
# 如果有 suggestion,可以保留
|
||
if "suggestion" not in resp and isinstance(resp.get("error"), dict):
|
||
resp["suggestion"] = resp["error"].get("suggestion")
|
||
|
||
return resp
|
||
|
||
# step 3 查询任务状态
|
||
async def poll_task(self, task_id: str, poll_interval: float, poll_timeout: float, request_timeout: float, callback_url: str) -> Dict[str, Any]:
|
||
start = asyncio.get_running_loop().time()
|
||
last_line = ""
|
||
|
||
while True:
|
||
resp = await self.request_json("GET", f"/task/{task_id}", request_timeout=request_timeout)
|
||
data = resp.get("data") or {}
|
||
|
||
status = str(data.get("status", "unknown")).lower()
|
||
progress = data.get("progress", 0)
|
||
elapsed = asyncio.get_running_loop().time() - start
|
||
|
||
line = f"[状态] {status:<10} | [进度] {progress:>3}% | [已等待] {elapsed:>7.1f}s"
|
||
if line != last_line:
|
||
logger.info(line)
|
||
print(line)
|
||
|
||
last_line = line
|
||
|
||
if status == "success":
|
||
return resp
|
||
|
||
if status == "failed":
|
||
await notify_callback(callback_url=callback_url, task_id=task_id, status="failed", result={})
|
||
error_message = data.get("error_message") or extract_error_message(resp)
|
||
raise TripoAPIError(f"任务失败 | task_id={task_id} | {error_message}")
|
||
|
||
if elapsed > poll_timeout:
|
||
await notify_callback(callback_url=callback_url, task_id=task_id, status="failed", result={})
|
||
raise TimeoutError(f"轮询超时: 已等待 {elapsed:.1f}s,task_id={task_id}")
|
||
|
||
await asyncio.sleep(poll_interval)
|
||
|
||
# step 4 上传结果
|
||
async def save_outputs(self, task_resp: Dict[str, Any], request_timeout: float, bucket_name: str, user_id: str):
|
||
data = task_resp.get("data") or {}
|
||
task_id = data.get("task_id", "unknown_task")
|
||
result = data.get("result") or {}
|
||
|
||
print("\n📥 开始异步处理并上传输出文件...")
|
||
logger.info("\n📥 开始异步处理并上传输出文件...")
|
||
|
||
outputs = {}
|
||
for key, value in result.items():
|
||
if not isinstance(value, dict) or 'url' not in value:
|
||
continue
|
||
|
||
url = value['url']
|
||
parsed = urlparse(url)
|
||
path = Path(parsed.path.split('?')[0])
|
||
ext = path.suffix.lower() or ".bin"
|
||
|
||
object_name = f"{user_id}/3d_result/{task_id}{ext}"
|
||
|
||
# 异步上传到 MinIO
|
||
await upload_file_to_minio_from_url_async(
|
||
url=url,
|
||
bucket_name=bucket_name,
|
||
object_name=object_name,
|
||
request_timeout=request_timeout
|
||
)
|
||
|
||
if value.get('type') == "glb":
|
||
outputs['glb_path'] = f"{bucket_name}/{object_name}"
|
||
elif value.get('type') == "webp":
|
||
outputs['glb_static_img_path'] = f"{bucket_name}/{object_name}"
|
||
else:
|
||
outputs[value.get('type', key)] = f"{bucket_name}/{object_name}"
|
||
|
||
# 异步分析 GLB 模型(CPU密集型任务)
|
||
if 'glb_path' in outputs:
|
||
glb_info = await analyze_mesh_async(outputs['glb_path'])
|
||
outputs['glb_info'] = glb_info
|
||
|
||
# outputs = {
|
||
# 'glb_path': 'test/3d_result/glb/aea689fd4ee14f53ac9ab0922f9fe5b3.glb',
|
||
# 'glb_static_img_path': 'test/3d_result/png/26a7fa7ca48641348847c1f4bca353db.png',
|
||
# 'glb_info': {'file_format': '.glb', 'vertex_count': 5275, 'centroid': [0.0044253334706297175, -0.01139796154609474, -0.06385942913980143], 'bounding_box_min': [-0.500163733959198, -0.18078294396400452, -0.29821905493736267], 'bounding_box_max': [0.49963313341140747, 0.17052923142910004, 0.3003925383090973], 'size': [0.9997968673706055, 0.35131217539310455, 0.59861159324646], 'size_ratio': [0.5127898063471029, 0.1801859040236737, 0.30702428962922335],
|
||
# 'size_ratio_percentage': [51.278980634710294, 18.01859040236737, 30.702428962922333]}}
|
||
return outputs
|
||
|
||
async def call_back_result(self, callback_url: str, result: Dict, task_id: str):
|
||
await notify_callback(
|
||
callback_url=callback_url,
|
||
task_id=task_id,
|
||
status="completed",
|
||
result=result,
|
||
)
|
||
return "ok"
|
||
|
||
|
||
async def upload_file_to_minio_from_url_async(url: str, bucket_name: str, object_name: str, request_timeout: float = 60.0, content_type: str = None):
|
||
"""
|
||
异步从 Tripo URL 下载文件并上传到 MinIO(最终修复版)
|
||
"""
|
||
try:
|
||
async with httpx.AsyncClient(timeout=request_timeout) as client:
|
||
async with client.stream("GET", url) as resp:
|
||
resp.raise_for_status()
|
||
|
||
# 正确方式:先读取所有内容为 bytes
|
||
data_bytes = await resp.aread()
|
||
|
||
if content_type is None:
|
||
content_type = get_mime_type(object_name)
|
||
|
||
# 关键修复:用 BytesIO 包装 bytes,让它拥有 .read() 方法
|
||
file_obj = io.BytesIO(data_bytes)
|
||
|
||
logger.info(f"开始上传到 MinIO → {bucket_name}/{object_name} | 大小: {len(data_bytes):,} bytes")
|
||
|
||
# 上传到 MinIO
|
||
result = minio_client.put_object(
|
||
bucket_name=bucket_name,
|
||
object_name=object_name,
|
||
data=file_obj, # ← 必须传 BytesIO 或有 .read() 的对象
|
||
length=len(data_bytes),
|
||
content_type=content_type,
|
||
part_size=0 # 自动分片
|
||
)
|
||
|
||
logger.info(f"✅ 成功上传到 MinIO: {bucket_name}/{object_name}")
|
||
return result
|
||
|
||
except httpx.HTTPError as e:
|
||
raise TripoAPIError(f"下载 Tripo 文件失败: {url} | {e}") from e
|
||
except S3Error as e:
|
||
raise TripoAPIError(f"上传到 MinIO 失败: {bucket_name}/{object_name} | {e}") from e
|
||
except Exception as e:
|
||
raise TripoAPIError(f"上传过程异常 {url}: {e}") from e
|
||
|
||
|
||
async def analyze_mesh_async(image_path: str) -> Dict:
|
||
"""异步包装 analyze_mesh(CPU密集型)"""
|
||
loop = asyncio.get_running_loop()
|
||
return await loop.run_in_executor(None, analyze_mesh_sync, image_path)
|
||
|
||
|
||
def analyze_mesh_sync(image_path: str):
|
||
"""同步版本(供 executor 调用)"""
|
||
bucket_name, object_name = image_path.split('/', 1)
|
||
vertices = load_mesh_from_minio(bucket_name=bucket_name, object_name=object_name)
|
||
|
||
min_coords = vertices.min(axis=0)
|
||
max_coords = vertices.max(axis=0)
|
||
centroid = vertices.mean(axis=0)
|
||
size = max_coords - min_coords
|
||
|
||
total_size = np.sum(size)
|
||
size_ratio = size / total_size if total_size != 0 else np.zeros(3)
|
||
|
||
return {
|
||
"file_format": os.path.splitext(image_path)[1].lower(),
|
||
"vertex_count": len(vertices),
|
||
"centroid": centroid.tolist(),
|
||
"bounding_box_min": min_coords.tolist(),
|
||
"bounding_box_max": max_coords.tolist(),
|
||
"size": size.tolist(),
|
||
"size_ratio": size_ratio.tolist(),
|
||
"size_ratio_percentage": (size_ratio * 100).tolist()
|
||
}
|
||
|
||
|
||
def get_mime_type(path):
|
||
mime, _ = mimetypes.guess_type(str(path))
|
||
return mime or "application/octet-stream"
|
||
|
||
|
||
def extract_error_message(payload: Any) -> str:
|
||
if isinstance(payload, dict):
|
||
for key in ("message", "error", "error_message", "detail", "suggestion"):
|
||
if payload.get(key):
|
||
return str(payload[key])
|
||
|
||
data = payload.get("data")
|
||
if isinstance(data, dict):
|
||
for key in ("message", "error", "error_message", "detail", "suggestion"):
|
||
if data.get(key):
|
||
return str(data[key])
|
||
|
||
return json.dumps(payload, ensure_ascii=False)[:800]
|
||
|
||
return str(payload)[:800]
|
||
|
||
|
||
def iter_urls(obj: Any, prefix: str = "output") -> Iterator[Tuple[str, str]]:
|
||
if isinstance(obj, dict):
|
||
for k, v in obj.items():
|
||
yield from iter_urls(v, f"{prefix}.{k}")
|
||
elif isinstance(obj, list):
|
||
for i, v in enumerate(obj):
|
||
yield from iter_urls(v, f"{prefix}[{i}]")
|
||
elif isinstance(obj, str) and obj.startswith(("http://", "https://")):
|
||
yield prefix, obj
|
||
|
||
|
||
def upload_file_to_minio_from_url(session: requests.Session, url: str, bucket_name: str, object_name: str, request_timeout: float = 30.0, content_type: str = "application/octet-stream"):
|
||
"""
|
||
从 URL 下载文件流,直接上传到 MinIO,不落地本地
|
||
"""
|
||
try:
|
||
with session.get(url, stream=True, timeout=request_timeout) as resp:
|
||
resp.raise_for_status()
|
||
|
||
# 获取文件大小(如果服务器返回 Content-Length)
|
||
content_length = int(resp.headers.get('Content-Length', 0))
|
||
|
||
# 如果无法获取长度,可以设为 -1(MinIO 会自动处理分块上传)
|
||
length = content_length if content_length > 0 else -1
|
||
|
||
# 直接把 response.raw 传给 put_object(最推荐的流式方式)
|
||
result = minio_client.put_object( # 假设你的 MinIO 客户端是 self.minio_client
|
||
bucket_name=bucket_name,
|
||
object_name=object_name,
|
||
data=resp.raw, # 关键:直接传 raw stream
|
||
length=length,
|
||
content_type=content_type,
|
||
part_size=0 # 0 表示让 MinIO 自动选择合适的分片大小
|
||
)
|
||
|
||
except requests.RequestException as e:
|
||
raise TripoAPIError(f"下载失败: {url} | {e}") from e
|
||
except S3Error as e:
|
||
raise TripoAPIError(f"上传到 MinIO 失败: {bucket_name}/{object_name} | {e}") from e
|
||
|
||
return result
|
||
|
||
|
||
def analyze_mesh(image_path: str):
|
||
# 加载模型顶点(直接从 MinIO)
|
||
bucket_name, object_name = image_path.split('/', 1)
|
||
vertices = load_mesh_from_minio(bucket_name=bucket_name, object_name=object_name)
|
||
|
||
# 计算各项指标
|
||
min_coords = vertices.min(axis=0)
|
||
max_coords = vertices.max(axis=0)
|
||
centroid = vertices.mean(axis=0)
|
||
size = max_coords - min_coords
|
||
|
||
total_size = np.sum(size)
|
||
size_ratio = size / total_size if total_size != 0 else np.zeros(3)
|
||
|
||
info = {
|
||
"file_format": os.path.splitext(image_path)[1].lower(),
|
||
"vertex_count": len(vertices),
|
||
"centroid": centroid.tolist(),
|
||
"bounding_box_min": min_coords.tolist(),
|
||
"bounding_box_max": max_coords.tolist(),
|
||
"size": size.tolist(),
|
||
"size_ratio": size_ratio.tolist(),
|
||
"size_ratio_percentage": (size_ratio * 100).tolist()
|
||
}
|
||
|
||
return info
|
||
|
||
|
||
def load_mesh_from_minio(object_name: str, bucket_name: str = "fida-user"):
|
||
"""
|
||
从 MinIO 直接加载 .glb / .gltf / .obj 文件,返回顶点数组
|
||
"""
|
||
try:
|
||
# 从 MinIO 获取文件流
|
||
response = minio_client.get_object(bucket_name, object_name)
|
||
|
||
# 读取为 bytes 并包装成 BytesIO
|
||
data = response.read()
|
||
file_obj = io.BytesIO(data)
|
||
|
||
file_ext = os.path.splitext(object_name)[1].lower()
|
||
|
||
# 根据后缀加载模型
|
||
if file_ext in ('.glb', '.gltf'):
|
||
mesh = trimesh.load(file_obj, file_type='glb')
|
||
elif file_ext == '.obj':
|
||
mesh = trimesh.load(file_obj, file_type='obj')
|
||
else:
|
||
raise ValueError(f"不支持的文件格式: {file_ext},仅支持 .obj 和 .glb/.gltf")
|
||
|
||
except S3Error as e:
|
||
raise RuntimeError(f"从 MinIO 获取模型失败: {object_name} | {e}") from e
|
||
except Exception as e:
|
||
raise RuntimeError(f"加载模型失败: {object_name} | {e}") from e
|
||
|
||
# 处理 Scene 或单个 Mesh
|
||
if isinstance(mesh, trimesh.Scene):
|
||
vertices = np.vstack([geom.vertices for geom in mesh.geometry.values()])
|
||
else:
|
||
vertices = mesh.vertices
|
||
|
||
if len(vertices) == 0:
|
||
raise ValueError(f"模型中未找到顶点数据: {object_name}")
|
||
|
||
return vertices
|
||
|
||
|
||
async def create_single_task(input_data: Tripo3dApiModel):
|
||
"""
|
||
异步版本:创建单个图片转 3D 的任务
|
||
"""
|
||
server = Triop3dApiServer()
|
||
|
||
# Step 1: 上传图片(异步)
|
||
print(f"开始上传图片: {input_data.input_images[0]}")
|
||
logger.info(f"开始上传图片: {input_data.input_images[0]}")
|
||
|
||
file_token = await server.upload_image(
|
||
image_path=input_data.input_images[0],
|
||
request_timeout=input_data.request_timeout
|
||
)
|
||
print(f"✅ 图片上传成功,file_token: {file_token}")
|
||
logger.info(f"✅ 图片上传成功,file_token: {file_token}")
|
||
|
||
# Step 2: 构建请求 payload
|
||
file_ext = Path(input_data.input_images[0]).suffix.lower().lstrip('.') or "png"
|
||
if file_ext == "jpeg":
|
||
file_ext = "jpg"
|
||
|
||
input_payload = {
|
||
"type": "image_to_model",
|
||
"file": {
|
||
"type": file_ext,
|
||
"file_token": file_token,
|
||
}
|
||
}
|
||
|
||
# 合并用户传入的参数(Pydantic Model 转 dict)
|
||
payload = input_payload | input_data.model_dump(exclude_unset=True)
|
||
|
||
# Step 3: 提交任务(异步)
|
||
logger.info("正在提交 Tripo3D 任务...")
|
||
|
||
resp = await server.create_task(
|
||
payload=payload,
|
||
request_timeout=input_data.request_timeout
|
||
)
|
||
|
||
return resp
|
||
|
||
|
||
async def create_multi_task(input_data: Tripo3dApiModel):
|
||
"""
|
||
异步版本:创建多图转 3D 的任务
|
||
"""
|
||
server = Triop3dApiServer()
|
||
|
||
# Step 1: 上传多张图片(异步)
|
||
logger.info(f"开始上传 {len(input_data.input_images)} 张图片...")
|
||
print(f"开始上传 {len(input_data.input_images)} 张图片...")
|
||
file_tokens = await server.upload_images(
|
||
image_paths=input_data.input_images,
|
||
request_timeout=input_data.request_timeout
|
||
)
|
||
logger.info(f"✅ 图片上传完成,共 {len(file_tokens)} 个 token")
|
||
print(f"✅ 图片上传完成,共 {len(file_tokens)} 个 token")
|
||
|
||
# Step 2: 构建多图 payload
|
||
files = []
|
||
for image_path, file_token in zip(input_data.input_images, file_tokens):
|
||
file_ext = Path(image_path).suffix.lower().lstrip('.') or "png"
|
||
if file_ext == "jpeg":
|
||
file_ext = "jpg"
|
||
|
||
files.append({
|
||
"type": file_ext,
|
||
"file_token": file_token,
|
||
})
|
||
while len(files) < 4:
|
||
files.append({})
|
||
|
||
if len(files) > 4:
|
||
files = files[:4]
|
||
|
||
payload: Dict[str, Any] = {
|
||
"type": "multiview_to_model",
|
||
"model_version": input_data.model_version,
|
||
"files": files,
|
||
"face_limit": 2000,
|
||
"texture": input_data.texture,
|
||
"pbr": input_data.pbr,
|
||
}
|
||
# Step 3: 提交任务(异步)
|
||
logger.info(f"正在提交多图 Tripo3D 任务...{payload}")
|
||
print(f"正在提交多图 Tripo3D 任务...{payload}")
|
||
resp = await server.create_task(payload=payload, request_timeout=input_data.request_timeout)
|
||
|
||
return resp
|
||
|
||
|
||
async def get_task_result_async(input_data: Tripo3dApiModel, task_id: str, api_task_id: str, callback_url: str):
|
||
server = Triop3dApiServer()
|
||
task_resp = await server.poll_task(
|
||
task_id=api_task_id,
|
||
poll_interval=input_data.poll_interval,
|
||
poll_timeout=input_data.poll_timeout,
|
||
request_timeout=input_data.request_timeout,
|
||
callback_url=callback_url
|
||
)
|
||
outputs = await server.save_outputs(
|
||
task_resp=task_resp,
|
||
request_timeout=input_data.request_timeout,
|
||
bucket_name=input_data.bucket_name,
|
||
user_id=input_data.user_id
|
||
)
|
||
|
||
print(f"tripo3d 任务处理完成 | api_task_id: {api_task_id} | status: success")
|
||
logger.info(f"tripo3d 任务处理完成 | api_task_id: {api_task_id} | status: success")
|
||
await server.call_back_result(callback_url, outputs, task_id)
|
||
|
||
|
||
async def single_img_to_model_async(input_data: Tripo3dApiModel):
|
||
"""
|
||
完整的单图转 3D 异步流程
|
||
"""
|
||
try:
|
||
# Step 1: 创建任务
|
||
task_id = await create_single_task(input_data)
|
||
|
||
# Step 2: 轮询任务状态 + 处理输出 + 回调
|
||
await get_task_result_async(input_data, task_id, input_data.callback_url)
|
||
return task_id
|
||
|
||
except Exception as e:
|
||
logger.error(f"单图转 3D 任务失败 | error: {e}", exc_info=True)
|
||
# 可在此处调用失败回调
|
||
await notify_callback(
|
||
callback_url=input_data.callback_url,
|
||
task_id="unknown",
|
||
status="failed",
|
||
result={"error": str(e)}
|
||
)
|
||
raise
|
||
|
||
|
||
async def multi_img_to_model_async(input_data: Tripo3dApiModel):
|
||
"""
|
||
完整的多图转 3D 异步流程
|
||
"""
|
||
try:
|
||
# Step 1: 创建多图任务
|
||
task_id = await create_multi_task(input_data)
|
||
|
||
# Step 2: 轮询任务 + 处理输出 + 回调
|
||
await get_task_result_async(input_data, task_id, input_data.callback_url)
|
||
|
||
except Exception as e:
|
||
logger.error(f"多图转 3D 任务失败 | error: {e}", exc_info=True)
|
||
# 失败回调
|
||
await notify_callback(
|
||
callback_url=input_data.callback_url,
|
||
task_id="unknown",
|
||
status="failed",
|
||
result={"error": str(e)}
|
||
)
|
||
raise
|
||
|
||
|
||
if __name__ == '__main__':
|
||
# input_data = Tripo3dApiModel(input_images=['test/img_to_3d_data/example_multi_image/mushroom_1.png'], bucket_name='test', user_id='test', callback_url="http://18.167.251.121:10015/api/image/webhook/img-to-3d")
|
||
# asyncio.run(single_img_to_model_async(input_data))
|
||
input_data = Tripo3dApiModel(
|
||
input_images=['test/img_to_3d_data/example_multi_image/mushroom_3.png', 'test/img_to_3d_data/example_multi_image/mushroom_2.png', 'test/img_to_3d_data/example_multi_image/mushroom_1.png'],
|
||
bucket_name='test', user_id='test', callback_url="http://18.167.251.121:10015/api/image/webhook/img-to-3d",
|
||
face_limit=4000
|
||
)
|
||
asyncio.run(multi_img_to_model_async(input_data))
|