164 lines
6.9 KiB
Python
164 lines
6.9 KiB
Python
"""文生图 — text to image(游戏资产流程的第一步)。
|
||
|
||
tripocli img -p "a cute cartoon wooden barrel" -m nano-banana-2 --amount 1 --resolution 2K --wait -o ./refs
|
||
|
||
实测流程(页面上真操作抓到的请求):
|
||
|
||
POST /v2/studio/image/gen_image_v2
|
||
{"if_upload":true,"sketch_to_render":false,"t_pose":false,"amount":4,
|
||
"model_version":"gemini_3.1_flash_image_preview","prompt":"...","resolution":"4K","scale":"1:1"}
|
||
→ {task_id, asset_id, remaining_free_count}
|
||
|
||
POST /v2/studio/image/get_image_asset {"asset_id": "..."}
|
||
→ {status, type:"generate_image", output:{data:[{bucket,key,url}]}}
|
||
|
||
异步:先 gen_image_v2 拿 asset_id,再轮询 get_image_asset 直到 status=success,从 output.data 取图。
|
||
"""
|
||
|
||
import os
|
||
import time
|
||
|
||
from ..errors import TripoAPIError, TripoTimeout
|
||
|
||
NAME = "img"
|
||
HELP = "文生图:用文本生成参考图(游戏资产流程的第一步)"
|
||
|
||
# 界面上的模型名 → API 的 model_version(逐个在页面上选中并抓包确认)
|
||
IMAGE_MODELS = {
|
||
"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",
|
||
"nanobanana-pro": "gemini_3_pro_image_preview",
|
||
"gpt-image-2": "gpt_image_2",
|
||
"gpt-image-2.5": "gpt_image_2.5_sunburst",
|
||
"midjourney": "midjourney",
|
||
}
|
||
DEFAULT_MODEL = "nano-banana-2" # 免费/非会员默认可用模型 (Nano Banana 2 / Gemini 3.1 Flash)
|
||
|
||
SCALES = ["1:1", "16:9", "9:16", "4:3", "3:4"]
|
||
RESOLUTIONS = ["1K", "2K", "4K"]
|
||
|
||
|
||
def resolve_model(name):
|
||
"""Accept either a friendly alias or a raw model_version string."""
|
||
if not name:
|
||
return IMAGE_MODELS[DEFAULT_MODEL]
|
||
key = name.strip().lower()
|
||
return IMAGE_MODELS.get(key, name.strip())
|
||
|
||
|
||
def add_arguments(parser):
|
||
parser.add_argument("-p", "--prompt", help="图片描述提示词(用 --asset 取已有结果时可省略)")
|
||
parser.add_argument("--asset", help="直接取已有 asset_id 的结果,不发起新生成")
|
||
parser.add_argument("--templates", action="store_true", help="列出官方模板(角色拆分/T姿势/3D增强…)后退出")
|
||
parser.add_argument("--amount", type=int, default=1, choices=[1, 2, 3, 4], help="生成数量 1-4(默认 1,省额度)")
|
||
parser.add_argument("--scale", default="1:1", choices=SCALES, help="图片比例(默认 1:1)")
|
||
parser.add_argument("--resolution", default="1K", choices=RESOLUTIONS, help="分辨率 1K/2K/4K(默认 1K)")
|
||
parser.add_argument(
|
||
"-m", "--model", default=DEFAULT_MODEL,
|
||
help="图像模型: " + " / ".join(IMAGE_MODELS) + f"(默认 {DEFAULT_MODEL}),也可直接传 model_version",
|
||
)
|
||
parser.add_argument("--t-pose", action="store_true", help="人形角色输出 T 姿势(便于后续绑骨)")
|
||
parser.add_argument("-w", "--wait", action="store_true", help="等待生成完成")
|
||
parser.add_argument("-o", "--output", help="图片保存目录(配合 --wait)")
|
||
parser.add_argument("--timeout", type=int, default=300, help="等待超时秒数(默认 300)")
|
||
|
||
|
||
def _poll_asset(client, asset_id, timeout):
|
||
deadline = time.time() + timeout
|
||
while time.time() < deadline:
|
||
data = client.get_data("POST", "/v2/studio/image/get_image_asset", body={"asset_id": asset_id}) or {}
|
||
status = data.get("status")
|
||
if status == "success":
|
||
return data
|
||
if status in ("failed", "banned"):
|
||
raise TripoAPIError(f"图片生成失败: status={status}")
|
||
time.sleep(3)
|
||
raise TripoTimeout(f"图片生成在 {timeout}s 内未完成(asset_id={asset_id})")
|
||
|
||
|
||
def _report(ctx, asset_id, asset, output_dir):
|
||
images = ((asset.get("output") or {}).get("data")) or []
|
||
|
||
saved = []
|
||
if output_dir and images:
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
for index, item in enumerate(images):
|
||
target = os.path.abspath(os.path.join(output_dir, f"image_{index}.png"))
|
||
ctx.client.download(item["url"], target)
|
||
saved.append(target)
|
||
|
||
payload = {"asset_id": asset_id, "status": asset.get("status"), "images": images, "saved": saved}
|
||
ctx.out.data(
|
||
payload,
|
||
human=lambda: (
|
||
ctx.out.line(f"🎉 生成完成(status={asset.get('status')}),共 {len(images)} 张"),
|
||
*[ctx.out.line(f" {path}") for path in saved],
|
||
*([] if saved else [ctx.out.line(f" {item['url'][:90]}") for item in images]),
|
||
),
|
||
)
|
||
return payload
|
||
|
||
|
||
def run(ctx):
|
||
args = ctx.args
|
||
|
||
if args.templates:
|
||
# 模板是客户端的「提示词+参数」预设,接口只给元数据(实测:请求体里没有 template_id)。
|
||
data = ctx.client.get_data("GET", "/v2/studio/image/available_templates") or {}
|
||
templates = data.get("templates") or []
|
||
|
||
def show():
|
||
for item in templates:
|
||
tags = ", ".join(item.get("secondary_tag") or [])
|
||
ctx.out.line(f" {item.get('title', ''):<22} [{item.get('category', '')}] {tags}")
|
||
if item.get("description"):
|
||
ctx.out.line(f" {item['description'].splitlines()[0]}")
|
||
|
||
ctx.out.data({"templates": templates}, human=show)
|
||
return data
|
||
|
||
if args.asset:
|
||
if args.wait:
|
||
asset = _poll_asset(ctx.client, args.asset, args.timeout)
|
||
else:
|
||
asset = ctx.client.get_data(
|
||
"POST", "/v2/studio/image/get_image_asset", body={"asset_id": args.asset}
|
||
) or {}
|
||
return _report(ctx, args.asset, asset, args.output)
|
||
|
||
if not args.prompt:
|
||
raise ValueError("需要 -p/--prompt 发起生成,或用 --asset 取已有结果")
|
||
|
||
model_version = resolve_model(args.model)
|
||
body = {
|
||
"if_upload": True,
|
||
"sketch_to_render": False,
|
||
"t_pose": args.t_pose,
|
||
"amount": args.amount,
|
||
"model_version": model_version,
|
||
"prompt": args.prompt,
|
||
"resolution": args.resolution,
|
||
"scale": args.scale,
|
||
}
|
||
ctx.out.line(f"🎨 生成图片: \"{args.prompt}\" (x{args.amount}, {args.scale}, {args.resolution}, {model_version})")
|
||
data = ctx.client.get_data("POST", "/v2/studio/image/gen_image_v2", body=body) or {}
|
||
asset_id = data.get("asset_id") or data.get("task_id")
|
||
if not asset_id:
|
||
raise TripoAPIError(f"gen_image_v2 未返回 asset_id: {data}")
|
||
|
||
if not args.wait:
|
||
ctx.out.data(
|
||
data,
|
||
human=lambda: (
|
||
ctx.out.line(f"✅ 已提交 asset_id={asset_id}(免费剩余 {data.get('remaining_free_count')})"),
|
||
ctx.out.line(f"取图: tripocli img --asset {asset_id} --wait -o ./refs"),
|
||
),
|
||
)
|
||
return data
|
||
|
||
asset = _poll_asset(ctx.client, asset_id, args.timeout)
|
||
return _report(ctx, asset_id, asset, args.output)
|