"""文生图 — 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)