Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b416a743f5 | ||
|
|
671fe2f55a | ||
|
|
61b7a22df2 |
No files matched your search
@@ -8,16 +8,53 @@
|
|||||||
|
|
||||||
## 一、安装与更新
|
## 一、安装与更新
|
||||||
|
|
||||||
### 1. 安装为全局命令(推荐)
|
### 1. 一行命令安装(推荐,免装 Python)
|
||||||
|
|
||||||
|
自动识别系统与架构,拉取最新 Release 的独立可执行文件并装进 PATH:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
curl -fsSL https://git.usetoks.com/Mrpzx/tripocli/raw/branch/main/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
安装到 `/usr/local/bin`(可写时)或 `~/.local/bin`。装完直接可用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tripocli --version
|
||||||
|
tripocli --help
|
||||||
|
```
|
||||||
|
|
||||||
|
可选参数(环境变量):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 指定版本
|
||||||
|
curl -fsSL .../install.sh | TRIPOCLI_VERSION=v2.0.0 sh
|
||||||
|
# 指定安装目录
|
||||||
|
curl -fsSL .../install.sh | TRIPOCLI_INSTALL_DIR=~/.local/bin sh
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以手动下载:到 [Releases](https://git.usetoks.com/Mrpzx/tripocli/releases) 取对应平台的文件,`chmod +x` 后直接运行。
|
||||||
|
|
||||||
|
| 平台 | 文件 |
|
||||||
|
|---|---|
|
||||||
|
| Linux x86_64 | `tripocli-<版本>-linux-x86_64` |
|
||||||
|
| macOS(Apple Silicon) | `tripocli-<版本>-macos-arm64` |
|
||||||
|
|
||||||
|
> Windows 暂无独立可执行文件(PyInstaller 无法跨平台编译),请用下面的 pip 方式。
|
||||||
|
|
||||||
|
### 2. 用 pip 安装(需要 Python ≥ 3.8)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 直接从 Gitea 仓库安装,无需先克隆
|
||||||
|
pip install "git+https://git.usetoks.com/Mrpzx/tripocli.git"
|
||||||
|
|
||||||
|
# 或在克隆后的项目目录里安装
|
||||||
cd <项目目录>
|
cd <项目目录>
|
||||||
pip install .
|
pip install .
|
||||||
pyenv rehash # 使用 pyenv 时执行一次以刷新命令路径
|
pyenv rehash # 使用 pyenv 时执行一次以刷新命令路径
|
||||||
tripocli --version
|
tripocli --version
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 免安装直接运行
|
### 3. 免安装直接运行
|
||||||
|
|
||||||
无需 `pip`,直接运行仓库内的入口脚本:
|
无需 `pip`,直接运行仓库内的入口脚本:
|
||||||
|
|
||||||
@@ -25,9 +62,11 @@ tripocli --version
|
|||||||
<项目目录>/tripocli --version
|
<项目目录>/tripocli --version
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 更新
|
### 4. 更新
|
||||||
|
|
||||||
修改或拉取新源码后,一条命令重装全局 CLI:
|
- 用安装脚本装的:**重跑一遍安装命令**即可(自动取最新版本);
|
||||||
|
- 用 pip 装的:`pip install -U "git+https://git.usetoks.com/Mrpzx/tripocli.git"`;
|
||||||
|
- 从源码跑且已 `pip install .` 的,可用内置命令从源码更新:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tripocli update # 从项目源码更新全局命令
|
tripocli update # 从项目源码更新全局命令
|
||||||
@@ -110,10 +149,16 @@ tripocli project <project_id> # 查看项目详情、模型直链与已重定
|
|||||||
### 3. 生成命令
|
### 3. 生成命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 文生图(支持模型: nano-banana-2 / nano-banana-pro / gpt-image-2 / gpt-image-2.5 / midjourney)
|
# 文生图(免会员默认模型: nanobanana / nano-banana-2;会员模型: nano-banana-pro / gpt-image-2 / midjourney)
|
||||||
tripocli img -p "a stylized treasure chest, game asset" --amount 1 --scale 1:1 --resolution 2K --wait -o ./refs
|
tripocli img -p "a stylized treasure chest, game asset" --amount 1 --scale 1:1 --resolution 2K --wait -o ./refs
|
||||||
|
tripocli img -p "anime knight" -m nanobanana --t-pose --wait -o ./knight.png
|
||||||
tripocli img --templates # 查看官方提示词模板
|
tripocli img --templates # 查看官方提示词模板
|
||||||
|
|
||||||
|
# 图片 4K 超分放大与智能抠图
|
||||||
|
tripocli img-upscale -a <asset_id> --wait -o ./4k.png # 4K 高清放大(对应网页 4K 按钮)
|
||||||
|
tripocli img-split -a <asset_id> --wait -o ./cutout.webp # 智能抠图透明背景(对应网页剪刀)
|
||||||
|
tripocli imgs --delete <asset_id> # 删除图片资产(对应网页垃圾桶)
|
||||||
|
|
||||||
# 单图转 4 视角参考图
|
# 单图转 4 视角参考图
|
||||||
tripocli imgmv -i ./refs/image_0.png --wait -o ./views
|
tripocli imgmv -i ./refs/image_0.png --wait -o ./views
|
||||||
|
|
||||||
|
|||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# tripocli 一行安装脚本(Linux / macOS,无需预装 Python)
|
||||||
|
#
|
||||||
|
# curl -fsSL https://git.usetoks.com/Mrpzx/tripocli/raw/branch/main/install.sh | sh
|
||||||
|
#
|
||||||
|
# 可选环境变量:
|
||||||
|
# TRIPOCLI_VERSION=v2.0.0 指定版本(默认取最新 release)
|
||||||
|
# TRIPOCLI_INSTALL_DIR=/usr/local/bin 指定安装目录
|
||||||
|
#
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REPO="Mrpzx/tripocli"
|
||||||
|
HOST="${TRIPOCLI_HOST:-https://git.usetoks.com}"
|
||||||
|
BIN="tripocli"
|
||||||
|
|
||||||
|
info() { printf '%s\n' "$*"; }
|
||||||
|
die() { printf '错误: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
need() { command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1"; }
|
||||||
|
need curl
|
||||||
|
need uname
|
||||||
|
|
||||||
|
# ---- 1) 识别平台 -----------------------------------------------------------
|
||||||
|
os=$(uname -s)
|
||||||
|
arch=$(uname -m)
|
||||||
|
case "$os" in
|
||||||
|
Linux) os_tag="linux" ;;
|
||||||
|
Darwin) os_tag="macos" ;;
|
||||||
|
*) die "暂不支持的系统:$os(Windows 请用:pip install git+$HOST/$REPO.git)" ;;
|
||||||
|
esac
|
||||||
|
case "$arch" in
|
||||||
|
x86_64|amd64) arch_tag="x86_64" ;;
|
||||||
|
arm64|aarch64) arch_tag="arm64" ;;
|
||||||
|
*) die "暂不支持的架构:$arch" ;;
|
||||||
|
esac
|
||||||
|
info "==> 平台:$os_tag / $arch_tag"
|
||||||
|
|
||||||
|
# ---- 2) 解析版本(默认最新 release)----------------------------------------
|
||||||
|
version="${TRIPOCLI_VERSION:-}"
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
info "==> 查询最新版本…"
|
||||||
|
version=$(curl -fsSL "$HOST/api/v1/repos/$REPO/releases/latest" 2>/dev/null \
|
||||||
|
| tr ',' '\n' | sed -n 's/.*"tag_name":"\([^"]*\)".*/\1/p' | head -1)
|
||||||
|
[ -n "$version" ] || die "无法获取最新版本,可用 TRIPOCLI_VERSION=v2.0.0 指定"
|
||||||
|
fi
|
||||||
|
# 资产名里版本号不带前导 v(tag=v2.0.0 → 资产 tripocli-2.0.0-…)
|
||||||
|
ver_num="${version#v}"
|
||||||
|
asset="${BIN}-${ver_num}-${os_tag}-${arch_tag}"
|
||||||
|
url="$HOST/$REPO/releases/download/$version/$asset"
|
||||||
|
info "==> 版本:$version"
|
||||||
|
|
||||||
|
# ---- 3) 下载 ---------------------------------------------------------------
|
||||||
|
tmp="$(mktemp "${TMPDIR:-/tmp}/tripocli.XXXXXX")"
|
||||||
|
trap 'rm -f "$tmp"' EXIT INT TERM
|
||||||
|
info "==> 下载:$asset"
|
||||||
|
curl -fSL -sS -o "$tmp" "$url" || die "下载失败:$url"
|
||||||
|
chmod +x "$tmp"
|
||||||
|
# 简单校验:确认是可执行文件而不是错误页面
|
||||||
|
size=$(wc -c < "$tmp" | tr -d ' ')
|
||||||
|
[ "$size" -gt 1000000 ] || die "下载内容异常(仅 ${size} 字节),可能是版本或平台不存在"
|
||||||
|
|
||||||
|
# ---- 4) 安装 ---------------------------------------------------------------
|
||||||
|
dir="${TRIPOCLI_INSTALL_DIR:-}"
|
||||||
|
if [ -z "$dir" ]; then
|
||||||
|
if [ -w /usr/local/bin ] 2>/dev/null; then
|
||||||
|
dir=/usr/local/bin
|
||||||
|
else
|
||||||
|
dir="$HOME/.local/bin"
|
||||||
|
mkdir -p "$dir"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -d "$dir" ] || mkdir -p "$dir" 2>/dev/null || die "无法创建安装目录:$dir"
|
||||||
|
mv "$tmp" "$dir/$BIN"
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
chmod +x "$dir/$BIN"
|
||||||
|
info "==> 已安装到:$dir/$BIN"
|
||||||
|
|
||||||
|
# ---- 5) 校验 & 提示 --------------------------------------------------------
|
||||||
|
if command -v "$BIN" >/dev/null 2>&1; then
|
||||||
|
info "==> 安装成功:$("$BIN" --version 2>/dev/null || echo "$BIN")"
|
||||||
|
else
|
||||||
|
info "==> 安装成功,但 $dir 不在 PATH 里。请把下面这行加到 ~/.zshrc 或 ~/.bashrc:"
|
||||||
|
info " export PATH=\"$dir:\$PATH\""
|
||||||
|
fi
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
下一步:
|
||||||
|
1) 从浏览器复制 Tripo 的 Cookie(ory_kratos_session)
|
||||||
|
2) tripocli account add --cookie "ory_kratos_session=..."
|
||||||
|
3) tripocli balance # 验证连通
|
||||||
|
4) tripocli --help # 查看全部命令
|
||||||
|
|
||||||
|
也可以跑 tripocli serve,在本地网页里管理账号。
|
||||||
|
EOF
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tripocli"
|
name = "tripocli"
|
||||||
version = "2.0.0"
|
version = "2.1.0"
|
||||||
description = "Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯标准库零依赖)"
|
description = "Tripo AI 3D 生成与加工 CLI(面向 Agent 与开发者,纯标准库零依赖)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@ file. ``tripo/client.py`` holds the HTTP plumbing, ``tripo/config.py`` the
|
|||||||
credentials, and ``tripo/cli.py`` wires the feature modules into an argparse CLI.
|
credentials, and ``tripo/cli.py`` wires the feature modules into an argparse CLI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.1.0"
|
||||||
+1
-1
@@ -30,7 +30,7 @@ def _request(api_base, cookie, device_id=None, timeout=30):
|
|||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Origin": ORIGIN,
|
"Origin": ORIGIN,
|
||||||
"Referer": ORIGIN + "/",
|
"Referer": ORIGIN + "/",
|
||||||
"User-Agent": "tripocli/2.0.0",
|
"User-Agent": "tripocli/2.1.0",
|
||||||
"Cookie": cookie,
|
"Cookie": cookie,
|
||||||
}
|
}
|
||||||
if device_id:
|
if device_id:
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ RETRY_ATTEMPTS = 4
|
|||||||
RETRY_BACKOFF = 1.5
|
RETRY_BACKOFF = 1.5
|
||||||
|
|
||||||
ORIGIN = "https://studio.tripo3d.ai"
|
ORIGIN = "https://studio.tripo3d.ai"
|
||||||
USER_AGENT = "tripocli/2.0.0 (Mozilla/5.0; AI Agent Client)"
|
USER_AGENT = "tripocli/2.1.0 (Mozilla/5.0; AI Agent Client)"
|
||||||
|
|
||||||
# Model formats the studio's "上传 3D 模型" importer accepts (UI: OBJ/FBX/STL/GLB ≤150MB).
|
# Model formats the studio's "上传 3D 模型" importer accepts (UI: OBJ/FBX/STL/GLB ≤150MB).
|
||||||
MODEL_FORMATS = ("glb", "obj", "fbx", "stl")
|
MODEL_FORMATS = ("glb", "obj", "fbx", "stl")
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ from . import (
|
|||||||
image_assets,
|
image_assets,
|
||||||
image_gen,
|
image_gen,
|
||||||
image_multiview,
|
image_multiview,
|
||||||
|
image_split,
|
||||||
image_to_3d,
|
image_to_3d,
|
||||||
|
image_upscale,
|
||||||
import_model,
|
import_model,
|
||||||
inspect_model,
|
inspect_model,
|
||||||
mesh_edit,
|
mesh_edit,
|
||||||
@@ -51,6 +53,8 @@ MODULES = [
|
|||||||
text_to_3d, # text
|
text_to_3d, # text
|
||||||
image_gen, # img (text to image)
|
image_gen, # img (text to image)
|
||||||
image_assets, # imgs (list image assets)
|
image_assets, # imgs (list image assets)
|
||||||
|
image_upscale, # img-upscale (4K image upscale)
|
||||||
|
image_split, # img-split (auto cutout / background removal)
|
||||||
image_multiview, # imgmv (one image -> 4 views)
|
image_multiview, # imgmv (one image -> 4 views)
|
||||||
image_to_3d, # image
|
image_to_3d, # image
|
||||||
import_model, # import (upload own glb/obj/fbx/stl)
|
import_model, # import (upload own glb/obj/fbx/stl)
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ HELP = "列出我的图片资产(生成过的参考图)"
|
|||||||
def add_arguments(parser):
|
def add_arguments(parser):
|
||||||
parser.add_argument("--page", type=int, default=1, help="页码(默认 1)")
|
parser.add_argument("--page", type=int, default=1, help="页码(默认 1)")
|
||||||
parser.add_argument("--size", type=int, default=8, help="每页数量(默认 8)")
|
parser.add_argument("--size", type=int, default=8, help="每页数量(默认 8)")
|
||||||
|
parser.add_argument("--delete", help="删除指定的图片资产 asset_id(对应网页端垃圾桶按钮)")
|
||||||
|
|
||||||
|
|
||||||
def run(ctx):
|
def run(ctx):
|
||||||
args = ctx.args
|
args = ctx.args
|
||||||
|
if args.delete:
|
||||||
|
target_id = args.delete.strip()
|
||||||
|
body = {"items": [{"asset_id": target_id, "keys": []}]}
|
||||||
|
ctx.client.get_data("POST", "/v2/studio/image/batch_delete", body=body)
|
||||||
|
ctx.out.line(f"🗑️ 已成功删除图片资产: {target_id}")
|
||||||
|
return {"deleted": target_id}
|
||||||
|
|
||||||
data = ctx.client.get_data(
|
data = ctx.client.get_data(
|
||||||
"POST", "/v2/studio/image/image_assets",
|
"POST", "/v2/studio/image/image_assets",
|
||||||
body={"page_num": args.page, "page_size": args.size},
|
body={"page_num": args.page, "page_size": args.size},
|
||||||
|
|||||||
@@ -26,12 +26,16 @@ HELP = "文生图:用文本生成参考图(游戏资产流程的第一步)
|
|||||||
# 界面上的模型名 → API 的 model_version(逐个在页面上选中并抓包确认)
|
# 界面上的模型名 → API 的 model_version(逐个在页面上选中并抓包确认)
|
||||||
IMAGE_MODELS = {
|
IMAGE_MODELS = {
|
||||||
"nano-banana-2": "gemini_3.1_flash_image_preview",
|
"nano-banana-2": "gemini_3.1_flash_image_preview",
|
||||||
|
"nanobanana": "gemini_3.1_flash_image_preview",
|
||||||
|
"nano-banana": "gemini_3.1_flash_image_preview",
|
||||||
|
"banana": "gemini_3.1_flash_image_preview",
|
||||||
"nano-banana-pro": "gemini_3_pro_image_preview",
|
"nano-banana-pro": "gemini_3_pro_image_preview",
|
||||||
|
"nanobanana-pro": "gemini_3_pro_image_preview",
|
||||||
"gpt-image-2": "gpt_image_2",
|
"gpt-image-2": "gpt_image_2",
|
||||||
"gpt-image-2.5": "gpt_image_2.5_sunburst",
|
"gpt-image-2.5": "gpt_image_2.5_sunburst",
|
||||||
"midjourney": "midjourney",
|
"midjourney": "midjourney",
|
||||||
}
|
}
|
||||||
DEFAULT_MODEL = "nano-banana-2"
|
DEFAULT_MODEL = "nano-banana-2" # 免费/非会员默认可用模型 (Nano Banana 2 / Gemini 3.1 Flash)
|
||||||
|
|
||||||
SCALES = ["1:1", "16:9", "9:16", "4:3", "3:4"]
|
SCALES = ["1:1", "16:9", "9:16", "4:3", "3:4"]
|
||||||
RESOLUTIONS = ["1K", "2K", "4K"]
|
RESOLUTIONS = ["1K", "2K", "4K"]
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""图片智能抠图/切分 — auto cutout / split image.
|
||||||
|
|
||||||
|
tripocli img-split -a <asset_id> --wait -o ./cutout.png
|
||||||
|
tripocli img-split (自动取最新一张参考图抠图)
|
||||||
|
|
||||||
|
实测流程(页面上剪刀图标抓到的请求):
|
||||||
|
POST /v2/studio/image/split
|
||||||
|
{"asset_id": "...", "resource_key": "generate-image/..."}
|
||||||
|
→ {"task_id": "...", "asset_id": "..."}
|
||||||
|
|
||||||
|
轮询:
|
||||||
|
POST /v2/studio/image/get_image_asset {"asset_id": "..."}
|
||||||
|
→ status == "success", type == "split_image", output.data[0].url
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ..errors import TripoAPIError, TripoTimeout
|
||||||
|
from .image_upscale import resolve_asset_and_key
|
||||||
|
|
||||||
|
NAME = "img-split"
|
||||||
|
HELP = "图片智能抠图/切分:主体背景去除与透明化(对应网页端剪刀按钮)"
|
||||||
|
|
||||||
|
|
||||||
|
def add_arguments(parser):
|
||||||
|
parser.add_argument("-a", "--asset", help="图片资产 ID(默认自动取最近生成的一张)")
|
||||||
|
parser.add_argument("--key", help="指定图片 resource_key(默认自动解析)")
|
||||||
|
parser.add_argument("-w", "--wait", action="store_true", default=True, help="等待抠图完成(默认等待)")
|
||||||
|
parser.add_argument("--no-wait", dest="wait", action="store_false", help="不等待,提交后直接退出")
|
||||||
|
parser.add_argument("-o", "--output", help="抠图后的图片保存路径")
|
||||||
|
parser.add_argument("--timeout", type=int, default=180, help="等待超时秒数(默认 180)")
|
||||||
|
|
||||||
|
|
||||||
|
def run(ctx):
|
||||||
|
args = ctx.args
|
||||||
|
asset_id, key = resolve_asset_and_key(ctx, args.asset, args.key)
|
||||||
|
ctx.out.line(f"✂️ 提交智能抠图任务: asset={asset_id}")
|
||||||
|
|
||||||
|
resp = ctx.client.get_data("POST", "/v2/studio/image/split", body={
|
||||||
|
"asset_id": asset_id,
|
||||||
|
"resource_key": key,
|
||||||
|
}) or {}
|
||||||
|
|
||||||
|
new_asset_id = resp.get("asset_id") or resp.get("task_id")
|
||||||
|
if not new_asset_id:
|
||||||
|
raise TripoAPIError(f"split 未返回 asset_id: {resp}")
|
||||||
|
|
||||||
|
if not args.wait:
|
||||||
|
ctx.out.data(resp, human=lambda: ctx.out.line(f"✅ 任务已提交,asset_id: {new_asset_id}"))
|
||||||
|
return resp
|
||||||
|
|
||||||
|
ctx.out.line(f"⏳ 等待智能抠图完成: {new_asset_id}")
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < args.timeout:
|
||||||
|
info = ctx.client.get_data("POST", "/v2/studio/image/get_image_asset", body={"asset_id": new_asset_id}) or {}
|
||||||
|
status = info.get("status")
|
||||||
|
if status == "success":
|
||||||
|
ctx.out.line("🎉 智能抠图完成!")
|
||||||
|
out_imgs = (info.get("output") or {}).get("data") or []
|
||||||
|
url = out_imgs[0].get("url") if out_imgs else None
|
||||||
|
if url:
|
||||||
|
ctx.out.line(f"抠图结果地址: {url}")
|
||||||
|
if args.output:
|
||||||
|
save_path = args.output
|
||||||
|
if os.path.isdir(save_path):
|
||||||
|
save_path = os.path.join(save_path, f"{new_asset_id}_cutout.webp")
|
||||||
|
ctx.client.download_file(url, save_path)
|
||||||
|
ctx.out.line(f"💾 已保存到: {save_path}")
|
||||||
|
return info
|
||||||
|
if status in ("failed", "expired"):
|
||||||
|
raise TripoAPIError(f"智能抠图失败: status={status}")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
raise TripoTimeout(f"智能抠图超时(超过 {args.timeout} 秒)")
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"""图片 4K 高清放大 — upscale reference image to 4K.
|
||||||
|
|
||||||
|
tripocli img-upscale -a <asset_id> --wait -o ./upscaled.png
|
||||||
|
tripocli img-upscale (自动取最新一张参考图放大)
|
||||||
|
|
||||||
|
实测流程(页面上 4K 图标抓到的请求):
|
||||||
|
POST /v2/studio/image/upscale
|
||||||
|
{"asset_id": "...", "resource_key": "generate-image/..."}
|
||||||
|
→ {"task_id": "...", "asset_id": "..."}
|
||||||
|
|
||||||
|
轮询:
|
||||||
|
POST /v2/studio/image/get_image_asset {"asset_id": "..."}
|
||||||
|
→ status == "success", type == "upscale_image", output.data[0].url
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ..errors import TripoAPIError, TripoTimeout
|
||||||
|
|
||||||
|
NAME = "img-upscale"
|
||||||
|
HELP = "图片 4K 高清放大:提升参考图分辨率(对应网页端 4K 按钮)"
|
||||||
|
|
||||||
|
|
||||||
|
def add_arguments(parser):
|
||||||
|
parser.add_argument("-a", "--asset", help="图片资产 ID(默认自动取最近生成的一张)")
|
||||||
|
parser.add_argument("--key", help="指定图片 resource_key(默认自动解析)")
|
||||||
|
parser.add_argument("-w", "--wait", action="store_true", default=True, help="等待放大完成(默认等待)")
|
||||||
|
parser.add_argument("--no-wait", dest="wait", action="store_false", help="不等待,提交后直接退出")
|
||||||
|
parser.add_argument("-o", "--output", help="放大后的图片保存路径")
|
||||||
|
parser.add_argument("--timeout", type=int, default=180, help="等待超时秒数(默认 180)")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_asset_and_key(ctx, asset_id, resource_key):
|
||||||
|
if not asset_id:
|
||||||
|
# 取最新的一张图片资产
|
||||||
|
data = ctx.client.get_data(
|
||||||
|
"POST", "/v2/studio/image/image_assets",
|
||||||
|
body={"page_num": 1, "page_size": 1},
|
||||||
|
) or {}
|
||||||
|
assets = data.get("assets") or []
|
||||||
|
if not assets:
|
||||||
|
raise TripoAPIError("未找到任何图片资产,请用 -a 指定 asset_id")
|
||||||
|
asset_id = assets[0].get("asset_id")
|
||||||
|
|
||||||
|
if not resource_key:
|
||||||
|
info = ctx.client.get_data("POST", "/v2/studio/image/get_image_asset", body={"asset_id": asset_id}) or {}
|
||||||
|
out_data = (info.get("output") or {}).get("data") or []
|
||||||
|
if out_data and out_data[0].get("key"):
|
||||||
|
resource_key = out_data[0].get("key")
|
||||||
|
else:
|
||||||
|
in_imgs = (info.get("input") or {}).get("prompt_image") or []
|
||||||
|
if in_imgs and in_imgs[0].get("key"):
|
||||||
|
resource_key = in_imgs[0].get("key")
|
||||||
|
|
||||||
|
if not resource_key:
|
||||||
|
raise TripoAPIError(f"无法从图片资产 {asset_id} 解析 resource_key,请用 --key 显式传入")
|
||||||
|
|
||||||
|
return asset_id, resource_key
|
||||||
|
|
||||||
|
|
||||||
|
def run(ctx):
|
||||||
|
args = ctx.args
|
||||||
|
asset_id, key = resolve_asset_and_key(ctx, args.asset, args.key)
|
||||||
|
ctx.out.line(f"🔍 提交 4K 放大任务: asset={asset_id}")
|
||||||
|
|
||||||
|
resp = ctx.client.get_data("POST", "/v2/studio/image/upscale", body={
|
||||||
|
"asset_id": asset_id,
|
||||||
|
"resource_key": key,
|
||||||
|
}) or {}
|
||||||
|
|
||||||
|
new_asset_id = resp.get("asset_id") or resp.get("task_id")
|
||||||
|
if not new_asset_id:
|
||||||
|
raise TripoAPIError(f"upscale 未返回 asset_id: {resp}")
|
||||||
|
|
||||||
|
if not args.wait:
|
||||||
|
ctx.out.data(resp, human=lambda: ctx.out.line(f"✅ 任务已提交,asset_id: {new_asset_id}"))
|
||||||
|
return resp
|
||||||
|
|
||||||
|
# 轮询直到完成
|
||||||
|
ctx.out.line(f"⏳ 等待 4K 放大完成: {new_asset_id}")
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < args.timeout:
|
||||||
|
info = ctx.client.get_data("POST", "/v2/studio/image/get_image_asset", body={"asset_id": new_asset_id}) or {}
|
||||||
|
status = info.get("status")
|
||||||
|
if status == "success":
|
||||||
|
ctx.out.line("🎉 4K 放大完成!")
|
||||||
|
out_imgs = (info.get("output") or {}).get("data") or []
|
||||||
|
url = out_imgs[0].get("url") if out_imgs else None
|
||||||
|
if url:
|
||||||
|
ctx.out.line(f"图片地址: {url}")
|
||||||
|
if args.output:
|
||||||
|
save_path = args.output
|
||||||
|
if os.path.isdir(save_path):
|
||||||
|
save_path = os.path.join(save_path, f"{new_asset_id}_4k.png")
|
||||||
|
ctx.client.download_file(url, save_path)
|
||||||
|
ctx.out.line(f"💾 已保存到: {save_path}")
|
||||||
|
return info
|
||||||
|
if status in ("failed", "expired"):
|
||||||
|
raise TripoAPIError(f"4K 放大失败: status={status}")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
raise TripoTimeout(f"4K 放大超时(超过 {args.timeout} 秒)")
|
||||||
Reference in new issue
Block a user